mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 07:08:56 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
403d3c090d | ||
|
|
9283c4b497 | ||
|
|
0d46354c48 | ||
|
|
13a108ddbc | ||
|
|
5524e881e6 | ||
|
|
a33c29f10a | ||
|
|
fc02071c8f | ||
|
|
06556d44f3 | ||
|
|
13084d22a7 | ||
|
|
14787d3a6a | ||
|
|
e2da2fa8d3 | ||
|
|
6dade8b11f | ||
|
|
557b17ad5b | ||
|
|
64222d231f | ||
|
|
b7fa722109 | ||
|
|
4a1d983168 | ||
|
|
6fa1dabecb | ||
|
|
3b2ba0950e | ||
|
|
9f40936452 | ||
|
|
b2bae6ba94 | ||
|
|
406deaa9da | ||
|
|
65b1e60a3d | ||
|
|
6ed9b26657 | ||
|
|
cf3298bf6f | ||
|
|
00948c0be1 | ||
|
|
ed9de74f91 | ||
|
|
41587211e0 | ||
|
|
b4176d6fee | ||
|
|
ccdf6b3c6d | ||
|
|
8a05077792 | ||
|
|
df7772bfe0 | ||
|
|
6ae9970efd | ||
|
|
b92072fc6a | ||
|
|
5bdf90df9e | ||
|
|
22980177b6 | ||
|
|
837cbe5f26 | ||
|
|
e95116c773 | ||
|
|
27300ba8da | ||
|
|
801ab9066f | ||
|
|
2a631f3551 | ||
|
|
66fefaa827 | ||
|
|
c986fd6c66 | ||
|
|
29f436ab95 | ||
|
|
d827d954ed | ||
|
|
8ae2c4d42d | ||
|
|
9027c41fe8 | ||
|
|
a5748d620b | ||
|
|
d1734feb21 | ||
|
|
00b5e983d9 | ||
|
|
efd3d3dfb0 | ||
|
|
aeba93d0a3 | ||
|
|
7ca456d20e | ||
|
|
13ac8a0f53 | ||
|
|
fe6cacd87e | ||
|
|
00923685d2 | ||
|
|
bd7dec0f11 | ||
|
|
9bb21f486c | ||
|
|
d0f5733db3 | ||
|
|
5b610afa8f | ||
|
|
2cb8a8f5a5 | ||
|
|
3217332e46 | ||
|
|
a25ef81cf4 | ||
|
|
eac11a6439 | ||
|
|
a330830cde |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 }}'
|
||||
|
||||
@@ -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,3 +1,3 @@
|
||||
{
|
||||
".": "1.39.1"
|
||||
".": "1.40.1"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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 ./
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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()) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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', {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)`);
|
||||
|
||||
Vendored
+1
-1
@@ -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 = {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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> {
|
||||
|
||||
Generated
+1224
-525
File diff suppressed because it is too large
Load Diff
+14
-14
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user