mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 15:18:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5a90e921b | ||
|
|
32b0ded17c | ||
|
|
dee8781e51 | ||
|
|
64fd21c9c2 | ||
|
|
f9e0c2058f | ||
|
|
16730b14a1 | ||
|
|
05c65d31ed | ||
|
|
a8c791b030 | ||
|
|
a3a87f4580 | ||
|
|
ffa43915f0 | ||
|
|
7bca19dae7 | ||
|
|
6b6cc8aa34 | ||
|
|
03a1ce3895 | ||
|
|
82fea1fe41 | ||
|
|
084b048ff3 | ||
|
|
085b46c995 | ||
|
|
eaaf1b0b4f | ||
|
|
265d280efc | ||
|
|
e6e3d92971 | ||
|
|
bf744f48dc | ||
|
|
ec06efe86a | ||
|
|
56f57625b0 | ||
|
|
2110954bb8 | ||
|
|
d616e57daf | ||
|
|
d702919735 | ||
|
|
fe7e82f7d3 | ||
|
|
03857916c4 | ||
|
|
1a7265017a | ||
|
|
89f8e43ad1 | ||
|
|
6875146211 | ||
|
|
27ccd1a570 | ||
|
|
a5b959a6d5 | ||
|
|
b67888f938 | ||
|
|
af8c2d86fe | ||
|
|
29635a9b6a | ||
|
|
65d7f5f3e2 | ||
|
|
c26a6a28e4 | ||
|
|
19754c69e5 | ||
|
|
1e4ac34c44 | ||
|
|
89e6d307f2 | ||
|
|
e7e9616f7e | ||
|
|
6e52b2a7af | ||
|
|
2e704f3ff7 | ||
|
|
8f62a0a618 | ||
|
|
e0f50218d7 | ||
|
|
2c5d3e14f7 | ||
|
|
851e3f559a | ||
|
|
cefd6e5685 | ||
|
|
25de4fded9 | ||
|
|
ca6b24304c | ||
|
|
b415443126 | ||
|
|
bcf50bd0c5 | ||
|
|
7c81eda46a | ||
|
|
abde909b71 | ||
|
|
48347a51dc | ||
|
|
f64c00c8d3 | ||
|
|
87f6b9e81a | ||
|
|
0a2d9ba952 | ||
|
|
5aeec130b1 | ||
|
|
a26008f74c | ||
|
|
c7e294482b | ||
|
|
5ff10c3e5c | ||
|
|
be17544598 | ||
|
|
8929ab3b97 | ||
|
|
d1501052ad | ||
|
|
4cc88f1e16 | ||
|
|
4e6bc33a22 | ||
|
|
81033700e2 | ||
|
|
a17f650ccb | ||
|
|
59efc15f3a | ||
|
|
cb509426ec | ||
|
|
aa8144b739 | ||
|
|
f11eb2459b | ||
|
|
cb74db47f7 | ||
|
|
299b37a721 | ||
|
|
ffcb8202f0 | ||
|
|
e60b94b553 | ||
|
|
fbabe9674c | ||
|
|
8ab96e124f | ||
|
|
483acc3ca1 | ||
|
|
e0deef8889 | ||
|
|
bbb0ea329c | ||
|
|
bf69db3e82 | ||
|
|
adc08125fe | ||
|
|
aa9a916d61 | ||
|
|
5e3bd75f09 | ||
|
|
04d8522b3b | ||
|
|
dd1e0db407 | ||
|
|
2fa8afe37f | ||
|
|
5afd16ac65 | ||
|
|
9a2425a202 | ||
|
|
af02370777 | ||
|
|
9c8aee17dc | ||
|
|
a822873748 | ||
|
|
6e843d0f1c | ||
|
|
346cb90e9e | ||
|
|
6619146028 | ||
|
|
56589dccf1 | ||
|
|
982af4b724 | ||
|
|
7a516bba16 | ||
|
|
2080e36f19 | ||
|
|
a71f43d75b | ||
|
|
c20293078a | ||
|
|
37636b243f | ||
|
|
aae7312d8a | ||
|
|
3bbf89cba0 | ||
|
|
f976d4efb4 | ||
|
|
a904e7a396 | ||
|
|
12521add39 | ||
|
|
2dabc1e61b | ||
|
|
e5dc271a78 | ||
|
|
4e128e26fe | ||
|
|
b4c0a59501 | ||
|
|
9032bb3d13 | ||
|
|
8a6d90d70e | ||
|
|
b17487e6bb | ||
|
|
838629ca42 | ||
|
|
51a93418fa | ||
|
|
ef00a64bf8 | ||
|
|
ef4d9e1e55 | ||
|
|
2d08baf620 | ||
|
|
8cf41a137a | ||
|
|
61b08c94bf | ||
|
|
c87ac9cc2b | ||
|
|
ed20e2a1bc | ||
|
|
d4e3aa9d97 | ||
|
|
91aed7e98e | ||
|
|
fd19c86cb1 | ||
|
|
ad4bed8921 | ||
|
|
01d32cb230 | ||
|
|
746066c920 | ||
|
|
4815ac7110 | ||
|
|
035e2a3023 | ||
|
|
311ea070b1 | ||
|
|
736caac46d | ||
|
|
157dff14d3 | ||
|
|
a152bded93 | ||
|
|
6587812b72 | ||
|
|
c8848be981 | ||
|
|
baae082258 | ||
|
|
c8dcd716ab | ||
|
|
45ac44dd06 | ||
|
|
da04d223b4 | ||
|
|
d26ea1a411 | ||
|
|
9d5d61754d | ||
|
|
2bb77a31cf | ||
|
|
86ae0ed90f | ||
|
|
2540fbd771 | ||
|
|
354ada5c4c | ||
|
|
0fec5958fa | ||
|
|
ccc08a3fc7 | ||
|
|
5889cdd9ab | ||
|
|
93af087264 | ||
|
|
2d5bb2462e | ||
|
|
b71aa0a3fd | ||
|
|
1fd1686ee6 | ||
|
|
fc073e8743 | ||
|
|
be48d545cc | ||
|
|
13443a42ce | ||
|
|
071842ef49 | ||
|
|
366e29d4d9 | ||
|
|
02a5a1b14f | ||
|
|
64b26e47da | ||
|
|
1f2bd841c4 | ||
|
|
1e905598f2 | ||
|
|
e76b0ff282 |
@@ -7,7 +7,7 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
**IMPORTANT:** Before submitting:
|
||||
- Make sure this device is not already supported in the [dev branch](https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html)
|
||||
- Make sure this device is not already supported in the dev branch by checking the [dev branch changelog](https://gist.github.com/Koenkk/bfd4c3d1725a2cccacc11d6ba51008ba#new-supported-devices)
|
||||
- Make sure there is no existing issue or PR for this device already, search for your device here: https://github.com/Koenkk/zigbee2mqtt/issues
|
||||
- Follow this [guide](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html)
|
||||
- If you are using the Home Assistant addon and are still on 1.18.1, check the first point of [this](https://github.com/Koenkk/zigbee2mqtt/releases/tag/1.19.0)
|
||||
@@ -36,20 +36,8 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: External converter
|
||||
description: In case you tried to create an external converter, put it below
|
||||
label: External defintion
|
||||
description: See [Creating the external definition](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html#_2-creating-the-external-definition)
|
||||
render: shell
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Lights
|
||||
In case this device is a light, fill in the fields below:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Supported color modes
|
||||
placeholder: color temperature, color (rgb)
|
||||
- type: input
|
||||
attributes:
|
||||
label: Color temperature range
|
||||
description: In case this light supports color temperature, provide the color temperature range. [How to do this](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html#_3-1-retrieving-color-temperature-range-only-required-for-lights-which-support-color-temperature).
|
||||
placeholder: '{"colorTempPhysicalMin":153,"colorTempPhysicalMax":454}'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
node: [16, 18, 20]
|
||||
node: [18, 20]
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: true
|
||||
steps:
|
||||
|
||||
@@ -16,8 +16,8 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Delete untagged images
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -20,15 +20,12 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: google-github-actions/release-please-action@v3
|
||||
- uses: google-github-actions/release-please-action@v4
|
||||
id: release
|
||||
with:
|
||||
release-type: node
|
||||
package-name: release-please-action
|
||||
default-branch: dev
|
||||
target-branch: dev
|
||||
token: ${{secrets.GH_TOKEN}}
|
||||
include-v-in-tag: false
|
||||
skip-github-release: true
|
||||
|
||||
# Checkout repos
|
||||
- uses: actions/checkout@v4
|
||||
@@ -53,10 +50,11 @@ jobs:
|
||||
MASTER_Z2M_VERSION=$(cat z2m-master/package.json | jq -r '.version')
|
||||
MASTER_ZHC_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee-herdsman-converters"')
|
||||
MASTER_ZH_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee-herdsman"')
|
||||
MASTER_FRONTEND_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee2mqtt-frontend"')
|
||||
wget -q -O - https://raw.githubusercontent.com/Koenkk/zigbee2mqtt/release-please--branches--dev--components--release-please-action/CHANGELOG.md > z2m/CHANGELOG.md || true
|
||||
cd z2m
|
||||
npm ci
|
||||
node scripts/generateChangelog.js $MASTER_Z2M_VERSION $MASTER_ZHC_VERSION $MASTER_ZH_VERSION >> ../changelog.md
|
||||
node scripts/generateChangelog.js $MASTER_Z2M_VERSION $MASTER_ZHC_VERSION $MASTER_ZH_VERSION $MASTER_FRONTEND_VERSION >> ../changelog.md
|
||||
env:
|
||||
GH_TOKEN: ${{secrets.GH_TOKEN}}
|
||||
- name: Update changelog in release PR
|
||||
@@ -65,7 +63,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{secrets.GH_TOKEN}}
|
||||
- name: Upload changelog
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: changelog
|
||||
path: changelog.md
|
||||
@@ -85,25 +83,13 @@ jobs:
|
||||
with:
|
||||
ref: dev
|
||||
token: ${{secrets.GH_TOKEN}}
|
||||
- name: Download changelog
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: changelog
|
||||
path: ../changelog
|
||||
- uses: ncipollo/release-action@v1
|
||||
name: Create release
|
||||
with:
|
||||
bodyFile: ../changelog/changelog.md
|
||||
draft: true
|
||||
name: ${{ needs.release-please.outputs.version }}
|
||||
tag: ${{ needs.release-please.outputs.version }}
|
||||
- name: Merge dev -> master
|
||||
uses: devmasx/merge-branch@master
|
||||
with:
|
||||
type: now
|
||||
from_branch: dev
|
||||
target_branch: master
|
||||
github_token: ${{secrets.GH_TOKEN}}
|
||||
run: |
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git checkout master
|
||||
git merge -m 'chore: merge dev to master'
|
||||
git push origin master
|
||||
- name: Promote to dev
|
||||
run: |
|
||||
jq '.version = "${{ needs.release-please.outputs.version }}-dev"' package.json > package.json.tmp
|
||||
@@ -113,3 +99,19 @@ jobs:
|
||||
- uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: "chore: promote to dev"
|
||||
- name: Download changelog
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: changelog
|
||||
path: ../changelog
|
||||
- name: update release
|
||||
id: update_release
|
||||
uses: tubone24/update_release@v1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
TAG_NAME: ${{ needs.release-please.outputs.version }}
|
||||
with:
|
||||
body_path: ../changelog/changelog.md
|
||||
- name: Trigger HA addon update
|
||||
run: |
|
||||
curl -XPOST -H "Authorization: token ${{ secrets.GH_TOKEN }}" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/zigbee2mqtt/hassio-zigbee2mqtt/dispatches --data "{\"event_type\": \"release\", \"client_payload\": { \"version\": \"${{ needs.release-please.outputs.version }}-1\"}}"
|
||||
|
||||
@@ -9,12 +9,12 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days'
|
||||
stale-pr-message: 'This pull request is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days'
|
||||
days-before-stale: 30
|
||||
days-before-close: 7
|
||||
days-before-stale: 180
|
||||
days-before-close: 30
|
||||
exempt-issue-labels: dont-stale
|
||||
operations-per-run: 500
|
||||
|
||||
@@ -22,9 +22,9 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
# connect-gzip-static only supports node 16
|
||||
# @types/ws 8.5.5 is broken
|
||||
- run: npx npm-check-updates -u -x connect-gzip-static -x @types/ws
|
||||
# connect-gzip-static@4.0.0 requires Node 20 >=
|
||||
- run: npx npm-check-updates -u -x connect-gzip-static
|
||||
- run: rm -f package-lock.json
|
||||
- run: npm install
|
||||
- uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
".": "1.33.2",
|
||||
"bootstrap-sha": "7ee207fc0e05f32c9636c0298b48eea1b91ed9e1"
|
||||
".": "1.35.0",
|
||||
"bootstrap-sha": "7ee207fc0e05f32c9636c0298b48eea1b91ed9e1",
|
||||
"include-v-in-tag": false
|
||||
}
|
||||
+146
@@ -1,5 +1,151 @@
|
||||
# Changelog
|
||||
|
||||
## [1.35.0](https://github.com/Koenkk/zigbee2mqtt/compare/1.34.0...v1.35.0) (2024-01-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Support automatically generating device definitions ([#20358](https://github.com/Koenkk/zigbee2mqtt/issues/20358)) ([56f5762](https://github.com/Koenkk/zigbee2mqtt/commit/56f57625b0d5ce5b3bf6fa6a1a048f6ae8902311))
|
||||
* Support bridge discovery for Home Assistant ([#19994](https://github.com/Koenkk/zigbee2mqtt/issues/19994)) ([f11eb24](https://github.com/Koenkk/zigbee2mqtt/commit/f11eb2459bd1ad89a1588b8d2d023d949faf3313))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add Home Assistant `entity_category` for various Ubisys D1 entities ([#20393](https://github.com/Koenkk/zigbee2mqtt/issues/20393)) ([085b46c](https://github.com/Koenkk/zigbee2mqtt/commit/085b46c995c888751ca5a1ff73391b8f56591826))
|
||||
* Allow `null` for `zigbee_ota_override_index_location` option ([#20151](https://github.com/Koenkk/zigbee2mqtt/issues/20151)) ([48347a5](https://github.com/Koenkk/zigbee2mqtt/commit/48347a51dcff3ce8ed533c71cc27651bc884228b))
|
||||
* Clear old Home Assistant discovery topics when exposes are changed ([#20313](https://github.com/Koenkk/zigbee2mqtt/issues/20313)) ([a5b959a](https://github.com/Koenkk/zigbee2mqtt/commit/a5b959a6d520679bce1586c50f01a57f13d6af01))
|
||||
* Don't crash on startup when external converters fails to load https://github.com/Koenkk/zigbee2mqtt/issues/20010 ([8ab96e1](https://github.com/Koenkk/zigbee2mqtt/commit/8ab96e124fa5e24e504b2a11ae6cf9836d93f5c8))
|
||||
* Fix availability checks not stopped on extension stop ([#20093](https://github.com/Koenkk/zigbee2mqtt/issues/20093)) ([5aeec13](https://github.com/Koenkk/zigbee2mqtt/commit/5aeec130b19e112ad52cbc506a810dcdfc2e2605))
|
||||
* Fix build on Node 20+ ([#19983](https://github.com/Koenkk/zigbee2mqtt/issues/19983)) ([9c8aee1](https://github.com/Koenkk/zigbee2mqtt/commit/9c8aee17dc9ee0cea311d99c282f1558d2fb841b))
|
||||
* Fix Home Assistant `Entity category `config` is invalid for sensors` warning https://github.com/Koenkk/zigbee2mqtt/issues/20252 ([8f62a0a](https://github.com/Koenkk/zigbee2mqtt/commit/8f62a0a61899837c63d7053efd68b7a315d623ac))
|
||||
* Fix load of empty configuration `.yaml` file failing ([#20289](https://github.com/Koenkk/zigbee2mqtt/issues/20289)) ([b67888f](https://github.com/Koenkk/zigbee2mqtt/commit/b67888f938e9e04b8547d7569b78058ba3fb017b))
|
||||
* **ignore:** Ability to use a specified endpoint in the Tz.Converter ([#20356](https://github.com/Koenkk/zigbee2mqtt/issues/20356)) ([d702919](https://github.com/Koenkk/zigbee2mqtt/commit/d702919735c2f399d73c3fb902721e4b5367b320))
|
||||
* **ignore:** Changes for zhc 18.0.0 ([#20390](https://github.com/Koenkk/zigbee2mqtt/issues/20390)) ([e6e3d92](https://github.com/Koenkk/zigbee2mqtt/commit/e6e3d92971162494ff999f74d13b00429b164c51))
|
||||
* **ignore:** Downgrade connect-gzip-static to 3.0.1 https://github.com/Koenkk/zigbee2mqtt/issues/20149 ([f64c00c](https://github.com/Koenkk/zigbee2mqtt/commit/f64c00c8d34dae64279a607368c82c06dc9dc53a))
|
||||
* **ignore:** Fix "Permit join timeout" template ([#20096](https://github.com/Koenkk/zigbee2mqtt/issues/20096)) ([59efc15](https://github.com/Koenkk/zigbee2mqtt/commit/59efc15f3adb58954f3a9eb524d728679fa28e77))
|
||||
* **ignore:** fix bind test ([d150105](https://github.com/Koenkk/zigbee2mqtt/commit/d1501052ad1dc7d1cdf9e0f925c3e9694c748226))
|
||||
* **ignore:** fix dd1e0db407f80add7355879823cf519ae869a7f6 ([04d8522](https://github.com/Koenkk/zigbee2mqtt/commit/04d8522b3ba95f83e0b569373726e4da7a1ece13))
|
||||
* **ignore:** fix dee8781e51f2aac466e273fb81cbdd31da56ac18 ([32b0ded](https://github.com/Koenkk/zigbee2mqtt/commit/32b0ded17c781fea0474c9928d795a3eb3c81acf))
|
||||
* **ignore:** fix homeassistant.test.js ([dee8781](https://github.com/Koenkk/zigbee2mqtt/commit/dee8781e51f2aac466e273fb81cbdd31da56ac18))
|
||||
* **ignore:** Fix OTA sensor discoverd while device doesn't support it https://github.com/Koenkk/zigbee2mqtt/issues/20131 ([abde909](https://github.com/Koenkk/zigbee2mqtt/commit/abde909b7106ffdc0d841c44b9791aaafd874cc2))
|
||||
* **ignore:** improve 8f62a0a ([2e704f3](https://github.com/Koenkk/zigbee2mqtt/commit/2e704f3ff7ac1dbe1b6f12c7cdace29850a288cf))
|
||||
* **ignore:** Revert module to commonjs ([6b6cc8a](https://github.com/Koenkk/zigbee2mqtt/commit/6b6cc8aa34ec8b63db332cd8ad33d3592e6c515b))
|
||||
* **ignore:** Update alpine to 3.19.0 ([#20392](https://github.com/Koenkk/zigbee2mqtt/issues/20392)) ([eaaf1b0](https://github.com/Koenkk/zigbee2mqtt/commit/eaaf1b0b4f6ebd631117ae13666e5e0ebcb89b23))
|
||||
* **ignore:** update dependencies ([#19896](https://github.com/Koenkk/zigbee2mqtt/issues/19896)) ([6e843d0](https://github.com/Koenkk/zigbee2mqtt/commit/6e843d0f1cdf0e76a2a07cf3d33f3287f0a5e70c))
|
||||
* **ignore:** update dependencies ([#20023](https://github.com/Koenkk/zigbee2mqtt/issues/20023)) ([e0deef8](https://github.com/Koenkk/zigbee2mqtt/commit/e0deef8889835949af99e4dfd4c47a182d329514))
|
||||
* **ignore:** update dependencies ([#20133](https://github.com/Koenkk/zigbee2mqtt/issues/20133)) ([b415443](https://github.com/Koenkk/zigbee2mqtt/commit/b415443126e3ee0bda25a9fee8e37615c2290b2e))
|
||||
* **ignore:** update dependencies ([#20249](https://github.com/Koenkk/zigbee2mqtt/issues/20249)) ([6e52b2a](https://github.com/Koenkk/zigbee2mqtt/commit/6e52b2a7af0ec9bd636a9be12ff7aa16e2c37f04))
|
||||
* **ignore:** update dependencies ([#20256](https://github.com/Koenkk/zigbee2mqtt/issues/20256)) ([1e4ac34](https://github.com/Koenkk/zigbee2mqtt/commit/1e4ac34c443e29df9631b97ed83c407c2c174af1))
|
||||
* **ignore:** update dependencies ([#20257](https://github.com/Koenkk/zigbee2mqtt/issues/20257)) ([19754c6](https://github.com/Koenkk/zigbee2mqtt/commit/19754c69e51d642dd9360aa27c3d0a121f8d5d61))
|
||||
* **ignore:** update dependencies ([#20368](https://github.com/Koenkk/zigbee2mqtt/issues/20368)) ([0385791](https://github.com/Koenkk/zigbee2mqtt/commit/03857916c455a08c42bc241bc643af7a91061b5d))
|
||||
* **ignore:** update dependencies ([#20452](https://github.com/Koenkk/zigbee2mqtt/issues/20452)) ([16730b1](https://github.com/Koenkk/zigbee2mqtt/commit/16730b14a1d1de396fd8f6260567a1da4afae623))
|
||||
* **ignore:** Update tsconfig for Node 18 ([084b048](https://github.com/Koenkk/zigbee2mqtt/commit/084b048ff389261edbd72f05e0c461e725574b6e))
|
||||
* **ignore:** update zigbee-herdsman to 0.25.1 ([#19985](https://github.com/Koenkk/zigbee2mqtt/issues/19985)) ([a822873](https://github.com/Koenkk/zigbee2mqtt/commit/a8228737480dac0d61768131c7b4850a614db4fc))
|
||||
* **ignore:** update zigbee-herdsman to 0.25.2 ([#19997](https://github.com/Koenkk/zigbee2mqtt/issues/19997)) ([9a2425a](https://github.com/Koenkk/zigbee2mqtt/commit/9a2425a202bc70a6d7f4ba534e145bcb3736f041))
|
||||
* **ignore:** update zigbee-herdsman to 0.25.3 ([#20025](https://github.com/Koenkk/zigbee2mqtt/issues/20025)) ([483acc3](https://github.com/Koenkk/zigbee2mqtt/commit/483acc3ca156e441bb82d9e8040ccf9dc7a5e1e5))
|
||||
* **ignore:** update zigbee-herdsman to 0.26.0 ([#20092](https://github.com/Koenkk/zigbee2mqtt/issues/20092)) ([8929ab3](https://github.com/Koenkk/zigbee2mqtt/commit/8929ab3b9704e924fc2f0f5e84e6d15e5c14a8b5))
|
||||
* **ignore:** update zigbee-herdsman to 0.26.1 ([#20143](https://github.com/Koenkk/zigbee2mqtt/issues/20143)) ([a26008f](https://github.com/Koenkk/zigbee2mqtt/commit/a26008f74c7a8ef138e51debc79965fde53ca61b))
|
||||
* **ignore:** update zigbee-herdsman to 0.27.1 ([#20262](https://github.com/Koenkk/zigbee2mqtt/issues/20262)) ([65d7f5f](https://github.com/Koenkk/zigbee2mqtt/commit/65d7f5f3e27ad4a8dbe2abeca1f5ffa349c9631b))
|
||||
* **ignore:** update zigbee-herdsman to 0.28.0 ([#20357](https://github.com/Koenkk/zigbee2mqtt/issues/20357)) ([89f8e43](https://github.com/Koenkk/zigbee2mqtt/commit/89f8e43ad11ffd84cf6a3c9d3bd57d5577063e15))
|
||||
* **ignore:** update zigbee-herdsman to 0.29.0 ([#20403](https://github.com/Koenkk/zigbee2mqtt/issues/20403)) ([03a1ce3](https://github.com/Koenkk/zigbee2mqtt/commit/03a1ce3895cbc9dafb1c83a7013e1f96409db05a))
|
||||
* **ignore:** update zigbee-herdsman to 0.29.1 ([#20406](https://github.com/Koenkk/zigbee2mqtt/issues/20406)) ([7bca19d](https://github.com/Koenkk/zigbee2mqtt/commit/7bca19dae777eb919a0017b4c52304ec83972e73))
|
||||
* **ignore:** update zigbee-herdsman to 0.30.0 ([#20464](https://github.com/Koenkk/zigbee2mqtt/issues/20464)) ([05c65d3](https://github.com/Koenkk/zigbee2mqtt/commit/05c65d31ed76f0f5f080518a4196ca7999a41adc))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.131.0 ([#19987](https://github.com/Koenkk/zigbee2mqtt/issues/19987)) ([af02370](https://github.com/Koenkk/zigbee2mqtt/commit/af02370777fc5e81363810c759359e5ba17e3bf7))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.132.0 ([#20005](https://github.com/Koenkk/zigbee2mqtt/issues/20005)) ([5afd16a](https://github.com/Koenkk/zigbee2mqtt/commit/5afd16ac652560250ec9a21f4d05fba7e25af853))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.0.0 ([#20008](https://github.com/Koenkk/zigbee2mqtt/issues/20008)) ([2fa8afe](https://github.com/Koenkk/zigbee2mqtt/commit/2fa8afe37f99425234b2841a3ffd8c954a7b78c6))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.0.1 ([#20012](https://github.com/Koenkk/zigbee2mqtt/issues/20012)) ([5e3bd75](https://github.com/Koenkk/zigbee2mqtt/commit/5e3bd75f098b28a5df544ef5b06eab9f35020419))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.0.2 ([#20027](https://github.com/Koenkk/zigbee2mqtt/issues/20027)) ([e60b94b](https://github.com/Koenkk/zigbee2mqtt/commit/e60b94b55329e30f00c1dec5094f139233d08521))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.1.0 ([#20059](https://github.com/Koenkk/zigbee2mqtt/issues/20059)) ([cb74db4](https://github.com/Koenkk/zigbee2mqtt/commit/cb74db47f7140357796618350c1ae73b7b1d4d1a))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.11.0 ([#20228](https://github.com/Koenkk/zigbee2mqtt/issues/20228)) ([2c5d3e1](https://github.com/Koenkk/zigbee2mqtt/commit/2c5d3e14f779ff138949f11f6a443fd8821e0120))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.12.0 ([#20258](https://github.com/Koenkk/zigbee2mqtt/issues/20258)) ([c26a6a2](https://github.com/Koenkk/zigbee2mqtt/commit/c26a6a28e4562d7cc97763f6c02782525575f84f))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.13.0 ([#20280](https://github.com/Koenkk/zigbee2mqtt/issues/20280)) ([29635a9](https://github.com/Koenkk/zigbee2mqtt/commit/29635a9b6aedd8f140c126a36813bb922f6a450e))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.14.1 ([#20301](https://github.com/Koenkk/zigbee2mqtt/issues/20301)) ([af8c2d8](https://github.com/Koenkk/zigbee2mqtt/commit/af8c2d86fe1b50934ec8ccd675b298dc2f86a29a))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.15.0 ([#20318](https://github.com/Koenkk/zigbee2mqtt/issues/20318)) ([27ccd1a](https://github.com/Koenkk/zigbee2mqtt/commit/27ccd1a5704b366f86b4c708865831abe40cd2ad))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.16.0 ([#20326](https://github.com/Koenkk/zigbee2mqtt/issues/20326)) ([6875146](https://github.com/Koenkk/zigbee2mqtt/commit/687514621152c48809d922af9e712a45c762173d))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.17.0 ([#20359](https://github.com/Koenkk/zigbee2mqtt/issues/20359)) ([1a72650](https://github.com/Koenkk/zigbee2mqtt/commit/1a7265017a0b7facbe88058daa1ef47fcc79747e))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.18.0 ([#20374](https://github.com/Koenkk/zigbee2mqtt/issues/20374)) ([fe7e82f](https://github.com/Koenkk/zigbee2mqtt/commit/fe7e82f7d34db4d79448f198f51902592633fbc1))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.2.0 ([#20094](https://github.com/Koenkk/zigbee2mqtt/issues/20094)) ([cb50942](https://github.com/Koenkk/zigbee2mqtt/commit/cb509426eca455d693723b5631313163b931dbc9))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.3.1 ([#20109](https://github.com/Koenkk/zigbee2mqtt/issues/20109)) ([4e6bc33](https://github.com/Koenkk/zigbee2mqtt/commit/4e6bc33a2203591a9bcee84eb90cefdb9c883c69))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.4.0 ([#20120](https://github.com/Koenkk/zigbee2mqtt/issues/20120)) ([4cc88f1](https://github.com/Koenkk/zigbee2mqtt/commit/4cc88f1e16e5bd8e901c555c6950f14d46013787))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.5.0 ([#20125](https://github.com/Koenkk/zigbee2mqtt/issues/20125)) ([5ff10c3](https://github.com/Koenkk/zigbee2mqtt/commit/5ff10c3e5ced22a6070360f76ea9ffd7e9805194))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.6.0 ([#20145](https://github.com/Koenkk/zigbee2mqtt/issues/20145)) ([87f6b9e](https://github.com/Koenkk/zigbee2mqtt/commit/87f6b9e81a4efaa857c06ba179665535e1beb742))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.7.0 ([#20167](https://github.com/Koenkk/zigbee2mqtt/issues/20167)) ([7c81eda](https://github.com/Koenkk/zigbee2mqtt/commit/7c81eda46a1b7d3ce190a11f89618e60afaba7b5))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.8.0 ([#20185](https://github.com/Koenkk/zigbee2mqtt/issues/20185)) ([bcf50bd](https://github.com/Koenkk/zigbee2mqtt/commit/bcf50bd0c532d822768d380cb99f793e00db07f0))
|
||||
* **ignore:** update zigbee-herdsman-converters to 16.9.0 ([#20200](https://github.com/Koenkk/zigbee2mqtt/issues/20200)) ([25de4fd](https://github.com/Koenkk/zigbee2mqtt/commit/25de4fded9bd67bafb2e8a596490988c624dce88))
|
||||
* **ignore:** update zigbee-herdsman-converters to 17.0.0 ([#20378](https://github.com/Koenkk/zigbee2mqtt/issues/20378)) ([2110954](https://github.com/Koenkk/zigbee2mqtt/commit/2110954bb8c47b4fb9b3b536e857183cb4fc83ac))
|
||||
* **ignore:** update zigbee-herdsman-converters to 18.0.0 ([#20391](https://github.com/Koenkk/zigbee2mqtt/issues/20391)) ([bf744f4](https://github.com/Koenkk/zigbee2mqtt/commit/bf744f48dc820f46f9944cf52513437908586d79))
|
||||
* **ignore:** update zigbee-herdsman-converters to 18.1.0 ([#20426](https://github.com/Koenkk/zigbee2mqtt/issues/20426)) ([a3a87f4](https://github.com/Koenkk/zigbee2mqtt/commit/a3a87f4580ad4b2a77d1c039232035f7cf5797ac))
|
||||
* **ignore:** update zigbee-herdsman-converters to 18.2.0 ([#20451](https://github.com/Koenkk/zigbee2mqtt/issues/20451)) ([a8c791b](https://github.com/Koenkk/zigbee2mqtt/commit/a8c791b0302cb885671bdfd0dd785a83185dffe0))
|
||||
* **ignore:** update zigbee-herdsman-converters to 18.3.0 ([#20465](https://github.com/Koenkk/zigbee2mqtt/issues/20465)) ([f9e0c20](https://github.com/Koenkk/zigbee2mqtt/commit/f9e0c2058fe15517d6b17445eb48ad6c9777c3db))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.6.148 ([#20034](https://github.com/Koenkk/zigbee2mqtt/issues/20034)) ([ffcb820](https://github.com/Koenkk/zigbee2mqtt/commit/ffcb8202f01d9ba627a362abdbc350793e269647))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.6.149 ([#20141](https://github.com/Koenkk/zigbee2mqtt/issues/20141)) ([c7e2944](https://github.com/Koenkk/zigbee2mqtt/commit/c7e294482bfd7dacb5fc70858041141070a6971d))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.6.150 ([#20377](https://github.com/Koenkk/zigbee2mqtt/issues/20377)) ([d616e57](https://github.com/Koenkk/zigbee2mqtt/commit/d616e57daf89f36b145a46bec01b80deaaaae5a7))
|
||||
* **ignore:** Use zhc types ([#20006](https://github.com/Koenkk/zigbee2mqtt/issues/20006)) ([dd1e0db](https://github.com/Koenkk/zigbee2mqtt/commit/dd1e0db407f80add7355879823cf519ae869a7f6))
|
||||
* Make update script work from anywhere ([#19965](https://github.com/Koenkk/zigbee2mqtt/issues/19965)) ([346cb90](https://github.com/Koenkk/zigbee2mqtt/commit/346cb90e9e78fd08ffb49cb954bcbee2f3bc9191))
|
||||
* Only republish changed scenes to Home Assistant when changed ([#20248](https://github.com/Koenkk/zigbee2mqtt/issues/20248)) ([e0f5021](https://github.com/Koenkk/zigbee2mqtt/commit/e0f50218d70c1932fbc32f28f60737a4bff5f048))
|
||||
* Prevent duplicate Home Assistant scene entities ([#20097](https://github.com/Koenkk/zigbee2mqtt/issues/20097)) ([a17f650](https://github.com/Koenkk/zigbee2mqtt/commit/a17f650ccbb8113874dc5dd5f5ad3e3419dc6c15))
|
||||
* Support Node 21 and drop 16 ([adc0812](https://github.com/Koenkk/zigbee2mqtt/commit/adc08125fe892dfe456edf3a4d85da23b6f5798d))
|
||||
|
||||
## [1.34.0](https://github.com/Koenkk/zigbee2mqtt/compare/1.33.2...1.34.0) (2023-12-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add Home Assistant scene discovery ([#19838](https://github.com/Koenkk/zigbee2mqtt/issues/19838)) ([838629c](https://github.com/Koenkk/zigbee2mqtt/commit/838629ca42162d3d910b06bbc100b6270f75de33))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix `in_progress` attribute for Home Assistant update sensor ([#19542](https://github.com/Koenkk/zigbee2mqtt/issues/19542)) ([1f2bd84](https://github.com/Koenkk/zigbee2mqtt/commit/1f2bd841c4548777fc0c0f6ad6c54dfe913b3fd0))
|
||||
* Fix entity category for Home Assistant `binary_sensor` https://github.com/Koenkk/zigbee2mqtt/pull/19474 ([071842e](https://github.com/Koenkk/zigbee2mqtt/commit/071842ef49eccc002efefbad3abf2780e6fdd0a2))
|
||||
* Fix Home Assistant `in_progress` Erroneous JSON error https://github.com/Koenkk/zigbee2mqtt/issues/19597 ([2540fbd](https://github.com/Koenkk/zigbee2mqtt/commit/2540fbd771d01ee18cbb72c67cb0a24b44131912))
|
||||
* Fix invalid JSON error in Home Assistant update entity ([#19659](https://github.com/Koenkk/zigbee2mqtt/issues/19659)) ([da04d22](https://github.com/Koenkk/zigbee2mqtt/commit/da04d223b446000df5cc59ccb38115eb3e5eac96))
|
||||
* Fix unable to configure again after configure fails ([#19908](https://github.com/Koenkk/zigbee2mqtt/issues/19908)) ([a904e7a](https://github.com/Koenkk/zigbee2mqtt/commit/a904e7a396e34bbeef2e624702d55e9d0522b242))
|
||||
* **ignore:** Fix color_temp_percent test ([01d32cb](https://github.com/Koenkk/zigbee2mqtt/commit/01d32cb2301f6db11f61bca113a2db8d86c7acc1))
|
||||
* **ignore:** Fixes quote type for update JSON attribute discovery ([#19668](https://github.com/Koenkk/zigbee2mqtt/issues/19668)) ([baae082](https://github.com/Koenkk/zigbee2mqtt/commit/baae0822583fb29e079c57746b047cb602f017a7))
|
||||
* **ignore:** remove console.log in test ([8cf41a1](https://github.com/Koenkk/zigbee2mqtt/commit/8cf41a137aa8275e61a244b0096796466b10648d))
|
||||
* **ignore:** remove unused variable ([#19584](https://github.com/Koenkk/zigbee2mqtt/issues/19584)) ([be48d54](https://github.com/Koenkk/zigbee2mqtt/commit/be48d545cc03c95b0a6302b67cc04b4e7d187065))
|
||||
* **ignore:** update dependencies ([#19570](https://github.com/Koenkk/zigbee2mqtt/issues/19570)) ([fc073e8](https://github.com/Koenkk/zigbee2mqtt/commit/fc073e8743c0ba9c05d0a952f6bf3ae6fa804f68))
|
||||
* **ignore:** update dependencies ([#19679](https://github.com/Koenkk/zigbee2mqtt/issues/19679)) ([c8848be](https://github.com/Koenkk/zigbee2mqtt/commit/c8848be981c709ebb52bfb9c7347ab3a88cce82e))
|
||||
* **ignore:** update zigbee-herdsman to 0.22.0 ([#19547](https://github.com/Koenkk/zigbee2mqtt/issues/19547)) ([02a5a1b](https://github.com/Koenkk/zigbee2mqtt/commit/02a5a1b14f4907e7d170ef3e3e4f93b176c4bbc5))
|
||||
* **ignore:** update zigbee-herdsman to 0.22.1 ([#19602](https://github.com/Koenkk/zigbee2mqtt/issues/19602)) ([2d5bb24](https://github.com/Koenkk/zigbee2mqtt/commit/2d5bb2462eb547bc75df1bace2f01b6b15e6d171))
|
||||
* **ignore:** update zigbee-herdsman to 0.23.0 ([#19700](https://github.com/Koenkk/zigbee2mqtt/issues/19700)) ([a152bde](https://github.com/Koenkk/zigbee2mqtt/commit/a152bded93877cfab3839b9b22e7d22ce43fb263))
|
||||
* **ignore:** update zigbee-herdsman to 0.24.0 ([#19926](https://github.com/Koenkk/zigbee2mqtt/issues/19926)) ([e5dc271](https://github.com/Koenkk/zigbee2mqtt/commit/e5dc271a783aeec0c28e106f0f259a083d737d7c))
|
||||
* **ignore:** update zigbee-herdsman to 0.25.0 ([#19933](https://github.com/Koenkk/zigbee2mqtt/issues/19933)) ([2dabc1e](https://github.com/Koenkk/zigbee2mqtt/commit/2dabc1e61b897fdb041a42bda7adc6acac6bb2b3))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.107.0 ([#19548](https://github.com/Koenkk/zigbee2mqtt/issues/19548)) ([366e29d](https://github.com/Koenkk/zigbee2mqtt/commit/366e29d4d9126eaeca2476cc0fa56cf7faf70764))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.108.0 ([#19589](https://github.com/Koenkk/zigbee2mqtt/issues/19589)) ([1fd1686](https://github.com/Koenkk/zigbee2mqtt/commit/1fd1686ee63cbd2b41d9d0a13669f9c665eb36da))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.109.0 ([#19601](https://github.com/Koenkk/zigbee2mqtt/issues/19601)) ([b71aa0a](https://github.com/Koenkk/zigbee2mqtt/commit/b71aa0a3fd9a8e6a7fcc5e7bdb4a2f2b37cd690a))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.110.0 ([#19619](https://github.com/Koenkk/zigbee2mqtt/issues/19619)) ([5889cdd](https://github.com/Koenkk/zigbee2mqtt/commit/5889cdd9abe6b41899a4a7422b696bde8bbb0390))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.111.0 ([#19630](https://github.com/Koenkk/zigbee2mqtt/issues/19630)) ([354ada5](https://github.com/Koenkk/zigbee2mqtt/commit/354ada5c4c33d88677ce7f8b072b682dde6f19aa))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.112.0 ([#19643](https://github.com/Koenkk/zigbee2mqtt/issues/19643)) ([d26ea1a](https://github.com/Koenkk/zigbee2mqtt/commit/d26ea1a4112f9e142ac72e2add82784012c28c51))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.113.0 ([#19663](https://github.com/Koenkk/zigbee2mqtt/issues/19663)) ([c8dcd71](https://github.com/Koenkk/zigbee2mqtt/commit/c8dcd716ab271bdbe9d259943fb77d6d6accbc06))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.114.0 ([#19692](https://github.com/Koenkk/zigbee2mqtt/issues/19692)) ([6587812](https://github.com/Koenkk/zigbee2mqtt/commit/6587812b725d28dc200eb42f6b0714ed1ed76e9f))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.115.0 ([#19701](https://github.com/Koenkk/zigbee2mqtt/issues/19701)) ([157dff1](https://github.com/Koenkk/zigbee2mqtt/commit/157dff14d3ac32c70f13f5f7be0a85e2763bf2a6))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.116.0 ([#19714](https://github.com/Koenkk/zigbee2mqtt/issues/19714)) ([4815ac7](https://github.com/Koenkk/zigbee2mqtt/commit/4815ac711003632ac21977b120dea7444ebeda3e))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.117.0 ([#19727](https://github.com/Koenkk/zigbee2mqtt/issues/19727)) ([746066c](https://github.com/Koenkk/zigbee2mqtt/commit/746066c9205a7ab77393d1934aacb7f17209a1e0))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.119.0 ([#19740](https://github.com/Koenkk/zigbee2mqtt/issues/19740)) ([ad4bed8](https://github.com/Koenkk/zigbee2mqtt/commit/ad4bed89215e1727f0966708f510b8c933ac1a32))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.120.0 ([#19761](https://github.com/Koenkk/zigbee2mqtt/issues/19761)) ([fd19c86](https://github.com/Koenkk/zigbee2mqtt/commit/fd19c86cb19513dadae9a44ee8b50d287cc1860e))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.121.0 ([#19777](https://github.com/Koenkk/zigbee2mqtt/issues/19777)) ([d4e3aa9](https://github.com/Koenkk/zigbee2mqtt/commit/d4e3aa9d976b67cb1519ca78c9b8de5e70247ad9))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.123.1 ([#19809](https://github.com/Koenkk/zigbee2mqtt/issues/19809)) ([2d08baf](https://github.com/Koenkk/zigbee2mqtt/commit/2d08baf6204bde6f2ec90dba4a6a77963b14310c))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.124.0 ([#19836](https://github.com/Koenkk/zigbee2mqtt/issues/19836)) ([ef4d9e1](https://github.com/Koenkk/zigbee2mqtt/commit/ef4d9e1e558561c1625d1a0d41ddc718d40b5c61))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.125.0 ([#19854](https://github.com/Koenkk/zigbee2mqtt/issues/19854)) ([ef00a64](https://github.com/Koenkk/zigbee2mqtt/commit/ef00a64bf87adf38346461f5fa15757052400b2a))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.125.1 ([#19867](https://github.com/Koenkk/zigbee2mqtt/issues/19867)) ([51a9341](https://github.com/Koenkk/zigbee2mqtt/commit/51a93418fa2de62e6691ef23a65725d28d9c8b42))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.126.0 ([#19878](https://github.com/Koenkk/zigbee2mqtt/issues/19878)) ([b17487e](https://github.com/Koenkk/zigbee2mqtt/commit/b17487e6bb8d04bca0aaa5b0ad2338dc9eef46aa))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.127.0 ([#19894](https://github.com/Koenkk/zigbee2mqtt/issues/19894)) ([8a6d90d](https://github.com/Koenkk/zigbee2mqtt/commit/8a6d90d70e9ebcb2eeeacf385f4c3d47adeebc88))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.128.0 ([#19906](https://github.com/Koenkk/zigbee2mqtt/issues/19906)) ([4e128e2](https://github.com/Koenkk/zigbee2mqtt/commit/4e128e26fee314f2bdcce014132f85d5167ce0bf))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.129.0 ([#19934](https://github.com/Koenkk/zigbee2mqtt/issues/19934)) ([12521ad](https://github.com/Koenkk/zigbee2mqtt/commit/12521add3968cc9ed5394cf128bf2917f75597a5))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.130.0 ([#19942](https://github.com/Koenkk/zigbee2mqtt/issues/19942)) ([f976d4e](https://github.com/Koenkk/zigbee2mqtt/commit/f976d4efb48440ee7899490d31f14d820d71087d))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.130.1 ([#19945](https://github.com/Koenkk/zigbee2mqtt/issues/19945)) ([3bbf89c](https://github.com/Koenkk/zigbee2mqtt/commit/3bbf89cba038c6bdfab08c3e2860e561b2845a43))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.6.143 ([#19642](https://github.com/Koenkk/zigbee2mqtt/issues/19642)) ([9d5d617](https://github.com/Koenkk/zigbee2mqtt/commit/9d5d61754d2808c83eee48dfc6a0e0c4c740002a))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.6.144 ([#19776](https://github.com/Koenkk/zigbee2mqtt/issues/19776)) ([91aed7e](https://github.com/Koenkk/zigbee2mqtt/commit/91aed7e98e65d1c1c1469e3bfaab74233f0002bf))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.6.145 ([#19778](https://github.com/Koenkk/zigbee2mqtt/issues/19778)) ([ed20e2a](https://github.com/Koenkk/zigbee2mqtt/commit/ed20e2a1bc0a474157880c2c54f7c2d656658901))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.6.146 ([#19784](https://github.com/Koenkk/zigbee2mqtt/issues/19784)) ([c87ac9c](https://github.com/Koenkk/zigbee2mqtt/commit/c87ac9cc2b1d360ff0962deed973545a9846fcd6))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.6.147 ([#19801](https://github.com/Koenkk/zigbee2mqtt/issues/19801)) ([61b08c9](https://github.com/Koenkk/zigbee2mqtt/commit/61b08c94bfd3dd58efdb22632476f410fac56d13))
|
||||
* Make frontend listen on both IPv4 and IPv6 by default ([#19660](https://github.com/Koenkk/zigbee2mqtt/issues/19660)) ([45ac44d](https://github.com/Koenkk/zigbee2mqtt/commit/45ac44dd064a3f8aca87a25e7a0b2daafe53c5d3))
|
||||
* Map climate `idle` to `idle` instead of `off` in Home Assistant ([#19545](https://github.com/Koenkk/zigbee2mqtt/issues/19545)) ([64b26e4](https://github.com/Koenkk/zigbee2mqtt/commit/64b26e47da3d765bfde8ccac8f9a1e304b28c697))
|
||||
* Set entity category `config` for Home Assistant `temperature_display_mode` sensor ([#19631](https://github.com/Koenkk/zigbee2mqtt/issues/19631)) ([86ae0ed](https://github.com/Koenkk/zigbee2mqtt/commit/86ae0ed90fb1d94f5cb09545355968affa5654ca))
|
||||
* **typo:** correct misspell ([#19581](https://github.com/Koenkk/zigbee2mqtt/issues/19581)) ([13443a4](https://github.com/Koenkk/zigbee2mqtt/commit/13443a42ce7239a7308efb829c8ff007e59ca45d))
|
||||
|
||||
## [1.33.2](https://github.com/Koenkk/zigbee2mqtt/compare/1.33.1...1.33.2) (2023-11-01)
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,5 @@ module.exports = {
|
||||
assumptions: {setPublicClassFields: true},
|
||||
plugins: [
|
||||
['@babel/plugin-proposal-decorators', {'legacy': true}],
|
||||
['@babel/plugin-proposal-class-properties'],
|
||||
],
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.18.4 as base
|
||||
FROM alpine:3.18.5 as base
|
||||
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache tzdata eudev tini nodejs
|
||||
|
||||
+2
-2
@@ -121,7 +121,7 @@ export class Controller {
|
||||
const devices = this.zigbee.devices(false);
|
||||
logger.info(`Currently ${devices.length} devices are joined:`);
|
||||
for (const device of devices) {
|
||||
const model = device.definition ?
|
||||
const model = device.isSupported ?
|
||||
`${device.definition.model} - ${device.definition.vendor} ${device.definition.description}` :
|
||||
'Not supported';
|
||||
logger.info(`${device.name} (${device.ieeeAddr}): ${model} (${device.zh.type})`);
|
||||
@@ -241,7 +241,7 @@ export class Controller {
|
||||
|
||||
if (entity.isDevice() && settings.get().mqtt.include_device_information) {
|
||||
message.device = {
|
||||
friendlyName: entity.name, model: entity.definition ? entity.definition.model : 'unknown',
|
||||
friendlyName: entity.name, model: entity.definition?.model,
|
||||
ieeeAddr: entity.ieeeAddr, networkAddress: entity.zh.networkAddress, type: entity.zh.type,
|
||||
manufacturerID: entity.zh.manufacturerID,
|
||||
powerSource: entity.zh.powerSource, applicationVersion: entity.zh.applicationVersion,
|
||||
|
||||
@@ -4,6 +4,7 @@ import utils from '../util/utils';
|
||||
import * as settings from '../util/settings';
|
||||
import debounce from 'debounce';
|
||||
import bind from 'bind-decorator';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
const retrieveOnReconnect = [
|
||||
{keys: ['state']},
|
||||
@@ -17,6 +18,7 @@ export default class Availability extends Extension {
|
||||
private retrieveStateDebouncers: {[s: string]: () => void} = {};
|
||||
private pingQueue: Device[] = [];
|
||||
private pingQueueExecuting = false;
|
||||
private stopped = false;
|
||||
|
||||
private getTimeout(device: Device): number {
|
||||
if (typeof device.options.availability === 'object' && device.options.availability?.timeout != null) {
|
||||
@@ -92,6 +94,11 @@ export default class Availability extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.stopped) {
|
||||
// Exit here to avoid triggering any follow-up activity (e.g., re-queuing another ping attempt).
|
||||
return;
|
||||
}
|
||||
|
||||
this.publishAvailability(device, !pingedSuccessfully);
|
||||
this.resetTimer(device);
|
||||
this.removeFromPingQueue(device);
|
||||
@@ -103,6 +110,10 @@ export default class Availability extends Extension {
|
||||
}
|
||||
|
||||
override async start(): Promise<void> {
|
||||
if (this.stopped) {
|
||||
throw new Error('This extension cannot be restarted.');
|
||||
}
|
||||
|
||||
this.eventBus.onEntityRenamed(this, (data) => {
|
||||
if (utils.isAvailabilityEnabledForEntity(data.entity, settings.get())) {
|
||||
this.mqtt.publish(`${data.from}/availability`, null, {retain: true, qos: 1});
|
||||
@@ -183,8 +194,10 @@ export default class Availability extends Extension {
|
||||
}
|
||||
|
||||
override async stop(): Promise<void> {
|
||||
this.stopped = true;
|
||||
this.pingQueue = [];
|
||||
Object.values(this.timers).forEach((t) => clearTimeout(t));
|
||||
super.stop();
|
||||
await super.stop();
|
||||
}
|
||||
|
||||
private retrieveState(device: Device): void {
|
||||
@@ -199,8 +212,13 @@ export default class Availability extends Extension {
|
||||
for (const item of retrieveOnReconnect) {
|
||||
if (item.condition && this.state.get(device) && !item.condition(this.state.get(device))) continue;
|
||||
const converter = device.definition.toZigbee.find((c) => c.key.find((k) => item.keys.includes(k)));
|
||||
await converter?.convertGet?.(device.endpoint(), item.keys[0],
|
||||
{message: this.state.get(device), mapped: device.definition})
|
||||
const options: KeyValue = device.options;
|
||||
const state = this.state.get(device);
|
||||
const meta: zhc.Tz.Meta = {
|
||||
message: this.state.get(device), mapped: device.definition, logger, endpoint_name: null,
|
||||
options, state, device: device.zh,
|
||||
};
|
||||
await converter?.convertGet?.(device.endpoint(), item.keys[0], meta)
|
||||
.catch((e) => {
|
||||
logger.error(`Failed to read state of '${device.name}' after reconnect (${e.message})`);
|
||||
});
|
||||
|
||||
+23
-27
@@ -12,13 +12,13 @@ import Group from '../model/group';
|
||||
import data from '../util/data';
|
||||
import JSZip from 'jszip';
|
||||
import fs from 'fs';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
|
||||
|
||||
type Scene = {id: number, name: string};
|
||||
type DefinitionPayload = {
|
||||
model: string, vendor: string, description: string, exposes: zhc.DefinitionExpose[], supports_ota:
|
||||
boolean, icon: string, options: zhc.DefinitionExpose[],
|
||||
model: string, vendor: string, description: string, exposes: zhc.Expose[], supports_ota:
|
||||
boolean, icon: string, options: zhc.Expose[],
|
||||
};
|
||||
|
||||
export default class Bridge extends Extension {
|
||||
@@ -35,6 +35,7 @@ export default class Bridge extends Extension {
|
||||
'device/options': this.deviceOptions,
|
||||
'device/configure_reporting': this.deviceConfigureReporting,
|
||||
'device/remove': this.deviceRemove,
|
||||
'device/generate_external_definition': this.deviceGenerateExternalDefinition,
|
||||
'device/rename': this.deviceRename,
|
||||
'group/add': this.groupAdd,
|
||||
'group/options': this.groupOptions,
|
||||
@@ -100,7 +101,7 @@ export default class Bridge extends Extension {
|
||||
const payload: KeyValue =
|
||||
{friendly_name: data.device.name, status: data.status, ieee_address: data.device.ieeeAddr};
|
||||
if (data.status === 'successful') {
|
||||
payload.supported = !!data.device.definition;
|
||||
payload.supported = data.device.isSupported;
|
||||
payload.definition = this.getDefinitionPayload(data.device);
|
||||
}
|
||||
publishEvent('device_interview', payload);
|
||||
@@ -451,6 +452,18 @@ export default class Bridge extends Extension {
|
||||
}, null);
|
||||
}
|
||||
|
||||
@bind async deviceGenerateExternalDefinition(message: string | KeyValue): Promise<MQTTResponse> {
|
||||
if (typeof message !== 'object' || !message.hasOwnProperty('id')) {
|
||||
throw new Error(`Invalid payload`);
|
||||
}
|
||||
|
||||
const parsedID = utils.parseEntityID(message.id);
|
||||
const device = this.getEntity('device', parsedID.ID) as Device;
|
||||
const source = await zhc.generateExternalDefinitionSource(device.zh);
|
||||
|
||||
return utils.getResponse(message, {id: message.id, source}, null);
|
||||
}
|
||||
|
||||
async renameEntity(entityType: 'group' | 'device', message: string | KeyValue): Promise<MQTTResponse> {
|
||||
const deviceAndHasLast = entityType === 'device' && typeof message === 'object' && message.last === true;
|
||||
if (typeof message !== 'object' || (!message.hasOwnProperty('from') && !deviceAndHasLast) ||
|
||||
@@ -605,25 +618,6 @@ export default class Bridge extends Extension {
|
||||
'bridge/info', stringify(payload), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
|
||||
}
|
||||
|
||||
private getScenes(entity: zh.Endpoint | zh.Group): Scene[] {
|
||||
const scenes: {[id: number]: Scene} = {};
|
||||
const endpoints = utils.isEndpoint(entity) ? [entity] : entity.members;
|
||||
const groupID = utils.isEndpoint(entity) ? 0 : entity.groupID;
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
for (const [key, data] of Object.entries(endpoint.meta?.scenes || {})) {
|
||||
const split = key.split('_');
|
||||
const sceneID = parseInt(split[0], 10);
|
||||
const sceneGroupID = parseInt(split[1], 10);
|
||||
if (sceneGroupID === groupID) {
|
||||
scenes[sceneID] = {id: sceneID, name: (data as KeyValue).name || `Scene ${sceneID}`};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(scenes);
|
||||
}
|
||||
|
||||
async publishDevices(): Promise<void> {
|
||||
interface Data {
|
||||
bindings: {cluster: string, target: {type: string, endpoint?: number, ieee_address?: string, id?: number}}[]
|
||||
@@ -636,7 +630,7 @@ export default class Bridge extends Extension {
|
||||
const endpoints: {[s: number]: Data} = {};
|
||||
for (const endpoint of device.zh.endpoints) {
|
||||
const data: Data = {
|
||||
scenes: this.getScenes(endpoint),
|
||||
scenes: utils.getScenes(endpoint),
|
||||
bindings: [],
|
||||
configured_reportings: [],
|
||||
clusters: {
|
||||
@@ -669,7 +663,7 @@ export default class Bridge extends Extension {
|
||||
ieee_address: device.ieeeAddr,
|
||||
type: device.zh.type,
|
||||
network_address: device.zh.networkAddress,
|
||||
supported: !!device.definition,
|
||||
supported: device.isSupported,
|
||||
friendly_name: device.name,
|
||||
disabled: !!device.options.disabled,
|
||||
description: device.options.description,
|
||||
@@ -695,7 +689,7 @@ export default class Bridge extends Extension {
|
||||
id: g.ID,
|
||||
friendly_name: g.ID === 901 ? 'default_bind_group' : g.name,
|
||||
description: g.options.description,
|
||||
scenes: this.getScenes(g.zh),
|
||||
scenes: utils.getScenes(g.zh),
|
||||
members: g.zh.members.map((e) => {
|
||||
return {ieee_address: e.getDevice().ieeeAddr, endpoint: e.ID};
|
||||
}),
|
||||
@@ -707,7 +701,9 @@ export default class Bridge extends Extension {
|
||||
|
||||
getDefinitionPayload(device: Device): DefinitionPayload {
|
||||
if (!device.definition) return null;
|
||||
let icon = device.options.icon ? device.options.icon : device.definition.icon;
|
||||
// @ts-expect-error icon is valid for external definitions
|
||||
const definitionIcon = device.definition.icon;
|
||||
let icon = device.options.icon ?? definitionIcon;
|
||||
if (icon) {
|
||||
icon = icon.replace('${zigbeeModel}', utils.sanitizeImageParameter(device.zh.modelID));
|
||||
icon = icon.replace('${model}', utils.sanitizeImageParameter(device.definition.model));
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as settings from '../util/settings';
|
||||
import utils from '../util/utils';
|
||||
import logger from '../util/logger';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import zhc from 'zigbee-herdsman-converters';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
import Extension from './extension';
|
||||
import bind from 'bind-decorator';
|
||||
import Device from '../model/device';
|
||||
@@ -114,8 +114,7 @@ export default class Configure extends Extension {
|
||||
|
||||
logger.info(`Configuring '${device.name}'`);
|
||||
try {
|
||||
await device.definition.configure(device.zh, this.zigbee.firstCoordinatorEndpoint(), logger,
|
||||
device.options);
|
||||
await device.definition.configure(device.zh, this.zigbee.firstCoordinatorEndpoint(), logger);
|
||||
logger.info(`Successfully configured '${device.name}'`);
|
||||
device.zh.meta.configured = zhc.getConfigureKey(device.definition);
|
||||
device.zh.save();
|
||||
@@ -129,8 +128,8 @@ export default class Configure extends Extension {
|
||||
if (throwError) {
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
this.configuring.delete(device.ieeeAddr);
|
||||
}
|
||||
|
||||
this.configuring.delete(device.ieeeAddr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import zhc from 'zigbee-herdsman-converters';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
import * as settings from '../util/settings';
|
||||
import utils from '../util/utils';
|
||||
import {loadExternalConverter} from '../util/utils';
|
||||
import Extension from './extension';
|
||||
import logger from '../util/logger';
|
||||
|
||||
export default class ExternalConverters extends Extension {
|
||||
constructor(zigbee: Zigbee, mqtt: MQTT, state: State, publishEntityState: PublishEntityState,
|
||||
@@ -9,10 +10,23 @@ export default class ExternalConverters extends Extension {
|
||||
restartCallback: () => void, addExtension: (extension: Extension) => Promise<void>) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension);
|
||||
|
||||
for (const definition of utils.getExternalConvertersDefinitions(settings.get())) {
|
||||
const toAdd = {...definition};
|
||||
delete toAdd['homeassistant'];
|
||||
zhc.addDeviceDefinition(toAdd);
|
||||
for (const file of settings.get().external_converters) {
|
||||
try {
|
||||
for (const definition of loadExternalConverter(file)) {
|
||||
const toAdd = {...definition};
|
||||
delete toAdd['homeassistant'];
|
||||
zhc.addDefinition(toAdd);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to load external converter file '${file}' (${error.message})`);
|
||||
logger.error(
|
||||
`Probably there is a syntax error in the file or the external converter is not ` +
|
||||
`compatible with the current Zigbee2MQTT version`);
|
||||
logger.error(
|
||||
`Note that external converters are not meant for long term usage, it's meant for local ` +
|
||||
`testing after which a pull request should be created to add out-of-the-box support for the device`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,10 @@ export default class Frontend extends Extension {
|
||||
this.wss = new WebSocket.Server({noServer: true});
|
||||
this.wss.on('connection', this.onWebSocketConnection);
|
||||
|
||||
if (this.host.startsWith('/')) {
|
||||
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}`);
|
||||
} else {
|
||||
|
||||
@@ -7,13 +7,14 @@ import bind from 'bind-decorator';
|
||||
import Extension from './extension';
|
||||
import Device from '../model/device';
|
||||
import Group from '../model/group';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
const topicRegex =
|
||||
new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/group/members/(remove|add|remove_all)$`);
|
||||
const legacyTopicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(.+)/(remove|add|remove_all)$`);
|
||||
const legacyTopicRegexRemoveAll = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/remove_all$`);
|
||||
|
||||
const stateProperties: {[s: string]: (value: string, exposes: zhc.DefinitionExpose[]) => boolean} = {
|
||||
const stateProperties: {[s: string]: (value: string, exposes: zhc.Expose[]) => boolean} = {
|
||||
'state': () => true,
|
||||
'brightness': (value, exposes) =>
|
||||
!!exposes.find((e) => e.type === 'light' && e.features.find((f) => f.name === 'brightness')),
|
||||
|
||||
+382
-63
@@ -1,10 +1,11 @@
|
||||
import * as settings from '../util/settings';
|
||||
import logger from '../util/logger';
|
||||
import utils from '../util/utils';
|
||||
import utils, {isNumericExposeFeature, isBinaryExposeFeature, isEnumExposeFeature} from '../util/utils';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import assert from 'assert';
|
||||
import Extension from './extension';
|
||||
import bind from 'bind-decorator';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
interface MockProperty {property: string, value: KeyValue | string}
|
||||
|
||||
@@ -52,7 +53,7 @@ const legacyMapping = [
|
||||
},
|
||||
];
|
||||
|
||||
const featurePropertyWithoutEndpoint = (feature: zhc.DefinitionExposeFeature): string => {
|
||||
const featurePropertyWithoutEndpoint = (feature: zhc.Feature): string => {
|
||||
if (feature.endpoint) {
|
||||
return feature.property.slice(0, -1 + -1 * feature.endpoint.length);
|
||||
} else {
|
||||
@@ -60,6 +61,47 @@ const featurePropertyWithoutEndpoint = (feature: zhc.DefinitionExposeFeature): s
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This class handles the bridge entity configuration for Home Assistant Discovery.
|
||||
*/
|
||||
class Bridge {
|
||||
private coordinatorIeeeAddress: string;
|
||||
private coordinatorType: string;
|
||||
private coordinatorFirmwareVersion: string;
|
||||
private discoveryEntries: DiscoveryEntry[];
|
||||
|
||||
readonly options: {
|
||||
ID?: string,
|
||||
homeassistant?: KeyValue,
|
||||
};
|
||||
|
||||
/* eslint-disable brace-style */
|
||||
get ID(): string {return this.coordinatorIeeeAddress;}
|
||||
get name(): string {return 'bridge';}
|
||||
get hardwareVersion(): string {return this.coordinatorType;}
|
||||
get firmwareVersion(): string {return this.coordinatorFirmwareVersion;}
|
||||
get configs(): DiscoveryEntry[] {return this.discoveryEntries;}
|
||||
|
||||
constructor(ieeeAdress: string, version: zh.CoordinatorVersion, discovery: DiscoveryEntry[]) {
|
||||
this.coordinatorIeeeAddress = ieeeAdress;
|
||||
this.coordinatorType = version.type;
|
||||
/* istanbul ignore next */
|
||||
this.coordinatorFirmwareVersion = version.meta.revision ? `${version.meta.revision}` : '';
|
||||
this.discoveryEntries = discovery;
|
||||
|
||||
this.options = {
|
||||
ID: `bridge_${ieeeAdress}`,
|
||||
homeassistant: {
|
||||
name: `Zigbee2MQTT Bridge`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
isDevice(): this is Device {return false;}
|
||||
isGroup(): this is Group {return false;}
|
||||
/* eslint-enable brace-style */
|
||||
}
|
||||
|
||||
/**
|
||||
* This extensions handles integration with HomeAssistant
|
||||
*/
|
||||
@@ -72,6 +114,8 @@ export default class HomeAssistant extends Extension {
|
||||
private entityAttributes = settings.get().homeassistant.legacy_entity_attributes;
|
||||
private zigbee2MQTTVersion: string;
|
||||
private discoveryOrigin: {name: string, sw: string, url: string};
|
||||
private bridge: Bridge;
|
||||
private bridgeIdentifier: string;
|
||||
|
||||
constructor(zigbee: Zigbee, mqtt: MQTT, state: State, publishEntityState: PublishEntityState,
|
||||
eventBus: EventBus, enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
|
||||
@@ -89,7 +133,8 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
this.zigbee2MQTTVersion = (await utils.getZigbee2MQTTVersion(false)).version;
|
||||
this.discoveryOrigin = {name: 'Zigbee2MQTT', sw: this.zigbee2MQTTVersion, url: 'https://www.zigbee2mqtt.io'};
|
||||
|
||||
this.bridge = this.getBridgeEntity(await this.zigbee.getCoordinatorVersion());
|
||||
this.bridgeIdentifier = this.getDevicePayload(this.bridge).identifiers[0];
|
||||
this.eventBus.onDeviceRemoved(this, this.onDeviceRemoved);
|
||||
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
|
||||
this.eventBus.onEntityRenamed(this, this.onEntityRenamed);
|
||||
@@ -99,6 +144,7 @@ export default class HomeAssistant extends Extension {
|
||||
this.eventBus.onDeviceJoined(this, this.onZigbeeEvent);
|
||||
this.eventBus.onDeviceInterview(this, this.onZigbeeEvent);
|
||||
this.eventBus.onDeviceMessage(this, this.onZigbeeEvent);
|
||||
this.eventBus.onScenesChanged(this, this.onScenesChanged);
|
||||
this.eventBus.onEntityOptionsChanged(this, (data) => this.discover(data.entity, true));
|
||||
|
||||
this.mqtt.subscribe(this.statusTopic);
|
||||
@@ -106,7 +152,7 @@ export default class HomeAssistant extends Extension {
|
||||
this.mqtt.subscribe(`${this.discoveryTopic}/#`);
|
||||
|
||||
// MQTT discovery of all paired devices on startup.
|
||||
for (const entity of [...this.zigbee.devices(false), ...this.zigbee.groups()]) {
|
||||
for (const entity of [this.bridge, ...this.zigbee.devices(false), ...this.zigbee.groups()]) {
|
||||
this.discover(entity, true);
|
||||
}
|
||||
|
||||
@@ -114,8 +160,8 @@ export default class HomeAssistant extends Extension {
|
||||
this.eventBus.emitPublishAvailability();
|
||||
}
|
||||
|
||||
private exposeToConfig(exposes: zhc.DefinitionExpose[], entityType: 'device' | 'group',
|
||||
allExposes: zhc.DefinitionExpose[], definition?: zhc.Definition): DiscoveryEntry[] {
|
||||
private exposeToConfig(exposes: zhc.Expose[], entityType: 'device' | 'group',
|
||||
allExposes: zhc.Expose[], definition?: zhc.Definition): DiscoveryEntry[] {
|
||||
// For groups an array of exposes (of the same type) is passed, this is to determine e.g. what features
|
||||
// to use for a bulb (e.g. color_xy/color_temp)
|
||||
assert(entityType === 'group' || exposes.length === 1, 'Multiple exposes for device not allowed');
|
||||
@@ -125,7 +171,7 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
const discoveryEntries: DiscoveryEntry[] = [];
|
||||
const endpoint = entityType === 'device' ? exposes[0].endpoint : undefined;
|
||||
const getProperty = (feature: zhc.DefinitionExposeFeature): string => entityType === 'group' ?
|
||||
const getProperty = (feature: zhc.Feature): string => entityType === 'group' ?
|
||||
featurePropertyWithoutEndpoint(feature) : feature.property;
|
||||
|
||||
/* istanbul ignore else */
|
||||
@@ -169,7 +215,7 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
if (hasColorTemp) {
|
||||
const colorTemps = exposes.map((expose) => expose.features.find((e) => e.name === 'color_temp'))
|
||||
.filter((e) => e);
|
||||
.filter((e) => e).filter(isNumericExposeFeature);
|
||||
const max = Math.min(...colorTemps.map((e) => e.value_max));
|
||||
const min = Math.max(...colorTemps.map((e) => e.value_min));
|
||||
discoveryEntry.discovery_payload.max_mireds = max;
|
||||
@@ -177,7 +223,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
const effects = utils.arrayUnique(utils.flatten(
|
||||
allExposes.filter((e) => e.type === 'enum' && e.name === 'effect').map((e) => e.values)));
|
||||
allExposes.filter(isEnumExposeFeature).filter((e) => e.name === 'effect').map((e) => e.values)));
|
||||
if (effects.length) {
|
||||
discoveryEntry.discovery_payload.effect = true;
|
||||
discoveryEntry.discovery_payload.effect_list = effects;
|
||||
@@ -185,7 +231,7 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
} else if (firstExpose.type === 'switch') {
|
||||
const state = firstExpose.features.find((f) => f.name === 'state');
|
||||
const state = firstExpose.features.filter(isBinaryExposeFeature).find((f) => f.name === 'state');
|
||||
const property = getProperty(state);
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'switch',
|
||||
@@ -217,7 +263,8 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
} else if (firstExpose.type === 'climate') {
|
||||
const setpointProperties = ['occupied_heating_setpoint', 'current_heating_setpoint'];
|
||||
const setpoint = firstExpose.features.find((f) => setpointProperties.includes(f.name));
|
||||
const setpoint = firstExpose.features.filter(isNumericExposeFeature)
|
||||
.find((f) => setpointProperties.includes(f.name));
|
||||
assert(setpoint, 'No setpoint found');
|
||||
const temperature = firstExpose.features.find((f) => f.name === 'local_temperature');
|
||||
assert(temperature, 'No temperature found');
|
||||
@@ -242,7 +289,7 @@ export default class HomeAssistant extends Extension {
|
||||
},
|
||||
};
|
||||
|
||||
const mode = firstExpose.features.find((f) => f.name === 'system_mode');
|
||||
const mode = firstExpose.features.filter(isEnumExposeFeature).find((f) => f.name === 'system_mode');
|
||||
if (mode) {
|
||||
if (mode.values.includes('sleep')) {
|
||||
// 'sleep' is not supported by Home Assistant, but is valid according to ZCL
|
||||
@@ -261,7 +308,7 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntry.mockProperties.push({property: state.property, value: null});
|
||||
discoveryEntry.discovery_payload.action_topic = true;
|
||||
discoveryEntry.discovery_payload.action_template = `{% set values = ` +
|
||||
`{None:None,'idle':'off','heat':'heating','cool':'cooling','fan_only':'fan'}` +
|
||||
`{None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'}` +
|
||||
` %}{{ values[value_json.${state.property}] }}`;
|
||||
}
|
||||
|
||||
@@ -282,7 +329,8 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntry.discovery_payload.temperature_state_topic = true;
|
||||
}
|
||||
|
||||
const fanMode = firstExpose.features.find((f) => f.name === 'fan_mode');
|
||||
const fanMode = firstExpose.features.filter(isEnumExposeFeature)
|
||||
.find((f) => f.name === 'fan_mode');
|
||||
if (fanMode) {
|
||||
discoveryEntry.discovery_payload.fan_modes = fanMode.values;
|
||||
discoveryEntry.discovery_payload.fan_mode_command_topic = true;
|
||||
@@ -291,7 +339,8 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntry.discovery_payload.fan_mode_state_topic = true;
|
||||
}
|
||||
|
||||
const swingMode = firstExpose.features.find((f) => f.name === 'swing_mode');
|
||||
const swingMode = firstExpose.features.filter(isEnumExposeFeature)
|
||||
.find((f) => f.name === 'swing_mode');
|
||||
if (swingMode) {
|
||||
discoveryEntry.discovery_payload.swing_modes = swingMode.values;
|
||||
discoveryEntry.discovery_payload.swing_mode_command_topic = true;
|
||||
@@ -300,7 +349,7 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntry.discovery_payload.swing_mode_state_topic = true;
|
||||
}
|
||||
|
||||
const preset = firstExpose.features.find((f) => f.name === 'preset');
|
||||
const preset = firstExpose.features.filter(isEnumExposeFeature).find((f) => f.name === 'preset');
|
||||
if (preset) {
|
||||
discoveryEntry.discovery_payload.preset_modes = preset.values;
|
||||
discoveryEntry.discovery_payload.preset_mode_command_topic = 'preset';
|
||||
@@ -309,7 +358,8 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntry.discovery_payload.preset_mode_state_topic = true;
|
||||
}
|
||||
|
||||
const tempCalibration = firstExpose.features.find((f) => f.name === 'local_temperature_calibration');
|
||||
const tempCalibration = firstExpose.features.filter(isNumericExposeFeature)
|
||||
.find((f) => f.name === 'local_temperature_calibration');
|
||||
if (tempCalibration) {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'number',
|
||||
@@ -336,7 +386,8 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
}
|
||||
|
||||
const piHeatingDemand = firstExpose.features.find((f) => f.name === 'pi_heating_demand');
|
||||
const piHeatingDemand = firstExpose.features.filter(isNumericExposeFeature)
|
||||
.find((f) => f.name === 'pi_heating_demand');
|
||||
if (piHeatingDemand) {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'sensor',
|
||||
@@ -357,7 +408,7 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
} else if (firstExpose.type === 'lock') {
|
||||
assert(!endpoint, `Endpoint not supported for lock type`);
|
||||
const state = firstExpose.features.find((f) => f.name === 'state');
|
||||
const state = firstExpose.features.filter(isBinaryExposeFeature).find((f) => f.name === 'state');
|
||||
assert(state, 'No state found');
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'lock',
|
||||
@@ -403,8 +454,8 @@ export default class HomeAssistant extends Extension {
|
||||
?.features.find((f) => f.name === 'position');
|
||||
const tilt = exposes.find((expose) => expose.features.find((e) => e.name === 'tilt'))
|
||||
?.features.find((f) => f.name === 'tilt');
|
||||
const motorState = allExposes?.find((e) => e.type === 'enum' &&
|
||||
['motor_state', 'moving'].includes(e.name) && e.access === ACCESS_STATE);
|
||||
const motorState = allExposes?.filter(isEnumExposeFeature)
|
||||
.find((e) => ['motor_state', 'moving'].includes(e.name) && e.access === ACCESS_STATE);
|
||||
const running = allExposes?.find((e) => e.type === 'binary' && e.name === 'running');
|
||||
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
@@ -435,9 +486,9 @@ export default class HomeAssistant extends Extension {
|
||||
const closingLookup = ['closing', 'close', 'backward', 'back', 'reverse', 'down', 'declining'];
|
||||
const stoppedLookup = ['stopped', 'stop', 'pause', 'paused'];
|
||||
|
||||
const openingState = motorState.values.find((s) => openingLookup.includes(s.toLowerCase()));
|
||||
const closingState = motorState.values.find((s) => closingLookup.includes(s.toLowerCase()));
|
||||
const stoppedState = motorState.values.find((s) => stoppedLookup.includes(s.toLowerCase()));
|
||||
const openingState = motorState.values.find((s) => openingLookup.includes(s.toString().toLowerCase()));
|
||||
const closingState = motorState.values.find((s) => closingLookup.includes(s.toString().toLowerCase()));
|
||||
const stoppedState = motorState.values.find((s) => stoppedLookup.includes(s.toString().toLowerCase()));
|
||||
|
||||
if (openingState && closingState && stoppedState) {
|
||||
discoveryEntry.discovery_payload.state_opening = openingState;
|
||||
@@ -495,7 +546,7 @@ export default class HomeAssistant extends Extension {
|
||||
},
|
||||
};
|
||||
|
||||
const speed = firstExpose.features.find((e) => e.name === 'mode');
|
||||
const speed = firstExpose.features.filter(isEnumExposeFeature).find((e) => e.name === 'mode');
|
||||
if (speed) {
|
||||
// A fan entity in Home Assistant 2021.3 and above may have a speed,
|
||||
// controlled by a percentage from 1 to 100, and/or non-speed presets.
|
||||
@@ -523,7 +574,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
const allowed = [...speeds, ...presets];
|
||||
speed.values.forEach((s) => assert(allowed.includes(s)));
|
||||
speed.values.forEach((s) => assert(allowed.includes(s.toString())));
|
||||
const percentValues = speeds.map((s, i) => `'${s}':${i}`).join(', ');
|
||||
const percentCommands = speeds.map((s, i) => `${i}:'${s}'`).join(', ');
|
||||
const presetList = presets.map((s) => `'${s}'`).join(', ');
|
||||
@@ -546,11 +597,16 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
} else if (firstExpose.type === 'binary') {
|
||||
} else if (isBinaryExposeFeature(firstExpose)) {
|
||||
const lookup: {[s: string]: KeyValue}= {
|
||||
battery_low: {entity_category: 'diagnostic', device_class: 'battery'},
|
||||
button_lock: {entity_category: 'config', icon: 'mdi:lock'},
|
||||
calibration: {entity_category: 'config', icon: 'mdi:progress-wrench'},
|
||||
capabilities_configurable_curve: {entity_category: 'diagnostic', icon: 'mdi:tune'},
|
||||
capabilities_forward_phase_control: {entity_category: 'diagnostic', icon: 'mdi:tune'},
|
||||
capabilities_overload_detection: {entity_category: 'diagnostic', icon: 'mdi:tune'},
|
||||
capabilities_reactance_discriminator: {entity_category: 'diagnostic', icon: 'mdi:tune'},
|
||||
capabilities_reverse_phase_control: {entity_category: 'diagnostic', icon: 'mdi:tune'},
|
||||
carbon_monoxide: {device_class: 'carbon_monoxide'},
|
||||
card: {entity_category: 'config', icon: 'mdi:clipboard-check'},
|
||||
child_lock: {entity_category: 'config', icon: 'mdi:account-lock'},
|
||||
@@ -576,6 +632,11 @@ export default class HomeAssistant extends Extension {
|
||||
presence: {device_class: 'presence'},
|
||||
smoke: {device_class: 'smoke'},
|
||||
sos: {device_class: 'safety'},
|
||||
status_capacitive_load: {entity_category: 'diagnostic', icon: 'mdi:tune'},
|
||||
status_forward_phase_control: {entity_category: 'diagnostic', icon: 'mdi:tune'},
|
||||
status_inductive_load: {entity_category: 'diagnostic', icon: 'mdi:tune'},
|
||||
status_overload: {entity_category: 'diagnostic', icon: 'mdi:tune'},
|
||||
status_reverse_phase_control: {entity_category: 'diagnostic', icon: 'mdi:tune'},
|
||||
tamper: {device_class: 'tamper'},
|
||||
temperature_scale: {entity_category: 'config', icon: 'mdi:temperature-celsius'},
|
||||
test: {entity_category: 'diagnostic', icon: 'mdi:test-tube'},
|
||||
@@ -635,7 +696,7 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
}
|
||||
} else if (firstExpose.type === 'numeric') {
|
||||
} else if (isNumericExposeFeature(firstExpose)) {
|
||||
const lookup: {[s: string]: KeyValue} = {
|
||||
ac_frequency: {device_class: 'frequency', enabled_by_default: false, entity_category: 'diagnostic',
|
||||
state_class: 'measurement'},
|
||||
@@ -651,6 +712,10 @@ export default class HomeAssistant extends Extension {
|
||||
auto_relock_time: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
away_preset_days: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
away_preset_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
ballast_maximum_level: {entity_category: 'config'},
|
||||
ballast_minimum_level: {entity_category: 'config'},
|
||||
ballast_physical_maximum_level: {entity_category: 'diagnostic'},
|
||||
ballast_physical_minimum_level: {entity_category: 'diagnostic'},
|
||||
battery: {device_class: 'battery', entity_category: 'diagnostic', state_class: 'measurement'},
|
||||
battery2: {device_class: 'battery', entity_category: 'diagnostic', state_class: 'measurement'},
|
||||
battery_voltage: {device_class: 'voltage', entity_category: 'diagnostic', state_class: 'measurement',
|
||||
@@ -713,6 +778,7 @@ export default class HomeAssistant extends Extension {
|
||||
max_temperature_limit: {entity_category: 'config', icon: 'mdi:thermometer-high'},
|
||||
min_temperature_limit: {entity_category: 'config', icon: 'mdi:thermometer-low'},
|
||||
min_temperature: {entity_category: 'config', icon: 'mdi:thermometer-low'},
|
||||
minimum_on_level: {entity_category: 'config'},
|
||||
measurement_poll_interval: {entity_category: 'config', icon: 'mdi:clock-out'},
|
||||
occupancy_timeout: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
pm10: {device_class: 'pm10', state_class: 'measurement'},
|
||||
@@ -805,6 +871,12 @@ export default class HomeAssistant extends Extension {
|
||||
delete discoveryEntry.discovery_payload.device_class;
|
||||
}
|
||||
|
||||
// entity_category config is not allowed for sensors
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/20252
|
||||
if (discoveryEntry.discovery_payload.entity_category === 'config') {
|
||||
discoveryEntry.discovery_payload.entity_category = 'diagnostic';
|
||||
}
|
||||
|
||||
// Let Home Assistant generate entity name when device_class is present
|
||||
if (discoveryEntry.discovery_payload.device_class) delete discoveryEntry.discovery_payload.name;
|
||||
|
||||
@@ -846,7 +918,7 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
}
|
||||
} else if (firstExpose.type === 'enum') {
|
||||
} else if (isEnumExposeFeature(firstExpose)) {
|
||||
const lookup: {[s: string]: KeyValue} = {
|
||||
action: {icon: 'mdi:gesture-double-tap'},
|
||||
alarm_humidity: {entity_category: 'config', icon: 'mdi:water-percent-alert'},
|
||||
@@ -877,6 +949,7 @@ export default class HomeAssistant extends Extension {
|
||||
sound_volume: {entity_category: 'config', icon: 'mdi:volume-high'},
|
||||
status: {icon: 'mdi:state-machine'},
|
||||
switch_type: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
temperature_display_mode: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
temperature_sensor_select: {entity_category: 'config', icon: 'mdi:home-thermometer'},
|
||||
thermostat_unit: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
volume: {entity_category: 'config', icon: 'mdi: volume-high'},
|
||||
@@ -927,6 +1000,7 @@ export default class HomeAssistant extends Extension {
|
||||
const settableText = firstExpose.type === 'text' && firstExpose.access & ACCESS_SET;
|
||||
const lookup: {[s: string]: KeyValue} = {
|
||||
action: {icon: 'mdi:gesture-double-tap'},
|
||||
level_config: {entity_category: 'diagnostic'},
|
||||
programming_mode: {icon: 'mdi:calendar-clock'},
|
||||
program: {value_template: `{{ value_json.${firstExpose.property}|default('',true) ` +
|
||||
`| truncate(254, True, '', 0) }}`},
|
||||
@@ -969,7 +1043,7 @@ export default class HomeAssistant extends Extension {
|
||||
// If a sensor has entity category `config`, then change
|
||||
// it to `diagnostic`. Sensors have no input, so can't be configured.
|
||||
// https://github.com/Koenkk/zigbee2mqtt/pull/19474
|
||||
if (d.type === 'sensor' && d.discovery_payload.entity_category === 'config') {
|
||||
if (['binary_sensor', 'sensor'].includes(d.type) && d.discovery_payload.entity_category === 'config') {
|
||||
d.discovery_payload.entity_category = 'diagnostic';
|
||||
}
|
||||
});
|
||||
@@ -1080,8 +1154,9 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
private getConfigs(entity: Device | Group): DiscoveryEntry[] {
|
||||
private getConfigs(entity: Device | Group | Bridge): DiscoveryEntry[] {
|
||||
const isDevice = entity.isDevice();
|
||||
const isGroup = entity.isGroup();
|
||||
/* istanbul ignore next */
|
||||
if (!entity || (isDevice && !entity.definition)) return [];
|
||||
|
||||
@@ -1104,9 +1179,9 @@ export default class HomeAssistant extends Extension {
|
||||
// @ts-ignore
|
||||
configs.push(entity.definition.homeassistant);
|
||||
}
|
||||
} else { // group
|
||||
const exposesByType: {[s: string]: zhc.DefinitionExpose[]} = {};
|
||||
const allExposes: zhc.DefinitionExpose[] = [];
|
||||
} else if (isGroup) { // group
|
||||
const exposesByType: {[s: string]: zhc.Expose[]} = {};
|
||||
const allExposes: zhc.Expose[] = [];
|
||||
|
||||
entity.zh.members.map((e) => this.zigbee.resolveEntity(e.getDevice()) as Device)
|
||||
.filter((d) => d.definition).forEach((device) => {
|
||||
@@ -1128,6 +1203,9 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
configs = [].concat(...Object.values(exposesByType)
|
||||
.map((exposes) => this.exposeToConfig(exposes, 'group', allExposes)));
|
||||
} else {
|
||||
// Discover bridge config.
|
||||
configs.push(...entity.configs);
|
||||
}
|
||||
|
||||
if (isDevice && settings.get().advanced.last_seen !== 'disable') {
|
||||
@@ -1152,7 +1230,7 @@ export default class HomeAssistant extends Extension {
|
||||
configs.push(config);
|
||||
}
|
||||
|
||||
if (isDevice && entity.definition.hasOwnProperty('ota')) {
|
||||
if (isDevice && entity.definition.ota) {
|
||||
const updateStateSensor: DiscoveryEntry = {
|
||||
type: 'sensor',
|
||||
object_id: 'update_state',
|
||||
@@ -1198,12 +1276,34 @@ export default class HomeAssistant extends Extension {
|
||||
value_template: `{{ value_json['update']['installed_version'] }}`,
|
||||
latest_version_template: `{{ value_json['update']['latest_version'] }}`,
|
||||
json_attributes_topic: `${settings.get().mqtt.base_topic}/${entity.name}`, // state topic
|
||||
json_attributes_template: `{"in_progress": "{{ value_json['update']['state'] == 'updating' }}"}`,
|
||||
json_attributes_template:
|
||||
`{"in_progress": {{ iif(value_json['update']['state'] == 'updating', 'true', 'false') }} }`,
|
||||
},
|
||||
};
|
||||
configs.push(updateSensor);
|
||||
}
|
||||
|
||||
// Discover scenes.
|
||||
const endpointsOrGroups = isDevice ? entity.zh.endpoints : isGroup ? [entity.zh] : [];
|
||||
endpointsOrGroups.forEach((endpointOrGroup) => {
|
||||
utils.getScenes(endpointOrGroup).forEach((scene) => {
|
||||
const sceneEntry: DiscoveryEntry = {
|
||||
type: 'scene',
|
||||
object_id: `scene_${scene.id}`,
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: `${scene.name}`,
|
||||
state_topic: false,
|
||||
command_topic: true,
|
||||
payload_on: `{ "scene_recall": ${scene.id} }`,
|
||||
object_id_postfix: `_${scene.name.replace(/\s+/g, '_').toLowerCase()}`,
|
||||
},
|
||||
};
|
||||
|
||||
configs.push(sceneEntry);
|
||||
});
|
||||
});
|
||||
|
||||
if (isDevice && entity.options.hasOwnProperty('legacy') && !entity.options.legacy) {
|
||||
configs = configs.filter((c) => c !== sensorClick);
|
||||
}
|
||||
@@ -1230,21 +1330,26 @@ export default class HomeAssistant extends Extension {
|
||||
return configs;
|
||||
}
|
||||
|
||||
private getDiscoverKey(entity: Device | Group): string | number {
|
||||
return entity.isDevice() ? entity.ieeeAddr : entity.ID;
|
||||
private getDiscoverKey(entity: Device | Group | Bridge): string | number {
|
||||
return entity.ID;
|
||||
}
|
||||
|
||||
private discover(entity: Device | Group, force=false): void {
|
||||
private discover(entity: Device | Group | Bridge, force=false): void {
|
||||
// Check if already discovered and check if there are configs.
|
||||
const discoverKey = this.getDiscoverKey(entity);
|
||||
const discover = force || !this.discovered[discoverKey];
|
||||
|
||||
if (entity.isGroup()) {
|
||||
if (!discover || entity.zh.members.length === 0) return;
|
||||
} else if (!discover || !entity.definition || entity.zh.interviewing ||
|
||||
(entity.options.hasOwnProperty('homeassistant') && !entity.options.homeassistant)) {
|
||||
// Handle type differences.
|
||||
const isDevice = entity.isDevice();
|
||||
const isGroup = entity.isGroup();
|
||||
|
||||
if (isGroup && (!discover || entity.zh.members.length === 0)) {
|
||||
return;
|
||||
} else if (isDevice && (!discover || !entity.definition || entity.zh.interviewing ||
|
||||
(entity.options.hasOwnProperty('homeassistant') && !entity.options.homeassistant))) {
|
||||
return;
|
||||
}
|
||||
const lastDiscovered = this.discovered[discoverKey];
|
||||
|
||||
this.discovered[discoverKey] = {topics: new Set(), mockProperties: new Set(), objectIDs: new Set()};
|
||||
this.getConfigs(entity).forEach((config) => {
|
||||
@@ -1287,6 +1392,11 @@ export default class HomeAssistant extends Extension {
|
||||
payload.object_id += `_${config.object_id}`;
|
||||
}
|
||||
|
||||
// Allow customization of the `payload.object_id` without touching the other uses of `config.object_id`
|
||||
// (e.g. for setting the `payload.unique_id` and as an internal key).
|
||||
payload.object_id = `${payload.object_id}${payload.object_id_postfix ?? ''}`;
|
||||
delete payload.object_id_postfix;
|
||||
|
||||
// Set unique_id
|
||||
payload.unique_id = `${entity.options.ID}_${config.object_id}_${settings.get().mqtt.base_topic}`;
|
||||
|
||||
@@ -1294,20 +1404,27 @@ export default class HomeAssistant extends Extension {
|
||||
payload.device = devicePayload;
|
||||
payload.origin = this.discoveryOrigin;
|
||||
|
||||
// Availability payload
|
||||
payload.availability = [{topic: `${settings.get().mqtt.base_topic}/bridge/state`}];
|
||||
// Availability payload (can be disabled by setting `payload.availability = false`).
|
||||
if (! payload.hasOwnProperty('availability') || payload.availability) {
|
||||
payload.availability = [{topic: `${settings.get().mqtt.base_topic}/bridge/state`}];
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (utils.isAvailabilityEnabledForEntity(entity, settings.get())) {
|
||||
payload.availability_mode = 'all';
|
||||
payload.availability.push({topic: `${baseTopic}/availability`});
|
||||
}
|
||||
if (isDevice||isGroup) {
|
||||
if (utils.isAvailabilityEnabledForEntity(entity, settings.get())) {
|
||||
payload.availability_mode = 'all';
|
||||
payload.availability.push({topic: `${baseTopic}/availability`});
|
||||
}
|
||||
} else { // Bridge availability is different.
|
||||
payload.availability_mode = 'all';
|
||||
}
|
||||
|
||||
if (entity.isDevice() && entity.options.disabled) {
|
||||
// Mark disabled device always as unavailable
|
||||
payload.availability.forEach((a: KeyValue) => a.value_template = '{{ "offline" }}');
|
||||
} else if (!settings.get().advanced.legacy_availability_payload) {
|
||||
payload.availability.forEach((a: KeyValue) => a.value_template = '{{ value_json.state }}');
|
||||
if (isDevice && entity.options.disabled) {
|
||||
// Mark disabled device always as unavailable
|
||||
payload.availability.forEach((a: KeyValue) => a.value_template = '{{ "offline" }}');
|
||||
} else if (!settings.get().advanced.legacy_availability_payload) {
|
||||
payload.availability.forEach((a: KeyValue) => a.value_template = '{{ value_json.state }}');
|
||||
}
|
||||
} else {
|
||||
delete payload.availability;
|
||||
}
|
||||
|
||||
const commandTopicPrefix = payload.command_topic_prefix ? `${payload.command_topic_prefix}/` : '';
|
||||
@@ -1443,6 +1560,11 @@ export default class HomeAssistant extends Extension {
|
||||
config.mockProperties?.forEach((mockProperty) =>
|
||||
this.discovered[discoverKey].mockProperties.add(mockProperty));
|
||||
});
|
||||
lastDiscovered?.topics?.forEach((topic) => {
|
||||
if (!this.discovered[discoverKey].topics.has(topic)) {
|
||||
this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@bind private onMQTTMessage(data: eventdata.MQTTMessage): void {
|
||||
@@ -1469,7 +1591,7 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
// Group discovery topic uses "ENCODEDBASETOPIC_GROUPID", device use ieeeAddr
|
||||
const ID = discoveryMatch[2].includes('_') ? discoveryMatch[2].split('_')[1] : discoveryMatch[2];
|
||||
const entity = this.zigbee.resolveEntity(ID);
|
||||
const entity = ID === this.bridge.ID ? this.bridge : this.zigbee.resolveEntity(ID);
|
||||
let clear = !entity || entity.isDevice() && !entity.definition;
|
||||
|
||||
// Only save when topic matches otherwise config is not updated when renamed by editing configuration.yaml
|
||||
@@ -1518,7 +1640,35 @@ export default class HomeAssistant extends Extension {
|
||||
this.discover(data.device);
|
||||
}
|
||||
|
||||
private getDevicePayload(entity: Device | Group): KeyValue {
|
||||
@bind async onScenesChanged(): Promise<void> {
|
||||
// Re-trigger MQTT discovery of all devices and groups, similar to bridge.ts
|
||||
const entities = [...this.zigbee.devices(), ...this.zigbee.groups()];
|
||||
const clearedEntities = new Set<Device|Group>();
|
||||
|
||||
// First, clear existing scene discovery topics
|
||||
entities.forEach((entity) => {
|
||||
logger.debug(`Clearing Home Assistant scene discovery topics for '${entity.name}'`);
|
||||
this.discovered[this.getDiscoverKey(entity)]?.topics.forEach((topic) => {
|
||||
if (topic.startsWith('scene')) {
|
||||
this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false);
|
||||
clearedEntities.add(entity);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Make sure Home Assistant deletes the old entity first otherwise another one (_2) is created
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/12610
|
||||
logger.debug(`Finished clearing scene discovery topics, waiting for Home Assistant.`);
|
||||
await utils.sleep(2);
|
||||
|
||||
// Re-discover all entities (including their new scenes).
|
||||
logger.debug(`Re-discovering entities with their scenes.`);
|
||||
clearedEntities.forEach((entity) => {
|
||||
this.discover(entity, true);
|
||||
});
|
||||
}
|
||||
|
||||
private getDevicePayload(entity: Device | Group | Bridge): KeyValue {
|
||||
const identifierPostfix = entity.isGroup() ?
|
||||
`zigbee2mqtt_${this.getEncodedBaseTopic()}` : 'zigbee2mqtt';
|
||||
|
||||
@@ -1534,22 +1684,37 @@ export default class HomeAssistant extends Extension {
|
||||
sw_version: `Zigbee2MQTT ${this.zigbee2MQTTVersion}`,
|
||||
};
|
||||
|
||||
const url = settings.get().frontend?.url ?? '';
|
||||
if (entity.isDevice()) {
|
||||
payload.model = `${entity.definition.description} (${entity.definition.model})`;
|
||||
payload.manufacturer = entity.definition.vendor;
|
||||
payload.sw_version = entity.zh.softwareBuildID;
|
||||
payload.configuration_url = `${url}/#/device/${entity.ieeeAddr}/info`;
|
||||
} else if (entity.isGroup()) {
|
||||
payload.model = 'Group';
|
||||
payload.manufacturer = 'Zigbee2MQTT';
|
||||
payload.configuration_url = `${url}/#/group/${entity.ID}`;
|
||||
} else {
|
||||
payload.model = 'Bridge';
|
||||
payload.manufacturer = 'Zigbee2MQTT';
|
||||
payload.hw_version = `${entity.hardwareVersion} ${entity.firmwareVersion}`;
|
||||
payload.sw_version = this.zigbee2MQTTVersion;
|
||||
payload.configuration_url = `${url}/#/settings`;
|
||||
}
|
||||
|
||||
if (settings.get().frontend?.url) {
|
||||
const url = settings.get().frontend?.url;
|
||||
payload.configuration_url = entity.isDevice() ? `${url}/#/device/${entity.ieeeAddr}/info` :
|
||||
`${url}/#/group/${entity.ID}`;
|
||||
if (!url) {
|
||||
delete payload.configuration_url;
|
||||
}
|
||||
|
||||
// Link devices & groups to bridge.
|
||||
if (entity !== this.bridge) {
|
||||
payload.via_device = this.bridgeIdentifier;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
override adjustMessageBeforePublish(entity: Device | Group, message: KeyValue): void {
|
||||
override adjustMessageBeforePublish(entity: Device | Group | Bridge, message: KeyValue): void {
|
||||
const discoverKey = this.getDiscoverKey(entity);
|
||||
this.discovered[discoverKey]?.mockProperties?.forEach((mockProperty) => {
|
||||
if (!message.hasOwnProperty(mockProperty.property)) {
|
||||
@@ -1576,7 +1741,7 @@ export default class HomeAssistant extends Extension {
|
||||
return settings.get().mqtt.base_topic.split('').map((s) => s.charCodeAt(0).toString()).join('');
|
||||
}
|
||||
|
||||
private getDiscoveryTopic(config: DiscoveryEntry, entity: Device | Group): string {
|
||||
private getDiscoveryTopic(config: DiscoveryEntry, entity: Device | Group | Bridge): string {
|
||||
const key = entity.isDevice() ? entity.ieeeAddr : `${this.getEncodedBaseTopic()}_${entity.ID}`;
|
||||
return `${config.type}/${key}/${config.object_id}/config`;
|
||||
}
|
||||
@@ -1625,4 +1790,158 @@ export default class HomeAssistant extends Extension {
|
||||
_clearDiscoveredTrigger(): void {
|
||||
this.discoveredTriggers = {};
|
||||
}
|
||||
|
||||
private getBridgeEntity(coordinatorVersion: zh.CoordinatorVersion): Bridge {
|
||||
const coordinatorIeeeAddress = this.zigbee.firstCoordinatorEndpoint().deviceIeeeAddress;
|
||||
const discovery: DiscoveryEntry[] = [];
|
||||
const bridge = new Bridge(coordinatorIeeeAddress, coordinatorVersion, discovery);
|
||||
const baseTopic = `${settings.get().mqtt.base_topic}/${bridge.name}`;
|
||||
|
||||
discovery.push(
|
||||
// Binary sensors.
|
||||
{
|
||||
type: 'binary_sensor',
|
||||
object_id: 'connection_state',
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: 'Connection state',
|
||||
device_class: 'connectivity',
|
||||
entity_category: 'diagnostic',
|
||||
state_topic: true,
|
||||
state_topic_postfix: 'state',
|
||||
value_template: '{{ value_json.state }}',
|
||||
payload_on: 'online',
|
||||
payload_off: 'offline',
|
||||
availability: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'binary_sensor',
|
||||
object_id: 'restart_required',
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: 'Restart required',
|
||||
device_class: 'problem',
|
||||
entity_category: 'diagnostic',
|
||||
enabled_by_default: false,
|
||||
state_topic: true,
|
||||
state_topic_postfix: 'info',
|
||||
value_template: '{{ value_json.restart_required }}',
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
},
|
||||
},
|
||||
|
||||
// Buttons.
|
||||
{
|
||||
type: 'button',
|
||||
object_id: 'restart',
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: 'Restart',
|
||||
device_class: 'restart',
|
||||
state_topic: false,
|
||||
command_topic: `${baseTopic}/request/restart`,
|
||||
payload_press: '',
|
||||
},
|
||||
},
|
||||
|
||||
// Selects.
|
||||
{
|
||||
type: 'select',
|
||||
object_id: 'log_level',
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: 'Log level',
|
||||
entity_category: 'config',
|
||||
state_topic: true,
|
||||
state_topic_postfix: 'info',
|
||||
value_template: '{{ value_json.log_level | lower }}',
|
||||
command_topic: `${baseTopic}/request/options`,
|
||||
command_template:
|
||||
'{"options": {"advanced": {"log_level": "{{ value }}" } } }',
|
||||
options: ['info', 'warn', 'error', 'debug'],
|
||||
},
|
||||
},
|
||||
// Sensors:
|
||||
{
|
||||
type: 'sensor',
|
||||
object_id: 'version',
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: 'Version',
|
||||
icon: 'mdi:zigbee',
|
||||
entity_category: 'diagnostic',
|
||||
state_topic: true,
|
||||
state_topic_postfix: 'info',
|
||||
value_template: '{{ value_json.version }}',
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
type: 'sensor',
|
||||
object_id: 'coordinator_version',
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: 'Coordinator version',
|
||||
icon: 'mdi:chip',
|
||||
entity_category: 'diagnostic',
|
||||
enabled_by_default: false,
|
||||
state_topic: true,
|
||||
state_topic_postfix: 'info',
|
||||
value_template: '{{ value_json.coordinator.meta.revision }}',
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
type: 'sensor',
|
||||
object_id: 'network_map',
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: 'Network map',
|
||||
entity_category: 'diagnostic',
|
||||
enabled_by_default: false,
|
||||
state_topic: true,
|
||||
state_topic_postfix: 'response/networkmap',
|
||||
value_template: '{{ now().strftime(\'%Y-%m-%d %H:%M:%S\') }}',
|
||||
json_attributes_topic: `${baseTopic}/response/networkmap`,
|
||||
json_attributes_template: '{{ value_json.data.value | tojson }}',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'sensor',
|
||||
object_id: 'permit_join_timeout',
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: 'Permit join timeout',
|
||||
device_class: 'duration',
|
||||
unit_of_measurement: 's',
|
||||
entity_category: 'diagnostic',
|
||||
state_topic: true,
|
||||
state_topic_postfix: 'info',
|
||||
value_template:
|
||||
'{{ iif(value_json.permit_join_timeout is defined, value_json.permit_join_timeout, None) }}',
|
||||
},
|
||||
},
|
||||
|
||||
// Switches.
|
||||
{
|
||||
type: 'switch',
|
||||
object_id: 'permit_join',
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: 'Permit join',
|
||||
icon: 'mdi:human-greeting-proximity',
|
||||
state_topic: true,
|
||||
state_topic_postfix: 'info',
|
||||
value_template: '{{ value_json.permit_join | lower }}',
|
||||
command_topic: `${baseTopic}/request/permit_join`,
|
||||
payload_on: 'true',
|
||||
payload_off: 'false',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return bridge;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as settings from '../../util/settings';
|
||||
import logger from '../../util/logger';
|
||||
import zigbeeHerdsmanConverters from 'zigbee-herdsman-converters';
|
||||
import utils from '../../util/utils';
|
||||
import assert from 'assert';
|
||||
import Extension from '../extension';
|
||||
@@ -90,7 +89,7 @@ export default class BridgeLegacy extends Extension {
|
||||
@bind async reset(): Promise<void> {
|
||||
try {
|
||||
await this.zigbee.reset('soft');
|
||||
logger.info('Soft resetted ZNP');
|
||||
logger.info('Soft reset ZNP');
|
||||
} catch (error) {
|
||||
logger.error('Soft reset failed');
|
||||
}
|
||||
@@ -140,7 +139,7 @@ export default class BridgeLegacy extends Extension {
|
||||
};
|
||||
|
||||
if (device.zh.type !== 'Coordinator') {
|
||||
const definition = zigbeeHerdsmanConverters.findByDevice(device.zh);
|
||||
const definition = device.definition;
|
||||
payload.model = definition ? definition.model : device.zh.modelID;
|
||||
payload.vendor = definition ? definition.vendor : '-';
|
||||
payload.description = definition ? definition.description : '-';
|
||||
@@ -385,7 +384,7 @@ export default class BridgeLegacy extends Extension {
|
||||
);
|
||||
} else if (type === 'deviceInterview') {
|
||||
if (data.status === 'successful') {
|
||||
if (resolvedEntity.definition) {
|
||||
if (resolvedEntity.isSupported) {
|
||||
const {vendor, description, model} = resolvedEntity.definition;
|
||||
const log = {friendly_name: resolvedEntity.name, model, vendor, description, supported: true};
|
||||
this.mqtt.publish(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import zigbeeHerdsmanConverters from 'zigbee-herdsman-converters';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
import logger from '../../util/logger';
|
||||
import * as settings from '../../util/settings';
|
||||
import Extension from '../extension';
|
||||
@@ -7,14 +7,14 @@ const defaultConfiguration = {
|
||||
minimumReportInterval: 3, maximumReportInterval: 300, reportableChange: 1,
|
||||
};
|
||||
|
||||
const ZNLDP12LM = zigbeeHerdsmanConverters.definitions.find((d) => d.model === 'ZNLDP12LM');
|
||||
const ZNLDP12LM = zhc.definitions.find((d) => d.model === 'ZNLDP12LM');
|
||||
|
||||
const devicesNotSupportingReporting = [
|
||||
zigbeeHerdsmanConverters.definitions.find((d) => d.model === 'CC2530.ROUTER'),
|
||||
zigbeeHerdsmanConverters.definitions.find((d) => d.model === 'BASICZBR3'),
|
||||
zigbeeHerdsmanConverters.definitions.find((d) => d.model === 'ZM-CSW032-D'),
|
||||
zigbeeHerdsmanConverters.definitions.find((d) => d.model === 'TS0001'),
|
||||
zigbeeHerdsmanConverters.definitions.find((d) => d.model === 'TS0115'),
|
||||
zhc.definitions.find((d) => d.model === 'CC2530.ROUTER'),
|
||||
zhc.definitions.find((d) => d.model === 'BASICZBR3'),
|
||||
zhc.definitions.find((d) => d.model === 'ZM-CSW032-D'),
|
||||
zhc.definitions.find((d) => d.model === 'TS0001'),
|
||||
zhc.definitions.find((d) => d.model === 'TS0115'),
|
||||
];
|
||||
|
||||
const reportKey = 1;
|
||||
|
||||
@@ -43,7 +43,7 @@ export default class SoftReset extends Extension {
|
||||
|
||||
try {
|
||||
await this.zigbee.reset('soft');
|
||||
logger.warn('Soft resetted ZNP due to timeout');
|
||||
logger.warn('Soft reset ZNP due to timeout');
|
||||
} catch (error) {
|
||||
logger.warn('Soft reset failed, trying stop/start');
|
||||
|
||||
|
||||
@@ -99,12 +99,7 @@ export default class NetworkMap extends Extension {
|
||||
|
||||
// Add the device model
|
||||
if (node.type !== 'Coordinator') {
|
||||
if (node.definition) {
|
||||
labels.push(`${node.definition.vendor} ${node.definition.description} (${node.definition.model})`);
|
||||
} else {
|
||||
// This model is not supported by zigbee-herdsman-converters, add zigbee model information
|
||||
labels.push(`${node.manufacturerName} ${node.modelID}`);
|
||||
}
|
||||
labels.push(`${node.definition?.vendor} ${node.definition?.description} (${node.definition?.model})`);
|
||||
}
|
||||
|
||||
// Add the device last_seen timestamp
|
||||
@@ -177,12 +172,7 @@ export default class NetworkMap extends Extension {
|
||||
if (node.type !== 'Coordinator') {
|
||||
text.push(`---`);
|
||||
const definition = (this.zigbee.resolveEntity(node.ieeeAddr) as Device).definition;
|
||||
if (definition) {
|
||||
text.push(`${definition.vendor} ${definition.description} (${definition.model})`);
|
||||
} else {
|
||||
// This model is not supported by zigbee-herdsman-converters, add zigbee model information
|
||||
text.push(`${node.manufacturerName} ${node.modelID}`);
|
||||
}
|
||||
text.push(`${definition?.vendor} ${definition?.description} (${definition?.model})`);
|
||||
}
|
||||
|
||||
// Add the device last_seen timestamp
|
||||
@@ -269,8 +259,7 @@ export default class NetworkMap extends Extension {
|
||||
vendor: device.definition.vendor,
|
||||
description: device.definition.description,
|
||||
supports: Array.from(new Set((device.exposes()).map((e) => {
|
||||
return e.hasOwnProperty('name') ? e.name :
|
||||
`${e.type} (${e.features.map((f) => f.name).join(', ')})`;
|
||||
return e.name ?? `${e.type} (${e.features.map((f) => f.name).join(', ')})`;
|
||||
}))).join(', '),
|
||||
} : null;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import zhc from 'zigbee-herdsman-converters';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
import Extension from './extension';
|
||||
|
||||
/**
|
||||
@@ -39,12 +39,13 @@ export default class OnEvent extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
private async callOnEvent(device: Device, type: string, data: KeyValue): Promise<void> {
|
||||
private async callOnEvent(device: Device, type: zhc.OnEventType, data: KeyValue): Promise<void> {
|
||||
const state = this.state.get(device);
|
||||
zhc.onEvent(type, data, device.zh, device.options, state);
|
||||
zhc.onEvent(type, data, device.zh);
|
||||
|
||||
if (device.definition?.onEvent) {
|
||||
await device.definition.onEvent(type, data, device.zh, device.options, state);
|
||||
const options: KeyValue = device.options;
|
||||
await device.definition.onEvent(type, data, device.zh, options, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@ import * as settings from '../util/settings';
|
||||
import logger from '../util/logger';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import utils from '../util/utils';
|
||||
import * as tradfriOTA from 'zigbee-herdsman-converters/lib/ota/tradfri';
|
||||
import * as zigbeeOTA from 'zigbee-herdsman-converters/lib/ota/zigbeeOTA';
|
||||
import Extension from './extension';
|
||||
import bind from 'bind-decorator';
|
||||
import Device from '../model/device';
|
||||
import dataDir from '../util/data';
|
||||
import * as URI from 'uri-js';
|
||||
import path from 'path';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
function isValidUrl(url: string): boolean {
|
||||
let parsed;
|
||||
@@ -45,7 +44,7 @@ export default class OTAUpdate extends Extension {
|
||||
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
|
||||
this.eventBus.onDeviceMessage(this, this.onZigbeeEvent);
|
||||
if (settings.get().ota.ikea_ota_use_test_url) {
|
||||
tradfriOTA.useTestURL();
|
||||
zhc.ota.tradfri.useTestURL();
|
||||
}
|
||||
|
||||
// Let zigbeeOTA module know if the override index file is provided
|
||||
@@ -56,11 +55,11 @@ export default class OTAUpdate extends Extension {
|
||||
overrideOTAIndex = dataDir.joinPath(overrideOTAIndex);
|
||||
}
|
||||
|
||||
zigbeeOTA.useIndexOverride(overrideOTAIndex);
|
||||
zhc.ota.zigbeeOTA.useIndexOverride(overrideOTAIndex);
|
||||
}
|
||||
|
||||
// In order to support local firmware files we need to let zigbeeOTA know where the data directory is
|
||||
zigbeeOTA.setDataDir(dataDir.getPath());
|
||||
zhc.ota.setDataDir(dataDir.getPath());
|
||||
|
||||
// In case Zigbee2MQTT is restared during an update, progress and remaining values are still in state.
|
||||
// remove them.
|
||||
@@ -80,7 +79,7 @@ export default class OTAUpdate extends Extension {
|
||||
logger.debug(`Device '${data.device.name}' requested OTA`);
|
||||
|
||||
const automaticOTACheckDisabled = settings.get().ota.disable_automatic_update_check;
|
||||
let supportsOTA = data.device.definition.hasOwnProperty('ota');
|
||||
let supportsOTA = !!data.device.definition.ota;
|
||||
if (supportsOTA && !automaticOTACheckDisabled) {
|
||||
// When a device does a next image request, it will usually do it a few times after each other
|
||||
// with only 10 - 60 seconds inbetween. It doesn't make sense to check for a new update
|
||||
@@ -93,7 +92,9 @@ export default class OTAUpdate extends Extension {
|
||||
this.lastChecked[data.device.ieeeAddr] = Date.now();
|
||||
let availableResult: zhc.OtaUpdateAvailableResult = null;
|
||||
try {
|
||||
availableResult = await data.device.definition.ota.isUpdateAvailable(data.device.zh, logger, data.data);
|
||||
// @ts-expect-error typing guaranteed by data.type
|
||||
const dataData: zhc.ota.ImageInfo = data.data;
|
||||
availableResult = await data.device.definition.ota.isUpdateAvailable(data.device.zh, logger, dataData);
|
||||
} catch (e) {
|
||||
supportsOTA = false;
|
||||
logger.debug(`Failed to check if update available for '${data.device.name}' (${e.message})`);
|
||||
@@ -201,7 +202,7 @@ export default class OTAUpdate extends Extension {
|
||||
}
|
||||
|
||||
try {
|
||||
const availableResult = await device.definition.ota.isUpdateAvailable(device.zh, logger);
|
||||
const availableResult = await device.definition.ota.isUpdateAvailable(device.zh, logger, null);
|
||||
const msg = `${availableResult.available ? 'Update' : 'No update'} available for '${device.name}'`;
|
||||
logger.info(msg);
|
||||
|
||||
|
||||
+26
-25
@@ -1,6 +1,6 @@
|
||||
|
||||
import * as settings from '../util/settings';
|
||||
import zigbeeHerdsmanConverters from 'zigbee-herdsman-converters';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
import * as philips from 'zigbee-herdsman-converters/lib/philips';
|
||||
import logger from '../util/logger';
|
||||
import utils from '../util/utils';
|
||||
@@ -17,19 +17,19 @@ const sceneConverterKeys = ['scene_store', 'scene_add', 'scene_remove', 'scene_r
|
||||
|
||||
// Legacy: don't provide default converters anymore, this is required by older z2m installs not saving group members
|
||||
const defaultGroupConverters = [
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_onoff_brightness,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_color_colortemp,
|
||||
zhc.toZigbee.light_onoff_brightness,
|
||||
zhc.toZigbee.light_color_colortemp,
|
||||
philips.tz.effect, // Support Hue effects for groups
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.ignore_transition,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.cover_position_tilt,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.thermostat_occupied_heating_setpoint,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.tint_scene,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_move,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_step,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp_step,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp_move,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_hue_saturation_move,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_hue_saturation_step,
|
||||
zhc.toZigbee.ignore_transition,
|
||||
zhc.toZigbee.cover_position_tilt,
|
||||
zhc.toZigbee.thermostat_occupied_heating_setpoint,
|
||||
zhc.toZigbee.tint_scene,
|
||||
zhc.toZigbee.light_brightness_move,
|
||||
zhc.toZigbee.light_brightness_step,
|
||||
zhc.toZigbee.light_colortemp_step,
|
||||
zhc.toZigbee.light_colortemp_move,
|
||||
zhc.toZigbee.light_hue_saturation_move,
|
||||
zhc.toZigbee.light_hue_saturation_step,
|
||||
];
|
||||
|
||||
interface ParsedTopic {ID: string, endpoint: string, attribute: string, type: 'get' | 'set'}
|
||||
@@ -81,8 +81,8 @@ export default class Publish extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
legacyRetrieveState(re: Device | Group, converter: zhc.ToZigbeeConverter, result: zhc.ToZigbeeConverterResult,
|
||||
target: zh.Endpoint | zh.Group, key: string, meta: zhc.ToZigbeeConverterGetMeta): void {
|
||||
legacyRetrieveState(re: Device | Group, converter: zhc.Tz.Converter, result: zhc.Tz.ConvertSetResult,
|
||||
target: zh.Endpoint | zh.Group, key: string, meta: zhc.Tz.Meta): void {
|
||||
// It's possible for devices to get out of sync when writing an attribute that's not reportable.
|
||||
// So here we re-read the value after a specified timeout, this timeout could for example be the
|
||||
// transition time of a color change or for forcing a state read for devices that don't
|
||||
@@ -140,18 +140,14 @@ export default class Publish extends Extension {
|
||||
const membersState = re instanceof Group ?
|
||||
Object.fromEntries(re.zh.members.map((e) => [e.getDevice().ieeeAddr,
|
||||
this.state.get(this.zigbee.resolveEntity(e.getDevice().ieeeAddr))])) : null;
|
||||
let converters: zhc.ToZigbeeConverter[];
|
||||
let converters: zhc.Tz.Converter[];
|
||||
{
|
||||
if (Array.isArray(definition)) {
|
||||
const c = new Set(definition.map((d) => d.toZigbee).flat());
|
||||
// @ts-expect-error
|
||||
if (c.size == 0) converters = defaultGroupConverters;
|
||||
else converters = Array.from(c);
|
||||
} else if (definition) {
|
||||
converters = definition.toZigbee;
|
||||
} else {
|
||||
converters = [zigbeeHerdsmanConverters.toZigbeeConverters.read,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.write];
|
||||
converters = definition.toZigbee;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +173,7 @@ export default class Publish extends Extension {
|
||||
entries.sort((a) => (['state', 'brightness', 'brightness_percent'].includes(a[0]) ? sorter : sorter * -1));
|
||||
|
||||
// For each attribute call the corresponding converter
|
||||
const usedConverters: {[s: number]: zhc.ToZigbeeConverter[]} = {};
|
||||
const usedConverters: {[s: number]: zhc.Tz.Converter[]} = {};
|
||||
const toPublish: {[s: number | string]: KeyValue} = {};
|
||||
const toPublishEntity: {[s: number | string]: Device | Group} = {};
|
||||
const addToToPublish = (entity: Device | Group, payload: KeyValue): void => {
|
||||
@@ -208,7 +204,9 @@ export default class Publish extends Extension {
|
||||
}
|
||||
|
||||
if (!usedConverters.hasOwnProperty(endpointOrGroupID)) usedConverters[endpointOrGroupID] = [];
|
||||
const converter = converters.find((c) => c.key.includes(key));
|
||||
/* istanbul ignore next */
|
||||
const converter = converters.find((c) =>
|
||||
c.key.includes(key) && (!c.endpoint || c.endpoint == endpointName));
|
||||
|
||||
if (parsedTopic.type === 'set' && usedConverters[endpointOrGroupID].includes(converter)) {
|
||||
// Use a converter for set only once
|
||||
@@ -228,8 +226,11 @@ export default class Publish extends Extension {
|
||||
}
|
||||
|
||||
// Converter didn't return a result, skip
|
||||
const meta = {endpoint_name: endpointName, options: entitySettings, message: {...message}, logger, device,
|
||||
state: entityState, membersState, mapped: definition};
|
||||
const entitySettingsKeyValue: KeyValue = entitySettings;
|
||||
const meta = {
|
||||
endpoint_name: endpointName, options: entitySettingsKeyValue,
|
||||
message: {...message}, logger, device, state: entityState, membersState, mapped: definition,
|
||||
};
|
||||
|
||||
// Strip endpoint name from meta.message properties.
|
||||
if (endpointName) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import Extension from './extension';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import bind from 'bind-decorator';
|
||||
import utils from '../util/utils';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
type DebounceFunction = (() => void) & { clear(): void; } & { flush(): void; };
|
||||
|
||||
@@ -77,17 +78,8 @@ export default class Receive extends Extension {
|
||||
}
|
||||
|
||||
shouldProcess(data: eventdata.DeviceMessage): boolean {
|
||||
if (!data.device.definition) {
|
||||
if (data.device.zh.interviewing) {
|
||||
logger.debug(`Skipping message, definition is undefined and still interviewing`);
|
||||
} else {
|
||||
logger.warn(
|
||||
`Received message from unsupported device with Zigbee model '${data.device.zh.modelID}' ` +
|
||||
`and manufacturer name '${data.device.zh.manufacturerName}'`);
|
||||
// eslint-disable-next-line max-len
|
||||
logger.warn(`Please see: https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html`);
|
||||
}
|
||||
|
||||
if (!data.device.definition || data.device.zh.interviewing) {
|
||||
logger.debug(`Skipping message, still interviewing`);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -125,6 +117,9 @@ export default class Receive extends Extension {
|
||||
// - If NO payload is returned do nothing. This is for non-standard behaviour
|
||||
// for e.g. click switches where we need to count number of clicks and detect long presses.
|
||||
const publish = (payload: KeyValue): void => {
|
||||
const options: KeyValue = data.device.options;
|
||||
zhc.postProcessConvertedFromZigbeeMessage(data.device.definition, payload, options);
|
||||
|
||||
if (settings.get().advanced.elapsed) {
|
||||
const now = Date.now();
|
||||
if (this.elapsed[data.device.ieeeAddr]) {
|
||||
@@ -147,8 +142,10 @@ export default class Receive extends Extension {
|
||||
let payload: KeyValue = {};
|
||||
for (const converter of converters) {
|
||||
try {
|
||||
const convertData = {...data, device: data.device.zh};
|
||||
const options: KeyValue = data.device.options;
|
||||
const converted = await converter.convert(
|
||||
data.device.definition, data, publish, data.device.options, meta);
|
||||
data.device.definition, convertData, publish, options, meta);
|
||||
if (converted) {
|
||||
payload = {...payload, ...converted};
|
||||
}
|
||||
|
||||
+14
-12
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable brace-style */
|
||||
import * as settings from '../util/settings';
|
||||
import zigbeeHerdsmanConverters from 'zigbee-herdsman-converters';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
export default class Device {
|
||||
public zh: zh.Device;
|
||||
private _definition: zhc.Definition;
|
||||
public definition: zhc.Definition;
|
||||
private _definitionModelID: string;
|
||||
|
||||
get ieeeAddr(): string {return this.zh.ieeeAddr;}
|
||||
@@ -13,29 +13,31 @@ export default class Device {
|
||||
get name(): string {
|
||||
return this.zh.type === 'Coordinator' ? 'Coordinator' : this.options?.friendly_name || this.ieeeAddr;
|
||||
}
|
||||
get definition(): zhc.Definition {
|
||||
// Some devices can change modelID, reconsider the definition in that case.
|
||||
// https://github.com/Koenkk/zigbee-herdsman-converters/issues/3016
|
||||
if (!this.zh.interviewing && (!this._definition || this._definitionModelID !== this.zh.modelID)) {
|
||||
this._definition = zigbeeHerdsmanConverters.findByDevice(this.zh);
|
||||
this._definitionModelID = this.zh.modelID;
|
||||
}
|
||||
return this._definition;
|
||||
get isSupported(): boolean {
|
||||
return this.zh.type === 'Coordinator' || (this.definition && !this.definition.generated);
|
||||
}
|
||||
|
||||
constructor(device: zh.Device) {
|
||||
this.zh = device;
|
||||
}
|
||||
|
||||
exposes(): zhc.DefinitionExpose[] {
|
||||
exposes(): zhc.Expose[] {
|
||||
/* istanbul ignore if */
|
||||
if (typeof this.definition.exposes == 'function') {
|
||||
return this.definition.exposes(this.zh, this.options);
|
||||
const options: KeyValue = this.options;
|
||||
return this.definition.exposes(this.zh, options);
|
||||
} else {
|
||||
return this.definition.exposes;
|
||||
}
|
||||
}
|
||||
|
||||
async resolveDefinition(): Promise<void> {
|
||||
if (!this.zh.interviewing && (!this.definition || this._definitionModelID !== this.zh.modelID)) {
|
||||
this.definition = await zhc.findByDevice(this.zh, true);
|
||||
this._definitionModelID = this.zh.modelID;
|
||||
}
|
||||
}
|
||||
|
||||
ensureInSettings(): void {
|
||||
if (this.zh.type !== 'Coordinator' && !settings.getDevice(this.zh.ieeeAddr)) {
|
||||
settings.addDevice(this.zh.ieeeAddr);
|
||||
|
||||
+2
-3
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable brace-style */
|
||||
import * as settings from '../util/settings';
|
||||
import zigbeeHerdsmanConverters from 'zigbee-herdsman-converters';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
export default class Group {
|
||||
public zh: zh.Group;
|
||||
@@ -24,8 +24,7 @@ export default class Group {
|
||||
}
|
||||
|
||||
membersDefinitions(): zhc.Definition[] {
|
||||
return this.zh.members.map((m) =>
|
||||
zigbeeHerdsmanConverters.findByDevice(m.getDevice())).filter((d) => d) as zhc.Definition[];
|
||||
return this.membersDevices().map((d) => d.definition).filter((d) => d);
|
||||
}
|
||||
|
||||
isDevice(): this is Device {return false;}
|
||||
|
||||
Vendored
+5
-68
@@ -21,6 +21,8 @@ import type {
|
||||
FrameControl as ZHFrameControl,
|
||||
} from 'zigbee-herdsman/dist/zcl';
|
||||
|
||||
import type * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import type * as ZHEvents from 'zigbee-herdsman/dist/controller/events';
|
||||
|
||||
import type TypeEventBus from 'lib/eventBus';
|
||||
@@ -44,8 +46,11 @@ declare global {
|
||||
type Extension = TypeExtension;
|
||||
|
||||
// Types
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type ExternalDefinition = zhc.Definition & {homeassistant: any};
|
||||
interface MQTTResponse {data: KeyValue, status: 'error' | 'ok', error?: string, transaction?: string}
|
||||
interface MQTTOptions {qos?: QoS, retain?: boolean, properties?: {messageExpiryInterval: number}}
|
||||
type Scene = {id: number, name: string};
|
||||
type StateChangeReason = 'publishDebounce' | 'groupOptimistic' | 'lastSeenChanged' | 'publishCached';
|
||||
type PublishEntityState = (entity: Device | Group, payload: KeyValue,
|
||||
stateChangeReason?: StateChangeReason) => Promise<void>;
|
||||
@@ -70,73 +75,6 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
// zigbee-herdsman-converters
|
||||
namespace zhc {
|
||||
interface Logger {
|
||||
info: (message: string) => void;
|
||||
warn: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
debug: (message: string) => void;
|
||||
}
|
||||
|
||||
interface ToZigbeeConverterGetMeta {message?: KeyValue, mapped?: Definition | Definition[]}
|
||||
|
||||
interface ToZigbeeConverterResult {state: KeyValue,
|
||||
membersState: {[s: string]: KeyValue}, readAfterWriteTime?: number}
|
||||
|
||||
interface ToZigbeeConverter {
|
||||
key: string[],
|
||||
convertGet?: (entity: zh.Endpoint | zh.Group, key: string, meta: ToZigbeeConverterGetMeta) => Promise<void>
|
||||
convertSet?: (entity: zh.Endpoint | zh.Group, key: string, value: KeyValue | string | number,
|
||||
meta: {state: KeyValue}) => Promise<ToZigbeeConverterResult>
|
||||
}
|
||||
|
||||
interface FromZigbeeConverter {
|
||||
cluster: string,
|
||||
type: string[] | string,
|
||||
convert: (model: Definition, msg: KeyValue, publish: (payload: KeyValue) => void, options: KeyValue,
|
||||
meta: {state: KeyValue, logger: Logger, device: zh.Device}) => Promise<KeyValue>,
|
||||
}
|
||||
|
||||
interface DefinitionExposeFeature {name: string, label: string, endpoint?: string,
|
||||
property: string, value_max?: number, value_min?: number, unit?: string,
|
||||
value_off?: string, value_on?: string, value_step?: number, values: string[], access: number}
|
||||
|
||||
interface DefinitionExpose {
|
||||
type: string, name?: string, label?: string, features?: DefinitionExposeFeature[],
|
||||
endpoint?: string, values?: string[], value_off?: string, value_on?: string, value_step?: number,
|
||||
access: number, property: string, unit?: string,
|
||||
value_min?: number, value_max?: number}
|
||||
|
||||
interface OtaUpdateAvailableResult {available: boolean, currentFileVersion: number, otaFileVersion: number}
|
||||
|
||||
interface Definition {
|
||||
model: string,
|
||||
zigbeeModel: string[],
|
||||
endpoint?: (device: zh.Device) => {[s: string]: number}
|
||||
toZigbee: ToZigbeeConverter[]
|
||||
fromZigbee: FromZigbeeConverter[]
|
||||
icon?: string
|
||||
description: string
|
||||
options: zhc.DefinitionExpose[],
|
||||
vendor: string
|
||||
exposes: DefinitionExpose[] | ((device: zh.Device, options: KeyValue) => DefinitionExpose[])
|
||||
configure?: (device: zh.Device, coordinatorEndpoint: zh.Endpoint, logger: Logger,
|
||||
options?: DeviceOptions) => Promise<void>;
|
||||
onEvent?: (type: string, data: KeyValue, device: zh.Device,
|
||||
settings: KeyValue, state: KeyValue) => Promise<void>;
|
||||
ota?: {
|
||||
isUpdateAvailable: (device: zh.Device, logger: Logger, data?: KeyValue)
|
||||
=> Promise<OtaUpdateAvailableResult>;
|
||||
updateToLatest: (device: zh.Device, logger: Logger,
|
||||
onProgress: (progress: number, remaining: number) => void) => Promise<number>;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type ExternalDefinition = Definition & {homeassistant: any};
|
||||
}
|
||||
|
||||
namespace eventdata {
|
||||
type EntityRenamed = { entity: Device | Group, homeAssisantRename: boolean, from: string, to: string };
|
||||
type DeviceRemoved = { ieeeAddr: string, name: string };
|
||||
@@ -316,4 +254,3 @@ declare global {
|
||||
qos?: 0 | 1 | 2,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
declare module 'zigbee-herdsman-converters' {
|
||||
export function findByDevice(device: zh.Device): zhc.Definition;
|
||||
export function getConfigureKey(definition: zhc.Definition): string | number;
|
||||
export const toZigbeeConverters: {[s: string]: zhc.ToZigbeeConverter};
|
||||
export const definitions: zhc.Definition[];
|
||||
export function addDeviceDefinition(definition: zhc.Definition): Promise<void>;
|
||||
export function onEvent(type: string, data: KeyValue, device: zh.Device, settings: KeyValue,
|
||||
state: KeyValue): Promise<void>;
|
||||
}
|
||||
@@ -345,7 +345,7 @@
|
||||
"default": false
|
||||
},
|
||||
"zigbee_ota_override_index_location": {
|
||||
"type": "string",
|
||||
"type": ["string", "null"],
|
||||
"title": "OTA index override file name",
|
||||
"requiresRestart": true,
|
||||
"description": "Location of override OTA index file",
|
||||
@@ -371,11 +371,10 @@
|
||||
"requiresRestart": true
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"type": ["string", "null"],
|
||||
"title": "Bind host",
|
||||
"description": "Frontend binding host. Binds to a unix socket when an absolute path is given instead.",
|
||||
"examples": ["127.0.0.1", "/run/zigbee2mqtt/zigbee2mqtt.sock"],
|
||||
"default": "0.0.0.0",
|
||||
"examples": ["127.0.0.1", "::1", "/run/zigbee2mqtt/zigbee2mqtt.sock"],
|
||||
"requiresRestart": true
|
||||
},
|
||||
"auth_token": {
|
||||
|
||||
@@ -150,7 +150,7 @@ function loadSettingsWithDefaults(): void {
|
||||
}
|
||||
|
||||
if (_settingsWithDefaults.frontend) {
|
||||
const defaults = {port: 8080, auth_token: false, host: '0.0.0.0'};
|
||||
const defaults = {port: 8080, auth_token: false};
|
||||
const s = typeof _settingsWithDefaults.frontend === 'object' ? _settingsWithDefaults.frontend : {};
|
||||
// @ts-ignore
|
||||
_settingsWithDefaults.frontend = {};
|
||||
|
||||
+46
-18
@@ -6,6 +6,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import {detailedDiff} from 'deep-object-diff';
|
||||
import objectAssignDeep from 'object-assign-deep';
|
||||
import type * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
// construct a local ISO8601 string (instead of UTC-based)
|
||||
// Example:
|
||||
@@ -161,25 +162,21 @@ function loadModuleFromFile(modulePath: string): unknown {
|
||||
return loadModuleFromText(moduleCode);
|
||||
}
|
||||
|
||||
function* getExternalConvertersDefinitions(settings: Settings): Generator<zhc.ExternalDefinition> {
|
||||
const externalConverters = settings.external_converters;
|
||||
export function* loadExternalConverter(moduleName: string): Generator<ExternalDefinition> {
|
||||
let converter;
|
||||
|
||||
for (const moduleName of externalConverters) {
|
||||
let converter;
|
||||
if (moduleName.endsWith('.js')) {
|
||||
converter = loadModuleFromFile(data.joinPath(moduleName));
|
||||
} else {
|
||||
converter = require(moduleName);
|
||||
}
|
||||
|
||||
if (moduleName.endsWith('.js')) {
|
||||
converter = loadModuleFromFile(data.joinPath(moduleName));
|
||||
} else {
|
||||
converter = require(moduleName);
|
||||
}
|
||||
|
||||
if (Array.isArray(converter)) {
|
||||
for (const item of converter) {
|
||||
yield item;
|
||||
}
|
||||
} else {
|
||||
yield converter;
|
||||
if (Array.isArray(converter)) {
|
||||
for (const item of converter) {
|
||||
yield item;
|
||||
}
|
||||
} else {
|
||||
yield converter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,6 +368,18 @@ function clone(obj: KeyValue): KeyValue {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
export function isNumericExposeFeature(feature: zhc.Feature): feature is zhc.Numeric {
|
||||
return feature?.type === 'numeric';
|
||||
}
|
||||
|
||||
export function isEnumExposeFeature(feature: zhc.Feature): feature is zhc.Enum {
|
||||
return feature?.type === 'enum';
|
||||
}
|
||||
|
||||
export function isBinaryExposeFeature(feature: zhc.Feature): feature is zhc.Binary {
|
||||
return feature?.type === 'binary';
|
||||
}
|
||||
|
||||
function computeSettingsToChange(current: KeyValue, new_: KeyValue): KeyValue {
|
||||
const diff: KeyValue = detailedDiff(current, new_);
|
||||
|
||||
@@ -404,11 +413,30 @@ function computeSettingsToChange(current: KeyValue, new_: KeyValue): KeyValue {
|
||||
return newSettings;
|
||||
}
|
||||
|
||||
function getScenes(entity: zh.Endpoint | zh.Group): Scene[] {
|
||||
const scenes: {[id: number]: Scene} = {};
|
||||
const endpoints = isEndpoint(entity) ? [entity] : entity.members;
|
||||
const groupID = isEndpoint(entity) ? 0 : entity.groupID;
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
for (const [key, data] of Object.entries(endpoint.meta?.scenes || {})) {
|
||||
const split = key.split('_');
|
||||
const sceneID = parseInt(split[0], 10);
|
||||
const sceneGroupID = parseInt(split[1], 10);
|
||||
if (sceneGroupID === groupID) {
|
||||
scenes[sceneID] = {id: sceneID, name: (data as KeyValue).name || `Scene ${sceneID}`};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(scenes);
|
||||
}
|
||||
|
||||
export default {
|
||||
endpointNames, capitalize, getZigbee2MQTTVersion, getDependencyVersion, formatDate, objectHasProperties,
|
||||
equalsPartial, getObjectProperty, getResponse, parseJSON, loadModuleFromText, loadModuleFromFile,
|
||||
getExternalConvertersDefinitions, removeNullPropertiesFromObject, toNetworkAddressHex, toSnakeCase,
|
||||
removeNullPropertiesFromObject, toNetworkAddressHex, toSnakeCase,
|
||||
parseEntityID, isEndpoint, isZHGroup, hours, minutes, seconds, validateFriendlyName, sleep,
|
||||
sanitizeImageParameter, isAvailabilityEnabledForEntity, publishLastSeen, availabilityPayload,
|
||||
getAllFiles, filterProperties, flatten, arrayUnique, clone, computeSettingsToChange,
|
||||
getAllFiles, filterProperties, flatten, arrayUnique, clone, computeSettingsToChange, getScenes,
|
||||
};
|
||||
|
||||
+2
-1
@@ -4,7 +4,8 @@ import equals from 'fast-deep-equal/es6';
|
||||
|
||||
function read(file: string): KeyValue {
|
||||
try {
|
||||
return yaml.load(fs.readFileSync(file, 'utf8'));
|
||||
const result = yaml.load(fs.readFileSync(file, 'utf8'));
|
||||
return result as KeyValue ?? {};
|
||||
} catch (error) {
|
||||
if (error.name === 'YAMLException') {
|
||||
error.file = file;
|
||||
|
||||
+11
-4
@@ -63,6 +63,10 @@ export default class Zigbee {
|
||||
throw error;
|
||||
}
|
||||
|
||||
for (const device of this.devices(false)) {
|
||||
await device.resolveDefinition();
|
||||
}
|
||||
|
||||
this.herdsman.on('adapterDisconnected', () => this.eventBus.emitAdapterDisconnected());
|
||||
this.herdsman.on('lastSeenChanged', (data: ZHEvents.LastSeenChangedPayload) => {
|
||||
this.eventBus.emitLastSeenChanged({device: this.resolveDevice(data.device.ieeeAddr), reason: data.reason});
|
||||
@@ -80,15 +84,17 @@ export default class Zigbee {
|
||||
logger.debug(`Device '${device.name}' announced itself`);
|
||||
this.eventBus.emitDeviceAnnounce({device});
|
||||
});
|
||||
this.herdsman.on('deviceInterview', (data: ZHEvents.DeviceInterviewPayload) => {
|
||||
this.herdsman.on('deviceInterview', async (data: ZHEvents.DeviceInterviewPayload) => {
|
||||
const device = this.resolveDevice(data.device.ieeeAddr);
|
||||
await device.resolveDefinition();
|
||||
/* istanbul ignore if */ if (!device) return; // Prevent potential race
|
||||
const d = {device, status: data.status};
|
||||
this.logDeviceInterview(d);
|
||||
this.eventBus.emitDeviceInterview(d);
|
||||
});
|
||||
this.herdsman.on('deviceJoined', (data: ZHEvents.DeviceJoinedPayload) => {
|
||||
this.herdsman.on('deviceJoined', async (data: ZHEvents.DeviceJoinedPayload) => {
|
||||
const device = this.resolveDevice(data.device.ieeeAddr);
|
||||
await device.resolveDefinition();
|
||||
/* istanbul ignore if */ if (!device) return; // Prevent potential race
|
||||
logger.info(`Device '${device.name}' joined`);
|
||||
this.eventBus.emitDeviceJoined({device});
|
||||
@@ -98,8 +104,9 @@ export default class Zigbee {
|
||||
logger.warn(`Device '${name}' left the network`);
|
||||
this.eventBus.emitDeviceLeave({ieeeAddr: data.ieeeAddr, name});
|
||||
});
|
||||
this.herdsman.on('message', (data: ZHEvents.MessagePayload) => {
|
||||
this.herdsman.on('message', async (data: ZHEvents.MessagePayload) => {
|
||||
const device = this.resolveDevice(data.device.ieeeAddr);
|
||||
await device.resolveDefinition();
|
||||
logger.debug(`Received Zigbee message from '${device.name}', type '${data.type}', ` +
|
||||
`cluster '${data.cluster}', data '${stringify(data.data)}' from endpoint ${data.endpoint.ID}` +
|
||||
(data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``) +
|
||||
@@ -149,7 +156,7 @@ export default class Zigbee {
|
||||
if (data.status === 'successful') {
|
||||
logger.info(`Successfully interviewed '${name}', device has successfully been paired`);
|
||||
|
||||
if (data.device.definition) {
|
||||
if (data.device.isSupported) {
|
||||
const {vendor, description, model} = data.device.definition;
|
||||
logger.info(`Device '${name}' is supported, identified as: ${vendor} ${description} (${model})`);
|
||||
} else {
|
||||
|
||||
Generated
+1224
-8547
File diff suppressed because it is too large
Load Diff
+33
-29
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "1.33.2",
|
||||
"version": "1.35.0",
|
||||
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -8,7 +8,7 @@
|
||||
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16 || ^18 || ^19 || ^20"
|
||||
"node": "^18 || ^20 || ^21"
|
||||
},
|
||||
"keywords": [
|
||||
"xiaomi",
|
||||
@@ -39,19 +39,19 @@
|
||||
"dependencies": {
|
||||
"ajv": "^8.12.0",
|
||||
"bind-decorator": "^1.0.11",
|
||||
"connect-gzip-static": "2.1.1",
|
||||
"debounce": "^1.2.1",
|
||||
"connect-gzip-static": "3.0.1",
|
||||
"debounce": "^2.0.0",
|
||||
"deep-object-diff": "^1.1.9",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"finalhandler": "^1.2.0",
|
||||
"git-last-commit": "^1.0.1",
|
||||
"humanize-duration": "^3.30.0",
|
||||
"humanize-duration": "^3.31.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"jszip": "^3.10.1",
|
||||
"mkdir-recursive": "^0.4.0",
|
||||
"moment": "^2.29.4",
|
||||
"mqtt": "5.1.3",
|
||||
"moment": "^2.30.1",
|
||||
"mqtt": "^5.3.4",
|
||||
"object-assign-deep": "^0.4.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"semver": "^7.5.4",
|
||||
@@ -60,36 +60,40 @@
|
||||
"winston": "^3.11.0",
|
||||
"winston-syslog": "^2.7.0",
|
||||
"winston-transport": "^4.6.0",
|
||||
"ws": "^8.14.2",
|
||||
"zigbee-herdsman": "0.21.0",
|
||||
"zigbee-herdsman-converters": "15.106.0",
|
||||
"zigbee2mqtt-frontend": "0.6.142"
|
||||
"ws": "^8.16.0",
|
||||
"zigbee-herdsman": "0.30.0",
|
||||
"zigbee-herdsman-converters": "18.3.0",
|
||||
"zigbee2mqtt-frontend": "0.6.150"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.23.2",
|
||||
"@babel/preset-env": "^7.23.2",
|
||||
"@babel/preset-typescript": "^7.23.2",
|
||||
"@types/debounce": "^1.2.3",
|
||||
"@types/finalhandler": "^1.2.2",
|
||||
"@types/humanize-duration": "^3.27.2",
|
||||
"@types/jest": "^29.5.6",
|
||||
"@types/js-yaml": "^4.0.8",
|
||||
"@types/node": "^20.8.9",
|
||||
"@types/object-assign-deep": "^0.4.2",
|
||||
"@types/readable-stream": "4.0.4",
|
||||
"@babel/core": "^7.23.7",
|
||||
"@babel/plugin-proposal-decorators": "^7.23.7",
|
||||
"@babel/preset-env": "^7.23.7",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@types/debounce": "^1.2.4",
|
||||
"@types/finalhandler": "^1.2.3",
|
||||
"@types/humanize-duration": "^3.27.3",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/object-assign-deep": "^0.4.3",
|
||||
"@types/readable-stream": "4.0.10",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.9.0",
|
||||
"@typescript-eslint/parser": "^6.9.0",
|
||||
"@types/ws": "8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||
"@typescript-eslint/parser": "^6.16.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
"eslint": "^8.52.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-jest": "^27.6.0",
|
||||
"jest": "^29.7.0",
|
||||
"tmp": "^0.2.1",
|
||||
"typescript": "^5.2.2"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"overrides": {
|
||||
"zigbee-herdsman-converters": {
|
||||
"zigbee-herdsman": "$zigbee-herdsman"
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
"coverageThreshold": {
|
||||
|
||||
@@ -8,6 +8,7 @@ const zhc = require('zigbee-herdsman-converters');
|
||||
const z2mTillVersion = process.argv[2];
|
||||
const zhcTillVersion = process.argv[3];
|
||||
const zhTillVersion = process.argv[4];
|
||||
const frontendTillVersion = process.argv[5];
|
||||
|
||||
const changelogs = [
|
||||
{tillVersion: z2mTillVersion, project: 'koenkk/zigbee2mqtt',
|
||||
@@ -16,16 +17,19 @@ const changelogs = [
|
||||
contents: fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'zigbee-herdsman-converters', 'CHANGELOG.md'), 'utf-8').split('\n')},
|
||||
{tillVersion: zhTillVersion, project: 'koenkk/zigbee-herdsman',
|
||||
contents: fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'zigbee-herdsman', 'CHANGELOG.md'), 'utf-8').split('\n')},
|
||||
{tillVersion: frontendTillVersion, project: 'nurikk/zigbee2mqtt-frontend', isFrontend: true,
|
||||
contents: fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'zigbee2mqtt-frontend', 'CHANGELOG.md'), 'utf-8').split('\n')},
|
||||
];
|
||||
|
||||
const releaseRe = /## \[(.+)\]/;
|
||||
const changes = {features: [], fixes: [], detect: [], add: [], error: []};
|
||||
const changes = {features: [], fixes: [], detect: [], add: [], error: [], frontend: []};
|
||||
let context = null;
|
||||
const changeRe = [
|
||||
/^\* (\*\*(.+):\*\*)?(.+)\((\[#\d+\]\(.+\))\) \(\[.+\]\(https:.+\/(.+)\)\)$/,
|
||||
/^\* (\*\*(.+):\*\*)?(.+)(https:\/\/github\.com.+) \(\[.+\]\(https:.+\/(.+)\)\)$/,
|
||||
/^\* (\*\*(.+):\*\*)?(.+)() \(\[.+\]\(https:.+\/(.+)\)\)$/,
|
||||
];
|
||||
const frontendChangeRe = /^\* (\*\*.+:\*\* )()?(.+) \(\[.+\]\(https:.+\/()(.+)\)\)(.+)?$/;
|
||||
|
||||
let commitUserLookup = {};
|
||||
const commitUserFile = path.join(__dirname, 'commit-user-lookup.json');
|
||||
@@ -36,7 +40,7 @@ if (fs.existsSync(commitUserFile)) {
|
||||
for (const changelog of changelogs) {
|
||||
for (const line of changelog.contents) {
|
||||
const releaseMatch = line.match(releaseRe);
|
||||
const changeMatch = changeRe.map((re) => line.match(re)).find((e) => e);
|
||||
const changeMatch = changelog.isFrontend ? line.match(frontendChangeRe) : changeRe.map((re) => line.match(re)).find((e) => e);
|
||||
if (releaseMatch) {
|
||||
if (releaseMatch[1] === changelog.tillVersion) {
|
||||
break;
|
||||
@@ -48,45 +52,52 @@ for (const changelog of changelogs) {
|
||||
} else if (line.startsWith('* **ignore:**')) {
|
||||
continue;
|
||||
} else if (changeMatch) {
|
||||
let localContext = changeMatch[2] ? changeMatch[2] : context;
|
||||
if (!changes[localContext]) throw new Error(`Unknown context: ${localContext}`);
|
||||
let localContext = changelog.isFrontend ? 'frontend' : (changeMatch[2] ? changeMatch[2] : context);
|
||||
if (!changes[localContext]) localContext = 'error';
|
||||
|
||||
const commitUserKey = `${changelog.project}-${changeMatch[5]} `;
|
||||
const commit = changeMatch[5];
|
||||
const commitUserKey = `${changelog.project}-${commit} `;
|
||||
let user = commitUserKey in commitUserLookup ? commitUserLookup[commitUserKey] :
|
||||
execSync(`curl -s https://api.github.com/repos/${changelog.project}/commits/${changeMatch[5]} | jq -r '.author.login'`).toString().trim();
|
||||
execSync(`curl -s https://api.github.com/repos/${changelog.project}/commits/${commit} | jq -r '.author.login'`).toString().trim();
|
||||
if (user !== 'null') commitUserLookup[commitUserKey] = user;
|
||||
const messages = [];
|
||||
let message = changeMatch[3].trim();
|
||||
if (message.endsWith('.')) message = message.substring(0, message.length - 1);
|
||||
message = message.charAt(0).toUpperCase() + message.slice(1);
|
||||
|
||||
const otherUser = message.match(/\[@(.+)\]\(https:\/\/github.com\/.+\)/) || message.match(/@(.+)/);
|
||||
if (otherUser) {
|
||||
user = otherUser[1];
|
||||
message = message.replace(otherUser[0], '');
|
||||
}
|
||||
|
||||
if (localContext === 'add') {
|
||||
for (const model of message.split(',')) {
|
||||
const definition = zhc.definitions.find((d) => d.model === model.trim());
|
||||
if (definition) {
|
||||
messages.push(`\`${definition.model}\` ${definition.vendor} ${definition.description}`);
|
||||
} else {
|
||||
changes['error'].push(`${line} (model '${model}' does not exist)`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (changelog.isFrontend) {
|
||||
changes[localContext].push(`- [${commit.slice(0, 7)}](https://github.com/${changelog.project}/commit/${commit}) ${message} (@${user})`);
|
||||
messages.push(message);
|
||||
}
|
||||
} else {
|
||||
const otherUser = message.match(/\[@(.+)\]\(https:\/\/github.com\/.+\)/) || message.match(/@(.+)/);
|
||||
if (otherUser) {
|
||||
user = otherUser[1];
|
||||
message = message.replace(otherUser[0], '');
|
||||
}
|
||||
|
||||
let issue = changeMatch[4].trim();
|
||||
if (issue && !issue.startsWith('[#')) issue = `[#${issue.split('/').pop()}](${issue})`;
|
||||
if (!issue) {
|
||||
issue = '_NO_ISSUE_';
|
||||
localContext = 'error';
|
||||
}
|
||||
if (localContext === 'add') {
|
||||
for (const model of message.split(',')) {
|
||||
const definition = zhc.definitions.find((d) => d.model === model.trim());
|
||||
if (definition) {
|
||||
messages.push(`\`${definition.model}\` ${definition.vendor} ${definition.description}`);
|
||||
} else {
|
||||
changes['error'].push(`${line} (model '${model}' does not exist)`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
messages.forEach((m) => changes[localContext].push(`- ${issue} ${m} (@${user})`));
|
||||
} else if (line === '# Changelog' || !line) {
|
||||
let issue = changeMatch[4].trim();
|
||||
if (issue && !issue.startsWith('[#')) issue = `[#${issue.split('/').pop()}](${issue})`;
|
||||
if (!issue) {
|
||||
issue = '_NO_ISSUE_';
|
||||
localContext = 'error';
|
||||
}
|
||||
|
||||
messages.forEach((m) => changes[localContext].push(`- ${issue} ${m} (@${user})`));
|
||||
}
|
||||
} else if (line === '# Changelog' || line === '### ⚠ BREAKING CHANGES' || !line) {
|
||||
continue;
|
||||
} else {
|
||||
throw new Error(`Unmatched line: ${line}`);
|
||||
@@ -98,6 +109,7 @@ let result = '';
|
||||
const names = [
|
||||
['features', 'Improvements'],
|
||||
['fixes', 'Fixes'],
|
||||
['frontend', 'Frontend'],
|
||||
['add', 'New supported devices'],
|
||||
['detect', 'Fixed device detections'],
|
||||
['error', 'Changelog generator error'],
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const filename = process.argv[2];
|
||||
const text = fs.readFileSync(filename, 'utf8');
|
||||
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
|
||||
|
||||
for (const line of text.split('\n')) {
|
||||
const [modelNumber, user] = line.split(',').map((t) => t.trim());
|
||||
const model = zigbeeHerdsmanConverters.devices.find((d) => d.model === modelNumber);
|
||||
if (!model) throw new Error(`${line} does not exist`);
|
||||
console.log(`- \`${modelNumber}\` ${model.vendor} ${model.description} (${user})`);
|
||||
}
|
||||
+1
-1
@@ -24,7 +24,7 @@ echo "Cloning Zigbee2MQTT git repository"
|
||||
git clone https://github.com/Koenkk/zigbee2mqtt.git /opt/zigbee2mqtt
|
||||
chown -R pi:pi /opt/zigbee2mqtt
|
||||
|
||||
echo "Running install. This might take a while and can produce som expected errors"
|
||||
echo "Running install. This might take a while and can produce some expected errors"
|
||||
cd /opt/zigbee2mqtt || exit
|
||||
su pi -c "npm ci"
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import zigbeeHerdsman from './stub/zigbeeHerdsman';
|
||||
import utils from '../lib/util/utils';
|
||||
import * as settings from '../lib/util/settings';
|
||||
import Controller from '../lib/controller';
|
||||
import Availability from '../lib/extension/availability';
|
||||
import flushPromises from './lib/flushPromises';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
|
||||
@@ -344,4 +345,28 @@ describe('Availability', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability',
|
||||
'online', {retain: true, qos: 1}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should clear the ping queue on stop', async () => {
|
||||
const availability = controller.extensions.find((extension) => extension instanceof Availability);
|
||||
const publishAvailabilitySpy = jest.spyOn(availability, 'publishAvailability');
|
||||
|
||||
devices.bulb_color.zh = { ping: jest.fn().mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 1000)))};
|
||||
availability.addToPingQueue(devices.bulb_color);
|
||||
availability.addToPingQueue(devices.bulb_color_2);
|
||||
|
||||
await availability.stop();
|
||||
await advancedTime(utils.minutes(1));
|
||||
|
||||
expect(availability.pingQueue).toEqual([]);
|
||||
// Validate the stop-interrupt implicitly by checking that it prevents further function invocations
|
||||
expect(publishAvailabilitySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should prevent instance restart', async () => {
|
||||
const availability = controller.extensions.find((extension) => extension instanceof Availability);
|
||||
|
||||
await availability.stop();
|
||||
|
||||
await expect(() => availability.start()).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -157,7 +157,7 @@ describe('Bind', () => {
|
||||
expect(target.configureReporting).toHaveBeenCalledWith("genOnOff",[{"attribute": "onOff", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 0, "reportableChange": 0}]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 5, "reportableChange": 1}]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith("lightingColorCtrl",[{"attribute":"colorTemperature","minimumReportInterval":5,"maximumReportInterval":0xFFFF,"reportableChange":1},{"attribute":"currentX","minimumReportInterval":5,"maximumReportInterval":0xFFFF,"reportableChange":1},{"attribute":"currentY","minimumReportInterval":5,"maximumReportInterval":0xFFFF,"reportableChange":1}]);
|
||||
expect(zigbeeHerdsman.devices.bulb_color.meta.configured).toBe(2);
|
||||
expect(zigbeeHerdsman.devices.bulb_color.meta.configured).toBe(1324213189);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/unbind',
|
||||
stringify({"data":{"from":"remote","to":"bulb_color","clusters":["genScenes","genOnOff","genLevelCtrl"],"failed":[]},"status":"ok"}),
|
||||
|
||||
+43
-5
File diff suppressed because one or more lines are too long
@@ -546,7 +546,7 @@ describe('Controller', () => {
|
||||
device = controller.zigbee.resolveEntity('unsupported2');
|
||||
await controller.publishEntityState(device, {state: 'ON'});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/unsupported2', stringify({"state":"ON","device":{"friendlyName":"unsupported2","model":"unknown","ieeeAddr":"0x0017880104e45529","networkAddress":6536,"type":"EndDevice","manufacturerID":0,"powerSource":"Battery","dateCode":null, "softwareBuildID": null}}), {"qos": 0, "retain": false}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/unsupported2', stringify({"state":"ON","device":{"friendlyName":"unsupported2","model":"notSupportedModelID","ieeeAddr":"0x0017880104e45529","networkAddress":6536,"type":"EndDevice","manufacturerID":0,"powerSource":"Battery","dateCode":null, "softwareBuildID": null}}), {"qos": 0, "retain": false}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should publish entity state without retain', async () => {
|
||||
|
||||
@@ -10,9 +10,9 @@ const flushPromises = require('./lib/flushPromises');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
zigbeeHerdsmanConverters.addDeviceDefinition = jest.fn();
|
||||
zigbeeHerdsmanConverters.addDefinition = jest.fn();
|
||||
|
||||
const mocksClear = [zigbeeHerdsmanConverters.addDeviceDefinition, zigbeeHerdsman.permitJoin,
|
||||
const mocksClear = [zigbeeHerdsmanConverters.addDefinition, zigbeeHerdsman.permitJoin,
|
||||
mockExit, MQTT.end, zigbeeHerdsman.stop, logger.debug,
|
||||
MQTT.publish, MQTT.connect, zigbeeHerdsman.devices.bulb_color.removeFromNetwork,
|
||||
zigbeeHerdsman.devices.bulb.removeFromNetwork, logger.error,
|
||||
@@ -67,16 +67,15 @@ describe('Loads external converters', () => {
|
||||
it('Does not load external converters', async () => {
|
||||
settings.set(['external_converters'], []);
|
||||
await resetExtension();
|
||||
expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenCalledTimes(0);
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Loads external converters', async () => {
|
||||
fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter.js'), path.join(data.mockDir, 'mock-external-converter.js'));
|
||||
const devicesCount = zigbeeHerdsman.devices.lenght;
|
||||
settings.set(['external_converters'], ['mock-external-converter.js']);
|
||||
await resetExtension();
|
||||
expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenCalledWith({
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledWith({
|
||||
mock: true,
|
||||
zigbeeModel: ['external_converter_device'],
|
||||
vendor: 'external',
|
||||
@@ -90,11 +89,10 @@ describe('Loads external converters', () => {
|
||||
|
||||
it('Loads multiple external converters', async () => {
|
||||
fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter-multiple.js'), path.join(data.mockDir, 'mock-external-converter-multiple.js'));
|
||||
const devicesCount = zigbeeHerdsman.devices.lenght;
|
||||
settings.set(['external_converters'], ['mock-external-converter-multiple.js']);
|
||||
await resetExtension();
|
||||
expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenCalledTimes(2);
|
||||
expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenNthCalledWith(1, {
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(2);
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(1, {
|
||||
mock: 1,
|
||||
model: 'external_converters_device_1',
|
||||
zigbeeModel: ['external_converter_device_1'],
|
||||
@@ -104,7 +102,7 @@ describe('Loads external converters', () => {
|
||||
toZigbee: [],
|
||||
exposes: []
|
||||
});
|
||||
expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenNthCalledWith(2, {
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(2, {
|
||||
mock: 2,
|
||||
model: 'external_converters_device_2',
|
||||
zigbeeModel: ['external_converter_device_2'],
|
||||
@@ -119,8 +117,8 @@ describe('Loads external converters', () => {
|
||||
it('Loads external converters from package', async () => {
|
||||
settings.set(['external_converters'], ['mock-external-converter-module']);
|
||||
await resetExtension();
|
||||
expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenCalledWith({
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledWith({
|
||||
mock: true
|
||||
});
|
||||
});
|
||||
@@ -128,12 +126,20 @@ describe('Loads external converters', () => {
|
||||
it('Loads multiple external converters from package', async () => {
|
||||
settings.set(['external_converters'], ['mock-multiple-external-converter-module']);
|
||||
await resetExtension();
|
||||
expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenCalledTimes(2);
|
||||
expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenNthCalledWith(1, {
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(2);
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(1, {
|
||||
mock: 1
|
||||
});
|
||||
expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenNthCalledWith(2, {
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(2, {
|
||||
mock: 2
|
||||
});
|
||||
});
|
||||
|
||||
it('Loads external converters with error', async () => {
|
||||
fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter.js'), path.join(data.mockDir, 'mock-external-converter.js'));
|
||||
settings.set(['external_converters'], ['mock-external-converter.js']);
|
||||
zigbeeHerdsmanConverters.addDefinition.mockImplementationOnce(() => {throw new Error('Invalid definition!')});
|
||||
await resetExtension();
|
||||
expect(logger.error).toHaveBeenCalledWith(`Failed to load external converter file 'mock-external-converter.js' (Invalid definition!)`);
|
||||
});
|
||||
});
|
||||
@@ -135,6 +135,30 @@ describe('Frontend', () => {
|
||||
mockHTTPS.implementation.listen.mockClear();
|
||||
});
|
||||
|
||||
it('Start/stop without host', async () => {
|
||||
settings.set(['frontend'], {port: 8081});
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
expect(mockNodeStatic.variables.path).toBe("my/dummy/path");
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081);
|
||||
const mockWSClient = {
|
||||
implementation: {
|
||||
terminate: jest.fn(),
|
||||
send: jest.fn(),
|
||||
},
|
||||
events: {},
|
||||
};
|
||||
mockWS.implementation.clients.push(mockWSClient.implementation);
|
||||
await controller.stop();
|
||||
expect(mockWSClient.implementation.terminate).toHaveBeenCalledTimes(1);
|
||||
expect(mockHTTP.implementation.close).toHaveBeenCalledTimes(1);
|
||||
expect(mockWS.implementation.close).toHaveBeenCalledTimes(1);
|
||||
mockWS.implementation.close.mockClear();
|
||||
mockHTTP.implementation.close.mockClear();
|
||||
mockHTTP.implementation.listen.mockClear();
|
||||
mockHTTPS.implementation.listen.mockClear();
|
||||
});
|
||||
|
||||
it('Start/stop unix socket', async () => {
|
||||
settings.set(['frontend'], {host: "/tmp/zigbee2mqtt.sock"});
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ describe('Groups', () => {
|
||||
settings.reRead();
|
||||
MQTT.publish.mockClear();
|
||||
zigbeeHerdsman.groups.gledopto_group.command.mockClear();
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.__clearStore__();
|
||||
zigbeeHerdsmanConverters.toZigbee.__clearStore__();
|
||||
controller.state.state = {};
|
||||
})
|
||||
|
||||
|
||||
+412
-32
@@ -11,6 +11,7 @@ const path = require('path');
|
||||
|
||||
describe('HomeAssistant extension', () => {
|
||||
let version;
|
||||
let z2m_version;
|
||||
let controller;
|
||||
let extension;
|
||||
let origin;
|
||||
@@ -32,9 +33,9 @@ describe('HomeAssistant extension', () => {
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
version = await require('../lib/util/utils').default.getZigbee2MQTTVersion();
|
||||
origin = {name: 'Zigbee2MQTT', sw: version.version, url: 'https://www.zigbee2mqtt.io'}
|
||||
version = `Zigbee2MQTT ${version.version}`;
|
||||
z2m_version = (await require('../lib/util/utils').default.getZigbee2MQTTVersion()).version;
|
||||
origin = {name: 'Zigbee2MQTT', sw: z2m_version, url: 'https://www.zigbee2mqtt.io'};
|
||||
version = `Zigbee2MQTT ${z2m_version}`;
|
||||
jest.useFakeTimers();
|
||||
settings.set(['homeassistant'], true);
|
||||
data.writeDefaultConfiguration();
|
||||
@@ -51,9 +52,9 @@ describe('HomeAssistant extension', () => {
|
||||
|
||||
it('Should not have duplicate type/object_ids in a mapping', () => {
|
||||
const duplicated = [];
|
||||
require('zigbee-herdsman-converters').devices.forEach((d) => {
|
||||
require('zigbee-herdsman-converters').definitions.forEach((d) => {
|
||||
const exposes = typeof d.exposes == 'function' ? d.exposes() : d.exposes;
|
||||
const device = {definition: d, isDevice: () => true, options: {}, exposes: () => exposes};
|
||||
const device = {definition: d, isDevice: () => true, isGroup: () => false, options: {}, exposes: () => exposes, zh: {endpoints: []}};
|
||||
const configs = extension.getConfigs(device);
|
||||
const cfg_type_object_ids = [];
|
||||
|
||||
@@ -87,6 +88,9 @@ describe('HomeAssistant extension', () => {
|
||||
"identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"],
|
||||
"name":"ha_discovery_group",
|
||||
"sw_version": version,
|
||||
"model": "Group",
|
||||
"manufacturer": "Zigbee2MQTT",
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
"max_mireds": 454,
|
||||
"min_mireds": 250,
|
||||
@@ -130,6 +134,9 @@ describe('HomeAssistant extension', () => {
|
||||
"identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"],
|
||||
"name":"ha_discovery_group",
|
||||
"sw_version": version,
|
||||
"model": "Group",
|
||||
"manufacturer": "Zigbee2MQTT",
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
"json_attributes_topic":"zigbee2mqtt/ha_discovery_group",
|
||||
"name":null,
|
||||
@@ -165,6 +172,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
'enabled_by_default': true,
|
||||
@@ -194,6 +202,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
@@ -222,6 +231,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
@@ -251,6 +261,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
@@ -281,6 +292,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
@@ -302,7 +314,8 @@ describe('HomeAssistant extension', () => {
|
||||
"manufacturer":"Xiaomi",
|
||||
"model":"Aqara double key wired wall switch without neutral wire. Doesn't work as a router and doesn't support power meter (QBKG03LM)",
|
||||
"name":"wall_switch_double",
|
||||
"sw_version": null
|
||||
"sw_version": null,
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
"json_attributes_topic":"zigbee2mqtt/wall_switch_double",
|
||||
"name":"Left",
|
||||
@@ -332,7 +345,8 @@ describe('HomeAssistant extension', () => {
|
||||
"manufacturer":"Xiaomi",
|
||||
"model":"Aqara double key wired wall switch without neutral wire. Doesn't work as a router and doesn't support power meter (QBKG03LM)",
|
||||
"name":"wall_switch_double",
|
||||
"sw_version": null
|
||||
"sw_version": null,
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
"json_attributes_topic":"zigbee2mqtt/wall_switch_double",
|
||||
"name":"Right",
|
||||
@@ -369,6 +383,7 @@ describe('HomeAssistant extension', () => {
|
||||
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
|
||||
"name":"bulb",
|
||||
"sw_version": null,
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
"effect":true,
|
||||
"effect_list":[
|
||||
@@ -427,6 +442,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
@@ -455,6 +471,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
@@ -483,6 +500,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
@@ -495,7 +513,7 @@ describe('HomeAssistant extension', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Should discover devices with overriden user configuration', async () => {
|
||||
it('Should discover devices with overridden user configuration', async () => {
|
||||
settings.set(['devices', '0x0017880104e45522'], {
|
||||
homeassistant: {
|
||||
expire_after: 30,
|
||||
@@ -541,6 +559,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': 'test',
|
||||
'model': 'custom model',
|
||||
'manufacturer': 'From Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
'expire_after': 90,
|
||||
@@ -568,6 +587,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'custom model',
|
||||
'manufacturer': 'Not from Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'origin': origin,
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
@@ -584,7 +604,7 @@ describe('HomeAssistant extension', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Should discover devices with overriden name', async () => {
|
||||
it('Should discover devices with overridden name', async () => {
|
||||
settings.set(['devices', '0x0017880104e45522'], {
|
||||
homeassistant: {
|
||||
name: "Weather Sensor",
|
||||
@@ -614,6 +634,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
'enabled_by_default': true,
|
||||
@@ -643,6 +664,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
@@ -655,7 +677,7 @@ describe('HomeAssistant extension', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Should discover devices with overriden user configuration affecting type and object_id', async () => {
|
||||
it('Should discover devices with overridden user configuration affecting type and object_id', async () => {
|
||||
settings.set(['devices', '0x0017880104e45541'], {
|
||||
friendly_name: 'my_switch',
|
||||
homeassistant: {
|
||||
@@ -685,7 +707,8 @@ describe('HomeAssistant extension', () => {
|
||||
"manufacturer": "Xiaomi",
|
||||
"model": "Aqara single key wired wall switch without neutral wire. Doesn't work as a router and doesn't support power meter (QBKG04LM)",
|
||||
"name": "my_switch",
|
||||
"sw_version": null
|
||||
"sw_version": null,
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
"json_attributes_topic": "zigbee2mqtt/my_switch",
|
||||
"name": "my_light_name_override",
|
||||
@@ -768,7 +791,8 @@ describe('HomeAssistant extension', () => {
|
||||
"name":"fan",
|
||||
"sw_version": null,
|
||||
"model":"Universal wink enabled white ceiling fan premier remote control (99432)",
|
||||
"manufacturer":"Hampton Bay"
|
||||
"manufacturer":"Hampton Bay",
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
@@ -785,7 +809,7 @@ describe('HomeAssistant extension', () => {
|
||||
let payload;
|
||||
|
||||
payload = {
|
||||
"action_template":"{% set values = {None:None,'idle':'off','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}",
|
||||
"action_template":"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}",
|
||||
"action_topic":"zigbee2mqtt/TS0601_thermostat",
|
||||
"availability":[
|
||||
{
|
||||
@@ -801,7 +825,8 @@ describe('HomeAssistant extension', () => {
|
||||
"manufacturer":"TuYa",
|
||||
"model":"Radiator valve with thermostat (TS0601_thermostat)",
|
||||
"name":"TS0601_thermostat",
|
||||
"sw_version":null
|
||||
"sw_version":null,
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
"preset_mode_command_topic":"zigbee2mqtt/TS0601_thermostat/set/preset",
|
||||
"preset_modes":[
|
||||
@@ -870,7 +895,8 @@ describe('HomeAssistant extension', () => {
|
||||
name: 'smart vent',
|
||||
sw_version: null,
|
||||
model: 'Smart vent (SV01)',
|
||||
manufacturer: 'Keen Home'
|
||||
manufacturer: 'Keen Home',
|
||||
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
@@ -890,7 +916,8 @@ describe('HomeAssistant extension', () => {
|
||||
"manufacturer": "Siglis",
|
||||
"model": "zigfred plus smart in-wall switch (ZFP-1A-CH)",
|
||||
"name": "zigfred_plus",
|
||||
"sw_version": null
|
||||
"sw_version": null,
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
"json_attributes_topic": "zigbee2mqtt/zigfred_plus/l6",
|
||||
"name": "L6",
|
||||
@@ -942,6 +969,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
@@ -1070,6 +1098,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
@@ -1207,6 +1236,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability_mode': 'all',
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}],
|
||||
@@ -1257,13 +1287,6 @@ describe('HomeAssistant extension', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Should not clear discovery when unsupported device is removed', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'unsupported2');
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should refresh discovery when device is renamed', async () => {
|
||||
await MQTT.events.message('homeassistant/device_automation/0x0017880104e45522/action_double/config', stringify({topic: 'zigbee2mqtt/weather_sensor/action'}));
|
||||
await flushPromises();
|
||||
@@ -1290,6 +1313,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
@@ -1324,7 +1348,8 @@ describe('HomeAssistant extension', () => {
|
||||
"name":"weather_sensor_renamed",
|
||||
"sw_version": null,
|
||||
"model":"Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)",
|
||||
"manufacturer":"Xiaomi"
|
||||
"manufacturer":"Xiaomi",
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
}
|
||||
}),
|
||||
{ retain: true, qos: 1 },
|
||||
@@ -1349,6 +1374,9 @@ describe('HomeAssistant extension', () => {
|
||||
"identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"],
|
||||
"name":"ha_discovery_group_new",
|
||||
"sw_version": version,
|
||||
"model": "Group",
|
||||
"manufacturer": "Zigbee2MQTT",
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
"json_attributes_topic":"zigbee2mqtt/ha_discovery_group_new",
|
||||
"max_mireds": 454,
|
||||
@@ -1422,6 +1450,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
@@ -1453,7 +1482,8 @@ describe('HomeAssistant extension', () => {
|
||||
"name":"bulb",
|
||||
'sw_version': null,
|
||||
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
|
||||
"manufacturer":"IKEA"
|
||||
"manufacturer":"IKEA",
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
'device_class': 'update',
|
||||
@@ -1495,7 +1525,8 @@ describe('HomeAssistant extension', () => {
|
||||
"name":"button",
|
||||
"sw_version": null,
|
||||
"model":"Aqara wireless switch (WXKG11LM)",
|
||||
"manufacturer":"Xiaomi"
|
||||
"manufacturer":"Xiaomi",
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1520,7 +1551,8 @@ describe('HomeAssistant extension', () => {
|
||||
"name":"button",
|
||||
"sw_version": null,
|
||||
"model":"Aqara wireless switch (WXKG11LM)",
|
||||
"manufacturer":"Xiaomi"
|
||||
"manufacturer":"Xiaomi",
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1609,7 +1641,7 @@ describe('HomeAssistant extension', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).not.toHaveBeenCalledWith('homeassistant/device_automation/0x0017880104e45520/action_double/config', expect.any(String), expect.any(Object), expect.any(Function));
|
||||
|
||||
// Should rediscover when already discovered in previous session but with diferent name
|
||||
// Should rediscover when already discovered in previous session but with different name
|
||||
controller.extensions.find((e) => e.constructor.name === 'HomeAssistant')._clearDiscoveredTrigger();
|
||||
await MQTT.events.message('homeassistant/device_automation/0x0017880104e45520/action_double/config', stringify({topic: 'zigbee2mqtt/button_other_name/action'}));
|
||||
await flushPromises();
|
||||
@@ -1685,7 +1717,8 @@ describe('HomeAssistant extension', () => {
|
||||
"name":"button",
|
||||
"sw_version": null,
|
||||
"model":"Aqara wireless switch (WXKG11LM)",
|
||||
"manufacturer":"Xiaomi"
|
||||
"manufacturer":"Xiaomi",
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1860,6 +1893,7 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae'
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
@@ -1887,6 +1921,9 @@ describe('HomeAssistant extension', () => {
|
||||
"identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"],
|
||||
"name":"ha_discovery_group",
|
||||
"sw_version": version,
|
||||
"model": "Group",
|
||||
"manufacturer": "Zigbee2MQTT",
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
"json_attributes_topic":"zigbee2mqtt/ha_discovery_group",
|
||||
"max_mireds": 454,
|
||||
@@ -1938,6 +1975,9 @@ describe('HomeAssistant extension', () => {
|
||||
"identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"],
|
||||
"name":"ha_discovery_group",
|
||||
"sw_version": version,
|
||||
"model": "Group",
|
||||
"manufacturer": "Zigbee2MQTT",
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
"max_mireds": 454,
|
||||
"min_mireds": 250,
|
||||
@@ -1998,7 +2038,8 @@ describe('HomeAssistant extension', () => {
|
||||
"manufacturer":"IKEA",
|
||||
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
|
||||
"name":"bulb",
|
||||
"sw_version":null
|
||||
"sw_version":null,
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
"effect":true,
|
||||
"effect_list":[
|
||||
@@ -2048,7 +2089,8 @@ describe('HomeAssistant extension', () => {
|
||||
"manufacturer":"IKEA",
|
||||
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
|
||||
"name":"bulb",
|
||||
"sw_version": null
|
||||
"sw_version": null,
|
||||
"via_device": "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
"enabled_by_default":false,
|
||||
"icon":"mdi:clock",
|
||||
@@ -2096,7 +2138,8 @@ describe('HomeAssistant extension', () => {
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'configuration_url': 'http://zigbee.mqtt/#/device/0x0017880104e45522/info'
|
||||
'configuration_url': 'http://zigbee.mqtt/#/device/0x0017880104e45522/info',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
@@ -2108,4 +2151,341 @@ describe('HomeAssistant extension', () => {
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should rediscover scenes when a scene is changed', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
controller.eventBus.emitScenesChanged();
|
||||
await flushPromises();
|
||||
|
||||
// Discovery messages for scenes have been purged.
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
`homeassistant/scene/0x000b57fffec6a5b4/scene_1/config`,
|
||||
null,
|
||||
{retain: true, qos: 1},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
`homeassistant/scene/1221051039810110150109113116116_9/scene_4/config`,
|
||||
null,
|
||||
{retain: true, qos: 1},
|
||||
expect.any(Function),
|
||||
);
|
||||
jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
|
||||
let payload = {
|
||||
'name': 'Chill scene',
|
||||
'command_topic': 'zigbee2mqtt/bulb_color_2/set',
|
||||
'payload_on': '{ "scene_recall": 1 }',
|
||||
'json_attributes_topic': 'zigbee2mqtt/bulb_color_2',
|
||||
'object_id': 'bulb_color_2_1_chill_scene',
|
||||
'unique_id': '0x000b57fffec6a5b4_scene_1_zigbee2mqtt',
|
||||
'device': {
|
||||
'identifiers': [ 'zigbee2mqtt_0x000b57fffec6a5b4' ],
|
||||
'name': 'bulb_color_2',
|
||||
'sw_version': '5.127.1.26581',
|
||||
'model': 'Hue Go (7146060PH)',
|
||||
'manufacturer': 'Philips',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
'origin': origin,
|
||||
'availability': [ { 'topic': 'zigbee2mqtt/bridge/state' } ]
|
||||
}
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
`homeassistant/scene/0x000b57fffec6a5b4/scene_1/config`,
|
||||
stringify(payload),
|
||||
{retain: true, qos: 1},
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
payload = {
|
||||
'name': 'Scene 4',
|
||||
'command_topic': 'zigbee2mqtt/ha_discovery_group/set',
|
||||
'payload_on': '{ "scene_recall": 4 }',
|
||||
'json_attributes_topic':'zigbee2mqtt/ha_discovery_group',
|
||||
'object_id': 'ha_discovery_group_4_scene_4',
|
||||
'unique_id': '9_scene_4_zigbee2mqtt',
|
||||
'device': {
|
||||
'identifiers': [ 'zigbee2mqtt_1221051039810110150109113116116_9' ],
|
||||
'name': 'ha_discovery_group',
|
||||
'sw_version': version,
|
||||
'model': 'Group',
|
||||
'manufacturer': 'Zigbee2MQTT',
|
||||
'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae'
|
||||
},
|
||||
'origin': origin,
|
||||
'availability': [ { 'topic': 'zigbee2mqtt/bridge/state' } ]
|
||||
}
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
`homeassistant/scene/1221051039810110150109113116116_9/scene_4/config`,
|
||||
stringify(payload),
|
||||
{retain: true, qos: 1},
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
// Only discovery entries for entities with scenes need to be republished.
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes( 16 );
|
||||
});
|
||||
|
||||
it('Should not clear bridge entities unnecessarily', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
const topic = 'homeassistant/button/1221051039810110150109113116116_0x00124b00120144ae/restart/config';
|
||||
const payload = {
|
||||
'name': 'Restart',
|
||||
'object_id': 'zigbee2mqtt_bridge_restart',
|
||||
'unique_id': 'bridge_0x00124b00120144ae_restart_zigbee2mqtt',
|
||||
'device_class': 'restart',
|
||||
'command_topic': 'zigbee2mqtt/bridge/request/restart',
|
||||
'payload_press': '',
|
||||
'origin': origin,
|
||||
'device': {
|
||||
'name': 'Zigbee2MQTT Bridge',
|
||||
'identifiers': ['zigbee2mqtt_bridge_0x00124b00120144ae'],
|
||||
'manufacturer': 'Zigbee2MQTT',
|
||||
'model': 'Bridge',
|
||||
'hw_version': 'z-Stack 20190425',
|
||||
'sw_version': z2m_version,
|
||||
},
|
||||
'availability': [{'topic': 'zigbee2mqtt/bridge/state'}],
|
||||
'availability_mode': 'all',
|
||||
};
|
||||
|
||||
controller.eventBus.emitMQTTMessage({
|
||||
topic: topic,
|
||||
message: stringify(payload),
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(MQTT.publish).not.toHaveBeenCalledWith(
|
||||
topic,
|
||||
null,
|
||||
{retain: true, qos: 1},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should discover bridge entities', async () => {
|
||||
settings.set(['advanced', 'homeassistant_legacy_entity_attributes'], false);
|
||||
await resetExtension();
|
||||
|
||||
const devicePayload = {
|
||||
'name': 'Zigbee2MQTT Bridge',
|
||||
'identifiers': ['zigbee2mqtt_bridge_0x00124b00120144ae'],
|
||||
'manufacturer': 'Zigbee2MQTT',
|
||||
'model': 'Bridge',
|
||||
'hw_version': 'z-Stack 20190425',
|
||||
'sw_version': z2m_version,
|
||||
};
|
||||
|
||||
// Binary sensors.
|
||||
let payload;
|
||||
payload = {
|
||||
'name': 'Connection state',
|
||||
'object_id': 'zigbee2mqtt_bridge_connection_state',
|
||||
'entity_category': 'diagnostic',
|
||||
'device_class': 'connectivity',
|
||||
'unique_id': 'bridge_0x00124b00120144ae_connection_state_zigbee2mqtt',
|
||||
'state_topic': 'zigbee2mqtt/bridge/state',
|
||||
'value_template': '{{ value_json.state }}',
|
||||
'payload_on': 'online',
|
||||
'payload_off': 'offline',
|
||||
'origin': origin,
|
||||
'device': devicePayload,
|
||||
};
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/binary_sensor/1221051039810110150109113116116_0x00124b00120144ae/connection_state/config',
|
||||
stringify(payload),
|
||||
{ retain: true, qos: 1 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
payload = {
|
||||
'name': 'Restart required',
|
||||
'object_id': 'zigbee2mqtt_bridge_restart_required',
|
||||
'entity_category': 'diagnostic',
|
||||
'device_class': 'problem',
|
||||
'enabled_by_default': false,
|
||||
'unique_id': 'bridge_0x00124b00120144ae_restart_required_zigbee2mqtt',
|
||||
'state_topic': 'zigbee2mqtt/bridge/info',
|
||||
'value_template': '{{ value_json.restart_required }}',
|
||||
'payload_on': true,
|
||||
'payload_off': false,
|
||||
'origin': origin,
|
||||
'device': devicePayload,
|
||||
'availability': [{'topic': 'zigbee2mqtt/bridge/state'}],
|
||||
'availability_mode': 'all',
|
||||
};
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/binary_sensor/1221051039810110150109113116116_0x00124b00120144ae/restart_required/config',
|
||||
stringify(payload),
|
||||
{ retain: true, qos: 1 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
// Buttons.
|
||||
payload = {
|
||||
'name': 'Restart',
|
||||
'object_id': 'zigbee2mqtt_bridge_restart',
|
||||
'unique_id': 'bridge_0x00124b00120144ae_restart_zigbee2mqtt',
|
||||
'device_class': 'restart',
|
||||
'command_topic': 'zigbee2mqtt/bridge/request/restart',
|
||||
'payload_press': '',
|
||||
'origin': origin,
|
||||
'device': devicePayload,
|
||||
'availability': [{'topic': 'zigbee2mqtt/bridge/state'}],
|
||||
'availability_mode': 'all',
|
||||
};
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/button/1221051039810110150109113116116_0x00124b00120144ae/restart/config',
|
||||
stringify(payload),
|
||||
{ retain: true, qos: 1 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
// Selects.
|
||||
payload = {
|
||||
'name': 'Log level',
|
||||
'object_id': 'zigbee2mqtt_bridge_log_level',
|
||||
'entity_category': 'config',
|
||||
'unique_id': 'bridge_0x00124b00120144ae_log_level_zigbee2mqtt',
|
||||
'state_topic': 'zigbee2mqtt/bridge/info',
|
||||
'value_template': '{{ value_json.log_level | lower }}',
|
||||
'command_topic': 'zigbee2mqtt/bridge/request/options',
|
||||
'command_template':
|
||||
'{"options": {"advanced": {"log_level": "{{ value }}" } } }',
|
||||
'options': ['info', 'warn', 'error', 'debug'],
|
||||
'origin': origin,
|
||||
'device': devicePayload,
|
||||
'availability': [{'topic': 'zigbee2mqtt/bridge/state'}],
|
||||
'availability_mode': 'all',
|
||||
};
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/select/1221051039810110150109113116116_0x00124b00120144ae/log_level/config',
|
||||
stringify(payload),
|
||||
{ retain: true, qos: 1 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
// Sensors.
|
||||
payload = {
|
||||
'name': 'Version',
|
||||
'object_id': 'zigbee2mqtt_bridge_version',
|
||||
'entity_category': 'diagnostic',
|
||||
'icon': 'mdi:zigbee',
|
||||
'unique_id': 'bridge_0x00124b00120144ae_version_zigbee2mqtt',
|
||||
'state_topic': 'zigbee2mqtt/bridge/info',
|
||||
'value_template': '{{ value_json.version }}',
|
||||
'origin': origin,
|
||||
'device': devicePayload,
|
||||
'availability': [{'topic': 'zigbee2mqtt/bridge/state'}],
|
||||
'availability_mode': 'all',
|
||||
};
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/sensor/1221051039810110150109113116116_0x00124b00120144ae/version/config',
|
||||
stringify(payload),
|
||||
{ retain: true, qos: 1 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
payload = {
|
||||
'name': 'Coordinator version',
|
||||
'object_id': 'zigbee2mqtt_bridge_coordinator_version',
|
||||
'entity_category': 'diagnostic',
|
||||
'enabled_by_default': false,
|
||||
'icon': 'mdi:chip',
|
||||
'unique_id': 'bridge_0x00124b00120144ae_coordinator_version_zigbee2mqtt',
|
||||
'state_topic': 'zigbee2mqtt/bridge/info',
|
||||
'value_template': '{{ value_json.coordinator.meta.revision }}',
|
||||
'origin': origin,
|
||||
'device': devicePayload,
|
||||
'availability': [{'topic': 'zigbee2mqtt/bridge/state'}],
|
||||
'availability_mode': 'all',
|
||||
};
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/sensor/1221051039810110150109113116116_0x00124b00120144ae/coordinator_version/config',
|
||||
stringify(payload),
|
||||
{ retain: true, qos: 1 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
payload = {
|
||||
'name': 'Network map',
|
||||
'object_id': 'zigbee2mqtt_bridge_network_map',
|
||||
'entity_category': 'diagnostic',
|
||||
'enabled_by_default': false,
|
||||
'unique_id': 'bridge_0x00124b00120144ae_network_map_zigbee2mqtt',
|
||||
'state_topic': 'zigbee2mqtt/bridge/response/networkmap',
|
||||
'value_template': "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}",
|
||||
'json_attributes_topic': 'zigbee2mqtt/bridge/response/networkmap',
|
||||
'json_attributes_template': '{{ value_json.data.value | tojson }}',
|
||||
'origin': origin,
|
||||
'device': devicePayload,
|
||||
'availability': [{'topic': 'zigbee2mqtt/bridge/state'}],
|
||||
'availability_mode': 'all',
|
||||
};
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/sensor/1221051039810110150109113116116_0x00124b00120144ae/network_map/config',
|
||||
stringify(payload),
|
||||
{ retain: true, qos: 1 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
payload = {
|
||||
'name': 'Permit join timeout',
|
||||
'object_id': 'zigbee2mqtt_bridge_permit_join_timeout',
|
||||
'entity_category': 'diagnostic',
|
||||
'device_class': 'duration',
|
||||
'unit_of_measurement': 's',
|
||||
'unique_id': 'bridge_0x00124b00120144ae_permit_join_timeout_zigbee2mqtt',
|
||||
'state_topic': 'zigbee2mqtt/bridge/info',
|
||||
'value_template': '{{ iif(value_json.permit_join_timeout is defined, value_json.permit_join_timeout, None) }}',
|
||||
'origin': origin,
|
||||
'device': devicePayload,
|
||||
'availability': [{'topic': 'zigbee2mqtt/bridge/state'}],
|
||||
'availability_mode': 'all',
|
||||
};
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/sensor/1221051039810110150109113116116_0x00124b00120144ae/permit_join_timeout/config',
|
||||
stringify(payload),
|
||||
{ retain: true, qos: 1 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
// Switches.
|
||||
payload = {
|
||||
'name': 'Permit join',
|
||||
'object_id': 'zigbee2mqtt_bridge_permit_join',
|
||||
'icon': 'mdi:human-greeting-proximity',
|
||||
'unique_id': 'bridge_0x00124b00120144ae_permit_join_zigbee2mqtt',
|
||||
'state_topic': 'zigbee2mqtt/bridge/info',
|
||||
'value_template': '{{ value_json.permit_join | lower }}',
|
||||
'command_topic': 'zigbee2mqtt/bridge/request/permit_join',
|
||||
'payload_on': 'true',
|
||||
'payload_off': 'false',
|
||||
'origin': origin,
|
||||
'device': devicePayload,
|
||||
'availability': [ { 'topic': 'zigbee2mqtt/bridge/state' } ],
|
||||
'availability_mode': 'all'
|
||||
};
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/switch/1221051039810110150109113116116_0x00124b00120144ae/permit_join/config',
|
||||
stringify(payload),
|
||||
{ retain: true, qos: 1 },
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should remove discovery entries for removed exposes when device options change', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/options', stringify({"id": "0xf4ce368a38be56a1", "options": {"dimmer_1_enabled": "false", "dimmer_1_dimming_enabled": "false"}}));
|
||||
await flushPromises();
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/light/0xf4ce368a38be56a1/light_l2/config',
|
||||
null,
|
||||
{ retain: true, qos: 1 },
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,7 +102,7 @@ describe('Networkmap', () => {
|
||||
let call = MQTT.publish.mock.calls[0];
|
||||
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/raw');
|
||||
|
||||
const expected = {"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":110,"lqi":110,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":100,"lqi":100,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"sourceIeeeAddr":"0x0017880104e45559","sourceNwkAddr":6540,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","model":"LED1545G12","supports":"light (state, brightness, color_temp, color_temp_startup), effect, power_on_behavior, color_options, linkquality","vendor":"IKEA"},"failed":[],"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","lastSeen":1000,"modelID":"TRADFRI bulb E27 WS opal 980lm","networkAddress":40369,"type":"Router"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch (2016 model)","model":"WXKG02LM_rev1","supports":"battery, action, voltage, power_outage_count, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":null,"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"description": "external","model":"external_converter_device","supports":"linkquality","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]};
|
||||
const expected = {"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":110,"lqi":110,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":100,"lqi":100,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"sourceIeeeAddr":"0x0017880104e45559","sourceNwkAddr":6540,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","model":"LED1545G12","supports":"light (state, brightness, color_temp, color_temp_startup), effect, power_on_behavior, color_options, linkquality","vendor":"IKEA"},"failed":[],"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","lastSeen":1000,"modelID":"TRADFRI bulb E27 WS opal 980lm","networkAddress":40369,"type":"Router"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch (2016 model)","model":"WXKG02LM_rev1","supports":"battery, action, voltage, power_outage_count, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":{"description":"Automatically generated definition","model":"notSupportedModelID","supports":"linkquality","vendor":"Boef"},"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"description":"external","model":"external_converter_device","supports":"linkquality","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]};
|
||||
expect(JSON.parse(call[1])).toStrictEqual(expected);
|
||||
|
||||
/**
|
||||
@@ -150,7 +150,7 @@ describe('Networkmap', () => {
|
||||
"0x000b57fffec6a5b3" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="110"]
|
||||
"0x0017880104e45521" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{button_double_key|0x0017880104e45521 (0x198a)|Xiaomi Aqara double key wireless wall switch (2016 model) (WXKG02LM_rev1)|9 seconds ago}"];
|
||||
"0x0017880104e45521" -> "0x0017880104e45559" [penwidth=1, weight=0, color="#994444", label="130"]
|
||||
"0x0017880104e45525" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{0x0017880104e45525|0x0017880104e45525 (0x1988)failed: lqi,routingTable|Boef notSupportedModelID|9 seconds ago}"];
|
||||
"0x0017880104e45525" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{0x0017880104e45525|0x0017880104e45525 (0x1988)failed: lqi,routingTable|Boef Automatically generated definition (notSupportedModelID)|9 seconds ago}"];
|
||||
"0x0017880104e45559" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{cc2530_router|0x0017880104e45559 (0x198c)|Custom devices (DiY) [CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/) (CC2530.ROUTER)|9 seconds ago}"];
|
||||
"0x0017880104e45559" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="100"]
|
||||
"0x0017880104e45511" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{0x0017880104e45511|0x0017880104e45511 (0x045a)|external external (external_converter_device)|9 seconds ago}"];
|
||||
@@ -197,7 +197,7 @@ describe('Networkmap', () => {
|
||||
---
|
||||
0x0017880104e45525 (0x1988) failed: lqi,routingTable
|
||||
---
|
||||
Boef notSupportedModelID
|
||||
Boef Automatically generated definition (notSupportedModelID)
|
||||
---
|
||||
9 seconds ago
|
||||
]
|
||||
@@ -276,12 +276,12 @@ describe('Networkmap', () => {
|
||||
let call = MQTT.publish.mock.calls[0];
|
||||
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
|
||||
|
||||
const expected = {"data":{"routes":true,"type":"raw","value":{"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":110,"lqi":110,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":100,"lqi":100,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"sourceIeeeAddr":"0x0017880104e45559","sourceNwkAddr":6540,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","model":"LED1545G12","supports":"light (state, brightness, color_temp, color_temp_startup), effect, power_on_behavior, color_options, linkquality","vendor":"IKEA"},"failed":[],"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","lastSeen":1000,"modelID":"TRADFRI bulb E27 WS opal 980lm","networkAddress":40369,"type":"Router"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch (2016 model)","model":"WXKG02LM_rev1","supports":"battery, action, voltage, power_outage_count, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":null,"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"description":"external","model":"external_converter_device","supports":"linkquality","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]}},"status":"ok"};
|
||||
const expected = {"data":{"routes":true,"type":"raw","value":{"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":110,"lqi":110,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":100,"lqi":100,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"sourceIeeeAddr":"0x0017880104e45559","sourceNwkAddr":6540,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","model":"LED1545G12","supports":"light (state, brightness, color_temp, color_temp_startup), effect, power_on_behavior, color_options, linkquality","vendor":"IKEA"},"failed":[],"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","lastSeen":1000,"modelID":"TRADFRI bulb E27 WS opal 980lm","networkAddress":40369,"type":"Router"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch (2016 model)","model":"WXKG02LM_rev1","supports":"battery, action, voltage, power_outage_count, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":{"description":"Automatically generated definition","model":"notSupportedModelID","supports":"linkquality","vendor":"Boef"},"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"description":"external","model":"external_converter_device","supports":"linkquality","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]}},"status":"ok"};
|
||||
const actual = JSON.parse(call[1]);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should throw error when rquesting invalid type', async () => {
|
||||
it('Should throw error when requesting invalid type', async () => {
|
||||
mock();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', 'not_existing');
|
||||
@@ -303,7 +303,7 @@ describe('Networkmap', () => {
|
||||
let call = MQTT.publish.mock.calls[0];
|
||||
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
|
||||
|
||||
const expected = {"data":{"routes":true,"type":"raw","value":{"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch (2016 model)","model":"WXKG02LM_rev1","supports":"battery, action, voltage, power_outage_count, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":null,"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"description":"external","model":"external_converter_device","supports":"linkquality","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]}},"status":"ok"};
|
||||
const expected = {"data":{"routes":true,"type":"raw","value":{"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch (2016 model)","model":"WXKG02LM_rev1","supports":"battery, action, voltage, power_outage_count, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":{"description":"Automatically generated definition","model":"notSupportedModelID","supports":"linkquality","vendor":"Boef"},"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"description":"external","model":"external_converter_device","supports":"linkquality","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]}},"status":"ok"}
|
||||
const actual = JSON.parse(call[1]);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -7,13 +7,12 @@ const MQTT = require('./stub/mqtt');
|
||||
const settings = require('../lib/util/settings');
|
||||
const Controller = require('../lib/controller');
|
||||
const flushPromises = require('./lib/flushPromises');
|
||||
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
||||
|
||||
const mocksClear = [MQTT.publish, logger.warn, logger.debug];
|
||||
|
||||
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
const mockOnEvent = jest.fn();
|
||||
const mappedLivolo = zigbeeHerdsmanConverters.findByDevice(zigbeeHerdsman.devices.LIVOLO);
|
||||
const mappedLivolo = zigbeeHerdsmanConverters.findByModel('TI0001');
|
||||
mappedLivolo.onEvent = mockOnEvent;
|
||||
zigbeeHerdsmanConverters.onEvent = jest.fn();
|
||||
|
||||
@@ -76,6 +75,6 @@ describe('On event', () => {
|
||||
await zigbeeHerdsman.events.deviceAnnounce({device});
|
||||
await flushPromises();
|
||||
expect(zigbeeHerdsmanConverters.onEvent).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsmanConverters.onEvent).toHaveBeenCalledWith('deviceAnnounce', {device}, device, settings.getDevice(device.ieeeAddr), {});
|
||||
expect(zigbeeHerdsmanConverters.onEvent).toHaveBeenCalledWith('deviceAnnounce', {device}, device);
|
||||
});
|
||||
});
|
||||
|
||||
+20
-20
@@ -65,7 +65,7 @@ describe('OTA update', () => {
|
||||
count++;
|
||||
return {swBuildId: count, dateCode: '2019010' + count}
|
||||
});
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
logger.info.mockClear();
|
||||
device.save.mockClear();
|
||||
@@ -120,7 +120,7 @@ describe('OTA update', () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const endpoint = device.endpoints[0];
|
||||
endpoint.read.mockImplementation(() => {return {swBuildId: 1, dateCode: '2019010'}});
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
device.save.mockClear();
|
||||
mapped.ota.updateToLatest.mockImplementationOnce((a, b, onUpdate) => {
|
||||
@@ -143,7 +143,7 @@ describe('OTA update', () => {
|
||||
|
||||
it('Should be able to check if OTA update is available', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 10, otaFileVersion: 10});
|
||||
@@ -172,9 +172,9 @@ describe('OTA update', () => {
|
||||
|
||||
it('Should handle if OTA update check fails', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {throw new Error('RF singals disturbed because of dogs barking')});
|
||||
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {throw new Error('RF signals disturbed because of dogs barking')});
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "bulb");
|
||||
await flushPromises();
|
||||
@@ -182,7 +182,7 @@ describe('OTA update', () => {
|
||||
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/ota_update/check',
|
||||
stringify({"data":{"id": "bulb"},"status":"error","error": `Failed to check if update available for 'bulb' (RF singals disturbed because of dogs barking)`}),
|
||||
stringify({"data":{"id": "bulb"},"status":"error","error": `Failed to check if update available for 'bulb' (RF signals disturbed because of dogs barking)`}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
@@ -209,7 +209,7 @@ describe('OTA update', () => {
|
||||
|
||||
it('Should refuse to check/update when already in progress', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
|
||||
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {
|
||||
@@ -234,7 +234,7 @@ describe('OTA update', () => {
|
||||
const endpoint = device.endpoints[0];
|
||||
endpoint.read.mockImplementation(() => {throw new Error('Failed!')});
|
||||
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', "bulb");
|
||||
await flushPromises();
|
||||
@@ -249,7 +249,7 @@ describe('OTA update', () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
device.endpoints[0].commandResponse.mockClear();
|
||||
const data = {imageType: 12382};
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12});
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10, meta: {zclTransactionSequenceNumber: 10}};
|
||||
@@ -283,7 +283,7 @@ describe('OTA update', () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
device.endpoints[0].commandResponse.mockClear();
|
||||
const data = {imageType: 12382};
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {throw new Error('Nothing to find here')})
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10, meta: {zclTransactionSequenceNumber: 10}};
|
||||
@@ -305,7 +305,7 @@ describe('OTA update', () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
device.endpoints[0].commandResponse.mockClear();
|
||||
const data = {imageType: 12382};
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 13, otaFileVersion: 13});
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10, meta: {zclTransactionSequenceNumber: 10}};
|
||||
@@ -327,7 +327,7 @@ describe('OTA update', () => {
|
||||
settings.set(['ota', 'disable_automatic_update_check'], true);
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const data = {imageType: 12382};
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 13});
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10, meta: {zclTransactionSequenceNumber: 10}};
|
||||
@@ -366,7 +366,7 @@ describe('OTA update', () => {
|
||||
count++;
|
||||
return {swBuildId: count, dateCode: '2019010' + count}
|
||||
});
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
logger.info.mockClear();
|
||||
logger.error.mockClear();
|
||||
@@ -386,7 +386,7 @@ describe('OTA update', () => {
|
||||
expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0.00%`);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10.00%, ≈ 60 minutes remaining`);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190103","softwareBuildID":3}'`);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190103","softwareBuildID":3}'`);
|
||||
expect(logger.error).toHaveBeenCalledTimes(0);
|
||||
expect(device.save).toHaveBeenCalledTimes(2);
|
||||
expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {'sendWhen': 'immediate'});
|
||||
@@ -396,7 +396,7 @@ describe('OTA update', () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const endpoint = device.endpoints[0];
|
||||
endpoint.read.mockImplementation(() => {return {swBuildId: 1, dateCode: '2019010'}});
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
logger.info.mockClear();
|
||||
logger.error.mockClear();
|
||||
@@ -413,7 +413,7 @@ describe('OTA update', () => {
|
||||
|
||||
it('Legacy api: Should be able to check if OTA update is available', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
|
||||
logger.info.mockClear();
|
||||
@@ -435,16 +435,16 @@ describe('OTA update', () => {
|
||||
|
||||
it('Legacy api: Should handle if OTA update check fails', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
logger.error.mockClear();
|
||||
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {throw new Error('RF singals disturbed because of dogs barking')});
|
||||
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {throw new Error('RF signals disturbed because of dogs barking')});
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0);
|
||||
expect(logger.error).toHaveBeenCalledWith(`Failed to check if update available for 'bulb' (RF singals disturbed because of dogs barking)`);
|
||||
expect(logger.error).toHaveBeenCalledWith(`Failed to check if update available for 'bulb' (RF signals disturbed because of dogs barking)`);
|
||||
});
|
||||
|
||||
it('Legacy api: Should not check for OTA when device does not support it', async () => {
|
||||
@@ -463,7 +463,7 @@ describe('OTA update', () => {
|
||||
return {swBuildId: 1, dateCode: '2019010'}
|
||||
});
|
||||
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
logger.info.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb');
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('Publish', () => {
|
||||
g.command.mockClear();
|
||||
});
|
||||
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.__clearStore__();
|
||||
zigbeeHerdsmanConverters.toZigbee.__clearStore__();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -145,7 +145,7 @@ describe('Publish', () => {
|
||||
it('Should publish messages to zigbee devices with color_temp in %', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color_temp_percent: '100'}));
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color_temp_percent: 100}));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 500, transtime: 0}, {});
|
||||
@@ -1276,7 +1276,7 @@ describe('Publish', () => {
|
||||
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 53, "warningduration": 10, "strobedutycycle": 0, "strobelevel": 1}, {disableDefaultResponse: true});
|
||||
});
|
||||
|
||||
it('Shouldnt do anythign when device is not supported', async () => {
|
||||
it('Shouldnt do anything when device is not supported', async () => {
|
||||
const payload = {state: 'ON'};
|
||||
await MQTT.events.message('zigbee2mqtt/unsupported2/set', stringify(payload));
|
||||
await flushPromises();
|
||||
@@ -1343,7 +1343,7 @@ describe('Publish', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 20, transition: 0.0}));
|
||||
await flushPromises();
|
||||
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.__clearStore__();
|
||||
zigbeeHerdsmanConverters.toZigbee.__clearStore__();
|
||||
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({"state": "ON", "transition": 1.0}));
|
||||
await flushPromises();
|
||||
|
||||
+6
-18
@@ -182,7 +182,7 @@ describe('Receive', () => {
|
||||
|
||||
// Summary:
|
||||
// First send multiple measurements to device that is debouncing. Make sure only one message is sent out to MQTT. This also ensures first message is cached to "State".
|
||||
// Then send another measurement to that same device and trigger asyncronous event to push data from Cache. Newest value should be sent out.
|
||||
// Then send another measurement to that same device and trigger asynchronous event to push data from Cache. Newest value should be sent out.
|
||||
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1);
|
||||
@@ -201,11 +201,11 @@ describe('Receive', () => {
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
|
||||
|
||||
// Send another Zigbee messages...
|
||||
// Send another Zigbee message...
|
||||
await zigbeeHerdsman.events.message({data: {measuredValue: 9}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
const realDevice = controller.zigbee.resolveEntity(device);
|
||||
|
||||
// Trigger asyncronous event while device is "debouncing" to trigger Message to be sent out from State cache.
|
||||
// Trigger asynchronous event while device is "debouncing" to trigger Message to be sent out from State cache.
|
||||
await controller.publishEntityState( realDevice, {} );
|
||||
jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
@@ -213,7 +213,7 @@ describe('Receive', () => {
|
||||
// Total of 3 messages should have triggered.
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Test that message pushed by asyncronous message contains NEW measurement and not old.
|
||||
// Test that message pushed by asynchronous message contains NEW measurement and not old.
|
||||
expect(JSON.parse(MQTT.publish.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(MQTT.publish.mock.calls[2][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
|
||||
@@ -388,18 +388,6 @@ describe('Receive', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should not handle messages from unsupported devices and link to docs', async () => {
|
||||
const device = zigbeeHerdsman.devices.unsupported;
|
||||
const data = {onOff: 1};
|
||||
logger.warn.mockClear();
|
||||
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(logger.warn).toHaveBeenCalledWith(`Received message from unsupported device with Zigbee model 'notSupportedModelID' and manufacturer name 'notSupportedMfg'`);
|
||||
expect(logger.warn).toHaveBeenCalledWith(`Please see: https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html`);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should not handle messages from still interviewing devices with unknown definition', async () => {
|
||||
const device = zigbeeHerdsman.devices.interviewing;
|
||||
const data = {onOff: 1};
|
||||
@@ -408,7 +396,7 @@ describe('Receive', () => {
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
expect(logger.debug).toHaveBeenCalledWith(`Skipping message, definition is undefined and still interviewing`);
|
||||
expect(logger.debug).toHaveBeenCalledWith(`Skipping message, still interviewing`);
|
||||
});
|
||||
|
||||
it('Should handle a command', async () => {
|
||||
@@ -474,6 +462,6 @@ describe('Receive', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_OLD');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({energy: 6.648, power: 496});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({energy: 6.65, power: 496});
|
||||
});
|
||||
});
|
||||
|
||||
+19
-13
@@ -335,22 +335,15 @@ describe('Settings', () => {
|
||||
expect(read(devicesFile)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should add devices for first file when using 2 separates file', () => {
|
||||
function extractFromMultipleDeviceConfigs(contentDevices2) {
|
||||
const contentConfiguration = {
|
||||
devices: ['devices.yaml', 'devices2.yaml']
|
||||
devices: ['devices.yaml', 'devices2.yaml'],
|
||||
};
|
||||
|
||||
const contentDevices = {
|
||||
'0x12345678': {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
},
|
||||
};
|
||||
|
||||
const contentDevices2 = {
|
||||
'0x87654321': {
|
||||
friendly_name: '0x87654321',
|
||||
retain: false,
|
||||
retain: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -365,15 +358,28 @@ describe('Settings', () => {
|
||||
const expected = {
|
||||
'0x12345678': {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
retain: false,
|
||||
},
|
||||
'0x1234': {
|
||||
'0x1234': {
|
||||
friendly_name: '0x1234',
|
||||
},
|
||||
};
|
||||
|
||||
expect(read(devicesFile)).toStrictEqual(expected);
|
||||
expect(read(devicesFile2)).toStrictEqual(contentDevices2);
|
||||
}
|
||||
|
||||
it('Should add devices for first file when using 2 separates file', () => {
|
||||
extractFromMultipleDeviceConfigs({
|
||||
'0x87654321': {
|
||||
friendly_name: '0x87654321',
|
||||
retain: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('Should add devices for first file when using 2 separates file and the second file is empty', () => {
|
||||
extractFromMultipleDeviceConfigs(null)
|
||||
});
|
||||
|
||||
it('Should add devices to a separate file if devices.yaml doesnt exist', () => {
|
||||
@@ -935,7 +941,7 @@ describe('Settings', () => {
|
||||
});
|
||||
|
||||
settings.reRead();
|
||||
expect(settings.get().frontend).toStrictEqual({port: 8080, auth_token: false, host: '0.0.0.0'})
|
||||
expect(settings.get().frontend).toStrictEqual({port: 8080, auth_token: false})
|
||||
});
|
||||
|
||||
it('Baudrate config', () => {
|
||||
|
||||
@@ -134,7 +134,7 @@ const bulb_color = new Device('Router', '0x000b57fffec6a5b3', 40399, 4107, [new
|
||||
const bulb_color_2 = new Device('Router', '0x000b57fffec6a5b4', 401292, 4107, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b4', [], {lightingColorCtrl: {colorCapabilities: 254}}, [], null, null, {'scenes': {'1_0': {name: 'Chill scene', state: {state: 'ON'}}, '4_9': {state: {state: 'OFF'}}}})], true, "Mains (single phase)", "LLC020", false, 'Philips', '2019.09', '5.127.1.26581');
|
||||
const bulb_2 = new Device('Router', '0x000b57fffec6a5b7', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b7', [], {lightingColorCtrl: {colorCapabilities: 17}})], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm");
|
||||
const TS0601_thermostat = new Device('EndDevice', '0x0017882104a44559', 6544,4151, [new Endpoint(1, [], [], '0x0017882104a44559')], true, "Mains (single phase)", 'kud7u2l');
|
||||
const ZNCZ02LM = new Device('Router', '0x0017880104e45524', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45524')], true, "Mains (single phase)", "lumi.plug");
|
||||
const ZNCZ02LM = new Device('Router', '0x0017880104e45524', 6540,4151, [new Endpoint(1, [0, 6], [], '0x0017880104e45524')], true, "Mains (single phase)", "lumi.plug");
|
||||
const GLEDOPTO_2ID = new Device('Router', '0x0017880104e45724', 6540,4151, [new Endpoint(11, [0,3,4,5,6,8,768], [], '0x0017880104e45724', [], {}, [], 49246, 528), new Endpoint(12, [0, 3, 4, 5, 6, 8, 768], [], '0x0017880104e45724', [], {}, [], 260, 258), new Endpoint(13, [4096], [4096], '0x0017880104e45724', [], {}, [], 49246, 57694), new Endpoint(15, [0, 3, 4, 5, 6, 8, 768], [], '0x0017880104e45724', [], {}, [], 49246, 256)], true, "Mains (single phase)", 'GL-C-007', false, 'GLEDOPTO');
|
||||
const QBKG03LM = new Device('Router', '0x0017880104e45542', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45542'), new Endpoint(2, [0, 6], [], '0x0017880104e45542'), new Endpoint(3, [0, 6], [], '0x0017880104e45542')], true, "Mains (single phase)", 'lumi.ctrl_neutral2');
|
||||
const zigfred_plus = new Device('Router', '0xf4ce368a38be56a1', 6589, 0x129C, [new Endpoint(5, [0, 3, 4, 5, 6, 8, 0x0300, 0xFC42], [0xFC42], '0xf4ce368a38be56a1'), new Endpoint(7, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), new Endpoint(8, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), new Endpoint(9, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), new Endpoint(10, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), new Endpoint(11, [0, 3, 4, 5, 0x0102], [], '0xf4ce368a38be56a1'), new Endpoint(12, [0, 3, 4, 5, 0x0102], [], '0xf4ce368a38be56a1')], true, "Mains (single phase)", 'zigfred plus', false, 'Siglis');
|
||||
|
||||
+3
-2
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"target": "es2018",
|
||||
"lib": ["es2018"],
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"moduleResolution": "node",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ -d data-backup ]; then
|
||||
echo "ERROR: Backup directory exists. May be previous restoring was failed?"
|
||||
@@ -26,6 +27,9 @@ git pull
|
||||
echo "Installing dependencies..."
|
||||
npm ci
|
||||
|
||||
echo "Building..."
|
||||
npm run build
|
||||
|
||||
echo "Restore configuration..."
|
||||
cp -R data-backup/* data
|
||||
rm -rf data-backup
|
||||
|
||||
Reference in New Issue
Block a user