Compare commits

...
88 Commits
Author SHA1 Message Date
Koen Kanters b0b02c30b2 chore(dev): release 2.8.0 (#30420) 2026-02-01 20:19:58 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9ddbb2a21a fix(ignore): bump zigbee2mqtt-windfront from 2.8.0 to 2.8.1 in the minor-patch group (#30879)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-01 20:17:41 +01:00
Koen Kanters 49b752da35 fix(ignore): update zigbee-herdsman-converters to 25.120.0 (#30876) 2026-01-31 13:35:35 +00:00
Koen Kanters 788b0d3993 fix(ignore): update zigbee-herdsman-converters to 25.119.0 (#30874) 2026-01-31 12:48:45 +00:00
Koen Kanters f68834049c fix(ignore): update zigbee-herdsman-converters to 25.118.0 (#30865) 2026-01-30 20:26:15 +00:00
Koen Kanters 4afbce62eb fix(ignore): update zigbee-herdsman-converters to 25.117.0 (#30849) 2026-01-29 19:07:37 +00:00
burmistrzak 4f412cb4da fix: Home Assistant: Change device_class of eco2 to volatile_organic_compounds_parts (#30840) 2026-01-29 19:48:41 +01:00
Koen Kanters 5a4d6b56ad fix(ignore): update zigbee-herdsman-converters to 25.116.0 (#30839) 2026-01-28 20:59:48 +00:00
burmistrzak 26a0fd4de0 fix: Home Assistant: discover temperature sensor for thermostats (#30804) 2026-01-28 21:47:03 +01:00
dependabot[bot] 8d980ae6ec fix(ignore): bump zigbee2mqtt-windfront in the minor-patch group
Bumps the minor-patch group with 1 update: [zigbee2mqtt-windfront](https://github.com/Nerivec/zigbee2mqtt-windfront).


Updates `zigbee2mqtt-windfront` from 2.7.0 to 2.8.0
- [Release notes](https://github.com/Nerivec/zigbee2mqtt-windfront/releases)
- [Commits](https://github.com/Nerivec/zigbee2mqtt-windfront/compare/v2.7.0...v2.8.0)

---
updated-dependencies:
- dependency-name: zigbee2mqtt-windfront
  dependency-version: 2.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-27 20:46:31 +01:00
Koen Kanters 99e06a8d3e fix(ignore): update zigbee-herdsman-converters to 25.115.0 (#30826) 2026-01-27 19:46:20 +00:00
NerivecandKoen Kanters 476efaa6e1 fix: OTA availability detection (#30815)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-01-27 20:37:18 +01:00
Koenkk 10478e797e fix(ignore): update zigbee-herdsman to 9.0.2 2026-01-27 20:37:18 +01:00
Stephan GarlandandKoen Kanters 5de47a0d93 fix: Network map: escape double-quotes and backslashes in device attributes (#30746)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-01-27 19:59:53 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 14bffd58b1 fix(ignore): bump @biomejs/biome from 2.3.12 to 2.3.13 in the minor-patch group (#30810)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 21:49:13 +01:00
Koen Kanters 999df2913f fix(ignore): update zigbee-herdsman-converters to 25.114.0 (#30803) 2026-01-25 20:55:24 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Nerivec
da1f77ec6e fix(ignore): bump the minor-patch group with 2 updates (#30790)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2026-01-24 20:59:45 +00:00
Koen Kanters 036ee471dd fix(ignore): update zigbee-herdsman-converters to 25.113.0 (#30789) 2026-01-24 20:31:04 +00:00
NerivecandKoen Kanters dd1c449796 feat: Improve OTA (#30566)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-01-24 21:27:44 +01:00
Koen Kanters 6d5e7e334a fix(ignore): update zigbee-herdsman to 9.0.1 (#30788) 2026-01-24 20:21:57 +00:00
burmistrzak 8e682956e5 fix: Home Assistant: Use temperature_delta for calibration (#30784) 2026-01-24 21:03:39 +01:00
Koen Kanters 971ddaa3cb fix(ignore): update zigbee-herdsman to 9.0.0 (#30787) 2026-01-24 19:45:15 +00:00
Koen Kanters ad51d12a2d fix(ignore): update zigbee-herdsman-converters to 25.112.0 (#30767) 2026-01-23 19:49:13 +00:00
Rohan Kapoor d9e4244f80 fix(ignore): Replace multiple area occupancy fields with camelCase instead of PascalCase (#30745) 2026-01-23 20:38:15 +01:00
Koen Kanters 2bb44f7a8b fix(ignore): update zigbee-herdsman-converters to 25.111.0 (#30739) 2026-01-21 20:53:41 +00:00
Koen Kanters 20af50201e fix(ignore): update zigbee-herdsman-converters to 25.110.0 (#30726) 2026-01-20 19:45:17 +00:00
Rohan Kapoor 2e36084852 fix: Home Assistant: treat Area1-4Occupancy as occupancy (#30712) 2026-01-20 19:34:39 +00:00
Koen Kanters 9d266bda9a fix(ignore): update zigbee-herdsman to 8.1.0 (#30725) 2026-01-20 19:29:58 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 043dd0ff1c fix(ignore): bump @types/node from 24.10.7 to 24.10.9 in the minor-patch group (#30713)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-20 20:09:54 +01:00
Koen Kanters ee6afaf1c6 fix(ignore): update zigbee-herdsman-converters to 25.109.0 (#30709) 2026-01-18 20:48:31 +00:00
Ignacio Hernandez-Ros 4815d5149d fix: Home Assistant: add gas device class (#30653) 2026-01-18 20:29:39 +01:00
Patrick ZAJDAandKoen Kanters 6f0b02e009 fix: Home Assistant: expose group settings override (#30627)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-01-17 15:25:36 +01:00
Koen Kanters 5e1b7d4830 fix(ignore): update zigbee-herdsman-converters to 25.108.0 (#30672) 2026-01-16 20:04:18 +00:00
Koen Kanters 535f75e90c fix(ignore): update zigbee-herdsman-converters to 25.107.0 (#30662) 2026-01-15 21:08:45 +00:00
Koen Kanters d44cdbde4f fix(ignore): update zigbee-herdsman to 8.0.3 (#30632) 2026-01-13 20:19:13 +00:00
Koen Kanters 9a41ec2a39 fix(ignore): update zigbee-herdsman-converters to 25.106.0 (#30631) 2026-01-13 20:03:22 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1d87d2f234 fix(ignore): bump the minor-patch group with 2 updates (#30617)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-12 20:53:11 +01:00
Koen Kanters 29c757145d fix(ignore): update zigbee-herdsman-converters to 25.105.0 (#30605) 2026-01-11 20:47:06 +00:00
Koen Kanters e68403d8d3 fix(ignore): update zigbee-herdsman-converters to 25.104.0 (#30564) 2026-01-08 20:09:11 +00:00
Koen Kanters a32c8eec36 fix: Home Assistant: mark load_estimate as state_class measurement https://github.com/Koenkk/zigbee-herdsman-converters/issues/11240 2026-01-08 20:54:25 +01:00
Koen Kanters d4522c53ca fix(ignore): update zigbee-herdsman-converters to 25.103.0 (#30552) 2026-01-07 19:38:42 +00:00
Koen Kanters 57245e5234 fix(ignore): update zigbee-herdsman-converters to 25.102.0 (#30537) 2026-01-06 20:44:54 +00:00
Koen Kanters 4313baca09 fix(ignore): update zigbee-herdsman-converters to 25.101.0 (#30520) 2026-01-05 20:56:26 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ed52c49e34 fix(ignore): bump @biomejs/biome from 2.3.10 to 2.3.11 in the minor-patch group (#30511)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-05 21:29:35 +01:00
Koen Kanters a2b81ab894 fix(ignore): update zigbee-herdsman-converters to 25.100.0 (#30499) 2026-01-04 20:27:22 +00:00
Koen Kanters 3e60f915ff fix: Home Assisant: set state class to measurement for illuminane_raw https://github.com/Koenkk/zigbee2mqtt/issues/30439 2026-01-04 20:02:44 +01:00
rhysfredandrhys 1eaa2f3e75 fix: Log error before renaming failed to load converters (#30436)
Co-authored-by: rhys <rhys@frontleftspeaker.com>
2026-01-04 19:51:46 +01:00
Koen Kantersandburmistrzak 3adf7d5d8e fix(ignore): update zigbee-herdsman-converters to 25.99.0 (#30481)
Co-authored-by: burmistrzak <61958704+burmistrzak@users.noreply.github.com>
2026-01-04 18:51:31 +00:00
Koen Kanters 1147cf16dd fix(ignore): update zigbee-herdsman to 8.0.2 (#30480) 2026-01-03 20:59:33 +00:00
github-actions[bot] 97a9b15240 chore: promote to dev 2026-01-01 13:53:53 +00:00
Koen Kanters 3a49c95786 chore(dev): release 2.7.2 (#30051) 2026-01-01 14:41:19 +01:00
Koen Kanters 0eba0c953d fix(ignore): update zigbee-herdsman-converters to 25.98.0 (#30417) 2026-01-01 13:23:35 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4d6269e7e8 fix(ignore): bump zigbee2mqtt-windfront from 2.6.1 to 2.6.2 in the minor-patch group (#30412)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-31 19:56:27 +00:00
Koen Kanters 3727f078ec fix(ignore): update zigbee-herdsman-converters to 25.97.0 (#30396) 2025-12-30 14:29:46 +00:00
dependabot[bot] 93c9704fcd fix(ignore): bump @types/node from 24.10.2 to 24.10.4 in the minor-patch group (#30390) 2025-12-30 13:43:39 +01:00
Koen Kanters 2721b0ea78 fix(ignore): update zigbee-herdsman-converters to 25.96.0 (#30383) 2025-12-28 20:58:41 +00:00
Koen Kanters 4c48e9cfbc fix(ignore): update zigbee-herdsman-converters to 25.95.0 (#30371) 2025-12-27 20:42:04 +00:00
Koen Kanters 7cb5c58dc0 fix(ignore): update zigbee-herdsman-converters to 25.94.0 (#30355) 2025-12-26 14:37:38 +00:00
Koen Kanters c61f67f061 fix(ignore): update zigbee-herdsman-converters to 25.93.0 (#30331) 2025-12-23 20:58:54 +00:00
Nerivec 58d98c7263 fix: log dir tz format not working on some systems (#30324) 2025-12-23 17:45:04 +01:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Nerivec
b10fa93700 fix(ignore): bump the minor-patch group with 2 updates (#30309)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2025-12-22 15:23:39 +00:00
Koen Kanters c28a024664 fix(ignore): update zigbee-herdsman-converters to 25.92.0 (#30300) 2025-12-21 21:28:27 +00:00
Nerivec 8549b37228 fix: Proper timezone in logs dir (#30297) 2025-12-21 20:44:05 +01:00
Koen Kanters 4c02e3da8f fix(ignore): update zigbee-herdsman-converters to 25.91.0 (#30279) 2025-12-19 21:02:06 +00:00
Koen Kanters ab0894715a fix(ignore): update zigbee-herdsman-converters to 25.90.0 (#30267) 2025-12-18 21:36:30 +00:00
Koen Kanters 1688a3b532 fix(ignore): update zigbee-herdsman to 8.0.1 (#30240) 2025-12-16 16:51:45 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 97694d2241 chore(deps): bump actions/cache from 4 to 5 (#30221)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 17:45:15 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d6088c56db chore(deps): bump peter-evans/create-pull-request from 7 to 8 (#30222)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 17:45:06 +01:00
Koen Kanters b39b8d478e fix: Don't fail to start when removing old log directory fails (#30216) 2025-12-16 17:44:38 +01:00
Koen Kanters 01304cfc66 fix(ignore): update zigbee-herdsman-converters to 25.89.0 (#30215) 2025-12-14 20:40:29 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1d15c2142f fix(ignore): bump the minor-patch group with 2 updates (#30207)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-14 16:40:27 +00:00
Koen Kanters 3818c50b8a fix(ignore): update zigbee-herdsman-converters to 25.88.0 (#30196) 2025-12-13 20:34:30 +00:00
Koen KantersandNerivec 7f5f6a5849 fix(ignore): update zigbee-herdsman to 8.0.0 and zigbee-herdsman-converters to 25.87.0 (#30186)
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2025-12-13 20:40:09 +01:00
Nerivec 0025ef87c5 fix: Biome floating promises detection (#30137) 2025-12-13 20:38:03 +01:00
Koen Kanters 3460f157df fix(ignore): update zigbee-herdsman-converters to 25.86.0 (#30143) 2025-12-10 21:14:41 +00:00
Koen Kanters ecb0af4057 fix(ignore): update zigbee-herdsman to 7.0.6 (#30142) 2025-12-10 21:11:13 +00:00
Koen Kanters 5db6ae0a19 fix(ignore): update zigbee-herdsman-converters to 25.85.0 (#30120) 2025-12-09 20:15:54 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ad3c09036b fix(ignore): bump the minor-patch group across 1 directory with 4 updates (#30117)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-09 20:33:03 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0c89629526 chore(deps): bump alpine from 3.22 to 3.23 in /docker (#30094)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-09 20:04:22 +01:00
Koen Kanters 4574ffc71a fix(ignore): update zigbee-herdsman to 7.0.5 (#30116) 2025-12-09 18:46:55 +00:00
Koen Kanters 1707f3abf3 chore: merge master to dev 2025-12-07 21:26:17 +01:00
Koen Kanters 8916fb7b50 chore: lint issueBot 2025-12-07 21:25:58 +01:00
Koen Kanters 6e70ef24d0 chore: merge master to dev 2025-12-07 21:22:54 +01:00
Koen Kanters 8a3c4bd59f chore: issueBot: handle duplicates 2025-12-07 21:22:34 +01:00
Koen Kanters c18d1329c8 fix(ignore): update zigbee-herdsman-converters to 25.84.0 (#30081) 2025-12-07 19:56:11 +00:00
Yuxin Wang ed1e7b333f fix: Remove extra > in onboarding(#30065) 2025-12-07 20:22:04 +01:00
github-actions[bot] 95c38810a8 chore: promote to dev 2025-12-06 20:39:43 +00:00
Koen Kanters b70701f8b8 chore: fix changelog generator undefined whitelabel vendor/description 2025-12-06 21:23:09 +01:00
39 changed files with 2230 additions and 716 deletions
+2 -2
View File
@@ -42,7 +42,7 @@ jobs:
ref: master
- name: Restore cache commit-user-lookup.json
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
path: z2m/scripts/commit-user-lookup.json
key: commit-user-lookup-dummy
@@ -67,7 +67,7 @@ jobs:
env:
GH_TOKEN: ${{secrets.GH_TOKEN}}
- name: Save cache commit-user-lookup.json
uses: actions/cache/save@v4
uses: actions/cache/save@v5
if: always()
with:
path: z2m/scripts/commit-user-lookup.json
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
- run: |
pnpm install ${{ github.event.client_payload.package }}@${{ github.event.client_payload.version }} --save-exact
pnpm install --no-frozen-lockfile
- uses: peter-evans/create-pull-request@v7
- uses: peter-evans/create-pull-request@v8
id: cpr
with:
commit-message: 'fix(ignore): update ${{ github.event.client_payload.package }} to ${{ github.event.client_payload.version }}'
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "2.7.1"
".": "2.8.0"
}
+92
View File
@@ -1,5 +1,97 @@
# Changelog
## [2.8.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.7.2...2.8.0) (2026-02-01)
### Features
* Improve OTA ([#30566](https://github.com/Koenkk/zigbee2mqtt/issues/30566)) ([dd1c449](https://github.com/Koenkk/zigbee2mqtt/commit/dd1c44979667360d77f05ac30729011f9f2d6cd6))
### Bug Fixes
* Home Assisant: set state class to `measurement` for `illuminane_raw` https://github.com/Koenkk/zigbee2mqtt/issues/30439 ([3e60f91](https://github.com/Koenkk/zigbee2mqtt/commit/3e60f915ffe1de640690695ccf6536ad235c336e))
* Home Assistant: add `gas` device class ([#30653](https://github.com/Koenkk/zigbee2mqtt/issues/30653)) ([4815d51](https://github.com/Koenkk/zigbee2mqtt/commit/4815d5149d3fbc70fe374fc4aa99213ac5f8a8c7))
* Home Assistant: Change `device_class` of `eco2` to `volatile_organic_compounds_parts` ([#30840](https://github.com/Koenkk/zigbee2mqtt/issues/30840)) ([4f412cb](https://github.com/Koenkk/zigbee2mqtt/commit/4f412cb4da6217fcca57b5ea1afc40d34d662e19))
* Home Assistant: discover temperature sensor for thermostats ([#30804](https://github.com/Koenkk/zigbee2mqtt/issues/30804)) ([26a0fd4](https://github.com/Koenkk/zigbee2mqtt/commit/26a0fd4de0f39d10a404517f42d4c37aff1eff8e))
* Home Assistant: expose group settings override ([#30627](https://github.com/Koenkk/zigbee2mqtt/issues/30627)) ([6f0b02e](https://github.com/Koenkk/zigbee2mqtt/commit/6f0b02e0093cc7fd61d30c00fb1cd61d0cfdf09f))
* Home Assistant: mark `load_estimate` as `state_class` `measurement` https://github.com/Koenkk/zigbee-herdsman-converters/issues/11240 ([a32c8ee](https://github.com/Koenkk/zigbee2mqtt/commit/a32c8eec3604a3ad0d16e87edf027581db7f6920))
* Home Assistant: treat `Area1-4Occupancy` as `occupancy` ([#30712](https://github.com/Koenkk/zigbee2mqtt/issues/30712)) ([2e36084](https://github.com/Koenkk/zigbee2mqtt/commit/2e36084852e84f26dd375820a87c0a8520d4c3e7))
* Home Assistant: Use `temperature_delta` for calibration ([#30784](https://github.com/Koenkk/zigbee2mqtt/issues/30784)) ([8e68295](https://github.com/Koenkk/zigbee2mqtt/commit/8e682956e5977101ed4ca80e795f413a5bfb4f09))
* **ignore:** bump @biomejs/biome from 2.3.10 to 2.3.11 in the minor-patch group ([#30511](https://github.com/Koenkk/zigbee2mqtt/issues/30511)) ([ed52c49](https://github.com/Koenkk/zigbee2mqtt/commit/ed52c49e34a200f9adf5deb2065c78fbe8df3189))
* **ignore:** bump @biomejs/biome from 2.3.12 to 2.3.13 in the minor-patch group ([#30810](https://github.com/Koenkk/zigbee2mqtt/issues/30810)) ([14bffd5](https://github.com/Koenkk/zigbee2mqtt/commit/14bffd58b1ae90cf5c32b654ddbb7a43d12a36cc))
* **ignore:** bump @types/node from 24.10.7 to 24.10.9 in the minor-patch group ([#30713](https://github.com/Koenkk/zigbee2mqtt/issues/30713)) ([043dd0f](https://github.com/Koenkk/zigbee2mqtt/commit/043dd0ff1cc94ffa8ea59f6de674e0363f9f6920))
* **ignore:** bump the minor-patch group with 2 updates ([#30617](https://github.com/Koenkk/zigbee2mqtt/issues/30617)) ([1d87d2f](https://github.com/Koenkk/zigbee2mqtt/commit/1d87d2f2349f9eed6351a032645eec075476f148))
* **ignore:** bump the minor-patch group with 2 updates ([#30790](https://github.com/Koenkk/zigbee2mqtt/issues/30790)) ([da1f77e](https://github.com/Koenkk/zigbee2mqtt/commit/da1f77ec6eb599a480f66cc82edb12acc6c22ca8))
* **ignore:** bump zigbee2mqtt-windfront from 2.8.0 to 2.8.1 in the minor-patch group ([#30879](https://github.com/Koenkk/zigbee2mqtt/issues/30879)) ([9ddbb2a](https://github.com/Koenkk/zigbee2mqtt/commit/9ddbb2a21a0d302ed39ac4c658587d9f33c48960))
* **ignore:** bump zigbee2mqtt-windfront in the minor-patch group ([8d980ae](https://github.com/Koenkk/zigbee2mqtt/commit/8d980ae6ecd759c6c6fb42fc7fbb1d701a3317fa))
* **ignore:** Replace multiple area occupancy fields with camelCase instead of PascalCase ([#30745](https://github.com/Koenkk/zigbee2mqtt/issues/30745)) ([d9e4244](https://github.com/Koenkk/zigbee2mqtt/commit/d9e4244f801aaa24b8347ab684521391df1ce0e2))
* **ignore:** update zigbee-herdsman to 8.0.2 ([#30480](https://github.com/Koenkk/zigbee2mqtt/issues/30480)) ([1147cf1](https://github.com/Koenkk/zigbee2mqtt/commit/1147cf16dd62dce5863a93983835c46e8e92e95d))
* **ignore:** update zigbee-herdsman to 8.0.3 ([#30632](https://github.com/Koenkk/zigbee2mqtt/issues/30632)) ([d44cdbd](https://github.com/Koenkk/zigbee2mqtt/commit/d44cdbde4f5247f69af4c79fd45a62c03037eca7))
* **ignore:** update zigbee-herdsman to 8.1.0 ([#30725](https://github.com/Koenkk/zigbee2mqtt/issues/30725)) ([9d266bd](https://github.com/Koenkk/zigbee2mqtt/commit/9d266bda9a34efbe726f8d984da48e5ba3c46a83))
* **ignore:** update zigbee-herdsman to 9.0.0 ([#30787](https://github.com/Koenkk/zigbee2mqtt/issues/30787)) ([971ddaa](https://github.com/Koenkk/zigbee2mqtt/commit/971ddaa3cbc2132150f7cde4cdbe0575349eabac))
* **ignore:** update zigbee-herdsman to 9.0.1 ([#30788](https://github.com/Koenkk/zigbee2mqtt/issues/30788)) ([6d5e7e3](https://github.com/Koenkk/zigbee2mqtt/commit/6d5e7e334a9a560b9f6293cf448ad84de8504b67))
* **ignore:** update zigbee-herdsman to 9.0.2 ([10478e7](https://github.com/Koenkk/zigbee2mqtt/commit/10478e797e1733f9bf66defce0e84b6cc1f23020))
* **ignore:** update zigbee-herdsman-converters to 25.100.0 ([#30499](https://github.com/Koenkk/zigbee2mqtt/issues/30499)) ([a2b81ab](https://github.com/Koenkk/zigbee2mqtt/commit/a2b81ab89491f8babf40001404969817f19c5b5e))
* **ignore:** update zigbee-herdsman-converters to 25.101.0 ([#30520](https://github.com/Koenkk/zigbee2mqtt/issues/30520)) ([4313bac](https://github.com/Koenkk/zigbee2mqtt/commit/4313baca09b8476466b8134c551b59f92996b53f))
* **ignore:** update zigbee-herdsman-converters to 25.102.0 ([#30537](https://github.com/Koenkk/zigbee2mqtt/issues/30537)) ([57245e5](https://github.com/Koenkk/zigbee2mqtt/commit/57245e52343568678a4dace9bf569c05f508decc))
* **ignore:** update zigbee-herdsman-converters to 25.103.0 ([#30552](https://github.com/Koenkk/zigbee2mqtt/issues/30552)) ([d4522c5](https://github.com/Koenkk/zigbee2mqtt/commit/d4522c53caca86ba3a087fa67839e2ebf5d9deca))
* **ignore:** update zigbee-herdsman-converters to 25.104.0 ([#30564](https://github.com/Koenkk/zigbee2mqtt/issues/30564)) ([e68403d](https://github.com/Koenkk/zigbee2mqtt/commit/e68403d8d3ac68f2d09df066d1be8db87f2e7977))
* **ignore:** update zigbee-herdsman-converters to 25.105.0 ([#30605](https://github.com/Koenkk/zigbee2mqtt/issues/30605)) ([29c7571](https://github.com/Koenkk/zigbee2mqtt/commit/29c757145d6ed16a8629b68379cd85fabe45a8f1))
* **ignore:** update zigbee-herdsman-converters to 25.106.0 ([#30631](https://github.com/Koenkk/zigbee2mqtt/issues/30631)) ([9a41ec2](https://github.com/Koenkk/zigbee2mqtt/commit/9a41ec2a39ca2373dbdcb15b09aa0eb09116c31e))
* **ignore:** update zigbee-herdsman-converters to 25.107.0 ([#30662](https://github.com/Koenkk/zigbee2mqtt/issues/30662)) ([535f75e](https://github.com/Koenkk/zigbee2mqtt/commit/535f75e90ca936afcac0c79e4c2452dce7b9b08d))
* **ignore:** update zigbee-herdsman-converters to 25.108.0 ([#30672](https://github.com/Koenkk/zigbee2mqtt/issues/30672)) ([5e1b7d4](https://github.com/Koenkk/zigbee2mqtt/commit/5e1b7d4830b5597eb993f32e3a6d1edd84b48e4c))
* **ignore:** update zigbee-herdsman-converters to 25.109.0 ([#30709](https://github.com/Koenkk/zigbee2mqtt/issues/30709)) ([ee6afaf](https://github.com/Koenkk/zigbee2mqtt/commit/ee6afaf1c608b89efef452d7cb337345b035d76a))
* **ignore:** update zigbee-herdsman-converters to 25.110.0 ([#30726](https://github.com/Koenkk/zigbee2mqtt/issues/30726)) ([20af502](https://github.com/Koenkk/zigbee2mqtt/commit/20af50201ef1e081223ee8b2f0b6304777c7b7f1))
* **ignore:** update zigbee-herdsman-converters to 25.111.0 ([#30739](https://github.com/Koenkk/zigbee2mqtt/issues/30739)) ([2bb44f7](https://github.com/Koenkk/zigbee2mqtt/commit/2bb44f7a8b8bd9fc1a21b127b764a5d697e65c3e))
* **ignore:** update zigbee-herdsman-converters to 25.112.0 ([#30767](https://github.com/Koenkk/zigbee2mqtt/issues/30767)) ([ad51d12](https://github.com/Koenkk/zigbee2mqtt/commit/ad51d12a2d1325e0b0c1b56a633b229f0f3d048b))
* **ignore:** update zigbee-herdsman-converters to 25.113.0 ([#30789](https://github.com/Koenkk/zigbee2mqtt/issues/30789)) ([036ee47](https://github.com/Koenkk/zigbee2mqtt/commit/036ee471ddfda094a3ae3f970dedb5600c9d8808))
* **ignore:** update zigbee-herdsman-converters to 25.114.0 ([#30803](https://github.com/Koenkk/zigbee2mqtt/issues/30803)) ([999df29](https://github.com/Koenkk/zigbee2mqtt/commit/999df2913ff7e47fe9438ed9398a0922c0b40916))
* **ignore:** update zigbee-herdsman-converters to 25.115.0 ([#30826](https://github.com/Koenkk/zigbee2mqtt/issues/30826)) ([99e06a8](https://github.com/Koenkk/zigbee2mqtt/commit/99e06a8d3ea66ed2a6acf6cddece2f2070cce6ae))
* **ignore:** update zigbee-herdsman-converters to 25.116.0 ([#30839](https://github.com/Koenkk/zigbee2mqtt/issues/30839)) ([5a4d6b5](https://github.com/Koenkk/zigbee2mqtt/commit/5a4d6b56ad2ea62378b7dd161eba31916f08c65b))
* **ignore:** update zigbee-herdsman-converters to 25.117.0 ([#30849](https://github.com/Koenkk/zigbee2mqtt/issues/30849)) ([4afbce6](https://github.com/Koenkk/zigbee2mqtt/commit/4afbce62ebc667fc7d94b8b23c0bb0125f065d79))
* **ignore:** update zigbee-herdsman-converters to 25.118.0 ([#30865](https://github.com/Koenkk/zigbee2mqtt/issues/30865)) ([f688340](https://github.com/Koenkk/zigbee2mqtt/commit/f68834049cde265f731ab271b50fcfa6f85fa971))
* **ignore:** update zigbee-herdsman-converters to 25.119.0 ([#30874](https://github.com/Koenkk/zigbee2mqtt/issues/30874)) ([788b0d3](https://github.com/Koenkk/zigbee2mqtt/commit/788b0d3993c877adc0361a4b2f9d2df79cea0b8d))
* **ignore:** update zigbee-herdsman-converters to 25.120.0 ([#30876](https://github.com/Koenkk/zigbee2mqtt/issues/30876)) ([49b752d](https://github.com/Koenkk/zigbee2mqtt/commit/49b752da3527920dcdfae6a617f4aa084f6a8901))
* **ignore:** update zigbee-herdsman-converters to 25.99.0 ([#30481](https://github.com/Koenkk/zigbee2mqtt/issues/30481)) ([3adf7d5](https://github.com/Koenkk/zigbee2mqtt/commit/3adf7d5d8ec6da3fabbd9f293d5d0bcaef3c9144))
* Log error before renaming failed to load converters ([#30436](https://github.com/Koenkk/zigbee2mqtt/issues/30436)) ([1eaa2f3](https://github.com/Koenkk/zigbee2mqtt/commit/1eaa2f3e759351bc746dec2ccc57b92cdcf1b5d7))
* Network map: escape double-quotes and backslashes in device attributes ([#30746](https://github.com/Koenkk/zigbee2mqtt/issues/30746)) ([5de47a0](https://github.com/Koenkk/zigbee2mqtt/commit/5de47a0d93f5fdc187e4f8a7489580a107a2fd49))
* OTA availability detection ([#30815](https://github.com/Koenkk/zigbee2mqtt/issues/30815)) ([476efaa](https://github.com/Koenkk/zigbee2mqtt/commit/476efaa6e18a1d2ea069ea9c128536356df547ac))
## [2.7.2](https://github.com/Koenkk/zigbee2mqtt/compare/2.7.1...2.7.2) (2026-01-01)
### Bug Fixes
* Biome floating promises detection ([#30137](https://github.com/Koenkk/zigbee2mqtt/issues/30137)) ([0025ef8](https://github.com/Koenkk/zigbee2mqtt/commit/0025ef87c502970b94e1b720516d42631e21323c))
* Don't fail to start when removing old log directory fails ([#30216](https://github.com/Koenkk/zigbee2mqtt/issues/30216)) ([b39b8d4](https://github.com/Koenkk/zigbee2mqtt/commit/b39b8d478e99c5a5ee5a3a610547168b3724ca91))
* **ignore:** bump @types/node from 24.10.2 to 24.10.4 in the minor-patch group ([#30390](https://github.com/Koenkk/zigbee2mqtt/issues/30390)) ([93c9704](https://github.com/Koenkk/zigbee2mqtt/commit/93c9704fcd5552e234dd03a7ae1cddc11403622b))
* **ignore:** bump the minor-patch group across 1 directory with 4 updates ([#30117](https://github.com/Koenkk/zigbee2mqtt/issues/30117)) ([ad3c090](https://github.com/Koenkk/zigbee2mqtt/commit/ad3c09036b804eb108238036f22776d83ea371b2))
* **ignore:** bump the minor-patch group with 2 updates ([#30207](https://github.com/Koenkk/zigbee2mqtt/issues/30207)) ([1d15c21](https://github.com/Koenkk/zigbee2mqtt/commit/1d15c2142fefb9c7ce96c947e5ac7ddc6101439f))
* **ignore:** bump the minor-patch group with 2 updates ([#30309](https://github.com/Koenkk/zigbee2mqtt/issues/30309)) ([b10fa93](https://github.com/Koenkk/zigbee2mqtt/commit/b10fa93700febe84634040dbdeb4857258cd2882))
* **ignore:** bump zigbee2mqtt-windfront from 2.6.1 to 2.6.2 in the minor-patch group ([#30412](https://github.com/Koenkk/zigbee2mqtt/issues/30412)) ([4d6269e](https://github.com/Koenkk/zigbee2mqtt/commit/4d6269e7e89af52df0b2576925abade2f6b60e09))
* **ignore:** update zigbee-herdsman to 7.0.5 ([#30116](https://github.com/Koenkk/zigbee2mqtt/issues/30116)) ([4574ffc](https://github.com/Koenkk/zigbee2mqtt/commit/4574ffc71a799846c6a04121c3b5908f968ff0be))
* **ignore:** update zigbee-herdsman to 7.0.6 ([#30142](https://github.com/Koenkk/zigbee2mqtt/issues/30142)) ([ecb0af4](https://github.com/Koenkk/zigbee2mqtt/commit/ecb0af40575ebaadf887685c50cd1a9968cc1c68))
* **ignore:** update zigbee-herdsman to 8.0.0 and zigbee-herdsman-converters to 25.87.0 ([#30186](https://github.com/Koenkk/zigbee2mqtt/issues/30186)) ([7f5f6a5](https://github.com/Koenkk/zigbee2mqtt/commit/7f5f6a5849d940a6e5288865edf7999c41af12e2))
* **ignore:** update zigbee-herdsman to 8.0.1 ([#30240](https://github.com/Koenkk/zigbee2mqtt/issues/30240)) ([1688a3b](https://github.com/Koenkk/zigbee2mqtt/commit/1688a3b5323a8774ad2c36775fdb99f48057f72a))
* **ignore:** update zigbee-herdsman-converters to 25.84.0 ([#30081](https://github.com/Koenkk/zigbee2mqtt/issues/30081)) ([c18d132](https://github.com/Koenkk/zigbee2mqtt/commit/c18d1329c801ce48cf5277c00682ad2f9098f383))
* **ignore:** update zigbee-herdsman-converters to 25.85.0 ([#30120](https://github.com/Koenkk/zigbee2mqtt/issues/30120)) ([5db6ae0](https://github.com/Koenkk/zigbee2mqtt/commit/5db6ae0a197a180abc5e497aebe6e59fcb9b6b3c))
* **ignore:** update zigbee-herdsman-converters to 25.86.0 ([#30143](https://github.com/Koenkk/zigbee2mqtt/issues/30143)) ([3460f15](https://github.com/Koenkk/zigbee2mqtt/commit/3460f157df84d10f8de87a37d6b0b0b243de6874))
* **ignore:** update zigbee-herdsman-converters to 25.88.0 ([#30196](https://github.com/Koenkk/zigbee2mqtt/issues/30196)) ([3818c50](https://github.com/Koenkk/zigbee2mqtt/commit/3818c50b8ae516e56ad07e8fce3c451f35e1b678))
* **ignore:** update zigbee-herdsman-converters to 25.89.0 ([#30215](https://github.com/Koenkk/zigbee2mqtt/issues/30215)) ([01304cf](https://github.com/Koenkk/zigbee2mqtt/commit/01304cfc6604b86acf181dc3f07688f56ecbe8a1))
* **ignore:** update zigbee-herdsman-converters to 25.90.0 ([#30267](https://github.com/Koenkk/zigbee2mqtt/issues/30267)) ([ab08947](https://github.com/Koenkk/zigbee2mqtt/commit/ab0894715a002bd5c14d8af45a67e5e546889590))
* **ignore:** update zigbee-herdsman-converters to 25.91.0 ([#30279](https://github.com/Koenkk/zigbee2mqtt/issues/30279)) ([4c02e3d](https://github.com/Koenkk/zigbee2mqtt/commit/4c02e3da8f66cafbdcde89303bb1aea6eaf73bcd))
* **ignore:** update zigbee-herdsman-converters to 25.92.0 ([#30300](https://github.com/Koenkk/zigbee2mqtt/issues/30300)) ([c28a024](https://github.com/Koenkk/zigbee2mqtt/commit/c28a024664989fe8a044babe3cdf43403b0ee2b5))
* **ignore:** update zigbee-herdsman-converters to 25.93.0 ([#30331](https://github.com/Koenkk/zigbee2mqtt/issues/30331)) ([c61f67f](https://github.com/Koenkk/zigbee2mqtt/commit/c61f67f061d2766e99c5cb9c99c02b172832271c))
* **ignore:** update zigbee-herdsman-converters to 25.94.0 ([#30355](https://github.com/Koenkk/zigbee2mqtt/issues/30355)) ([7cb5c58](https://github.com/Koenkk/zigbee2mqtt/commit/7cb5c58dc028f86c59935b64a738444950ef8e12))
* **ignore:** update zigbee-herdsman-converters to 25.95.0 ([#30371](https://github.com/Koenkk/zigbee2mqtt/issues/30371)) ([4c48e9c](https://github.com/Koenkk/zigbee2mqtt/commit/4c48e9cfbcedb9cd6aedee9f694db2993038eaee))
* **ignore:** update zigbee-herdsman-converters to 25.96.0 ([#30383](https://github.com/Koenkk/zigbee2mqtt/issues/30383)) ([2721b0e](https://github.com/Koenkk/zigbee2mqtt/commit/2721b0ea78f4e86e6fe3b5f4831c5787a28585a8))
* **ignore:** update zigbee-herdsman-converters to 25.97.0 ([#30396](https://github.com/Koenkk/zigbee2mqtt/issues/30396)) ([3727f07](https://github.com/Koenkk/zigbee2mqtt/commit/3727f078ecde73a5c444c42909f36b4a0f35d8c3))
* **ignore:** update zigbee-herdsman-converters to 25.98.0 ([#30417](https://github.com/Koenkk/zigbee2mqtt/issues/30417)) ([0eba0c9](https://github.com/Koenkk/zigbee2mqtt/commit/0eba0c953d7543b2850a9b9672ffbc3d87840fb3))
* log dir tz format not working on some systems ([#30324](https://github.com/Koenkk/zigbee2mqtt/issues/30324)) ([58d98c7](https://github.com/Koenkk/zigbee2mqtt/commit/58d98c7263dbe687eb677199eb760799ecff8e8f))
* Proper timezone in logs dir ([#30297](https://github.com/Koenkk/zigbee2mqtt/issues/30297)) ([8549b37](https://github.com/Koenkk/zigbee2mqtt/commit/8549b372283da8b55804c4049c7341294ced1538))
* Remove extra `>` in onboarding([#30065](https://github.com/Koenkk/zigbee2mqtt/issues/30065)) ([ed1e7b3](https://github.com/Koenkk/zigbee2mqtt/commit/ed1e7b333f481a4b3a37b021f2058331b86ebd84))
## [2.7.1](https://github.com/Koenkk/zigbee2mqtt/compare/2.7.0...2.7.1) (2025-12-06)
+7 -2
View File
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.12/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
@@ -12,7 +12,7 @@
"bracketSpacing": false
},
"files": {
"includes": ["**", "!package.json"]
"includes": ["**", "!package.json", "!!**/dist", "!!**/coverage"]
},
"linter": {
"includes": ["**"],
@@ -72,6 +72,11 @@
"suspicious": {
"noConstEnum": "off",
"useAwait": "error"
},
"nursery": {
"useExhaustiveSwitchCases": "error",
"noFloatingPromises": "error",
"noMisusedPromises": "error"
}
}
},
+2 -2
View File
@@ -1,5 +1,5 @@
# Indicates the configuration version (used by configuration migrations)
version: 4
version: 5
# Home Assistant integration (MQTT discovery)
homeassistant:
@@ -15,7 +15,7 @@ mqtt:
# MQTT base topic for zigbee2mqtt MQTT messages
base_topic: zigbee2mqtt
# MQTT server URL
server: 'mqtt://localhost'
server: "mqtt://localhost"
# MQTT server authentication, uncomment if required:
# user: my_user
# password: my_password
+1 -1
View File
@@ -1,6 +1,6 @@
ARG TARGETPLATFORM
FROM alpine:3.22 AS base
FROM alpine:3.23 AS base
ENV NODE_ENV=production
WORKDIR /app
+2 -2
View File
@@ -174,11 +174,11 @@ async function handleQuit() {
if (require.main === module || require.main.filename.endsWith(`${path.sep}cli.js`)) {
if (process.argv.length === 3 && process.argv[2] === "writehash") {
writeHash();
void writeHash();
} else {
process.on("SIGINT", handleQuit);
process.on("SIGTERM", handleQuit);
start();
void start();
}
} else {
process.on("SIGINT", handleQuit);
+8 -7
View File
@@ -99,7 +99,11 @@ export class Controller {
// Start zigbee
try {
await this.zigbee.start();
this.eventBus.onAdapterDisconnected(this, this.onZigbeeAdapterDisconnected);
this.eventBus.onAdapterDisconnected(this, async () => {
logger.error("Adapter disconnected, stopping");
await this.stop(false, 2);
});
} catch (error) {
logger.error("Failed to start zigbee-herdsman");
logger.error(
@@ -157,7 +161,9 @@ export class Controller {
}
}
this.eventBus.onLastSeenChanged(this, (data) => utils.publishLastSeen(data, settings.get(), false, this.publishEntityState));
this.eventBus.onLastSeenChanged(this, (data) => {
utils.publishLastSeen(data, settings.get(), false, this.publishEntityState).catch(() => {});
});
logger.info("Zigbee2MQTT started!");
@@ -317,11 +323,6 @@ export class Controller {
return await this.exitCallback(code, restart);
}
@bind async onZigbeeAdapterDisconnected(): Promise<void> {
logger.error("Adapter disconnected, stopping");
await this.stop(false, 2);
}
@bind async publishEntityState(entity: Group | Device, payload: KeyValue, stateChangeReason?: StateChangeReason): Promise<void> {
let message: Zigbee2MQTTAPI["{friendlyName}"] = {...payload};
+1 -1
View File
@@ -61,7 +61,7 @@ export default class EventBus {
public emitAdapterDisconnected(): void {
this.emitter.emit("adapterDisconnected");
}
public onAdapterDisconnected(key: ListenerKey, callback: () => void): void {
public onAdapterDisconnected(key: ListenerKey, callback: () => Promise<void>): void {
this.on("adapterDisconnected", callback, key);
}
+11 -17
View File
@@ -1,4 +1,5 @@
import fs from "node:fs";
import path from "node:path";
import bind from "bind-decorator";
import stringify from "json-stable-stringify-without-jsonify";
import JSZip from "jszip";
@@ -319,14 +320,18 @@ export default class Bridge extends Extension {
@bind async backup(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/backup">> {
await this.zigbee.backup();
const dataPath = data.getPath();
const files = utils
.getAllFiles(dataPath)
.map((f) => [f, f.substring(dataPath.length + 1)])
.filter((f) => !f[1].startsWith("log"));
const files = utils.getAllFiles(dataPath);
const zip = new JSZip();
const logDir = `log${path.sep}`;
const otaDir = `ota${path.sep}`;
for (const f of files) {
zip.file(f[1], fs.readFileSync(f[0]));
const name = f.slice(dataPath.length + 1);
// XXX: `log` could technically be something else depending on `log_directory` setting
if (!name.startsWith(logDir) && !name.startsWith(otaDir)) {
zip.file(name, fs.readFileSync(f));
}
}
const base64Zip = await zip.generateAsync({type: "base64"});
@@ -568,19 +573,8 @@ export default class Bridge extends Extension {
}
const device = this.getEntity("device", message.id);
logger.info(`Interviewing '${device.name}'`);
try {
await device.zh.interview(true);
logger.info(`Successfully interviewed '${device.name}'`);
} catch (error) {
throw new Error(`interview of '${device.name}' (${device.ieeeAddr}) failed: ${error}`, {cause: error});
}
// A re-interview can for example result in a different modelId, therefore reconsider the definition.
await device.resolveDefinition(true);
this.eventBus.emitDevicesChanged();
this.eventBus.emitExposesChanged({device});
await device.reInterview(this.eventBus);
return utils.getResponse(message, {id: message.id});
}
+1
View File
@@ -75,6 +75,7 @@ export default class Configure extends Extension {
await this.configure(data.device, "zigbee_event");
});
// TODO: this is triggering for any `data.status`, but should only for `successful`? (relies on `device.definition?` early-return?)
this.eventBus.onDeviceInterview(this, (data) => this.configure(data.device, "zigbee_event"));
this.eventBus.onLastSeenChanged(this, (data) => this.configure(data.device, "zigbee_event"));
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
+3 -3
View File
@@ -202,14 +202,14 @@ export default abstract class ExternalJSExtension<M> extends Extension {
const mod = await this.importFile(filePath);
await this.loadJS(extension.name, mod.default);
} catch (error) {
// change ext so Z2M doesn't try to load it again and again
fs.renameSync(filePath, `${filePath}.invalid`);
logger.error(
`Invalid external ${this.mqttTopic} '${extension.name}' was ignored and renamed to prevent interference with Zigbee2MQTT. (${(error as Error).message})`,
);
// biome-ignore lint/style/noNonNullAssertion: always Error
logger.debug((error as Error).stack!);
// change ext so Z2M doesn't try to load it again and again
fs.renameSync(filePath, `${filePath}.invalid`);
}
}
}
+32 -3
View File
@@ -50,6 +50,10 @@ const COVER_STOPPED_LOOKUP: ReadonlyArray<string> = ["stopped", "stop", "pause",
const SWITCH_DIFFERENT: ReadonlyArray<string> = ["valve_detection", "window_detection", "auto_lock", "away_mode"];
const BINARY_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
activity_led_indicator: {icon: "mdi:led-on"},
area1Occupancy: {device_class: "occupancy"},
area2Occupancy: {device_class: "occupancy"},
area3Occupancy: {device_class: "occupancy"},
area4Occupancy: {device_class: "occupancy"},
auto_off: {icon: "mdi:flash-auto"},
battery_low: {entity_category: "diagnostic", device_class: "battery"},
button_lock: {entity_category: "config", icon: "mdi:lock"},
@@ -150,7 +154,7 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
},
distance: {device_class: "distance", state_class: "measurement"},
duration: {entity_category: "config", icon: "mdi:timer"},
eco2: {device_class: "carbon_dioxide", state_class: "measurement"},
eco2: {device_class: "volatile_organic_compounds_parts", state_class: "measurement"},
eco_temperature: {entity_category: "config", icon: "mdi:thermometer"},
energy: {device_class: "energy", state_class: "total_increasing"},
external_temperature_input: {device_class: "temperature", icon: "mdi:thermometer"},
@@ -158,6 +162,7 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
external_humidity: {device_class: "humidity", icon: "mdi:water-percent"},
formaldehyd: {state_class: "measurement"},
flow: {device_class: "volume_flow_rate", state_class: "measurement"},
gas: {device_class: "gas", state_class: "total_increasing", icon: "mdi:meter-gas"},
gas_density: {icon: "mdi:google-circles-communities", state_class: "measurement"},
hcho: {icon: "mdi:air-filter", state_class: "measurement"},
humidity: {device_class: "humidity", state_class: "measurement"},
@@ -166,6 +171,7 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
humidity_min: {entity_category: "config", icon: "mdi:water-percent"},
illuminance_calibration: {entity_category: "config", icon: "mdi:wrench-clock"},
illuminance: {device_class: "illuminance", state_class: "measurement"},
illuminance_raw: {state_class: "measurement"},
internalTemperature: {
device_class: "temperature",
entity_category: "diagnostic",
@@ -177,6 +183,7 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
icon: "mdi:signal",
state_class: "measurement",
},
load_estimate: {state_class: "measurement"},
local_temperature: {device_class: "temperature", state_class: "measurement"},
max_range: {entity_category: "config", icon: "mdi:signal-distance-variant"},
max_temperature: {entity_category: "config", icon: "mdi:thermometer-high"},
@@ -420,6 +427,7 @@ export class HomeAssistant extends Extension {
this.eventBus.onGroupMembersChanged(this, this.onGroupMembersChanged);
this.eventBus.onDeviceAnnounce(this, this.onZigbeeEvent);
this.eventBus.onDeviceJoined(this, this.onZigbeeEvent);
// TODO: this is triggering for any `data.status`?
this.eventBus.onDeviceInterview(this, this.onZigbeeEvent);
this.eventBus.onDeviceMessage(this, this.onZigbeeEvent);
this.eventBus.onScenesChanged(this, this.onScenesChanged);
@@ -689,9 +697,8 @@ export class HomeAssistant extends Extension {
command_topic: true,
command_topic_prefix: endpoint,
command_topic_postfix: tempCalibration.property,
device_class: "temperature",
device_class: "temperature_delta",
entity_category: "config",
icon: "mdi:math-compass",
...(tempCalibration.unit && {unit_of_measurement: tempCalibration.unit}),
},
};
@@ -752,6 +759,28 @@ export class HomeAssistant extends Extension {
discoveryEntries.push(discoveryEntry);
}
const localTemperature = (firstExpose as zhc.Climate).features.filter(isNumericExpose).find((f) => f.name === "local_temperature");
const temperatureSensor = allExposes?.filter(isNumericExpose).find((e) => e.name === "temperature" && e.access & ACCESS_STATE);
const localTemperatureSensor = allExposes
?.filter(isNumericExpose)
.find((e) => e.name === "local_temperature" && e.access & ACCESS_STATE);
if (localTemperature && !temperatureSensor && !localTemperatureSensor) {
const discoveryEntry: DiscoveryEntry = {
type: "sensor",
object_id: endpoint ? `${localTemperature.name}_${endpoint}` : `${localTemperature.name}`,
mockProperties: [{property: localTemperature.property, value: null}],
discovery_payload: {
name: endpoint ? `${localTemperature.label} ${endpoint}` : localTemperature.label,
value_template: `{{ value_json.${localTemperature.property} }}`,
...(localTemperature.unit && {unit_of_measurement: localTemperature.unit}),
device_class: "temperature",
state_class: "measurement",
},
};
discoveryEntries.push(discoveryEntry);
}
const currentHumidity = allExposes?.filter(isNumericExpose).find((e) => e.name === "humidity" && e.access & ACCESS_STATE);
if (currentHumidity) {
discoveryEntry.discovery_payload.current_humidity_template = `{{ value_json.${currentHumidity.property} }}`;
+3 -1
View File
@@ -92,6 +92,8 @@ export default class NetworkMap extends Extension {
}
labels.push(lastSeen);
// Escape backslashes and double-quotes to avoid breaking Graphviz .dot files.
const escapedLabels = labels.map((label) => label.replace(/\\/g, "\\\\").replace(/"/g, '\\"'));
// Shape the record according to device type
if (node.type === "Coordinator") {
@@ -103,7 +105,7 @@ export default class NetworkMap extends Extension {
}
// Add the device with its labels to the graph as a node.
text += ` "${node.ieeeAddr}" [${style}, label="{${labels.join("|")}}"];\n`;
text += ` "${node.ieeeAddr}" [${style}, label="{${escapedLabels.join("|")}}"];\n`;
/**
* Add an edge between the device and its child to the graph
+2
View File
@@ -37,6 +37,8 @@ export default class OnEvent extends Extension {
}
});
this.eventBus.onDeviceInterview(this, async (data) => {
// TODO: this is triggering for any `data.status`, any use outside `successful`?
// ZHC definition would skip from `device.definition?` but OnEvent from ZHC index wouldn't => triple triggering?
await this.callOnEvent(data.device, {type: "deviceInterview", data: {...this.#getOnEventBaseData(data.device), status: data.status}});
});
this.eventBus.onDeviceAnnounce(this, async (data) => {
+282 -197
View File
@@ -1,10 +1,10 @@
import assert from "node:assert";
import path from "node:path";
import {existsSync, mkdirSync, rmSync, writeFileSync} from "node:fs";
import {join} from "node:path";
import bind from "bind-decorator";
import stringify from "json-stable-stringify-without-jsonify";
import {Zcl} from "zigbee-herdsman";
import type {Ota} from "zigbee-herdsman-converters";
import {ota} from "zigbee-herdsman-converters";
import {setOtaConfiguration, Zcl} from "zigbee-herdsman";
import type {OtaDataSettings, OtaSource, OtaUpdateAvailableResult} from "zigbee-herdsman/dist/controller/tstype";
import Device from "../model/device";
import type {Zigbee2MQTTAPI} from "../types/api";
import dataDir from "../util/data";
@@ -22,54 +22,65 @@ export interface UpdatePayload {
state: UpdateState;
installed_version: number | null;
latest_version: number | null;
latest_source: string | null;
latest_release_notes: string | null;
};
}
/**
* Write to `dataDir` and return created path
*/
function writeFirmwareHexToDataDir(hex: string, fileName: string | undefined, deviceIeee: string): string {
if (!fileName) {
fileName = `${deviceIeee}_${Date.now()}`;
}
const baseDir = dataDir.joinPath("ota");
if (!existsSync(baseDir)) {
mkdirSync(baseDir, {recursive: true});
}
const filePath = join(baseDir, fileName);
writeFileSync(filePath, Buffer.from(hex, "hex"));
return filePath;
}
export default class OTAUpdate extends Extension {
#topicRegex = new RegExp(
`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check|schedule|unschedule)/?(downgrade)?`,
"i",
);
private inProgress = new Set<string>();
private lastChecked = new Map<string, number>();
private scheduledUpgrades = new Set<string>();
private scheduledDowngrades = new Set<string>();
#inProgress = new Set<string>();
#lastChecked = new Map<string, number>();
// biome-ignore lint/suspicious/useAwait: API
override async start(): Promise<void> {
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
this.eventBus.onDeviceMessage(this, this.onZigbeeEvent);
const otaSettings = settings.get().ota;
// Let OTA module know if the override index file is provided
let overrideIndexLocation = otaSettings.zigbee_ota_override_index_location;
// If the file name is not a full path, then treat it as a relative to the data directory
if (overrideIndexLocation && !ota.isValidUrl(overrideIndexLocation) && !path.isAbsolute(overrideIndexLocation)) {
overrideIndexLocation = dataDir.joinPath(overrideIndexLocation);
}
// In order to support local firmware files we need to let zigbeeOTA know where the data directory is
ota.setConfiguration({
dataDir: dataDir.getPath(),
overrideIndexLocation,
// TODO: implement me
imageBlockResponseDelay: otaSettings.image_block_response_delay,
defaultMaximumDataSize: otaSettings.default_maximum_data_size,
});
setOtaConfiguration(dataDir.getPath(), settings.get().ota.zigbee_ota_override_index_location);
// 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);
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";
this.state.get(device).update.state = "idle";
}
}
}
private removeProgressAndRemainingFromState(device: Device): void {
// mostly intended for testing
clearState(): void {
this.#inProgress.clear();
this.#lastChecked.clear();
}
#removeProgressAndRemainingFromState(device: Device): void {
const deviceState = this.state.get(device);
if (deviceState.update) {
@@ -79,7 +90,7 @@ export default class OTAUpdate extends Extension {
}
@bind private async onZigbeeEvent(data: eventdata.DeviceMessage): Promise<void> {
if (data.type !== "commandQueryNextImageRequest" || !data.device.definition || this.inProgress.has(data.device.ieeeAddr)) {
if (data.type !== "commandQueryNextImageRequest" || !data.device.definition || this.#inProgress.has(data.device.ieeeAddr)) {
return;
}
@@ -91,115 +102,84 @@ export default class OTAUpdate extends Extension {
logger.debug(`Device '${data.device.name}' requested OTA`);
if (data.device.definition.ota) {
if (this.scheduledUpgrades.has(data.device.ieeeAddr) || this.scheduledDowngrades.has(data.device.ieeeAddr)) {
this.inProgress.add(data.device.ieeeAddr);
if (data.device.zh.scheduledOta) {
// allow custom source to override check for definition `ota`
if (data.device.zh.scheduledOta?.url !== undefined || data.device.definition.ota) {
this.#inProgress.add(data.device.ieeeAddr);
logger.info(`Updating '${data.device.name}' to latest firmware`);
try {
const fileVersion = await ota.update(
data.device.zh,
data.device.otaExtraMetas,
this.scheduledDowngrades.has(data.device.ieeeAddr),
async (progress, remaining) => {
let msg = `Update of '${data.device.name}' at ${progress.toFixed(2)}%`;
if (remaining) {
msg += `, ≈ ${Math.round(remaining / 60)} minutes remaining`;
}
logger.info(msg);
await this.publishEntityState(
data.device,
this.getEntityPublishPayload(data.device, "updating", progress, remaining ?? undefined),
);
},
data.data as Ota.ImageInfo,
data.meta.zclTransactionSequenceNumber,
);
// remove right away on update success or no image in case any of the below calls fail
this.scheduledUpgrades.delete(data.device.ieeeAddr);
this.scheduledDowngrades.delete(data.device.ieeeAddr);
if (fileVersion === undefined) {
logger.info(`No image currently available for '${data.device.name}'. Unscheduling.`);
// XXX: superfluous?
this.removeProgressAndRemainingFromState(data.device);
await this.publishEntityState(data.device, this.getEntityPublishPayload(data.device, "idle"));
this.inProgress.delete(data.device.ieeeAddr);
return;
}
logger.info(`Finished update of '${data.device.name}'`);
this.removeProgressAndRemainingFromState(data.device);
await this.publishEntityState(
const otaSettings = settings.get().ota;
const [, toVersion] = await this.#update(
undefined, // uses internally registered schedule
data.device,
this.getEntityPublishPayload(data.device, {available: false, currentFileVersion: fileVersion, otaFileVersion: fileVersion}),
data.data as Zcl.ClustersTypes.TClusterCommandPayload<"genOta", "queryNextImageRequest">,
data.meta.zclTransactionSequenceNumber,
{
// fallbacks are only to satisfy typing, should always be defined from settings defaults
requestTimeout: otaSettings.image_block_request_timeout ?? /* v8 ignore next */ 150000,
responseDelay: otaSettings.image_block_response_delay ?? /* v8 ignore next */ 250,
baseSize: otaSettings.default_maximum_data_size ?? /* v8 ignore next */ 50,
},
data.endpoint,
);
const firmwareTo = await this.readSoftwareBuildIDAndDateCode(data.device);
logger.info(() => `Device '${data.device.name}' was updated to '${stringify(firmwareTo)}'`);
/**
* Re-configure after reading software build ID and date code, some devices use a
* custom attribute for this (e.g. Develco SMSZB-120)
*/
this.eventBus.emitReconfigure({device: data.device});
this.eventBus.emitDevicesChanged();
if (toVersion === undefined) {
logger.info(`No OTA image currently available for '${data.device.name}'. Unscheduled.`);
}
} catch (e) {
logger.debug(`Update of '${data.device.name}' failed (${e}). Retry scheduled for next request.`);
logger.debug(`OTA update of '${data.device.name}' failed (${e}). Retry scheduled for next request.`);
this.removeProgressAndRemainingFromState(data.device);
await this.publishEntityState(data.device, this.getEntityPublishPayload(data.device, "scheduled"));
this.#removeProgressAndRemainingFromState(data.device);
await this.publishEntityState(data.device, this.#getEntityPublishPayload(data.device, "scheduled"));
}
this.inProgress.delete(data.device.ieeeAddr);
this.#inProgress.delete(data.device.ieeeAddr);
return; // we're done
}
}
if (!settings.get().ota.disable_automatic_update_check) {
if (data.device.definition.ota) {
if (!data.device.options.disable_automatic_update_check && !settings.get().ota.disable_automatic_update_check) {
// When a device does a next image request, it will usually do it a few times after each other
// with only 10 - 60 seconds inbetween. It doesn't make sense to check for a new update
// each time, so this interval can be set by the user. The default is 1,440 minutes (one day).
const updateCheckInterval = settings.get().ota.update_check_interval * 1000 * 60;
const deviceLastChecked = this.lastChecked.get(data.device.ieeeAddr);
const deviceLastChecked = this.#lastChecked.get(data.device.ieeeAddr);
const check = deviceLastChecked !== undefined ? Date.now() - deviceLastChecked > updateCheckInterval : true;
if (!check) {
return;
}
this.inProgress.add(data.device.ieeeAddr);
this.lastChecked.set(data.device.ieeeAddr, Date.now());
let availableResult: Ota.UpdateAvailableResult | undefined;
this.#inProgress.add(data.device.ieeeAddr);
this.#lastChecked.set(data.device.ieeeAddr, Date.now());
let availableResult: OtaUpdateAvailableResult | undefined;
try {
// never use 'previous' when responding to device request
availableResult = await ota.isUpdateAvailable(data.device.zh, data.device.otaExtraMetas, data.data as Ota.ImageInfo, false);
// auto-check defaults to zigbee-OTA + potential local index, and never `downgrade`
availableResult = await data.device.zh.checkOta(
{downgrade: false},
data.data as Zcl.ClustersTypes.TClusterCommandPayload<"genOta", "queryNextImageRequest">,
data.device.otaExtraMetas,
data.endpoint,
);
} catch (error) {
logger.debug(`Failed to check if update available for '${data.device.name}' (${error})`);
logger.debug(`Failed to check if OTA update available for '${data.device.name}' (${error})`);
}
await this.publishEntityState(data.device, this.getEntityPublishPayload(data.device, availableResult ?? "idle"));
await this.publishEntityState(data.device, this.#getEntityPublishPayload(data.device, availableResult ?? "idle"));
if (availableResult?.available) {
const message = `Update available for '${data.device.name}'`;
logger.info(message);
logger.info(`OTA update available for '${data.device.name}'`);
}
}
}
// Respond to stop the client from requesting OTAs
const endpoint = data.device.zh.endpoints.find((e) => e.supportsOutputCluster("genOta")) || data.endpoint;
await endpoint.commandResponse(
await data.endpoint.commandResponse(
"genOta",
"queryNextImageResponse",
{status: Zcl.Status.NO_IMAGE_AVAILABLE},
@@ -207,47 +187,53 @@ export default class OTAUpdate extends Extension {
data.meta.zclTransactionSequenceNumber,
);
logger.debug(`Responded to OTA request of '${data.device.name}' with 'NO_IMAGE_AVAILABLE'`);
this.inProgress.delete(data.device.ieeeAddr);
this.#inProgress.delete(data.device.ieeeAddr);
}
private async readSoftwareBuildIDAndDateCode(
async #readSoftwareBuildIDAndDateCode(
device: Device,
sendPolicy?: "immediate",
): Promise<{softwareBuildID: string; dateCode: string} | undefined> {
try {
const endpoint = device.zh.endpoints.find((e) => e.supportsInputCluster("genBasic"));
assert(endpoint);
const result = await endpoint.read("genBasic", ["dateCode", "swBuildId"], {sendPolicy});
return {softwareBuildID: result.swBuildId, dateCode: result.dateCode};
} catch {
return undefined;
}
}
private getEntityPublishPayload(
device: Device,
state: Ota.UpdateAvailableResult | UpdateState,
progress?: number,
remaining?: number,
): UpdatePayload {
const deviceUpdateState = this.state.get(device).update;
const payload: UpdatePayload = {
update: {
state: typeof state === "string" ? state : state.available ? "available" : "idle",
installed_version: typeof state === "string" ? deviceUpdateState?.installed_version : state.currentFileVersion,
latest_version: typeof state === "string" ? deviceUpdateState?.latest_version : state.otaFileVersion,
},
};
#getEntityPublishPayload(device: Device, state: OtaUpdateAvailableResult | UpdateState, progress?: number, remaining?: number): UpdatePayload {
const deviceUpdateState = this.state.get(device).update as UpdatePayload["update"];
const update: UpdatePayload["update"] =
typeof state === "string"
? {
state,
installed_version: deviceUpdateState?.installed_version,
latest_version: deviceUpdateState?.latest_version,
latest_source: deviceUpdateState?.latest_source,
latest_release_notes: deviceUpdateState?.latest_release_notes,
}
: {
state: state.available ? "available" : "idle",
installed_version: state.current.fileVersion,
latest_version: state.availableMeta?.fileVersion ?? state.current.fileVersion,
latest_source: state.availableMeta?.url || null,
latest_release_notes: state.availableMeta?.releaseNotes || null,
};
if (progress !== undefined) {
payload.update.progress = progress;
update.progress = progress;
}
if (remaining !== undefined) {
payload.update.remaining = Math.round(remaining);
update.remaining = Math.round(remaining);
}
return payload;
return {update};
}
@bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
@@ -265,7 +251,14 @@ export default class OTAUpdate extends Extension {
| Zigbee2MQTTAPI["bridge/request/device/ota_update/schedule"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/schedule/downgrade"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/unschedule"];
const ID = (typeof message === "object" && message.id !== undefined ? message.id : message) as string;
// TODO: deprecated 3.0 should remove string payload, enforce object
const messageObject = typeof message === "object";
if (messageObject) {
assert(message.id, "Invalid payload");
}
const ID = (messageObject ? message.id : message) as string;
const device = this.zigbee.resolveEntity(ID);
const type = topicMatch[1] as "check" | "update" | "schedule" | "unschedule";
const downgrade = topicMatch[2] === "downgrade";
@@ -274,34 +267,53 @@ export default class OTAUpdate extends Extension {
if (!(device instanceof Device)) {
error = `Device '${ID}' does not exist`;
} else if (!device.definition || !device.definition.ota) {
error = `Device '${device.name}' does not support OTA updates`;
} else if (this.inProgress.has(device.ieeeAddr)) {
} else if (this.#inProgress.has(device.ieeeAddr)) {
// also guards against scheduling while check/update op in progress that could result in undesired OTA state
error = `Update or check for update already in progress for '${device.name}'`;
error = `OTA update or check for update already in progress for '${device.name}'`;
} else {
switch (type) {
case "check": {
this.inProgress.add(device.ieeeAddr);
this.#inProgress.add(device.ieeeAddr);
logger.info(`Checking if update available for '${device.name}'`);
const source: OtaSource = {downgrade};
if (messageObject) {
const payload = message as
| Zigbee2MQTTAPI["bridge/request/device/ota_update/check"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/check/downgrade"];
if (payload.url) {
source.url = payload.url;
} else if (!device.definition || !device.definition.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
} else if (!device.definition || !device.definition.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
logger.info(`Checking if OTA update available for '${device.name}'`);
try {
const availableResult = await ota.isUpdateAvailable(device.zh, device.otaExtraMetas, undefined, downgrade);
const availableResult = await device.zh.checkOta(source, undefined, device.otaExtraMetas);
logger.info(`${availableResult.available ? "Update" : "No update"} available for '${device.name}'`);
logger.info(`${availableResult.available ? "" : "No "}OTA update available for '${device.name}'`);
await this.publishEntityState(device, this.getEntityPublishPayload(device, availableResult));
this.lastChecked.set(device.ieeeAddr, Date.now());
await this.publishEntityState(device, this.#getEntityPublishPayload(device, availableResult));
this.#lastChecked.set(device.ieeeAddr, Date.now());
const response = utils.getResponse<"bridge/response/device/ota_update/check">(message, {
id: ID,
update_available: availableResult.available,
downgrade: source.downgrade,
source: availableResult.availableMeta?.url,
release_notes: availableResult.availableMeta?.releaseNotes,
});
await this.mqtt.publish("bridge/response/device/ota_update/check", stringify(response));
} catch (e) {
error = `Failed to check if update available for '${device.name}' (${(e as Error).message})`;
error = `Failed to check if OTA update available for '${device.name}' (${(e as Error).message})`;
errorStack = (e as Error).stack;
}
@@ -309,94 +321,113 @@ export default class OTAUpdate extends Extension {
}
case "update": {
this.inProgress.add(device.ieeeAddr);
this.#inProgress.add(device.ieeeAddr);
if (this.scheduledUpgrades.delete(device.ieeeAddr)) {
logger.info(`Previously scheduled '${device.name}' upgrade was cancelled by manual update`);
} else if (this.scheduledDowngrades.delete(device.ieeeAddr)) {
logger.info(`Previously scheduled '${device.name}' downgrade was cancelled by manual update`);
}
const otaSettings = settings.get().ota;
const source: OtaSource = {downgrade};
const dataSettings: OtaDataSettings = {
// fallbacks are only to satisfy typing, should always be defined from settings defaults
requestTimeout: otaSettings.image_block_request_timeout ?? /* v8 ignore next */ 150000,
responseDelay: otaSettings.image_block_response_delay ?? /* v8 ignore next */ 250,
baseSize: otaSettings.default_maximum_data_size ?? /* v8 ignore next */ 50,
};
logger.info(`Updating '${device.name}' to ${downgrade ? "previous" : "latest"} firmware`);
if (messageObject) {
const payload = message as
| Zigbee2MQTTAPI["bridge/request/device/ota_update/update"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/update/downgrade"];
try {
const firmwareFrom = await this.readSoftwareBuildIDAndDateCode(device, "immediate");
const fileVersion = await ota.update(device.zh, device.otaExtraMetas, downgrade, async (progress, remaining) => {
let msg = `Update of '${device.name}' at ${progress.toFixed(2)}%`;
if (payload.hex) {
assert(payload.hex.data);
if (remaining) {
msg += `, ≈ ${Math.round(remaining / 60)} minutes remaining`;
}
logger.info(msg);
await this.publishEntityState(device, this.getEntityPublishPayload(device, "updating", progress, remaining ?? undefined));
});
if (fileVersion === undefined) {
throw new Error("No image currently available");
// write to `dataDir` and pass created path as source URL
source.url = writeFirmwareHexToDataDir(payload.hex.data, payload.hex.file_name, device.ieeeAddr);
} else if (payload.url) {
source.url = payload.url;
} else if (!device.definition || !device.definition.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
logger.info(`Finished update of '${device.name}'`);
this.removeProgressAndRemainingFromState(device);
await this.publishEntityState(
device,
this.getEntityPublishPayload(device, {available: false, currentFileVersion: fileVersion, otaFileVersion: fileVersion}),
);
if (payload.image_block_request_timeout) {
dataSettings.requestTimeout = payload.image_block_request_timeout;
}
const firmwareTo = await this.readSoftwareBuildIDAndDateCode(device);
if (payload.image_block_response_delay) {
dataSettings.responseDelay = payload.image_block_response_delay;
}
logger.info(() => `Device '${device.name}' was updated from '${stringify(firmwareFrom)}' to '${stringify(firmwareTo)}'`);
if (payload.default_maximum_data_size) {
dataSettings.baseSize = payload.default_maximum_data_size;
}
} else if (!device.definition || !device.definition.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
/**
* Re-configure after reading software build ID and date code, some devices use a
* custom attribute for this (e.g. Develco SMSZB-120)
*/
this.eventBus.emitReconfigure({device});
this.eventBus.emitDevicesChanged();
logger.info(`OTA updating '${device.name}' to ${downgrade ? "previous" : "latest"} firmware`);
try {
const firmwareFrom = await this.#readSoftwareBuildIDAndDateCode(device, "immediate");
const [fromVersion, toVersion] = await this.#update(source, device, undefined, undefined, dataSettings);
if (toVersion === undefined) {
error = `Update of '${device.name}' failed (No image currently available)`;
break;
}
const firmwareTo = await this.#readSoftwareBuildIDAndDateCode(device);
const response = utils.getResponse<"bridge/response/device/ota_update/update">(message, {
id: ID,
from: firmwareFrom ? {software_build_id: firmwareFrom.softwareBuildID, date_code: firmwareFrom.dateCode} : undefined,
to: firmwareTo ? {software_build_id: firmwareTo.softwareBuildID, date_code: firmwareTo.dateCode} : undefined,
from: {
file_version: fromVersion,
software_build_id: firmwareFrom?.softwareBuildID,
date_code: firmwareFrom?.dateCode,
},
to: {file_version: toVersion, software_build_id: firmwareTo?.softwareBuildID, date_code: firmwareTo?.dateCode},
});
await this.mqtt.publish("bridge/response/device/ota_update/update", stringify(response));
} catch (e) {
logger.debug(`Update of '${device.name}' failed (${e})`);
error = `Update of '${device.name}' failed (${(e as Error).message})`;
logger.debug(`OTA update of '${device.name}' failed (${e})`);
error = `OTA update of '${device.name}' failed (${(e as Error).message})`;
errorStack = (e as Error).stack;
this.removeProgressAndRemainingFromState(device);
await this.publishEntityState(device, this.getEntityPublishPayload(device, "available"));
this.#removeProgressAndRemainingFromState(device);
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "available"));
}
break;
}
case "schedule": {
// ensure only one type scheduled by deleting from the other if necessary
if (downgrade) {
if (this.scheduledUpgrades.delete(device.ieeeAddr)) {
logger.info(`Previously scheduled '${device.name}' upgrade was cancelled in favor of new downgrade request`);
}
const source: OtaSource = {downgrade};
this.scheduledDowngrades.add(device.ieeeAddr);
} else {
if (this.scheduledDowngrades.delete(device.ieeeAddr)) {
logger.info(`Previously scheduled '${device.name}' downgrade was cancelled in favor of new upgrade request`);
}
if (messageObject) {
const payload = message as
| Zigbee2MQTTAPI["bridge/request/device/ota_update/schedule"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/schedule/downgrade"];
this.scheduledUpgrades.add(device.ieeeAddr);
if (payload.hex) {
assert(payload.hex.data);
// write to `dataDir` and pass created path as source URL
source.url = writeFirmwareHexToDataDir(payload.hex.data, payload.hex.file_name, device.ieeeAddr);
} else if (payload.url) {
source.url = payload.url;
} else if (!device.definition || !device.definition.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
} else if (!device.definition || !device.definition.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
logger.info(`Scheduled '${device.name}' to ${downgrade ? "downgrade" : "upgrade"} firmware on next request from device`);
device.zh.scheduleOta(source);
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "scheduled"));
await this.publishEntityState(device, this.getEntityPublishPayload(device, "scheduled", undefined, undefined));
const response = utils.getResponse<"bridge/response/device/ota_update/schedule">(message, {
id: ID,
});
const response = utils.getResponse<"bridge/response/device/ota_update/schedule">(message, {id: ID, url: source.url});
await this.mqtt.publish("bridge/response/device/ota_update/schedule", stringify(response));
@@ -404,13 +435,12 @@ export default class OTAUpdate extends Extension {
}
case "unschedule": {
if (this.scheduledUpgrades.delete(device.ieeeAddr)) {
logger.info(`Previously scheduled '${device.name}' upgrade was cancelled`);
} else if (this.scheduledDowngrades.delete(device.ieeeAddr)) {
logger.info(`Previously scheduled '${device.name}' downgrade was cancelled`);
if (device.zh.scheduledOta?.url?.startsWith(dataDir.joinPath("ota"))) {
rmSync(device.zh.scheduledOta.url, {force: true});
}
await this.publishEntityState(device, this.getEntityPublishPayload(device, "idle", undefined, undefined));
device.zh.unscheduleOta();
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "idle"));
const response = utils.getResponse<"bridge/response/device/ota_update/unschedule">(message, {
id: ID,
@@ -422,7 +452,7 @@ export default class OTAUpdate extends Extension {
}
}
this.inProgress.delete(device.ieeeAddr);
this.#inProgress.delete(device.ieeeAddr);
}
if (error) {
@@ -436,4 +466,59 @@ export default class OTAUpdate extends Extension {
}
}
}
/**
* Do the bulk of the update work (hand over to zigbee-herdsman, then re-interview).
*
* `dataSettings` object may be mutated by zigbee-herdsman to fit request (e.g. known manuf quirk)
*/
async #update(
source: OtaSource | undefined,
device: Device,
requestPayload: Zcl.ClustersTypes.TClusterCommandPayload<"genOta", "queryNextImageRequest"> | undefined,
requestTsn: number | undefined,
dataSettings: OtaDataSettings,
endpoint?: zh.Endpoint,
): Promise<[from: number, to: number | undefined]> {
const [from, to] = await device.zh.updateOta(
source,
requestPayload,
requestTsn,
device.otaExtraMetas,
async (progress, remaining) => {
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "updating", progress, remaining));
},
dataSettings,
endpoint,
);
if (to === undefined) {
this.#removeProgressAndRemainingFromState(device);
await this.publishEntityState(device, this.#getEntityPublishPayload(device, {available: false, current: from}));
return [from.fileVersion, undefined];
}
logger.info(`Finished update of '${device.name}'`);
this.#removeProgressAndRemainingFromState(device);
await this.publishEntityState(device, this.#getEntityPublishPayload(device, {available: false, current: to}));
logger.info(() => `Device '${device.name}' was OTA updated from '${from.fileVersion}' to '${to.fileVersion}'`);
// OTA update can bring new features & co, force full re-interview and re-configure, same as a "device joined"
if (device.zh.meta.configured !== undefined) {
delete device.zh.meta.configured;
device.zh.save();
}
setTimeout(() => {
device.reInterview(this.eventBus).catch((error) => {
logger.error(`${error.message}. Re-try manually after some time.`);
});
}, 5000);
return [from.fileVersion, to.fileVersion];
}
}
+20 -2
View File
@@ -1,9 +1,10 @@
import assert from "node:assert";
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
import type {OtaExtraMetas} from "zigbee-herdsman/dist/controller/tstype";
import type {CustomClusters} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
import * as zhc from "zigbee-herdsman-converters";
import {access, Numeric} from "zigbee-herdsman-converters";
import logger from "../util/logger";
import * as settings from "../util/settings";
const LINKQUALITY = new Numeric("linkquality", access.STATE)
@@ -38,7 +39,7 @@ export default class Device {
get customClusters(): CustomClusters {
return this.zh.customClusters;
}
get otaExtraMetas(): zhc.Ota.ExtraMetas {
get otaExtraMetas(): OtaExtraMetas {
return typeof this.definition?.ota === "object" ? this.definition.ota : {};
}
get interviewed(): boolean {
@@ -69,6 +70,23 @@ export default class Device {
}
}
async reInterview(eventBus: EventBus): Promise<void> {
// logic follows that of receiving `deviceInterview` event from ZH layer
logger.info(`Interviewing '${this.name}'`);
eventBus.emitDeviceInterview({status: "started", device: this});
try {
await this.zh.interview(true);
// A re-interview can for example result in a different modelId, therefore reconsider the definition.
await this.resolveDefinition(true);
logger.info(`Successfully interviewed '${this.name}'`);
eventBus.emitDeviceInterview({status: "successful", device: this});
} catch (error) {
eventBus.emitDeviceInterview({status: "failed", device: this});
throw new Error(`Interview of '${this.name}' (${this.ieeeAddr}) failed: ${error}`);
}
}
ensureInSettings(): void {
if (this.zh.type !== "Coordinator" && !settings.getDevice(this.zh.ieeeAddr)) {
settings.addDevice(this.zh.ieeeAddr);
+48 -4
View File
@@ -55,6 +55,7 @@ export interface Zigbee2MQTTDeviceOptions {
friendly_name: string;
description?: string;
qos?: 0 | 1 | 2;
disable_automatic_update_check?: boolean;
}
export interface Zigbee2MQTTGroupOptions {
@@ -140,6 +141,7 @@ export interface Zigbee2MQTTSettings {
update_check_interval: number;
disable_automatic_update_check: boolean;
zigbee_ota_override_index_location?: string;
image_block_request_timeout?: number;
image_block_response_delay?: number;
default_maximum_data_size?: number;
};
@@ -599,51 +601,93 @@ export interface Zigbee2MQTTAPI {
"bridge/request/device/ota_update/check": {
id: string;
/** expected to point to an index json if provided */
url?: string | null;
};
"bridge/request/device/ota_update/check/downgrade": {
id: string;
/** expected to point to an index json if provided */
url?: string | null;
};
"bridge/response/device/ota_update/check": {
id: string;
update_available: boolean;
downgrade?: boolean;
source?: string;
release_notes?: string;
};
"bridge/request/device/ota_update/update": {
id: string;
url?: string | null;
/**
* Full firmware file in hex form (expected compatible with `Buffer.from(hex, "hex")`)
* If file name supplied, will be used instead of <ieee_utc> to store firmware in data dir.
*/
hex?: {data: string; file_name?: string} | null;
image_block_request_timeout?: number | null;
image_block_response_delay?: number | null;
/** may be overridden internally to match specific device needs */
default_maximum_data_size?: number | null;
};
"bridge/request/device/ota_update/update/downgrade": {
id: string;
url?: string | null;
/**
* Full firmware file in hex form (expected compatible with `Buffer.from(hex, "hex")`)
* If file name supplied, will be used instead of <ieee_utc> to store firmware in data dir.
*/
hex?: {data: string; file_name?: string} | null;
image_block_request_timeout?: number | null;
image_block_response_delay?: number | null;
/** may be overridden internally to match specific device needs */
default_maximum_data_size?: number | null;
};
"bridge/response/device/ota_update/update": {
id: string;
from:
| {
software_build_id: string;
date_code: string;
file_version: number;
software_build_id?: string;
date_code?: string;
}
| undefined;
to:
| {
software_build_id: string;
date_code: string;
file_version: number;
software_build_id?: string;
date_code?: string;
}
| undefined;
};
"bridge/request/device/ota_update/schedule": {
id: string;
url?: string | null;
/**
* Full firmware file in hex form (expected compatible with `Buffer.from(hex, "hex")`)
* If file name supplied, will be used instead of <ieee_utc> to store firmware in data dir.
*/
hex?: {data: string; file_name?: string} | null;
};
"bridge/request/device/ota_update/schedule/downgrade": {
id: string;
url?: string | null;
/**
* Full firmware file in hex form (expected compatible with `Buffer.from(hex, "hex")`)
* If file name supplied, will be used instead of <ieee_utc> to store firmware in data dir.
*/
hex?: {data: string; file_name?: string} | null;
};
"bridge/response/device/ota_update/schedule": {
id: string;
url?: string;
};
"bridge/request/device/ota_update/unschedule": {
+23 -18
View File
@@ -9,27 +9,25 @@ import * as settings from "./settings";
const NAMESPACE_SEPARATOR = ":";
class Logger {
// @ts-expect-error initalized in `init`
private level: settings.LogLevel;
// @ts-expect-error initalized in `init`
private output: string[];
// @ts-expect-error initalized in `init`
private directory: string;
// @ts-expect-error initalized in `init`
private logger: winston.Logger;
// @ts-expect-error initalized in `init`
private fileTransport: winston.transports.FileTransportInstance;
private level!: settings.LogLevel;
private output!: string[];
private directory!: string;
private logger!: winston.Logger;
private fileTransport: winston.transports.FileTransportInstance | undefined;
private debugNamespaceIgnoreRegex?: RegExp;
// @ts-expect-error initalized in `init`
private namespacedLevels: Record<string, settings.LogLevel>;
// @ts-expect-error initalized in `init`
private cachedNamespacedLevels: Record<string, settings.LogLevel>;
private namespacedLevels!: Record<string, settings.LogLevel>;
private cachedNamespacedLevels!: Record<string, settings.LogLevel>;
public init(): void {
// What transports to enable
this.output = settings.get().advanced.log_output;
// Directory to log to
const timestamp = new Date().toISOString().slice(0, 19).replace("T", ".").replace(/:/g, "-");
const date = new Date();
// offset UTC by current timezone, ISO keeps "Z" (UTC) which is then wrong but we strip it
const timestamp = new Date(date.getTime() - date.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 19)
.replace("T", ".")
.replaceAll(":", "-");
this.directory = settings.get().advanced.log_directory.replace("%TIMESTAMP%", timestamp);
const logFilename = settings.get().advanced.log_file.replace("%TIMESTAMP%", timestamp);
this.level = settings.get().advanced.log_level;
@@ -240,7 +238,11 @@ class Logger {
for (const dir of directories) {
this.debug(`Removing old log directory '${dir.path}'`);
rimrafSync(dir.path);
try {
rimrafSync(dir.path);
} catch (e) {
this.error(`Failed to remove old log directory '${dir.path}': ${e}`);
}
}
}
}
@@ -262,7 +264,10 @@ class Logger {
// @ts-expect-error workaround
this.fileTransport.on("open", () => this.fileTransport._dest.on("finish", resolve));
}
this.fileTransport.end();
if (this.fileTransport) {
this.fileTransport.end();
}
});
}
}
+1 -1
View File
@@ -146,7 +146,7 @@ function generateHtmlForm(currentSettings: RecursivePartial<Settings>, devices:
id="serial_rtscts"
name="serial_rtscts"
${currentSettings.serial?.rtscts ? "checked" : ""}
${process.env.ZIGBEE2MQTT_CONFIG_SERIAL_RTSCTS ? "disabled" : ""}>
${process.env.ZIGBEE2MQTT_CONFIG_SERIAL_RTSCTS ? "disabled" : ""}
style="margin-bottom: 1rem;">
<small>Can be ignored for networked coordinators (TCP).</small>
</fieldset>
+27 -4
View File
@@ -366,13 +366,20 @@
"description": "Location of override OTA index file",
"examples": ["index.json"]
},
"image_block_request_timeout": {
"type": "number",
"title": "Image block request timeout",
"description": "Timeout (in milliseconds) during OTA updates. You can increase this value if your device is requesting blocks too slowly.",
"default": 150000,
"minimum": 10000,
"maximum": 2147483647
},
"image_block_response_delay": {
"type": "number",
"title": "Image block response delay",
"description": "Limits the rate of requests (in milliseconds) during OTA updates to reduce network congestion. You can increase this value if your network appears unstable during OTA.",
"default": 250,
"minimum": 50,
"requiresRestart": true
"minimum": 50
},
"default_maximum_data_size": {
"type": "number",
@@ -380,8 +387,7 @@
"description": "The size of file chunks sent during an update (in bytes). Note: This value may get ignored for manufacturers that require specific values.",
"default": 50,
"minimum": 10,
"maximum": 100,
"requiresRestart": true
"maximum": 100
}
}
},
@@ -930,6 +936,12 @@
"description": "Name of the device in Home Assistant"
}
}
},
"disable_automatic_update_check": {
"type": "boolean",
"title": "Disable automatic update check",
"description": "Zigbee devices may request a firmware update, and do so frequently, causing Zigbee2MQTT to reach out to third party servers. If you disable these device initiated checks, you can still initiate a firmware update check manually.",
"default": false
}
},
"required": ["friendly_name"]
@@ -965,6 +977,17 @@
"items": {
"type": "string"
}
},
"homeassistant": {
"type": ["object", "null"],
"title": "Home Assistant",
"properties": {
"name": {
"type": ["string", "null"],
"title": "Home Assistant name",
"description": "Name of the group in Home Assistant"
}
}
}
},
"required": ["friendly_name"]
+2 -1
View File
@@ -10,7 +10,7 @@ import yaml from "./yaml";
export {schemaJson};
// When updating also update:
// - https://github.com/Koenkk/zigbee2mqtt/blob/dev/data/configuration.example.yaml#L2
export const CURRENT_VERSION = 4;
export const CURRENT_VERSION = 5;
/** NOTE: by order of priority, lower index is lower level (more important) */
export const LOG_LEVELS: readonly string[] = ["error", "warning", "info", "debug"] as const;
export type LogLevel = "error" | "warning" | "info" | "debug";
@@ -82,6 +82,7 @@ export const defaults = {
ota: {
update_check_interval: 24 * 60,
disable_automatic_update_check: false,
image_block_request_timeout: 150000,
image_block_response_delay: 250,
default_maximum_data_size: 50,
},
+58 -3
View File
@@ -1,5 +1,4 @@
import {copyFileSync, writeFileSync} from "node:fs";
import {copyFileSync, existsSync, readFileSync, writeFileSync} from "node:fs";
import data from "./data";
import * as settings from "./settings";
import utils from "./utils";
@@ -29,7 +28,7 @@ interface SettingsCustomHandler extends Omit<SettingsMigration, "path"> {
execute: (currentSettings: Partial<Settings>) => [validPath: boolean, previousValue: unknown, changed: boolean];
}
const SUPPORTED_VERSIONS: Settings["version"][] = [undefined, 2, 3, settings.CURRENT_VERSION];
const SUPPORTED_VERSIONS: Settings["version"][] = [undefined, 2, 3, 4, settings.CURRENT_VERSION];
function backupSettings(version: number): void {
const filePath = data.joinPath("configuration.yaml");
@@ -481,6 +480,58 @@ function migrateToFour(
});
}
function migrateToFive(
_currentSettings: Partial<Settings>,
transfers: SettingsTransfer[],
changes: SettingsChange[],
additions: SettingsAdd[],
removals: SettingsRemove[],
customHandlers: SettingsCustomHandler[],
): void {
transfers.push();
changes.push({
path: ["version"],
note: "Migrated settings to version 5",
newValue: 5,
});
additions.push();
removals.push();
const resetOtaStates = (_currentSettings: Partial<Settings>): ReturnType<SettingsCustomHandler["execute"]> => {
const filePath = data.joinPath("state.json");
if (existsSync(filePath)) {
try {
const stateFile = readFileSync(filePath, "utf8");
const stateObj = JSON.parse(stateFile) as KeyValue;
for (const key in stateObj) {
const entry = stateObj[key];
if (entry.update) {
delete entry.update;
}
}
writeFileSync(filePath, JSON.stringify(stateObj), "utf8");
// hack, this does not actually affect settings, we just want to ensure the migration note triggers
return [true, undefined, true];
} catch (error) {
console.error(`Failed to write state to '${filePath}' (${error})`);
}
}
return [false, undefined, false];
};
customHandlers.push({
note: "Reset all cached OTA states due to internal changes.",
noteIf: () => true,
execute: (currentSettings) => resetOtaStates(currentSettings),
});
}
/**
* Order of execution:
* - Transfer
@@ -533,6 +584,10 @@ export function migrateIfNecessary(): void {
migrationNotesFileName = "migration-3-to-4.log";
migrateToFour(currentSettings, transfers, changes, additions, removals, customHandlers);
} else if (currentSettings.version === 4) {
migrationNotesFileName = "migration-4-to-5.log";
migrateToFive(currentSettings, transfers, changes, additions, removals, customHandlers);
}
for (const transfer of transfers) {
+10 -10
View File
@@ -1,6 +1,6 @@
{
"name": "zigbee2mqtt",
"version": "2.7.1",
"version": "2.8.0",
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
"main": "index.js",
"types": "dist/types/api.d.ts",
@@ -48,7 +48,7 @@
"express-static-gzip": "^3.0.0",
"fast-deep-equal": "^3.1.3",
"finalhandler": "^2.1.1",
"humanize-duration": "^3.33.1",
"humanize-duration": "^3.33.2",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"jszip": "^3.10.1",
@@ -58,23 +58,23 @@
"semver": "^7.7.3",
"source-map-support": "^0.5.21",
"throttleit": "^2.1.0",
"winston": "^3.18.3",
"winston": "^3.19.0",
"winston-syslog": "^2.7.1",
"winston-transport": "^4.9.0",
"ws": "^8.18.1",
"zigbee-herdsman": "7.0.4",
"zigbee-herdsman-converters": "25.83.1",
"ws": "^8.19.0",
"zigbee-herdsman": "9.0.2",
"zigbee-herdsman-converters": "25.120.0",
"zigbee2mqtt-frontend": "0.9.21",
"zigbee2mqtt-windfront": "2.4.2"
"zigbee2mqtt-windfront": "2.8.1"
},
"devDependencies": {
"@biomejs/biome": "^2.3.8",
"@biomejs/biome": "^2.3.13",
"@types/finalhandler": "^1.2.3",
"@types/humanize-duration": "^3.27.4",
"@types/js-yaml": "^4.0.9",
"@types/node": "^24.10.1",
"@types/node": "^24.10.9",
"@types/object-assign-deep": "^0.4.3",
"@types/readable-stream": "4.0.22",
"@types/readable-stream": "4.0.23",
"@types/serve-static": "^2.2.0",
"@types/ws": "8.18.1",
"@vitest/coverage-v8": "^3.1.1",
+125 -132
View File
@@ -5,7 +5,7 @@ settings:
excludeLinksFromLockfile: false
overrides:
zigbee-herdsman: 7.0.4
zigbee-herdsman: 9.0.2
importers:
@@ -30,8 +30,8 @@ importers:
specifier: ^2.1.1
version: 2.1.1
humanize-duration:
specifier: ^3.33.1
version: 3.33.1
specifier: ^3.33.2
version: 3.33.2
js-yaml:
specifier: ^4.1.0
version: 4.1.1
@@ -60,33 +60,33 @@ importers:
specifier: ^2.1.0
version: 2.1.0
winston:
specifier: ^3.18.3
version: 3.18.3
specifier: ^3.19.0
version: 3.19.0
winston-syslog:
specifier: ^2.7.1
version: 2.7.1(winston@3.18.3)
version: 2.7.1(winston@3.19.0)
winston-transport:
specifier: ^4.9.0
version: 4.9.0
ws:
specifier: ^8.18.1
version: 8.18.3
specifier: ^8.19.0
version: 8.19.0
zigbee-herdsman:
specifier: 7.0.4
version: 7.0.4
specifier: 9.0.2
version: 9.0.2
zigbee-herdsman-converters:
specifier: 25.83.1
version: 25.83.1
specifier: 25.120.0
version: 25.120.0
zigbee2mqtt-frontend:
specifier: 0.9.21
version: 0.9.21
zigbee2mqtt-windfront:
specifier: 2.4.2
version: 2.4.2
specifier: 2.8.1
version: 2.8.1
devDependencies:
'@biomejs/biome':
specifier: ^2.3.8
version: 2.3.8
specifier: ^2.3.13
version: 2.3.13
'@types/finalhandler':
specifier: ^1.2.3
version: 1.2.4
@@ -97,14 +97,14 @@ importers:
specifier: ^4.0.9
version: 4.0.9
'@types/node':
specifier: ^24.10.1
version: 24.10.1
specifier: ^24.10.9
version: 24.10.9
'@types/object-assign-deep':
specifier: ^0.4.3
version: 0.4.3
'@types/readable-stream':
specifier: 4.0.22
version: 4.0.22
specifier: 4.0.23
version: 4.0.23
'@types/serve-static':
specifier: ^2.2.0
version: 2.2.0
@@ -113,7 +113,7 @@ importers:
version: 8.18.1
'@vitest/coverage-v8':
specifier: ^3.1.1
version: 3.2.4(vitest@3.2.4(@types/node@24.10.1))
version: 3.2.4(vitest@3.2.4(@types/node@24.10.9))
tmp:
specifier: ^0.2.5
version: 0.2.5
@@ -122,7 +122,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^3.1.1
version: 3.2.4(@types/node@24.10.1)
version: 3.2.4(@types/node@24.10.9)
optionalDependencies:
unix-dgram:
specifier: ^2.0.7
@@ -159,55 +159,55 @@ packages:
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
'@biomejs/biome@2.3.8':
resolution: {integrity: sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA==}
'@biomejs/biome@2.3.13':
resolution: {integrity: sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA==}
engines: {node: '>=14.21.3'}
hasBin: true
'@biomejs/cli-darwin-arm64@2.3.8':
resolution: {integrity: sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww==}
'@biomejs/cli-darwin-arm64@2.3.13':
resolution: {integrity: sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-x64@2.3.8':
resolution: {integrity: sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA==}
'@biomejs/cli-darwin-x64@2.3.13':
resolution: {integrity: sha512-AGr8OoemT/ejynbIu56qeil2+F2WLkIjn2d8jGK1JkchxnMUhYOfnqc9sVzcRxpG9Ycvw4weQ5sprRvtb7Yhcw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-linux-arm64-musl@2.3.8':
resolution: {integrity: sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA==}
'@biomejs/cli-linux-arm64-musl@2.3.13':
resolution: {integrity: sha512-TUdDCSY+Eo/EHjhJz7P2GnWwfqet+lFxBZzGHldrvULr59AgahamLs/N85SC4+bdF86EhqDuuw9rYLvLFWWlXA==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-arm64@2.3.8':
resolution: {integrity: sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g==}
'@biomejs/cli-linux-arm64@2.3.13':
resolution: {integrity: sha512-xvOiFkrDNu607MPMBUQ6huHmBG1PZLOrqhtK6pXJW3GjfVqJg0Z/qpTdhXfcqWdSZHcT+Nct2fOgewZvytESkw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-x64-musl@2.3.8':
resolution: {integrity: sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA==}
'@biomejs/cli-linux-x64-musl@2.3.13':
resolution: {integrity: sha512-0bdwFVSbbM//Sds6OjtnmQGp4eUjOTt6kHvR/1P0ieR9GcTUAlPNvPC3DiavTqq302W34Ae2T6u5VVNGuQtGlQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-linux-x64@2.3.8':
resolution: {integrity: sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw==}
'@biomejs/cli-linux-x64@2.3.13':
resolution: {integrity: sha512-s+YsZlgiXNq8XkgHs6xdvKDFOj/bwTEevqEY6rC2I3cBHbxXYU1LOZstH3Ffw9hE5tE1sqT7U23C00MzkXztMw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-win32-arm64@2.3.8':
resolution: {integrity: sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg==}
'@biomejs/cli-win32-arm64@2.3.13':
resolution: {integrity: sha512-QweDxY89fq0VvrxME+wS/BXKmqMrOTZlN9SqQ79kQSIc3FrEwvW/PvUegQF6XIVaekncDykB5dzPqjbwSKs9DA==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
'@biomejs/cli-win32-x64@2.3.8':
resolution: {integrity: sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w==}
'@biomejs/cli-win32-x64@2.3.13':
resolution: {integrity: sha512-trDw2ogdM2lyav9WFQsdsfdVy1dvZALymRpgmWsvSez0BJzBjulhOT/t+wyKeh3pZWvwP3VMs1SoOKwO3wecMQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
@@ -557,14 +557,14 @@ packages:
'@types/js-yaml@4.0.9':
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
'@types/node@24.10.1':
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
'@types/node@24.10.9':
resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==}
'@types/object-assign-deep@0.4.3':
resolution: {integrity: sha512-d9Gxaj5j1hzrxJ61EFEg13B4g4FgrT/DYtcDWFXPehR8DF2SUZbVMFtZIs8exkVRiqrqBpdTc/lUUZjncsPpMw==}
'@types/readable-stream@4.0.22':
resolution: {integrity: sha512-/FFhJpfCLAPwAcN3mFycNUa77ddnr8jTgF5VmSNetaemWB2cIlfCA9t0YTM3JAT0wOcv8D4tjPo7pkDhK3EJIg==}
'@types/readable-stream@4.0.23':
resolution: {integrity: sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==}
'@types/serve-static@2.2.0':
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
@@ -695,23 +695,23 @@ packages:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-convert@3.1.2:
resolution: {integrity: sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==}
color-convert@3.1.3:
resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==}
engines: {node: '>=14.6'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
color-name@2.0.2:
resolution: {integrity: sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==}
color-name@2.1.0:
resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==}
engines: {node: '>=12.20'}
color-string@2.1.2:
resolution: {integrity: sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==}
color-string@2.1.4:
resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==}
engines: {node: '>=18'}
color@5.0.2:
resolution: {integrity: sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==}
color@5.0.3:
resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==}
engines: {node: '>=18'}
commist@3.2.0:
@@ -893,11 +893,11 @@ packages:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
humanize-duration@3.33.1:
resolution: {integrity: sha512-hwzSCymnRdFx9YdRkQQ0OYequXiVAV6ZGQA2uzocwB0F4309Ke6pO8dg0P8LHhRQJyVjGteRTAA/zNfEcpXn8A==}
humanize-duration@3.33.2:
resolution: {integrity: sha512-K7Ny/ULO1hDm2nnhvAY+SJV1skxFb61fd073SG1IWJl+D44ULrruCuTyjHKjBVVcSuTlnY99DKtgEG39CM5QOQ==}
iconv-lite@0.7.0:
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
iconv-lite@0.7.2:
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
engines: {node: '>=0.10.0'}
ieee754@1.2.1:
@@ -1018,10 +1018,6 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
mixin-deep@2.0.1:
resolution: {integrity: sha512-imbHQNRglyaplMmjBLL3V5R6Bfq5oM+ivds3SKgc6oRtzErEnBUUc5No11Z2pilkUvl42gJvi285xTNswcKCMA==}
engines: {node: '>=6'}
mqtt-packet@9.0.2:
resolution: {integrity: sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==}
@@ -1431,8 +1427,8 @@ packages:
resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==}
engines: {node: '>= 12.0.0'}
winston@3.18.3:
resolution: {integrity: sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==}
winston@3.19.0:
resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==}
engines: {node: '>= 12.0.0'}
worker-factory@7.0.45:
@@ -1455,8 +1451,8 @@ packages:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
ws@8.19.0:
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@@ -1467,12 +1463,12 @@ packages:
utf-8-validate:
optional: true
zigbee-herdsman-converters@25.83.1:
resolution: {integrity: sha512-oAU2TiQYMQVepKr9Yds2ihmg+gCydqChZ8sUQxHmvjBmAQdMPkhjVBkv0HV26bD2h7VnkhmENv8aDzDkScvJYw==}
zigbee-herdsman-converters@25.120.0:
resolution: {integrity: sha512-xB2ZMZRvGZWMQlBY7zszLILJ8AOGO0NmRRICwcxLK4gtWATnyuol2G6BfD7nBEgz73XgcsrERFFNMM5mDYsQaw==}
engines: {node: '>=20.15.0'}
zigbee-herdsman@7.0.4:
resolution: {integrity: sha512-/MXhrBKfrbpdr/ODetZZpPdXVCovBEmVEoXt5EXJFwrnzx16IrlUiE96Pgf4uLwmoZNJcixQxVq+mAsf/2SCwQ==}
zigbee-herdsman@9.0.2:
resolution: {integrity: sha512-tIg/JycgyxrS8HTWR7+42NVeOXScRylISZM2p5eM4E4gv3sWv2YzoOJD4DjXBM6FSen4vZ/9NSLD48akn7mJKw==}
zigbee-on-host@0.2.4:
resolution: {integrity: sha512-NIG6CWp+Yfn7PjqEIRvenHqpwT1U7rSkyimnFOUIFpnmxQOJKrmcwWDfn6WjbU/YIYYWSQPlj+g13icu1xwlyg==}
@@ -1482,8 +1478,8 @@ packages:
resolution: {integrity: sha512-ClHYlG7g0v/tnIsD6kycNCi1ZEKIbQuU7lGJR3WqYcDmuqEMVcHDGbRiRwYRQcZGMxkqCf7guxNsXiKnC10+kg==}
engines: {node: '>=20.11'}
zigbee2mqtt-windfront@2.4.2:
resolution: {integrity: sha512-Tp77ZQ0yaQzeLOpgfWnkcKrMddjwnWwFHQxmtssbTVHvJUFpTQgB2Pbezj6ji5Wlr1xa8m8/J6S5dQms1TpQOg==}
zigbee2mqtt-windfront@2.8.1:
resolution: {integrity: sha512-9zFZzWERV2whmfwJVOjajKkCMWTk+drfiwr9MzRj4X4p536OUUWyEXB2ob8/N69GRkuFaVMXgRiY7iSZ4YWB+Q==}
engines: {node: '>=22.12.0'}
snapshots:
@@ -1510,39 +1506,39 @@ snapshots:
'@bcoe/v8-coverage@1.0.2': {}
'@biomejs/biome@2.3.8':
'@biomejs/biome@2.3.13':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 2.3.8
'@biomejs/cli-darwin-x64': 2.3.8
'@biomejs/cli-linux-arm64': 2.3.8
'@biomejs/cli-linux-arm64-musl': 2.3.8
'@biomejs/cli-linux-x64': 2.3.8
'@biomejs/cli-linux-x64-musl': 2.3.8
'@biomejs/cli-win32-arm64': 2.3.8
'@biomejs/cli-win32-x64': 2.3.8
'@biomejs/cli-darwin-arm64': 2.3.13
'@biomejs/cli-darwin-x64': 2.3.13
'@biomejs/cli-linux-arm64': 2.3.13
'@biomejs/cli-linux-arm64-musl': 2.3.13
'@biomejs/cli-linux-x64': 2.3.13
'@biomejs/cli-linux-x64-musl': 2.3.13
'@biomejs/cli-win32-arm64': 2.3.13
'@biomejs/cli-win32-x64': 2.3.13
'@biomejs/cli-darwin-arm64@2.3.8':
'@biomejs/cli-darwin-arm64@2.3.13':
optional: true
'@biomejs/cli-darwin-x64@2.3.8':
'@biomejs/cli-darwin-x64@2.3.13':
optional: true
'@biomejs/cli-linux-arm64-musl@2.3.8':
'@biomejs/cli-linux-arm64-musl@2.3.13':
optional: true
'@biomejs/cli-linux-arm64@2.3.8':
'@biomejs/cli-linux-arm64@2.3.13':
optional: true
'@biomejs/cli-linux-x64-musl@2.3.8':
'@biomejs/cli-linux-x64-musl@2.3.13':
optional: true
'@biomejs/cli-linux-x64@2.3.8':
'@biomejs/cli-linux-x64@2.3.13':
optional: true
'@biomejs/cli-win32-arm64@2.3.8':
'@biomejs/cli-win32-arm64@2.3.13':
optional: true
'@biomejs/cli-win32-x64@2.3.8':
'@biomejs/cli-win32-x64@2.3.13':
optional: true
'@colors/colors@1.6.0': {}
@@ -1756,7 +1752,7 @@ snapshots:
'@so-ric/colorspace@1.1.6':
dependencies:
color: 5.0.2
color: 5.0.3
text-hex: 1.0.0
'@types/chai@5.2.2':
@@ -1769,7 +1765,7 @@ snapshots:
'@types/finalhandler@1.2.4':
dependencies:
'@types/node': 24.10.1
'@types/node': 24.10.9
'@types/http-errors@2.0.5': {}
@@ -1777,28 +1773,28 @@ snapshots:
'@types/js-yaml@4.0.9': {}
'@types/node@24.10.1':
'@types/node@24.10.9':
dependencies:
undici-types: 7.16.0
'@types/object-assign-deep@0.4.3': {}
'@types/readable-stream@4.0.22':
'@types/readable-stream@4.0.23':
dependencies:
'@types/node': 24.10.1
'@types/node': 24.10.9
'@types/serve-static@2.2.0':
dependencies:
'@types/http-errors': 2.0.5
'@types/node': 24.10.1
'@types/node': 24.10.9
'@types/triple-beam@1.3.5': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 24.10.1
'@types/node': 24.10.9
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.10.1))':
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.10.9))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
@@ -1813,7 +1809,7 @@ snapshots:
std-env: 3.9.0
test-exclude: 7.0.1
tinyrainbow: 2.0.0
vitest: 3.2.4(@types/node@24.10.1)
vitest: 3.2.4(@types/node@24.10.9)
transitivePeerDependencies:
- supports-color
@@ -1825,13 +1821,13 @@ snapshots:
chai: 5.2.0
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.10.1))':
'@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.10.9))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
vite: 6.3.5(@types/node@24.10.1)
vite: 6.3.5(@types/node@24.10.9)
'@vitest/pretty-format@3.2.4':
dependencies:
@@ -1905,7 +1901,7 @@ snapshots:
bl@6.1.3:
dependencies:
'@types/readable-stream': 4.0.22
'@types/readable-stream': 4.0.23
buffer: 6.0.3
inherits: 2.0.4
readable-stream: 4.7.0
@@ -1949,22 +1945,22 @@ snapshots:
dependencies:
color-name: 1.1.4
color-convert@3.1.2:
color-convert@3.1.3:
dependencies:
color-name: 2.0.2
color-name: 2.1.0
color-name@1.1.4: {}
color-name@2.0.2: {}
color-name@2.1.0: {}
color-string@2.1.2:
color-string@2.1.4:
dependencies:
color-name: 2.0.2
color-name: 2.1.0
color@5.0.2:
color@5.0.3:
dependencies:
color-convert: 3.1.2
color-string: 2.1.2
color-convert: 3.1.3
color-string: 2.1.4
commist@3.2.0: {}
@@ -2141,9 +2137,9 @@ snapshots:
statuses: 2.0.1
toidentifier: 1.0.1
humanize-duration@3.33.1: {}
humanize-duration@3.33.2: {}
iconv-lite@0.7.0:
iconv-lite@0.7.2:
dependencies:
safer-buffer: 2.1.2
@@ -2262,8 +2258,6 @@ snapshots:
minipass@7.1.2: {}
mixin-deep@2.0.1: {}
mqtt-packet@9.0.2:
dependencies:
bl: 6.1.3
@@ -2274,7 +2268,7 @@ snapshots:
mqtt@5.14.1:
dependencies:
'@types/readable-stream': 4.0.22
'@types/readable-stream': 4.0.23
'@types/ws': 8.18.1
commist: 3.2.0
concat-stream: 2.0.0
@@ -2289,7 +2283,7 @@ snapshots:
socks: 2.8.7
split2: 4.2.0
worker-timers: 8.0.24
ws: 8.18.3
ws: 8.19.0
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -2595,13 +2589,13 @@ snapshots:
util-deprecate@1.0.2: {}
vite-node@3.2.4(@types/node@24.10.1):
vite-node@3.2.4(@types/node@24.10.9):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 6.3.5(@types/node@24.10.1)
vite: 6.3.5(@types/node@24.10.9)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -2616,7 +2610,7 @@ snapshots:
- tsx
- yaml
vite@6.3.5(@types/node@24.10.1):
vite@6.3.5(@types/node@24.10.9):
dependencies:
esbuild: 0.25.5
fdir: 6.4.6(picomatch@4.0.2)
@@ -2625,14 +2619,14 @@ snapshots:
rollup: 4.44.0
tinyglobby: 0.2.14
optionalDependencies:
'@types/node': 24.10.1
'@types/node': 24.10.9
fsevents: 2.3.3
vitest@3.2.4(@types/node@24.10.1):
vitest@3.2.4(@types/node@24.10.9):
dependencies:
'@types/chai': 5.2.2
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.10.1))
'@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.10.9))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
@@ -2650,11 +2644,11 @@ snapshots:
tinyglobby: 0.2.14
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 6.3.5(@types/node@24.10.1)
vite-node: 3.2.4(@types/node@24.10.1)
vite: 6.3.5(@types/node@24.10.9)
vite-node: 3.2.4(@types/node@24.10.9)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.10.1
'@types/node': 24.10.9
transitivePeerDependencies:
- jiti
- less
@@ -2678,11 +2672,11 @@ snapshots:
siginfo: 2.0.0
stackback: 0.0.2
winston-syslog@2.7.1(winston@3.18.3):
winston-syslog@2.7.1(winston@3.19.0):
dependencies:
glossy: 0.1.7
triple-beam: 1.4.1
winston: 3.18.3
winston: 3.19.0
winston-transport: 4.9.0
optionalDependencies:
unix-dgram: 2.0.6
@@ -2693,7 +2687,7 @@ snapshots:
readable-stream: 3.6.2
triple-beam: 1.4.1
winston@3.18.3:
winston@3.19.0:
dependencies:
'@colors/colors': 1.6.0
'@dabh/diagnostics': 2.0.8
@@ -2746,17 +2740,17 @@ snapshots:
string-width: 5.1.2
strip-ansi: 7.1.2
ws@8.18.3: {}
ws@8.19.0: {}
zigbee-herdsman-converters@25.83.1:
zigbee-herdsman-converters@25.120.0:
dependencies:
iconv-lite: 0.7.0
iconv-lite: 0.7.2
semver: 7.7.3
zigbee-herdsman: 7.0.4
zigbee-herdsman: 9.0.2
transitivePeerDependencies:
- supports-color
zigbee-herdsman@7.0.4:
zigbee-herdsman@9.0.2:
dependencies:
'@date-fns/tz': 1.4.1
'@serialport/bindings-cpp': 13.0.1
@@ -2765,7 +2759,6 @@ snapshots:
bonjour-service: 1.3.0
debounce: 3.0.0
fast-deep-equal: 3.1.3
mixin-deep: 2.0.1
slip: 1.0.2
zigbee-on-host: 0.2.4
transitivePeerDependencies:
@@ -2775,4 +2768,4 @@ snapshots:
zigbee2mqtt-frontend@0.9.21: {}
zigbee2mqtt-windfront@2.4.2: {}
zigbee2mqtt-windfront@2.8.1: {}
+7 -1
View File
@@ -59,7 +59,13 @@ if (fs.existsSync(commitUserFile)) {
}
const definitions = definitionIndex.default.map((d) => zhc.prepareDefinition(d));
const whiteLabels = definitions.filter((d) => d.whiteLabel).flatMap((d) => d.whiteLabel);
const whiteLabels = definitions
.filter((d) => d.whiteLabel)
.flatMap((d) =>
d.whiteLabel.map((wl) => {
return {model: wl.model, vendor: wl.vendor ?? d.vendor, description: wl.description ?? d.description};
}),
);
const capitalizeFirstChar = (str) => str.charAt(0).toUpperCase() + str.slice(1);
for (const changelog of changelogs) {
+37
View File
@@ -1,5 +1,40 @@
import {execSync} from "node:child_process";
async function checkDuplicateIssue(github, context, name) {
// Search for existing issues with the same `name`
const searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:issue -is:pr "${name}" label:"new device support","external converter"`;
try {
const searchResults = await github.rest.search.issuesAndPullRequests({q: searchQuery, per_page: 100});
// Filter out the current issue and return the first duplicate found
const existingIssues = searchResults.data.items.filter((item) => item.number !== context.payload.issue.number).map((i) => `#${i.number}`);
if (existingIssues.length > 0) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
body: `👋 Hi there! This issue appears to be a duplicate of ${existingIssues.join(", ")}
This issue will be closed. Please follow the existing issue for updates.`,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
state: "closed",
});
return true;
}
} catch (error) {
console.error(`Error searching for duplicate issues with ${name}:`, error);
}
return false;
}
export async function newDeviceSupport(github, _core, context, zhcDir) {
const issue = context.payload.issue;
// Hide previous bot comments
@@ -27,6 +62,7 @@ export async function newDeviceSupport(github, _core, context, zhcDir) {
console.log("Found tuyaManufacturerNames", tuyaManufacturerNames);
if (tuyaManufacturerNames.length > 0) {
for (const [fullName, partialName] of tuyaManufacturerNames) {
if (await checkDuplicateIssue(github, context, fullName)) return;
const fullMatch = (() => {
try {
return execSync(`grep -r --include="*.ts" "${fullName}" "${zhcDir}"`, {encoding: "utf8"});
@@ -93,6 +129,7 @@ Let us know if it works so we can support this device out-of-the-box!`,
if (zigbeeModels.length > 0) {
for (const zigbeeModel of zigbeeModels) {
if (await checkDuplicateIssue(github, context, fullName)) return;
const fullMatch = (() => {
try {
return execSync(`grep -r --include="*.ts" '"${zigbeeModel}"' "${zhcDir}"`, {encoding: "utf8"});
+1 -1
View File
@@ -91,4 +91,4 @@ if (processArgs.length !== 1) {
const eraser = new ZStackNvMemEraser(processArgs[0]);
eraser.start();
void eraser.start();
+4 -1
View File
@@ -322,6 +322,7 @@ describe("Extension: Bridge", () => {
ota: {
default_maximum_data_size: 50,
disable_automatic_update_check: false,
image_block_request_timeout: 150000,
image_block_response_delay: 250,
update_check_interval: 1440,
},
@@ -3263,7 +3264,7 @@ describe("Extension: Bridge", () => {
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/interview",
stringify({data: {}, status: "error", error: "interview of 'bulb' (0x000b57fffec6a5b2) failed: Error: something went wrong"}),
stringify({data: {}, status: "error", error: "Interview of 'bulb' (0x000b57fffec6a5b2) failed: Error: something went wrong"}),
{},
);
});
@@ -4085,6 +4086,8 @@ describe("Extension: Bridge", () => {
fs.writeFileSync(path.join(data.mockDir, "ext_converters", "afile.js"), "test123");
fs.mkdirSync(path.join(data.mockDir, "log"));
fs.writeFileSync(path.join(data.mockDir, "log", "log.log"), "test123");
fs.mkdirSync(path.join(data.mockDir, "ota"));
fs.writeFileSync(path.join(data.mockDir, "ota", "my.ota"), "test123");
fs.mkdirSync(path.join(data.mockDir, "ext_converters", "123"));
fs.writeFileSync(path.join(data.mockDir, "ext_converters", "123", "myfile.js"), "test123");
fs.symlinkSync(
+39 -5
View File
@@ -129,6 +129,7 @@ describe("Extension: HomeAssistant", () => {
it("Should discover devices and groups", async () => {
settings.set(["homeassistant", "experimental_event_entities"], true);
settings.set(["groups", "9", "homeassistant"], {name: "HA Discovery Group", icon: "mdi:lightbulb-group"});
await resetExtension();
let payload;
@@ -140,12 +141,13 @@ describe("Extension: HomeAssistant", () => {
command_topic: "zigbee2mqtt/ha_discovery_group/set",
device: {
identifiers: ["zigbee2mqtt_1221051039810110150109113116116_9"],
name: "ha_discovery_group",
name: "HA Discovery Group",
sw_version: version,
model: "Group",
manufacturer: "Zigbee2MQTT",
via_device: "zigbee2mqtt_bridge_0x00124b00120144ae",
},
icon: "mdi:lightbulb-group",
max_mireds: 454,
min_mireds: 250,
name: null,
@@ -209,12 +211,13 @@ describe("Extension: HomeAssistant", () => {
command_topic: "zigbee2mqtt/ha_discovery_group/set",
device: {
identifiers: ["zigbee2mqtt_1221051039810110150109113116116_9"],
name: "ha_discovery_group",
name: "HA Discovery Group",
sw_version: version,
model: "Group",
manufacturer: "Zigbee2MQTT",
via_device: "zigbee2mqtt_bridge_0x00124b00120144ae",
},
icon: "mdi:lightbulb-group",
name: null,
payload_off: "OFF",
payload_on: "ON",
@@ -1218,10 +1221,13 @@ describe("Extension: HomeAssistant", () => {
},
max_temp: "30",
min_temp: "5",
mode_command_topic: "zigbee2mqtt/bosch_rm230z/set/system_mode",
mode_state_template: "{{ value_json.system_mode }}",
mode_command_topic: "zigbee2mqtt/bosch_rm230z/set",
mode_state_template:
"{% set values = {'schedule':'auto','manual':'heat','pause':'off'} %}{% set value = value_json.operating_mode %}{% if value == \"manual\" %}{{ value_json.system_mode }}{% else %}{{ values[value] if value in values.keys() else 'off' }}{% endif %}",
mode_command_template:
"{% set values = { 'auto':'schedule','heat':'manual','cool':'manual','off':'pause'} %}{% if value == \"heat\" or value == \"cool\" %}{\"operating_mode\": \"manual\", \"system_mode\": \"{{ value }}\"}{% else %}{\"operating_mode\": \"{{ values[value] if value in values.keys() else 'pause' }}\"}{% endif %}",
mode_state_topic: "zigbee2mqtt/bosch_rm230z",
modes: ["off", "heat", "cool"],
modes: ["off", "heat", "cool", "auto"],
name: null,
object_id: "bosch_rm230z",
origin,
@@ -1242,6 +1248,34 @@ describe("Extension: HomeAssistant", () => {
});
});
it("Should discover seperate temperature sensor for thermostat", () => {
const payload = {
availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}],
default_entity_id: "sensor.bosch_rm230z_local_temperature",
device: {
identifiers: ["zigbee2mqtt_0x18fc2600000d7ae3"],
manufacturer: "Bosch",
model: "Room thermostat II 230V",
model_id: "BTH-RM230Z",
name: "bosch_rm230z",
via_device: "zigbee2mqtt_bridge_0x00124b00120144ae",
},
device_class: "temperature",
object_id: "bosch_rm230z_local_temperature",
origin,
state_class: "measurement",
state_topic: "zigbee2mqtt/bosch_rm230z",
unique_id: "0x18fc2600000d7ae3_local_temperature_zigbee2mqtt",
unit_of_measurement: "°C",
value_template: "{{ value_json.local_temperature }}",
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/sensor/0x18fc2600000d7ae3/local_temperature/config", stringify(payload), {
qos: 1,
retain: true,
});
});
it("Should discover devices with cover_position", () => {
let payload;
+22 -11
View File
@@ -367,23 +367,34 @@ describe("Extension: NetworkMap", () => {
it("Output graphviz networkmap", async () => {
mock();
const device = devices.bulb_color;
device.lastSeen = undefined;
const endpoint = device.getEndpoint(1);
const data = {modelID: "test"};
const payload = {data, cluster: "genOnOff", device, endpoint, type: "readResponse", linkquality: 10};
await mockZHEvents.message(payload);
mockMQTTEvents.message("zigbee2mqtt/bridge/request/networkmap", stringify({type: "graphviz", routes: true}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bridge/response/networkmap");
const z2mDevice = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr);
const originalDef = z2mDevice.definition;
z2mDevice.definition = {
...originalDef,
description: `${originalDef.description} 6"`,
};
try {
const device = devices.bulb_color;
device.lastSeen = undefined;
const endpoint = device.getEndpoint(1);
const data = {modelID: "test"};
const payload = {data, cluster: "genOnOff", device, endpoint, type: "readResponse", linkquality: 10};
await mockZHEvents.message(payload);
mockMQTTEvents.message("zigbee2mqtt/bridge/request/networkmap", stringify({type: "graphviz", routes: true}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bridge/response/networkmap");
} finally {
z2mDevice.definition = originalDef;
}
const expected = `digraph G {
node[shape=record];
"0x00124b00120144ae" [style="bold, filled", fillcolor="#e04e5d", fontcolor="#ffffff", label="{Coordinator|0x00124b00120144ae (0x0000)|0 seconds ago}"];
"0x000b57fffec6a5b2" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb|0x000b57fffec6a5b2 (0x9db1)|IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)|9 seconds ago}"];
"0x000b57fffec6a5b2" -> "0x00124b00120144ae" [penwidth=2, weight=1, color="#009900", label="92 (routes: 0x198c)"]
"0x000b57fffec6a5b3" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb_color|0x000b57fffec6a5b3 (0x9dcf)|Philips Hue Go (7146060PH)|unknown}"];
"0x000b57fffec6a5b3" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb_color|0x000b57fffec6a5b3 (0x9dcf)|Philips Hue Go 6\\" (7146060PH)|unknown}"];
"0x000b57fffec6a5b3" -> "0x00124b00120144ae" [penwidth=0.5, weight=0, color="#994444", label="120"]
"0x000b57fffec6a5b3" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="110"]
"0x0017880104e45521" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{button_double_key|0x0017880104e45521 (0x198a)|Aqara Wireless remote switch (double rocker), 2016 model (WXKG02LM_rev1)|9 seconds ago}"];
File diff suppressed because it is too large Load Diff
+411 -71
View File
@@ -73,7 +73,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({brightness: "200"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 200, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({brightness: 200, state: "ON"});
@@ -119,7 +124,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({brightness_percent: "92"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 235, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 235, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: "ON", brightness: 235});
@@ -132,7 +142,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({brightness: 230}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 230, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 230, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: "ON", brightness: 230});
@@ -145,7 +160,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({color_temp: "222"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 222, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColorTemp",
{colortemp: 222, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color_mode: "color_temp", color_temp: 222});
@@ -158,7 +178,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({color_temp_percent: 100}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 500, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColorTemp",
{colortemp: 500, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color_mode: "color_temp", color_temp: 500});
@@ -223,7 +248,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/0x0017882194e45543/set", stringify({state_l2: "ON", brightness_l2: 50}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/QS-Zigbee-D02-TRIAC-2C-LN", stringify({brightness_l2: 50, state_l2: "ON"}), {
retain: false,
qos: 0,
@@ -284,7 +314,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({color: {x: 100, y: 50}}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColor",
{colorx: 6553500, colory: 3276750, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color_mode: "xy", color: {x: 100, y: 50}});
@@ -298,7 +333,12 @@ describe("Extension: Publish", () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {});
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColor",
{colorx: 6553500, colory: 3276750, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb_color",
@@ -313,8 +353,18 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({color: {x: 100, y: 50}, brightness: 20}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColor",
{colorx: 6553500, colory: 3276750, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb_color",
@@ -329,8 +379,18 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({color: {x: 100, y: 50}, brightness: 20, state: "ON"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColor",
{colorx: 6553500, colory: 3276750, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb_color",
@@ -345,7 +405,13 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({color: {x: 100, y: 50}, brightness: 20, state: "OFF"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command).toHaveBeenNthCalledWith(1, "lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {});
expect(endpoint.command).toHaveBeenNthCalledWith(
1,
"lightingColorCtrl",
"moveToColor",
{colorx: 6553500, colory: 3276750, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(endpoint.command).toHaveBeenNthCalledWith(2, "genOnOff", "off", {}, {});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
@@ -361,7 +427,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({color: {r: 100, g: 200, b: 10}}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17806, colory: 43155, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColor",
{colorx: 17806, colory: 43155, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: "xy"});
@@ -373,7 +444,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({color: {rgb: "100,200,10"}}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17806, colory: 43155, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColor",
{colorx: 17806, colory: 43155, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: "xy"});
@@ -385,7 +461,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON", brightness: "50"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: "ON", brightness: 50});
@@ -410,7 +491,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({brightness_percent: 50}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 128, transtime: 0}, {});
expect(group.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 128, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/group_1");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({state: "ON", brightness: 128});
@@ -449,7 +535,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON", brightness: 50}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {});
expect(group.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/group_1");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({state: "ON", brightness: 50});
@@ -475,7 +566,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({color: {x: 0.37, y: 0.28}}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 24248, colory: 18350, transtime: 0}, {});
expect(group.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColor",
{colorx: 24248, colory: 18350, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/group_1");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({color: {x: 0.37, y: 0.28}, color_mode: "xy"});
@@ -488,7 +584,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({color_temp: 100}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 100, transtime: 0}, {});
expect(group.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColorTemp",
{colortemp: 100, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/group_1");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({color_temp: 100, color_mode: "color_temp"});
@@ -515,7 +616,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({brightness: "200"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 200, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
});
@@ -526,7 +632,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({brightness: "200"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 200, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: "ON"});
});
@@ -681,7 +792,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set/color", "#64C80A");
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17806, colory: 43155, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColor",
{colorx: 17806, colory: 43155, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: "xy"});
@@ -798,7 +914,12 @@ describe("Extension: Publish", () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColor", {colorx: 45940, colory: 19595, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual([
"lightingColorCtrl",
"moveToColor",
{colorx: 45940, colory: 19595, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should also use on/off cluster when controlling group with switch", async () => {
@@ -809,7 +930,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/switch_group/set", stringify({state: "ON", brightness: 100}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(2);
expect(group.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 100, transtime: 0}, {});
expect(group.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 100, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(group.command).toHaveBeenCalledWith("genOnOff", "on", {}, {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/switch_group", stringify({state: "ON", brightness: 100}), {
retain: false,
@@ -835,7 +961,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({brightness: 100}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 100, transtime: 200}, {});
expect(group.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 100, transtime: 200, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // zigbee2mqtt/bulb_color + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/group_1");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({state: "ON", brightness: 100});
@@ -850,7 +981,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 200}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 200, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should use transition from device_options on brightness command", async () => {
@@ -861,7 +997,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 200}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 200, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should turn bulb on with correct brightness when device is turned off twice and brightness is reported", async () => {
@@ -909,10 +1050,34 @@ describe("Extension: Publish", () => {
});
expect(endpoint.command).toHaveBeenCalledTimes(4);
expect(endpoint.command).toHaveBeenNthCalledWith(1, "genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 0}, {});
expect(endpoint.command).toHaveBeenNthCalledWith(2, "genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 0}, {});
expect(endpoint.command).toHaveBeenNthCalledWith(3, "genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 0}, {});
expect(endpoint.command).toHaveBeenNthCalledWith(4, "genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 0}, {});
expect(endpoint.command).toHaveBeenNthCalledWith(
1,
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 200, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(endpoint.command).toHaveBeenNthCalledWith(
2,
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 0, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(endpoint.command).toHaveBeenNthCalledWith(
3,
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 0, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(endpoint.command).toHaveBeenNthCalledWith(
4,
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 200, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
});
it("Should turn bulb on with full brightness when transition is used and no brightness is known", async () => {
@@ -929,8 +1094,18 @@ describe("Extension: Publish", () => {
qos: 0,
});
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 5}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 254, transtime: 5}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 0, transtime: 5, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[1]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 254, transtime: 5, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Transition parameter should not influence brightness on state ON", async () => {
@@ -944,9 +1119,19 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON", transition: 1}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(3);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[1]).toEqual(["genOnOff", "on", {}, {}]);
expect(endpoint.command.mock.calls[2]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 10}, {}]);
expect(endpoint.command.mock.calls[2]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 10, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should use transition when color temp", async () => {
@@ -957,7 +1142,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 200, transtime: 200}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"lightingColorCtrl",
"moveToColorTemp",
{colortemp: 200, transtime: 200, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should use transition only once when setting brightness and color temperature for TRADFRI", async () => {
@@ -967,8 +1157,18 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 200, transtime: 200}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[1]).toEqual([
"lightingColorCtrl",
"moveToColorTemp",
{colortemp: 200, transtime: 200, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should use transition only once when setting brightness and color temperature for group which contains TRADFRI", async () => {
@@ -976,8 +1176,18 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/group_with_tradfri/set", stringify({state: "ON", transition: 60, brightness: 20, color_temp: 400}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(2);
expect(group.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {}]);
expect(group.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 400, transtime: 600}, {}]);
expect(group.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(group.command.mock.calls[1]).toEqual([
"lightingColorCtrl",
"moveToColorTemp",
{colortemp: 400, transtime: 600, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Message transition should overrule options transition", async () => {
@@ -988,7 +1198,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 100}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 200, transtime: 100, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should set state with brightness before color", async () => {
@@ -998,8 +1213,18 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 100, transtime: 30}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColor", {colorx: 45940, colory: 19595, transtime: 30}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 100, transtime: 30, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[1]).toEqual([
"lightingColorCtrl",
"moveToColor",
{colorx: 45940, colory: 19595, transtime: 30, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should turn device off when brightness 0 is send", async () => {
@@ -1012,9 +1237,24 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(3);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[2]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[1]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 0, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[2]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync.mock.calls[0]).toEqual([
"zigbee2mqtt/bulb_color",
@@ -1043,9 +1283,24 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(3);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 30}, {}]);
expect(endpoint.command.mock.calls[2]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[1]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 0, transtime: 30, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[2]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync.mock.calls[0]).toEqual([
"zigbee2mqtt/bulb_color",
@@ -1074,7 +1329,7 @@ describe("Extension: Publish", () => {
expect(endpoint.command.mock.calls[0]).toEqual([
"lightingColorCtrl",
"enhancedMoveToHueAndSaturation",
{enhancehue: 45510, saturation: 127, transtime: 0},
{enhancehue: 45510, saturation: 127, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
@@ -1129,7 +1384,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 254, transtime: 30}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 254, transtime: 30, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should turn device on with on/off with transition when transition 0 is provided", async () => {
@@ -1139,7 +1399,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 254, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 254, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should turn device off with onOff on off with transition", async () => {
@@ -1149,7 +1414,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 10}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 0, transtime: 10, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: "OFF"});
@@ -1168,7 +1438,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF", transition: 3}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 30}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 0, transtime: 30, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0]).toEqual([
"zigbee2mqtt/bulb_color",
@@ -1197,7 +1472,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON", transition: 3}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 30}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 200, transtime: 30, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync.mock.calls[2]).toEqual([
"zigbee2mqtt/bulb_color",
@@ -1219,7 +1499,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF", transition: 3}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 30}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 0, transtime: 30, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0]).toEqual([
"zigbee2mqtt/bulb_color",
@@ -1231,7 +1516,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 200, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync.mock.calls[1]).toEqual([
"zigbee2mqtt/bulb_color",
@@ -1264,7 +1554,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 100, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"lightingColorCtrl",
"moveToColorTemp",
{colortemp: 100, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: "ON", color_temp: 100, color_mode: "color_temp"});
@@ -1281,7 +1576,12 @@ describe("Extension: Publish", () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 100, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual([
"lightingColorCtrl",
"moveToColorTemp",
{colortemp: 100, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb_color",
@@ -1300,7 +1600,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "moveToColor", {colorx: 26869, colory: 16384, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"lightingColorCtrl",
"moveToColor",
{colorx: 26869, colory: 16384, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color: {x: 0.41, y: 0.25}, state: "ON", color_mode: "xy"});
@@ -1363,7 +1668,7 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({brightness_move: -40}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "move", {movemode: 1, rate: 40}, {});
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "move", {movemode: 1, rate: 40, optionsMask: 0, optionsOverride: 0}, {});
});
it("Should publish brightness_move down to zigbee devices", async () => {
@@ -1371,7 +1676,7 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({brightness_move: 30}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "move", {movemode: 0, rate: 30}, {});
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "move", {movemode: 0, rate: 30, optionsMask: 0, optionsOverride: 0}, {});
});
it("HS2WD-E burglar warning", async () => {
@@ -1442,7 +1747,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/roller_shutter/set", stringify({state: "OPEN"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 255, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 255, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/roller_shutter");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({position: 100});
@@ -1476,7 +1786,12 @@ describe("Extension: Publish", () => {
await vi.runOnlyPendingTimersAsync();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/GL-S-007ZS", stringify({state: "ON", brightness: 20}), {
qos: 0,
retain: false,
@@ -1509,8 +1824,18 @@ describe("Extension: Publish", () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 10}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 10, optionsMask: 0, optionsOverride: 0},
{},
);
});
it("Should restore brightness when its turned off without transition and is turned on with", async () => {
@@ -1527,7 +1852,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON", brightness: 123}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 123, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 123, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
await flushPromises();
@@ -1537,7 +1867,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON", transition: 1.0}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(4);
expect(endpoint.command.mock.calls[3]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 123, transtime: 10}, {}]);
expect(endpoint.command.mock.calls[3]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 123, transtime: 10, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Shouldnt use moveToLevelWithOnOff on turn on when no transition has been used as some devices do not turn on in that case", async () => {
@@ -1556,7 +1891,12 @@ describe("Extension: Publish", () => {
expect(endpoint.command).toHaveBeenCalledTimes(4);
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 150, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 150, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[2]).toEqual(["genOnOff", "off", {}, {}]);
expect(endpoint.command.mock.calls[3]).toEqual(["genOnOff", "on", {}, {}]);
@@ -1601,7 +1941,7 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/group_tradfri_remote/set", stringify({scene_recall: 1}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith("genScenes", "recall", {groupid: 15071, sceneid: 1}, {});
expect(group.command).toHaveBeenCalledWith("genScenes", "recall", {groupid: 15071, sceneid: 1, transitionTime: 0xffff}, {});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(8);
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(
1,
+27
View File
@@ -12,6 +12,14 @@ import Transport from "winston-transport";
import logger from "../lib/util/logger";
import * as settings from "../lib/util/settings";
vi.mock("rimraf", async (importOriginal) => {
const actual = await importOriginal<typeof import("rimraf")>();
return {
...actual,
rimrafSync: vi.fn(actual.rimrafSync),
};
});
describe("Logger", () => {
let consoleWriteSpy: MockInstance;
const dir = tmp.dirSync();
@@ -43,6 +51,7 @@ describe("Logger", () => {
it("Create log directory", () => {
const dirs = fs.readdirSync(dir.name);
expect(dirs.length).toBe(1);
expect(dirs[0]).toMatch(/^[0-9]{4}-[0-9]{2}-[0-9]{2}\.[0-9]{2}-[0-9]{2}-[0-9]{2}$/);
});
it("Should cleanup (default setting)", () => {
@@ -59,6 +68,24 @@ describe("Logger", () => {
expect(fs.readdirSync(dir.name).length).toBe(10);
});
it("Should handle cleanup error", () => {
for (const d of fs.readdirSync(dir.name)) {
rimrafSync(path.join(dir.name, d));
}
for (let i = 0; i < 20; i++) {
fs.mkdirSync(path.join(dir.name, `log_${i}`));
}
vi.mocked(rimrafSync).mockImplementationOnce(() => {
throw new Error("EACCES: permission denied");
});
const errorSpy = vi.spyOn(logger, "error");
logger.init();
expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/Failed to remove old log directory '.*': Error: EACCES: permission denied/));
});
it("Should cleanup (15 folders setting)", () => {
for (const d of fs.readdirSync(dir.name)) {
rimrafSync(path.join(dir.name, d));
+7 -4
View File
@@ -1,9 +1,8 @@
import fs from "node:fs";
import path from "node:path";
import stringify from "json-stable-stringify-without-jsonify";
import tmp from "tmp";
import {vi} from "vitest";
import yaml from "../../lib/util/yaml";
export const mockDir: string = tmp.dirSync().name;
@@ -278,6 +277,10 @@ export function stateExists(): boolean {
return fs.existsSync(stateFile);
}
export function readState(): Record<string, unknown> {
return JSON.parse(fs.readFileSync(stateFile, "utf8"));
}
const defaultState = {
"0x000b57fffec6a5b2": {
state: "ON",
@@ -297,8 +300,8 @@ export function getDefaultState(): typeof defaultState {
return defaultState;
}
export function writeDefaultState(): void {
fs.writeFileSync(path.join(mockDir, "state.json"), stringify(defaultState));
export function writeDefaultState(data: Record<string, unknown> = defaultState): void {
fs.writeFileSync(stateFile, stringify(data));
}
export function writeEmptyDatabase(): void {
+1 -1
View File
@@ -1,5 +1,5 @@
import {vi} from "vitest";
import type Transport from "winston-transport";
import type {LogLevel} from "../../lib/util/settings";
let level: LogLevel = "info";
+35
View File
@@ -3,6 +3,8 @@ import {type Mock, vi} from "vitest";
import type {AdapterTypes} from "zigbee-herdsman";
import {Zcl} from "zigbee-herdsman";
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
import type {OtaDataSettings, OtaExtraMetas, OtaSource, OtaUpdateAvailableResult} from "zigbee-herdsman/dist/controller/tstype";
import type {TClusterCommandPayload} from "zigbee-herdsman/dist/zspec/zcl/definition/clusters-types";
import type {BindingTableEntry, LQITableEntry, RoutingTableEntry} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes";
import {DEFAULT_BIND_GROUP_ID} from "../../lib/util/utils";
import type {EventHandler} from "./utils";
@@ -254,6 +256,34 @@ export class Device {
routingTable: Mock<() => Promise<RoutingTableEntry[]>>;
bindingTable: Mock<() => Promise<BindingTableEntry[]>>;
clearAllBindings: Mock<() => Promise<void>>;
checkOta =
vi.fn<
(
source: OtaSource,
current: TClusterCommandPayload<"genOta", "queryNextImageRequest"> | undefined,
extraMetas: OtaExtraMetas,
endpoint: Endpoint,
) => Promise<OtaUpdateAvailableResult>
>();
updateOta =
vi.fn<
(
source: Readonly<OtaSource> | undefined,
requestPayload: TClusterCommandPayload<"genOta", "queryNextImageRequest"> | undefined,
requestTsn: number | undefined,
extraMetas: Readonly<OtaExtraMetas>,
onProgress: (progress: number, remaining: number) => void,
dataSettings: OtaDataSettings,
endpoint: Endpoint,
) => Promise<[from: OtaUpdateAvailableResult["current"], to: OtaUpdateAvailableResult["current"] | undefined]>
>();
scheduleOta = vi.fn((source) => {
this.scheduledOta = source;
});
unscheduleOta = vi.fn(() => {
this.scheduledOta = undefined;
});
scheduledOta: OtaSource | undefined = undefined;
constructor(
type: string,
@@ -311,7 +341,12 @@ export class Device {
this.routingTable.mockClear();
this.bindingTable.mockClear();
this.clearAllBindings.mockClear();
this.checkOta.mockClear();
this.updateOta.mockClear();
this.scheduleOta.mockClear();
this.unscheduleOta.mockClear();
this.meta = {};
this.scheduledOta = undefined;
for (const ep of this.endpoints) {
ep.mockClear();
+78 -2
View File
@@ -1,12 +1,13 @@
// biome-ignore assist/source/organizeImports: import mocks first
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it} from "vitest";
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import * as data from "./mocks/data";
import {existsSync, readFileSync, rmSync} from "node:fs";
import {existsSync, readFileSync, rmSync, writeFileSync} from "node:fs";
import objectAssignDeep from "object-assign-deep";
import mockedData from "../lib/util/data";
import * as settings from "../lib/util/settings";
import * as settingsMigration from "../lib/util/settingsMigration";
import path from "node:path";
describe("Settings Migration", () => {
beforeAll(() => {});
@@ -916,4 +917,79 @@ describe("Settings Migration", () => {
expect(migrationNotesContent).toContain("[SPECIAL] Device icons are now saved as images.");
});
});
describe("Migrates v4 to v5", () => {
const BASE_CONFIG = {
version: 4,
mqtt: {
server: "mqtt://localhost",
},
};
const DEFAULT_STATE: Record<string, unknown> = {
"0x000b57fffec6a5b2": {
state: "ON",
brightness: 50,
color_temp: 370,
linkquality: 99,
},
"0x0017880104e45517": {
brightness: 255,
update: {state: "idle"},
},
1: {
state: "ON",
update: {state: "available", installed_version: 1, latest_version: 2},
},
};
beforeEach(() => {
settings.testing.CURRENT_VERSION = 5; // stop update after this version
data.writeDefaultConfiguration(BASE_CONFIG);
data.writeDefaultState(DEFAULT_STATE);
settings.reRead();
});
it("Update", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
afterSettings.version = 5;
expect(settings.getPersistedSettings()).toStrictEqual(beforeSettings);
expect(data.readState()).toStrictEqual(DEFAULT_STATE);
settingsMigration.migrateIfNecessary();
const migratedSettings = settings.getPersistedSettings();
expect(migratedSettings).toStrictEqual(afterSettings);
// @ts-expect-error workaround
const migratedState = objectAssignDeep.noMutate({}, DEFAULT_STATE);
delete (migratedState["0x0017880104e45517"] as Record<string, unknown>).update;
delete (migratedState[1] as Record<string, unknown>).update;
expect(data.readState()).toStrictEqual(migratedState);
});
it("handles errors gracefully", () => {
const consoleErrorSpy = vi.spyOn(console, "error");
writeFileSync(path.join(data.mockDir, "state.json"), "notjson", "utf8");
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
afterSettings.version = 5;
expect(settings.getPersistedSettings()).toStrictEqual(beforeSettings);
settingsMigration.migrateIfNecessary();
const migratedSettings = settings.getPersistedSettings();
expect(migratedSettings).toStrictEqual(afterSettings);
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to write state"));
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("is not valid JSON"));
});
});
});