Compare commits

...
492 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
Koen Kanters 3473aa3f40 chore(dev): release 2.9.0 (#30896) 2026-03-01 14:57:03 +01:00
Koen Kanters 8ee5b1f03b fix(ignore): update zigbee-herdsman to 9.0.9 (#31215) 2026-03-01 13:46:14 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 89192f9dec fix(ignore): bump the minor-patch group with 2 updates (#31211)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-28 23:10:27 +00:00
Koen Kanters a9a6ebe007 fix(ignore): update zigbee-herdsman-converters to 26.12.0 (#31208) 2026-02-28 20:03:57 +00:00
Koen Kanters c7d54ec360 fix(ignore): update zigbee-herdsman to 9.0.8 (#31207) 2026-02-28 19:42:46 +00:00
Koen Kanters c880d36f29 fix(ignore): update zigbee-herdsman-converters to 26.11.0 (#31185) 2026-02-25 20:56:05 +00:00
Koen Kanters e1e856d7da fix(ignore): Fix enableDisableExtension (#31176) 2026-02-24 20:46:30 +01:00
Koen Kanters b9d9e5e75c fix(ignore): update zigbee-herdsman-converters to 26.10.1 (#31175) 2026-02-24 19:31:23 +00:00
Nerivec f8385b8899 fix: Improve startup signals behavior (#31130) 2026-02-24 20:23:52 +01:00
Koen Kanters a77f48f5c4 fix: Fix Home Assistant options cannot be updated while running (#31151) 2026-02-24 20:23:35 +01:00
Koen Kanters 79bcf276f5 fix(ignore): update zigbee-herdsman to 9.0.7 (#31174) 2026-02-24 19:06:50 +00:00
Nerivec 93dc9a3664 fix: update zigbee2mqtt-windfront to 2.9.0 (#31162) 2026-02-23 21:02:59 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1d5184f2bd fix(ignore): bump @biomejs/biome from 2.4.1 to 2.4.4 in the minor-patch group (#31159)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 21:29:40 +01:00
Nerivec 8ecf353dd6 feat: Improve onboarding (#31152) 2026-02-23 21:29:31 +01:00
Koen Kanters 8560c11e06 fix(ignore): update zigbee-herdsman to 9.0.6 (#31149) 2026-02-22 20:29:08 +00:00
Koen Kanters 4d287eed0c fix(ignore): update zigbee-herdsman-converters to 26.9.0 (#31137) 2026-02-21 20:20:19 +00:00
DanielDeni 8d1240eba1 fix: Home Assistant: add weather station sensor discovery lookup entries (#31136) 2026-02-21 21:07:11 +01:00
NerivecandCopilot 0c43c98306 fix: Improve configure attempts lazy+cleanup (#31126)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-21 21:06:32 +01:00
Koen Kanters 396eb38e4b fix(ignore): update zigbee-herdsman-converters to 26.8.0 (#31118) 2026-02-19 19:43:14 +00:00
Koen Kanters 5957085fcc fix(ignore): update zigbee-herdsman to 9.0.5 (#31117) 2026-02-19 19:36:47 +00:00
Koen Kanters 02a2fd8fef fix: Home Assistant: add state_class: measurement for external_humidity and occupancy_level https://github.com/Koenkk/zigbee2mqtt/issues/31022 2026-02-19 20:29:43 +01:00
Koen Kanters 9d566f4c01 fix(ignore): update zigbee-herdsman-converters to 26.7.0 (#31090) 2026-02-17 20:15:20 +00:00
dependabot[bot] 42d88fe90d fix(ignore): bump the minor-patch group with 4 updates (#31081) 2026-02-16 19:54:17 +01:00
Koen Kanters 909d2b32b7 fix(ignore): update zigbee-herdsman-converters to 26.6.0 (#31076) 2026-02-15 19:34:36 +00:00
Koen Kanters 061262ff97 fix(ignore): update zigbee-herdsman-converters to 26.5.0 (#31058) 2026-02-13 20:49:19 +00:00
Koen Kanters 142754b71c fix(ignore): update zigbee-herdsman-converters to 26.4.0 (#31036) 2026-02-11 20:42:49 +00:00
Koen Kanters c599e19bf2 fix(ignore): update zigbee-herdsman-converters to 26.3.0 (#31028) 2026-02-10 19:47:25 +00:00
Koen Kanters 28a9f39dbc fix: Home Assistant: set state_class measurement for external_temperature https://github.com/Koenkk/zigbee2mqtt/issues/31022 2026-02-10 20:30:58 +01:00
Koen Kanters 4e360d0bf2 fix(ignore): update zigbee-herdsman-converters to 26.2.1 (#31015) 2026-02-09 21:06:38 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Koen Kanters
15e385bac6 fix(ignore): bump the minor-patch group with 3 updates (#31011)
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-02-09 20:57:32 +01:00
SnazzyBean c7dae39799 fix: Home Assistant: expose siren entity for IAS warning devices (#31000) 2026-02-09 20:42:03 +01:00
Koen Kanters b585e31304 fix(ignore): update zigbee-herdsman-converters to 26.2.0 (#30998) 2026-02-08 20:38:11 +00:00
Koen Kanters bf888de478 fix(ignore): update zigbee-herdsman to 9.0.4 (#30996) 2026-02-08 20:08:09 +00:00
Koen Kanters f1156db227 fix(ignore): update zigbee-herdsman-converters to 26.1.0 (#30981) 2026-02-06 21:16:11 +00:00
DataGhost d475aa5be5 fix: Home Assistant: add measurement state_class to _count entities (#30978) 2026-02-06 21:47:26 +01:00
+2
Koen Kantersdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>burmistrzakrhysfredrhysPatrick ZAJDAIgnacio Hernandez-RosRohan KapoorNerivecStephan GarlandAndrei LAZAROV
b71461e03c fix(ignore): Changes for new ZHC definition.version (#30402)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: burmistrzak <61958704+burmistrzak@users.noreply.github.com>
Co-authored-by: rhysfred <rhysfred@users.noreply.github.com>
Co-authored-by: rhys <rhys@frontleftspeaker.com>
Co-authored-by: Patrick ZAJDA <patrick@zajda.fr>
Co-authored-by: Ignacio Hernandez-Ros <ignacio@hernandez-ros.com>
Co-authored-by: Rohan Kapoor <rohan@rohankapoor.com>
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
Co-authored-by: Stephan Garland <stephan.garland@affirm.com>
Co-authored-by: Andrei LAZAROV <andrei_lazarov@yahoo.com>
2026-02-05 21:29:58 +01:00
Koenkk 6cce05a514 fix(ignore): update zigbee-herdsman-converters to 26.0.0 2026-02-05 21:29:58 +01:00
Koen Kanters 30d311e849 fix(ignore): update zigbee-herdsman to 9.0.3 (#30960) 2026-02-05 20:15:03 +00:00
Koen Kanters 402ed6094e fix(ignore): update zigbee-herdsman-converters to 25.122.0 (#30935) 2026-02-03 19:31:34 +00:00
Andrei LAZAROV e381d54f7e chore: readme: update integration logos and add links (#30926) 2026-02-03 20:05:11 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4b039103c0 fix(ignore): bump mqtt from 5.14.1 to 5.15.0 in the minor-patch group (#30918)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 20:43:51 +01:00
Koen Kanters 8d8d37e388 fix: Home Assistant: discover temperature_probe as temperature device_class https://github.com/Koenkk/zigbee2mqtt/issues/30862 2026-02-02 20:12:34 +01:00
Koen Kanters 09d010ea06 fix(ignore): update zigbee-herdsman-converters to 25.121.0 (#30898) 2026-02-01 20:05:04 +00:00
github-actions[bot] ede1dc5951 chore: promote to dev 2026-02-01 19:37:17 +00:00
Koen Kanters b0b02c30b2 chore(dev): release 2.8.0 (#30420) 2026-02-01 20:19:58 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9ddbb2a21a fix(ignore): bump zigbee2mqtt-windfront from 2.8.0 to 2.8.1 in the minor-patch group (#30879)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-01 20:17:41 +01:00
Koen Kanters 49b752da35 fix(ignore): update zigbee-herdsman-converters to 25.120.0 (#30876) 2026-01-31 13:35:35 +00:00
Koen Kanters 788b0d3993 fix(ignore): update zigbee-herdsman-converters to 25.119.0 (#30874) 2026-01-31 12:48:45 +00:00
Koen Kanters f68834049c fix(ignore): update zigbee-herdsman-converters to 25.118.0 (#30865) 2026-01-30 20:26:15 +00:00
Koen Kanters 4afbce62eb fix(ignore): update zigbee-herdsman-converters to 25.117.0 (#30849) 2026-01-29 19:07:37 +00:00
burmistrzak 4f412cb4da fix: Home Assistant: Change device_class of eco2 to volatile_organic_compounds_parts (#30840) 2026-01-29 19:48:41 +01:00
Koen Kanters 5a4d6b56ad fix(ignore): update zigbee-herdsman-converters to 25.116.0 (#30839) 2026-01-28 20:59:48 +00:00
burmistrzak 26a0fd4de0 fix: Home Assistant: discover temperature sensor for thermostats (#30804) 2026-01-28 21:47:03 +01:00
dependabot[bot] 8d980ae6ec fix(ignore): bump zigbee2mqtt-windfront in the minor-patch group
Bumps the minor-patch group with 1 update: [zigbee2mqtt-windfront](https://github.com/Nerivec/zigbee2mqtt-windfront).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-27 20:46:31 +01:00
Koen Kanters 99e06a8d3e fix(ignore): update zigbee-herdsman-converters to 25.115.0 (#30826) 2026-01-27 19:46:20 +00:00
NerivecandKoen Kanters 476efaa6e1 fix: OTA availability detection (#30815)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-01-27 20:37:18 +01:00
Koenkk 10478e797e fix(ignore): update zigbee-herdsman to 9.0.2 2026-01-27 20:37:18 +01:00
Stephan GarlandandKoen Kanters 5de47a0d93 fix: Network map: escape double-quotes and backslashes in device attributes (#30746)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-01-27 19:59:53 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 14bffd58b1 fix(ignore): bump @biomejs/biome from 2.3.12 to 2.3.13 in the minor-patch group (#30810)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 21:49:13 +01:00
Koen Kanters 999df2913f fix(ignore): update zigbee-herdsman-converters to 25.114.0 (#30803) 2026-01-25 20:55:24 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Nerivec
da1f77ec6e fix(ignore): bump the minor-patch group with 2 updates (#30790)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2026-01-24 20:59:45 +00:00
Koen Kanters 036ee471dd fix(ignore): update zigbee-herdsman-converters to 25.113.0 (#30789) 2026-01-24 20:31:04 +00:00
NerivecandKoen Kanters dd1c449796 feat: Improve OTA (#30566)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-01-24 21:27:44 +01:00
Koen Kanters 6d5e7e334a fix(ignore): update zigbee-herdsman to 9.0.1 (#30788) 2026-01-24 20:21:57 +00:00
burmistrzak 8e682956e5 fix: Home Assistant: Use temperature_delta for calibration (#30784) 2026-01-24 21:03:39 +01:00
Koen Kanters 971ddaa3cb fix(ignore): update zigbee-herdsman to 9.0.0 (#30787) 2026-01-24 19:45:15 +00:00
Koen Kanters ad51d12a2d fix(ignore): update zigbee-herdsman-converters to 25.112.0 (#30767) 2026-01-23 19:49:13 +00:00
Rohan Kapoor d9e4244f80 fix(ignore): Replace multiple area occupancy fields with camelCase instead of PascalCase (#30745) 2026-01-23 20:38:15 +01:00
Koen Kanters 2bb44f7a8b fix(ignore): update zigbee-herdsman-converters to 25.111.0 (#30739) 2026-01-21 20:53:41 +00:00
Koen Kanters 20af50201e fix(ignore): update zigbee-herdsman-converters to 25.110.0 (#30726) 2026-01-20 19:45:17 +00:00
Rohan Kapoor 2e36084852 fix: Home Assistant: treat Area1-4Occupancy as occupancy (#30712) 2026-01-20 19:34:39 +00:00
Koen Kanters 9d266bda9a fix(ignore): update zigbee-herdsman to 8.1.0 (#30725) 2026-01-20 19:29:58 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 043dd0ff1c fix(ignore): bump @types/node from 24.10.7 to 24.10.9 in the minor-patch group (#30713)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-20 20:09:54 +01:00
Koen Kanters ee6afaf1c6 fix(ignore): update zigbee-herdsman-converters to 25.109.0 (#30709) 2026-01-18 20:48:31 +00:00
Ignacio Hernandez-Ros 4815d5149d fix: Home Assistant: add gas device class (#30653) 2026-01-18 20:29:39 +01:00
Patrick ZAJDAandKoen Kanters 6f0b02e009 fix: Home Assistant: expose group settings override (#30627)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-01-17 15:25:36 +01:00
Koen Kanters 5e1b7d4830 fix(ignore): update zigbee-herdsman-converters to 25.108.0 (#30672) 2026-01-16 20:04:18 +00:00
Koen Kanters 535f75e90c fix(ignore): update zigbee-herdsman-converters to 25.107.0 (#30662) 2026-01-15 21:08:45 +00:00
Koen Kanters d44cdbde4f fix(ignore): update zigbee-herdsman to 8.0.3 (#30632) 2026-01-13 20:19:13 +00:00
Koen Kanters 9a41ec2a39 fix(ignore): update zigbee-herdsman-converters to 25.106.0 (#30631) 2026-01-13 20:03:22 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1d87d2f234 fix(ignore): bump the minor-patch group with 2 updates (#30617)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-12 20:53:11 +01:00
Koen Kanters 29c757145d fix(ignore): update zigbee-herdsman-converters to 25.105.0 (#30605) 2026-01-11 20:47:06 +00:00
Koen Kanters e68403d8d3 fix(ignore): update zigbee-herdsman-converters to 25.104.0 (#30564) 2026-01-08 20:09:11 +00:00
Koen Kanters a32c8eec36 fix: Home Assistant: mark load_estimate as state_class measurement https://github.com/Koenkk/zigbee-herdsman-converters/issues/11240 2026-01-08 20:54:25 +01:00
Koen Kanters d4522c53ca fix(ignore): update zigbee-herdsman-converters to 25.103.0 (#30552) 2026-01-07 19:38:42 +00:00
Koen Kanters 57245e5234 fix(ignore): update zigbee-herdsman-converters to 25.102.0 (#30537) 2026-01-06 20:44:54 +00:00
Koen Kanters 4313baca09 fix(ignore): update zigbee-herdsman-converters to 25.101.0 (#30520) 2026-01-05 20:56:26 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ed52c49e34 fix(ignore): bump @biomejs/biome from 2.3.10 to 2.3.11 in the minor-patch group (#30511)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-05 21:29:35 +01:00
Koen Kanters a2b81ab894 fix(ignore): update zigbee-herdsman-converters to 25.100.0 (#30499) 2026-01-04 20:27:22 +00:00
Koen Kanters 3e60f915ff fix: Home Assisant: set state class to measurement for illuminane_raw https://github.com/Koenkk/zigbee2mqtt/issues/30439 2026-01-04 20:02:44 +01:00
rhysfredandrhys 1eaa2f3e75 fix: Log error before renaming failed to load converters (#30436)
Co-authored-by: rhys <rhys@frontleftspeaker.com>
2026-01-04 19:51:46 +01:00
Koen Kantersandburmistrzak 3adf7d5d8e fix(ignore): update zigbee-herdsman-converters to 25.99.0 (#30481)
Co-authored-by: burmistrzak <61958704+burmistrzak@users.noreply.github.com>
2026-01-04 18:51:31 +00:00
Koen Kanters 1147cf16dd fix(ignore): update zigbee-herdsman to 8.0.2 (#30480) 2026-01-03 20:59:33 +00:00
github-actions[bot] 97a9b15240 chore: promote to dev 2026-01-01 13:53:53 +00:00
Koen Kanters 3a49c95786 chore(dev): release 2.7.2 (#30051) 2026-01-01 14:41:19 +01:00
Koen Kanters 0eba0c953d fix(ignore): update zigbee-herdsman-converters to 25.98.0 (#30417) 2026-01-01 13:23:35 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4d6269e7e8 fix(ignore): bump zigbee2mqtt-windfront from 2.6.1 to 2.6.2 in the minor-patch group (#30412)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-31 19:56:27 +00:00
Koen Kanters 3727f078ec fix(ignore): update zigbee-herdsman-converters to 25.97.0 (#30396) 2025-12-30 14:29:46 +00:00
dependabot[bot] 93c9704fcd fix(ignore): bump @types/node from 24.10.2 to 24.10.4 in the minor-patch group (#30390) 2025-12-30 13:43:39 +01:00
Koen Kanters 2721b0ea78 fix(ignore): update zigbee-herdsman-converters to 25.96.0 (#30383) 2025-12-28 20:58:41 +00:00
Koen Kanters 4c48e9cfbc fix(ignore): update zigbee-herdsman-converters to 25.95.0 (#30371) 2025-12-27 20:42:04 +00:00
Koen Kanters 7cb5c58dc0 fix(ignore): update zigbee-herdsman-converters to 25.94.0 (#30355) 2025-12-26 14:37:38 +00:00
Koen Kanters c61f67f061 fix(ignore): update zigbee-herdsman-converters to 25.93.0 (#30331) 2025-12-23 20:58:54 +00:00
Nerivec 58d98c7263 fix: log dir tz format not working on some systems (#30324) 2025-12-23 17:45:04 +01:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Nerivec
b10fa93700 fix(ignore): bump the minor-patch group with 2 updates (#30309)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2025-12-22 15:23:39 +00:00
Koen Kanters c28a024664 fix(ignore): update zigbee-herdsman-converters to 25.92.0 (#30300) 2025-12-21 21:28:27 +00:00
Nerivec 8549b37228 fix: Proper timezone in logs dir (#30297) 2025-12-21 20:44:05 +01:00
Koen Kanters 4c02e3da8f fix(ignore): update zigbee-herdsman-converters to 25.91.0 (#30279) 2025-12-19 21:02:06 +00:00
Koen Kanters ab0894715a fix(ignore): update zigbee-herdsman-converters to 25.90.0 (#30267) 2025-12-18 21:36:30 +00:00
Koen Kanters 1688a3b532 fix(ignore): update zigbee-herdsman to 8.0.1 (#30240) 2025-12-16 16:51:45 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 97694d2241 chore(deps): bump actions/cache from 4 to 5 (#30221)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 17:45:15 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d6088c56db chore(deps): bump peter-evans/create-pull-request from 7 to 8 (#30222)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 17:45:06 +01:00
Koen Kanters b39b8d478e fix: Don't fail to start when removing old log directory fails (#30216) 2025-12-16 17:44:38 +01:00
Koen Kanters 01304cfc66 fix(ignore): update zigbee-herdsman-converters to 25.89.0 (#30215) 2025-12-14 20:40:29 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1d15c2142f fix(ignore): bump the minor-patch group with 2 updates (#30207)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-14 16:40:27 +00:00
Koen Kanters 3818c50b8a fix(ignore): update zigbee-herdsman-converters to 25.88.0 (#30196) 2025-12-13 20:34:30 +00:00
Koen KantersandNerivec 7f5f6a5849 fix(ignore): update zigbee-herdsman to 8.0.0 and zigbee-herdsman-converters to 25.87.0 (#30186)
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2025-12-13 20:40:09 +01:00
Nerivec 0025ef87c5 fix: Biome floating promises detection (#30137) 2025-12-13 20:38:03 +01:00
Koen Kanters 3460f157df fix(ignore): update zigbee-herdsman-converters to 25.86.0 (#30143) 2025-12-10 21:14:41 +00:00
Koen Kanters ecb0af4057 fix(ignore): update zigbee-herdsman to 7.0.6 (#30142) 2025-12-10 21:11:13 +00:00
Koen Kanters 5db6ae0a19 fix(ignore): update zigbee-herdsman-converters to 25.85.0 (#30120) 2025-12-09 20:15:54 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ad3c09036b fix(ignore): bump the minor-patch group across 1 directory with 4 updates (#30117)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-09 20:33:03 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0c89629526 chore(deps): bump alpine from 3.22 to 3.23 in /docker (#30094)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-09 20:04:22 +01:00
Koen Kanters 4574ffc71a fix(ignore): update zigbee-herdsman to 7.0.5 (#30116) 2025-12-09 18:46:55 +00:00
Koen Kanters 1707f3abf3 chore: merge master to dev 2025-12-07 21:26:17 +01:00
Koen Kanters 8916fb7b50 chore: lint issueBot 2025-12-07 21:25:58 +01:00
Koen Kanters 6e70ef24d0 chore: merge master to dev 2025-12-07 21:22:54 +01:00
Koen Kanters 8a3c4bd59f chore: issueBot: handle duplicates 2025-12-07 21:22:34 +01:00
Koen Kanters c18d1329c8 fix(ignore): update zigbee-herdsman-converters to 25.84.0 (#30081) 2025-12-07 19:56:11 +00:00
Yuxin Wang ed1e7b333f fix: Remove extra > in onboarding(#30065) 2025-12-07 20:22:04 +01:00
github-actions[bot] 95c38810a8 chore: promote to dev 2025-12-06 20:39:43 +00:00
Koen Kanters b70701f8b8 chore: fix changelog generator undefined whitelabel vendor/description 2025-12-06 21:23:09 +01:00
Koen Kanters 6d30fa156c chore(dev): release 2.7.1 (#29926) 2025-12-06 21:14:47 +01:00
Koen Kanters 9b6035387b fix(ignore): update zigbee-herdsman-converters to 25.83.1 (#30050) 2025-12-06 20:06:52 +00:00
burmistrzakandKoen Kanters 9ca85dfac8 fix: Home Assistant: Translate boolean to state topic for current_humidity_topic (#30014)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2025-12-06 20:46:27 +01:00
Koen Kanters 334fa2a60d fix(ignore): update zigbee-herdsman to 7.0.4 (#30041) 2025-12-06 13:22:31 +00:00
Koen Kanters a7f1345a91 fix(ignore): update zigbee-herdsman-converters to 25.83.0 (#30011) 2025-12-04 20:54:41 +00:00
Koen Kanters bae9bb8812 fix(ignore): update zigbee-herdsman to 7.0.3 (#30008) 2025-12-04 20:02:17 +00:00
Koen Kanters 740edd8b58 fix(ignore): update zigbee-herdsman-converters to 25.82.0 (#29990) 2025-12-03 19:29:19 +00:00
Koen Kanters 267fee3414 Revert "chore: retry 'Docker build and push' on failure"
This reverts commit 91f4b76970.
2025-12-03 20:10:50 +01:00
burmistrzak fa99c6dcca fix: Home Assistant: Accept multiple access levels for current_humidity source property (#29936) 2025-12-03 19:34:35 +01:00
Koen Kanters 91f4b76970 chore: retry 'Docker build and push' on failure 2025-12-02 21:57:12 +01:00
Koen Kanters fd4282b98d fix(ignore): update zigbee-herdsman-converters to 25.81.0 (#29959) 2025-12-02 20:48:36 +00:00
Koen Kanters 403bd4a725 fix(ignore): update zigbee-herdsman to 7.0.2 (#29956) 2025-12-02 20:16:21 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ce27ff3cbf fix(ignore): bump finalhandler from 2.1.0 to 2.1.1 in the minor-patch group (#29920)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 21:00:30 +01:00
github-actions[bot] 5f474e03b8 chore: promote to dev 2025-12-01 20:35:56 +00:00
Koen Kanters 110149fa20 chore(dev): release 2.7.0 (#29396) 2025-12-01 21:22:06 +01:00
Koen Kanters 445890a071 fix(ignore): update zigbee-herdsman-converters to 25.80.0 (#29924) 2025-12-01 20:17:02 +00:00
Nerivec 3dce8548ce fix(ignore): bump zigbee2mqtt-windfront to 2.4.2 (#29919) 2025-12-01 16:02:29 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4a020fd8de fix(ignore): bump zigbee2mqtt-windfront from 2.4.0 to 2.4.1 in the minor-patch group (#29905)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-30 15:36:04 +00:00
Koen Kanters d411fc5c69 fix(ignore): update zigbee-herdsman-converters to 25.79.0 (#29903) 2025-11-30 14:06:44 +00:00
burmistrzakandKoen Kanters 631c4f6a57 fix: Home Assistant: expose current_humidity for climate devices exposing humidity (#29842)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2025-11-29 13:32:28 +01:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Nerivec
c02ed09211 fix(ignore): bump the minor-patch group with 2 updates (#29859)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2025-11-27 20:12:22 +00:00
Koen Kanters 49b4876768 fix(ignore): update zigbee-herdsman-converters to 25.78.0 (#29856) 2025-11-27 19:16:34 +00:00
Koen Kanters 802271ed37 fix(ignore): update zigbee-herdsman to 7.0.1 (#29855) 2025-11-27 19:00:08 +00:00
Nerivec 42fc6e03a7 fix: Reporting payload detection fixes (#29854) 2025-11-27 19:46:31 +01:00
Koen Kanters 35d8a5da7f fix(ignore): update zigbee-herdsman-converters to 25.77.0 (#29838) 2025-11-26 20:00:02 +00:00
Koen Kanters 72d862f4a0 fix(ignore): update zigbee-herdsman-converters to 25.76.0 (#29823) 2025-11-25 19:57:30 +00:00
Nerivec ad0696856a fix(ignore): prop name convention on new API endpoints (#29820) 2025-11-25 20:18:14 +01:00
NerivecandKoen Kanters a61646d584 feat: Add action bridge/request API (#29730)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2025-11-24 21:57:48 +01:00
Koen Kanters 854df498bd fix(ignore): update zigbee-herdsman-converters to 25.75.0 (#29811) 2025-11-24 20:54:10 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Koen Kanters
e2232e9e91 fix(ignore): bump the minor-patch group with 3 updates (#29804)
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>
2025-11-24 21:33:29 +01:00
dependabot[bot] 6775c4f17b chore(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 21:31:41 +01:00
Nerivec 17520f7cfb fix(ignore): update zigbee2mqtt-windfront to 2.3.1 (#29788) 2025-11-23 22:27:04 +00:00
Koen Kanters 1707bcab88 chore: merge master to dev 2025-11-23 22:05:07 +01:00
Koen Kanters c577c8e5f0 chore: lint issueBot 2025-11-23 22:04:48 +01:00
Koen Kanters 7b8efbe2c4 chore: merge master to dev 2025-11-23 21:59:31 +01:00
Koen Kanters 459b695f60 chore: issueBot: add logging and search in title 2025-11-23 21:59:13 +01:00
NerivecandKoen Kanters f26ade4f93 feat: Add new bind/reporting/map features (#29750)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2025-11-23 21:50:11 +01:00
Koenkk 3e2aefbcf8 fix(ignore): update zigbee-herdsman to 7.0.0 2025-11-23 21:50:11 +01:00
Koen Kanters ba77ce7c8d fix(ignore): update zigbee-herdsman-converters to 25.74.0 (#29770) 2025-11-22 20:47:44 +00:00
Alex Freidel 4c21b66079 fix: Add port 8080 EXPOSE to container (#29754) 2025-11-22 13:24:24 +01:00
Koen Kanters be774a4d0d fix(ignore): update zigbee-herdsman-converters to 25.73.0 (#29752) 2025-11-21 19:48:11 +00:00
Koen Kanters 7b7b988960 fix(ignore): update zigbee-herdsman-converters to 25.72.0 (#29735) 2025-11-20 21:00:08 +00:00
Koen Kanters d6bf5174be fix(ignore): update zigbee-herdsman-converters to 25.71.0 (#29716) 2025-11-19 20:57:49 +00:00
Koen Kanters 73225e2436 fix(ignore): update zigbee-herdsman-converters to 25.70.0 (#29684) 2025-11-17 21:08:22 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Koen Kanters
afedab9680 chore(deps): bump js-yaml from 4.1.0 to 4.1.1 (#29675)
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>
2025-11-17 21:58:42 +01:00
Koen Kanters 53b7eaabf0 fix(ignore): update zigbee-herdsman to 6.4.2 (#29683) 2025-11-17 20:49:33 +00:00
Koen Kantersandburmistrzak 16d0e56968 fix(ignore): update zigbee-herdsman-converters to 25.69.0 (#29659)
Co-authored-by: burmistrzak <61958704+burmistrzak@users.noreply.github.com>
2025-11-17 20:11:05 +00:00
dependabot[bot] aea41b5c82 chore(deps): bump js-yaml from 4.1.0 to 4.1.1 (#29649) 2025-11-16 15:28:44 +01:00
Koen Kanters 564e9ab9eb fix(ignore): update zigbee-herdsman-converters to 25.68.0 (#29606) 2025-11-12 19:51:37 +00:00
Koen Kanters 61c6ec6111 fix(ignore): update zigbee-herdsman to 6.4.1 (#29595) 2025-11-11 21:09:25 +00:00
Koen Kanters 4794b3c81f fix(ignore): update zigbee-herdsman-converters to 25.67.0 (#29572) 2025-11-10 17:16:16 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 517b286583 fix(ignore): bump @biomejs/biome from 2.3.3 to 2.3.4 in the minor-patch group (#29571)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 18:09:42 +01:00
Louis Sautier aee173d10a chore: update build command in README (#29562) 2025-11-10 18:03:18 +01:00
Koen Kanters 940fff244c fix(ignore): update zigbee-herdsman-converters to 25.66.0 (#29558) 2025-11-09 20:50:48 +00:00
Koen Kanters 8822aedb5e fix(ignore): update zigbee-herdsman-converters to 25.65.0 (#29523) 2025-11-07 20:42:22 +00:00
Koen Kanters e788c520a9 fix(ignore): update zigbee-herdsman to 6.4.0 (#29522) 2025-11-07 20:37:54 +00:00
Koen Kanters 39c5bc8c04 fix(ignore): update zigbee-herdsman to 6.3.3 (#29500) 2025-11-06 21:53:56 +00:00
Koen Kanters 5c4e43fd10 fix(ignore): update zigbee-herdsman-converters to 25.64.0 (#29499) 2025-11-06 19:23:23 +00:00
Koen Kanters 5a6a9cf4d2 fix(ignore): update zigbee-herdsman-converters to 25.63.0 (#29482) 2025-11-05 20:18:35 +00:00
Koen Kanters ad0e91e61e chore: merge master to dev 2025-11-04 21:22:30 +01:00
Koen Kanters 5797b56871 chore: refactor issue bot 2025-11-04 21:22:09 +01:00
Koen Kanters ea1c5a8f4e fix(ignore): update zigbee-herdsman-converters to 25.62.0 (#29449) 2025-11-04 20:08:45 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8099322cc2 fix(ignore): bump debounce from 2.2.0 to 3.0.0 (#29444)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 22:18:45 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 619b939b14 fix(ignore): bump the minor-patch group with 2 updates (#29443)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 22:18:32 +01:00
Koen Kanters eeed56ef2c fix(ignore): update zigbee-herdsman-converters to 25.60.0 (#29426) 2025-11-02 20:13:40 +00:00
github-actions[bot] e9b4a6221b chore: promote to dev 2025-11-01 21:04:00 +00:00
Koen Kanters 6e001b48ec chore(dev): release 2.6.3 (#28788) 2025-11-01 21:46:15 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dc7749b57e fix(ignore): bump the minor-patch group across 1 directory with 4 updates (#29395)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-01 21:40:59 +01:00
Koen Kanters 40bf14b93c fix(ignore): update zigbee-herdsman-converters to 25.59.0 (#29382) 2025-11-01 20:26:36 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b2c31f89c6 fix(ignore): bump @types/serve-static from 1.15.10 to 2.2.0 (#29394)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-01 20:50:11 +01:00
Koen Kanters faa65f78a2 fix(ignore): update zigbee-herdsman-converters to 25.57.0 (#29370) 2025-10-30 19:48:34 +00:00
Joseph Petersen c30630091d fix: Fix input onboarding environment variable disabled check (#29354) 2025-10-30 20:23:21 +01:00
Koen Kanters 61d99b7ece fix(ignore): update zigbee-herdsman-converters to 25.56.0 (#29338) 2025-10-28 20:23:32 +00:00
Koen Kanters e037a2c21c fix: Fix onEvent called twice first time when device options are changed (#29322) 2025-10-28 21:19:55 +01:00
Koen Kanters 592c256f96 fix(ignore): update zigbee-herdsman-converters to 25.55.0 (#29323) 2025-10-27 20:50:16 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 82f1092302 fix(ignore): bump the minor-patch group with 5 updates (#29314)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 20:58:01 +01:00
Koen Kanters 39e43d8f40 chore: merge master to dev 2025-10-27 20:43:36 +01:00
Koen Kanters 1ea7f650a2 chore: fix external converter/new device support bot 2025-10-27 20:43:11 +01:00
Koen Kanters 003c81b745 fix(ignore): update zigbee-herdsman-converters to 25.54.0 (#29306) 2025-10-26 21:28:39 +00:00
Koen Kanters 6c48498e0d fix: Require at least Node ^22.2.0 (#29285) 2025-10-25 20:39:14 +02:00
Koen Kanters e96630669a chore: merge master to dev 2025-10-25 20:33:06 +02:00
Koen Kanters 961265ad5e chore: extend external converter/new device bot to detected already support zigbee model 2025-10-25 20:32:39 +02:00
Koen Kanters 8ac2f55d87 fix: Require at least Node ^20.15.0 (#29284) 2025-10-25 20:14:58 +02:00
Koen Kanters 469d40c4aa chore: merge master to dev 2025-10-25 20:05:16 +02:00
Koen Kanters 5b844ea64e chore: fix external definition and new device support issue template render 2025-10-25 20:04:52 +02:00
Koen Kanters d719c1f368 fix(ignore): update zigbee-herdsman-converters to 25.53.0 (#29266) 2025-10-24 21:10:42 +00:00
Koen Kanters 0993652420 chore: merge master to dev 2025-10-23 21:01:31 +02:00
Koen Kanters 846b06ccab chore: update external converter template 2025-10-23 21:01:05 +02:00
Koen Kanters 3330d2aaed chore: merge master to dev 2025-10-23 20:52:40 +02:00
Koen Kanters bc650ccf82 chore: update external converter/new device support bot https://github.com/Nerivec/zigbee2mqtt-windfront/pull/298 2025-10-23 20:52:15 +02:00
Koen Kanters 968df11b16 chore: merge master to dev 2025-10-23 20:41:32 +02:00
Koen Kanters c4c129987d chore: Update and add new issue templates https://github.com/Nerivec/zigbee2mqtt-windfront/pull/298 2025-10-23 20:41:01 +02:00
Koen Kanters 05382a9692 fix(ignore): update zigbee-herdsman-converters to 25.52.0 (#29247) 2025-10-23 18:06:36 +00:00
Koen Kanters ef15924260 fix(ignore): update zigbee-herdsman to 6.3.2 (#29246) 2025-10-23 17:57:41 +00:00
Koen Kanters c0190c18ed fix: Home Assistant: fix device and group configuration URL (#29211) 2025-10-22 19:54:54 +02:00
Koen Kanters 238ea7c4c8 fix(ignore): update zigbee-herdsman-converters to 25.51.1 (#29212) 2025-10-21 19:28:14 +00:00
David Dreschner 174ba64449 fix: Home Assistant: make PI heating demand from writable (#29188) 2025-10-21 20:30:17 +02:00
Koen Kanters 408052282d fix(ignore): update zigbee-herdsman-converters to 25.51.0 (#29195) 2025-10-20 19:06:56 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d8fc69dac3 fix(ignore): bump @types/node from 24.7.2 to 24.8.1 in the minor-patch group (#29191)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 20:48:52 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0da5fe6837 chore(deps): bump actions/setup-node from 5 to 6 (#29190)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 20:48:40 +02:00
Koen Kanters df70f11a15 fix(ignore): update zigbee-herdsman-converters to 25.50.0 (#29180) 2025-10-19 19:18:25 +00:00
Koen Kanters 3ecb0c5e41 fix(ignore): update zigbee-herdsman-converters to 25.49.0 (#29154) 2025-10-18 12:46:47 +00:00
Koen Kanters 52d250b72f chore: add OIDC permission for tokenless publising 2025-10-18 14:38:34 +02:00
Koen Kanters feac6aa677 fix(ignore): update zigbee-herdsman to 6.3.1 (#29153) 2025-10-18 12:30:56 +00:00
Koen Kanters 1d69715a5a fix(ignore): update zigbee-herdsman-converters to 25.48.0 (#29141) 2025-10-17 18:09:35 +00:00
Koen Kanters eaaed1fec7 fix: Rename ZigBee -> Zigbee (#29131) 2025-10-16 22:40:39 +02:00
Koen Kanters b81dcdbf40 chore: merge master to dev 2025-10-16 22:19:39 +02:00
Koen Kanters 129a80d1bb chore: update device support bot 2025-10-16 22:19:23 +02:00
Koen Kanters 03a81c6ab1 fix(ignore): update zigbee-herdsman-converters to 25.47.0 (#29130) 2025-10-16 19:00:35 +00:00
Koen Kanters 799f71b9b4 chore: merge master to dev 2025-10-16 20:47:32 +02:00
Koen Kanters 38d305eee6 chore: add new device support bot 2025-10-16 20:47:04 +02:00
Koen Kanters 354d06aebc fix(ignore): update zigbee-herdsman to 6.3.0 (#29113) 2025-10-15 19:49:31 +00:00
Koen KantersandDavid Dreschner ad9b114b1a fix(ignore): update zigbee-herdsman-converters to 25.46.0 (#29096)
Co-authored-by: David Dreschner <34408442+DerDreschner@users.noreply.github.com>
2025-10-15 19:20:31 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> aa916ad604 fix(ignore): bump the minor-patch group with 3 updates (#29068)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-14 22:01:32 +02:00
Koen Kanters bafc8e4284 fix(ignore): update zigbee-herdsman-converters to 25.44.0 (#29050) 2025-10-12 19:26:32 +00:00
Koen Kanters 9c19aa9494 fix(ignore): update zigbee-herdsman-converters to 25.43.0 (#29014) 2025-10-11 12:48:52 +00:00
Koen Kanters 85c894dc4f chore: Update AGENTS.md and copilot-instructions.md (#28966) 2025-10-08 22:11:10 +02:00
Koen Kanters 5f893c5657 chore: merge master to dev 2025-10-08 21:32:27 +02:00
Koen Kanters 8865429d79 chore: update wrong device template 2025-10-08 21:32:08 +02:00
Koen Kanters 1a77675043 chore: merge master to dev 2025-10-08 21:30:43 +02:00
Koen Kanters 8018422823 chore: update wrong device template 2025-10-08 21:30:22 +02:00
Koen Kanters 8b1f5a555c fix(ignore): update zigbee-herdsman-converters to 25.42.0 (#28963) 2025-10-08 19:07:11 +00:00
Koen Kanters 79a27e3b06 fix(ignore): update zigbee-herdsman-converters to 25.41.0 (#28945) 2025-10-07 20:30:50 +00:00
Koen Kanters 5f08c3e83b fix(ignore): update zigbee-herdsman-converters to 25.40.0 (#28928) 2025-10-06 18:48:38 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Nerivec
e24ec79980 fix(ignore): bump the minor-patch group with 4 updates (#28916)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2025-10-06 20:10:21 +02:00
Nerivec d18dbb360d chore: add copilot prompts (#28919) 2025-10-06 20:09:58 +02:00
Koen Kanters 74335fbb68 fix: Remove Moment.js dependency (#28797) 2025-10-06 20:06:41 +02:00
Koen Kanters 8b4a4be2a4 fix(ignore): update zigbee-herdsman to 6.2.0 (#28900) 2025-10-05 19:56:17 +00:00
Koen Kanters 548f646f2f fix(ignore): update zigbee-herdsman-converters to 25.39.0 (#28899) 2025-10-05 19:28:13 +00:00
Koen Kanters a75a40b042 fix(ignore): update zigbee-herdsman-converters to 25.38.0 (#28872) 2025-10-04 12:42:17 +00:00
Dirk van Donkelaar 09fd675d0f fix: Add pi cooling demand to Home Assistant auto discovery (#28843) 2025-10-04 14:07:26 +02:00
Koen Kanters 1d38da6ac5 fix(ignore): update zigbee-herdsman-converters to 25.37.2 (#28859) 2025-10-03 19:18:52 +00:00
Koen Kanters c66008b36b fix(ignore): update zigbee-herdsman-converters to 25.37.1 (#28857) 2025-10-03 19:00:53 +00:00
Dirk van Donkelaar ab82f0e9a8 chore: Fix test command in CONTRIBUTING.md (#28844) 2025-10-03 19:47:43 +02:00
Koen Kanters d1ce146542 fix(ignore): update zigbee-herdsman-converters to 25.37.0 (#28837) 2025-10-02 19:34:49 +00:00
Koen Kanters cf93c649aa chore: vitest remove globals 2025-10-01 20:30:11 +02:00
Koen Kanters ca489daad8 fix(ignore): update zigbee-herdsman-converters to 25.36.0 (#28790) 2025-10-01 18:26:29 +00:00
github-actions[bot] 16f3f4d649 chore: promote to dev 2025-10-01 18:00:16 +00:00
102 changed files with 9159 additions and 2948 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"]
}
}
}
@@ -0,0 +1,55 @@
name: External converter
description: Submit an external converter
title: '[External Converter]: '
labels: [external converter]
body:
- type: markdown
attributes:
value: |
**IMPORTANT:** In case the external converter fully works, please consider submitting a [pull request](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html#_4-create-a-pull-request) instead.
- type: input
id: link
attributes:
label: Link
description: Link of this device (product page)
placeholder: https://www.linktomydevice.org
validations:
required: true
- type: input
id: database
attributes:
label: Database entry
description: Entry of this device in `data/database.db` after pairing it
placeholder: '{"id":53,"type":"Router","ieeeAddr":"0x10458d00024284f69","nwkAddr":10148,"manufId":4151,"manufName":"LUMI","powerSource":"DC Source","modelId":"lumi.relay.c2acn01","epList":[1,2],"endpoints":{"1":{"profId":260,"epId":1,"devId":257,"inClusterList":[0,3,4,5,1,2,10,6,16,2820,12],"outClusterList":[25,10],"clusters":{"genBasic":{"attributes":{"modelId":"lumi.relay.c2acn01","appVersion":1,"manufacturerName":"LUMI","powerSource":4,"zclVersion":0,"stackVersion":2,"hwVersion":18,"dateCode":"8-6-2020"}},"genAnalogInput":{"attributes":{"presentValue":129.04425048828125}},"genOnOff":{"attributes":{"61440":117440715,"onOff":1}}},"binds":[],"configuredReportings":[],"meta":{}},"2":{"profId":260,"epId":2,"devId":257,"inClusterList":[6,16,4,5],"outClusterList":[],"clusters":{"genOnOff":{"attributes":{"61440":237478966,"onOff":0}}},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":1,"stackVersion":2,"hwVersion":18,"dateCode":"8-6-2020","zclVersion":0,"interviewCompleted":true,"meta":{},"lastSeen":1640285631405}'
validations:
required: true
- type: input
id: z2m_version
attributes:
label: Zigbee2MQTT version
description: Can be found in the frontend -> settings -> about -> Zigbee2MQTT version. Are you running Zigbee2MQTT 1.18.1? Then read [this](https://github.com/Koenkk/zigbee2mqtt/releases/tag/1.19.0).
placeholder: '1.22.1'
validations:
required: true
- type: textarea
id: external_definition
attributes:
label: External converter
description: Code of the external converter, to retrieve in the Z2M frontend go to Settings -> Dev Console -> External Converters
render: JavaScript
validations:
required: true
- type: textarea
attributes:
label: What does/doesn't work with the external definition?
description: Indicate what works and what doesn't work with the external definition.
validations:
required: true
- type: textarea
id: notes
attributes:
label: Notes
description: Any additional information or context that might be helpful
placeholder: Add any relevant notes or context here
validations:
required: false
+10 -8
View File
@@ -36,17 +36,11 @@ body:
validations:
required: true
- type: textarea
id: notes
attributes:
label: Comments
placeholder: I tried to follow the supporting new device page but got stuck at...
validations:
required: true
- type: textarea
id: external_definition
attributes:
label: External definition
description: See [Creating the external definition](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html#_2-creating-the-external-definition)
render: shell
render: JavaScript
validations:
required: true
- type: textarea
@@ -55,3 +49,11 @@ body:
description: Indicate what works and what doesn't work with the external definition.
validations:
required: true
- type: textarea
id: notes
attributes:
label: Notes
description: Any additional information or context that might be helpful
placeholder: Add any relevant notes or context here
validations:
required: false
@@ -83,3 +83,11 @@ body:
placeholder: Click here and drag the file into it or click on "Attach files by.." below
validations:
required: false
- type: textarea
id: notes
attributes:
label: Notes
description: Any additional information or context that might be helpful
placeholder: Add any relevant notes or context here
validations:
required: false
+4 -1
View File
@@ -6,7 +6,10 @@ body:
- type: markdown
attributes:
value: |
Only use this if the device is detected as **supported** and is **fully functional** but has a wrong picture, vendor, model or description.
Submit this issue only if your device is detected as **supported** and works **fully**, but displays an incorrect picture, vendor, model, or description.
**IMPORTANT**: Before opening this issue, please review the [guide](https://www.zigbee2mqtt.io/advanced/support-new-devices/02_support_new_tuya_devices.html#fixing-tuya-device-detection) for instructions on fixing the device detection yourself.
This usually only takes 5 minutes.
- type: input
id: link
attributes:
+450 -34
View File
@@ -1,43 +1,459 @@
# Copilot Coding Agent Instructions for Koenkk/zigbee2mqtt
# GitHub Copilot Instructions
Welcome! These instructions help ensure Copilot Coding Agent can efficiently collaborate on this repository.
## Priority Guidelines
## 1. Codebase Overview
- **Main language:** JavaScript (Node.js), TypeScript
- **Purpose:** Zigbee to MQTT bridge for home automation.
- **Core directories:**
- `src/`: Main source code (device adapters, communication, logic).
- `test/`: Automated tests.
- `docs/`: Documentation.
- `data/`: Configuration and database
When generating code for this repository:
## 2. Preferred Practices
- **Branching:** Use feature branches (`feat/xyz`) for enhancements and `fix/xyz` for bug fixes.
- **Commits:** Write clear, descriptive commit messages (imperative mood, < 72 chars).
- **Pull Requests:** Reference related issues, provide context, and include before/after behavior if modifying logic. Tag with appropriate labels.
- **Testing:** All code changes should include or update relevant tests. A test coverage of 100% is enforced. Run `npm test:coverage` before submitting PRs.
- **Linting:** Code must pass lint and formatting checks (`npm run check`). Use Biome for formatting.
- **Documentation:** Update relevant docs when adding features or changing behavior.
1. **Version Compatibility**: Always detect and respect the exact versions of languages, frameworks, and libraries used in this project
2. **Context Files**: Prioritize patterns and standards defined in the .github/copilot directory
3. **Codebase Patterns**: When context files don't provide specific guidance, scan the codebase for established patterns
4. **Architectural Consistency**: Maintain our layered architectural style with clear separation between controller, extensions, models, and utilities
5. **Code Quality**: Prioritize maintainability, performance, security, and testability in all generated code
## 3. Review & Feedback
- All PRs require review by maintainers.
- Automated checks must pass before merging.
- If youre fixing a bug, include steps to reproduce in the PR description.
## Technology Stack
## 4. Security & Secrets
- Do not commit secrets, credentials, or private keys.
- Follow the repositorys security policy for vulnerability disclosures.
### Core Technologies
- **Language**: TypeScript 5.9.3 with target `esnext` and module `NodeNext`
- **Runtime**: Node.js ^20 || ^22 || ^24
- **Package Manager**: pnpm 10.12.1
- **Testing**: Vitest 3.1.1 with @vitest/coverage-v8
- **Linting/Formatting**: Biome 2.2.5 (configured with 4-space indents, 150 line width, no bracket spacing)
## 5. Communication
- Use Discussions and Issues for questions and proposals.
- Respect community guidelines and code of conduct.
### Key Dependencies
- **zigbee-herdsman**: 6.2.0 (exact version - critical for Zigbee protocol compatibility)
- **zigbee-herdsman-converters**: 25.42.0 (exact version - device definitions)
- **MQTT**: mqtt 5.14.1
- **Logging**: winston 3.18.3
- **YAML**: js-yaml 4.1.0
- **Decorators**: bind-decorator 1.0.11
- **WebSocket**: ws 8.18.1
## 6. Special Instructions for Copilot Coding Agent
- Suggest code changes that strictly adhere to existing styles and patterns.
- Explain reasoning in PR descriptions when implementing complex changes.
- Prioritize backward compatibility unless otherwise specified.
- If uncertain, prompt for clarification via PR comment before proceeding.
### TypeScript Configuration
- **Strict Mode**: Enabled with `noImplicitAny` and `noImplicitThis`
- **Module System**: NodeNext with ESM interop
- **Decorators**: Experimental decorators enabled
- **Composite**: True (for project references)
- **Source Maps**: Inline source maps enabled
- **Output**: Compiled to `dist/` directory
---
## Project Architecture
For more details, see [Best practices for Copilot coding agent in your repository](https://gh.io/copilot-coding-agent-tips) and the repository's CONTRIBUTING.md.
### Directory Structure
```
lib/ # Source TypeScript files
├── controller.ts # Main controller orchestrating all components
├── mqtt.ts # MQTT client management
├── zigbee.ts # Zigbee network management
├── state.ts # State management
├── eventBus.ts # Event-driven communication
├── extension/ # Extension system (plugins)
│ ├── extension.ts # Abstract base class
│ ├── availability.ts
│ ├── bind.ts
│ ├── bridge.ts
│ ├── configure.ts
│ └── ...
├── model/ # Domain models
│ ├── device.ts
│ └── group.ts
├── util/ # Utility functions
│ ├── logger.ts
│ ├── settings.ts
│ ├── utils.ts
│ └── ...
└── types/ # TypeScript type definitions
└── api.ts
test/ # Vitest test files
data/ # Runtime configuration and data
```
### Architectural Patterns
#### Extension Pattern
All extensions inherit from the abstract `Extension` base class:
```typescript
abstract class Extension {
protected zigbee: Zigbee;
protected mqtt: Mqtt;
protected state: State;
protected publishEntityState: PublishEntityState;
protected eventBus: EventBus;
async start(): Promise<void> {}
async stop(): Promise<void> {}
}
```
#### Event-Driven Architecture
Use the `EventBus` for component communication. Events are strongly typed:
```typescript
interface EventBusMap {
deviceMessage: [data: eventdata.DeviceMessage];
mqttMessage: [data: eventdata.MQTTMessage];
publishEntityState: [data: eventdata.PublishEntityState];
// ... other events
}
```
#### Dependency Injection
The `Controller` class instantiates and injects dependencies into extensions. Follow this pattern when creating new extensions.
## Code Style and Conventions
### Naming Conventions
- **Classes**: PascalCase (e.g., `Extension`, `Device`, `EventBus`)
- **Interfaces/Types**: PascalCase (e.g., `MqttPublishOptions`, `DeviceOptions`)
- **Functions/Methods**: camelCase (e.g., `publishEntityState`, `enableDisableExtension`)
- **Constants**: SCREAMING_SNAKE_CASE for top-level constants (e.g., `CURRENT_VERSION`, `LOG_LEVELS`)
- **Private members**: Prefix with underscore for private class fields only when needed to distinguish from public properties (e.g., `_definitionModelID`)
- **Files**: camelCase for TypeScript files (e.g., `eventBus.ts`, `externalJS.ts`)
### Import Organization
Follow this import order (separated by blank lines):
1. Node.js built-in modules (use `node:` prefix: `import fs from "node:fs"`)
2. Third-party libraries (e.g., `bind-decorator`, `mqtt`)
3. Type-only imports from external packages (using `type` keyword)
4. Internal absolute imports from project root
5. Type-only imports from internal modules
Example:
```typescript
import fs from "node:fs";
import bind from "bind-decorator";
import type {IClientOptions} from "mqtt";
import {connectAsync} from "mqtt";
import type {Zigbee2MQTTAPI} from "./types/api";
import logger from "./util/logger";
import * as settings from "./util/settings";
```
### Type Annotations
- Use `type` imports for TypeScript types: `import type * as zhc from "zigbee-herdsman-converters"`
- Explicitly type function parameters and return types
- Use `KeyValue` type for generic object payloads: `type KeyValue = Record<string, any>`
- Prefer interfaces for object shapes, type aliases for unions/intersections
- Use namespace exports for related types: `export type * as ZSpec from "zigbee-herdsman/dist/zspec"`
### Async/Await Patterns
- Always use `async/await` for asynchronous operations
- Return types should be explicitly `Promise<Type>`
- Methods that don't return values should be `Promise<void>`
- Use `Awaited<ReturnType<typeof fn>>` for inferring async function return types
### Decorators
Use `@bind` decorator from `bind-decorator` for methods that need `this` binding:
```typescript
@bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
// Implementation
}
```
### Error Handling
- Use `throw new Error("message")` for explicit errors
- Include descriptive error messages
- Log errors using the logger: `logger.error("message")`
- For Zigbee-herdsman errors, log the stack trace: `logger.error((error as Error).stack!)`
- Catch and handle errors at appropriate boundaries (controller level)
### Logging
Use the centralized logger (winston-based):
```typescript
import logger from "./util/logger";
logger.info("message");
logger.warning("message");
logger.error("message");
logger.debug("message");
```
- Use namespaced loggers for specific modules (created internally by logger)
- Log levels: `error`, `warning`, `info`, `debug` (from most to least critical)
- Include relevant context in log messages (device names, IEEE addresses, etc.)
## Code Quality Standards
### Maintainability
- Write self-documenting code with clear, descriptive names
- Keep methods focused on single responsibilities
- Abstract classes should define clear contracts with protected members for subclasses
- Use constructor dependency injection for required dependencies
- Limit function complexity - methods should be concise and focused
- Use TypeScript's strict mode features (`noImplicitAny`, `noImplicitThis`)
### Performance
- 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 util for consistent object serialization
- Cache computed values when appropriate (see device model patterns)
- Use getter methods for computed properties that should be cached
### Security
- Validate input using Ajv JSON schema validation (see `settings.ts` pattern)
- Sanitize file paths using `path.join` from Node.js
- Use YAML safe loading: `yaml.safeLoad()`
- Handle sensitive data (credentials, tokens) through settings with proper defaults
- Never log sensitive information (passwords, tokens)
### Testability
- Write tests using Vitest with describe/it/expect patterns
- Mock external dependencies using Vitest's `vi.mock()`
- Use `beforeEach`, `afterEach`, `beforeAll`, `afterAll` for test setup/teardown
- Place test files in `test/` directory with `.test.ts` extension
- Mock constructors and modules in the pattern shown in `test/controller.test.ts`
- Use `flushPromises()` utility for async test synchronization
- Target 100% code coverage (configured in vitest.config.mts)
## Testing Standards
### Unit Testing Structure
```typescript
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
describe("ComponentName", () => {
beforeEach(() => {
// Setup
});
it("Should do something specific", async () => {
// Arrange
const input = {};
// Act
const result = await someFunction(input);
// Assert
expect(result).toBe(expected);
});
});
```
### Mocking Patterns
- Create mock modules in `test/mocks/` directory
- Use `vi.fn()` for function mocks
- Use `vi.mock()` for module mocks
- Clear mocks in `afterEach` or between tests
- Mock external libraries like `mqtt`, `zigbee-herdsman` consistently
### Test Coverage
- All code in `lib/**` should be covered
- Use coverage reports: `pnpm test:coverage`
- Thresholds set to 100% (can be adjusted per project needs)
- Tests should cover both success and failure paths
## Documentation Standards
### JSDoc Comments
Use JSDoc-style comments for classes and public methods:
```typescript
/**
* Besides initializing variables, the constructor should do nothing!
*
* @param {Zigbee} zigbee Zigbee controller
* @param {Mqtt} mqtt MQTT controller
* @param {State} state State controller
* @param {Function} publishEntityState Method to publish device state to MQTT.
* @param {EventBus} eventBus The event bus
*/
constructor(zigbee: Zigbee, mqtt: Mqtt, state: State, ...) {
```
### Comment Style
- Use single-line comments (`//`) for implementation notes
- Use JSDoc (`/** */`) for public APIs and class/method documentation
- Include context for non-obvious logic
- Document parameters with their types and purposes
- Use `@param` tags with TypeScript types in braces
- Use biome-ignore comments when necessary: `// biome-ignore lint/rule: reason`
### Code Documentation
- Document complex algorithms or business logic
- Explain "why" not just "what" when logic is non-trivial
- Include links to relevant issues or documentation when applicable
- Document deprecations and breaking changes
## TypeScript-Specific Guidelines
### Module System
- Use ES modules with `import`/`export` syntax
- Default exports for main classes: `export default class Device {}`
- Named exports for utilities and types: `export const LOG_LEVELS = ...`
- Namespace exports for related types: `export type * as ZSpec from ...`
- Use `.js` extension in imports for local modules when using dynamic imports: `await import("./extension/frontend.js")`
### Type Safety
- Enable all strict type checking options
- Use type guards and assertions when necessary: `asserts expose is zhc.Numeric`
- Prefer `unknown` over `any` when type is truly unknown
- Use `// biome-ignore lint/suspicious/noExplicitAny: API` when `any` is necessary
- Define proper interfaces for external module types (e.g., `unix-dgram.d.ts`)
### Generic Types
- Use generics for reusable, type-safe abstractions
- Example: `abstract class ExternalJSExtension<M> extends Extension`
- Constrain generics when appropriate
- Document generic type parameters
### Utility Types
- Use built-in utility types: `Partial`, `Required`, `Pick`, `Omit`, `Record`
- Use `Awaited<ReturnType<typeof fn>>` for async function return types
- Define custom utility types when patterns emerge
- Use `type` for aliases, `interface` for object shapes
## Version Control and Releases
### Versioning Strategy
- Follow Semantic Versioning (MAJOR.MINOR.PATCH)
- Current version managed in `package.json`
- Use `-dev` suffix for development versions (e.g., `2.6.2-dev`)
- Configuration version tracked separately: `CURRENT_VERSION = 4`
### Changelog
- Maintain CHANGELOG.md with all changes
- Group changes by type: Bug Fixes, Features, Breaking Changes
- Include issue/PR references: `([#28583](url))`
- Include commit references: `([09f33b3](url))`
- Use conventional commits format
### Git Workflow
- Development on `dev` branch
- Production releases from `master` branch
- Use meaningful commit messages
- Reference issues in commits
## Project-Specific Patterns
### Settings Management
- All configuration loaded through `util/settings.ts`
- Validate settings using Ajv with JSON schema
- Schema defined in `settings.schema.json`
- Support runtime setting changes with restart detection
- Use `settings.get()` to access current configuration
- Use `settings.getDevice(ieeeAddr)` for device-specific config
### Device and Group Models
- Devices and groups are domain models wrapping `zigbee-herdsman` entities
- Access underlying entity via `.zh` property
- Expose computed properties as getters
- Include definition from `zigbee-herdsman-converters`
- Handle coordinator devices specially (type checking)
### MQTT Integration
- MQTT client wrapped in `Mqtt` class
- Publish options: `retain`, `qos` properties
- Topics follow pattern: `{base_topic}/{device}/{attribute}`
- Event-based message handling via EventBus
- Clean disconnect handling with retry logic
### Extension System
- Extensions are loosely coupled plugins
- Lifecycle: constructor → start() → stop()
- Constructor should only assign properties (no side effects)
- Use EventBus for inter-extension communication
- Extensions can be enabled/disabled at runtime
- External extensions loaded from `data/external_extensions/`
### State Management
- State persisted to `state.json`
- Cached in memory for performance
- Device states include all exposed attributes
- State changes trigger events via EventBus
## Best Practices Specific to This Project
1. **Never use language features beyond TypeScript 5.9.3 or ES2024**
2. **Always respect exact versions of zigbee-herdsman and zigbee-herdsman-converters** - these are critical for device compatibility
3. **Use the EventBus for all component communication** - avoid direct coupling
4. **Follow the Extension pattern for new features** - don't add logic directly to Controller
5. **Log appropriately** - info for user-relevant events, debug for developer info, error for failures
6. **Test with real Zigbee scenarios** - many edge cases exist with different device types
7. **Handle coordinator specially** - coordinator is a device but with unique behavior
8. **Validate all external input** - MQTT messages, configuration files, device data
9. **Use the bind decorator** for event handlers to preserve `this` context
10. **Match the exact code formatting** - Biome enforces 4 spaces, 150 line width, no bracket spacing
## Common Patterns to Follow
### Creating a New Extension
1. Extend `Extension` abstract class
2. Accept all dependencies in constructor
3. Implement `start()` method for initialization
4. Subscribe to EventBus events in `start()`
5. Implement `stop()` method for cleanup
6. Export as default: `export default class MyExtension extends Extension`
### Accessing Device Information
```typescript
const device: Device; // Our wrapper
device.ieeeAddr; // IEEE address
device.name; // Friendly name
device.zh; // Underlying zigbee-herdsman device
device.definition; // zigbee-herdsman-converters definition
device.options; // User configuration
```
### Publishing MQTT Messages
```typescript
await this.mqtt.publish(topic, message, {retain: true, qos: 0});
```
### Emitting Events
```typescript
this.eventBus.emit('deviceMessage', {device, message});
```
### Listening to Events
```typescript
this.eventBus.on('deviceMessage', this.onDeviceMessage, this);
```
## Integration Points
### Zigbee-Herdsman Integration
- Start controller: `await this.zigbee.start()`
- Access coordinator: `this.zigbee.coordinator()`
- Device operations through `zigbee-herdsman` API
- Event handling through EventBus wrappers
### MQTT Integration
- Connect: `await this.mqtt.connect()`
- Subscribe: `await this.mqtt.subscribe(topic)`
- Publish: `await this.mqtt.publish(topic, message, options)`
- Handle messages via EventBus `mqttMessage` event
### Frontend Integration
- Optional extension loaded dynamically
- Serves static files with compression
- WebSocket support for real-time updates
- Configurable port and base URL
### Home Assistant Integration
- Optional extension for discovery
- Publishes discovery messages to MQTT
- Supports entities, sensors, and devices
- Configurable discovery topic
## Critical Compatibility Notes
1. **Node.js**: Only versions 20, 22, and 24 are supported
2. **TypeScript**: Features must be compatible with 5.9.3
3. **Zigbee Libraries**: Exact versions are critical - do not suggest upgrades without testing
4. **MQTT Protocol**: Uses MQTT 3.1.1 and 5.0 features
5. **ES Modules**: Project uses ESM with NodeNext resolution
6. **Experimental Decorators**: Required for `@bind` decorator support
## When in Doubt
1. **Search for similar patterns** in the existing codebase
2. **Check existing extensions** for implementation examples
3. **Follow the controller and extension architecture** - don't bypass it
4. **Consult the test files** for usage examples
5. **Match the exact style** - run `pnpm check` to verify
6. **Prioritize consistency** over external best practices
7. **Test thoroughly** - this project controls real hardware
## Resources
- Repository: https://github.com/Koenkk/zigbee2mqtt
- Documentation: https://koenkk.github.io/zigbee2mqtt
- License: GPL-3.0
- Issue Tracker: https://github.com/Koenkk/zigbee2mqtt/issues
+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
@@ -0,0 +1,294 @@
---
description: 'Technology-agnostic blueprint generator for creating comprehensive copilot-instructions.md files that guide GitHub Copilot to produce code consistent with project standards, architecture patterns, and exact technology versions by analyzing existing codebase patterns and avoiding assumptions.'
mode: 'agent'
---
# Copilot Instructions Blueprint Generator
## Configuration Variables
${PROJECT_TYPE="Auto-detect|.NET|Java|JavaScript|TypeScript|React|Angular|Python|Multiple|Other"} <!-- Primary technology -->
${ARCHITECTURE_STYLE="Layered|Microservices|Monolithic|Domain-Driven|Event-Driven|Serverless|Mixed"} <!-- Architectural approach -->
${CODE_QUALITY_FOCUS="Maintainability|Performance|Security|Accessibility|Testability|All"} <!-- Quality priorities -->
${DOCUMENTATION_LEVEL="Minimal|Standard|Comprehensive"} <!-- Documentation requirements -->
${TESTING_REQUIREMENTS="Unit|Integration|E2E|TDD|BDD|All"} <!-- Testing approach -->
${VERSIONING="Semantic|CalVer|Custom"} <!-- Versioning approach -->
## Generated Prompt
"Generate a comprehensive copilot-instructions.md file that will guide GitHub Copilot to produce code consistent with our project's standards, architecture, and technology versions. The instructions must be strictly based on actual code patterns in our codebase and avoid making any assumptions. Follow this approach:
### 1. Core Instruction Structure
```markdown
# GitHub Copilot Instructions
## Priority Guidelines
When generating code for this repository:
1. **Version Compatibility**: Always detect and respect the exact versions of languages, frameworks, and libraries used in this project
2. **Context Files**: Prioritize patterns and standards defined in the .github/copilot directory
3. **Codebase Patterns**: When context files don't provide specific guidance, scan the codebase for established patterns
4. **Architectural Consistency**: Maintain our ${ARCHITECTURE_STYLE} architectural style and established boundaries
5. **Code Quality**: Prioritize ${CODE_QUALITY_FOCUS == "All" ? "maintainability, performance, security, accessibility, and testability" : CODE_QUALITY_FOCUS} in all generated code
## Technology Version Detection
Before generating code, scan the codebase to identify:
1. **Language Versions**: Detect the exact versions of programming languages in use
- Examine project files, configuration files, and package managers
- Look for language-specific version indicators (e.g., <LangVersion> in .NET projects)
- Never use language features beyond the detected version
2. **Framework Versions**: Identify the exact versions of all frameworks
- Check package.json, .csproj, pom.xml, requirements.txt, etc.
- Respect version constraints when generating code
- Never suggest features not available in the detected framework versions
3. **Library Versions**: Note the exact versions of key libraries and dependencies
- Generate code compatible with these specific versions
- Never use APIs or features not available in the detected versions
## Context Files
Prioritize the following files in .github/copilot directory (if they exist):
- **architecture.md**: System architecture guidelines
- **tech-stack.md**: Technology versions and framework details
- **coding-standards.md**: Code style and formatting standards
- **folder-structure.md**: Project organization guidelines
- **exemplars.md**: Exemplary code patterns to follow
## Codebase Scanning Instructions
When context files don't provide specific guidance:
1. Identify similar files to the one being modified or created
2. Analyze patterns for:
- Naming conventions
- Code organization
- Error handling
- Logging approaches
- Documentation style
- Testing patterns
3. Follow the most consistent patterns found in the codebase
4. When conflicting patterns exist, prioritize patterns in newer files or files with higher test coverage
5. Never introduce patterns not found in the existing codebase
## Code Quality Standards
${CODE_QUALITY_FOCUS.includes("Maintainability") || CODE_QUALITY_FOCUS == "All" ? `### Maintainability
- Write self-documenting code with clear naming
- Follow the naming and organization conventions evident in the codebase
- Follow established patterns for consistency
- Keep functions focused on single responsibilities
- Limit function complexity and length to match existing patterns` : ""}
${CODE_QUALITY_FOCUS.includes("Performance") || CODE_QUALITY_FOCUS == "All" ? `### Performance
- Follow existing patterns for memory and resource management
- Match existing patterns for handling computationally expensive operations
- Follow established patterns for asynchronous operations
- Apply caching consistently with existing patterns
- Optimize according to patterns evident in the codebase` : ""}
${CODE_QUALITY_FOCUS.includes("Security") || CODE_QUALITY_FOCUS == "All" ? `### Security
- Follow existing patterns for input validation
- Apply the same sanitization techniques used in the codebase
- Use parameterized queries matching existing patterns
- Follow established authentication and authorization patterns
- Handle sensitive data according to existing patterns` : ""}
${CODE_QUALITY_FOCUS.includes("Accessibility") || CODE_QUALITY_FOCUS == "All" ? `### Accessibility
- Follow existing accessibility patterns in the codebase
- Match ARIA attribute usage with existing components
- Maintain keyboard navigation support consistent with existing code
- Follow established patterns for color and contrast
- Apply text alternative patterns consistent with the codebase` : ""}
${CODE_QUALITY_FOCUS.includes("Testability") || CODE_QUALITY_FOCUS == "All" ? `### Testability
- Follow established patterns for testable code
- Match dependency injection approaches used in the codebase
- Apply the same patterns for managing dependencies
- Follow established mocking and test double patterns
- Match the testing style used in existing tests` : ""}
## Documentation Requirements
${DOCUMENTATION_LEVEL == "Minimal" ?
`- Match the level and style of comments found in existing code
- Document according to patterns observed in the codebase
- Follow existing patterns for documenting non-obvious behavior
- Use the same format for parameter descriptions as existing code` : ""}
${DOCUMENTATION_LEVEL == "Standard" ?
`- Follow the exact documentation format found in the codebase
- Match the XML/JSDoc style and completeness of existing comments
- Document parameters, returns, and exceptions in the same style
- Follow existing patterns for usage examples
- Match class-level documentation style and content` : ""}
${DOCUMENTATION_LEVEL == "Comprehensive" ?
`- Follow the most detailed documentation patterns found in the codebase
- Match the style and completeness of the best-documented code
- Document exactly as the most thoroughly documented files do
- Follow existing patterns for linking documentation
- Match the level of detail in explanations of design decisions` : ""}
## Testing Approach
${TESTING_REQUIREMENTS.includes("Unit") || TESTING_REQUIREMENTS == "All" ?
`### Unit Testing
- Match the exact structure and style of existing unit tests
- Follow the same naming conventions for test classes and methods
- Use the same assertion patterns found in existing tests
- Apply the same mocking approach used in the codebase
- Follow existing patterns for test isolation` : ""}
${TESTING_REQUIREMENTS.includes("Integration") || TESTING_REQUIREMENTS == "All" ?
`### Integration Testing
- Follow the same integration test patterns found in the codebase
- Match existing patterns for test data setup and teardown
- Use the same approach for testing component interactions
- Follow existing patterns for verifying system behavior` : ""}
${TESTING_REQUIREMENTS.includes("E2E") || TESTING_REQUIREMENTS == "All" ?
`### End-to-End Testing
- Match the existing E2E test structure and patterns
- Follow established patterns for UI testing
- Apply the same approach for verifying user journeys` : ""}
${TESTING_REQUIREMENTS.includes("TDD") || TESTING_REQUIREMENTS == "All" ?
`### Test-Driven Development
- Follow TDD patterns evident in the codebase
- Match the progression of test cases seen in existing code
- Apply the same refactoring patterns after tests pass` : ""}
${TESTING_REQUIREMENTS.includes("BDD") || TESTING_REQUIREMENTS == "All" ?
`### Behavior-Driven Development
- Match the existing Given-When-Then structure in tests
- Follow the same patterns for behavior descriptions
- Apply the same level of business focus in test cases` : ""}
## Technology-Specific Guidelines
${PROJECT_TYPE == ".NET" || PROJECT_TYPE == "Auto-detect" || PROJECT_TYPE == "Multiple" ? `### .NET Guidelines
- Detect and strictly adhere to the specific .NET version in use
- Use only C# language features compatible with the detected version
- Follow LINQ usage patterns exactly as they appear in the codebase
- Match async/await usage patterns from existing code
- Apply the same dependency injection approach used in the codebase
- Use the same collection types and patterns found in existing code` : ""}
${PROJECT_TYPE == "Java" || PROJECT_TYPE == "Auto-detect" || PROJECT_TYPE == "Multiple" ? `### Java Guidelines
- Detect and adhere to the specific Java version in use
- Follow the exact same design patterns found in the codebase
- Match exception handling patterns from existing code
- Use the same collection types and approaches found in the codebase
- Apply the dependency injection patterns evident in existing code` : ""}
${PROJECT_TYPE == "JavaScript" || PROJECT_TYPE == "TypeScript" || PROJECT_TYPE == "Auto-detect" || PROJECT_TYPE == "Multiple" ? `### JavaScript/TypeScript Guidelines
- Detect and adhere to the specific ECMAScript/TypeScript version in use
- Follow the same module import/export patterns found in the codebase
- Match TypeScript type definitions with existing patterns
- Use the same async patterns (promises, async/await) as existing code
- Follow error handling patterns from similar files` : ""}
${PROJECT_TYPE == "React" || PROJECT_TYPE == "Auto-detect" || PROJECT_TYPE == "Multiple" ? `### React Guidelines
- Detect and adhere to the specific React version in use
- Match component structure patterns from existing components
- Follow the same hooks and lifecycle patterns found in the codebase
- Apply the same state management approach used in existing components
- Match prop typing and validation patterns from existing code` : ""}
${PROJECT_TYPE == "Angular" || PROJECT_TYPE == "Auto-detect" || PROJECT_TYPE == "Multiple" ? `### Angular Guidelines
- Detect and adhere to the specific Angular version in use
- Follow the same component and module patterns found in the codebase
- Match decorator usage exactly as seen in existing code
- Apply the same RxJS patterns found in the codebase
- Follow existing patterns for component communication` : ""}
${PROJECT_TYPE == "Python" || PROJECT_TYPE == "Auto-detect" || PROJECT_TYPE == "Multiple" ? `### Python Guidelines
- Detect and adhere to the specific Python version in use
- Follow the same import organization found in existing modules
- Match type hinting approaches if used in the codebase
- Apply the same error handling patterns found in existing code
- Follow the same module organization patterns` : ""}
## Version Control Guidelines
${VERSIONING == "Semantic" ?
`- Follow Semantic Versioning patterns as applied in the codebase
- Match existing patterns for documenting breaking changes
- Follow the same approach for deprecation notices` : ""}
${VERSIONING == "CalVer" ?
`- Follow Calendar Versioning patterns as applied in the codebase
- Match existing patterns for documenting changes
- Follow the same approach for highlighting significant changes` : ""}
${VERSIONING == "Custom" ?
`- Match the exact versioning pattern observed in the codebase
- Follow the same changelog format used in existing documentation
- Apply the same tagging conventions used in the project` : ""}
## General Best Practices
- Follow naming conventions exactly as they appear in existing code
- Match code organization patterns from similar files
- Apply error handling consistent with existing patterns
- Follow the same approach to testing as seen in the codebase
- Match logging patterns from existing code
- Use the same approach to configuration as seen in the codebase
## Project-Specific Guidance
- Scan the codebase thoroughly before generating any code
- Respect existing architectural boundaries without exception
- Match the style and patterns of surrounding code
- When in doubt, prioritize consistency with existing code over external best practices
```
### 2. Codebase Analysis Instructions
To create the copilot-instructions.md file, first analyze the codebase to:
1. **Identify Exact Technology Versions**:
- ${PROJECT_TYPE == "Auto-detect" ? "Detect all programming languages, frameworks, and libraries by scanning file extensions and configuration files" : `Focus on ${PROJECT_TYPE} technologies`}
- Extract precise version information from project files, package.json, .csproj, etc.
- Document version constraints and compatibility requirements
2. **Understand Architecture**:
- Analyze folder structure and module organization
- Identify clear layer boundaries and component relationships
- Document communication patterns between components
3. **Document Code Patterns**:
- Catalog naming conventions for different code elements
- Note documentation styles and completeness
- Document error handling patterns
- Map testing approaches and coverage
4. **Note Quality Standards**:
- Identify performance optimization techniques actually used
- Document security practices implemented in the code
- Note accessibility features present (if applicable)
- Document code quality patterns evident in the codebase
### 3. Implementation Notes
The final copilot-instructions.md should:
- Be placed in the .github directory
- Reference only patterns and standards that exist in the codebase
- Include explicit version compatibility requirements
- Avoid prescribing any practices not evident in the code
- Provide concrete examples from the codebase
- Be comprehensive yet concise enough for Copilot to effectively use
Important: Only include guidance based on patterns actually observed in the codebase. Explicitly instruct Copilot to prioritize consistency with existing code over external best practices or newer language features.
"
## Expected Output
A comprehensive copilot-instructions.md file that will guide GitHub Copilot to produce code that is perfectly compatible with your existing technology versions and follows your established patterns and architecture.
+249
View File
@@ -0,0 +1,249 @@
---
description: "Prompt for generating an AGENTS.md file for a repository"
mode: "agent"
---
# Create highquality AGENTS.md file
You are a code agent. Your task is to create a complete, accurate AGENTS.md at the root of this repository that follows the public guidance at https://agents.md/.
AGENTS.md is an open format designed to provide coding agents with the context and instructions they need to work effectively on a project.
## What is AGENTS.md?
AGENTS.md is a Markdown file that serves as a "README for agents" - a dedicated, predictable place to provide context and instructions to help AI coding agents work on your project. It complements README.md by containing detailed technical context that coding agents need but might clutter a human-focused README.
## Key Principles
- **Agent-focused**: Contains detailed technical instructions for automated tools
- **Complements README.md**: Doesn't replace human documentation but adds agent-specific context
- **Standardized location**: Placed at repository root (or subproject roots for monorepos)
- **Open format**: Uses standard Markdown with flexible structure
- **Ecosystem compatibility**: Works across 20+ different AI coding tools and agents
## File Structure and Content Guidelines
### 1. Required Setup
- Create the file as `AGENTS.md` in the repository root
- Use standard Markdown formatting
- No required fields - flexible structure based on project needs
### 2. Essential Sections to Include
#### Project Overview
- Brief description of what the project does
- Architecture overview if complex
- Key technologies and frameworks used
#### Setup Commands
- Installation instructions
- Environment setup steps
- Dependency management commands
- Database setup if applicable
#### Development Workflow
- How to start development server
- Build commands
- Watch/hot-reload setup
- Package manager specifics (npm, pnpm, yarn, etc.)
#### Testing Instructions
- How to run tests (unit, integration, e2e)
- Test file locations and naming conventions
- Coverage requirements
- Specific test patterns or frameworks used
- How to run subset of tests or focus on specific areas
#### Code Style Guidelines
- Language-specific conventions
- Linting and formatting rules
- File organization patterns
- Naming conventions
- Import/export patterns
#### Build and Deployment
- Build commands and outputs
- Environment configurations
- Deployment steps and requirements
- CI/CD pipeline information
### 3. Optional but Recommended Sections
#### Security Considerations
- Security testing requirements
- Secrets management
- Authentication patterns
- Permission models
#### Monorepo Instructions (if applicable)
- How to work with multiple packages
- Cross-package dependencies
- Selective building/testing
- Package-specific commands
#### Pull Request Guidelines
- Title format requirements
- Required checks before submission
- Review process
- Commit message conventions
#### Debugging and Troubleshooting
- Common issues and solutions
- Logging patterns
- Debug configuration
- Performance considerations
## Example Template
Use this as a starting template and customize based on the specific project:
```markdown
# AGENTS.md
## Project Overview
[Brief description of the project, its purpose, and key technologies]
## Setup Commands
- Install dependencies: `[package manager] install`
- Start development server: `[command]`
- Build for production: `[command]`
## Development Workflow
- [Development server startup instructions]
- [Hot reload/watch mode information]
- [Environment variable setup]
## Testing Instructions
- Run all tests: `[command]`
- Run unit tests: `[command]`
- Run integration tests: `[command]`
- Test coverage: `[command]`
- [Specific testing patterns or requirements]
## Code Style
- [Language and framework conventions]
- [Linting rules and commands]
- [Formatting requirements]
- [File organization patterns]
## Build and Deployment
- [Build process details]
- [Output directories]
- [Environment-specific builds]
- [Deployment commands]
## Pull Request Guidelines
- Title format: [component] Brief description
- Required checks: `[lint command]`, `[test command]`
- [Review requirements]
## Additional Notes
- [Any project-specific context]
- [Common gotchas or troubleshooting tips]
- [Performance considerations]
```
## Working Example from agents.md
Here's a real example from the agents.md website:
```markdown
# Sample AGENTS.md file
## Dev environment tips
- Use `pnpm dlx turbo run where <project_name>` to jump to a package instead of scanning with `ls`.
- Run `pnpm install --filter <project_name>` to add the package to your workspace so Vite, ESLint, and TypeScript can see it.
- Use `pnpm create vite@latest <project_name> -- --template react-ts` to spin up a new React + Vite package with TypeScript checks ready.
- Check the name field inside each package's package.json to confirm the right name—skip the top-level one.
## Testing instructions
- Find the CI plan in the .github/workflows folder.
- Run `pnpm turbo run test --filter <project_name>` to run every check defined for that package.
- From the package root you can just call `pnpm test`. The commit should pass all tests before you merge.
- To focus on one step, add the Vitest pattern: `pnpm vitest run -t "<test name>"`.
- Fix any test or type errors until the whole suite is green.
- After moving files or changing imports, run `pnpm lint --filter <project_name>` to be sure ESLint and TypeScript rules still pass.
- Add or update tests for the code you change, even if nobody asked.
## PR instructions
- Title format: [<project_name>] <Title>
- Always run `pnpm lint` and `pnpm test` before committing.
```
## Implementation Steps
1. **Analyze the project structure** to understand:
- Programming languages and frameworks used
- Package managers and build tools
- Testing frameworks
- Project architecture (monorepo, single package, etc.)
2. **Identify key workflows** by examining:
- package.json scripts
- Makefile or other build files
- CI/CD configuration files
- Documentation files
3. **Create comprehensive sections** covering:
- All essential setup and development commands
- Testing strategies and commands
- Code style and conventions
- Build and deployment processes
4. **Include specific, actionable commands** that agents can execute directly
5. **Test the instructions** by ensuring all commands work as documented
6. **Keep it focused** on what agents need to know, not general project information
## Best Practices
- **Be specific**: Include exact commands, not vague descriptions
- **Use code blocks**: Wrap commands in backticks for clarity
- **Include context**: Explain why certain steps are needed
- **Stay current**: Update as the project evolves
- **Test commands**: Ensure all listed commands actually work
- **Consider nested files**: For monorepos, create AGENTS.md files in subprojects as needed
## Monorepo Considerations
For large monorepos:
- Place a main AGENTS.md at the repository root
- Create additional AGENTS.md files in subproject directories
- The closest AGENTS.md file takes precedence for any given location
- Include navigation tips between packages/projects
## Final Notes
- AGENTS.md works with 20+ AI coding tools including Cursor, Aider, Gemini CLI, and many others
- The format is intentionally flexible - adapt it to your project's needs
- Focus on actionable instructions that help agents understand and work with your codebase
- This is living documentation - update it as your project evolves
When creating the AGENTS.md file, prioritize clarity, completeness, and actionability. The goal is to give any coding agent enough context to effectively contribute to the project without requiring additional human guidance.
@@ -0,0 +1,127 @@
---
mode: 'agent'
description: 'Create a new specification file for the solution, optimized for Generative AI consumption.'
tools: ['changes', 'codebase', 'editFiles', 'extensions', 'fetch', 'githubRepo', 'openSimpleBrowser', 'problems', 'runTasks', 'search', 'searchResults', 'terminalLastCommand', 'terminalSelection', 'testFailure', 'usages', 'vscodeAPI']
---
# Create Specification
Your goal is to create a new specification file for `${input:SpecPurpose}`.
The specification file must define the requirements, constraints, and interfaces for the solution components in a manner that is clear, unambiguous, and structured for effective use by Generative AIs. Follow established documentation standards and ensure the content is machine-readable and self-contained.
## Best Practices for AI-Ready Specifications
- Use precise, explicit, and unambiguous language.
- Clearly distinguish between requirements, constraints, and recommendations.
- Use structured formatting (headings, lists, tables) for easy parsing.
- Avoid idioms, metaphors, or context-dependent references.
- Define all acronyms and domain-specific terms.
- Include examples and edge cases where applicable.
- Ensure the document is self-contained and does not rely on external context.
The specification should be saved in the [/spec/](/spec/) directory and named according to the following convention: `spec-[a-z0-9-]+.md`, where the name should be descriptive of the specification's content and starting with the highlevel purpose, which is one of [schema, tool, data, infrastructure, process, architecture, or design].
The specification file must be formatted in well formed Markdown.
Specification files must follow the template below, ensuring that all sections are filled out appropriately. The front matter for the markdown should be structured correctly as per the example following:
```md
---
title: [Concise Title Describing the Specification's Focus]
version: [Optional: e.g., 1.0, Date]
date_created: [YYYY-MM-DD]
last_updated: [Optional: YYYY-MM-DD]
owner: [Optional: Team/Individual responsible for this spec]
tags: [Optional: List of relevant tags or categories, e.g., `infrastructure`, `process`, `design`, `app` etc]
---
# Introduction
[A short concise introduction to the specification and the goal it is intended to achieve.]
## 1. Purpose & Scope
[Provide a clear, concise description of the specification's purpose and the scope of its application. State the intended audience and any assumptions.]
## 2. Definitions
[List and define all acronyms, abbreviations, and domain-specific terms used in this specification.]
## 3. Requirements, Constraints & Guidelines
[Explicitly list all requirements, constraints, rules, and guidelines. Use bullet points or tables for clarity.]
- **REQ-001**: Requirement 1
- **SEC-001**: Security Requirement 1
- **[3 LETTERS]-001**: Other Requirement 1
- **CON-001**: Constraint 1
- **GUD-001**: Guideline 1
- **PAT-001**: Pattern to follow 1
## 4. Interfaces & Data Contracts
[Describe the interfaces, APIs, data contracts, or integration points. Use tables or code blocks for schemas and examples.]
## 5. Acceptance Criteria
[Define clear, testable acceptance criteria for each requirement using Given-When-Then format where appropriate.]
- **AC-001**: Given [context], When [action], Then [expected outcome]
- **AC-002**: The system shall [specific behavior] when [condition]
- **AC-003**: [Additional acceptance criteria as needed]
## 6. Test Automation Strategy
[Define the testing approach, frameworks, and automation requirements.]
- **Test Levels**: Unit, Integration, End-to-End
- **Frameworks**: MSTest, FluentAssertions, Moq (for .NET applications)
- **Test Data Management**: [approach for test data creation and cleanup]
- **CI/CD Integration**: [automated testing in GitHub Actions pipelines]
- **Coverage Requirements**: [minimum code coverage thresholds]
- **Performance Testing**: [approach for load and performance testing]
## 7. Rationale & Context
[Explain the reasoning behind the requirements, constraints, and guidelines. Provide context for design decisions.]
## 8. Dependencies & External Integrations
[Define the external systems, services, and architectural dependencies required for this specification. Focus on **what** is needed rather than **how** it's implemented. Avoid specific package or library versions unless they represent architectural constraints.]
### External Systems
- **EXT-001**: [External system name] - [Purpose and integration type]
### Third-Party Services
- **SVC-001**: [Service name] - [Required capabilities and SLA requirements]
### Infrastructure Dependencies
- **INF-001**: [Infrastructure component] - [Requirements and constraints]
### Data Dependencies
- **DAT-001**: [External data source] - [Format, frequency, and access requirements]
### Technology Platform Dependencies
- **PLT-001**: [Platform/runtime requirement] - [Version constraints and rationale]
### Compliance Dependencies
- **COM-001**: [Regulatory or compliance requirement] - [Impact on implementation]
**Note**: This section should focus on architectural and business dependencies, not specific package implementations. For example, specify "OAuth 2.0 authentication library" rather than "Microsoft.AspNetCore.Authentication.JwtBearer v6.0.1".
## 9. Examples & Edge Cases
```code
// Code snippet or data example demonstrating the correct application of the guidelines, including edge cases
```
## 10. Validation Criteria
[List the criteria or tests that must be satisfied for compliance with this specification.]
## 11. Related Specifications / Further Reading
[Link to related spec 1]
[Link to relevant external documentation]
```
@@ -0,0 +1,15 @@
---
mode: 'agent'
description: 'Review and refactor code in your project according to defined instructions'
---
## Role
You're a senior expert software engineer with extensive experience in maintaining projects over a long time and ensuring clean code and best practices.
## Task
1. Take a deep breath, and review all coding guidelines instructions in `.github/instructions/*.md` and `.github/copilot-instructions.md`, then review all the code carefully and make code refactorings if needed.
2. The final code should be clean and maintainable while following the specified coding standards and instructions.
3. Do not split up the code, keep the existing files intact.
4. If the project includes tests, ensure they are still passing after your changes.
@@ -0,0 +1,127 @@
---
mode: 'agent'
description: 'Update an existing specification file for the solution, optimized for Generative AI consumption based on new requirements or updates to any existing code.'
tools: ['changes', 'codebase', 'editFiles', 'extensions', 'fetch', 'githubRepo', 'openSimpleBrowser', 'problems', 'runTasks', 'search', 'searchResults', 'terminalLastCommand', 'terminalSelection', 'testFailure', 'usages', 'vscodeAPI']
---
# Update Specification
Your goal is to update the existing specification file `${file}` based on new requirements or updates to any existing code.
The specification file must define the requirements, constraints, and interfaces for the solution components in a manner that is clear, unambiguous, and structured for effective use by Generative AIs. Follow established documentation standards and ensure the content is machine-readable and self-contained.
## Best Practices for AI-Ready Specifications
- Use precise, explicit, and unambiguous language.
- Clearly distinguish between requirements, constraints, and recommendations.
- Use structured formatting (headings, lists, tables) for easy parsing.
- Avoid idioms, metaphors, or context-dependent references.
- Define all acronyms and domain-specific terms.
- Include examples and edge cases where applicable.
- Ensure the document is self-contained and does not rely on external context.
The specification should be saved in the [/spec/](/spec/) directory and named according to the following convention: `[a-z0-9-]+.md`, where the name should be descriptive of the specification's content and starting with the highlevel purpose, which is one of [schema, tool, data, infrastructure, process, architecture, or design].
The specification file must be formatted in well formed Markdown.
Specification files must follow the template below, ensuring that all sections are filled out appropriately. The front matter for the markdown should be structured correctly as per the example following:
```md
---
title: [Concise Title Describing the Specification's Focus]
version: [Optional: e.g., 1.0, Date]
date_created: [YYYY-MM-DD]
last_updated: [Optional: YYYY-MM-DD]
owner: [Optional: Team/Individual responsible for this spec]
tags: [Optional: List of relevant tags or categories, e.g., `infrastructure`, `process`, `design`, `app` etc]
---
# Introduction
[A short concise introduction to the specification and the goal it is intended to achieve.]
## 1. Purpose & Scope
[Provide a clear, concise description of the specification's purpose and the scope of its application. State the intended audience and any assumptions.]
## 2. Definitions
[List and define all acronyms, abbreviations, and domain-specific terms used in this specification.]
## 3. Requirements, Constraints & Guidelines
[Explicitly list all requirements, constraints, rules, and guidelines. Use bullet points or tables for clarity.]
- **REQ-001**: Requirement 1
- **SEC-001**: Security Requirement 1
- **[3 LETTERS]-001**: Other Requirement 1
- **CON-001**: Constraint 1
- **GUD-001**: Guideline 1
- **PAT-001**: Pattern to follow 1
## 4. Interfaces & Data Contracts
[Describe the interfaces, APIs, data contracts, or integration points. Use tables or code blocks for schemas and examples.]
## 5. Acceptance Criteria
[Define clear, testable acceptance criteria for each requirement using Given-When-Then format where appropriate.]
- **AC-001**: Given [context], When [action], Then [expected outcome]
- **AC-002**: The system shall [specific behavior] when [condition]
- **AC-003**: [Additional acceptance criteria as needed]
## 6. Test Automation Strategy
[Define the testing approach, frameworks, and automation requirements.]
- **Test Levels**: Unit, Integration, End-to-End
- **Frameworks**: MSTest, FluentAssertions, Moq (for .NET applications)
- **Test Data Management**: [approach for test data creation and cleanup]
- **CI/CD Integration**: [automated testing in GitHub Actions pipelines]
- **Coverage Requirements**: [minimum code coverage thresholds]
- **Performance Testing**: [approach for load and performance testing]
## 7. Rationale & Context
[Explain the reasoning behind the requirements, constraints, and guidelines. Provide context for design decisions.]
## 8. Dependencies & External Integrations
[Define the external systems, services, and architectural dependencies required for this specification. Focus on **what** is needed rather than **how** it's implemented. Avoid specific package or library versions unless they represent architectural constraints.]
### External Systems
- **EXT-001**: [External system name] - [Purpose and integration type]
### Third-Party Services
- **SVC-001**: [Service name] - [Required capabilities and SLA requirements]
### Infrastructure Dependencies
- **INF-001**: [Infrastructure component] - [Requirements and constraints]
### Data Dependencies
- **DAT-001**: [External data source] - [Format, frequency, and access requirements]
### Technology Platform Dependencies
- **PLT-001**: [Platform/runtime requirement] - [Version constraints and rationale]
### Compliance Dependencies
- **COM-001**: [Regulatory or compliance requirement] - [Impact on implementation]
**Note**: This section should focus on architectural and business dependencies, not specific package implementations. For example, specify "OAuth 2.0 authentication library" rather than "Microsoft.AspNetCore.Authentication.JwtBearer v6.0.1".
## 9. Examples & Edge Cases
```code
// Code snippet or data example demonstrating the correct application of the guidelines, including edge cases
```
## 10. Validation Criteria
[List the criteria or tests that must be satisfied for compliance with this specification.]
## 11. Related Specifications / Further Reading
[Link to related spec 1]
[Link to relevant external documentation]
```
+16 -15
View File
@@ -12,6 +12,7 @@ on:
permissions:
contents: write
pull-requests: write
id-token: write
jobs:
ci:
@@ -21,18 +22,18 @@ jobs:
actions: read # for Nerivec/action-ci-bench
pull-requests: write # for Nerivec/action-ci-bench
steps:
- uses: actions/checkout@v5
- 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@v5
- 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@v5
- uses: actions/setup-node@v7
with:
node-version: 24
registry-url: https://registry.npmjs.org/
@@ -62,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
@@ -77,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
@@ -125,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: |
@@ -138,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
@@ -215,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@v5
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v5
- 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@v5
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: |
+38
View File
@@ -0,0 +1,38 @@
name: Issue Bot
on:
issues:
types: [opened, edited]
permissions:
issues: write
contents: read
jobs:
comment:
runs-on: ubuntu-latest
if: |
contains(github.event.issue.labels.*.name, 'new device support') ||
contains(github.event.issue.labels.*.name, 'external converter') ||
startsWith(github.event.issue.title, '[New device support]') ||
startsWith(github.event.issue.title, '[External Converter]')
steps:
- uses: actions/checkout@v7
with:
sparse-checkout: |
scripts
path: z2m
- uses: actions/checkout@v7
with:
repository: Koenkk/zigbee-herdsman-converters
ref: master
fetch-depth: 1
path: zhc
- name: Comment on new device support/external converter issue
uses: actions/github-script@v9
with:
script: |
const {newDeviceSupport} = await import("${{ github.workspace }}/z2m/scripts/issueBot.mjs")
await newDeviceSupport(github, core, context, "${{ github.workspace }}/zhc")
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
merge-master-to-dev:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- 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@v5
- 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@v5
- uses: actions/checkout@v7
with:
repository: koenkk/zigbee2mqtt
path: ./z2m
- uses: actions/checkout@v5
- uses: actions/checkout@v7
with:
repository: koenkk/zigbee2mqtt
path: ./z2m-master
ref: master
- name: Restore cache commit-user-lookup.json
uses: actions/cache/restore@v4
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@v4
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'
+4 -4
View File
@@ -13,19 +13,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v7
with:
ref: dev
token: ${{ secrets.GH_TOKEN }}
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
- run: |
pnpm install ${{ github.event.client_payload.package }}@${{ github.event.client_payload.version }} --save-exact
pnpm install --no-frozen-lockfile
- uses: peter-evans/create-pull-request@v7
- uses: peter-evans/create-pull-request@v8
id: cpr
with:
commit-message: 'fix(ignore): update ${{ github.event.client_payload.package }} to ${{ github.event.client_payload.version }}'
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "2.6.2"
".": "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"
}
}
+386
View File
@@ -0,0 +1,386 @@
# AGENTS.md
## Project Overview
Zigbee2MQTT is a Zigbee to MQTT bridge that allows you to use your Zigbee devices without the vendor's bridge or gateway. It bridges events and allows you to control Zigbee devices via MQTT, integrating them with any smart home infrastructure.
### Architecture
- **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**:
- `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)
- `winston` (3.18.3 - logging)
### Project Structure
```
lib/ # TypeScript source code
├── controller.ts # Main controller orchestrating components
├── mqtt.ts # MQTT client management
├── zigbee.ts # Zigbee network management
├── state.ts # State management
├── eventBus.ts # Event-driven communication
├── extension/ # Extension system (plugins)
│ └── extension.ts # Abstract base class
├── model/ # Domain models (Device, Group)
├── util/ # Utility functions
└── types/ # TypeScript type definitions
test/ # Vitest test files with mocks
data/ # Runtime configuration and database
dist/ # Compiled JavaScript output
```
## Setup Commands
### Prerequisites
- Node.js version 20, 22, or 24
- pnpm 10.12.1 (will be auto-installed via corepack if not present)
### Installation
```bash
# Install dependencies (uses pnpm lockfile)
pnpm install --frozen-lockfile
# For development without lockfile restrictions
pnpm install
```
### Initial Build
```bash
# Full build (TypeScript compilation + hash generation)
pnpm run build
# Build type definitions only
pnpm run build:types
```
## Development Workflow
### Starting Development
```bash
# Watch mode - recompile on file changes
pnpm run build:watch
# In another terminal, start Zigbee2MQTT
pnpm start
```
### Code Quality Checks
```bash
# Run Biome linter and formatter (check only)
pnpm run check
# Auto-fix linting and formatting issues
pnpm run check:w
# The check runs with --error-on-warnings flag
# Configuration: biome.json (4-space indent, 150 line width, no bracket spacing)
```
### Clean Build
```bash
# Remove build artifacts
pnpm run clean
# Removes: coverage/, dist/, tsconfig.tsbuildinfo
```
## Testing Instructions
### Running Tests
```bash
# Run all tests once
pnpm test
# Run tests with coverage report
pnpm run test:coverage
# Watch mode - re-run tests on changes
pnpm run test:watch
# Run benchmarks
pnpm run bench
```
### Test Requirements
- **Coverage**: 100% code coverage is enforced (configured in `test/vitest.config.mts`)
- **Framework**: Vitest 3.1.1 with @vitest/coverage-v8
- **Test Files**: Located in `test/` directory with `.test.ts` extension
- **Mocks**: Centralized in `test/mocks/` directory
- **Coverage Report**: Generated in `coverage/` directory (HTML report at `coverage/index.html`)
### Running Specific Tests
```bash
# Run tests matching a pattern
pnpm vitest run -t "test name pattern" --config ./test/vitest.config.mts
# Run specific test file
pnpm vitest run test/controller.test.ts --config ./test/vitest.config.mts
# Focus on one test area in watch mode
pnpm vitest watch -t "Extension" --config ./test/vitest.config.mts
```
## Code Style Guidelines
### TypeScript Conventions
- **Module System**: ES modules with NodeNext resolution
- **Target**: ESNext
- **Strict Mode**: Enabled (`noImplicitAny`, `noImplicitThis`)
- **Decorators**: Experimental decorators enabled (used for `@bind` from `bind-decorator`)
### Import Order
1. Node.js built-in modules (with `node:` prefix)
2. Third-party libraries
3. Type-only imports from external packages (using `type` keyword)
4. Internal absolute imports
5. Type-only imports from internal modules
Example:
```typescript
import fs from "node:fs";
import bind from "bind-decorator";
import type {IClientOptions} from "mqtt";
import {connectAsync} from "mqtt";
import type {Zigbee2MQTTAPI} from "./types/api";
import logger from "./util/logger";
```
### Naming Conventions
- **Classes**: PascalCase (e.g., `Extension`, `Device`)
- **Functions/Methods**: camelCase (e.g., `publishEntityState`)
- **Constants**: SCREAMING_SNAKE_CASE (e.g., `CURRENT_VERSION`)
- **Interfaces/Types**: PascalCase (e.g., `MqttPublishOptions`)
- **Files**: camelCase for TypeScript (e.g., `eventBus.ts`)
### Code Patterns
- **Async/Await**: Always use async/await, explicitly type return as `Promise<Type>`
- **Error Handling**: Use `throw new Error("message")`, log with winston logger
- **Event Handlers**: Use `@bind` decorator to preserve `this` context
- **Logging**: Use `logger.info()`, `logger.warning()`, `logger.error()`, `logger.debug()`
### Formatting Rules (Biome)
- 4-space indentation
- 150 character line width
- No bracket spacing in objects
- Run `pnpm run check:w` to auto-format
## Build and Deployment
### Build Process
```bash
# Production build
pnpm run build
# Outputs:
# - Compiled JavaScript in dist/
# - Type definitions in dist/types/
# - Includes hash generation for version tracking
```
### Pre-publish
```bash
# Automatically runs before publishing
pnpm run prepack
# Performs: clean + build
```
### Environment Setup
- Configuration stored in `data/configuration.yaml`
- Database in `data/database.db`
- Logs in `data/log/`
- External extensions in `data/external_extensions/`
- External converters in `data/external_converters/`
## Architecture Patterns
### Extension System
All features are implemented as extensions that inherit from the abstract `Extension` base class:
```typescript
abstract class Extension {
protected zigbee: Zigbee;
protected mqtt: Mqtt;
protected state: State;
protected publishEntityState: PublishEntityState;
protected eventBus: EventBus;
async start(): Promise<void> {} // Initialize extension
async stop(): Promise<void> {} // Cleanup extension
}
```
**Key Points**:
- Constructor should only assign properties (no side effects)
- Initialization happens in `start()` method
- Use EventBus for inter-component communication
- Extensions are loaded and managed by the Controller
### Event-Driven Communication
Components communicate via the strongly-typed EventBus:
```typescript
// Emit events
this.eventBus.emit('deviceMessage', {device, message});
// Listen to events
this.eventBus.on('deviceMessage', this.onDeviceMessage, this);
```
### Dependency Injection
The Controller instantiates and injects dependencies into all extensions. Follow this pattern when creating new extensions.
## Pull Request Guidelines
### Target Branch
- **Always create PRs against the `dev` branch**
- The `master` branch is for production releases only
### Before Submitting
```bash
# Run all checks
pnpm run check
pnpm test
# Ensure 100% code coverage
pnpm run test:coverage
# Build successfully
pnpm run build
```
### PR Requirements
- All CI checks must pass (linting, tests, build)
- 100% test coverage maintained
- Code follows Biome formatting rules
- Commit messages should be descriptive
- Reference related issues when applicable
### CI Pipeline
The GitHub Actions CI workflow (`.github/workflows/ci.yml`) runs:
1. Biome code quality checks (`pnpm run check`)
2. TypeScript compilation (`pnpm run build`)
3. Full test suite with coverage (`pnpm run test:coverage`)
4. Benchmarks (on dev branch and PRs)
5. Docker image builds (on dev branch and tags)
## Working with Device Support
### Adding New Devices
**Important**: Device support is NOT added to this repository. All device definitions live in `zigbee-herdsman-converters`.
- Follow the guide at: https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html
- No changes to zigbee2mqtt codebase are needed for new devices
- Device definitions are automatically picked up from `zigbee-herdsman-converters`
## Debugging and Troubleshooting
### Development Setup
For the easiest development experience, set up a bare-metal installation following:
https://www.zigbee2mqtt.io/guide/installation/01_linux.html
### Logging
- Winston logger is initialized in `lib/util/logger.ts`
- Log levels: `error`, `warning`, `info`, `debug`
- Logs are written to console and/or file based on configuration
- Use structured logging with context (device names, IEEE addresses)
### Common Issues
1. **Import errors after file moves**: Run `pnpm run check` to verify TypeScript and ESLint
2. **Test failures**: Check if mocks in `test/mocks/` need updates
3. **Build errors**: Ensure Node.js version is 20, 22, or 24
4. **Coverage issues**: View HTML report at `coverage/index.html` to identify uncovered code
### Performance Considerations
- 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
## Critical Version Requirements
### Exact Versions
These dependencies use **exact versions** (no semver ranges) - do not upgrade without thorough testing:
- `zigbee-herdsman@6.2.0` - Critical for Zigbee protocol compatibility
- `zigbee-herdsman-converters@25.42.0` - Device definitions must match herdsman version
### Node.js Compatibility
Only these Node.js versions are supported:
- Node.js 20.x
- Node.js 22.x
- Node.js 24.x
Using other versions may cause runtime errors or incompatibilities.
## Additional Notes
### Package Manager
This project **requires pnpm 10.12.1**. The `packageManager` field in package.json enforces this via Corepack.
Do not use npm or yarn - they will not respect the pnpm-specific configuration.
### TypeScript Compilation
- Source files in `lib/` are compiled to `dist/`
- Type definitions exported from `dist/types/api.d.ts`
- Source maps are inlined for debugging
- Uses composite project references for faster incremental builds
### External Extensions
To load external extensions:
1. Place JavaScript files in `data/external_extensions/`
2. They will be automatically loaded on startup
3. No configuration changes needed
### Code Quality Tools
- **Linting/Formatting**: Biome 2.2.5 (replaces ESLint + Prettier)
- **Type Checking**: TypeScript 5.9.3
- **Testing**: Vitest 3.1.1
- **Coverage**: @vitest/coverage-v8
### Documentation
- Main documentation: https://www.zigbee2mqtt.io/
- Contributing guide: `CONTRIBUTING.md`
- Coding standards: `.github/copilot-instructions.md`
- Issue tracker: https://github.com/Koenkk/zigbee2mqtt/issues
+508 -1
View File
@@ -1,5 +1,512 @@
# 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)
### Features
* Improve onboarding ([#31152](https://github.com/Koenkk/zigbee2mqtt/issues/31152)) ([8ecf353](https://github.com/Koenkk/zigbee2mqtt/commit/8ecf353dd634cc56f8cbbfbe21646945c262a11e))
### Bug Fixes
* Fix Home Assistant options cannot be updated while running ([#31151](https://github.com/Koenkk/zigbee2mqtt/issues/31151)) ([a77f48f](https://github.com/Koenkk/zigbee2mqtt/commit/a77f48f5c464903712f7143bb6a85ea701a39c5e))
* Home Assistant: add `measurement` `state_class` to `_count` entities ([#30978](https://github.com/Koenkk/zigbee2mqtt/issues/30978)) ([d475aa5](https://github.com/Koenkk/zigbee2mqtt/commit/d475aa5be5caa85a5dc3f0883dc40b2bcddc09b2))
* Home Assistant: add `state_class: measurement` for `external_humidity` and `occupancy_level` https://github.com/Koenkk/zigbee2mqtt/issues/31022 ([02a2fd8](https://github.com/Koenkk/zigbee2mqtt/commit/02a2fd8fef786be9488c1adc97093b1ec6bff9e1))
* Home Assistant: add weather station sensor discovery lookup entries ([#31136](https://github.com/Koenkk/zigbee2mqtt/issues/31136)) ([8d1240e](https://github.com/Koenkk/zigbee2mqtt/commit/8d1240eba161e8eb92ce6fdda757710a07c4456e))
* Home Assistant: discover `temperature_probe` as `temperature` `device_class` https://github.com/Koenkk/zigbee2mqtt/issues/30862 ([8d8d37e](https://github.com/Koenkk/zigbee2mqtt/commit/8d8d37e3888cbfa4899831874ee9df6e306ddb52))
* Home Assistant: expose siren entity for IAS warning devices ([#31000](https://github.com/Koenkk/zigbee2mqtt/issues/31000)) ([c7dae39](https://github.com/Koenkk/zigbee2mqtt/commit/c7dae397999102ec30c6681bc6a7577e97c8b707))
* Home Assistant: set `state_class` `measurement` for `external_temperature` https://github.com/Koenkk/zigbee2mqtt/issues/31022 ([28a9f39](https://github.com/Koenkk/zigbee2mqtt/commit/28a9f39dbcd5a04e5eb5630855de2b82ec676dac))
* **ignore:** bump @biomejs/biome from 2.4.1 to 2.4.4 in the minor-patch group ([#31159](https://github.com/Koenkk/zigbee2mqtt/issues/31159)) ([1d5184f](https://github.com/Koenkk/zigbee2mqtt/commit/1d5184f2bd46afa50faa470a97e807888ed81a78))
* **ignore:** bump mqtt from 5.14.1 to 5.15.0 in the minor-patch group ([#30918](https://github.com/Koenkk/zigbee2mqtt/issues/30918)) ([4b03910](https://github.com/Koenkk/zigbee2mqtt/commit/4b039103c0a9bd32458011dbc2e7bea0e609d0c7))
* **ignore:** bump the minor-patch group with 2 updates ([#31211](https://github.com/Koenkk/zigbee2mqtt/issues/31211)) ([89192f9](https://github.com/Koenkk/zigbee2mqtt/commit/89192f9decdf41338ec925c88860b560906b3ad4))
* **ignore:** bump the minor-patch group with 3 updates ([#31011](https://github.com/Koenkk/zigbee2mqtt/issues/31011)) ([15e385b](https://github.com/Koenkk/zigbee2mqtt/commit/15e385bac6dbfd87869df87f1be9a66cf4db76f1))
* **ignore:** bump the minor-patch group with 4 updates ([#31081](https://github.com/Koenkk/zigbee2mqtt/issues/31081)) ([42d88fe](https://github.com/Koenkk/zigbee2mqtt/commit/42d88fe90d6272f2c8241ac7a6e6b2c5ce5a55af))
* **ignore:** Changes for new ZHC `definition.version` ([#30402](https://github.com/Koenkk/zigbee2mqtt/issues/30402)) ([b71461e](https://github.com/Koenkk/zigbee2mqtt/commit/b71461e03cd8ceea5365f4c0b2e794e34341325e))
* **ignore:** Fix enableDisableExtension ([#31176](https://github.com/Koenkk/zigbee2mqtt/issues/31176)) ([e1e856d](https://github.com/Koenkk/zigbee2mqtt/commit/e1e856d7da9c92952c3de94853794337fea1945d))
* **ignore:** update zigbee-herdsman to 9.0.3 ([#30960](https://github.com/Koenkk/zigbee2mqtt/issues/30960)) ([30d311e](https://github.com/Koenkk/zigbee2mqtt/commit/30d311e8494c00e6ade7759a0a2bfdc9ab79022d))
* **ignore:** update zigbee-herdsman to 9.0.4 ([#30996](https://github.com/Koenkk/zigbee2mqtt/issues/30996)) ([bf888de](https://github.com/Koenkk/zigbee2mqtt/commit/bf888de4789b01e824ca6b5f82506691b4f689a9))
* **ignore:** update zigbee-herdsman to 9.0.5 ([#31117](https://github.com/Koenkk/zigbee2mqtt/issues/31117)) ([5957085](https://github.com/Koenkk/zigbee2mqtt/commit/5957085fcc8d6acdedf426a3f16632f1de55a061))
* **ignore:** update zigbee-herdsman to 9.0.6 ([#31149](https://github.com/Koenkk/zigbee2mqtt/issues/31149)) ([8560c11](https://github.com/Koenkk/zigbee2mqtt/commit/8560c11e0667304a643eac907d3c62e35bcc2e23))
* **ignore:** update zigbee-herdsman to 9.0.7 ([#31174](https://github.com/Koenkk/zigbee2mqtt/issues/31174)) ([79bcf27](https://github.com/Koenkk/zigbee2mqtt/commit/79bcf276f57454a77ea76b6d8286544abd4d7cca))
* **ignore:** update zigbee-herdsman to 9.0.8 ([#31207](https://github.com/Koenkk/zigbee2mqtt/issues/31207)) ([c7d54ec](https://github.com/Koenkk/zigbee2mqtt/commit/c7d54ec360ee34ba4cb062dcd474154fb70467a5))
* **ignore:** update zigbee-herdsman to 9.0.9 ([#31215](https://github.com/Koenkk/zigbee2mqtt/issues/31215)) ([8ee5b1f](https://github.com/Koenkk/zigbee2mqtt/commit/8ee5b1f03bdc06062d58913e827f086b6bbcb026))
* **ignore:** update zigbee-herdsman-converters to 25.121.0 ([#30898](https://github.com/Koenkk/zigbee2mqtt/issues/30898)) ([09d010e](https://github.com/Koenkk/zigbee2mqtt/commit/09d010ea067446d26b6c77411aac78d045dbc6ea))
* **ignore:** update zigbee-herdsman-converters to 25.122.0 ([#30935](https://github.com/Koenkk/zigbee2mqtt/issues/30935)) ([402ed60](https://github.com/Koenkk/zigbee2mqtt/commit/402ed6094eedb40ecdb841b4b1f7d29d59afc89c))
* **ignore:** update zigbee-herdsman-converters to 26.0.0 ([6cce05a](https://github.com/Koenkk/zigbee2mqtt/commit/6cce05a5141089e24211d85d1d77d09f8d2b36f0))
* **ignore:** update zigbee-herdsman-converters to 26.1.0 ([#30981](https://github.com/Koenkk/zigbee2mqtt/issues/30981)) ([f1156db](https://github.com/Koenkk/zigbee2mqtt/commit/f1156db227bf937354d6ab4e70d6da59307dedad))
* **ignore:** update zigbee-herdsman-converters to 26.10.1 ([#31175](https://github.com/Koenkk/zigbee2mqtt/issues/31175)) ([b9d9e5e](https://github.com/Koenkk/zigbee2mqtt/commit/b9d9e5e75c3f175ec0fc69f6315c5ca75a8e8128))
* **ignore:** update zigbee-herdsman-converters to 26.11.0 ([#31185](https://github.com/Koenkk/zigbee2mqtt/issues/31185)) ([c880d36](https://github.com/Koenkk/zigbee2mqtt/commit/c880d36f29a8f146170cae34bef15b86d01f5889))
* **ignore:** update zigbee-herdsman-converters to 26.12.0 ([#31208](https://github.com/Koenkk/zigbee2mqtt/issues/31208)) ([a9a6ebe](https://github.com/Koenkk/zigbee2mqtt/commit/a9a6ebe00743b47bd00667ae64d0f0fb25f5e38d))
* **ignore:** update zigbee-herdsman-converters to 26.2.0 ([#30998](https://github.com/Koenkk/zigbee2mqtt/issues/30998)) ([b585e31](https://github.com/Koenkk/zigbee2mqtt/commit/b585e3130431c7c27b18b9779ed038b9e6dd30c6))
* **ignore:** update zigbee-herdsman-converters to 26.2.1 ([#31015](https://github.com/Koenkk/zigbee2mqtt/issues/31015)) ([4e360d0](https://github.com/Koenkk/zigbee2mqtt/commit/4e360d0bf2fed651f7c801bcce8455dc8cf4d649))
* **ignore:** update zigbee-herdsman-converters to 26.3.0 ([#31028](https://github.com/Koenkk/zigbee2mqtt/issues/31028)) ([c599e19](https://github.com/Koenkk/zigbee2mqtt/commit/c599e19bf2252b90554ec3a68abd64487acd8882))
* **ignore:** update zigbee-herdsman-converters to 26.4.0 ([#31036](https://github.com/Koenkk/zigbee2mqtt/issues/31036)) ([142754b](https://github.com/Koenkk/zigbee2mqtt/commit/142754b71cf99ac97d5d25d6a124c90d17bf321d))
* **ignore:** update zigbee-herdsman-converters to 26.5.0 ([#31058](https://github.com/Koenkk/zigbee2mqtt/issues/31058)) ([061262f](https://github.com/Koenkk/zigbee2mqtt/commit/061262ff97c5f30571f77df5b06362023c282fa6))
* **ignore:** update zigbee-herdsman-converters to 26.6.0 ([#31076](https://github.com/Koenkk/zigbee2mqtt/issues/31076)) ([909d2b3](https://github.com/Koenkk/zigbee2mqtt/commit/909d2b32b7ee7fe9144351b82d174e31fd0bad40))
* **ignore:** update zigbee-herdsman-converters to 26.7.0 ([#31090](https://github.com/Koenkk/zigbee2mqtt/issues/31090)) ([9d566f4](https://github.com/Koenkk/zigbee2mqtt/commit/9d566f4c015e7a31adaeef3b04bcc3c2312d583f))
* **ignore:** update zigbee-herdsman-converters to 26.8.0 ([#31118](https://github.com/Koenkk/zigbee2mqtt/issues/31118)) ([396eb38](https://github.com/Koenkk/zigbee2mqtt/commit/396eb38e4b9a329a651dec46ec816be537a3c997))
* **ignore:** update zigbee-herdsman-converters to 26.9.0 ([#31137](https://github.com/Koenkk/zigbee2mqtt/issues/31137)) ([4d287ee](https://github.com/Koenkk/zigbee2mqtt/commit/4d287eed0c2b746e1cfbd18ad61ed7b5129a98d9))
* Improve configure attempts lazy+cleanup ([#31126](https://github.com/Koenkk/zigbee2mqtt/issues/31126)) ([0c43c98](https://github.com/Koenkk/zigbee2mqtt/commit/0c43c983063c1aa01b85318c85bf7101f0085430))
* Improve startup signals behavior ([#31130](https://github.com/Koenkk/zigbee2mqtt/issues/31130)) ([f8385b8](https://github.com/Koenkk/zigbee2mqtt/commit/f8385b88993cfcf3a27c315999c86b961fd8fbfa))
* update zigbee2mqtt-windfront to 2.9.0 ([#31162](https://github.com/Koenkk/zigbee2mqtt/issues/31162)) ([93dc9a3](https://github.com/Koenkk/zigbee2mqtt/commit/93dc9a3664f23a4a13a55e54c16714d721e596d7))
## [2.8.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.7.2...2.8.0) (2026-02-01)
### Features
* Improve OTA ([#30566](https://github.com/Koenkk/zigbee2mqtt/issues/30566)) ([dd1c449](https://github.com/Koenkk/zigbee2mqtt/commit/dd1c44979667360d77f05ac30729011f9f2d6cd6))
### Bug Fixes
* Home Assisant: set state class to `measurement` for `illuminane_raw` https://github.com/Koenkk/zigbee2mqtt/issues/30439 ([3e60f91](https://github.com/Koenkk/zigbee2mqtt/commit/3e60f915ffe1de640690695ccf6536ad235c336e))
* Home Assistant: add `gas` device class ([#30653](https://github.com/Koenkk/zigbee2mqtt/issues/30653)) ([4815d51](https://github.com/Koenkk/zigbee2mqtt/commit/4815d5149d3fbc70fe374fc4aa99213ac5f8a8c7))
* Home Assistant: Change `device_class` of `eco2` to `volatile_organic_compounds_parts` ([#30840](https://github.com/Koenkk/zigbee2mqtt/issues/30840)) ([4f412cb](https://github.com/Koenkk/zigbee2mqtt/commit/4f412cb4da6217fcca57b5ea1afc40d34d662e19))
* Home Assistant: discover temperature sensor for thermostats ([#30804](https://github.com/Koenkk/zigbee2mqtt/issues/30804)) ([26a0fd4](https://github.com/Koenkk/zigbee2mqtt/commit/26a0fd4de0f39d10a404517f42d4c37aff1eff8e))
* Home Assistant: expose group settings override ([#30627](https://github.com/Koenkk/zigbee2mqtt/issues/30627)) ([6f0b02e](https://github.com/Koenkk/zigbee2mqtt/commit/6f0b02e0093cc7fd61d30c00fb1cd61d0cfdf09f))
* Home Assistant: mark `load_estimate` as `state_class` `measurement` https://github.com/Koenkk/zigbee-herdsman-converters/issues/11240 ([a32c8ee](https://github.com/Koenkk/zigbee2mqtt/commit/a32c8eec3604a3ad0d16e87edf027581db7f6920))
* Home Assistant: treat `Area1-4Occupancy` as `occupancy` ([#30712](https://github.com/Koenkk/zigbee2mqtt/issues/30712)) ([2e36084](https://github.com/Koenkk/zigbee2mqtt/commit/2e36084852e84f26dd375820a87c0a8520d4c3e7))
* Home Assistant: Use `temperature_delta` for calibration ([#30784](https://github.com/Koenkk/zigbee2mqtt/issues/30784)) ([8e68295](https://github.com/Koenkk/zigbee2mqtt/commit/8e682956e5977101ed4ca80e795f413a5bfb4f09))
* **ignore:** bump @biomejs/biome from 2.3.10 to 2.3.11 in the minor-patch group ([#30511](https://github.com/Koenkk/zigbee2mqtt/issues/30511)) ([ed52c49](https://github.com/Koenkk/zigbee2mqtt/commit/ed52c49e34a200f9adf5deb2065c78fbe8df3189))
* **ignore:** bump @biomejs/biome from 2.3.12 to 2.3.13 in the minor-patch group ([#30810](https://github.com/Koenkk/zigbee2mqtt/issues/30810)) ([14bffd5](https://github.com/Koenkk/zigbee2mqtt/commit/14bffd58b1ae90cf5c32b654ddbb7a43d12a36cc))
* **ignore:** bump @types/node from 24.10.7 to 24.10.9 in the minor-patch group ([#30713](https://github.com/Koenkk/zigbee2mqtt/issues/30713)) ([043dd0f](https://github.com/Koenkk/zigbee2mqtt/commit/043dd0ff1cc94ffa8ea59f6de674e0363f9f6920))
* **ignore:** bump the minor-patch group with 2 updates ([#30617](https://github.com/Koenkk/zigbee2mqtt/issues/30617)) ([1d87d2f](https://github.com/Koenkk/zigbee2mqtt/commit/1d87d2f2349f9eed6351a032645eec075476f148))
* **ignore:** bump the minor-patch group with 2 updates ([#30790](https://github.com/Koenkk/zigbee2mqtt/issues/30790)) ([da1f77e](https://github.com/Koenkk/zigbee2mqtt/commit/da1f77ec6eb599a480f66cc82edb12acc6c22ca8))
* **ignore:** bump zigbee2mqtt-windfront from 2.8.0 to 2.8.1 in the minor-patch group ([#30879](https://github.com/Koenkk/zigbee2mqtt/issues/30879)) ([9ddbb2a](https://github.com/Koenkk/zigbee2mqtt/commit/9ddbb2a21a0d302ed39ac4c658587d9f33c48960))
* **ignore:** bump zigbee2mqtt-windfront in the minor-patch group ([8d980ae](https://github.com/Koenkk/zigbee2mqtt/commit/8d980ae6ecd759c6c6fb42fc7fbb1d701a3317fa))
* **ignore:** Replace multiple area occupancy fields with camelCase instead of PascalCase ([#30745](https://github.com/Koenkk/zigbee2mqtt/issues/30745)) ([d9e4244](https://github.com/Koenkk/zigbee2mqtt/commit/d9e4244f801aaa24b8347ab684521391df1ce0e2))
* **ignore:** update zigbee-herdsman to 8.0.2 ([#30480](https://github.com/Koenkk/zigbee2mqtt/issues/30480)) ([1147cf1](https://github.com/Koenkk/zigbee2mqtt/commit/1147cf16dd62dce5863a93983835c46e8e92e95d))
* **ignore:** update zigbee-herdsman to 8.0.3 ([#30632](https://github.com/Koenkk/zigbee2mqtt/issues/30632)) ([d44cdbd](https://github.com/Koenkk/zigbee2mqtt/commit/d44cdbde4f5247f69af4c79fd45a62c03037eca7))
* **ignore:** update zigbee-herdsman to 8.1.0 ([#30725](https://github.com/Koenkk/zigbee2mqtt/issues/30725)) ([9d266bd](https://github.com/Koenkk/zigbee2mqtt/commit/9d266bda9a34efbe726f8d984da48e5ba3c46a83))
* **ignore:** update zigbee-herdsman to 9.0.0 ([#30787](https://github.com/Koenkk/zigbee2mqtt/issues/30787)) ([971ddaa](https://github.com/Koenkk/zigbee2mqtt/commit/971ddaa3cbc2132150f7cde4cdbe0575349eabac))
* **ignore:** update zigbee-herdsman to 9.0.1 ([#30788](https://github.com/Koenkk/zigbee2mqtt/issues/30788)) ([6d5e7e3](https://github.com/Koenkk/zigbee2mqtt/commit/6d5e7e334a9a560b9f6293cf448ad84de8504b67))
* **ignore:** update zigbee-herdsman to 9.0.2 ([10478e7](https://github.com/Koenkk/zigbee2mqtt/commit/10478e797e1733f9bf66defce0e84b6cc1f23020))
* **ignore:** update zigbee-herdsman-converters to 25.100.0 ([#30499](https://github.com/Koenkk/zigbee2mqtt/issues/30499)) ([a2b81ab](https://github.com/Koenkk/zigbee2mqtt/commit/a2b81ab89491f8babf40001404969817f19c5b5e))
* **ignore:** update zigbee-herdsman-converters to 25.101.0 ([#30520](https://github.com/Koenkk/zigbee2mqtt/issues/30520)) ([4313bac](https://github.com/Koenkk/zigbee2mqtt/commit/4313baca09b8476466b8134c551b59f92996b53f))
* **ignore:** update zigbee-herdsman-converters to 25.102.0 ([#30537](https://github.com/Koenkk/zigbee2mqtt/issues/30537)) ([57245e5](https://github.com/Koenkk/zigbee2mqtt/commit/57245e52343568678a4dace9bf569c05f508decc))
* **ignore:** update zigbee-herdsman-converters to 25.103.0 ([#30552](https://github.com/Koenkk/zigbee2mqtt/issues/30552)) ([d4522c5](https://github.com/Koenkk/zigbee2mqtt/commit/d4522c53caca86ba3a087fa67839e2ebf5d9deca))
* **ignore:** update zigbee-herdsman-converters to 25.104.0 ([#30564](https://github.com/Koenkk/zigbee2mqtt/issues/30564)) ([e68403d](https://github.com/Koenkk/zigbee2mqtt/commit/e68403d8d3ac68f2d09df066d1be8db87f2e7977))
* **ignore:** update zigbee-herdsman-converters to 25.105.0 ([#30605](https://github.com/Koenkk/zigbee2mqtt/issues/30605)) ([29c7571](https://github.com/Koenkk/zigbee2mqtt/commit/29c757145d6ed16a8629b68379cd85fabe45a8f1))
* **ignore:** update zigbee-herdsman-converters to 25.106.0 ([#30631](https://github.com/Koenkk/zigbee2mqtt/issues/30631)) ([9a41ec2](https://github.com/Koenkk/zigbee2mqtt/commit/9a41ec2a39ca2373dbdcb15b09aa0eb09116c31e))
* **ignore:** update zigbee-herdsman-converters to 25.107.0 ([#30662](https://github.com/Koenkk/zigbee2mqtt/issues/30662)) ([535f75e](https://github.com/Koenkk/zigbee2mqtt/commit/535f75e90ca936afcac0c79e4c2452dce7b9b08d))
* **ignore:** update zigbee-herdsman-converters to 25.108.0 ([#30672](https://github.com/Koenkk/zigbee2mqtt/issues/30672)) ([5e1b7d4](https://github.com/Koenkk/zigbee2mqtt/commit/5e1b7d4830b5597eb993f32e3a6d1edd84b48e4c))
* **ignore:** update zigbee-herdsman-converters to 25.109.0 ([#30709](https://github.com/Koenkk/zigbee2mqtt/issues/30709)) ([ee6afaf](https://github.com/Koenkk/zigbee2mqtt/commit/ee6afaf1c608b89efef452d7cb337345b035d76a))
* **ignore:** update zigbee-herdsman-converters to 25.110.0 ([#30726](https://github.com/Koenkk/zigbee2mqtt/issues/30726)) ([20af502](https://github.com/Koenkk/zigbee2mqtt/commit/20af50201ef1e081223ee8b2f0b6304777c7b7f1))
* **ignore:** update zigbee-herdsman-converters to 25.111.0 ([#30739](https://github.com/Koenkk/zigbee2mqtt/issues/30739)) ([2bb44f7](https://github.com/Koenkk/zigbee2mqtt/commit/2bb44f7a8b8bd9fc1a21b127b764a5d697e65c3e))
* **ignore:** update zigbee-herdsman-converters to 25.112.0 ([#30767](https://github.com/Koenkk/zigbee2mqtt/issues/30767)) ([ad51d12](https://github.com/Koenkk/zigbee2mqtt/commit/ad51d12a2d1325e0b0c1b56a633b229f0f3d048b))
* **ignore:** update zigbee-herdsman-converters to 25.113.0 ([#30789](https://github.com/Koenkk/zigbee2mqtt/issues/30789)) ([036ee47](https://github.com/Koenkk/zigbee2mqtt/commit/036ee471ddfda094a3ae3f970dedb5600c9d8808))
* **ignore:** update zigbee-herdsman-converters to 25.114.0 ([#30803](https://github.com/Koenkk/zigbee2mqtt/issues/30803)) ([999df29](https://github.com/Koenkk/zigbee2mqtt/commit/999df2913ff7e47fe9438ed9398a0922c0b40916))
* **ignore:** update zigbee-herdsman-converters to 25.115.0 ([#30826](https://github.com/Koenkk/zigbee2mqtt/issues/30826)) ([99e06a8](https://github.com/Koenkk/zigbee2mqtt/commit/99e06a8d3ea66ed2a6acf6cddece2f2070cce6ae))
* **ignore:** update zigbee-herdsman-converters to 25.116.0 ([#30839](https://github.com/Koenkk/zigbee2mqtt/issues/30839)) ([5a4d6b5](https://github.com/Koenkk/zigbee2mqtt/commit/5a4d6b56ad2ea62378b7dd161eba31916f08c65b))
* **ignore:** update zigbee-herdsman-converters to 25.117.0 ([#30849](https://github.com/Koenkk/zigbee2mqtt/issues/30849)) ([4afbce6](https://github.com/Koenkk/zigbee2mqtt/commit/4afbce62ebc667fc7d94b8b23c0bb0125f065d79))
* **ignore:** update zigbee-herdsman-converters to 25.118.0 ([#30865](https://github.com/Koenkk/zigbee2mqtt/issues/30865)) ([f688340](https://github.com/Koenkk/zigbee2mqtt/commit/f68834049cde265f731ab271b50fcfa6f85fa971))
* **ignore:** update zigbee-herdsman-converters to 25.119.0 ([#30874](https://github.com/Koenkk/zigbee2mqtt/issues/30874)) ([788b0d3](https://github.com/Koenkk/zigbee2mqtt/commit/788b0d3993c877adc0361a4b2f9d2df79cea0b8d))
* **ignore:** update zigbee-herdsman-converters to 25.120.0 ([#30876](https://github.com/Koenkk/zigbee2mqtt/issues/30876)) ([49b752d](https://github.com/Koenkk/zigbee2mqtt/commit/49b752da3527920dcdfae6a617f4aa084f6a8901))
* **ignore:** update zigbee-herdsman-converters to 25.99.0 ([#30481](https://github.com/Koenkk/zigbee2mqtt/issues/30481)) ([3adf7d5](https://github.com/Koenkk/zigbee2mqtt/commit/3adf7d5d8ec6da3fabbd9f293d5d0bcaef3c9144))
* Log error before renaming failed to load converters ([#30436](https://github.com/Koenkk/zigbee2mqtt/issues/30436)) ([1eaa2f3](https://github.com/Koenkk/zigbee2mqtt/commit/1eaa2f3e759351bc746dec2ccc57b92cdcf1b5d7))
* Network map: escape double-quotes and backslashes in device attributes ([#30746](https://github.com/Koenkk/zigbee2mqtt/issues/30746)) ([5de47a0](https://github.com/Koenkk/zigbee2mqtt/commit/5de47a0d93f5fdc187e4f8a7489580a107a2fd49))
* OTA availability detection ([#30815](https://github.com/Koenkk/zigbee2mqtt/issues/30815)) ([476efaa](https://github.com/Koenkk/zigbee2mqtt/commit/476efaa6e18a1d2ea069ea9c128536356df547ac))
## [2.7.2](https://github.com/Koenkk/zigbee2mqtt/compare/2.7.1...2.7.2) (2026-01-01)
### Bug Fixes
* Biome floating promises detection ([#30137](https://github.com/Koenkk/zigbee2mqtt/issues/30137)) ([0025ef8](https://github.com/Koenkk/zigbee2mqtt/commit/0025ef87c502970b94e1b720516d42631e21323c))
* Don't fail to start when removing old log directory fails ([#30216](https://github.com/Koenkk/zigbee2mqtt/issues/30216)) ([b39b8d4](https://github.com/Koenkk/zigbee2mqtt/commit/b39b8d478e99c5a5ee5a3a610547168b3724ca91))
* **ignore:** bump @types/node from 24.10.2 to 24.10.4 in the minor-patch group ([#30390](https://github.com/Koenkk/zigbee2mqtt/issues/30390)) ([93c9704](https://github.com/Koenkk/zigbee2mqtt/commit/93c9704fcd5552e234dd03a7ae1cddc11403622b))
* **ignore:** bump the minor-patch group across 1 directory with 4 updates ([#30117](https://github.com/Koenkk/zigbee2mqtt/issues/30117)) ([ad3c090](https://github.com/Koenkk/zigbee2mqtt/commit/ad3c09036b804eb108238036f22776d83ea371b2))
* **ignore:** bump the minor-patch group with 2 updates ([#30207](https://github.com/Koenkk/zigbee2mqtt/issues/30207)) ([1d15c21](https://github.com/Koenkk/zigbee2mqtt/commit/1d15c2142fefb9c7ce96c947e5ac7ddc6101439f))
* **ignore:** bump the minor-patch group with 2 updates ([#30309](https://github.com/Koenkk/zigbee2mqtt/issues/30309)) ([b10fa93](https://github.com/Koenkk/zigbee2mqtt/commit/b10fa93700febe84634040dbdeb4857258cd2882))
* **ignore:** bump zigbee2mqtt-windfront from 2.6.1 to 2.6.2 in the minor-patch group ([#30412](https://github.com/Koenkk/zigbee2mqtt/issues/30412)) ([4d6269e](https://github.com/Koenkk/zigbee2mqtt/commit/4d6269e7e89af52df0b2576925abade2f6b60e09))
* **ignore:** update zigbee-herdsman to 7.0.5 ([#30116](https://github.com/Koenkk/zigbee2mqtt/issues/30116)) ([4574ffc](https://github.com/Koenkk/zigbee2mqtt/commit/4574ffc71a799846c6a04121c3b5908f968ff0be))
* **ignore:** update zigbee-herdsman to 7.0.6 ([#30142](https://github.com/Koenkk/zigbee2mqtt/issues/30142)) ([ecb0af4](https://github.com/Koenkk/zigbee2mqtt/commit/ecb0af40575ebaadf887685c50cd1a9968cc1c68))
* **ignore:** update zigbee-herdsman to 8.0.0 and zigbee-herdsman-converters to 25.87.0 ([#30186](https://github.com/Koenkk/zigbee2mqtt/issues/30186)) ([7f5f6a5](https://github.com/Koenkk/zigbee2mqtt/commit/7f5f6a5849d940a6e5288865edf7999c41af12e2))
* **ignore:** update zigbee-herdsman to 8.0.1 ([#30240](https://github.com/Koenkk/zigbee2mqtt/issues/30240)) ([1688a3b](https://github.com/Koenkk/zigbee2mqtt/commit/1688a3b5323a8774ad2c36775fdb99f48057f72a))
* **ignore:** update zigbee-herdsman-converters to 25.84.0 ([#30081](https://github.com/Koenkk/zigbee2mqtt/issues/30081)) ([c18d132](https://github.com/Koenkk/zigbee2mqtt/commit/c18d1329c801ce48cf5277c00682ad2f9098f383))
* **ignore:** update zigbee-herdsman-converters to 25.85.0 ([#30120](https://github.com/Koenkk/zigbee2mqtt/issues/30120)) ([5db6ae0](https://github.com/Koenkk/zigbee2mqtt/commit/5db6ae0a197a180abc5e497aebe6e59fcb9b6b3c))
* **ignore:** update zigbee-herdsman-converters to 25.86.0 ([#30143](https://github.com/Koenkk/zigbee2mqtt/issues/30143)) ([3460f15](https://github.com/Koenkk/zigbee2mqtt/commit/3460f157df84d10f8de87a37d6b0b0b243de6874))
* **ignore:** update zigbee-herdsman-converters to 25.88.0 ([#30196](https://github.com/Koenkk/zigbee2mqtt/issues/30196)) ([3818c50](https://github.com/Koenkk/zigbee2mqtt/commit/3818c50b8ae516e56ad07e8fce3c451f35e1b678))
* **ignore:** update zigbee-herdsman-converters to 25.89.0 ([#30215](https://github.com/Koenkk/zigbee2mqtt/issues/30215)) ([01304cf](https://github.com/Koenkk/zigbee2mqtt/commit/01304cfc6604b86acf181dc3f07688f56ecbe8a1))
* **ignore:** update zigbee-herdsman-converters to 25.90.0 ([#30267](https://github.com/Koenkk/zigbee2mqtt/issues/30267)) ([ab08947](https://github.com/Koenkk/zigbee2mqtt/commit/ab0894715a002bd5c14d8af45a67e5e546889590))
* **ignore:** update zigbee-herdsman-converters to 25.91.0 ([#30279](https://github.com/Koenkk/zigbee2mqtt/issues/30279)) ([4c02e3d](https://github.com/Koenkk/zigbee2mqtt/commit/4c02e3da8f66cafbdcde89303bb1aea6eaf73bcd))
* **ignore:** update zigbee-herdsman-converters to 25.92.0 ([#30300](https://github.com/Koenkk/zigbee2mqtt/issues/30300)) ([c28a024](https://github.com/Koenkk/zigbee2mqtt/commit/c28a024664989fe8a044babe3cdf43403b0ee2b5))
* **ignore:** update zigbee-herdsman-converters to 25.93.0 ([#30331](https://github.com/Koenkk/zigbee2mqtt/issues/30331)) ([c61f67f](https://github.com/Koenkk/zigbee2mqtt/commit/c61f67f061d2766e99c5cb9c99c02b172832271c))
* **ignore:** update zigbee-herdsman-converters to 25.94.0 ([#30355](https://github.com/Koenkk/zigbee2mqtt/issues/30355)) ([7cb5c58](https://github.com/Koenkk/zigbee2mqtt/commit/7cb5c58dc028f86c59935b64a738444950ef8e12))
* **ignore:** update zigbee-herdsman-converters to 25.95.0 ([#30371](https://github.com/Koenkk/zigbee2mqtt/issues/30371)) ([4c48e9c](https://github.com/Koenkk/zigbee2mqtt/commit/4c48e9cfbcedb9cd6aedee9f694db2993038eaee))
* **ignore:** update zigbee-herdsman-converters to 25.96.0 ([#30383](https://github.com/Koenkk/zigbee2mqtt/issues/30383)) ([2721b0e](https://github.com/Koenkk/zigbee2mqtt/commit/2721b0ea78f4e86e6fe3b5f4831c5787a28585a8))
* **ignore:** update zigbee-herdsman-converters to 25.97.0 ([#30396](https://github.com/Koenkk/zigbee2mqtt/issues/30396)) ([3727f07](https://github.com/Koenkk/zigbee2mqtt/commit/3727f078ecde73a5c444c42909f36b4a0f35d8c3))
* **ignore:** update zigbee-herdsman-converters to 25.98.0 ([#30417](https://github.com/Koenkk/zigbee2mqtt/issues/30417)) ([0eba0c9](https://github.com/Koenkk/zigbee2mqtt/commit/0eba0c953d7543b2850a9b9672ffbc3d87840fb3))
* log dir tz format not working on some systems ([#30324](https://github.com/Koenkk/zigbee2mqtt/issues/30324)) ([58d98c7](https://github.com/Koenkk/zigbee2mqtt/commit/58d98c7263dbe687eb677199eb760799ecff8e8f))
* Proper timezone in logs dir ([#30297](https://github.com/Koenkk/zigbee2mqtt/issues/30297)) ([8549b37](https://github.com/Koenkk/zigbee2mqtt/commit/8549b372283da8b55804c4049c7341294ced1538))
* Remove extra `>` in onboarding([#30065](https://github.com/Koenkk/zigbee2mqtt/issues/30065)) ([ed1e7b3](https://github.com/Koenkk/zigbee2mqtt/commit/ed1e7b333f481a4b3a37b021f2058331b86ebd84))
## [2.7.1](https://github.com/Koenkk/zigbee2mqtt/compare/2.7.0...2.7.1) (2025-12-06)
### Bug Fixes
* Home Assistant: Accept multiple access levels for `current_humidity` source property ([#29936](https://github.com/Koenkk/zigbee2mqtt/issues/29936)) ([fa99c6d](https://github.com/Koenkk/zigbee2mqtt/commit/fa99c6dcca97d2c8c90136249293dea3d42e0998))
* Home Assistant: Translate boolean to state topic for `current_humidity_topic` ([#30014](https://github.com/Koenkk/zigbee2mqtt/issues/30014)) ([9ca85df](https://github.com/Koenkk/zigbee2mqtt/commit/9ca85dfac8b0f0421ad0622dfdbe36c8766d22b4))
* **ignore:** bump finalhandler from 2.1.0 to 2.1.1 in the minor-patch group ([#29920](https://github.com/Koenkk/zigbee2mqtt/issues/29920)) ([ce27ff3](https://github.com/Koenkk/zigbee2mqtt/commit/ce27ff3cbfac204579b38d034e71733e4e4bb18f))
* **ignore:** update zigbee-herdsman to 7.0.2 ([#29956](https://github.com/Koenkk/zigbee2mqtt/issues/29956)) ([403bd4a](https://github.com/Koenkk/zigbee2mqtt/commit/403bd4a72541bf7342de246b033c5090b186df32))
* **ignore:** update zigbee-herdsman to 7.0.3 ([#30008](https://github.com/Koenkk/zigbee2mqtt/issues/30008)) ([bae9bb8](https://github.com/Koenkk/zigbee2mqtt/commit/bae9bb8812c81e46d4068b4284e0f9957821f88a))
* **ignore:** update zigbee-herdsman to 7.0.4 ([#30041](https://github.com/Koenkk/zigbee2mqtt/issues/30041)) ([334fa2a](https://github.com/Koenkk/zigbee2mqtt/commit/334fa2a60d04c66749978b8b8f4b9b5af6c7c7eb))
* **ignore:** update zigbee-herdsman-converters to 25.81.0 ([#29959](https://github.com/Koenkk/zigbee2mqtt/issues/29959)) ([fd4282b](https://github.com/Koenkk/zigbee2mqtt/commit/fd4282b98d9630f9a98e82f627c12c5920d810b4))
* **ignore:** update zigbee-herdsman-converters to 25.82.0 ([#29990](https://github.com/Koenkk/zigbee2mqtt/issues/29990)) ([740edd8](https://github.com/Koenkk/zigbee2mqtt/commit/740edd8b58b7d72b7a2a5247f7f6ac8ee99ff0fa))
* **ignore:** update zigbee-herdsman-converters to 25.83.0 ([#30011](https://github.com/Koenkk/zigbee2mqtt/issues/30011)) ([a7f1345](https://github.com/Koenkk/zigbee2mqtt/commit/a7f1345a9164f6c83cb81688c33511b14646fab8))
* **ignore:** update zigbee-herdsman-converters to 25.83.1 ([#30050](https://github.com/Koenkk/zigbee2mqtt/issues/30050)) ([9b60353](https://github.com/Koenkk/zigbee2mqtt/commit/9b6035387be6f285fab455f7845baa75f3f722be))
## [2.7.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.6.3...2.7.0) (2025-12-01)
### Features
* Add `action` bridge/request API ([#29730](https://github.com/Koenkk/zigbee2mqtt/issues/29730)) ([a61646d](https://github.com/Koenkk/zigbee2mqtt/commit/a61646d584c0a0de46abb3545cd964f65d84312d))
* Add new bind/reporting/map features ([#29750](https://github.com/Koenkk/zigbee2mqtt/issues/29750)) ([f26ade4](https://github.com/Koenkk/zigbee2mqtt/commit/f26ade4f938350d10ed22a32a2e30365f7514a72))
### Bug Fixes
* Add port 8080 `EXPOSE` to container ([#29754](https://github.com/Koenkk/zigbee2mqtt/issues/29754)) ([4c21b66](https://github.com/Koenkk/zigbee2mqtt/commit/4c21b660794ee3b85c9a9a6d01b0df5ec8bce639))
* Home Assistant: expose `current_humidity` for `climate` devices exposing `humidity` ([#29842](https://github.com/Koenkk/zigbee2mqtt/issues/29842)) ([631c4f6](https://github.com/Koenkk/zigbee2mqtt/commit/631c4f6a5729888d65a1125e0cca8f9ba2ff0e59))
* **ignore:** bump @biomejs/biome from 2.3.3 to 2.3.4 in the minor-patch group ([#29571](https://github.com/Koenkk/zigbee2mqtt/issues/29571)) ([517b286](https://github.com/Koenkk/zigbee2mqtt/commit/517b2865835a7c21b4c5ce68018fd30ae45d1a3f))
* **ignore:** bump debounce from 2.2.0 to 3.0.0 ([#29444](https://github.com/Koenkk/zigbee2mqtt/issues/29444)) ([8099322](https://github.com/Koenkk/zigbee2mqtt/commit/8099322cc2e93e5c1faf355d41485f2c673e0cea))
* **ignore:** bump the minor-patch group with 2 updates ([#29443](https://github.com/Koenkk/zigbee2mqtt/issues/29443)) ([619b939](https://github.com/Koenkk/zigbee2mqtt/commit/619b939b148394bd6a083c17cc5cf254c8eb3aea))
* **ignore:** bump the minor-patch group with 2 updates ([#29859](https://github.com/Koenkk/zigbee2mqtt/issues/29859)) ([c02ed09](https://github.com/Koenkk/zigbee2mqtt/commit/c02ed092116c7454220c0611bff5d078e12a8ecc))
* **ignore:** bump the minor-patch group with 3 updates ([#29804](https://github.com/Koenkk/zigbee2mqtt/issues/29804)) ([e2232e9](https://github.com/Koenkk/zigbee2mqtt/commit/e2232e9e9173ea9ec55152ac33bc0ee0141471bb))
* **ignore:** bump zigbee2mqtt-windfront from 2.4.0 to 2.4.1 in the minor-patch group ([#29905](https://github.com/Koenkk/zigbee2mqtt/issues/29905)) ([4a020fd](https://github.com/Koenkk/zigbee2mqtt/commit/4a020fd8de2dab2973b859c34b934416a15e82a5))
* **ignore:** bump zigbee2mqtt-windfront to 2.4.2 ([#29919](https://github.com/Koenkk/zigbee2mqtt/issues/29919)) ([3dce854](https://github.com/Koenkk/zigbee2mqtt/commit/3dce8548ceee111f49a5b3460fdb2cbb4f47da62))
* **ignore:** prop name convention on new API endpoints ([#29820](https://github.com/Koenkk/zigbee2mqtt/issues/29820)) ([ad06968](https://github.com/Koenkk/zigbee2mqtt/commit/ad0696856a9a63fc74b9c2115f08d7fd97850545))
* **ignore:** update zigbee-herdsman to 6.3.3 ([#29500](https://github.com/Koenkk/zigbee2mqtt/issues/29500)) ([39c5bc8](https://github.com/Koenkk/zigbee2mqtt/commit/39c5bc8c0446a7ccdffe6f82ae9e7f31f60f20ba))
* **ignore:** update zigbee-herdsman to 6.4.0 ([#29522](https://github.com/Koenkk/zigbee2mqtt/issues/29522)) ([e788c52](https://github.com/Koenkk/zigbee2mqtt/commit/e788c520a9268498daf0f88d7bedb289546d226e))
* **ignore:** update zigbee-herdsman to 6.4.1 ([#29595](https://github.com/Koenkk/zigbee2mqtt/issues/29595)) ([61c6ec6](https://github.com/Koenkk/zigbee2mqtt/commit/61c6ec611174529e4c041a56146ba8405cfe9ed8))
* **ignore:** update zigbee-herdsman to 6.4.2 ([#29683](https://github.com/Koenkk/zigbee2mqtt/issues/29683)) ([53b7eaa](https://github.com/Koenkk/zigbee2mqtt/commit/53b7eaabf0647c8d1edb0de24b89776991af576c))
* **ignore:** update zigbee-herdsman to 7.0.0 ([3e2aefb](https://github.com/Koenkk/zigbee2mqtt/commit/3e2aefbcf84f43d45c1d6ca33397c8bb76648822))
* **ignore:** update zigbee-herdsman to 7.0.1 ([#29855](https://github.com/Koenkk/zigbee2mqtt/issues/29855)) ([802271e](https://github.com/Koenkk/zigbee2mqtt/commit/802271ed37d404944d315f83f3b623ba9e424bb9))
* **ignore:** update zigbee-herdsman-converters to 25.60.0 ([#29426](https://github.com/Koenkk/zigbee2mqtt/issues/29426)) ([eeed56e](https://github.com/Koenkk/zigbee2mqtt/commit/eeed56ef2c01dfe67e7a0cd7cafd35ef1876f5b4))
* **ignore:** update zigbee-herdsman-converters to 25.62.0 ([#29449](https://github.com/Koenkk/zigbee2mqtt/issues/29449)) ([ea1c5a8](https://github.com/Koenkk/zigbee2mqtt/commit/ea1c5a8f4e8537a2f8c59988bb712f62a0adffd7))
* **ignore:** update zigbee-herdsman-converters to 25.63.0 ([#29482](https://github.com/Koenkk/zigbee2mqtt/issues/29482)) ([5a6a9cf](https://github.com/Koenkk/zigbee2mqtt/commit/5a6a9cf4d2de803a10cd38bc19f3ec256eff5929))
* **ignore:** update zigbee-herdsman-converters to 25.64.0 ([#29499](https://github.com/Koenkk/zigbee2mqtt/issues/29499)) ([5c4e43f](https://github.com/Koenkk/zigbee2mqtt/commit/5c4e43fd109321ddcfad3a67b60423f0197c51a6))
* **ignore:** update zigbee-herdsman-converters to 25.65.0 ([#29523](https://github.com/Koenkk/zigbee2mqtt/issues/29523)) ([8822aed](https://github.com/Koenkk/zigbee2mqtt/commit/8822aedb5e254f5be5f6cda0cfe18fc6f69c84d6))
* **ignore:** update zigbee-herdsman-converters to 25.66.0 ([#29558](https://github.com/Koenkk/zigbee2mqtt/issues/29558)) ([940fff2](https://github.com/Koenkk/zigbee2mqtt/commit/940fff244c39da04d0de5d7e4076359dc6245aed))
* **ignore:** update zigbee-herdsman-converters to 25.67.0 ([#29572](https://github.com/Koenkk/zigbee2mqtt/issues/29572)) ([4794b3c](https://github.com/Koenkk/zigbee2mqtt/commit/4794b3c81f9e38660f8549995b7f3ef8b4cd4318))
* **ignore:** update zigbee-herdsman-converters to 25.68.0 ([#29606](https://github.com/Koenkk/zigbee2mqtt/issues/29606)) ([564e9ab](https://github.com/Koenkk/zigbee2mqtt/commit/564e9ab9eb49c3517913917af8b23d4758c6b6c7))
* **ignore:** update zigbee-herdsman-converters to 25.69.0 ([#29659](https://github.com/Koenkk/zigbee2mqtt/issues/29659)) ([16d0e56](https://github.com/Koenkk/zigbee2mqtt/commit/16d0e5696859e79be4701154dd35f7780e47acae))
* **ignore:** update zigbee-herdsman-converters to 25.70.0 ([#29684](https://github.com/Koenkk/zigbee2mqtt/issues/29684)) ([73225e2](https://github.com/Koenkk/zigbee2mqtt/commit/73225e2436bb56ff4ed47d787e8a1793fd10998d))
* **ignore:** update zigbee-herdsman-converters to 25.71.0 ([#29716](https://github.com/Koenkk/zigbee2mqtt/issues/29716)) ([d6bf517](https://github.com/Koenkk/zigbee2mqtt/commit/d6bf5174be905128432e90d54b3c43c2a68e75ac))
* **ignore:** update zigbee-herdsman-converters to 25.72.0 ([#29735](https://github.com/Koenkk/zigbee2mqtt/issues/29735)) ([7b7b988](https://github.com/Koenkk/zigbee2mqtt/commit/7b7b9889607a6e48001ae3cda5950386c24f5fd6))
* **ignore:** update zigbee-herdsman-converters to 25.73.0 ([#29752](https://github.com/Koenkk/zigbee2mqtt/issues/29752)) ([be774a4](https://github.com/Koenkk/zigbee2mqtt/commit/be774a4d0d70122ced0f6ca79f49c060a7d2d2cb))
* **ignore:** update zigbee-herdsman-converters to 25.74.0 ([#29770](https://github.com/Koenkk/zigbee2mqtt/issues/29770)) ([ba77ce7](https://github.com/Koenkk/zigbee2mqtt/commit/ba77ce7c8df0b29845a090c9defb4ce8aaf0f924))
* **ignore:** update zigbee-herdsman-converters to 25.75.0 ([#29811](https://github.com/Koenkk/zigbee2mqtt/issues/29811)) ([854df49](https://github.com/Koenkk/zigbee2mqtt/commit/854df498bd3c8999393b683052dfba6b906ba669))
* **ignore:** update zigbee-herdsman-converters to 25.76.0 ([#29823](https://github.com/Koenkk/zigbee2mqtt/issues/29823)) ([72d862f](https://github.com/Koenkk/zigbee2mqtt/commit/72d862f4a080bfcd91bf99d4c13387e719dfdb8e))
* **ignore:** update zigbee-herdsman-converters to 25.77.0 ([#29838](https://github.com/Koenkk/zigbee2mqtt/issues/29838)) ([35d8a5d](https://github.com/Koenkk/zigbee2mqtt/commit/35d8a5da7f1d817727f8b029da1992901f5b4115))
* **ignore:** update zigbee-herdsman-converters to 25.78.0 ([#29856](https://github.com/Koenkk/zigbee2mqtt/issues/29856)) ([49b4876](https://github.com/Koenkk/zigbee2mqtt/commit/49b4876768c5d15fcff1ad0c9c2d02dda96543d0))
* **ignore:** update zigbee-herdsman-converters to 25.79.0 ([#29903](https://github.com/Koenkk/zigbee2mqtt/issues/29903)) ([d411fc5](https://github.com/Koenkk/zigbee2mqtt/commit/d411fc5c693300863a19c2798f0a7a05cfc7e55f))
* **ignore:** update zigbee-herdsman-converters to 25.80.0 ([#29924](https://github.com/Koenkk/zigbee2mqtt/issues/29924)) ([445890a](https://github.com/Koenkk/zigbee2mqtt/commit/445890a0710ba7f6b101b651bf379f10390f9e7e))
* **ignore:** update zigbee2mqtt-windfront to 2.3.1 ([#29788](https://github.com/Koenkk/zigbee2mqtt/issues/29788)) ([17520f7](https://github.com/Koenkk/zigbee2mqtt/commit/17520f7cfb69b577ebd986b86da80931a15a39db))
* Reporting payload detection fixes ([#29854](https://github.com/Koenkk/zigbee2mqtt/issues/29854)) ([42fc6e0](https://github.com/Koenkk/zigbee2mqtt/commit/42fc6e03a7a3092502e3c3d1defbcb3ed6438230))
## [2.6.3](https://github.com/Koenkk/zigbee2mqtt/compare/2.6.2...2.6.3) (2025-11-01)
### Bug Fixes
* Add pi cooling demand to Home Assistant auto discovery ([#28843](https://github.com/Koenkk/zigbee2mqtt/issues/28843)) ([09fd675](https://github.com/Koenkk/zigbee2mqtt/commit/09fd675d0f215af11e64515ea60b91ed28b5e683))
* Fix `onEvent` called twice first time when device options are changed ([#29322](https://github.com/Koenkk/zigbee2mqtt/issues/29322)) ([e037a2c](https://github.com/Koenkk/zigbee2mqtt/commit/e037a2c21cb985b6e71d9c1fa169f0886e8af832))
* Fix input onboarding environment variable disabled check ([#29354](https://github.com/Koenkk/zigbee2mqtt/issues/29354)) ([c306300](https://github.com/Koenkk/zigbee2mqtt/commit/c30630091d1ac14fed6df9c5ee43a275db877821))
* Home Assistant: fix device and group configuration URL ([#29211](https://github.com/Koenkk/zigbee2mqtt/issues/29211)) ([c0190c1](https://github.com/Koenkk/zigbee2mqtt/commit/c0190c18ede7accb407b805f6016c3242c11146a))
* Home Assistant: make PI heating demand from writable ([#29188](https://github.com/Koenkk/zigbee2mqtt/issues/29188)) ([174ba64](https://github.com/Koenkk/zigbee2mqtt/commit/174ba64449b04777d1d5743fbb7b498a29c40ae2))
* **ignore:** bump @types/node from 24.7.2 to 24.8.1 in the minor-patch group ([#29191](https://github.com/Koenkk/zigbee2mqtt/issues/29191)) ([d8fc69d](https://github.com/Koenkk/zigbee2mqtt/commit/d8fc69dac3cba33f3d69e377e338e03facbc19e7))
* **ignore:** bump @types/serve-static from 1.15.10 to 2.2.0 ([#29394](https://github.com/Koenkk/zigbee2mqtt/issues/29394)) ([b2c31f8](https://github.com/Koenkk/zigbee2mqtt/commit/b2c31f89c69cfe04d0d671235bd18eb3cfa7ed72))
* **ignore:** bump the minor-patch group across 1 directory with 4 updates ([#29395](https://github.com/Koenkk/zigbee2mqtt/issues/29395)) ([dc7749b](https://github.com/Koenkk/zigbee2mqtt/commit/dc7749b57e188835f1243fad4b1d9dcccd7dca7d))
* **ignore:** bump the minor-patch group with 3 updates ([#29068](https://github.com/Koenkk/zigbee2mqtt/issues/29068)) ([aa916ad](https://github.com/Koenkk/zigbee2mqtt/commit/aa916ad60451fea471217e65fbdbb38635879c8e))
* **ignore:** bump the minor-patch group with 4 updates ([#28916](https://github.com/Koenkk/zigbee2mqtt/issues/28916)) ([e24ec79](https://github.com/Koenkk/zigbee2mqtt/commit/e24ec79980535d62b02e849359e23a189f2255ed))
* **ignore:** bump the minor-patch group with 5 updates ([#29314](https://github.com/Koenkk/zigbee2mqtt/issues/29314)) ([82f1092](https://github.com/Koenkk/zigbee2mqtt/commit/82f10923027806f5cd3b31e27184883e07768c71))
* **ignore:** update zigbee-herdsman to 6.2.0 ([#28900](https://github.com/Koenkk/zigbee2mqtt/issues/28900)) ([8b4a4be](https://github.com/Koenkk/zigbee2mqtt/commit/8b4a4be2a4404a8b165bb62c43bca73894d6b13f))
* **ignore:** update zigbee-herdsman to 6.3.0 ([#29113](https://github.com/Koenkk/zigbee2mqtt/issues/29113)) ([354d06a](https://github.com/Koenkk/zigbee2mqtt/commit/354d06aebc8fc786309ce9d66403e335799d46ab))
* **ignore:** update zigbee-herdsman to 6.3.1 ([#29153](https://github.com/Koenkk/zigbee2mqtt/issues/29153)) ([feac6aa](https://github.com/Koenkk/zigbee2mqtt/commit/feac6aa677cb169bf3c9b47aa4e6687ea8e6432d))
* **ignore:** update zigbee-herdsman to 6.3.2 ([#29246](https://github.com/Koenkk/zigbee2mqtt/issues/29246)) ([ef15924](https://github.com/Koenkk/zigbee2mqtt/commit/ef15924260cf2aa0094f2f93db0ddd8fcfa0ac6f))
* **ignore:** update zigbee-herdsman-converters to 25.36.0 ([#28790](https://github.com/Koenkk/zigbee2mqtt/issues/28790)) ([ca489da](https://github.com/Koenkk/zigbee2mqtt/commit/ca489daad8def2683f4a5c248e8098d01cc7cc6f))
* **ignore:** update zigbee-herdsman-converters to 25.37.0 ([#28837](https://github.com/Koenkk/zigbee2mqtt/issues/28837)) ([d1ce146](https://github.com/Koenkk/zigbee2mqtt/commit/d1ce1465421f81a68e03ffe76e928e2ea33cdde0))
* **ignore:** update zigbee-herdsman-converters to 25.37.1 ([#28857](https://github.com/Koenkk/zigbee2mqtt/issues/28857)) ([c66008b](https://github.com/Koenkk/zigbee2mqtt/commit/c66008b36bccdc1626a7f095f1af498b100610e3))
* **ignore:** update zigbee-herdsman-converters to 25.37.2 ([#28859](https://github.com/Koenkk/zigbee2mqtt/issues/28859)) ([1d38da6](https://github.com/Koenkk/zigbee2mqtt/commit/1d38da6ac5ac6a54c1c13a09003545f9accbfa7a))
* **ignore:** update zigbee-herdsman-converters to 25.38.0 ([#28872](https://github.com/Koenkk/zigbee2mqtt/issues/28872)) ([a75a40b](https://github.com/Koenkk/zigbee2mqtt/commit/a75a40b042c8276e2b52c666b555fae4f656fa04))
* **ignore:** update zigbee-herdsman-converters to 25.39.0 ([#28899](https://github.com/Koenkk/zigbee2mqtt/issues/28899)) ([548f646](https://github.com/Koenkk/zigbee2mqtt/commit/548f646f2fc1f53e18f84aec817f845de8edb04d))
* **ignore:** update zigbee-herdsman-converters to 25.40.0 ([#28928](https://github.com/Koenkk/zigbee2mqtt/issues/28928)) ([5f08c3e](https://github.com/Koenkk/zigbee2mqtt/commit/5f08c3e83beaa98f6e5cf5fd32e106730e86f611))
* **ignore:** update zigbee-herdsman-converters to 25.41.0 ([#28945](https://github.com/Koenkk/zigbee2mqtt/issues/28945)) ([79a27e3](https://github.com/Koenkk/zigbee2mqtt/commit/79a27e3b060dd552110a1851252dd6a17524be9b))
* **ignore:** update zigbee-herdsman-converters to 25.42.0 ([#28963](https://github.com/Koenkk/zigbee2mqtt/issues/28963)) ([8b1f5a5](https://github.com/Koenkk/zigbee2mqtt/commit/8b1f5a555c571990e9e895b285060872ba27402f))
* **ignore:** update zigbee-herdsman-converters to 25.43.0 ([#29014](https://github.com/Koenkk/zigbee2mqtt/issues/29014)) ([9c19aa9](https://github.com/Koenkk/zigbee2mqtt/commit/9c19aa94948205489b50ac79063b2a065495a8a8))
* **ignore:** update zigbee-herdsman-converters to 25.44.0 ([#29050](https://github.com/Koenkk/zigbee2mqtt/issues/29050)) ([bafc8e4](https://github.com/Koenkk/zigbee2mqtt/commit/bafc8e4284d9493388c8ad8497bb55781238c547))
* **ignore:** update zigbee-herdsman-converters to 25.46.0 ([#29096](https://github.com/Koenkk/zigbee2mqtt/issues/29096)) ([ad9b114](https://github.com/Koenkk/zigbee2mqtt/commit/ad9b114b1af8225e8e1414ffa23b322271fb1694))
* **ignore:** update zigbee-herdsman-converters to 25.47.0 ([#29130](https://github.com/Koenkk/zigbee2mqtt/issues/29130)) ([03a81c6](https://github.com/Koenkk/zigbee2mqtt/commit/03a81c6ab14d9ae7b2a9fdd7513dcbbe55cdc16c))
* **ignore:** update zigbee-herdsman-converters to 25.48.0 ([#29141](https://github.com/Koenkk/zigbee2mqtt/issues/29141)) ([1d69715](https://github.com/Koenkk/zigbee2mqtt/commit/1d69715a5a75779509f81788a1541dd4c492ca9e))
* **ignore:** update zigbee-herdsman-converters to 25.49.0 ([#29154](https://github.com/Koenkk/zigbee2mqtt/issues/29154)) ([3ecb0c5](https://github.com/Koenkk/zigbee2mqtt/commit/3ecb0c5e41b514bfb824926fc9cf19636ec68ff5))
* **ignore:** update zigbee-herdsman-converters to 25.50.0 ([#29180](https://github.com/Koenkk/zigbee2mqtt/issues/29180)) ([df70f11](https://github.com/Koenkk/zigbee2mqtt/commit/df70f11a154e27208188432be453fac821f73597))
* **ignore:** update zigbee-herdsman-converters to 25.51.0 ([#29195](https://github.com/Koenkk/zigbee2mqtt/issues/29195)) ([4080522](https://github.com/Koenkk/zigbee2mqtt/commit/408052282d9a28f46df33266112cf52dbc0f98c0))
* **ignore:** update zigbee-herdsman-converters to 25.51.1 ([#29212](https://github.com/Koenkk/zigbee2mqtt/issues/29212)) ([238ea7c](https://github.com/Koenkk/zigbee2mqtt/commit/238ea7c4c8073cb0164281cc0cdec05a3212d6ad))
* **ignore:** update zigbee-herdsman-converters to 25.52.0 ([#29247](https://github.com/Koenkk/zigbee2mqtt/issues/29247)) ([05382a9](https://github.com/Koenkk/zigbee2mqtt/commit/05382a969232d5c1a286ba1a85c6422d0a81a39f))
* **ignore:** update zigbee-herdsman-converters to 25.53.0 ([#29266](https://github.com/Koenkk/zigbee2mqtt/issues/29266)) ([d719c1f](https://github.com/Koenkk/zigbee2mqtt/commit/d719c1f36848c08efaad3eda8e90fb5d61c4c657))
* **ignore:** update zigbee-herdsman-converters to 25.54.0 ([#29306](https://github.com/Koenkk/zigbee2mqtt/issues/29306)) ([003c81b](https://github.com/Koenkk/zigbee2mqtt/commit/003c81b745be7f3262d6e778957aec6a9cf3a404))
* **ignore:** update zigbee-herdsman-converters to 25.55.0 ([#29323](https://github.com/Koenkk/zigbee2mqtt/issues/29323)) ([592c256](https://github.com/Koenkk/zigbee2mqtt/commit/592c256f96cca5219eb8620422f3ebf73fba3c1e))
* **ignore:** update zigbee-herdsman-converters to 25.56.0 ([#29338](https://github.com/Koenkk/zigbee2mqtt/issues/29338)) ([61d99b7](https://github.com/Koenkk/zigbee2mqtt/commit/61d99b7ece63af35055fabbd9c07d43b3cbc80f9))
* **ignore:** update zigbee-herdsman-converters to 25.57.0 ([#29370](https://github.com/Koenkk/zigbee2mqtt/issues/29370)) ([faa65f7](https://github.com/Koenkk/zigbee2mqtt/commit/faa65f78a29f5738a83d6fd67dd749f7d02adb39))
* **ignore:** update zigbee-herdsman-converters to 25.59.0 ([#29382](https://github.com/Koenkk/zigbee2mqtt/issues/29382)) ([40bf14b](https://github.com/Koenkk/zigbee2mqtt/commit/40bf14b93cb94292cce954916eba30f48d3170e2))
* Remove Moment.js dependency ([#28797](https://github.com/Koenkk/zigbee2mqtt/issues/28797)) ([74335fb](https://github.com/Koenkk/zigbee2mqtt/commit/74335fbb686edd63e5965c50b6cfc853abd07717))
* Rename `ZigBee` -&gt; `Zigbee` ([#29131](https://github.com/Koenkk/zigbee2mqtt/issues/29131)) ([eaaed1f](https://github.com/Koenkk/zigbee2mqtt/commit/eaaed1fec79401cb0b15b6e4bb4ddfba7bcf3d39))
* Require at least Node ^20.15.0 ([#29284](https://github.com/Koenkk/zigbee2mqtt/issues/29284)) ([8ac2f55](https://github.com/Koenkk/zigbee2mqtt/commit/8ac2f55d87c3bebbda31cf1e9f1806e98b78c114))
* Require at least Node ^22.2.0 ([#29285](https://github.com/Koenkk/zigbee2mqtt/issues/29285)) ([6c48498](https://github.com/Koenkk/zigbee2mqtt/commit/6c48498e0d1cbf49ab5ccc4d7539c932ea92a22e))
## [2.6.2](https://github.com/Koenkk/zigbee2mqtt/compare/2.6.1...2.6.2) (2025-10-01)
@@ -329,7 +836,7 @@
* Frontend: support filtering notifications ([#26805](https://github.com/Koenkk/zigbee2mqtt/issues/26805)) ([f872a51](https://github.com/Koenkk/zigbee2mqtt/commit/f872a5180c5618b7d3e7c0d11f1283a454dcbc6b))
* Onboarding ([#26608](https://github.com/Koenkk/zigbee2mqtt/issues/26608)) ([05d6f08](https://github.com/Koenkk/zigbee2mqtt/commit/05d6f08a0dd6a5097b7d19b8de3b39e37e9a8533))
* Support ZigBee on Host adapter ([#26742](https://github.com/Koenkk/zigbee2mqtt/issues/26742)) ([92955c0](https://github.com/Koenkk/zigbee2mqtt/commit/92955c02a4de09cbed79249db8bf0e6529423b93))
* Support Zigbee on Host adapter ([#26742](https://github.com/Koenkk/zigbee2mqtt/issues/26742)) ([92955c0](https://github.com/Koenkk/zigbee2mqtt/commit/92955c02a4de09cbed79249db8bf0e6529423b93))
* Use dynamic import for external JS ([#26522](https://github.com/Koenkk/zigbee2mqtt/issues/26522)) ([badf107](https://github.com/Koenkk/zigbee2mqtt/commit/badf1079919baa06f160a42c34e9f742a9b584c9))
+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-with-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).
+8 -8
View File
@@ -48,7 +48,7 @@ Zigbee2MQTT integrates well with (almost) every home automation solution because
### [Home Assistant](https://www.home-assistant.io/)
<img align="left" height="75px" width="75px" src="https://user-images.githubusercontent.com/7738048/40914297-49e6e560-6800-11e8-8904-36cce896e5a8.png">
<a href="https://www.home-assistant.io/"><img align="left" src="https://avatars.githubusercontent.com/u/13844975?s=75" style="padding-right: 20px;" alt="Home Assistant"></a>
<br clear="right">
@@ -58,7 +58,7 @@ Zigbee2MQTT integrates well with (almost) every home automation solution because
### [Homey](https://homey.app/)
<img align="left" height="75px" width="75px" src="https://etc.athom.com/logo/white/256.png">
<a href="https://homey.app/"><img align="left" src="https://avatars.githubusercontent.com/u/8502422?s=75" style="padding-right: 20px;" alt="Homey"></a>
<br clear="right">
@@ -68,7 +68,7 @@ Integration implemented in the [Homey App](https://homey.app/nl-nl/app/com.gruij
### [Domoticz](https://www.domoticz.com/)
<img align="left" height="75px" width="75px" src="https://user-images.githubusercontent.com/2734836/47615848-b8dd8700-dabd-11e8-9d77-175002dd8987.png">
<a href="https://www.domoticz.com/"><img align="left" src="https://avatars.githubusercontent.com/u/6067010?s=75" style="padding-right: 20px;" alt="Domoticz"></a>
<br clear="right">
@@ -78,7 +78,7 @@ Integration implemented in Domoticz ([documentation](https://www.domoticz.com/wi
### [Gladys Assistant](https://gladysassistant.com/)
<img align="left" height="75px" width="75px" src="./images/gladys-assistant-logo.jpg">
<a href="https://gladysassistant.com/"><img align="left" src="https://avatars.githubusercontent.com/u/7365162?s=75" style="padding-right: 20px;" alt="Gladys Assistant"></a>
<br clear="right">
@@ -86,13 +86,13 @@ Integration implemented natively in Gladys Assistant ([documentation](https://gl
<br clear="both">
### [IoBroker](https://www.iobroker.net/)
### [ioBroker](https://www.iobroker.net/)
<img align="left" height="75px" width="75px" src="https://forum.iobroker.net/assets/uploads/system/site-logo.png">
<a href="https://www.iobroker.net/"><img align="left" src="https://avatars.githubusercontent.com/u/7231148?s=75" style="padding-right: 20px;" alt="ioBroker"></a>
<br clear="right">
Integration implemented in IoBroker ([documentation](https://github.com/o0shojo0o/ioBroker.zigbee2mqtt)).
Integration implemented in ioBroker ([documentation](https://github.com/o0shojo0o/ioBroker.zigbee2mqtt)).
<br clear="both">
@@ -106,7 +106,7 @@ Zigbee2MQTT is made up of three modules, each developed in its own Github projec
### Developing
Zigbee2MQTT uses TypeScript. Therefore after making changes to files in the `lib/` directory you need to recompile Zigbee2MQTT. This can be done by executing `pnpm run build`. For faster development instead of running `pnpm run build` you can run `pnpm run build-watch` in another terminal session, this will recompile as you change files.
Zigbee2MQTT uses TypeScript. Therefore after making changes to files in the `lib/` directory you need to recompile Zigbee2MQTT. This can be done by executing `pnpm run build`. For faster development instead of running `pnpm run build` you can run `pnpm run build:watch` in another terminal session, this will recompile as you change files.
Before running any of the commands, you'll first need to run `pnpm install --include=dev`.
Before submitting changes run `pnpm run check:w` then `pnpm run test:coverage`.
+7 -2
View File
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.4/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"]
"includes": ["**", "!package.json", "!!**/dist", "!!**/coverage", "!images"]
},
"linter": {
"includes": ["**"],
@@ -72,6 +72,11 @@
"suspicious": {
"noConstEnum": "off",
"useAwait": "error"
},
"nursery": {
"useExhaustiveSwitchCases": "error",
"noFloatingPromises": "error",
"noMisusedPromises": "error"
}
}
},
+2 -2
View File
@@ -1,5 +1,5 @@
# Indicates the configuration version (used by configuration migrations)
version: 4
version: 5
# Home Assistant integration (MQTT discovery)
homeassistant:
@@ -15,7 +15,7 @@ mqtt:
# MQTT base topic for zigbee2mqtt MQTT messages
base_topic: zigbee2mqtt
# MQTT server URL
server: 'mqtt://localhost'
server: "mqtt://localhost"
# MQTT server authentication, uncomment if required:
# user: my_user
# password: my_password
+4 -1
View File
@@ -1,6 +1,6 @@
ARG TARGETPLATFORM
FROM alpine:3.22 AS base
FROM alpine:3.24 AS base
ENV NODE_ENV=production
WORKDIR /app
@@ -52,5 +52,8 @@ RUN mkdir /app/data
ARG COMMIT
RUN echo "$COMMIT" > dist/.hash
# Expose the default frontend port (see lib/util/settings.ts defaults.frontend.port)
EXPOSE 8080
ENTRYPOINT ["docker-entrypoint.sh"]
CMD [ "/sbin/tini", "--", "node", "index.js"]
+8 -8
View File
@@ -7,7 +7,7 @@
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-12" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="AiRs1-JiV6ekzLb-Xg2S-1" target="AiRs1-JiV6ekzLb-Xg2S-6" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-76" value="ZigBee" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="AiRs1-JiV6ekzLb-Xg2S-12" vertex="1" connectable="0">
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-76" value="Zigbee" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="AiRs1-JiV6ekzLb-Xg2S-12" vertex="1" connectable="0">
<mxGeometry x="0.3" relative="1" as="geometry">
<mxPoint y="30" as="offset" />
</mxGeometry>
@@ -18,7 +18,7 @@
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;startArrow=classic;startFill=1;" parent="1" source="AiRs1-JiV6ekzLb-Xg2S-2" target="AiRs1-JiV6ekzLb-Xg2S-6" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-72" value="ZigBee" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="AiRs1-JiV6ekzLb-Xg2S-10" vertex="1" connectable="0">
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-72" value="Zigbee" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="AiRs1-JiV6ekzLb-Xg2S-10" vertex="1" connectable="0">
<mxGeometry x="0.3" relative="1" as="geometry">
<mxPoint y="-30" as="offset" />
</mxGeometry>
@@ -29,7 +29,7 @@
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0.75;entryY=0;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="AiRs1-JiV6ekzLb-Xg2S-3" target="AiRs1-JiV6ekzLb-Xg2S-6" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-73" value="ZigBee" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="AiRs1-JiV6ekzLb-Xg2S-11" vertex="1" connectable="0">
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-73" value="Zigbee" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="AiRs1-JiV6ekzLb-Xg2S-11" vertex="1" connectable="0">
<mxGeometry x="-0.1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
@@ -42,7 +42,7 @@
</Array>
</mxGeometry>
</mxCell>
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-70" value="ZigBee Binding" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="AiRs1-JiV6ekzLb-Xg2S-68" vertex="1" connectable="0">
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-70" value="Zigbee Binding" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="AiRs1-JiV6ekzLb-Xg2S-68" vertex="1" connectable="0">
<mxGeometry x="0.1431" relative="1" as="geometry">
<mxPoint y="60" as="offset" />
</mxGeometry>
@@ -50,7 +50,7 @@
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-69" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;dashed=1;" parent="1" source="AiRs1-JiV6ekzLb-Xg2S-3" target="AiRs1-JiV6ekzLb-Xg2S-2" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-71" value="ZigBee Binding" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="AiRs1-JiV6ekzLb-Xg2S-69" vertex="1" connectable="0">
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-71" value="Zigbee Binding" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="AiRs1-JiV6ekzLb-Xg2S-69" vertex="1" connectable="0">
<mxGeometry x="0.0914" relative="1" as="geometry">
<mxPoint y="10" as="offset" />
</mxGeometry>
@@ -61,7 +61,7 @@
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-13" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="AiRs1-JiV6ekzLb-Xg2S-4" target="AiRs1-JiV6ekzLb-Xg2S-6" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-75" value="ZigBee" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="AiRs1-JiV6ekzLb-Xg2S-13" vertex="1" connectable="0">
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-75" value="Zigbee" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="AiRs1-JiV6ekzLb-Xg2S-13" vertex="1" connectable="0">
<mxGeometry x="-0.1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
@@ -72,7 +72,7 @@
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="AiRs1-JiV6ekzLb-Xg2S-5" target="AiRs1-JiV6ekzLb-Xg2S-6" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-74" value="ZigBee" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="AiRs1-JiV6ekzLb-Xg2S-14" vertex="1" connectable="0">
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-74" value="Zigbee" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="AiRs1-JiV6ekzLb-Xg2S-14" vertex="1" connectable="0">
<mxGeometry x="0.3" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
@@ -88,7 +88,7 @@
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-6" value="&lt;div&gt;ZigBee&lt;/div&gt;&lt;div&gt;Coordinator&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;(zStack, EmberZNet...)&lt;/font&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;fontSize=14;aspect=fixed;" parent="1" vertex="1">
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-6" value="&lt;div&gt;Zigbee&lt;/div&gt;&lt;div&gt;Coordinator&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;(zStack, EmberZNet...)&lt;/font&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;fontSize=14;aspect=fixed;" parent="1" vertex="1">
<mxGeometry x="1075" y="375" width="150" height="150" as="geometry" />
</mxCell>
<mxCell id="AiRs1-JiV6ekzLb-Xg2S-33" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="AiRs1-JiV6ekzLb-Xg2S-7" target="AiRs1-JiV6ekzLb-Xg2S-9" edge="1">
+31 -22
View File
@@ -1,21 +1,18 @@
const semver = require("semver");
const engines = require("./package.json").engines;
const fs = require("node:fs");
const os = require("node:os");
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;
let stopping = false;
const watchdog = process.env.Z2M_WATCHDOG != null;
let watchdogCount = 0;
let unsolicitedStop = false;
// csv in minutes, default: 1min, 5min, 15min, 30min, 60min
let watchdogDelays = [2000, 60000, 300000, 900000, 1800000, 3600000];
if (watchdog && process.env.Z2M_WATCHDOG !== "default") {
if (/^\d+(.\d+)?(,\d+(.\d+)?)*$/.test(process.env.Z2M_WATCHDOG)) {
if (process.env.Z2M_WATCHDOG != null && process.env.Z2M_WATCHDOG !== "default") {
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'.`);
@@ -25,6 +22,7 @@ if (watchdog && process.env.Z2M_WATCHDOG !== "default") {
const hashFile = path.join(__dirname, "dist", ".hash");
/** @type {(code: number) => Promise<void>} */
async function triggerWatchdog(code) {
const delay = watchdogDelays[watchdogCount];
watchdogCount += 1;
@@ -41,14 +39,16 @@ async function triggerWatchdog(code) {
}
}
/** @type {() => Promise<void>} */
async function restart() {
await stop(true);
await start();
}
/** @type {(code: number, restart?: boolean) => Promise<void>} */
async function exit(code, restart = false) {
if (!restart) {
if (watchdog && unsolicitedStop) {
if (process.env.Z2M_WATCHDOG != null && unsolicitedStop) {
await triggerWatchdog(code);
} else {
process.exit(code);
@@ -56,6 +56,7 @@ async function exit(code, restart = false) {
}
}
/** @type {() => Promise<string>} */
async function currentHash() {
return await new Promise((resolve) => {
exec("git rev-parse --short=8 HEAD", (error, stdout) => {
@@ -70,20 +71,23 @@ async function currentHash() {
});
}
/** @type {() => Promise<void>} */
async function writeHash() {
const hash = await currentHash();
fs.writeFileSync(hashFile, hash);
}
/** @type {(reason: "initial build" | "hash changed") => Promise<void>} */
async function build(reason) {
process.stdout.write(`Building Zigbee2MQTT... (${reason})`);
const {totalmem} = await import("node:os");
return await new Promise((resolve, reject) => {
const env = {...process.env};
const mb600 = 629145600;
if (mb600 > os.totalmem() && !env.NODE_OPTIONS) {
if (mb600 > totalmem() && !env.NODE_OPTIONS) {
// Prevent OOM on tsc compile for system with low memory
// https://github.com/Koenkk/zigbee2mqtt/issues/12034
env.NODE_OPTIONS = "--max_old_space_size=256";
@@ -107,6 +111,7 @@ async function build(reason) {
});
}
/** @type {() => Promise<void>} */
async function checkDist() {
if (!fs.existsSync(hashFile)) {
await build("initial build");
@@ -120,20 +125,22 @@ async function checkDist() {
}
}
/** @type {() => Promise<void>} */
async function start() {
console.log(`Starting Zigbee2MQTT ${watchdog ? `with watchdog (${watchdogDelays})` : "without watchdog"}.`);
console.log(`Starting Zigbee2MQTT ${process.env.Z2M_WATCHDOG != null ? `with watchdog (${watchdogDelays})` : "without watchdog"}.`);
await checkDist();
// gc
{
const version = engines.node;
const packageJSON = (await import("./package.json", {with: {type: "json"}})).default;
const version = packageJSON.engines.node;
const {satisfies} = await import("semver");
if (!semver.satisfies(process.version, version)) {
if (!satisfies(process.version, version)) {
console.log(`\t\tZigbee2MQTT requires node version ${version}, you are running ${process.version}!\n`);
}
const {onboard} = require("./dist/util/onboarding");
const {onboard} = await import("./dist/util/onboarding.js");
const success = await onboard();
if (!success) {
@@ -143,7 +150,7 @@ async function start() {
}
}
const {Controller} = require("./dist/controller");
const {Controller} = await import("./dist/controller.js");
controller = new Controller(restart, exit);
await controller.start();
@@ -153,32 +160,34 @@ async function start() {
watchdogCount = 0; // reset
}
async function stop(restart) {
/** @type {(restart: boolean, signal?: NodeJS.Signals) => Promise<void>} */
async function stop(restart, signal = undefined) {
// `handleQuit` or `restart` never unsolicited
unsolicitedStop = false;
await controller.stop(restart);
await controller?.stop(restart, undefined, signal);
}
async function handleQuit() {
/** @type {(signal: NodeJS.Signals) => Promise<void>} */
async function handleQuit(signal) {
if (!stopping) {
if (controller) {
stopping = true;
await stop(false);
await stop(false, signal);
} else {
process.exit(0);
}
}
}
if (require.main === module || require.main.filename.endsWith(`${path.sep}cli.js`)) {
if (require.main === module || require.main?.filename.endsWith(`${path.sep}cli.js`)) {
if (process.argv.length === 3 && process.argv[2] === "writehash") {
writeHash();
void writeHash();
} else {
process.on("SIGINT", handleQuit);
process.on("SIGTERM", handleQuit);
start();
void start();
}
} else {
process.on("SIGINT", handleQuit);
+100 -43
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,10 +23,13 @@ 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";
export class Controller {
/** Allows canceling in-progress startup sequence if necessary. Signal can be passed to long-running ops as necessary for finer control. */
#startAbortController: AbortController | undefined;
public readonly eventBus: EventBus;
public readonly zigbee: Zigbee;
public readonly state: State;
@@ -48,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,
@@ -60,25 +61,35 @@ 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> {
this.#startAbortController = new AbortController();
const abortSignal = this.#startAbortController.signal;
if (settings.get().frontend.enabled) {
const {Frontend} = await import("./extension/frontend.js");
@@ -93,29 +104,48 @@ export class Controller {
this.state.start();
if (abortSignal.aborted) {
logger.info(`Zigbee2MQTT start aborted, reason=${abortSignal.reason}`);
return;
}
const info = await utils.getZigbee2MQTTVersion();
logger.info(`Starting Zigbee2MQTT version ${info.version} (commit #${info.commitHash})`);
// Start zigbee
try {
await this.zigbee.start();
this.eventBus.onAdapterDisconnected(this, this.onZigbeeAdapterDisconnected);
} catch (error) {
logger.error("Failed to start zigbee-herdsman");
logger.error(
"Check https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start_crashes-runtime.html for possible solutions",
);
logger.error("Exiting...");
// biome-ignore lint/style/noNonNullAssertion: always Error
logger.error((error as Error).stack!);
const started = await this.zigbee.start(abortSignal);
/* v8 ignore start */
if ((error as Error).message.includes("USB adapter discovery error (No valid USB adapter found)")) {
logger.error("If this happens after updating to Zigbee2MQTT 2.0.0, see https://github.com/Koenkk/zigbee2mqtt/discussions/24364");
if (started) {
this.eventBus.onAdapterDisconnected(this, async () => {
logger.error("Adapter disconnected, stopping");
await this.stop(false, 2);
});
}
/* v8 ignore stop */
} catch (error) {
// skip if aborted by `stop`
if (error !== "SIGINT" && error !== "SIGTERM" && error !== "STOPABORT") {
logger.error("Failed to start zigbee-herdsman");
logger.error(
"Check https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start_crashes-runtime.html for possible solutions",
);
logger.error("Exiting...");
// biome-ignore lint/style/noNonNullAssertion: always Error
logger.error((error as Error).stack!);
return await this.exit(1);
/* v8 ignore start */
if ((error as Error).message.includes("USB adapter discovery error (No valid USB adapter found)")) {
logger.error("If this happens after updating to Zigbee2MQTT 2.0.0, see https://github.com/Koenkk/zigbee2mqtt/discussions/24364");
}
/* v8 ignore stop */
return await this.exit(1);
}
}
if (abortSignal.aborted) {
logger.info(`Zigbee2MQTT start aborted, reason=${abortSignal.reason}`);
return;
}
// Log zigbee clients on startup
@@ -143,9 +173,19 @@ export class Controller {
return await this.exit(1);
}
if (abortSignal.aborted) {
logger.info(`Zigbee2MQTT start aborted, reason=${abortSignal.reason}`);
return;
}
// copy current Set of extensions to ignore possible external extensions added while looping
for (const extension of new Set(this.extensions)) {
await this.startExtension(extension);
if (abortSignal.aborted) {
logger.info(`Zigbee2MQTT start aborted, reason=${abortSignal.reason}`);
return;
}
}
// Send all cached states.
@@ -153,21 +193,32 @@ export class Controller {
for (const entity of this.zigbee.devicesAndGroupsIterator()) {
if (this.state.exists(entity)) {
await this.publishEntityState(entity, this.state.get(entity), "publishCached");
if (abortSignal.aborted) {
logger.info(`Zigbee2MQTT start aborted, reason=${abortSignal.reason}`);
return;
}
}
}
}
this.eventBus.onLastSeenChanged(this, (data) => utils.publishLastSeen(data, settings.get(), false, this.publishEntityState));
this.eventBus.onLastSeenChanged(this, (data) => {
utils.publishLastSeen(data, settings.get(), false, this.publishEntityState).catch(() => {});
});
logger.info("Zigbee2MQTT started!");
this.sdNotify = await initSdNotify();
settings.setOnboarding(false);
this.#startAbortController = undefined; // gc
}
@bind async enableDisableExtension(enable: boolean, name: string): Promise<void> {
if (enable) {
let extension: Extension;
switch (name) {
case "Frontend": {
if (!settings.get().frontend.enabled) {
@@ -178,7 +229,7 @@ export class Controller {
/* v8 ignore start */
const {Frontend} = await import("./extension/frontend.js");
await this.addExtension(new Frontend(...this.extensionArgs));
extension = new Frontend(...this.extensionArgs);
break;
/* v8 ignore stop */
@@ -190,7 +241,7 @@ export class Controller {
const {HomeAssistant} = await import("./extension/homeassistant.js");
await this.addExtension(new HomeAssistant(...this.extensionArgs));
extension = new HomeAssistant(...this.extensionArgs);
break;
}
@@ -200,6 +251,14 @@ export class Controller {
);
}
}
const existingExtension = this.getExtension(name);
if (existingExtension) {
await this.removeExtension(existingExtension);
}
this.extensions.add(extension);
} else {
switch (name) {
case "Frontend": {
@@ -281,7 +340,10 @@ export class Controller {
}
}
async stop(restart = false, code = 0): Promise<void> {
async stop(restart = false, code = 0, signal: NodeJS.Signals | undefined = undefined): Promise<void> {
logger.info(`Stopping Zigbee2MQTT (restart=${restart}, code=${code}, signal=${signal})`);
this.#startAbortController?.abort(signal ?? "STOPABORT");
this.sdNotify?.notifyStopping();
let localCode = 0;
@@ -317,11 +379,6 @@ export class Controller {
return await this.exitCallback(code, restart);
}
@bind async onZigbeeAdapterDisconnected(): Promise<void> {
logger.error("Adapter disconnected, stopping");
await this.stop(false, 2);
}
@bind async publishEntityState(entity: Group | Device, payload: KeyValue, stateChangeReason?: StateChangeReason): Promise<void> {
let message: Zigbee2MQTTAPI["{friendlyName}"] = {...payload};
@@ -405,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"])) {
@@ -423,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);
}
}
+7 -6
View File
@@ -61,7 +61,7 @@ export default class EventBus {
public emitAdapterDisconnected(): void {
this.emitter.emit("adapterDisconnected");
}
public onAdapterDisconnected(key: ListenerKey, callback: () => void): void {
public onAdapterDisconnected(key: ListenerKey, callback: () => Promise<void>): void {
this.on("adapterDisconnected", callback, key);
}
@@ -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);
}
}
}
+34 -6
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";
@@ -184,7 +184,7 @@ interface ParsedMQTTMessage {
}
export default class Bind extends Extension {
#topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind)`);
#topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind|binds/clear)`);
private pollDebouncers: {[s: string]: () => void} = {};
// biome-ignore lint/suspicious/useAwait: API
@@ -276,6 +276,36 @@ export default class Bind extends Extension {
}
@bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
if (data.topic.endsWith("binds/clear")) {
const message = JSON.parse(data.message) as Zigbee2MQTTAPI["bridge/request/device/binds/clear"];
if (typeof message !== "object" || typeof message.target !== "string") {
await this.publishResponse("binds/clear", message, {}, "Invalid payload");
return;
}
const target = this.zigbee.resolveEntity(message.target);
if (!(target instanceof Device)) {
await this.publishResponse("binds/clear", message, {}, "Invalid target");
return;
}
// this list is raw (not resolved) to allow clearing any specific target (not only currently known)
const eui64List = message.ieee_list ?? ["0xffffffffffffffff"];
await target.zh.clearAllBindings(eui64List);
const responseData: Zigbee2MQTTAPI["bridge/response/device/binds/clear"] = {
target: message.target,
ieee_list: eui64List,
};
await this.publishResponse("binds/clear", message, responseData);
this.eventBus.emitDevicesChanged();
return;
}
const [raw, parsed, error] = this.parseMQTTMessage(data);
if (!raw || !parsed) {
@@ -389,7 +419,7 @@ export default class Bind extends Extension {
}
private async publishResponse<T extends Zigbee2MQTTResponseEndpoints>(
type: ParsedMQTTMessage["type"],
type: ParsedMQTTMessage["type"] | "binds/clear",
request: KeyValue,
data: Zigbee2MQTTAPI[T],
error?: string,
@@ -488,9 +518,7 @@ export default class Bind extends Extension {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
for (const endpoint of device.zh.endpoints) {
for (const bind of endpoint.binds) {
allBinds.push(bind);
}
allBinds.push(...endpoint.binds);
}
}
+127 -46
View File
@@ -1,7 +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";
@@ -14,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";
@@ -31,7 +32,10 @@ export default class Bridge extends Extension {
private logTransport!: winston.transport;
private requestLookup: {[key: string]: (message: KeyValue | string) => Promise<Zigbee2MQTTResponse<Zigbee2MQTTResponseEndpoints>>} = {
"device/options": this.deviceOptions,
"device/configure_reporting": this.deviceConfigureReporting,
/** @deprecated 3.0 */
"device/configure_reporting": this.deviceReportingConfigure,
"device/reporting/configure": this.deviceReportingConfigure,
"device/reporting/read": this.deviceReportingRead,
"device/remove": this.deviceRemove,
"device/interview": this.deviceInterview,
"device/generate_external_definition": this.deviceGenerateExternalDefinition,
@@ -50,6 +54,7 @@ export default class Bridge extends Extension {
health_check: this.healthCheck,
coordinator_check: this.coordinatorCheck,
options: this.bridgeOptions,
action: this.action,
};
override async start(): Promise<void> {
@@ -136,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();
@@ -238,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) {
@@ -257,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});
}
@@ -315,18 +328,27 @@ export default class Bridge extends Extension {
@bind async backup(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/backup">> {
await this.zigbee.backup();
const dataPath = data.getPath();
const files = utils
.getAllFiles(dataPath)
.map((f) => [f, f.substring(dataPath.length + 1)])
.filter((f) => !f[1].startsWith("log"));
const zip = new JSZip();
const files = utils.getAllFiles(dataPath);
const zipFiles: Record<string, Uint8Array> = {};
const logDir = `log${path.sep}`;
const otaDir = `ota${path.sep}`;
for (const f of files) {
zip.file(f[1], fs.readFileSync(f[0]));
const name = f.slice(dataPath.length + 1);
// XXX: `log` could technically be something else depending on `log_directory` setting
if (!name.startsWith(logDir) && !name.startsWith(otaDir)) {
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">> {
@@ -455,27 +477,31 @@ 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});
}
@bind async deviceConfigureReporting(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/device/configure_reporting">> {
@bind async deviceReportingConfigure(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/device/reporting/configure">> {
if (
typeof message !== "object" ||
message.id === undefined ||
message.endpoint === undefined ||
message.cluster === undefined ||
message.maximum_report_interval === undefined ||
message.minimum_report_interval === undefined ||
message.reportable_change === undefined ||
message.attribute === undefined
message.attribute === undefined ||
typeof message.maximum_report_interval !== "number" ||
typeof message.minimum_report_interval !== "number" ||
(message.reportable_change !== undefined && typeof message.reportable_change !== "number")
) {
throw new Error("Invalid payload");
}
@@ -518,25 +544,54 @@ export default class Bridge extends Extension {
});
}
@bind async deviceReportingRead(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/device/reporting/read">> {
if (
typeof message !== "object" ||
message.id === undefined ||
message.endpoint === undefined ||
message.cluster === undefined ||
message.configs === undefined
) {
throw new Error("Invalid payload");
}
const device = this.getEntity("device", message.id);
const endpoint = device.endpoint(message.endpoint);
if (!endpoint) {
throw new Error(`Device '${device.ID}' does not have endpoint '${message.endpoint}'`);
}
const response = await endpoint.readReportingConfig(
message.cluster,
message.configs,
message.manufacturer_code ? {manufacturerCode: message.manufacturer_code} : {},
);
await this.publishDevices();
const responseData: Zigbee2MQTTAPI["bridge/response/device/reporting/read"] = {
id: message.id,
endpoint: message.endpoint,
cluster: message.cluster,
configs: response,
};
if (message.manufacturer_code) {
responseData.manufacturer_code = message.manufacturer_code;
}
return utils.getResponse(message, responseData);
}
@bind async deviceInterview(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/device/interview">> {
if (typeof message !== "object" || message.id === undefined) {
throw new Error("Invalid payload");
}
const device = this.getEntity("device", message.id);
logger.info(`Interviewing '${device.name}'`);
try {
await device.zh.interview(true);
logger.info(`Successfully interviewed '${device.name}'`);
} catch (error) {
throw new Error(`interview of '${device.name}' (${device.ieeeAddr}) failed: ${error}`, {cause: error});
}
// A re-interview can for example result in a different modelId, therefore reconsider the definition.
await device.resolveDefinition(true);
this.eventBus.emitDevicesChanged();
this.eventBus.emitExposesChanged({device});
await device.reInterview(this.eventBus);
return utils.getResponse(message, {id: message.id});
}
@@ -554,6 +609,22 @@ export default class Bridge extends Extension {
return utils.getResponse(message, {id: message.id, source});
}
@bind async action(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/action">> {
if (typeof message !== "object" || !message.action) {
throw new Error("Invalid payload");
}
const action = zhc.ACTIONS[message.action];
if (action === undefined) {
throw new Error("Invalid action");
}
const response = await action(this.zigbee.zhController, message.params ?? {});
return utils.getResponse(message, response);
}
async renameEntity<T extends "device" | "group">(
entityType: T,
message: string | KeyValue,
@@ -599,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})`;
}
@@ -625,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);
@@ -652,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(
@@ -812,6 +891,7 @@ export default class Bridge extends Extension {
const data: Zigbee2MQTTAPI["bridge/definitions"] = {
clusters: Zcl.Clusters,
custom_clusters: {},
actions: Object.keys(zhc.ACTIONS),
};
for (const device of this.zigbee.devicesIterator((d) => !utils.objectIsEmpty(d.customClusters))) {
@@ -845,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,
};
+34 -20
View File
@@ -1,10 +1,9 @@
import bind from "bind-decorator";
import stringify from "json-stable-stringify-without-jsonify";
import * as zhc from "zigbee-herdsman-converters";
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";
@@ -12,8 +11,8 @@ import Extension from "./extension";
* This extension calls the zigbee-herdsman-converters definition configure() method
*/
export default class Configure extends Extension {
private configuring = new Set();
private attempts: {[s: string]: number} = {};
private configuring = new Set<string>();
private attempts = new Map<string, number>();
private topic = `${settings.get().mqtt.base_topic}/bridge/request/device/configure`;
@bind private async onReconfigure(data: eventdata.Reconfigure): Promise<void> {
@@ -39,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 {
@@ -75,6 +74,7 @@ export default class Configure extends Extension {
await this.configure(data.device, "zigbee_event");
});
// TODO: this is triggering for any `data.status`, but should only for `successful`? (relies on `device.definition?` early-return?)
this.eventBus.onDeviceInterview(this, (data) => this.configure(data.device, "zigbee_event"));
this.eventBus.onLastSeenChanged(this, (data) => this.configure(data.device, "zigbee_event"));
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
@@ -93,42 +93,56 @@ export default class Configure extends Extension {
return;
}
const definitionVersion = device.definition.version;
if (!force) {
if (device.options.disabled || !device.interviewed) {
return;
}
if (device.zh.meta?.configured !== undefined) {
return;
}
// Only configure end devices when it is active, otherwise it will likely fails as they are sleeping.
if (device.zh.type === "EndDevice" && event !== "zigbee_event") {
return;
}
const shouldReconfigure =
// Should always reconfigure when not configured before
device.zh.meta?.configured === undefined ||
// Or should reconfigure when definition.version is not '0.0.0' and differs from last `meta.configured`.
// In older Z2M versions the stored `meta.configured` was the hash of the configure function.
// Since we don't want to reconfigure all devices, we don't re-configure when the definition has the default version of '0.0.0'.
(definitionVersion !== "0.0.0" && device.zh.meta?.configured !== definitionVersion);
if (!shouldReconfigure) {
return;
}
}
if (this.configuring.has(device.ieeeAddr) || (this.attempts[device.ieeeAddr] >= 3 && !force)) {
if (this.configuring.has(device.ieeeAddr)) {
return;
}
const attempts = this.attempts.get(device.ieeeAddr) ?? 0;
if (attempts >= 3 && !force) {
return;
}
this.configuring.add(device.ieeeAddr);
if (this.attempts[device.ieeeAddr] === undefined) {
this.attempts[device.ieeeAddr] = 0;
}
logger.info(`Configuring '${device.name}'`);
try {
await device.definition.configure(device.zh, this.zigbee.firstCoordinatorEndpoint(), device.definition);
logger.info(`Successfully configured '${device.name}'`);
device.zh.meta.configured = zhc.getConfigureKey(device.definition);
this.attempts.delete(device.ieeeAddr);
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) {
this.attempts[device.ieeeAddr]++;
const attempt = this.attempts[device.ieeeAddr];
const msg = `Failed to configure '${device.name}', attempt ${attempt} (${(error as Error).stack})`;
const newAttempts = attempts + 1;
this.attempts.set(device.ieeeAddr, newAttempts);
const msg = `Failed to configure '${device.name}', attempt ${newAttempts} (${(error as Error).stack})`;
logger.error(msg);
if (throwError) {
+10 -5
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();
@@ -202,14 +207,14 @@ export default abstract class ExternalJSExtension<M> extends Extension {
const mod = await this.importFile(filePath);
await this.loadJS(extension.name, mod.default);
} catch (error) {
// change ext so Z2M doesn't try to load it again and again
fs.renameSync(filePath, `${filePath}.invalid`);
logger.error(
`Invalid external ${this.mqttTopic} '${extension.name}' was ignored and renamed to prevent interference with Zigbee2MQTT. (${(error as Error).message})`,
);
// biome-ignore lint/style/noNonNullAssertion: always Error
logger.debug((error as Error).stack!);
// change ext so Z2M doesn't try to load it again and again
fs.renameSync(filePath, `${filePath}.invalid`);
}
}
}
+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
+31 -23
View File
@@ -1,9 +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";
@@ -91,6 +92,8 @@ export default class NetworkMap extends Extension {
}
labels.push(lastSeen);
// Escape backslashes and double-quotes to avoid breaking Graphviz .dot files.
const escapedLabels = labels.map((label) => label.replace(/\\/g, "\\\\").replace(/"/g, '\\"'));
// Shape the record according to device type
if (node.type === "Coordinator") {
@@ -102,7 +105,7 @@ export default class NetworkMap extends Extension {
}
// Add the device with its labels to the graph as a node.
text += ` "${node.ieeeAddr}" [${style}, label="{${labels.join("|")}}"];\n`;
text += ` "${node.ieeeAddr}" [${style}, label="{${escapedLabels.join("|")}}"];\n`;
/**
* Add an edge between the device and its child to the graph
@@ -185,8 +188,8 @@ export default class NetworkMap extends Extension {
async networkScan(includeRoutes: boolean): Promise<Zigbee2MQTTNetworkMap> {
logger.info(`Starting network scan (includeRoutes '${includeRoutes}')`);
const lqis = new Map<Device, zh.LQI>();
const routingTables = new Map<Device, zh.RoutingTable>();
const lqis = new Map<Device, LQITableEntry[]>();
const routingTables = new Map<Device, RoutingTableEntry[]>();
const failed = new Map<Device, string[]>();
const requestWithRetry = async <T>(request: () => Promise<T>): Promise<T> => {
try {
@@ -210,7 +213,7 @@ export default class NetworkMap extends Extension {
await utils.sleep(1); // sleep 1 second between each scan to reduce stress on network.
try {
const result = await requestWithRetry<zh.LQI>(async () => await device.zh.lqi());
const result = await requestWithRetry<LQITableEntry[]>(async () => await device.zh.lqi());
lqis.set(device, result);
logger.debug(`LQI succeeded for '${device.name}'`);
} catch (error) {
@@ -222,7 +225,7 @@ export default class NetworkMap extends Extension {
if (includeRoutes) {
try {
const result = await requestWithRetry<zh.RoutingTable>(async () => await device.zh.routingTable());
const result = await requestWithRetry<RoutingTableEntry[]>(async () => await device.zh.routingTable());
routingTables.set(device, result);
logger.debug(`Routing table succeeded for '${device.name}'`);
} catch (error) {
@@ -274,42 +277,47 @@ export default class NetworkMap extends Extension {
}
// Add links
for (const [device, lqi] of lqis) {
for (const neighbor of lqi.neighbors) {
for (const [device, table] of lqis) {
for (const neighbor of table) {
if (neighbor.relationship > 3) {
// Relationship is not active, skip it
continue;
}
let neighborEui64 = neighbor.eui64;
// Some Xiaomi devices return 0x00 as the neighbor ieeeAddr (obviously not correct).
// Determine the correct ieeeAddr based on the networkAddress.
if (neighbor.ieeeAddr === "0x0000000000000000") {
const neighborDevice = this.zigbee.deviceByNetworkAddress(neighbor.networkAddress);
if (neighborEui64 === "0x0000000000000000") {
const neighborDevice = this.zigbee.deviceByNetworkAddress(neighbor.nwkAddress);
if (neighborDevice) {
neighbor.ieeeAddr = neighborDevice.ieeeAddr;
neighborEui64 = neighborDevice.ieeeAddr as Eui64;
}
}
const link: Zigbee2MQTTNetworkMap["links"][number] = {
source: {ieeeAddr: neighbor.ieeeAddr, networkAddress: neighbor.networkAddress},
source: {ieeeAddr: neighborEui64, networkAddress: neighbor.nwkAddress},
target: {ieeeAddr: device.ieeeAddr, networkAddress: device.zh.networkAddress},
linkquality: neighbor.linkquality,
depth: neighbor.depth,
routes: [],
// DEPRECATED:
sourceIeeeAddr: neighbor.ieeeAddr,
targetIeeeAddr: device.ieeeAddr,
sourceNwkAddr: neighbor.networkAddress,
lqi: neighbor.linkquality,
deviceType: neighbor.deviceType,
rxOnWhenIdle: neighbor.rxOnWhenIdle,
relationship: neighbor.relationship,
permitJoining: neighbor.permitJoining,
depth: neighbor.depth,
lqi: neighbor.lqi,
routes: [],
// below are @deprecated
sourceIeeeAddr: neighborEui64,
targetIeeeAddr: device.ieeeAddr,
sourceNwkAddr: neighbor.nwkAddress,
linkquality: neighbor.lqi,
};
const routingTable = routingTables.get(device);
if (routingTable) {
for (const entry of routingTable.table) {
if (entry.status === "ACTIVE" && entry.nextHop === neighbor.networkAddress) {
for (const entry of routingTable) {
if (entry.nextHopAddress === neighbor.nwkAddress) {
link.routes.push(entry);
}
}
+10 -3
View File
@@ -37,6 +37,8 @@ export default class OnEvent extends Extension {
}
});
this.eventBus.onDeviceInterview(this, async (data) => {
// TODO: this is triggering for any `data.status`, any use outside `successful`?
// ZHC definition would skip from `device.definition?` but OnEvent from ZHC index wouldn't => triple triggering?
await this.callOnEvent(data.device, {type: "deviceInterview", data: {...this.#getOnEventBaseData(data.device), status: data.status}});
});
this.eventBus.onDeviceAnnounce(this, async (data) => {
@@ -59,8 +61,11 @@ export default class OnEvent extends Extension {
override async stop(): Promise<void> {
await super.stop();
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
await this.callOnEvent(device, {type: "stop", data: {ieeeAddr: device.ieeeAddr}});
// ensure properly started, else this throws undesired errors (e.g. SIGINT during startup)
if (this.zigbee.zhController !== undefined) {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
await this.callOnEvent(device, {type: "stop", data: {ieeeAddr: device.ieeeAddr}});
}
}
}
@@ -69,7 +74,9 @@ export default class OnEvent extends Extension {
return;
}
if (event.type !== "start" && event.type !== "stop" && !this.#startCalled.has(device.ieeeAddr) && device.definition?.onEvent) {
if (event.type === "start") {
this.#startCalled.add(device.ieeeAddr);
} else if (event.type !== "stop" && !this.#startCalled.has(device.ieeeAddr) && device.definition?.onEvent) {
this.#startCalled.add(device.ieeeAddr);
await device.definition.onEvent({type: "start", data: this.#getOnEventBaseData(device)});
}
+307 -205
View File
@@ -1,15 +1,15 @@
import assert from "node:assert";
import path from "node:path";
import {existsSync, mkdirSync, rmSync, writeFileSync} from "node:fs";
import {join} from "node:path";
import bind from "bind-decorator";
import stringify from "json-stable-stringify-without-jsonify";
import {Zcl} from "zigbee-herdsman";
import type {Ota} from "zigbee-herdsman-converters";
import {ota} from "zigbee-herdsman-converters";
import {setOtaConfiguration, Zcl} from "zigbee-herdsman";
import type {OtaDataSettings, OtaSource, OtaUpdateAvailableResult} from "zigbee-herdsman/dist/controller/tstype";
import Device from "../model/device";
import type {Zigbee2MQTTAPI} from "../types/api";
import dataDir from "../util/data";
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";
@@ -22,54 +22,65 @@ export interface UpdatePayload {
state: UpdateState;
installed_version: number | null;
latest_version: number | null;
latest_source: string | null;
latest_release_notes: string | null;
};
}
/**
* Write to `dataDir` and return created path
*/
function writeFirmwareHexToDataDir(hex: string, fileName: string | undefined, deviceIeee: string): string {
if (!fileName) {
fileName = `${deviceIeee}_${Date.now()}`;
}
const baseDir = dataDir.joinPath("ota");
if (!existsSync(baseDir)) {
mkdirSync(baseDir, {recursive: true});
}
const filePath = join(baseDir, fileName);
writeFileSync(filePath, Buffer.from(hex, "hex"));
return filePath;
}
export default class OTAUpdate extends Extension {
#topicRegex = new RegExp(
`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check|schedule|unschedule)/?(downgrade)?`,
`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check|schedule|unschedule)/?(downgrade|abort)?`,
"i",
);
private inProgress = new Set<string>();
private lastChecked = new Map<string, number>();
private scheduledUpgrades = new Set<string>();
private scheduledDowngrades = new Set<string>();
#inProgress = new Set<string>();
#lastChecked = new Map<string, number>();
// biome-ignore lint/suspicious/useAwait: API
override async start(): Promise<void> {
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
this.eventBus.onDeviceMessage(this, this.onZigbeeEvent);
const otaSettings = settings.get().ota;
// Let OTA module know if the override index file is provided
let overrideIndexLocation = otaSettings.zigbee_ota_override_index_location;
// If the file name is not a full path, then treat it as a relative to the data directory
if (overrideIndexLocation && !ota.isValidUrl(overrideIndexLocation) && !path.isAbsolute(overrideIndexLocation)) {
overrideIndexLocation = dataDir.joinPath(overrideIndexLocation);
}
// In order to support local firmware files we need to let zigbeeOTA know where the data directory is
ota.setConfiguration({
dataDir: dataDir.getPath(),
overrideIndexLocation,
// TODO: implement me
imageBlockResponseDelay: otaSettings.image_block_response_delay,
defaultMaximumDataSize: otaSettings.default_maximum_data_size,
});
setOtaConfiguration(dataDir.getPath(), settings.get().ota.zigbee_ota_override_index_location);
// In case Zigbee2MQTT is restared during an update, progress and remaining values are still in state, remove them.
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
this.removeProgressAndRemainingFromState(device);
this.#removeProgressAndRemainingFromState(device);
// Reset update state, e.g. when Z2M restarted during update.
if (this.state.get(device).update?.state === "updating") {
this.state.get(device).update.state = "available";
this.state.get(device).update.state = "idle";
}
}
}
private removeProgressAndRemainingFromState(device: Device): void {
// mostly intended for testing
clearState(): void {
this.#inProgress.clear();
this.#lastChecked.clear();
}
#removeProgressAndRemainingFromState(device: Device): void {
const deviceState = this.state.get(device);
if (deviceState.update) {
@@ -79,7 +90,7 @@ export default class OTAUpdate extends Extension {
}
@bind private async onZigbeeEvent(data: eventdata.DeviceMessage): Promise<void> {
if (data.type !== "commandQueryNextImageRequest" || !data.device.definition || this.inProgress.has(data.device.ieeeAddr)) {
if (data.type !== "commandQueryNextImageRequest" || !data.device.definition || this.#inProgress.has(data.device.ieeeAddr)) {
return;
}
@@ -91,115 +102,84 @@ export default class OTAUpdate extends Extension {
logger.debug(`Device '${data.device.name}' requested OTA`);
if (data.device.definition.ota) {
if (this.scheduledUpgrades.has(data.device.ieeeAddr) || this.scheduledDowngrades.has(data.device.ieeeAddr)) {
this.inProgress.add(data.device.ieeeAddr);
if (data.device.zh.scheduledOta) {
// allow custom source to override check for definition `ota`
if (data.device.zh.scheduledOta?.url !== undefined || data.device.definition.ota) {
this.#inProgress.add(data.device.ieeeAddr);
logger.info(`Updating '${data.device.name}' to latest firmware`);
try {
const fileVersion = await ota.update(
data.device.zh,
data.device.otaExtraMetas,
this.scheduledDowngrades.has(data.device.ieeeAddr),
async (progress, remaining) => {
let msg = `Update of '${data.device.name}' at ${progress.toFixed(2)}%`;
if (remaining) {
msg += `, ≈ ${Math.round(remaining / 60)} minutes remaining`;
}
logger.info(msg);
await this.publishEntityState(
data.device,
this.getEntityPublishPayload(data.device, "updating", progress, remaining ?? undefined),
);
},
data.data as Ota.ImageInfo,
data.meta.zclTransactionSequenceNumber,
);
// remove right away on update success or no image in case any of the below calls fail
this.scheduledUpgrades.delete(data.device.ieeeAddr);
this.scheduledDowngrades.delete(data.device.ieeeAddr);
if (fileVersion === undefined) {
logger.info(`No image currently available for '${data.device.name}'. Unscheduling.`);
// XXX: superfluous?
this.removeProgressAndRemainingFromState(data.device);
await this.publishEntityState(data.device, this.getEntityPublishPayload(data.device, "idle"));
this.inProgress.delete(data.device.ieeeAddr);
return;
}
logger.info(`Finished update of '${data.device.name}'`);
this.removeProgressAndRemainingFromState(data.device);
await this.publishEntityState(
const otaSettings = settings.get().ota;
const [, toVersion] = await this.#update(
undefined, // uses internally registered schedule
data.device,
this.getEntityPublishPayload(data.device, {available: false, currentFileVersion: fileVersion, otaFileVersion: fileVersion}),
data.data as Zcl.ClustersTypes.TClusterCommandPayload<"genOta", "queryNextImageRequest">,
data.meta.zclTransactionSequenceNumber,
{
// fallbacks are only to satisfy typing, should always be defined from settings defaults
requestTimeout: otaSettings.image_block_request_timeout ?? /* v8 ignore next */ 150000,
responseDelay: otaSettings.image_block_response_delay ?? /* v8 ignore next */ 250,
baseSize: otaSettings.default_maximum_data_size ?? /* v8 ignore next */ 50,
},
data.endpoint,
);
const firmwareTo = await this.readSoftwareBuildIDAndDateCode(data.device);
logger.info(() => `Device '${data.device.name}' was updated to '${stringify(firmwareTo)}'`);
/**
* Re-configure after reading software build ID and date code, some devices use a
* custom attribute for this (e.g. Develco SMSZB-120)
*/
this.eventBus.emitReconfigure({device: data.device});
this.eventBus.emitDevicesChanged();
if (toVersion === undefined) {
logger.info(`No OTA image currently available for '${data.device.name}'. Unscheduled.`);
}
} catch (e) {
logger.debug(`Update of '${data.device.name}' failed (${e}). Retry scheduled for next request.`);
logger.debug(`OTA update of '${data.device.name}' failed (${e}). Retry scheduled for next request.`);
this.removeProgressAndRemainingFromState(data.device);
await this.publishEntityState(data.device, this.getEntityPublishPayload(data.device, "scheduled"));
this.#removeProgressAndRemainingFromState(data.device);
await this.publishEntityState(data.device, this.#getEntityPublishPayload(data.device, "scheduled"));
}
this.inProgress.delete(data.device.ieeeAddr);
this.#inProgress.delete(data.device.ieeeAddr);
return; // we're done
}
}
if (!settings.get().ota.disable_automatic_update_check) {
if (data.device.definition.ota) {
if (!data.device.options.disable_automatic_update_check && !settings.get().ota.disable_automatic_update_check) {
// When a device does a next image request, it will usually do it a few times after each other
// with only 10 - 60 seconds inbetween. It doesn't make sense to check for a new update
// each time, so this interval can be set by the user. The default is 1,440 minutes (one day).
const updateCheckInterval = settings.get().ota.update_check_interval * 1000 * 60;
const deviceLastChecked = this.lastChecked.get(data.device.ieeeAddr);
const deviceLastChecked = this.#lastChecked.get(data.device.ieeeAddr);
const check = deviceLastChecked !== undefined ? Date.now() - deviceLastChecked > updateCheckInterval : true;
if (!check) {
return;
}
this.inProgress.add(data.device.ieeeAddr);
this.lastChecked.set(data.device.ieeeAddr, Date.now());
let availableResult: Ota.UpdateAvailableResult | undefined;
this.#inProgress.add(data.device.ieeeAddr);
this.#lastChecked.set(data.device.ieeeAddr, Date.now());
let availableResult: OtaUpdateAvailableResult | undefined;
try {
// never use 'previous' when responding to device request
availableResult = await ota.isUpdateAvailable(data.device.zh, data.device.otaExtraMetas, data.data as Ota.ImageInfo, false);
// auto-check defaults to zigbee-OTA + potential local index, and never `downgrade`
availableResult = await data.device.zh.checkOta(
{downgrade: false},
data.data as Zcl.ClustersTypes.TClusterCommandPayload<"genOta", "queryNextImageRequest">,
data.device.otaExtraMetas,
data.endpoint,
);
} catch (error) {
logger.debug(`Failed to check if update available for '${data.device.name}' (${error})`);
logger.debug(`Failed to check if OTA update available for '${data.device.name}' (${error})`);
}
await this.publishEntityState(data.device, this.getEntityPublishPayload(data.device, availableResult ?? "idle"));
await this.publishEntityState(data.device, this.#getEntityPublishPayload(data.device, availableResult ?? "idle"));
if (availableResult?.available) {
const message = `Update available for '${data.device.name}'`;
logger.info(message);
logger.info(`OTA update available for '${data.device.name}'`);
}
}
}
// Respond to stop the client from requesting OTAs
const endpoint = data.device.zh.endpoints.find((e) => e.supportsOutputCluster("genOta")) || data.endpoint;
await endpoint.commandResponse(
await data.endpoint.commandResponse(
"genOta",
"queryNextImageResponse",
{status: Zcl.Status.NO_IMAGE_AVAILABLE},
@@ -207,47 +187,53 @@ export default class OTAUpdate extends Extension {
data.meta.zclTransactionSequenceNumber,
);
logger.debug(`Responded to OTA request of '${data.device.name}' with 'NO_IMAGE_AVAILABLE'`);
this.inProgress.delete(data.device.ieeeAddr);
this.#inProgress.delete(data.device.ieeeAddr);
}
private async readSoftwareBuildIDAndDateCode(
async #readSoftwareBuildIDAndDateCode(
device: Device,
sendPolicy?: "immediate",
): Promise<{softwareBuildID: string; dateCode: string} | undefined> {
try {
const endpoint = device.zh.endpoints.find((e) => e.supportsInputCluster("genBasic"));
assert(endpoint);
const result = await endpoint.read("genBasic", ["dateCode", "swBuildId"], {sendPolicy});
return {softwareBuildID: result.swBuildId, dateCode: result.dateCode};
} catch {
return undefined;
}
}
private getEntityPublishPayload(
device: Device,
state: Ota.UpdateAvailableResult | UpdateState,
progress?: number,
remaining?: number,
): UpdatePayload {
const deviceUpdateState = this.state.get(device).update;
const payload: UpdatePayload = {
update: {
state: typeof state === "string" ? state : state.available ? "available" : "idle",
installed_version: typeof state === "string" ? deviceUpdateState?.installed_version : state.currentFileVersion,
latest_version: typeof state === "string" ? deviceUpdateState?.latest_version : state.otaFileVersion,
},
};
#getEntityPublishPayload(device: Device, state: OtaUpdateAvailableResult | UpdateState, progress?: number, remaining?: number): UpdatePayload {
const deviceUpdateState = this.state.get(device).update as UpdatePayload["update"];
const update: UpdatePayload["update"] =
typeof state === "string"
? {
state,
installed_version: deviceUpdateState?.installed_version,
latest_version: deviceUpdateState?.latest_version,
latest_source: deviceUpdateState?.latest_source,
latest_release_notes: deviceUpdateState?.latest_release_notes,
}
: {
state: state.available ? "available" : "idle",
installed_version: state.current.fileVersion,
latest_version: state.availableMeta?.fileVersion ?? state.current.fileVersion,
latest_source: state.availableMeta?.url || null,
latest_release_notes: state.availableMeta?.releaseNotes || null,
};
if (progress !== undefined) {
payload.update.progress = progress;
update.progress = progress;
}
if (remaining !== undefined) {
payload.update.remaining = Math.round(remaining);
update.remaining = Math.round(remaining);
}
return payload;
return {update};
}
@bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
@@ -262,46 +248,86 @@ 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"];
const ID = (typeof message === "object" && message.id !== undefined ? message.id : message) as string;
const device = this.zigbee.resolveEntity(ID);
// TODO: deprecated 3.0 should remove string payload, enforce object
const messageObject = typeof message === "object";
if (messageObject) {
assert(message.id, "Invalid payload");
}
const id = (messageObject ? message.id : message) as string;
const device = this.zigbee.resolveEntity(id);
const type = topicMatch[1] as "check" | "update" | "schedule" | "unschedule";
const downgrade = topicMatch[2] === "downgrade";
const abort = topicMatch[2] === "abort";
let error: string | undefined;
let errorStack: string | undefined;
if (!(device instanceof Device)) {
error = `Device '${ID}' does not exist`;
} else if (!device.definition || !device.definition.ota) {
error = `Device '${device.name}' does not support OTA updates`;
} else if (this.inProgress.has(device.ieeeAddr)) {
// also guards against scheduling while check/update op in progress that could result in undesired OTA state
error = `Update or check for update already in progress for '${device.name}'`;
error = `Device '${id}' does not exist`;
} else if (this.#inProgress.has(device.ieeeAddr)) {
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": {
this.inProgress.add(device.ieeeAddr);
this.#inProgress.add(device.ieeeAddr);
logger.info(`Checking if update available for '${device.name}'`);
const source: OtaSource = {downgrade};
if (messageObject) {
const payload = message as
| Zigbee2MQTTAPI["bridge/request/device/ota_update/check"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/check/downgrade"];
if (payload.url) {
source.url = payload.url;
} else if (!device.definition?.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
} else if (!device.definition?.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
logger.info(`Checking if OTA update available for '${device.name}'`);
try {
const availableResult = await ota.isUpdateAvailable(device.zh, device.otaExtraMetas, undefined, downgrade);
const availableResult = await device.zh.checkOta(source, undefined, device.otaExtraMetas);
logger.info(`${availableResult.available ? "Update" : "No update"} available for '${device.name}'`);
logger.info(`${availableResult.available ? "" : "No "}OTA update available for '${device.name}'`);
await this.publishEntityState(device, this.getEntityPublishPayload(device, availableResult));
this.lastChecked.set(device.ieeeAddr, Date.now());
await this.publishEntityState(device, this.#getEntityPublishPayload(device, availableResult));
this.#lastChecked.set(device.ieeeAddr, Date.now());
const response = utils.getResponse<"bridge/response/device/ota_update/check">(message, {
id: ID,
id,
update_available: availableResult.available,
downgrade: source.downgrade,
source: availableResult.availableMeta?.url,
release_notes: availableResult.availableMeta?.releaseNotes,
});
await this.mqtt.publish("bridge/response/device/ota_update/check", stringify(response));
} catch (e) {
error = `Failed to check if update available for '${device.name}' (${(e as Error).message})`;
error = `Failed to check if OTA update available for '${device.name}' (${(e as Error).message})`;
errorStack = (e as Error).stack;
}
@@ -309,94 +335,118 @@ export default class OTAUpdate extends Extension {
}
case "update": {
this.inProgress.add(device.ieeeAddr);
if (this.scheduledUpgrades.delete(device.ieeeAddr)) {
logger.info(`Previously scheduled '${device.name}' upgrade was cancelled by manual update`);
} else if (this.scheduledDowngrades.delete(device.ieeeAddr)) {
logger.info(`Previously scheduled '${device.name}' downgrade was cancelled by manual update`);
if (abort) {
error = `No OTA in progress to abort for device '${device.name}'`;
break;
}
logger.info(`Updating '${device.name}' to ${downgrade ? "previous" : "latest"} firmware`);
this.#inProgress.add(device.ieeeAddr);
try {
const firmwareFrom = await this.readSoftwareBuildIDAndDateCode(device, "immediate");
const fileVersion = await ota.update(device.zh, device.otaExtraMetas, downgrade, async (progress, remaining) => {
let msg = `Update of '${device.name}' at ${progress.toFixed(2)}%`;
const otaSettings = settings.get().ota;
const source: OtaSource = {downgrade};
const dataSettings: OtaDataSettings = {
// fallbacks are only to satisfy typing, should always be defined from settings defaults
requestTimeout: otaSettings.image_block_request_timeout ?? /* v8 ignore next */ 150000,
responseDelay: otaSettings.image_block_response_delay ?? /* v8 ignore next */ 250,
baseSize: otaSettings.default_maximum_data_size ?? /* v8 ignore next */ 50,
};
if (remaining) {
msg += `, ≈ ${Math.round(remaining / 60)} minutes remaining`;
}
if (messageObject) {
const payload = message as
| Zigbee2MQTTAPI["bridge/request/device/ota_update/update"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/update/downgrade"];
logger.info(msg);
if (payload.hex) {
assert(payload.hex.data);
await this.publishEntityState(device, this.getEntityPublishPayload(device, "updating", progress, remaining ?? undefined));
});
if (fileVersion === undefined) {
throw new Error("No image currently available");
// write to `dataDir` and pass created path as source URL
source.url = writeFirmwareHexToDataDir(payload.hex.data, payload.hex.file_name, device.ieeeAddr);
} else if (payload.url) {
source.url = payload.url;
} else if (!device.definition?.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
logger.info(`Finished update of '${device.name}'`);
this.removeProgressAndRemainingFromState(device);
await this.publishEntityState(
device,
this.getEntityPublishPayload(device, {available: false, currentFileVersion: fileVersion, otaFileVersion: fileVersion}),
);
if (payload.image_block_request_timeout) {
dataSettings.requestTimeout = payload.image_block_request_timeout;
}
const firmwareTo = await this.readSoftwareBuildIDAndDateCode(device);
if (payload.image_block_response_delay) {
dataSettings.responseDelay = payload.image_block_response_delay;
}
logger.info(() => `Device '${device.name}' was updated from '${stringify(firmwareFrom)}' to '${stringify(firmwareTo)}'`);
if (payload.default_maximum_data_size) {
dataSettings.baseSize = payload.default_maximum_data_size;
}
} else if (!device.definition?.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
/**
* Re-configure after reading software build ID and date code, some devices use a
* custom attribute for this (e.g. Develco SMSZB-120)
*/
this.eventBus.emitReconfigure({device});
this.eventBus.emitDevicesChanged();
logger.info(`OTA updating '${device.name}' to ${downgrade ? "previous" : "latest"} firmware`);
try {
const firmwareFrom = await this.#readSoftwareBuildIDAndDateCode(device, "immediate");
const [fromVersion, toVersion] = await this.#update(source, device, undefined, undefined, dataSettings);
if (toVersion === undefined) {
error = `Update of '${device.name}' failed (No image currently available)`;
break;
}
const firmwareTo = await this.#readSoftwareBuildIDAndDateCode(device);
const response = utils.getResponse<"bridge/response/device/ota_update/update">(message, {
id: ID,
from: firmwareFrom ? {software_build_id: firmwareFrom.softwareBuildID, date_code: firmwareFrom.dateCode} : undefined,
to: firmwareTo ? {software_build_id: firmwareTo.softwareBuildID, date_code: firmwareTo.dateCode} : undefined,
id,
from: {
file_version: fromVersion,
software_build_id: firmwareFrom?.softwareBuildID,
date_code: firmwareFrom?.dateCode,
},
to: {file_version: toVersion, software_build_id: firmwareTo?.softwareBuildID, date_code: firmwareTo?.dateCode},
});
await this.mqtt.publish("bridge/response/device/ota_update/update", stringify(response));
} catch (e) {
logger.debug(`Update of '${device.name}' failed (${e})`);
error = `Update of '${device.name}' failed (${(e as Error).message})`;
logger.debug(`OTA update of '${device.name}' failed (${e})`);
error = `OTA update of '${device.name}' failed (${(e as Error).message})`;
errorStack = (e as Error).stack;
this.removeProgressAndRemainingFromState(device);
await this.publishEntityState(device, this.getEntityPublishPayload(device, "available"));
this.#removeProgressAndRemainingFromState(device);
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "available"));
}
break;
}
case "schedule": {
// ensure only one type scheduled by deleting from the other if necessary
if (downgrade) {
if (this.scheduledUpgrades.delete(device.ieeeAddr)) {
logger.info(`Previously scheduled '${device.name}' upgrade was cancelled in favor of new downgrade request`);
}
const source: OtaSource = {downgrade};
this.scheduledDowngrades.add(device.ieeeAddr);
} else {
if (this.scheduledDowngrades.delete(device.ieeeAddr)) {
logger.info(`Previously scheduled '${device.name}' downgrade was cancelled in favor of new upgrade request`);
}
if (messageObject) {
const payload = message as
| Zigbee2MQTTAPI["bridge/request/device/ota_update/schedule"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/schedule/downgrade"];
this.scheduledUpgrades.add(device.ieeeAddr);
if (payload.hex) {
assert(payload.hex.data);
// write to `dataDir` and pass created path as source URL
source.url = writeFirmwareHexToDataDir(payload.hex.data, payload.hex.file_name, device.ieeeAddr);
} else if (payload.url) {
source.url = payload.url;
} else if (!device.definition?.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
} else if (!device.definition?.ota) {
error = `Device '${device.name}' does not support OTA updates`;
break;
}
logger.info(`Scheduled '${device.name}' to ${downgrade ? "downgrade" : "upgrade"} firmware on next request from device`);
device.zh.scheduleOta(source);
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "scheduled"));
await this.publishEntityState(device, this.getEntityPublishPayload(device, "scheduled", undefined, undefined));
const response = utils.getResponse<"bridge/response/device/ota_update/schedule">(message, {
id: ID,
});
const response = utils.getResponse<"bridge/response/device/ota_update/schedule">(message, {id, url: source.url});
await this.mqtt.publish("bridge/response/device/ota_update/schedule", stringify(response));
@@ -404,17 +454,14 @@ export default class OTAUpdate extends Extension {
}
case "unschedule": {
if (this.scheduledUpgrades.delete(device.ieeeAddr)) {
logger.info(`Previously scheduled '${device.name}' upgrade was cancelled`);
} else if (this.scheduledDowngrades.delete(device.ieeeAddr)) {
logger.info(`Previously scheduled '${device.name}' downgrade was cancelled`);
if (device.zh.scheduledOta?.url?.startsWith(dataDir.joinPath("ota"))) {
rmSync(device.zh.scheduledOta.url, {force: true});
}
await this.publishEntityState(device, this.getEntityPublishPayload(device, "idle", undefined, undefined));
device.zh.unscheduleOta();
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "idle"));
const response = utils.getResponse<"bridge/response/device/ota_update/unschedule">(message, {
id: ID,
});
const response = utils.getResponse<"bridge/response/device/ota_update/unschedule">(message, {id});
await this.mqtt.publish("bridge/response/device/ota_update/unschedule", stringify(response));
@@ -422,7 +469,7 @@ export default class OTAUpdate extends Extension {
}
}
this.inProgress.delete(device.ieeeAddr);
this.#inProgress.delete(device.ieeeAddr);
}
if (error) {
@@ -436,4 +483,59 @@ export default class OTAUpdate extends Extension {
}
}
}
/**
* Do the bulk of the update work (hand over to zigbee-herdsman, then re-interview).
*
* `dataSettings` object may be mutated by zigbee-herdsman to fit request (e.g. known manuf quirk)
*/
async #update(
source: OtaSource | undefined,
device: Device,
requestPayload: Zcl.ClustersTypes.TClusterCommandPayload<"genOta", "queryNextImageRequest"> | undefined,
requestTsn: number | undefined,
dataSettings: OtaDataSettings,
endpoint?: zh.Endpoint,
): Promise<[from: number, to: number | undefined]> {
const [from, to] = await device.zh.updateOta(
source,
requestPayload,
requestTsn,
device.otaExtraMetas,
async (progress, remaining) => {
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "updating", progress, remaining));
},
dataSettings,
endpoint,
);
if (to === undefined) {
this.#removeProgressAndRemainingFromState(device);
await this.publishEntityState(device, this.#getEntityPublishPayload(device, {available: false, current: from}));
return [from.fileVersion, undefined];
}
logger.info(`Finished update of '${device.name}'`);
this.#removeProgressAndRemainingFromState(device);
await this.publishEntityState(device, this.#getEntityPublishPayload(device, {available: false, current: to}));
logger.info(() => `Device '${device.name}' was OTA updated from '${from.fileVersion}' to '${to.fileVersion}'`);
// OTA update can bring new features & co, force full re-interview and re-configure, same as a "device joined"
if (device.zh.meta.configured !== undefined) {
delete device.zh.meta.configured;
device.zh.save();
}
setTimeout(() => {
device.reInterview(this.eventBus).catch((error) => {
logger.error(`${error.message}. Re-try manually after some time.`);
});
}, 5000);
return [from.fileVersion, to.fileVersion];
}
}
+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";
+24 -3
View File
@@ -1,9 +1,10 @@
import assert from "node:assert";
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
import type {OtaExtraMetas} from "zigbee-herdsman/dist/controller/tstype";
import type {CustomClusters} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
import * as zhc from "zigbee-herdsman-converters";
import {access, Numeric} from "zigbee-herdsman-converters";
import logger from "../util/logger";
import * as settings from "../util/settings";
const LINKQUALITY = new Numeric("linkquality", access.STATE)
@@ -17,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;
@@ -38,7 +40,7 @@ export default class Device {
get customClusters(): CustomClusters {
return this.zh.customClusters;
}
get otaExtraMetas(): zhc.Ota.ExtraMetas {
get otaExtraMetas(): OtaExtraMetas {
return typeof this.definition?.ota === "object" ? this.definition.ota : {};
}
get interviewed(): boolean {
@@ -63,9 +65,28 @@ 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;
}
}
async reInterview(eventBus: EventBus): Promise<void> {
// logic follows that of receiving `deviceInterview` event from ZH layer
logger.info(`Interviewing '${this.name}'`);
eventBus.emitDeviceInterview({status: "started", device: this});
try {
await this.zh.interview(true);
// A re-interview can for example result in a different modelId, therefore reconsider the definition.
await this.resolveDefinition(true);
logger.info(`Successfully interviewed '${this.name}'`);
eventBus.emitDeviceInterview({status: "successful", device: this});
} catch (error) {
eventBus.emitDeviceInterview({status: "failed", device: this});
throw new Error(`Interview of '${this.name}' (${this.ieeeAddr}) failed: ${error}`);
}
}
+11 -1
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
@@ -152,7 +157,11 @@ export default class Mqtt {
const stateData: Zigbee2MQTTAPI["bridge/state"] = {state: "offline"};
await this.publish("bridge/state", JSON.stringify(stateData), {clientOptions: {retain: true}});
// prevent undesirable error when receiving SIGTERM/SIGINT during startup
if (this.client) {
await this.publish("bridge/state", JSON.stringify(stateData), {clientOptions: {retain: true}});
}
this.eventBus.removeListeners(this);
logger.info("Disconnecting from MQTT server");
await this.client?.endAsync();
@@ -194,6 +203,7 @@ export default class Mqtt {
}
async publish(topic: string, payload: string, options: Partial<MqttPublishOptions> = {}): Promise<void> {
// TODO: add `options.validateTopic: boolean` to bypass these checks when topic is "controlled"
if (topic.includes("+") || topic.includes("#")) {
// https://github.com/Koenkk/zigbee2mqtt/issues/26939#issuecomment-2772309646
logger.error(`Topic '${topic}' includes wildcard characters, skipping publish.`);
+8 -5
View File
@@ -51,11 +51,14 @@ class State {
}
stop(): void {
// Remove any invalid states (ie when the device has left the network) when the system is stopped
for (const [key] of this.state) {
if (typeof key === "string" && key.startsWith("0x") && !this.zigbee.resolveEntity(key)) {
// string key = ieeeAddr
this.state.delete(key);
// ensure properly started, else this throws undesired errors (e.g. SIGINT during startup)
if (this.zigbee.zhController !== undefined) {
// Remove any invalid states (ie when the device has left the network) when the system is stopped
for (const [key] of this.state) {
if (typeof key === "string" && key.startsWith("0x") && !this.zigbee.resolveEntity(key)) {
// string key = ieeeAddr
this.state.delete(key);
}
}
}
+154 -24
View File
@@ -1,5 +1,8 @@
import type * as zigbeeHerdsman from "zigbee-herdsman/dist";
import type {ClusterDefinition, ClusterName, CustomClusters} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
import type {ZclPayload} from "zigbee-herdsman/dist/adapter/events";
import type {Eui64} from "zigbee-herdsman/dist/zspec/tstypes";
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";
@@ -29,6 +32,33 @@ import type {LogLevel, schemaJson} from "../util/settings";
// biome-ignore lint/suspicious/noExplicitAny: API
type KeyValue = Record<string, any>;
export interface OnboardInitData {
page: "form";
settings: Zigbee2MQTTSettings;
settingsSchema: typeof schemaJson;
devices: {
name: string;
path: Zigbee2MQTTSettings["serial"]["port"];
adapter?: Zigbee2MQTTSettings["serial"]["adapter"];
baudRate?: Zigbee2MQTTSettings["serial"]["baudrate"];
rtscts?: Zigbee2MQTTSettings["serial"]["rtscts"];
}[];
}
export interface OnboardDoneData {
page: "done";
frontendUrl: string | null;
}
export interface OnboardFailureData {
page: "failure";
errors: string[];
}
export type OnboardData = OnboardInitData | OnboardDoneData | OnboardFailureData;
export type OnboardSubmitResponse = {success: true; frontendUrl: string | null} | {success: false; error: string};
export interface Zigbee2MQTTDeviceOptions {
disabled?: boolean;
retention?: number;
@@ -52,6 +82,7 @@ export interface Zigbee2MQTTDeviceOptions {
friendly_name: string;
description?: string;
qos?: 0 | 1 | 2;
disable_automatic_update_check?: boolean;
}
export interface Zigbee2MQTTGroupOptions {
@@ -102,6 +133,7 @@ export interface Zigbee2MQTTSettings {
cert?: string;
client_id?: string;
reject_unauthorized?: boolean;
server_name?: string;
maximum_packet_size: number;
};
serial: {
@@ -137,6 +169,7 @@ export interface Zigbee2MQTTSettings {
update_check_interval: number;
disable_automatic_update_check: boolean;
zigbee_ota_override_index_location?: string;
image_block_request_timeout?: number;
image_block_response_delay?: number;
default_maximum_data_size?: number;
};
@@ -183,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 */
@@ -227,6 +262,7 @@ export interface Zigbee2MQTTDeviceDefinition {
exposes: zigbeeHerdsmanConverter.Expose[];
supports_ota: boolean;
options: zigbeeHerdsmanConverter.Option[];
version: `0.0.${number}`;
icon: string;
}
@@ -278,18 +314,21 @@ export interface Zigbee2MQTTNetworkMap {
links: {
source: {ieeeAddr: string; networkAddress: number};
target: {ieeeAddr: string; networkAddress: number};
linkquality: number;
depth: number;
routes: {
destinationAddress: number;
status: string;
nextHop: number;
}[];
sourceIeeeAddr: string;
targetIeeeAddr: string;
sourceNwkAddr: number;
lqi: number;
deviceType: number;
rxOnWhenIdle: number;
relationship: number;
permitJoining: number;
depth: number;
lqi: number;
routes: RoutingTableEntry[];
/** @deprecated 3.0 */
linkquality: number;
/** @deprecated 3.0 */
sourceIeeeAddr: string;
/** @deprecated 3.0 */
targetIeeeAddr: string;
/** @deprecated 3.0 */
sourceNwkAddr: number;
}[];
}
@@ -308,8 +347,9 @@ export interface Zigbee2MQTTAPI {
};
"bridge/definitions": {
clusters: Readonly<Record<ClusterName, Readonly<ClusterDefinition>>>;
clusters: Readonly<Record<ClusterName, Cluster>>;
custom_clusters: Record<string, CustomClusters>;
actions: string[];
};
"bridge/event":
@@ -558,6 +598,16 @@ export interface Zigbee2MQTTAPI {
failed: string[];
};
"bridge/request/device/binds/clear": {
target: string;
ieee_list?: Eui64[];
};
"bridge/response/device/binds/clear": {
target: string;
ieee_list?: Eui64[];
};
"bridge/request/device/configure":
| {
id: string | number;
@@ -572,61 +622,113 @@ 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": {
id: string;
/** expected to point to an index json if provided */
url?: string | null;
};
"bridge/request/device/ota_update/check/downgrade": {
id: string;
/** expected to point to an index json if provided */
url?: string | null;
};
"bridge/response/device/ota_update/check": {
id: string;
update_available: boolean;
downgrade?: boolean;
source?: string;
release_notes?: string;
};
"bridge/request/device/ota_update/update": {
id: string;
url?: string | null;
/**
* Full firmware file in hex form (expected compatible with `Buffer.from(hex, "hex")`)
* If file name supplied, will be used instead of <ieee_utc> to store firmware in data dir.
*/
hex?: {data: string; file_name?: string} | null;
image_block_request_timeout?: number | null;
image_block_response_delay?: number | null;
/** may be overridden internally to match specific device needs */
default_maximum_data_size?: number | null;
};
"bridge/request/device/ota_update/update/downgrade": {
id: string;
url?: string | null;
/**
* Full firmware file in hex form (expected compatible with `Buffer.from(hex, "hex")`)
* If file name supplied, will be used instead of <ieee_utc> to store firmware in data dir.
*/
hex?: {data: string; file_name?: string} | null;
image_block_request_timeout?: number | null;
image_block_response_delay?: number | null;
/** may be overridden internally to match specific device needs */
default_maximum_data_size?: number | null;
};
"bridge/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:
| {
software_build_id: string;
date_code: string;
file_version: number;
software_build_id?: string;
date_code?: string;
}
| undefined;
to:
| {
software_build_id: string;
date_code: string;
file_version: number;
software_build_id?: string;
date_code?: string;
}
| undefined;
};
"bridge/request/device/ota_update/schedule": {
id: string;
url?: string | null;
/**
* Full firmware file in hex form (expected compatible with `Buffer.from(hex, "hex")`)
* If file name supplied, will be used instead of <ieee_utc> to store firmware in data dir.
*/
hex?: {data: string; file_name?: string} | null;
};
"bridge/request/device/ota_update/schedule/downgrade": {
id: string;
url?: string | null;
/**
* Full firmware file in hex form (expected compatible with `Buffer.from(hex, "hex")`)
* If file name supplied, will be used instead of <ieee_utc> to store firmware in data dir.
*/
hex?: {data: string; file_name?: string} | null;
};
"bridge/response/device/ota_update/schedule": {
id: string;
url?: string;
};
"bridge/request/device/ota_update/unschedule": {
@@ -686,25 +788,41 @@ export interface Zigbee2MQTTAPI {
homeassistant_rename: boolean;
};
"bridge/request/device/configure_reporting": {
"bridge/request/device/reporting/configure": {
id: string;
endpoint: string | number;
cluster: string | number;
attribute: string | number | {ID: number; type: number};
minimum_report_interval: number;
maximum_report_interval: number;
reportable_change: number;
reportable_change?: number;
option: Record<string, unknown>;
};
"bridge/response/device/configure_reporting": {
"bridge/response/device/reporting/configure": {
id: string;
endpoint: string | number;
cluster: string | number;
attribute: string | number | {ID: number; type: number};
minimum_report_interval: number;
maximum_report_interval: number;
reportable_change: number;
reportable_change?: number;
};
"bridge/request/device/reporting/read": {
id: string;
endpoint: string | number;
cluster: string | number;
configs: {direction?: number; attribute: string | number | {ID: number; type: number}}[];
manufacturer_code?: number;
};
"bridge/response/device/reporting/read": {
id: string;
endpoint: string | number;
cluster: string | number;
configs: zigbeeHerdsman.Zcl.ClustersTypes.TFoundation["readReportConfigRsp"];
manufacturer_code?: number;
};
"bridge/request/group/remove": {
@@ -821,6 +939,10 @@ export interface Zigbee2MQTTAPI {
channel: number;
};
"bridge/request/action": {action: string; params?: Record<string, unknown>};
"bridge/response/action": GenericZdoResponse | ZclPayload | undefined;
/**
* entity state response
*/
@@ -883,12 +1005,14 @@ export type Zigbee2MQTTRequestEndpoints =
| "bridge/request/options"
| "bridge/request/device/bind"
| "bridge/request/device/unbind"
| "bridge/request/device/binds/clear"
| "bridge/request/device/configure"
| "bridge/request/device/remove"
| "bridge/request/device/ota_update/check"
| "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"
@@ -896,7 +1020,8 @@ export type Zigbee2MQTTRequestEndpoints =
| "bridge/request/device/generate_external_definition"
| "bridge/request/device/options"
| "bridge/request/device/rename"
| "bridge/request/device/configure_reporting"
| "bridge/request/device/reporting/configure"
| "bridge/request/device/reporting/read"
| "bridge/request/group/remove"
| "bridge/request/group/add"
| "bridge/request/group/rename"
@@ -907,6 +1032,7 @@ export type Zigbee2MQTTRequestEndpoints =
| "bridge/request/touchlink/factory_reset"
| "bridge/request/touchlink/scan"
| "bridge/request/touchlink/identify"
| "bridge/request/action"
| "{friendlyNameOrId}/set"
| "{friendlyNameOrId}/set/{attribute}"
| "{friendlyNameOrId}/{endpoint}/set"
@@ -931,17 +1057,20 @@ export type Zigbee2MQTTResponseEndpoints =
| "bridge/response/options"
| "bridge/response/device/bind"
| "bridge/response/device/unbind"
| "bridge/response/device/binds/clear"
| "bridge/response/device/configure"
| "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"
| "bridge/response/device/generate_external_definition"
| "bridge/response/device/options"
| "bridge/response/device/rename"
| "bridge/response/device/configure_reporting"
| "bridge/response/device/reporting/configure"
| "bridge/response/device/reporting/read"
| "bridge/response/group/remove"
| "bridge/response/group/add"
| "bridge/response/group/rename"
@@ -951,7 +1080,8 @@ export type Zigbee2MQTTResponseEndpoints =
| "bridge/response/group/members/remove_all"
| "bridge/response/touchlink/factory_reset"
| "bridge/response/touchlink/scan"
| "bridge/response/touchlink/identify";
| "bridge/response/touchlink/identify"
| "bridge/response/action";
export type Zigbee2MQTTRequest<T extends Zigbee2MQTTRequestEndpoints> = {
transaction?: string;
-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;
}
-3
View File
@@ -36,9 +36,6 @@ declare global {
type Endpoint = ZHModels.Endpoint;
type Device = ZHModels.Device;
type Group = ZHModels.Group;
// biome-ignore lint/style/useNamingConvention: API
type LQI = ZHAdapterTypes.Lqi;
type RoutingTable = ZHAdapterTypes.RoutingTable;
type CoordinatorVersion = ZHAdapterTypes.CoordinatorVersion;
type NetworkParameters = ZHAdapterTypes.NetworkParameters;
interface Bind {
+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;
+28 -29
View File
@@ -2,36 +2,31 @@ import assert from "node:assert";
import fs from "node:fs";
import path from "node:path";
import moment from "moment";
import {rimrafSync} from "rimraf";
import winston from "winston";
import * as settings from "./settings";
const NAMESPACE_SEPARATOR = ":";
class Logger {
// @ts-expect-error initalized in `init`
private level: settings.LogLevel;
// @ts-expect-error initalized in `init`
private output: string[];
// @ts-expect-error initalized in `init`
private directory: string;
// @ts-expect-error initalized in `init`
private logger: winston.Logger;
// @ts-expect-error initalized in `init`
private fileTransport: winston.transports.FileTransportInstance;
private level!: settings.LogLevel;
private output!: string[];
private directory!: string;
private logger!: winston.Logger;
private fileTransport: winston.transports.FileTransportInstance | undefined;
private debugNamespaceIgnoreRegex?: RegExp;
// @ts-expect-error initalized in `init`
private namespacedLevels: Record<string, settings.LogLevel>;
// @ts-expect-error initalized in `init`
private cachedNamespacedLevels: Record<string, settings.LogLevel>;
private namespacedLevels!: Record<string, settings.LogLevel>;
private cachedNamespacedLevels!: Record<string, settings.LogLevel>;
public init(): void {
// What transports to enable
this.output = settings.get().advanced.log_output;
// Directory to log to
const timestamp = moment(Date.now()).format("YYYY-MM-DD.HH-mm-ss");
const date = new Date();
// offset UTC by current timezone, ISO keeps "Z" (UTC) which is then wrong but we strip it
const timestamp = new Date(date.getTime() - date.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 19)
.replace("T", ".")
.replaceAll(":", "-");
this.directory = settings.get().advanced.log_directory.replace("%TIMESTAMP%", timestamp);
const logFilename = settings.get().advanced.log_file.replace("%TIMESTAMP%", timestamp);
this.level = settings.get().advanced.log_level;
@@ -40,11 +35,12 @@ class Logger {
assert(settings.LOG_LEVELS.includes(this.level), `'${this.level}' is not valid log_level, use one of '${settings.LOG_LEVELS.join(", ")}'`);
const timestampFormat = (): string => moment().format(settings.get().advanced.timestamp_format);
this.logger = winston.createLogger({
level: "debug",
format: winston.format.combine(winston.format.errors({stack: true}), winston.format.timestamp({format: timestampFormat})),
format: winston.format.combine(
winston.format.errors({stack: true}),
winston.format.timestamp({format: settings.get().advanced.timestamp_format}),
),
levels: winston.config.syslog.levels,
});
@@ -145,11 +141,7 @@ class Logger {
}
public getDebugNamespaceIgnore(): string {
return (
this.debugNamespaceIgnoreRegex
?.toString()
.slice(1, -1) /* remove slashes */ ?? ""
);
return this.debugNamespaceIgnoreRegex?.toString().slice(1, -1) /* remove slashes */ ?? "";
}
public setDebugNamespaceIgnore(value: string): void {
@@ -241,7 +233,11 @@ class Logger {
for (const dir of directories) {
this.debug(`Removing old log directory '${dir.path}'`);
rimrafSync(dir.path);
try {
fs.rmSync(dir.path, {recursive: true, force: true});
} catch (e) {
this.error(`Failed to remove old log directory '${dir.path}': ${e}`);
}
}
}
}
@@ -263,7 +259,10 @@ class Logger {
// @ts-expect-error workaround
this.fileTransport.on("open", () => this.fileTransport._dest.on("finish", resolve));
}
this.fileTransport.end();
if (this.fileTransport) {
this.fileTransport.end();
}
});
}
}
+191 -455
View File
@@ -1,392 +1,100 @@
import {existsSync, mkdirSync} from "node:fs";
import {existsSync, mkdirSync, writeFileSync} from "node:fs";
import type {ServerResponse} from "node:http";
import {createServer} from "node:http";
import {parse} from "node:querystring";
import path from "node:path";
import expressStaticGzip from "express-static-gzip";
import {type Unzipped, unzip} from "fflate";
import finalhandler from "finalhandler";
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";
type OnboardSettings = {
mqtt_base_topic?: string;
mqtt_server?: string;
mqtt_user?: string;
mqtt_password?: string;
serial_port?: string;
serial_adapter?: Settings["serial"]["adapter"];
serial_baudrate?: string;
serial_rtscts?: "on";
network_channel?: string;
network_key?: string;
network_pan_id?: string;
network_ext_pan_id?: string;
frontend_enabled?: "on";
frontend_port?: string;
homeassistant_enabled?: "on";
log_level?: Settings["advanced"]["log_level"];
/** same as extension/frontend */
const FILE_SERVER_OPTIONS: expressStaticGzip.ExpressStaticGzipOptions = {
enableBrotli: true,
serveStatic: {
/* v8 ignore start */
setHeaders: (res: ServerResponse, path: string): void => {
if (path.endsWith("index.html")) {
res.setHeader("Cache-Control", "no-store");
}
},
/* v8 ignore stop */
},
};
function escapeHtml(s: string): string {
return s.replace(/[^0-9A-Za-z \-_.]/g, (c) => `&#${c.charCodeAt(0)};`);
}
function generateHtmlDone(frontendUrl: string | undefined): string {
return `
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Zigbee2MQTT Onboarding</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css">
</head>
<body>
<main>
<h1>Zigbee2MQTT Onboarding</h1>
<p>Settings saved.</p>
<p>Zigbee2MQTT is now starting...</p>
<small>${frontendUrl ? `Redirecting to Zigbee2MQTT frontend at <a href="${frontendUrl}">${frontendUrl}</a> in 30 seconds.` : "You can close this page."}</small>
</main>
${frontendUrl ? `<script>setTimeout(() => { window.location.replace("${frontendUrl}"); }, 30000);</script>` : ""}
</body>
</html>
`;
}
function generateHtmlForm(currentSettings: RecursivePartial<Settings>, devices: Awaited<ReturnType<typeof findAllDevices>>): string {
let devicesSelect = "";
if (devices.length > 0) {
devicesSelect += '<select id="found_device" onchange="setFoundDevice(this)">';
devicesSelect += '<option value="">Select a device</option>';
for (const device of devices) {
// just in case name has commas, remove them to not mess with `split` logic
const deviceStr = `${device.name.replaceAll(",", "")}, ${device.path}, ${device.adapter ?? "unknown"}`;
devicesSelect += `<option value="${deviceStr}">${deviceStr}</option>`;
}
devicesSelect += "</select>";
devicesSelect += "<small>Optionally allows to configure coordinator port and type (if known) automatically.</small>";
} else {
devicesSelect = "<small>No device found</small>";
}
let generateCheckbox = "";
if (
Array.isArray(currentSettings.advanced?.network_key) ||
typeof currentSettings.advanced?.pan_id === "number" ||
Array.isArray(currentSettings.advanced?.ext_pan_id)
) {
generateCheckbox = `
<label for="generate_network">
<input
type="checkbox"
id="generate_network"
onclick="setGenerate(this)"
${process.env.ZIGBEE2MQTT_CONFIG_ADVANCED_NETWORK_KEY || process.env.ZIGBEE2MQTT_CONFIG_ADVANCED_PAN_ID || process.env.ZIGBEE2MQTT_CONFIG_ADVANCED_EXT_PAN_ID ? "disabled" : ""}>
Generate network?
</label>
`;
}
/* v8 ignore start */
return `
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Zigbee2MQTT Onboarding</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css">
</head>
<body>
<main>
<h1>Zigbee2MQTT Onboarding</h1>
<p>Set the base configuration to start Zigbee2MQTT.</p>
<p>Optional fields will either be ignored or fallback to defaults if not set (see appropriate documentation page for more details).</p>
<p>If a field is disabled, it means <a href="https://www.zigbee2mqtt.io/guide/configuration/#environment-variables" target="_blank">environment variables</a> are being used to override specific values (for example, through the Home Assistant add-on configuration page).</p>
<hr>
<form method="post">
<fieldset ${process.env.ZIGBEE2MQTT_CONFIG_SERIAL || process.env.ZIGBEE2MQTT_CONFIG_SERIAL_PORT || process.env.ZIGBEE2MQTT_CONFIG_SERIAL_ADAPTER ? "disabled" : ""}>
<label for="found_device">Found Devices</label>
${devicesSelect}
</fieldset>
<fieldset ${process.env.ZIGBEE2MQTT_CONFIG_SERIAL ? "disabled" : ""}>
<label for="serial_port">Coordinator/Adapter Port/Path</label>
<input
type="text"
id="serial_port"
name="serial_port"
value="${currentSettings.serial?.port ?? ""}"
required
${process.env.ZIGBEE2MQTT_CONFIG_SERIAL_PORT ? "disabled" : ""}>
<label for="serial_adapter">Coordinator/Adapter Type/Stack/Driver</label>
<select id="serial_adapter" name="serial_adapter" required ${process.env.ZIGBEE2MQTT_CONFIG_SERIAL_ADAPTER ? "disabled" : ""}>
<option value="zstack" ${currentSettings.serial?.adapter === "zstack" ? "selected" : ""}>zstack</option>
<option value="ember" ${currentSettings.serial?.adapter === "ember" ? "selected" : ""}>ember</option>
<option value="deconz" ${currentSettings.serial?.adapter === "deconz" ? "selected" : ""}>deconz</option>
<option value="zigate" ${currentSettings.serial?.adapter === "zigate" ? "selected" : ""}>zigate</option>
<option value="zboss" ${currentSettings.serial?.adapter === "zboss" ? "selected" : ""}>zboss</option>
</select>
<label for="serial_baudrate">Coordinator/Adapter Baudrate</label>
<select id="serial_baudrate" name="serial_baudrate" ${process.env.ZIGBEE2MQTT_CONFIG_SERIAL_BAUDRATE ? "disabled" : ""}>
<option value="38400" ${currentSettings.serial?.baudrate === 38400 ? "selected" : ""}>38400</option>
<option value="57600" ${currentSettings.serial?.baudrate === 57600 ? "selected" : ""}>57600</option>
<option value="115200" ${!currentSettings.serial?.baudrate || currentSettings.serial?.baudrate === 115200 ? "selected" : ""}>115200</option>
<option value="230400" ${currentSettings.serial?.baudrate === 230400 ? "selected" : ""}>230400</option>
<option value="460800" ${currentSettings.serial?.baudrate === 460800 ? "selected" : ""}>460800</option>
<option value="921600" ${currentSettings.serial?.baudrate === 921600 ? "selected" : ""}>921600</option>
</select>
<small>Can be ignored for networked coordinators (TCP).</small>
<label for="serial_rtscts">Coordinator/Adapter Hardware Flow Control ("rtscts: true")</label>
<input
type="checkbox"
id="serial_rtscts"
name="serial_rtscts"
${currentSettings.serial?.rtscts ? "checked" : ""}
style="margin-bottom: 1rem;">
<small>Can be ignored for networked coordinators (TCP).</small>
</fieldset>
<small>
<a href="https://www.zigbee2mqtt.io/guide/configuration/adapter-settings.html" target="_blank">https://www.zigbee2mqtt.io/guide/configuration/adapter-settings.html</a>
</small>
<hr>
<fieldset ${process.env.ZIGBEE2MQTT_CONFIG_ADVANCED ? "disabled" : ""}>
<label for="closest_wifi_channel">Closest WiFi Channel</label>
<input
type="number"
min="0"
max="14"
id="closest_wifi_channel"
value="0"
onclick="setBestZigbeeChannel(this)"
${process.env.ZIGBEE2MQTT_CONFIG_ADVANCED_CHANNEL ? "disabled" : ""}>
<small>Optionally set to your closest WiFi channel to pick the best value for "Network channel" below.</small>
<label for="network_channel">Network Channel</label>
<input
type="number"
min="11"
max="26"
id="network_channel"
name="network_channel"
value="${currentSettings.advanced?.channel ?? "25"}"
required
${process.env.ZIGBEE2MQTT_CONFIG_ADVANCED_CHANNEL ? "disabled" : ""}>
</fieldset>
<fieldset ${process.env.ZIGBEE2MQTT_CONFIG_ADVANCED ? "disabled" : ""}>
${generateCheckbox}
<label for="network_key">Network Key</label>
<input
type="text"
id="network_key"
name="network_key"
value="${currentSettings.advanced?.network_key ?? "GENERATE"}"
pattern="^([0-9]+(,[0-9]+){15})|GENERATE$"
required
${process.env.ZIGBEE2MQTT_CONFIG_ADVANCED_NETWORK_KEY ? "disabled" : ""}>
<label for="network_pan_id">Network PAN ID</label>
<input
type="text"
id="network_pan_id"
name="network_pan_id"
value="${currentSettings.advanced?.pan_id ?? "GENERATE"}"
pattern="^([0-9]{1,5})|GENERATE$"
required
${process.env.ZIGBEE2MQTT_CONFIG_ADVANCED_PAN_ID ? "disabled" : ""}>
<label for="network_ext_pan_id">Network Extended PAN ID</label>
<input
type="text"
id="network_ext_pan_id"
name="network_ext_pan_id"
value="${currentSettings.advanced?.ext_pan_id ?? "GENERATE"}"
pattern="^([0-9]+(,[0-9]+){7})|GENERATE$"
required
${process.env.ZIGBEE2MQTT_CONFIG_ADVANCED_EXT_PAN_ID ? "disabled" : ""}>
</fieldset>
<small>
<a href="https://www.zigbee2mqtt.io/guide/configuration/zigbee-network.html" target="_blank">https://www.zigbee2mqtt.io/guide/configuration/zigbee-network.html</a>
</small>
<hr>
<fieldset ${process.env.ZIGBEE2MQTT_CONFIG_MQTT ? "disabled" : ""}>
<label for="mqtt_base_topic">MQTT Base Topic</label>
<input
type="text"
id="mqtt_base_topic"
name="mqtt_base_topic"
value="${currentSettings.mqtt?.base_topic ?? "zigbee2mqtt"}"
required
${process.env.ZIGBEE2MQTT_CONFIG_MQTT_BASE_TOPIC ? "disabled" : ""}>
<label for="mqtt_server">MQTT Server</label>
<input
type="text"
id="mqtt_server"
name="mqtt_server"
value="${currentSettings.mqtt?.server ?? "mqtt://localhost:1883"}"
required
${process.env.ZIGBEE2MQTT_CONFIG_MQTT_SERVER ? "disabled" : ""}>
<label for="mqtt_user">MQTT User</label>
<input
type="text"
id="mqtt_user"
name="mqtt_user"
value="${currentSettings.mqtt?.user ?? ""}"
${process.env.ZIGBEE2MQTT_CONFIG_MQTT_USER ? "disabled" : ""}>
<small>Optional. Set only if using authentication.</small>
<label for="mqtt_password">MQTT Password</label>
<input
type="password"
id="mqtt_password"
name="mqtt_password"
value="${currentSettings.mqtt?.password ?? ""}"
${process.env.ZIGBEE2MQTT_CONFIG_MQTT_PASSWORD ? "disabled" : ""}>
<small>Optional. Set only if using authentication.</small>
</fieldset>
<small>
<a href="https://www.zigbee2mqtt.io/guide/configuration/mqtt.html" target="_blank">https://www.zigbee2mqtt.io/guide/configuration/mqtt.html</a>
</small>
<hr>
<fieldset ${process.env.ZIGBEE2MQTT_CONFIG_FRONTEND ? "disabled" : ""}>
<label for="frontend_enabled">
<input
type="checkbox"
id="frontend_enabled"
name="frontend_enabled"
${currentSettings.frontend?.enabled ? "checked" : ""}
${process.env.ZIGBEE2MQTT_CONFIG_FRONTEND_ENABLED ? "disabled" : ""}>
Frontend enabled?
</label>
<label for="frontend_port">Frontend Port</label>
<input
type="number"
min="0"
max="65535"
id="frontend_port"
name="frontend_port"
value="${currentSettings.frontend?.port ?? "8080"}"
required
${process.env.ZIGBEE2MQTT_CONFIG_FRONTEND_PORT ? "disabled" : ""}>
</fieldset>
<small>
<a href="https://www.zigbee2mqtt.io/guide/configuration/frontend.html" target="_blank">https://www.zigbee2mqtt.io/guide/configuration/frontend.html</a>
</small>
<fieldset ${process.env.ZIGBEE2MQTT_CONFIG_HOMEASSISTANT ? "disabled" : ""}>
<label for="homeassistant_enabled" ${process.env.ZIGBEE2MQTT_CONFIG_HOMEASSISTANT_ENABLED ? "disabled" : ""}>
<input type="checkbox" id="homeassistant_enabled" name="homeassistant_enabled" ${currentSettings.homeassistant?.enabled ? "checked" : ""}>
Home Assistant enabled?
</label>
</fieldset>
<small>
<a href="https://www.zigbee2mqtt.io/guide/configuration/homeassistant.html" target="_blank">https://www.zigbee2mqtt.io/guide/configuration/homeassistant.html</a>
</small>
<hr>
<fieldset ${process.env.ZIGBEE2MQTT_CONFIG_ADVANCED ? "disabled" : ""}>
<label for="log_level">Log Level</label>
<select id="log_level" name="log_level" ${process.env.ZIGBEE2MQTT_CONFIG_ADVANCED_LOG_LEVEL ? "disabled" : ""}>
<option value="error" ${currentSettings.advanced?.log_level === "error" ? "selected" : ""}>error</option>
<option value="warning" ${currentSettings.advanced?.log_level === "warning" ? "selected" : ""}>warning</option>
<option value="info" ${!currentSettings.advanced?.log_level || currentSettings.advanced?.log_level === "info" ? "selected" : ""}>info</option>
<option value="debug" ${currentSettings.advanced?.log_level === "debug" ? "selected" : ""}>debug</option>
</select>
</fieldset>
<small>
<a href="https://www.zigbee2mqtt.io/guide/configuration/logging.html" target="_blank">https://www.zigbee2mqtt.io/guide/configuration/logging.html</a>
</small>
<hr>
<input type="submit" value="Submit">
</form>
</main>
<script>
function setFoundDevice(e) {
if (!e.value) {
return;
}
const [, path, adapter] = e.value.split(", ");
const serialPortEl = document.querySelector("#serial_port");
serialPortEl.value = path;
const serialAdapterEl = document.querySelector("#serial_adapter");
if (['zstack', 'ember', 'deconz', 'zigate', 'zboss'].includes(adapter)) {
serialAdapterEl.value = adapter;
} else {
serialAdapterEl.value = '';
}
}
function setBestZigbeeChannel(e) {
const wifiChannel = parseInt(e.value, 10);
const networkChannelEl = document.querySelector("#network_channel");
if (wifiChannel >= 11) {
// WiFi 11-14
networkChannelEl.value = 15;
} else if (wifiChannel >= 6) {
// WiFi 6-10
networkChannelEl.value = 11;
} else {
// WiFi 1-5
networkChannelEl.value = 25;
}
}
function setGenerate(e) {
document.querySelector("#network_key").value = e.checked ? "GENERATE" : "${currentSettings.advanced?.network_key ?? "GENERATE"}";
document.querySelector("#network_pan_id").value = e.checked ? "GENERATE" : "${currentSettings.advanced?.pan_id ?? "GENERATE"}";
document.querySelector("#network_ext_pan_id").value = e.checked ? "GENERATE" : "${currentSettings.advanced?.ext_pan_id ?? "GENERATE"}";
}
</script>
</body>
</html>
`;
/* v8 ignore stop */
}
function generateHtmlError(errors: string): string {
return `
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Zigbee2MQTT Onboarding</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css">
</head>
<body>
<main>
<h1>Zigbee2MQTT configuration is not valid</h1>
<p style="color: #F00;">Found the following errors:</p>
${errors}
<hr>
<p>If you don't know how to solve this, read <a href="https://www.zigbee2mqtt.io/guide/configuration" target="_blank">https://www.zigbee2mqtt.io/guide/configuration</a></p>
<form method="post" action="/">
<input type="submit" value="Close">
</form>
</main>
</body>
</html>
`;
}
function getServerUrl(): URL {
return new URL(process.env.Z2M_ONBOARD_URL ?? "http://0.0.0.0:8080");
}
function getZipEntryTargetPath(entryName: string): string {
const normalizedEntry = entryName.replace(/\\/g, "/");
if (!normalizedEntry || normalizedEntry.startsWith("/") || normalizedEntry.includes("\0")) {
throw new Error(`Invalid ZIP entry path '${entryName}'`);
}
const basePath = path.resolve(data.getPath());
const targetPath = path.resolve(basePath, normalizedEntry);
const relativePath = path.relative(basePath, targetPath);
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
throw new Error(`Unsafe ZIP entry path '${entryName}'`);
}
return targetPath;
}
async function extractZipDataToDataPath(zipContent: Buffer): Promise<void> {
const entries = await new Promise<Unzipped>((resolve, reject) => {
unzip(zipContent, (error, data) => (error ? reject(error) : resolve(data)));
});
for (const name in entries) {
const targetPath = getZipEntryTargetPath(name);
// directory entries are identified by a trailing slash
if (name.endsWith("/")) {
mkdirSync(targetPath, {recursive: true});
continue;
}
mkdirSync(path.dirname(targetPath), {recursive: true});
writeFileSync(targetPath, entries[name]);
}
}
async function startOnboardingServer(): Promise<boolean> {
const currentSettings = settings.get();
const serverUrl = getServerUrl();
let server: ReturnType<typeof createServer> | undefined;
let failed = false;
const fileServer = expressStaticGzip((await import("zigbee2mqtt-windfront")).default.getOnboardingPath(), FILE_SERVER_OPTIONS);
const success = await new Promise<boolean>((resolve) => {
server = createServer(async (req, res) => {
const pathname = new URL(req.url /* v8 ignore next */ ?? "/", serverUrl).pathname;
if (req.method === "GET" && pathname === "/data") {
const payload: OnboardData = {
page: "form",
settings: currentSettings,
settingsSchema: settings.schemaJson,
devices: await findAllDevices(),
};
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(stringify(payload));
return;
}
if (req.method === "POST") {
if (failed) {
res.end(() => {
resolve(false);
});
} else {
if (pathname === "/submit") {
let body = "";
req.on("data", (chunk) => {
@@ -394,95 +102,111 @@ async function startOnboardingServer(): Promise<boolean> {
});
req.on("end", () => {
const result = parse(body) as unknown as OnboardSettings;
const frontendEnabled = result.frontend_enabled === "on";
const updatedSettings: RecursivePartial<Settings> = {
mqtt: {
base_topic: result.mqtt_base_topic,
server: result.mqtt_server,
user: result.mqtt_user || undefined, // empty string => removed
password: result.mqtt_password || undefined, // empty string => removed
},
serial: {
port: result.serial_port,
adapter: result.serial_adapter,
baudrate: result.serial_baudrate ? Number.parseInt(result.serial_baudrate, 10) : undefined,
rtscts: result.serial_rtscts === "on",
},
advanced: {
log_level: result.log_level,
channel: result.network_channel ? Number.parseInt(result.network_channel, 10) : undefined,
network_key: result.network_key
? result.network_key === "GENERATE"
? result.network_key
: result.network_key.split(",").map((v) => Number.parseInt(v, 10))
: undefined,
pan_id: result.network_pan_id
? result.network_pan_id === "GENERATE"
? result.network_pan_id
: Number.parseInt(result.network_pan_id, 10)
: undefined,
ext_pan_id: result.network_ext_pan_id
? result.network_ext_pan_id === "GENERATE"
? result.network_ext_pan_id
: result.network_ext_pan_id.split(",").map((v) => Number.parseInt(v, 10))
: undefined,
},
frontend: {
enabled: frontendEnabled,
port: result.frontend_port ? Number.parseInt(result.frontend_port, 10) : undefined,
},
homeassistant: {
enabled: result.homeassistant_enabled === "on",
},
};
try {
settings.apply(updatedSettings);
const result = (body ? JSON.parse(body) : {}) as RecursivePartial<Zigbee2MQTTSettings>;
// to redirect, make sure frontend "will be" enabled, and host isn't socket
settings.apply(result);
const appliedSettings = settings.get();
const redirect =
!process.env.Z2M_ONBOARD_NO_REDIRECT &&
frontendEnabled &&
(!currentSettings.frontend?.host || !currentSettings.frontend.host.startsWith("/"));
const protocol = currentSettings.frontend?.ssl_cert && currentSettings.frontend.ssl_key ? "https" : "http";
appliedSettings.frontend.enabled &&
!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}`
: null;
const payload: OnboardSubmitResponse = {success: true, frontendUrl};
res.setHeader("Content-Type", "text/html");
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(
generateHtmlDone(
redirect
? /* v8 ignore next */ `${protocol}://${currentSettings.frontend?.host ?? "localhost"}:${currentSettings.frontend?.port ?? "8080"}${currentSettings.frontend?.base_url ?? "/"}`
: undefined,
),
() => {
resolve(true);
},
);
res.end(stringify(payload), () => {
resolve(true);
});
} catch (error) {
console.error(`Failed to apply configuration: ${(error as Error).message}`);
failed = true;
if (process.env.Z2M_ONBOARD_NO_FAILURE_PAGE) {
res.end(() => {
resolve(false);
});
} else {
res.setHeader("Content-Type", "text/html");
res.writeHead(406);
res.end(generateHtmlError(`<p>${escapeHtml((error as Error).message)}</p>`));
}
const payload: OnboardSubmitResponse = {success: false, error: (error as Error).message};
res.setHeader("Content-Type", "application/json");
res.writeHead(406);
res.end(stringify(payload));
}
});
req.on("error", (error: Error) => {
console.error(`Failed to parse request body: ${error.message}`);
const payload: OnboardSubmitResponse = {success: false, error: error.message};
res.setHeader("Content-Type", "application/json");
res.writeHead(406);
res.end(stringify(payload));
});
return;
}
if (pathname === "/submit-zip") {
let body = "";
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", async () => {
try {
if (!body) {
throw new Error("Invalid ZIP payload: missing content");
}
const zipContent = Buffer.from(body, "base64");
await extractZipDataToDataPath(zipContent);
const payload: OnboardSubmitResponse = {success: true, frontendUrl: null};
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(stringify(payload), () => {
resolve(true);
});
} catch (error) {
console.error(`Failed to apply ZIP data: ${(error as Error).message}`);
const payload: OnboardSubmitResponse = {success: false, error: (error as Error).message};
res.setHeader("Content-Type", "application/json");
res.writeHead(406);
res.end(stringify(payload));
}
});
req.on("error", (error: Error) => {
console.error(`Failed to parse ZIP request body: ${error.message}`);
const payload: OnboardSubmitResponse = {success: false, error: error.message};
res.setHeader("Content-Type", "application/json");
res.writeHead(406);
res.end(stringify(payload));
});
return;
}
} else {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(generateHtmlForm(currentSettings, await findAllDevices()));
}
const next = finalhandler(req, res);
fileServer(req, res, next);
});
server.listen(Number.parseInt(serverUrl.port), serverUrl.hostname, () => {
server.on("error", (error: Error) => {
console.error("Failed to start onboarding server", error);
resolve(false);
});
server.listen(Number.parseInt(serverUrl.port, 10), serverUrl.hostname, () => {
console.log(`Onboarding page is available at ${serverUrl.href}`);
});
});
@@ -492,24 +216,40 @@ async function startOnboardingServer(): Promise<boolean> {
return success;
}
async function startFailureServer(errors: string): Promise<void> {
async function startFailureServer(errors: string[]): Promise<void> {
const serverUrl = getServerUrl();
let server: ReturnType<typeof createServer> | undefined;
const fileServer = expressStaticGzip((await import("zigbee2mqtt-windfront")).default.getOnboardingPath(), FILE_SERVER_OPTIONS);
await new Promise<void>((resolve) => {
server = createServer((req, res) => {
if (req.method === "POST") {
const pathname = new URL(req.url /* v8 ignore next */ ?? "/", serverUrl).pathname;
if (req.method === "GET" && pathname === "/data") {
const payload: OnboardFailureData = {page: "failure", errors};
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(stringify(payload));
return;
}
if (req.method === "POST" && pathname === "/submit") {
res.writeHead(200);
res.end(() => {
resolve();
});
} else {
res.setHeader("Content-Type", "text/html");
res.writeHead(406);
res.end(generateHtmlError(errors));
return;
}
const next = finalhandler(req, res);
fileServer(req, res, next);
});
server.listen(Number.parseInt(serverUrl.port), serverUrl.hostname, () => {
server.listen(Number.parseInt(serverUrl.port, 10), serverUrl.hostname, () => {
console.error(`Failure page is available at ${serverUrl.href}`);
});
});
@@ -518,23 +258,19 @@ async function startFailureServer(errors: string): Promise<void> {
}
async function onSettingsErrors(errors: string[]): Promise<void> {
let pErrors = "";
console.error("\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
console.error(" READ THIS CAREFULLY\n");
console.error("Refusing to start because configuration is not valid, found the following errors:");
for (const error of errors) {
console.error(`- ${error}`);
pErrors += `<p>- ${escapeHtml(error)}</p>`;
}
console.error("\nIf you don't know how to solve this, read https://www.zigbee2mqtt.io/guide/configuration");
console.error("\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n");
if (!process.env.Z2M_ONBOARD_NO_SERVER && !process.env.Z2M_ONBOARD_NO_FAILURE_PAGE) {
await startFailureServer(pErrors);
await startFailureServer(errors);
}
}
+61 -15
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",
@@ -366,13 +375,20 @@
"description": "Location of override OTA index file",
"examples": ["index.json"]
},
"image_block_request_timeout": {
"type": "number",
"title": "Image block request timeout",
"description": "Timeout (in milliseconds) during OTA updates. You can increase this value if your device is requesting blocks too slowly.",
"default": 150000,
"minimum": 10000,
"maximum": 2147483647
},
"image_block_response_delay": {
"type": "number",
"title": "Image block response delay",
"description": "Limits the rate of requests (in milliseconds) during OTA updates to reduce network congestion. You can increase this value if your network appears unstable during OTA.",
"default": 250,
"minimum": 50,
"requiresRestart": true
"minimum": 50
},
"default_maximum_data_size": {
"type": "number",
@@ -380,8 +396,7 @@
"description": "The size of file chunks sent during an update (in bytes). Note: This value may get ignored for manufacturers that require specific values.",
"default": 50,
"minimum": 10,
"maximum": 100,
"requiresRestart": true
"maximum": 100
}
}
},
@@ -680,7 +695,7 @@
],
"title": "Pan ID",
"requiresRestart": true,
"description": "ZigBee pan ID, changing requires re-pairing all devices!",
"description": "Zigbee pan ID, changing requires re-pairing all devices!",
"default": 6754
},
"ext_pan_id": {
@@ -707,7 +722,7 @@
"minimum": 11,
"maximum": 26,
"default": 11,
"title": "ZigBee channel",
"title": "Zigbee channel",
"requiresRestart": true,
"description": "Zigbee channel, changing might require re-pairing some devices! (Note: use a ZLL channel: 11, 15, 20, or 25 to avoid problems)",
"examples": [15, 20, 25]
@@ -733,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": {
@@ -784,7 +799,7 @@
"type": "string",
"title": "Timestamp format",
"requiresRestart": true,
"description": "Log timestamp format",
"description": "Log timestamp format (see https://github.com/taylorhakes/fecha?tab=readme-ov-file#formatting-tokens for all supported tokens)",
"default": "YYYY-MM-DD HH:mm:ss",
"examples": ["YYYY-MM-DD HH:mm:ss.SSS"]
},
@@ -794,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
}
}
},
@@ -842,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",
@@ -930,6 +953,12 @@
"description": "Name of the device in Home Assistant"
}
}
},
"disable_automatic_update_check": {
"type": "boolean",
"title": "Disable automatic update check",
"description": "Zigbee devices may request a firmware update, and do so frequently, causing Zigbee2MQTT to reach out to third party servers. If you disable these device initiated checks, you can still initiate a firmware update check manually.",
"default": false
}
},
"required": ["friendly_name"]
@@ -941,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"],
@@ -965,6 +1000,17 @@
"items": {
"type": "string"
}
},
"homeassistant": {
"type": ["object", "null"],
"title": "Home Assistant",
"properties": {
"name": {
"type": ["string", "null"],
"title": "Home Assistant name",
"description": "Name of the group in Home Assistant"
}
}
}
},
"required": ["friendly_name"]
+5 -2
View File
@@ -10,7 +10,7 @@ import yaml from "./yaml";
export {schemaJson};
// When updating also update:
// - https://github.com/Koenkk/zigbee2mqtt/blob/dev/data/configuration.example.yaml#L2
export const CURRENT_VERSION = 4;
export const CURRENT_VERSION = 5;
/** NOTE: by order of priority, lower index is lower level (more important) */
export const LOG_LEVELS: readonly string[] = ["error", "warning", "info", "debug"] as const;
export type LogLevel = "error" | "warning" | "info" | "debug";
@@ -82,6 +82,7 @@ export const defaults = {
ota: {
update_check_interval: 24 * 60,
disable_automatic_update_check: false,
image_block_request_timeout: 150000,
image_block_response_delay: 250,
default_maximum_data_size: 50,
},
@@ -112,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,
@@ -166,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,
@@ -607,7 +610,7 @@ export function addGroup(name: string, id?: string): GroupOptions {
id = "1";
while (settings.groups[id]) {
id = (Number.parseInt(id) + 1).toString();
id = (Number.parseInt(id, 10) + 1).toString();
}
} else {
// ensure provided ID is not in use
+58 -3
View File
@@ -1,5 +1,4 @@
import {copyFileSync, writeFileSync} from "node:fs";
import {copyFileSync, existsSync, readFileSync, writeFileSync} from "node:fs";
import data from "./data";
import * as settings from "./settings";
import utils from "./utils";
@@ -29,7 +28,7 @@ interface SettingsCustomHandler extends Omit<SettingsMigration, "path"> {
execute: (currentSettings: Partial<Settings>) => [validPath: boolean, previousValue: unknown, changed: boolean];
}
const SUPPORTED_VERSIONS: Settings["version"][] = [undefined, 2, 3, settings.CURRENT_VERSION];
const SUPPORTED_VERSIONS: Settings["version"][] = [undefined, 2, 3, 4, settings.CURRENT_VERSION];
function backupSettings(version: number): void {
const filePath = data.joinPath("configuration.yaml");
@@ -481,6 +480,58 @@ function migrateToFour(
});
}
function migrateToFive(
_currentSettings: Partial<Settings>,
transfers: SettingsTransfer[],
changes: SettingsChange[],
additions: SettingsAdd[],
removals: SettingsRemove[],
customHandlers: SettingsCustomHandler[],
): void {
transfers.push();
changes.push({
path: ["version"],
note: "Migrated settings to version 5",
newValue: 5,
});
additions.push();
removals.push();
const resetOtaStates = (_currentSettings: Partial<Settings>): ReturnType<SettingsCustomHandler["execute"]> => {
const filePath = data.joinPath("state.json");
if (existsSync(filePath)) {
try {
const stateFile = readFileSync(filePath, "utf8");
const stateObj = JSON.parse(stateFile) as KeyValue;
for (const key in stateObj) {
const entry = stateObj[key];
if (entry.update) {
delete entry.update;
}
}
writeFileSync(filePath, JSON.stringify(stateObj), "utf8");
// hack, this does not actually affect settings, we just want to ensure the migration note triggers
return [true, undefined, true];
} catch (error) {
console.error(`Failed to write state to '${filePath}' (${error})`);
}
}
return [false, undefined, false];
};
customHandlers.push({
note: "Reset all cached OTA states due to internal changes.",
noteIf: () => true,
execute: (currentSettings) => resetOtaStates(currentSettings),
});
}
/**
* Order of execution:
* - Transfer
@@ -533,6 +584,10 @@ export function migrateIfNecessary(): void {
migrationNotesFileName = "migration-3-to-4.log";
migrateToFour(currentSettings, transfers, changes, additions, removals, customHandlers);
} else if (currentSettings.version === 4) {
migrationNotesFileName = "migration-4-to-5.log";
migrateToFive(currentSettings, transfers, changes, additions, removals, customHandlers);
}
for (const transfer of transfers) {
+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));
}
}
+100 -62
View File
@@ -1,21 +1,20 @@
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";
import Device from "./model/device";
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 = /^(.+?)(?:\/([^/]+))?$/;
export default class Zigbee {
private herdsman!: Controller;
#herdsman!: Controller;
private eventBus: EventBus;
private groupLookup = new Map<number /* group ID */, Group>();
private deviceLookup = new Map<string /* IEEE address */, Device>();
@@ -25,7 +24,11 @@ export default class Zigbee {
this.eventBus = eventBus;
}
async start(): Promise<StartResult> {
get zhController() {
return this.#herdsman;
}
async start(abortSignal: AbortSignal): Promise<boolean> {
const infoHerdsman = await utils.getDependencyVersion("zigbee-herdsman");
logger.info(`Starting zigbee-herdsman (${infoHerdsman.version})`);
const panId = settings.get().advanced.pan_id;
@@ -58,42 +61,46 @@ 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;
try {
this.herdsman = new Controller(herdsmanSettings);
startResult = await this.herdsman.start();
this.#herdsman = new Controller(herdsmanSettings);
startResult = await this.#herdsman.start(abortSignal);
} catch (error) {
logger.error("Error while starting zigbee-herdsman");
throw error;
}
this.coordinatorIeeeAddr = this.herdsman.getDevicesByType("Coordinator")[0].ieeeAddr;
await this.resolveDevicesDefinitions();
this.coordinatorIeeeAddr = this.#herdsman.getDevicesByType("Coordinator")[0].ieeeAddr;
await this.resolveDevicesDefinitions(false, abortSignal);
this.herdsman.on("adapterDisconnected", () => this.eventBus.emitAdapterDisconnected());
this.herdsman.on("lastSeenChanged", (data: ZHEvents.LastSeenChangedPayload) => {
if (abortSignal.aborted) {
return false;
}
this.#herdsman.on("adapterDisconnected", () => this.eventBus.emitAdapterDisconnected());
this.#herdsman.on("lastSeenChanged", (data: ZHEvents.LastSeenChangedPayload) => {
// biome-ignore lint/style/noNonNullAssertion: assumed valid
this.eventBus.emitLastSeenChanged({device: this.resolveDevice(data.device.ieeeAddr)!, reason: data.reason});
});
this.herdsman.on("permitJoinChanged", (data: ZHEvents.PermitJoinChangedPayload) => {
this.#herdsman.on("permitJoinChanged", (data: ZHEvents.PermitJoinChangedPayload) => {
this.eventBus.emitPermitJoinChanged(data);
});
this.herdsman.on("deviceNetworkAddressChanged", (data: ZHEvents.DeviceNetworkAddressChangedPayload) => {
this.#herdsman.on("deviceNetworkAddressChanged", (data: ZHEvents.DeviceNetworkAddressChangedPayload) => {
// biome-ignore lint/style/noNonNullAssertion: assumed valid
const device = this.resolveDevice(data.device.ieeeAddr)!;
logger.debug(`Device '${device.name}' changed network address`);
this.eventBus.emitDeviceNetworkAddressChanged({device});
});
this.herdsman.on("deviceAnnounce", (data: ZHEvents.DeviceAnnouncePayload) => {
this.#herdsman.on("deviceAnnounce", (data: ZHEvents.DeviceAnnouncePayload) => {
// biome-ignore lint/style/noNonNullAssertion: assumed valid
const device = this.resolveDevice(data.device.ieeeAddr)!;
logger.debug(`Device '${device.name}' announced itself`);
this.eventBus.emitDeviceAnnounce({device});
});
this.herdsman.on("deviceInterview", async (data: ZHEvents.DeviceInterviewPayload) => {
this.#herdsman.on("deviceInterview", async (data: ZHEvents.DeviceInterviewPayload) => {
const device = this.resolveDevice(data.device.ieeeAddr);
/* v8 ignore next */ if (!device) return; // Prevent potential race
await device.resolveDefinition();
@@ -101,19 +108,19 @@ export default class Zigbee {
this.logDeviceInterview(d);
this.eventBus.emitDeviceInterview(d);
});
this.herdsman.on("deviceJoined", async (data: ZHEvents.DeviceJoinedPayload) => {
this.#herdsman.on("deviceJoined", async (data: ZHEvents.DeviceJoinedPayload) => {
const device = this.resolveDevice(data.device.ieeeAddr);
/* v8 ignore next */ if (!device) return; // Prevent potential race
await device.resolveDefinition();
logger.info(`Device '${device.name}' joined`);
this.eventBus.emitDeviceJoined({device});
});
this.herdsman.on("deviceLeave", (data: ZHEvents.DeviceLeavePayload) => {
this.#herdsman.on("deviceLeave", (data: ZHEvents.DeviceLeavePayload) => {
const name = settings.getDevice(data.ieeeAddr)?.friendly_name || data.ieeeAddr;
logger.warning(`Device '${name}' left the network`);
this.eventBus.emitDeviceLeave({ieeeAddr: data.ieeeAddr, name, device: this.deviceLookup.get(data.ieeeAddr)});
});
this.herdsman.on("message", async (data: ZHEvents.MessagePayload) => {
this.#herdsman.on("message", async (data: ZHEvents.MessagePayload) => {
// biome-ignore lint/style/noNonNullAssertion: assumed valid
const device = this.resolveDevice(data.device.ieeeAddr)!;
await device.resolveDefinition();
@@ -129,32 +136,55 @@ export default class Zigbee {
logger.info(`zigbee-herdsman started (${startResult})`);
logger.info(`Coordinator firmware version: '${stringify(await this.getCoordinatorVersion())}'`);
logger.debug(`Zigbee network parameters: ${stringify(await this.herdsman.getNetworkParameters())}`);
logger.debug(`Zigbee network parameters: ${stringify(await this.#herdsman.getNetworkParameters())}`);
for (const device of this.devicesIterator(utils.deviceNotCoordinator)) {
// If a passlist is used, all other device will be removed from the network.
const passlist = settings.get().passlist;
const blocklist = settings.get().blocklist;
const remove = async (device: Device): Promise<void> => {
try {
await device.zh.removeFromNetwork();
} catch (error) {
logger.error(`Failed to remove '${device.ieeeAddr}' (${(error as Error).message})`);
}
};
if (abortSignal.aborted) {
return false;
}
if (passlist.length > 0) {
const remove = async (device: Device): Promise<void> => {
try {
await device.zh.removeFromNetwork();
} catch (error) {
logger.error(`Failed to remove '${device.ieeeAddr}' (${(error as Error).message})`);
}
};
// If a passlist is used, all other device will be removed from the network.
const passlist = settings.get().passlist;
if (passlist.length > 0) {
for (const device of this.devicesIterator(utils.deviceNotCoordinator)) {
if (!passlist.includes(device.ieeeAddr)) {
logger.warning(`Device not on passlist currently connected (${device.ieeeAddr}), removing...`);
logger.warning(`Device not on passlist currently on the network (${device.ieeeAddr}), removing...`);
await remove(device);
if (abortSignal.aborted) {
return false;
}
}
}
} else {
for (const ieee of settings.get().blocklist) {
const device = this.resolveDevice(ieee);
if (device) {
if (device.zh.type === "Coordinator") {
continue;
}
logger.warning(`Device on blocklist currently on the network (${device.ieeeAddr}), removing...`);
await remove(device);
if (abortSignal.aborted) {
return false;
}
} else {
logger.debug(`Ignoring blocklist device ${ieee}, not currently on the network`);
}
} else if (blocklist.includes(device.ieeeAddr)) {
logger.warning(`Device on blocklist currently connected (${device.ieeeAddr}), removing...`);
await remove(device);
}
}
return startResult;
return true;
}
private logDeviceInterview(data: eventdata.DeviceInterview): void {
@@ -198,39 +228,39 @@ export default class Zigbee {
}
async getCoordinatorVersion(): Promise<zh.CoordinatorVersion> {
return await this.herdsman.getCoordinatorVersion();
return await this.#herdsman.getCoordinatorVersion();
}
isStopping(): boolean {
return this.herdsman.isStopping();
return this.#herdsman.isStopping();
}
async backup(): Promise<void> {
return await this.herdsman.backup();
return await this.#herdsman.backup();
}
async coordinatorCheck(): Promise<{missingRouters: Device[]}> {
const check = await this.herdsman.coordinatorCheck();
const check = await this.#herdsman.coordinatorCheck();
// biome-ignore lint/style/noNonNullAssertion: assumed valid
return {missingRouters: check.missingRouters.map((d) => this.resolveDevice(d.ieeeAddr)!)};
}
async getNetworkParameters(): Promise<zh.NetworkParameters> {
return await this.herdsman.getNetworkParameters();
return await this.#herdsman.getNetworkParameters();
}
async stop(): Promise<void> {
logger.info("Stopping zigbee-herdsman...");
await this.herdsman.stop();
await this.#herdsman?.stop(); // could be undefined if this is called during startup (abort)
logger.info("Stopped zigbee-herdsman");
}
getPermitJoin(): boolean {
return this.herdsman.getPermitJoin();
return this.#herdsman.getPermitJoin();
}
getPermitJoinEnd(): number | undefined {
return this.herdsman.getPermitJoinEnd();
return this.#herdsman.getPermitJoinEnd();
}
async permitJoin(time: number, device?: Device): Promise<void> {
@@ -240,18 +270,22 @@ export default class Zigbee {
logger.info("Zigbee: disabling joining new devices.");
}
await this.herdsman.permitJoin(time, device?.zh);
await this.#herdsman.permitJoin(time, device?.zh);
}
async resolveDevicesDefinitions(ignoreCache = false): Promise<void> {
async resolveDevicesDefinitions(ignoreCache = false, abortSignal: AbortSignal | undefined = undefined): Promise<void> {
for (const device of this.devicesIterator(utils.deviceNotCoordinator)) {
await device.resolveDefinition(ignoreCache);
if (abortSignal?.aborted) {
return;
}
}
}
@bind private resolveDevice(ieeeAddr: string): Device | undefined {
if (!this.deviceLookup.has(ieeeAddr)) {
const device = this.herdsman.getDeviceByIeeeAddr(ieeeAddr);
const device = this.#herdsman.getDeviceByIeeeAddr(ieeeAddr);
if (device) {
this.deviceLookup.set(ieeeAddr, new Device(device));
}
@@ -266,7 +300,7 @@ export default class Zigbee {
private resolveGroup(groupID: number): Group | undefined {
if (!this.groupLookup.has(groupID)) {
const group = this.herdsman.getGroupByID(groupID);
const group = this.#herdsman.getGroupByID(groupID);
if (group) {
this.groupLookup.set(groupID, new Group(group, this.resolveDevice));
@@ -339,33 +373,33 @@ export default class Zigbee {
}
firstCoordinatorEndpoint(): zh.Endpoint {
return this.herdsman.getDevicesByType("Coordinator")[0].endpoints[0];
return this.#herdsman.getDevicesByType("Coordinator")[0].endpoints[0];
}
*devicesAndGroupsIterator(
devicePredicate?: (value: zh.Device) => boolean,
groupPredicate?: (value: zh.Group) => boolean,
): Generator<Device | Group> {
for (const device of this.herdsman.getDevicesIterator(devicePredicate)) {
for (const device of this.#herdsman.getDevicesIterator(devicePredicate)) {
// biome-ignore lint/style/noNonNullAssertion: assumed valid
yield this.resolveDevice(device.ieeeAddr)!;
}
for (const group of this.herdsman.getGroupsIterator(groupPredicate)) {
for (const group of this.#herdsman.getGroupsIterator(groupPredicate)) {
// biome-ignore lint/style/noNonNullAssertion: assumed valid
yield this.resolveGroup(group.groupID)!;
}
}
*groupsIterator(predicate?: (value: zh.Group) => boolean): Generator<Group> {
for (const group of this.herdsman.getGroupsIterator(predicate)) {
for (const group of this.#herdsman.getGroupsIterator(predicate)) {
// biome-ignore lint/style/noNonNullAssertion: assumed valid
yield this.resolveGroup(group.groupID)!;
}
}
*devicesIterator(predicate?: (value: zh.Device) => boolean): Generator<Device> {
for (const device of this.herdsman.getDevicesIterator(predicate)) {
for (const device of this.#herdsman.getDevicesIterator(predicate)) {
// biome-ignore lint/style/noNonNullAssertion: assumed valid
yield this.resolveDevice(device.ieeeAddr)!;
}
@@ -399,33 +433,33 @@ export default class Zigbee {
}
async touchlinkFactoryResetFirst(): Promise<boolean> {
return await this.herdsman.touchlinkFactoryResetFirst();
return await this.#herdsman.touchlink.factoryResetFirst();
}
async touchlinkFactoryReset(ieeeAddr: string, channel: number): Promise<boolean> {
return await this.herdsman.touchlinkFactoryReset(ieeeAddr, channel);
return await this.#herdsman.touchlink.factoryReset(ieeeAddr, channel);
}
async addInstallCode(installCode: string): Promise<void> {
await this.herdsman.addInstallCode(installCode);
await this.#herdsman.addInstallCode(installCode);
}
async touchlinkIdentify(ieeeAddr: string, channel: number): Promise<void> {
await this.herdsman.touchlinkIdentify(ieeeAddr, channel);
await this.#herdsman.touchlink.identify(ieeeAddr, channel);
}
async touchlinkScan(): Promise<{ieeeAddr: string; channel: number}[]> {
return await this.herdsman.touchlinkScan();
return await this.#herdsman.touchlink.scan();
}
createGroup(id: number): Group {
this.herdsman.createGroup(id);
this.#herdsman.createGroup(id);
// biome-ignore lint/style/noNonNullAssertion: just created
return this.resolveGroup(id)!;
}
deviceByNetworkAddress(networkAddress: number): Device | undefined {
const device = this.herdsman.getDeviceByNetworkAddress(networkAddress);
const device = this.#herdsman.getDeviceByNetworkAddress(networkAddress);
return device && this.resolveDevice(device.ieeeAddr);
}
@@ -433,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);
}
}
+25 -30
View File
@@ -1,16 +1,16 @@
{
"name": "zigbee2mqtt",
"version": "2.6.2",
"version": "2.13.0",
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
"main": "index.js",
"types": "dist/types/api.d.ts",
"packageManager": "pnpm@10.12.1",
"packageManager": "pnpm@10.18.3",
"repository": {
"type": "git",
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
},
"engines": {
"node": "^20 || ^22 || ^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,45 +42,40 @@
},
"homepage": "https://koenkk.github.io/zigbee2mqtt",
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.20.0",
"bind-decorator": "^1.0.11",
"debounce": "^2.2.0",
"express-static-gzip": "^3.0.0",
"debounce": "^3.0.0",
"express-static-gzip": "^3.0.1",
"fast-deep-equal": "^3.1.3",
"finalhandler": "^2.1.0",
"humanize-duration": "^3.33.1",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"jszip": "^3.10.1",
"moment": "^2.30.1",
"mqtt": "^5.14.1",
"fflate": "^0.8.3",
"finalhandler": "^2.1.1",
"humanize-duration": "^3.34.0",
"js-yaml": "^5.2.2",
"mqtt": "^5.15.2",
"object-assign-deep": "^0.4.0",
"rimraf": "^6.0.1",
"semver": "^7.7.1",
"source-map-support": "^0.5.21",
"throttleit": "^2.1.0",
"winston": "^3.18.2",
"semver": "^7.8.5",
"throttleit": "^3.0.0",
"winston": "^3.19.0",
"winston-syslog": "^2.7.1",
"winston-transport": "^4.9.0",
"ws": "^8.18.1",
"zigbee-herdsman": "6.1.5",
"zigbee-herdsman-converters": "25.35.1",
"ws": "^8.21.1",
"zigbee-herdsman": "10.8.0",
"zigbee-herdsman-converters": "26.90.0",
"zigbee2mqtt-frontend": "0.9.21",
"zigbee2mqtt-windfront": "2.2.1"
"zigbee2mqtt-windfront": "2.14.0"
},
"devDependencies": {
"@biomejs/biome": "^2.2.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.6.0",
"@types/node": "^26.1.2",
"@types/object-assign-deep": "^0.4.3",
"@types/readable-stream": "4.0.21",
"@types/serve-static": "^1.15.7",
"@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": {
+527 -870
View File
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -59,7 +59,13 @@ if (fs.existsSync(commitUserFile)) {
}
const definitions = definitionIndex.default.map((d) => zhc.prepareDefinition(d));
const whiteLabels = definitions.filter((d) => d.whiteLabel).flatMap((d) => d.whiteLabel);
const whiteLabels = definitions
.filter((d) => d.whiteLabel)
.flatMap((d) =>
d.whiteLabel.map((wl) => {
return {model: wl.model, vendor: wl.vendor ?? d.vendor, description: wl.description ?? d.description};
}),
);
const capitalizeFirstChar = (str) => str.charAt(0).toUpperCase() + str.slice(1);
for (const changelog of changelogs) {
+176
View File
@@ -0,0 +1,176 @@
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`
const searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:issue -is:pr "${name}" label:"new device support","external converter"`;
try {
const searchResults = await github.rest.search.issuesAndPullRequests({q: searchQuery, per_page: 100});
// Filter out the current issue and return the first duplicate found
const existingIssues = searchResults.data.items.filter((item) => item.number !== context.payload.issue.number).map((i) => `#${i.number}`);
if (existingIssues.length > 0) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
body: `👋 Hi there! This issue appears to be a duplicate of ${existingIssues.join(", ")}
This issue will be closed. Please follow the existing issue for updates.`,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
state: "closed",
});
return true;
}
} catch (error) {
console.error(`Error searching for duplicate issues with ${name}:`, error);
}
return false;
}
export async function newDeviceSupport(github, _core, context, zhcDir) {
const issue = context.payload.issue;
// Hide previous bot comments
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
});
for (const comment of comments.data) {
if (comment.user.type === "Bot" && comment.user.login === "github-actions[bot]") {
await github.graphql(`mutation {
minimizeComment(input: {subjectId: "${comment.node_id}", classifier: OUTDATED}) {
clientMutationId
}
}`);
}
}
const titleAndBody = `${issue.title}\n\n${issue.body ?? ""}`;
// Check if Tuya manufacturer name is already supported.
const tuyaManufacturerNameRe = /['"](_T\w+_(\w+))['"]/g;
const tuyaManufacturerNames = Array.from(titleAndBody.matchAll(tuyaManufacturerNameRe), (m) => [m[1], m[2]]);
console.log("Found tuyaManufacturerNames", tuyaManufacturerNames);
if (tuyaManufacturerNames.length > 0) {
for (const [fullName, partialName] of tuyaManufacturerNames) {
if (await checkDuplicateIssue(github, context, fullName)) return;
const fullMatch = grepTypeScriptFiles(fullName, zhcDir);
console.log(`Checking full match for '${fullName}', result: '${fullMatch}'`);
if (fullMatch) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `👋 Hi there! The Tuya device with manufacturer name \`${fullName}\` is already supported in the latest dev branch.
See this [guide](https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html) on how to update, after updating you can remove your external converter.
In case you created the external converter with the goal to extend or fix an issue with the out-of-the-box support, please submit a pull request.
For instructions on how to create a pull request see the [docs](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html#_4-create-a-pull-request).
If you need help with the process, feel free to ask here and we'll be happy to assist.`,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: "closed",
});
return;
}
const partialMatch = grepTypeScriptFiles(partialName, zhcDir);
console.log(`Checking partial match for '${partialName}', result: '${partialMatch}'`);
if (partialMatch) {
const candidates = Array.from(partialMatch.matchAll(tuyaManufacturerNameRe), (m) => m[1]);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `👋 Hi there! A similar Tuya device of which the manufacturer name also ends with \`_${partialName}\` is already supported.
This means the device can probably be easily be supported by re-using the existing converter.
I found the following matches: ${candidates.map((c) => `\`${c}\``).join(", ")}
Try to stop Z2M, change all occurrences of \`${fullName}\` in the \`data/database.db\` to one of the matches above and start Z2M.
Let us know if it works so we can support this device out-of-the-box!`,
});
return;
}
}
} else {
// Check if zigbee model is already supported.
const zigbeeModelRe = /zigbeeModel: \[['"](.+)['"]\]/g;
const zigbeeModels = Array.from(titleAndBody.matchAll(zigbeeModelRe), (m) => m[1]);
if (zigbeeModels.length > 0) {
for (const zigbeeModel of zigbeeModels) {
if (await checkDuplicateIssue(github, context, zigbeeModel)) return;
const fullMatch = grepTypeScriptFiles(`"${zigbeeModel}"`, zhcDir);
if (fullMatch) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `👋 Hi there! The device with zigbee model \`${zigbeeModel}\` is already supported in the latest dev branch.
See this [guide](https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html) on how to update, after updating you can remove your external converter.
In case you created the external converter with the goal to extend or fix an issue with the out-of-the-box support, please submit a pull request.
For instructions on how to create a pull request see the [docs](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html#_4-create-a-pull-request).
If you need help with the process, feel free to ask here and we'll be happy to assist.`,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: "closed",
});
return;
}
}
}
}
// Create a request to pull request comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `🙏 Thank you for creating this issue and sharing your external converter!
In case all features work, please submit a pull request on this repository so the device can be supported out-of-the-box with the next release.
For instructions on how to create a pull request see the [docs](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html#_4-create-a-pull-request).
If **NOT** all features work, please follow the [How To Support new devices](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html).
${
tuyaManufacturerNames.length > 0
? "Since this is a Tuya also consider [How To Support new Tuya devices](https://www.zigbee2mqtt.io/advanced/support-new-devices/02_support_new_tuya_devices.html)."
: ""
}
If you need help with the process, feel free to ask here and we'll be happy to assist.`,
});
}
+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));
+1 -1
View File
@@ -91,4 +91,4 @@ if (processArgs.length !== 1) {
const eraser = new ZStackNvMemEraser(processArgs[0]);
eraser.start();
void eraser.start();
+46 -44
View File
@@ -1,7 +1,6 @@
import {existsSync, mkdirSync} from "node:fs";
import stringify from "json-stable-stringify-without-jsonify";
import {bench, describe} from "vitest";
import {Zcl, Zdo, ZSpec} from "zigbee-herdsman";
import {bench, describe, vi} from "vitest";
import {type Controller, Zcl, Zdo, ZSpec} from "zigbee-herdsman";
import type Adapter from "zigbee-herdsman/dist/adapter/adapter";
import type {ZclPayload} from "zigbee-herdsman/dist/adapter/events";
import {Device, InterviewState} from "zigbee-herdsman/dist/controller/model/device";
@@ -12,8 +11,51 @@ 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) => {
const actual = await importOriginal<typeof import("zigbee-herdsman")>();
class MockHerdsman {
on: Controller["on"] = vi.fn();
start: Controller["start"] = async () => "resumed" as const;
stop: Controller["stop"] = async () => {};
isStopping: Controller["isStopping"] = () => false;
getCoordinatorVersion: Controller["getCoordinatorVersion"] = async () =>
Promise.resolve({
type: "Dummy",
meta: {revision: "9.9.9"},
});
getNetworkParameters: Controller["getNetworkParameters"] = async () => Promise.resolve({...NETWORK_PARAMS});
getPermitJoin: Controller["getPermitJoin"] = () => false;
getPermitJoinEnd: Controller["getPermitJoinEnd"] = () => undefined;
getDeviceByIeeeAddr: Controller["getDeviceByIeeeAddr"] = (ieeeAddr) => ZH_DEVICES.find((device) => device.ieeeAddr === ieeeAddr);
getGroupByID: Controller["getGroupByID"] = (id) => ZH_GROUPS.find((group) => group.groupID === id);
getDevicesByType: Controller["getDevicesByType"] = (type) => ZH_DEVICES.filter((device) => device.type === type);
getDeviceByNetworkAddress: Controller["getDeviceByNetworkAddress"] = (networkAddress) =>
ZH_DEVICES.find((device) => device.networkAddress === networkAddress);
*getDevicesIterator(predicate: ((device: Device) => boolean) | undefined) {
for (const device of ZH_DEVICES) {
if (!predicate || predicate(device)) {
yield device;
}
}
}
*getGroupsIterator(predicate: ((group: Group) => boolean) | undefined) {
for (const group of ZH_GROUPS) {
if (!predicate || predicate(group)) {
yield group;
}
}
}
}
return {
...actual,
Controller: MockHerdsman,
};
});
process.env.ZIGBEE2MQTT_DATA = "data-bench";
data._testReload();
@@ -215,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({
@@ -365,45 +407,6 @@ const initController = async () => {
async () => {},
);
controller.zigbee.start = async () => {
await controller.zigbee.resolveDevicesDefinitions();
return "resumed";
};
controller.zigbee.stop = async () => {
await Promise.resolve();
};
// @ts-expect-error mocking private
controller.zigbee.herdsman = {
isStopping: () => false,
getCoordinatorVersion: async () =>
Promise.resolve({
type: "Dummy",
meta: {revision: "9.9.9"},
}),
getNetworkParameters: async () => Promise.resolve({...NETWORK_PARAMS}),
getPermitJoin: () => false,
getPermitJoinEnd: () => undefined,
getDeviceByIeeeAddr: (ieeeAddr) => ZH_DEVICES.find((device) => device.ieeeAddr === ieeeAddr),
getGroupByID: (id) => ZH_GROUPS.find((group) => group.groupID === id),
getDevicesByType: (type) => ZH_DEVICES.filter((device) => device.type === type),
getDeviceByNetworkAddress: (networkAddress) => ZH_DEVICES.find((device) => device.networkAddress === networkAddress),
*getDevicesIterator(predicate) {
for (const device of ZH_DEVICES) {
if (!predicate || predicate(device)) {
yield device;
}
}
},
*getGroupsIterator(predicate) {
for (const group of ZH_GROUPS) {
if (!predicate || predicate(group)) {
yield group;
}
}
},
};
// all dummies, can trigger `controller.mqtt.onMessage(topic, message)` as needed
// @ts-expect-error mocking private
controller.mqtt.client = {
@@ -418,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 () => {
+269 -4
View File
@@ -17,14 +17,15 @@ 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";
import type Device from "../lib/model/device";
import * as settings from "../lib/util/settings";
import Frontend from "../lib/extension/frontend";
const LOG_MQTT_NS = "z2m:mqtt";
@@ -54,6 +55,7 @@ const mocksClear = [
describe("Controller", () => {
let controller: Controller;
let mockExit: Mock;
let stopAfter = true;
const getZ2MDevice = (zhDevice: string | number | ZhDevice): Device => {
return controller.zigbee.resolveEntity(zhDevice)! as Device;
@@ -64,6 +66,7 @@ describe("Controller", () => {
});
beforeEach(() => {
stopAfter = true;
returnDevices.splice(0);
mockExit = vi.fn();
data.writeDefaultConfiguration();
@@ -79,7 +82,10 @@ describe("Controller", () => {
});
afterEach(async () => {
await controller?.stop();
if (stopAfter) {
await controller?.stop();
}
await flushPromises();
});
@@ -106,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", {
@@ -143,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,
};
@@ -160,6 +167,7 @@ describe("Controller", () => {
username: "user1",
clientId: "my_client_id",
rejectUnauthorized: false,
servername: "mqtt.example.com",
protocolVersion: 5,
properties: {maximumPacketSize: 20000},
};
@@ -323,11 +331,14 @@ describe("Controller", () => {
});
it("Should remove device on blocklist on startup", async () => {
settings.set(["blocklist"], [devices.bulb_color.ieeeAddr]);
settings.set(["blocklist"], [devices.bulb_color.ieeeAddr, "0x9998889998889990", "0x00124b00120144ae"]);
await controller.start();
await flushPromises();
expect(devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(1);
expect(devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(0);
expect(devices.coordinator.removeFromNetwork).toHaveBeenCalledTimes(0);
const debugCalls = mockLogger.debug.mock.calls.map((c) => (typeof c[0] === "string" ? c[0] : c[0]()));
expect(debugCalls.find((v) => v === "Ignoring blocklist device 0x9998889998889990, not currently on the network")).toBeDefined();
});
it("Start controller fails", async () => {
@@ -409,6 +420,252 @@ describe("Controller", () => {
delete process.env.NOTIFY_SOCKET;
});
describe("aborts during startup", () => {
let stateStartSpy: MockInstance<() => void>;
let zigbeeStartSpy: MockInstance<() => Promise<boolean>>;
let mqttConnectSpy: MockInstance<() => Promise<void>>;
let extensionStartSpy: MockInstance<() => Promise<void>>;
let publishEntityStateSpy: MockInstance<() => Promise<void>>;
let extensionStopSpy: MockInstance<() => Promise<void>>;
let mqttDisconnectSpy: MockInstance<() => Promise<void>>;
let mqttPublishSpy: MockInstance<() => Promise<void>>;
beforeEach(() => {
stateStartSpy = vi.spyOn(controller.state, "start");
zigbeeStartSpy = vi.spyOn(controller.zigbee, "start");
mqttConnectSpy = vi.spyOn(controller.mqtt, "connect");
const [firstExt] = controller.extensions;
extensionStartSpy = vi.spyOn(firstExt, "start");
publishEntityStateSpy = vi.spyOn(controller, "publishEntityState");
extensionStopSpy = vi.spyOn(firstExt, "stop");
mqttDisconnectSpy = vi.spyOn(controller.mqtt, "disconnect");
mqttPublishSpy = vi.spyOn(controller.mqtt, "publish");
});
afterEach(() => {
stopAfter = false;
stateStartSpy.mockRestore();
zigbeeStartSpy.mockRestore();
mqttConnectSpy.mockRestore();
extensionStartSpy.mockRestore();
publishEntityStateSpy.mockRestore();
extensionStopSpy.mockRestore();
mqttDisconnectSpy.mockRestore();
mqttPublishSpy.mockRestore();
});
it("during state start", async () => {
stateStartSpy.mockImplementationOnce(async () => {
await controller.stop(undefined, undefined, "SIGINT");
});
await controller.start();
await flushPromises();
expect(stateStartSpy).toHaveBeenCalledTimes(1);
expect(zigbeeStartSpy).toHaveBeenCalledTimes(0);
expect(mqttConnectSpy).toHaveBeenCalledTimes(0);
expect(extensionStartSpy).toHaveBeenCalledTimes(0);
expect(publishEntityStateSpy).toHaveBeenCalledTimes(0);
expect(extensionStopSpy).toHaveBeenCalledTimes(1);
expect(mqttDisconnectSpy).toHaveBeenCalledTimes(1);
expect(mqttPublishSpy).toHaveBeenCalledTimes(0);
});
it("during zigbee resolve definitions", async () => {
const resolveDevicesDefinitionsSpy = vi.spyOn(controller.zigbee, "resolveDevicesDefinitions");
resolveDevicesDefinitionsSpy.mockImplementationOnce(async (ignoreCache, abortSignal) => {
const device = controller.zigbee.resolveEntity(devices.bulb)! as Device;
device.resolveDefinition = vi.fn(async () => {
await controller.stop(undefined, undefined, "SIGINT");
});
return await controller.zigbee.resolveDevicesDefinitions(ignoreCache, abortSignal);
});
await controller.start();
await flushPromises();
expect(stateStartSpy).toHaveBeenCalledTimes(1);
expect(zigbeeStartSpy).toHaveBeenCalledTimes(1);
expect(mqttConnectSpy).toHaveBeenCalledTimes(0);
expect(extensionStartSpy).toHaveBeenCalledTimes(0);
expect(publishEntityStateSpy).toHaveBeenCalledTimes(0);
expect(extensionStopSpy).toHaveBeenCalledTimes(1);
expect(mqttDisconnectSpy).toHaveBeenCalledTimes(1);
expect(mqttPublishSpy).toHaveBeenCalledTimes(0);
});
it("during MQTT connect", async () => {
mqttConnectSpy.mockImplementationOnce(async () => {
await controller.stop(undefined, undefined, "SIGINT");
});
await controller.start();
await flushPromises();
expect(stateStartSpy).toHaveBeenCalledTimes(1);
expect(zigbeeStartSpy).toHaveBeenCalledTimes(1);
expect(mqttConnectSpy).toHaveBeenCalledTimes(1);
expect(extensionStartSpy).toHaveBeenCalledTimes(0);
expect(publishEntityStateSpy).toHaveBeenCalledTimes(0);
expect(extensionStopSpy).toHaveBeenCalledTimes(1);
expect(mqttDisconnectSpy).toHaveBeenCalledTimes(1);
expect(mqttPublishSpy).toHaveBeenCalledTimes(0);
});
it("during extension start", async () => {
extensionStartSpy.mockImplementationOnce(async () => {
await controller.stop(undefined, undefined, "SIGINT");
});
await controller.start();
await flushPromises();
expect(stateStartSpy).toHaveBeenCalledTimes(1);
expect(zigbeeStartSpy).toHaveBeenCalledTimes(1);
expect(mqttConnectSpy).toHaveBeenCalledTimes(1);
expect(extensionStartSpy).toHaveBeenCalledTimes(1);
expect(publishEntityStateSpy).toHaveBeenCalledTimes(0);
expect(extensionStopSpy).toHaveBeenCalledTimes(1);
expect(mqttDisconnectSpy).toHaveBeenCalledTimes(1);
expect(mqttPublishSpy).toHaveBeenCalledTimes(3);
});
it("during entity state publish", async () => {
publishEntityStateSpy.mockImplementationOnce(async () => {
await controller.stop(undefined, undefined, "SIGINT");
});
await controller.start();
await flushPromises();
expect(stateStartSpy).toHaveBeenCalledTimes(1);
expect(zigbeeStartSpy).toHaveBeenCalledTimes(1);
expect(mqttConnectSpy).toHaveBeenCalledTimes(1);
expect(extensionStartSpy).toHaveBeenCalledTimes(1);
expect(publishEntityStateSpy).toHaveBeenCalledTimes(1);
expect(extensionStopSpy).toHaveBeenCalledTimes(1);
expect(mqttDisconnectSpy).toHaveBeenCalledTimes(1);
expect(mqttPublishSpy).toHaveBeenCalledTimes(13);
});
it("without signal during state start", async () => {
stateStartSpy.mockImplementationOnce(async () => {
await controller.stop();
});
await controller.start();
await flushPromises();
expect(stateStartSpy).toHaveBeenCalledTimes(1);
expect(zigbeeStartSpy).toHaveBeenCalledTimes(0);
expect(mqttConnectSpy).toHaveBeenCalledTimes(0);
expect(extensionStartSpy).toHaveBeenCalledTimes(0);
expect(publishEntityStateSpy).toHaveBeenCalledTimes(0);
expect(extensionStopSpy).toHaveBeenCalledTimes(1);
expect(mqttDisconnectSpy).toHaveBeenCalledTimes(1);
expect(mqttPublishSpy).toHaveBeenCalledTimes(0);
});
it("without signal during MQTT connect", async () => {
mqttConnectSpy.mockImplementationOnce(async () => {
await controller.stop();
});
await controller.start();
await flushPromises();
expect(stateStartSpy).toHaveBeenCalledTimes(1);
expect(zigbeeStartSpy).toHaveBeenCalledTimes(1);
expect(mqttConnectSpy).toHaveBeenCalledTimes(1);
expect(extensionStartSpy).toHaveBeenCalledTimes(0);
expect(publishEntityStateSpy).toHaveBeenCalledTimes(0);
expect(extensionStopSpy).toHaveBeenCalledTimes(1);
expect(mqttDisconnectSpy).toHaveBeenCalledTimes(1);
expect(mqttPublishSpy).toHaveBeenCalledTimes(0);
});
it("during ZH Controller start", async () => {
mockZHController.start.mockImplementationOnce(async (abortSignal) => {
void controller.stop(undefined, undefined, "SIGINT");
abortSignal?.throwIfAborted();
return await Promise.resolve("resumed");
});
await controller.start();
await flushPromises();
expect(stateStartSpy).toHaveBeenCalledTimes(1);
expect(zigbeeStartSpy).toHaveBeenCalledTimes(1);
expect(mqttConnectSpy).toHaveBeenCalledTimes(0);
expect(extensionStartSpy).toHaveBeenCalledTimes(0);
expect(publishEntityStateSpy).toHaveBeenCalledTimes(0);
expect(extensionStopSpy).toHaveBeenCalledTimes(1);
expect(mqttDisconnectSpy).toHaveBeenCalledTimes(1);
expect(mqttPublishSpy).toHaveBeenCalledTimes(0);
});
it("during ZH Controller getNetworkParameters", async () => {
mockZHController.getNetworkParameters.mockImplementationOnce(async () => {
await controller.stop(undefined, undefined, "SIGINT");
return await Promise.resolve({panID: 0x162a, extendedPanID: "0x64c5fd698daf0c00", channel: 15, nwkUpdateID: 0});
});
await controller.start();
await flushPromises();
expect(stateStartSpy).toHaveBeenCalledTimes(1);
expect(zigbeeStartSpy).toHaveBeenCalledTimes(1);
expect(mqttConnectSpy).toHaveBeenCalledTimes(0);
expect(extensionStartSpy).toHaveBeenCalledTimes(0);
expect(publishEntityStateSpy).toHaveBeenCalledTimes(0);
expect(extensionStopSpy).toHaveBeenCalledTimes(1);
expect(mqttDisconnectSpy).toHaveBeenCalledTimes(1);
expect(mqttPublishSpy).toHaveBeenCalledTimes(0);
});
it("during ZH Controller passlist", async () => {
settings.set(["passlist"], [devices.bulb_color.ieeeAddr]);
devices.bulb.removeFromNetwork.mockImplementationOnce(async () => {
await controller.stop(undefined, undefined, "SIGINT");
});
await controller.start();
await flushPromises();
expect(stateStartSpy).toHaveBeenCalledTimes(1);
expect(zigbeeStartSpy).toHaveBeenCalledTimes(1);
expect(mqttConnectSpy).toHaveBeenCalledTimes(0);
expect(extensionStartSpy).toHaveBeenCalledTimes(0);
expect(publishEntityStateSpy).toHaveBeenCalledTimes(0);
expect(extensionStopSpy).toHaveBeenCalledTimes(1);
expect(mqttDisconnectSpy).toHaveBeenCalledTimes(1);
expect(mqttPublishSpy).toHaveBeenCalledTimes(0);
});
it("during ZH Controller blocklist", async () => {
settings.set(["blocklist"], [devices.bulb_color.ieeeAddr]);
devices.bulb_color.removeFromNetwork.mockImplementationOnce(async () => {
await controller.stop(undefined, undefined, "SIGINT");
});
await controller.start();
await flushPromises();
expect(stateStartSpy).toHaveBeenCalledTimes(1);
expect(zigbeeStartSpy).toHaveBeenCalledTimes(1);
expect(mqttConnectSpy).toHaveBeenCalledTimes(0);
expect(extensionStartSpy).toHaveBeenCalledTimes(0);
expect(publishEntityStateSpy).toHaveBeenCalledTimes(0);
expect(extensionStopSpy).toHaveBeenCalledTimes(1);
expect(mqttDisconnectSpy).toHaveBeenCalledTimes(1);
expect(mqttPublishSpy).toHaveBeenCalledTimes(0);
});
});
it("Start controller adapter disconnects", async () => {
// Fail to stop extension exit code 1 should not override adapter disconnect exit code 2
vi.spyOn(Array.from(controller.extensions)[0], "stop").mockRejectedValueOnce(new Error("failed"));
@@ -1168,4 +1425,12 @@ describe("Controller", () => {
await controller.enableDisableExtension(false, "Availability");
}).rejects.toThrow("Built-in extension Availability cannot be disabled at runtime");
});
it("throws error when adding extension twice", async () => {
settings.set(["frontend", "enabled"], true);
await controller.start();
await expect(async () => {
await controller.addExtension(new Frontend(...controller.extensionArgs));
}).rejects.toThrow("Extension with name Frontend already present");
});
});
+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";
+82 -2
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";
@@ -358,7 +358,7 @@ describe("Extension: Bind", () => {
{attribute: "currentX", minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1},
{attribute: "currentY", minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1},
]);
expect(devices.bulb_color.meta.configured).toBe(332242049);
expect(devices.bulb_color.meta.configured).toBe("0.0.0");
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/unbind",
stringify({
@@ -795,4 +795,84 @@ describe("Extension: Bind", () => {
// Should only call Hue bulb, not e.g. tradfri
expect(devices.bulb_2.getEndpoint(1)!.read).toHaveBeenCalledTimes(0);
});
it("clears all bindings", async () => {
const device = devices.remote;
device.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/binds/clear", stringify({transaction: "1234", target: "remote"}));
await flushPromises();
expect(device.clearAllBindings).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/binds/clear",
stringify({
transaction: "1234",
data: {
target: "remote",
ieee_list: ["0xffffffffffffffff"],
},
status: "ok",
}),
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
});
it("clears targeted bindings", async () => {
const device = devices.remote;
const target = devices.bulb_color;
device.mockClear();
mockMQTTEvents.message(
"zigbee2mqtt/bridge/request/device/binds/clear",
stringify({transaction: "1234", target: "remote", ieee_list: [target.ieeeAddr]}),
);
await flushPromises();
expect(device.clearAllBindings).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/binds/clear",
stringify({
transaction: "1234",
data: {
target: "remote",
ieee_list: [target.ieeeAddr],
},
status: "ok",
}),
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
});
it("throw on invalid clears bindings payload", async () => {
const device = devices.remote;
device.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/binds/clear", stringify({targetz: "remote"}));
await flushPromises();
expect(device.clearAllBindings).toHaveBeenCalledTimes(0);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/binds/clear",
stringify({data: {}, status: "error", error: "Invalid payload"}),
{},
);
});
it("throw on invalid clears bindings target", async () => {
const device = devices.remote;
device.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/binds/clear", stringify({target: "remotez"}));
await flushPromises();
expect(device.clearAllBindings).toHaveBeenCalledTimes(0);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/binds/clear",
stringify({data: {}, status: "error", error: "Invalid target"}),
{},
);
});
});
File diff suppressed because it is too large Load Diff
+123 -25
View File
@@ -4,13 +4,15 @@ import * as data from "../mocks/data";
import {mockLogger} from "../mocks/logger";
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
import {flushPromises} from "../mocks/utils";
import {type Device, devices, type Endpoint, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
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";
import Configure from "../../lib/extension/configure";
import * as settings from "../../lib/util/settings";
import assert from "node:assert";
const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.debug];
@@ -23,12 +25,9 @@ describe("Extension: Configure", () => {
await controller.addExtension(new Configure(...controller.extensionArgs));
};
const mockClear = (device: Device): void => {
const mockClear = (device: ZhDevice): void => {
for (const endpoint of device.endpoints) {
endpoint.read.mockClear();
endpoint.write.mockClear();
endpoint.configureReporting.mockClear();
endpoint.bind.mockClear();
endpoint.mockClear();
}
};
@@ -42,7 +41,7 @@ describe("Extension: Configure", () => {
const endpoint2 = device.getEndpoint(2)!;
expect(endpoint2.write).toHaveBeenCalledTimes(1);
expect(endpoint2.write).toHaveBeenCalledWith("genBasic", {49: {type: 25, value: 11}}, {disableDefaultResponse: true, manufacturerCode: 4107});
expect(device.meta.configured).toBe(332242049);
expect(device.meta.configured).toBe("0.0.0");
};
const expectBulbConfigured = (): void => {
@@ -147,17 +146,75 @@ describe("Extension: Configure", () => {
expectBulbConfigured();
});
it("Should re-configure when the version of the definition changes to a different value than the default (0.0.0)", async () => {
// Device is initially configured (definition has uses default version of 0.0.0)
const device = devices.bulb;
expectBulbConfigured();
// Nothing happens when receiving a Zigbee message
mockClear(device);
await mockZHEvents.lastSeenChanged({device});
await flushPromises();
expectBulbNotConfigured();
// Simulate that the definition version changes
const resolvedEntity = controller.zigbee.resolveEntity(device);
assert(resolvedEntity instanceof Device && resolvedEntity.definition);
assert(resolvedEntity.definition.version === "0.0.0");
try {
// Change the version to a different value
resolvedEntity.definition.version = "0.0.1";
// Now it should re-configure upon receiving a Zigbee message
mockClear(device);
await mockZHEvents.lastSeenChanged({device});
await flushPromises();
expectBulbConfigured();
} finally {
resolvedEntity.definition.version = "0.0.0";
}
});
it("Should NOT re-configure when version of the definition is 0.0.0", async () => {
// This test the migration from the old configureKey (hash of the configure function) to the new definition version system.
// See commment in `configure.ts` -> `shouldReconfigure` for more details.
// Device is initially configured (definition has uses default version of 0.0.0)
const device = devices.bulb;
expectBulbConfigured();
device.meta.configured = 1321;
// Nothing happens when receiving a Zigbee message
mockClear(device);
await mockZHEvents.lastSeenChanged({device});
await flushPromises();
expectBulbNotConfigured();
});
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 () => {
@@ -182,11 +239,11 @@ describe("Extension: Configure", () => {
});
it("Fail to configure via MQTT when device has no configure", async () => {
await mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/configure", stringify({id: "0x0017882104a44562", transaction: 20}));
await mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/configure", stringify({id: "0x0017980134e45545", transaction: 20}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/configure",
stringify({data: {}, status: "error", error: "Device 'TS0601_cover_switch' cannot be configured", transaction: 20}),
stringify({data: {}, status: "error", error: "Device '0x0017980134e45545' cannot be configured", transaction: 20}),
{},
);
});
@@ -227,27 +284,68 @@ describe("Extension: Configure", () => {
});
it("Should configure max 3 times when fails", async () => {
// @ts-expect-error private
(controller.getExtension("Configure")! as Configure).attempts = {};
const device = devices.remote;
delete device.meta.configured;
const endpoint = device.getEndpoint(1)!;
mockClear(device);
endpoint.bind.mockRejectedValueOnce(new Error("BLA"));
endpoint.bind.mockRejectedValueOnce(new Error("BLA1"));
await mockZHEvents.lastSeenChanged({device});
await flushPromises();
endpoint.bind.mockRejectedValueOnce(new Error("BLA"));
endpoint.bind.mockRejectedValueOnce(new Error("BLA2"));
await mockZHEvents.lastSeenChanged({device});
await flushPromises();
endpoint.bind.mockRejectedValueOnce(new Error("BLA"));
endpoint.bind.mockRejectedValueOnce(new Error("BLA3"));
await mockZHEvents.lastSeenChanged({device});
await flushPromises();
endpoint.bind.mockRejectedValueOnce(new Error("BLA"));
await mockZHEvents.lastSeenChanged({device});
await flushPromises();
endpoint.bind.mockRejectedValueOnce(new Error("BLA"));
await mockZHEvents.lastSeenChanged({device});
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(3);
});
it("successfully configures after couple fails", async () => {
const device = devices.remote;
delete device.meta.configured;
const endpoint = device.getEndpoint(1)!;
mockClear(device);
endpoint.bind.mockRejectedValueOnce(new Error("BLA1"));
await mockZHEvents.lastSeenChanged({device});
await flushPromises();
endpoint.bind.mockRejectedValueOnce(new Error("BLA2"));
await mockZHEvents.lastSeenChanged({device});
await flushPromises();
await mockZHEvents.lastSeenChanged({device});
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(2 /* fails */ + 2 /* definition */);
});
it("successfully configures with force after fails", async () => {
const device = devices.remote;
delete device.meta.configured;
const endpoint = device.getEndpoint(1)!;
mockClear(device);
endpoint.bind.mockRejectedValueOnce(new Error("BLA1"));
await mockZHEvents.lastSeenChanged({device});
await flushPromises();
endpoint.bind.mockRejectedValueOnce(new Error("BLA2"));
await mockZHEvents.lastSeenChanged({device});
await flushPromises();
endpoint.bind.mockRejectedValueOnce(new Error("BLA3"));
await mockZHEvents.lastSeenChanged({device});
await flushPromises();
await mockZHEvents.lastSeenChanged({device});
await flushPromises();
await mockZHEvents.lastSeenChanged({device});
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(3);
endpoint.bind.mockRejectedValueOnce(new Error("BLA4"));
await mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/configure", stringify({id: device.ieeeAddr}));
await flushPromises();
await mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/configure", stringify({id: device.ieeeAddr}));
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(4 /* fails */ + 2 /* definition */);
});
});
+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 -5
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},
},
@@ -478,10 +484,6 @@ describe("Extension: Frontend", () => {
await controller.enableDisableExtension(true, "Frontend");
await expect(async () => {
await controller.enableDisableExtension(true, "Frontend");
}).rejects.toThrow("Extension with name Frontend already present");
settings.set(["frontend", "enabled"], false);
await controller.enableDisableExtension(false, "Frontend");
+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";
File diff suppressed because it is too large Load Diff
+144 -70
View File
@@ -1,5 +1,6 @@
// biome-ignore assist/source/organizeImports: import mocks first
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import type {Eui64} from "zigbee-herdsman/dist/zspec/tstypes";
import * as data from "../mocks/data";
import {mockLogger} from "../mocks/logger";
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
@@ -9,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";
@@ -25,6 +26,22 @@ returnDevices.push(
const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.debug];
const LQI_BASE_RSP = {
extendedPanId: [],
deviceType: 0,
rxOnWhenIdle: 0,
reserved1: 0,
permitJoining: 0,
reserved2: 0,
};
const ROUTING_BASE_RSP = {
memoryConstrained: 0,
manyToOne: 0,
routeRecordRequired: 0,
reserved1: 0,
};
describe("Extension: NetworkMap", () => {
let controller: Controller;
@@ -39,58 +56,62 @@ describe("Extension: NetworkMap", () => {
* | -> CC2530_ROUTER -> WXKG02LM_rev1
*
*/
devices.coordinator.lqi.mockResolvedValueOnce({
neighbors: [
{
ieeeAddr: devices.bulb_color.ieeeAddr,
networkAddress: devices.bulb_color.networkAddress,
relationship: 2,
depth: 1,
linkquality: 120,
},
{ieeeAddr: devices.bulb.ieeeAddr, networkAddress: devices.bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92},
{
ieeeAddr: devices.external_converter_device.ieeeAddr,
networkAddress: devices.external_converter_device.networkAddress,
relationship: 2,
depth: 1,
linkquality: 92,
},
],
});
devices.coordinator.routingTable.mockResolvedValueOnce({
table: [{destinationAddress: devices.CC2530_ROUTER.networkAddress, status: "ACTIVE", nextHop: devices.bulb.networkAddress}],
});
devices.bulb.lqi.mockResolvedValueOnce({
neighbors: [
{
ieeeAddr: devices.bulb_color.ieeeAddr,
networkAddress: devices.bulb_color.networkAddress,
relationship: 1,
depth: 2,
linkquality: 110,
},
{
ieeeAddr: devices.CC2530_ROUTER.ieeeAddr,
networkAddress: devices.CC2530_ROUTER.networkAddress,
relationship: 1,
depth: 2,
linkquality: 100,
},
],
});
devices.CC2530_ROUTER.lqi.mockResolvedValueOnce({
neighbors: [
{ieeeAddr: "0x0000000000000000", networkAddress: devices.WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, linkquality: 130},
{
ieeeAddr: devices.bulb_color.ieeeAddr,
networkAddress: devices.bulb_color.networkAddress,
relationship: 4,
depth: 2,
linkquality: 130,
},
],
});
devices.coordinator.lqi.mockResolvedValueOnce([
{
eui64: devices.bulb_color.ieeeAddr as Eui64,
nwkAddress: devices.bulb_color.networkAddress,
relationship: 2,
depth: 1,
lqi: 120,
...LQI_BASE_RSP,
},
{eui64: devices.bulb.ieeeAddr as Eui64, nwkAddress: devices.bulb.networkAddress, relationship: 2, depth: 1, lqi: 92, ...LQI_BASE_RSP},
{
eui64: devices.external_converter_device.ieeeAddr as Eui64,
nwkAddress: devices.external_converter_device.networkAddress,
relationship: 2,
depth: 1,
lqi: 92,
...LQI_BASE_RSP,
},
]);
devices.coordinator.routingTable.mockResolvedValueOnce([
{
destinationAddress: devices.CC2530_ROUTER.networkAddress,
status: "ACTIVE",
nextHopAddress: devices.bulb.networkAddress,
...ROUTING_BASE_RSP,
},
]);
devices.bulb.lqi.mockResolvedValueOnce([
{
eui64: devices.bulb_color.ieeeAddr as Eui64,
nwkAddress: devices.bulb_color.networkAddress,
relationship: 1,
depth: 2,
lqi: 110,
...LQI_BASE_RSP,
},
{
eui64: devices.CC2530_ROUTER.ieeeAddr as Eui64,
nwkAddress: devices.CC2530_ROUTER.networkAddress,
relationship: 1,
depth: 2,
lqi: 100,
...LQI_BASE_RSP,
},
]);
devices.CC2530_ROUTER.lqi.mockResolvedValueOnce([
{eui64: "0x0000000000000000", nwkAddress: devices.WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, lqi: 130, ...LQI_BASE_RSP},
{
eui64: devices.bulb_color.ieeeAddr as Eui64,
nwkAddress: devices.bulb_color.networkAddress,
relationship: 4,
depth: 2,
lqi: 130,
...LQI_BASE_RSP,
},
]);
devices.unsupported_router.lqi.mockRejectedValueOnce("failed").mockRejectedValueOnce("failed");
devices.unsupported_router.routingTable.mockRejectedValueOnce("failed").mockRejectedValueOnce("failed");
};
@@ -143,6 +164,9 @@ describe("Extension: NetworkMap", () => {
value: {
links: [
{
deviceType: 0,
rxOnWhenIdle: 0,
permitJoining: 0,
depth: 1,
linkquality: 120,
lqi: 120,
@@ -155,11 +179,14 @@ describe("Extension: NetworkMap", () => {
targetIeeeAddr: "0x00124b00120144ae",
},
{
deviceType: 0,
rxOnWhenIdle: 0,
permitJoining: 0,
depth: 1,
linkquality: 92,
lqi: 92,
relationship: 2,
routes: [{destinationAddress: 6540, nextHop: 40369, status: "ACTIVE"}],
routes: [{destinationAddress: 6540, nextHopAddress: 40369, status: "ACTIVE", ...ROUTING_BASE_RSP}],
source: {ieeeAddr: "0x000b57fffec6a5b2", networkAddress: 40369},
sourceIeeeAddr: "0x000b57fffec6a5b2",
sourceNwkAddr: 40369,
@@ -167,6 +194,9 @@ describe("Extension: NetworkMap", () => {
targetIeeeAddr: "0x00124b00120144ae",
},
{
deviceType: 0,
rxOnWhenIdle: 0,
permitJoining: 0,
depth: 1,
linkquality: 92,
lqi: 92,
@@ -179,6 +209,9 @@ describe("Extension: NetworkMap", () => {
targetIeeeAddr: "0x00124b00120144ae",
},
{
deviceType: 0,
rxOnWhenIdle: 0,
permitJoining: 0,
depth: 2,
linkquality: 110,
lqi: 110,
@@ -191,6 +224,9 @@ describe("Extension: NetworkMap", () => {
targetIeeeAddr: "0x000b57fffec6a5b2",
},
{
deviceType: 0,
rxOnWhenIdle: 0,
permitJoining: 0,
depth: 2,
linkquality: 100,
lqi: 100,
@@ -203,6 +239,9 @@ describe("Extension: NetworkMap", () => {
targetIeeeAddr: "0x000b57fffec6a5b2",
},
{
deviceType: 0,
rxOnWhenIdle: 0,
permitJoining: 0,
depth: 2,
linkquality: 130,
lqi: 130,
@@ -247,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: [],
@@ -328,23 +367,34 @@ describe("Extension: NetworkMap", () => {
it("Output graphviz networkmap", async () => {
mock();
const device = devices.bulb_color;
device.lastSeen = undefined;
const endpoint = device.getEndpoint(1);
const data = {modelID: "test"};
const payload = {data, cluster: "genOnOff", device, endpoint, type: "readResponse", linkquality: 10};
await mockZHEvents.message(payload);
mockMQTTEvents.message("zigbee2mqtt/bridge/request/networkmap", stringify({type: "graphviz", routes: true}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bridge/response/networkmap");
const z2mDevice = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr);
const originalDef = z2mDevice.definition;
z2mDevice.definition = {
...originalDef,
description: `${originalDef.description} 6"`,
};
try {
const device = devices.bulb_color;
device.lastSeen = undefined;
const endpoint = device.getEndpoint(1);
const data = {modelID: "test"};
const payload = {data, cluster: "genOnOff", device, endpoint, type: "readResponse", linkquality: 10};
await mockZHEvents.message(payload);
mockMQTTEvents.message("zigbee2mqtt/bridge/request/networkmap", stringify({type: "graphviz", routes: true}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bridge/response/networkmap");
} finally {
z2mDevice.definition = originalDef;
}
const expected = `digraph G {
node[shape=record];
"0x00124b00120144ae" [style="bold, filled", fillcolor="#e04e5d", fontcolor="#ffffff", label="{Coordinator|0x00124b00120144ae (0x0000)|0 seconds ago}"];
"0x000b57fffec6a5b2" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb|0x000b57fffec6a5b2 (0x9db1)|IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)|9 seconds ago}"];
"0x000b57fffec6a5b2" -> "0x00124b00120144ae" [penwidth=2, weight=1, color="#009900", label="92 (routes: 0x198c)"]
"0x000b57fffec6a5b3" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb_color|0x000b57fffec6a5b3 (0x9dcf)|Philips Hue Go (7146060PH)|unknown}"];
"0x000b57fffec6a5b3" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb_color|0x000b57fffec6a5b3 (0x9dcf)|Philips Hue Go 6\\" (7146060PH)|unknown}"];
"0x000b57fffec6a5b3" -> "0x00124b00120144ae" [penwidth=0.5, weight=0, color="#994444", label="120"]
"0x000b57fffec6a5b3" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="110"]
"0x0017880104e45521" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{button_double_key|0x0017880104e45521 (0x198a)|Aqara Wireless remote switch (double rocker), 2016 model (WXKG02LM_rev1)|9 seconds ago}"];
@@ -492,6 +542,9 @@ describe("Extension: NetworkMap", () => {
value: {
links: [
{
deviceType: 0,
rxOnWhenIdle: 0,
permitJoining: 0,
depth: 1,
linkquality: 120,
lqi: 120,
@@ -504,11 +557,14 @@ describe("Extension: NetworkMap", () => {
targetIeeeAddr: "0x00124b00120144ae",
},
{
deviceType: 0,
rxOnWhenIdle: 0,
permitJoining: 0,
depth: 1,
linkquality: 92,
lqi: 92,
relationship: 2,
routes: [{destinationAddress: 6540, nextHop: 40369, status: "ACTIVE"}],
routes: [{destinationAddress: 6540, nextHopAddress: 40369, status: "ACTIVE", ...ROUTING_BASE_RSP}],
source: {ieeeAddr: "0x000b57fffec6a5b2", networkAddress: 40369},
sourceIeeeAddr: "0x000b57fffec6a5b2",
sourceNwkAddr: 40369,
@@ -516,6 +572,9 @@ describe("Extension: NetworkMap", () => {
targetIeeeAddr: "0x00124b00120144ae",
},
{
deviceType: 0,
rxOnWhenIdle: 0,
permitJoining: 0,
depth: 1,
linkquality: 92,
lqi: 92,
@@ -528,6 +587,9 @@ describe("Extension: NetworkMap", () => {
targetIeeeAddr: "0x00124b00120144ae",
},
{
deviceType: 0,
rxOnWhenIdle: 0,
permitJoining: 0,
depth: 2,
linkquality: 130,
lqi: 130,
@@ -554,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: [],
@@ -649,6 +711,9 @@ describe("Extension: NetworkMap", () => {
value: {
links: [
{
deviceType: 0,
rxOnWhenIdle: 0,
permitJoining: 0,
depth: 1,
linkquality: 120,
lqi: 120,
@@ -661,11 +726,14 @@ describe("Extension: NetworkMap", () => {
targetIeeeAddr: "0x00124b00120144ae",
},
{
deviceType: 0,
rxOnWhenIdle: 0,
permitJoining: 0,
depth: 1,
linkquality: 92,
lqi: 92,
relationship: 2,
routes: [{destinationAddress: 6540, nextHop: 40369, status: "ACTIVE"}],
routes: [{destinationAddress: 6540, nextHopAddress: 40369, status: "ACTIVE", ...ROUTING_BASE_RSP}],
source: {ieeeAddr: "0x000b57fffec6a5b2", networkAddress: 40369},
sourceIeeeAddr: "0x000b57fffec6a5b2",
sourceNwkAddr: 40369,
@@ -673,6 +741,9 @@ describe("Extension: NetworkMap", () => {
targetIeeeAddr: "0x00124b00120144ae",
},
{
deviceType: 0,
rxOnWhenIdle: 0,
permitJoining: 0,
depth: 1,
linkquality: 92,
lqi: 92,
@@ -685,6 +756,9 @@ describe("Extension: NetworkMap", () => {
targetIeeeAddr: "0x00124b00120144ae",
},
{
deviceType: 0,
rxOnWhenIdle: 0,
permitJoining: 0,
depth: 2,
linkquality: 130,
lqi: 130,
@@ -711,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: [],
+3 -12
View File
@@ -1,5 +1,5 @@
// biome-ignore assist/source/organizeImports: import mocks first
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi, assert} from "vitest";
import * as data from "../mocks/data";
import {mockLogger} from "../mocks/logger";
import {mockMQTTPublishAsync} from "../mocks/mqtt";
@@ -28,11 +28,6 @@ describe("Extension: OnEvent", () => {
return controller.zigbee.resolveEntity(zhDevice)! as Device;
};
const clearOnEventSpies = (): void => {
onEventSpy.mockClear();
deviceOnEventSpy.mockClear();
};
beforeAll(async () => {
vi.useFakeTimers();
data.writeDefaultConfiguration();
@@ -52,7 +47,8 @@ describe("Extension: OnEvent", () => {
}
await controller.removeExtension(controller.getExtension("OnEvent")!);
clearOnEventSpies();
onEventSpy.mockClear();
deviceOnEventSpy.mockClear();
await controller.addExtension(new OnEvent(...controller.extensionArgs));
});
@@ -88,7 +84,6 @@ describe("Extension: OnEvent", () => {
});
it("calls on device events", async () => {
clearOnEventSpies();
await mockZHEvents.deviceAnnounce({device: devices.LIVOLO});
await flushPromises();
@@ -177,14 +172,10 @@ describe("Extension: OnEvent", () => {
it("does not block startup on failure", async () => {
await controller.removeExtension(controller.getExtension("OnEvent")!);
clearOnEventSpies();
deviceOnEventSpy.mockImplementationOnce(async () => {
await new Promise((resolve) => setTimeout(resolve, 10000));
throw new Error("Failed");
});
await controller.addExtension(new OnEvent(...controller.extensionArgs));
expect(onEventSpy).toHaveBeenCalledTimes(2);
expect(deviceOnEventSpy).toHaveBeenCalledTimes(1);
});
});
File diff suppressed because it is too large Load Diff
+412 -72
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";
@@ -73,7 +73,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({brightness: "200"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 200, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({brightness: 200, state: "ON"});
@@ -119,7 +124,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({brightness_percent: "92"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 235, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 235, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: "ON", brightness: 235});
@@ -132,7 +142,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({brightness: 230}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 230, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 230, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: "ON", brightness: 230});
@@ -145,7 +160,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({color_temp: "222"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 222, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColorTemp",
{colortemp: 222, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color_mode: "color_temp", color_temp: 222});
@@ -158,7 +178,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({color_temp_percent: 100}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 500, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColorTemp",
{colortemp: 500, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color_mode: "color_temp", color_temp: 500});
@@ -223,7 +248,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/0x0017882194e45543/set", stringify({state_l2: "ON", brightness_l2: 50}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/QS-Zigbee-D02-TRIAC-2C-LN", stringify({brightness_l2: 50, state_l2: "ON"}), {
retain: false,
qos: 0,
@@ -284,7 +314,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({color: {x: 100, y: 50}}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColor",
{colorx: 6553500, colory: 3276750, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color_mode: "xy", color: {x: 100, y: 50}});
@@ -298,7 +333,12 @@ describe("Extension: Publish", () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {});
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColor",
{colorx: 6553500, colory: 3276750, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb_color",
@@ -313,8 +353,18 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({color: {x: 100, y: 50}, brightness: 20}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColor",
{colorx: 6553500, colory: 3276750, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb_color",
@@ -329,8 +379,18 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({color: {x: 100, y: 50}, brightness: 20, state: "ON"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColor",
{colorx: 6553500, colory: 3276750, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb_color",
@@ -345,7 +405,13 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({color: {x: 100, y: 50}, brightness: 20, state: "OFF"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command).toHaveBeenNthCalledWith(1, "lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {});
expect(endpoint.command).toHaveBeenNthCalledWith(
1,
"lightingColorCtrl",
"moveToColor",
{colorx: 6553500, colory: 3276750, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(endpoint.command).toHaveBeenNthCalledWith(2, "genOnOff", "off", {}, {});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
@@ -361,7 +427,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({color: {r: 100, g: 200, b: 10}}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17806, colory: 43155, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColor",
{colorx: 17806, colory: 43155, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: "xy"});
@@ -373,7 +444,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({color: {rgb: "100,200,10"}}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17806, colory: 43155, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColor",
{colorx: 17806, colory: 43155, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: "xy"});
@@ -385,7 +461,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON", brightness: "50"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: "ON", brightness: 50});
@@ -410,7 +491,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({brightness_percent: 50}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 128, transtime: 0}, {});
expect(group.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 128, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/group_1");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({state: "ON", brightness: 128});
@@ -449,7 +535,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON", brightness: 50}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {});
expect(group.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/group_1");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({state: "ON", brightness: 50});
@@ -475,7 +566,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({color: {x: 0.37, y: 0.28}}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 24248, colory: 18350, transtime: 0}, {});
expect(group.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColor",
{colorx: 24248, colory: 18350, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/group_1");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({color: {x: 0.37, y: 0.28}, color_mode: "xy"});
@@ -488,7 +584,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({color_temp: 100}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 100, transtime: 0}, {});
expect(group.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColorTemp",
{colortemp: 100, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/group_1");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({color_temp: 100, color_mode: "color_temp"});
@@ -515,7 +616,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({brightness: "200"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 200, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
});
@@ -526,7 +632,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({brightness: "200"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 200, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: "ON"});
});
@@ -681,7 +792,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set/color", "#64C80A");
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17806, colory: 43155, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"lightingColorCtrl",
"moveToColor",
{colorx: 17806, colory: 43155, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: "xy"});
@@ -798,7 +914,12 @@ describe("Extension: Publish", () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColor", {colorx: 45940, colory: 19595, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual([
"lightingColorCtrl",
"moveToColor",
{colorx: 45940, colory: 19595, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should also use on/off cluster when controlling group with switch", async () => {
@@ -809,7 +930,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/switch_group/set", stringify({state: "ON", brightness: 100}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(2);
expect(group.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 100, transtime: 0}, {});
expect(group.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 100, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(group.command).toHaveBeenCalledWith("genOnOff", "on", {}, {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/switch_group", stringify({state: "ON", brightness: 100}), {
retain: false,
@@ -835,7 +961,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({brightness: 100}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 100, transtime: 200}, {});
expect(group.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 100, transtime: 200, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // zigbee2mqtt/bulb_color + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/group_1");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({state: "ON", brightness: 100});
@@ -850,7 +981,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 200}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 200, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should use transition from device_options on brightness command", async () => {
@@ -861,7 +997,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 200}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 200, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should turn bulb on with correct brightness when device is turned off twice and brightness is reported", async () => {
@@ -909,10 +1050,34 @@ describe("Extension: Publish", () => {
});
expect(endpoint.command).toHaveBeenCalledTimes(4);
expect(endpoint.command).toHaveBeenNthCalledWith(1, "genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 0}, {});
expect(endpoint.command).toHaveBeenNthCalledWith(2, "genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 0}, {});
expect(endpoint.command).toHaveBeenNthCalledWith(3, "genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 0}, {});
expect(endpoint.command).toHaveBeenNthCalledWith(4, "genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 0}, {});
expect(endpoint.command).toHaveBeenNthCalledWith(
1,
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 200, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(endpoint.command).toHaveBeenNthCalledWith(
2,
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 0, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(endpoint.command).toHaveBeenNthCalledWith(
3,
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 0, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(endpoint.command).toHaveBeenNthCalledWith(
4,
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 200, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
});
it("Should turn bulb on with full brightness when transition is used and no brightness is known", async () => {
@@ -929,8 +1094,18 @@ describe("Extension: Publish", () => {
qos: 0,
});
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 5}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 254, transtime: 5}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 0, transtime: 5, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[1]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 254, transtime: 5, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Transition parameter should not influence brightness on state ON", async () => {
@@ -944,9 +1119,19 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON", transition: 1}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(3);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[1]).toEqual(["genOnOff", "on", {}, {}]);
expect(endpoint.command.mock.calls[2]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 10}, {}]);
expect(endpoint.command.mock.calls[2]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 10, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should use transition when color temp", async () => {
@@ -957,7 +1142,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 200, transtime: 200}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"lightingColorCtrl",
"moveToColorTemp",
{colortemp: 200, transtime: 200, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should use transition only once when setting brightness and color temperature for TRADFRI", async () => {
@@ -967,8 +1157,18 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 200, transtime: 200}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[1]).toEqual([
"lightingColorCtrl",
"moveToColorTemp",
{colortemp: 200, transtime: 200, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should use transition only once when setting brightness and color temperature for group which contains TRADFRI", async () => {
@@ -976,8 +1176,18 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/group_with_tradfri/set", stringify({state: "ON", transition: 60, brightness: 20, color_temp: 400}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(2);
expect(group.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {}]);
expect(group.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 400, transtime: 600}, {}]);
expect(group.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(group.command.mock.calls[1]).toEqual([
"lightingColorCtrl",
"moveToColorTemp",
{colortemp: 400, transtime: 600, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Message transition should overrule options transition", async () => {
@@ -988,7 +1198,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 100}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 200, transtime: 100, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should set state with brightness before color", async () => {
@@ -998,8 +1213,18 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 100, transtime: 30}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColor", {colorx: 45940, colory: 19595, transtime: 30}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 100, transtime: 30, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[1]).toEqual([
"lightingColorCtrl",
"moveToColor",
{colorx: 45940, colory: 19595, transtime: 30, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should turn device off when brightness 0 is send", async () => {
@@ -1012,9 +1237,24 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(3);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[2]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[1]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 0, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[2]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync.mock.calls[0]).toEqual([
"zigbee2mqtt/bulb_color",
@@ -1043,9 +1283,24 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(3);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 30}, {}]);
expect(endpoint.command.mock.calls[2]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[1]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 0, transtime: 30, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[2]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 50, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync.mock.calls[0]).toEqual([
"zigbee2mqtt/bulb_color",
@@ -1074,7 +1329,7 @@ describe("Extension: Publish", () => {
expect(endpoint.command.mock.calls[0]).toEqual([
"lightingColorCtrl",
"enhancedMoveToHueAndSaturation",
{enhancehue: 45510, saturation: 127, transtime: 0},
{enhancehue: 45510, saturation: 127, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
@@ -1129,7 +1384,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 254, transtime: 30}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 254, transtime: 30, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should turn device on with on/off with transition when transition 0 is provided", async () => {
@@ -1139,7 +1399,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 254, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 254, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Should turn device off with onOff on off with transition", async () => {
@@ -1149,7 +1414,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 10}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 0, transtime: 10, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: "OFF"});
@@ -1168,7 +1438,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF", transition: 3}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 30}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 0, transtime: 30, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0]).toEqual([
"zigbee2mqtt/bulb_color",
@@ -1197,7 +1472,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON", transition: 3}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 30}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 200, transtime: 30, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync.mock.calls[2]).toEqual([
"zigbee2mqtt/bulb_color",
@@ -1219,7 +1499,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF", transition: 3}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 30}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 0, transtime: 30, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0]).toEqual([
"zigbee2mqtt/bulb_color",
@@ -1231,7 +1516,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 200, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync.mock.calls[1]).toEqual([
"zigbee2mqtt/bulb_color",
@@ -1264,7 +1554,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 100, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"lightingColorCtrl",
"moveToColorTemp",
{colortemp: 100, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: "ON", color_temp: 100, color_mode: "color_temp"});
@@ -1281,7 +1576,12 @@ describe("Extension: Publish", () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 100, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual([
"lightingColorCtrl",
"moveToColorTemp",
{colortemp: 100, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb_color",
@@ -1300,7 +1600,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "moveToColor", {colorx: 26869, colory: 16384, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual([
"lightingColorCtrl",
"moveToColor",
{colorx: 26869, colory: 16384, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bulb_color");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color: {x: 0.41, y: 0.25}, state: "ON", color_mode: "xy"});
@@ -1363,7 +1668,7 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({brightness_move: -40}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "move", {movemode: 1, rate: 40}, {});
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "move", {movemode: 1, rate: 40, optionsMask: 0, optionsOverride: 0}, {});
});
it("Should publish brightness_move down to zigbee devices", async () => {
@@ -1371,7 +1676,7 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({brightness_move: 30}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "move", {movemode: 0, rate: 30}, {});
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "move", {movemode: 0, rate: 30, optionsMask: 0, optionsOverride: 0}, {});
});
it("HS2WD-E burglar warning", async () => {
@@ -1442,7 +1747,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/roller_shutter/set", stringify({state: "OPEN"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 255, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 255, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/roller_shutter");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({position: 100});
@@ -1476,7 +1786,12 @@ describe("Extension: Publish", () => {
await vi.runOnlyPendingTimersAsync();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/GL-S-007ZS", stringify({state: "ON", brightness: 20}), {
qos: 0,
retain: false,
@@ -1509,8 +1824,18 @@ describe("Extension: Publish", () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 10}, {});
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
);
expect(endpoint.command).toHaveBeenCalledWith(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 20, transtime: 10, optionsMask: 0, optionsOverride: 0},
{},
);
});
it("Should restore brightness when its turned off without transition and is turned on with", async () => {
@@ -1527,7 +1852,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON", brightness: 123}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 123, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 123, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
await flushPromises();
@@ -1537,7 +1867,12 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON", transition: 1.0}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(4);
expect(endpoint.command.mock.calls[3]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 123, transtime: 10}, {}]);
expect(endpoint.command.mock.calls[3]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 123, transtime: 10, optionsMask: 0, optionsOverride: 0},
{},
]);
});
it("Shouldnt use moveToLevelWithOnOff on turn on when no transition has been used as some devices do not turn on in that case", async () => {
@@ -1556,7 +1891,12 @@ describe("Extension: Publish", () => {
expect(endpoint.command).toHaveBeenCalledTimes(4);
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 150, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual([
"genLevelCtrl",
"moveToLevelWithOnOff",
{level: 150, transtime: 0, optionsMask: 0, optionsOverride: 0},
{},
]);
expect(endpoint.command.mock.calls[2]).toEqual(["genOnOff", "off", {}, {}]);
expect(endpoint.command.mock.calls[3]).toEqual(["genOnOff", "on", {}, {}]);
@@ -1601,7 +1941,7 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/group_tradfri_remote/set", stringify({scene_recall: 1}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith("genScenes", "recall", {groupid: 15071, sceneid: 1}, {});
expect(group.command).toHaveBeenCalledWith("genScenes", "recall", {groupid: 15071, sceneid: 1, transitionTime: 0xffff}, {});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(8);
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(
1,
+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";
+29 -4
View File
@@ -5,13 +5,14 @@ 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";
const rmSync = (target: string): void => fs.rmSync(target, {recursive: true, force: true});
describe("Logger", () => {
let consoleWriteSpy: MockInstance;
const dir = tmp.dirSync();
@@ -43,11 +44,12 @@ describe("Logger", () => {
it("Create log directory", () => {
const dirs = fs.readdirSync(dir.name);
expect(dirs.length).toBe(1);
expect(dirs[0]).toMatch(/^[0-9]{4}-[0-9]{2}-[0-9]{2}\.[0-9]{2}-[0-9]{2}-[0-9]{2}$/);
});
it("Should cleanup (default setting)", () => {
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++) {
@@ -59,9 +61,32 @@ describe("Logger", () => {
expect(fs.readdirSync(dir.name).length).toBe(10);
});
it("Should handle cleanup error", () => {
for (const d of fs.readdirSync(dir.name)) {
rmSync(path.join(dir.name, d));
}
for (let i = 0; i < 20; i++) {
fs.mkdirSync(path.join(dir.name, `log_${i}`));
}
const rmSyncSpy = vi.spyOn(fs, "rmSync").mockImplementationOnce(() => {
throw new Error("EACCES: permission denied");
});
const errorSpy = vi.spyOn(logger, "error");
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++) {
@@ -76,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++) {
+11 -5
View File
@@ -1,9 +1,8 @@
import fs from "node:fs";
import path from "node:path";
import stringify from "json-stable-stringify-without-jsonify";
import tmp from "tmp";
import {vi} from "vitest";
import {stringify} from "../../lib/util/stringify";
import yaml from "../../lib/util/yaml";
export const mockDir: string = tmp.dirSync().name;
@@ -26,6 +25,9 @@ export const DEFAULT_CONFIGURATION = {
"0x18fc2600000d7ae2": {
friendly_name: "bosch_radiator",
},
"0x18fc2600000d7ae3": {
friendly_name: "bosch_rm230z",
},
"0x000b57fffec6a5b2": {
retain: true,
friendly_name: "bulb",
@@ -275,6 +277,10 @@ export function stateExists(): boolean {
return fs.existsSync(stateFile);
}
export function readState(): Record<string, unknown> {
return JSON.parse(fs.readFileSync(stateFile, "utf8"));
}
const defaultState = {
"0x000b57fffec6a5b2": {
state: "ON",
@@ -294,8 +300,8 @@ export function getDefaultState(): typeof defaultState {
return defaultState;
}
export function writeDefaultState(): void {
fs.writeFileSync(path.join(mockDir, "state.json"), stringify(defaultState));
export function writeDefaultState(data: Record<string, unknown> = defaultState): void {
fs.writeFileSync(stateFile, stringify(data));
}
export function writeEmptyDatabase(): void {
+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,
};
}),
}));
+1 -1
View File
@@ -1,5 +1,5 @@
import {vi} from "vitest";
import type Transport from "winston-transport";
import type {LogLevel} from "../../lib/util/settings";
let level: LogLevel = "info";
+1 -1
View File
@@ -1,5 +1,5 @@
import type {IClientPublishOptions} from "mqtt";
import {vi} from "vitest";
import type {EventHandler} from "./utils";
export const events: Record<string, EventHandler> = {};
+1
View File
@@ -1,3 +1,4 @@
import {vi} from "vitest";
import utils from "../../lib/util/utils";
const spy = vi.spyOn(utils, "sleep");
-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;
+1
View File
@@ -1,3 +1,4 @@
import {vi} from "vitest";
import type {DefinitionWithExtend} from "zigbee-herdsman-converters";
export type EventHandler = (...args: unknown[]) => unknown;
+75 -11
View File
@@ -1,9 +1,11 @@
import assert from "node:assert";
import type {Mock} from "vitest";
import {type Mock, vi} from "vitest";
import type {AdapterTypes} from "zigbee-herdsman";
import {Zcl} from "zigbee-herdsman";
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
import type {OtaDataSettings, OtaExtraMetas, OtaSource, OtaUpdateAvailableResult} from "zigbee-herdsman/dist/controller/tstype";
import type {TClusterCommandPayload} from "zigbee-herdsman/dist/zspec/zcl/definition/clusters-types";
import type {BindingTableEntry, LQITableEntry, RoutingTableEntry} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes";
import {DEFAULT_BIND_GROUP_ID} from "../../lib/util/utils";
import type {EventHandler} from "./utils";
@@ -104,6 +106,7 @@ export class Endpoint {
unbind: Mock;
save: Mock;
configureReporting: Mock;
readReportingConfig: Mock;
meta: Record<string, unknown>;
binds: ZHBind[];
profileID: number | undefined;
@@ -138,6 +141,7 @@ export class Endpoint {
this.unbind = vi.fn();
this.save = vi.fn();
this.configureReporting = vi.fn();
this.readReportingConfig = vi.fn();
this.meta = meta;
this.binds = binds;
this.profileID = profileID;
@@ -218,6 +222,7 @@ export class Endpoint {
this.unbind.mockClear();
this.save.mockClear();
this.configureReporting.mockClear();
this.readReportingConfig.mockClear();
this.addToGroup.mockClear();
this.removeFromGroup.mockClear();
this.getClusterAttributeValue.mockClear();
@@ -247,8 +252,39 @@ export class Device {
lastSeen: number | undefined;
isDeleted: boolean;
linkquality?: number;
lqi: Mock;
routingTable: Mock;
lqi: Mock<() => Promise<LQITableEntry[]>>;
routingTable: Mock<() => Promise<RoutingTableEntry[]>>;
bindingTable: Mock<() => Promise<BindingTableEntry[]>>;
clearAllBindings: Mock<() => Promise<void>>;
checkOta =
vi.fn<
(
source: OtaSource,
current: TClusterCommandPayload<"genOta", "queryNextImageRequest"> | undefined,
extraMetas: OtaExtraMetas,
endpoint: Endpoint,
) => Promise<OtaUpdateAvailableResult>
>();
updateOta =
vi.fn<
(
source: Readonly<OtaSource> | undefined,
requestPayload: TClusterCommandPayload<"genOta", "queryNextImageRequest"> | undefined,
requestTsn: number | undefined,
extraMetas: Readonly<OtaExtraMetas>,
onProgress: (progress: number, remaining: number) => void,
dataSettings: OtaDataSettings,
endpoint: Endpoint,
) => Promise<[from: OtaUpdateAvailableResult["current"], to: OtaUpdateAvailableResult["current"] | undefined]>
>();
scheduleOta = vi.fn((source) => {
this.scheduledOta = source;
});
unscheduleOta = vi.fn(() => {
this.scheduledOta = undefined;
});
abortOta = vi.fn();
scheduledOta: OtaSource | undefined = undefined;
constructor(
type: string,
@@ -285,8 +321,10 @@ export class Device {
this.manufacturerName = manufacturerName;
this.lastSeen = 1000;
this.isDeleted = false;
this.lqi = vi.fn(() => ({neighbors: []}));
this.routingTable = vi.fn(() => ({table: []}));
this.lqi = vi.fn(() => Promise.resolve([] as LQITableEntry[]));
this.routingTable = vi.fn(() => Promise.resolve([] as RoutingTableEntry[]));
this.bindingTable = vi.fn(() => Promise.resolve([] as BindingTableEntry[]));
this.clearAllBindings = vi.fn(() => {});
}
getEndpoint(ID: number): Endpoint | undefined {
@@ -302,7 +340,15 @@ export class Device {
this.save.mockClear();
this.lqi.mockClear();
this.routingTable.mockClear();
this.bindingTable.mockClear();
this.clearAllBindings.mockClear();
this.checkOta.mockClear();
this.updateOta.mockClear();
this.scheduleOta.mockClear();
this.unscheduleOta.mockClear();
this.abortOta.mockClear();
this.meta = {};
this.scheduledOta = undefined;
for (const ep of this.endpoints) {
ep.mockClear();
@@ -1128,18 +1174,33 @@ export const devices = {
"DC Source",
"FanBee1",
),
BTH_RM230Z: new Device(
"Router",
"0x18fc2600000d7ae3",
35902,
4617,
[new Endpoint(1, [0, 3, 513, 516, 1029, 2821], [10, 25], "0x18fc2600000d7ae3")],
InterviewState.Successful,
"Mains (single phase)",
"RBSH-RTH0-ZB-EU",
undefined,
undefined,
undefined,
CUSTOM_CLUSTER_BTHRA,
),
};
export const mockController = {
on: (type: string, handler: EventHandler): void => {
events[type] = handler;
},
start: vi.fn((): Promise<AdapterTypes.StartResult> => Promise.resolve("reset")),
start: vi.fn(async (abortSignal?: AbortSignal): Promise<AdapterTypes.StartResult> => {
abortSignal?.throwIfAborted();
return await Promise.resolve("reset");
}),
stop: vi.fn(),
touchlinkIdentify: vi.fn(),
touchlinkScan: vi.fn(),
touchlinkFactoryReset: vi.fn(),
touchlinkFactoryResetFirst: vi.fn(),
touchlink: {identify: vi.fn(), scan: vi.fn(), factoryReset: vi.fn(), factoryResetFirst: vi.fn()},
addInstallCode: vi.fn(),
permitJoin: vi.fn(),
getPermitJoin: vi.fn((): boolean => false),
@@ -1195,6 +1256,9 @@ export const mockController = {
groups[`group_${groupID}` as keyof typeof groups] = group;
return group;
}),
sendRaw: vi.fn(async (/*rawPayload: RawPayload, customClusters: CustomClusters = {}*/) =>
Promise.resolve([0x00, {nwkAddress: 0x1234, eui64: "", startIndex: 0, assocDevList: []}]),
),
};
vi.mock("zigbee-herdsman", async (importOriginal) => ({
+637 -149
View File
File diff suppressed because it is too large Load Diff
+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)) {
+78 -2
View File
@@ -1,12 +1,13 @@
// biome-ignore assist/source/organizeImports: import mocks first
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it} from "vitest";
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import * as data from "./mocks/data";
import {existsSync, readFileSync, rmSync} from "node:fs";
import {existsSync, readFileSync, rmSync, writeFileSync} from "node:fs";
import objectAssignDeep from "object-assign-deep";
import mockedData from "../lib/util/data";
import * as settings from "../lib/util/settings";
import * as settingsMigration from "../lib/util/settingsMigration";
import path from "node:path";
describe("Settings Migration", () => {
beforeAll(() => {});
@@ -916,4 +917,79 @@ describe("Settings Migration", () => {
expect(migrationNotesContent).toContain("[SPECIAL] Device icons are now saved as images.");
});
});
describe("Migrates v4 to v5", () => {
const BASE_CONFIG = {
version: 4,
mqtt: {
server: "mqtt://localhost",
},
};
const DEFAULT_STATE: Record<string, unknown> = {
"0x000b57fffec6a5b2": {
state: "ON",
brightness: 50,
color_temp: 370,
linkquality: 99,
},
"0x0017880104e45517": {
brightness: 255,
update: {state: "idle"},
},
1: {
state: "ON",
update: {state: "available", installed_version: 1, latest_version: 2},
},
};
beforeEach(() => {
settings.testing.CURRENT_VERSION = 5; // stop update after this version
data.writeDefaultConfiguration(BASE_CONFIG);
data.writeDefaultState(DEFAULT_STATE);
settings.reRead();
});
it("Update", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
afterSettings.version = 5;
expect(settings.getPersistedSettings()).toStrictEqual(beforeSettings);
expect(data.readState()).toStrictEqual(DEFAULT_STATE);
settingsMigration.migrateIfNecessary();
const migratedSettings = settings.getPersistedSettings();
expect(migratedSettings).toStrictEqual(afterSettings);
// @ts-expect-error workaround
const migratedState = objectAssignDeep.noMutate({}, DEFAULT_STATE);
delete (migratedState["0x0017880104e45517"] as Record<string, unknown>).update;
delete (migratedState[1] as Record<string, unknown>).update;
expect(data.readState()).toStrictEqual(migratedState);
});
it("handles errors gracefully", () => {
const consoleErrorSpy = vi.spyOn(console, "error");
writeFileSync(path.join(data.mockDir, "state.json"), "notjson", "utf8");
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
afterSettings.version = 5;
expect(settings.getPersistedSettings()).toStrictEqual(beforeSettings);
settingsMigration.migrateIfNecessary();
const migratedSettings = settings.getPersistedSettings();
expect(migratedSettings).toStrictEqual(afterSettings);
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to write state"));
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("is not valid JSON"));
});
});
});
+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");
});
});

Some files were not shown because too many files have changed in this diff Show More