Compare commits

...
64 Commits
Author SHA1 Message Date
Koen Kanters 403d3c090d chore(dev): release 1.40.1 (#23797) 2024-09-07 13:25:22 +02:00
Koen Kanters 9283c4b497 fix(ignore): update zigbee-herdsman-converters to 20.12.1 (#23884) 2024-09-07 11:17:24 +00:00
Koen Kanters 0d46354c48 fix(ignore): update zigbee-herdsman to 0.57.3 (#23877) 2024-09-06 20:26:15 +00:00
Koen Kanters 13a108ddbc fix: Log stack trace when update check fails https://github.com/Koenkk/zigbee2mqtt/discussions/23798 2024-09-06 14:37:20 +02:00
Koen Kanters 5524e881e6 fix(ignore): update zigbee-herdsman to 0.57.2 (#23872) 2024-09-06 12:19:21 +00:00
Koen Kanters a33c29f10a fix(ignore): update zigbee-herdsman-converters to 20.11.0 (#23837) 2024-09-05 12:39:50 +00:00
Koen Kanters fc02071c8f chore: fix e2da2fa8d3 2024-09-05 14:37:30 +02:00
Koen Kanters 06556d44f3 chore: merge master to dev 2024-09-05 13:35:31 +02:00
Koen Kanters 13084d22a7 chore: Update new device support template 2024-09-05 13:35:15 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 14787d3a6a chore(deps): bump peter-evans/create-pull-request from 6 to 7 (#23832)
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 6 to 7.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v6...v7)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-03 20:21:00 +02:00
Jose Luis Pedrosaandjose e2da2fa8d3 chore: Invoke chart CI (#23819)
The workflow name on the chart repository is OnZigbee2MQTTRelease, this event means that a new version of zigbe2mqtt is has been released, it should probably trigger too the release, but that is a chart repo concern

Co-authored-by: jose <jose.pedrosa@reddit.com>
2024-09-03 20:20:40 +02:00
Koen Kanters 6dade8b11f fix(ignore): update zigbee-herdsman-converters to 20.9.0 (#23822) 2024-09-02 20:48:09 +00:00
Koen Kanters 557b17ad5b fix: Fix Cannot read properties of undefined (reading 'some') https://github.com/Koenkk/zigbee2mqtt/issues/23821 2024-09-02 22:22:19 +02:00
Koen Kanters 64222d231f Update zigbee-herdsman-converters to 20.8.5 (#23799)
* fix(ignore): update zigbee-herdsman-converters to 20.8.5

* Update utils.ts
2024-09-01 19:23:14 +00:00
Koen Kanters b7fa722109 fix(ignore): update dependencies (#23790) 2024-09-01 20:23:49 +02:00
github-actions[bot] 4a1d983168 chore: promote to dev 2024-09-01 18:22:59 +00:00
Koen Kanters 6fa1dabecb chore(dev): release 1.40.0 (#23473) 2024-09-01 20:12:13 +02:00
Koen Kanters 3b2ba0950e fix(ignore): update zigbee-herdsman-converters to 20.8.4 (#23791) 2024-09-01 18:10:07 +00:00
Koen Kanters 9f40936452 fix(ignore): update zigbee-herdsman-converters to 20.8.1 (#23783) 2024-08-30 18:35:59 +00:00
Koen Kanters b2bae6ba94 fix: Add logging when loading external converters https://github.com/Koenkk/zigbee2mqtt/issues/23703 2024-08-30 20:23:55 +02:00
Koen Kanters 406deaa9da fix(ignore): update zigbee-herdsman to 0.57.1 (#23781) 2024-08-30 18:14:13 +00:00
Koen Kanters 65b1e60a3d fix(ignore): update zigbee-herdsman-converters to 20.8.0 (#23777) 2024-08-29 19:28:15 +00:00
Koen Kanters 6ed9b26657 fix(ignore): update zigbee-herdsman-converters to 20.7.0 (#23759) 2024-08-27 18:57:31 +00:00
Koen Kanters cf3298bf6f fix: Update watchdog delays (#23757) 2024-08-27 20:50:24 +02:00
Koen Kanters 00948c0be1 chore: merge master to dev 2024-08-27 20:48:05 +02:00
Koen Kanters ed9de74f91 chore: Update New device support request template 2024-08-27 20:47:45 +02:00
Koen Kanters 41587211e0 fix(ignore): update zigbee-herdsman to 0.57.0 (#23758) 2024-08-27 18:43:23 +00:00
Ilya Kirov b4176d6fee feat: Add support for zboss adapter (#23747)
https://github.com/Koenkk/zigbee-herdsman/pull/1165
2024-08-27 20:37:00 +02:00
Koen Kanters ccdf6b3c6d fix(ignore): update zigbee-herdsman-converters to 20.6.0 (#23750) 2024-08-26 19:06:20 +00:00
Koen Kanters 8a05077792 fix(ignore): update dependencies (#23731) 2024-08-26 20:12:08 +02:00
Koen Kanters df7772bfe0 fix(ignore): update zigbee-herdsman-converters to 20.5.0 (#23723) 2024-08-24 13:07:53 +00:00
Koen Kanters 6ae9970efd fix(ignore): update zigbee-herdsman-converters to 20.4.0 (#23692) 2024-08-21 19:45:53 +00:00
Koen Kanters b92072fc6a Merge remote-tracking branch 'origin/master' into dev 2024-08-20 21:38:19 +02:00
Koen Kanters 5bdf90df9e chore: Trigger zigbee2mqtt-chart release on release https://github.com/Koenkk/zigbee2mqtt-chart/issues/7 2024-08-20 21:36:01 +02:00
Koen Kanters 22980177b6 fix(ignore): Fix crash when retrieving state (#23679) 2024-08-20 20:57:09 +02:00
Koen Kanters 837cbe5f26 fix(ignore): update zigbee-herdsman-converters to 20.3.0 (#23666) 2024-08-19 19:13:00 +00:00
Koen Kanters e95116c773 fix(ignore): update zigbee-herdsman to 0.56.2 (#23658) 2024-08-18 12:38:04 +00:00
Koen Kanters 27300ba8da fix(ignore): update dependencies (#23653) 2024-08-18 14:03:00 +02:00
Koen Kanters 801ab9066f fix(ignore): update zigbee-herdsman to 0.56.0 (#23643) 2024-08-16 20:57:46 +00:00
Koen Kanters 2a631f3551 chore: merge master to dev 2024-08-16 22:31:18 +02:00
Koen Kanters 66fefaa827 chore: Add fail_pr_to_master.yaml 2024-08-16 22:30:56 +02:00
Koen Kanters c986fd6c66 fix(ignore): update zigbee-herdsman-converters to 20.2.0 (#23628) 2024-08-15 19:32:00 +00:00
Koen Kanters 29f436ab95 fix(ignore): update zigbee-herdsman-converters to 20.1.1 (#23612) 2024-08-13 19:40:53 +00:00
Koen KantersandKoenkk d827d954ed fix(ignore): update zigbee-herdsman-converters to 20.0.0 (#23595)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-12 11:39:30 +00:00
Koen Kanters 8ae2c4d42d chore: merge master to dev 2024-08-12 13:15:23 +02:00
Koen Kanters 9027c41fe8 Revert "Added pictures for _TZE204_ncti2pro (ZSVIOT PN6) (#23552)"
This reverts commit 00b5e983d9.
2024-08-12 13:15:02 +02:00
Andre Klärner a5748d620b fix: Add OCI labels to image (#23578)
this adds Docker Labels according to the OCI Image Format Specification
which means that tools like renovate can automatically find the
changelogs from a docker image, helping the people like me judge the
impact of upgrading their z2m instance from the pull request renovate
generates.
2024-08-11 21:59:44 +02:00
Koen Kanters d1734feb21 chore: merge master to dev 2024-08-09 14:37:08 +02:00
Vitaliy 00b5e983d9 Added pictures for _TZE204_ncti2pro (ZSVIOT PN6) (#23552)
* Added pictures for _TZE204_ncti2pro (ZSVIOT PN6)

* Removed jpg file and renamed TS0601_pn6_way_controller.png to PN6.png
2024-08-09 14:36:52 +02:00
Koen Kanters efd3d3dfb0 fix: Set zigbee-herdsman transmit power through settings on startup (#23566)
* fix: Set zigbee-herdsman transmit power through settings on startup

* use advanced
2024-08-09 12:33:59 +00:00
Koen KantersandKoenkk aeba93d0a3 fix(ignore): update zigbee-herdsman-converters to 19.75.0 (#23565)
* fix(ignore): update zigbee-herdsman-converters to 19.75.0

* fix

---------

Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-08 21:14:32 +02:00
Koen KantersandKoenkk 7ca456d20e fix(ignore): update zigbee-herdsman to 0.55.5 (#23564)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-08 19:06:42 +00:00
Nerivec 13ac8a0f53 fix: Improve looping performance (#23541)
* Use herdsman improved looping.

* Fix lint.

* Fix tests.

* Feedback
2024-08-08 20:21:40 +02:00
Koen KantersandKoenkk fe6cacd87e fix(ignore): update zigbee-herdsman-converters to 19.74.0 (#23539)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-06 19:42:34 +00:00
Koen KantersandKoenkk 00923685d2 fix(ignore): update zigbee-herdsman-converters to 19.73.1 (#23527)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-05 21:14:09 +00:00
Koen KantersandKoenkk bd7dec0f11 fix(ignore): update dependencies (#23508)
* fix(ignore): update dependencies

* update

---------

Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-05 21:08:40 +00:00
Koen Kanters 9bb21f486c chore: merge master to dev 2024-08-04 22:14:52 +02:00
LaurentandKoen Kanters d0f5733db3 fix: Remove group from Home Assistant when removed (#23492)
* Fire event when group gets removed

* Add a unit test for the new behavior

* Make prettier happy

* Add missing return type

* Merge device and group removal events

* Unbreak bridgeLegacy.ts

* Update homeassistant.ts

* Update bridge.ts

* Update bridge.ts

---------

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2024-08-04 22:14:36 +02:00
Koen Kanters 5b610afa8f fix: Add min/max for some adapter options (#23503)
* fix: Add min/max for some adapter options

* update
2024-08-04 19:53:29 +00:00
Koen KantersandKoenkk 2cb8a8f5a5 fix(ignore): update zigbee-herdsman-converters to 19.73.0 (#23495)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-03 11:05:54 +00:00
Koen Kanters 3217332e46 fix: Fix expose category ignored by HA discovery (#23494) 2024-08-03 13:00:29 +02:00
Koen KantersandKoenkk a25ef81cf4 fix(ignore): update zigbee-herdsman to 0.55.4 (#23493)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-03 10:31:41 +00:00
Koen Kanters eac11a6439 chore: fix package.json indentation 2024-08-01 21:11:44 +02:00
github-actions[bot] a330830cde chore: promote to dev 2024-08-01 18:57:20 +00:00
40 changed files with 1872 additions and 801 deletions
@@ -27,6 +27,14 @@ body:
placeholder: '{"id":53,"type":"Router","ieeeAddr":"0x10458d00024284f69","nwkAddr":10148,"manufId":4151,"manufName":"LUMI","powerSource":"DC Source","modelId":"lumi.relay.c2acn01","epList":[1,2],"endpoints":{"1":{"profId":260,"epId":1,"devId":257,"inClusterList":[0,3,4,5,1,2,10,6,16,2820,12],"outClusterList":[25,10],"clusters":{"genBasic":{"attributes":{"modelId":"lumi.relay.c2acn01","appVersion":1,"manufacturerName":"LUMI","powerSource":4,"zclVersion":0,"stackVersion":2,"hwVersion":18,"dateCode":"8-6-2020"}},"genAnalogInput":{"attributes":{"presentValue":129.04425048828125}},"genOnOff":{"attributes":{"61440":117440715,"onOff":1}}},"binds":[],"configuredReportings":[],"meta":{}},"2":{"profId":260,"epId":2,"devId":257,"inClusterList":[6,16,4,5],"outClusterList":[],"clusters":{"genOnOff":{"attributes":{"61440":237478966,"onOff":0}}},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":1,"stackVersion":2,"hwVersion":18,"dateCode":"8-6-2020","zclVersion":0,"interviewCompleted":true,"meta":{},"lastSeen":1640285631405}'
validations:
required: true
- type: input
id: z2m_version
attributes:
label: Zigbee2MQTT version
description: Can be found in the frontend -> settings -> about -> Zigbee2MQTT version. Are you running Zigbee2MQTT 1.18.1? Then read [this](https://github.com/Koenkk/zigbee2mqtt/releases/tag/1.19.0).
placeholder: '1.22.1'
validations:
required: true
- type: textarea
id: notes
attributes:
@@ -41,3 +49,9 @@ body:
render: shell
validations:
required: true
- type: textarea
attributes:
label: What does/doesn't work with the external definition?
description: Indicate what works and what doesn't work with the external definition.
validations:
required: true
+14 -2
View File
@@ -98,6 +98,18 @@ jobs:
-H "Content-Type: application/json" \
https://api.github.com/repos/zigbee2mqtt/hassio-zigbee2mqtt/dispatches \
--data "{\"event_type\": \"release\", \"client_payload\": { \"version\": \"$TAG-1\"}}"
- name: 'release: Trigger zigbee2mqtt-chart image update'
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
run: |
TAG=${GITHUB_REF#refs/*/}
echo "Triggering with tag '$TAG'"
curl \
-X POST \
-H "Authorization: token ${{ secrets.GH_TOKEN }}" \
-H "Accept: application/vnd.github.everest-preview+json" \
-H "Content-Type: application/json" \
https://api.github.com/repos/Koenkk/zigbee2mqtt-chart/actions/workflows/OnZigbee2MQTTRelease/dispatches \
-d '{"ref":"main","inputs":{"zigbee2mqtt_version":"$TAG" }}'
- name: 'release: merge dev -> master and promote dev'
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
run: |
@@ -110,8 +122,8 @@ jobs:
git merge --ff-only origin/dev
git push origin master
git checkout dev
jq ".version = \"$TAG-dev\"" package.json > package.json.tmp
jq ".version = \"$TAG-dev\"" package-lock.json > package-lock.json.tmp
jq --indent 4 ".version = \"$TAG-dev\"" package.json > package.json.tmp
jq --indent 4 ".version = \"$TAG-dev\"" package-lock.json > package-lock.json.tmp
mv package.json.tmp package.json
mv package-lock.json.tmp package-lock.json
git add -A
+19
View File
@@ -0,0 +1,19 @@
name: Fail PR to master
on:
pull_request:
branches:
- master
jobs:
fail_pr:
runs-on: ubuntu-latest
steps:
- name: Fail PR to master
run: |
if [[ "${{ github.event.pull_request.title }}" == "chore(dev): release"* ]]; then
echo "PR title starts with 'chore(dev): release', allowing PR"
else
echo "Pull requests to the master branch are not allowed, target dev branch"
exit 1
fi
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
node-version: 20
cache: npm
- run: npm install ${{ github.event.client_payload.package }}@${{ github.event.client_payload.version }} --save-exact
- uses: peter-evans/create-pull-request@v6
- uses: peter-evans/create-pull-request@v7
id: cpr
with:
commit-message: 'fix(ignore): update ${{ github.event.client_payload.package }} to ${{ github.event.client_payload.version }}'
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
- run: npx npm-check-updates -u -x connect-gzip-static -x eslint
- run: rm -f package-lock.json
- run: npm install
- uses: peter-evans/create-pull-request@v6
- uses: peter-evans/create-pull-request@v7
with:
commit-message: 'fix(ignore): update dependencies'
branch: 'deps/all'
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "1.39.1"
".": "1.40.1"
}
+58
View File
@@ -1,5 +1,63 @@
# Changelog
## [1.40.1](https://github.com/Koenkk/zigbee2mqtt/compare/1.40.0...1.40.1) (2024-09-07)
### Bug Fixes
* Fix `Cannot read properties of undefined (reading 'some')` https://github.com/Koenkk/zigbee2mqtt/issues/23821 ([557b17a](https://github.com/Koenkk/zigbee2mqtt/commit/557b17ad5b6dc375e361fb3ec5a84134be761a00))
* **ignore:** update dependencies ([#23790](https://github.com/Koenkk/zigbee2mqtt/issues/23790)) ([b7fa722](https://github.com/Koenkk/zigbee2mqtt/commit/b7fa7221093656cee3c908aa1c0e45e350aa43c9))
* **ignore:** update zigbee-herdsman to 0.57.2 ([#23872](https://github.com/Koenkk/zigbee2mqtt/issues/23872)) ([5524e88](https://github.com/Koenkk/zigbee2mqtt/commit/5524e881e6458afac2c5ae712bc6d0a1090e7115))
* **ignore:** update zigbee-herdsman to 0.57.3 ([#23877](https://github.com/Koenkk/zigbee2mqtt/issues/23877)) ([0d46354](https://github.com/Koenkk/zigbee2mqtt/commit/0d46354c485ba5c644af8d933b1689468b3a058c))
* **ignore:** update zigbee-herdsman-converters to 20.11.0 ([#23837](https://github.com/Koenkk/zigbee2mqtt/issues/23837)) ([a33c29f](https://github.com/Koenkk/zigbee2mqtt/commit/a33c29f10af5fc423ff9668af59826a37d9784bf))
* **ignore:** update zigbee-herdsman-converters to 20.12.1 ([#23884](https://github.com/Koenkk/zigbee2mqtt/issues/23884)) ([9283c4b](https://github.com/Koenkk/zigbee2mqtt/commit/9283c4b4973e9e7309081a9444427c87f9309f91))
* **ignore:** update zigbee-herdsman-converters to 20.9.0 ([#23822](https://github.com/Koenkk/zigbee2mqtt/issues/23822)) ([6dade8b](https://github.com/Koenkk/zigbee2mqtt/commit/6dade8b11faa13096191f9774a7a1dd320af6bde))
* Log stack trace when update check fails https://github.com/Koenkk/zigbee2mqtt/discussions/23798 ([13a108d](https://github.com/Koenkk/zigbee2mqtt/commit/13a108ddbc8f33d1cb23b92e2c67854fbd2dc338))
## [1.40.0](https://github.com/Koenkk/zigbee2mqtt/compare/1.39.1...1.40.0) (2024-09-01)
### Features
* Add support for `zboss` adapter ([#23747](https://github.com/Koenkk/zigbee2mqtt/issues/23747)) ([b4176d6](https://github.com/Koenkk/zigbee2mqtt/commit/b4176d6fee5c735ef224eaefdae57fd9d8e99da1))
### Bug Fixes
* Add logging when loading external converters https://github.com/Koenkk/zigbee2mqtt/issues/23703 ([b2bae6b](https://github.com/Koenkk/zigbee2mqtt/commit/b2bae6ba94871469ea42d8e6fcdde5f4f812a86d))
* Add min/max for some adapter options ([#23503](https://github.com/Koenkk/zigbee2mqtt/issues/23503)) ([5b610af](https://github.com/Koenkk/zigbee2mqtt/commit/5b610afa8fbebc9158b08282914a7102b8619eb3))
* Add OCI labels to image ([#23578](https://github.com/Koenkk/zigbee2mqtt/issues/23578)) ([a5748d6](https://github.com/Koenkk/zigbee2mqtt/commit/a5748d620bdff04abc145cb6cef89855d7776d2c))
* Fix expose category ignored by HA discovery ([#23494](https://github.com/Koenkk/zigbee2mqtt/issues/23494)) ([3217332](https://github.com/Koenkk/zigbee2mqtt/commit/3217332e463194abd54154a8c7fc1d427220559f))
* **ignore:** Fix crash when retrieving state ([#23679](https://github.com/Koenkk/zigbee2mqtt/issues/23679)) ([2298017](https://github.com/Koenkk/zigbee2mqtt/commit/22980177b641b381c6683236b988e7525a0768c9))
* **ignore:** update dependencies ([#23508](https://github.com/Koenkk/zigbee2mqtt/issues/23508)) ([bd7dec0](https://github.com/Koenkk/zigbee2mqtt/commit/bd7dec0f11954491ac21d13a7225ff3ed549e856))
* **ignore:** update dependencies ([#23653](https://github.com/Koenkk/zigbee2mqtt/issues/23653)) ([27300ba](https://github.com/Koenkk/zigbee2mqtt/commit/27300ba8da8288bed65b61750b732c95a8b532e6))
* **ignore:** update dependencies ([#23731](https://github.com/Koenkk/zigbee2mqtt/issues/23731)) ([8a05077](https://github.com/Koenkk/zigbee2mqtt/commit/8a05077792ef062bd5aaac9d2e7b85cce9e73fa5))
* **ignore:** update zigbee-herdsman to 0.55.4 ([#23493](https://github.com/Koenkk/zigbee2mqtt/issues/23493)) ([a25ef81](https://github.com/Koenkk/zigbee2mqtt/commit/a25ef81cf4afe3aae4af621480bff74f3e7116a0))
* **ignore:** update zigbee-herdsman to 0.55.5 ([#23564](https://github.com/Koenkk/zigbee2mqtt/issues/23564)) ([7ca456d](https://github.com/Koenkk/zigbee2mqtt/commit/7ca456d20e45b0e7677f37ff1d93ecaee3e908c1))
* **ignore:** update zigbee-herdsman to 0.56.0 ([#23643](https://github.com/Koenkk/zigbee2mqtt/issues/23643)) ([801ab90](https://github.com/Koenkk/zigbee2mqtt/commit/801ab9066fe5e6b8e53193d4e5f9935342280e0c))
* **ignore:** update zigbee-herdsman to 0.56.2 ([#23658](https://github.com/Koenkk/zigbee2mqtt/issues/23658)) ([e95116c](https://github.com/Koenkk/zigbee2mqtt/commit/e95116c7736464bc1ab25c1a6bc635cfead0f7b5))
* **ignore:** update zigbee-herdsman to 0.57.0 ([#23758](https://github.com/Koenkk/zigbee2mqtt/issues/23758)) ([4158721](https://github.com/Koenkk/zigbee2mqtt/commit/41587211e0609bcc4449138d889ed2eafcce921a))
* **ignore:** update zigbee-herdsman to 0.57.1 ([#23781](https://github.com/Koenkk/zigbee2mqtt/issues/23781)) ([406deaa](https://github.com/Koenkk/zigbee2mqtt/commit/406deaa9dad4ab58a1b2834a909daf9abc23e678))
* **ignore:** update zigbee-herdsman-converters to 19.73.0 ([#23495](https://github.com/Koenkk/zigbee2mqtt/issues/23495)) ([2cb8a8f](https://github.com/Koenkk/zigbee2mqtt/commit/2cb8a8f5a5647b177a9dc3d0dc262bec406e26b1))
* **ignore:** update zigbee-herdsman-converters to 19.73.1 ([#23527](https://github.com/Koenkk/zigbee2mqtt/issues/23527)) ([0092368](https://github.com/Koenkk/zigbee2mqtt/commit/00923685d29b7de994e92d7e0b2429288375504b))
* **ignore:** update zigbee-herdsman-converters to 19.74.0 ([#23539](https://github.com/Koenkk/zigbee2mqtt/issues/23539)) ([fe6cacd](https://github.com/Koenkk/zigbee2mqtt/commit/fe6cacd87e7cbba4c91575a4e84ab7e6a7f9d3fe))
* **ignore:** update zigbee-herdsman-converters to 19.75.0 ([#23565](https://github.com/Koenkk/zigbee2mqtt/issues/23565)) ([aeba93d](https://github.com/Koenkk/zigbee2mqtt/commit/aeba93d0a3c2552f5a4103e3dd54f8315aad799b))
* **ignore:** update zigbee-herdsman-converters to 20.0.0 ([#23595](https://github.com/Koenkk/zigbee2mqtt/issues/23595)) ([d827d95](https://github.com/Koenkk/zigbee2mqtt/commit/d827d954ed3a61fc00cbae4952f18eaaf8e5438b))
* **ignore:** update zigbee-herdsman-converters to 20.1.1 ([#23612](https://github.com/Koenkk/zigbee2mqtt/issues/23612)) ([29f436a](https://github.com/Koenkk/zigbee2mqtt/commit/29f436ab953a36ffd13e837b52fb693ca3ac2389))
* **ignore:** update zigbee-herdsman-converters to 20.2.0 ([#23628](https://github.com/Koenkk/zigbee2mqtt/issues/23628)) ([c986fd6](https://github.com/Koenkk/zigbee2mqtt/commit/c986fd6c66bd31384bfd801c7e1f53190af1bb62))
* **ignore:** update zigbee-herdsman-converters to 20.3.0 ([#23666](https://github.com/Koenkk/zigbee2mqtt/issues/23666)) ([837cbe5](https://github.com/Koenkk/zigbee2mqtt/commit/837cbe5f266cc7c10b71a5771fa2334f91291069))
* **ignore:** update zigbee-herdsman-converters to 20.4.0 ([#23692](https://github.com/Koenkk/zigbee2mqtt/issues/23692)) ([6ae9970](https://github.com/Koenkk/zigbee2mqtt/commit/6ae9970efd44fb156a4438bfa95845aa498deb42))
* **ignore:** update zigbee-herdsman-converters to 20.5.0 ([#23723](https://github.com/Koenkk/zigbee2mqtt/issues/23723)) ([df7772b](https://github.com/Koenkk/zigbee2mqtt/commit/df7772bfe06949771a8b9d7a4f40459322b4bf92))
* **ignore:** update zigbee-herdsman-converters to 20.6.0 ([#23750](https://github.com/Koenkk/zigbee2mqtt/issues/23750)) ([ccdf6b3](https://github.com/Koenkk/zigbee2mqtt/commit/ccdf6b3c6da984e798ba878d35d4286408b760a1))
* **ignore:** update zigbee-herdsman-converters to 20.7.0 ([#23759](https://github.com/Koenkk/zigbee2mqtt/issues/23759)) ([6ed9b26](https://github.com/Koenkk/zigbee2mqtt/commit/6ed9b266577da363710919dc332bfc232f4485d5))
* **ignore:** update zigbee-herdsman-converters to 20.8.0 ([#23777](https://github.com/Koenkk/zigbee2mqtt/issues/23777)) ([65b1e60](https://github.com/Koenkk/zigbee2mqtt/commit/65b1e60a3d16353342d870d42225e536c1595286))
* **ignore:** update zigbee-herdsman-converters to 20.8.1 ([#23783](https://github.com/Koenkk/zigbee2mqtt/issues/23783)) ([9f40936](https://github.com/Koenkk/zigbee2mqtt/commit/9f409364524d8a3c87d0c06348088cb1e8c92384))
* **ignore:** update zigbee-herdsman-converters to 20.8.4 ([#23791](https://github.com/Koenkk/zigbee2mqtt/issues/23791)) ([3b2ba09](https://github.com/Koenkk/zigbee2mqtt/commit/3b2ba0950e99b481b16518921a15f4f3ed195e01))
* Improve looping performance ([#23541](https://github.com/Koenkk/zigbee2mqtt/issues/23541)) ([13ac8a0](https://github.com/Koenkk/zigbee2mqtt/commit/13ac8a0f539ece1ccc9f67ef4a8e5dd9bc8de09f))
* Remove group from Home Assistant when removed ([#23492](https://github.com/Koenkk/zigbee2mqtt/issues/23492)) ([d0f5733](https://github.com/Koenkk/zigbee2mqtt/commit/d0f5733db37038f0f7602f43f45b55462d58a4be))
* Set zigbee-herdsman transmit power through settings on startup ([#23566](https://github.com/Koenkk/zigbee2mqtt/issues/23566)) ([efd3d3d](https://github.com/Koenkk/zigbee2mqtt/commit/efd3d3dfb09b3d3a20bcf38d19c049688a87f129))
* Update watchdog delays ([#23757](https://github.com/Koenkk/zigbee2mqtt/issues/23757)) ([cf3298b](https://github.com/Koenkk/zigbee2mqtt/commit/cf3298bf6fc6c8c4023c24916e5e99a25edf2e21))
## [1.39.1](https://github.com/Koenkk/zigbee2mqtt/compare/1.39.0...1.39.1) (2024-08-01)
+7
View File
@@ -18,6 +18,13 @@ RUN apk add --no-cache --virtual .buildtools make gcc g++ python3 linux-headers
# Release
FROM base as release
LABEL org.opencontainers.image.authors="Koen Kanters"
LABEL org.opencontainers.image.title="zigbee2mqtt"
LABEL org.opencontainers.image.description="Zigbee to MQTT bridge using Zigbee-herdsman"
LABEL org.opencontainers.image.url="https://github.com/Koenkk/zigbee2mqtt"
LABEL org.opencontainers.image.documentation="https://www.zigbee2mqtt.io/"
LABEL org.opencontainers.image.source="https://github.com/Koenkk/zigbee2mqtt"
COPY --from=dependencies_and_build /app/node_modules ./node_modules
COPY dist ./dist
COPY package.json LICENSE index.js data/configuration.yaml data/configuration.example.yaml ./
+1 -1
View File
@@ -13,7 +13,7 @@ let watchdog = process.env.Z2M_WATCHDOG != undefined;
let watchdogCount = 0;
let unsolicitedStop = false;
// csv in minutes, default: 1min, 5min, 15min, 30min, 60min
let watchdogDelays = [60000, 300000, 900000, 1800000, 3600000];
let watchdogDelays = [2000, 60000, 300000, 900000, 1800000, 3600000];
if (watchdog && process.env.Z2M_WATCHDOG !== 'default') {
if (/^(?:(?:[0-9]*[.])?[0-9]+)+(?:,?(?:[0-9]*[.])?[0-9]+)*$/.test(process.env.Z2M_WATCHDOG)) {
+12 -7
View File
@@ -66,6 +66,7 @@ type ExtensionArgs = [
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let sdNotify: any = null;
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
sdNotify = process.env.NOTIFY_SOCKET ? require('sd-notify') : null;
} catch {
// sd-notify is optional
@@ -156,15 +157,19 @@ export class Controller {
}
// Log zigbee clients on startup
const devices = this.zigbee.devices(false);
logger.info(`Currently ${devices.length} devices are joined:`);
for (const device of devices) {
let deviceCount = 0;
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
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})`);
deviceCount++;
}
logger.info(`Currently ${deviceCount} devices are joined.`);
// Enable zigbee join
try {
if (settings.get().permit_join) {
@@ -175,14 +180,14 @@ export class Controller {
await this.zigbee.permitJoin(settings.get().permit_join);
} catch (error) {
logger.error(`Failed to set permit join to ${settings.get().permit_join}`);
logger.error(`Failed to set permit join to ${settings.get().permit_join} (${error.message})`);
}
// MQTT
try {
await this.mqtt.connect();
} catch (error) {
logger.error(`MQTT failed to connect, exiting...`);
logger.error(`MQTT failed to connect, exiting... (${error.message})`);
await this.zigbee.stop();
return this.exit(1);
}
@@ -192,7 +197,7 @@ export class Controller {
// Send all cached states.
if (settings.get().advanced.cache_state_send_on_startup && settings.get().advanced.cache_state) {
for (const entity of [...devices, ...this.zigbee.groups()]) {
for (const entity of this.zigbee.devicesAndGroupsIterator()) {
if (this.state.exists(entity)) {
await this.publishEntityState(entity, this.state.get(entity), 'publishCached');
}
@@ -247,7 +252,7 @@ export class Controller {
await this.zigbee.stop();
logger.info('Stopped Zigbee2MQTT');
} catch (error) {
logger.error('Failed to stop Zigbee2MQTT');
logger.error(`Failed to stop Zigbee2MQTT (${error.message})`);
code = 1;
}
+2 -2
View File
@@ -41,10 +41,10 @@ export default class EventBus {
this.on('deviceRenamed', callback, key);
}
public emitDeviceRemoved(data: eventdata.DeviceRemoved): void {
public emitEntityRemoved(data: eventdata.EntityRemoved): void {
this.emitter.emit('deviceRemoved', data);
}
public onDeviceRemoved(key: ListenerKey, callback: (data: eventdata.DeviceRemoved) => void): void {
public onEntityRemoved(key: ListenerKey, callback: (data: eventdata.EntityRemoved) => void): void {
this.on('deviceRemoved', callback, key);
}
+8 -6
View File
@@ -66,7 +66,9 @@ export default class Availability extends Extension {
private removeFromPingQueue(device: Device): void {
const index = this.pingQueue.findIndex((d) => d.ieeeAddr === device.ieeeAddr);
index != -1 && this.pingQueue.splice(index, 1);
if (index != -1) {
this.pingQueue.splice(index, 1);
}
}
private async pingQueueExecuteNext(): Promise<void> {
@@ -128,7 +130,7 @@ export default class Availability extends Extension {
}
});
this.eventBus.onDeviceRemoved(this, (data) => clearTimeout(this.timers[data.ieeeAddr]));
this.eventBus.onEntityRemoved(this, (data) => data.type == 'device' && clearTimeout(this.timers[data.id]));
this.eventBus.onDeviceLeave(this, (data) => clearTimeout(this.timers[data.ieeeAddr]));
this.eventBus.onDeviceAnnounce(this, (data) => this.retrieveState(data.device));
this.eventBus.onLastSeenChanged(this, this.onLastSeenChanged);
@@ -138,7 +140,7 @@ export default class Availability extends Extension {
await this.publishAvailabilityForAllEntities();
// Start availability for the devices
for (const device of this.zigbee.devices(false)) {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
if (utils.isAvailabilityEnabledForEntity(device, settings.get())) {
this.resetTimer(device);
@@ -151,7 +153,7 @@ export default class Availability extends Extension {
}
@bind private async publishAvailabilityForAllEntities(): Promise<void> {
for (const entity of [...this.zigbee.devices(false), ...this.zigbee.groups()]) {
for (const entity of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
if (utils.isAvailabilityEnabledForEntity(entity, settings.get())) {
await this.publishAvailability(entity, true, false, true);
}
@@ -185,7 +187,7 @@ export default class Availability extends Extension {
await this.mqtt.publish(topic, payload, {retain: true, qos: 1});
if (!skipGroups && entity.isDevice()) {
for (const group of this.zigbee.groups()) {
for (const group of this.zigbee.groupsIterator()) {
if (group.hasMember(entity) && utils.isAvailabilityEnabledForEntity(group, settings.get())) {
await this.publishAvailability(group, false, forcePublish);
}
@@ -228,7 +230,7 @@ export default class Availability extends Extension {
continue;
}
const converter = device.definition.toZigbee.find((c) => c.key.find((k) => item.keys.includes(k)));
const converter = device.definition.toZigbee.find((c) => !c.key || c.key.find((k) => item.keys.includes(k)));
const options: KeyValue = device.options;
const state = this.state.get(device);
const meta: zhc.Tz.Meta = {
+7 -5
View File
@@ -363,7 +363,7 @@ export default class Bind extends Extension {
if (data.action === 'add') {
const bindsToGroup: zh.Bind[] = [];
for (const device of this.zigbee.devices(false)) {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
for (const endpoint of device.zh.endpoints) {
for (const bind of endpoint.binds) {
if (bind.target === data.group.zh) {
@@ -429,7 +429,7 @@ export default class Bind extends Extension {
await endpoint.configureReporting(bind.cluster.name, items);
logger.info(`Successfully setup reporting for '${entity}' cluster '${bind.cluster.name}'`);
} catch (error) {
logger.warning(`Failed to setup reporting for '${entity}' cluster '${bind.cluster.name}'`);
logger.warning(`Failed to setup reporting for '${entity}' cluster '${bind.cluster.name}' (${error.message})`);
}
}
}
@@ -443,7 +443,7 @@ export default class Bind extends Extension {
const endpoints = utils.isEndpoint(target) ? [target] : target.members;
const allBinds: zh.Bind[] = [];
for (const device of this.zigbee.devices(false)) {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
for (const endpoint of device.zh.endpoints) {
for (const bind of endpoint.binds) {
allBinds.push(bind);
@@ -489,7 +489,7 @@ export default class Bind extends Extension {
await endpoint.configureReporting(cluster, items);
logger.info(`Successfully disabled reporting for '${entity}' cluster '${cluster}'`);
} catch (error) {
logger.warning(`Failed to disable reporting for '${entity}' cluster '${cluster}'`);
logger.warning(`Failed to disable reporting for '${entity}' cluster '${cluster}' (${error.message})`);
}
}
@@ -555,7 +555,9 @@ export default class Bind extends Extension {
try {
await endpoint.read(poll.read.cluster, readAttrs);
} catch (error) {
logger.error(`Failed to poll ${readAttrs} from ${this.zigbee.resolveEntity(endpoint.getDevice()).name}`);
logger.error(
`Failed to poll ${readAttrs} from ${this.zigbee.resolveEntity(endpoint.getDevice()).name} (${error.message})`,
);
}
}, 1000);
}
+42 -24
View File
@@ -618,7 +618,6 @@ export default class Bridge extends Extension {
try {
logger.info(`Removing ${entityType} '${entity.name}'${blockForceLog}`);
const ieeeAddr = entity.isDevice() && entity.ieeeAddr;
const name = entity.name;
if (entity instanceof Device) {
@@ -641,7 +640,9 @@ export default class Bridge extends Extension {
// Fire event
if (entity instanceof Device) {
this.eventBus.emitDeviceRemoved({ieeeAddr, name});
this.eventBus.emitEntityRemoved({id: entityID, name, type: 'device'});
} else {
this.eventBus.emitEntityRemoved({id: entityID, name, type: 'group'});
}
// Remove from configuration.yaml
@@ -686,7 +687,9 @@ export default class Bridge extends Extension {
const config = objectAssignDeep({}, settings.get());
delete config.advanced.network_key;
delete config.mqtt.password;
config.frontend && delete config.frontend.auth_token;
if (config.frontend) {
delete config.frontend.auth_token;
}
const payload = {
version: this.zigbee2mqttVersion.version,
commit: this.zigbee2mqttVersion.commitHash,
@@ -722,8 +725,12 @@ export default class Bridge extends Extension {
scenes: Scene[];
}
const devices = this.zigbee.devices().map((device) => {
// XXX: definition<>DefinitionPayload don't match to use `Device[]` type here
const devices: KeyValue[] = [];
for (const device of this.zigbee.devicesIterator()) {
const endpoints: {[s: number]: Data} = {};
for (const endpoint of device.zh.endpoints) {
const data: Data = {
scenes: utils.getScenes(endpoint),
@@ -755,7 +762,7 @@ export default class Bridge extends Extension {
endpoints[endpoint.ID] = data;
}
return {
devices.push({
ieee_address: device.ieeeAddr,
type: device.zh.type,
network_address: device.zh.networkAddress,
@@ -772,24 +779,32 @@ export default class Bridge extends Extension {
interview_completed: device.zh.interviewCompleted,
manufacturer: device.zh.manufacturerName,
endpoints,
};
});
});
}
await this.mqtt.publish('bridge/devices', stringify(devices), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
}
async publishGroups(): Promise<void> {
const groups = this.zigbee.groups().map((g) => {
return {
id: g.ID,
friendly_name: g.ID === 901 ? 'default_bind_group' : g.name,
description: g.options.description,
scenes: utils.getScenes(g.zh),
members: g.zh.members.map((e) => {
return {ieee_address: e.getDevice().ieeeAddr, endpoint: e.ID};
}),
};
});
// XXX: id<>ID can't use `Group[]` type
const groups: KeyValue[] = [];
for (const group of this.zigbee.groupsIterator()) {
const members = [];
for (const member of group.zh.members) {
members.push({ieee_address: member.getDevice().ieeeAddr, endpoint: member.ID});
}
groups.push({
id: group.ID,
friendly_name: group.ID === 901 ? 'default_bind_group' : group.name,
description: group.options.description,
scenes: utils.getScenes(group.zh),
members,
});
}
await this.mqtt.publish('bridge/groups', stringify(groups), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
}
@@ -804,20 +819,23 @@ export default class Bridge extends Extension {
custom_clusters: {},
};
for (const device of this.zigbee.devices()) {
if (Object.keys(device.customClusters).length !== 0) {
data.custom_clusters[device.ieeeAddr] = device.customClusters;
}
for (const device of this.zigbee.devicesIterator((d) => !utils.objectIsEmpty(d.customClusters))) {
data.custom_clusters[device.ieeeAddr] = device.customClusters;
}
await this.mqtt.publish('bridge/definitions', stringify(data), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
}
getDefinitionPayload(device: Device): DefinitionPayload {
if (!device.definition) return null;
getDefinitionPayload(device: Device): DefinitionPayload | null {
if (!device.definition) {
return null;
}
// TODO: better typing to avoid @ts-expect-error
// @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 -3
View File
@@ -68,9 +68,8 @@ export default class Configure extends Extension {
setImmediate(async () => {
// Only configure routers on startup, end devices are likely sleeping and
// will reconfigure once they send a message
for (const device of this.zigbee.devices(false).filter((d) => d.zh.type === 'Router')) {
// Sleep 10 seconds between configuring on startup to not DDoS the coordinator
// when many devices have to be configured.
for (const device of this.zigbee.devicesIterator((d) => d.type === 'Router')) {
// Sleep 10 seconds between configuring on startup to not DDoS the coordinator when many devices have to be configured.
await utils.sleep(10);
await this.configure(device, 'started');
}
+1
View File
@@ -25,6 +25,7 @@ export default class ExternalConverters extends Extension {
delete toAdd['homeassistant'];
zhc.addDefinition(toAdd);
}
logger.info(`Loaded external converter '${file}'`);
} catch (error) {
logger.error(`Failed to load external converter file '${file}' (${error.message})`);
logger.error(
+1 -1
View File
@@ -139,7 +139,7 @@ export default class Frontend extends Extension {
}
}
for (const device of this.zigbee.devices(false)) {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
const payload = this.state.get(device);
const lastSeen = settings.get().advanced.last_seen;
/* istanbul ignore if */
+18 -11
View File
@@ -51,7 +51,6 @@ export default class Groups extends Extension {
private async syncGroupsWithSettings(): Promise<void> {
const settingsGroups = settings.getGroups();
const zigbeeGroups = this.zigbee.groups();
const addRemoveFromGroup = async (
action: 'add' | 'remove',
@@ -76,7 +75,7 @@ export default class Groups extends Extension {
for (const settingGroup of settingsGroups) {
const groupID = settingGroup.ID;
const zigbeeGroup = zigbeeGroups.find((g) => g.ID === groupID) || this.zigbee.createGroup(groupID);
const zigbeeGroup = this.zigbee.groupsIterator((g) => g.groupID === groupID).next().value || this.zigbee.createGroup(groupID);
const settingsEndpoints: zh.Endpoint[] = [];
for (const d of settingGroup.devices) {
@@ -113,13 +112,11 @@ export default class Groups extends Extension {
}
}
for (const zigbeeGroup of zigbeeGroups) {
if (!settingsGroups.some((g) => g.ID === zigbeeGroup.ID)) {
for (const endpoint of zigbeeGroup.zh.members) {
const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr).friendly_name;
for (const zigbeeGroup of this.zigbee.groupsIterator((zg) => !settingsGroups.some((sg) => sg.ID === zg.groupID))) {
for (const endpoint of zigbeeGroup.zh.members) {
const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr).friendly_name;
await addRemoveFromGroup('remove', deviceName, zigbeeGroup.ID, endpoint, zigbeeGroup);
}
await addRemoveFromGroup('remove', deviceName, zigbeeGroup.ID, endpoint, zigbeeGroup);
}
}
}
@@ -153,7 +150,13 @@ export default class Groups extends Extension {
if (payloadKeys.length) {
const entity = data.entity;
const groups = this.zigbee.groups().filter((g) => g.options && (g.options.optimistic == undefined || g.options.optimistic));
const groups = [];
for (const group of this.zigbee.groupsIterator()) {
if (group.options && (group.options.optimistic == undefined || group.options.optimistic)) {
groups.push(group);
}
}
if (entity instanceof Device) {
for (const group of groups) {
@@ -359,7 +362,7 @@ export default class Groups extends Extension {
resolvedEntityEndpoint,
} = parsed;
let error = parsed.error;
let changedGroups: Group[] = [];
const changedGroups: Group[] = [];
if (!error) {
try {
@@ -406,7 +409,11 @@ export default class Groups extends Extension {
} else {
// remove_all
logger.info(`Removing '${resolvedEntityDevice.name}' from all groups`);
changedGroups = this.zigbee.groups().filter((g) => g.zh.members.includes(resolvedEntityEndpoint));
for (const group of this.zigbee.groupsIterator((g) => g.members.includes(resolvedEntityEndpoint))) {
changedGroups.push(group);
}
await resolvedEntityEndpoint.removeFromAllGroups();
for (const settingsGroup of settings.getGroups()) {
+26 -14
View File
@@ -201,7 +201,7 @@ export default class HomeAssistant extends Extension {
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.onEntityRemoved(this, this.onEntityRemoved);
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
this.eventBus.onEntityRenamed(this, this.onEntityRenamed);
this.eventBus.onPublishEntityState(this, this.onPublishEntityState);
@@ -225,7 +225,9 @@ export default class HomeAssistant extends Extension {
const discoverWait = 5;
// Discover with `published = false`, this will populate `this.discovered` without publishing the discoveries.
// This is needed for clearing outdated entries in `this.onMQTTMessage()`
for (const e of [this.bridge, ...this.zigbee.devices(false), ...this.zigbee.groups()]) {
await this.discover(this.bridge, false);
for (const e of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
await this.discover(e, false);
}
@@ -235,7 +237,9 @@ export default class HomeAssistant extends Extension {
this.mqtt.unsubscribe(`${this.discoveryTopic}/#`);
logger.debug(`Discovering entities to Home Assistant`);
for (const e of [this.bridge, ...this.zigbee.devices(false), ...this.zigbee.groups()]) {
await this.discover(this.bridge);
for (const e of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
await this.discover(e);
}
}, utils.seconds(discoverWait));
@@ -244,8 +248,8 @@ export default class HomeAssistant extends Extension {
this.eventBus.emitPublishAvailability();
}
private getDiscovered(entity: Device | Group | Bridge | string): Discovered {
const ID = typeof entity === 'string' ? entity : entity.ID;
private getDiscovered(entity: Device | Group | Bridge | string | number): Discovered {
const ID = typeof entity === 'string' || typeof entity === 'number' ? entity : entity.ID;
if (!(ID in this.discovered)) {
this.discovered[ID] = {messages: {}, triggers: new Set(), mockProperties: new Set(), discovered: false};
}
@@ -596,8 +600,8 @@ export default class HomeAssistant extends Extension {
// If curtains do not have `running`, `motor_state` or `moving` properties.
if (!discoveryEntry.discovery_payload.value_template) {
(discoveryEntry.discovery_payload.value_template = `{{ value_json.${featurePropertyWithoutEndpoint(state)} }}`),
(discoveryEntry.discovery_payload.state_open = 'OPEN');
discoveryEntry.discovery_payload.value_template = `{{ value_json.${featurePropertyWithoutEndpoint(state)} }}`;
discoveryEntry.discovery_payload.state_open = 'OPEN';
discoveryEntry.discovery_payload.state_closed = 'CLOSE';
discoveryEntry.discovery_payload.state_stopped = 'STOP';
}
@@ -810,7 +814,7 @@ export default class HomeAssistant extends Extension {
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'},
battery: {device_class: 'battery', state_class: 'measurement'},
battery2: {device_class: 'battery', entity_category: 'diagnostic', state_class: 'measurement'},
battery_voltage: {device_class: 'voltage', entity_category: 'diagnostic', state_class: 'measurement', enabled_by_default: true},
boost_heating_countdown: {device_class: 'duration'},
@@ -1014,7 +1018,7 @@ export default class HomeAssistant extends Extension {
};
if (lookup[firstExpose.name]?.device_class === 'temperature') {
discoveryEntry.discovery_payload.device_class == lookup[firstExpose.name]?.device_class;
discoveryEntry.discovery_payload.device_class = lookup[firstExpose.name]?.device_class;
} else {
delete discoveryEntry.discovery_payload.device_class;
}
@@ -1175,6 +1179,14 @@ export default class HomeAssistant extends Extension {
throw new Error(`Unsupported exposes type: '${firstExpose.type}'`);
}
// Exposes with category 'config' or 'diagnostic' are always added to the respective category.
// This takes precedence over definitions in this file.
if (firstExpose.category === 'config') {
discoveryEntries.forEach((d) => (d.discovery_payload.entity_category = 'config'));
} else if (firstExpose.category === 'diagnostic') {
discoveryEntries.forEach((d) => (d.discovery_payload.entity_category = 'diagnostic'));
}
discoveryEntries.forEach((d) => {
// If a sensor has entity category `config`, then change
// it to `diagnostic`. Sensors have no input, so can't be configured.
@@ -1194,15 +1206,15 @@ export default class HomeAssistant extends Extension {
return discoveryEntries;
}
@bind async onDeviceRemoved(data: eventdata.DeviceRemoved): Promise<void> {
@bind async onEntityRemoved(data: eventdata.EntityRemoved): Promise<void> {
logger.debug(`Clearing Home Assistant discovery for '${data.name}'`);
const discovered = this.getDiscovered(data.ieeeAddr);
const discovered = this.getDiscovered(data.id);
for (const topic of Object.keys(discovered.messages)) {
await this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false);
}
delete this.discovered[data.ieeeAddr];
delete this.discovered[data.id];
}
@bind async onGroupMembersChanged(data: eventdata.GroupMembersChanged): Promise<void> {
@@ -1737,7 +1749,7 @@ export default class HomeAssistant extends Extension {
if (!isDeviceAutomation && (!message.availability || !message.availability[0].topic.startsWith(baseTopic))) {
return;
}
} catch (e) {
} catch {
return;
}
@@ -1772,7 +1784,7 @@ export default class HomeAssistant extends Extension {
} else if ((data.topic === this.statusTopic || data.topic === defaultStatusTopic) && data.message.toLowerCase() === 'online') {
const timer = setTimeout(async () => {
// Publish all device states.
for (const entity of [...this.zigbee.devices(false), ...this.zigbee.groups()]) {
for (const entity of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
if (this.state.exists(entity)) {
await this.publishEntityState(entity, this.state.get(entity), 'publishCached');
}
+11 -9
View File
@@ -61,7 +61,7 @@ export default class BridgeLegacy extends Extension {
let json = null;
try {
json = JSON.parse(message);
} catch (e) {
} catch {
logger.error('Failed to parse message as JSON');
return;
}
@@ -86,7 +86,7 @@ export default class BridgeLegacy extends Extension {
try {
await this.zigbee.reset('soft');
logger.info('Soft reset ZNP');
} catch (error) {
} catch {
logger.error('Soft reset failed');
}
}
@@ -127,7 +127,9 @@ export default class BridgeLegacy extends Extension {
@bind async devices(topic: string): Promise<void> {
const coordinator = await this.zigbee.getCoordinatorVersion();
const devices = this.zigbee.devices().map((device) => {
const devices: KeyValue[] = [];
for (const device of this.zigbee.devicesIterator()) {
const payload: KeyValue = {
ieeeAddr: device.ieeeAddr,
type: device.zh.type,
@@ -155,8 +157,8 @@ export default class BridgeLegacy extends Extension {
payload.lastSeen = Date.now();
}
return payload;
});
devices.push(payload);
}
if (topic.split('/').pop() == 'get') {
await this.mqtt.publish(`bridge/config/devices`, stringify(devices), {}, settings.get().mqtt.base_topic, false, false);
@@ -179,7 +181,7 @@ export default class BridgeLegacy extends Extension {
let json = null;
try {
json = JSON.parse(message);
} catch (e) {
} catch {
logger.error(invalid);
return;
}
@@ -213,7 +215,7 @@ export default class BridgeLegacy extends Extension {
}
await this.mqtt.publish('bridge/log', stringify({type: `${isGroup ? 'group' : 'device'}_renamed`, message: {from, to}}));
} catch (error) {
} catch {
logger.error(`Failed to rename - ${from} to ${to}`);
}
}
@@ -231,7 +233,7 @@ export default class BridgeLegacy extends Extension {
if (json.hasOwnProperty('friendly_name')) {
name = json.friendly_name;
}
} catch (e) {
} catch {
// just friendly_name
name = message;
}
@@ -295,7 +297,7 @@ export default class BridgeLegacy extends Extension {
const cleanup = async (): Promise<void> => {
// Fire event
this.eventBus.emitDeviceRemoved({ieeeAddr, name});
this.eventBus.emitEntityRemoved({id: ieeeAddr, name: name, type: 'device'});
// Remove from configuration.yaml
settings.removeDevice(entity.ieeeAddr);
+7 -4
View File
@@ -2,6 +2,7 @@ import * as zhc from 'zigbee-herdsman-converters';
import logger from '../../util/logger';
import * as settings from '../../util/settings';
import utils from '../../util/utils';
import Extension from '../extension';
const defaultConfiguration = {
@@ -106,9 +107,11 @@ export default class Report extends Extension {
}
}
this.enabled
? await ep.bind(cluster, this.zigbee.firstCoordinatorEndpoint())
: await ep.unbind(cluster, this.zigbee.firstCoordinatorEndpoint());
if (this.enabled) {
await ep.bind(cluster, this.zigbee.firstCoordinatorEndpoint());
} else {
await ep.unbind(cluster, this.zigbee.firstCoordinatorEndpoint());
}
await ep.configureReporting(cluster, items);
logger.info(`Successfully ${term2} reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`);
@@ -176,7 +179,7 @@ export default class Report extends Extension {
}
override async start(): Promise<void> {
for (const device of this.zigbee.devices(false)) {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
if (this.shouldSetupReporting(device, null)) {
await this.setupReporting(device);
}
+2 -2
View File
@@ -45,7 +45,7 @@ export default class SoftReset extends Extension {
await this.zigbee.reset('soft');
logger.warning('Soft reset ZNP due to timeout');
} catch (error) {
logger.warning('Soft reset failed, trying stop/start');
logger.warning(`Soft reset failed, trying stop/start (${error.message})`);
await this.zigbee.stop();
logger.warning('Zigbee stopped');
@@ -53,7 +53,7 @@ export default class SoftReset extends Extension {
try {
await this.zigbee.start();
} catch (error) {
logger.error('Failed to restart!');
logger.error(`Failed to restart! (${error.message})`);
}
}
+42 -26
View File
@@ -210,31 +210,31 @@ export default class NetworkMap extends Extension {
async networkScan(includeRoutes: boolean): Promise<Topology> {
logger.info(`Starting network scan (includeRoutes '${includeRoutes}')`);
const devices = this.zigbee.devices().filter((d) => d.zh.type !== 'GreenPower' && !d.options.disabled);
const lqis: Map<Device, zh.LQI> = new Map();
const routingTables: Map<Device, zh.RoutingTable> = new Map();
const failed: Map<Device, string[]> = new Map();
const requestWithRetry = async <T>(request: () => Promise<T>): Promise<T> => {
try {
const result = await request();
return result;
} catch {
// Network is possibly congested, sleep 5 seconds to let the network settle.
await utils.sleep(5);
return request();
}
};
for (const device of this.zigbee.devicesIterator((d) => d.type !== 'GreenPower' && d.type !== 'EndDevice')) {
if (device.options.disabled) {
continue;
}
for (const device of devices.filter((d) => d.zh.type != 'EndDevice')) {
failed.set(device, []);
await utils.sleep(1); // sleep 1 second between each scan to reduce stress on network.
const doRequest = async <T>(request: () => Promise<T>, firstAttempt = true): Promise<T> => {
try {
return await request();
} catch (error) {
if (!firstAttempt) {
throw error;
} else {
// Network is possibly congested, sleep 5 seconds to let the network settle.
await utils.sleep(5);
return doRequest(request, false);
}
}
};
try {
const result = await doRequest<zh.LQI>(async () => device.zh.lqi());
const result = await requestWithRetry<zh.LQI>(async () => device.zh.lqi());
lqis.set(device, result);
logger.debug(`LQI succeeded for '${device.name}'`);
} catch (error) {
@@ -245,12 +245,13 @@ export default class NetworkMap extends Extension {
if (includeRoutes) {
try {
const result = await doRequest(async () => device.zh.routingTable());
const result = await requestWithRetry<zh.RoutingTable>(async () => device.zh.routingTable());
routingTables.set(device, result);
logger.debug(`Routing table succeeded for '${device.name}'`);
} catch (error) {
failed.get(device).push('routingTable');
logger.error(`Failed to execute routing table for '${device.name}'`);
logger.debug(error.stack);
}
}
}
@@ -258,8 +259,14 @@ export default class NetworkMap extends Extension {
logger.info(`Network scan finished`);
const topology: Topology = {nodes: [], links: []};
// Add nodes
for (const device of devices) {
// XXX: display GP/disabled devices in the map, better feedback than just hiding them?
for (const device of this.zigbee.devicesIterator((d) => d.type !== 'GreenPower')) {
if (device.options.disabled) {
continue;
}
// Add nodes
const definition = device.definition
? {
model: device.definition.model,
@@ -289,7 +296,7 @@ export default class NetworkMap extends Extension {
}
// Add links
lqis.forEach((lqi, device) => {
for (const [device, lqi] of lqis) {
for (const neighbor of lqi.neighbors) {
if (neighbor.relationship > 3) {
// Relationship is not active, skip it
@@ -298,9 +305,13 @@ export default class NetworkMap extends Extension {
// Some Xiaomi devices return 0x00 as the neighbor ieeeAddr (obviously not correct).
// Determine the correct ieeeAddr based on the networkAddress.
const neighborDevice = this.zigbee.deviceByNetworkAddress(neighbor.networkAddress);
if (neighbor.ieeeAddr === '0x0000000000000000' && neighborDevice) {
neighbor.ieeeAddr = neighborDevice.ieeeAddr;
if (neighbor.ieeeAddr === '0x0000000000000000') {
const neighborDevice = this.zigbee.deviceByNetworkAddress(neighbor.networkAddress);
/* istanbul ignore else */
if (neighborDevice) {
neighbor.ieeeAddr = neighborDevice.ieeeAddr;
}
}
const link: Link = {
@@ -318,13 +329,18 @@ export default class NetworkMap extends Extension {
};
const routingTable = routingTables.get(device);
if (routingTable) {
link.routes = routingTable.table.filter((t) => t.status === 'ACTIVE' && t.nextHop === neighbor.networkAddress);
for (const entry of routingTable.table) {
if (entry.status === 'ACTIVE' && entry.nextHop === neighbor.networkAddress) {
link.routes.push(entry);
}
}
}
topology.links.push(link);
}
});
}
return topology;
}
+4 -2
View File
@@ -1,5 +1,6 @@
import * as zhc from 'zigbee-herdsman-converters';
import utils from '../util/utils';
import Extension from './extension';
/**
@@ -7,7 +8,7 @@ import Extension from './extension';
*/
export default class OnEvent extends Extension {
override async start(): Promise<void> {
for (const device of this.zigbee.devices(false)) {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
await this.callOnEvent(device, 'start', {});
}
@@ -31,7 +32,8 @@ export default class OnEvent extends Extension {
override async stop(): Promise<void> {
await super.stop();
for (const device of this.zigbee.devices(false)) {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
await this.callOnEvent(device, 'stop', {});
}
}
+9 -6
View File
@@ -16,7 +16,7 @@ function isValidUrl(url: string): boolean {
let parsed;
try {
parsed = URI.parse(url);
} catch (_) {
} catch {
// istanbul ignore next
return false;
}
@@ -65,10 +65,10 @@ export default class OTAUpdate extends Extension {
// In order to support local firmware files we need to let zigbeeOTA know where the data directory is
zhc.ota.setDataDir(dataDir.getPath());
// In case Zigbee2MQTT is restared during an update, progress and remaining values are still in state.
// remove them.
for (const device of this.zigbee.devices(false)) {
// In case Zigbee2MQTT is restared during an update, progress and remaining values are still in state, remove them.
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
this.removeProgressAndRemainingFromState(device);
// Reset update state, e.g. when Z2M restarted during update.
if (this.state.get(device).update?.state === 'updating') {
this.state.get(device).update.state = 'available';
@@ -104,6 +104,7 @@ export default class OTAUpdate extends Extension {
} catch (e) {
supportsOTA = false;
logger.debug(`Failed to check if update available for '${data.device.name}' (${e.message})`);
logger.debug(e.stack);
}
const payload = this.getEntityPublishPayload(data.device, availableResult ?? 'idle');
@@ -138,7 +139,7 @@ export default class OTAUpdate extends Extension {
const endpoint = device.zh.endpoints.find((e) => e.supportsInputCluster('genBasic'));
const result = await endpoint.read('genBasic', ['dateCode', 'swBuildId'], {sendPolicy});
return {softwareBuildID: result.swBuildId, dateCode: result.dateCode};
} catch (e) {
} catch {
return null;
}
}
@@ -319,7 +320,9 @@ export default class OTAUpdate extends Extension {
if (error) {
logger.error(error);
errorStack && logger.debug(errorStack);
if (errorStack) {
logger.debug(errorStack);
}
}
}
}
+6 -5
View File
@@ -74,13 +74,13 @@ export default class Publish extends Extension {
if (parsedTopic.attribute) {
try {
return {[parsedTopic.attribute]: JSON.parse(data.message)};
} catch (e) {
} catch {
return {[parsedTopic.attribute]: data.message};
}
} else {
try {
return JSON.parse(data.message);
} catch (e) {
} catch {
if (stateValues.includes(data.message.toLowerCase())) {
return {state: data.message};
} else {
@@ -229,7 +229,8 @@ export default class Publish extends Extension {
if (!usedConverters.hasOwnProperty(endpointOrGroupID)) usedConverters[endpointOrGroupID] = [];
/* istanbul ignore next */
const converter = converters.find((c) => c.key.includes(key) && (!c.endpoint || c.endpoint == endpointName));
// Match any key if the toZigbee converter defines no key.
const converter = converters.find((c) => (!c.key || c.key.includes(key)) && (!c.endpoint || c.endpoint == endpointName));
if (parsedTopic.type === 'set' && usedConverters[endpointOrGroupID].includes(converter)) {
// Use a converter for set only once
@@ -317,12 +318,12 @@ export default class Publish extends Extension {
}
for (const [ID, payload] of Object.entries(toPublish)) {
if (Object.keys(payload).length != 0) {
if (!utils.objectIsEmpty(payload)) {
await this.publishEntityState(toPublishEntity[ID], payload);
}
}
const scenesChanged = Object.values(usedConverters).some((cl) => cl.some((c) => c.key.some((k) => sceneConverterKeys.includes(k))));
const scenesChanged = Object.values(usedConverters).some((cl) => cl.some((c) => c.key?.some((k) => sceneConverterKeys.includes(k))));
if (scenesChanged) {
this.eventBus.emitScenesChanged({entity: re});
}
+1 -1
View File
@@ -163,7 +163,7 @@ export default class Receive extends Extension {
}
}
if (Object.keys(payload).length) {
if (!utils.objectIsEmpty(payload)) {
await publish(payload);
} else {
await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, settings.get(), true, this.publishEntityState);
+2 -2
View File
@@ -65,8 +65,8 @@ class State {
try {
this.state = JSON.parse(fs.readFileSync(this.file, 'utf8'));
logger.debug(`Loaded state from file ${this.file}`);
} catch (e) {
logger.debug(`Failed to load state from file ${this.file} (corrupt file?)`);
} catch (error) {
logger.debug(`Failed to load state from file ${this.file} (corrupt file?) (${error.message})`);
}
} else {
logger.debug(`Can't load state from file ${this.file} (doesn't exist)`);
+1 -1
View File
@@ -73,7 +73,7 @@ declare global {
namespace eventdata {
type EntityRenamed = {entity: Device | Group; homeAssisantRename: boolean; from: string; to: string};
type DeviceRemoved = {ieeeAddr: string; name: string};
type EntityRemoved = {id: number | string; name: string; type: 'device' | 'group'};
type MQTTMessage = {topic: string; message: string};
type MQTTMessagePublished = {topic: string; payload: string; options: {retain: boolean; qos: number}};
type StateChange = {
+7 -1
View File
@@ -227,7 +227,7 @@
},
"adapter": {
"type": ["string"],
"enum": ["deconz", "zstack", "zigate", "ezsp", "auto", "ember"],
"enum": ["deconz", "zstack", "zigate", "ezsp", "auto", "ember", "zboss"],
"title": "Adapter",
"default": "auto",
"requiresRestart": true,
@@ -643,12 +643,16 @@
"title": "Adapter concurrency",
"requiresRestart": true,
"type": ["number", "null"],
"minimum": 1,
"maximum": 64,
"description": "Adapter concurrency (e.g. 2 for CC2531 or 16 for CC26X2R1) (default: null, uses recommended value)"
},
"adapter_delay": {
"type": ["number", "null"],
"requiresRestart": true,
"title": "Adapter delay",
"minimum": 0,
"maximum": 1000,
"description": "Adapter delay"
},
"cache_state": {
@@ -711,6 +715,8 @@
"type": ["number", "null"],
"title": "Transmit power",
"requiresRestart": true,
"minimum": -128,
"maximum": 127,
"description": "Transmit power of adapter, only available for Z-Stack (CC253*/CC2652/CC1352) adapters, CC2652 = 5dbm, CC1352 max is = 20dbm (5dbm default)"
},
"output": {
+31 -25
View File
@@ -7,7 +7,7 @@ import schemaJson from './settings.schema.json';
import utils from './utils';
import yaml from './yaml';
export let schema = schemaJson;
// @ts-ignore
// @ts-expect-error
schema = {};
objectAssignDeep(schema, schemaJson);
@@ -140,16 +140,16 @@ function loadSettingsWithDefaults(): void {
'homeassistant_legacy_entity_attributes',
'homeassistant_status_topic',
]) {
// @ts-ignore
// @ts-expect-error
if (_settingsWithDefaults.advanced[key] !== undefined) {
// @ts-ignore
// @ts-expect-error
sLegacy[key.replace('homeassistant_', '')] = _settingsWithDefaults.advanced[key];
}
}
}
const s = typeof _settingsWithDefaults.homeassistant === 'object' ? _settingsWithDefaults.homeassistant : {};
// @ts-ignore
// @ts-expect-error
_settingsWithDefaults.homeassistant = {};
objectAssignDeep(_settingsWithDefaults.homeassistant, defaults, sLegacy, s);
}
@@ -157,7 +157,7 @@ function loadSettingsWithDefaults(): void {
if (_settingsWithDefaults.availability || _settingsWithDefaults.advanced?.availability_timeout) {
const defaults = {};
const s = typeof _settingsWithDefaults.availability === 'object' ? _settingsWithDefaults.availability : {};
// @ts-ignore
// @ts-expect-error
_settingsWithDefaults.availability = {};
objectAssignDeep(_settingsWithDefaults.availability, defaults, s);
}
@@ -165,35 +165,34 @@ function loadSettingsWithDefaults(): void {
if (_settingsWithDefaults.frontend) {
const defaults = {port: 8080, auth_token: false};
const s = typeof _settingsWithDefaults.frontend === 'object' ? _settingsWithDefaults.frontend : {};
// @ts-ignore
_settingsWithDefaults.frontend = {};
objectAssignDeep(_settingsWithDefaults.frontend, defaults, s);
}
if (_settings.advanced?.hasOwnProperty('baudrate') && _settings.serial?.baudrate == null) {
// @ts-ignore
// @ts-expect-error
_settingsWithDefaults.serial.baudrate = _settings.advanced.baudrate;
}
if (_settings.advanced?.hasOwnProperty('rtscts') && _settings.serial?.rtscts == null) {
// @ts-ignore
// @ts-expect-error
_settingsWithDefaults.serial.rtscts = _settings.advanced.rtscts;
}
if (_settings.advanced?.hasOwnProperty('ikea_ota_use_test_url') && _settings.ota?.ikea_ota_use_test_url == null) {
// @ts-ignore
// @ts-expect-error
_settingsWithDefaults.ota.ikea_ota_use_test_url = _settings.advanced.ikea_ota_use_test_url;
}
// @ts-ignore
// @ts-expect-error
if (_settings.experimental?.hasOwnProperty('transmit_power') && _settings.advanced?.transmit_power == null) {
// @ts-ignore
// @ts-expect-error
_settingsWithDefaults.advanced.transmit_power = _settings.experimental.transmit_power;
}
// @ts-ignore
// @ts-expect-error
if (_settings.experimental?.hasOwnProperty('output') && _settings.advanced?.output == null) {
// @ts-ignore
// @ts-expect-error
_settingsWithDefaults.advanced.output = _settings.experimental.output;
}
@@ -201,10 +200,17 @@ function loadSettingsWithDefaults(): void {
_settingsWithDefaults.advanced.log_level = 'warning';
}
// @ts-ignore
_settingsWithDefaults.ban && _settingsWithDefaults.blocklist.push(..._settingsWithDefaults.ban);
// @ts-ignore
_settingsWithDefaults.whitelist && _settingsWithDefaults.passlist.push(..._settingsWithDefaults.whitelist);
// @ts-expect-error
if (_settingsWithDefaults.ban) {
// @ts-expect-error
_settingsWithDefaults.blocklist.push(..._settingsWithDefaults.ban);
}
// @ts-expect-error
if (_settingsWithDefaults.whitelist) {
// @ts-expect-error
_settingsWithDefaults.passlist.push(..._settingsWithDefaults.whitelist);
}
}
function parseValueRef(text: string): {filename: string; key: string} | null {
@@ -257,7 +263,7 @@ function write(): void {
.filter((f: string, i: number) => i !== 0)
.map((f: string) => yaml.readIfExists(data.joinPath(f), {}))
.map((c: KeyValue) => Object.keys(c))
// @ts-ignore
// @ts-expect-error
.forEach((k: string) => delete content[k]);
}
@@ -395,12 +401,12 @@ function read(): Settings {
// Read devices/groups configuration from separate file if specified.
const readDevicesOrGroups = (type: 'devices' | 'groups'): void => {
if (typeof s[type] === 'string' || (Array.isArray(s[type]) && Array(s[type]).length > 0)) {
/* eslint-disable-line */ // @ts-ignore
/* eslint-disable-line */
const files: string[] = Array.isArray(s[type]) ? s[type] : [s[type]];
s[type] = {};
for (const file of files) {
const content = yaml.readIfExists(data.joinPath(file), {});
/* eslint-disable-line */ // @ts-ignore
/* eslint-disable-line */ // @ts-expect-error
s[type] = objectAssignDeep.noMutate(s[type], content);
}
}
@@ -422,20 +428,20 @@ function applyEnvironmentVariables(settings: Partial<Settings>): void {
const envVariableName = `ZIGBEE2MQTT_CONFIG_${envPart}${key}`.toUpperCase();
if (process.env[envVariableName]) {
const setting = path.reduce((acc, val) => {
/* eslint-disable-line */ // @ts-ignore
/* eslint-disable-line */ // @ts-expect-error
acc[val] = acc[val] || {};
/* eslint-disable-line */ // @ts-ignore
/* eslint-disable-line */ // @ts-expect-error
return acc[val];
}, settings);
if (type.indexOf('object') >= 0 || type.indexOf('array') >= 0) {
try {
setting[key] = JSON.parse(process.env[envVariableName]);
} catch (error) {
} catch {
setting[key] = process.env[envVariableName];
}
} else if (type.indexOf('number') >= 0) {
/* eslint-disable-line */ // @ts-ignore
/* eslint-disable-line */ // @ts-expect-error
setting[key] = process.env[envVariableName] * 1;
} else if (type.indexOf('boolean') >= 0) {
setting[key] = process.env[envVariableName].toLowerCase() === 'true';
@@ -499,7 +505,7 @@ export function set(path: string[], value: string | number | boolean | KeyValue)
export function apply(settings: Record<string, unknown>): boolean {
getInternalSettings(); // Ensure _settings is initialized.
/* eslint-disable-line */ // @ts-ignore
/* eslint-disable-line */ // @ts-expect-error
const newSettings = objectAssignDeep.noMutate(_settings, settings);
utils.removeNullPropertiesFromObject(newSettings, NULLABLE_SETTINGS);
ajvSetting(newSettings);
+18 -5
View File
@@ -60,7 +60,7 @@ async function getZigbee2MQTTVersion(includeCommitHash = true): Promise<{commitH
if (err) {
try {
commitHash = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', '.hash'), 'utf-8');
} catch (error) {
} catch {
/* istanbul ignore next */
commitHash = 'unknown';
}
@@ -92,6 +92,12 @@ function formatDate(time: number, type: 'ISO_8601' | 'ISO_8601_local' | 'epoch'
}
}
function objectIsEmpty(object: object): boolean {
// much faster than checking `Object.keys(object).length`
for (const k in object) return false;
return true;
}
function objectHasProperties(object: {[s: string]: unknown}, properties: string[]): boolean {
for (const property of properties) {
if (!object.hasOwnProperty(property)) {
@@ -128,7 +134,7 @@ function getResponse(request: KeyValue | string, data: KeyValue, error: string):
function parseJSON(value: string, fallback: string): KeyValue | string {
try {
return JSON.parse(value);
} catch (e) {
} catch {
return fallback;
}
}
@@ -162,6 +168,7 @@ export function* loadExternalConverter(moduleName: string): Generator<ExternalDe
if (moduleName.endsWith('.js')) {
converter = loadModuleFromFile(data.joinPath(moduleName));
} else {
// eslint-disable-next-line @typescript-eslint/no-require-imports
converter = require(moduleName);
}
@@ -365,15 +372,15 @@ function filterProperties(filter: string[], data: KeyValue): void {
}
}
export function isNumericExposeFeature(feature: zhc.Feature): feature is zhc.Numeric {
export function isNumericExposeFeature(feature: zhc.Expose): feature is zhc.Numeric {
return feature?.type === 'numeric';
}
export function isEnumExposeFeature(feature: zhc.Feature): feature is zhc.Enum {
export function isEnumExposeFeature(feature: zhc.Expose): feature is zhc.Enum {
return feature?.type === 'enum';
}
export function isBinaryExposeFeature(feature: zhc.Feature): feature is zhc.Binary {
export function isBinaryExposeFeature(feature: zhc.Expose): feature is zhc.Binary {
return feature?.type === 'binary';
}
@@ -396,6 +403,10 @@ function getScenes(entity: zh.Endpoint | zh.Group): Scene[] {
return Object.values(scenes);
}
function deviceNotCoordinator(device: zh.Device): boolean {
return device.type !== 'Coordinator';
}
/* istanbul ignore next */
const noop = (): void => {};
@@ -404,6 +415,7 @@ export default {
getZigbee2MQTTVersion,
getDependencyVersion,
formatDate,
objectIsEmpty,
objectHasProperties,
equalsPartial,
getObjectProperty,
@@ -430,5 +442,6 @@ export default {
flatten,
arrayUnique,
getScenes,
deviceNotCoordinator,
noop,
};
+30 -24
View File
@@ -50,6 +50,7 @@ export default class Zigbee {
concurrent: settings.get().advanced.adapter_concurrent,
delay: settings.get().advanced.adapter_delay,
disableLED: settings.get().serial.disable_led,
transmitPower: settings.get().advanced.transmit_power,
},
acceptJoiningDeviceHandler: this.acceptJoiningDeviceHandler,
};
@@ -66,7 +67,7 @@ export default class Zigbee {
throw error;
}
for (const device of this.devices(false)) {
for (const device of this.devicesIterator(utils.deviceNotCoordinator)) {
await device.resolveDefinition();
}
@@ -124,7 +125,7 @@ export default class Zigbee {
logger.info(`Coordinator firmware version: '${stringify(await this.getCoordinatorVersion())}'`);
logger.debug(`Zigbee network parameters: ${stringify(await this.herdsman.getNetworkParameters())}`);
for (const device of this.devices(false)) {
for (const device of this.devicesIterator(utils.deviceNotCoordinator)) {
// If a passlist is used, all other device will be removed from the network.
const passlist = settings.get().passlist;
const blocklist = settings.get().blocklist;
@@ -135,24 +136,18 @@ export default class Zigbee {
logger.error(`Failed to remove '${device.ieeeAddr}' (${error.message})`);
}
};
if (passlist.length > 0) {
if (!passlist.includes(device.ieeeAddr)) {
logger.warning(`Device which is not on passlist connected (${device.ieeeAddr}), removing...`);
logger.warning(`Device not on passlist currently connected (${device.ieeeAddr}), removing...`);
await remove(device);
}
} else if (blocklist.includes(device.ieeeAddr)) {
logger.warning(`Device on blocklist is connected (${device.ieeeAddr}), removing...`);
logger.warning(`Device on blocklist currently connected (${device.ieeeAddr}), removing...`);
await remove(device);
}
}
// Check if we have to set a transmit power
if (settings.get().advanced.hasOwnProperty('transmit_power')) {
const transmitPower = settings.get().advanced.transmit_power;
await this.herdsman.setTransmitPower(transmitPower);
logger.info(`Set transmit power to '${transmitPower}'`);
}
return startResult;
}
@@ -253,7 +248,9 @@ export default class Zigbee {
@bind private resolveDevice(ieeeAddr: string): Device {
if (!this.deviceLookup[ieeeAddr]) {
const device = this.herdsman.getDeviceByIeeeAddr(ieeeAddr);
device && (this.deviceLookup[ieeeAddr] = new Device(device));
if (device) {
this.deviceLookup[ieeeAddr] = new Device(device);
}
}
const device = this.deviceLookup[ieeeAddr];
@@ -325,20 +322,29 @@ export default class Zigbee {
return this.herdsman.getDevicesByType('Coordinator')[0].endpoints[0];
}
groups(): Group[] {
return this.herdsman.getGroups().map((g) => this.resolveGroup(g.groupID));
}
devices(includeCoordinator = true): Device[] {
const devices: Device[] = [];
for (const device of this.herdsman.getDevices()) {
if (includeCoordinator || device.type !== 'Coordinator') {
devices.push(this.resolveDevice(device.ieeeAddr));
}
*devicesAndGroupsIterator(
devicePredicate?: (value: zh.Device) => boolean,
groupPredicate?: (value: zh.Group) => boolean,
): Generator<Device | Group> {
for (const device of this.herdsman.getDevicesIterator(devicePredicate)) {
yield this.resolveDevice(device.ieeeAddr);
}
return devices;
for (const group of this.herdsman.getGroupsIterator(groupPredicate)) {
yield this.resolveGroup(group.groupID);
}
}
*groupsIterator(predicate?: (value: zh.Group) => boolean): Generator<Group> {
for (const group of this.herdsman.getGroupsIterator(predicate)) {
yield this.resolveGroup(group.groupID);
}
}
*devicesIterator(predicate?: (value: zh.Device) => boolean): Generator<Device> {
for (const device of this.herdsman.getDevicesIterator(predicate)) {
yield this.resolveDevice(device.ieeeAddr);
}
}
@bind private async acceptJoiningDeviceHandler(ieeeAddr: string): Promise<boolean> {
+1224 -525
View File
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -1,6 +1,6 @@
{
"name": "zigbee2mqtt",
"version": "1.39.1",
"version": "1.40.1",
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
"main": "index.js",
"repository": {
@@ -50,40 +50,40 @@
"jszip": "^3.10.1",
"mkdir-recursive": "^0.4.0",
"moment": "^2.30.1",
"mqtt": "^5.9.0",
"mqtt": "^5.10.1",
"object-assign-deep": "^0.4.0",
"rimraf": "^6.0.1",
"semver": "^7.6.3",
"source-map-support": "^0.5.21",
"uri-js": "^4.4.1",
"winston": "^3.13.1",
"winston-syslog": "^2.7.0",
"winston": "^3.14.2",
"winston-syslog": "^2.7.1",
"winston-transport": "^4.7.1",
"ws": "^8.18.0",
"zigbee-herdsman": "0.55.3",
"zigbee-herdsman-converters": "19.72.0",
"zigbee-herdsman": "0.57.3",
"zigbee-herdsman-converters": "20.12.1",
"zigbee2mqtt-frontend": "0.7.4"
},
"devDependencies": {
"@babel/core": "^7.24.9",
"@babel/core": "^7.25.2",
"@babel/plugin-proposal-decorators": "^7.24.7",
"@babel/preset-env": "^7.25.0",
"@babel/preset-env": "^7.25.4",
"@babel/preset-typescript": "^7.24.7",
"@types/finalhandler": "^1.2.3",
"@types/humanize-duration": "^3.27.4",
"@types/jest": "^29.5.12",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.14.12",
"@types/node": "^22.5.1",
"@types/object-assign-deep": "^0.4.3",
"@types/readable-stream": "4.0.15",
"@types/ws": "8.5.11",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^7.17.0",
"@types/ws": "8.5.12",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"babel-jest": "^29.7.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-perfectionist": "^3.0.0",
"eslint-plugin-jest": "^28.8.1",
"eslint-plugin-perfectionist": "^3.3.0",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"tmp": "^0.2.3",
+4 -10
View File
@@ -66,6 +66,7 @@ describe('Controller', () => {
});
it('Start controller', async () => {
settings.set(['advanced', 'transmit_power'], 14);
await controller.start();
expect(zigbeeHerdsman.constructor).toHaveBeenCalledWith({
network: {
@@ -78,14 +79,14 @@ describe('Controller', () => {
databaseBackupPath: path.join(data.mockDir, 'database.db.backup'),
backupPath: path.join(data.mockDir, 'coordinator_backup.json'),
acceptJoiningDeviceHandler: expect.any(Function),
adapter: {concurrent: null, delay: null, disableLED: false},
adapter: {concurrent: null, delay: null, disableLED: false, transmitPower: 14},
serialPort: {baudRate: undefined, rtscts: undefined, path: '/dev/dummy'},
});
expect(zigbeeHerdsman.start).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(0);
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, undefined, undefined);
expect(logger.info).toHaveBeenCalledWith(`Currently ${Object.values(zigbeeHerdsman.devices).length - 1} devices are joined:`);
expect(logger.info).toHaveBeenCalledWith(`Currently ${Object.values(zigbeeHerdsman.devices).length - 1} devices are joined.`);
expect(logger.info).toHaveBeenCalledWith(
'bulb (0x000b57fffec6a5b2): LED1545G12 - IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (Router)',
);
@@ -277,7 +278,7 @@ describe('Controller', () => {
await controller.start();
await flushPromises();
expect(logger.error).toHaveBeenCalledWith('MQTT error: addr not found');
expect(logger.error).toHaveBeenCalledWith('MQTT failed to connect, exiting...');
expect(logger.error).toHaveBeenCalledWith('MQTT failed to connect, exiting... (addr not found)');
expect(mockExit).toHaveBeenCalledTimes(1);
expect(mockExit).toHaveBeenCalledWith(1, false);
});
@@ -289,13 +290,6 @@ describe('Controller', () => {
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, undefined);
});
it('Start controller with transmit power', async () => {
settings.set(['experimental', 'transmit_power'], 14);
await controller.start();
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledWith(14);
});
it('Start controller and stop with restart', async () => {
await controller.start();
await controller.stop(true);
+13
View File
@@ -1443,6 +1443,19 @@ describe('HomeAssistant extension', () => {
);
});
it('Should clear discovery when group is removed', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/group/remove', stringify({id: 'ha_discovery_group'}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'homeassistant/light/1221051039810110150109113116116_9/light/config',
null,
{retain: true, qos: 1},
expect.any(Function),
);
});
it('Should refresh discovery when device is renamed', async () => {
await MQTT.events.message(
'homeassistant/device_automation/0x0017880104e45522/action_double/config',
+190 -56
View File
@@ -66,62 +66,40 @@ describe('Networkmap', () => {
* | -> CC2530_ROUTER -> WXKG02LM_rev1
*
*/
coordinator.lqi = () => {
return {
neighbors: [
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 2, depth: 1, linkquality: 120},
{ieeeAddr: bulb.ieeeAddr, networkAddress: bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92},
{
ieeeAddr: external_converter_device.ieeeAddr,
networkAddress: external_converter_device.networkAddress,
relationship: 2,
depth: 1,
linkquality: 92,
},
],
};
};
coordinator.routingTable = () => {
return {table: [{destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress}]};
};
bulb.lqi = () => {
return {
neighbors: [
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 1, depth: 2, linkquality: 110},
{ieeeAddr: CC2530_ROUTER.ieeeAddr, networkAddress: CC2530_ROUTER.networkAddress, relationship: 1, depth: 2, linkquality: 100},
],
};
};
bulb.routingTable = () => {
return {table: []};
};
bulb_color.lqi = () => {
return {neighbors: []};
};
bulb_color.routingTable = () => {
return {table: []};
};
CC2530_ROUTER.lqi = () => {
return {
neighbors: [
{ieeeAddr: '0x0000000000000000', networkAddress: WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, linkquality: 130},
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 4, depth: 2, linkquality: 130},
],
};
};
CC2530_ROUTER.routingTable = () => {
return {table: []};
};
unsupported_router.lqi = () => {
throw new Error('failed');
};
unsupported_router.routingTable = () => {
throw new Error('failed');
};
coordinator.lqi = jest.fn().mockResolvedValue({
neighbors: [
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 2, depth: 1, linkquality: 120},
{ieeeAddr: bulb.ieeeAddr, networkAddress: bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92},
{
ieeeAddr: external_converter_device.ieeeAddr,
networkAddress: external_converter_device.networkAddress,
relationship: 2,
depth: 1,
linkquality: 92,
},
],
});
coordinator.routingTable = jest.fn().mockResolvedValue({
table: [{destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress}],
});
bulb.lqi = jest.fn().mockResolvedValue({
neighbors: [
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 1, depth: 2, linkquality: 110},
{ieeeAddr: CC2530_ROUTER.ieeeAddr, networkAddress: CC2530_ROUTER.networkAddress, relationship: 1, depth: 2, linkquality: 100},
],
});
bulb.routingTable = jest.fn().mockResolvedValue({table: []});
bulb_color.lqi = jest.fn().mockResolvedValue({neighbors: []});
bulb_color.routingTable = jest.fn().mockResolvedValue({table: []});
CC2530_ROUTER.lqi = jest.fn().mockResolvedValue({
neighbors: [
{ieeeAddr: '0x0000000000000000', networkAddress: WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, linkquality: 130},
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 4, depth: 2, linkquality: 130},
],
});
CC2530_ROUTER.routingTable = jest.fn().mockResolvedValue({table: []});
unsupported_router.lqi = jest.fn().mockRejectedValue(new Error('failed'));
unsupported_router.routingTable = jest.fn().mockRejectedValue(new Error('failed'));
}
it('Output raw networkmap legacy api', async () => {
@@ -826,4 +804,160 @@ describe('Networkmap', () => {
const actual = JSON.parse(call[1]);
expect(actual).toStrictEqual(expected);
});
it('Handles retrying request when first attempt fails', async () => {
settings.set(['devices', '0x000b57fffec6a5b2', 'disabled'], true);
mock();
bulb.lqi.mockRejectedValueOnce(new Error('failed'));
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
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: 'Wireless remote switch (double rocker), 2016 model',
model: 'WXKG02LM_rev1',
supports: 'battery, voltage, power_outage_count, action, linkquality',
vendor: 'Aqara',
},
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: 'action, linkquality',
vendor: 'Boef',
},
failed: ['lqi', 'routingTable'],
friendlyName: '0x0017880104e45525',
ieeeAddr: '0x0017880104e45525',
lastSeen: 1000,
manufacturerName: 'Boef',
modelID: 'notSupportedModelID',
networkAddress: 6536,
type: 'Router',
},
{
definition: {
description: 'CC2530 router',
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);
});
});
+16 -4
View File
@@ -857,8 +857,14 @@ const mock = {
events[type] = handler;
},
stop: jest.fn(),
getDevices: jest.fn().mockImplementation(() => {
return Object.values(devices).filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr));
getDevicesIterator: jest.fn().mockImplementation(function* (predicate) {
for (const key in devices) {
const device = devices[key];
if ((returnDevices.length === 0 || returnDevices.includes(device.ieeeAddr)) && (!predicate || predicate(device))) {
yield device;
}
}
}),
getDevicesByType: jest.fn().mockImplementation((type) => {
return Object.values(devices)
@@ -875,8 +881,14 @@ const mock = {
.filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr))
.find((d) => d.networkAddress === networkAddress);
}),
getGroups: jest.fn().mockImplementation((query) => {
return Object.values(groups);
getGroupsIterator: jest.fn().mockImplementation(function* (predicate) {
for (const key in groups) {
const group = groups[key];
if (!predicate || predicate(group)) {
yield group;
}
}
}),
getGroupByID: jest.fn().mockImplementation((groupID) => {
return Object.values(groups).find((d) => d.groupID === groupID);
+5
View File
@@ -4,6 +4,11 @@ const versionHerdsman = require('../node_modules/zigbee-herdsman/package.json').
const versionHerdsmanConverters = require('../node_modules/zigbee-herdsman-converters/package.json').version;
describe('Utils', () => {
it('Object is empty', () => {
expect(utils.objectIsEmpty({})).toBeTruthy();
expect(utils.objectIsEmpty({a: 1})).toBeFalsy();
});
it('Object has properties', () => {
expect(utils.objectHasProperties({a: 1, b: 2, c: 3}, ['a', 'b'])).toBeTruthy();
expect(utils.objectHasProperties({a: 1, b: 2, c: 3}, ['a', 'b', 'd'])).toBeFalsy();