Compare commits

..
167 Commits
Author SHA1 Message Date
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
Koen Kanters 4d247f3ae1 chore(dev): release 2.6.2 (#28441) 2025-10-01 19:47:06 +02:00
dependabot[bot] c9d01f4118 fix(ignore): bump the minor-patch group with 4 updates (#28778) 2025-10-01 19:36:13 +02:00
Koen Kanters 1283859a12 fix(ignore): update zigbee-herdsman-converters to 25.35.1 (#28774) 2025-09-30 19:36:38 +00:00
Koen Kanters a0d91cc063 fix(ignore): update zigbee-herdsman to 6.1.5 (#28773) 2025-09-30 18:53:15 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 116634193a fix(ignore): bump zigbee2mqtt-windfront from 2.1.0 to 2.2.0 in the minor-patch group (#28764)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 21:44:25 +02:00
Koen Kanters f6c0ce735f fix(ignore): update zigbee-herdsman-converters to 25.34.0 (#28763) 2025-09-29 19:30:13 +00:00
Koen Kanters 508318de57 fix(ignore): update zigbee-herdsman to 6.1.4 (#28762) 2025-09-29 19:06:13 +00:00
Koen Kanters 26c74b5778 fix(ignore): update zigbee-herdsman-converters to 25.33.0 (#28758) 2025-09-28 19:41:34 +00:00
Koen Kanters 87c474d2f5 fix(ignore): update zigbee-herdsman-converters to 25.32.0 (#28746) 2025-09-26 19:02:13 +00:00
Koen Kantersandnurikk 2c70deb411 fix(ignore): update zigbee2mqtt-frontend to 0.9.21 (#28745)
Co-authored-by: nurikk <1525421+nurikk@users.noreply.github.com>
2025-09-26 18:45:24 +00:00
Koen Kanters a310627e19 fix(ignore): update zigbee-herdsman-converters to 25.31.0 (#28726) 2025-09-24 19:07:57 +00:00
Koen Kanters 77926bc1c3 fix(ignore): update zigbee-herdsman-converters to 25.30.0 (#28707) 2025-09-23 19:53:50 +00:00
Koen Kanters 892f92448e fix(ignore): update zigbee-herdsman to 6.1.3 (#28717) 2025-09-23 19:44:54 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e8847d1a34 fix(ignore): bump @types/node from 24.4.0 to 24.5.2 in the minor-patch group (#28700)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-22 21:53:43 +02:00
Koen Kanters 79c0c0933a fix(ignore): update zigbee-herdsman to 6.1.2 (#28689) 2025-09-21 13:25:20 +00:00
Jori HuismanandNerivec a5e1e8c0c3 fix: Handle empty or null Home Assistant name in device settings (#28364)
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2025-09-21 14:54:18 +02:00
Koen Kanters 382d37e839 fix(ignore): update zigbee-herdsman-converters to 25.29.0 (#28654) 2025-09-18 18:47:04 +00:00
dependabot[bot] c3e1525f60 fix(ignore): bump the minor-patch group with 3 updates (#28638) 2025-09-15 22:29:20 +02:00
Koen Kanters 2cb7dcc12a fix(ignore): update zigbee-herdsman-converters to 25.26.0 (#28629) 2025-09-14 18:30:00 +00:00
Koen Kanters b2f74607cc fix(ignore): update zigbee-herdsman-converters to 25.25.0 (#28621) 2025-09-13 19:56:35 +00:00
Koen Kanters 2edf85f6e1 fix(ignore): update zigbee-herdsman-converters to 25.24.0 (#28608) 2025-09-12 19:12:00 +00:00
Koen Kanters 09f33b31a9 fix: Fix DatabaseEntry with ID 'X' does not exist when adding device to delete group ID (#28583) 2025-09-11 20:53:58 +02:00
Koen Kanters 3f258342d8 fix(ignore): update zigbee-herdsman-converters to 25.23.0 (#28598) 2025-09-11 18:41:41 +00:00
Koen Kanters c28ca79d8a fix(ignore): update zigbee-herdsman to 6.1.1 (#28597) 2025-09-11 18:23:33 +00:00
Koen Kanters 5f6fae0df7 fix(ignore): update zigbee-herdsman-converters to 25.22.0 (#28580) 2025-09-10 18:58:53 +00:00
Koen Kanters 15e00ea8a4 fix: HomeAssistant: add default_entity_id to discovery message (#28560) 2025-09-09 21:09:42 +02:00
Koen Kanters 01d5863487 fix(ignore): update zigbee-herdsman-converters to 25.21.0 (#28561) 2025-09-09 18:57:23 +00:00
Koen Kanters fefae9957c fix: Make windfront the default frontend (#28546) 2025-09-09 20:05:41 +02:00
Koen Kanters a297b9255b fix(ignore): update zigbee-herdsman-converters to 25.20.0 (#28545) 2025-09-08 19:03:46 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 86b458111b fix(ignore): bump the minor-patch group with 3 updates (#28541)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 20:43:58 +02:00
dependabot[bot] f022f55be6 chore(deps): bump actions/stale from 9 to 10 (#28539) 2025-09-08 20:20:46 +02:00
dependabot[bot] dc991be28f chore(deps): bump actions/github-script from 7 to 8 (#28540) 2025-09-08 20:20:30 +02:00
dependabot[bot] 62b62d224f chore(deps): bump actions/setup-node from 4 to 5 (#28538) 2025-09-08 20:08:57 +02:00
Koen Kanters 5f2630c93b fix(ignore): update zigbee-herdsman-converters to 25.19.0 (#28533) 2025-09-07 20:19:09 +00:00
Koen Kanters e8e4977327 fix(ignore): update zigbee-herdsman-converters to 25.18.0 (#28518) 2025-09-06 18:45:43 +00:00
Koen Kanters 54d729e37a fix(ignore): update zigbee-herdsman-converters to 25.17.1 (#28511) 2025-09-05 18:33:51 +00:00
Koen Kanters 3018e40dae fix(ignore): update zigbee-herdsman-converters to 25.17.0 (#28480) 2025-09-04 18:52:23 +00:00
Koen Kanters 1df1f5d388 chore: merge master to dev 2025-09-03 19:58:04 +02:00
Koen Kanters d44463a330 chore: fix copilot-instructions 2025-09-03 19:57:41 +02:00
Nerivec e52203a1b8 fix(ignore): update zigbee2mqtt-windfront to 2.1.0 (#28471) 2025-09-03 10:55:48 +00:00
Koen Kanters 073a261738 chore: merge master to dev 2025-09-02 21:00:50 +02:00
Koen Kanters 64ebb98ad1 chore: add copilot instructions 2025-09-02 21:00:30 +02:00
Koen Kanters 3f27f79d7d fix(ignore): update zigbee-herdsman-converters to 25.15.0 (#28462) 2025-09-02 18:57:50 +00:00
Koen Kanters 06862c7259 fix(ignore): update zigbee-herdsman to 6.1.0 (#28460) 2025-09-02 18:00:00 +00:00
github-actions[bot] 0482d8b4e9 chore: promote to dev 2025-09-01 19:14:09 +00:00
Koen Kanters e9889e5d20 chore(dev): release 2.6.1 (#28144) 2025-09-01 20:57:13 +02:00
Koen Kanters 77886530fe fix(ignore): update zigbee-herdsman to 6.0.4 (#28438) 2025-09-01 18:52:39 +00:00
Koen Kanters bf22ddac46 fix(ignore): update zigbee-herdsman-converters to 25.14.0 (#28439) 2025-09-01 18:18:38 +00:00
Koen Kanters 203019aa26 fix(ignore): update zigbee-herdsman-converters to 25.13.0 (#28427) 2025-08-31 13:14:19 +00:00
Nerivec 08903f68d9 fix(ignore): update zigbee2mqtt-windfront to 2.0.1 (#28420) 2025-08-30 18:57:33 +00:00
Koen Kanters bdeab61001 fix(ignore): update zigbee-herdsman-converters to 25.12.0 (#28417) 2025-08-30 12:03:46 +00:00
Koen Kanters c07fce936f fix(ignore): update zigbee-herdsman-converters to 25.11.0 (#28407) 2025-08-28 18:31:42 +00:00
Koen Kanters 264440ebcc fix(ignore): update zigbee-herdsman to 6.0.2 (#28406) 2025-08-28 18:19:11 +00:00
Koen Kanters 32d82651e2 fix(ignore): update zigbee-herdsman-converters to 25.10.0 (#28399) 2025-08-27 18:29:45 +00:00
NerivecandKoen Kanters 9079c16d16 chore: switch benchmarking tool (#28396)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2025-08-27 20:12:30 +02:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Nerivec
a8fd23b6cc fix(ignore): bump the minor-patch group with 2 updates (#28383)
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-08-27 19:58:35 +02:00
Nerivec 2df363f3af fix: update zigbee2mqtt-windfront to 2.0.0 (#28389) 2025-08-26 15:47:44 +00:00
Koen Kanters 0147441c93 fix(ignore): update zigbee-herdsman-converters to 25.9.0 (#28380) 2025-08-25 20:49:30 +02:00
Koen Kanters 4d52fdd290 chore: Update changelog generation to include zigbee2mqtt-windfront (#28345) 2025-08-25 20:42:08 +02:00
Koen Kanters 8e6ba7975b fix(ignore): update zigbee-herdsman-converters to 25.8.0 (#28373) 2025-08-24 19:23:32 +00:00
Koen Kanters 7bd7bce69c fix(ignore): update zigbee-herdsman-converters to 25.7.0 (#28361) 2025-08-22 14:36:25 +00:00
Koen Kanters 4bd0c04bdc fix(ignore): update zigbee-herdsman-converters to 25.6.0 (#28344) 2025-08-20 18:50:40 +00:00
Joseph Spiros ccfab84e7f fix: Home Assistant: use "occupancy" device_class for human presence sensors (#28329) 2025-08-20 20:14:23 +02:00
Koen Kanters 23f831a1fe fix(ignore): update zigbee-herdsman-converters to 25.5.0 (#28328) 2025-08-19 18:35:14 +00:00
Koen KantersandNerivec 384bd309a6 fix(ignore): update zigbee-herdsman-converters to 25.4.0 (#28313)
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2025-08-18 21:33:42 +02:00
Koen KantersandNerivec 5f7c00515a fix(ignore): update zigbee-herdsman-converters to 25.3.0 (#28301)
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2025-08-18 21:33:42 +02:00
Koen Kanters bed8beccbc fix(ignore): update zigbee-herdsman to 6.0.1 (#28287) 2025-08-18 21:33:42 +02:00
Nerivec b21f926b3e fix: Support for new ZH typing (#28257) 2025-08-18 21:33:42 +02:00
Nerivec 6e7cc30da7 chore: skip unnecessary deps in dev-types branch (#28312) 2025-08-18 21:22:02 +02:00
Koen Kanters 4c9784255b fix(ignore): update zigbee-herdsman-converters to 25.2.0 (#28280) 2025-08-15 10:44:18 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Nerivec
1dd6cc3da5 fix(ignore): bump the minor-patch group with 5 updates (#28237)
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-08-14 20:52:49 +02:00
Nerivec d04ab53f1d fix(ignore): update zigbee2mqtt-windfront to 1.8.1 (#28272) 2025-08-14 10:52:50 +00:00
Nerivec f4cfcbacde fix(ignore): update zigbee2mqtt-windfront to 1.8.0 (#28266) 2025-08-13 22:52:18 +00:00
Koen Kanters 2b1780879a fix(ignore): update zigbee-herdsman-converters to 25.1.0 (#28264) 2025-08-13 19:39:33 +00:00
Koen Kantersandnurikk 800b85c812 fix(ignore): update zigbee2mqtt-frontend to 0.9.20 (#28250)
Co-authored-by: nurikk <1525421+nurikk@users.noreply.github.com>
2025-08-12 18:09:26 +00:00
Alexis "Horgix" Chotard c5fa13215c chore(github): fix link to "fail to start" FAQ in problem report template (#28244) 2025-08-12 19:42:19 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 6414ade05d chore(deps): bump actions/checkout from 4 to 5 (#28238)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 19:36:31 +02:00
Koen Kanters 5286570bd6 fix: Update to refactored zhc api and fix onEvent(stop) not being called when device is force removed (#27984) 2025-08-11 20:32:59 +02:00
Koen Kanters 3160219824 fix(ignore): update zigbee-herdsman-converters to 25.0.0 (#28236) 2025-08-11 20:26:49 +02:00
Koen Kanters bd1be88602 fix(ignore): update zigbee-herdsman to 6.0.0 (#28235) 2025-08-11 20:26:07 +02:00
Nerivec a102fac2c8 fix: Set minimum for some settings (#28209) 2025-08-09 14:40:27 +02:00
Nerivec 63a4207061 fix: Always unlink node_modules symlinks on stop (#28202) 2025-08-08 19:38:04 +02:00
Nerivec 092c95b716 fix(ignore): update zigbee2mqtt-windfront to 1.6.0 (#28207) 2025-08-08 10:55:58 +00:00
Koen Kanters 09f046c72d fix(ignore): update zigbee-herdsman-converters to 24.14.0 (#28199) 2025-08-07 18:28:28 +00:00
Kamil Breguła b7cbd9b5d8 fix: Home Assistant: set icon and state class for orientation sensors (#28192) 2025-08-07 19:29:22 +02:00
Koen Kanters e12add17be chore: merge master to dev 2025-08-06 20:04:47 +02:00
dependabot[bot] bda5bf9e96 chore(deps-dev): bump tmp from 0.2.3 to 0.2.4 (#28186) 2025-08-06 20:04:33 +02:00
Koen Kanters 2056b89bac fix(ignore): update zigbee-herdsman-converters to 24.13.0 (#28175) 2025-08-05 19:04:52 +00:00
Koen Kanters d698770169 Revert "Revert "chore: cleanup Dockerfile (#28166)""
This reverts commit b41e3e781e.
2025-08-05 19:44:31 +02:00
Koen Kanters b41e3e781e Revert "chore: cleanup Dockerfile (#28166)"
This reverts commit e8cec3542f.
2025-08-04 21:40:19 +02:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>NerivecKoen Kanters
b72abd675f fix(ignore): bump the minor-patch group with 4 updates (#28147)
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>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2025-08-04 21:32:51 +02:00
Koen Kanters 9836f0e27a fix(ignore): update zigbee-herdsman-converters to 24.12.0 (#28170) 2025-08-04 19:27:39 +00:00
NerivecandKoen Kanters e8cec3542f chore: cleanup Dockerfile (#28166)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2025-08-04 20:53:07 +02:00
krkk 050ebfc9f5 fix: Remove 60 MB (28%) of dev dependencies from Docker image (#28160) 2025-08-04 20:36:44 +02:00
github-actions[bot] 449b99b0c0 chore: promote to dev 2025-08-01 15:52:58 +00:00
60 changed files with 3142 additions and 1121 deletions
@@ -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
+9 -1
View File
@@ -6,7 +6,7 @@ body:
attributes:
value: |
**IMPORTANT:** Before submitting:
- Zigbee2MQTT fails to start? Read [this](https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start.html)
- Zigbee2MQTT fails to start? Read [this](https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start_crashes-runtime.html)
- Raspberry Pi users? Make sure to check [this](https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start_crashes-runtime.html#raspberry-pi-users-use-a-good-power-supply)
- You read the [FAQ](https://www.zigbee2mqtt.io/guide/faq/)
- Are you using an EZSP adapter (e.g. Dongle-E/SkyConnect)? Try the [new driver](https://github.com/Koenkk/zigbee2mqtt/discussions/21462)
@@ -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:
+459
View File
@@ -0,0 +1,459 @@
# 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 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
## Technology Stack
### 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)
### 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
### 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
### 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 `rimrafSync` for synchronous file deletion when appropriate
- Leverage async/await for I/O operations to avoid blocking
- Use JSON stable stringify for consistent object serialization: `json-stable-stringify-without-jsonify`
- 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
@@ -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]
```
+20 -9
View File
@@ -12,25 +12,28 @@ on:
permissions:
contents: write
pull-requests: write
id-token: write
jobs:
ci:
runs-on: ubuntu-latest
permissions:
id-token: write
id-token: write # for npm publish
actions: read # for Nerivec/action-ci-bench
pull-requests: write # for Nerivec/action-ci-bench
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
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@v4
- uses: actions/checkout@v5
if: ((github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push') == false
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 24
registry-url: https://registry.npmjs.org/
@@ -50,10 +53,13 @@ jobs:
- name: Bench
if: github.ref == 'refs/heads/dev' || (github.event_name == 'pull_request' && !startsWith(github.head_ref, 'release-please-'))
uses: CodSpeedHQ/action@v3
uses: Nerivec/action-ci-bench@main
with:
run: pnpm run bench
token: ${{ secrets.CODSPEED_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
compare-against: dev
base-cmd: npx vitest bench --run --config ./test/vitest.config.mts --outputJson bench.json
compare-cmd: npx vitest bench --run --config ./test/vitest.config.mts --compare bench.json
base-result: bench.json
- name: Log in to the Docker container registry
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
@@ -107,6 +113,11 @@ jobs:
git checkout dev-types
rm -rf *
cp -R $temp_dir/* .
# remove unnecessary dependencies
KEEP_DEPS='["zigbee-herdsman","zigbee-herdsman-converters"]'
tmp_pkg=$(mktemp)
jq --indent 4 --argjson keep "$KEEP_DEPS" 'def filterDeps: (. // {}) | with_entries(select(.key as $k | $keep | index($k))); .dependencies |= filterDeps | .devDependencies |= filterDeps | .optionalDependencies |= filterDeps' package.json > "$tmp_pkg"
mv "$tmp_pkg" package.json
git add -A
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
@@ -209,11 +220,11 @@ jobs:
runs-on: ${{ matrix.os }}
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
cache: pnpm
+1 -1
View File
@@ -10,6 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout repository'
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: 'Dependency review'
uses: actions/dependency-review-action@v4
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
steps:
- name: Delete untagged images
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
merge-master-to-dev:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: devmasx/merge-branch@master
with:
type: now
@@ -0,0 +1,148 @@
name: New Device Support/External Converter Issue Bot
on:
issues:
types: [opened, edited]
permissions:
issues: write
contents: read
jobs:
comment:
runs-on: ubuntu-latest
steps:
- name: Comment on new device support/external converter issue
uses: actions/github-script@v8
with:
script: |
const issue = context.payload.issue;
const newDeviceSupport = issue.title && issue.title.startsWith("[New device support]");
const externalConverter = issue.title && issue.title.startsWith("[External Converter]");
// Only proceed for new device support or external converter issues
if (!newDeviceSupport && !externalConverter) {
return;
}
// 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
}
}
`);
}
}
// Checkout the repo and grep for the manufacturer name
const execSync = require('child_process').execSync;
execSync(`git clone https://github.com/Koenkk/zigbee-herdsman-converters.git zhc`, {stdio: 'ignore'});
// Check if Tuya manufacturer name is already supported.
const tuyaManufacturerNameRe = /['"](_T\w+_(\w+))['"]/g;
const tuyaManufacturerNames = Array.from(issue.body.matchAll(tuyaManufacturerNameRe), m => [m[1], m[2]]);
if (tuyaManufacturerNames.length > 0) {
for (const [fullName, partialName] of tuyaManufacturerNames) {
const fullMatch = (() => { try { return execSync(`grep -r --include="*.ts" "${fullName}" zhc`, {encoding: 'utf8'}); } catch { return undefined; } })();
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."
].join('\n')
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: "closed",
});
return;
}
const partialMatch = (() => { try { return execSync(`grep -r --include="*.ts" "${partialName}" zhc`, {encoding: 'utf8'}); } catch { return undefined; } })();
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!",
].join('\n')
});
return;
}
}
} else {
// Check if zigbee model is already supported.
const zigbeeModelRe = /zigbeeModel: \[['"](.+)['"]\]/g;
const zigbeeModels = Array.from(issue.body.matchAll(zigbeeModelRe), m => m[1]);
if (zigbeeModels.length > 0) {
for (const zigbeeModel of zigbeeModels) {
const fullMatch = (() => { try { return execSync(`grep -r --include="*.ts" '"${zigbeeModel}"' zhc`, {encoding: 'utf8'}); } catch { return undefined; } })();
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."
].join('\n')
});
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."
].join('\n')
});
+5 -4
View File
@@ -20,7 +20,7 @@ jobs:
with:
version: 9
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 24
@@ -31,11 +31,11 @@ jobs:
token: ${{secrets.GH_TOKEN}}
# Checkout repos
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
repository: koenkk/zigbee2mqtt
path: ./z2m
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
repository: koenkk/zigbee2mqtt
path: ./z2m-master
@@ -54,10 +54,11 @@ jobs:
MASTER_ZHC_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee-herdsman-converters"')
MASTER_ZH_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee-herdsman"')
MASTER_FRONTEND_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee2mqtt-frontend"')
MASTER_WINDFRONT_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee2mqtt-windfront"')
wget -q -O - https://raw.githubusercontent.com/Koenkk/zigbee2mqtt/release-please--branches--dev--components--zigbee2mqtt/CHANGELOG.md > z2m/CHANGELOG.md
cd z2m
pnpm i --frozen-lockfile
node scripts/generateChangelog.js $MASTER_Z2M_VERSION $MASTER_ZHC_VERSION $MASTER_ZH_VERSION $MASTER_FRONTEND_VERSION >> ../changelog.md
node scripts/generateChangelog.mjs $MASTER_Z2M_VERSION $MASTER_ZHC_VERSION $MASTER_ZH_VERSION $MASTER_FRONTEND_VERSION $MASTER_WINDFRONT_VERSION >> ../changelog.md
env:
GH_TOKEN: ${{secrets.GH_TOKEN}}
- name: Update changelog gist
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v10
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'
+2 -2
View File
@@ -13,12 +13,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
ref: dev
token: ${{ secrets.GH_TOKEN }}
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "2.6.0"
".": "2.6.3"
}
+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 `rimrafSync` 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
+137 -1
View File
@@ -1,5 +1,141 @@
# Changelog
## [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)
### Bug Fixes
* Fix `DatabaseEntry with ID 'X' does not exist` when adding device to delete group ID ([#28583](https://github.com/Koenkk/zigbee2mqtt/issues/28583)) ([09f33b3](https://github.com/Koenkk/zigbee2mqtt/commit/09f33b31a940c72ecfd42586427f54b3567b097b))
* Handle empty or null Home Assistant name in device settings ([#28364](https://github.com/Koenkk/zigbee2mqtt/issues/28364)) ([a5e1e8c](https://github.com/Koenkk/zigbee2mqtt/commit/a5e1e8c0c316f500cdc19d7c47b5156ed0b1e0f0))
* HomeAssistant: add `default_entity_id` to discovery message ([#28560](https://github.com/Koenkk/zigbee2mqtt/issues/28560)) ([15e00ea](https://github.com/Koenkk/zigbee2mqtt/commit/15e00ea8a49cc26e6c359bb60fbec0e76499fbf1))
* **ignore:** bump @types/node from 24.4.0 to 24.5.2 in the minor-patch group ([#28700](https://github.com/Koenkk/zigbee2mqtt/issues/28700)) ([e8847d1](https://github.com/Koenkk/zigbee2mqtt/commit/e8847d1a342d04cd2e82704518cfb23745010dd1))
* **ignore:** bump the minor-patch group with 3 updates ([#28541](https://github.com/Koenkk/zigbee2mqtt/issues/28541)) ([86b4581](https://github.com/Koenkk/zigbee2mqtt/commit/86b458111b44a2e99f4abacaaadc4caf0d6b912c))
* **ignore:** bump the minor-patch group with 3 updates ([#28638](https://github.com/Koenkk/zigbee2mqtt/issues/28638)) ([c3e1525](https://github.com/Koenkk/zigbee2mqtt/commit/c3e1525f60d577ff2669761516fcfb23b4ee1bb7))
* **ignore:** bump the minor-patch group with 4 updates ([#28778](https://github.com/Koenkk/zigbee2mqtt/issues/28778)) ([c9d01f4](https://github.com/Koenkk/zigbee2mqtt/commit/c9d01f4118cdef2a54bae43b09edb5e54c4f3042))
* **ignore:** bump zigbee2mqtt-windfront from 2.1.0 to 2.2.0 in the minor-patch group ([#28764](https://github.com/Koenkk/zigbee2mqtt/issues/28764)) ([1166341](https://github.com/Koenkk/zigbee2mqtt/commit/116634193a0c7693c6431ceccb373cf5e3f99e40))
* **ignore:** update zigbee-herdsman to 6.1.0 ([#28460](https://github.com/Koenkk/zigbee2mqtt/issues/28460)) ([06862c7](https://github.com/Koenkk/zigbee2mqtt/commit/06862c72594abf083411cf517c8dfb3db12fd4a9))
* **ignore:** update zigbee-herdsman to 6.1.1 ([#28597](https://github.com/Koenkk/zigbee2mqtt/issues/28597)) ([c28ca79](https://github.com/Koenkk/zigbee2mqtt/commit/c28ca79d8a8d5264c3e53f1d1eb96ce5dce1f90e))
* **ignore:** update zigbee-herdsman to 6.1.2 ([#28689](https://github.com/Koenkk/zigbee2mqtt/issues/28689)) ([79c0c09](https://github.com/Koenkk/zigbee2mqtt/commit/79c0c0933ab2500ae4d5bd42dea57923729f8d86))
* **ignore:** update zigbee-herdsman to 6.1.3 ([#28717](https://github.com/Koenkk/zigbee2mqtt/issues/28717)) ([892f924](https://github.com/Koenkk/zigbee2mqtt/commit/892f92448e256acd59d329cde4b7519d18cd829a))
* **ignore:** update zigbee-herdsman to 6.1.4 ([#28762](https://github.com/Koenkk/zigbee2mqtt/issues/28762)) ([508318d](https://github.com/Koenkk/zigbee2mqtt/commit/508318de57267791422d731fcb9a423fa01c8062))
* **ignore:** update zigbee-herdsman to 6.1.5 ([#28773](https://github.com/Koenkk/zigbee2mqtt/issues/28773)) ([a0d91cc](https://github.com/Koenkk/zigbee2mqtt/commit/a0d91cc063437cc595dd693e33fe5b1f044cd6bf))
* **ignore:** update zigbee-herdsman-converters to 25.15.0 ([#28462](https://github.com/Koenkk/zigbee2mqtt/issues/28462)) ([3f27f79](https://github.com/Koenkk/zigbee2mqtt/commit/3f27f79d7dabd1951c9c1f2f3dfc24d6bc741edd))
* **ignore:** update zigbee-herdsman-converters to 25.17.0 ([#28480](https://github.com/Koenkk/zigbee2mqtt/issues/28480)) ([3018e40](https://github.com/Koenkk/zigbee2mqtt/commit/3018e40daef092d74de4eea9e2cb8f174899cd71))
* **ignore:** update zigbee-herdsman-converters to 25.17.1 ([#28511](https://github.com/Koenkk/zigbee2mqtt/issues/28511)) ([54d729e](https://github.com/Koenkk/zigbee2mqtt/commit/54d729e37a80b0d7a22b67b6223e28685cbf1974))
* **ignore:** update zigbee-herdsman-converters to 25.18.0 ([#28518](https://github.com/Koenkk/zigbee2mqtt/issues/28518)) ([e8e4977](https://github.com/Koenkk/zigbee2mqtt/commit/e8e4977327c147f6338d323662823dd3316d54a3))
* **ignore:** update zigbee-herdsman-converters to 25.19.0 ([#28533](https://github.com/Koenkk/zigbee2mqtt/issues/28533)) ([5f2630c](https://github.com/Koenkk/zigbee2mqtt/commit/5f2630c93b8541eafbb737c6eb134155ae2fa6f5))
* **ignore:** update zigbee-herdsman-converters to 25.20.0 ([#28545](https://github.com/Koenkk/zigbee2mqtt/issues/28545)) ([a297b92](https://github.com/Koenkk/zigbee2mqtt/commit/a297b9255b961d19c0164f6dc29fcaf3cc10aab6))
* **ignore:** update zigbee-herdsman-converters to 25.21.0 ([#28561](https://github.com/Koenkk/zigbee2mqtt/issues/28561)) ([01d5863](https://github.com/Koenkk/zigbee2mqtt/commit/01d5863487322729fed61da167b6754289d26919))
* **ignore:** update zigbee-herdsman-converters to 25.22.0 ([#28580](https://github.com/Koenkk/zigbee2mqtt/issues/28580)) ([5f6fae0](https://github.com/Koenkk/zigbee2mqtt/commit/5f6fae0df729b8fafa4d8835ba48dc5e25578330))
* **ignore:** update zigbee-herdsman-converters to 25.23.0 ([#28598](https://github.com/Koenkk/zigbee2mqtt/issues/28598)) ([3f25834](https://github.com/Koenkk/zigbee2mqtt/commit/3f258342d8acd2cd4f69902679735dee2a7e0e01))
* **ignore:** update zigbee-herdsman-converters to 25.24.0 ([#28608](https://github.com/Koenkk/zigbee2mqtt/issues/28608)) ([2edf85f](https://github.com/Koenkk/zigbee2mqtt/commit/2edf85f6e1c8b0a42eacfd38a4fd4a43875e86d1))
* **ignore:** update zigbee-herdsman-converters to 25.25.0 ([#28621](https://github.com/Koenkk/zigbee2mqtt/issues/28621)) ([b2f7460](https://github.com/Koenkk/zigbee2mqtt/commit/b2f74607cc32d17df12fc7361b9bab7eafebf5bd))
* **ignore:** update zigbee-herdsman-converters to 25.26.0 ([#28629](https://github.com/Koenkk/zigbee2mqtt/issues/28629)) ([2cb7dcc](https://github.com/Koenkk/zigbee2mqtt/commit/2cb7dcc12ab00dbc966717ab70b5f8ad8a86012e))
* **ignore:** update zigbee-herdsman-converters to 25.29.0 ([#28654](https://github.com/Koenkk/zigbee2mqtt/issues/28654)) ([382d37e](https://github.com/Koenkk/zigbee2mqtt/commit/382d37e839bad69c02c6a560b32964b03305ac67))
* **ignore:** update zigbee-herdsman-converters to 25.30.0 ([#28707](https://github.com/Koenkk/zigbee2mqtt/issues/28707)) ([77926bc](https://github.com/Koenkk/zigbee2mqtt/commit/77926bc1c31a86bf2ded7838216a59380f3bde1e))
* **ignore:** update zigbee-herdsman-converters to 25.31.0 ([#28726](https://github.com/Koenkk/zigbee2mqtt/issues/28726)) ([a310627](https://github.com/Koenkk/zigbee2mqtt/commit/a310627e19fb0f684be407d7d54bf90ff112041a))
* **ignore:** update zigbee-herdsman-converters to 25.32.0 ([#28746](https://github.com/Koenkk/zigbee2mqtt/issues/28746)) ([87c474d](https://github.com/Koenkk/zigbee2mqtt/commit/87c474d2f5433f74fda122a42d4ffa16c64b3a00))
* **ignore:** update zigbee-herdsman-converters to 25.33.0 ([#28758](https://github.com/Koenkk/zigbee2mqtt/issues/28758)) ([26c74b5](https://github.com/Koenkk/zigbee2mqtt/commit/26c74b57786e0feffe5d1bb3ae7406e25449a591))
* **ignore:** update zigbee-herdsman-converters to 25.34.0 ([#28763](https://github.com/Koenkk/zigbee2mqtt/issues/28763)) ([f6c0ce7](https://github.com/Koenkk/zigbee2mqtt/commit/f6c0ce735ff582e8d07a11f951a1faa9720367c1))
* **ignore:** update zigbee-herdsman-converters to 25.35.1 ([#28774](https://github.com/Koenkk/zigbee2mqtt/issues/28774)) ([1283859](https://github.com/Koenkk/zigbee2mqtt/commit/1283859a12d09ed1b7ea2605acbf2293adc80df0))
* **ignore:** update zigbee2mqtt-frontend to 0.9.21 ([#28745](https://github.com/Koenkk/zigbee2mqtt/issues/28745)) ([2c70deb](https://github.com/Koenkk/zigbee2mqtt/commit/2c70deb411fbf670f14b7c5457e96d21f7203822))
* **ignore:** update zigbee2mqtt-windfront to 2.1.0 ([#28471](https://github.com/Koenkk/zigbee2mqtt/issues/28471)) ([e52203a](https://github.com/Koenkk/zigbee2mqtt/commit/e52203a1b8b543820506780981de997e6e7edeff))
* Make windfront the default frontend ([#28546](https://github.com/Koenkk/zigbee2mqtt/issues/28546)) ([fefae99](https://github.com/Koenkk/zigbee2mqtt/commit/fefae9957c9d35bc2cfa1264cb23c4731d21a64a))
## [2.6.1](https://github.com/Koenkk/zigbee2mqtt/compare/2.6.0...2.6.1) (2025-09-01)
### Bug Fixes
* Always unlink `node_modules` symlinks on stop ([#28202](https://github.com/Koenkk/zigbee2mqtt/issues/28202)) ([63a4207](https://github.com/Koenkk/zigbee2mqtt/commit/63a4207061039e4dab50f5f999febe4d87397856))
* Home Assistant: set icon and state class for orientation sensors ([#28192](https://github.com/Koenkk/zigbee2mqtt/issues/28192)) ([b7cbd9b](https://github.com/Koenkk/zigbee2mqtt/commit/b7cbd9b5d830fc1b936208076c440bc561552220))
* Home Assistant: use "occupancy" device_class for human presence sensors ([#28329](https://github.com/Koenkk/zigbee2mqtt/issues/28329)) ([ccfab84](https://github.com/Koenkk/zigbee2mqtt/commit/ccfab84e7f93be151e841834943d8a65500fe5d9))
* **ignore:** bump the minor-patch group with 2 updates ([#28383](https://github.com/Koenkk/zigbee2mqtt/issues/28383)) ([a8fd23b](https://github.com/Koenkk/zigbee2mqtt/commit/a8fd23b6cc97468f345216913ebb6c8f27e83789))
* **ignore:** bump the minor-patch group with 4 updates ([#28147](https://github.com/Koenkk/zigbee2mqtt/issues/28147)) ([b72abd6](https://github.com/Koenkk/zigbee2mqtt/commit/b72abd675f409fca324b34936c1bfb6f3d8aefd4))
* **ignore:** bump the minor-patch group with 5 updates ([#28237](https://github.com/Koenkk/zigbee2mqtt/issues/28237)) ([1dd6cc3](https://github.com/Koenkk/zigbee2mqtt/commit/1dd6cc3da54527eb5560c7edec7e56324ef38ea1))
* **ignore:** update zigbee-herdsman to 6.0.0 ([#28235](https://github.com/Koenkk/zigbee2mqtt/issues/28235)) ([bd1be88](https://github.com/Koenkk/zigbee2mqtt/commit/bd1be886022ac13bb71828fc773df0f8f144ec8a))
* **ignore:** update zigbee-herdsman to 6.0.1 ([#28287](https://github.com/Koenkk/zigbee2mqtt/issues/28287)) ([bed8bec](https://github.com/Koenkk/zigbee2mqtt/commit/bed8beccbc54c06430bdd97bcfb8d14158756216))
* **ignore:** update zigbee-herdsman to 6.0.2 ([#28406](https://github.com/Koenkk/zigbee2mqtt/issues/28406)) ([264440e](https://github.com/Koenkk/zigbee2mqtt/commit/264440ebcc7c86fb6f2fbacd76a3b1bc451f01ad))
* **ignore:** update zigbee-herdsman to 6.0.4 ([#28438](https://github.com/Koenkk/zigbee2mqtt/issues/28438)) ([7788653](https://github.com/Koenkk/zigbee2mqtt/commit/77886530fe8c6599cfa1a1a797743fc07cdee75c))
* **ignore:** update zigbee-herdsman-converters to 24.12.0 ([#28170](https://github.com/Koenkk/zigbee2mqtt/issues/28170)) ([9836f0e](https://github.com/Koenkk/zigbee2mqtt/commit/9836f0e27a57863bdbd98f9770ea37036ccf243c))
* **ignore:** update zigbee-herdsman-converters to 24.13.0 ([#28175](https://github.com/Koenkk/zigbee2mqtt/issues/28175)) ([2056b89](https://github.com/Koenkk/zigbee2mqtt/commit/2056b89baca9018254e23b02fbf0a5250314159b))
* **ignore:** update zigbee-herdsman-converters to 24.14.0 ([#28199](https://github.com/Koenkk/zigbee2mqtt/issues/28199)) ([09f046c](https://github.com/Koenkk/zigbee2mqtt/commit/09f046c72d309ad4270e404376bcf009b3411b00))
* **ignore:** update zigbee-herdsman-converters to 25.0.0 ([#28236](https://github.com/Koenkk/zigbee2mqtt/issues/28236)) ([3160219](https://github.com/Koenkk/zigbee2mqtt/commit/3160219824bcb12c4cbff232d2b7ec5c5a73317b))
* **ignore:** update zigbee-herdsman-converters to 25.1.0 ([#28264](https://github.com/Koenkk/zigbee2mqtt/issues/28264)) ([2b17808](https://github.com/Koenkk/zigbee2mqtt/commit/2b1780879a62c8f1a044be7f2a525517f8af4082))
* **ignore:** update zigbee-herdsman-converters to 25.10.0 ([#28399](https://github.com/Koenkk/zigbee2mqtt/issues/28399)) ([32d8265](https://github.com/Koenkk/zigbee2mqtt/commit/32d82651e233665e1850d86d7f8a30a1cdead507))
* **ignore:** update zigbee-herdsman-converters to 25.11.0 ([#28407](https://github.com/Koenkk/zigbee2mqtt/issues/28407)) ([c07fce9](https://github.com/Koenkk/zigbee2mqtt/commit/c07fce936f8bbbfe6405938221c426ba8dd5eff2))
* **ignore:** update zigbee-herdsman-converters to 25.12.0 ([#28417](https://github.com/Koenkk/zigbee2mqtt/issues/28417)) ([bdeab61](https://github.com/Koenkk/zigbee2mqtt/commit/bdeab6100195a129980cf9d0151d6eb1b849a802))
* **ignore:** update zigbee-herdsman-converters to 25.13.0 ([#28427](https://github.com/Koenkk/zigbee2mqtt/issues/28427)) ([203019a](https://github.com/Koenkk/zigbee2mqtt/commit/203019aa26e3ee028e68d1ef9b4d032624fd7991))
* **ignore:** update zigbee-herdsman-converters to 25.14.0 ([#28439](https://github.com/Koenkk/zigbee2mqtt/issues/28439)) ([bf22dda](https://github.com/Koenkk/zigbee2mqtt/commit/bf22ddac466db6523b659b422a36c968700c385f))
* **ignore:** update zigbee-herdsman-converters to 25.2.0 ([#28280](https://github.com/Koenkk/zigbee2mqtt/issues/28280)) ([4c97842](https://github.com/Koenkk/zigbee2mqtt/commit/4c9784255bab79d7d8a394135bfd6f5b9e40ef9b))
* **ignore:** update zigbee-herdsman-converters to 25.3.0 ([#28301](https://github.com/Koenkk/zigbee2mqtt/issues/28301)) ([5f7c005](https://github.com/Koenkk/zigbee2mqtt/commit/5f7c00515adc8941c51b6ede219529719949a9bf))
* **ignore:** update zigbee-herdsman-converters to 25.4.0 ([#28313](https://github.com/Koenkk/zigbee2mqtt/issues/28313)) ([384bd30](https://github.com/Koenkk/zigbee2mqtt/commit/384bd309a6d980ba1555a7dcfe13539df67699a1))
* **ignore:** update zigbee-herdsman-converters to 25.5.0 ([#28328](https://github.com/Koenkk/zigbee2mqtt/issues/28328)) ([23f831a](https://github.com/Koenkk/zigbee2mqtt/commit/23f831a1feae67aacd5b76ac81bf3c857065f5d3))
* **ignore:** update zigbee-herdsman-converters to 25.6.0 ([#28344](https://github.com/Koenkk/zigbee2mqtt/issues/28344)) ([4bd0c04](https://github.com/Koenkk/zigbee2mqtt/commit/4bd0c04bdcdf22e08e152d52f3f5d9a565d8f5f7))
* **ignore:** update zigbee-herdsman-converters to 25.7.0 ([#28361](https://github.com/Koenkk/zigbee2mqtt/issues/28361)) ([7bd7bce](https://github.com/Koenkk/zigbee2mqtt/commit/7bd7bce69c4065d94a809baef80966ee07691efc))
* **ignore:** update zigbee-herdsman-converters to 25.8.0 ([#28373](https://github.com/Koenkk/zigbee2mqtt/issues/28373)) ([8e6ba79](https://github.com/Koenkk/zigbee2mqtt/commit/8e6ba7975b292b4ba0817de8b7877fb7b9892aed))
* **ignore:** update zigbee-herdsman-converters to 25.9.0 ([#28380](https://github.com/Koenkk/zigbee2mqtt/issues/28380)) ([0147441](https://github.com/Koenkk/zigbee2mqtt/commit/0147441c935a4a77f4fccdb59499672de64fabd5))
* **ignore:** update zigbee2mqtt-frontend to 0.9.20 ([#28250](https://github.com/Koenkk/zigbee2mqtt/issues/28250)) ([800b85c](https://github.com/Koenkk/zigbee2mqtt/commit/800b85c812e57e982a689554b64cbe73aee4f192))
* **ignore:** update zigbee2mqtt-windfront to 1.6.0 ([#28207](https://github.com/Koenkk/zigbee2mqtt/issues/28207)) ([092c95b](https://github.com/Koenkk/zigbee2mqtt/commit/092c95b7169d7e23c30db154988fb50d4099d750))
* **ignore:** update zigbee2mqtt-windfront to 1.8.0 ([#28266](https://github.com/Koenkk/zigbee2mqtt/issues/28266)) ([f4cfcba](https://github.com/Koenkk/zigbee2mqtt/commit/f4cfcbacde8cab83e0a7ea742f39f87c2bceb1e1))
* **ignore:** update zigbee2mqtt-windfront to 1.8.1 ([#28272](https://github.com/Koenkk/zigbee2mqtt/issues/28272)) ([d04ab53](https://github.com/Koenkk/zigbee2mqtt/commit/d04ab53f1d584aed400ab8fd0bcd985f78611cb5))
* **ignore:** update zigbee2mqtt-windfront to 2.0.1 ([#28420](https://github.com/Koenkk/zigbee2mqtt/issues/28420)) ([08903f6](https://github.com/Koenkk/zigbee2mqtt/commit/08903f68d9a9e20994a1da2eb4835ad28ca68877))
* Remove 60 MB (28%) of dev dependencies from Docker image ([#28160](https://github.com/Koenkk/zigbee2mqtt/issues/28160)) ([050ebfc](https://github.com/Koenkk/zigbee2mqtt/commit/050ebfc9f5dd5a2de07f5402d3c99ac8ab3b12b4))
* Set minimum for some settings ([#28209](https://github.com/Koenkk/zigbee2mqtt/issues/28209)) ([a102fac](https://github.com/Koenkk/zigbee2mqtt/commit/a102fac2c8ab09a126fddfb0cc7170d57bf114c0))
* Support for new ZH typing ([#28257](https://github.com/Koenkk/zigbee2mqtt/issues/28257)) ([b21f926](https://github.com/Koenkk/zigbee2mqtt/commit/b21f926b3e152176d09dc7f95fbc02b2b5ec13da))
* Update to refactored zhc api and fix `onEvent(stop)` not being called when device is force removed ([#27984](https://github.com/Koenkk/zigbee2mqtt/issues/27984)) ([5286570](https://github.com/Koenkk/zigbee2mqtt/commit/5286570bd625a714fad432dd693308caf59344de))
* update zigbee2mqtt-windfront to 2.0.0 ([#28389](https://github.com/Koenkk/zigbee2mqtt/issues/28389)) ([2df363f](https://github.com/Koenkk/zigbee2mqtt/commit/2df363f3af8f13c5fde91bab7ac276de3be7ee44))
## [2.6.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.5.1...2.6.0) (2025-08-01)
@@ -243,7 +379,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))
+1 -1
View File
@@ -4,5 +4,5 @@ Everybody is invited and welcome to contribute to Zigbee2MQTT. Zigbee2MQTT is wr
- 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`.
- You can run the tests locally by executing `pnpm test`. Zigbee2MQTT enforces 100% code coverage, in case you add new code check if your code is covered by running `pnpm run test:coverage`. The coverage report can be found under `coverage/lcov-report/index.html`. Linting is also enforced and can be run with `pnpm run eslint`.
- When you want to add support for a new device no changes to Zigbee2MQTT have to be made, only to zigbee-herdsman-converters. You can find a guide for it [here](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html).
+1 -1
View File
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
+7 -4
View File
@@ -13,12 +13,15 @@ COPY package.json pnpm-lock.yaml ./
# Make and such are needed to compile serialport for riscv64
# Install pnpm, configure a tmp-based store, then use it under a cache mount so each arch gets its own store
# NOTE: We do not install devDependencies here (the --prod argument in pnpm install) as we use an already
# built artifact (./dist directory) from previous GitHub runner stages. Omitting devDependencies here
# saves storage as this node_modules/ is being copied straight to the release stage later.
RUN --mount=type=cache,id=pnpm-store,target=/tmp/pnpm-store \
apk add --no-cache make gcc g++ python3 linux-headers npm && \
npm install -g pnpm@$(sed -n 's/.*"packageManager": "pnpm@\([0-9.]*\)".*/\1/p' package.json) && \
apk add --no-cache make gcc g++ python3 linux-headers npm jq && \
npm install -g $(jq -r '.packageManager' package.json) && \
pnpm config set store-dir /tmp/pnpm-store && \
pnpm install --frozen-lockfile --no-optional && \
# serialport has outdated prebuilds that appear to fail on some archs, force build on target platform
pnpm install --frozen-lockfile --prod --no-optional && \
# force serialport to rebuild on current platform instead of using prebuilds
rm -rf $(find ./node_modules/.pnpm/ -wholename "*/@serialport/bindings-cpp/prebuilds" -type d) && \
pnpm rebuild @serialport/bindings-cpp
+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">
+6 -6
View File
@@ -8,8 +8,8 @@ interface EventBusMap {
adapterDisconnected: [];
permitJoinChanged: [data: eventdata.PermitJoinChanged];
publishAvailability: [];
deviceRenamed: [data: eventdata.EntityRenamed];
deviceRemoved: [data: eventdata.EntityRemoved];
entityRenamed: [data: eventdata.EntityRenamed];
entityRemoved: [data: eventdata.EntityRemoved];
lastSeenChanged: [data: eventdata.LastSeenChanged];
deviceNetworkAddressChanged: [data: eventdata.DeviceNetworkAddressChanged];
deviceAnnounce: [data: eventdata.DeviceAnnounce];
@@ -73,17 +73,17 @@ export default class EventBus {
}
public emitEntityRenamed(data: eventdata.EntityRenamed): void {
this.emitter.emit("deviceRenamed", data);
this.emitter.emit("entityRenamed", data);
}
public onEntityRenamed(key: ListenerKey, callback: (data: eventdata.EntityRenamed) => void): void {
this.on("deviceRenamed", callback, key);
this.on("entityRenamed", callback, key);
}
public emitEntityRemoved(data: eventdata.EntityRemoved): void {
this.emitter.emit("deviceRemoved", data);
this.emitter.emit("entityRemoved", data);
}
public onEntityRemoved(key: ListenerKey, callback: (data: eventdata.EntityRemoved) => void): void {
this.on("deviceRemoved", callback, key);
this.on("entityRemoved", callback, key);
}
public emitLastSeenChanged(data: eventdata.LastSeenChanged): void {
+1 -1
View File
@@ -207,7 +207,7 @@ export default class Availability extends Extension {
await this.publishAvailability(data.entity, false, true);
}
});
this.eventBus.onEntityRemoved(this, (data) => data.type === "device" && this.clearTimer(data.id));
this.eventBus.onEntityRemoved(this, (data) => data.entity.isDevice() && this.clearTimer(data.entity.ID));
this.eventBus.onDeviceLeave(this, (data) => this.clearTimer(data.ieeeAddr));
this.eventBus.onDeviceAnnounce(this, (data) => this.retrieveState(data.device));
this.eventBus.onLastSeenChanged(this, this.onLastSeenChanged);
+30 -51
View File
@@ -3,6 +3,7 @@ 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";
import Device from "../model/device";
import Group from "../model/group";
@@ -43,53 +44,33 @@ const getColorCapabilities = async (endpoint: zh.Endpoint): Promise<{colorTemper
};
};
const REPORT_CLUSTERS: Readonly<
Partial<
Record<
ClusterName,
Readonly<{
attribute: string;
minimumReportInterval: number;
maximumReportInterval: number;
reportableChange: number;
condition?: (endpoint: zh.Endpoint) => Promise<boolean>;
}>[]
>
>
> = {
genOnOff: [{attribute: "onOff", ...DEFAULT_REPORT_CONFIG, minimumReportInterval: 0, reportableChange: 0}],
genLevelCtrl: [{attribute: "currentLevel", ...DEFAULT_REPORT_CONFIG}],
const REPORT_CLUSTERS = {
genOnOff: [{attribute: "onOff" as const, ...DEFAULT_REPORT_CONFIG, minimumReportInterval: 0, reportableChange: 0}],
genLevelCtrl: [{attribute: "currentLevel" as const, ...DEFAULT_REPORT_CONFIG}],
lightingColorCtrl: [
{
attribute: "colorTemperature",
attribute: "colorTemperature" as const,
...DEFAULT_REPORT_CONFIG,
condition: async (endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorTemperature,
condition: async (endpoint: zh.Endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorTemperature,
},
{
attribute: "currentX",
attribute: "currentX" as const,
...DEFAULT_REPORT_CONFIG,
condition: async (endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorXY,
condition: async (endpoint: zh.Endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorXY,
},
{
attribute: "currentY",
attribute: "currentY" as const,
...DEFAULT_REPORT_CONFIG,
condition: async (endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorXY,
condition: async (endpoint: zh.Endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorXY,
},
],
closuresWindowCovering: [
{attribute: "currentPositionLiftPercentage", ...DEFAULT_REPORT_CONFIG},
{attribute: "currentPositionTiltPercentage", ...DEFAULT_REPORT_CONFIG},
{attribute: "currentPositionLiftPercentage" as const, ...DEFAULT_REPORT_CONFIG},
{attribute: "currentPositionTiltPercentage" as const, ...DEFAULT_REPORT_CONFIG},
],
};
type PollOnMessage = {
cluster: Readonly<Partial<Record<ClusterName, {type: string; data: KeyValue}[]>>>;
read: Readonly<{cluster: string; attributes: string[]; attributesForEndpoint?: (endpoint: zh.Endpoint) => Promise<string[]>}>;
manufacturerIDs: readonly Zcl.ManufacturerCode[];
manufacturerNames: readonly string[];
}[];
const POLL_ON_MESSAGE: Readonly<PollOnMessage> = [
const POLL_ON_MESSAGE = [
{
// On messages that have the cluster and type of below
cluster: {
@@ -109,7 +90,7 @@ const POLL_ON_MESSAGE: Readonly<PollOnMessage> = [
genScenes: [{type: "commandRecall", data: {}}],
},
// Read the following attributes
read: {cluster: "genLevelCtrl", attributes: ["currentLevel"]},
read: {cluster: "genLevelCtrl" as const, attributes: ["currentLevel"] as TClusterAttributeKeys<"genLevelCtrl">},
// When the bound devices/members of group have the following manufacturerIDs
manufacturerIDs: [
Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V,
@@ -141,7 +122,7 @@ const POLL_ON_MESSAGE: Readonly<PollOnMessage> = [
{type: "commandHueNotification", data: {button: 4}},
],
},
read: {cluster: "genOnOff", attributes: ["onOff"]},
read: {cluster: "genOnOff" as const, attributes: ["onOff"] as TClusterAttributeKeys<"genOnOff">},
manufacturerIDs: [
Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V,
Zcl.ManufacturerCode.ATMEL,
@@ -157,13 +138,13 @@ const POLL_ON_MESSAGE: Readonly<PollOnMessage> = [
genScenes: [{type: "commandRecall", data: {}}],
},
read: {
cluster: "lightingColorCtrl",
attributes: [] as string[],
cluster: "lightingColorCtrl" as const,
attributes: [] as TClusterAttributeKeys<"lightingColorCtrl">,
// Since not all devices support the same attributes they need to be calculated dynamically
// depending on the capabilities of the endpoint.
attributesForEndpoint: async (endpoint): Promise<string[]> => {
attributesForEndpoint: async (endpoint: zh.Endpoint): Promise<TClusterAttributeKeys<"lightingColorCtrl">> => {
const supportedAttrs = await getColorCapabilities(endpoint);
const readAttrs: string[] = [];
const readAttrs: TClusterAttributeKeys<"lightingColorCtrl"> = [];
if (supportedAttrs.colorXY) {
readAttrs.push("currentX", "currentY");
@@ -480,16 +461,15 @@ export default class Bind extends Extension {
const items = [];
// biome-ignore lint/style/noNonNullAssertion: valid from outer `if`
for (const c of REPORT_CLUSTERS[bind.cluster.name as ClusterName]!) {
if (!c.condition || (await c.condition(endpoint))) {
const i = {...c};
delete i.condition;
for (const c of REPORT_CLUSTERS[bind.cluster.name as keyof typeof REPORT_CLUSTERS]!) {
if (!("condition" in c) || !c.condition || (await c.condition(endpoint))) {
const {attribute, minimumReportInterval, maximumReportInterval, reportableChange} = c;
items.push(i);
items.push({attribute, minimumReportInterval, maximumReportInterval, reportableChange});
}
}
await endpoint.configureReporting(bind.cluster.name, items);
await endpoint.configureReporting(bind.cluster.name as keyof typeof REPORT_CLUSTERS, items);
logger.info(`Successfully setup reporting for '${entity}' cluster '${bind.cluster.name}'`);
} catch (error) {
logger.warning(`Failed to setup reporting for '${entity}' cluster '${bind.cluster.name}' (${(error as Error).message})`);
@@ -539,16 +519,15 @@ export default class Bind extends Extension {
const items = [];
// biome-ignore lint/style/noNonNullAssertion: valid from loop (pushed to array only if in)
for (const item of REPORT_CLUSTERS[cluster as ClusterName]!) {
if (!item.condition || (await item.condition(endpoint))) {
const i = {...item};
delete i.condition;
for (const item of REPORT_CLUSTERS[cluster as keyof typeof REPORT_CLUSTERS]!) {
if (!("condition" in item) || !item.condition || (await item.condition(endpoint))) {
const {attribute, minimumReportInterval, reportableChange} = item;
items.push({...i, maximumReportInterval: 0xffff});
items.push({attribute, minimumReportInterval, maximumReportInterval: 0xffff, reportableChange});
}
}
await endpoint.configureReporting(cluster, items);
await endpoint.configureReporting(cluster as keyof typeof REPORT_CLUSTERS, items);
logger.info(`Successfully disabled reporting for '${entity}' cluster '${cluster}'`);
} catch (error) {
logger.warning(`Failed to disable reporting for '${entity}' cluster '${cluster}' (${(error as Error).message})`);
@@ -569,7 +548,7 @@ export default class Bind extends Extension {
* When we receive a message from a Hue dimmer we read the brightness from the bulb (if bound).
*/
const polls = POLL_ON_MESSAGE.filter((p) =>
p.cluster[data.cluster as ClusterName]?.some((c) => c.type === data.type && utils.equalsPartial(data.data, c.data)),
p.cluster[data.cluster as keyof (typeof p)["cluster"]]?.some((c) => c.type === data.type && utils.equalsPartial(data.data, c.data)),
);
if (polls.length) {
+3 -2
View File
@@ -630,7 +630,6 @@ export default class Bridge extends Extension {
await entity.zh.removeFromNetwork();
}
this.eventBus.emitEntityRemoved({id: entity.ID, name: friendlyName, type: "device"});
settings.removeDevice(entity.ID as string);
} else {
if (force) {
@@ -639,10 +638,12 @@ export default class Bridge extends Extension {
await entity.zh.removeFromNetwork();
}
this.eventBus.emitEntityRemoved({id: entity.ID, name: friendlyName, type: "group"});
this.zigbee.removeGroupFromLookup(entity.ID);
settings.removeGroup(entity.ID);
}
this.eventBus.emitEntityRemoved({entity, name: friendlyName});
// Remove from state
this.state.remove(entity.ID);
+15 -2
View File
@@ -57,13 +57,15 @@ export default abstract class ExternalJSExtension<M> extends Extension {
if (!basePathInZ2mDir) {
logger.debug(`External JS folder '${this.folderName}' is outside the Z2M install dir, creating a symlink to 'node_modules'`);
const nodeModulesSymlink = path.join(this.basePath, "node_modules");
/* v8 ignore start */
if (fs.existsSync(nodeModulesSymlink)) {
fs.unlinkSync(nodeModulesSymlink);
}
/* v8 ignore stop */
// Type `junction` is required on Windows.
// https://github.com/nodejs/node/issues/18518#issuecomment-513866491
/* v8 ignore next */
fs.symlinkSync(nodeModulesPath, nodeModulesSymlink, os.platform() === "win32" ? "junction" : "dir");
fs.symlinkSync(nodeModulesPath, nodeModulesSymlink, /* v8 ignore next */ os.platform() === "win32" ? "junction" : "dir");
}
}
}
@@ -75,6 +77,17 @@ export default abstract class ExternalJSExtension<M> extends Extension {
await this.publishExternalJS();
}
override async stop(): Promise<void> {
await super.stop();
// ensure "node_modules" is never followed & included in 3rd-party backup systems
const nodeModulesSymlink = path.join(this.basePath, "node_modules");
if (fs.existsSync(nodeModulesSymlink)) {
fs.unlinkSync(nodeModulesSymlink);
}
}
private getFilePath(name: string, mkBasePath = false): string {
if (mkBasePath && !fs.existsSync(this.basePath)) {
fs.mkdirSync(this.basePath, {recursive: true});
+55 -13
View File
@@ -81,7 +81,7 @@ const BINARY_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
noise_detected: {device_class: "sound"},
occupancy: {device_class: "occupancy"},
power_outage_memory: {entity_category: "config", icon: "mdi:memory"},
presence: {device_class: "presence"},
presence: {device_class: "occupancy"},
setup: {device_class: "running"},
smoke: {device_class: "smoke"},
sos: {device_class: "safety"},
@@ -236,9 +236,12 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
device_class: "water",
state_class: "total_increasing",
},
x_axis: {icon: "mdi:axis-x-arrow"},
y_axis: {icon: "mdi:axis-y-arrow"},
z_axis: {icon: "mdi:axis-z-arrow"},
x: {icon: "mdi:axis-x-arrow", state_class: "measurement"},
x_axis: {icon: "mdi:axis-x-arrow", state_class: "measurement"},
y: {icon: "mdi:axis-y-arrow", state_class: "measurement"},
y_axis: {icon: "mdi:axis-y-arrow", state_class: "measurement"},
z: {icon: "mdi:axis-z-arrow", state_class: "measurement"},
z_axis: {icon: "mdi:axis-z-arrow", state_class: "measurement"},
} as const;
const ENUM_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
action: {icon: "mdi:gesture-double-tap"},
@@ -703,19 +706,49 @@ export class HomeAssistant extends Extension {
const piHeatingDemand = (firstExpose as zhc.Climate).features.filter(isNumericExpose).find((f) => f.name === "pi_heating_demand");
if (piHeatingDemand) {
const discoveryEntry: DiscoveryEntry = {
type: "sensor",
object_id: endpoint ? /* v8 ignore next */ `${piHeatingDemand.name}_${endpoint}` : `${piHeatingDemand.name}`,
const discoveryEntry: Partial<DiscoveryEntry> = {
object_id: endpoint ? `${piHeatingDemand.name}_${endpoint}` : `${piHeatingDemand.name}`,
mockProperties: [{property: piHeatingDemand.property, value: null}],
discovery_payload: {
name: endpoint ? /* v8 ignore next */ `${piHeatingDemand.label} ${endpoint}` : piHeatingDemand.label,
name: endpoint ? `${piHeatingDemand.label} ${endpoint}` : piHeatingDemand.label,
value_template: `{{ value_json.${piHeatingDemand.property} }}`,
...(piHeatingDemand.unit && {unit_of_measurement: piHeatingDemand.unit}),
entity_category: "diagnostic",
icon: "mdi:radiator",
},
};
assert(discoveryEntry.discovery_payload);
if (piHeatingDemand.access & ACCESS_SET) {
discoveryEntry.type = "number";
discoveryEntry.discovery_payload.command_topic = true;
discoveryEntry.discovery_payload.command_topic_prefix = endpoint;
discoveryEntry.discovery_payload.command_topic_postfix = piHeatingDemand.property;
discoveryEntry.discovery_payload.min = piHeatingDemand.value_min;
discoveryEntry.discovery_payload.max = piHeatingDemand.value_max;
} else {
discoveryEntry.type = "sensor";
discoveryEntry.discovery_payload.entity_category = "diagnostic";
}
discoveryEntries.push(<DiscoveryEntry>discoveryEntry);
}
const piCoolingDemand = (firstExpose as zhc.Climate).features.filter(isNumericExpose).find((f) => f.name === "pi_cooling_demand");
if (piCoolingDemand) {
const discoveryEntry: DiscoveryEntry = {
type: "sensor",
object_id: endpoint ? /* v8 ignore next */ `${piCoolingDemand.name}_${endpoint}` : `${piCoolingDemand.name}`,
mockProperties: [{property: piCoolingDemand.property, value: null}],
discovery_payload: {
name: endpoint ? /* v8 ignore next */ `${piCoolingDemand.label} ${endpoint}` : piCoolingDemand.label,
value_template: `{{ value_json.${piCoolingDemand.property} }}`,
...(piCoolingDemand.unit && {unit_of_measurement: piCoolingDemand.unit}),
entity_category: "diagnostic",
icon: "mdi:air-conditioner",
},
};
discoveryEntries.push(discoveryEntry);
}
@@ -1218,13 +1251,13 @@ export class HomeAssistant extends Extension {
@bind async onEntityRemoved(data: eventdata.EntityRemoved): Promise<void> {
logger.debug(`Clearing Home Assistant discovery for '${data.name}'`);
const discovered = this.getDiscovered(data.id);
const discovered = this.getDiscovered(data.entity.ID);
for (const topic of Object.keys(discovered.messages)) {
await this.mqtt.publish(topic, "", {clientOptions: {retain: true, qos: 1}, baseTopic: this.discoveryTopic, skipReceive: false});
}
delete this.discovered[data.id];
delete this.discovered[data.entity.ID];
}
@bind async onGroupMembersChanged(data: eventdata.GroupMembersChanged): Promise<void> {
@@ -1512,6 +1545,11 @@ export class HomeAssistant extends Extension {
payload.object_id = `${payload.object_id}${payload.object_id_postfix ?? ""}`;
delete payload.object_id_postfix;
// Set `default_entity_id`, as of HA 2025.10 this replaces the `object_id`.
// For migration purposes we set both for now.
// https://github.com/home-assistant/core/pull/151775
payload.default_entity_id = `${config.type}.${payload.object_id}`;
// Set unique_id
payload.unique_id = `${entity.options.ID}_${config.object_id}_${settings.get().mqtt.base_topic}`;
@@ -1819,6 +1857,10 @@ export class HomeAssistant extends Extension {
};
const url = settings.get().frontend?.url ?? "";
// Since zigbee2mqtt-windfront support multiple instances the configuration URL contains the
// instance ID. Since we don't know which instance it is we always point to 0.
// https://github.com/Koenkk/zigbee2mqtt/issues/28936
const urlEntityPostfix = settings.get().frontend.package === "zigbee2mqtt-windfront" ? "0/" : "";
if (entity.isDevice()) {
assert(entity.definition, `Cannot 'getDevicePayload' for unsupported device`);
payload.model = entity.definition.description;
@@ -1826,11 +1868,11 @@ export class HomeAssistant extends Extension {
payload.manufacturer = entity.definition.vendor;
payload.sw_version = entity.zh.softwareBuildID;
payload.hw_version = entity.zh.hardwareVersion;
payload.configuration_url = `${url}/#/device/${entity.ieeeAddr}/info`;
payload.configuration_url = `${url}/#/device/${urlEntityPostfix}${entity.ieeeAddr}/info`;
} else if (entity.isGroup()) {
payload.model = "Group";
payload.manufacturer = "Zigbee2MQTT";
payload.configuration_url = `${url}/#/group/${entity.ID}`;
payload.configuration_url = `${url}/#/group/${urlEntityPostfix}${entity.ID}`;
} else {
payload.model = "Bridge";
payload.manufacturer = "Zigbee2MQTT";
+37 -25
View File
@@ -1,4 +1,4 @@
import {onEvent} from "zigbee-herdsman-converters";
import {onEvent, type OnEvent as ZhcOnEvent} from "zigbee-herdsman-converters";
import utils from "../util/utils";
import Extension from "./extension";
@@ -7,42 +7,50 @@ import Extension from "./extension";
* This extension calls the zigbee-herdsman-converters onEvent.
*/
export default class OnEvent extends Extension {
readonly #startCalled: Set<string> = new Set();
#getOnEventBaseData(device: Device): ZhcOnEvent.BaseData {
const deviceExposesChanged = (): void => this.eventBus.emitExposesAndDevicesChanged(device);
const state = this.state.get(device);
const options = device.options as KeyValue;
return {deviceExposesChanged, device: device.zh, state, options};
}
// biome-ignore lint/suspicious/useAwait: API
override async start(): Promise<void> {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
// don't await, in case of repeated failures this would hold startup
this.callOnEvent(device, "start", {}).catch(utils.noop);
this.callOnEvent(device, {type: "start", data: this.#getOnEventBaseData(device)}).catch(utils.noop);
}
this.eventBus.onDeviceMessage(this, async (data) => {
await this.callOnEvent(data.device, "message", {
endpoint: data.endpoint,
meta: data.meta,
cluster: typeof data.cluster === "string" ? data.cluster : /* v8 ignore next */ undefined, // XXX: ZH typing is wrong?
type: data.type,
data: data.data, // XXX: typing is a bit convoluted: ZHC has `KeyValueAny` here while Z2M has `KeyValue | Array<string | number>`
});
});
this.eventBus.onDeviceJoined(this, async (data) => {
await this.callOnEvent(data.device, "deviceJoined", {});
await this.callOnEvent(data.device, {type: "deviceJoined", data: this.#getOnEventBaseData(data.device)});
});
this.eventBus.onDeviceLeave(this, async (data) => {
if (data.device) {
await this.callOnEvent(data.device, "stop", {});
await this.callOnEvent(data.device, {type: "stop", data: {ieeeAddr: data.device.ieeeAddr}});
}
});
this.eventBus.onEntityRemoved(this, async (data) => {
if (data.entity.isDevice()) {
await this.callOnEvent(data.entity, {type: "stop", data: {ieeeAddr: data.entity.ieeeAddr}});
}
});
this.eventBus.onDeviceInterview(this, async (data) => {
await this.callOnEvent(data.device, "deviceInterview", {});
await this.callOnEvent(data.device, {type: "deviceInterview", data: {...this.#getOnEventBaseData(data.device), status: data.status}});
});
this.eventBus.onDeviceAnnounce(this, async (data) => {
await this.callOnEvent(data.device, "deviceAnnounce", {});
await this.callOnEvent(data.device, {type: "deviceAnnounce", data: this.#getOnEventBaseData(data.device)});
});
this.eventBus.onDeviceNetworkAddressChanged(this, async (data) => {
await this.callOnEvent(data.device, "deviceNetworkAddressChanged", {});
await this.callOnEvent(data.device, {type: "deviceNetworkAddressChanged", data: this.#getOnEventBaseData(data.device)});
});
this.eventBus.onEntityOptionsChanged(this, async (data) => {
if (data.entity.isDevice()) {
await this.callOnEvent(data.entity, "deviceOptionsChanged", {});
await this.callOnEvent(data.entity, {
type: "deviceOptionsChanged",
data: {...this.#getOnEventBaseData(data.entity), from: data.from, to: data.to},
});
this.eventBus.emitDevicesChanged();
}
});
@@ -52,23 +60,27 @@ export default class OnEvent extends Extension {
await super.stop();
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
await this.callOnEvent(device, "stop", {});
await this.callOnEvent(device, {type: "stop", data: {ieeeAddr: device.ieeeAddr}});
}
}
private async callOnEvent(device: Device, type: Parameters<typeof onEvent>[0], data: Parameters<typeof onEvent>[1]): Promise<void> {
private async callOnEvent(device: Device, event: ZhcOnEvent.Event): Promise<void> {
if (device.options.disabled) {
return;
}
const state = this.state.get(device);
const deviceExposesChanged = (): void => this.eventBus.emitExposesAndDevicesChanged(device);
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)});
}
await onEvent(type, data, device.zh, {deviceExposesChanged});
await onEvent(event);
await device.definition?.onEvent?.(event);
if (device.definition?.onEvent) {
const options: KeyValue = device.options;
await device.definition.onEvent(type, data, device.zh, options, state, {deviceExposesChanged});
if (event.type === "stop") {
this.#startCalled.delete(device.ieeeAddr);
}
}
}
+1 -1
View File
@@ -136,7 +136,7 @@ export default class Receive extends Extension {
const publish = async (payload: KeyValue): Promise<void> => {
assert(data.device.definition);
const options: KeyValue = data.device.options;
zhc.postProcessConvertedFromZigbeeMessage(data.device.definition, payload, options);
zhc.postProcessConvertedFromZigbeeMessage(data.device.definition, payload, options, data.device.zh);
if (settings.get().advanced.elapsed) {
const now = Date.now();
+18
View File
@@ -0,0 +1,18 @@
// 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 */
type Transferable = import("node:worker_threads").Transferable;
/** @deprecated DOM SHIM, DO NOT USE */
const addEventListener: import("node:events").EventEmitter["addListener"];
/** @deprecated DOM SHIM, DO NOT USE */
const removeEventListener: import("node:events").EventEmitter["removeListener"];
/** @deprecated DOM SHIM, DO NOT USE */
const postMessage: import("node:worker_threads").MessagePort["postMessage"];
}
export {};
+2 -2
View File
@@ -49,7 +49,7 @@ declare global {
namespace eventdata {
type EntityRenamed = {entity: Device | Group; homeAssisantRename: boolean; from: string; to: string};
type EntityRemoved = {id: string; name: string; type: "device"} | {id: number; name: string; type: "group"};
type EntityRemoved = {entity: Device | Group; name: string};
type MQTTMessage = {topic: string; message: string};
type MQTTMessagePublished = {topic: string; payload: string; options: MqttPublishOptions};
type StateChange = {
@@ -82,7 +82,7 @@ declare global {
groupID: number; // XXX: should this be `?`
cluster: string | number;
data: KeyValue | Array<string | number>;
meta: {zclTransactionSequenceNumber?: number; manufacturerCode?: number; frameControl?: ZHFrameControl};
meta: {zclTransactionSequenceNumber?: number; manufacturerCode?: number; frameControl?: ZHFrameControl; rawData: Buffer};
};
type ScenesChanged = {entity: Device | Group};
}
+5 -6
View File
@@ -2,10 +2,8 @@ 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 = ":";
@@ -31,7 +29,7 @@ class Logger {
// 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 timestamp = new Date().toISOString().slice(0, 19).replace("T", ".").replace(/:/g, "-");
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 +38,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,
});
+8 -2
View File
@@ -146,6 +146,7 @@ function generateHtmlForm(currentSettings: RecursivePartial<Settings>, devices:
id="serial_rtscts"
name="serial_rtscts"
${currentSettings.serial?.rtscts ? "checked" : ""}
${process.env.ZIGBEE2MQTT_CONFIG_SERIAL_RTSCTS ? "disabled" : ""}>
style="margin-bottom: 1rem;">
<small>Can be ignored for networked coordinators (TCP).</small>
</fieldset>
@@ -272,8 +273,13 @@ function generateHtmlForm(currentSettings: RecursivePartial<Settings>, devices:
<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" : ""}>
<label for="homeassistant_enabled">
<input
type="checkbox"
id="homeassistant_enabled"
name="homeassistant_enabled"
${currentSettings.homeassistant?.enabled ? "checked" : ""}
${process.env.ZIGBEE2MQTT_CONFIG_HOMEASSISTANT_ENABLED ? "disabled" : ""}>
Home Assistant enabled?
</label>
</fieldset>
+22 -9
View File
@@ -67,6 +67,7 @@
"title": "Timeout",
"requiresRestart": true,
"default": 10,
"minimum": 1,
"description": "Time after which an active device will be marked as offline in minutes"
},
"max_jitter": {
@@ -103,6 +104,7 @@
"title": "Timeout",
"requiresRestart": true,
"default": 1500,
"minimum": 1,
"description": "Time after which an passive device will be marked as offline in minutes"
}
},
@@ -348,7 +350,8 @@
"type": "number",
"title": "Update check interval",
"description": "Your device may request a check for a new firmware update. This value determines how frequently third party servers may actually be contacted to look for firmware updates. The value is set in minutes, and the default is 1 day.",
"default": 1440
"default": 1440,
"minimum": 1
},
"disable_automatic_update_check": {
"type": "boolean",
@@ -395,9 +398,9 @@
},
"package": {
"type": "string",
"enum": ["zigbee2mqtt-frontend", "zigbee2mqtt-windfront"],
"enum": ["zigbee2mqtt-windfront", "zigbee2mqtt-frontend"],
"title": "Package",
"default": "zigbee2mqtt-frontend",
"default": "zigbee2mqtt-windfront",
"requiresRestart": true,
"description": "Package used for the frontend"
},
@@ -560,7 +563,14 @@
"title": "Log Namespaced Levels",
"description": "Set individual log levels for certain namespaces",
"default": {},
"examples": [{"z2m:mqtt": "warning"}, {"zh:ember:uart:ash": "info"}]
"examples": [
{
"z2m:mqtt": "warning"
},
{
"zh:ember:uart:ash": "info"
}
]
},
"log_debug_to_mqtt_frontend": {
"type": "boolean",
@@ -663,12 +673,14 @@
},
{
"type": "number",
"title": "Pan ID (number)"
"title": "Pan ID (number)",
"minimum": 1,
"maximum": 65534
}
],
"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": {
@@ -695,7 +707,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]
@@ -772,7 +784,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"]
},
@@ -803,6 +815,7 @@
"title": "Interval",
"description": "Interval between checks in minutes",
"default": 10,
"minimum": 1,
"requiresRestart": true
},
"reset_on_check": {
@@ -912,7 +925,7 @@
"title": "Home Assistant",
"properties": {
"name": {
"type": "string",
"type": ["string", "null"],
"title": "Home Assistant name",
"description": "Name of the device in Home Assistant"
}
+1 -3
View File
@@ -1,9 +1,7 @@
import path from "node:path";
import type {ValidateFunction} from "ajv";
import Ajv from "ajv";
import objectAssignDeep from "object-assign-deep";
import data from "./data";
import schemaJson from "./settings.schema.json";
import utils from "./utils";
@@ -42,7 +40,7 @@ export const defaults = {
},
frontend: {
enabled: false,
package: "zigbee2mqtt-frontend",
package: "zigbee2mqtt-windfront",
port: 8080,
base_url: "/",
},
+4
View File
@@ -432,4 +432,8 @@ export default class Zigbee {
groupByID(id: number): Group | undefined {
return this.resolveGroup(id);
}
removeGroupFromLookup(id: number): void {
this.groupLookup.delete(id);
}
}
+19 -21
View File
@@ -1,16 +1,16 @@
{
"name": "zigbee2mqtt",
"version": "2.6.0",
"version": "2.6.3",
"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": "^20.15.0 || ^22.2.0 || ^24"
},
"keywords": [
"xiaomi",
@@ -48,40 +48,38 @@
"express-static-gzip": "^3.0.0",
"fast-deep-equal": "^3.1.3",
"finalhandler": "^2.1.0",
"humanize-duration": "^3.32.1",
"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.11.0",
"mqtt": "^5.14.1",
"object-assign-deep": "^0.4.0",
"rimraf": "^6.0.1",
"semver": "^7.7.1",
"rimraf": "^6.1.0",
"semver": "^7.7.3",
"source-map-support": "^0.5.21",
"throttleit": "^2.1.0",
"winston": "^3.17.0",
"winston": "^3.18.3",
"winston-syslog": "^2.7.1",
"winston-transport": "^4.9.0",
"ws": "^8.18.1",
"zigbee-herdsman": "5.0.4",
"zigbee-herdsman-converters": "24.11.0",
"zigbee2mqtt-frontend": "0.9.19",
"zigbee2mqtt-windfront": "^1.5.2"
"zigbee-herdsman": "6.3.2",
"zigbee-herdsman-converters": "25.59.0",
"zigbee2mqtt-frontend": "0.9.21",
"zigbee2mqtt-windfront": "2.2.3"
},
"devDependencies": {
"@biomejs/biome": "^2.0.5",
"@codspeed/vitest-plugin": "^4.0.1",
"@biomejs/biome": "^2.3.2",
"@types/finalhandler": "^1.2.3",
"@types/humanize-duration": "^3.27.4",
"@types/js-yaml": "^4.0.9",
"@types/node": "^24.0.3",
"@types/node": "^24.9.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.22",
"@types/serve-static": "^2.2.0",
"@types/ws": "8.18.1",
"@vitest/coverage-v8": "^3.1.1",
"tmp": "^0.2.3",
"typescript": "^5.8.3",
"tmp": "^0.2.5",
"typescript": "^5.9.3",
"vitest": "^3.1.1"
},
"pnpm": {
@@ -99,6 +97,6 @@
"zigbee2mqtt": "cli.js"
},
"optionalDependencies": {
"unix-dgram": "^2.0.6"
"unix-dgram": "^2.0.7"
}
}
+323 -577
View File
File diff suppressed because it is too large Load Diff
-157
View File
@@ -1,157 +0,0 @@
const path = require("node:path");
const fs = require("node:fs");
const process = require("node:process");
const {execSync} = require("node:child_process");
const zhc = require("zigbee-herdsman-converters");
const z2mTillVersion = process.argv[2];
const zhcTillVersion = process.argv[3];
const zhTillVersion = process.argv[4];
const frontendTillVersion = process.argv[5];
const changelogs = [
{
tillVersion: z2mTillVersion,
project: "koenkk/zigbee2mqtt",
contents: fs.readFileSync(path.join(__dirname, "..", "CHANGELOG.md"), "utf-8").split("\n"),
},
{
tillVersion: zhcTillVersion,
project: "koenkk/zigbee-herdsman-converters",
contents: fs.readFileSync(path.join(__dirname, "..", "node_modules", "zigbee-herdsman-converters", "CHANGELOG.md"), "utf-8").split("\n"),
},
{
tillVersion: zhTillVersion,
project: "koenkk/zigbee-herdsman",
contents: fs.readFileSync(path.join(__dirname, "..", "node_modules", "zigbee-herdsman", "CHANGELOG.md"), "utf-8").split("\n"),
},
{
tillVersion: frontendTillVersion,
project: "nurikk/zigbee2mqtt-frontend",
isFrontend: true,
contents: fs.readFileSync(path.join(__dirname, "..", "node_modules", "zigbee2mqtt-frontend", "CHANGELOG.md"), "utf-8").split("\n"),
},
];
const releaseRe = /## \[(.+)\]/;
const changes = {features: [], fixes: [], detect: [], add: [], error: [], frontend: []};
let context = null;
const changeRe = [
/^\* (\*\*(.+):\*\*)?(.+)\((\[#\d+\]\(.+\))\) \(\[.+\]\(https:.+\/(.+)\)\)$/,
/^\* (\*\*(.+):\*\*)?(.+)(https:\/\/github\.com.+) \(\[.+\]\(https:.+\/(.+)\)\)$/,
/^\* (\*\*(.+):\*\*)?(.+)() \(\[.+\]\(https:.+\/(.+)\)\)$/,
];
const frontendChangeRe = /^\* (\*\*.+:\*\* )()?(.+) \(\[.+\]\(https:.+\/()(.+)\)\)(.+)?$/;
let commitUserLookup = {};
const commitUserFile = path.join(__dirname, "commit-user-lookup.json");
if (fs.existsSync(commitUserFile)) {
commitUserLookup = JSON.parse(fs.readFileSync(commitUserFile, "utf8"));
}
const definitions = require("zigbee-herdsman-converters/devices/index").default.map((d) => zhc.prepareDefinition(d));
const whiteLabels = definitions.filter((d) => d.whiteLabel).flatMap((d) => d.whiteLabel);
const capitalizeFirstChar = (str) => str.charAt(0).toUpperCase() + str.slice(1);
for (const changelog of changelogs) {
for (const line of changelog.contents) {
const releaseMatch = line.match(releaseRe);
const changeMatch = changelog.isFrontend ? line.match(frontendChangeRe) : changeRe.map((re) => line.match(re)).find((e) => e);
if (releaseMatch) {
if (releaseMatch[1] === changelog.tillVersion) {
break;
}
} else if (line === "### Features") {
context = "features";
} else if (line === "### Bug Fixes") {
context = "fixes";
} else if (line.startsWith("* **ignore:**")) {
// continue;
} else if (changeMatch) {
let localContext = changelog.isFrontend ? "frontend" : changeMatch[2] ? changeMatch[2] : context;
if (!changes[localContext]) localContext = "error";
const commit = changeMatch[5];
const commitUserKey = `${changelog.project}-${commit} `;
let user =
commitUserKey in commitUserLookup
? commitUserLookup[commitUserKey]
: execSync(`curl -s https://api.github.com/repos/${changelog.project}/commits/${commit} | jq -r '.author.login'`)
.toString()
.trim();
if (user !== "null") commitUserLookup[commitUserKey] = user;
const messages = [];
let message = changeMatch[3].trim();
if (message.endsWith(".")) message = message.substring(0, message.length - 1);
if (changelog.isFrontend) {
changes[localContext].push(
`- [${commit.slice(0, 7)}](https://github.com/${changelog.project}/commit/${commit}) ${message} (@${user})`,
);
messages.push(capitalizeFirstChar(message));
} else {
const otherUser = message.match(/\[@(.+)\]\(https:\/\/github.com\/.+\)/) || message.match(/@(.+)/);
if (otherUser) {
user = otherUser[1];
message = message.replace(otherUser[0], "");
}
if (localContext === "add") {
for (const model of message.split(",")) {
const definition = definitions.find((d) => d.model === model.trim());
const whiteLabel = whiteLabels.find((d) => d.model === model.trim());
const match = definition || whiteLabel;
if (match) {
messages.push(`\`${match.model}\` ${match.vendor} ${match.description}`);
} else {
changes.error.push(`${line} (model '${model}' does not exist)`);
}
}
} else {
messages.push(capitalizeFirstChar(message));
}
let issue = changeMatch[4].trim();
if (issue && !issue.startsWith("[#")) issue = `[#${issue.split("/").pop()}](${issue})`;
if (!issue) {
issue = "_NO_ISSUE_";
localContext = "error";
}
for (const message of messages) {
changes[localContext].push(`- ${issue} ${message} (@${user})`);
}
}
} else if (line === "# Changelog" || line === "### ⚠ BREAKING CHANGES" || !line) {
// continue;
} else {
changes.error.push(`- Unmatched line: ${line}`);
}
}
}
let result = "";
const names = [
["features", "Improvements"],
["fixes", "Fixes"],
["frontend", "Frontend"],
["add", "New supported devices"],
["detect", "Fixed device detections"],
["error", "Changelog generator error"],
];
for (const name of names) {
result += `# ${name[1]}\n`;
if (name[0] === "add") {
result += `This release adds support for ${changes.add.length} devices: \n`;
}
for (const change of changes[name[0]]) {
result += `${change}\n`;
}
result += "\n";
}
fs.writeFileSync(commitUserFile, JSON.stringify(commitUserLookup), "utf-8");
console.log(result.trim());
+185
View File
@@ -0,0 +1,185 @@
import {execSync} from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import zhc from "zigbee-herdsman-converters";
import definitionIndex from "zigbee-herdsman-converters/devices/index";
const z2mTillVersion = process.argv[2];
const zhcTillVersion = process.argv[3];
const zhTillVersion = process.argv[4];
const frontendTillVersion = process.argv[5];
const windfrontTillVersion = process.argv[6];
const changelogs = [
{
tillVersion: z2mTillVersion,
project: "koenkk/zigbee2mqtt",
contents: fs.readFileSync(path.join(import.meta.dirname, "..", "CHANGELOG.md"), "utf-8").split("\n"),
},
{
tillVersion: zhcTillVersion,
project: "koenkk/zigbee-herdsman-converters",
contents: fs
.readFileSync(path.join(import.meta.dirname, "..", "node_modules", "zigbee-herdsman-converters", "CHANGELOG.md"), "utf-8")
.split("\n"),
},
{
tillVersion: zhTillVersion,
project: "koenkk/zigbee-herdsman",
contents: fs.readFileSync(path.join(import.meta.dirname, "..", "node_modules", "zigbee-herdsman", "CHANGELOG.md"), "utf-8").split("\n"),
},
{
tillVersion: frontendTillVersion,
project: "nurikk/zigbee2mqtt-frontend",
isFrontend: true,
contents: fs.readFileSync(path.join(import.meta.dirname, "..", "node_modules", "zigbee2mqtt-frontend", "CHANGELOG.md"), "utf-8").split("\n"),
},
{
tillVersion: windfrontTillVersion,
project: "Nerivec/zigbee2mqtt-windfront",
},
];
const releaseRe = /## \[(.+)\]/;
const windfrontChangeRe = /^\* (feat|fix): (.+?)(?: by @([^\s]+) in (https:\/\/github\.com\/Nerivec\/zigbee2mqtt-windfront\/pull\/(\d+)))?$/gm;
const changes = {features: [], fixes: [], detect: [], add: [], error: [], frontend: [], windfront: []};
let context = null;
const changeRe = [
/^\* (\*\*(.+):\*\*)?(.+)\((\[#\d+\]\(.+\))\) \(\[.+\]\(https:.+\/(.+)\)\)$/,
/^\* (\*\*(.+):\*\*)?(.+)(https:\/\/github\.com.+) \(\[.+\]\(https:.+\/(.+)\)\)$/,
/^\* (\*\*(.+):\*\*)?(.+)() \(\[.+\]\(https:.+\/(.+)\)\)$/,
];
const frontendChangeRe = /^\* (\*\*.+:\*\* )()?(.+) \(\[.+\]\(https:.+\/()(.+)\)\)(.+)?$/;
let commitUserLookup = {};
const commitUserFile = path.join(import.meta.dirname, "commit-user-lookup.json");
if (fs.existsSync(commitUserFile)) {
commitUserLookup = JSON.parse(fs.readFileSync(commitUserFile, "utf8"));
}
const definitions = definitionIndex.default.map((d) => zhc.prepareDefinition(d));
const whiteLabels = definitions.filter((d) => d.whiteLabel).flatMap((d) => d.whiteLabel);
const capitalizeFirstChar = (str) => str.charAt(0).toUpperCase() + str.slice(1);
for (const changelog of changelogs) {
if (changelog.project === "Nerivec/zigbee2mqtt-windfront") {
const releaseRsp = await fetch("https://api.github.com/repos/Nerivec/zigbee2mqtt-windfront/releases");
const releases = await releaseRsp.json();
for (const release of releases) {
if (release.name === `v${windfrontTillVersion}`) {
break;
}
let match = windfrontChangeRe.exec(release.body);
while (match !== null) {
const [, type, message, user, prLink, prId] = match;
const entry = `- ${prId && prLink ? `[#${prId}](${prLink}) ` : ""}${type}: ${message} ${user ? `(@${user.split("[")[0]})` : ""}`;
changes.windfront.push(entry);
match = windfrontChangeRe.exec(release.body);
}
}
} else {
for (const line of changelog.contents) {
const releaseMatch = line.match(releaseRe);
const changeMatch = changelog.isFrontend ? line.match(frontendChangeRe) : changeRe.map((re) => line.match(re)).find((e) => e);
if (releaseMatch) {
if (releaseMatch[1] === changelog.tillVersion) {
break;
}
} else if (line === "### Features") {
context = "features";
} else if (line === "### Bug Fixes") {
context = "fixes";
} else if (line.startsWith("* **ignore:**")) {
// continue;
} else if (changeMatch) {
let localContext = changelog.isFrontend ? "frontend" : changeMatch[2] ? changeMatch[2] : context;
if (!changes[localContext]) localContext = "error";
const commit = changeMatch[5];
const commitUserKey = `${changelog.project}-${commit} `;
let user =
commitUserKey in commitUserLookup
? commitUserLookup[commitUserKey]
: execSync(`curl -s https://api.github.com/repos/${changelog.project}/commits/${commit} | jq -r '.author.login'`)
.toString()
.trim();
if (user !== "null") commitUserLookup[commitUserKey] = user;
const messages = [];
let message = changeMatch[3].trim();
if (message.endsWith(".")) message = message.substring(0, message.length - 1);
if (changelog.isFrontend) {
changes[localContext].push(
`- [${commit.slice(0, 7)}](https://github.com/${changelog.project}/commit/${commit}) ${message} (@${user})`,
);
messages.push(capitalizeFirstChar(message));
} else {
const otherUser = message.match(/\[@(.+)\]\(https:\/\/github.com\/.+\)/) || message.match(/@(.+)/);
if (otherUser) {
user = otherUser[1];
message = message.replace(otherUser[0], "");
}
if (localContext === "add") {
for (const model of message.split(",")) {
const definition = definitions.find((d) => d.model === model.trim());
const whiteLabel = whiteLabels.find((d) => d.model === model.trim());
const match = definition || whiteLabel;
if (match) {
messages.push(`\`${match.model}\` ${match.vendor} ${match.description}`);
} else {
changes.error.push(`${line} (model '${model}' does not exist)`);
}
}
} else {
messages.push(capitalizeFirstChar(message));
}
let issue = changeMatch[4].trim();
if (issue && !issue.startsWith("[#")) issue = `[#${issue.split("/").pop()}](${issue})`;
if (!issue) {
issue = "_NO_ISSUE_";
localContext = "error";
}
for (const message of messages) {
changes[localContext].push(`- ${issue} ${message} (@${user})`);
}
}
} else if (line === "# Changelog" || line === "### ⚠ BREAKING CHANGES" || !line) {
// continue;
} else {
changes.error.push(`- Unmatched line: ${line}`);
}
}
}
}
let result = "";
const names = [
["features", "Improvements"],
["fixes", "Fixes"],
["windfront", "Windfront"],
["frontend", "Frontend"],
["add", "New supported devices"],
["detect", "Fixed device detections"],
["error", "Changelog generator error"],
];
for (const name of names) {
result += `# ${name[1]}\n`;
if (name[0] === "add") {
result += `This release adds support for ${changes.add.length} devices: \n`;
}
for (const change of changes[name[0]]) {
result += `${change}\n`;
}
result += "\n";
}
fs.writeFileSync(commitUserFile, JSON.stringify(commitUserLookup), "utf-8");
console.log(result.trim());
+14
View File
@@ -0,0 +1,14 @@
import {hrtime} from "node:process";
import type {bench} from "vitest";
export const BENCH_OPTIONS: NonNullable<Parameters<typeof bench>[2]> = {
throws: true,
warmupTime: 1000,
now: () => Number(hrtime.bigint()) / 1e6,
setup: (_task, mode) => {
// Run the garbage collector before warmup at each cycle
if (mode === "warmup" && typeof globalThis.gc === "function") {
globalThis.gc();
}
},
};
+69 -82
View File
@@ -1,6 +1,6 @@
import {existsSync, mkdirSync} from "node:fs";
import stringify from "json-stable-stringify-without-jsonify";
import {afterAll, afterEach, beforeAll, beforeEach, bench, describe} from "vitest";
import {bench, describe} from "vitest";
import {Zcl, Zdo, ZSpec} from "zigbee-herdsman";
import type Adapter from "zigbee-herdsman/dist/adapter/adapter";
import type {ZclPayload} from "zigbee-herdsman/dist/adapter/events";
@@ -12,6 +12,7 @@ import type {DeviceType} from "zigbee-herdsman/dist/controller/tstype";
import {Foundation} from "zigbee-herdsman/dist/zspec/zcl/definition/foundation";
import type {RequestToResponseMap} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes";
import data from "../lib/util/data";
import {BENCH_OPTIONS} from "./benchOptions";
process.env.ZIGBEE2MQTT_DATA = "data-bench";
data._testReload();
@@ -433,69 +434,9 @@ const initController = async () => {
};
describe("Controller with dummy zigbee/mqtt", () => {
describe("defaults start & stop", () => {
beforeEach(async () => {
initDevices();
initGroups();
await initSettings();
await initController();
});
afterEach(() => {
unmockGlobalThis();
});
bench(
"[defaults] start & stop controller",
async () => {
const mockedGlobal = mockGlobalThis();
await controller.start();
await settle(mockedGlobal);
if ((await controller.zigbee.getCoordinatorVersion()).type !== "Dummy") {
throw new Error("Invalid");
}
await controller.stop();
},
{throws: true},
);
});
describe("HA start & stop", () => {
beforeEach(async () => {
initDevices();
initGroups();
await initSettings([[["homeassistant", "enabled"], true]]);
await initController();
});
afterEach(() => {
unmockGlobalThis();
});
bench(
"[HA] start & stop controller",
async () => {
const mockedGlobal = mockGlobalThis();
await controller.start();
controller.mqtt.onMessage("homeassistant/status", Buffer.from("online", "utf8"));
await settle(mockedGlobal);
if ((await controller.zigbee.getCoordinatorVersion()).type !== "Dummy") {
throw new Error("Invalid");
}
await controller.stop();
},
{throws: true},
);
});
describe("defaults runtime", () => {
beforeAll(async () => {
bench(
"[defaults] start & stop controller",
async () => {
initDevices();
initGroups();
await initSettings();
@@ -504,12 +445,56 @@ describe("Controller with dummy zigbee/mqtt", () => {
await controller.start();
await settle(mockedGlobal);
});
afterAll(async () => {
if ((await controller.zigbee.getCoordinatorVersion()).type !== "Dummy") {
throw new Error("Invalid");
}
await controller.stop();
unmockGlobalThis();
});
},
BENCH_OPTIONS,
);
bench(
"[HA] start & stop controller",
async () => {
initDevices();
initGroups();
await initSettings([[["homeassistant", "enabled"], true]]);
await initController();
const mockedGlobal = mockGlobalThis();
await controller.start();
controller.mqtt.onMessage("homeassistant/status", Buffer.from("online", "utf8"));
await settle(mockedGlobal);
if ((await controller.zigbee.getCoordinatorVersion()).type !== "Dummy") {
throw new Error("Invalid");
}
await controller.stop();
unmockGlobalThis();
},
BENCH_OPTIONS,
);
describe("defaults runtime", () => {
const setup: NonNullable<Parameters<typeof bench>[2]>["setup"] = async (task, mode) => {
BENCH_OPTIONS.setup!(task, mode);
initDevices();
initGroups();
await initSettings();
await initController();
const mockedGlobal = mockGlobalThis();
await controller.start();
await settle(mockedGlobal);
};
const teardown = async () => {
await controller.stop();
unmockGlobalThis();
};
bench(
"[defaults] receive device message",
@@ -528,7 +513,7 @@ describe("Controller with dummy zigbee/mqtt", () => {
});
await settle(mockedGlobal);
},
{throws: true},
{...BENCH_OPTIONS, setup, teardown},
);
bench(
@@ -539,7 +524,7 @@ describe("Controller with dummy zigbee/mqtt", () => {
controller.mqtt.onMessage("zigbee2mqtt/0xf1f1f1f1f1f1f1f1/set", Buffer.from(`{"state": "OFF"}`, "utf8"));
await settle(mockedGlobal);
},
{throws: true},
{...BENCH_OPTIONS, setup, teardown},
);
bench(
@@ -562,12 +547,13 @@ describe("Controller with dummy zigbee/mqtt", () => {
throw new Error("Invalid state");
}
},
{throws: true},
{...BENCH_OPTIONS, setup, teardown},
);
});
describe("defaults/stress runtime", () => {
beforeAll(async () => {
const setup: NonNullable<Parameters<typeof bench>[2]>["setup"] = async (task, mode) => {
BENCH_OPTIONS.setup!(task, mode);
initDevices();
initGroups();
addManyDevices();
@@ -577,12 +563,12 @@ describe("Controller with dummy zigbee/mqtt", () => {
await controller.start();
await settle(mockedGlobal);
}, 60000);
};
afterAll(async () => {
const teardown = async () => {
await controller.stop();
unmockGlobalThis();
});
};
// this is mostly just to confirm the number of devices does not influence the processing (much)
bench(
@@ -602,12 +588,13 @@ describe("Controller with dummy zigbee/mqtt", () => {
});
await settle(mockedGlobal);
},
{throws: true},
{...BENCH_OPTIONS, setup, teardown},
);
});
describe("HA runtime", () => {
beforeAll(async () => {
const setup: NonNullable<Parameters<typeof bench>[2]>["setup"] = async (task, mode) => {
BENCH_OPTIONS.setup!(task, mode);
initDevices();
initGroups();
await initSettings([[["homeassistant", "enabled"], true]]);
@@ -617,12 +604,12 @@ describe("Controller with dummy zigbee/mqtt", () => {
await controller.start();
controller.mqtt.onMessage("homeassistant/status", Buffer.from("online", "utf8"));
await settle(mockedGlobal);
});
};
afterAll(async () => {
const teardown = async () => {
await controller.stop();
unmockGlobalThis();
});
};
bench(
"[HA] receive device message",
@@ -641,7 +628,7 @@ describe("Controller with dummy zigbee/mqtt", () => {
});
await settle(mockedGlobal);
},
{throws: true},
{...BENCH_OPTIONS, setup, teardown},
);
bench(
@@ -652,7 +639,7 @@ describe("Controller with dummy zigbee/mqtt", () => {
controller.mqtt.onMessage("zigbee2mqtt/0xf1f1f1f1f1f1f1f1/set", Buffer.from(`{"state": "OFF"}`, "utf8"));
await settle(mockedGlobal);
},
{throws: true},
{...BENCH_OPTIONS, setup, teardown},
);
bench(
@@ -666,7 +653,7 @@ describe("Controller with dummy zigbee/mqtt", () => {
);
await settle(mockedGlobal);
},
{throws: true},
{...BENCH_OPTIONS, setup, teardown},
);
});
});
+3 -2
View File
@@ -285,7 +285,7 @@ describe("Extension: Bridge", () => {
},
frontend: {
enabled: false,
package: "zigbee2mqtt-frontend",
package: "zigbee2mqtt-windfront",
port: 8080,
base_url: "/",
},
@@ -2971,11 +2971,13 @@ describe("Extension: Bridge", () => {
it("Should allow to remove group", async () => {
const group = groups.group_1;
const removeGroupFromLookup = vi.spyOn(controller.zigbee, "removeGroupFromLookup");
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/remove", "group_1");
await flushPromises();
expect(group.removeFromNetwork).toHaveBeenCalledTimes(1);
expect(settings.getGroup("group_1")).toBeUndefined();
expect(removeGroupFromLookup).toHaveBeenCalledWith(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/remove",
@@ -3298,7 +3300,6 @@ describe("Extension: Bridge", () => {
" vendor: '',\n" +
" description: 'Automatically generated definition',\n" +
' extend: [m.onOff({"powerOnBehavior":false})],\n' +
" meta: {},\n" +
"};\n",
},
status: "ok",
+2 -2
View File
@@ -182,11 +182,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: "0x0017882104a44559", 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_thermostat' cannot be configured", transaction: 20}),
stringify({data: {}, status: "error", error: "Device '0x0017980134e45545' cannot be configured", transaction: 20}),
{},
);
});
+4 -2
View File
@@ -81,10 +81,12 @@ describe("Extension: ExternalConverters", () => {
});
afterEach(async () => {
fs.rmSync(mockBasePath, {recursive: true, force: true});
await controller?.stop();
await flushPromises();
expect(fs.existsSync(path.join(mockBasePath, "node_modules"))).toStrictEqual(false);
fs.rmSync(mockBasePath, {recursive: true, force: true});
});
describe("from folder", () => {
+4 -2
View File
@@ -68,10 +68,12 @@ describe("Extension: ExternalExtensions", () => {
});
afterEach(async () => {
fs.rmSync(mockBasePath, {recursive: true, force: true});
await controller?.stop();
await flushPromises();
expect(fs.existsSync(path.join(mockBasePath, "node_modules"))).toStrictEqual(false);
fs.rmSync(mockBasePath, {recursive: true, force: true});
});
describe("from folder", () => {
+1 -1
View File
@@ -86,7 +86,7 @@ vi.mock("express-static-gzip", () => ({
}),
}));
vi.mock("zigbee2mqtt-frontend", () => ({
vi.mock("zigbee2mqtt-windfront", () => ({
default: {
getPath: (): string => frontendPath,
},
+137 -5
View File
@@ -166,6 +166,7 @@ describe("Extension: HomeAssistant", () => {
"stop_hue_effect",
],
object_id: "ha_discovery_group",
default_entity_id: "light.ha_discovery_group",
unique_id: "9_light_zigbee2mqtt",
origin: origin,
};
@@ -190,6 +191,7 @@ describe("Extension: HomeAssistant", () => {
},
name: null,
object_id: "bulb_enddevice",
default_entity_id: "light.bulb_enddevice",
origin: origin,
schema: "json",
state_topic: "zigbee2mqtt/bulb_enddevice",
@@ -218,6 +220,7 @@ describe("Extension: HomeAssistant", () => {
payload_on: "ON",
state_topic: "zigbee2mqtt/ha_discovery_group",
object_id: "ha_discovery_group",
default_entity_id: "switch.ha_discovery_group",
unique_id: "9_switch_zigbee2mqtt",
origin: origin,
value_template: "{{ value_json.state }}",
@@ -236,6 +239,7 @@ describe("Extension: HomeAssistant", () => {
value_template: "{{ value_json.temperature }}",
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
unique_id: "0x0017880104e45522_temperature_zigbee2mqtt",
origin: origin,
device: {
@@ -262,6 +266,7 @@ describe("Extension: HomeAssistant", () => {
value_template: "{{ value_json.humidity }}",
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_humidity",
default_entity_id: "sensor.weather_sensor_humidity",
unique_id: "0x0017880104e45522_humidity_zigbee2mqtt",
origin: origin,
enabled_by_default: true,
@@ -288,6 +293,7 @@ describe("Extension: HomeAssistant", () => {
value_template: "{{ value_json.pressure }}",
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_pressure",
default_entity_id: "sensor.weather_sensor_pressure",
unique_id: "0x0017880104e45522_pressure_zigbee2mqtt",
origin: origin,
enabled_by_default: true,
@@ -314,6 +320,7 @@ describe("Extension: HomeAssistant", () => {
value_template: "{{ value_json.battery }}",
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_battery",
default_entity_id: "sensor.weather_sensor_battery",
unique_id: "0x0017880104e45522_battery_zigbee2mqtt",
origin: origin,
enabled_by_default: true,
@@ -344,6 +351,7 @@ describe("Extension: HomeAssistant", () => {
state_topic: "zigbee2mqtt/weather_sensor",
name: "Linkquality",
object_id: "weather_sensor_linkquality",
default_entity_id: "sensor.weather_sensor_linkquality",
unique_id: "0x0017880104e45522_linkquality_zigbee2mqtt",
origin: origin,
device: {
@@ -378,6 +386,7 @@ describe("Extension: HomeAssistant", () => {
payload_on: "ON",
state_topic: "zigbee2mqtt/wall_switch_double",
object_id: "wall_switch_double_left",
default_entity_id: "switch.wall_switch_double_left",
unique_id: "0x0017880104e45542_switch_left_zigbee2mqtt",
origin: origin,
value_template: "{{ value_json.state_left }}",
@@ -404,6 +413,7 @@ describe("Extension: HomeAssistant", () => {
payload_on: "ON",
state_topic: "zigbee2mqtt/wall_switch_double",
object_id: "wall_switch_double_right",
default_entity_id: "switch.wall_switch_double_right",
unique_id: "0x0017880104e45542_switch_right_zigbee2mqtt",
origin: origin,
value_template: "{{ value_json.state_right }}",
@@ -436,6 +446,7 @@ describe("Extension: HomeAssistant", () => {
schema: "json",
state_topic: "zigbee2mqtt/bulb",
object_id: "bulb",
default_entity_id: "light.bulb",
unique_id: "0x000b57fffec6a5b2_light_zigbee2mqtt",
origin: origin,
};
@@ -459,6 +470,7 @@ describe("Extension: HomeAssistant", () => {
icon: "mdi:gesture-double-tap",
name: "Action",
object_id: "button_action",
default_entity_id: "event.button_action",
origin,
state_topic: "zigbee2mqtt/button",
unique_id: "0x0017880104e45520_action_zigbee2mqtt",
@@ -510,6 +522,7 @@ describe("Extension: HomeAssistant", () => {
value_template: "{{ value_json.humidity }}",
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_humidity",
default_entity_id: "sensor.weather_sensor_humidity",
unique_id: "0x0017880104e45522_humidity_zigbee2mqtt",
origin: origin,
enabled_by_default: true,
@@ -581,6 +594,7 @@ describe("Extension: HomeAssistant", () => {
value_template: "{{ value_json.temperature }}",
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
unique_id: "0x0017880104e45522_temperature_zigbee2mqtt",
origin: origin,
device: {
@@ -606,6 +620,7 @@ describe("Extension: HomeAssistant", () => {
value_template: "{{ value_json.humidity }}",
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_humidity",
default_entity_id: "sensor.weather_sensor_humidity",
unique_id: "0x0017880104e45522_humidity_zigbee2mqtt",
origin: origin,
enabled_by_default: true,
@@ -633,6 +648,7 @@ describe("Extension: HomeAssistant", () => {
state_topic: "zigbee2mqtt/weather_sensor",
enabled_by_default: true,
object_id: "weather_sensor_pressure",
default_entity_id: "sensor.weather_sensor_pressure",
unique_id: "0x0017880104e45522_pressure_zigbee2mqtt",
origin: origin,
device: {
@@ -690,6 +706,7 @@ describe("Extension: HomeAssistant", () => {
state_topic: "zigbee2mqtt/weather_sensor",
enabled_by_default: true,
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
unique_id: "0x0017880104e45522_temperature_zigbee2mqtt",
origin: origin,
device: {
@@ -731,6 +748,7 @@ describe("Extension: HomeAssistant", () => {
expire_after: 30,
icon: "mdi:test",
object_id: "weather_sensor_humidity",
default_entity_id: "sensor.weather_sensor_humidity",
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/sensor/0x0017880104e45522/humidity/config", stringify(payload), {
@@ -760,6 +778,7 @@ describe("Extension: HomeAssistant", () => {
value_template: "{{ value_json.temperature }}",
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
unique_id: "0x0017880104e45522_temperature_zigbee2mqtt",
origin: origin,
device: {
@@ -786,6 +805,7 @@ describe("Extension: HomeAssistant", () => {
value_template: "{{ value_json.humidity }}",
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_humidity",
default_entity_id: "sensor.weather_sensor_humidity",
unique_id: "0x0017880104e45522_humidity_zigbee2mqtt",
origin: origin,
enabled_by_default: true,
@@ -813,6 +833,7 @@ describe("Extension: HomeAssistant", () => {
switch: {
type: "light",
object_id: "light",
default_entity_id: "light.light",
},
light: {
type: "this should be ignored",
@@ -841,6 +862,7 @@ describe("Extension: HomeAssistant", () => {
payload_on: "ON",
state_topic: "zigbee2mqtt/my_switch",
object_id: "my_switch",
default_entity_id: "light.my_switch",
unique_id: "0x0017880104e45541_light_zigbee2mqtt",
origin: origin,
value_template: "{{ value_json.state }}",
@@ -899,6 +921,7 @@ describe("Extension: HomeAssistant", () => {
speed_range_max: 4,
name: null,
object_id: "fan",
default_entity_id: "fan.fan",
unique_id: "0x0017880104e45548_fan_zigbee2mqtt",
origin: origin,
device: {
@@ -931,6 +954,7 @@ describe("Extension: HomeAssistant", () => {
speed_range_max: 254,
name: null,
object_id: "fanbee",
default_entity_id: "fan.fanbee",
unique_id: "0x00124b00cfcf3298_fan_zigbee2mqtt",
origin: origin,
device: {
@@ -992,6 +1016,7 @@ describe("Extension: HomeAssistant", () => {
temperature_state_topic: "zigbee2mqtt/TS0601_thermostat",
temperature_unit: "C",
object_id: "ts0601_thermostat",
default_entity_id: "climate.ts0601_thermostat",
unique_id: "0x0017882104a44559_climate_zigbee2mqtt",
origin: origin,
};
@@ -1002,6 +1027,77 @@ describe("Extension: HomeAssistant", () => {
});
});
it("Should discover thermostat devices with read-only PI heating demand", () => {
const payload = {
availability: [
{
topic: "zigbee2mqtt/bridge/state",
value_template: "{{ value_json.state }}",
},
],
default_entity_id: "sensor.thermostat_pi_heating_demand",
device: {
identifiers: ["zigbee2mqtt_0x0017880104e45550"],
manufacturer: "eCozy",
model: "Smart heating thermostat",
model_id: "1TST-EU",
name: "thermostat",
via_device: "zigbee2mqtt_bridge_0x00124b00120144ae",
},
entity_category: "diagnostic",
icon: "mdi:radiator",
name: "PI heating demand",
object_id: "thermostat_pi_heating_demand",
origin: origin,
state_topic: "zigbee2mqtt/thermostat",
unique_id: "0x0017880104e45550_pi_heating_demand_zigbee2mqtt",
unit_of_measurement: "%",
value_template: "{{ value_json.pi_heating_demand }}",
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/sensor/0x0017880104e45550/pi_heating_demand/config", stringify(payload), {
retain: true,
qos: 1,
});
});
it("Should discover thermostat devices with writable PI heating demand", () => {
const payload = {
availability: [
{
topic: "zigbee2mqtt/bridge/state",
value_template: "{{ value_json.state }}",
},
],
command_topic: "zigbee2mqtt/bosch_radiator/set/pi_heating_demand",
default_entity_id: "number.bosch_radiator_pi_heating_demand",
device: {
identifiers: ["zigbee2mqtt_0x18fc2600000d7ae2"],
manufacturer: "Bosch",
model: "Radiator thermostat II",
model_id: "BTH-RA",
name: "bosch_radiator",
sw_version: "3.05.09",
via_device: "zigbee2mqtt_bridge_0x00124b00120144ae",
},
icon: "mdi:radiator",
max: 100,
min: 0,
name: "PI heating demand",
object_id: "bosch_radiator_pi_heating_demand",
origin: origin,
state_topic: "zigbee2mqtt/bosch_radiator",
unique_id: "0x18fc2600000d7ae2_pi_heating_demand_zigbee2mqtt",
unit_of_measurement: "%",
value_template: "{{ value_json.pi_heating_demand }}",
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/number/0x18fc2600000d7ae2/pi_heating_demand/config", stringify(payload), {
retain: true,
qos: 1,
});
});
it("Should discover Bosch BTH-RA with a compatibility mapping", () => {
const payload = {
action_template:
@@ -1021,14 +1117,15 @@ describe("Extension: HomeAssistant", () => {
},
max_temp: "30",
min_temp: "5",
mode_command_template: `{% set values = { 'auto':'schedule','heat':'manual','off':'pause'} %}{"operating_mode": "{{ values[value] if value in values.keys() else 'pause' }}"}`,
mode_command_template: `{% set values = { 'auto':'schedule', 'heat':'manual', 'off':'pause' } %}{"operating_mode": "{{ values[value] if value in values.keys() else 'pause' }}"}`,
mode_command_topic: "zigbee2mqtt/bosch_radiator/set",
mode_state_template:
"{% set values = {'schedule':'auto','manual':'heat','pause':'off'} %}{% set value = value_json.operating_mode %}{{ values[value] if value in values.keys() else 'off' }}",
"{% set values = { 'schedule':'auto', 'manual':'heat', 'pause':'off' } %}{% set value = value_json.operating_mode %}{{ values[value] if value in values.keys() else 'off' }}",
mode_state_topic: "zigbee2mqtt/bosch_radiator",
modes: ["off", "heat", "auto"],
name: null,
object_id: "bosch_radiator",
default_entity_id: "climate.bosch_radiator",
origin: origin,
temp_step: 0.5,
temperature_command_topic: "zigbee2mqtt/bosch_radiator/set/occupied_heating_setpoint",
@@ -1081,6 +1178,7 @@ describe("Extension: HomeAssistant", () => {
modes: ["heat"],
name: null,
object_id: "bosch_radiator",
default_entity_id: "climate.bosch_radiator",
origin: origin,
temp_step: 0.5,
temperature_command_topic: "zigbee2mqtt/bosch_radiator/set/occupied_heating_setpoint",
@@ -1115,6 +1213,7 @@ describe("Extension: HomeAssistant", () => {
state_stopped: "STOP",
name: null,
object_id: "smart_vent",
default_entity_id: "cover.smart_vent",
unique_id: "0x0017880104e45551_cover_zigbee2mqtt",
origin: origin,
device: {
@@ -1157,6 +1256,7 @@ describe("Extension: HomeAssistant", () => {
tilt_status_template: "{{ value_json.tilt }}",
tilt_status_topic: "zigbee2mqtt/zigfred_plus/l6",
object_id: "zigfred_plus_l6",
default_entity_id: "cover.zigfred_plus_l6",
unique_id: "0xf4ce368a38be56a1_cover_l6_zigbee2mqtt",
origin: origin,
value_template: "{{ value_json.state }}",
@@ -1187,6 +1287,7 @@ describe("Extension: HomeAssistant", () => {
},
name: "Left",
object_id: "0xa4c138018cf95021_left",
default_entity_id: "cover.0xa4c138018cf95021_left",
origin: origin,
position_template: "{{ value_json.position }}",
position_topic: "zigbee2mqtt/0xa4c138018cf95021/left",
@@ -1217,6 +1318,7 @@ describe("Extension: HomeAssistant", () => {
},
name: "Right",
object_id: "0xa4c138018cf95021_right",
default_entity_id: "cover.0xa4c138018cf95021_right",
origin: origin,
position_template: "{{ value_json.position }}",
position_topic: "zigbee2mqtt/0xa4c138018cf95021/right",
@@ -1254,6 +1356,7 @@ describe("Extension: HomeAssistant", () => {
state_topic: "zigbee2mqtt/weather_sensor",
enabled_by_default: true,
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
unique_id: "0x0017880104e45522_temperature_zigbee2mqtt",
origin: origin,
device: {
@@ -1432,6 +1535,7 @@ describe("Extension: HomeAssistant", () => {
value_template: "{{ value_json.temperature }}",
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
unique_id: "0x0017880104e45522_temperature_zigbee2mqtt",
origin: origin,
device: {
@@ -1592,6 +1696,7 @@ describe("Extension: HomeAssistant", () => {
value_template: "{{ value_json.temperature }}",
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
unique_id: "0x0017880104e45522_temperature_zigbee2mqtt",
origin: origin,
device: {
@@ -1661,6 +1766,7 @@ describe("Extension: HomeAssistant", () => {
value_template: "{{ value_json.temperature }}",
state_topic: "zigbee2mqtt/weather_sensor_renamed",
object_id: "weather_sensor_renamed_temperature",
default_entity_id: "sensor.weather_sensor_renamed_temperature",
origin: origin,
unique_id: "0x0017880104e45522_temperature_zigbee2mqtt",
device: {
@@ -1746,6 +1852,7 @@ describe("Extension: HomeAssistant", () => {
"stop_hue_effect",
],
object_id: "ha_discovery_group_new",
default_entity_id: "light.ha_discovery_group_new",
unique_id: "9_light_zigbee2mqtt",
origin: origin,
};
@@ -1782,6 +1889,7 @@ describe("Extension: HomeAssistant", () => {
value_template: "{{ value_json.temperature }}",
state_topic: "zigbee2mqtt/weather_sensor_renamed",
object_id: "weather_sensor_renamed_temperature",
default_entity_id: "sensor.weather_sensor_renamed_temperature",
unique_id: "0x0017880104e45522_temperature_zigbee2mqtt",
origin: origin,
device: {
@@ -1818,6 +1926,7 @@ describe("Extension: HomeAssistant", () => {
entity_picture: "https://github.com/Koenkk/zigbee2mqtt/raw/master/images/logo.png",
name: null,
object_id: "bulb",
default_entity_id: "update.bulb",
origin,
payload_install: `{"id": "0x000b57fffec6a5b2"}`,
state_topic: "zigbee2mqtt/bulb",
@@ -1981,6 +2090,7 @@ describe("Extension: HomeAssistant", () => {
icon: "mdi:gesture-double-tap",
name: "Action",
object_id: "button_action",
default_entity_id: "event.button_action",
origin: origin,
state_topic: "zigbee2mqtt/button",
unique_id: "0x0017880104e45520_action_zigbee2mqtt",
@@ -2211,6 +2321,7 @@ describe("Extension: HomeAssistant", () => {
"stop_hue_effect",
],
object_id: "ha_discovery_group",
default_entity_id: "light.ha_discovery_group",
unique_id: "9_light_zigbee2mqtt",
origin: origin,
};
@@ -2255,6 +2366,7 @@ describe("Extension: HomeAssistant", () => {
"stop_hue_effect",
],
object_id: "ha_discovery_group",
default_entity_id: "light.ha_discovery_group",
unique_id: "9_light_zigbee2mqtt",
origin: origin,
};
@@ -2297,6 +2409,7 @@ describe("Extension: HomeAssistant", () => {
state_topic: "zigbee2mqtt/bulb",
supported_color_modes: ["color_temp"],
object_id: "bulb",
default_entity_id: "light.bulb",
unique_id: "0x000b57fffec6a5b2_light_zigbee2mqtt",
origin: origin,
};
@@ -2331,6 +2444,7 @@ describe("Extension: HomeAssistant", () => {
name: "Last seen",
state_topic: "zigbee2mqtt/bulb",
object_id: "bulb_last_seen",
default_entity_id: "sensor.bulb_last_seen",
unique_id: "0x000b57fffec6a5b2_last_seen_zigbee2mqtt",
origin: origin,
value_template: "{{ value_json.last_seen }}",
@@ -2344,7 +2458,12 @@ describe("Extension: HomeAssistant", () => {
});
});
it("Should discover devices with configuration url", async () => {
it.each([
// Windfront includes the instance ID in the URL.
["zigbee2mqtt-windfront", "http://zigbee.mqtt/#/device/0/0x0017880104e45522/info"],
["zigbee2mqtt-frontend", "http://zigbee.mqtt/#/device/0x0017880104e45522/info"],
])("Should discover devices with configuration url (%s)", async (packageName: string, expectedUrl: string) => {
settings.set(["frontend", "package"], packageName);
settings.set(["frontend", "url"], "http://zigbee.mqtt");
await resetExtension();
@@ -2358,6 +2477,7 @@ describe("Extension: HomeAssistant", () => {
value_template: "{{ value_json.temperature }}",
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
unique_id: "0x0017880104e45522_temperature_zigbee2mqtt",
origin: origin,
device: {
@@ -2366,7 +2486,7 @@ describe("Extension: HomeAssistant", () => {
model: "Temperature and humidity sensor",
model_id: "WSDCGQ11LM",
manufacturer: "Aqara",
configuration_url: "http://zigbee.mqtt/#/device/0x0017880104e45522/info",
configuration_url: expectedUrl,
via_device: "zigbee2mqtt_bridge_0x00124b00120144ae",
},
availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}],
@@ -2398,6 +2518,7 @@ describe("Extension: HomeAssistant", () => {
command_topic: "zigbee2mqtt/bulb_color_2/set",
payload_on: '{ "scene_recall": 1 }',
object_id: "bulb_color_2_1_chill_scene",
default_entity_id: "scene.bulb_color_2_1_chill_scene",
unique_id: "0x000b57fffec6a5b4_scene_1_zigbee2mqtt",
device: {
identifiers: ["zigbee2mqtt_0x000b57fffec6a5b4"],
@@ -2437,6 +2558,7 @@ describe("Extension: HomeAssistant", () => {
command_topic: "zigbee2mqtt/ha_discovery_group/set",
payload_on: '{ "scene_recall": 4 }',
object_id: "ha_discovery_group_4_scene_4",
default_entity_id: "scene.ha_discovery_group_4_scene_4",
unique_id: "9_scene_4_zigbee2mqtt",
device: {
identifiers: ["zigbee2mqtt_1221051039810110150109113116116_9"],
@@ -2465,6 +2587,7 @@ describe("Extension: HomeAssistant", () => {
const payload = {
name: "Restart",
object_id: "zigbee2mqtt_bridge_restart",
default_entity_id: "light.zigbee2mqtt_bridge_restart",
unique_id: "bridge_0x00124b00120144ae_restart_zigbee2mqtt",
device_class: "restart",
command_topic: "zigbee2mqtt/bridge/request/restart",
@@ -2506,6 +2629,7 @@ describe("Extension: HomeAssistant", () => {
payload = {
name: "Connection state",
object_id: "zigbee2mqtt_bridge_connection_state",
default_entity_id: "binary_sensor.zigbee2mqtt_bridge_connection_state",
entity_category: "diagnostic",
device_class: "connectivity",
unique_id: "bridge_0x00124b00120144ae_connection_state_zigbee2mqtt",
@@ -2525,6 +2649,7 @@ describe("Extension: HomeAssistant", () => {
payload = {
name: "Restart required",
object_id: "zigbee2mqtt_bridge_restart_required",
default_entity_id: "binary_sensor.zigbee2mqtt_bridge_restart_required",
entity_category: "diagnostic",
device_class: "problem",
enabled_by_default: false,
@@ -2548,6 +2673,7 @@ describe("Extension: HomeAssistant", () => {
payload = {
name: "Restart",
object_id: "zigbee2mqtt_bridge_restart",
default_entity_id: "button.zigbee2mqtt_bridge_restart",
unique_id: "bridge_0x00124b00120144ae_restart_zigbee2mqtt",
device_class: "restart",
command_topic: "zigbee2mqtt/bridge/request/restart",
@@ -2567,6 +2693,7 @@ describe("Extension: HomeAssistant", () => {
payload = {
name: "Log level",
object_id: "zigbee2mqtt_bridge_log_level",
default_entity_id: "select.zigbee2mqtt_bridge_log_level",
entity_category: "config",
unique_id: "bridge_0x00124b00120144ae_log_level_zigbee2mqtt",
state_topic: "zigbee2mqtt/bridge/info",
@@ -2589,6 +2716,7 @@ describe("Extension: HomeAssistant", () => {
payload = {
name: "Version",
object_id: "zigbee2mqtt_bridge_version",
default_entity_id: "sensor.zigbee2mqtt_bridge_version",
entity_category: "diagnostic",
icon: "mdi:zigbee",
unique_id: "bridge_0x00124b00120144ae_version_zigbee2mqtt",
@@ -2608,6 +2736,7 @@ describe("Extension: HomeAssistant", () => {
payload = {
name: "Coordinator version",
object_id: "zigbee2mqtt_bridge_coordinator_version",
default_entity_id: "sensor.zigbee2mqtt_bridge_coordinator_version",
entity_category: "diagnostic",
enabled_by_default: false,
icon: "mdi:chip",
@@ -2628,6 +2757,7 @@ describe("Extension: HomeAssistant", () => {
payload = {
name: "Network map",
object_id: "zigbee2mqtt_bridge_network_map",
default_entity_id: "sensor.zigbee2mqtt_bridge_network_map",
entity_category: "diagnostic",
enabled_by_default: false,
unique_id: "bridge_0x00124b00120144ae_network_map_zigbee2mqtt",
@@ -2650,6 +2780,7 @@ describe("Extension: HomeAssistant", () => {
payload = {
name: "Permit join",
object_id: "zigbee2mqtt_bridge_permit_join",
default_entity_id: "switch.zigbee2mqtt_bridge_permit_join",
icon: "mdi:human-greeting-proximity",
unique_id: "bridge_0x00124b00120144ae_permit_join_zigbee2mqtt",
state_topic: "zigbee2mqtt/bridge/info",
@@ -2686,7 +2817,7 @@ describe("Extension: HomeAssistant", () => {
mockMQTTPublishAsync.mockClear();
const device = devices["BMCT-SLZ"];
const data = {deviceMode: 0};
const msg = {data, cluster: "boschSpecific", device, endpoint: device.getEndpoint(1), type: "attributeReport", linkquality: 10};
const msg = {data, cluster: "boschEnergyDevice", device, endpoint: device.getEndpoint(1), type: "attributeReport", linkquality: 10};
resetDiscoveryPayloads("0x18fc26000000cafe");
await mockZHEvents.message(msg);
await flushPromises();
@@ -2705,6 +2836,7 @@ describe("Extension: HomeAssistant", () => {
icon: "mdi:tune",
name: "Device mode",
object_id: "0x18fc26000000cafe_device_mode",
default_entity_id: "select.0x18fc26000000cafe_device_mode",
options: ["light", "shutter", "disabled"],
origin: origin,
state_topic: "zigbee2mqtt/0x18fc26000000cafe",
+89 -87
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";
@@ -9,7 +9,7 @@ import {devices, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHer
import type {MockInstance} from "vitest";
import * as zhc from "zigbee-herdsman-converters";
import type {OnEvent as DefinitionOnEvent} from "zigbee-herdsman-converters/lib/types";
import type {OnEvent as ZhcOnEvent} from "zigbee-herdsman-converters/lib/types";
import {Controller} from "../../lib/controller";
import OnEvent from "../../lib/extension/onEvent";
import type Device from "../../lib/model/device";
@@ -22,17 +22,12 @@ returnDevices.push(devices.bulb.ieeeAddr, devices.LIVOLO.ieeeAddr, devices.coord
describe("Extension: OnEvent", () => {
let controller: Controller;
let onEventSpy: MockInstance<typeof zhc.onEvent>;
let deviceOnEventSpy: MockInstance<DefinitionOnEvent>;
let deviceOnEventSpy: MockInstance<ZhcOnEvent.Handler>;
const getZ2MDevice = (zhDevice: string | number | ZhDevice): Device => {
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));
});
@@ -65,115 +61,121 @@ describe("Extension: OnEvent", () => {
it("starts & stops", async () => {
expect(onEventSpy).toHaveBeenCalledTimes(2);
expect(deviceOnEventSpy).toHaveBeenCalledTimes(1);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(
1,
"start",
{},
devices.LIVOLO,
settings.getDevice(devices.LIVOLO.ieeeAddr),
{},
{deviceExposesChanged: expect.any(Function)},
);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(1, {
type: "start",
data: {
device: devices.LIVOLO,
options: settings.getDevice(devices.LIVOLO.ieeeAddr),
state: {},
deviceExposesChanged: expect.any(Function),
},
});
await controller.stop();
expect(onEventSpy).toHaveBeenCalledTimes(4);
expect(deviceOnEventSpy).toHaveBeenCalledTimes(2);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(
2,
"stop",
{},
devices.LIVOLO,
settings.getDevice(devices.LIVOLO.ieeeAddr),
{},
{deviceExposesChanged: expect.any(Function)},
);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(2, {
type: "stop",
data: {
ieeeAddr: devices.LIVOLO.ieeeAddr,
},
});
});
it("calls on device events", async () => {
clearOnEventSpies();
await mockZHEvents.deviceAnnounce({device: devices.LIVOLO});
await flushPromises();
expect(deviceOnEventSpy).toHaveBeenCalledTimes(1);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(
1,
"deviceAnnounce",
{},
devices.LIVOLO,
settings.getDevice(devices.LIVOLO.ieeeAddr),
{},
{deviceExposesChanged: expect.any(Function)},
);
// Should always call with 'start' event first
expect(deviceOnEventSpy).toHaveBeenCalledTimes(2);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(1, {
type: "start",
data: {
device: devices.LIVOLO,
options: settings.getDevice(devices.LIVOLO.ieeeAddr),
state: {},
deviceExposesChanged: expect.any(Function),
},
});
// Device announce
expect(deviceOnEventSpy).toHaveBeenCalledTimes(2);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(2, {
type: "deviceAnnounce",
data: {
device: devices.LIVOLO,
options: settings.getDevice(devices.LIVOLO.ieeeAddr),
state: {},
deviceExposesChanged: expect.any(Function),
},
});
// Check deviceExposesChanged()
const emitExposesAndDevicesChangedSpy = vi.spyOn(
// @ts-expect-error protected
controller.getExtension("OnEvent")!.eventBus,
"emitExposesAndDevicesChanged",
);
deviceOnEventSpy.mock.calls[0][5]!.deviceExposesChanged();
assert(deviceOnEventSpy.mock.calls[0][0]!.type === "start");
deviceOnEventSpy.mock.calls[0][0]!.data.deviceExposesChanged();
expect(emitExposesAndDevicesChangedSpy).toHaveBeenCalledTimes(1);
expect(emitExposesAndDevicesChangedSpy).toHaveBeenCalledWith(getZ2MDevice(devices.LIVOLO));
// Call `stop` when device leaves
await mockZHEvents.deviceLeave({ieeeAddr: devices.LIVOLO.ieeeAddr});
await flushPromises();
expect(deviceOnEventSpy).toHaveBeenCalledTimes(2);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(
2,
"stop",
{},
devices.LIVOLO,
settings.getDevice(devices.LIVOLO.ieeeAddr),
{},
{deviceExposesChanged: expect.any(Function)},
);
});
it("calls on device message", async () => {
clearOnEventSpies();
await mockZHEvents.message({
type: "attributeReport",
device: devices.LIVOLO,
endpoint: devices.LIVOLO.endpoints[0],
linkquality: 213,
groupID: 0,
cluster: "genBasic",
data: {zclVersion: 8},
meta: {zclTransactionSequenceNumber: 1, manufacturerCode: devices.LIVOLO.manufacturerID},
});
await flushPromises();
expect(deviceOnEventSpy).toHaveBeenCalledTimes(1);
expect(deviceOnEventSpy).toHaveBeenCalledWith(
"message",
{
type: "attributeReport",
endpoint: devices.LIVOLO.endpoints[0],
cluster: "genBasic",
data: {zclVersion: 8},
meta: {zclTransactionSequenceNumber: 1, manufacturerCode: devices.LIVOLO.manufacturerID},
expect(deviceOnEventSpy).toHaveBeenCalledTimes(3);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(3, {
type: "stop",
data: {
ieeeAddr: devices.LIVOLO.ieeeAddr,
},
devices.LIVOLO,
settings.getDevice(devices.LIVOLO.ieeeAddr),
{},
{deviceExposesChanged: expect.any(Function)},
);
});
// Call `stop` when device is removed
// @ts-expect-error private
controller.eventBus.emitEntityRemoved({entity: getZ2MDevice(devices.LIVOLO)});
await flushPromises();
expect(deviceOnEventSpy).toHaveBeenCalledTimes(4);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(4, {
type: "stop",
data: {
ieeeAddr: devices.LIVOLO.ieeeAddr,
},
});
// Device interview, should call with 'start' first as 'stop' was called
await mockZHEvents.deviceInterview({device: devices.LIVOLO, status: "started"});
await flushPromises();
expect(deviceOnEventSpy).toHaveBeenCalledTimes(6);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(5, {
type: "start",
data: {
device: devices.LIVOLO,
options: settings.getDevice(devices.LIVOLO.ieeeAddr),
state: {},
deviceExposesChanged: expect.any(Function),
},
});
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(6, {
type: "deviceInterview",
data: {
device: devices.LIVOLO,
options: settings.getDevice(devices.LIVOLO.ieeeAddr),
state: {},
deviceExposesChanged: expect.any(Function),
status: "started",
},
});
});
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);
});
});
+8 -8
View File
@@ -239,7 +239,7 @@ describe("Extension: Publish", () => {
expect(endpoint.command).toHaveBeenCalledWith(
"manuSpecificTuya",
"dataRequest",
{dpValues: [{data: [1], datatype: 1, dp: 2}], seq: expect.any(Number)},
{dpValues: [{data: Buffer.from([1]), datatype: 1, dp: 2}], seq: expect.any(Number)},
{disableDefaultResponse: true},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/TS0601_switch", stringify({state_l2: "ON"}), {retain: false, qos: 0});
@@ -256,19 +256,19 @@ describe("Extension: Publish", () => {
expect(endpoint.command).toHaveBeenCalledWith(
"manuSpecificTuya",
"dataRequest",
{dpValues: [{data: [0], datatype: 4, dp: 1}], seq: expect.any(Number)},
{dpValues: [{data: Buffer.from([0]), datatype: 4, dp: 1}], seq: expect.any(Number)},
{disableDefaultResponse: true},
);
expect(endpoint.command).toHaveBeenCalledWith(
"manuSpecificTuya",
"dataRequest",
{dpValues: [{data: [1], datatype: 1, dp: 102}], seq: expect.any(Number)},
{dpValues: [{data: Buffer.from([1]), datatype: 1, dp: 102}], seq: expect.any(Number)},
{disableDefaultResponse: true},
);
expect(endpoint.command).toHaveBeenCalledWith(
"manuSpecificTuya",
"dataRequest",
{dpValues: [{data: [0], datatype: 1, dp: 101}], seq: expect.any(Number)},
{dpValues: [{data: Buffer.from([0]), datatype: 1, dp: 101}], seq: expect.any(Number)},
{disableDefaultResponse: true},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
@@ -425,7 +425,7 @@ describe("Extension: Publish", () => {
expect(group.command).toHaveBeenCalledWith(
"manuSpecificTuya",
"dataRequest",
{dpValues: [{data: [1], datatype: 1, dp: 7}], seq: expect.any(Number)},
{dpValues: [{data: Buffer.from([1]), datatype: 1, dp: 7}], seq: expect.any(Number)},
{disableDefaultResponse: true},
);
});
@@ -1074,7 +1074,7 @@ describe("Extension: Publish", () => {
expect(endpoint.command.mock.calls[0]).toEqual([
"lightingColorCtrl",
"enhancedMoveToHueAndSaturation",
{direction: 0, enhancehue: 45510, saturation: 127, transtime: 0},
{enhancehue: 45510, saturation: 127, transtime: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
@@ -1442,7 +1442,7 @@ 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}, {});
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});
@@ -1458,7 +1458,7 @@ describe("Extension: Publish", () => {
expect(endpoint.command).toHaveBeenCalledWith(
"manuSpecificTuya",
"dataRequest",
{dpValues: [{data: [1], datatype: 1, dp: 3}], seq: expect.any(Number)},
{dpValues: [{data: Buffer.from([1]), datatype: 1, dp: 3}], seq: expect.any(Number)},
{disableDefaultResponse: true},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
+1 -1
View File
@@ -731,7 +731,7 @@ describe("Extension: Receive", () => {
it("Should emit DevicesChanged event when a converter announces changed exposes", async () => {
const device = devices["BMCT-SLZ"];
const data = {deviceMode: 0};
const payload = {data, cluster: "boschSpecific", device, endpoint: device.getEndpoint(1), type: "attributeReport", linkquality: 10};
const payload = {data, cluster: "boschEnergyDevice", device, endpoint: device.getEndpoint(1), type: "attributeReport", linkquality: 10};
await mockZHEvents.message(payload);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bridge/devices");
});
+1
View File
@@ -1,3 +1,4 @@
import {vi} from "vitest";
import utils from "../../lib/util/utils";
const spy = vi.spyOn(utils, "sleep");
+1
View File
@@ -1,3 +1,4 @@
import {vi} from "vitest";
import type {DefinitionWithExtend} from "zigbee-herdsman-converters";
export type EventHandler = (...args: unknown[]) => unknown;
+1 -1
View File
@@ -1047,6 +1047,6 @@ describe("Settings", () => {
write(configurationFile, {frontend: {enabled: true}});
settings.reRead();
expect(settings.get().frontend).toStrictEqual({enabled: true, package: "zigbee2mqtt-frontend", port: 8080, base_url: "/"});
expect(settings.get().frontend).toStrictEqual({enabled: true, package: "zigbee2mqtt-windfront", port: 8080, base_url: "/"});
});
});
+1 -3
View File
@@ -1,10 +1,8 @@
import codspeedPlugin from "@codspeed/vitest-plugin";
import {defineConfig} from "vitest/config";
export default defineConfig({
plugins: [codspeedPlugin()],
plugins: [],
test: {
globals: true,
onConsoleLog(_log: string, _type: "stdout" | "stderr"): boolean | undefined {
return false;
},
+2 -2
View File
@@ -3,8 +3,8 @@
"allowSyntheticDefaultImports": true,
"module": "NodeNext",
"esModuleInterop": true,
"target": "ES2022",
"lib": ["ES2022"],
"target": "esnext",
"lib": ["esnext"],
"strict": true,
"noImplicitAny": true,
"noImplicitThis": true,