Compare commits

...
222 Commits
Author SHA1 Message Date
Koen Kanters fcbb7ff44b chore(dev): release 2.13.0 (#32446) 2026-08-01 07:50:18 +02:00
Koen Kanters e5e36ac79c fix(ignore): update zigbee-herdsman-converters to 26.90.0 (#32715) 2026-08-01 05:19:47 +00:00
Koen Kanters 28e410bbf4 fix(ignore): update zigbee-herdsman to 10.8.0 (#32701) 2026-07-30 18:57:05 +00:00
Koen Kanters b29fc0e493 fix(ignore): update zigbee-herdsman-converters to 26.89.0 (#32700) 2026-07-30 18:53:16 +00:00
Alexander Chepurnoy 316413b31c fix: Replace jszip with fflate (#32683) 2026-07-30 20:36:38 +02:00
dependabot[bot] a4862c9769 chore: bump actions/stale from 10 to 11 (#32691) 2026-07-30 00:56:42 +02:00
dependabot[bot] 3348cea190 fix(ignore): bump the minor-patch group with 3 updates (#32692) 2026-07-30 00:56:01 +02:00
Nerivec fe96204f0b fix: Publish groups on device leave (#32676) 2026-07-29 06:43:46 +02:00
Koen Kanters 7598b38ed8 chore: merge master to dev 2026-07-28 18:06:23 +02:00
Kobi Hikri 52d5316377 chore: compare the PR title from env instead of interpolating it into the shell (#32675) 2026-07-28 18:05:45 +02:00
Jan BouwhuisandKoen Kanters 32506b4e8a feat: Home Assistant: add discovery support for Tuya infrared receiver (learn mode) and emitter features (#32625)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-07-28 18:03:36 +02:00
Koen Kanters 3bec9e87b1 fix(ignore): update zigbee-herdsman-converters to 26.88.0 (#32674) 2026-07-28 16:00:03 +00:00
luar123 b46c77bff0 fix: Avoid running resolveDefinition in parallel (#32662) 2026-07-28 17:20:41 +02:00
Koen Kanters e6e0b6f8f2 fix(ignore): update zigbee-herdsman-converters to 26.87.0 (#32660) 2026-07-26 06:13:48 +00:00
Koenkk 756a824488 fix(ignore): update zigbee-herdsman-converters to 26.86.0 2026-07-24 21:42:03 +02:00
NerivecandKoen Kanters 83ab5228ff feat: Add clear_cache option to device remove request (#32631)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-07-24 21:40:04 +02:00
Nerivec 4f18b4e38c fix: Remove json-stable-stringify-without-jsonify dep (#32643) 2026-07-24 20:40:26 +02:00
Andy 5e56c454c3 fix(ignore): Remove attribute_and_json warnings for HA (#32616) 2026-07-23 00:32:56 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> bbad134e8e fix(ignore): bump ws from 8.21.0 to 8.21.1 in the minor-patch group across 1 directory (#32624)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 19:58:08 +00:00
Alexander ChepurnoyandKoen Kanters 2d941000a7 fix: Replace source-map-support with native Node source map support (#32620)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-07-21 21:32:55 +02:00
Koen Kanters 4ad1ab21e6 chore: merge master to dev 2026-07-21 21:24:03 +02:00
Koen Kanters 6835cbf5a7 chore: update dependabot config 2026-07-21 21:23:43 +02:00
Koen Kanters b8d388ca7a fix(ignore): update zigbee-herdsman to 10.6.3 (#32623) 2026-07-21 19:13:20 +00:00
Alexander Chepurnoy c9ddef703a fix: Drop redundant types js yaml (#32619) 2026-07-21 19:31:22 +02:00
Andy 4174e9cb78 fix: Fix restartRequired flag (#31947) 2026-07-21 19:25:32 +02:00
Koen Kanters 752ab37302 fix(ignore): update zigbee-herdsman-converters to 26.85.0 (#32612) 2026-07-20 15:24:58 +00:00
Andy d6dea17961 fix: Home Assistant: discover device trigger when mqtt output = attribute_and_json and add warnings about incompatible settings (#32603) 2026-07-20 17:08:06 +02:00
Koen Kanters e78b23f5eb fix(ignore): update zigbee-herdsman-converters to 26.84.0 (#32598) 2026-07-18 18:49:36 +00:00
Koen Kanters 22d85a6729 fix(ignore): update zigbee-herdsman-converters to 26.83.0 (#32581) 2026-07-16 17:42:08 +00:00
Alexander Chepurnoy a9ce4b2522 fix: replace rimraf with native fs.rmSync (#32579) 2026-07-16 19:06:28 +02:00
dependabot[bot] e889592baf fix(ignore): bump actions/setup-node from 6 to 7 (#32574) 2026-07-15 19:13:08 +02:00
Koen Kanters 576e1954ef fix(ignore): update zigbee-herdsman-converters to 26.82.0 (#32572) 2026-07-15 10:59:07 +00:00
Jan Bouwhuis 8940f6dabf chore: Add initial devcontainer support (#32555) 2026-07-12 19:08:06 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fa12ebe27c fix(ignore): bump typescript from 6.0.3 to 7.0.2 (#32552)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-12 08:26:53 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 3f7c0ca715 fix(ignore): bump throttleit from 2.1.0 to 3.0.0 (#32551)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-12 08:26:37 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 66db1bdbcd fix(ignore): bump zigbee2mqtt-windfront from 2.12.1 to 2.13.0 in the minor-patch group (#32550)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-12 08:23:12 +02:00
luar123 a80c2db4c6 fix: Home Assistant: fix action published to wrong topic (#32544) 2026-07-12 07:52:32 +02:00
Koen Kanters e475de15c9 fix(ignore): update zigbee-herdsman-converters to 26.81.0 (#32543) 2026-07-11 19:37:24 +00:00
Andy 2ca80b8572 fix: experimental_event_entities and legacy_action_sensor require restart (#32535) 2026-07-11 21:13:46 +02:00
Koen Kanters 912fe4c250 fix(ignore): update zigbee-herdsman-converters to 26.80.0 (#32522) 2026-07-09 10:55:54 +00:00
dependabot[bot] 74478fd955 fix(ignore): bump the minor-patch group with 2 updates (#32514) 2026-07-08 19:30:55 +02:00
Koen Kanters 5591207dea fix: Support Node 26, remove Node 20 support (#32508) 2026-07-07 22:38:52 +02:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Koen Kanters
83ca2746a7 fix(ignore): bump @types/node from 24.13.2 to 26.1.0 (#32453)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-07-07 22:31:18 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8f0d981765 fix(ignore): bump the minor-patch group with 2 updates (#32504)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 14:47:07 +02:00
Koen Kanters 6e557162cb fix(ignore): update zigbee-herdsman to 10.6.2 (#32500) 2026-07-06 18:57:40 +00:00
Koen Kanters a2973f21f6 fix(ignore): update zigbee-herdsman-converters to 26.79.0 (#32499) 2026-07-06 18:52:20 +00:00
Koen Kanters f88d99294b fix(ignore): update zigbee-herdsman-converters to 26.78.0 (#32489) 2026-07-05 10:06:58 +00:00
Maximilian Rink 344776b633 fix: Refresh exposes after manual device configure (#32486) 2026-07-05 08:50:33 +02:00
Koen Kanters 4b0c3067ff fix(ignore): update zigbee-herdsman-converters to 26.77.0 (#32462) 2026-07-02 05:40:32 +00:00
dependabot[bot] e8ba0b24f8 fix(ignore): bump the minor-patch group with 5 updates (#32452) 2026-07-01 19:35:51 +02:00
github-actions[bot] 2cfdd807ed chore: promote to dev 2026-07-01 06:13:56 +00:00
Koen Kanters aa909a8a62 chore(dev): release 2.12.1 (#32254) 2026-07-01 08:03:12 +02:00
Koen Kanters f867694831 fix(ignore): update zigbee-herdsman to 10.6.1 (#32442) 2026-06-30 19:14:25 +00:00
Koen Kanters 740de2d363 fix(ignore): update zigbee-herdsman-converters to 26.76.0 (#32441) 2026-06-30 19:09:32 +00:00
Darafei Praliaskouski e8a7e6f29d fix: Home Assistant: mark device settings as config (#32439) 2026-06-30 21:00:16 +02:00
Koen Kanters 3760e63183 fix(ignore): update zigbee-herdsman-converters to 26.75.0 (#32431) 2026-06-29 18:29:42 +00:00
Koen Kanters dcb16b8271 fix(ignore): update zigbee-herdsman to 10.6.0 (#32422) 2026-06-28 14:25:30 +00:00
Koen Kanters 31fb3a6e89 fix(ignore): update zigbee-herdsman-converters to 26.74.0 (#32420) 2026-06-27 19:58:50 +00:00
Ilya KirovandKoen Kanters 0018ca8f80 fix: Home Assistant: support cooling setpoint in climate discovery (#32411)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-06-27 21:38:16 +02:00
Rohan KapoorandCursor 2f26083ce1 fix: Add mqtt.server_name option to override TLS SNI (#32417)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-27 21:37:48 +02:00
Koen Kanters 8d5fdada38 fix(ignore): update zigbee-herdsman-converters to 26.73.0 (#32407) 2026-06-25 19:28:37 +00:00
Koen Kanters 6ab52df08a fix(ignore): update zigbee-herdsman to 10.5.0 (#32406) 2026-06-25 18:03:18 +00:00
Maximilian Rink 0140708076 fix: Home Assistant: apply expose-level Home Assistant discovery metadata (#32380) 2026-06-25 19:54:59 +02:00
DanielDeni 7775ac5cd2 fix: Home Assistant: unit conversion for derived weather sensors by restoring device_class with name preservation (#32392) 2026-06-25 19:31:41 +02:00
Maximilian Rink 814552a615 fix: Home Assistant: pass device options to HA discovery overrides (#32379) 2026-06-25 18:45:36 +02:00
dependabot[bot] 280442b671 fix(ignore): bump actions/cache from 5 to 6 (#32394) 2026-06-25 18:33:24 +02:00
Koen Kanters 628f97f498 fix(ignore): update zigbee-herdsman to 10.4.2 (#32388) 2026-06-23 18:11:24 +00:00
Koen Kanters b58503bb57 fix(ignore): update zigbee-herdsman-converters to 26.72.0 (#32387) 2026-06-23 18:06:00 +00:00
Maximilian Rink 98a0f94870 fix: Home Assistant: mark thermostat configuration switches as config (#32378) 2026-06-23 19:37:13 +02:00
Maximilian Rink 72be1d746b fix: Home Assistant: mark legacy action sensors diagnostic (#32377) 2026-06-23 19:36:44 +02:00
Maximilian Rink 20ab0a2a4f fix: Home Assistant: allow discovery user overrides to win over converter (#32361) 2026-06-23 19:11:04 +02:00
Koen Kanters 87689523ae fix(ignore): update zigbee-herdsman-converters to 26.71.0 (#32375) 2026-06-22 19:02:14 +00:00
Koen Kanters 2cbf740f1b chore: merge master to dev 2026-06-22 20:52:22 +02:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Koen Kanters
262139a43f fix(ignore): bump js-yaml from 4.2.0 to 5.0.0 (#32371)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-06-22 20:52:09 +02:00
Koen Kanters e85ff5513c chore(dependabot): add commit-message prefix for Docker and GH actions 2026-06-22 20:52:03 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9a7cac1e4c fix(ignore): bump semver from 7.8.4 to 7.8.5 in the minor-patch group (#32369)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 20:42:05 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 679436e09b fix(ignore): bump actions/checkout from 6 to 7 (#32368)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 20:40:41 +02:00
Koen Kanters c6e7d2dd16 fix(ignore): update zigbee-herdsman-converters to 26.70.0 (#32357) 2026-06-21 06:00:12 +00:00
Daniel 29ee3a7552 fix: Home Assistant: add conductivity discovery for soil_fertility (#32356) 2026-06-21 07:54:03 +02:00
Koen Kanters 0377f965a3 fix(ignore): update zigbee-herdsman-converters to 26.69.0 (#32350) 2026-06-20 06:28:20 +00:00
Koen Kanters 1de8609456 fix(ignore): update zigbee-herdsman-converters to 26.68.0 (#32340) 2026-06-18 19:22:15 +00:00
Koen Kanters 95c67c0e22 fix(ignore): update zigbee-herdsman-converters to 26.67.0 (#32334) 2026-06-17 19:19:25 +00:00
Koen Kanters 2f44449953 fix(ignore): update zigbee-herdsman to 10.4.1 (#32333) 2026-06-17 19:08:29 +00:00
Koen Kanters 299b9e64f5 fix(ignore): update zigbee-herdsman-converters to 26.66.0 (#32315) 2026-06-15 19:17:00 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Koen Kanters
719de7adcf fix(ignore): bump the minor-patch group with 3 updates (#32312)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-06-15 21:15:04 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 758a80b30c fix: Docker: bump alpine from 3.23 to 3.24 (#32311)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 20:55:10 +02:00
Koen Kanters f4f97d1920 fix(ignore): update zigbee-herdsman-converters to 26.65.0 (#32298) 2026-06-13 19:05:52 +00:00
Koen Kanters a68419f0f5 fix(ignore): update zigbee-herdsman-converters to 26.64.0 (#32282) 2026-06-11 19:58:38 +00:00
quyentonndbsandKai Tanaka ced6d93d20 fix: improve zigbee2mqtt maintenance path (#32255)
Co-authored-by: Kai Tanaka <275430420+quyentonndbs@users.noreply.github.com>
2026-06-11 08:14:36 +02:00
kryoz_ a81e90b981 fix: Republish bridge/state online when HA comes online (#32258) 2026-06-10 21:18:33 +02:00
github-actions[bot] 0cc3b1e7f1 chore: promote to dev 2026-06-09 19:18:48 +00:00
Koen Kanters fd8bafc81d chore(dev): release 2.12.0 (#32157) 2026-06-09 21:02:26 +02:00
Koen Kanters 68daf4f1de fix(ignore): update zigbee-herdsman-converters to 26.63.0 (#32252) 2026-06-09 18:57:20 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 41f6886591 fix(ignore): bump the minor-patch group across 1 directory with 3 updates (#32236)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 20:24:54 +02:00
Koen Kanters 582f153896 fix(ignore): update zigbee-herdsman to 10.4.0 (#32249) 2026-06-09 18:19:56 +00:00
Nerivec 6c2c874641 fix(ignore): missing type for new OTA endpoint (#32195) 2026-06-03 19:24:16 +00:00
Koen Kanters 120f6ac779 fix(ignore): update zigbee-herdsman-converters to 26.62.0 (#32194) 2026-06-03 19:05:29 +00:00
de58711e11 feat: Add ability to abort running OTA (#32022)
Co-authored-by: Andy <andrei_lazarov@yahoo.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-06-03 21:01:44 +02:00
Koen Kanters ea6ccac938 fix(ignore): update zigbee-herdsman to 10.3.0 (#32193) 2026-06-03 18:57:34 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ef8dcb4dc2 fix(ignore): bump the minor-patch group with 2 updates (#32183)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-03 20:42:49 +02:00
Koen Kanters babf7a17d4 fix(ignore): update zigbee-herdsman-converters to 26.61.2 (#32180) 2026-06-02 18:31:41 +00:00
Koen Kanters f9e1bac588 fix(ignore): update zigbee-herdsman to 10.2.0 (#32179) 2026-06-02 18:25:44 +00:00
github-actions[bot] 6aac849452 chore: promote to dev 2026-06-01 18:52:00 +00:00
Koen Kanters 2b5a670ba2 chore(dev): release 2.11.0 (#31938) 2026-06-01 20:28:15 +02:00
Koen Kanters 58feb78362 fix(ignore): update zigbee-herdsman-converters to 26.61.1 (#32156) 2026-06-01 18:21:24 +00:00
Koen Kanters c6e13ccb45 fix(ignore): update zigbee-herdsman-converters to 26.60.0 (#32150) 2026-05-31 19:07:26 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Nerivec
ac8c45f67f fix(ignore): bump the minor-patch group with 2 updates (#32137)
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-05-29 21:08:01 +00:00
Koen Kanters a8364ad67b fix(ignore): update zigbee-herdsman-converters to 26.59.1 (#32136) 2026-05-29 18:54:19 +00:00
Koen Kanters e2b49113c7 fix(ignore): update zigbee-herdsman-converters to 26.58.0 (#32126) 2026-05-28 19:42:40 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7b9407d020 fix(deps-dev): bump tmp from 0.2.5 to 0.2.6 (#32122)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-28 21:15:16 +02:00
Koen Kanters 873ad6082c chore: merge master to dev 2026-05-28 21:07:03 +02:00
Koen Kanters 8beaf488f7 chore: update CONTRIBUTING.md 2026-05-28 21:06:41 +02:00
Koen Kanters 7a1c2d52f0 fix(ignore): update zigbee-herdsman-converters to 26.57.0 (#32113) 2026-05-28 18:40:36 +00:00
Koen Kanters 5a79c338d4 chore: merge master to dev 2026-05-26 21:31:53 +02:00
Koen Kanters 16fd422c4a chore: add note about AI pull requests 2026-05-26 21:31:34 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e5f70d5ba8 fix(ignore): bump the minor-patch group with 3 updates (#32106)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 20:57:57 +02:00
Koen Kanters 0618aca73a chore: merge master to dev 2026-05-23 20:53:41 +02:00
Koen Kanters cfc7ab8dd8 chore: fix command injection in issueBot.mjs 2026-05-23 20:53:25 +02:00
Koen Kanters 56aab4e59c fix(ignore): update zigbee-herdsman-converters to 26.56.0 (#32087) 2026-05-23 18:36:39 +00:00
Koen Kanters 7075b78a46 fix(ignore): update zigbee-herdsman-converters to 26.55.0 (#32068) 2026-05-21 19:45:51 +00:00
Andy b31e8c286e fix: Fix default value of "optimistic" group setting (#32054) 2026-05-21 21:34:52 +02:00
Koen Kanters 2c5b536134 fix(ignore): update zigbee-herdsman-converters to 26.54.0 (#32065) 2026-05-20 20:17:53 +00:00
Koen Kanters 52201c96bf fix(ignore): update zigbee-herdsman-converters to 26.53.0 (#32051) 2026-05-19 18:13:25 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> bf964cc6d9 fix(deps): bump ws from 8.20.0 to 8.20.1 (#32041)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 19:27:30 +02:00
Koen Kanters 1e114dee70 fix(ignore): update zigbee-herdsman-converters to 26.52.0 (#32040) 2026-05-18 19:35:03 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Koen Kanters
897aa8fb7c fix(deps): bump brace-expansion from 2.0.2 to 5.0.6 (#32038)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-05-18 21:28:14 +02:00
Nerivec bbcbed12d5 fix: Prevent invalid external JS file name on save (#32037) 2026-05-18 21:22:18 +02:00
Koen Kanters 36c4db7f8f fix(ignore): update zigbee-herdsman to 10.1.0 (#32027) 2026-05-17 15:46:23 +00:00
Koen Kanters 5458e5084d fix(ignore): update zigbee-herdsman-converters to 26.51.1 (#32016) 2026-05-15 12:18:35 +00:00
Koen Kanters 744e4a6d5c fix(ignore): update zigbee-herdsman-converters to 26.51.0 (#31999) 2026-05-13 19:04:44 +00:00
Koen Kanters 6ea21c383b fix(ignore): update zigbee-herdsman-converters to 26.50.0 (#31988) 2026-05-12 18:19:05 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1a1c09449c fix(deps): bump pnpm/action-setup from 5 to 6 (#31698)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 19:56:50 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9426c3ea11 fix(ignore): bump the minor-patch group with 3 updates (#31982)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 19:47:15 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4d9e9504ef fix(deps): bump actions/dependency-review-action from 4 to 5 (#31980)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 19:47:03 +02:00
Koen Kanters 32499d14d5 fix(ignore): update zigbee-herdsman-converters to 26.49.0 (#31976) 2026-05-11 19:25:35 +00:00
Richard c4fd415cbe fix: Use Jinja-safe property access in HA discovery templates (#31930) 2026-05-11 17:28:04 +02:00
Koen Kanters 502030c016 fix(ignore): update zigbee-herdsman-converters to 26.48.0 (#31959) 2026-05-09 19:44:55 +00:00
Koen Kanters 7a05464f03 fix(ignore): update zigbee-herdsman-converters to 26.47.0 (#31946) 2026-05-08 18:34:03 +00:00
Nerivec 15fd9b371e feat: allow to disable external JS extensions (#31826) 2026-05-08 20:26:17 +02:00
Koen Kanters 04a43652d6 fix(ignore): update zigbee-herdsman to 10.0.8 (#31945) 2026-05-08 18:16:17 +00:00
github-actions[bot] 7b37e97e00 chore: promote to dev 2026-05-07 17:57:27 +00:00
Koen Kanters 94751abaac chore(dev): release 2.10.1 (#31831) 2026-05-07 19:44:19 +02:00
Koen Kanters cc8456617a fix(ignore): update zigbee-herdsman-converters to 26.46.0 (#31937) 2026-05-07 17:36:55 +00:00
Koen Kanters 8727abdefc fix(ignore): update zigbee-herdsman-converters to 26.45.0 (#31925) 2026-05-06 20:05:18 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 24e90273c9 fix(ignore): bump express-static-gzip from 3.0.0 to 3.0.1 in the minor-patch group (#31924)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 21:59:24 +02:00
Koen Kanters 845bdb7dba fix(ignore): update zigbee-herdsman-converters to 26.44.0 (#31900) 2026-05-05 18:57:33 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 55f12ff520 fix(ignore): bump @biomejs/biome from 2.4.13 to 2.4.14 in the minor-patch group across 1 directory (#31881)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 20:30:06 +02:00
Koen Kanters 19eb05a0e0 fix(ignore): update zigbee-herdsman-converters to 26.43.0 (#31878) 2026-05-05 05:29:59 +00:00
Nerivec 9f7ea9b7c7 fix: Replace deprecated url.parse (#31845) 2026-05-04 20:29:24 +02:00
github-actions[bot] 0d7b8f189c chore: promote to dev 2026-05-01 17:40:55 +00:00
Koen Kanters 4639243cf9 chore(dev): release 2.10.0 (#31572) 2026-05-01 19:28:02 +02:00
Koen Kanters df26459b46 fix(ignore): update zigbee-herdsman-converters to 26.42.0 (#31830) 2026-05-01 15:30:23 +00:00
Koen Kanters c58162344f fix(ignore): update zigbee-herdsman-converters to 26.41.0 (#31820) 2026-04-30 19:56:07 +00:00
Andy 5974286445 fix: Improve transmit power description (#31735) 2026-04-28 22:13:25 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4e527bbce8 fix(deps): bump googleapis/release-please-action from 4 to 5 (#31805)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 22:11:58 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b3983b35de fix(ignore): bump the minor-patch group with 3 updates (#31806)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 22:11:47 +02:00
Koen KantersandMikhail Samin 429c5aea58 fix(ignore): update zigbee-herdsman-converters to 26.40.0 (#31800)
Co-authored-by: Mikhail Samin <i@mishasamin.xyz>
2026-04-26 20:05:21 +00:00
Koen Kanters e71507835b fix(ignore): update zigbee-herdsman-converters to 26.39.1 (#31783) 2026-04-23 19:24:07 +00:00
Koen Kanters 3bb3d56c05 fix(ignore): update zigbee-herdsman-converters to 26.38.1 (#31777) 2026-04-22 19:19:16 +00:00
Koen Kanters 305b7bcafa fix(ignore): update zigbee-herdsman-converters to 26.38.0 (#31768) 2026-04-21 19:39:41 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7ba232e27d fix(ignore): bump the minor-patch group with 2 updates (#31758)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 20:44:01 +02:00
Koen Kanters 78f440bada fix(ignore): update zigbee-herdsman-converters to 26.37.0 (#31749) 2026-04-19 19:39:12 +00:00
Koen Kanters a54775dea1 fix(ignore): update zigbee-herdsman-converters to 26.36.0 (#31737) 2026-04-17 18:01:27 +00:00
hoellenandKoen Kanters 041972669a feat: Home Assistant: add group entities in discovery config (#31663)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-04-17 19:14:54 +02:00
Koen Kanters 0a7dfcef59 fix(ignore): update zigbee-herdsman-converters to 26.35.0 (#31713) 2026-04-14 19:09:08 +00:00
Koen Kanters 55d8d1f4d0 fix(ignore): update zigbee-herdsman-converters to 26.34.0 (#31702) 2026-04-13 19:19:40 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> cdd7357522 fix(ignore): bump @biomejs/biome from 2.4.10 to 2.4.11 in the minor-patch group (#31699)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 21:00:46 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4294512486 chore(deps): bump actions/github-script from 8 to 9 (#31697)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 21:00:32 +02:00
Koen Kanters 0c9ec722db fix(ignore): update zigbee-herdsman-converters to 26.33.1 (#31674) 2026-04-11 12:19:45 +00:00
Koen Kanters a0204ad3f0 fix(ignore): update zigbee-herdsman to 10.0.7 (#31673) 2026-04-11 11:32:03 +00:00
toastedcornflakes 4a63e65981 fix: Clarify units of pause_on_backoff_gt (#31668) 2026-04-11 12:47:09 +02:00
Koen Kanters 4818cc4163 fix(ignore): update zigbee-herdsman-converters to 26.32.0 (#31653) 2026-04-09 19:18:29 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Koen Kanters
9e27181b64 fix(ignore): bump typescript from 5.9.3 to 6.0.2 (#31560)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-04-08 20:28:59 +02:00
Koen Kanters a50065cbb4 fix(ignore): update zigbee-herdsman-converters to 26.31.0 (#31637) 2026-04-07 19:14:49 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e6dd5c73bc fix(ignore): bump @types/node from 24.12.0 to 24.12.2 in the minor-patch group across 1 directory (#31628)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 20:55:57 +02:00
Koen Kanters aef3242319 fix(ignore): update zigbee-herdsman to 10.0.6 (#31636) 2026-04-07 18:31:15 +00:00
Koen Kanters 6b567c1286 fix(ignore): update zigbee-herdsman-converters to 26.30.0 (#31608) 2026-04-05 06:07:21 +00:00
Koen Kanters 23f9847994 fix(ignore): update zigbee-herdsman-converters to 26.29.0 (#31586) 2026-04-02 18:14:41 +00:00
Koen Kanters 08e4e9edeb fix(ignore): update zigbee-herdsman-converters to 26.28.0 (#31574) 2026-04-01 20:08:13 +00:00
github-actions[bot] 673d81f51a chore: promote to dev 2026-04-01 19:40:05 +00:00
Koen Kanters 2b485a98c5 chore(dev): release 2.9.2 (#31229) 2026-04-01 21:26:12 +02:00
Koen Kanters 62ab814d73 fix(ignore): update zigbee-herdsman-converters to 26.27.0 (#31563) 2026-03-31 19:00:42 +00:00
Koen Kanters fb3189b743 fix(ignore): update zigbee-herdsman to 10.0.5 (#31562) 2026-03-31 18:57:20 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Nerivec
73d8ae8956 fix(ignore): bump the minor-patch group with 3 updates (#31559)
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-03-31 20:46:21 +02:00
Koen Kanters 969db9ae85 fix(ignore): update zigbee-herdsman-converters to 26.26.0 (#31510) 2026-03-25 19:20:04 +00:00
Koen Kanters 66985a8ce8 fix(ignore): update zigbee-herdsman-converters to 26.25.0 (#31497) 2026-03-24 21:22:34 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 52b063bb32 fix(ignore): bump the minor-patch group with 3 updates (#31489)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-24 21:48:41 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 3f2c00cdb7 chore(deps): bump pnpm/action-setup from 4 to 5 (#31488)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-24 21:48:28 +01:00
Koen Kanters dde637af2f fix(ignore): update zigbee-herdsman-converters to 26.24.0 (#31479) 2026-03-22 20:06:24 +00:00
Koen Kanters 867179c430 fix(ignore): update zigbee-herdsman to 10.0.4 (#31478) 2026-03-22 19:52:17 +00:00
Koen Kanters 2e5e362818 fix(ignore): update zigbee-herdsman-converters to 26.23.0 (#31462) 2026-03-20 20:26:14 +00:00
Koen Kanters 64286ebb30 fix(ignore): update zigbee-herdsman-converters to 26.22.0 (#31451) 2026-03-20 20:17:22 +00:00
Koen Kanters 6fb9d563e6 fix(ignore): update zigbee-herdsman to 10.0.3 (#31450) 2026-03-19 19:01:07 +00:00
Koen Kanters e054d7124b fix(ignore): update zigbee-herdsman-converters to 26.21.0 (#31427) 2026-03-17 19:18:59 +00:00
Koen Kanters 35567cb67f chore: change sorting to prevent merge conflict when both deps are updated at the same time 2026-03-17 20:14:34 +01:00
Koen Kanters 075f87c782 fix(ignore): update zigbee-herdsman to 10.0.2 (#31413) 2026-03-16 20:00:37 +00:00
Koen Kanters 9f49d0ec67 fix(ignore): update zigbee-herdsman-converters to 26.20.0 (#31415) 2026-03-16 19:57:51 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 67aa5099df fix(ignore): bump the minor-patch group with 2 updates (#31411)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 17:42:22 +01:00
Koen Kanters 69641d5467 fix(ignore): update zigbee-herdsman-converters to 26.19.1 (#31384) 2026-03-13 13:58:52 +00:00
Koen Kanters 6fca3744c9 fix(ignore): update zigbee-herdsman to 10.0.1 (#31383) 2026-03-13 13:11:23 +00:00
Koen Kanters f60db2d7a6 fix(ignore): update zigbee-herdsman-converters to 26.18.0 (#31362) 2026-03-12 06:47:49 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 08bf2cbc07 chore(deps): bump docker/build-push-action from 6 to 7 (#31325)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 07:14:47 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> c790e7b68f chore(deps): bump docker/metadata-action from 5 to 6 (#31327)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 07:14:14 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> a8633760bc chore(deps): bump docker/login-action from 3 to 4 (#31326)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 07:14:10 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ff01c8718d chore(deps): bump docker/setup-qemu-action from 3 to 4 (#31324)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 07:13:55 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> cd6977bce6 chore(deps): bump docker/setup-buildx-action from 3 to 4 (#31323)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 07:13:52 +01:00
Koen Kanters 9143924482 fix(ignore): update zigbee-herdsman-converters to 26.17.0 (#31314) 2026-03-09 06:01:53 +00:00
NerivecandKoen Kanters 0f781ee9c0 fix: Match new cluster-related typing from ZH (#31298)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-03-09 06:55:11 +01:00
Andrei LAZAROVandNerivec 9f1eb764a6 fix: Export definition version (#31270)
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2026-03-09 06:28:20 +01:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Nerivec
b7f1cecb49 fix(ignore): bump the minor-patch group with 3 updates (#31295)
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-03-07 12:24:25 +00:00
Koen Kanters 05946b5955 fix(ignore): update zigbee-herdsman-converters to 26.16.1 (#31268) 2026-03-07 06:14:12 +00:00
Nerivec 4a8369295c fix: Reintroduce onboarding improvements (#31273) 2026-03-07 06:21:11 +01:00
Koen Kanters 1747618afa fix(ignore): update zigbee-herdsman to 9.0.11 (#31257) 2026-03-03 19:19:31 +00:00
Koen Kanters 284d86e93b fix(ignore): update zigbee-herdsman-converters to 26.14.0 (#31246) 2026-03-03 06:06:45 +00:00
Koen Kanters d6323825e4 fix(ignore): update zigbee-herdsman to 9.0.10 (#31245) 2026-03-03 05:52:27 +00:00
DanielDeni b7b43030eb fix: Home Assistant: fix entity names for derived weather sensors by removing device_class (#31234) 2026-03-03 06:03:42 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> aefbb8be14 fix(ignore): bump @biomejs/biome from 2.4.4 to 2.4.5 in the minor-patch group (#31237)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-03 06:02:12 +01:00
Koen Kanters fc3a1968f6 fix(ignore): update zigbee-herdsman-converters to 26.13.0 (#31231) 2026-03-02 11:45:35 +00:00
github-actions[bot] c56fbca457 chore: promote to dev 2026-03-02 11:26:18 +00:00
Koen Kanters 85875aee27 chore(dev): release 2.9.1 (#31217) 2026-03-02 12:12:38 +01:00
Koen Kanters 7f0a9aa2a3 fix: Fix onboarding (#31228) 2026-03-02 12:10:18 +01:00
github-actions[bot] ff528567e6 chore: promote to dev 2026-03-01 14:08:34 +00:00
77 changed files with 2442 additions and 1142 deletions
+23
View File
@@ -0,0 +1,23 @@
{
"image": "mcr.microsoft.com/devcontainers/javascript-node:24",
"postCreateCommand": "pnpm config set store-dir /home/node/.local/share/pnpm/store && npm install typescript -g",
"customizations": {
"vscode": {
"settings": {
"workbench.colorTheme": "Default Dark Modern",
"window.menuBarVisibility": "classic",
"editor.defaultFormatter": "biomejs.biome",
"notebook.defaultFormatter": "biomejs.biome",
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.tabSize": 4,
"editor.insertSpaces": true,
"files.defaultLanguage": "typescript",
"files.eol": "\n"
},
"extensions": ["biomejs.biome", "vitest.explorer"]
}
}
}
+6 -6
View File
@@ -78,7 +78,7 @@ abstract class Extension {
protected state: State;
protected publishEntityState: PublishEntityState;
protected eventBus: EventBus;
async start(): Promise<void> {}
async stop(): Promise<void> {}
}
@@ -181,9 +181,9 @@ logger.debug("message");
- Use TypeScript's strict mode features (`noImplicitAny`, `noImplicitThis`)
### Performance
- Use `rimrafSync` for synchronous file deletion when appropriate
- Use `fs.rmSync(path, {recursive: true, force: true})` for synchronous file deletion when appropriate
- Leverage async/await for I/O operations to avoid blocking
- Use JSON stable stringify for consistent object serialization: `json-stable-stringify-without-jsonify`
- Use JSON stable stringify util for consistent object serialization
- Cache computed values when appropriate (see device model patterns)
- Use getter methods for computed properties that should be cached
@@ -217,10 +217,10 @@ describe("ComponentName", () => {
it("Should do something specific", async () => {
// Arrange
const input = {};
// Act
const result = await someFunction(input);
// Assert
expect(result).toBe(expected);
});
@@ -414,7 +414,7 @@ this.eventBus.on('deviceMessage', this.onDeviceMessage, this);
- Device operations through `zigbee-herdsman` API
- Event handling through EventBus wrappers
### MQTT Integration
### MQTT Integration
- Connect: `await this.mqtt.connect()`
- Subscribe: `await this.mqtt.subscribe(topic)`
- Publish: `await this.mqtt.publish(topic, message, options)`
+4
View File
@@ -19,8 +19,12 @@ updates:
schedule:
interval: weekly
target-branch: dev
commit-message:
prefix: fix(ignore)
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
target-branch: dev
commit-message:
prefix: chore
+15 -15
View File
@@ -22,18 +22,18 @@ jobs:
actions: read # for Nerivec/action-ci-bench
pull-requests: write # for Nerivec/action-ci-bench
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
with:
# Required for `release: merge dev -> master and promote dev`
token: ${{ secrets.GH_TOKEN }}
- uses: actions/checkout@v6
- uses: actions/checkout@v7
if: ((github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push') == false
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version: 24
registry-url: https://registry.npmjs.org/
@@ -63,14 +63,14 @@ jobs:
- name: Log in to the Docker container registry
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: koenkk
password: ${{ secrets.DOCKER_KEY }}
- name: Log in to the GitHub container registry
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: koenkk
@@ -78,20 +78,20 @@ jobs:
- name: Docker setup - QEMU
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
with:
platforms: all
- name: Docker setup - Buildx
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
id: buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
with:
version: latest
- name: dev - Docker build and push
if: github.ref == 'refs/heads/dev' && github.event_name == 'push'
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
file: docker/Dockerfile
@@ -126,7 +126,7 @@ jobs:
- name: release - Docker meta
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
id: meta
with:
images: |
@@ -139,7 +139,7 @@ jobs:
- name: release - Docker build and push
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
file: docker/Dockerfile
@@ -216,15 +216,15 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [20, 22, 24]
node: [22, 24, 26]
runs-on: ${{ matrix.os }}
continue-on-error: true
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
cache: pnpm
+2 -2
View File
@@ -10,6 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout repository'
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: 'Dependency review'
uses: actions/dependency-review-action@v4
uses: actions/dependency-review-action@v5
+3 -1
View File
@@ -10,8 +10,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Fail PR to master
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
if [[ "${{ github.event.pull_request.title }}" == "chore(dev): release"* ]]; then
if [[ "$PR_TITLE" == "chore(dev): release"* ]]; then
echo "PR title starts with 'chore(dev): release', allowing PR"
else
echo "Pull requests to the master branch are not allowed, target dev branch"
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
steps:
- name: Delete untagged images
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
+3 -3
View File
@@ -17,13 +17,13 @@ jobs:
startsWith(github.event.issue.title, '[New device support]') ||
startsWith(github.event.issue.title, '[External Converter]')
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
sparse-checkout: |
scripts
path: z2m
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
repository: Koenkk/zigbee-herdsman-converters
ref: master
@@ -31,7 +31,7 @@ jobs:
path: zhc
- name: Comment on new device support/external converter issue
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const {newDeviceSupport} = await import("${{ github.workspace }}/z2m/scripts/issueBot.mjs")
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
merge-master-to-dev:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: devmasx/merge-branch@master
with:
type: now
+7 -7
View File
@@ -16,33 +16,33 @@ jobs:
release_created: ${{ steps.release.outputs.release_created }}
version: '${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}}'
steps:
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
with:
version: 9
- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version: 24
- uses: googleapis/release-please-action@v4
- uses: googleapis/release-please-action@v5
id: release
with:
target-branch: dev
token: ${{secrets.GH_TOKEN}}
# Checkout repos
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
repository: koenkk/zigbee2mqtt
path: ./z2m
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
repository: koenkk/zigbee2mqtt
path: ./z2m-master
ref: master
- name: Restore cache commit-user-lookup.json
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
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@v5
uses: actions/cache/save@v6
if: always()
with:
path: z2m/scripts/commit-user-lookup.json
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
- uses: actions/stale@v11
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days'
+3 -3
View File
@@ -13,12 +13,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
ref: dev
token: ${{ secrets.GH_TOKEN }}
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "2.9.0"
".": "2.13.0"
}
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["biomejs.biome", "vitest.explorer"]
}
+11
View File
@@ -0,0 +1,11 @@
{
"editor.defaultFormatter": "biomejs.biome",
"notebook.defaultFormatter": "biomejs.biome",
"editor.tabSize": 4,
"editor.insertSpaces": true,
"files.defaultLanguage": "typescript",
"files.eol": "\n",
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
}
}
+4 -4
View File
@@ -9,7 +9,7 @@ Zigbee2MQTT is a Zigbee to MQTT bridge that allows you to use your Zigbee device
- **Language**: TypeScript 5.9.3 compiled to JavaScript (ES modules with NodeNext resolution)
- **Runtime**: Node.js (versions 20, 22, or 24)
- **Package Manager**: pnpm 10.12.1 (strictly enforced via `packageManager` field)
- **Core Dependencies**:
- **Core Dependencies**:
- `zigbee-herdsman` (6.2.0 - exact version, handles Zigbee adapter communication)
- `zigbee-herdsman-converters` (25.42.0 - exact version, device definitions)
- `mqtt` (5.14.1 - MQTT client)
@@ -227,7 +227,7 @@ abstract class Extension {
protected state: State;
protected publishEntityState: PublishEntityState;
protected eventBus: EventBus;
async start(): Promise<void> {} // Initialize extension
async stop(): Promise<void> {} // Cleanup extension
}
@@ -326,7 +326,7 @@ https://www.zigbee2mqtt.io/guide/installation/01_linux.html
### Performance Considerations
- Use `rimrafSync` for synchronous file operations
- Use `fs.rmSync(path, {recursive: true, force: true})` for synchronous file operations
- Leverage async/await to avoid blocking
- Cache computed values in getters when appropriate
- EventBus provides loose coupling between components
@@ -344,7 +344,7 @@ These dependencies use **exact versions** (no semver ranges) - do not upgrade wi
Only these Node.js versions are supported:
- Node.js 20.x
- Node.js 22.x
- Node.js 22.x
- Node.js 24.x
Using other versions may cause runtime errors or incompatibilities.
+248
View File
@@ -1,5 +1,253 @@
# Changelog
## [2.13.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.12.1...2.13.0) (2026-08-01)
### Features
* Add `clear_cache` option to device remove request ([#32631](https://github.com/Koenkk/zigbee2mqtt/issues/32631)) ([83ab522](https://github.com/Koenkk/zigbee2mqtt/commit/83ab5228ff24779773e67bfc217d89f7c7520daa))
* Home Assistant: add discovery support for Tuya infrared receiver (learn mode) and emitter features ([#32625](https://github.com/Koenkk/zigbee2mqtt/issues/32625)) ([32506b4](https://github.com/Koenkk/zigbee2mqtt/commit/32506b4e8abf81996966632719384f85bad7a66c))
### Bug Fixes
* `experimental_event_entities` and `legacy_action_sensor` require restart ([#32535](https://github.com/Koenkk/zigbee2mqtt/issues/32535)) ([2ca80b8](https://github.com/Koenkk/zigbee2mqtt/commit/2ca80b8572293a7c63c019c1b433e942772220c4))
* Avoid running resolveDefinition in parallel ([#32662](https://github.com/Koenkk/zigbee2mqtt/issues/32662)) ([b46c77b](https://github.com/Koenkk/zigbee2mqtt/commit/b46c77bff0612527bdfc7540ea31ec258bd002b1))
* Drop redundant types js yaml ([#32619](https://github.com/Koenkk/zigbee2mqtt/issues/32619)) ([c9ddef7](https://github.com/Koenkk/zigbee2mqtt/commit/c9ddef703a1f83a2dc5c84a1a1eaa087bee3f671))
* Fix restartRequired flag ([#31947](https://github.com/Koenkk/zigbee2mqtt/issues/31947)) ([4174e9c](https://github.com/Koenkk/zigbee2mqtt/commit/4174e9cb783b73527349182b35e06eda4512b101))
* Home Assistant: discover device trigger when mqtt output = attribute_and_json and add warnings about incompatible settings ([#32603](https://github.com/Koenkk/zigbee2mqtt/issues/32603)) ([d6dea17](https://github.com/Koenkk/zigbee2mqtt/commit/d6dea17961e05b8ca8ce05d7852efd483031260d))
* Home Assistant: fix action published to wrong topic ([#32544](https://github.com/Koenkk/zigbee2mqtt/issues/32544)) ([a80c2db](https://github.com/Koenkk/zigbee2mqtt/commit/a80c2db4c6038426fa5102477f6da34355f54134))
* **ignore:** bump @types/node from 24.13.2 to 26.1.0 ([#32453](https://github.com/Koenkk/zigbee2mqtt/issues/32453)) ([83ca274](https://github.com/Koenkk/zigbee2mqtt/commit/83ca2746a789bb92bfdb51506e875edfe5cb06b3))
* **ignore:** bump actions/setup-node from 6 to 7 ([#32574](https://github.com/Koenkk/zigbee2mqtt/issues/32574)) ([e889592](https://github.com/Koenkk/zigbee2mqtt/commit/e889592baf9b2738c7d2088247ea3f888cd08629))
* **ignore:** bump the minor-patch group with 2 updates ([#32504](https://github.com/Koenkk/zigbee2mqtt/issues/32504)) ([8f0d981](https://github.com/Koenkk/zigbee2mqtt/commit/8f0d9817652f97414d927e2d3e4f2b101e3d55ca))
* **ignore:** bump the minor-patch group with 2 updates ([#32514](https://github.com/Koenkk/zigbee2mqtt/issues/32514)) ([74478fd](https://github.com/Koenkk/zigbee2mqtt/commit/74478fd955dc434e039fac547bfe69db5a2f55ae))
* **ignore:** bump the minor-patch group with 3 updates ([#32692](https://github.com/Koenkk/zigbee2mqtt/issues/32692)) ([3348cea](https://github.com/Koenkk/zigbee2mqtt/commit/3348cea190125b9a466434cc5ea95ab7a857559d))
* **ignore:** bump the minor-patch group with 5 updates ([#32452](https://github.com/Koenkk/zigbee2mqtt/issues/32452)) ([e8ba0b2](https://github.com/Koenkk/zigbee2mqtt/commit/e8ba0b24f8b8528e98904234a32515713f457781))
* **ignore:** bump throttleit from 2.1.0 to 3.0.0 ([#32551](https://github.com/Koenkk/zigbee2mqtt/issues/32551)) ([3f7c0ca](https://github.com/Koenkk/zigbee2mqtt/commit/3f7c0ca71502a2a522a11587a878534f1fd42fe1))
* **ignore:** bump typescript from 6.0.3 to 7.0.2 ([#32552](https://github.com/Koenkk/zigbee2mqtt/issues/32552)) ([fa12ebe](https://github.com/Koenkk/zigbee2mqtt/commit/fa12ebe27c1598e7ac1108f3e3d7d3ffbe2eccab))
* **ignore:** bump ws from 8.21.0 to 8.21.1 in the minor-patch group across 1 directory ([#32624](https://github.com/Koenkk/zigbee2mqtt/issues/32624)) ([bbad134](https://github.com/Koenkk/zigbee2mqtt/commit/bbad134e8ea420200e6b5f75a17bd5a5a9d601fe))
* **ignore:** bump zigbee2mqtt-windfront from 2.12.1 to 2.13.0 in the minor-patch group ([#32550](https://github.com/Koenkk/zigbee2mqtt/issues/32550)) ([66db1bd](https://github.com/Koenkk/zigbee2mqtt/commit/66db1bdbcd8b6303c82d7c77679860537b0868e1))
* **ignore:** Remove attribute_and_json warnings for HA ([#32616](https://github.com/Koenkk/zigbee2mqtt/issues/32616)) ([5e56c45](https://github.com/Koenkk/zigbee2mqtt/commit/5e56c454c3fee03c43152c807ec3e449d6211728))
* **ignore:** update zigbee-herdsman to 10.6.2 ([#32500](https://github.com/Koenkk/zigbee2mqtt/issues/32500)) ([6e55716](https://github.com/Koenkk/zigbee2mqtt/commit/6e557162cbf548735c5d907231cec69fa94f6c53))
* **ignore:** update zigbee-herdsman to 10.6.3 ([#32623](https://github.com/Koenkk/zigbee2mqtt/issues/32623)) ([b8d388c](https://github.com/Koenkk/zigbee2mqtt/commit/b8d388ca7afce29cc6f09c452cbe5c01e79d8244))
* **ignore:** update zigbee-herdsman to 10.8.0 ([#32701](https://github.com/Koenkk/zigbee2mqtt/issues/32701)) ([28e410b](https://github.com/Koenkk/zigbee2mqtt/commit/28e410bbf442f32e9be4298d19fe45ac586b8a88))
* **ignore:** update zigbee-herdsman-converters to 26.77.0 ([#32462](https://github.com/Koenkk/zigbee2mqtt/issues/32462)) ([4b0c306](https://github.com/Koenkk/zigbee2mqtt/commit/4b0c3067ffeef42d66417445480048a8e546bc68))
* **ignore:** update zigbee-herdsman-converters to 26.78.0 ([#32489](https://github.com/Koenkk/zigbee2mqtt/issues/32489)) ([f88d992](https://github.com/Koenkk/zigbee2mqtt/commit/f88d99294b8dc42f8657673e80b7ed721220db40))
* **ignore:** update zigbee-herdsman-converters to 26.79.0 ([#32499](https://github.com/Koenkk/zigbee2mqtt/issues/32499)) ([a2973f2](https://github.com/Koenkk/zigbee2mqtt/commit/a2973f21f6cfebc175b6b1acfcb73ca7cab95c5f))
* **ignore:** update zigbee-herdsman-converters to 26.80.0 ([#32522](https://github.com/Koenkk/zigbee2mqtt/issues/32522)) ([912fe4c](https://github.com/Koenkk/zigbee2mqtt/commit/912fe4c250f514f6a0e3c97cf1438b15cef1ec6a))
* **ignore:** update zigbee-herdsman-converters to 26.81.0 ([#32543](https://github.com/Koenkk/zigbee2mqtt/issues/32543)) ([e475de1](https://github.com/Koenkk/zigbee2mqtt/commit/e475de15c901c493dc93f4194fe55b9359712ce3))
* **ignore:** update zigbee-herdsman-converters to 26.82.0 ([#32572](https://github.com/Koenkk/zigbee2mqtt/issues/32572)) ([576e195](https://github.com/Koenkk/zigbee2mqtt/commit/576e1954efcbe39d8209284e37e4df223a3f37a8))
* **ignore:** update zigbee-herdsman-converters to 26.83.0 ([#32581](https://github.com/Koenkk/zigbee2mqtt/issues/32581)) ([22d85a6](https://github.com/Koenkk/zigbee2mqtt/commit/22d85a6729f5bbba72308c58123b0b6345f6095a))
* **ignore:** update zigbee-herdsman-converters to 26.84.0 ([#32598](https://github.com/Koenkk/zigbee2mqtt/issues/32598)) ([e78b23f](https://github.com/Koenkk/zigbee2mqtt/commit/e78b23f5ebeb388402cae31db982759385e74fc5))
* **ignore:** update zigbee-herdsman-converters to 26.85.0 ([#32612](https://github.com/Koenkk/zigbee2mqtt/issues/32612)) ([752ab37](https://github.com/Koenkk/zigbee2mqtt/commit/752ab37302cffaa2cad664e5f43c61451e099ee6))
* **ignore:** update zigbee-herdsman-converters to 26.86.0 ([756a824](https://github.com/Koenkk/zigbee2mqtt/commit/756a824488377f973b710d6293b8deadc310ce42))
* **ignore:** update zigbee-herdsman-converters to 26.87.0 ([#32660](https://github.com/Koenkk/zigbee2mqtt/issues/32660)) ([e6e0b6f](https://github.com/Koenkk/zigbee2mqtt/commit/e6e0b6f8f2b498b771625019b52d42ccbd4aa967))
* **ignore:** update zigbee-herdsman-converters to 26.88.0 ([#32674](https://github.com/Koenkk/zigbee2mqtt/issues/32674)) ([3bec9e8](https://github.com/Koenkk/zigbee2mqtt/commit/3bec9e87b17985b59e8a50d3c27677056838d26b))
* **ignore:** update zigbee-herdsman-converters to 26.89.0 ([#32700](https://github.com/Koenkk/zigbee2mqtt/issues/32700)) ([b29fc0e](https://github.com/Koenkk/zigbee2mqtt/commit/b29fc0e49352f583d418a36bc6708d5745d1a788))
* **ignore:** update zigbee-herdsman-converters to 26.90.0 ([#32715](https://github.com/Koenkk/zigbee2mqtt/issues/32715)) ([e5e36ac](https://github.com/Koenkk/zigbee2mqtt/commit/e5e36ac79c2e1b89ac0fb81f95bcd655455531c1))
* Publish groups on device leave ([#32676](https://github.com/Koenkk/zigbee2mqtt/issues/32676)) ([fe96204](https://github.com/Koenkk/zigbee2mqtt/commit/fe96204f0b0f2b858ec664d408eb21545dc791d2))
* Refresh exposes after manual device configure ([#32486](https://github.com/Koenkk/zigbee2mqtt/issues/32486)) ([344776b](https://github.com/Koenkk/zigbee2mqtt/commit/344776b63379ab91bf7b35e9c208cff0f88bb84b))
* Remove json-stable-stringify-without-jsonify dep ([#32643](https://github.com/Koenkk/zigbee2mqtt/issues/32643)) ([4f18b4e](https://github.com/Koenkk/zigbee2mqtt/commit/4f18b4e38c276c515eb3b132e3328b88ff62bf95))
* Replace jszip with fflate ([#32683](https://github.com/Koenkk/zigbee2mqtt/issues/32683)) ([316413b](https://github.com/Koenkk/zigbee2mqtt/commit/316413b31c760c4b34c5afb14222bca11236d9d0))
* replace rimraf with native fs.rmSync ([#32579](https://github.com/Koenkk/zigbee2mqtt/issues/32579)) ([a9ce4b2](https://github.com/Koenkk/zigbee2mqtt/commit/a9ce4b2522c2d0a5bfcae3497cd98d3889b6f440))
* Replace source-map-support with native Node source map support ([#32620](https://github.com/Koenkk/zigbee2mqtt/issues/32620)) ([2d94100](https://github.com/Koenkk/zigbee2mqtt/commit/2d941000a73e83fbee983e98121510a6710183fc))
* Support Node 26, remove Node 20 support ([#32508](https://github.com/Koenkk/zigbee2mqtt/issues/32508)) ([5591207](https://github.com/Koenkk/zigbee2mqtt/commit/5591207deaea177c0019fe679f4e197e2efcb286))
## [2.12.1](https://github.com/Koenkk/zigbee2mqtt/compare/2.12.0...2.12.1) (2026-06-30)
### Bug Fixes
* Add `mqtt.server_name` option to override TLS SNI ([#32417](https://github.com/Koenkk/zigbee2mqtt/issues/32417)) ([2f26083](https://github.com/Koenkk/zigbee2mqtt/commit/2f26083ce13c30227ac8d1d891751d4c253b3572))
* Docker: bump alpine from 3.23 to 3.24 ([#32311](https://github.com/Koenkk/zigbee2mqtt/issues/32311)) ([758a80b](https://github.com/Koenkk/zigbee2mqtt/commit/758a80b30c5162b9adb0c2be1785670276905e09))
* Home Assistant: add conductivity discovery for soil_fertility ([#32356](https://github.com/Koenkk/zigbee2mqtt/issues/32356)) ([29ee3a7](https://github.com/Koenkk/zigbee2mqtt/commit/29ee3a7552017e648c534708a93329e8b3fc52c5))
* Home Assistant: allow discovery user overrides to win over converter ([#32361](https://github.com/Koenkk/zigbee2mqtt/issues/32361)) ([20ab0a2](https://github.com/Koenkk/zigbee2mqtt/commit/20ab0a2a4f44b760b6db73d105dc8578426b27a0))
* Home Assistant: apply expose-level Home Assistant discovery metadata ([#32380](https://github.com/Koenkk/zigbee2mqtt/issues/32380)) ([0140708](https://github.com/Koenkk/zigbee2mqtt/commit/0140708076fbd99d33d896e8f214b0e2a8aad8f3))
* Home Assistant: mark device settings as config ([#32439](https://github.com/Koenkk/zigbee2mqtt/issues/32439)) ([e8a7e6f](https://github.com/Koenkk/zigbee2mqtt/commit/e8a7e6f29da3773ffc2bda63623298a307fbb597))
* Home Assistant: mark legacy action sensors diagnostic ([#32377](https://github.com/Koenkk/zigbee2mqtt/issues/32377)) ([72be1d7](https://github.com/Koenkk/zigbee2mqtt/commit/72be1d746bb79fda65b056e4441dcd73bf6c69c6))
* Home Assistant: mark thermostat configuration switches as config ([#32378](https://github.com/Koenkk/zigbee2mqtt/issues/32378)) ([98a0f94](https://github.com/Koenkk/zigbee2mqtt/commit/98a0f9487072cf5a64a901f305c20a73e25c594b))
* Home Assistant: pass device options to HA discovery overrides ([#32379](https://github.com/Koenkk/zigbee2mqtt/issues/32379)) ([814552a](https://github.com/Koenkk/zigbee2mqtt/commit/814552a615d33ac24c38160f7a24514c7b358623))
* Home Assistant: support cooling setpoint in climate discovery ([#32411](https://github.com/Koenkk/zigbee2mqtt/issues/32411)) ([0018ca8](https://github.com/Koenkk/zigbee2mqtt/commit/0018ca8f809708d22221c9d557b3208ba2e5c057))
* Home Assistant: unit conversion for derived weather sensors by restoring device_class with name preservation ([#32392](https://github.com/Koenkk/zigbee2mqtt/issues/32392)) ([7775ac5](https://github.com/Koenkk/zigbee2mqtt/commit/7775ac5cd2ae3929580e401640d97b20facf1491))
* **ignore:** bump actions/cache from 5 to 6 ([#32394](https://github.com/Koenkk/zigbee2mqtt/issues/32394)) ([280442b](https://github.com/Koenkk/zigbee2mqtt/commit/280442b6710c03cfa4776d69a14396b29d32c4ba))
* **ignore:** bump actions/checkout from 6 to 7 ([#32368](https://github.com/Koenkk/zigbee2mqtt/issues/32368)) ([679436e](https://github.com/Koenkk/zigbee2mqtt/commit/679436e09b40edea5bb17094e9aa9115733b916f))
* **ignore:** bump js-yaml from 4.2.0 to 5.0.0 ([#32371](https://github.com/Koenkk/zigbee2mqtt/issues/32371)) ([262139a](https://github.com/Koenkk/zigbee2mqtt/commit/262139a43f878ceef2f327369432e33b74fbd972))
* **ignore:** bump semver from 7.8.4 to 7.8.5 in the minor-patch group ([#32369](https://github.com/Koenkk/zigbee2mqtt/issues/32369)) ([9a7cac1](https://github.com/Koenkk/zigbee2mqtt/commit/9a7cac1e4c82c1b0ef566b13620e2d4d438d725e))
* **ignore:** bump the minor-patch group with 3 updates ([#32312](https://github.com/Koenkk/zigbee2mqtt/issues/32312)) ([719de7a](https://github.com/Koenkk/zigbee2mqtt/commit/719de7adcfffc64e2e9133142045e5dd56e4e8d4))
* **ignore:** update zigbee-herdsman to 10.4.1 ([#32333](https://github.com/Koenkk/zigbee2mqtt/issues/32333)) ([2f44449](https://github.com/Koenkk/zigbee2mqtt/commit/2f4444995361257cbeef0c780532381ec6cc254f))
* **ignore:** update zigbee-herdsman to 10.4.2 ([#32388](https://github.com/Koenkk/zigbee2mqtt/issues/32388)) ([628f97f](https://github.com/Koenkk/zigbee2mqtt/commit/628f97f498747cd2e6be5808c7c609b948af528f))
* **ignore:** update zigbee-herdsman to 10.5.0 ([#32406](https://github.com/Koenkk/zigbee2mqtt/issues/32406)) ([6ab52df](https://github.com/Koenkk/zigbee2mqtt/commit/6ab52df08af1e9b80eb4b58752b7f403d52844a7))
* **ignore:** update zigbee-herdsman to 10.6.0 ([#32422](https://github.com/Koenkk/zigbee2mqtt/issues/32422)) ([dcb16b8](https://github.com/Koenkk/zigbee2mqtt/commit/dcb16b8271349390deee9dc0a0ca4d02239a54bc))
* **ignore:** update zigbee-herdsman to 10.6.1 ([#32442](https://github.com/Koenkk/zigbee2mqtt/issues/32442)) ([f867694](https://github.com/Koenkk/zigbee2mqtt/commit/f86769483102762798272655c4fb117f33263489))
* **ignore:** update zigbee-herdsman-converters to 26.64.0 ([#32282](https://github.com/Koenkk/zigbee2mqtt/issues/32282)) ([a68419f](https://github.com/Koenkk/zigbee2mqtt/commit/a68419f0f58324c1809055938cc59bdd07442aa9))
* **ignore:** update zigbee-herdsman-converters to 26.65.0 ([#32298](https://github.com/Koenkk/zigbee2mqtt/issues/32298)) ([f4f97d1](https://github.com/Koenkk/zigbee2mqtt/commit/f4f97d19208ab951256b69fd7673bfaba667af65))
* **ignore:** update zigbee-herdsman-converters to 26.66.0 ([#32315](https://github.com/Koenkk/zigbee2mqtt/issues/32315)) ([299b9e6](https://github.com/Koenkk/zigbee2mqtt/commit/299b9e64f52c99d1d3034321e82fc40a49bec604))
* **ignore:** update zigbee-herdsman-converters to 26.67.0 ([#32334](https://github.com/Koenkk/zigbee2mqtt/issues/32334)) ([95c67c0](https://github.com/Koenkk/zigbee2mqtt/commit/95c67c0e2271e24a236dabe717772c16f52a3017))
* **ignore:** update zigbee-herdsman-converters to 26.68.0 ([#32340](https://github.com/Koenkk/zigbee2mqtt/issues/32340)) ([1de8609](https://github.com/Koenkk/zigbee2mqtt/commit/1de860945645e748d6173a0368c4ccb44e9f5f13))
* **ignore:** update zigbee-herdsman-converters to 26.69.0 ([#32350](https://github.com/Koenkk/zigbee2mqtt/issues/32350)) ([0377f96](https://github.com/Koenkk/zigbee2mqtt/commit/0377f965a3a3c21e42afb885f8b3685d6de2fdd1))
* **ignore:** update zigbee-herdsman-converters to 26.70.0 ([#32357](https://github.com/Koenkk/zigbee2mqtt/issues/32357)) ([c6e7d2d](https://github.com/Koenkk/zigbee2mqtt/commit/c6e7d2dd16b1a7369b8f784caf3ceed6296a9da2))
* **ignore:** update zigbee-herdsman-converters to 26.71.0 ([#32375](https://github.com/Koenkk/zigbee2mqtt/issues/32375)) ([8768952](https://github.com/Koenkk/zigbee2mqtt/commit/87689523ae685258fa20ee8c03b7a09e8df0f4ba))
* **ignore:** update zigbee-herdsman-converters to 26.72.0 ([#32387](https://github.com/Koenkk/zigbee2mqtt/issues/32387)) ([b58503b](https://github.com/Koenkk/zigbee2mqtt/commit/b58503bb57054e24855bcde353b43be6a61273c6))
* **ignore:** update zigbee-herdsman-converters to 26.73.0 ([#32407](https://github.com/Koenkk/zigbee2mqtt/issues/32407)) ([8d5fdad](https://github.com/Koenkk/zigbee2mqtt/commit/8d5fdada38ce32f8e9ee86b3e39b2e00e2e76529))
* **ignore:** update zigbee-herdsman-converters to 26.74.0 ([#32420](https://github.com/Koenkk/zigbee2mqtt/issues/32420)) ([31fb3a6](https://github.com/Koenkk/zigbee2mqtt/commit/31fb3a6e895170993e893e9ea0d63c6600859259))
* **ignore:** update zigbee-herdsman-converters to 26.75.0 ([#32431](https://github.com/Koenkk/zigbee2mqtt/issues/32431)) ([3760e63](https://github.com/Koenkk/zigbee2mqtt/commit/3760e631835c8e6e11cea5241bb2c8cf3d374722))
* **ignore:** update zigbee-herdsman-converters to 26.76.0 ([#32441](https://github.com/Koenkk/zigbee2mqtt/issues/32441)) ([740de2d](https://github.com/Koenkk/zigbee2mqtt/commit/740de2d363109707a90cdfa8c019361f4423a1aa))
* improve zigbee2mqtt maintenance path ([#32255](https://github.com/Koenkk/zigbee2mqtt/issues/32255)) ([ced6d93](https://github.com/Koenkk/zigbee2mqtt/commit/ced6d93d20502f115d4d0b17b5d292fee7ad30e4))
* Republish bridge/state online when HA comes online ([#32258](https://github.com/Koenkk/zigbee2mqtt/issues/32258)) ([a81e90b](https://github.com/Koenkk/zigbee2mqtt/commit/a81e90b9812acd795a11a78b6d0489c080dc8f5b))
## [2.12.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.11.0...2.12.0) (2026-06-09)
### Features
* Add ability to abort running OTA ([#32022](https://github.com/Koenkk/zigbee2mqtt/issues/32022)) ([de58711](https://github.com/Koenkk/zigbee2mqtt/commit/de58711e11a5ca07c5b55a0c17a1d9f3f452c110))
### Bug Fixes
* **ignore:** bump the minor-patch group across 1 directory with 3 updates ([#32236](https://github.com/Koenkk/zigbee2mqtt/issues/32236)) ([41f6886](https://github.com/Koenkk/zigbee2mqtt/commit/41f688659119012a9a84063bd67c2769a1b1892c))
* **ignore:** bump the minor-patch group with 2 updates ([#32183](https://github.com/Koenkk/zigbee2mqtt/issues/32183)) ([ef8dcb4](https://github.com/Koenkk/zigbee2mqtt/commit/ef8dcb4dc2800defd97fda7523297eab8d2566dc))
* **ignore:** missing type for new OTA endpoint ([#32195](https://github.com/Koenkk/zigbee2mqtt/issues/32195)) ([6c2c874](https://github.com/Koenkk/zigbee2mqtt/commit/6c2c874641ada9cae1c9bdd963a3201cb1cf0fef))
* **ignore:** update zigbee-herdsman to 10.2.0 ([#32179](https://github.com/Koenkk/zigbee2mqtt/issues/32179)) ([f9e1bac](https://github.com/Koenkk/zigbee2mqtt/commit/f9e1bac588e65b223e75f4a2b2e9d528933fdbd8))
* **ignore:** update zigbee-herdsman to 10.3.0 ([#32193](https://github.com/Koenkk/zigbee2mqtt/issues/32193)) ([ea6ccac](https://github.com/Koenkk/zigbee2mqtt/commit/ea6ccac9380af1d9f5672174b2545409f36c2769))
* **ignore:** update zigbee-herdsman to 10.4.0 ([#32249](https://github.com/Koenkk/zigbee2mqtt/issues/32249)) ([582f153](https://github.com/Koenkk/zigbee2mqtt/commit/582f1538961af9c1b6434e6a1bc4acebc63a2541))
* **ignore:** update zigbee-herdsman-converters to 26.61.2 ([#32180](https://github.com/Koenkk/zigbee2mqtt/issues/32180)) ([babf7a1](https://github.com/Koenkk/zigbee2mqtt/commit/babf7a17d4617217297eef45f6b8307265f9b921))
* **ignore:** update zigbee-herdsman-converters to 26.62.0 ([#32194](https://github.com/Koenkk/zigbee2mqtt/issues/32194)) ([120f6ac](https://github.com/Koenkk/zigbee2mqtt/commit/120f6ac779c9fda6914fc68c56674e589c0e82ba))
* **ignore:** update zigbee-herdsman-converters to 26.63.0 ([#32252](https://github.com/Koenkk/zigbee2mqtt/issues/32252)) ([68daf4f](https://github.com/Koenkk/zigbee2mqtt/commit/68daf4f1deafad3ee6e542acbd77d403340e2caa))
## [2.11.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.10.1...2.11.0) (2026-06-01)
### Features
* allow to disable external JS extensions ([#31826](https://github.com/Koenkk/zigbee2mqtt/issues/31826)) ([15fd9b3](https://github.com/Koenkk/zigbee2mqtt/commit/15fd9b371e30ed352cf2de2d241a0d0f7fafa9d1))
### Bug Fixes
* **deps-dev:** bump tmp from 0.2.5 to 0.2.6 ([#32122](https://github.com/Koenkk/zigbee2mqtt/issues/32122)) ([7b9407d](https://github.com/Koenkk/zigbee2mqtt/commit/7b9407d020b9db1093b360f5f6e217bbab7afb63))
* **deps:** bump actions/dependency-review-action from 4 to 5 ([#31980](https://github.com/Koenkk/zigbee2mqtt/issues/31980)) ([4d9e950](https://github.com/Koenkk/zigbee2mqtt/commit/4d9e9504ef9c857a1985c79d3a6799552abc3aea))
* **deps:** bump brace-expansion from 2.0.2 to 5.0.6 ([#32038](https://github.com/Koenkk/zigbee2mqtt/issues/32038)) ([897aa8f](https://github.com/Koenkk/zigbee2mqtt/commit/897aa8fb7c82dbc9c906f986477c4c1673bb3199))
* **deps:** bump pnpm/action-setup from 5 to 6 ([#31698](https://github.com/Koenkk/zigbee2mqtt/issues/31698)) ([1a1c094](https://github.com/Koenkk/zigbee2mqtt/commit/1a1c09449c9d077ea06048d7e2ecea90ff850c9b))
* **deps:** bump ws from 8.20.0 to 8.20.1 ([#32041](https://github.com/Koenkk/zigbee2mqtt/issues/32041)) ([bf964cc](https://github.com/Koenkk/zigbee2mqtt/commit/bf964cc6d976d70e687844e5a09c4577763dc0d3))
* Fix default value of "optimistic" group setting ([#32054](https://github.com/Koenkk/zigbee2mqtt/issues/32054)) ([b31e8c2](https://github.com/Koenkk/zigbee2mqtt/commit/b31e8c286e7a77517a88e3e7eb2247e9a0fd2cc1))
* **ignore:** bump the minor-patch group with 2 updates ([#32137](https://github.com/Koenkk/zigbee2mqtt/issues/32137)) ([ac8c45f](https://github.com/Koenkk/zigbee2mqtt/commit/ac8c45f67f0a4aae2688e55b98dabdc23f73a259))
* **ignore:** bump the minor-patch group with 3 updates ([#31982](https://github.com/Koenkk/zigbee2mqtt/issues/31982)) ([9426c3e](https://github.com/Koenkk/zigbee2mqtt/commit/9426c3ea118de9e60326e8dc257a0c2474380f8c))
* **ignore:** bump the minor-patch group with 3 updates ([#32106](https://github.com/Koenkk/zigbee2mqtt/issues/32106)) ([e5f70d5](https://github.com/Koenkk/zigbee2mqtt/commit/e5f70d5ba82923f9f37fbda79bed08d581706129))
* **ignore:** update zigbee-herdsman to 10.0.8 ([#31945](https://github.com/Koenkk/zigbee2mqtt/issues/31945)) ([04a4365](https://github.com/Koenkk/zigbee2mqtt/commit/04a43652d6e131694e29e06ca646eeac37ed3c7d))
* **ignore:** update zigbee-herdsman to 10.1.0 ([#32027](https://github.com/Koenkk/zigbee2mqtt/issues/32027)) ([36c4db7](https://github.com/Koenkk/zigbee2mqtt/commit/36c4db7f8f4e3438f6ebb82cd6ecadf6c54e231e))
* **ignore:** update zigbee-herdsman-converters to 26.47.0 ([#31946](https://github.com/Koenkk/zigbee2mqtt/issues/31946)) ([7a05464](https://github.com/Koenkk/zigbee2mqtt/commit/7a05464f0355380ee855415bc270632263e3f5d3))
* **ignore:** update zigbee-herdsman-converters to 26.48.0 ([#31959](https://github.com/Koenkk/zigbee2mqtt/issues/31959)) ([502030c](https://github.com/Koenkk/zigbee2mqtt/commit/502030c0168c8ba6eaf7495a9014489f2f7a7c50))
* **ignore:** update zigbee-herdsman-converters to 26.49.0 ([#31976](https://github.com/Koenkk/zigbee2mqtt/issues/31976)) ([32499d1](https://github.com/Koenkk/zigbee2mqtt/commit/32499d14d5f0c9eada160d596d6be92da6c4af95))
* **ignore:** update zigbee-herdsman-converters to 26.50.0 ([#31988](https://github.com/Koenkk/zigbee2mqtt/issues/31988)) ([6ea21c3](https://github.com/Koenkk/zigbee2mqtt/commit/6ea21c383b4801f28e30b022194e71375db94b17))
* **ignore:** update zigbee-herdsman-converters to 26.51.0 ([#31999](https://github.com/Koenkk/zigbee2mqtt/issues/31999)) ([744e4a6](https://github.com/Koenkk/zigbee2mqtt/commit/744e4a6d5c4dd179e7c3d1a2b4fbcdc820195547))
* **ignore:** update zigbee-herdsman-converters to 26.51.1 ([#32016](https://github.com/Koenkk/zigbee2mqtt/issues/32016)) ([5458e50](https://github.com/Koenkk/zigbee2mqtt/commit/5458e5084def411aaad9af08e415acb6bc16d181))
* **ignore:** update zigbee-herdsman-converters to 26.52.0 ([#32040](https://github.com/Koenkk/zigbee2mqtt/issues/32040)) ([1e114de](https://github.com/Koenkk/zigbee2mqtt/commit/1e114dee70b8079eda31d4d0cc52fa073a772798))
* **ignore:** update zigbee-herdsman-converters to 26.53.0 ([#32051](https://github.com/Koenkk/zigbee2mqtt/issues/32051)) ([52201c9](https://github.com/Koenkk/zigbee2mqtt/commit/52201c96bf897af9bcad30435575923a0452b6dd))
* **ignore:** update zigbee-herdsman-converters to 26.54.0 ([#32065](https://github.com/Koenkk/zigbee2mqtt/issues/32065)) ([2c5b536](https://github.com/Koenkk/zigbee2mqtt/commit/2c5b53613448ae4c6548f874a36acc1226d53ec3))
* **ignore:** update zigbee-herdsman-converters to 26.55.0 ([#32068](https://github.com/Koenkk/zigbee2mqtt/issues/32068)) ([7075b78](https://github.com/Koenkk/zigbee2mqtt/commit/7075b78a462a0d1f76cf91cc78601d90edfd8b4e))
* **ignore:** update zigbee-herdsman-converters to 26.56.0 ([#32087](https://github.com/Koenkk/zigbee2mqtt/issues/32087)) ([56aab4e](https://github.com/Koenkk/zigbee2mqtt/commit/56aab4e59c4ea7bdbd93a1ed6632171994d8ed56))
* **ignore:** update zigbee-herdsman-converters to 26.57.0 ([#32113](https://github.com/Koenkk/zigbee2mqtt/issues/32113)) ([7a1c2d5](https://github.com/Koenkk/zigbee2mqtt/commit/7a1c2d52f0b0699854f3c8c9375761cb0b22ac55))
* **ignore:** update zigbee-herdsman-converters to 26.58.0 ([#32126](https://github.com/Koenkk/zigbee2mqtt/issues/32126)) ([e2b4911](https://github.com/Koenkk/zigbee2mqtt/commit/e2b49113c7932c4cd8ef7b23a27e3cd5d4e7150d))
* **ignore:** update zigbee-herdsman-converters to 26.59.1 ([#32136](https://github.com/Koenkk/zigbee2mqtt/issues/32136)) ([a8364ad](https://github.com/Koenkk/zigbee2mqtt/commit/a8364ad67b0dcbde93b1d6445c269a821321e9b3))
* **ignore:** update zigbee-herdsman-converters to 26.60.0 ([#32150](https://github.com/Koenkk/zigbee2mqtt/issues/32150)) ([c6e13cc](https://github.com/Koenkk/zigbee2mqtt/commit/c6e13ccb45666f9604e8c9c9655bbe7ea682712d))
* **ignore:** update zigbee-herdsman-converters to 26.61.1 ([#32156](https://github.com/Koenkk/zigbee2mqtt/issues/32156)) ([58feb78](https://github.com/Koenkk/zigbee2mqtt/commit/58feb783626fd2429bea0f8a0db37bdc107c431b))
* Prevent invalid external JS file name on save ([#32037](https://github.com/Koenkk/zigbee2mqtt/issues/32037)) ([bbcbed1](https://github.com/Koenkk/zigbee2mqtt/commit/bbcbed12d53408eaa8ceec110c7071fff8e158e4))
* Use Jinja-safe property access in HA discovery templates ([#31930](https://github.com/Koenkk/zigbee2mqtt/issues/31930)) ([c4fd415](https://github.com/Koenkk/zigbee2mqtt/commit/c4fd415cbe7b01719679b1a96d2b6e07b0793125))
## [2.10.1](https://github.com/Koenkk/zigbee2mqtt/compare/2.10.0...2.10.1) (2026-05-07)
### Bug Fixes
* **ignore:** bump @biomejs/biome from 2.4.13 to 2.4.14 in the minor-patch group across 1 directory ([#31881](https://github.com/Koenkk/zigbee2mqtt/issues/31881)) ([55f12ff](https://github.com/Koenkk/zigbee2mqtt/commit/55f12ff52063d576be0fe82891678c52181f3b8e))
* **ignore:** bump express-static-gzip from 3.0.0 to 3.0.1 in the minor-patch group ([#31924](https://github.com/Koenkk/zigbee2mqtt/issues/31924)) ([24e9027](https://github.com/Koenkk/zigbee2mqtt/commit/24e90273c9e712e122887e74ccf8208a8b42e762))
* **ignore:** update zigbee-herdsman-converters to 26.43.0 ([#31878](https://github.com/Koenkk/zigbee2mqtt/issues/31878)) ([19eb05a](https://github.com/Koenkk/zigbee2mqtt/commit/19eb05a0e023c7e2e8639c1dcda18399e96ae74d))
* **ignore:** update zigbee-herdsman-converters to 26.44.0 ([#31900](https://github.com/Koenkk/zigbee2mqtt/issues/31900)) ([845bdb7](https://github.com/Koenkk/zigbee2mqtt/commit/845bdb7dba5fe0f5e67e1b92c68872da0f3abf6a))
* **ignore:** update zigbee-herdsman-converters to 26.45.0 ([#31925](https://github.com/Koenkk/zigbee2mqtt/issues/31925)) ([8727abd](https://github.com/Koenkk/zigbee2mqtt/commit/8727abdefc9114a5e2e1036d764271c90065e0a1))
* **ignore:** update zigbee-herdsman-converters to 26.46.0 ([#31937](https://github.com/Koenkk/zigbee2mqtt/issues/31937)) ([cc84566](https://github.com/Koenkk/zigbee2mqtt/commit/cc8456617a4a389e7297e380901bfd6bb0223b32))
* Replace deprecated `url.parse` ([#31845](https://github.com/Koenkk/zigbee2mqtt/issues/31845)) ([9f7ea9b](https://github.com/Koenkk/zigbee2mqtt/commit/9f7ea9b7c79db7a781b431fbe43568e15647f8e5))
## [2.10.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.9.2...2.10.0) (2026-05-01)
### Features
* Home Assistant: add group entities in discovery config ([#31663](https://github.com/Koenkk/zigbee2mqtt/issues/31663)) ([0419726](https://github.com/Koenkk/zigbee2mqtt/commit/041972669a6a8108e8fee1a0fffb555292371285))
### Bug Fixes
* Clarify units of pause_on_backoff_gt ([#31668](https://github.com/Koenkk/zigbee2mqtt/issues/31668)) ([4a63e65](https://github.com/Koenkk/zigbee2mqtt/commit/4a63e65981b6bbd746a730e7ca2291aba7a6c975))
* **deps:** bump googleapis/release-please-action from 4 to 5 ([#31805](https://github.com/Koenkk/zigbee2mqtt/issues/31805)) ([4e527bb](https://github.com/Koenkk/zigbee2mqtt/commit/4e527bbce8612c85fd636a1c493059af9793991f))
* **ignore:** bump @biomejs/biome from 2.4.10 to 2.4.11 in the minor-patch group ([#31699](https://github.com/Koenkk/zigbee2mqtt/issues/31699)) ([cdd7357](https://github.com/Koenkk/zigbee2mqtt/commit/cdd73575227527ae7eca5732d96687a30cea8538))
* **ignore:** bump @types/node from 24.12.0 to 24.12.2 in the minor-patch group across 1 directory ([#31628](https://github.com/Koenkk/zigbee2mqtt/issues/31628)) ([e6dd5c7](https://github.com/Koenkk/zigbee2mqtt/commit/e6dd5c73bc2d76b32c45ef19633626dbcdde8379))
* **ignore:** bump the minor-patch group with 2 updates ([#31758](https://github.com/Koenkk/zigbee2mqtt/issues/31758)) ([7ba232e](https://github.com/Koenkk/zigbee2mqtt/commit/7ba232e27d973119530119ca136aaab3e8e22ab6))
* **ignore:** bump the minor-patch group with 3 updates ([#31806](https://github.com/Koenkk/zigbee2mqtt/issues/31806)) ([b3983b3](https://github.com/Koenkk/zigbee2mqtt/commit/b3983b35de0b6242d8e4d01f0d1d1e535e2d0b60))
* **ignore:** bump typescript from 5.9.3 to 6.0.2 ([#31560](https://github.com/Koenkk/zigbee2mqtt/issues/31560)) ([9e27181](https://github.com/Koenkk/zigbee2mqtt/commit/9e27181b64b771250db283b5a4b1da7835470ee6))
* **ignore:** update zigbee-herdsman to 10.0.6 ([#31636](https://github.com/Koenkk/zigbee2mqtt/issues/31636)) ([aef3242](https://github.com/Koenkk/zigbee2mqtt/commit/aef32423195914aa9ef08875d4fe6de36df71f11))
* **ignore:** update zigbee-herdsman to 10.0.7 ([#31673](https://github.com/Koenkk/zigbee2mqtt/issues/31673)) ([a0204ad](https://github.com/Koenkk/zigbee2mqtt/commit/a0204ad3f00ed366e4c41455ae4189bd31870a95))
* **ignore:** update zigbee-herdsman-converters to 26.28.0 ([#31574](https://github.com/Koenkk/zigbee2mqtt/issues/31574)) ([08e4e9e](https://github.com/Koenkk/zigbee2mqtt/commit/08e4e9edeb048a07f89d6283e5bea04274a3fb47))
* **ignore:** update zigbee-herdsman-converters to 26.29.0 ([#31586](https://github.com/Koenkk/zigbee2mqtt/issues/31586)) ([23f9847](https://github.com/Koenkk/zigbee2mqtt/commit/23f9847994da94fa8b38ba823a8e55418b006e7c))
* **ignore:** update zigbee-herdsman-converters to 26.30.0 ([#31608](https://github.com/Koenkk/zigbee2mqtt/issues/31608)) ([6b567c1](https://github.com/Koenkk/zigbee2mqtt/commit/6b567c1286201aa9fef4744f7378798bc852c9ff))
* **ignore:** update zigbee-herdsman-converters to 26.31.0 ([#31637](https://github.com/Koenkk/zigbee2mqtt/issues/31637)) ([a50065c](https://github.com/Koenkk/zigbee2mqtt/commit/a50065cbb40bb149a87046bb83a7602a5587b3b0))
* **ignore:** update zigbee-herdsman-converters to 26.32.0 ([#31653](https://github.com/Koenkk/zigbee2mqtt/issues/31653)) ([4818cc4](https://github.com/Koenkk/zigbee2mqtt/commit/4818cc4163d578990339daf6c5e725bb40a08216))
* **ignore:** update zigbee-herdsman-converters to 26.33.1 ([#31674](https://github.com/Koenkk/zigbee2mqtt/issues/31674)) ([0c9ec72](https://github.com/Koenkk/zigbee2mqtt/commit/0c9ec722db3f53c57870de7f09de9971d69cbda8))
* **ignore:** update zigbee-herdsman-converters to 26.34.0 ([#31702](https://github.com/Koenkk/zigbee2mqtt/issues/31702)) ([55d8d1f](https://github.com/Koenkk/zigbee2mqtt/commit/55d8d1f4d0a5895ce146dbec98630d82adcd4e22))
* **ignore:** update zigbee-herdsman-converters to 26.35.0 ([#31713](https://github.com/Koenkk/zigbee2mqtt/issues/31713)) ([0a7dfce](https://github.com/Koenkk/zigbee2mqtt/commit/0a7dfcef596c3201f9f0cd81960c0e4ad8add0c4))
* **ignore:** update zigbee-herdsman-converters to 26.36.0 ([#31737](https://github.com/Koenkk/zigbee2mqtt/issues/31737)) ([a54775d](https://github.com/Koenkk/zigbee2mqtt/commit/a54775dea10a14b6d2f25ebb3c79a8a9d979e970))
* **ignore:** update zigbee-herdsman-converters to 26.37.0 ([#31749](https://github.com/Koenkk/zigbee2mqtt/issues/31749)) ([78f440b](https://github.com/Koenkk/zigbee2mqtt/commit/78f440bada9a14abb1d84af12fc77b1af7d250ea))
* **ignore:** update zigbee-herdsman-converters to 26.38.0 ([#31768](https://github.com/Koenkk/zigbee2mqtt/issues/31768)) ([305b7bc](https://github.com/Koenkk/zigbee2mqtt/commit/305b7bcafab59250173eddb9c08cb2fed0d966a0))
* **ignore:** update zigbee-herdsman-converters to 26.38.1 ([#31777](https://github.com/Koenkk/zigbee2mqtt/issues/31777)) ([3bb3d56](https://github.com/Koenkk/zigbee2mqtt/commit/3bb3d56c05b92e349d2b117973df6cf9414b8450))
* **ignore:** update zigbee-herdsman-converters to 26.39.1 ([#31783](https://github.com/Koenkk/zigbee2mqtt/issues/31783)) ([e715078](https://github.com/Koenkk/zigbee2mqtt/commit/e71507835b2c0673ec0df488e9d1db39a7b64a05))
* **ignore:** update zigbee-herdsman-converters to 26.40.0 ([#31800](https://github.com/Koenkk/zigbee2mqtt/issues/31800)) ([429c5ae](https://github.com/Koenkk/zigbee2mqtt/commit/429c5aea585da1525347adc13712bc556b4ea2ae))
* **ignore:** update zigbee-herdsman-converters to 26.41.0 ([#31820](https://github.com/Koenkk/zigbee2mqtt/issues/31820)) ([c581623](https://github.com/Koenkk/zigbee2mqtt/commit/c58162344fddc1cdb49027f6dcc9f1f35030bc61))
* **ignore:** update zigbee-herdsman-converters to 26.42.0 ([#31830](https://github.com/Koenkk/zigbee2mqtt/issues/31830)) ([df26459](https://github.com/Koenkk/zigbee2mqtt/commit/df26459b46e67f4f3b1079eb6fa6a439bf1979af))
* Improve transmit power description ([#31735](https://github.com/Koenkk/zigbee2mqtt/issues/31735)) ([5974286](https://github.com/Koenkk/zigbee2mqtt/commit/59742864457a9e29783363ebde8ede0e0aca5ea2))
## [2.9.2](https://github.com/Koenkk/zigbee2mqtt/compare/2.9.1...2.9.2) (2026-03-31)
### Bug Fixes
* Export definition version ([#31270](https://github.com/Koenkk/zigbee2mqtt/issues/31270)) ([9f1eb76](https://github.com/Koenkk/zigbee2mqtt/commit/9f1eb764a62b041a7d9782066a8ffcaa3562eaad))
* Home Assistant: fix entity names for derived weather sensors by removing `device_class` ([#31234](https://github.com/Koenkk/zigbee2mqtt/issues/31234)) ([b7b4303](https://github.com/Koenkk/zigbee2mqtt/commit/b7b43030eb8aaa144dbb5477805638d860efdd38))
* **ignore:** bump @biomejs/biome from 2.4.4 to 2.4.5 in the minor-patch group ([#31237](https://github.com/Koenkk/zigbee2mqtt/issues/31237)) ([aefbb8b](https://github.com/Koenkk/zigbee2mqtt/commit/aefbb8be14ae5d84495278cbe9a6ca9d750553cb))
* **ignore:** bump the minor-patch group with 2 updates ([#31411](https://github.com/Koenkk/zigbee2mqtt/issues/31411)) ([67aa509](https://github.com/Koenkk/zigbee2mqtt/commit/67aa5099df2371eb9ee220be7a8821f0d4f8b38b))
* **ignore:** bump the minor-patch group with 3 updates ([#31295](https://github.com/Koenkk/zigbee2mqtt/issues/31295)) ([b7f1cec](https://github.com/Koenkk/zigbee2mqtt/commit/b7f1cecb49f088b7fc79dcef068034ca1d012c1d))
* **ignore:** bump the minor-patch group with 3 updates ([#31489](https://github.com/Koenkk/zigbee2mqtt/issues/31489)) ([52b063b](https://github.com/Koenkk/zigbee2mqtt/commit/52b063bb32429e14b30319c905e596b7632bd674))
* **ignore:** bump the minor-patch group with 3 updates ([#31559](https://github.com/Koenkk/zigbee2mqtt/issues/31559)) ([73d8ae8](https://github.com/Koenkk/zigbee2mqtt/commit/73d8ae895613af0d77f0a684f7305d688af11146))
* **ignore:** update zigbee-herdsman to 10.0.1 ([#31383](https://github.com/Koenkk/zigbee2mqtt/issues/31383)) ([6fca374](https://github.com/Koenkk/zigbee2mqtt/commit/6fca3744c9715dd4afb7fc777cf783c84bfb91fc))
* **ignore:** update zigbee-herdsman to 10.0.2 ([#31413](https://github.com/Koenkk/zigbee2mqtt/issues/31413)) ([075f87c](https://github.com/Koenkk/zigbee2mqtt/commit/075f87c7822c29c74fc1c4031866aca8c445c9be))
* **ignore:** update zigbee-herdsman to 10.0.3 ([#31450](https://github.com/Koenkk/zigbee2mqtt/issues/31450)) ([6fb9d56](https://github.com/Koenkk/zigbee2mqtt/commit/6fb9d563e69c2db3b31fd0f163e5f30924047e3f))
* **ignore:** update zigbee-herdsman to 10.0.4 ([#31478](https://github.com/Koenkk/zigbee2mqtt/issues/31478)) ([867179c](https://github.com/Koenkk/zigbee2mqtt/commit/867179c430eacf9bd66ef359b9a7435f2299d944))
* **ignore:** update zigbee-herdsman to 10.0.5 ([#31562](https://github.com/Koenkk/zigbee2mqtt/issues/31562)) ([fb3189b](https://github.com/Koenkk/zigbee2mqtt/commit/fb3189b7431cb203092bbe235bd35194e2b44a44))
* **ignore:** update zigbee-herdsman to 9.0.10 ([#31245](https://github.com/Koenkk/zigbee2mqtt/issues/31245)) ([d632382](https://github.com/Koenkk/zigbee2mqtt/commit/d6323825e4ae3168d52a096e7647bc7174d3cdb1))
* **ignore:** update zigbee-herdsman to 9.0.11 ([#31257](https://github.com/Koenkk/zigbee2mqtt/issues/31257)) ([1747618](https://github.com/Koenkk/zigbee2mqtt/commit/1747618afa3948ac9fb8899d94fba8456198e4b5))
* **ignore:** update zigbee-herdsman-converters to 26.13.0 ([#31231](https://github.com/Koenkk/zigbee2mqtt/issues/31231)) ([fc3a196](https://github.com/Koenkk/zigbee2mqtt/commit/fc3a1968f6635d246ad2d68e5c6b79d495fd96bb))
* **ignore:** update zigbee-herdsman-converters to 26.14.0 ([#31246](https://github.com/Koenkk/zigbee2mqtt/issues/31246)) ([284d86e](https://github.com/Koenkk/zigbee2mqtt/commit/284d86e93ba8cd83139d6dd0217519f67c01e565))
* **ignore:** update zigbee-herdsman-converters to 26.16.1 ([#31268](https://github.com/Koenkk/zigbee2mqtt/issues/31268)) ([05946b5](https://github.com/Koenkk/zigbee2mqtt/commit/05946b59552608c6ae8a8b02098552fe611f027a))
* **ignore:** update zigbee-herdsman-converters to 26.17.0 ([#31314](https://github.com/Koenkk/zigbee2mqtt/issues/31314)) ([9143924](https://github.com/Koenkk/zigbee2mqtt/commit/91439244823d9897998a57448321d2e8af9b083f))
* **ignore:** update zigbee-herdsman-converters to 26.18.0 ([#31362](https://github.com/Koenkk/zigbee2mqtt/issues/31362)) ([f60db2d](https://github.com/Koenkk/zigbee2mqtt/commit/f60db2d7a641ab5fccbb3a7b6a0e1e73983559ef))
* **ignore:** update zigbee-herdsman-converters to 26.19.1 ([#31384](https://github.com/Koenkk/zigbee2mqtt/issues/31384)) ([69641d5](https://github.com/Koenkk/zigbee2mqtt/commit/69641d5467ec5e3ddfebb4ff98142d0ff990c814))
* **ignore:** update zigbee-herdsman-converters to 26.20.0 ([#31415](https://github.com/Koenkk/zigbee2mqtt/issues/31415)) ([9f49d0e](https://github.com/Koenkk/zigbee2mqtt/commit/9f49d0ec6738ebd1ea65a3bf1aae553548217f2e))
* **ignore:** update zigbee-herdsman-converters to 26.21.0 ([#31427](https://github.com/Koenkk/zigbee2mqtt/issues/31427)) ([e054d71](https://github.com/Koenkk/zigbee2mqtt/commit/e054d7124bd4dc5bdb18c9de4a6923b06df6b929))
* **ignore:** update zigbee-herdsman-converters to 26.22.0 ([#31451](https://github.com/Koenkk/zigbee2mqtt/issues/31451)) ([64286eb](https://github.com/Koenkk/zigbee2mqtt/commit/64286ebb3054bc15e020aab431c1d3af76e2c914))
* **ignore:** update zigbee-herdsman-converters to 26.23.0 ([#31462](https://github.com/Koenkk/zigbee2mqtt/issues/31462)) ([2e5e362](https://github.com/Koenkk/zigbee2mqtt/commit/2e5e3628188765cddbd4116f41703eab491670df))
* **ignore:** update zigbee-herdsman-converters to 26.24.0 ([#31479](https://github.com/Koenkk/zigbee2mqtt/issues/31479)) ([dde637a](https://github.com/Koenkk/zigbee2mqtt/commit/dde637af2f8449ae24684fa34a32bec106c24913))
* **ignore:** update zigbee-herdsman-converters to 26.25.0 ([#31497](https://github.com/Koenkk/zigbee2mqtt/issues/31497)) ([66985a8](https://github.com/Koenkk/zigbee2mqtt/commit/66985a8ce8d4cefbc4ddacc754e182e0b5953cea))
* **ignore:** update zigbee-herdsman-converters to 26.26.0 ([#31510](https://github.com/Koenkk/zigbee2mqtt/issues/31510)) ([969db9a](https://github.com/Koenkk/zigbee2mqtt/commit/969db9ae85838b955e07a80f5f72f655f6beffcf))
* **ignore:** update zigbee-herdsman-converters to 26.27.0 ([#31563](https://github.com/Koenkk/zigbee2mqtt/issues/31563)) ([62ab814](https://github.com/Koenkk/zigbee2mqtt/commit/62ab814d73b7de957fb8b28679b9dea8ae087133))
* Match new cluster-related typing from ZH ([#31298](https://github.com/Koenkk/zigbee2mqtt/issues/31298)) ([0f781ee](https://github.com/Koenkk/zigbee2mqtt/commit/0f781ee9c03ae502745841f4dd7d6f3775c620d4))
* Reintroduce onboarding improvements ([#31273](https://github.com/Koenkk/zigbee2mqtt/issues/31273)) ([4a83692](https://github.com/Koenkk/zigbee2mqtt/commit/4a8369295c5d6c07ff1ac66f90a35b0e8b915296))
## [2.9.1](https://github.com/Koenkk/zigbee2mqtt/compare/2.9.0...2.9.1) (2026-03-02)
### Bug Fixes
* Fix onboarding ([#31228](https://github.com/Koenkk/zigbee2mqtt/issues/31228)) ([7f0a9aa](https://github.com/Koenkk/zigbee2mqtt/commit/7f0a9aa2a30ab79c182d8ea9e875339e9031fdbd))
## [2.9.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.8.0...2.9.0) (2026-03-01)
+10 -4
View File
@@ -1,8 +1,14 @@
# Contributing to Zigbee2MQTT
Everybody is invited and welcome to contribute to Zigbee2MQTT. Zigbee2MQTT is written in JavaScript and is based upon [zigbee-herdsman](https://github.com/koenkk/zigbee-herdsman) and [zigbee-herdsman-converters](https://github.com/koenkk/zigbee-herdsman-converters). Zigbee-herdsman-converters contains all device definitions, zigbee-herdsman is responsible for handling all communication with the adapter.
> [!WARNING]
> Any AI-driven pull request with more than 500 lines of code will not be considered. If wanting to submit something that requires more, split the work into easily review-able pull requests that can be introduced in increments (e.g. pre-refactor, base feature, additional features).
Everybody is invited and welcomed to contribute to Zigbee2MQTT.
Zigbee2MQTT is written in TypeScript.
It uses [zigbee-herdsman](https://github.com/koenkk/zigbee-herdsman) for communication with the adapter/coordinator and [zigbee-herdsman-converters](https://github.com/koenkk/zigbee-herdsman-converters) to provide device-specific definitions.
- Pull requests are always created against the [**dev**](https://github.com/Koenkk/zigbee2mqtt/tree/dev) branch.
- Easiest way to start developing Zigbee2MQTT is by setting up a development environment (aka bare-metal installation). You can follow this [guide](https://www.zigbee2mqtt.io/guide/installation/01_linux.html) to do this.
- You can run the tests locally by executing `pnpm test`. Zigbee2MQTT enforces 100% code coverage, in case you add new code check if your code is covered by running `pnpm run test:coverage`. The coverage report can be found under `coverage/lcov-report/index.html`. Linting is also enforced and can be run with `pnpm run eslint`.
- When you want to add support for a new device no changes to Zigbee2MQTT have to be made, only to zigbee-herdsman-converters. You can find a guide for it [here](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html).
- Easiest way to start developing Zigbee2MQTT is by setting up a development environment (a.k.a. bare-metal installation). You can follow this [guide](https://www.zigbee2mqtt.io/guide/installation/01_linux.html) to do this.
- You can run the tests locally by executing `pnpm test`. Zigbee2MQTT enforces 100% code coverage, in case you add new code check if your code is covered by running `pnpm run test:coverage`. The coverage report can be found under `coverage/lcov-report/index.html`.
- Linting & formatting is also enforced and can be run with `pnpm run check` (can use `pnpm run check:w` to fix small issues automatically).
- If you want to add support for a new device no change to Zigbee2MQTT has to be made, only to zigbee-herdsman-converters. You can find a guide for it [here](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html).
+2 -2
View File
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.1/schema.json",
"$schema": "https://biomejs.dev/schemas/2.5.3/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
@@ -12,7 +12,7 @@
"bracketSpacing": false
},
"files": {
"includes": ["**", "!package.json", "!!**/dist", "!!**/coverage"]
"includes": ["**", "!package.json", "!!**/dist", "!!**/coverage", "!images"]
},
"linter": {
"includes": ["**"],
+1 -1
View File
@@ -1,6 +1,6 @@
ARG TARGETPLATFORM
FROM alpine:3.23 AS base
FROM alpine:3.24 AS base
ENV NODE_ENV=production
WORKDIR /app
+2 -2
View File
@@ -1,7 +1,7 @@
const fs = require("node:fs");
const path = require("node:path");
const {exec} = require("node:child_process");
require("source-map-support").install();
process.setSourceMapsEnabled(true);
/** @type {import("./dist/controller").Controller | undefined} */
let controller;
@@ -12,7 +12,7 @@ let unsolicitedStop = false;
let watchdogDelays = [2000, 60000, 300000, 900000, 1800000, 3600000];
if (process.env.Z2M_WATCHDOG != null && process.env.Z2M_WATCHDOG !== "default") {
if (/^\d+(.\d+)?(,\d+(.\d+)?)*$/.test(process.env.Z2M_WATCHDOG)) {
if (/^\d+(\.\d+)?(,\d+(\.\d+)?)*$/.test(process.env.Z2M_WATCHDOG)) {
watchdogDelays = process.env.Z2M_WATCHDOG.split(",").map((v) => Number.parseFloat(v) * 60000);
} else {
console.log(`Invalid watchdog delays (must use number-only CSV format representing minutes, example: 'Z2M_WATCHDOG=1,5,15,30,60'.`);
+28 -20
View File
@@ -1,5 +1,4 @@
import bind from "bind-decorator";
import stringify from "json-stable-stringify-without-jsonify";
import {setLogger as zhSetLogger} from "zigbee-herdsman";
import {setLogger as zhcSetLogger} from "zigbee-herdsman-converters";
import EventBus from "./eventBus";
@@ -24,6 +23,7 @@ import type {Zigbee2MQTTAPI} from "./types/api";
import logger from "./util/logger";
import {initSdNotify} from "./util/sd-notify";
import * as settings from "./util/settings";
import {stringify} from "./util/stringify";
import utils from "./util/utils";
import Zigbee from "./zigbee";
@@ -50,7 +50,6 @@ export class Controller {
this.state = new State(this.eventBus, this.zigbee);
this.restartCallback = restartCallback;
this.exitCallback = exitCallback;
// Initialize extensions.
this.extensionArgs = [
this.zigbee,
@@ -62,22 +61,29 @@ export class Controller {
this.restartCallback,
this.addExtension,
];
this.extensions = new Set();
this.extensions = new Set([
new ExtensionExternalConverters(...this.extensionArgs),
new ExtensionOnEvent(...this.extensionArgs),
new ExtensionBridge(...this.extensionArgs),
new ExtensionPublish(...this.extensionArgs),
new ExtensionReceive(...this.extensionArgs),
new ExtensionConfigure(...this.extensionArgs),
new ExtensionNetworkMap(...this.extensionArgs),
new ExtensionGroups(...this.extensionArgs),
new ExtensionBind(...this.extensionArgs),
new ExtensionOTAUpdate(...this.extensionArgs),
new ExtensionExternalExtensions(...this.extensionArgs),
new ExtensionAvailability(...this.extensionArgs),
new ExtensionHealth(...this.extensionArgs),
]);
if (settings.get().advanced.enable_external_js) {
this.extensions.add(new ExtensionExternalConverters(...this.extensionArgs));
} else {
logger.info("External JS (converters/extensions) is disabled");
}
this.extensions.add(new ExtensionOnEvent(...this.extensionArgs));
this.extensions.add(new ExtensionBridge(...this.extensionArgs));
this.extensions.add(new ExtensionPublish(...this.extensionArgs));
this.extensions.add(new ExtensionReceive(...this.extensionArgs));
this.extensions.add(new ExtensionConfigure(...this.extensionArgs));
this.extensions.add(new ExtensionNetworkMap(...this.extensionArgs));
this.extensions.add(new ExtensionGroups(...this.extensionArgs));
this.extensions.add(new ExtensionBind(...this.extensionArgs));
this.extensions.add(new ExtensionOTAUpdate(...this.extensionArgs));
this.extensions.add(new ExtensionAvailability(...this.extensionArgs));
this.extensions.add(new ExtensionHealth(...this.extensionArgs));
if (settings.get().advanced.enable_external_js) {
this.extensions.add(new ExtensionExternalExtensions(...this.extensionArgs));
}
}
async start(): Promise<void> {
@@ -247,10 +253,12 @@ export class Controller {
}
const existingExtension = this.getExtension(name);
if (existingExtension) {
await this.removeExtension(existingExtension);
}
await this.extensions.add(extension);
this.extensions.add(extension);
} else {
switch (name) {
case "Frontend": {
@@ -454,7 +462,7 @@ export class Controller {
async iteratePayloadAttributeOutput(topicRoot: string, payload: KeyValue, options: Partial<MqttPublishOptions>): Promise<void> {
for (const [key, value] of Object.entries(payload)) {
let subPayload = value;
let message = null;
let message: string | undefined;
// Special cases
if (key === "color" && utils.objectHasProperties(subPayload, ["r", "g", "b"])) {
@@ -472,7 +480,7 @@ export class Controller {
message = typeof subPayload === "string" ? subPayload : stringify(subPayload);
}
if (message !== null) {
if (message !== undefined) {
await this.mqtt.publish(`${topicRoot}${key}`, message, options);
}
}
+6 -5
View File
@@ -247,19 +247,20 @@ export default class EventBus {
this.callbacksByExtension.set(key.constructor.name, []);
}
const wrappedCallback = async (...args: never[]): Promise<void> => {
const typedCallback = callback as (...args: EventBusMap[K]) => Promise<void> | void;
const wrappedCallback = (async (...args: EventBusMap[K]): Promise<void> => {
try {
await callback(...args);
await typedCallback(...args);
} catch (error) {
logger.error(`EventBus error '${key.constructor.name}/${event}': ${(error as Error).message}`);
// biome-ignore lint/style/noNonNullAssertion: always Error
logger.debug((error as Error).stack!);
}
};
}) as EventBusListener<keyof EventBusMap>;
// biome-ignore lint/style/noNonNullAssertion: just created if wasn't valid
this.callbacksByExtension.get(key.constructor.name)!.push({event, callback: wrappedCallback});
this.emitter.on(event, wrappedCallback as EventBusListener<K>);
(this.emitter as events.EventEmitter).on(event, wrappedCallback);
}
public removeListeners(key: ListenerKey): void {
@@ -267,7 +268,7 @@ export default class EventBus {
if (callbacks) {
for (const cb of callbacks) {
this.emitter.removeListener(cb.event, cb.callback);
(this.emitter as events.EventEmitter).removeListener(cb.event, cb.callback);
}
}
}
+1 -1
View File
@@ -1,7 +1,6 @@
import assert from "node:assert";
import bind from "bind-decorator";
import debounce from "debounce";
import stringify from "json-stable-stringify-without-jsonify";
import {Zcl} from "zigbee-herdsman";
import type {TClusterAttributeKeys} from "zigbee-herdsman/dist/zspec/zcl/definition/clusters-types";
import type {ClusterName} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
@@ -10,6 +9,7 @@ import Group from "../model/group";
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from "../types/api";
import logger from "../util/logger";
import * as settings from "../util/settings";
import {stringify} from "../util/stringify";
import utils, {DEFAULT_BIND_GROUP_ID} from "../util/utils";
import Extension from "./extension";
+50 -24
View File
@@ -1,8 +1,7 @@
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";
import {zip} from "fflate";
import objectAssignDeep from "object-assign-deep";
import type winston from "winston";
import Transport from "winston-transport";
@@ -15,6 +14,7 @@ import type {Zigbee2MQTTAPI, Zigbee2MQTTDevice, Zigbee2MQTTResponse, Zigbee2MQTT
import data from "../util/data";
import logger from "../util/logger";
import * as settings from "../util/settings";
import {stringify} from "../util/stringify";
import utils, {assertString, DEFAULT_BIND_GROUP_ID} from "../util/utils";
import Extension from "./extension";
@@ -141,6 +141,7 @@ export default class Bridge extends Extension {
await this.mqtt.publish("bridge/event", stringify(payload));
});
this.eventBus.onDeviceLeave(this, async (data) => {
await this.publishGroups();
await this.publishDevices();
await this.publishDefinitions();
@@ -243,7 +244,10 @@ export default class Bridge extends Extension {
}
const newSettings = message.options as Partial<Settings>;
this.restartRequired = settings.apply(newSettings);
const newRestartRequired = settings.apply(newSettings);
if (newRestartRequired) {
this.restartRequired = newRestartRequired;
}
// Apply some settings on-the-fly.
if (newSettings.homeassistant) {
@@ -262,7 +266,11 @@ export default class Bridge extends Extension {
logger.setDebugNamespaceIgnore(settings.get().advanced.log_debug_namespace_ignore);
}
logger.info("Successfully changed options");
if (newRestartRequired) {
logger.info("Changes require restart to take effect");
} else {
logger.info("Successfully changed options");
}
await this.publishInfo();
return utils.getResponse(message, {restart_required: this.restartRequired});
}
@@ -321,7 +329,7 @@ export default class Bridge extends Extension {
await this.zigbee.backup();
const dataPath = data.getPath();
const files = utils.getAllFiles(dataPath);
const zip = new JSZip();
const zipFiles: Record<string, Uint8Array> = {};
const logDir = `log${path.sep}`;
const otaDir = `ota${path.sep}`;
@@ -330,12 +338,17 @@ export default class Bridge extends Extension {
// 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));
zipFiles[name] = fs.readFileSync(f);
}
}
const base64Zip = await zip.generateAsync({type: "base64"});
return utils.getResponse(message, {zip: base64Zip});
const zipContent = await new Promise<Uint8Array>((resolve, reject) => {
// `jszip` defaulted to `STORE`, so backups used to be uncompressed; `fflate`'s default level shrinks them substantially
zip(zipFiles, {level: 6}, (error, data) => (error ? reject(error) : resolve(data)));
});
// TODO: replace with `zipContent.toBase64()` once the Node requirement is >=25
return utils.getResponse(message, {zip: Buffer.from(zipContent).toString("base64")});
}
@bind async installCodeAdd(message: KeyValue | string): Promise<Zigbee2MQTTResponse<"bridge/response/install_code/add">> {
@@ -464,12 +477,16 @@ export default class Bridge extends Extension {
}
}
const restartRequired = settings.changeEntityOptions(ID, message.options);
if (restartRequired) this.restartRequired = true;
const newRestartRequired = settings.changeEntityOptions(ID, message.options);
if (newRestartRequired) this.restartRequired = true;
const newOptions = cleanup(entity.options);
await this.publishInfo();
logger.info(`Changed config for ${entityType} ${ID}`);
if (newRestartRequired) {
logger.info(`New config for ${entityType} ${ID} requires restart to take effect`);
} else {
logger.info(`Successfully changed config for ${entityType} ${ID}`);
}
this.eventBus.emitEntityOptionsChanged({from: oldOptions, to: newOptions, entity});
return utils.getResponse(message, {from: oldOptions, to: newOptions, id: ID, restart_required: this.restartRequired});
@@ -653,20 +670,25 @@ export default class Bridge extends Extension {
entityType: T,
message: string | KeyValue,
): Promise<Zigbee2MQTTResponse<T extends "device" ? "bridge/response/device/remove" : "bridge/response/group/remove">> {
const ID = typeof message === "object" ? message.id : message.trim();
const messageIsObject = typeof message === "object";
const ID = messageIsObject ? message.id : message.trim();
const entity = this.getEntity(entityType, ID);
// note: entity.name is dynamically retrieved, will change once device is removed (friendly => ieee)
const friendlyName = entity.name;
let block = false;
let force = false;
let clearCache = false;
let blockForceLog = "";
if (entityType === "device" && typeof message === "object") {
block = !!message.block;
force = !!message.force;
blockForceLog = ` (block: ${block}, force: ${force})`;
} else if (entityType === "group" && typeof message === "object") {
force = !!message.force;
if (entityType === "device" && messageIsObject) {
const payload = message as Zigbee2MQTTAPI["bridge/request/device/remove"];
block = !!payload.block;
force = !!payload.force;
clearCache = !!payload.clear_cache;
blockForceLog = ` (block: ${block}, force: ${force}, clear cache: ${clearCache})`;
} else if (entityType === "group" && messageIsObject) {
const payload = message as Zigbee2MQTTAPI["bridge/request/group/remove"];
force = !!payload.force;
blockForceLog = ` (force: ${force})`;
}
@@ -679,9 +701,13 @@ export default class Bridge extends Extension {
}
if (force) {
entity.zh.removeFromDatabase();
entity.zh.removeFromDatabase(clearCache);
} else {
await entity.zh.removeFromNetwork();
await entity.zh.removeFromNetwork(clearCache);
}
if (clearCache) {
this.zigbee.removeDeviceFromLookup(entity.ID);
}
settings.removeDevice(entity.ID as string);
@@ -706,19 +732,18 @@ export default class Bridge extends Extension {
logger.info(`Successfully removed ${entityType} '${friendlyName}'${blockForceLog}`);
await this.publishGroups();
if (entity instanceof Device) {
await this.publishGroups();
await this.publishDevices();
// Refresh Cluster definition
await this.publishDefinitions();
const responseData: Zigbee2MQTTAPI["bridge/response/device/remove"] = {id: ID, block, force};
const responseData: Zigbee2MQTTAPI["bridge/response/device/remove"] = {id: ID, block, force, clear_cache: clearCache};
return utils.getResponse(message, responseData);
}
await this.publishGroups();
const responseData: Zigbee2MQTTAPI["bridge/response/group/remove"] = {id: ID, force};
return utils.getResponse(
@@ -900,6 +925,7 @@ export default class Bridge extends Extension {
exposes: device.exposes(),
supports_ota: !!device.definition.ota,
options: device.definition.options ?? [],
version: device.definition.version,
icon,
};
+3 -3
View File
@@ -1,9 +1,9 @@
import bind from "bind-decorator";
import stringify from "json-stable-stringify-without-jsonify";
import Device from "../model/device";
import type {Zigbee2MQTTAPI} from "../types/api";
import logger from "../util/logger";
import * as settings from "../util/settings";
import {stringify} from "../util/stringify";
import utils from "../util/utils";
import Extension from "./extension";
@@ -38,7 +38,7 @@ export default class Configure extends Extension {
if (!device || !(device instanceof Device)) {
error = `Device '${ID}' does not exist`;
} else if (!device.definition || !device.definition.configure) {
} else if (!device.definition?.configure) {
error = `Device '${device.name}' cannot be configured`;
} else {
try {
@@ -137,7 +137,7 @@ export default class Configure extends Extension {
logger.info(`Successfully configured '${device.name}' (definition v${definitionVersion})`);
device.zh.meta.configured = definitionVersion;
device.zh.save();
this.eventBus.emitDevicesChanged();
this.eventBus.emitExposesAndDevicesChanged(device);
} catch (error) {
const newAttempts = attempts + 1;
this.attempts.set(device.ieeeAddr, newAttempts);
+7 -2
View File
@@ -2,12 +2,11 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import bind from "bind-decorator";
import stringify from "json-stable-stringify-without-jsonify";
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponse} from "../types/api";
import data from "../util/data";
import logger from "../util/logger";
import * as settings from "../util/settings";
import {stringify} from "../util/stringify";
import utils from "../util/utils";
import Extension from "./extension";
@@ -176,7 +175,13 @@ export default abstract class ExternalJSExtension<M> extends Extension {
}
const {name, code} = message;
if (!name.endsWith(".mjs") && !name.endsWith(".js") && !name.endsWith(".cjs")) {
return utils.getResponse(message, {}, "JavaScript file must have '.mjs', '.js' or '.cjs' extension");
}
const filePath = this.getFilePath(name, true);
try {
fs.writeFileSync(filePath, code, "utf8");
this.symlinkNodeModulesIfNecessary();
+3 -5
View File
@@ -5,16 +5,14 @@ import {createServer} from "node:http";
import {createServer as createSecureServer} from "node:https";
import type {Socket} from "node:net";
import {posix} from "node:path";
import {parse} from "node:url";
import bind from "bind-decorator";
import expressStaticGzip from "express-static-gzip";
import finalhandler from "finalhandler";
import stringify from "json-stable-stringify-without-jsonify";
import WebSocket from "ws";
import data from "../util/data";
import logger from "../util/logger";
import * as settings from "../util/settings";
import {stringify} from "../util/stringify";
import utils from "../util/utils";
import Extension from "./extension";
@@ -157,10 +155,10 @@ export class Frontend extends Extension {
@bind private onUpgrade(request: IncomingMessage, socket: Socket, head: Buffer): void {
this.wss.handleUpgrade(request, socket, head, (ws) => {
// biome-ignore lint/style/noNonNullAssertion: `Only valid for request obtained from Server`
const {query} = parse(request.url!, true);
const {searchParams} = new URL(request.url!, "http://localhost"); // dummy base, may not be absolute
const authToken = settings.get().frontend.auth_token;
if (!authToken || authToken === query.token) {
if (!authToken || authToken === searchParams.get("token")) {
this.wss.emit("connection", ws, request);
} else {
ws.close(4401, "Unauthorized");
+2 -2
View File
@@ -1,13 +1,13 @@
import assert from "node:assert";
import bind from "bind-decorator";
import equals from "fast-deep-equal/es6";
import stringify from "json-stable-stringify-without-jsonify";
import type * as zhc from "zigbee-herdsman-converters";
import Device from "../model/device";
import Group from "../model/group";
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from "../types/api";
import logger from "../util/logger";
import * as settings from "../util/settings";
import {stringify} from "../util/stringify";
import utils, {isLightExpose} from "../util/utils";
import Extension from "./extension";
@@ -168,7 +168,7 @@ export default class Groups extends Extension {
endpointNames &&
endpointNames.length >= member.ID &&
device.definition?.meta?.multiEndpoint &&
(!device.definition.meta.multiEndpointSkip || !device.definition.meta.multiEndpointSkip.includes("state"))
!device.definition.meta.multiEndpointSkip?.includes("state")
? `state_${endpointNames[member.ID - 1]}`
: "state";
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,10 +1,10 @@
import bind from "bind-decorator";
import stringify from "json-stable-stringify-without-jsonify";
import type {Eui64} from "zigbee-herdsman/dist/zspec/tstypes";
import type {LQITableEntry, RoutingTableEntry} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes";
import type {Zigbee2MQTTAPI, Zigbee2MQTTNetworkMap} from "../types/api";
import logger from "../util/logger";
import * as settings from "../util/settings";
import {stringify} from "../util/stringify";
import utils from "../util/utils";
import Extension from "./extension";
+36 -19
View File
@@ -2,7 +2,6 @@ import assert from "node:assert";
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 {setOtaConfiguration, Zcl} from "zigbee-herdsman";
import type {OtaDataSettings, OtaSource, OtaUpdateAvailableResult} from "zigbee-herdsman/dist/controller/tstype";
import Device from "../model/device";
@@ -10,6 +9,7 @@ import type {Zigbee2MQTTAPI} from "../types/api";
import dataDir from "../util/data";
import logger from "../util/logger";
import * as settings from "../util/settings";
import {stringify} from "../util/stringify";
import utils from "../util/utils";
import Extension from "./extension";
@@ -50,7 +50,7 @@ function writeFirmwareHexToDataDir(hex: string, fileName: string | undefined, de
export default class OTAUpdate extends Extension {
#topicRegex = new RegExp(
`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check|schedule|unschedule)/?(downgrade)?`,
`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check|schedule|unschedule)/?(downgrade|abort)?`,
"i",
);
#inProgress = new Set<string>();
@@ -248,6 +248,7 @@ export default class OTAUpdate extends Extension {
| Zigbee2MQTTAPI["bridge/request/device/ota_update/check/downgrade"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/update"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/update/downgrade"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/update/abort"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/schedule"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/schedule/downgrade"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/unschedule"];
@@ -258,18 +259,31 @@ export default class OTAUpdate extends Extension {
assert(message.id, "Invalid payload");
}
const ID = (messageObject ? message.id : message) as string;
const device = this.zigbee.resolveEntity(ID);
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";
const abort = topicMatch[2] === "abort";
let error: string | undefined;
let errorStack: string | undefined;
if (!(device instanceof Device)) {
error = `Device '${ID}' does not exist`;
error = `Device '${id}' does not exist`;
} 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 = `OTA update or check for update already in progress for '${device.name}'`;
if (abort) {
device.zh.abortOta();
this.#inProgress.delete(device.ieeeAddr);
// cleanup same as a fail
this.#removeProgressAndRemainingFromState(device);
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "available"));
await this.mqtt.publish(
"bridge/response/device/ota_update/update/abort",
stringify(utils.getResponse<"bridge/response/device/ota_update/update/abort">(message, {id})),
);
} else {
// also guards against scheduling while check/update op in progress that could result in undesired OTA state
error = `OTA update or check for update already in progress for '${device.name}'`;
}
} else {
switch (type) {
case "check": {
@@ -284,11 +298,11 @@ export default class OTAUpdate extends Extension {
if (payload.url) {
source.url = payload.url;
} else if (!device.definition || !device.definition.ota) {
} else if (!device.definition?.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
} else if (!device.definition || !device.definition.ota) {
} else if (!device.definition?.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
@@ -304,7 +318,7 @@ export default class OTAUpdate extends Extension {
this.#lastChecked.set(device.ieeeAddr, Date.now());
const response = utils.getResponse<"bridge/response/device/ota_update/check">(message, {
id: ID,
id,
update_available: availableResult.available,
downgrade: source.downgrade,
source: availableResult.availableMeta?.url,
@@ -321,6 +335,11 @@ export default class OTAUpdate extends Extension {
}
case "update": {
if (abort) {
error = `No OTA in progress to abort for device '${device.name}'`;
break;
}
this.#inProgress.add(device.ieeeAddr);
const otaSettings = settings.get().ota;
@@ -344,7 +363,7 @@ export default class OTAUpdate extends Extension {
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) {
} else if (!device.definition?.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
@@ -360,7 +379,7 @@ export default class OTAUpdate extends Extension {
if (payload.default_maximum_data_size) {
dataSettings.baseSize = payload.default_maximum_data_size;
}
} else if (!device.definition || !device.definition.ota) {
} else if (!device.definition?.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
@@ -378,7 +397,7 @@ export default class OTAUpdate extends Extension {
const firmwareTo = await this.#readSoftwareBuildIDAndDateCode(device);
const response = utils.getResponse<"bridge/response/device/ota_update/update">(message, {
id: ID,
id,
from: {
file_version: fromVersion,
software_build_id: firmwareFrom?.softwareBuildID,
@@ -415,11 +434,11 @@ export default class OTAUpdate extends Extension {
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) {
} else if (!device.definition?.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
} else if (!device.definition || !device.definition.ota) {
} else if (!device.definition?.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
@@ -427,7 +446,7 @@ export default class OTAUpdate extends Extension {
device.zh.scheduleOta(source);
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "scheduled"));
const response = utils.getResponse<"bridge/response/device/ota_update/schedule">(message, {id: ID, url: source.url});
const response = utils.getResponse<"bridge/response/device/ota_update/schedule">(message, {id, url: source.url});
await this.mqtt.publish("bridge/response/device/ota_update/schedule", stringify(response));
@@ -442,9 +461,7 @@ export default class OTAUpdate extends Extension {
device.zh.unscheduleOta();
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "idle"));
const response = utils.getResponse<"bridge/response/device/ota_update/unschedule">(message, {
id: ID,
});
const response = utils.getResponse<"bridge/response/device/ota_update/unschedule">(message, {id});
await this.mqtt.publish("bridge/response/device/ota_update/unschedule", stringify(response));
+1 -2
View File
@@ -1,11 +1,10 @@
import bind from "bind-decorator";
import stringify from "json-stable-stringify-without-jsonify";
import type * as zhc from "zigbee-herdsman-converters";
import Device from "../model/device";
import Group from "../model/group";
import logger from "../util/logger";
import * as settings from "../util/settings";
import {stringify} from "../util/stringify";
import utils from "../util/utils";
import Extension from "./extension";
+1 -4
View File
@@ -1,14 +1,11 @@
import assert from "node:assert";
import bind from "bind-decorator";
import debounce from "debounce";
import stringify from "json-stable-stringify-without-jsonify";
import throttle from "throttleit";
import * as zhc from "zigbee-herdsman-converters";
import logger from "../util/logger";
import * as settings from "../util/settings";
import {stringify} from "../util/stringify";
import utils from "../util/utils";
import Extension from "./extension";
+4 -1
View File
@@ -18,6 +18,7 @@ export default class Device {
public zh: zh.Device;
public definition?: zhc.Definition;
private _definitionModelID?: string;
#isResolvingDefinition?: boolean;
get ieeeAddr(): string {
return this.zh.ieeeAddr;
@@ -64,9 +65,11 @@ export default class Device {
}
async resolveDefinition(ignoreCache = false): Promise<void> {
if (this.interviewed && (!this.definition || this._definitionModelID !== this.zh.modelID || ignoreCache)) {
if (this.interviewed && !this.#isResolvingDefinition && (!this.definition || this._definitionModelID !== this.zh.modelID || ignoreCache)) {
this.#isResolvingDefinition = true;
this.definition = await zhc.findByDevice(this.zh, true);
this._definitionModelID = this.zh.modelID;
this.#isResolvingDefinition = false;
}
}
+5
View File
@@ -109,6 +109,11 @@ export default class Mqtt {
options.rejectUnauthorized = false;
}
if (mqttSettings.server_name) {
logger.debug(`MQTT SSL/TLS: SNI server name = ${mqttSettings.server_name}`);
options.servername = mqttSettings.server_name;
}
this.client = await connectAsync(mqttSettings.server, options);
// https://github.com/Koenkk/zigbee2mqtt/issues/9822
+18 -2
View File
@@ -1,7 +1,7 @@
import type * as zigbeeHerdsman from "zigbee-herdsman/dist";
import type {ZclPayload} from "zigbee-herdsman/dist/adapter/events";
import type {Eui64} from "zigbee-herdsman/dist/zspec/tstypes";
import type {ClusterDefinition, ClusterName, CustomClusters} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
import type {Cluster, ClusterName, CustomClusters} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
import type {GenericZdoResponse, RoutingTableEntry} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes";
import type * as zigbeeHerdsmanConverter from "zigbee-herdsman-converters";
import type {Base} from "zigbee-herdsman-converters/lib/exposes";
@@ -133,6 +133,7 @@ export interface Zigbee2MQTTSettings {
cert?: string;
client_id?: string;
reject_unauthorized?: boolean;
server_name?: string;
maximum_packet_size: number;
};
serial: {
@@ -215,6 +216,8 @@ export interface Zigbee2MQTTSettings {
timestamp_format: string;
output: "json" | "attribute" | "attribute_and_json";
transmit_power?: number;
/** 3.0: default to false (JSON schema & settings `defaults`) */
enable_external_js: boolean;
};
health: {
/** in minutes */
@@ -259,6 +262,7 @@ export interface Zigbee2MQTTDeviceDefinition {
exposes: zigbeeHerdsmanConverter.Expose[];
supports_ota: boolean;
options: zigbeeHerdsmanConverter.Option[];
version: `0.0.${number}`;
icon: string;
}
@@ -343,7 +347,7 @@ export interface Zigbee2MQTTAPI {
};
"bridge/definitions": {
clusters: Readonly<Record<ClusterName, Readonly<ClusterDefinition>>>;
clusters: Readonly<Record<ClusterName, Cluster>>;
custom_clusters: Record<string, CustomClusters>;
actions: string[];
};
@@ -618,12 +622,14 @@ export interface Zigbee2MQTTAPI {
id: string;
block?: boolean;
force?: boolean;
clear_cache?: boolean;
};
"bridge/response/device/remove": {
id: string;
block: boolean;
force: boolean;
clear_cache: boolean;
};
"bridge/request/device/ota_update/check": {
@@ -674,6 +680,14 @@ export interface Zigbee2MQTTAPI {
default_maximum_data_size?: number | null;
};
"bridge/request/device/ota_update/update/abort": {
id: string;
};
"bridge/response/device/ota_update/update/abort": {
id: string;
};
"bridge/response/device/ota_update/update": {
id: string;
from:
@@ -998,6 +1012,7 @@ export type Zigbee2MQTTRequestEndpoints =
| "bridge/request/device/ota_update/check/downgrade"
| "bridge/request/device/ota_update/update"
| "bridge/request/device/ota_update/update/downgrade"
| "bridge/request/device/ota_update/update/abort"
| "bridge/request/device/ota_update/schedule"
| "bridge/request/device/ota_update/schedule/downgrade"
| "bridge/request/device/ota_update/unschedule"
@@ -1047,6 +1062,7 @@ export type Zigbee2MQTTResponseEndpoints =
| "bridge/response/device/remove"
| "bridge/response/device/ota_update/check"
| "bridge/response/device/ota_update/update"
| "bridge/response/device/ota_update/update/abort"
| "bridge/response/device/ota_update/schedule"
| "bridge/response/device/ota_update/unschedule"
| "bridge/response/device/interview"
-3
View File
@@ -1,8 +1,5 @@
// minimal required because of sub-deps in mqtt >= 5.14.0 to avoid requiring `dom` type
declare global {
// map to node, doesn't really matter, just needs to be there, and the right "type vs value" to avoid lib check problems
/** @deprecated DOM SHIM, DO NOT USE */
type MessagePort = import("node:worker_threads").MessagePort;
/** @deprecated DOM SHIM, DO NOT USE */
type Worker = import("node:worker_threads").Worker;
/** @deprecated DOM SHIM, DO NOT USE */
-3
View File
@@ -1,3 +0,0 @@
declare module "json-stable-stringify-without-jsonify" {
export default function (obj: unknown): string;
}
+1 -1
View File
@@ -6,7 +6,7 @@ declare module "zigbee2mqtt-frontend" {
export default frontend;
}
declare module "http" {
declare module "node:http" {
interface IncomingMessage {
originalUrl?: string;
path?: string;
+1 -2
View File
@@ -2,7 +2,6 @@ import assert from "node:assert";
import fs from "node:fs";
import path from "node:path";
import {rimrafSync} from "rimraf";
import winston from "winston";
import * as settings from "./settings";
@@ -235,7 +234,7 @@ class Logger {
for (const dir of directories) {
this.debug(`Removing old log directory '${dir.path}'`);
try {
rimrafSync(dir.path);
fs.rmSync(dir.path, {recursive: true, force: true});
} catch (e) {
this.error(`Failed to remove old log directory '${dir.path}': ${e}`);
}
+11 -9
View File
@@ -3,11 +3,11 @@ import type {ServerResponse} from "node:http";
import {createServer} from "node:http";
import path from "node:path";
import expressStaticGzip from "express-static-gzip";
import {type Unzipped, unzip} from "fflate";
import finalhandler from "finalhandler";
import stringify from "json-stable-stringify-without-jsonify";
import JSZip from "jszip";
import {findAllDevices} from "zigbee-herdsman/dist/adapter/adapterDiscovery";
import type {OnboardData, OnboardFailureData, OnboardSubmitResponse, Zigbee2MQTTSettings} from "../types/api";
import {stringify} from "../util/stringify";
import data from "./data";
import * as settings from "./settings";
import {YAMLFileException} from "./yaml";
@@ -49,20 +49,22 @@ function getZipEntryTargetPath(entryName: string): string {
}
async function extractZipDataToDataPath(zipContent: Buffer): Promise<void> {
const zip = await JSZip.loadAsync(zipContent);
const entries = await new Promise<Unzipped>((resolve, reject) => {
unzip(zipContent, (error, data) => (error ? reject(error) : resolve(data)));
});
for (const key in zip.files) {
const entry = zip.files[key];
const targetPath = getZipEntryTargetPath(entry.name);
for (const name in entries) {
const targetPath = getZipEntryTargetPath(name);
if (entry.dir) {
// directory entries are identified by a trailing slash
if (name.endsWith("/")) {
mkdirSync(targetPath, {recursive: true});
continue;
}
mkdirSync(path.dirname(targetPath), {recursive: true});
writeFileSync(targetPath, await entry.async("nodebuffer"));
writeFileSync(targetPath, entries[name]);
}
}
@@ -109,7 +111,7 @@ async function startOnboardingServer(): Promise<boolean> {
const redirect =
!process.env.Z2M_ONBOARD_NO_REDIRECT &&
appliedSettings.frontend.enabled &&
(!appliedSettings.frontend.host || !appliedSettings.frontend.host.startsWith("/"));
!appliedSettings.frontend.host?.startsWith("/");
const protocol = appliedSettings.frontend.ssl_cert && appliedSettings.frontend.ssl_key ? "https" : "http";
const frontendUrl = redirect
? `${protocol}://${appliedSettings.frontend.host ?? "localhost"}:${appliedSettings.frontend.port}${appliedSettings.frontend.base_url}`
+31 -8
View File
@@ -9,7 +9,7 @@
"enabled": {
"type": "boolean",
"title": "Enabled",
"description": "Enable Home Assistant integration",
"description": "Enable Home Assistant integration. Also check 'cache_state' and 'output' options under 'advanced'.",
"default": false,
"requiresRestart": true
},
@@ -33,12 +33,14 @@
"type": "boolean",
"title": "Home Assistant legacy action sensors",
"description": "Home Assistant legacy actions sensor, when enabled a action sensor will be discoverd and an empty `action` will be send after every published action.",
"requiresRestart": true,
"default": false
},
"experimental_event_entities": {
"type": "boolean",
"title": "Home Assistant experimental event entities",
"description": "Home Assistant experimental event entities, when enabled Zigbee2MQTT will add event entities for exposed actions. The events and attributes are currently deemed experimental and subject to change.",
"requiresRestart": true,
"default": false
}
},
@@ -88,7 +90,7 @@
"title": "Pause on backoff greater than",
"default": 0,
"minimum": 0,
"description": "Pause availability pings when backoff reaches over this limit until a new Zigbee message is received from the device. A value of zero disables pausing."
"description": "Pause availability pings when the backoff multiplier reaches over this limit until a new Zigbee message is received from the device. A value of zero disables pausing."
}
},
"required": ["timeout"]
@@ -188,6 +190,13 @@
"description": "Disable self-signed SSL certificate",
"default": true
},
"server_name": {
"type": "string",
"title": "TLS server name (SNI)",
"requiresRestart": true,
"description": "Override the TLS SNI / hostname used for certificate verification when it differs from the host in 'server' (e.g. connecting to an internal service DNS name while validating a public certificate SAN). Leave unset to use the hostname from 'server'.",
"examples": ["mqtt.example.com"]
},
"include_device_information": {
"type": "boolean",
"title": "Include device information",
@@ -739,7 +748,7 @@
"cache_state": {
"type": "boolean",
"title": "Cache state",
"description": "MQTT message payload will contain all attributes, not only changed ones. Has to be true when integrating via Home Assistant",
"description": "MQTT message payload will contain all attributes, not only changed ones. Must be true when integrating via Home Assistant",
"default": true
},
"cache_state_persistent": {
@@ -800,14 +809,21 @@
"requiresRestart": true,
"minimum": -128,
"maximum": 127,
"description": "Transmit power of adapter, only available for Z-Stack (CC253*/CC2652/CC1352) adapters, CC2652 = 5dbm, CC1352 max is = 20dbm (5dbm default)"
"description": "Transmit power of adapter, in dBm (max is often 20, refer to chip specifications)"
},
"output": {
"type": "string",
"enum": ["attribute_and_json", "attribute", "json"],
"title": "MQTT output type",
"description": "Examples when 'state' of a device is published json: topic: 'zigbee2mqtt/my_bulb' payload '{\"state\": \"ON\"}' attribute: topic 'zigbee2mqtt/my_bulb/state' payload 'ON' attribute_and_json: both json and attribute (see above)",
"description": "How the 'state' of a device is published. json: topic 'zigbee2mqtt/my_bulb' payload '{\"state\": \"ON\"}'. attribute: topic 'zigbee2mqtt/my_bulb/state' payload 'ON'. attribute_and_json: both json and attribute (see above). Home Assistant requires json",
"default": "json"
},
"enable_external_js": {
"type": "boolean",
"title": "Enable external JS",
"description": "Enable external JavaScript (extensions and converters) that can execute arbitrary user-provided code. WARNING: If unused, it is advised to disable this.",
"default": true,
"requiresRestart": true
}
}
},
@@ -848,7 +864,8 @@
"retain": {
"type": "boolean",
"title": "Retain",
"description": "Retain MQTT messages of this device"
"description": "Retain MQTT messages of this device",
"default": false
},
"disabled": {
"type": "boolean",
@@ -953,10 +970,16 @@
"type": "string"
},
"retain": {
"type": "boolean"
"type": "boolean",
"title": "Retain",
"description": "Retain MQTT messages of this group",
"default": false
},
"optimistic": {
"type": "boolean"
"type": "boolean",
"title": "Optimistic",
"description": "Publish the expected state of group members after set",
"default": true
},
"qos": {
"type": ["number"],
+2
View File
@@ -113,6 +113,7 @@ export const defaults = {
network_key: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13],
timestamp_format: "YYYY-MM-DD HH:mm:ss",
output: "json",
enable_external_js: true,
},
health: {
interval: 10,
@@ -167,6 +168,7 @@ export function writeMinimalDefaults(): void {
network_key: "GENERATE",
pan_id: "GENERATE",
ext_pan_id: "GENERATE",
enable_external_js: false,
},
frontend: {
enabled: defaults.frontend.enabled,
+153
View File
@@ -0,0 +1,153 @@
// Stable stringify inspired by https://github.com/BridgeAR/safe-stable-stringify
// Takes advantage of Node env and Z2M's object-only use-case.
// biome-ignore lint/suspicious/noControlCharactersInRegex: escape regex
const STR_ESC_SEQ_REGEXP = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/;
// Escape C0 control characters, double quotes, the backslash and every code
// unit with a numeric value in the inclusive range 0xD800 to 0xDFFF.
function strEscape(str: string): string {
// Some magic numbers that worked out fine while benchmarking with v8 8.0
if (str.length < 5000 && !STR_ESC_SEQ_REGEXP.test(str)) {
return `"${str}"`;
}
return JSON.stringify(str);
}
function sort(array: string[]) {
// Insertion sort is very efficient for small input sizes, but it has a bad
// worst case complexity. Thus, use native array sort for bigger values.
if (array.length > 2e2) {
return array.sort();
}
for (let i = 1; i < array.length; i++) {
const currentValue = array[i];
let position = i;
while (position !== 0 && array[position - 1] > currentValue) {
array[position] = array[position - 1];
position--;
}
array[position] = currentValue;
}
}
function isTypedArray(value: unknown): value is unknown[] {
return ArrayBuffer.isView(value) && !(value instanceof DataView);
}
function stringifyTypedArray(array: unknown[]): string {
const isBigInt = typeof array[0] === "bigint";
let res = `"0":${isBigInt ? `"${array[0]}"` : array[0]}`;
for (let i = 1; i < array.length; i++) {
res += `,"${i}":${isBigInt ? `"${array[1]}"` : array[1]}`;
}
return res;
}
function stringifySimple(key: string, value: unknown, stack: unknown[]): string | undefined {
switch (typeof value) {
case "string":
return strEscape(value);
case "object": {
if (value === null) {
return "null";
}
if ("toJSON" in value && typeof value.toJSON === "function") {
value = value.toJSON(key);
// Prevent calling `toJSON` again
if (typeof value !== "object") {
return stringifySimple(key, value, stack);
}
if (value === null) {
return "null";
}
}
if (stack.indexOf(value) !== -1) {
return '"[Circular]"';
}
let res = "";
if (Array.isArray(value)) {
if (value.length === 0) {
return "[]";
}
stack.push(value);
let i = 0;
for (; i < value.length - 1; i++) {
const tmp = stringifySimple(`${i}`, value[i], stack);
res += tmp !== undefined ? tmp : "null";
res += ",";
}
const tmp = stringifySimple(`${i}`, value[i], stack);
res += tmp !== undefined ? tmp : "null";
stack.pop();
return `[${res}]`;
}
let keys = Object.keys(value);
const keysLength = keys.length;
if (keysLength === 0) {
return "{}";
}
let separator = "";
let propsToStringify = keysLength;
if (isTypedArray(value)) {
res += stringifyTypedArray(value);
keys = keys.slice(value.length);
propsToStringify -= value.length;
separator = ",";
}
sort(keys);
stack.push(value);
for (let i = 0; i < propsToStringify; i++) {
const valKey = keys[i];
const tmp = stringifySimple(valKey, (value as Record<string, unknown>)[valKey], stack);
if (tmp !== undefined) {
res += `${separator}${strEscape(valKey)}:${tmp}`;
separator = ",";
}
}
stack.pop();
return `{${res}}`;
}
case "number":
return Number.isFinite(value) ? `${value}` : "null";
case "boolean":
return value === true ? "true" : "false";
case "undefined":
return undefined;
case "bigint":
return `"${value}"`;
default:
return undefined;
}
}
export function stringify(value: object): string {
return stringifySimple("", value, []) ?? "null";
}
+3 -3
View File
@@ -2,7 +2,7 @@ import assert from "node:assert";
import fs from "node:fs";
import equals from "fast-deep-equal/es6";
import yaml, {YAMLException} from "js-yaml";
import {dump, load, YAMLException} from "js-yaml";
export class YAMLFileException extends YAMLException {
file: string;
@@ -20,7 +20,7 @@ export class YAMLFileException extends YAMLException {
function read(file: string): KeyValue {
try {
const result = yaml.load(fs.readFileSync(file, "utf8"));
const result = load(fs.readFileSync(file, "utf8"));
assert(result instanceof Object, `The content of ${file} is expected to be an object`);
return result as KeyValue;
} catch (error) {
@@ -40,7 +40,7 @@ function writeIfChanged(file: string, content: KeyValue): void {
const before = readIfExists(file);
if (!equals(before, content)) {
fs.writeFileSync(file, yaml.dump(content));
fs.writeFileSync(file, dump(content));
}
}
+8 -4
View File
@@ -1,6 +1,5 @@
import {randomInt} from "node:crypto";
import bind from "bind-decorator";
import stringify from "json-stable-stringify-without-jsonify";
import type {Events as ZHEvents} from "zigbee-herdsman";
import {Controller} from "zigbee-herdsman";
import type {StartResult} from "zigbee-herdsman/dist/adapter/tstype";
@@ -9,6 +8,7 @@ import Group from "./model/group";
import data from "./util/data";
import logger from "./util/logger";
import * as settings from "./util/settings";
import {stringify} from "./util/stringify";
import utils from "./util/utils";
const entityIDRegex = /^(.+?)(?:\/([^/]+))?$/;
@@ -61,7 +61,7 @@ export default class Zigbee {
logger.debug(
() =>
`Using zigbee-herdsman with settings: '${stringify(JSON.stringify(herdsmanSettings).replaceAll(JSON.stringify(herdsmanSettings.network.networkKey), '"HIDDEN"'))}'`,
`Using zigbee-herdsman with settings: '${stringify(herdsmanSettings).replaceAll(stringify(herdsmanSettings.network.networkKey), '"HIDDEN"')}'`,
);
let startResult: StartResult;
@@ -467,7 +467,11 @@ export default class Zigbee {
return this.resolveGroup(id);
}
removeGroupFromLookup(id: number): void {
this.groupLookup.delete(id);
removeDeviceFromLookup(ieee: string): boolean {
return this.deviceLookup.delete(ieee);
}
removeGroupFromLookup(id: number): boolean {
return this.groupLookup.delete(id);
}
}
+20 -24
View File
@@ -1,6 +1,6 @@
{
"name": "zigbee2mqtt",
"version": "2.9.0",
"version": "2.13.0",
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
"main": "index.js",
"types": "dist/types/api.d.ts",
@@ -10,7 +10,7 @@
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
},
"engines": {
"node": "^20.15.0 || ^22.2.0 || ^24"
"node": "^22.2.0 || ^24 || ^26"
},
"keywords": [
"xiaomi",
@@ -33,7 +33,7 @@
"test:watch": "vitest watch --config ./test/vitest.config.mts",
"bench": "vitest bench --run --config ./test/vitest.config.mts",
"prepack": "pnpm run clean && pnpm run build",
"clean": "rimraf coverage dist tsconfig.tsbuildinfo"
"clean": "node -e \"for (const p of ['coverage', 'dist', 'tsconfig.tsbuildinfo']) require('node:fs').rmSync(p, {recursive: true, force: true, maxRetries: process.platform === 'win32' ? 10 : 0})\""
},
"author": "Koen Kanters",
"license": "GPL-3.0",
@@ -42,44 +42,40 @@
},
"homepage": "https://koenkk.github.io/zigbee2mqtt",
"dependencies": {
"ajv": "^8.18.0",
"ajv": "^8.20.0",
"bind-decorator": "^1.0.11",
"debounce": "^3.0.0",
"express-static-gzip": "^3.0.0",
"express-static-gzip": "^3.0.1",
"fast-deep-equal": "^3.1.3",
"fflate": "^0.8.3",
"finalhandler": "^2.1.1",
"humanize-duration": "^3.33.2",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"jszip": "^3.10.1",
"mqtt": "^5.15.0",
"humanize-duration": "^3.34.0",
"js-yaml": "^5.2.2",
"mqtt": "^5.15.2",
"object-assign-deep": "^0.4.0",
"rimraf": "^6.1.3",
"semver": "^7.7.4",
"source-map-support": "^0.5.21",
"throttleit": "^2.1.0",
"semver": "^7.8.5",
"throttleit": "^3.0.0",
"winston": "^3.19.0",
"winston-syslog": "^2.7.1",
"winston-transport": "^4.9.0",
"ws": "^8.19.0",
"zigbee-herdsman": "9.0.9",
"zigbee-herdsman-converters": "26.12.0",
"ws": "^8.21.1",
"zigbee-herdsman": "10.8.0",
"zigbee-herdsman-converters": "26.90.0",
"zigbee2mqtt-frontend": "0.9.21",
"zigbee2mqtt-windfront": "2.9.2"
"zigbee2mqtt-windfront": "2.14.0"
},
"devDependencies": {
"@biomejs/biome": "^2.4.4",
"@biomejs/biome": "^2.5.3",
"@types/finalhandler": "^1.2.3",
"@types/humanize-duration": "^3.27.4",
"@types/js-yaml": "^4.0.9",
"@types/node": "^24.11.0",
"@types/node": "^26.1.2",
"@types/object-assign-deep": "^0.4.3",
"@types/readable-stream": "4.0.23",
"@types/readable-stream": "4.0.24",
"@types/serve-static": "^2.2.0",
"@types/ws": "8.18.1",
"@vitest/coverage-v8": "^3.1.1",
"tmp": "^0.2.5",
"typescript": "^5.9.3",
"tmp": "^0.2.7",
"typescript": "^7.0.2",
"vitest": "^3.1.1"
},
"pnpm": {
+435 -424
View File
File diff suppressed because it is too large Load Diff
+13 -23
View File
@@ -1,4 +1,12 @@
import {execSync} from "node:child_process";
import {execFileSync} from "node:child_process";
function grepTypeScriptFiles(needle, dir) {
try {
return execFileSync("grep", ["-r", "-F", "--include=*.ts", "--", needle, dir], {encoding: "utf8"});
} catch {
return undefined;
}
}
async function checkDuplicateIssue(github, context, name) {
// Search for existing issues with the same `name`
@@ -63,13 +71,7 @@ export async function newDeviceSupport(github, _core, context, zhcDir) {
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"});
} catch {
return undefined;
}
})();
const fullMatch = grepTypeScriptFiles(fullName, zhcDir);
console.log(`Checking full match for '${fullName}', result: '${fullMatch}'`);
if (fullMatch) {
@@ -94,13 +96,7 @@ If you need help with the process, feel free to ask here and we'll be happy to a
return;
}
const partialMatch = (() => {
try {
return execSync(`grep -r --include="*.ts" "${partialName}" "${zhcDir}"`, {encoding: "utf8"});
} catch {
return undefined;
}
})();
const partialMatch = grepTypeScriptFiles(partialName, zhcDir);
console.log(`Checking partial match for '${partialName}', result: '${partialMatch}'`);
if (partialMatch) {
@@ -129,14 +125,8 @@ 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"});
} catch {
return undefined;
}
})();
if (await checkDuplicateIssue(github, context, zigbeeModel)) return;
const fullMatch = grepTypeScriptFiles(`"${zigbeeModel}"`, zhcDir);
if (fullMatch) {
await github.rest.issues.createComment({
+2 -2
View File
@@ -18,5 +18,5 @@ const sandbox = {
};
vm.runInNewContext(moduleCode, sandbox, moduleFakePath);
const converter = sandbox.module.exports;
assert(!converter.toZigbee || !converter.toZigbee.includes(undefined));
assert(!converter.fromZigbee || !converter.fromZigbee.includes(undefined));
assert(!converter.toZigbee?.includes(undefined));
assert(!converter.fromZigbee?.includes(undefined));
+2 -3
View File
@@ -1,5 +1,4 @@
import {existsSync, mkdirSync} from "node:fs";
import stringify from "json-stable-stringify-without-jsonify";
import {bench, describe, vi} from "vitest";
import {type Controller, Zcl, Zdo, ZSpec} from "zigbee-herdsman";
import type Adapter from "zigbee-herdsman/dist/adapter/adapter";
@@ -12,6 +11,7 @@ import type {DeviceType} from "zigbee-herdsman/dist/controller/tstype";
import {Foundation} from "zigbee-herdsman/dist/zspec/zcl/definition/foundation";
import type {RequestToResponseMap} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes";
import data from "../lib/util/data";
import {stringify} from "../lib/util/stringify";
import {BENCH_OPTIONS} from "./benchOptions";
vi.doMock("zigbee-herdsman", async (importOriginal) => {
@@ -257,7 +257,7 @@ const adapter = {
switch (zclFrame.command.ID) {
case Foundation.read.ID: {
for (const attr of zclFrame.payload) {
const attribute = zclFrame.cluster.getAttribute(attr.attrId);
const attribute = Zcl.Utils.getClusterAttribute(zclFrame.cluster, attr.attrId, undefined);
if (attribute && attribute.type !== Zcl.DataType.NO_DATA && attribute.type < Zcl.DataType.OCTET_STR) {
payload.push({
@@ -421,7 +421,6 @@ const initController = async () => {
disconnecting: false,
disconnected: false,
endAsync: async () => {},
// @ts-expect-error Z2M does not make use of return
publishAsync: async () => {},
};
controller.mqtt.connect = async () => {
+4 -2
View File
@@ -17,9 +17,9 @@ import {devices, mockController as mockZHController, events as mockZHEvents, ret
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import stringify from "json-stable-stringify-without-jsonify";
import tmp from "tmp";
import {stringify} from "../lib/util/stringify";
import type {Mock, MockInstance} from "vitest";
import {Controller as ZHController} from "zigbee-herdsman";
import {Controller} from "../lib/controller";
@@ -112,7 +112,7 @@ describe("Controller", () => {
expect(mockLogger.info).toHaveBeenCalledWith(
"bulb (0x000b57fffec6a5b2): LED1545G12 - IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (Router)",
);
expect(mockLogger.info).toHaveBeenCalledWith("remote (0x0017880104e45517): 324131092621 - Philips Hue dimmer switch (EndDevice)");
expect(mockLogger.info).toHaveBeenCalledWith("remote (0x0017880104e45517): 324131092621 - Philips Hue dimmer switch gen 1 (EndDevice)");
expect(mockLogger.info).toHaveBeenCalledWith("0x0017880104e45518 (0x0017880104e45518): Not supported (EndDevice)");
expect(mockMQTTConnectAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTConnectAsync).toHaveBeenCalledWith("mqtt://localhost", {
@@ -149,6 +149,7 @@ describe("Controller", () => {
user: "user1",
client_id: "my_client_id",
reject_unauthorized: false,
server_name: "mqtt.example.com",
version: 5,
maximum_packet_size: 20000,
};
@@ -166,6 +167,7 @@ describe("Controller", () => {
username: "user1",
clientId: "my_client_id",
rejectUnauthorized: false,
servername: "mqtt.example.com",
protocolVersion: 5,
properties: {maximumPacketSize: 20000},
};
+15 -6
View File
@@ -15,12 +15,21 @@ describe("Data", () => {
const expected = tmp.dirSync().name;
process.env.ZIGBEE2MQTT_DATA = expected;
data._testReload();
const actual = data.getPath();
expect(actual).toBe(expected);
expect(data.joinPath("test")).toStrictEqual(path.join(expected, "test"));
expect(data.joinPath("/test")).toStrictEqual(path.resolve(expected, "/test"));
delete process.env.ZIGBEE2MQTT_DATA;
data._testReload();
try {
const actual = data.getPath();
expect(actual).toBe(expected);
expect(data.joinPath("test")).toStrictEqual(path.join(expected, "test"));
expect(data.joinPath("/test")).toStrictEqual(path.resolve(expected, "/test"));
} finally {
delete process.env.ZIGBEE2MQTT_DATA;
data._testReload();
}
});
it("Should return data path when joinPath with empty string", () => {
const expected = data.getPath();
expect(data.joinPath("")).toStrictEqual(expected);
});
});
});
+1 -1
View File
@@ -7,7 +7,7 @@ import {flushPromises} from "../mocks/utils";
import {devices, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHerdsman";
import assert from "node:assert";
import stringify from "json-stable-stringify-without-jsonify";
import {stringify} from "../../lib/util/stringify";
import {Controller} from "../../lib/controller";
import Availability from "../../lib/extension/availability";
import * as settings from "../../lib/util/settings";
+1 -1
View File
@@ -7,7 +7,7 @@ import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
import {flushPromises} from "../mocks/utils";
import {type Device, devices, groups, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
import stringify from "json-stable-stringify-without-jsonify";
import {stringify} from "../../lib/util/stringify";
import {Controller} from "../../lib/controller";
import Bind from "../../lib/extension/bind";
import * as settings from "../../lib/util/settings";
+196 -27
View File
@@ -2,7 +2,7 @@
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import {Zdo} from "zigbee-herdsman";
import * as data from "../mocks/data";
import {mockJSZipFile, mockJSZipGenerateAsync} from "../mocks/jszip";
import {mockFflateZip, mockFflateZipFailOnce} from "../mocks/fflate";
import {mockLogger} from "../mocks/logger";
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
import {flushPromises} from "../mocks/utils";
@@ -12,7 +12,7 @@ import assert from "node:assert";
import fs from "node:fs";
import {platform} from "node:os";
import path from "node:path";
import stringify from "json-stable-stringify-without-jsonify";
import {stringify} from "../../lib/util/stringify";
import type {Mock} from "vitest";
import {Controller} from "../../lib/controller";
import Bridge from "../../lib/extension/bridge";
@@ -144,6 +144,7 @@ describe("Extension: Bridge", () => {
output: "json",
pan_id: 6754,
timestamp_format: "YYYY-MM-DD HH:mm:ss",
enable_external_js: true,
},
blocklist: [],
device_options: {},
@@ -597,6 +598,7 @@ describe("Extension: Bridge", () => {
],
supports_ota: true,
vendor: "IKEA",
version: "0.0.0",
},
description: "this is my bulb",
disabled: false,
@@ -734,7 +736,7 @@ describe("Extension: Bridge", () => {
values: ["off", "on", "toggle", "previous"],
},
{
access: 2,
access: 3,
label: "Effect",
name: "effect",
property: "effect",
@@ -747,11 +749,41 @@ describe("Extension: Bridge", () => {
"candle",
"fireplace",
"colorloop",
"sunset",
"sunrise",
"sparkle",
"opal",
"glisten",
"underwater",
"cosmos",
"sunbeam",
"enchant",
"none",
"finish_effect",
"stop_effect",
"stop_hue_effect",
],
},
{
access: 3,
description: "Animation speed for the active effect (0=slowest, 1=fastest)",
label: "Effect speed",
name: "effect_speed",
property: "effect_speed",
type: "numeric",
value_max: 1,
value_min: 0,
value_step: 0.01,
},
{
access: 2,
description:
'Set the base color of the active effect without stopping it (hex e.g. #FF4400, or JSON {"x":0.6,"y":0.3})',
label: "Effect color",
name: "effect_color",
property: "effect_color",
type: "text",
},
{
access: 1,
category: "diagnostic",
@@ -767,6 +799,27 @@ describe("Extension: Bridge", () => {
],
model: "7146060PH",
options: [
{
access: 2,
description:
"Control this light using a Philips-specific protocol instead of standard Zigbee commands. When enabled, on/off, brightness, color, and color temperature are combined into single atomic commands. This is required to use the Effect color update mode. When disabled (default), standard Zigbee commands are used, which preserves the usual behavior, including simulating on/off transitions.",
label: "Hue native control",
name: "hue_native_control",
property: "hue_native_control",
type: "binary",
value_off: false,
value_on: true,
},
{
access: 2,
description:
"Controls what happens when color is changed while an effect is active (requires Hue native control). 'stop' (default): color change stops the effect (Hue app behavior). 'update': color change re-sends the effect with the new color.",
label: "Effect color mode",
name: "effect_color_mode",
property: "effect_color_mode",
type: "enum",
values: ["stop", "update"],
},
{
access: 2,
description:
@@ -778,6 +831,16 @@ describe("Extension: Bridge", () => {
value_min: 0,
value_step: 0.1,
},
{
access: 2,
description: "State actions will also be published as 'action' when true (default false).",
label: "State action",
name: "state_action",
property: "state_action",
type: "binary",
value_off: false,
value_on: true,
},
{
access: 2,
description:
@@ -789,19 +852,10 @@ describe("Extension: Bridge", () => {
value_off: false,
value_on: true,
},
{
access: 2,
description: "State actions will also be published as 'action' when true (default false).",
label: "State action",
name: "state_action",
property: "state_action",
type: "binary",
value_off: false,
value_on: true,
},
],
supports_ota: true,
vendor: "Philips",
version: "0.0.0",
},
disabled: false,
endpoints: {
@@ -831,7 +885,7 @@ describe("Extension: Bridge", () => {
{
definition: {
source: "native",
description: "Hue dimmer switch",
description: "Hue dimmer switch gen 1",
exposes: [
{
access: 1,
@@ -930,6 +984,7 @@ describe("Extension: Bridge", () => {
],
supports_ota: true,
vendor: "Philips",
version: "0.0.0",
},
disabled: false,
endpoints: {
@@ -1038,6 +1093,7 @@ describe("Extension: Bridge", () => {
],
supports_ota: false,
vendor: "notSupportedMfg",
version: "0.0.0",
},
disabled: false,
endpoints: {
@@ -1143,6 +1199,7 @@ describe("Extension: Bridge", () => {
],
supports_ota: false,
vendor: "Aqara",
version: "0.0.0",
},
disabled: false,
endpoints: {
@@ -1304,6 +1361,7 @@ describe("Extension: Bridge", () => {
],
supports_ota: false,
vendor: "Aqara",
version: "0.0.0",
},
disabled: false,
endpoints: {"1": {bindings: [], clusters: {input: ["genBasic"], output: []}, configured_reportings: [], scenes: []}},
@@ -1455,6 +1513,7 @@ describe("Extension: Bridge", () => {
],
supports_ota: true,
vendor: "Xiaomi",
version: "0.0.0",
},
disabled: false,
endpoints: {"1": {bindings: [], clusters: {input: ["genBasic", "genOnOff"], output: []}, configured_reportings: [], scenes: []}},
@@ -1914,6 +1973,7 @@ describe("Extension: Bridge", () => {
],
supports_ota: false,
vendor: "Siglis",
version: "0.0.0",
},
disabled: false,
endpoints: {
@@ -2204,6 +2264,7 @@ describe("Extension: Bridge", () => {
],
supports_ota: true,
vendor: "IKEA",
version: "0.0.0",
},
disabled: false,
endpoints: {
@@ -2639,6 +2700,7 @@ describe("Extension: Bridge", () => {
],
supports_ota: true,
vendor: "IKEA",
version: "0.0.0",
},
friendly_name: "bulb",
ieee_address: "0x000b57fffec6a5b2",
@@ -2725,6 +2787,7 @@ describe("Extension: Bridge", () => {
],
supports_ota: false,
vendor: "notSupportedMfg",
version: "0.0.0",
},
friendly_name: "0x0017880104e45518",
ieee_address: "0x0017880104e45518",
@@ -2743,7 +2806,7 @@ describe("Extension: Bridge", () => {
mockMQTTPublishAsync.mockClear();
await mockZHEvents.deviceLeave({ieeeAddr: devices.bulb.ieeeAddr});
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(4);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/event",
stringify({type: "device_leave", data: {ieee_address: "0x000b57fffec6a5b2", friendly_name: "bulb"}}),
@@ -2756,6 +2819,7 @@ describe("Extension: Bridge", () => {
expect.any(String),
{retain: true},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), {retain: true});
});
it("Should allow permit join on all", async () => {
@@ -2910,6 +2974,7 @@ describe("Extension: Bridge", () => {
it("Should allow to remove device by string", async () => {
const device = devices.bulb;
const removeSpy = vi.spyOn(controller.zigbee, "removeDeviceFromLookup");
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/remove", "bulb");
await flushPromises();
@@ -2918,11 +2983,12 @@ describe("Extension: Bridge", () => {
expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
expect(device.removeFromDatabase).not.toHaveBeenCalled();
expect(settings.getDevice("bulb")).toBeUndefined();
expect(removeSpy).not.toHaveBeenCalled();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", "", {retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {id: "bulb", block: false, force: false}, status: "ok"}),
stringify({data: {id: "bulb", block: false, force: false, clear_cache: false}, status: "ok"}),
{},
);
expect(settings.get().blocklist).toStrictEqual([]);
@@ -2932,52 +2998,76 @@ describe("Extension: Bridge", () => {
it("Should allow to remove device by object ID", async () => {
const device = devices.bulb;
const removeSpy = vi.spyOn(controller.zigbee, "removeDeviceFromLookup");
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/remove", stringify({id: "bulb"}));
await flushPromises();
expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
expect(device.removeFromDatabase).not.toHaveBeenCalled();
expect(settings.getDevice("bulb")).toBeUndefined();
expect(removeSpy).not.toHaveBeenCalled();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {id: "bulb", block: false, force: false}, status: "ok"}),
stringify({data: {id: "bulb", block: false, force: false, clear_cache: false}, status: "ok"}),
{},
);
});
it("Should allow to force remove device", async () => {
const device = devices.bulb;
const removeSpy = vi.spyOn(controller.zigbee, "removeDeviceFromLookup");
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/remove", stringify({id: "bulb", force: true}));
await flushPromises();
expect(device.removeFromDatabase).toHaveBeenCalledTimes(1);
expect(device.removeFromNetwork).not.toHaveBeenCalled();
expect(settings.getDevice("bulb")).toBeUndefined();
expect(removeSpy).not.toHaveBeenCalled();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {id: "bulb", block: false, force: true}, status: "ok"}),
stringify({data: {id: "bulb", block: false, force: true, clear_cache: false}, status: "ok"}),
{},
);
});
it("Should allow to block device", async () => {
const device = devices.bulb;
const removeSpy = vi.spyOn(controller.zigbee, "removeDeviceFromLookup");
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/remove", stringify({id: "bulb", block: true, force: true}));
await flushPromises();
expect(device.removeFromDatabase).toHaveBeenCalledTimes(1);
expect(settings.getDevice("bulb")).toBeUndefined();
expect(removeSpy).not.toHaveBeenCalled();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {id: "bulb", block: true, force: true}, status: "ok"}),
stringify({data: {id: "bulb", block: true, force: true, clear_cache: false}, status: "ok"}),
{},
);
expect(settings.get().blocklist).toStrictEqual(["0x000b57fffec6a5b2"]);
});
it("Should allow to clear cache when removing device", async () => {
const device = devices.bulb;
const removeSpy = vi.spyOn(controller.zigbee, "removeDeviceFromLookup");
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/remove", stringify({id: "bulb", clear_cache: true}));
await flushPromises();
expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
expect(device.removeFromDatabase).not.toHaveBeenCalled();
expect(settings.getDevice("bulb")).toBeUndefined();
expect(removeSpy).toHaveNthReturnedWith(1, true);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {id: "bulb", block: false, force: false, clear_cache: true}, status: "ok"}),
{},
);
});
it("Should allow to remove group", async () => {
const group = groups.group_1;
const removeGroupFromLookup = vi.spyOn(controller.zigbee, "removeGroupFromLookup");
@@ -3042,7 +3132,11 @@ describe("Extension: Bridge", () => {
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {}, status: "error", error: "Failed to remove device 'bulb' (block: false, force: false) (Error: device timeout)"}),
stringify({
data: {},
status: "error",
error: "Failed to remove device 'bulb' (block: false, force: false, clear cache: false) (Error: device timeout)",
}),
{},
);
});
@@ -4099,13 +4193,24 @@ describe("Extension: Bridge", () => {
mockMQTTEvents.message("zigbee2mqtt/bridge/request/backup", "");
await flushPromises();
expect(mockZHController.backup).toHaveBeenCalledTimes(1);
expect(mockJSZipFile).toHaveBeenCalledTimes(4);
expect(mockJSZipFile).toHaveBeenNthCalledWith(1, "configuration.yaml", expect.any(Object));
expect(mockJSZipFile).toHaveBeenNthCalledWith(2, path.join("ext_converters", "123", "myfile.js"), expect.any(Object));
expect(mockJSZipFile).toHaveBeenNthCalledWith(3, path.join("ext_converters", "afile.js"), expect.any(Object));
expect(mockJSZipFile).toHaveBeenNthCalledWith(4, "state.json", expect.any(Object));
expect(mockJSZipGenerateAsync).toHaveBeenCalledTimes(1);
expect(mockJSZipGenerateAsync).toHaveBeenNthCalledWith(1, {type: "base64"});
expect(mockFflateZip).toHaveBeenCalledTimes(1);
expect(mockFflateZip).toHaveBeenNthCalledWith(
1,
{
"configuration.yaml": expect.any(Buffer),
[path.join("ext_converters", "123", "myfile.js")]: expect.any(Buffer),
[path.join("ext_converters", "afile.js")]: expect.any(Buffer),
"state.json": expect.any(Buffer),
},
{level: 6},
expect.any(Function),
);
expect(Object.keys(mockFflateZip.mock.calls[0][0])).toStrictEqual([
"configuration.yaml",
path.join("ext_converters", "123", "myfile.js"),
path.join("ext_converters", "afile.js"),
"state.json",
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/backup",
stringify({data: {zip: "THISISBASE64"}, status: "ok"}),
@@ -4113,6 +4218,18 @@ describe("Extension: Bridge", () => {
);
});
it("Should return an error when the backup archive cannot be created", async () => {
mockMQTTPublishAsync.mockClear();
mockFflateZipFailOnce(new Error("invalid zip data"));
mockMQTTEvents.message("zigbee2mqtt/bridge/request/backup", "");
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/backup",
stringify({data: {}, status: "error", error: "invalid zip data"}),
{},
);
});
it("Should allow to restart", async () => {
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/restart", "");
@@ -4262,6 +4379,58 @@ describe("Extension: Bridge", () => {
);
});
it("Change options consecutively, check restart required", async () => {
settings.apply({health: {interval: 10, reset_on_check: false}});
mockMQTTPublishAsync.mockClear();
// Change option that doesn't require restart
mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {health: {reset_on_check: true}}}));
await flushPromises();
expect(settings.get().health.reset_on_check).toBe(true);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/options",
stringify({data: {restart_required: false}, status: "ok"}),
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.stringContaining('"restart_required":false'), {
retain: true,
});
mockMQTTPublishAsync.mockClear();
// Change option that requires restart
mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {health: {interval: 11}}}));
await flushPromises();
expect(settings.get().health.interval).toBe(11);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/options",
stringify({data: {restart_required: true}, status: "ok"}),
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.stringContaining('"restart_required":true'), {
retain: true,
});
mockMQTTPublishAsync.mockClear();
// Change option that doesn't require restart
mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {health: {reset_on_check: false}}}));
await flushPromises();
expect(settings.get().health.reset_on_check).toBe(false);
// System still requires restart
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/options",
stringify({data: {restart_required: true}, status: "ok"}),
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.stringContaining('"restart_required":true'), {
retain: true,
});
mockMQTTPublishAsync.mockClear();
});
it("Icon link handling", () => {
const bridge = controller.getExtension("Bridge")! as Bridge;
expect(bridge).toBeDefined();
+21 -9
View File
@@ -6,7 +6,7 @@ import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
import {flushPromises} from "../mocks/utils";
import {devices, type Endpoint, events as mockZHEvents, type Device as ZhDevice} from "../mocks/zigbeeHerdsman";
import stringify from "json-stable-stringify-without-jsonify";
import {stringify} from "../../lib/util/stringify";
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
import {Controller} from "../../lib/controller";
import Device from "../../lib/model/device";
@@ -193,16 +193,28 @@ describe("Extension: Configure", () => {
});
it("Should allow to configure via MQTT", async () => {
const emitDevicesChanged = vi.spyOn(controller.eventBus, "emitDevicesChanged");
const emitExposesChanged = vi.spyOn(controller.eventBus, "emitExposesChanged");
mockClear(devices.remote);
expectRemoteNotConfigured();
await mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/configure", "remote");
await flushPromises();
expectRemoteConfigured();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/configure",
stringify({data: {id: "remote"}, status: "ok"}),
{},
);
try {
await mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/configure", "remote");
await flushPromises();
expectRemoteConfigured();
expect(emitDevicesChanged).toHaveBeenCalledTimes(1);
expect(emitExposesChanged).toHaveBeenCalledWith({
device: controller.zigbee.resolveEntity(devices.remote),
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/configure",
stringify({data: {id: "remote"}, status: "ok"}),
{},
);
} finally {
emitExposesChanged.mockRestore();
emitDevicesChanged.mockRestore();
}
});
it("Fail to configure via MQTT when device does not exist", async () => {
+31 -1
View File
@@ -9,7 +9,7 @@ import {devices, mockController as mockZHController, returnDevices} from "../moc
import fs from "node:fs";
import path from "node:path";
import stringify from "json-stable-stringify-without-jsonify";
import {stringify} from "../../lib/util/stringify";
import * as zhc from "zigbee-herdsman-converters";
import {Controller} from "../../lib/controller";
import ExternalConverters from "../../lib/extension/externalConverters";
@@ -488,6 +488,25 @@ describe("Extension: ExternalConverters", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/converters", stringify([]), {retain: true});
});
it("returns error on invalid name", async () => {
const converterName = "foo1";
await resetExtension();
for (const mock of mocksClear) mock.mockClear();
await (controller.getExtension("ExternalConverters")! as ExternalConverters).onMQTTMessage({
topic: "zigbee2mqtt/bridge/request/converter/save",
message: {name: converterName, code: "a"},
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/converter/save",
expect.stringContaining(`JavaScript file must have '.mjs', '.js' or '.cjs' extension`),
{},
);
expect(writeFileSyncSpy).toHaveBeenCalledTimes(0);
});
it("returns error on invalid code", async () => {
const converterName = "foo1.js";
const converterCode = "definetly not a correct javascript code";
@@ -609,4 +628,15 @@ describe("Extension: ExternalConverters", () => {
);
});
});
it("doesn't add extension when external JS disabled", async () => {
settings.set(["advanced", "enable_external_js"], false);
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
await flushPromises();
expect(controller.getExtension("ExternalConverters")).toBeUndefined();
});
});
+31 -1
View File
@@ -8,7 +8,7 @@ import {devices, mockController as mockZHController, returnDevices} from "../moc
import fs from "node:fs";
import path from "node:path";
import stringify from "json-stable-stringify-without-jsonify";
import {stringify} from "../../lib/util/stringify";
import {Controller} from "../../lib/controller";
import ExternalExtensions from "../../lib/extension/externalExtensions";
import * as settings from "../../lib/util/settings";
@@ -299,6 +299,25 @@ describe("Extension: ExternalExtensions", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/extensions", stringify([]), {retain: true});
});
it("returns error on invalid name", async () => {
const extensionName = "foo1";
await resetExtension();
for (const mock of mocksClear) mock.mockClear();
await (controller.getExtension("ExternalExtensions")! as ExternalExtensions).onMQTTMessage({
topic: "zigbee2mqtt/bridge/request/extension/save",
message: {name: extensionName, code: "a"},
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/extension/save",
expect.stringContaining(`JavaScript file must have '.mjs', '.js' or '.cjs' extension`),
{},
);
expect(writeFileSyncSpy).toHaveBeenCalledTimes(0);
});
it("returns error on invalid code", async () => {
const extensionName = "foo1.js";
const extensionCode = "definetly not a correct javascript code";
@@ -365,4 +384,15 @@ describe("Extension: ExternalExtensions", () => {
);
});
});
it("doesn't add extension when external JS disabled", async () => {
settings.set(["advanced", "enable_external_js"], false);
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
await flushPromises();
expect(controller.getExtension("ExternalExtensions")).toBeUndefined();
});
});
+7 -1
View File
@@ -7,7 +7,7 @@ import {type EventHandler, flushPromises} from "../mocks/utils";
import {devices, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
import path from "node:path";
import stringify from "json-stable-stringify-without-jsonify";
import {stringify} from "../../lib/util/stringify";
import type {Mock} from "vitest";
import ws from "ws";
import {Controller} from "../../lib/controller";
@@ -245,6 +245,8 @@ describe("Extension: Frontend", () => {
stringify({
state: "ON",
effect: null,
effect_color: null,
effect_speed: null,
power_on_behavior: null,
linkquality: 20,
update: {state: null, installed_version: -1, latest_version: -1},
@@ -269,6 +271,8 @@ describe("Extension: Frontend", () => {
state: "ON",
power_on_behavior: null,
effect: null,
effect_color: null,
effect_speed: null,
linkquality: 20,
update: {state: null, installed_version: -1, latest_version: -1},
},
@@ -296,6 +300,8 @@ describe("Extension: Frontend", () => {
brightness: 90,
power_on_behavior: null,
effect: null,
effect_color: null,
effect_speed: null,
linkquality: 20,
update: {state: null, installed_version: -1, latest_version: -1},
},
+1 -1
View File
@@ -6,7 +6,7 @@ import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
import {flushPromises} from "../mocks/utils";
import {devices, groups, events as mockZHEvents, resetGroupMembers, returnDevices} from "../mocks/zigbeeHerdsman";
import stringify from "json-stable-stringify-without-jsonify";
import {stringify} from "../../lib/util/stringify";
import * as zhcGlobalStore from "zigbee-herdsman-converters/lib/store";
import {Controller} from "../../lib/controller";
import * as settings from "../../lib/util/settings";
+409 -69
View File
@@ -9,7 +9,7 @@ import type {Device as ZhDevice} from "../mocks/zigbeeHerdsman";
import {devices, groups, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
import assert from "node:assert";
import stringify from "json-stable-stringify-without-jsonify";
import {stringify} from "../../lib/util/stringify";
import type {MockInstance} from "vitest";
import * as zhc from "zigbee-herdsman-converters";
import type {KeyValueAny} from "zigbee-herdsman-converters/lib/types";
@@ -103,6 +103,7 @@ describe("Extension: HomeAssistant", () => {
definition: d,
isDevice: (): boolean => true,
isGroup: (): boolean => false,
endpoint: () => undefined,
options: {},
exposes: (): unknown[] => exposes,
zh: {endpoints: []},
@@ -124,7 +125,160 @@ describe("Extension: HomeAssistant", () => {
}
}
expect(duplicated).toHaveLength(0);
expect(duplicated).toStrictEqual([]);
});
it("Should mark thermostat configuration toggles as config entities", () => {
const switchExposes = [
new zhc.Switch().withLabel("Auto lock").withState("auto_lock", false, "Enable/disable auto lock", zhc.access.STATE_SET, "AUTO", "MANUAL"),
new zhc.Switch().withLabel("Away mode").withState("away_mode", false, "Enable/disable away mode", zhc.access.STATE_SET),
new zhc.Switch().withLabel("Valve detection").withState("valve_detection", true, "Valve detection", zhc.access.STATE_SET),
new zhc.Switch()
.withLabel("Window detection")
.withState("window_detection", true, "Enables/disables window detection", zhc.access.STATE_SET),
];
const binaryExposes = [
new zhc.Binary("frost_protection", zhc.access.STATE_SET, "ON", "OFF").withDescription("Anti-freeze protection"),
new zhc.Binary("heating_stop", zhc.access.STATE_SET, "ON", "OFF").withDescription("Heating stop"),
new zhc.Binary("away_mode", zhc.access.STATE_SET, "ON", "OFF").withDescription("Away mode"),
new zhc.Binary("window_detection", zhc.access.STATE_SET, "ON", "OFF").withDescription("Open window detection"),
];
const getDiscoveryConfigs = (expose: zhc.Expose): KeyValueAny[] => {
const device = {
definition: {},
isDevice: (): boolean => true,
isGroup: (): boolean => false,
endpoint: () => undefined,
options: {},
exposes: (): zhc.Expose[] => [expose],
zh: {endpoints: []},
};
// @ts-expect-error private method and minimal test device
return extension.getConfigs(device);
};
for (const expose of switchExposes) {
const [config] = getDiscoveryConfigs(expose);
expect(config.type).toStrictEqual("switch");
expect(config.object_id).toStrictEqual(expose.features[0].property);
expect(config.discovery_payload.entity_category).toStrictEqual("config");
expect(config.discovery_payload.command_topic_postfix).toStrictEqual(expose.features[0].property);
}
for (const expose of binaryExposes) {
const [config] = getDiscoveryConfigs(expose);
expect(config.type).toStrictEqual("switch");
expect(config.object_id).toStrictEqual(`switch_${expose.name}`);
expect(config.discovery_payload.entity_category).toStrictEqual("config");
expect(config.discovery_payload.command_topic_postfix).toStrictEqual(expose.property);
}
});
it("Should mark device settings as config entities", () => {
const getDiscoveryConfigs = (expose: zhc.Expose): KeyValueAny[] => {
const device = {
definition: {},
isDevice: (): boolean => true,
isGroup: (): boolean => false,
endpoint: () => undefined,
options: {},
exposes: (): zhc.Expose[] => [expose],
zh: {endpoints: []},
};
// @ts-expect-error private method and minimal test device
return extension.getConfigs(device);
};
const enumExposes = [
new zhc.Enum("set_limits", zhc.access.STATE_SET, ["START", "END", "RESET"]),
new zhc.Enum("motor_direction", zhc.access.STATE_SET, ["forward", "back"]),
new zhc.Enum("temperature_unit", zhc.access.STATE_SET, ["celsius", "fahrenheit"]),
];
for (const expose of enumExposes) {
const [config] = getDiscoveryConfigs(expose);
expect(config.type).toStrictEqual("select");
expect(config.object_id).toStrictEqual(expose.property);
expect(config.discovery_payload.entity_category).toStrictEqual("config");
}
const binaryExposes = [
new zhc.Binary("tilt_mode", zhc.access.STATE_SET, "ON", "OFF"),
new zhc.Binary("calibration_left", zhc.access.STATE_SET, "ON", "OFF"),
new zhc.Binary("motor_reversal_right", zhc.access.STATE_SET, "ON", "OFF"),
new zhc.Binary("enable_display", zhc.access.STATE_SET, "ON", "OFF"),
new zhc.Binary("indicator", zhc.access.STATE_SET, "ON", "OFF"),
];
for (const expose of binaryExposes) {
const [config] = getDiscoveryConfigs(expose);
expect(config.type).toStrictEqual("switch");
expect(config.object_id).toStrictEqual(`switch_${expose.property}`);
expect(config.discovery_payload.entity_category).toStrictEqual("config");
}
const numericExposes = [
new zhc.Numeric("calibration_time_left", zhc.access.STATE_SET),
new zhc.Numeric("comfort_temperature_min", zhc.access.STATE_SET),
new zhc.Numeric("comfort_humidity_max", zhc.access.STATE_SET),
new zhc.Numeric("measurement_interval", zhc.access.STATE_SET),
new zhc.Numeric("minimum_range", zhc.access.STATE_SET),
new zhc.Numeric("maximum_range", zhc.access.STATE_SET),
new zhc.Numeric("detection_delay", zhc.access.STATE_SET),
new zhc.Numeric("fading_time", zhc.access.STATE_SET),
new zhc.Numeric("large_motion_detection_sensitivity", zhc.access.STATE_SET),
new zhc.Numeric("medium_motion_detection_distance", zhc.access.STATE_SET),
new zhc.Numeric("small_detection_sensitivity", zhc.access.STATE_SET),
new zhc.Numeric("soil_calibration", zhc.access.STATE_SET),
new zhc.Numeric("soil_sampling", zhc.access.STATE_SET),
new zhc.Numeric("soil_warning", zhc.access.STATE_SET),
];
for (const expose of numericExposes) {
const [config] = getDiscoveryConfigs(expose);
expect(config.type).toStrictEqual("number");
expect(config.object_id).toStrictEqual(expose.property);
expect(config.discovery_payload.entity_category).toStrictEqual("config");
}
const [textConfig] = getDiscoveryConfigs(new zhc.Text("schedule_settings", zhc.access.STATE_SET));
expect(textConfig.type).toStrictEqual("text");
expect(textConfig.object_id).toStrictEqual("schedule_settings");
expect(textConfig.discovery_payload.entity_category).toStrictEqual("config");
});
it("Should apply expose-level Home Assistant discovery metadata", () => {
const createDevice = (exposes: zhc.Expose[]): Device =>
({
definition: {},
isDevice: (): boolean => true,
isGroup: (): boolean => false,
endpoint: () => undefined,
options: {},
exposes: (): zhc.Expose[] => exposes,
zh: {endpoints: []},
}) as Device;
const voltageExpose = new zhc.Numeric("voltage", zhc.access.STATE).withUnit("V");
Object.assign(voltageExpose, {
homeassistant: {
type: "valve",
entityCategory: "diagnostic",
deviceClass: "voltage",
enabledByDefault: false,
icon: "mdi:flash",
},
});
// @ts-expect-error private
const configs = extension.getConfigs(createDevice([voltageExpose]));
expect(configs.find((config) => config.object_id === "voltage")?.discovery_payload).toMatchObject({
device_class: "voltage",
enabled_by_default: false,
entity_category: "diagnostic",
icon: "mdi:flash",
});
expect(configs.find((config) => config.object_id === "voltage")?.discovery_payload).not.toHaveProperty("type");
});
it("Should discover devices and groups", async () => {
@@ -163,6 +317,16 @@ describe("Extension: HomeAssistant", () => {
"candle",
"fireplace",
"colorloop",
"sunset",
"sunrise",
"sparkle",
"opal",
"glisten",
"underwater",
"cosmos",
"sunbeam",
"enchant",
"none",
"finish_effect",
"stop_effect",
"stop_hue_effect",
@@ -170,6 +334,7 @@ describe("Extension: HomeAssistant", () => {
object_id: "ha_discovery_group",
default_entity_id: "light.ha_discovery_group",
unique_id: "9_light_zigbee2mqtt",
group: ["0x000b57fffec6a5b4_light_zigbee2mqtt", "0x000b57fffec6a5b7_light_zigbee2mqtt"],
origin: origin,
};
@@ -225,8 +390,9 @@ describe("Extension: HomeAssistant", () => {
object_id: "ha_discovery_group",
default_entity_id: "switch.ha_discovery_group",
unique_id: "9_switch_zigbee2mqtt",
group: ["0x0017880104e45542_switch_right_zigbee2mqtt"],
origin: origin,
value_template: "{{ value_json.state }}",
value_template: '{{ value_json["state"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
@@ -239,7 +405,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "°C",
device_class: "temperature",
state_class: "measurement",
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
@@ -266,7 +432,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "%",
device_class: "humidity",
state_class: "measurement",
value_template: "{{ value_json.humidity }}",
value_template: '{{ value_json["humidity"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_humidity",
default_entity_id: "sensor.weather_sensor_humidity",
@@ -293,7 +459,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "hPa",
device_class: "atmospheric_pressure",
state_class: "measurement",
value_template: "{{ value_json.pressure }}",
value_template: '{{ value_json["pressure"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_pressure",
default_entity_id: "sensor.weather_sensor_pressure",
@@ -320,7 +486,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "%",
device_class: "battery",
state_class: "measurement",
value_template: "{{ value_json.battery }}",
value_template: '{{ value_json["battery"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_battery",
default_entity_id: "sensor.weather_sensor_battery",
@@ -350,7 +516,7 @@ describe("Extension: HomeAssistant", () => {
entity_category: "diagnostic",
unit_of_measurement: "lqi",
state_class: "measurement",
value_template: "{{ value_json.linkquality }}",
value_template: '{{ value_json["linkquality"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
name: "Linkquality",
object_id: "weather_sensor_linkquality",
@@ -392,7 +558,7 @@ describe("Extension: HomeAssistant", () => {
default_entity_id: "switch.wall_switch_double_left",
unique_id: "0x0017880104e45542_switch_left_zigbee2mqtt",
origin: origin,
value_template: "{{ value_json.state_left }}",
value_template: '{{ value_json["state_left"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/switch/0x0017880104e45542/switch_left/config", stringify(payload), {
@@ -419,7 +585,7 @@ describe("Extension: HomeAssistant", () => {
default_entity_id: "switch.wall_switch_double_right",
unique_id: "0x0017880104e45542_switch_right_zigbee2mqtt",
origin: origin,
value_template: "{{ value_json.state_right }}",
value_template: '{{ value_json["state_right"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/switch/0x0017880104e45542/switch_right/config", stringify(payload), {
@@ -522,7 +688,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "%",
device_class: "humidity",
state_class: "measurement",
value_template: "{{ value_json.humidity }}",
value_template: '{{ value_json["humidity"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_humidity",
default_entity_id: "sensor.weather_sensor_humidity",
@@ -594,7 +760,7 @@ describe("Extension: HomeAssistant", () => {
device_class: "temperature",
state_class: "measurement",
enabled_by_default: true,
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
@@ -620,7 +786,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "%",
device_class: "humidity",
state_class: "measurement",
value_template: "{{ value_json.humidity }}",
value_template: '{{ value_json["humidity"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_humidity",
default_entity_id: "sensor.weather_sensor_humidity",
@@ -647,7 +813,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "hPa",
device_class: "atmospheric_pressure",
state_class: "measurement",
value_template: "{{ value_json.pressure }}",
value_template: '{{ value_json["pressure"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
enabled_by_default: true,
object_id: "weather_sensor_pressure",
@@ -705,7 +871,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "°C",
device_class: "temperature",
state_class: "measurement",
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
enabled_by_default: true,
object_id: "weather_sensor_temperature",
@@ -735,7 +901,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "%",
device_class: "humidity",
state_class: "measurement",
value_template: "{{ value_json.humidity }}",
value_template: '{{ value_json["humidity"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
enabled_by_default: true,
device: {
@@ -778,7 +944,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "°C",
device_class: "temperature",
state_class: "measurement",
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
@@ -805,7 +971,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "%",
device_class: "humidity",
state_class: "measurement",
value_template: "{{ value_json.humidity }}",
value_template: '{{ value_json["humidity"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_humidity",
default_entity_id: "sensor.weather_sensor_humidity",
@@ -868,7 +1034,7 @@ describe("Extension: HomeAssistant", () => {
default_entity_id: "light.my_switch",
unique_id: "0x0017880104e45541_light_zigbee2mqtt",
origin: origin,
value_template: "{{ value_json.state }}",
value_template: '{{ value_json["state"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/light/0x0017880104e45541/light/config", stringify(payload), {
@@ -914,11 +1080,11 @@ describe("Extension: HomeAssistant", () => {
command_topic: "zigbee2mqtt/fan/set/fan_state",
percentage_state_topic: "zigbee2mqtt/fan",
percentage_command_topic: "zigbee2mqtt/fan/set/fan_mode",
percentage_value_template: "{{ {'off':0, 'low':1, 'medium':2, 'high':3, 'on':4}[value_json.fan_mode] | default('None') }}",
percentage_value_template: "{{ {'off':0, 'low':1, 'medium':2, 'high':3, 'on':4}[value_json[\"fan_mode\"]] | default('None') }}",
percentage_command_template: "{{ {0:'off', 1:'low', 2:'medium', 3:'high', 4:'on'}[value] | default('') }}",
preset_mode_state_topic: "zigbee2mqtt/fan",
preset_mode_command_topic: "zigbee2mqtt/fan/set/fan_mode",
preset_mode_value_template: "{{ value_json.fan_mode if value_json.fan_mode in ['smart'] else 'None' | default('None') }}",
preset_mode_value_template: "{{ value_json[\"fan_mode\"] if value_json[\"fan_mode\"] in ['smart'] else 'None' | default('None') }}",
preset_modes: ["smart"],
speed_range_min: 1,
speed_range_max: 4,
@@ -990,7 +1156,7 @@ describe("Extension: HomeAssistant", () => {
command_topic: "zigbee2mqtt/fanbee/set/state",
percentage_state_topic: "zigbee2mqtt/fanbee",
percentage_command_topic: "zigbee2mqtt/fanbee/set/speed",
percentage_value_template: "{{ value_json.speed | default('None') }}",
percentage_value_template: "{{ value_json[\"speed\"] | default('None') }}",
percentage_command_template: "{{ value | default('') }}",
speed_range_min: 1,
speed_range_max: 254,
@@ -1023,7 +1189,7 @@ describe("Extension: HomeAssistant", () => {
it("Should discover thermostat devices", () => {
const payload = {
action_template:
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}",
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}",
action_topic: "zigbee2mqtt/TS0601_thermostat",
availability: [
{
@@ -1031,7 +1197,7 @@ describe("Extension: HomeAssistant", () => {
value_template: "{{ value_json.state }}",
},
],
current_temperature_template: "{{ value_json.local_temperature }}",
current_temperature_template: '{{ value_json["local_temperature"] }}',
current_temperature_topic: "zigbee2mqtt/TS0601_thermostat",
device: {
identifiers: ["zigbee2mqtt_0x0017882104a44559"],
@@ -1043,18 +1209,18 @@ describe("Extension: HomeAssistant", () => {
},
preset_mode_command_topic: "zigbee2mqtt/TS0601_thermostat/set/preset",
preset_modes: ["schedule", "manual", "boost", "complex", "comfort", "eco", "away"],
preset_mode_value_template: "{{ value_json.preset }}",
preset_mode_value_template: '{{ value_json["preset"] }}',
preset_mode_state_topic: "zigbee2mqtt/TS0601_thermostat",
max_temp: "35",
min_temp: "5",
mode_command_topic: "zigbee2mqtt/TS0601_thermostat/set/system_mode",
mode_state_template: "{{ value_json.system_mode }}",
mode_state_template: '{{ value_json["system_mode"] }}',
mode_state_topic: "zigbee2mqtt/TS0601_thermostat",
modes: ["heat", "auto", "off"],
name: null,
temp_step: 0.5,
temperature_command_topic: "zigbee2mqtt/TS0601_thermostat/set/current_heating_setpoint",
temperature_state_template: "{{ value_json.current_heating_setpoint }}",
temperature_state_template: '{{ value_json["current_heating_setpoint"] }}',
temperature_state_topic: "zigbee2mqtt/TS0601_thermostat",
temperature_unit: "C",
object_id: "ts0601_thermostat",
@@ -1094,7 +1260,7 @@ describe("Extension: HomeAssistant", () => {
state_topic: "zigbee2mqtt/thermostat",
unique_id: "0x0017880104e45550_pi_heating_demand_zigbee2mqtt",
unit_of_measurement: "%",
value_template: "{{ value_json.pi_heating_demand }}",
value_template: '{{ value_json["pi_heating_demand"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/sensor/0x0017880104e45550/pi_heating_demand/config", stringify(payload), {
@@ -1131,7 +1297,7 @@ describe("Extension: HomeAssistant", () => {
state_topic: "zigbee2mqtt/bosch_radiator",
unique_id: "0x18fc2600000d7ae2_pi_heating_demand_zigbee2mqtt",
unit_of_measurement: "%",
value_template: "{{ value_json.pi_heating_demand }}",
value_template: '{{ value_json["pi_heating_demand"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/number/0x18fc2600000d7ae2/pi_heating_demand/config", stringify(payload), {
@@ -1143,10 +1309,10 @@ describe("Extension: HomeAssistant", () => {
it("Should discover Bosch BTH-RA with a compatibility mapping", () => {
const payload = {
action_template:
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}",
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}",
action_topic: "zigbee2mqtt/bosch_radiator",
availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}],
current_temperature_template: "{{ value_json.local_temperature }}",
current_temperature_template: '{{ value_json["local_temperature"] }}',
current_temperature_topic: "zigbee2mqtt/bosch_radiator",
device: {
identifiers: ["zigbee2mqtt_0x18fc2600000d7ae2"],
@@ -1171,7 +1337,7 @@ describe("Extension: HomeAssistant", () => {
origin: origin,
temp_step: 0.5,
temperature_command_topic: "zigbee2mqtt/bosch_radiator/set/occupied_heating_setpoint",
temperature_state_template: "{{ value_json.occupied_heating_setpoint }}",
temperature_state_template: '{{ value_json["occupied_heating_setpoint"] }}',
temperature_state_topic: "zigbee2mqtt/bosch_radiator",
temperature_unit: "C",
unique_id: "0x18fc2600000d7ae2_climate_zigbee2mqtt",
@@ -1183,6 +1349,24 @@ describe("Extension: HomeAssistant", () => {
});
});
it("Should apply user configuration after converter compatibility mapping", async () => {
settings.set(["devices", "0x18fc2600000d7ae2", "homeassistant", "climate"], {
modes: ["off", "heat", "auto"],
mode_command_template: null,
});
await resetExtension();
await flushPromises();
const call = mockMQTTPublishAsync.mock.calls.find((c) => c[0] === "homeassistant/climate/0x18fc2600000d7ae2/climate/config");
expect(call).toBeDefined();
const payload = JSON.parse(call![1] as string);
expect(payload.modes).toStrictEqual(["off", "heat", "auto"]);
expect(payload.mode_command_template).toBeUndefined();
expect(payload.mode_command_topic).toStrictEqual("zigbee2mqtt/bosch_radiator/set");
});
it("does not throw when discovery payload override throws", async () => {
const bosch = getZ2MEntity(devices["RBSH-TRV0-ZB-EU"]) as Device;
assert(typeof bosch.definition?.meta?.overrideHaDiscoveryPayload === "function");
@@ -1198,10 +1382,10 @@ describe("Extension: HomeAssistant", () => {
const payload = {
action_template:
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}",
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}",
action_topic: "zigbee2mqtt/bosch_radiator",
availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}],
current_temperature_template: "{{ value_json.local_temperature }}",
current_temperature_template: '{{ value_json["local_temperature"] }}',
current_temperature_topic: "zigbee2mqtt/bosch_radiator",
device: {
identifiers: ["zigbee2mqtt_0x18fc2600000d7ae2"],
@@ -1215,7 +1399,7 @@ describe("Extension: HomeAssistant", () => {
max_temp: "30",
min_temp: "5",
mode_command_topic: "zigbee2mqtt/bosch_radiator/set/system_mode",
mode_state_template: "{{ value_json.system_mode }}",
mode_state_template: '{{ value_json["system_mode"] }}',
mode_state_topic: "zigbee2mqtt/bosch_radiator",
modes: ["heat"],
name: null,
@@ -1224,7 +1408,7 @@ describe("Extension: HomeAssistant", () => {
origin: origin,
temp_step: 0.5,
temperature_command_topic: "zigbee2mqtt/bosch_radiator/set/occupied_heating_setpoint",
temperature_state_template: "{{ value_json.occupied_heating_setpoint }}",
temperature_state_template: '{{ value_json["occupied_heating_setpoint"] }}',
temperature_state_topic: "zigbee2mqtt/bosch_radiator",
temperature_unit: "C",
unique_id: "0x18fc2600000d7ae2_climate_zigbee2mqtt",
@@ -1239,15 +1423,39 @@ describe("Extension: HomeAssistant", () => {
overrideSpy.mockRestore();
});
it("passes device options to discovery payload overrides", async () => {
const bosch = getZ2MEntity(devices["RBSH-TRV0-ZB-EU"]) as Device;
assert(typeof bosch.definition?.meta?.overrideHaDiscoveryPayload === "function");
const overrideSpy = vi.spyOn(bosch.definition.meta, "overrideHaDiscoveryPayload") as MockInstance;
settings.set(["devices", "0x18fc2600000d7ae2", "discovery_option_marker"], "passed");
overrideSpy.mockImplementation((payload, options) => {
if (payload.mode_command_topic?.endsWith("/system_mode")) {
payload.discovery_option_marker = options?.discovery_option_marker;
}
});
await resetExtension();
expect(overrideSpy).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({discovery_option_marker: "passed"}));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"homeassistant/climate/0x18fc2600000d7ae2/climate/config",
expect.stringContaining('"discovery_option_marker":"passed"'),
{qos: 1, retain: true},
);
overrideSpy.mockRestore();
});
it("Should discover Bosch BTH-RM230Z with a current_humidity attribute", () => {
const payload = {
action_template:
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}",
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}",
action_topic: "zigbee2mqtt/bosch_rm230z",
availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}],
current_humidity_template: "{{ value_json.humidity }}",
current_humidity_template: '{{ value_json["humidity"] }}',
current_humidity_topic: "zigbee2mqtt/bosch_rm230z",
current_temperature_template: "{{ value_json.local_temperature }}",
current_temperature_template: '{{ value_json["local_temperature"] }}',
current_temperature_topic: "zigbee2mqtt/bosch_rm230z",
default_entity_id: "climate.bosch_rm230z",
device: {
@@ -1262,20 +1470,20 @@ describe("Extension: HomeAssistant", () => {
min_temp: "5",
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 %}",
"{% set active_modes = ['heat'] %}{% set fallback_mode = 'heat' %}{% set values = {'schedule':'auto','pause':'off'} %}{% set value = value_json.operating_mode %}{% set mode = value_json.system_mode %}{% if value == 'manual' %}{{ mode if mode in active_modes else fallback_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 %}",
"{% set active_modes = ['heat'] %}{% set values = {'auto':'schedule','off':'pause'} %}{% if value in active_modes %}{\"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", "auto"],
modes: ["off", "heat", "auto"],
name: null,
object_id: "bosch_rm230z",
origin,
temp_step: 0.5,
temperature_high_command_topic: "zigbee2mqtt/bosch_rm230z/set/occupied_cooling_setpoint",
temperature_high_state_template: "{{ value_json.occupied_cooling_setpoint }}",
temperature_high_state_template: '{{ value_json["occupied_cooling_setpoint"] }}',
temperature_high_state_topic: "zigbee2mqtt/bosch_rm230z",
temperature_low_command_topic: "zigbee2mqtt/bosch_rm230z/set/occupied_heating_setpoint",
temperature_low_state_template: "{{ value_json.occupied_heating_setpoint }}",
temperature_low_state_template: '{{ value_json["occupied_heating_setpoint"] }}',
temperature_low_state_topic: "zigbee2mqtt/bosch_rm230z",
temperature_unit: "C",
unique_id: "0x18fc2600000d7ae3_climate_zigbee2mqtt",
@@ -1306,7 +1514,7 @@ describe("Extension: HomeAssistant", () => {
state_topic: "zigbee2mqtt/bosch_rm230z",
unique_id: "0x18fc2600000d7ae3_local_temperature_zigbee2mqtt",
unit_of_measurement: "°C",
value_template: "{{ value_json.local_temperature }}",
value_template: '{{ value_json["local_temperature"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/sensor/0x18fc2600000d7ae3/local_temperature/config", stringify(payload), {
@@ -1315,6 +1523,37 @@ describe("Extension: HomeAssistant", () => {
});
});
it("Should discover climate with cooling-only setpoint", () => {
const climateExpose = new zhc.Climate()
.withSetpoint("occupied_cooling_setpoint", 16, 32, 0.5)
.withLocalTemperature()
.withSystemMode(["off", "cool", "auto"]);
const device = {
definition: {},
isDevice: (): boolean => true,
isGroup: (): boolean => false,
endpoint: () => undefined,
options: {},
exposes: (): zhc.Expose[] => [climateExpose],
zh: {endpoints: []},
} as Device;
// @ts-expect-error private
const configs = extension.getConfigs(device);
const climate = configs.find((c) => c.type === "climate");
expect(climate).toBeDefined();
expect(climate!.discovery_payload).toMatchObject({
temperature_command_topic: "occupied_cooling_setpoint",
temperature_state_template: '{{ value_json["occupied_cooling_setpoint"] }}',
temperature_state_topic: true,
min_temp: "16",
max_temp: "32",
temp_step: 0.5,
});
expect(climate!.discovery_payload).not.toHaveProperty("temperature_low_command_topic");
expect(climate!.discovery_payload).not.toHaveProperty("temperature_high_command_topic");
});
it("Should discover devices with cover_position", () => {
let payload;
@@ -1323,9 +1562,9 @@ describe("Extension: HomeAssistant", () => {
position_topic: "zigbee2mqtt/smart vent",
set_position_topic: "zigbee2mqtt/smart vent/set",
set_position_template: '{ "position": {{ position }} }',
position_template: "{{ value_json.position }}",
position_template: '{{ value_json["position"] }}',
state_topic: "zigbee2mqtt/smart vent",
value_template: "{{ value_json.state }}",
value_template: '{{ value_json["state"] }}',
state_open: "OPEN",
state_closed: "CLOSE",
state_stopped: "STOP",
@@ -1362,7 +1601,7 @@ describe("Extension: HomeAssistant", () => {
via_device: "zigbee2mqtt_bridge_0x00124b00120144ae",
},
name: "L6",
position_template: "{{ value_json.position }}",
position_template: '{{ value_json["position"] }}',
position_topic: "zigbee2mqtt/zigfred_plus/l6",
set_position_template: '{ "position_l6": {{ position }} }',
set_position_topic: "zigbee2mqtt/zigfred_plus/l6/set",
@@ -1371,13 +1610,13 @@ describe("Extension: HomeAssistant", () => {
state_open: "OPEN",
state_topic: "zigbee2mqtt/zigfred_plus/l6",
tilt_command_topic: "zigbee2mqtt/zigfred_plus/l6/set/tilt",
tilt_status_template: "{{ value_json.tilt }}",
tilt_status_template: '{{ value_json["tilt"] }}',
tilt_status_topic: "zigbee2mqtt/zigfred_plus/l6",
object_id: "zigfred_plus_l6",
default_entity_id: "cover.zigfred_plus_l6",
unique_id: "0xf4ce368a38be56a1_cover_l6_zigbee2mqtt",
origin: origin,
value_template: "{{ value_json.state }}",
value_template: '{{ value_json["state"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/cover/0xf4ce368a38be56a1/cover_l6/config", stringify(payload), {
@@ -1407,7 +1646,7 @@ describe("Extension: HomeAssistant", () => {
object_id: "0xa4c138018cf95021_left",
default_entity_id: "cover.0xa4c138018cf95021_left",
origin: origin,
position_template: "{{ value_json.position }}",
position_template: '{{ value_json["position"] }}',
position_topic: "zigbee2mqtt/0xa4c138018cf95021/left",
set_position_template: '{ "position_left": {{ position }} }',
set_position_topic: "zigbee2mqtt/0xa4c138018cf95021/left/set",
@@ -1416,7 +1655,7 @@ describe("Extension: HomeAssistant", () => {
state_stopped: "STOP",
state_topic: "zigbee2mqtt/0xa4c138018cf95021/left",
unique_id: "0xa4c138018cf95021_cover_left_zigbee2mqtt",
value_template: '{% if "moving" in value_json and value_json.moving %} {{ value_json.moving }} {% else %} STOP {% endif %}',
value_template: '{% if "moving" in value_json and value_json["moving"] %} {{ value_json["moving"] }} {% else %} STOP {% endif %}',
};
const payload_right = {
availability: [
@@ -1438,7 +1677,7 @@ describe("Extension: HomeAssistant", () => {
object_id: "0xa4c138018cf95021_right",
default_entity_id: "cover.0xa4c138018cf95021_right",
origin: origin,
position_template: "{{ value_json.position }}",
position_template: '{{ value_json["position"] }}',
position_topic: "zigbee2mqtt/0xa4c138018cf95021/right",
set_position_template: '{ "position_right": {{ position }} }',
set_position_topic: "zigbee2mqtt/0xa4c138018cf95021/right/set",
@@ -1447,11 +1686,9 @@ describe("Extension: HomeAssistant", () => {
state_stopped: "STOP",
state_topic: "zigbee2mqtt/0xa4c138018cf95021/right",
unique_id: "0xa4c138018cf95021_cover_right_zigbee2mqtt",
value_template: '{% if "moving" in value_json and value_json.moving %} {{ value_json.moving }} {% else %} STOP {% endif %}',
value_template: '{% if "moving" in value_json and value_json["moving"] %} {{ value_json["moving"] }} {% else %} STOP {% endif %}',
};
console.log(mockMQTTPublishAsync.mock.calls.find((c) => c[0] === "homeassistant/cover/0xa4c138018cf95021/cover_left/config"));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/cover/0xa4c138018cf95021/cover_left/config", stringify(payload_left), {
retain: true,
qos: 1,
@@ -1462,6 +1699,62 @@ describe("Extension: HomeAssistant", () => {
});
});
it("Should discover an infrared emitter entity", () => {
const infraredEmitterExpose = new zhc.Text("emitter", zhc.access.SET).withHomeAssistant({
type: "infrared",
schema: "emitter",
valueTemplate: null,
});
const device = {
definition: {},
isDevice: (): boolean => true,
isGroup: (): boolean => false,
endpoint: () => undefined,
options: {},
exposes: (): zhc.Expose[] => [infraredEmitterExpose],
zh: {endpoints: []},
} as Device;
// @ts-expect-error private
const configs = extension.getConfigs(device);
const infrared = configs.find((c) => c.type === "infrared");
expect(infrared).toBeDefined();
expect(infrared!.discovery_payload).toMatchObject({
name: "Emitter",
schema: "emitter",
command_topic: true,
state_topic: 0,
});
expect(infrared!.discovery_payload).not.toHaveProperty("value_template");
});
it("Should discover an infrared receiver entity", () => {
const infraredReceiverExpose = new zhc.Text("receiver", zhc.access.STATE).withHomeAssistant({
type: "infrared",
schema: "receiver",
valueTemplate: "{{ json_value.emitter }}",
});
const device = {
definition: {},
isDevice: (): boolean => true,
isGroup: (): boolean => false,
endpoint: () => undefined,
options: {},
exposes: (): zhc.Expose[] => [infraredReceiverExpose],
zh: {endpoints: []},
} as Device;
// @ts-expect-error private
const configs = extension.getConfigs(device);
const infrared = configs.find((c) => c.type === "infrared");
expect(infrared).toBeDefined();
expect(infrared!.discovery_payload).toMatchObject({
name: "Receiver",
schema: "receiver",
});
expect(infrared!.discovery_payload).toHaveProperty("value_template");
});
it("Should discover devices with custom homeassistant.discovery_topic", async () => {
settings.set(["homeassistant", "discovery_topic"], "my_custom_discovery_topic");
await resetExtension();
@@ -1470,7 +1763,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "°C",
device_class: "temperature",
state_class: "measurement",
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
enabled_by_default: true,
object_id: "weather_sensor_temperature",
@@ -1502,7 +1795,7 @@ describe("Extension: HomeAssistant", () => {
await expect(async () => {
await controller.start();
}).rejects.toThrow("Home Assistant integration is not possible with attribute output!");
}).rejects.toThrow("Home Assistant integration requires 'output: json' under 'advanced'");
});
it("Should throw error when homeassistant.discovery_topic equals the mqtt.base_topic", async () => {
@@ -1518,7 +1811,9 @@ describe("Extension: HomeAssistant", () => {
settings.set(["advanced", "cache_state"], false);
mockLogger.warning.mockClear();
await resetExtension();
expect(mockLogger.warning).toHaveBeenCalledWith("In order for Home Assistant integration to work properly set `cache_state: true");
expect(mockLogger.warning).toHaveBeenCalledWith(
"In order for Home Assistant integration to work properly, set `cache_state: true` under `advanced`",
);
});
it("Should set missing values to null", async () => {
@@ -1558,6 +1853,8 @@ describe("Extension: HomeAssistant", () => {
color: {hue: 0, saturation: 100, h: 0, s: 100},
color_mode: "hs",
effect: null,
effect_color: null,
effect_speed: null,
linkquality: null,
state: null,
power_on_behavior: null,
@@ -1581,6 +1878,8 @@ describe("Extension: HomeAssistant", () => {
color: {x: 0.4576, y: 0.41},
color_mode: "xy",
effect: null,
effect_color: null,
effect_speed: null,
linkquality: null,
state: null,
power_on_behavior: null,
@@ -1603,6 +1902,8 @@ describe("Extension: HomeAssistant", () => {
stringify({
linkquality: null,
effect: null,
effect_color: null,
effect_speed: null,
state: "ON",
power_on_behavior: null,
update: {state: null, installed_version: -1, latest_version: -1},
@@ -1650,7 +1951,7 @@ describe("Extension: HomeAssistant", () => {
device_class: "temperature",
enabled_by_default: true,
state_class: "measurement",
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
@@ -1705,6 +2006,7 @@ describe("Extension: HomeAssistant", () => {
await flushPromises();
await vi.runOnlyPendingTimersAsync();
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/state", stringify({state: "online"}), {retain: true, qos: 1});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb",
stringify({
@@ -1745,6 +2047,7 @@ describe("Extension: HomeAssistant", () => {
await flushPromises();
await vi.runOnlyPendingTimersAsync();
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/state", stringify({state: "online"}), {retain: true, qos: 1});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb",
stringify({
@@ -1811,7 +2114,7 @@ describe("Extension: HomeAssistant", () => {
device_class: "temperature",
enabled_by_default: true,
state_class: "measurement",
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
@@ -1881,7 +2184,7 @@ describe("Extension: HomeAssistant", () => {
device_class: "temperature",
state_class: "measurement",
enabled_by_default: true,
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor_renamed",
object_id: "weather_sensor_renamed_temperature",
default_entity_id: "sensor.weather_sensor_renamed_temperature",
@@ -1965,6 +2268,16 @@ describe("Extension: HomeAssistant", () => {
"candle",
"fireplace",
"colorloop",
"sunset",
"sunrise",
"sparkle",
"opal",
"glisten",
"underwater",
"cosmos",
"sunbeam",
"enchant",
"none",
"finish_effect",
"stop_effect",
"stop_hue_effect",
@@ -1972,6 +2285,7 @@ describe("Extension: HomeAssistant", () => {
object_id: "ha_discovery_group_new",
default_entity_id: "light.ha_discovery_group_new",
unique_id: "9_light_zigbee2mqtt",
group: ["0x000b57fffec6a5b4_light_zigbee2mqtt", "0x000b57fffec6a5b7_light_zigbee2mqtt"],
origin: origin,
};
@@ -2004,7 +2318,7 @@ describe("Extension: HomeAssistant", () => {
device_class: "temperature",
state_class: "measurement",
enabled_by_default: true,
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor_renamed",
object_id: "weather_sensor_renamed_temperature",
default_entity_id: "sensor.weather_sensor_renamed_temperature",
@@ -2434,6 +2748,16 @@ describe("Extension: HomeAssistant", () => {
"candle",
"fireplace",
"colorloop",
"sunset",
"sunrise",
"sparkle",
"opal",
"glisten",
"underwater",
"cosmos",
"sunbeam",
"enchant",
"none",
"finish_effect",
"stop_effect",
"stop_hue_effect",
@@ -2441,6 +2765,7 @@ describe("Extension: HomeAssistant", () => {
object_id: "ha_discovery_group",
default_entity_id: "light.ha_discovery_group",
unique_id: "9_light_zigbee2mqtt",
group: ["0x000b57fffec6a5b4_light_zigbee2mqtt", "0x000b57fffec6a5b7_light_zigbee2mqtt"],
origin: origin,
};
@@ -2479,6 +2804,16 @@ describe("Extension: HomeAssistant", () => {
"candle",
"fireplace",
"colorloop",
"sunset",
"sunrise",
"sparkle",
"opal",
"glisten",
"underwater",
"cosmos",
"sunbeam",
"enchant",
"none",
"finish_effect",
"stop_effect",
"stop_hue_effect",
@@ -2486,6 +2821,7 @@ describe("Extension: HomeAssistant", () => {
object_id: "ha_discovery_group",
default_entity_id: "light.ha_discovery_group",
unique_id: "9_light_zigbee2mqtt",
group: ["0x000b57fffec6a5b4_light_zigbee2mqtt", "0x000b57fffec6a5b7_light_zigbee2mqtt"],
origin: origin,
};
@@ -2592,7 +2928,7 @@ describe("Extension: HomeAssistant", () => {
device_class: "temperature",
state_class: "measurement",
enabled_by_default: true,
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
@@ -2959,7 +3295,7 @@ describe("Extension: HomeAssistant", () => {
origin: origin,
state_topic: "zigbee2mqtt/0x18fc26000000cafe",
unique_id: "0x18fc26000000cafe_device_mode_zigbee2mqtt",
value_template: "{{ value_json.device_mode }}",
value_template: '{{ value_json["device_mode"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/select/0x18fc26000000cafe/device_mode/config", stringify(payload), {
retain: true,
@@ -2971,11 +3307,15 @@ describe("Extension: HomeAssistant", () => {
settings.set(["homeassistant", "legacy_action_sensor"], true);
await resetExtension();
// Should discovery action sensor
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/sensor/0x0017880104e45520/action/config", expect.any(String), {
retain: true,
qos: 1,
// Should discover action sensor as a diagnostic helper instead of a primary entity.
const actionDiscovery = mockMQTTPublishAsync.mock.calls.find(([topic]) => topic === "homeassistant/sensor/0x0017880104e45520/action/config");
assert(actionDiscovery);
expect(JSON.parse(actionDiscovery[1])).toMatchObject({
entity_category: "diagnostic",
name: "Action",
object_id: "button_action",
});
expect(actionDiscovery[2]).toStrictEqual({retain: true, qos: 1});
// Should counter an action payload with an empty payload
mockMQTTPublishAsync.mockClear();
+4 -4
View File
@@ -10,7 +10,7 @@ import {devices, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHer
import fs from "node:fs";
import path from "node:path";
import stringify from "json-stable-stringify-without-jsonify";
import {stringify} from "../../lib/util/stringify";
import {Controller} from "../../lib/controller";
import * as settings from "../../lib/util/settings";
@@ -286,7 +286,7 @@ describe("Extension: NetworkMap", () => {
description: "Hue Go",
model: "7146060PH",
supports:
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality",
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, effect_speed, effect_color, linkquality",
vendor: "Philips",
},
failed: [],
@@ -616,7 +616,7 @@ describe("Extension: NetworkMap", () => {
description: "Hue Go",
model: "7146060PH",
supports:
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality",
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, effect_speed, effect_color, linkquality",
vendor: "Philips",
},
failed: [],
@@ -785,7 +785,7 @@ describe("Extension: NetworkMap", () => {
description: "Hue Go",
model: "7146060PH",
supports:
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality",
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, effect_speed, effect_color, linkquality",
vendor: "Philips",
},
failed: [],
+66 -1
View File
@@ -9,7 +9,7 @@ import {devices, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
import {join} from "node:path";
import {existsSync, readFileSync, rmSync} from "node:fs";
import stringify from "json-stable-stringify-without-jsonify";
import {stringify} from "../../lib/util/stringify";
import {Controller} from "../../lib/controller";
import OTAUpdate from "../../lib/extension/otaUpdate";
import * as settings from "../../lib/util/settings";
@@ -546,6 +546,71 @@ describe("Extension: OTAUpdate", () => {
expect(existsSync(saveFilePath)).toStrictEqual(false);
});
it("aborts running OTA", async () => {
let timer: NodeJS.Timeout | undefined;
devices.bulb.updateOta.mockImplementationOnce(
async (_source, _requestPayload, _requestTsn, _extraMetas, onProgress, _dataSettings, _endpoint) => {
onProgress(0, 36000.5678);
onProgress(10, 3600.2123);
return await new Promise((resolve) => {
timer = setTimeout(
() =>
resolve([
{
...DEFAULT_CURRENT,
fileVersion: 1,
},
{
...DEFAULT_CURRENT,
fileVersion: 90,
},
]),
10000,
);
});
},
);
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/ota_update/update", stringify({id: "bulb"}));
await flushPromises();
await vi.advanceTimersByTimeAsync(9000);
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/ota_update/update/abort", stringify({id: "bulb"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb",
stringify({update: {state: "updating", progress: 0, remaining: 36001}}),
{
retain: true,
qos: 0,
},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb",
stringify({update: {state: "updating", progress: 10, remaining: 3600}}),
{retain: true, qos: 0},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({update: {state: "available"}}), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/ota_update/update/abort",
stringify({data: {id: "bulb"}, status: "ok"}),
{},
);
clearTimeout(timer);
});
it("handles abort when no running OTA", async () => {
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/ota_update/update/abort", stringify({id: "bulb"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/ota_update/update",
stringify({data: {}, status: "error", error: `No OTA in progress to abort for device 'bulb'`}),
{},
);
});
it("handles when OTA update fails", async () => {
devices.bulb.endpoints[0].read.mockImplementation(() => {
return {swBuildId: 1, dateCode: "2019010"};
+1 -1
View File
@@ -7,7 +7,7 @@ import * as mockSleep from "../mocks/sleep";
import {flushPromises} from "../mocks/utils";
import {devices, groups, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
import stringify from "json-stable-stringify-without-jsonify";
import {stringify} from "../../lib/util/stringify";
import {clearGlobalStore} from "zigbee-herdsman-converters";
import {Controller} from "../../lib/controller";
import {loadTopicGetSetRegex} from "../../lib/extension/publish";
+1 -1
View File
@@ -7,7 +7,7 @@ import * as mockSleep from "../mocks/sleep";
import {flushPromises} from "../mocks/utils";
import {devices, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
import stringify from "json-stable-stringify-without-jsonify";
import {stringify} from "../../lib/util/stringify";
import {Controller} from "../../lib/controller";
import * as settings from "../../lib/util/settings";
+13 -15
View File
@@ -5,20 +5,13 @@ import * as data from "./mocks/data";
import fs from "node:fs";
import {platform} from "node:os";
import path from "node:path";
import {rimrafSync} from "rimraf";
import tmp from "tmp";
import type {MockInstance} from "vitest";
import Transport from "winston-transport";
import logger from "../lib/util/logger";
import * as settings from "../lib/util/settings";
vi.mock("rimraf", async (importOriginal) => {
const actual = await importOriginal<typeof import("rimraf")>();
return {
...actual,
rimrafSync: vi.fn(actual.rimrafSync),
};
});
const rmSync = (target: string): void => fs.rmSync(target, {recursive: true, force: true});
describe("Logger", () => {
let consoleWriteSpy: MockInstance;
@@ -56,7 +49,7 @@ describe("Logger", () => {
it("Should cleanup (default setting)", () => {
for (const d of fs.readdirSync(dir.name)) {
rimrafSync(path.join(dir.name, d));
rmSync(path.join(dir.name, d));
}
for (let i = 0; i < 20; i++) {
@@ -70,25 +63,30 @@ describe("Logger", () => {
it("Should handle cleanup error", () => {
for (const d of fs.readdirSync(dir.name)) {
rimrafSync(path.join(dir.name, d));
rmSync(path.join(dir.name, d));
}
for (let i = 0; i < 20; i++) {
fs.mkdirSync(path.join(dir.name, `log_${i}`));
}
vi.mocked(rimrafSync).mockImplementationOnce(() => {
const rmSyncSpy = vi.spyOn(fs, "rmSync").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/));
try {
logger.init();
expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/Failed to remove old log directory '.*': Error: EACCES: permission denied/));
} finally {
rmSyncSpy.mockRestore();
}
});
it("Should cleanup (15 folders setting)", () => {
for (const d of fs.readdirSync(dir.name)) {
rimrafSync(path.join(dir.name, d));
rmSync(path.join(dir.name, d));
}
for (let i = 0; i < 20; i++) {
@@ -103,7 +101,7 @@ describe("Logger", () => {
it("Should not cleanup when there is no timestamp set", () => {
for (const d of fs.readdirSync(dir.name)) {
rimrafSync(path.join(dir.name, d));
rmSync(path.join(dir.name, d));
}
for (let i = 30; i < 50; i++) {
+1 -1
View File
@@ -1,8 +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 {stringify} from "../../lib/util/stringify";
import yaml from "../../lib/util/yaml";
export const mockDir: string = tmp.dirSync().name;
+19
View File
@@ -0,0 +1,19 @@
import type {AsyncZipOptions, AsyncZippable, FlateError} from "fflate";
/** `THISISBASE64` is valid base64, so it round-trips through `Buffer.from(...).toString("base64")` */
export const mockFflateZipContent = Uint8Array.from(Buffer.from("THISISBASE64", "base64"));
export const mockFflateZip = vi.fn((_data: AsyncZippable, _opts: AsyncZipOptions, cb: (error: FlateError | null, data: Uint8Array) => void): void => {
cb(null, mockFflateZipContent);
});
/** Makes the next `zip` call report the given error through its callback */
export const mockFflateZipFailOnce = (error: Error): void => {
mockFflateZip.mockImplementationOnce((_data, _opts, cb) => {
cb(error as FlateError, new Uint8Array());
});
};
vi.mock("fflate", () => ({
zip: mockFflateZip,
}));
-11
View File
@@ -1,11 +0,0 @@
export const mockJSZipFile = vi.fn();
export const mockJSZipGenerateAsync = vi.fn().mockReturnValue("THISISBASE64");
vi.mock("jszip", () => ({
default: vi.fn().mockImplementation(() => {
return {
file: mockJSZipFile,
generateAsync: mockJSZipGenerateAsync,
};
}),
}));
-4
View File
@@ -1,7 +1,3 @@
declare module "json-stable-stringify-without-jsonify" {
export default function (obj: unknown): string;
}
declare module "tmp" {
export function dirSync(): {
name: string;
+2
View File
@@ -283,6 +283,7 @@ export class Device {
unscheduleOta = vi.fn(() => {
this.scheduledOta = undefined;
});
abortOta = vi.fn();
scheduledOta: OtaSource | undefined = undefined;
constructor(
@@ -345,6 +346,7 @@ export class Device {
this.updateOta.mockClear();
this.scheduleOta.mockClear();
this.unscheduleOta.mockClear();
this.abortOta.mockClear();
this.meta = {};
this.scheduledOta = undefined;
+108 -184
View File
@@ -5,7 +5,7 @@ import * as data from "./mocks/data";
import {readFileSync, rmSync, writeFileSync} from "node:fs";
import {join} from "node:path";
import type {IncomingMessage, OutgoingHttpHeader, OutgoingHttpHeaders, RequestListener, Server, ServerResponse} from "node:http";
import JSZip from "jszip";
import {zipSync} from "fflate";
import type {findAllDevices} from "zigbee-herdsman/dist/adapter/adapterDiscovery";
import type {OnboardFailureData, OnboardInitData, OnboardSubmitResponse} from "../lib/types/api";
import {onboard} from "../lib/util/onboarding";
@@ -90,6 +90,7 @@ const SETTINGS_MINIMAL_DEFAULTS = {
network_key: "GENERATE",
pan_id: "GENERATE",
ext_pan_id: "GENERATE",
enable_external_js: false,
},
frontend: {
enabled: settings.defaults.frontend!.enabled,
@@ -147,6 +148,7 @@ const SAMPLE_SETTINGS_SAVE = {
network_key: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
pan_id: 12345,
ext_pan_id: [8, 7, 6, 5, 4, 3, 2, 1],
enable_external_js: false,
},
frontend: {
enabled: true,
@@ -538,205 +540,132 @@ describe("Onboarding", () => {
await responsePromise;
};
const createZipRestore = (): Awaited<ReturnType<typeof JSZip.loadAsync>> => {
return {
files: {
"configuration.yaml": {
name: "configuration.yaml",
dir: false,
// @ts-expect-error minimal mock
async: async () => await Promise.resolve(Buffer.from(JSON.stringify(SAMPLE_SETTINGS_SAVE))),
},
// @ts-expect-error minimal mock
"nested/": {
name: "nested/",
dir: true,
},
"nested/notes.txt": {
name: "nested/notes.txt",
dir: false,
// @ts-expect-error minimal mock
async: async () => await Promise.resolve(Buffer.from("zip-restore")),
},
},
};
};
const createZipPayload = (files: Parameters<typeof zipSync>[0]): string => Buffer.from(zipSync(files)).toString("base64");
const createZipRestore = (): string =>
createZipPayload({
"configuration.yaml": Buffer.from(JSON.stringify(SAMPLE_SETTINGS_SAVE)),
nested: {"notes.txt": Buffer.from("zip-restore")},
});
it("extracts uploaded ZIP files into the data path", async () => {
data.removeConfiguration();
const loadAsyncSpy = vi.spyOn(JSZip, "loadAsync").mockResolvedValue(createZipRestore());
try {
let p;
const submitData = await new Promise<OnboardSubmitResponse>((resolve, reject) => {
mockHttpOnListen.mockImplementationOnce(async () => {
try {
resolve(await submitZipPayload(Buffer.from("zip").toString("base64"), false, false));
} catch (error) {
reject(error);
}
});
p = onboard();
let p;
const submitData = await new Promise<OnboardSubmitResponse>((resolve, reject) => {
mockHttpOnListen.mockImplementationOnce(async () => {
try {
resolve(await submitZipPayload(createZipRestore(), false, false));
} catch (error) {
reject(error);
}
});
await expect(p).resolves.toStrictEqual(true);
expect(data.read()).toStrictEqual(SAMPLE_SETTINGS_SAVE);
expect(readFileSync(join(data.mockDir, "nested", "notes.txt"), "utf8")).toStrictEqual("zip-restore");
expect(loadAsyncSpy).toHaveBeenCalledTimes(1);
expect(submitData).toStrictEqual({success: true, frontendUrl: null});
} finally {
loadAsyncSpy.mockRestore();
}
p = onboard();
});
await expect(p).resolves.toStrictEqual(true);
expect(data.read()).toStrictEqual(SAMPLE_SETTINGS_SAVE);
expect(readFileSync(join(data.mockDir, "nested", "notes.txt"), "utf8")).toStrictEqual("zip-restore");
expect(submitData).toStrictEqual({success: true, frontendUrl: null});
});
it("rejects non-zip upload payloads", async () => {
data.removeConfiguration();
const loadAsyncSpy = vi
.spyOn(JSZip, "loadAsync")
.mockRejectedValueOnce(new Error("Can't find end of central directory : is this a zip file ?"))
.mockResolvedValueOnce(createZipRestore());
try {
let p;
const [firstSubmitData, secondSubmitData] = await new Promise<[OnboardSubmitResponse, OnboardSubmitResponse]>((resolve, reject) => {
mockHttpOnListen.mockImplementationOnce(async () => {
try {
const failedSubmit = await submitZipPayload(Buffer.from("ignored").toString("base64"), true, false);
const successfulSubmit = await submitZipPayload(Buffer.from("zip").toString("base64"), false, false);
let p;
const [firstSubmitData, secondSubmitData] = await new Promise<[OnboardSubmitResponse, OnboardSubmitResponse]>((resolve, reject) => {
mockHttpOnListen.mockImplementationOnce(async () => {
try {
const failedSubmit = await submitZipPayload(Buffer.from("not-a-zip-file").toString("base64"), true, false);
const successfulSubmit = await submitZipPayload(createZipRestore(), false, false);
resolve([failedSubmit, successfulSubmit]);
} catch (error) {
reject(error);
}
});
p = onboard();
resolve([failedSubmit, successfulSubmit]);
} catch (error) {
reject(error);
}
});
await expect(p).resolves.toStrictEqual(true);
expect(loadAsyncSpy).toHaveBeenCalledTimes(2);
expect(data.read()).toStrictEqual(SAMPLE_SETTINGS_SAVE);
expect(readFileSync(join(data.mockDir, "nested", "notes.txt"), "utf8")).toStrictEqual("zip-restore");
expect(firstSubmitData).toStrictEqual({success: false, error: expect.stringContaining("is this a zip file")});
expect(secondSubmitData).toStrictEqual({success: true, frontendUrl: null});
} finally {
loadAsyncSpy.mockRestore();
}
p = onboard();
});
await expect(p).resolves.toStrictEqual(true);
expect(data.read()).toStrictEqual(SAMPLE_SETTINGS_SAVE);
expect(readFileSync(join(data.mockDir, "nested", "notes.txt"), "utf8")).toStrictEqual("zip-restore");
expect(firstSubmitData).toStrictEqual({success: false, error: expect.stringContaining("invalid zip data")});
expect(secondSubmitData).toStrictEqual({success: true, frontendUrl: null});
});
it("rejects ZIP upload payloads with invalid entry paths", async () => {
data.removeConfiguration();
const loadAsyncSpy = vi
.spyOn(JSZip, "loadAsync")
.mockResolvedValueOnce({
files: {
"/dragons.txt": {
name: "/dragons.txt",
dir: false,
// @ts-expect-error minimal mock
async: async () => await Promise.resolve(Buffer.from("dragons")),
},
},
})
.mockResolvedValueOnce(createZipRestore());
try {
let p;
const [firstSubmitData, secondSubmitData] = await new Promise<[OnboardSubmitResponse, OnboardSubmitResponse]>((resolve, reject) => {
mockHttpOnListen.mockImplementationOnce(async () => {
try {
const failedSubmit = await submitZipPayload(Buffer.from("zip-invalid-path").toString("base64"), true, false);
const successfulSubmit = await submitZipPayload(Buffer.from("zip").toString("base64"), false, false);
let p;
const [firstSubmitData, secondSubmitData] = await new Promise<[OnboardSubmitResponse, OnboardSubmitResponse]>((resolve, reject) => {
mockHttpOnListen.mockImplementationOnce(async () => {
try {
const failedSubmit = await submitZipPayload(createZipPayload({"/dragons.txt": Buffer.from("dragons")}), true, false);
const successfulSubmit = await submitZipPayload(createZipRestore(), false, false);
resolve([failedSubmit, successfulSubmit]);
} catch (error) {
reject(error);
}
});
p = onboard();
resolve([failedSubmit, successfulSubmit]);
} catch (error) {
reject(error);
}
});
await expect(p).resolves.toStrictEqual(true);
expect(firstSubmitData).toStrictEqual({success: false, error: expect.stringContaining("Invalid ZIP entry path")});
expect(secondSubmitData).toStrictEqual({success: true, frontendUrl: null});
expect(loadAsyncSpy).toHaveBeenCalledTimes(2);
} finally {
loadAsyncSpy.mockRestore();
}
p = onboard();
});
await expect(p).resolves.toStrictEqual(true);
expect(firstSubmitData).toStrictEqual({success: false, error: expect.stringContaining("Invalid ZIP entry path")});
expect(secondSubmitData).toStrictEqual({success: true, frontendUrl: null});
});
it("rejects ZIP upload payloads with unsafe relative entry paths", async () => {
data.removeConfiguration();
const loadAsyncSpy = vi
.spyOn(JSZip, "loadAsync")
.mockResolvedValueOnce({
files: {
"../dragons.txt": {
name: "../dragons.txt",
dir: false,
// @ts-expect-error minimal mock
async: async () => await Promise.resolve(Buffer.from("dragons")),
},
},
})
.mockResolvedValueOnce(createZipRestore());
try {
let p;
const [firstSubmitData, secondSubmitData] = await new Promise<[OnboardSubmitResponse, OnboardSubmitResponse]>((resolve, reject) => {
mockHttpOnListen.mockImplementationOnce(async () => {
try {
const failedSubmit = await submitZipPayload(Buffer.from("zip-unsafe-path").toString("base64"), true, false);
const successfulSubmit = await submitZipPayload(Buffer.from("zip").toString("base64"), false, false);
let p;
const [firstSubmitData, secondSubmitData] = await new Promise<[OnboardSubmitResponse, OnboardSubmitResponse]>((resolve, reject) => {
mockHttpOnListen.mockImplementationOnce(async () => {
try {
const failedSubmit = await submitZipPayload(createZipPayload({"../dragons.txt": Buffer.from("dragons")}), true, false);
const successfulSubmit = await submitZipPayload(createZipRestore(), false, false);
resolve([failedSubmit, successfulSubmit]);
} catch (error) {
reject(error);
}
});
p = onboard();
resolve([failedSubmit, successfulSubmit]);
} catch (error) {
reject(error);
}
});
await expect(p).resolves.toStrictEqual(true);
expect(firstSubmitData).toStrictEqual({success: false, error: expect.stringContaining("Unsafe ZIP entry path")});
expect(secondSubmitData).toStrictEqual({success: true, frontendUrl: null});
expect(loadAsyncSpy).toHaveBeenCalledTimes(2);
} finally {
loadAsyncSpy.mockRestore();
}
p = onboard();
});
await expect(p).resolves.toStrictEqual(true);
expect(firstSubmitData).toStrictEqual({success: false, error: expect.stringContaining("Unsafe ZIP entry path")});
expect(secondSubmitData).toStrictEqual({success: true, frontendUrl: null});
});
it("handles empty ZIP upload payloads", async () => {
data.removeConfiguration();
const loadAsyncSpy = vi.spyOn(JSZip, "loadAsync").mockResolvedValue(createZipRestore());
try {
let p;
const [firstSubmitData, secondSubmitData] = await new Promise<[OnboardSubmitResponse, OnboardSubmitResponse]>((resolve, reject) => {
mockHttpOnListen.mockImplementationOnce(async () => {
try {
const failedSubmit = await submitZipPayload("", true, false);
const successfulSubmit = await submitZipPayload(Buffer.from("zip").toString("base64"), false, false);
let p;
const [firstSubmitData, secondSubmitData] = await new Promise<[OnboardSubmitResponse, OnboardSubmitResponse]>((resolve, reject) => {
mockHttpOnListen.mockImplementationOnce(async () => {
try {
const failedSubmit = await submitZipPayload("", true, false);
const successfulSubmit = await submitZipPayload(createZipRestore(), false, false);
resolve([failedSubmit, successfulSubmit]);
} catch (error) {
reject(error);
}
});
p = onboard();
resolve([failedSubmit, successfulSubmit]);
} catch (error) {
reject(error);
}
});
await expect(p).resolves.toStrictEqual(true);
expect(firstSubmitData).toStrictEqual({success: false, error: "Invalid ZIP payload: missing content"});
expect(secondSubmitData).toStrictEqual({success: true, frontendUrl: null});
expect(loadAsyncSpy).toHaveBeenCalledTimes(1);
} finally {
loadAsyncSpy.mockRestore();
}
p = onboard();
});
await expect(p).resolves.toStrictEqual(true);
expect(firstSubmitData).toStrictEqual({success: false, error: "Invalid ZIP payload: missing content"});
expect(secondSubmitData).toStrictEqual({success: true, frontendUrl: null});
});
it("handles request stream errors for submit endpoint", async () => {
@@ -765,32 +694,26 @@ describe("Onboarding", () => {
it("handles request stream errors for submit-zip endpoint", async () => {
data.removeConfiguration();
const loadAsyncSpy = vi.spyOn(JSZip, "loadAsync").mockResolvedValue(createZipRestore());
try {
let p;
const [firstSubmitData, secondSubmitData] = await new Promise<[OnboardSubmitResponse, OnboardSubmitResponse]>((resolve, reject) => {
mockHttpOnListen.mockImplementationOnce(async () => {
try {
const failedSubmit = await submitZipPayload("", true, true);
const successfulSubmit = await submitZipPayload(Buffer.from("zip").toString("base64"), false, false);
let p;
const [firstSubmitData, secondSubmitData] = await new Promise<[OnboardSubmitResponse, OnboardSubmitResponse]>((resolve, reject) => {
mockHttpOnListen.mockImplementationOnce(async () => {
try {
const failedSubmit = await submitZipPayload("", true, true);
const successfulSubmit = await submitZipPayload(createZipRestore(), false, false);
resolve([failedSubmit, successfulSubmit]);
} catch (error) {
reject(error);
}
});
p = onboard();
resolve([failedSubmit, successfulSubmit]);
} catch (error) {
reject(error);
}
});
await expect(p).resolves.toStrictEqual(true);
expect(firstSubmitData).toStrictEqual({success: false, error: "request error submit-zip"});
expect(secondSubmitData).toStrictEqual({success: true, frontendUrl: null});
expect(loadAsyncSpy).toHaveBeenCalledTimes(1);
} finally {
loadAsyncSpy.mockRestore();
}
p = onboard();
});
await expect(p).resolves.toStrictEqual(true);
expect(firstSubmitData).toStrictEqual({success: false, error: "request error submit-zip"});
expect(secondSubmitData).toStrictEqual({success: true, frontendUrl: null});
});
it("passes unknown onboarding routes to static file server", async () => {
@@ -963,6 +886,7 @@ describe("Onboarding", () => {
network_key: "GENERATE",
pan_id: "GENERATE",
ext_pan_id: "GENERATE",
enable_external_js: false,
},
serial: {
port: SAMPLE_SETTINGS_SAVE.serial.port,
+3 -3
View File
@@ -3,7 +3,7 @@ import {beforeEach, describe, expect, it} from "vitest";
import "./mocks/data";
import fs from "node:fs";
import yaml from "js-yaml";
import {dump, load} from "js-yaml";
import objectAssignDeep from "object-assign-deep";
import mockedData from "../lib/util/data";
@@ -21,14 +21,14 @@ const minimalConfig = {
describe("Settings", () => {
const write = (file: string, json: Record<string, unknown>, reread = true): void => {
fs.writeFileSync(file, yaml.dump(json));
fs.writeFileSync(file, dump(json));
if (reread) {
settings.reRead();
}
};
const read = (file: string): unknown => yaml.load(fs.readFileSync(file, "utf8"));
const read = (file: string): unknown => load(fs.readFileSync(file, "utf8"));
const remove = (file: string): void => {
if (fs.existsSync(file)) {
+34
View File
@@ -2,6 +2,7 @@ import {exec} from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import {describe, expect, it, vi} from "vitest";
import {stringify} from "../lib/util/stringify";
import utils, {assertString} from "../lib/util/utils";
// keep the implementations, just spy
@@ -155,4 +156,37 @@ describe("Utils", () => {
},
});
});
it("stable stringify", () => {
expect(
stringify({
a: "a",
b: 2,
3: "c",
d: Buffer.from([1, 2]),
e: new Int16Array([0xfffd, 0xff11]),
beef: 0xfacen,
zed: new BigUint64Array([1n, 0xffffffffn]),
ris: [1, undefined, "b", 0xfeefn, Number.NaN, undefined],
ls: undefined,
}),
).toStrictEqual(
`{"3":"c","a":"a","b":2,"beef":"64206","d":{"data":[1,2],"type":"Buffer"},"e":{"0":-3,"1":-239},"ris":[1,null,"b","65263",null,null],"zed":{"0":"1","1":"4294967295"}}`,
);
// @ts-expect-error intentional to reach code for coverage
expect(stringify(undefined)).toStrictEqual("null");
const circularObj: Record<string, unknown> = {a: 1, b: undefined};
circularObj.b = circularObj;
expect(stringify(circularObj)).toStrictEqual(`{"a":1,"b":"[Circular]"}`);
const toJSONIsString = {a: 1, toJSON: () => `{"a":1}`};
expect(stringify(toJSONIsString)).toStrictEqual(`"{\\"a\\":1}"`);
const toJSONIsNull = {a: 1, toJSON: () => null};
expect(stringify(toJSONIsNull)).toStrictEqual("null");
});
});
-1
View File
@@ -11,7 +11,6 @@
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"baseUrl": ".",
"rootDir": "lib",
"inlineSourceMap": true,
"resolveJsonModule": true,