Compare commits

..
115 Commits
Author SHA1 Message Date
Koen Kanters 672d2bf08a fix: Home Assistant: allow to override discovery type 2026-07-12 08:10:40 +02:00
luar123 a80c2db4c6 fix: Home Assistant: fix action published to wrong topic (#32544) 2026-07-12 07:52:32 +02:00
Koen Kanters e475de15c9 fix(ignore): update zigbee-herdsman-converters to 26.81.0 (#32543) 2026-07-11 19:37:24 +00:00
Andy 2ca80b8572 fix: experimental_event_entities and legacy_action_sensor require restart (#32535) 2026-07-11 21:13:46 +02:00
Koen Kanters 912fe4c250 fix(ignore): update zigbee-herdsman-converters to 26.80.0 (#32522) 2026-07-09 10:55:54 +00:00
dependabot[bot] 74478fd955 fix(ignore): bump the minor-patch group with 2 updates (#32514) 2026-07-08 19:30:55 +02:00
Koen Kanters 5591207dea fix: Support Node 26, remove Node 20 support (#32508) 2026-07-07 22:38:52 +02:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Koen Kanters
83ca2746a7 fix(ignore): bump @types/node from 24.13.2 to 26.1.0 (#32453)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-07-07 22:31:18 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8f0d981765 fix(ignore): bump the minor-patch group with 2 updates (#32504)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 14:47:07 +02:00
Koen Kanters 6e557162cb fix(ignore): update zigbee-herdsman to 10.6.2 (#32500) 2026-07-06 18:57:40 +00:00
Koen Kanters a2973f21f6 fix(ignore): update zigbee-herdsman-converters to 26.79.0 (#32499) 2026-07-06 18:52:20 +00:00
Koen Kanters f88d99294b fix(ignore): update zigbee-herdsman-converters to 26.78.0 (#32489) 2026-07-05 10:06:58 +00:00
Maximilian Rink 344776b633 fix: Refresh exposes after manual device configure (#32486) 2026-07-05 08:50:33 +02:00
Koen Kanters 4b0c3067ff fix(ignore): update zigbee-herdsman-converters to 26.77.0 (#32462) 2026-07-02 05:40:32 +00:00
dependabot[bot] e8ba0b24f8 fix(ignore): bump the minor-patch group with 5 updates (#32452) 2026-07-01 19:35:51 +02:00
github-actions[bot] 2cfdd807ed chore: promote to dev 2026-07-01 06:13:56 +00:00
Koen Kanters aa909a8a62 chore(dev): release 2.12.1 (#32254) 2026-07-01 08:03:12 +02:00
Koen Kanters f867694831 fix(ignore): update zigbee-herdsman to 10.6.1 (#32442) 2026-06-30 19:14:25 +00:00
Koen Kanters 740de2d363 fix(ignore): update zigbee-herdsman-converters to 26.76.0 (#32441) 2026-06-30 19:09:32 +00:00
Darafei Praliaskouski e8a7e6f29d fix: Home Assistant: mark device settings as config (#32439) 2026-06-30 21:00:16 +02:00
Koen Kanters 3760e63183 fix(ignore): update zigbee-herdsman-converters to 26.75.0 (#32431) 2026-06-29 18:29:42 +00:00
Koen Kanters dcb16b8271 fix(ignore): update zigbee-herdsman to 10.6.0 (#32422) 2026-06-28 14:25:30 +00:00
Koen Kanters 31fb3a6e89 fix(ignore): update zigbee-herdsman-converters to 26.74.0 (#32420) 2026-06-27 19:58:50 +00:00
Ilya KirovandKoen Kanters 0018ca8f80 fix: Home Assistant: support cooling setpoint in climate discovery (#32411)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-06-27 21:38:16 +02:00
Rohan KapoorandCursor 2f26083ce1 fix: Add mqtt.server_name option to override TLS SNI (#32417)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-27 21:37:48 +02:00
Koen Kanters 8d5fdada38 fix(ignore): update zigbee-herdsman-converters to 26.73.0 (#32407) 2026-06-25 19:28:37 +00:00
Koen Kanters 6ab52df08a fix(ignore): update zigbee-herdsman to 10.5.0 (#32406) 2026-06-25 18:03:18 +00:00
Maximilian Rink 0140708076 fix: Home Assistant: apply expose-level Home Assistant discovery metadata (#32380) 2026-06-25 19:54:59 +02:00
DanielDeni 7775ac5cd2 fix: Home Assistant: unit conversion for derived weather sensors by restoring device_class with name preservation (#32392) 2026-06-25 19:31:41 +02:00
Maximilian Rink 814552a615 fix: Home Assistant: pass device options to HA discovery overrides (#32379) 2026-06-25 18:45:36 +02:00
dependabot[bot] 280442b671 fix(ignore): bump actions/cache from 5 to 6 (#32394) 2026-06-25 18:33:24 +02:00
Koen Kanters 628f97f498 fix(ignore): update zigbee-herdsman to 10.4.2 (#32388) 2026-06-23 18:11:24 +00:00
Koen Kanters b58503bb57 fix(ignore): update zigbee-herdsman-converters to 26.72.0 (#32387) 2026-06-23 18:06:00 +00:00
Maximilian Rink 98a0f94870 fix: Home Assistant: mark thermostat configuration switches as config (#32378) 2026-06-23 19:37:13 +02:00
Maximilian Rink 72be1d746b fix: Home Assistant: mark legacy action sensors diagnostic (#32377) 2026-06-23 19:36:44 +02:00
Maximilian Rink 20ab0a2a4f fix: Home Assistant: allow discovery user overrides to win over converter (#32361) 2026-06-23 19:11:04 +02:00
Koen Kanters 87689523ae fix(ignore): update zigbee-herdsman-converters to 26.71.0 (#32375) 2026-06-22 19:02:14 +00:00
Koen Kanters 2cbf740f1b chore: merge master to dev 2026-06-22 20:52:22 +02:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Koen Kanters
262139a43f fix(ignore): bump js-yaml from 4.2.0 to 5.0.0 (#32371)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-06-22 20:52:09 +02:00
Koen Kanters e85ff5513c chore(dependabot): add commit-message prefix for Docker and GH actions 2026-06-22 20:52:03 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9a7cac1e4c fix(ignore): bump semver from 7.8.4 to 7.8.5 in the minor-patch group (#32369)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 20:42:05 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 679436e09b fix(ignore): bump actions/checkout from 6 to 7 (#32368)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 20:40:41 +02:00
Koen Kanters c6e7d2dd16 fix(ignore): update zigbee-herdsman-converters to 26.70.0 (#32357) 2026-06-21 06:00:12 +00:00
Daniel 29ee3a7552 fix: Home Assistant: add conductivity discovery for soil_fertility (#32356) 2026-06-21 07:54:03 +02:00
Koen Kanters 0377f965a3 fix(ignore): update zigbee-herdsman-converters to 26.69.0 (#32350) 2026-06-20 06:28:20 +00:00
Koen Kanters 1de8609456 fix(ignore): update zigbee-herdsman-converters to 26.68.0 (#32340) 2026-06-18 19:22:15 +00:00
Koen Kanters 95c67c0e22 fix(ignore): update zigbee-herdsman-converters to 26.67.0 (#32334) 2026-06-17 19:19:25 +00:00
Koen Kanters 2f44449953 fix(ignore): update zigbee-herdsman to 10.4.1 (#32333) 2026-06-17 19:08:29 +00:00
Koen Kanters 299b9e64f5 fix(ignore): update zigbee-herdsman-converters to 26.66.0 (#32315) 2026-06-15 19:17:00 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Koen Kanters
719de7adcf fix(ignore): bump the minor-patch group with 3 updates (#32312)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-06-15 21:15:04 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 758a80b30c fix: Docker: bump alpine from 3.23 to 3.24 (#32311)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 20:55:10 +02:00
Koen Kanters f4f97d1920 fix(ignore): update zigbee-herdsman-converters to 26.65.0 (#32298) 2026-06-13 19:05:52 +00:00
Koen Kanters a68419f0f5 fix(ignore): update zigbee-herdsman-converters to 26.64.0 (#32282) 2026-06-11 19:58:38 +00:00
quyentonndbsandKai Tanaka ced6d93d20 fix: improve zigbee2mqtt maintenance path (#32255)
Co-authored-by: Kai Tanaka <275430420+quyentonndbs@users.noreply.github.com>
2026-06-11 08:14:36 +02:00
kryoz_ a81e90b981 fix: Republish bridge/state online when HA comes online (#32258) 2026-06-10 21:18:33 +02:00
github-actions[bot] 0cc3b1e7f1 chore: promote to dev 2026-06-09 19:18:48 +00:00
Koen Kanters fd8bafc81d chore(dev): release 2.12.0 (#32157) 2026-06-09 21:02:26 +02:00
Koen Kanters 68daf4f1de fix(ignore): update zigbee-herdsman-converters to 26.63.0 (#32252) 2026-06-09 18:57:20 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 41f6886591 fix(ignore): bump the minor-patch group across 1 directory with 3 updates (#32236)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 20:24:54 +02:00
Koen Kanters 582f153896 fix(ignore): update zigbee-herdsman to 10.4.0 (#32249) 2026-06-09 18:19:56 +00:00
Nerivec 6c2c874641 fix(ignore): missing type for new OTA endpoint (#32195) 2026-06-03 19:24:16 +00:00
Koen Kanters 120f6ac779 fix(ignore): update zigbee-herdsman-converters to 26.62.0 (#32194) 2026-06-03 19:05:29 +00:00
de58711e11 feat: Add ability to abort running OTA (#32022)
Co-authored-by: Andy <andrei_lazarov@yahoo.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-06-03 21:01:44 +02:00
Koen Kanters ea6ccac938 fix(ignore): update zigbee-herdsman to 10.3.0 (#32193) 2026-06-03 18:57:34 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ef8dcb4dc2 fix(ignore): bump the minor-patch group with 2 updates (#32183)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-03 20:42:49 +02:00
Koen Kanters babf7a17d4 fix(ignore): update zigbee-herdsman-converters to 26.61.2 (#32180) 2026-06-02 18:31:41 +00:00
Koen Kanters f9e1bac588 fix(ignore): update zigbee-herdsman to 10.2.0 (#32179) 2026-06-02 18:25:44 +00:00
github-actions[bot] 6aac849452 chore: promote to dev 2026-06-01 18:52:00 +00:00
Koen Kanters 2b5a670ba2 chore(dev): release 2.11.0 (#31938) 2026-06-01 20:28:15 +02:00
Koen Kanters 58feb78362 fix(ignore): update zigbee-herdsman-converters to 26.61.1 (#32156) 2026-06-01 18:21:24 +00:00
Koen Kanters c6e13ccb45 fix(ignore): update zigbee-herdsman-converters to 26.60.0 (#32150) 2026-05-31 19:07:26 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Nerivec
ac8c45f67f fix(ignore): bump the minor-patch group with 2 updates (#32137)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2026-05-29 21:08:01 +00:00
Koen Kanters a8364ad67b fix(ignore): update zigbee-herdsman-converters to 26.59.1 (#32136) 2026-05-29 18:54:19 +00:00
Koen Kanters e2b49113c7 fix(ignore): update zigbee-herdsman-converters to 26.58.0 (#32126) 2026-05-28 19:42:40 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7b9407d020 fix(deps-dev): bump tmp from 0.2.5 to 0.2.6 (#32122)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-28 21:15:16 +02:00
Koen Kanters 873ad6082c chore: merge master to dev 2026-05-28 21:07:03 +02:00
Koen Kanters 8beaf488f7 chore: update CONTRIBUTING.md 2026-05-28 21:06:41 +02:00
Koen Kanters 7a1c2d52f0 fix(ignore): update zigbee-herdsman-converters to 26.57.0 (#32113) 2026-05-28 18:40:36 +00:00
Koen Kanters 5a79c338d4 chore: merge master to dev 2026-05-26 21:31:53 +02:00
Koen Kanters 16fd422c4a chore: add note about AI pull requests 2026-05-26 21:31:34 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e5f70d5ba8 fix(ignore): bump the minor-patch group with 3 updates (#32106)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 20:57:57 +02:00
Koen Kanters 0618aca73a chore: merge master to dev 2026-05-23 20:53:41 +02:00
Koen Kanters cfc7ab8dd8 chore: fix command injection in issueBot.mjs 2026-05-23 20:53:25 +02:00
Koen Kanters 56aab4e59c fix(ignore): update zigbee-herdsman-converters to 26.56.0 (#32087) 2026-05-23 18:36:39 +00:00
Koen Kanters 7075b78a46 fix(ignore): update zigbee-herdsman-converters to 26.55.0 (#32068) 2026-05-21 19:45:51 +00:00
Andy b31e8c286e fix: Fix default value of "optimistic" group setting (#32054) 2026-05-21 21:34:52 +02:00
Koen Kanters 2c5b536134 fix(ignore): update zigbee-herdsman-converters to 26.54.0 (#32065) 2026-05-20 20:17:53 +00:00
Koen Kanters 52201c96bf fix(ignore): update zigbee-herdsman-converters to 26.53.0 (#32051) 2026-05-19 18:13:25 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> bf964cc6d9 fix(deps): bump ws from 8.20.0 to 8.20.1 (#32041)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 19:27:30 +02:00
Koen Kanters 1e114dee70 fix(ignore): update zigbee-herdsman-converters to 26.52.0 (#32040) 2026-05-18 19:35:03 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Koen Kanters
897aa8fb7c fix(deps): bump brace-expansion from 2.0.2 to 5.0.6 (#32038)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-05-18 21:28:14 +02:00
Nerivec bbcbed12d5 fix: Prevent invalid external JS file name on save (#32037) 2026-05-18 21:22:18 +02:00
Koen Kanters 36c4db7f8f fix(ignore): update zigbee-herdsman to 10.1.0 (#32027) 2026-05-17 15:46:23 +00:00
Koen Kanters 5458e5084d fix(ignore): update zigbee-herdsman-converters to 26.51.1 (#32016) 2026-05-15 12:18:35 +00:00
Koen Kanters 744e4a6d5c fix(ignore): update zigbee-herdsman-converters to 26.51.0 (#31999) 2026-05-13 19:04:44 +00:00
Koen Kanters 6ea21c383b fix(ignore): update zigbee-herdsman-converters to 26.50.0 (#31988) 2026-05-12 18:19:05 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1a1c09449c fix(deps): bump pnpm/action-setup from 5 to 6 (#31698)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 19:56:50 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9426c3ea11 fix(ignore): bump the minor-patch group with 3 updates (#31982)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 19:47:15 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4d9e9504ef fix(deps): bump actions/dependency-review-action from 4 to 5 (#31980)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 19:47:03 +02:00
Koen Kanters 32499d14d5 fix(ignore): update zigbee-herdsman-converters to 26.49.0 (#31976) 2026-05-11 19:25:35 +00:00
Richard c4fd415cbe fix: Use Jinja-safe property access in HA discovery templates (#31930) 2026-05-11 17:28:04 +02:00
Koen Kanters 502030c016 fix(ignore): update zigbee-herdsman-converters to 26.48.0 (#31959) 2026-05-09 19:44:55 +00:00
Koen Kanters 7a05464f03 fix(ignore): update zigbee-herdsman-converters to 26.47.0 (#31946) 2026-05-08 18:34:03 +00:00
Nerivec 15fd9b371e feat: allow to disable external JS extensions (#31826) 2026-05-08 20:26:17 +02:00
Koen Kanters 04a43652d6 fix(ignore): update zigbee-herdsman to 10.0.8 (#31945) 2026-05-08 18:16:17 +00:00
github-actions[bot] 7b37e97e00 chore: promote to dev 2026-05-07 17:57:27 +00:00
Koen Kanters 94751abaac chore(dev): release 2.10.1 (#31831) 2026-05-07 19:44:19 +02:00
Koen Kanters cc8456617a fix(ignore): update zigbee-herdsman-converters to 26.46.0 (#31937) 2026-05-07 17:36:55 +00:00
Koen Kanters 8727abdefc fix(ignore): update zigbee-herdsman-converters to 26.45.0 (#31925) 2026-05-06 20:05:18 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 24e90273c9 fix(ignore): bump express-static-gzip from 3.0.0 to 3.0.1 in the minor-patch group (#31924)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 21:59:24 +02:00
Koen Kanters 845bdb7dba fix(ignore): update zigbee-herdsman-converters to 26.44.0 (#31900) 2026-05-05 18:57:33 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 55f12ff520 fix(ignore): bump @biomejs/biome from 2.4.13 to 2.4.14 in the minor-patch group across 1 directory (#31881)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 20:30:06 +02:00
Koen Kanters 19eb05a0e0 fix(ignore): update zigbee-herdsman-converters to 26.43.0 (#31878) 2026-05-05 05:29:59 +00:00
Nerivec 9f7ea9b7c7 fix: Replace deprecated url.parse (#31845) 2026-05-04 20:29:24 +02:00
github-actions[bot] 0d7b8f189c chore: promote to dev 2026-05-01 17:40:55 +00:00
41 changed files with 1133 additions and 495 deletions
+4
View File
@@ -19,8 +19,12 @@ updates:
schedule:
interval: weekly
target-branch: dev
commit-message:
prefix: fix(ignore)
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
target-branch: dev
commit-message:
prefix: fix(ignore)
+6 -6
View File
@@ -22,16 +22,16 @@ jobs:
actions: read # for Nerivec/action-ci-bench
pull-requests: write # for Nerivec/action-ci-bench
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
with:
# Required for `release: merge dev -> master and promote dev`
token: ${{ secrets.GH_TOKEN }}
- uses: actions/checkout@v6
- uses: actions/checkout@v7
if: ((github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push') == false
- uses: pnpm/action-setup@v5
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
@@ -216,13 +216,13 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [20, 22, 24]
node: [22, 24, 26]
runs-on: ${{ matrix.os }}
continue-on-error: true
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v5
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
+2 -2
View File
@@ -10,6 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout repository'
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: 'Dependency review'
uses: actions/dependency-review-action@v4
uses: actions/dependency-review-action@v5
+2 -2
View File
@@ -17,13 +17,13 @@ jobs:
startsWith(github.event.issue.title, '[New device support]') ||
startsWith(github.event.issue.title, '[External Converter]')
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
sparse-checkout: |
scripts
path: z2m
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
repository: Koenkk/zigbee-herdsman-converters
ref: master
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
merge-master-to-dev:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: devmasx/merge-branch@master
with:
type: now
+5 -5
View File
@@ -16,7 +16,7 @@ jobs:
release_created: ${{ steps.release.outputs.release_created }}
version: '${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}}'
steps:
- uses: pnpm/action-setup@v5
- uses: pnpm/action-setup@v6
with:
version: 9
@@ -31,18 +31,18 @@ jobs:
token: ${{secrets.GH_TOKEN}}
# Checkout repos
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
repository: koenkk/zigbee2mqtt
path: ./z2m
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
repository: koenkk/zigbee2mqtt
path: ./z2m-master
ref: master
- name: Restore cache commit-user-lookup.json
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: z2m/scripts/commit-user-lookup.json
key: commit-user-lookup-dummy
@@ -67,7 +67,7 @@ jobs:
env:
GH_TOKEN: ${{secrets.GH_TOKEN}}
- name: Save cache commit-user-lookup.json
uses: actions/cache/save@v5
uses: actions/cache/save@v6
if: always()
with:
path: z2m/scripts/commit-user-lookup.json
+2 -2
View File
@@ -13,11 +13,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
ref: dev
token: ${{ secrets.GH_TOKEN }}
- uses: pnpm/action-setup@v5
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
node-version: 24
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "2.10.0"
".": "2.12.1"
}
+115
View File
@@ -1,5 +1,120 @@
# Changelog
## [2.12.1](https://github.com/Koenkk/zigbee2mqtt/compare/2.12.0...2.12.1) (2026-06-30)
### Bug Fixes
* Add `mqtt.server_name` option to override TLS SNI ([#32417](https://github.com/Koenkk/zigbee2mqtt/issues/32417)) ([2f26083](https://github.com/Koenkk/zigbee2mqtt/commit/2f26083ce13c30227ac8d1d891751d4c253b3572))
* Docker: bump alpine from 3.23 to 3.24 ([#32311](https://github.com/Koenkk/zigbee2mqtt/issues/32311)) ([758a80b](https://github.com/Koenkk/zigbee2mqtt/commit/758a80b30c5162b9adb0c2be1785670276905e09))
* Home Assistant: add conductivity discovery for soil_fertility ([#32356](https://github.com/Koenkk/zigbee2mqtt/issues/32356)) ([29ee3a7](https://github.com/Koenkk/zigbee2mqtt/commit/29ee3a7552017e648c534708a93329e8b3fc52c5))
* Home Assistant: allow discovery user overrides to win over converter ([#32361](https://github.com/Koenkk/zigbee2mqtt/issues/32361)) ([20ab0a2](https://github.com/Koenkk/zigbee2mqtt/commit/20ab0a2a4f44b760b6db73d105dc8578426b27a0))
* Home Assistant: apply expose-level Home Assistant discovery metadata ([#32380](https://github.com/Koenkk/zigbee2mqtt/issues/32380)) ([0140708](https://github.com/Koenkk/zigbee2mqtt/commit/0140708076fbd99d33d896e8f214b0e2a8aad8f3))
* Home Assistant: mark device settings as config ([#32439](https://github.com/Koenkk/zigbee2mqtt/issues/32439)) ([e8a7e6f](https://github.com/Koenkk/zigbee2mqtt/commit/e8a7e6f29da3773ffc2bda63623298a307fbb597))
* Home Assistant: mark legacy action sensors diagnostic ([#32377](https://github.com/Koenkk/zigbee2mqtt/issues/32377)) ([72be1d7](https://github.com/Koenkk/zigbee2mqtt/commit/72be1d746bb79fda65b056e4441dcd73bf6c69c6))
* Home Assistant: mark thermostat configuration switches as config ([#32378](https://github.com/Koenkk/zigbee2mqtt/issues/32378)) ([98a0f94](https://github.com/Koenkk/zigbee2mqtt/commit/98a0f9487072cf5a64a901f305c20a73e25c594b))
* Home Assistant: pass device options to HA discovery overrides ([#32379](https://github.com/Koenkk/zigbee2mqtt/issues/32379)) ([814552a](https://github.com/Koenkk/zigbee2mqtt/commit/814552a615d33ac24c38160f7a24514c7b358623))
* Home Assistant: support cooling setpoint in climate discovery ([#32411](https://github.com/Koenkk/zigbee2mqtt/issues/32411)) ([0018ca8](https://github.com/Koenkk/zigbee2mqtt/commit/0018ca8f809708d22221c9d557b3208ba2e5c057))
* Home Assistant: unit conversion for derived weather sensors by restoring device_class with name preservation ([#32392](https://github.com/Koenkk/zigbee2mqtt/issues/32392)) ([7775ac5](https://github.com/Koenkk/zigbee2mqtt/commit/7775ac5cd2ae3929580e401640d97b20facf1491))
* **ignore:** bump actions/cache from 5 to 6 ([#32394](https://github.com/Koenkk/zigbee2mqtt/issues/32394)) ([280442b](https://github.com/Koenkk/zigbee2mqtt/commit/280442b6710c03cfa4776d69a14396b29d32c4ba))
* **ignore:** bump actions/checkout from 6 to 7 ([#32368](https://github.com/Koenkk/zigbee2mqtt/issues/32368)) ([679436e](https://github.com/Koenkk/zigbee2mqtt/commit/679436e09b40edea5bb17094e9aa9115733b916f))
* **ignore:** bump js-yaml from 4.2.0 to 5.0.0 ([#32371](https://github.com/Koenkk/zigbee2mqtt/issues/32371)) ([262139a](https://github.com/Koenkk/zigbee2mqtt/commit/262139a43f878ceef2f327369432e33b74fbd972))
* **ignore:** bump semver from 7.8.4 to 7.8.5 in the minor-patch group ([#32369](https://github.com/Koenkk/zigbee2mqtt/issues/32369)) ([9a7cac1](https://github.com/Koenkk/zigbee2mqtt/commit/9a7cac1e4c82c1b0ef566b13620e2d4d438d725e))
* **ignore:** bump the minor-patch group with 3 updates ([#32312](https://github.com/Koenkk/zigbee2mqtt/issues/32312)) ([719de7a](https://github.com/Koenkk/zigbee2mqtt/commit/719de7adcfffc64e2e9133142045e5dd56e4e8d4))
* **ignore:** update zigbee-herdsman to 10.4.1 ([#32333](https://github.com/Koenkk/zigbee2mqtt/issues/32333)) ([2f44449](https://github.com/Koenkk/zigbee2mqtt/commit/2f4444995361257cbeef0c780532381ec6cc254f))
* **ignore:** update zigbee-herdsman to 10.4.2 ([#32388](https://github.com/Koenkk/zigbee2mqtt/issues/32388)) ([628f97f](https://github.com/Koenkk/zigbee2mqtt/commit/628f97f498747cd2e6be5808c7c609b948af528f))
* **ignore:** update zigbee-herdsman to 10.5.0 ([#32406](https://github.com/Koenkk/zigbee2mqtt/issues/32406)) ([6ab52df](https://github.com/Koenkk/zigbee2mqtt/commit/6ab52df08af1e9b80eb4b58752b7f403d52844a7))
* **ignore:** update zigbee-herdsman to 10.6.0 ([#32422](https://github.com/Koenkk/zigbee2mqtt/issues/32422)) ([dcb16b8](https://github.com/Koenkk/zigbee2mqtt/commit/dcb16b8271349390deee9dc0a0ca4d02239a54bc))
* **ignore:** update zigbee-herdsman to 10.6.1 ([#32442](https://github.com/Koenkk/zigbee2mqtt/issues/32442)) ([f867694](https://github.com/Koenkk/zigbee2mqtt/commit/f86769483102762798272655c4fb117f33263489))
* **ignore:** update zigbee-herdsman-converters to 26.64.0 ([#32282](https://github.com/Koenkk/zigbee2mqtt/issues/32282)) ([a68419f](https://github.com/Koenkk/zigbee2mqtt/commit/a68419f0f58324c1809055938cc59bdd07442aa9))
* **ignore:** update zigbee-herdsman-converters to 26.65.0 ([#32298](https://github.com/Koenkk/zigbee2mqtt/issues/32298)) ([f4f97d1](https://github.com/Koenkk/zigbee2mqtt/commit/f4f97d19208ab951256b69fd7673bfaba667af65))
* **ignore:** update zigbee-herdsman-converters to 26.66.0 ([#32315](https://github.com/Koenkk/zigbee2mqtt/issues/32315)) ([299b9e6](https://github.com/Koenkk/zigbee2mqtt/commit/299b9e64f52c99d1d3034321e82fc40a49bec604))
* **ignore:** update zigbee-herdsman-converters to 26.67.0 ([#32334](https://github.com/Koenkk/zigbee2mqtt/issues/32334)) ([95c67c0](https://github.com/Koenkk/zigbee2mqtt/commit/95c67c0e2271e24a236dabe717772c16f52a3017))
* **ignore:** update zigbee-herdsman-converters to 26.68.0 ([#32340](https://github.com/Koenkk/zigbee2mqtt/issues/32340)) ([1de8609](https://github.com/Koenkk/zigbee2mqtt/commit/1de860945645e748d6173a0368c4ccb44e9f5f13))
* **ignore:** update zigbee-herdsman-converters to 26.69.0 ([#32350](https://github.com/Koenkk/zigbee2mqtt/issues/32350)) ([0377f96](https://github.com/Koenkk/zigbee2mqtt/commit/0377f965a3a3c21e42afb885f8b3685d6de2fdd1))
* **ignore:** update zigbee-herdsman-converters to 26.70.0 ([#32357](https://github.com/Koenkk/zigbee2mqtt/issues/32357)) ([c6e7d2d](https://github.com/Koenkk/zigbee2mqtt/commit/c6e7d2dd16b1a7369b8f784caf3ceed6296a9da2))
* **ignore:** update zigbee-herdsman-converters to 26.71.0 ([#32375](https://github.com/Koenkk/zigbee2mqtt/issues/32375)) ([8768952](https://github.com/Koenkk/zigbee2mqtt/commit/87689523ae685258fa20ee8c03b7a09e8df0f4ba))
* **ignore:** update zigbee-herdsman-converters to 26.72.0 ([#32387](https://github.com/Koenkk/zigbee2mqtt/issues/32387)) ([b58503b](https://github.com/Koenkk/zigbee2mqtt/commit/b58503bb57054e24855bcde353b43be6a61273c6))
* **ignore:** update zigbee-herdsman-converters to 26.73.0 ([#32407](https://github.com/Koenkk/zigbee2mqtt/issues/32407)) ([8d5fdad](https://github.com/Koenkk/zigbee2mqtt/commit/8d5fdada38ce32f8e9ee86b3e39b2e00e2e76529))
* **ignore:** update zigbee-herdsman-converters to 26.74.0 ([#32420](https://github.com/Koenkk/zigbee2mqtt/issues/32420)) ([31fb3a6](https://github.com/Koenkk/zigbee2mqtt/commit/31fb3a6e895170993e893e9ea0d63c6600859259))
* **ignore:** update zigbee-herdsman-converters to 26.75.0 ([#32431](https://github.com/Koenkk/zigbee2mqtt/issues/32431)) ([3760e63](https://github.com/Koenkk/zigbee2mqtt/commit/3760e631835c8e6e11cea5241bb2c8cf3d374722))
* **ignore:** update zigbee-herdsman-converters to 26.76.0 ([#32441](https://github.com/Koenkk/zigbee2mqtt/issues/32441)) ([740de2d](https://github.com/Koenkk/zigbee2mqtt/commit/740de2d363109707a90cdfa8c019361f4423a1aa))
* improve zigbee2mqtt maintenance path ([#32255](https://github.com/Koenkk/zigbee2mqtt/issues/32255)) ([ced6d93](https://github.com/Koenkk/zigbee2mqtt/commit/ced6d93d20502f115d4d0b17b5d292fee7ad30e4))
* Republish bridge/state online when HA comes online ([#32258](https://github.com/Koenkk/zigbee2mqtt/issues/32258)) ([a81e90b](https://github.com/Koenkk/zigbee2mqtt/commit/a81e90b9812acd795a11a78b6d0489c080dc8f5b))
## [2.12.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.11.0...2.12.0) (2026-06-09)
### Features
* Add ability to abort running OTA ([#32022](https://github.com/Koenkk/zigbee2mqtt/issues/32022)) ([de58711](https://github.com/Koenkk/zigbee2mqtt/commit/de58711e11a5ca07c5b55a0c17a1d9f3f452c110))
### Bug Fixes
* **ignore:** bump the minor-patch group across 1 directory with 3 updates ([#32236](https://github.com/Koenkk/zigbee2mqtt/issues/32236)) ([41f6886](https://github.com/Koenkk/zigbee2mqtt/commit/41f688659119012a9a84063bd67c2769a1b1892c))
* **ignore:** bump the minor-patch group with 2 updates ([#32183](https://github.com/Koenkk/zigbee2mqtt/issues/32183)) ([ef8dcb4](https://github.com/Koenkk/zigbee2mqtt/commit/ef8dcb4dc2800defd97fda7523297eab8d2566dc))
* **ignore:** missing type for new OTA endpoint ([#32195](https://github.com/Koenkk/zigbee2mqtt/issues/32195)) ([6c2c874](https://github.com/Koenkk/zigbee2mqtt/commit/6c2c874641ada9cae1c9bdd963a3201cb1cf0fef))
* **ignore:** update zigbee-herdsman to 10.2.0 ([#32179](https://github.com/Koenkk/zigbee2mqtt/issues/32179)) ([f9e1bac](https://github.com/Koenkk/zigbee2mqtt/commit/f9e1bac588e65b223e75f4a2b2e9d528933fdbd8))
* **ignore:** update zigbee-herdsman to 10.3.0 ([#32193](https://github.com/Koenkk/zigbee2mqtt/issues/32193)) ([ea6ccac](https://github.com/Koenkk/zigbee2mqtt/commit/ea6ccac9380af1d9f5672174b2545409f36c2769))
* **ignore:** update zigbee-herdsman to 10.4.0 ([#32249](https://github.com/Koenkk/zigbee2mqtt/issues/32249)) ([582f153](https://github.com/Koenkk/zigbee2mqtt/commit/582f1538961af9c1b6434e6a1bc4acebc63a2541))
* **ignore:** update zigbee-herdsman-converters to 26.61.2 ([#32180](https://github.com/Koenkk/zigbee2mqtt/issues/32180)) ([babf7a1](https://github.com/Koenkk/zigbee2mqtt/commit/babf7a17d4617217297eef45f6b8307265f9b921))
* **ignore:** update zigbee-herdsman-converters to 26.62.0 ([#32194](https://github.com/Koenkk/zigbee2mqtt/issues/32194)) ([120f6ac](https://github.com/Koenkk/zigbee2mqtt/commit/120f6ac779c9fda6914fc68c56674e589c0e82ba))
* **ignore:** update zigbee-herdsman-converters to 26.63.0 ([#32252](https://github.com/Koenkk/zigbee2mqtt/issues/32252)) ([68daf4f](https://github.com/Koenkk/zigbee2mqtt/commit/68daf4f1deafad3ee6e542acbd77d403340e2caa))
## [2.11.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.10.1...2.11.0) (2026-06-01)
### Features
* allow to disable external JS extensions ([#31826](https://github.com/Koenkk/zigbee2mqtt/issues/31826)) ([15fd9b3](https://github.com/Koenkk/zigbee2mqtt/commit/15fd9b371e30ed352cf2de2d241a0d0f7fafa9d1))
### Bug Fixes
* **deps-dev:** bump tmp from 0.2.5 to 0.2.6 ([#32122](https://github.com/Koenkk/zigbee2mqtt/issues/32122)) ([7b9407d](https://github.com/Koenkk/zigbee2mqtt/commit/7b9407d020b9db1093b360f5f6e217bbab7afb63))
* **deps:** bump actions/dependency-review-action from 4 to 5 ([#31980](https://github.com/Koenkk/zigbee2mqtt/issues/31980)) ([4d9e950](https://github.com/Koenkk/zigbee2mqtt/commit/4d9e9504ef9c857a1985c79d3a6799552abc3aea))
* **deps:** bump brace-expansion from 2.0.2 to 5.0.6 ([#32038](https://github.com/Koenkk/zigbee2mqtt/issues/32038)) ([897aa8f](https://github.com/Koenkk/zigbee2mqtt/commit/897aa8fb7c82dbc9c906f986477c4c1673bb3199))
* **deps:** bump pnpm/action-setup from 5 to 6 ([#31698](https://github.com/Koenkk/zigbee2mqtt/issues/31698)) ([1a1c094](https://github.com/Koenkk/zigbee2mqtt/commit/1a1c09449c9d077ea06048d7e2ecea90ff850c9b))
* **deps:** bump ws from 8.20.0 to 8.20.1 ([#32041](https://github.com/Koenkk/zigbee2mqtt/issues/32041)) ([bf964cc](https://github.com/Koenkk/zigbee2mqtt/commit/bf964cc6d976d70e687844e5a09c4577763dc0d3))
* Fix default value of "optimistic" group setting ([#32054](https://github.com/Koenkk/zigbee2mqtt/issues/32054)) ([b31e8c2](https://github.com/Koenkk/zigbee2mqtt/commit/b31e8c286e7a77517a88e3e7eb2247e9a0fd2cc1))
* **ignore:** bump the minor-patch group with 2 updates ([#32137](https://github.com/Koenkk/zigbee2mqtt/issues/32137)) ([ac8c45f](https://github.com/Koenkk/zigbee2mqtt/commit/ac8c45f67f0a4aae2688e55b98dabdc23f73a259))
* **ignore:** bump the minor-patch group with 3 updates ([#31982](https://github.com/Koenkk/zigbee2mqtt/issues/31982)) ([9426c3e](https://github.com/Koenkk/zigbee2mqtt/commit/9426c3ea118de9e60326e8dc257a0c2474380f8c))
* **ignore:** bump the minor-patch group with 3 updates ([#32106](https://github.com/Koenkk/zigbee2mqtt/issues/32106)) ([e5f70d5](https://github.com/Koenkk/zigbee2mqtt/commit/e5f70d5ba82923f9f37fbda79bed08d581706129))
* **ignore:** update zigbee-herdsman to 10.0.8 ([#31945](https://github.com/Koenkk/zigbee2mqtt/issues/31945)) ([04a4365](https://github.com/Koenkk/zigbee2mqtt/commit/04a43652d6e131694e29e06ca646eeac37ed3c7d))
* **ignore:** update zigbee-herdsman to 10.1.0 ([#32027](https://github.com/Koenkk/zigbee2mqtt/issues/32027)) ([36c4db7](https://github.com/Koenkk/zigbee2mqtt/commit/36c4db7f8f4e3438f6ebb82cd6ecadf6c54e231e))
* **ignore:** update zigbee-herdsman-converters to 26.47.0 ([#31946](https://github.com/Koenkk/zigbee2mqtt/issues/31946)) ([7a05464](https://github.com/Koenkk/zigbee2mqtt/commit/7a05464f0355380ee855415bc270632263e3f5d3))
* **ignore:** update zigbee-herdsman-converters to 26.48.0 ([#31959](https://github.com/Koenkk/zigbee2mqtt/issues/31959)) ([502030c](https://github.com/Koenkk/zigbee2mqtt/commit/502030c0168c8ba6eaf7495a9014489f2f7a7c50))
* **ignore:** update zigbee-herdsman-converters to 26.49.0 ([#31976](https://github.com/Koenkk/zigbee2mqtt/issues/31976)) ([32499d1](https://github.com/Koenkk/zigbee2mqtt/commit/32499d14d5f0c9eada160d596d6be92da6c4af95))
* **ignore:** update zigbee-herdsman-converters to 26.50.0 ([#31988](https://github.com/Koenkk/zigbee2mqtt/issues/31988)) ([6ea21c3](https://github.com/Koenkk/zigbee2mqtt/commit/6ea21c383b4801f28e30b022194e71375db94b17))
* **ignore:** update zigbee-herdsman-converters to 26.51.0 ([#31999](https://github.com/Koenkk/zigbee2mqtt/issues/31999)) ([744e4a6](https://github.com/Koenkk/zigbee2mqtt/commit/744e4a6d5c4dd179e7c3d1a2b4fbcdc820195547))
* **ignore:** update zigbee-herdsman-converters to 26.51.1 ([#32016](https://github.com/Koenkk/zigbee2mqtt/issues/32016)) ([5458e50](https://github.com/Koenkk/zigbee2mqtt/commit/5458e5084def411aaad9af08e415acb6bc16d181))
* **ignore:** update zigbee-herdsman-converters to 26.52.0 ([#32040](https://github.com/Koenkk/zigbee2mqtt/issues/32040)) ([1e114de](https://github.com/Koenkk/zigbee2mqtt/commit/1e114dee70b8079eda31d4d0cc52fa073a772798))
* **ignore:** update zigbee-herdsman-converters to 26.53.0 ([#32051](https://github.com/Koenkk/zigbee2mqtt/issues/32051)) ([52201c9](https://github.com/Koenkk/zigbee2mqtt/commit/52201c96bf897af9bcad30435575923a0452b6dd))
* **ignore:** update zigbee-herdsman-converters to 26.54.0 ([#32065](https://github.com/Koenkk/zigbee2mqtt/issues/32065)) ([2c5b536](https://github.com/Koenkk/zigbee2mqtt/commit/2c5b53613448ae4c6548f874a36acc1226d53ec3))
* **ignore:** update zigbee-herdsman-converters to 26.55.0 ([#32068](https://github.com/Koenkk/zigbee2mqtt/issues/32068)) ([7075b78](https://github.com/Koenkk/zigbee2mqtt/commit/7075b78a462a0d1f76cf91cc78601d90edfd8b4e))
* **ignore:** update zigbee-herdsman-converters to 26.56.0 ([#32087](https://github.com/Koenkk/zigbee2mqtt/issues/32087)) ([56aab4e](https://github.com/Koenkk/zigbee2mqtt/commit/56aab4e59c4ea7bdbd93a1ed6632171994d8ed56))
* **ignore:** update zigbee-herdsman-converters to 26.57.0 ([#32113](https://github.com/Koenkk/zigbee2mqtt/issues/32113)) ([7a1c2d5](https://github.com/Koenkk/zigbee2mqtt/commit/7a1c2d52f0b0699854f3c8c9375761cb0b22ac55))
* **ignore:** update zigbee-herdsman-converters to 26.58.0 ([#32126](https://github.com/Koenkk/zigbee2mqtt/issues/32126)) ([e2b4911](https://github.com/Koenkk/zigbee2mqtt/commit/e2b49113c7932c4cd8ef7b23a27e3cd5d4e7150d))
* **ignore:** update zigbee-herdsman-converters to 26.59.1 ([#32136](https://github.com/Koenkk/zigbee2mqtt/issues/32136)) ([a8364ad](https://github.com/Koenkk/zigbee2mqtt/commit/a8364ad67b0dcbde93b1d6445c269a821321e9b3))
* **ignore:** update zigbee-herdsman-converters to 26.60.0 ([#32150](https://github.com/Koenkk/zigbee2mqtt/issues/32150)) ([c6e13cc](https://github.com/Koenkk/zigbee2mqtt/commit/c6e13ccb45666f9604e8c9c9655bbe7ea682712d))
* **ignore:** update zigbee-herdsman-converters to 26.61.1 ([#32156](https://github.com/Koenkk/zigbee2mqtt/issues/32156)) ([58feb78](https://github.com/Koenkk/zigbee2mqtt/commit/58feb783626fd2429bea0f8a0db37bdc107c431b))
* Prevent invalid external JS file name on save ([#32037](https://github.com/Koenkk/zigbee2mqtt/issues/32037)) ([bbcbed1](https://github.com/Koenkk/zigbee2mqtt/commit/bbcbed12d53408eaa8ceec110c7071fff8e158e4))
* Use Jinja-safe property access in HA discovery templates ([#31930](https://github.com/Koenkk/zigbee2mqtt/issues/31930)) ([c4fd415](https://github.com/Koenkk/zigbee2mqtt/commit/c4fd415cbe7b01719679b1a96d2b6e07b0793125))
## [2.10.1](https://github.com/Koenkk/zigbee2mqtt/compare/2.10.0...2.10.1) (2026-05-07)
### Bug Fixes
* **ignore:** bump @biomejs/biome from 2.4.13 to 2.4.14 in the minor-patch group across 1 directory ([#31881](https://github.com/Koenkk/zigbee2mqtt/issues/31881)) ([55f12ff](https://github.com/Koenkk/zigbee2mqtt/commit/55f12ff52063d576be0fe82891678c52181f3b8e))
* **ignore:** bump express-static-gzip from 3.0.0 to 3.0.1 in the minor-patch group ([#31924](https://github.com/Koenkk/zigbee2mqtt/issues/31924)) ([24e9027](https://github.com/Koenkk/zigbee2mqtt/commit/24e90273c9e712e122887e74ccf8208a8b42e762))
* **ignore:** update zigbee-herdsman-converters to 26.43.0 ([#31878](https://github.com/Koenkk/zigbee2mqtt/issues/31878)) ([19eb05a](https://github.com/Koenkk/zigbee2mqtt/commit/19eb05a0e023c7e2e8639c1dcda18399e96ae74d))
* **ignore:** update zigbee-herdsman-converters to 26.44.0 ([#31900](https://github.com/Koenkk/zigbee2mqtt/issues/31900)) ([845bdb7](https://github.com/Koenkk/zigbee2mqtt/commit/845bdb7dba5fe0f5e67e1b92c68872da0f3abf6a))
* **ignore:** update zigbee-herdsman-converters to 26.45.0 ([#31925](https://github.com/Koenkk/zigbee2mqtt/issues/31925)) ([8727abd](https://github.com/Koenkk/zigbee2mqtt/commit/8727abdefc9114a5e2e1036d764271c90065e0a1))
* **ignore:** update zigbee-herdsman-converters to 26.46.0 ([#31937](https://github.com/Koenkk/zigbee2mqtt/issues/31937)) ([cc84566](https://github.com/Koenkk/zigbee2mqtt/commit/cc8456617a4a389e7297e380901bfd6bb0223b32))
* Replace deprecated `url.parse` ([#31845](https://github.com/Koenkk/zigbee2mqtt/issues/31845)) ([9f7ea9b](https://github.com/Koenkk/zigbee2mqtt/commit/9f7ea9b7c79db7a781b431fbe43568e15647f8e5))
## [2.10.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.9.2...2.10.0) (2026-05-01)
+10 -4
View File
@@ -1,8 +1,14 @@
# Contributing to Zigbee2MQTT
Everybody is invited and welcome to contribute to Zigbee2MQTT. Zigbee2MQTT is written in JavaScript and is based upon [zigbee-herdsman](https://github.com/koenkk/zigbee-herdsman) and [zigbee-herdsman-converters](https://github.com/koenkk/zigbee-herdsman-converters). Zigbee-herdsman-converters contains all device definitions, zigbee-herdsman is responsible for handling all communication with the adapter.
> [!WARNING]
> Any AI-driven pull request with more than 500 lines of code will not be considered. If wanting to submit something that requires more, split the work into easily review-able pull requests that can be introduced in increments (e.g. pre-refactor, base feature, additional features).
Everybody is invited and welcomed to contribute to Zigbee2MQTT.
Zigbee2MQTT is written in TypeScript.
It uses [zigbee-herdsman](https://github.com/koenkk/zigbee-herdsman) for communication with the adapter/coordinator and [zigbee-herdsman-converters](https://github.com/koenkk/zigbee-herdsman-converters) to provide device-specific definitions.
- Pull requests are always created against the [**dev**](https://github.com/Koenkk/zigbee2mqtt/tree/dev) branch.
- Easiest way to start developing Zigbee2MQTT is by setting up a development environment (aka bare-metal installation). You can follow this [guide](https://www.zigbee2mqtt.io/guide/installation/01_linux.html) to do this.
- You can run the tests locally by executing `pnpm test`. Zigbee2MQTT enforces 100% code coverage, in case you add new code check if your code is covered by running `pnpm run test:coverage`. The coverage report can be found under `coverage/lcov-report/index.html`. Linting is also enforced and can be run with `pnpm run eslint`.
- When you want to add support for a new device no changes to Zigbee2MQTT have to be made, only to zigbee-herdsman-converters. You can find a guide for it [here](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html).
- Easiest way to start developing Zigbee2MQTT is by setting up a development environment (a.k.a. bare-metal installation). You can follow this [guide](https://www.zigbee2mqtt.io/guide/installation/01_linux.html) to do this.
- You can run the tests locally by executing `pnpm test`. Zigbee2MQTT enforces 100% code coverage, in case you add new code check if your code is covered by running `pnpm run test:coverage`. The coverage report can be found under `coverage/lcov-report/index.html`.
- Linting & formatting is also enforced and can be run with `pnpm run check` (can use `pnpm run check:w` to fix small issues automatically).
- If you want to add support for a new device no change to Zigbee2MQTT has to be made, only to zigbee-herdsman-converters. You can find a guide for it [here](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html).
+2 -2
View File
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
@@ -12,7 +12,7 @@
"bracketSpacing": false
},
"files": {
"includes": ["**", "!package.json", "!!**/dist", "!!**/coverage"]
"includes": ["**", "!package.json", "!!**/dist", "!!**/coverage", "!images"]
},
"linter": {
"includes": ["**"],
+1 -1
View File
@@ -1,6 +1,6 @@
ARG TARGETPLATFORM
FROM alpine:3.23 AS base
FROM alpine:3.24 AS base
ENV NODE_ENV=production
WORKDIR /app
+1 -1
View File
@@ -12,7 +12,7 @@ let unsolicitedStop = false;
let watchdogDelays = [2000, 60000, 300000, 900000, 1800000, 3600000];
if (process.env.Z2M_WATCHDOG != null && process.env.Z2M_WATCHDOG !== "default") {
if (/^\d+(.\d+)?(,\d+(.\d+)?)*$/.test(process.env.Z2M_WATCHDOG)) {
if (/^\d+(\.\d+)?(,\d+(\.\d+)?)*$/.test(process.env.Z2M_WATCHDOG)) {
watchdogDelays = process.env.Z2M_WATCHDOG.split(",").map((v) => Number.parseFloat(v) * 60000);
} else {
console.log(`Invalid watchdog delays (must use number-only CSV format representing minutes, example: 'Z2M_WATCHDOG=1,5,15,30,60'.`);
+25 -17
View File
@@ -50,7 +50,6 @@ export class Controller {
this.state = new State(this.eventBus, this.zigbee);
this.restartCallback = restartCallback;
this.exitCallback = exitCallback;
// Initialize extensions.
this.extensionArgs = [
this.zigbee,
@@ -62,22 +61,29 @@ export class Controller {
this.restartCallback,
this.addExtension,
];
this.extensions = new Set();
this.extensions = new Set([
new ExtensionExternalConverters(...this.extensionArgs),
new ExtensionOnEvent(...this.extensionArgs),
new ExtensionBridge(...this.extensionArgs),
new ExtensionPublish(...this.extensionArgs),
new ExtensionReceive(...this.extensionArgs),
new ExtensionConfigure(...this.extensionArgs),
new ExtensionNetworkMap(...this.extensionArgs),
new ExtensionGroups(...this.extensionArgs),
new ExtensionBind(...this.extensionArgs),
new ExtensionOTAUpdate(...this.extensionArgs),
new ExtensionExternalExtensions(...this.extensionArgs),
new ExtensionAvailability(...this.extensionArgs),
new ExtensionHealth(...this.extensionArgs),
]);
if (settings.get().advanced.enable_external_js) {
this.extensions.add(new ExtensionExternalConverters(...this.extensionArgs));
} else {
logger.info("External JS (converters/extensions) is disabled");
}
this.extensions.add(new ExtensionOnEvent(...this.extensionArgs));
this.extensions.add(new ExtensionBridge(...this.extensionArgs));
this.extensions.add(new ExtensionPublish(...this.extensionArgs));
this.extensions.add(new ExtensionReceive(...this.extensionArgs));
this.extensions.add(new ExtensionConfigure(...this.extensionArgs));
this.extensions.add(new ExtensionNetworkMap(...this.extensionArgs));
this.extensions.add(new ExtensionGroups(...this.extensionArgs));
this.extensions.add(new ExtensionBind(...this.extensionArgs));
this.extensions.add(new ExtensionOTAUpdate(...this.extensionArgs));
this.extensions.add(new ExtensionAvailability(...this.extensionArgs));
this.extensions.add(new ExtensionHealth(...this.extensionArgs));
if (settings.get().advanced.enable_external_js) {
this.extensions.add(new ExtensionExternalExtensions(...this.extensionArgs));
}
}
async start(): Promise<void> {
@@ -247,10 +253,12 @@ export class Controller {
}
const existingExtension = this.getExtension(name);
if (existingExtension) {
await this.removeExtension(existingExtension);
}
await this.extensions.add(extension);
this.extensions.add(extension);
} else {
switch (name) {
case "Frontend": {
+6 -5
View File
@@ -247,19 +247,20 @@ export default class EventBus {
this.callbacksByExtension.set(key.constructor.name, []);
}
const wrappedCallback = async (...args: never[]): Promise<void> => {
const typedCallback = callback as (...args: EventBusMap[K]) => Promise<void> | void;
const wrappedCallback = (async (...args: EventBusMap[K]): Promise<void> => {
try {
await callback(...args);
await typedCallback(...args);
} catch (error) {
logger.error(`EventBus error '${key.constructor.name}/${event}': ${(error as Error).message}`);
// biome-ignore lint/style/noNonNullAssertion: always Error
logger.debug((error as Error).stack!);
}
};
}) as EventBusListener<keyof EventBusMap>;
// biome-ignore lint/style/noNonNullAssertion: just created if wasn't valid
this.callbacksByExtension.get(key.constructor.name)!.push({event, callback: wrappedCallback});
this.emitter.on(event, wrappedCallback as EventBusListener<K>);
(this.emitter as events.EventEmitter).on(event, wrappedCallback);
}
public removeListeners(key: ListenerKey): void {
@@ -267,7 +268,7 @@ export default class EventBus {
if (callbacks) {
for (const cb of callbacks) {
this.emitter.removeListener(cb.event, cb.callback);
(this.emitter as events.EventEmitter).removeListener(cb.event, cb.callback);
}
}
}
+1 -1
View File
@@ -137,7 +137,7 @@ export default class Configure extends Extension {
logger.info(`Successfully configured '${device.name}' (definition v${definitionVersion})`);
device.zh.meta.configured = definitionVersion;
device.zh.save();
this.eventBus.emitDevicesChanged();
this.eventBus.emitExposesAndDevicesChanged(device);
} catch (error) {
const newAttempts = attempts + 1;
this.attempts.set(device.ieeeAddr, newAttempts);
+6
View File
@@ -176,7 +176,13 @@ export default abstract class ExternalJSExtension<M> extends Extension {
}
const {name, code} = message;
if (!name.endsWith(".mjs") && !name.endsWith(".js") && !name.endsWith(".cjs")) {
return utils.getResponse(message, {}, "JavaScript file must have '.mjs', '.js' or '.cjs' extension");
}
const filePath = this.getFilePath(name, true);
try {
fs.writeFileSync(filePath, code, "utf8");
this.symlinkNodeModulesIfNecessary();
+2 -3
View File
@@ -5,7 +5,6 @@ import {createServer} from "node:http";
import {createServer as createSecureServer} from "node:https";
import type {Socket} from "node:net";
import {posix} from "node:path";
import {parse} from "node:url";
import bind from "bind-decorator";
import expressStaticGzip from "express-static-gzip";
import finalhandler from "finalhandler";
@@ -157,10 +156,10 @@ export class Frontend extends Extension {
@bind private onUpgrade(request: IncomingMessage, socket: Socket, head: Buffer): void {
this.wss.handleUpgrade(request, socket, head, (ws) => {
// biome-ignore lint/style/noNonNullAssertion: `Only valid for request obtained from Server`
const {query} = parse(request.url!, true);
const {searchParams} = new URL(request.url!, "http://localhost"); // dummy base, may not be absolute
const authToken = settings.get().frontend.auth_token;
if (!authToken || authToken === query.token) {
if (!authToken || authToken === searchParams.get("token")) {
this.wss.emit("connection", ws, request);
} else {
ws.close(4401, "Unauthorized");
+150 -68
View File
@@ -48,17 +48,31 @@ const GROUP_SUPPORTED_TYPES: ReadonlyArray<string> = ["light", "switch", "lock",
const COVER_OPENING_LOOKUP: ReadonlyArray<string> = ["opening", "open", "forward", "up", "rising"];
const COVER_CLOSING_LOOKUP: ReadonlyArray<string> = ["closing", "close", "backward", "back", "reverse", "down", "declining"];
const COVER_STOPPED_LOOKUP: ReadonlyArray<string> = ["stopped", "stop", "pause", "paused"];
const SWITCH_DIFFERENT: ReadonlyArray<string> = ["valve_detection", "window_detection", "auto_lock", "away_mode"];
const CONFIG_SWITCH_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
auto_lock: {entity_category: "config", icon: "mdi:lock"},
away_mode: {entity_category: "config", icon: "mdi:home-export-outline"},
comfort_smiley: {entity_category: "config", icon: "mdi:emoticon-happy-outline"},
enable_display: {entity_category: "config", icon: "mdi:monitor"},
indicator: {entity_category: "config", icon: "mdi:led-on"},
tilt_mode: {entity_category: "config", icon: "mdi:angle-acute"},
valve_detection: {entity_category: "config", icon: "mdi:pipe-valve"},
window_detection: {entity_category: "config", icon: "mdi:window-open-variant"},
} as const;
const SWITCH_DIFFERENT: ReadonlyArray<string> = Object.keys(CONFIG_SWITCH_DISCOVERY_LOOKUP);
const BINARY_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
activity_led_indicator: {icon: "mdi:led-on"},
area1Occupancy: {device_class: "occupancy"},
area2Occupancy: {device_class: "occupancy"},
area3Occupancy: {device_class: "occupancy"},
area4Occupancy: {device_class: "occupancy"},
auto_lock: {entity_category: "config", icon: "mdi:lock"},
auto_off: {icon: "mdi:flash-auto"},
away_mode: {entity_category: "config", icon: "mdi:home-export-outline"},
battery_low: {entity_category: "diagnostic", device_class: "battery"},
button_lock: {entity_category: "config", icon: "mdi:lock"},
calibration: {entity_category: "config", icon: "mdi:progress-wrench"},
calibration_left: {entity_category: "config", icon: "mdi:progress-wrench"},
calibration_right: {entity_category: "config", icon: "mdi:progress-wrench"},
capabilities_configurable_curve: {entity_category: "diagnostic", icon: "mdi:tune"},
capabilities_forward_phase_control: {entity_category: "diagnostic", icon: "mdi:tune"},
capabilities_overload_detection: {entity_category: "diagnostic", icon: "mdi:tune"},
@@ -71,16 +85,22 @@ const BINARY_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
consumer_connected: {device_class: "plug"},
contact: {device_class: "door"},
garage_door_contact: {device_class: "garage_door", payload_on: false, payload_off: true},
frost_protection: {entity_category: "config", icon: "mdi:snowflake-thermometer"},
heating_stop: {entity_category: "config", icon: "mdi:radiator-off"},
eco_mode: {entity_category: "config", icon: "mdi:leaf"},
enable_display: {entity_category: "config", icon: "mdi:monitor"},
expose_pin: {entity_category: "config", icon: "mdi:pin"},
flip_indicator_light: {entity_category: "config", icon: "mdi:arrow-left-right"},
gas: {device_class: "gas"},
indicator: {entity_category: "config", icon: "mdi:led-on"},
indicator_mode: {entity_category: "config", icon: "mdi:led-on"},
invert_cover: {entity_category: "config", icon: "mdi:arrow-left-right"},
led_disabled_night: {entity_category: "config", icon: "mdi:led-off"},
led_indication: {entity_category: "config", icon: "mdi:led-on"},
led_enable: {entity_category: "config", icon: "mdi:led-on"},
motor_reversal: {entity_category: "config", icon: "mdi:arrow-left-right"},
motor_reversal_left: {entity_category: "config", icon: "mdi:arrow-left-right"},
motor_reversal_right: {entity_category: "config", icon: "mdi:arrow-left-right"},
moving: {device_class: "moving"},
no_position_support: {entity_category: "config", icon: "mdi:minus-circle-outline"},
noise_detected: {device_class: "sound"},
@@ -101,14 +121,15 @@ const BINARY_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
temperature_scale: {entity_category: "config", icon: "mdi:temperature-celsius"},
test: {entity_category: "diagnostic", icon: "mdi:test-tube"},
th_heater: {icon: "mdi:heat-wave"},
tilt_mode: {entity_category: "config", icon: "mdi:angle-acute"},
trigger_indicator: {icon: "mdi:led-on"},
valve_alarm: {device_class: "problem"},
valve_detection: {icon: "mdi:pipe-valve"},
valve_detection: {entity_category: "config", icon: "mdi:pipe-valve"},
valve_state: {device_class: "opening"},
vibration: {device_class: "vibration"},
water_leak: {device_class: "moisture"},
window: {device_class: "window"},
window_detection: {icon: "mdi:window-open-variant"},
window_detection: {entity_category: "config", icon: "mdi:window-open-variant"},
window_open: {device_class: "window"},
} as const;
const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
@@ -120,7 +141,7 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
alarm_temperature_min: {device_class: "temperature", entity_category: "config", icon: "mdi:thermometer-low"},
angle: {icon: "angle-acute"},
angle_axis: {icon: "angle-acute"},
apparent_temperature: {icon: "mdi:thermometer-lines", state_class: "measurement"},
apparent_temperature: {device_class: "temperature", icon: "mdi:thermometer-lines", preserve_name: true, state_class: "measurement"},
aqi: {device_class: "aqi", state_class: "measurement"},
auto_relock_time: {entity_category: "config", icon: "mdi:timer"},
away_preset_days: {entity_category: "config", icon: "mdi:timer"},
@@ -137,8 +158,14 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
boost_time: {entity_category: "config", icon: "mdi:timer"},
calibration: {entity_category: "config", icon: "mdi:wrench-clock"},
calibration_time: {entity_category: "config", icon: "mdi:wrench-clock"},
calibration_time_left: {entity_category: "config", icon: "mdi:wrench-clock"},
calibration_time_right: {entity_category: "config", icon: "mdi:wrench-clock"},
co2: {device_class: "carbon_dioxide", state_class: "measurement"},
comfort_humidity_max: {device_class: "humidity", entity_category: "config", icon: "mdi:water-percent"},
comfort_humidity_min: {device_class: "humidity", entity_category: "config", icon: "mdi:water-percent"},
comfort_temperature: {entity_category: "config", icon: "mdi:thermometer"},
comfort_temperature_max: {device_class: "temperature", entity_category: "config", icon: "mdi:thermometer-high"},
comfort_temperature_min: {device_class: "temperature", entity_category: "config", icon: "mdi:thermometer-low"},
cpu_temperature: {
device_class: "temperature",
entity_category: "diagnostic",
@@ -149,13 +176,14 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
current_phase_b: {device_class: "current", state_class: "measurement"},
current_phase_c: {device_class: "current", state_class: "measurement"},
deadzone_temperature: {entity_category: "config", icon: "mdi:thermometer"},
detection_delay: {entity_category: "config", icon: "mdi:timer"},
detection_interval: {icon: "mdi:timer"},
device_temperature: {
device_class: "temperature",
entity_category: "diagnostic",
state_class: "measurement",
},
dew_point: {icon: "mdi:thermometer-water", state_class: "measurement"},
dew_point: {device_class: "temperature", icon: "mdi:thermometer-water", preserve_name: true, state_class: "measurement"},
distance: {device_class: "distance", state_class: "measurement"},
duration: {entity_category: "config", icon: "mdi:timer"},
eco2: {device_class: "volatile_organic_compounds_parts", state_class: "measurement"},
@@ -164,14 +192,15 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
external_temperature_input: {device_class: "temperature", icon: "mdi:thermometer"},
external_temperature: {device_class: "temperature", icon: "mdi:thermometer", state_class: "measurement"},
external_humidity: {device_class: "humidity", icon: "mdi:water-percent", state_class: "measurement"},
fading_time: {entity_category: "config", icon: "mdi:timer"},
formaldehyd: {state_class: "measurement"},
flow: {device_class: "volume_flow_rate", state_class: "measurement"},
gas: {device_class: "gas", state_class: "total_increasing", icon: "mdi:meter-gas"},
gas_density: {icon: "mdi:google-circles-communities", state_class: "measurement"},
gust_speed: {icon: "mdi:weather-windy-variant", state_class: "measurement"},
gust_speed: {device_class: "wind_speed", icon: "mdi:weather-windy-variant", preserve_name: true, state_class: "measurement"},
hcho: {icon: "mdi:air-filter", state_class: "measurement"},
heat_stress: {icon: "mdi:weather-sunny-alert", state_class: "measurement"},
humidex: {icon: "mdi:thermometer-alert", state_class: "measurement"},
humidex: {device_class: "temperature", icon: "mdi:thermometer-alert", preserve_name: true, state_class: "measurement"},
humidity: {device_class: "humidity", state_class: "measurement"},
humidity_calibration: {entity_category: "config", icon: "mdi:wrench-clock"},
humidity_max: {entity_category: "config", icon: "mdi:water-percent"},
@@ -179,6 +208,7 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
illuminance_calibration: {entity_category: "config", icon: "mdi:wrench-clock"},
illuminance: {device_class: "illuminance", state_class: "measurement"},
illuminance_raw: {state_class: "measurement"},
interval_time: {entity_category: "config", icon: "mdi:timer"},
internalTemperature: {
device_class: "temperature",
entity_category: "diagnostic",
@@ -192,13 +222,20 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
},
load_estimate: {state_class: "measurement"},
local_temperature: {device_class: "temperature", state_class: "measurement"},
large_motion_detection_distance: {entity_category: "config", icon: "mdi:signal-distance-variant"},
large_motion_detection_sensitivity: {entity_category: "config", icon: "mdi:motion-sensor"},
max_range: {entity_category: "config", icon: "mdi:signal-distance-variant"},
max_temperature: {entity_category: "config", icon: "mdi:thermometer-high"},
max_temperature_limit: {entity_category: "config", icon: "mdi:thermometer-high"},
maximum_range: {entity_category: "config", icon: "mdi:signal-distance-variant"},
measurement_interval: {entity_category: "config", icon: "mdi:clock-out"},
min_temperature_limit: {entity_category: "config", icon: "mdi:thermometer-low"},
min_temperature: {entity_category: "config", icon: "mdi:thermometer-low"},
minimum_range: {entity_category: "config", icon: "mdi:signal-distance-variant"},
minimum_on_level: {entity_category: "config"},
measurement_poll_interval: {entity_category: "config", icon: "mdi:clock-out"},
medium_motion_detection_distance: {entity_category: "config", icon: "mdi:signal-distance-variant"},
medium_motion_detection_sensitivity: {entity_category: "config", icon: "mdi:motion-sensor"},
motion_sensitivity: {entity_category: "config", icon: "mdi:motion-sensor"},
noise: {device_class: "sound_pressure", state_class: "measurement"},
noise_detect_level: {icon: "mdi:volume-equal"},
@@ -234,13 +271,21 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
icon: "mdi:brightness-5",
},
smoke_density: {icon: "mdi:google-circles-communities", state_class: "measurement"},
sensitivity: {entity_category: "config", icon: "mdi:tune"},
small_detection_distance: {entity_category: "config", icon: "mdi:signal-distance-variant"},
small_detection_sensitivity: {entity_category: "config", icon: "mdi:motion-sensor"},
soil_calibration: {entity_category: "config", icon: "mdi:wrench-clock"},
soil_fertility: {device_class: "conductivity", state_class: "measurement"},
soil_moisture: {device_class: "moisture", state_class: "measurement"},
soil_sampling: {entity_category: "config", icon: "mdi:clock-out"},
soil_warning: {entity_category: "config", icon: "mdi:water-percent-alert"},
temperature: {device_class: "temperature", state_class: "measurement"},
temperature_probe: {device_class: "temperature", state_class: "measurement"},
temperature_calibration: {entity_category: "config", icon: "mdi:wrench-clock"},
temperature_max: {entity_category: "config", icon: "mdi:thermometer-plus"},
temperature_min: {entity_category: "config", icon: "mdi:thermometer-minus"},
temperature_offset: {icon: "mdi:thermometer-lines"},
temperature_sampling: {entity_category: "config", icon: "mdi:clock-out"},
transition: {entity_category: "config", icon: "mdi:transition"},
trigger_count: {icon: "mdi:counter", enabled_by_default: false, state_class: "measurement"},
uv_index: {icon: "mdi:white-balance-sunny", state_class: "measurement"},
@@ -255,7 +300,7 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
device_class: "water",
state_class: "total_increasing",
},
wind_chill: {icon: "mdi:snowflake-thermometer", state_class: "measurement"},
wind_chill: {device_class: "temperature", icon: "mdi:snowflake-thermometer", preserve_name: true, state_class: "measurement"},
wind_direction: {icon: "mdi:compass-outline", state_class: "measurement"},
wind_speed: {device_class: "wind_speed", icon: "mdi:weather-windy", state_class: "measurement"},
x: {icon: "mdi:axis-x-arrow", state_class: "measurement"},
@@ -287,6 +332,7 @@ const ENUM_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
mode_phase_control: {entity_category: "config", icon: "mdi:tune"},
mode: {entity_category: "config", icon: "mdi:tune"},
mode_switch: {icon: "mdi:tune"},
motor_direction: {entity_category: "config", icon: "mdi:arrow-left-right"},
motion_sensitivity: {entity_category: "config", icon: "mdi:tune"},
operation_mode: {entity_category: "config", icon: "mdi:tune"},
power_on_behavior: {entity_category: "config", icon: "mdi:power-settings"},
@@ -297,11 +343,13 @@ const ENUM_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
sensitivity: {entity_category: "config", icon: "mdi:tune"},
sensor: {icon: "mdi:tune"},
sensors_type: {entity_category: "config", icon: "mdi:tune"},
set_limits: {entity_category: "config", icon: "mdi:ray-start-end"},
sound_volume: {entity_category: "config", icon: "mdi:volume-high"},
status: {icon: "mdi:state-machine"},
switch_type: {entity_category: "config", icon: "mdi:tune"},
temperature_display_mode: {entity_category: "config", icon: "mdi:thermometer"},
temperature_sensor_select: {entity_category: "config", icon: "mdi:home-thermometer"},
temperature_unit: {entity_category: "config", icon: "mdi:temperature-celsius"},
thermostat_unit: {entity_category: "config", icon: "mdi:thermometer"},
update: {device_class: "update"},
volume: {entity_category: "config", icon: "mdi: volume-high"},
@@ -313,7 +361,7 @@ const LIST_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
color_options: {icon: "mdi:palette"},
level_config: {entity_category: "diagnostic"},
programming_mode: {icon: "mdi:calendar-clock"},
schedule_settings: {icon: "mdi:calendar-clock"},
schedule_settings: {entity_category: "config", icon: "mdi:calendar-clock"},
} as const;
const featurePropertyWithoutEndpoint = (feature: zhc.Feature): string => {
@@ -324,6 +372,32 @@ const featurePropertyWithoutEndpoint = (feature: zhc.Feature): string => {
return feature.property;
};
const applyHomeAssistantExposeMetadata = (payload: DiscoveryEntry, homeAssistant: zhc.Expose["homeassistant"]): void => {
if (!homeAssistant) {
return;
}
if (homeAssistant.type !== undefined) {
payload.type = homeAssistant.type;
}
if (homeAssistant.entityCategory !== undefined) {
payload.discovery_payload.entity_category = homeAssistant.entityCategory;
}
if (homeAssistant.deviceClass !== undefined) {
payload.discovery_payload.device_class = homeAssistant.deviceClass;
}
if (homeAssistant.enabledByDefault !== undefined) {
payload.discovery_payload.enabled_by_default = homeAssistant.enabledByDefault;
}
if (homeAssistant.icon !== undefined) {
payload.discovery_payload.icon = homeAssistant.icon;
}
};
/**
* This class handles the bridge entity configuration for Home Assistant Discovery.
*/
@@ -590,7 +664,7 @@ export class HomeAssistant extends Extension {
name: endpointName ? utils.capitalize(endpointName) : null,
payload_off: state.value_off,
payload_on: state.value_on,
value_template: `{{ value_json.${property} }}`,
value_template: `{{ value_json["${property}"] }}`,
command_topic: true,
command_topic_prefix: endpointName,
},
@@ -602,25 +676,26 @@ export class HomeAssistant extends Extension {
discoveryEntry.discovery_payload.state_off = state.value_off;
discoveryEntry.discovery_payload.state_on = state.value_on;
discoveryEntry.object_id = property;
if (property === "window_detection") {
discoveryEntry.discovery_payload.icon = "mdi:window-open-variant";
}
Object.assign(discoveryEntry.discovery_payload, CONFIG_SWITCH_DISCOVERY_LOOKUP[property]);
}
discoveryEntries.push(discoveryEntry);
break;
}
case "climate": {
const setpointProperties = ["occupied_heating_setpoint", "current_heating_setpoint"];
const setpoint = (firstExpose as zhc.Climate).features.filter(isNumericExpose).find((f) => setpointProperties.includes(f.name));
const heatingSetpoint = (firstExpose as zhc.Climate).features
.filter(isNumericExpose)
.find((f) => ["occupied_heating_setpoint", "current_heating_setpoint"].includes(f.name));
const coolingSetpoint = (firstExpose as zhc.Climate).features
.filter(isNumericExpose)
.find((f) => f.name === "occupied_cooling_setpoint");
const primarySetpoint = heatingSetpoint ?? coolingSetpoint;
assert(
setpoint && setpoint.value_min !== undefined && setpoint.value_max !== undefined,
primarySetpoint && primarySetpoint.value_min !== undefined && primarySetpoint.value_max !== undefined,
"No setpoint found or it is missing value_min/max",
);
const temperature = (firstExpose as zhc.Climate).features.find((f) => f.name === "local_temperature");
assert(temperature, "No temperature found");
const discoveryEntry: DiscoveryEntry = {
type: "climate",
object_id: endpointName ? `climate_${endpointName}` : "climate",
@@ -631,12 +706,12 @@ export class HomeAssistant extends Extension {
state_topic: false,
temperature_unit: "C",
// Setpoint
temp_step: setpoint.value_step,
min_temp: setpoint.value_min.toString(),
max_temp: setpoint.value_max.toString(),
temp_step: primarySetpoint.value_step,
min_temp: primarySetpoint.value_min.toString(),
max_temp: primarySetpoint.value_max.toString(),
// Temperature
current_temperature_topic: true,
current_temperature_template: `{{ value_json.${temperature.property} }}`,
current_temperature_template: `{{ value_json["${temperature.property}"] }}`,
command_topic_prefix: endpointName,
},
};
@@ -650,7 +725,7 @@ export class HomeAssistant extends Extension {
mode.values.splice(mode.values.indexOf("sleep"), 1);
}
discoveryEntry.discovery_payload.mode_state_topic = true;
discoveryEntry.discovery_payload.mode_state_template = `{{ value_json.${mode.property} }}`;
discoveryEntry.discovery_payload.mode_state_template = `{{ value_json["${mode.property}"] }}`;
discoveryEntry.discovery_payload.modes = mode.values;
discoveryEntry.discovery_payload.mode_command_topic = true;
}
@@ -659,20 +734,19 @@ export class HomeAssistant extends Extension {
if (state) {
discoveryEntry.mockProperties.push({property: state.property, value: null});
discoveryEntry.discovery_payload.action_topic = true;
discoveryEntry.discovery_payload.action_template = `{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.${state.property}] }}`;
discoveryEntry.discovery_payload.action_template = `{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json["${state.property}"]] }}`;
}
const coolingSetpoint = (firstExpose as zhc.Climate).features.find((f) => f.name === "occupied_cooling_setpoint");
if (coolingSetpoint) {
discoveryEntry.discovery_payload.temperature_low_command_topic = setpoint.name;
discoveryEntry.discovery_payload.temperature_low_state_template = `{{ value_json.${setpoint.property} }}`;
if (heatingSetpoint && coolingSetpoint) {
discoveryEntry.discovery_payload.temperature_low_command_topic = heatingSetpoint.name;
discoveryEntry.discovery_payload.temperature_low_state_template = `{{ value_json["${heatingSetpoint.property}"] }}`;
discoveryEntry.discovery_payload.temperature_low_state_topic = true;
discoveryEntry.discovery_payload.temperature_high_command_topic = coolingSetpoint.name;
discoveryEntry.discovery_payload.temperature_high_state_template = `{{ value_json.${coolingSetpoint.property} }}`;
discoveryEntry.discovery_payload.temperature_high_state_template = `{{ value_json["${coolingSetpoint.property}"] }}`;
discoveryEntry.discovery_payload.temperature_high_state_topic = true;
} else {
discoveryEntry.discovery_payload.temperature_command_topic = setpoint.name;
discoveryEntry.discovery_payload.temperature_state_template = `{{ value_json.${setpoint.property} }}`;
discoveryEntry.discovery_payload.temperature_command_topic = primarySetpoint.name;
discoveryEntry.discovery_payload.temperature_state_template = `{{ value_json["${primarySetpoint.property}"] }}`;
discoveryEntry.discovery_payload.temperature_state_topic = true;
}
@@ -680,7 +754,7 @@ export class HomeAssistant extends Extension {
if (fanMode) {
discoveryEntry.discovery_payload.fan_modes = fanMode.values;
discoveryEntry.discovery_payload.fan_mode_command_topic = true;
discoveryEntry.discovery_payload.fan_mode_state_template = `{{ value_json.${fanMode.property} }}`;
discoveryEntry.discovery_payload.fan_mode_state_template = `{{ value_json["${fanMode.property}"] }}`;
discoveryEntry.discovery_payload.fan_mode_state_topic = true;
}
@@ -688,7 +762,7 @@ export class HomeAssistant extends Extension {
if (swingMode) {
discoveryEntry.discovery_payload.swing_modes = swingMode.values;
discoveryEntry.discovery_payload.swing_mode_command_topic = true;
discoveryEntry.discovery_payload.swing_mode_state_template = `{{ value_json.${swingMode.property} }}`;
discoveryEntry.discovery_payload.swing_mode_state_template = `{{ value_json["${swingMode.property}"] }}`;
discoveryEntry.discovery_payload.swing_mode_state_topic = true;
}
@@ -696,7 +770,7 @@ export class HomeAssistant extends Extension {
if (preset) {
discoveryEntry.discovery_payload.preset_modes = preset.values;
discoveryEntry.discovery_payload.preset_mode_command_topic = "preset";
discoveryEntry.discovery_payload.preset_mode_value_template = `{{ value_json.${preset.property} }}`;
discoveryEntry.discovery_payload.preset_mode_value_template = `{{ value_json["${preset.property}"] }}`;
discoveryEntry.discovery_payload.preset_mode_state_topic = true;
}
@@ -710,7 +784,7 @@ export class HomeAssistant extends Extension {
mockProperties: [{property: tempCalibration.property, value: null}],
discovery_payload: {
name: endpointName ? `${tempCalibration.label} ${endpointName}` : tempCalibration.label,
value_template: `{{ value_json.${tempCalibration.property} }}`,
value_template: `{{ value_json["${tempCalibration.property}"] }}`,
command_topic: true,
command_topic_prefix: endpointName,
command_topic_postfix: tempCalibration.property,
@@ -735,7 +809,7 @@ export class HomeAssistant extends Extension {
mockProperties: [{property: piHeatingDemand.property, value: null}],
discovery_payload: {
name: endpointName ? `${piHeatingDemand.label} ${endpointName}` : piHeatingDemand.label,
value_template: `{{ value_json.${piHeatingDemand.property} }}`,
value_template: `{{ value_json["${piHeatingDemand.property}"] }}`,
...(piHeatingDemand.unit && {unit_of_measurement: piHeatingDemand.unit}),
icon: "mdi:radiator",
},
@@ -766,7 +840,7 @@ export class HomeAssistant extends Extension {
mockProperties: [{property: piCoolingDemand.property, value: null}],
discovery_payload: {
name: endpointName ? /* v8 ignore next */ `${piCoolingDemand.label} ${endpointName}` : piCoolingDemand.label,
value_template: `{{ value_json.${piCoolingDemand.property} }}`,
value_template: `{{ value_json["${piCoolingDemand.property}"] }}`,
...(piCoolingDemand.unit && {unit_of_measurement: piCoolingDemand.unit}),
entity_category: "diagnostic",
icon: "mdi:air-conditioner",
@@ -788,7 +862,7 @@ export class HomeAssistant extends Extension {
mockProperties: [{property: localTemperature.property, value: null}],
discovery_payload: {
name: endpointName ? `${localTemperature.label} ${endpointName}` : localTemperature.label,
value_template: `{{ value_json.${localTemperature.property} }}`,
value_template: `{{ value_json["${localTemperature.property}"] }}`,
...(localTemperature.unit && {unit_of_measurement: localTemperature.unit}),
device_class: "temperature",
state_class: "measurement",
@@ -800,7 +874,7 @@ export class HomeAssistant extends Extension {
const currentHumidity = allExposes?.filter(isNumericExpose).find((e) => e.name === "humidity" && e.access & ACCESS_STATE);
if (currentHumidity) {
discoveryEntry.discovery_payload.current_humidity_template = `{{ value_json.${currentHumidity.property} }}`;
discoveryEntry.discovery_payload.current_humidity_template = `{{ value_json["${currentHumidity.property}"] }}`;
discoveryEntry.discovery_payload.current_humidity_topic = true;
}
@@ -820,7 +894,7 @@ export class HomeAssistant extends Extension {
name: endpointName ? utils.capitalize(endpointName) : null,
command_topic_prefix: endpointName,
command_topic: true,
value_template: `{{ value_json.${state.property} }}`,
value_template: `{{ value_json["${state.property}"] }}`,
state_locked: state.value_on,
state_unlocked: state.value_off,
/* v8 ignore next */
@@ -863,7 +937,7 @@ export class HomeAssistant extends Extension {
// The movement direction is calculated (assumed) in this case.
if (running) {
assert(position, `Cover must have 'position' when it has 'running'`);
discoveryEntry.discovery_payload.value_template = `{% if "${featurePropertyWithoutEndpoint(running)}" in value_json and value_json.${featurePropertyWithoutEndpoint(running)} %} {% if value_json.${featurePropertyWithoutEndpoint(position)} > 0 %} closing {% else %} opening {% endif %} {% else %} stopped {% endif %}`;
discoveryEntry.discovery_payload.value_template = `{% if "${featurePropertyWithoutEndpoint(running)}" in value_json and value_json["${featurePropertyWithoutEndpoint(running)}"] %} {% if value_json["${featurePropertyWithoutEndpoint(position)}"] > 0 %} closing {% else %} opening {% endif %} {% else %} stopped {% endif %}`;
}
// If curtains have `motor_state` or `moving` property, lookup for possible
@@ -877,13 +951,13 @@ export class HomeAssistant extends Extension {
discoveryEntry.discovery_payload.state_opening = openingState;
discoveryEntry.discovery_payload.state_closing = closingState;
discoveryEntry.discovery_payload.state_stopped = stoppedState;
discoveryEntry.discovery_payload.value_template = `{% if "${featurePropertyWithoutEndpoint(motorState)}" in value_json and value_json.${featurePropertyWithoutEndpoint(motorState)} %} {{ value_json.${featurePropertyWithoutEndpoint(motorState)} }} {% else %} ${stoppedState} {% endif %}`;
discoveryEntry.discovery_payload.value_template = `{% if "${featurePropertyWithoutEndpoint(motorState)}" in value_json and value_json["${featurePropertyWithoutEndpoint(motorState)}"] %} {{ value_json["${featurePropertyWithoutEndpoint(motorState)}"] }} {% else %} ${stoppedState} {% endif %}`;
}
}
// If curtains do not have `running`, `motor_state` or `moving` properties.
if (!discoveryEntry.discovery_payload.value_template) {
discoveryEntry.discovery_payload.value_template = `{{ value_json.${featurePropertyWithoutEndpoint(state)} }}`;
discoveryEntry.discovery_payload.value_template = `{{ value_json["${featurePropertyWithoutEndpoint(state)}"] }}`;
discoveryEntry.discovery_payload.state_open = "OPEN";
discoveryEntry.discovery_payload.state_closed = "CLOSE";
discoveryEntry.discovery_payload.state_stopped = "STOP";
@@ -898,7 +972,7 @@ export class HomeAssistant extends Extension {
if (position) {
discoveryEntry.discovery_payload = {
...discoveryEntry.discovery_payload,
position_template: `{{ value_json.${featurePropertyWithoutEndpoint(position)} }}`,
position_template: `{{ value_json["${featurePropertyWithoutEndpoint(position)}"] }}`,
set_position_template: `{ "${getProperty(position)}": {{ position }} }`,
set_position_topic: true,
position_topic: true,
@@ -910,7 +984,7 @@ export class HomeAssistant extends Extension {
...discoveryEntry.discovery_payload,
tilt_command_topic: true,
tilt_status_topic: true,
tilt_status_template: `{{ value_json.${featurePropertyWithoutEndpoint(tilt)} }}`,
tilt_status_template: `{{ value_json["${featurePropertyWithoutEndpoint(tilt)}"] }}`,
};
}
@@ -975,14 +1049,14 @@ export class HomeAssistant extends Extension {
discoveryEntry.discovery_payload.percentage_state_topic = true;
discoveryEntry.discovery_payload.percentage_command_topic = "fan_mode";
discoveryEntry.discovery_payload.percentage_value_template = `{{ {${percentValues}}[value_json.${modeEmulatedSpeed.property}] | default('None') }}`;
discoveryEntry.discovery_payload.percentage_value_template = `{{ {${percentValues}}[value_json["${modeEmulatedSpeed.property}"]] | default('None') }}`;
discoveryEntry.discovery_payload.percentage_command_template = `{{ {${percentCommands}}[value] | default('') }}`;
discoveryEntry.discovery_payload.speed_range_min = 1;
discoveryEntry.discovery_payload.speed_range_max = speeds.length - 1;
assert(presets.length !== 0);
discoveryEntry.discovery_payload.preset_mode_state_topic = true;
discoveryEntry.discovery_payload.preset_mode_command_topic = "fan_mode";
discoveryEntry.discovery_payload.preset_mode_value_template = `{{ value_json.${modeEmulatedSpeed.property} if value_json.${modeEmulatedSpeed.property} in [${presetList}] else 'None' | default('None') }}`;
discoveryEntry.discovery_payload.preset_mode_value_template = `{{ value_json["${modeEmulatedSpeed.property}"] if value_json["${modeEmulatedSpeed.property}"] in [${presetList}] else 'None' | default('None') }}`;
discoveryEntry.discovery_payload.preset_modes = presets;
// Emulate state based on mode
@@ -991,7 +1065,7 @@ export class HomeAssistant extends Extension {
} else if (nativeSpeed) {
discoveryEntry.discovery_payload.percentage_state_topic = true;
discoveryEntry.discovery_payload.percentage_command_topic = "speed";
discoveryEntry.discovery_payload.percentage_value_template = `{{ value_json.${nativeSpeed.property} | default('None') }}`;
discoveryEntry.discovery_payload.percentage_value_template = `{{ value_json["${nativeSpeed.property}"] | default('None') }}`;
discoveryEntry.discovery_payload.percentage_command_template = `{{ value | default('') }}`;
discoveryEntry.discovery_payload.speed_range_min = nativeSpeed.value_min;
discoveryEntry.discovery_payload.speed_range_max = nativeSpeed.value_max;
@@ -1021,8 +1095,8 @@ export class HomeAssistant extends Extension {
name: endpointName ? /* v8 ignore next */ `${firstExpose.label} ${endpointName}` : firstExpose.label,
value_template:
typeof firstExpose.value_on === "boolean"
? `{% if value_json.${firstExpose.property} %}true{% else %}false{% endif %}`
: `{{ value_json.${firstExpose.property} }}`,
? `{% if value_json["${firstExpose.property}"] %}true{% else %}false{% endif %}`
: `{{ value_json["${firstExpose.property}"] }}`,
payload_on: firstExpose.value_on.toString(),
payload_off: firstExpose.value_off.toString(),
command_topic: true,
@@ -1040,7 +1114,7 @@ export class HomeAssistant extends Extension {
mockProperties: [{property: firstExpose.property, value: null}],
discovery_payload: {
name: endpointName ? /* v8 ignore next */ `${firstExpose.label} ${endpointName}` : firstExpose.label,
value_template: `{{ value_json.${firstExpose.property} }}`,
value_template: `{{ value_json["${firstExpose.property}"] }}`,
payload_on: firstExpose.value_on,
payload_off: firstExpose.value_off,
...(BINARY_DISCOVERY_LOOKUP[firstExpose.name] || {}),
@@ -1065,7 +1139,7 @@ export class HomeAssistant extends Extension {
mockProperties: [{property: firstExpose.property, value: null}],
discovery_payload: {
name: endpointName ? `${firstExpose.label} ${endpointName}` : firstExpose.label,
value_template: `{{ value_json.${firstExpose.property} }}`,
value_template: `{{ value_json["${firstExpose.property}"] }}`,
command_topic: true,
command_topic_prefix: endpointName,
command_topic_postfix: firstExpose.property,
@@ -1116,7 +1190,7 @@ export class HomeAssistant extends Extension {
mockProperties: [{property: firstExpose.property, value: null}],
discovery_payload: {
name: endpointName ? `${firstExpose.label} ${endpointName}` : firstExpose.label,
value_template: `{{ value_json.${firstExpose.property} }}`,
value_template: `{{ value_json["${firstExpose.property}"] }}`,
enabled_by_default: !allowsSet,
...(firstExpose.unit && {unit_of_measurement: firstExpose.unit}),
...NUMERIC_DISCOVERY_LOOKUP[key],
@@ -1170,7 +1244,7 @@ export class HomeAssistant extends Extension {
}
}
const valueTemplate = firstExpose.access & ACCESS_STATE ? `{{ value_json.${firstExpose.property} }}` : undefined;
const valueTemplate = firstExpose.access & ACCESS_STATE ? `{{ value_json["${firstExpose.property}"] }}` : undefined;
/**
* If enum has only one item and has SET access then expose as BUTTON entity.
@@ -1226,6 +1300,7 @@ export class HomeAssistant extends Extension {
discovery_payload: {
name: endpointName ? `${firstExpose.label} ${endpointName}` : firstExpose.label,
value_template: valueTemplate,
...(firstExpose.property === "action" ? {entity_category: "diagnostic"} : {}),
...ENUM_DISCOVERY_LOOKUP[firstExpose.name],
},
});
@@ -1299,7 +1374,7 @@ export class HomeAssistant extends Extension {
discovery_payload: {
name: endpointName ? `${firstExposeTyped.label} ${endpointName}` : firstExposeTyped.label,
state_topic: firstExposeTyped.access & ACCESS_STATE,
value_template: `{{ value_json.${firstExposeTyped.property} }}`,
value_template: `{{ value_json["${firstExposeTyped.property}"] }}`,
command_topic_prefix: endpointName,
command_topic: true,
command_topic_postfix: firstExposeTyped.property,
@@ -1317,7 +1392,7 @@ export class HomeAssistant extends Extension {
name: endpointName ? `${firstExposeTyped.label} ${endpointName}` : firstExposeTyped.label,
// Truncate text if it's too long
// https://github.com/Koenkk/zigbee2mqtt/issues/23199
value_template: `{{ value_json.${firstExposeTyped.property} | default('',True) | string | truncate(254, True, '', 0) }}`,
value_template: `{{ value_json["${firstExposeTyped.property}"] | default('',True) | string | truncate(254, True, '', 0) }}`,
...LIST_DISCOVERY_LOOKUP[firstExposeTyped.name],
},
});
@@ -1335,6 +1410,8 @@ export class HomeAssistant extends Extension {
}
for (const entry of discoveryEntries) {
applyHomeAssistantExposeMetadata(entry, firstExpose.homeassistant);
// If a sensor has entity category `config`, then change
// it to `diagnostic`. Sensors have no input, so can't be configured.
// https://github.com/Koenkk/zigbee2mqtt/pull/19474
@@ -1347,8 +1424,9 @@ export class HomeAssistant extends Extension {
delete entry.discovery_payload.entity_category;
}
// Let Home Assistant generate entity name when device_class is present
if (entry.discovery_payload.device_class) {
// Let Home Assistant generate entity name when device_class is present.
// preserve_name allows device_class and explicit name to coexist (e.g. derived sensors).
if (entry.discovery_payload.device_class && !NUMERIC_DISCOVERY_LOOKUP[firstExpose.name]?.preserve_name) {
delete entry.discovery_payload.name;
}
@@ -1403,7 +1481,7 @@ export class HomeAssistant extends Extension {
if (match) {
const endpoint = match[1];
const endpointRegExp = new RegExp(`(.*)_${endpoint}`);
const endpointRegExp = new RegExp(`(.*)_${endpoint}$`);
const payload: KeyValue = {};
for (const key of Object.keys(data.message)) {
const keyMatch = endpointRegExp.exec(key);
@@ -1814,7 +1892,16 @@ export class HomeAssistant extends Extension {
payload.current_humidity_topic = stateTopic;
}
// Override configuration with user settings.
if (entity.isDevice()) {
try {
entity.definition?.meta?.overrideHaDiscoveryPayload?.(payload, entity.options);
} catch (error) {
logger.error(`Failed to override HA discovery payload (${(error as Error).stack})`);
}
}
// Override configuration with user settings after converter compatibility
// mappings, so per-device configuration remains the final authority.
if (entity.options.homeassistant != null) {
const add = (obj: KeyValue, ignoreName: boolean): void => {
for (const key in obj) {
@@ -1845,14 +1932,6 @@ export class HomeAssistant extends Extension {
}
}
if (entity.isDevice()) {
try {
entity.definition?.meta?.overrideHaDiscoveryPayload?.(payload);
} catch (error) {
logger.error(`Failed to override HA discovery payload (${(error as Error).stack})`);
}
}
const topic = this.getDiscoveryTopic(config, entity);
const payloadStr = stringify(payload);
newDiscoveredTopics.add(topic);
@@ -1938,6 +2017,9 @@ export class HomeAssistant extends Extension {
}
} else if (data.topic === this.statusTopic && data.message.toLowerCase() === "online") {
const timer = setTimeout(async () => {
// Re-publish bridge state so HA marks all entities as available before receiving cached device states.
await this.mqtt.publish("bridge/state", stringify({state: "online"}), {clientOptions: {retain: true, qos: 1}});
// Publish all device states.
for (const entity of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
if (this.state.exists(entity)) {
+29 -12
View File
@@ -50,7 +50,7 @@ function writeFirmwareHexToDataDir(hex: string, fileName: string | undefined, de
export default class OTAUpdate extends Extension {
#topicRegex = new RegExp(
`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check|schedule|unschedule)/?(downgrade)?`,
`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check|schedule|unschedule)/?(downgrade|abort)?`,
"i",
);
#inProgress = new Set<string>();
@@ -248,6 +248,7 @@ export default class OTAUpdate extends Extension {
| Zigbee2MQTTAPI["bridge/request/device/ota_update/check/downgrade"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/update"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/update/downgrade"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/update/abort"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/schedule"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/schedule/downgrade"]
| Zigbee2MQTTAPI["bridge/request/device/ota_update/unschedule"];
@@ -258,18 +259,31 @@ export default class OTAUpdate extends Extension {
assert(message.id, "Invalid payload");
}
const ID = (messageObject ? message.id : message) as string;
const device = this.zigbee.resolveEntity(ID);
const id = (messageObject ? message.id : message) as string;
const device = this.zigbee.resolveEntity(id);
const type = topicMatch[1] as "check" | "update" | "schedule" | "unschedule";
const downgrade = topicMatch[2] === "downgrade";
const abort = topicMatch[2] === "abort";
let error: string | undefined;
let errorStack: string | undefined;
if (!(device instanceof Device)) {
error = `Device '${ID}' does not exist`;
error = `Device '${id}' does not exist`;
} else if (this.#inProgress.has(device.ieeeAddr)) {
// also guards against scheduling while check/update op in progress that could result in undesired OTA state
error = `OTA update or check for update already in progress for '${device.name}'`;
if (abort) {
device.zh.abortOta();
this.#inProgress.delete(device.ieeeAddr);
// cleanup same as a fail
this.#removeProgressAndRemainingFromState(device);
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "available"));
await this.mqtt.publish(
"bridge/response/device/ota_update/update/abort",
stringify(utils.getResponse<"bridge/response/device/ota_update/update/abort">(message, {id})),
);
} else {
// also guards against scheduling while check/update op in progress that could result in undesired OTA state
error = `OTA update or check for update already in progress for '${device.name}'`;
}
} else {
switch (type) {
case "check": {
@@ -304,7 +318,7 @@ export default class OTAUpdate extends Extension {
this.#lastChecked.set(device.ieeeAddr, Date.now());
const response = utils.getResponse<"bridge/response/device/ota_update/check">(message, {
id: ID,
id,
update_available: availableResult.available,
downgrade: source.downgrade,
source: availableResult.availableMeta?.url,
@@ -321,6 +335,11 @@ export default class OTAUpdate extends Extension {
}
case "update": {
if (abort) {
error = `No OTA in progress to abort for device '${device.name}'`;
break;
}
this.#inProgress.add(device.ieeeAddr);
const otaSettings = settings.get().ota;
@@ -378,7 +397,7 @@ export default class OTAUpdate extends Extension {
const firmwareTo = await this.#readSoftwareBuildIDAndDateCode(device);
const response = utils.getResponse<"bridge/response/device/ota_update/update">(message, {
id: ID,
id,
from: {
file_version: fromVersion,
software_build_id: firmwareFrom?.softwareBuildID,
@@ -427,7 +446,7 @@ export default class OTAUpdate extends Extension {
device.zh.scheduleOta(source);
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "scheduled"));
const response = utils.getResponse<"bridge/response/device/ota_update/schedule">(message, {id: ID, url: source.url});
const response = utils.getResponse<"bridge/response/device/ota_update/schedule">(message, {id, url: source.url});
await this.mqtt.publish("bridge/response/device/ota_update/schedule", stringify(response));
@@ -442,9 +461,7 @@ export default class OTAUpdate extends Extension {
device.zh.unscheduleOta();
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "idle"));
const response = utils.getResponse<"bridge/response/device/ota_update/unschedule">(message, {
id: ID,
});
const response = utils.getResponse<"bridge/response/device/ota_update/unschedule">(message, {id});
await this.mqtt.publish("bridge/response/device/ota_update/unschedule", stringify(response));
+5
View File
@@ -109,6 +109,11 @@ export default class Mqtt {
options.rejectUnauthorized = false;
}
if (mqttSettings.server_name) {
logger.debug(`MQTT SSL/TLS: SNI server name = ${mqttSettings.server_name}`);
options.servername = mqttSettings.server_name;
}
this.client = await connectAsync(mqttSettings.server, options);
// https://github.com/Koenkk/zigbee2mqtt/issues/9822
+13
View File
@@ -133,6 +133,7 @@ export interface Zigbee2MQTTSettings {
cert?: string;
client_id?: string;
reject_unauthorized?: boolean;
server_name?: string;
maximum_packet_size: number;
};
serial: {
@@ -215,6 +216,8 @@ export interface Zigbee2MQTTSettings {
timestamp_format: string;
output: "json" | "attribute" | "attribute_and_json";
transmit_power?: number;
/** 3.0: default to false (JSON schema & settings `defaults`) */
enable_external_js: boolean;
};
health: {
/** in minutes */
@@ -675,6 +678,14 @@ export interface Zigbee2MQTTAPI {
default_maximum_data_size?: number | null;
};
"bridge/request/device/ota_update/update/abort": {
id: string;
};
"bridge/response/device/ota_update/update/abort": {
id: string;
};
"bridge/response/device/ota_update/update": {
id: string;
from:
@@ -999,6 +1010,7 @@ export type Zigbee2MQTTRequestEndpoints =
| "bridge/request/device/ota_update/check/downgrade"
| "bridge/request/device/ota_update/update"
| "bridge/request/device/ota_update/update/downgrade"
| "bridge/request/device/ota_update/update/abort"
| "bridge/request/device/ota_update/schedule"
| "bridge/request/device/ota_update/schedule/downgrade"
| "bridge/request/device/ota_update/unschedule"
@@ -1048,6 +1060,7 @@ export type Zigbee2MQTTResponseEndpoints =
| "bridge/response/device/remove"
| "bridge/response/device/ota_update/check"
| "bridge/response/device/ota_update/update"
| "bridge/response/device/ota_update/update/abort"
| "bridge/response/device/ota_update/schedule"
| "bridge/response/device/ota_update/unschedule"
| "bridge/response/device/interview"
-3
View File
@@ -1,8 +1,5 @@
// minimal required because of sub-deps in mqtt >= 5.14.0 to avoid requiring `dom` type
declare global {
// map to node, doesn't really matter, just needs to be there, and the right "type vs value" to avoid lib check problems
/** @deprecated DOM SHIM, DO NOT USE */
type MessagePort = import("node:worker_threads").MessagePort;
/** @deprecated DOM SHIM, DO NOT USE */
type Worker = import("node:worker_threads").Worker;
/** @deprecated DOM SHIM, DO NOT USE */
+1 -1
View File
@@ -6,7 +6,7 @@ declare module "zigbee2mqtt-frontend" {
export default frontend;
}
declare module "http" {
declare module "node:http" {
interface IncomingMessage {
originalUrl?: string;
path?: string;
+26 -3
View File
@@ -33,12 +33,14 @@
"type": "boolean",
"title": "Home Assistant legacy action sensors",
"description": "Home Assistant legacy actions sensor, when enabled a action sensor will be discoverd and an empty `action` will be send after every published action.",
"requiresRestart": true,
"default": false
},
"experimental_event_entities": {
"type": "boolean",
"title": "Home Assistant experimental event entities",
"description": "Home Assistant experimental event entities, when enabled Zigbee2MQTT will add event entities for exposed actions. The events and attributes are currently deemed experimental and subject to change.",
"requiresRestart": true,
"default": false
}
},
@@ -188,6 +190,13 @@
"description": "Disable self-signed SSL certificate",
"default": true
},
"server_name": {
"type": "string",
"title": "TLS server name (SNI)",
"requiresRestart": true,
"description": "Override the TLS SNI / hostname used for certificate verification when it differs from the host in 'server' (e.g. connecting to an internal service DNS name while validating a public certificate SAN). Leave unset to use the hostname from 'server'.",
"examples": ["mqtt.example.com"]
},
"include_device_information": {
"type": "boolean",
"title": "Include device information",
@@ -808,6 +817,13 @@
"title": "MQTT output type",
"description": "Examples when 'state' of a device is published json: topic: 'zigbee2mqtt/my_bulb' payload '{\"state\": \"ON\"}' attribute: topic 'zigbee2mqtt/my_bulb/state' payload 'ON' attribute_and_json: both json and attribute (see above)",
"default": "json"
},
"enable_external_js": {
"type": "boolean",
"title": "Enable external JS",
"description": "Enable external JavaScript (extensions and converters) that can execute arbitrary user-provided code. WARNING: If unused, it is advised to disable this.",
"default": true,
"requiresRestart": true
}
}
},
@@ -848,7 +864,8 @@
"retain": {
"type": "boolean",
"title": "Retain",
"description": "Retain MQTT messages of this device"
"description": "Retain MQTT messages of this device",
"default": false
},
"disabled": {
"type": "boolean",
@@ -953,10 +970,16 @@
"type": "string"
},
"retain": {
"type": "boolean"
"type": "boolean",
"title": "Retain",
"description": "Retain MQTT messages of this group",
"default": false
},
"optimistic": {
"type": "boolean"
"type": "boolean",
"title": "Optimistic",
"description": "Publish the expected state of group members after set",
"default": true
},
"qos": {
"type": ["number"],
+2
View File
@@ -113,6 +113,7 @@ export const defaults = {
network_key: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13],
timestamp_format: "YYYY-MM-DD HH:mm:ss",
output: "json",
enable_external_js: true,
},
health: {
interval: 10,
@@ -167,6 +168,7 @@ export function writeMinimalDefaults(): void {
network_key: "GENERATE",
pan_id: "GENERATE",
ext_pan_id: "GENERATE",
enable_external_js: false,
},
frontend: {
enabled: defaults.frontend.enabled,
+3 -3
View File
@@ -2,7 +2,7 @@ import assert from "node:assert";
import fs from "node:fs";
import equals from "fast-deep-equal/es6";
import yaml, {YAMLException} from "js-yaml";
import {dump, load, YAMLException} from "js-yaml";
export class YAMLFileException extends YAMLException {
file: string;
@@ -20,7 +20,7 @@ export class YAMLFileException extends YAMLException {
function read(file: string): KeyValue {
try {
const result = yaml.load(fs.readFileSync(file, "utf8"));
const result = load(fs.readFileSync(file, "utf8"));
assert(result instanceof Object, `The content of ${file} is expected to be an object`);
return result as KeyValue;
} catch (error) {
@@ -40,7 +40,7 @@ function writeIfChanged(file: string, content: KeyValue): void {
const before = readIfExists(file);
if (!equals(before, content)) {
fs.writeFileSync(file, yaml.dump(content));
fs.writeFileSync(file, dump(content));
}
}
+15 -15
View File
@@ -1,6 +1,6 @@
{
"name": "zigbee2mqtt",
"version": "2.10.0",
"version": "2.12.1-dev",
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
"main": "index.js",
"types": "dist/types/api.d.ts",
@@ -10,7 +10,7 @@
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
},
"engines": {
"node": "^20.15.0 || ^22.2.0 || ^24"
"node": "^22.2.0 || ^24 || ^26"
},
"keywords": [
"xiaomi",
@@ -45,40 +45,40 @@
"ajv": "^8.20.0",
"bind-decorator": "^1.0.11",
"debounce": "^3.0.0",
"express-static-gzip": "^3.0.0",
"express-static-gzip": "^3.0.1",
"fast-deep-equal": "^3.1.3",
"finalhandler": "^2.1.1",
"humanize-duration": "^3.33.2",
"js-yaml": "^4.1.0",
"humanize-duration": "^3.34.0",
"js-yaml": "^5.2.1",
"json-stable-stringify-without-jsonify": "^1.0.1",
"jszip": "^3.10.1",
"mqtt": "^5.15.1",
"mqtt": "^5.15.2",
"object-assign-deep": "^0.4.0",
"rimraf": "^6.1.3",
"semver": "^7.7.4",
"semver": "^7.8.5",
"source-map-support": "^0.5.21",
"throttleit": "^2.1.0",
"winston": "^3.19.0",
"winston-syslog": "^2.7.1",
"winston-transport": "^4.9.0",
"ws": "^8.20.0",
"zigbee-herdsman": "10.0.7",
"zigbee-herdsman-converters": "26.42.0",
"ws": "^8.21.0",
"zigbee-herdsman": "10.6.2",
"zigbee-herdsman-converters": "26.81.0",
"zigbee2mqtt-frontend": "0.9.21",
"zigbee2mqtt-windfront": "2.11.2"
"zigbee2mqtt-windfront": "2.12.1"
},
"devDependencies": {
"@biomejs/biome": "^2.4.13",
"@biomejs/biome": "^2.5.3",
"@types/finalhandler": "^1.2.3",
"@types/humanize-duration": "^3.27.4",
"@types/js-yaml": "^4.0.9",
"@types/node": "^24.12.2",
"@types/node": "^26.1.1",
"@types/object-assign-deep": "^0.4.3",
"@types/readable-stream": "4.0.23",
"@types/readable-stream": "4.0.24",
"@types/serve-static": "^2.2.0",
"@types/ws": "8.18.1",
"@vitest/coverage-v8": "^3.1.1",
"tmp": "^0.2.5",
"tmp": "^0.2.7",
"typescript": "^6.0.3",
"vitest": "^3.1.1"
},
+213 -232
View File
File diff suppressed because it is too large Load Diff
+13 -23
View File
@@ -1,4 +1,12 @@
import {execSync} from "node:child_process";
import {execFileSync} from "node:child_process";
function grepTypeScriptFiles(needle, dir) {
try {
return execFileSync("grep", ["-r", "-F", "--include=*.ts", "--", needle, dir], {encoding: "utf8"});
} catch {
return undefined;
}
}
async function checkDuplicateIssue(github, context, name) {
// Search for existing issues with the same `name`
@@ -63,13 +71,7 @@ export async function newDeviceSupport(github, _core, context, zhcDir) {
if (tuyaManufacturerNames.length > 0) {
for (const [fullName, partialName] of tuyaManufacturerNames) {
if (await checkDuplicateIssue(github, context, fullName)) return;
const fullMatch = (() => {
try {
return execSync(`grep -r --include="*.ts" "${fullName}" "${zhcDir}"`, {encoding: "utf8"});
} catch {
return undefined;
}
})();
const fullMatch = grepTypeScriptFiles(fullName, zhcDir);
console.log(`Checking full match for '${fullName}', result: '${fullMatch}'`);
if (fullMatch) {
@@ -94,13 +96,7 @@ If you need help with the process, feel free to ask here and we'll be happy to a
return;
}
const partialMatch = (() => {
try {
return execSync(`grep -r --include="*.ts" "${partialName}" "${zhcDir}"`, {encoding: "utf8"});
} catch {
return undefined;
}
})();
const partialMatch = grepTypeScriptFiles(partialName, zhcDir);
console.log(`Checking partial match for '${partialName}', result: '${partialMatch}'`);
if (partialMatch) {
@@ -129,14 +125,8 @@ Let us know if it works so we can support this device out-of-the-box!`,
if (zigbeeModels.length > 0) {
for (const zigbeeModel of zigbeeModels) {
if (await checkDuplicateIssue(github, context, fullName)) return;
const fullMatch = (() => {
try {
return execSync(`grep -r --include="*.ts" '"${zigbeeModel}"' "${zhcDir}"`, {encoding: "utf8"});
} catch {
return undefined;
}
})();
if (await checkDuplicateIssue(github, context, zigbeeModel)) return;
const fullMatch = grepTypeScriptFiles(`"${zigbeeModel}"`, zhcDir);
if (fullMatch) {
await github.rest.issues.createComment({
+2
View File
@@ -149,6 +149,7 @@ describe("Controller", () => {
user: "user1",
client_id: "my_client_id",
reject_unauthorized: false,
server_name: "mqtt.example.com",
version: 5,
maximum_packet_size: 20000,
};
@@ -166,6 +167,7 @@ describe("Controller", () => {
username: "user1",
clientId: "my_client_id",
rejectUnauthorized: false,
servername: "mqtt.example.com",
protocolVersion: 5,
properties: {maximumPacketSize: 20000},
};
+15 -6
View File
@@ -15,12 +15,21 @@ describe("Data", () => {
const expected = tmp.dirSync().name;
process.env.ZIGBEE2MQTT_DATA = expected;
data._testReload();
const actual = data.getPath();
expect(actual).toBe(expected);
expect(data.joinPath("test")).toStrictEqual(path.join(expected, "test"));
expect(data.joinPath("/test")).toStrictEqual(path.resolve(expected, "/test"));
delete process.env.ZIGBEE2MQTT_DATA;
data._testReload();
try {
const actual = data.getPath();
expect(actual).toBe(expected);
expect(data.joinPath("test")).toStrictEqual(path.join(expected, "test"));
expect(data.joinPath("/test")).toStrictEqual(path.resolve(expected, "/test"));
} finally {
delete process.env.ZIGBEE2MQTT_DATA;
data._testReload();
}
});
it("Should return data path when joinPath with empty string", () => {
const expected = data.getPath();
expect(data.joinPath("")).toStrictEqual(expected);
});
});
});
+2
View File
@@ -144,6 +144,7 @@ describe("Extension: Bridge", () => {
output: "json",
pan_id: 6754,
timestamp_format: "YYYY-MM-DD HH:mm:ss",
enable_external_js: true,
},
blocklist: [],
device_options: {},
@@ -749,6 +750,7 @@ describe("Extension: Bridge", () => {
"fireplace",
"colorloop",
"sunset",
"sunrise",
"sparkle",
"opal",
"glisten",
+20 -8
View File
@@ -193,16 +193,28 @@ describe("Extension: Configure", () => {
});
it("Should allow to configure via MQTT", async () => {
const emitDevicesChanged = vi.spyOn(controller.eventBus, "emitDevicesChanged");
const emitExposesChanged = vi.spyOn(controller.eventBus, "emitExposesChanged");
mockClear(devices.remote);
expectRemoteNotConfigured();
await mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/configure", "remote");
await flushPromises();
expectRemoteConfigured();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/configure",
stringify({data: {id: "remote"}, status: "ok"}),
{},
);
try {
await mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/configure", "remote");
await flushPromises();
expectRemoteConfigured();
expect(emitDevicesChanged).toHaveBeenCalledTimes(1);
expect(emitExposesChanged).toHaveBeenCalledWith({
device: controller.zigbee.resolveEntity(devices.remote),
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/configure",
stringify({data: {id: "remote"}, status: "ok"}),
{},
);
} finally {
emitExposesChanged.mockRestore();
emitDevicesChanged.mockRestore();
}
});
it("Fail to configure via MQTT when device does not exist", async () => {
@@ -488,6 +488,25 @@ describe("Extension: ExternalConverters", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/converters", stringify([]), {retain: true});
});
it("returns error on invalid name", async () => {
const converterName = "foo1";
await resetExtension();
for (const mock of mocksClear) mock.mockClear();
await (controller.getExtension("ExternalConverters")! as ExternalConverters).onMQTTMessage({
topic: "zigbee2mqtt/bridge/request/converter/save",
message: {name: converterName, code: "a"},
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/converter/save",
expect.stringContaining(`JavaScript file must have '.mjs', '.js' or '.cjs' extension`),
{},
);
expect(writeFileSyncSpy).toHaveBeenCalledTimes(0);
});
it("returns error on invalid code", async () => {
const converterName = "foo1.js";
const converterCode = "definetly not a correct javascript code";
@@ -609,4 +628,15 @@ describe("Extension: ExternalConverters", () => {
);
});
});
it("doesn't add extension when external JS disabled", async () => {
settings.set(["advanced", "enable_external_js"], false);
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
await flushPromises();
expect(controller.getExtension("ExternalConverters")).toBeUndefined();
});
});
@@ -299,6 +299,25 @@ describe("Extension: ExternalExtensions", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/extensions", stringify([]), {retain: true});
});
it("returns error on invalid name", async () => {
const extensionName = "foo1";
await resetExtension();
for (const mock of mocksClear) mock.mockClear();
await (controller.getExtension("ExternalExtensions")! as ExternalExtensions).onMQTTMessage({
topic: "zigbee2mqtt/bridge/request/extension/save",
message: {name: extensionName, code: "a"},
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/extension/save",
expect.stringContaining(`JavaScript file must have '.mjs', '.js' or '.cjs' extension`),
{},
);
expect(writeFileSyncSpy).toHaveBeenCalledTimes(0);
});
it("returns error on invalid code", async () => {
const extensionName = "foo1.js";
const extensionCode = "definetly not a correct javascript code";
@@ -365,4 +384,15 @@ describe("Extension: ExternalExtensions", () => {
);
});
});
it("doesn't add extension when external JS disabled", async () => {
settings.set(["advanced", "enable_external_js"], false);
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
await flushPromises();
expect(controller.getExtension("ExternalExtensions")).toBeUndefined();
});
});
+299 -65
View File
@@ -128,6 +128,159 @@ describe("Extension: HomeAssistant", () => {
expect(duplicated).toStrictEqual([]);
});
it("Should mark thermostat configuration toggles as config entities", () => {
const switchExposes = [
new zhc.Switch().withLabel("Auto lock").withState("auto_lock", false, "Enable/disable auto lock", zhc.access.STATE_SET, "AUTO", "MANUAL"),
new zhc.Switch().withLabel("Away mode").withState("away_mode", false, "Enable/disable away mode", zhc.access.STATE_SET),
new zhc.Switch().withLabel("Valve detection").withState("valve_detection", true, "Valve detection", zhc.access.STATE_SET),
new zhc.Switch()
.withLabel("Window detection")
.withState("window_detection", true, "Enables/disables window detection", zhc.access.STATE_SET),
];
const binaryExposes = [
new zhc.Binary("frost_protection", zhc.access.STATE_SET, "ON", "OFF").withDescription("Anti-freeze protection"),
new zhc.Binary("heating_stop", zhc.access.STATE_SET, "ON", "OFF").withDescription("Heating stop"),
new zhc.Binary("away_mode", zhc.access.STATE_SET, "ON", "OFF").withDescription("Away mode"),
new zhc.Binary("window_detection", zhc.access.STATE_SET, "ON", "OFF").withDescription("Open window detection"),
];
const getDiscoveryConfigs = (expose: zhc.Expose): KeyValueAny[] => {
const device = {
definition: {},
isDevice: (): boolean => true,
isGroup: (): boolean => false,
endpoint: () => undefined,
options: {},
exposes: (): zhc.Expose[] => [expose],
zh: {endpoints: []},
};
// @ts-expect-error private method and minimal test device
return extension.getConfigs(device);
};
for (const expose of switchExposes) {
const [config] = getDiscoveryConfigs(expose);
expect(config.type).toStrictEqual("switch");
expect(config.object_id).toStrictEqual(expose.features[0].property);
expect(config.discovery_payload.entity_category).toStrictEqual("config");
expect(config.discovery_payload.command_topic_postfix).toStrictEqual(expose.features[0].property);
}
for (const expose of binaryExposes) {
const [config] = getDiscoveryConfigs(expose);
expect(config.type).toStrictEqual("switch");
expect(config.object_id).toStrictEqual(`switch_${expose.name}`);
expect(config.discovery_payload.entity_category).toStrictEqual("config");
expect(config.discovery_payload.command_topic_postfix).toStrictEqual(expose.property);
}
});
it("Should mark device settings as config entities", () => {
const getDiscoveryConfigs = (expose: zhc.Expose): KeyValueAny[] => {
const device = {
definition: {},
isDevice: (): boolean => true,
isGroup: (): boolean => false,
endpoint: () => undefined,
options: {},
exposes: (): zhc.Expose[] => [expose],
zh: {endpoints: []},
};
// @ts-expect-error private method and minimal test device
return extension.getConfigs(device);
};
const enumExposes = [
new zhc.Enum("set_limits", zhc.access.STATE_SET, ["START", "END", "RESET"]),
new zhc.Enum("motor_direction", zhc.access.STATE_SET, ["forward", "back"]),
new zhc.Enum("temperature_unit", zhc.access.STATE_SET, ["celsius", "fahrenheit"]),
];
for (const expose of enumExposes) {
const [config] = getDiscoveryConfigs(expose);
expect(config.type).toStrictEqual("select");
expect(config.object_id).toStrictEqual(expose.property);
expect(config.discovery_payload.entity_category).toStrictEqual("config");
}
const binaryExposes = [
new zhc.Binary("tilt_mode", zhc.access.STATE_SET, "ON", "OFF"),
new zhc.Binary("calibration_left", zhc.access.STATE_SET, "ON", "OFF"),
new zhc.Binary("motor_reversal_right", zhc.access.STATE_SET, "ON", "OFF"),
new zhc.Binary("enable_display", zhc.access.STATE_SET, "ON", "OFF"),
new zhc.Binary("indicator", zhc.access.STATE_SET, "ON", "OFF"),
];
for (const expose of binaryExposes) {
const [config] = getDiscoveryConfigs(expose);
expect(config.type).toStrictEqual("switch");
expect(config.object_id).toStrictEqual(`switch_${expose.property}`);
expect(config.discovery_payload.entity_category).toStrictEqual("config");
}
const numericExposes = [
new zhc.Numeric("calibration_time_left", zhc.access.STATE_SET),
new zhc.Numeric("comfort_temperature_min", zhc.access.STATE_SET),
new zhc.Numeric("comfort_humidity_max", zhc.access.STATE_SET),
new zhc.Numeric("measurement_interval", zhc.access.STATE_SET),
new zhc.Numeric("minimum_range", zhc.access.STATE_SET),
new zhc.Numeric("maximum_range", zhc.access.STATE_SET),
new zhc.Numeric("detection_delay", zhc.access.STATE_SET),
new zhc.Numeric("fading_time", zhc.access.STATE_SET),
new zhc.Numeric("large_motion_detection_sensitivity", zhc.access.STATE_SET),
new zhc.Numeric("medium_motion_detection_distance", zhc.access.STATE_SET),
new zhc.Numeric("small_detection_sensitivity", zhc.access.STATE_SET),
new zhc.Numeric("soil_calibration", zhc.access.STATE_SET),
new zhc.Numeric("soil_sampling", zhc.access.STATE_SET),
new zhc.Numeric("soil_warning", zhc.access.STATE_SET),
];
for (const expose of numericExposes) {
const [config] = getDiscoveryConfigs(expose);
expect(config.type).toStrictEqual("number");
expect(config.object_id).toStrictEqual(expose.property);
expect(config.discovery_payload.entity_category).toStrictEqual("config");
}
const [textConfig] = getDiscoveryConfigs(new zhc.Text("schedule_settings", zhc.access.STATE_SET));
expect(textConfig.type).toStrictEqual("text");
expect(textConfig.object_id).toStrictEqual("schedule_settings");
expect(textConfig.discovery_payload.entity_category).toStrictEqual("config");
});
it("Should apply expose-level Home Assistant discovery metadata", () => {
const createDevice = (exposes: zhc.Expose[]): Device =>
({
definition: {},
isDevice: (): boolean => true,
isGroup: (): boolean => false,
endpoint: () => undefined,
options: {},
exposes: (): zhc.Expose[] => exposes,
zh: {endpoints: []},
}) as Device;
const voltageExpose = new zhc.Numeric("voltage", zhc.access.STATE).withUnit("V");
Object.assign(voltageExpose, {
homeassistant: {
type: "valve",
entityCategory: "diagnostic",
deviceClass: "voltage",
enabledByDefault: false,
icon: "mdi:flash",
},
});
// @ts-expect-error private
const configs = extension.getConfigs(createDevice([voltageExpose]));
expect(configs.find((config) => config.object_id === "voltage")?.discovery_payload).toMatchObject({
device_class: "voltage",
enabled_by_default: false,
entity_category: "diagnostic",
icon: "mdi:flash",
});
expect(configs.find((config) => config.object_id === "voltage")?.discovery_payload).not.toHaveProperty("type");
});
it("Should discover devices and groups", async () => {
settings.set(["homeassistant", "experimental_event_entities"], true);
settings.set(["groups", "9", "homeassistant"], {name: "HA Discovery Group", icon: "mdi:lightbulb-group"});
@@ -165,6 +318,7 @@ describe("Extension: HomeAssistant", () => {
"fireplace",
"colorloop",
"sunset",
"sunrise",
"sparkle",
"opal",
"glisten",
@@ -238,7 +392,7 @@ describe("Extension: HomeAssistant", () => {
unique_id: "9_switch_zigbee2mqtt",
group: ["0x0017880104e45542_switch_right_zigbee2mqtt"],
origin: origin,
value_template: "{{ value_json.state }}",
value_template: '{{ value_json["state"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
@@ -251,7 +405,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "°C",
device_class: "temperature",
state_class: "measurement",
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
@@ -278,7 +432,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "%",
device_class: "humidity",
state_class: "measurement",
value_template: "{{ value_json.humidity }}",
value_template: '{{ value_json["humidity"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_humidity",
default_entity_id: "sensor.weather_sensor_humidity",
@@ -305,7 +459,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "hPa",
device_class: "atmospheric_pressure",
state_class: "measurement",
value_template: "{{ value_json.pressure }}",
value_template: '{{ value_json["pressure"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_pressure",
default_entity_id: "sensor.weather_sensor_pressure",
@@ -332,7 +486,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "%",
device_class: "battery",
state_class: "measurement",
value_template: "{{ value_json.battery }}",
value_template: '{{ value_json["battery"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_battery",
default_entity_id: "sensor.weather_sensor_battery",
@@ -362,7 +516,7 @@ describe("Extension: HomeAssistant", () => {
entity_category: "diagnostic",
unit_of_measurement: "lqi",
state_class: "measurement",
value_template: "{{ value_json.linkquality }}",
value_template: '{{ value_json["linkquality"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
name: "Linkquality",
object_id: "weather_sensor_linkquality",
@@ -404,7 +558,7 @@ describe("Extension: HomeAssistant", () => {
default_entity_id: "switch.wall_switch_double_left",
unique_id: "0x0017880104e45542_switch_left_zigbee2mqtt",
origin: origin,
value_template: "{{ value_json.state_left }}",
value_template: '{{ value_json["state_left"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/switch/0x0017880104e45542/switch_left/config", stringify(payload), {
@@ -431,7 +585,7 @@ describe("Extension: HomeAssistant", () => {
default_entity_id: "switch.wall_switch_double_right",
unique_id: "0x0017880104e45542_switch_right_zigbee2mqtt",
origin: origin,
value_template: "{{ value_json.state_right }}",
value_template: '{{ value_json["state_right"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/switch/0x0017880104e45542/switch_right/config", stringify(payload), {
@@ -534,7 +688,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "%",
device_class: "humidity",
state_class: "measurement",
value_template: "{{ value_json.humidity }}",
value_template: '{{ value_json["humidity"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_humidity",
default_entity_id: "sensor.weather_sensor_humidity",
@@ -606,7 +760,7 @@ describe("Extension: HomeAssistant", () => {
device_class: "temperature",
state_class: "measurement",
enabled_by_default: true,
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
@@ -632,7 +786,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "%",
device_class: "humidity",
state_class: "measurement",
value_template: "{{ value_json.humidity }}",
value_template: '{{ value_json["humidity"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_humidity",
default_entity_id: "sensor.weather_sensor_humidity",
@@ -659,7 +813,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "hPa",
device_class: "atmospheric_pressure",
state_class: "measurement",
value_template: "{{ value_json.pressure }}",
value_template: '{{ value_json["pressure"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
enabled_by_default: true,
object_id: "weather_sensor_pressure",
@@ -717,7 +871,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "°C",
device_class: "temperature",
state_class: "measurement",
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
enabled_by_default: true,
object_id: "weather_sensor_temperature",
@@ -747,7 +901,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "%",
device_class: "humidity",
state_class: "measurement",
value_template: "{{ value_json.humidity }}",
value_template: '{{ value_json["humidity"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
enabled_by_default: true,
device: {
@@ -790,7 +944,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "°C",
device_class: "temperature",
state_class: "measurement",
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
@@ -817,7 +971,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "%",
device_class: "humidity",
state_class: "measurement",
value_template: "{{ value_json.humidity }}",
value_template: '{{ value_json["humidity"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_humidity",
default_entity_id: "sensor.weather_sensor_humidity",
@@ -880,7 +1034,7 @@ describe("Extension: HomeAssistant", () => {
default_entity_id: "light.my_switch",
unique_id: "0x0017880104e45541_light_zigbee2mqtt",
origin: origin,
value_template: "{{ value_json.state }}",
value_template: '{{ value_json["state"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/light/0x0017880104e45541/light/config", stringify(payload), {
@@ -926,11 +1080,11 @@ describe("Extension: HomeAssistant", () => {
command_topic: "zigbee2mqtt/fan/set/fan_state",
percentage_state_topic: "zigbee2mqtt/fan",
percentage_command_topic: "zigbee2mqtt/fan/set/fan_mode",
percentage_value_template: "{{ {'off':0, 'low':1, 'medium':2, 'high':3, 'on':4}[value_json.fan_mode] | default('None') }}",
percentage_value_template: "{{ {'off':0, 'low':1, 'medium':2, 'high':3, 'on':4}[value_json[\"fan_mode\"]] | default('None') }}",
percentage_command_template: "{{ {0:'off', 1:'low', 2:'medium', 3:'high', 4:'on'}[value] | default('') }}",
preset_mode_state_topic: "zigbee2mqtt/fan",
preset_mode_command_topic: "zigbee2mqtt/fan/set/fan_mode",
preset_mode_value_template: "{{ value_json.fan_mode if value_json.fan_mode in ['smart'] else 'None' | default('None') }}",
preset_mode_value_template: "{{ value_json[\"fan_mode\"] if value_json[\"fan_mode\"] in ['smart'] else 'None' | default('None') }}",
preset_modes: ["smart"],
speed_range_min: 1,
speed_range_max: 4,
@@ -1002,7 +1156,7 @@ describe("Extension: HomeAssistant", () => {
command_topic: "zigbee2mqtt/fanbee/set/state",
percentage_state_topic: "zigbee2mqtt/fanbee",
percentage_command_topic: "zigbee2mqtt/fanbee/set/speed",
percentage_value_template: "{{ value_json.speed | default('None') }}",
percentage_value_template: "{{ value_json[\"speed\"] | default('None') }}",
percentage_command_template: "{{ value | default('') }}",
speed_range_min: 1,
speed_range_max: 254,
@@ -1035,7 +1189,7 @@ describe("Extension: HomeAssistant", () => {
it("Should discover thermostat devices", () => {
const payload = {
action_template:
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}",
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}",
action_topic: "zigbee2mqtt/TS0601_thermostat",
availability: [
{
@@ -1043,7 +1197,7 @@ describe("Extension: HomeAssistant", () => {
value_template: "{{ value_json.state }}",
},
],
current_temperature_template: "{{ value_json.local_temperature }}",
current_temperature_template: '{{ value_json["local_temperature"] }}',
current_temperature_topic: "zigbee2mqtt/TS0601_thermostat",
device: {
identifiers: ["zigbee2mqtt_0x0017882104a44559"],
@@ -1055,18 +1209,18 @@ describe("Extension: HomeAssistant", () => {
},
preset_mode_command_topic: "zigbee2mqtt/TS0601_thermostat/set/preset",
preset_modes: ["schedule", "manual", "boost", "complex", "comfort", "eco", "away"],
preset_mode_value_template: "{{ value_json.preset }}",
preset_mode_value_template: '{{ value_json["preset"] }}',
preset_mode_state_topic: "zigbee2mqtt/TS0601_thermostat",
max_temp: "35",
min_temp: "5",
mode_command_topic: "zigbee2mqtt/TS0601_thermostat/set/system_mode",
mode_state_template: "{{ value_json.system_mode }}",
mode_state_template: '{{ value_json["system_mode"] }}',
mode_state_topic: "zigbee2mqtt/TS0601_thermostat",
modes: ["heat", "auto", "off"],
name: null,
temp_step: 0.5,
temperature_command_topic: "zigbee2mqtt/TS0601_thermostat/set/current_heating_setpoint",
temperature_state_template: "{{ value_json.current_heating_setpoint }}",
temperature_state_template: '{{ value_json["current_heating_setpoint"] }}',
temperature_state_topic: "zigbee2mqtt/TS0601_thermostat",
temperature_unit: "C",
object_id: "ts0601_thermostat",
@@ -1106,7 +1260,7 @@ describe("Extension: HomeAssistant", () => {
state_topic: "zigbee2mqtt/thermostat",
unique_id: "0x0017880104e45550_pi_heating_demand_zigbee2mqtt",
unit_of_measurement: "%",
value_template: "{{ value_json.pi_heating_demand }}",
value_template: '{{ value_json["pi_heating_demand"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/sensor/0x0017880104e45550/pi_heating_demand/config", stringify(payload), {
@@ -1143,7 +1297,7 @@ describe("Extension: HomeAssistant", () => {
state_topic: "zigbee2mqtt/bosch_radiator",
unique_id: "0x18fc2600000d7ae2_pi_heating_demand_zigbee2mqtt",
unit_of_measurement: "%",
value_template: "{{ value_json.pi_heating_demand }}",
value_template: '{{ value_json["pi_heating_demand"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/number/0x18fc2600000d7ae2/pi_heating_demand/config", stringify(payload), {
@@ -1155,10 +1309,10 @@ describe("Extension: HomeAssistant", () => {
it("Should discover Bosch BTH-RA with a compatibility mapping", () => {
const payload = {
action_template:
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}",
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}",
action_topic: "zigbee2mqtt/bosch_radiator",
availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}],
current_temperature_template: "{{ value_json.local_temperature }}",
current_temperature_template: '{{ value_json["local_temperature"] }}',
current_temperature_topic: "zigbee2mqtt/bosch_radiator",
device: {
identifiers: ["zigbee2mqtt_0x18fc2600000d7ae2"],
@@ -1183,7 +1337,7 @@ describe("Extension: HomeAssistant", () => {
origin: origin,
temp_step: 0.5,
temperature_command_topic: "zigbee2mqtt/bosch_radiator/set/occupied_heating_setpoint",
temperature_state_template: "{{ value_json.occupied_heating_setpoint }}",
temperature_state_template: '{{ value_json["occupied_heating_setpoint"] }}',
temperature_state_topic: "zigbee2mqtt/bosch_radiator",
temperature_unit: "C",
unique_id: "0x18fc2600000d7ae2_climate_zigbee2mqtt",
@@ -1195,6 +1349,24 @@ describe("Extension: HomeAssistant", () => {
});
});
it("Should apply user configuration after converter compatibility mapping", async () => {
settings.set(["devices", "0x18fc2600000d7ae2", "homeassistant", "climate"], {
modes: ["off", "heat", "auto"],
mode_command_template: null,
});
await resetExtension();
await flushPromises();
const call = mockMQTTPublishAsync.mock.calls.find((c) => c[0] === "homeassistant/climate/0x18fc2600000d7ae2/climate/config");
expect(call).toBeDefined();
const payload = JSON.parse(call![1] as string);
expect(payload.modes).toStrictEqual(["off", "heat", "auto"]);
expect(payload.mode_command_template).toBeUndefined();
expect(payload.mode_command_topic).toStrictEqual("zigbee2mqtt/bosch_radiator/set");
});
it("does not throw when discovery payload override throws", async () => {
const bosch = getZ2MEntity(devices["RBSH-TRV0-ZB-EU"]) as Device;
assert(typeof bosch.definition?.meta?.overrideHaDiscoveryPayload === "function");
@@ -1210,10 +1382,10 @@ describe("Extension: HomeAssistant", () => {
const payload = {
action_template:
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}",
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}",
action_topic: "zigbee2mqtt/bosch_radiator",
availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}],
current_temperature_template: "{{ value_json.local_temperature }}",
current_temperature_template: '{{ value_json["local_temperature"] }}',
current_temperature_topic: "zigbee2mqtt/bosch_radiator",
device: {
identifiers: ["zigbee2mqtt_0x18fc2600000d7ae2"],
@@ -1227,7 +1399,7 @@ describe("Extension: HomeAssistant", () => {
max_temp: "30",
min_temp: "5",
mode_command_topic: "zigbee2mqtt/bosch_radiator/set/system_mode",
mode_state_template: "{{ value_json.system_mode }}",
mode_state_template: '{{ value_json["system_mode"] }}',
mode_state_topic: "zigbee2mqtt/bosch_radiator",
modes: ["heat"],
name: null,
@@ -1236,7 +1408,7 @@ describe("Extension: HomeAssistant", () => {
origin: origin,
temp_step: 0.5,
temperature_command_topic: "zigbee2mqtt/bosch_radiator/set/occupied_heating_setpoint",
temperature_state_template: "{{ value_json.occupied_heating_setpoint }}",
temperature_state_template: '{{ value_json["occupied_heating_setpoint"] }}',
temperature_state_topic: "zigbee2mqtt/bosch_radiator",
temperature_unit: "C",
unique_id: "0x18fc2600000d7ae2_climate_zigbee2mqtt",
@@ -1251,15 +1423,39 @@ describe("Extension: HomeAssistant", () => {
overrideSpy.mockRestore();
});
it("passes device options to discovery payload overrides", async () => {
const bosch = getZ2MEntity(devices["RBSH-TRV0-ZB-EU"]) as Device;
assert(typeof bosch.definition?.meta?.overrideHaDiscoveryPayload === "function");
const overrideSpy = vi.spyOn(bosch.definition.meta, "overrideHaDiscoveryPayload") as MockInstance;
settings.set(["devices", "0x18fc2600000d7ae2", "discovery_option_marker"], "passed");
overrideSpy.mockImplementation((payload, options) => {
if (payload.mode_command_topic?.endsWith("/system_mode")) {
payload.discovery_option_marker = options?.discovery_option_marker;
}
});
await resetExtension();
expect(overrideSpy).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({discovery_option_marker: "passed"}));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"homeassistant/climate/0x18fc2600000d7ae2/climate/config",
expect.stringContaining('"discovery_option_marker":"passed"'),
{qos: 1, retain: true},
);
overrideSpy.mockRestore();
});
it("Should discover Bosch BTH-RM230Z with a current_humidity attribute", () => {
const payload = {
action_template:
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}",
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}",
action_topic: "zigbee2mqtt/bosch_rm230z",
availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}],
current_humidity_template: "{{ value_json.humidity }}",
current_humidity_template: '{{ value_json["humidity"] }}',
current_humidity_topic: "zigbee2mqtt/bosch_rm230z",
current_temperature_template: "{{ value_json.local_temperature }}",
current_temperature_template: '{{ value_json["local_temperature"] }}',
current_temperature_topic: "zigbee2mqtt/bosch_rm230z",
default_entity_id: "climate.bosch_rm230z",
device: {
@@ -1274,20 +1470,20 @@ describe("Extension: HomeAssistant", () => {
min_temp: "5",
mode_command_topic: "zigbee2mqtt/bosch_rm230z/set",
mode_state_template:
"{% set values = {'schedule':'auto','manual':'heat','pause':'off'} %}{% set value = value_json.operating_mode %}{% if value == \"manual\" %}{{ value_json.system_mode }}{% else %}{{ values[value] if value in values.keys() else 'off' }}{% endif %}",
"{% set active_modes = ['heat'] %}{% set fallback_mode = 'heat' %}{% set values = {'schedule':'auto','pause':'off'} %}{% set value = value_json.operating_mode %}{% set mode = value_json.system_mode %}{% if value == 'manual' %}{{ mode if mode in active_modes else fallback_mode }}{% else %}{{ values[value] if value in values.keys() else 'off' }}{% endif %}",
mode_command_template:
"{% set values = { 'auto':'schedule','heat':'manual','cool':'manual','off':'pause'} %}{% if value == \"heat\" or value == \"cool\" %}{\"operating_mode\": \"manual\", \"system_mode\": \"{{ value }}\"}{% else %}{\"operating_mode\": \"{{ values[value] if value in values.keys() else 'pause' }}\"}{% endif %}",
"{% set active_modes = ['heat'] %}{% set values = {'auto':'schedule','off':'pause'} %}{% if value in active_modes %}{\"operating_mode\": \"manual\", \"system_mode\": \"{{ value }}\"}{% else %}{\"operating_mode\": \"{{ values[value] if value in values.keys() else 'pause' }}\"}{% endif %}",
mode_state_topic: "zigbee2mqtt/bosch_rm230z",
modes: ["off", "heat", "cool", "auto"],
modes: ["off", "heat", "auto"],
name: null,
object_id: "bosch_rm230z",
origin,
temp_step: 0.5,
temperature_high_command_topic: "zigbee2mqtt/bosch_rm230z/set/occupied_cooling_setpoint",
temperature_high_state_template: "{{ value_json.occupied_cooling_setpoint }}",
temperature_high_state_template: '{{ value_json["occupied_cooling_setpoint"] }}',
temperature_high_state_topic: "zigbee2mqtt/bosch_rm230z",
temperature_low_command_topic: "zigbee2mqtt/bosch_rm230z/set/occupied_heating_setpoint",
temperature_low_state_template: "{{ value_json.occupied_heating_setpoint }}",
temperature_low_state_template: '{{ value_json["occupied_heating_setpoint"] }}',
temperature_low_state_topic: "zigbee2mqtt/bosch_rm230z",
temperature_unit: "C",
unique_id: "0x18fc2600000d7ae3_climate_zigbee2mqtt",
@@ -1318,7 +1514,7 @@ describe("Extension: HomeAssistant", () => {
state_topic: "zigbee2mqtt/bosch_rm230z",
unique_id: "0x18fc2600000d7ae3_local_temperature_zigbee2mqtt",
unit_of_measurement: "°C",
value_template: "{{ value_json.local_temperature }}",
value_template: '{{ value_json["local_temperature"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/sensor/0x18fc2600000d7ae3/local_temperature/config", stringify(payload), {
@@ -1327,6 +1523,37 @@ describe("Extension: HomeAssistant", () => {
});
});
it("Should discover climate with cooling-only setpoint", () => {
const climateExpose = new zhc.Climate()
.withSetpoint("occupied_cooling_setpoint", 16, 32, 0.5)
.withLocalTemperature()
.withSystemMode(["off", "cool", "auto"]);
const device = {
definition: {},
isDevice: (): boolean => true,
isGroup: (): boolean => false,
endpoint: () => undefined,
options: {},
exposes: (): zhc.Expose[] => [climateExpose],
zh: {endpoints: []},
} as Device;
// @ts-expect-error private
const configs = extension.getConfigs(device);
const climate = configs.find((c) => c.type === "climate");
expect(climate).toBeDefined();
expect(climate!.discovery_payload).toMatchObject({
temperature_command_topic: "occupied_cooling_setpoint",
temperature_state_template: '{{ value_json["occupied_cooling_setpoint"] }}',
temperature_state_topic: true,
min_temp: "16",
max_temp: "32",
temp_step: 0.5,
});
expect(climate!.discovery_payload).not.toHaveProperty("temperature_low_command_topic");
expect(climate!.discovery_payload).not.toHaveProperty("temperature_high_command_topic");
});
it("Should discover devices with cover_position", () => {
let payload;
@@ -1335,9 +1562,9 @@ describe("Extension: HomeAssistant", () => {
position_topic: "zigbee2mqtt/smart vent",
set_position_topic: "zigbee2mqtt/smart vent/set",
set_position_template: '{ "position": {{ position }} }',
position_template: "{{ value_json.position }}",
position_template: '{{ value_json["position"] }}',
state_topic: "zigbee2mqtt/smart vent",
value_template: "{{ value_json.state }}",
value_template: '{{ value_json["state"] }}',
state_open: "OPEN",
state_closed: "CLOSE",
state_stopped: "STOP",
@@ -1374,7 +1601,7 @@ describe("Extension: HomeAssistant", () => {
via_device: "zigbee2mqtt_bridge_0x00124b00120144ae",
},
name: "L6",
position_template: "{{ value_json.position }}",
position_template: '{{ value_json["position"] }}',
position_topic: "zigbee2mqtt/zigfred_plus/l6",
set_position_template: '{ "position_l6": {{ position }} }',
set_position_topic: "zigbee2mqtt/zigfred_plus/l6/set",
@@ -1383,13 +1610,13 @@ describe("Extension: HomeAssistant", () => {
state_open: "OPEN",
state_topic: "zigbee2mqtt/zigfred_plus/l6",
tilt_command_topic: "zigbee2mqtt/zigfred_plus/l6/set/tilt",
tilt_status_template: "{{ value_json.tilt }}",
tilt_status_template: '{{ value_json["tilt"] }}',
tilt_status_topic: "zigbee2mqtt/zigfred_plus/l6",
object_id: "zigfred_plus_l6",
default_entity_id: "cover.zigfred_plus_l6",
unique_id: "0xf4ce368a38be56a1_cover_l6_zigbee2mqtt",
origin: origin,
value_template: "{{ value_json.state }}",
value_template: '{{ value_json["state"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/cover/0xf4ce368a38be56a1/cover_l6/config", stringify(payload), {
@@ -1419,7 +1646,7 @@ describe("Extension: HomeAssistant", () => {
object_id: "0xa4c138018cf95021_left",
default_entity_id: "cover.0xa4c138018cf95021_left",
origin: origin,
position_template: "{{ value_json.position }}",
position_template: '{{ value_json["position"] }}',
position_topic: "zigbee2mqtt/0xa4c138018cf95021/left",
set_position_template: '{ "position_left": {{ position }} }',
set_position_topic: "zigbee2mqtt/0xa4c138018cf95021/left/set",
@@ -1428,7 +1655,7 @@ describe("Extension: HomeAssistant", () => {
state_stopped: "STOP",
state_topic: "zigbee2mqtt/0xa4c138018cf95021/left",
unique_id: "0xa4c138018cf95021_cover_left_zigbee2mqtt",
value_template: '{% if "moving" in value_json and value_json.moving %} {{ value_json.moving }} {% else %} STOP {% endif %}',
value_template: '{% if "moving" in value_json and value_json["moving"] %} {{ value_json["moving"] }} {% else %} STOP {% endif %}',
};
const payload_right = {
availability: [
@@ -1450,7 +1677,7 @@ describe("Extension: HomeAssistant", () => {
object_id: "0xa4c138018cf95021_right",
default_entity_id: "cover.0xa4c138018cf95021_right",
origin: origin,
position_template: "{{ value_json.position }}",
position_template: '{{ value_json["position"] }}',
position_topic: "zigbee2mqtt/0xa4c138018cf95021/right",
set_position_template: '{ "position_right": {{ position }} }',
set_position_topic: "zigbee2mqtt/0xa4c138018cf95021/right/set",
@@ -1459,11 +1686,9 @@ describe("Extension: HomeAssistant", () => {
state_stopped: "STOP",
state_topic: "zigbee2mqtt/0xa4c138018cf95021/right",
unique_id: "0xa4c138018cf95021_cover_right_zigbee2mqtt",
value_template: '{% if "moving" in value_json and value_json.moving %} {{ value_json.moving }} {% else %} STOP {% endif %}',
value_template: '{% if "moving" in value_json and value_json["moving"] %} {{ value_json["moving"] }} {% else %} STOP {% endif %}',
};
console.log(mockMQTTPublishAsync.mock.calls.find((c) => c[0] === "homeassistant/cover/0xa4c138018cf95021/cover_left/config"));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/cover/0xa4c138018cf95021/cover_left/config", stringify(payload_left), {
retain: true,
qos: 1,
@@ -1482,7 +1707,7 @@ describe("Extension: HomeAssistant", () => {
unit_of_measurement: "°C",
device_class: "temperature",
state_class: "measurement",
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
enabled_by_default: true,
object_id: "weather_sensor_temperature",
@@ -1668,7 +1893,7 @@ describe("Extension: HomeAssistant", () => {
device_class: "temperature",
enabled_by_default: true,
state_class: "measurement",
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
@@ -1723,6 +1948,7 @@ describe("Extension: HomeAssistant", () => {
await flushPromises();
await vi.runOnlyPendingTimersAsync();
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/state", stringify({state: "online"}), {retain: true, qos: 1});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb",
stringify({
@@ -1763,6 +1989,7 @@ describe("Extension: HomeAssistant", () => {
await flushPromises();
await vi.runOnlyPendingTimersAsync();
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/state", stringify({state: "online"}), {retain: true, qos: 1});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb",
stringify({
@@ -1829,7 +2056,7 @@ describe("Extension: HomeAssistant", () => {
device_class: "temperature",
enabled_by_default: true,
state_class: "measurement",
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
@@ -1899,7 +2126,7 @@ describe("Extension: HomeAssistant", () => {
device_class: "temperature",
state_class: "measurement",
enabled_by_default: true,
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor_renamed",
object_id: "weather_sensor_renamed_temperature",
default_entity_id: "sensor.weather_sensor_renamed_temperature",
@@ -1984,6 +2211,7 @@ describe("Extension: HomeAssistant", () => {
"fireplace",
"colorloop",
"sunset",
"sunrise",
"sparkle",
"opal",
"glisten",
@@ -2032,7 +2260,7 @@ describe("Extension: HomeAssistant", () => {
device_class: "temperature",
state_class: "measurement",
enabled_by_default: true,
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor_renamed",
object_id: "weather_sensor_renamed_temperature",
default_entity_id: "sensor.weather_sensor_renamed_temperature",
@@ -2463,6 +2691,7 @@ describe("Extension: HomeAssistant", () => {
"fireplace",
"colorloop",
"sunset",
"sunrise",
"sparkle",
"opal",
"glisten",
@@ -2518,6 +2747,7 @@ describe("Extension: HomeAssistant", () => {
"fireplace",
"colorloop",
"sunset",
"sunrise",
"sparkle",
"opal",
"glisten",
@@ -2640,7 +2870,7 @@ describe("Extension: HomeAssistant", () => {
device_class: "temperature",
state_class: "measurement",
enabled_by_default: true,
value_template: "{{ value_json.temperature }}",
value_template: '{{ value_json["temperature"] }}',
state_topic: "zigbee2mqtt/weather_sensor",
object_id: "weather_sensor_temperature",
default_entity_id: "sensor.weather_sensor_temperature",
@@ -3007,7 +3237,7 @@ describe("Extension: HomeAssistant", () => {
origin: origin,
state_topic: "zigbee2mqtt/0x18fc26000000cafe",
unique_id: "0x18fc26000000cafe_device_mode_zigbee2mqtt",
value_template: "{{ value_json.device_mode }}",
value_template: '{{ value_json["device_mode"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/select/0x18fc26000000cafe/device_mode/config", stringify(payload), {
retain: true,
@@ -3019,11 +3249,15 @@ describe("Extension: HomeAssistant", () => {
settings.set(["homeassistant", "legacy_action_sensor"], true);
await resetExtension();
// Should discovery action sensor
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/sensor/0x0017880104e45520/action/config", expect.any(String), {
retain: true,
qos: 1,
// Should discover action sensor as a diagnostic helper instead of a primary entity.
const actionDiscovery = mockMQTTPublishAsync.mock.calls.find(([topic]) => topic === "homeassistant/sensor/0x0017880104e45520/action/config");
assert(actionDiscovery);
expect(JSON.parse(actionDiscovery[1])).toMatchObject({
entity_category: "diagnostic",
name: "Action",
object_id: "button_action",
});
expect(actionDiscovery[2]).toStrictEqual({retain: true, qos: 1});
// Should counter an action payload with an empty payload
mockMQTTPublishAsync.mockClear();
+65
View File
@@ -546,6 +546,71 @@ describe("Extension: OTAUpdate", () => {
expect(existsSync(saveFilePath)).toStrictEqual(false);
});
it("aborts running OTA", async () => {
let timer: NodeJS.Timeout | undefined;
devices.bulb.updateOta.mockImplementationOnce(
async (_source, _requestPayload, _requestTsn, _extraMetas, onProgress, _dataSettings, _endpoint) => {
onProgress(0, 36000.5678);
onProgress(10, 3600.2123);
return await new Promise((resolve) => {
timer = setTimeout(
() =>
resolve([
{
...DEFAULT_CURRENT,
fileVersion: 1,
},
{
...DEFAULT_CURRENT,
fileVersion: 90,
},
]),
10000,
);
});
},
);
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/ota_update/update", stringify({id: "bulb"}));
await flushPromises();
await vi.advanceTimersByTimeAsync(9000);
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/ota_update/update/abort", stringify({id: "bulb"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb",
stringify({update: {state: "updating", progress: 0, remaining: 36001}}),
{
retain: true,
qos: 0,
},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb",
stringify({update: {state: "updating", progress: 10, remaining: 3600}}),
{retain: true, qos: 0},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({update: {state: "available"}}), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/ota_update/update/abort",
stringify({data: {id: "bulb"}, status: "ok"}),
{},
);
clearTimeout(timer);
});
it("handles abort when no running OTA", async () => {
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/ota_update/update/abort", stringify({id: "bulb"}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/ota_update/update",
stringify({data: {}, status: "error", error: `No OTA in progress to abort for device 'bulb'`}),
{},
);
});
it("handles when OTA update fails", async () => {
devices.bulb.endpoints[0].read.mockImplementation(() => {
return {swBuildId: 1, dateCode: "2019010"};
+2
View File
@@ -283,6 +283,7 @@ export class Device {
unscheduleOta = vi.fn(() => {
this.scheduledOta = undefined;
});
abortOta = vi.fn();
scheduledOta: OtaSource | undefined = undefined;
constructor(
@@ -345,6 +346,7 @@ export class Device {
this.updateOta.mockClear();
this.scheduleOta.mockClear();
this.unscheduleOta.mockClear();
this.abortOta.mockClear();
this.meta = {};
this.scheduledOta = undefined;
+3
View File
@@ -90,6 +90,7 @@ const SETTINGS_MINIMAL_DEFAULTS = {
network_key: "GENERATE",
pan_id: "GENERATE",
ext_pan_id: "GENERATE",
enable_external_js: false,
},
frontend: {
enabled: settings.defaults.frontend!.enabled,
@@ -147,6 +148,7 @@ const SAMPLE_SETTINGS_SAVE = {
network_key: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
pan_id: 12345,
ext_pan_id: [8, 7, 6, 5, 4, 3, 2, 1],
enable_external_js: false,
},
frontend: {
enabled: true,
@@ -963,6 +965,7 @@ describe("Onboarding", () => {
network_key: "GENERATE",
pan_id: "GENERATE",
ext_pan_id: "GENERATE",
enable_external_js: false,
},
serial: {
port: SAMPLE_SETTINGS_SAVE.serial.port,
+3 -3
View File
@@ -3,7 +3,7 @@ import {beforeEach, describe, expect, it} from "vitest";
import "./mocks/data";
import fs from "node:fs";
import yaml from "js-yaml";
import {dump, load} from "js-yaml";
import objectAssignDeep from "object-assign-deep";
import mockedData from "../lib/util/data";
@@ -21,14 +21,14 @@ const minimalConfig = {
describe("Settings", () => {
const write = (file: string, json: Record<string, unknown>, reread = true): void => {
fs.writeFileSync(file, yaml.dump(json));
fs.writeFileSync(file, dump(json));
if (reread) {
settings.reRead();
}
};
const read = (file: string): unknown => yaml.load(fs.readFileSync(file, "utf8"));
const read = (file: string): unknown => load(fs.readFileSync(file, "utf8"));
const remove = (file: string): void => {
if (fs.existsSync(file)) {