Compare commits

...
105 Commits
Author SHA1 Message Date
Koen Kanters 403d3c090d chore(dev): release 1.40.1 (#23797) 2024-09-07 13:25:22 +02:00
Koen Kanters 9283c4b497 fix(ignore): update zigbee-herdsman-converters to 20.12.1 (#23884) 2024-09-07 11:17:24 +00:00
Koen Kanters 0d46354c48 fix(ignore): update zigbee-herdsman to 0.57.3 (#23877) 2024-09-06 20:26:15 +00:00
Koen Kanters 13a108ddbc fix: Log stack trace when update check fails https://github.com/Koenkk/zigbee2mqtt/discussions/23798 2024-09-06 14:37:20 +02:00
Koen Kanters 5524e881e6 fix(ignore): update zigbee-herdsman to 0.57.2 (#23872) 2024-09-06 12:19:21 +00:00
Koen Kanters a33c29f10a fix(ignore): update zigbee-herdsman-converters to 20.11.0 (#23837) 2024-09-05 12:39:50 +00:00
Koen Kanters fc02071c8f chore: fix e2da2fa8d3 2024-09-05 14:37:30 +02:00
Koen Kanters 06556d44f3 chore: merge master to dev 2024-09-05 13:35:31 +02:00
Koen Kanters 13084d22a7 chore: Update new device support template 2024-09-05 13:35:15 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 14787d3a6a chore(deps): bump peter-evans/create-pull-request from 6 to 7 (#23832)
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 6 to 7.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v6...v7)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-03 20:21:00 +02:00
Jose Luis Pedrosaandjose e2da2fa8d3 chore: Invoke chart CI (#23819)
The workflow name on the chart repository is OnZigbee2MQTTRelease, this event means that a new version of zigbe2mqtt is has been released, it should probably trigger too the release, but that is a chart repo concern

Co-authored-by: jose <jose.pedrosa@reddit.com>
2024-09-03 20:20:40 +02:00
Koen Kanters 6dade8b11f fix(ignore): update zigbee-herdsman-converters to 20.9.0 (#23822) 2024-09-02 20:48:09 +00:00
Koen Kanters 557b17ad5b fix: Fix Cannot read properties of undefined (reading 'some') https://github.com/Koenkk/zigbee2mqtt/issues/23821 2024-09-02 22:22:19 +02:00
Koen Kanters 64222d231f Update zigbee-herdsman-converters to 20.8.5 (#23799)
* fix(ignore): update zigbee-herdsman-converters to 20.8.5

* Update utils.ts
2024-09-01 19:23:14 +00:00
Koen Kanters b7fa722109 fix(ignore): update dependencies (#23790) 2024-09-01 20:23:49 +02:00
github-actions[bot] 4a1d983168 chore: promote to dev 2024-09-01 18:22:59 +00:00
Koen Kanters 6fa1dabecb chore(dev): release 1.40.0 (#23473) 2024-09-01 20:12:13 +02:00
Koen Kanters 3b2ba0950e fix(ignore): update zigbee-herdsman-converters to 20.8.4 (#23791) 2024-09-01 18:10:07 +00:00
Koen Kanters 9f40936452 fix(ignore): update zigbee-herdsman-converters to 20.8.1 (#23783) 2024-08-30 18:35:59 +00:00
Koen Kanters b2bae6ba94 fix: Add logging when loading external converters https://github.com/Koenkk/zigbee2mqtt/issues/23703 2024-08-30 20:23:55 +02:00
Koen Kanters 406deaa9da fix(ignore): update zigbee-herdsman to 0.57.1 (#23781) 2024-08-30 18:14:13 +00:00
Koen Kanters 65b1e60a3d fix(ignore): update zigbee-herdsman-converters to 20.8.0 (#23777) 2024-08-29 19:28:15 +00:00
Koen Kanters 6ed9b26657 fix(ignore): update zigbee-herdsman-converters to 20.7.0 (#23759) 2024-08-27 18:57:31 +00:00
Koen Kanters cf3298bf6f fix: Update watchdog delays (#23757) 2024-08-27 20:50:24 +02:00
Koen Kanters 00948c0be1 chore: merge master to dev 2024-08-27 20:48:05 +02:00
Koen Kanters ed9de74f91 chore: Update New device support request template 2024-08-27 20:47:45 +02:00
Koen Kanters 41587211e0 fix(ignore): update zigbee-herdsman to 0.57.0 (#23758) 2024-08-27 18:43:23 +00:00
Ilya Kirov b4176d6fee feat: Add support for zboss adapter (#23747)
https://github.com/Koenkk/zigbee-herdsman/pull/1165
2024-08-27 20:37:00 +02:00
Koen Kanters ccdf6b3c6d fix(ignore): update zigbee-herdsman-converters to 20.6.0 (#23750) 2024-08-26 19:06:20 +00:00
Koen Kanters 8a05077792 fix(ignore): update dependencies (#23731) 2024-08-26 20:12:08 +02:00
Koen Kanters df7772bfe0 fix(ignore): update zigbee-herdsman-converters to 20.5.0 (#23723) 2024-08-24 13:07:53 +00:00
Koen Kanters 6ae9970efd fix(ignore): update zigbee-herdsman-converters to 20.4.0 (#23692) 2024-08-21 19:45:53 +00:00
Koen Kanters b92072fc6a Merge remote-tracking branch 'origin/master' into dev 2024-08-20 21:38:19 +02:00
Koen Kanters 5bdf90df9e chore: Trigger zigbee2mqtt-chart release on release https://github.com/Koenkk/zigbee2mqtt-chart/issues/7 2024-08-20 21:36:01 +02:00
Koen Kanters 22980177b6 fix(ignore): Fix crash when retrieving state (#23679) 2024-08-20 20:57:09 +02:00
Koen Kanters 837cbe5f26 fix(ignore): update zigbee-herdsman-converters to 20.3.0 (#23666) 2024-08-19 19:13:00 +00:00
Koen Kanters e95116c773 fix(ignore): update zigbee-herdsman to 0.56.2 (#23658) 2024-08-18 12:38:04 +00:00
Koen Kanters 27300ba8da fix(ignore): update dependencies (#23653) 2024-08-18 14:03:00 +02:00
Koen Kanters 801ab9066f fix(ignore): update zigbee-herdsman to 0.56.0 (#23643) 2024-08-16 20:57:46 +00:00
Koen Kanters 2a631f3551 chore: merge master to dev 2024-08-16 22:31:18 +02:00
Koen Kanters 66fefaa827 chore: Add fail_pr_to_master.yaml 2024-08-16 22:30:56 +02:00
Koen Kanters c986fd6c66 fix(ignore): update zigbee-herdsman-converters to 20.2.0 (#23628) 2024-08-15 19:32:00 +00:00
Koen Kanters 29f436ab95 fix(ignore): update zigbee-herdsman-converters to 20.1.1 (#23612) 2024-08-13 19:40:53 +00:00
Koen KantersandKoenkk d827d954ed fix(ignore): update zigbee-herdsman-converters to 20.0.0 (#23595)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-12 11:39:30 +00:00
Koen Kanters 8ae2c4d42d chore: merge master to dev 2024-08-12 13:15:23 +02:00
Koen Kanters 9027c41fe8 Revert "Added pictures for _TZE204_ncti2pro (ZSVIOT PN6) (#23552)"
This reverts commit 00b5e983d9.
2024-08-12 13:15:02 +02:00
Andre Klärner a5748d620b fix: Add OCI labels to image (#23578)
this adds Docker Labels according to the OCI Image Format Specification
which means that tools like renovate can automatically find the
changelogs from a docker image, helping the people like me judge the
impact of upgrading their z2m instance from the pull request renovate
generates.
2024-08-11 21:59:44 +02:00
Koen Kanters d1734feb21 chore: merge master to dev 2024-08-09 14:37:08 +02:00
Vitaliy 00b5e983d9 Added pictures for _TZE204_ncti2pro (ZSVIOT PN6) (#23552)
* Added pictures for _TZE204_ncti2pro (ZSVIOT PN6)

* Removed jpg file and renamed TS0601_pn6_way_controller.png to PN6.png
2024-08-09 14:36:52 +02:00
Koen Kanters efd3d3dfb0 fix: Set zigbee-herdsman transmit power through settings on startup (#23566)
* fix: Set zigbee-herdsman transmit power through settings on startup

* use advanced
2024-08-09 12:33:59 +00:00
Koen KantersandKoenkk aeba93d0a3 fix(ignore): update zigbee-herdsman-converters to 19.75.0 (#23565)
* fix(ignore): update zigbee-herdsman-converters to 19.75.0

* fix

---------

Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-08 21:14:32 +02:00
Koen KantersandKoenkk 7ca456d20e fix(ignore): update zigbee-herdsman to 0.55.5 (#23564)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-08 19:06:42 +00:00
Nerivec 13ac8a0f53 fix: Improve looping performance (#23541)
* Use herdsman improved looping.

* Fix lint.

* Fix tests.

* Feedback
2024-08-08 20:21:40 +02:00
Koen KantersandKoenkk fe6cacd87e fix(ignore): update zigbee-herdsman-converters to 19.74.0 (#23539)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-06 19:42:34 +00:00
Koen KantersandKoenkk 00923685d2 fix(ignore): update zigbee-herdsman-converters to 19.73.1 (#23527)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-05 21:14:09 +00:00
Koen KantersandKoenkk bd7dec0f11 fix(ignore): update dependencies (#23508)
* fix(ignore): update dependencies

* update

---------

Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-05 21:08:40 +00:00
Koen Kanters 9bb21f486c chore: merge master to dev 2024-08-04 22:14:52 +02:00
LaurentandKoen Kanters d0f5733db3 fix: Remove group from Home Assistant when removed (#23492)
* Fire event when group gets removed

* Add a unit test for the new behavior

* Make prettier happy

* Add missing return type

* Merge device and group removal events

* Unbreak bridgeLegacy.ts

* Update homeassistant.ts

* Update bridge.ts

* Update bridge.ts

---------

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2024-08-04 22:14:36 +02:00
Koen Kanters 5b610afa8f fix: Add min/max for some adapter options (#23503)
* fix: Add min/max for some adapter options

* update
2024-08-04 19:53:29 +00:00
Koen KantersandKoenkk 2cb8a8f5a5 fix(ignore): update zigbee-herdsman-converters to 19.73.0 (#23495)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-03 11:05:54 +00:00
Koen Kanters 3217332e46 fix: Fix expose category ignored by HA discovery (#23494) 2024-08-03 13:00:29 +02:00
Koen KantersandKoenkk a25ef81cf4 fix(ignore): update zigbee-herdsman to 0.55.4 (#23493)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-03 10:31:41 +00:00
Koen Kanters eac11a6439 chore: fix package.json indentation 2024-08-01 21:11:44 +02:00
github-actions[bot] a330830cde chore: promote to dev 2024-08-01 18:57:20 +00:00
Koen Kanters e132316ac4 chore(dev): release 1.39.1 (#23221) 2024-08-01 20:42:11 +02:00
Koen KantersandKoenkk 3dc9a05b0c fix(ignore): update zigbee-herdsman-converters to 19.72.0 (#23472)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-08-01 18:17:31 +00:00
Koen Kanters 7cc5209af3 chore: apply prettier to whole repo 2024-07-29 21:26:33 +02:00
Koen Kanters 02dc8a7b87 chore: apply prettier to whole repo 2024-07-29 21:21:24 +02:00
Koen KantersandKoenkk 3a573da83c fix(ignore): update dependencies (#23440)
* fix(ignore): update dependencies

* fix

---------

Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-28 14:55:34 +02:00
Koen Kantersandnurikk 26718eefea fix(ignore): update zigbee2mqtt-frontend to 0.7.4 (#23432)
Co-authored-by: nurikk <1525421+nurikk@users.noreply.github.com>
2024-07-26 21:09:02 +00:00
Koen KantersandKoenkk 115b187c1a fix(ignore): update zigbee-herdsman to 0.55.3 (#23431)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-26 20:57:00 +00:00
Koen KantersandKoenkk fd65b9a108 fix(ignore): update zigbee-herdsman to 0.55.2 (#23422)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-25 20:59:34 +00:00
Koen Kanters d9ba4524e4 fix(ignore): fix 5fcf295ab8 2024-07-25 21:59:40 +02:00
Koen Kanters 5fcf295ab8 fix: Fix race condition in frontend extension (#23412) 2024-07-24 20:20:24 +00:00
Nerivec b7d9d9e142 fix: Improve looping when retrieving all devices from herdsman (#23405) 2024-07-24 21:58:48 +02:00
Koen KantersandKoenkk 1bf3032b98 fix(ignore): update zigbee-herdsman-converters to 19.71.1 (#23404)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-23 20:26:33 +00:00
Koen Kantersandnurikk 85453091e7 fix(ignore): update zigbee2mqtt-frontend to 0.7.3 (#23394)
Co-authored-by: nurikk <1525421+nurikk@users.noreply.github.com>
2024-07-22 19:45:24 +00:00
Koen Kanters 26b1613948 fix(ignore): Remove some unecessary string concatenations 2024-07-22 21:33:24 +02:00
Koen KantersandKoenkk c32c411f88 fix(ignore): update zigbee-herdsman to 0.55.1 (#23392)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-22 18:33:07 +00:00
Koen KantersandKoenkk cda8927dc7 fix(ignore): update zigbee-herdsman to 0.55.0 (#23370)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-18 18:42:17 +00:00
Koen KantersandKoenkk 8c71afd93c fix(ignore): update zigbee-herdsman-converters to 19.70.0 (#23359)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-17 20:03:44 +00:00
Koen KantersandKoenkk 712b920abb fix(ignore): update zigbee-herdsman to 0.54.1 (#23358)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-17 19:13:50 +00:00
Koen KantersandKoenkk f795399e10 fix(ignore): update zigbee-herdsman to 0.54.0 (#23351)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-16 20:07:55 +00:00
Koen Kanters 541221bbbd fix(ignore): Update channel change description (#23333)
* fix(ignore): Update channel change description

* Update settings.schema.json
2024-07-16 22:02:41 +02:00
Koen KantersandKoenkk 24c9159e19 fix(ignore): update zigbee-herdsman-converters to 19.69.0 (#23346)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-15 20:45:09 +00:00
Koen KantersandKoenkk 5d05ebbfd2 fix(ignore): update dependencies (#23276)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-15 21:53:56 +02:00
Koen KantersandKoenkk bd78f56d5f fix(ignore): update zigbee-herdsman-converters to 19.68.0 (#23338)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-13 20:05:52 +00:00
Koen Kanters af7467bf62 fix(ignore): Update test cases for https://github.com/Koenkk/zigbee2mqtt/pull/23328 2024-07-13 21:20:33 +02:00
Koen Kanters e3fa0bfb65 fix: Ignore cache when re-interviewing device (#23328)
* fix: Ignore cache when re-interviewing device

* updates
2024-07-12 10:15:33 +00:00
Koen KantersandKoenkk 00661fac4b fix(ignore): update zigbee-herdsman to 0.53.1 (#23331)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-12 12:12:30 +02:00
Koen KantersandKoenkk 2fbb73a009 fix(ignore): update zigbee-herdsman-converters to 19.67.0 (#23327)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-11 19:33:03 +00:00
Koen Kanters 8397f47649 fix: Fix availability blocking other extensions startup (#23316)
* fix: Fix availability blocking frontend startup

* Process feedback

* fix
2024-07-11 21:07:01 +02:00
Koen KantersandKoenkk fe87b5dec7 fix(ignore): update zigbee-herdsman to 0.53.0 (#23317)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-10 19:54:18 +00:00
Koen KantersandKoenkk 2916d7ee52 fix(ignore): update zigbee-herdsman-converters to 19.66.0 (#23306)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-09 19:16:08 +00:00
Koen KantersandKoenkk eee1da620f fix(ignore): update zigbee-herdsman to 0.52.0 (#23299)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-08 20:00:37 +00:00
Koen KantersandKoenkk 5663afb27e fix(ignore): update zigbee-herdsman-converters to 19.65.0 (#23272)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-06 09:59:51 +00:00
Koen Kanters 1bf385c5ff fix(ignore): fix 6b9247c84d 2024-07-06 11:56:16 +02:00
Koen Kanters 6b9247c84d fix: Truncate text of Home Assistant text sensor https://github.com/Koenkk/zigbee2mqtt/issues/23199 2024-07-06 11:22:26 +02:00
Nerivec 6cb62e53cf fix: Fix syslog output (#23269) 2024-07-06 10:57:24 +02:00
Koen Kanters f97ad2e951 fix(ignore): fix tests 2024-07-03 22:37:12 +02:00
Koen KantersandKoenkk e917802f5f fix(ignore): update zigbee-herdsman-converters to 19.64.0 (#23252)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-03 22:23:38 +02:00
Gernot MessowandKoen Kanters 4210edf60e fix: Allow definition to override HA discovery payload (#23075)
* Added function to override payloads for non-conforming devices

* override implementation for Bosch BTH-RA
* unit test including mockup for this device

* Experimental and broken integration of overrideHaConfig

* Not working experiments; mqtt publish debug output

* fixes

* Updates

* fix

---------

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2024-07-03 21:27:45 +02:00
Koen KantersandKoenkk addc2385b6 fix(ignore): update zigbee-herdsman-converters to 19.63.0 (#23244)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-02 20:05:00 +00:00
Koen KantersandKoenkk 2dbbed14f0 fix(ignore): update zigbee-herdsman to 0.51.0 (#23223)
Co-authored-by: Koenkk <2892853+Koenkk@users.noreply.github.com>
2024-07-01 18:52:01 +00:00
github-actions[bot] 5c42883779 chore: promote to dev 2024-07-01 18:32:21 +00:00
71 changed files with 12823 additions and 11611 deletions
+53 -68
View File
@@ -1,78 +1,63 @@
module.exports = {
'env': {
env: {
'jest/globals': true,
'es6': true,
'node': true,
es6: true,
node: true,
},
'extends': ['eslint:recommended', 'plugin:jest/recommended', 'plugin:jest/style', 'prettier'],
'parserOptions': {
'ecmaVersion': 2018,
'sourceType': 'module',
extends: ['eslint:recommended', 'plugin:jest/recommended', 'plugin:jest/style', 'prettier'],
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
'rules': {
rules: {
'require-jsdoc': 'off',
'no-prototype-builtins': 'off',
'@typescript-eslint/no-floating-promises': 'error',
},
'plugins': [
'jest',
'perfectionist',
plugins: ['jest', 'perfectionist'],
overrides: [
{
files: ['*.ts'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: ['plugin:@typescript-eslint/recommended'],
parserOptions: {
project: './tsconfig.json',
},
rules: {
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-unused-vars': 'error',
'no-return-await': 'error',
'perfectionist/sort-imports': [
'error',
{
groups: [
'type',
['builtin', 'external'],
'internal-type',
'internal',
['parent-type', 'sibling-type', 'index-type'],
['parent', 'sibling', 'index'],
'object',
'unknown',
],
customGroups: {
value: {},
type: {},
},
newlinesBetween: 'always',
internalPattern: ['~/**'],
type: 'natural',
order: 'asc',
ignoreCase: false,
},
],
},
},
],
'overrides': [{
files: ['*.ts'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: ['plugin:@typescript-eslint/recommended'],
parserOptions: {
project: './tsconfig.json',
},
rules: {
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-unused-vars': 'error',
'no-return-await': 'error',
"perfectionist/sort-imports": [
"error",
{
"groups": [
"type",
[
"builtin",
"external"
],
"internal-type",
"internal",
[
"parent-type",
"sibling-type",
"index-type"
],
[
"parent",
"sibling",
"index"
],
"object",
"unknown"
],
"custom-groups": {
"value": {},
"type": {}
},
"newlines-between": "always",
"internal-pattern": [
"~/**"
],
"type": "natural",
"order": "asc",
"ignore-case": false
}
],
},
}],
};
+2 -2
View File
@@ -1,4 +1,4 @@
github: [koenkk]
custom:
- https://www.paypal.me/koenkk
- https://www.buymeacoffee.com/koenkk
- https://www.paypal.me/koenkk
- https://www.buymeacoffee.com/koenkk
+18 -18
View File
@@ -1,20 +1,20 @@
blank_issues_enabled: false
contact_links:
- name: "IMPORTANT: Check development branch changelog first!!"
url: https://gist.github.com/Koenkk/bfd4c3d1725a2cccacc11d6ba51008ba
about: "Before submitting an issue, check that the issue hasn't already been solved in the development branch. Click 'Open' to see the release notes of the development branch. In case it is, you can read here how to switch to the development branch: https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html"
- name: Questions/discussion
url: https://github.com/Koenkk/zigbee2mqtt/discussions/new
about: Ask questions, discuss about devices or show things you made
- name: Frontend issues
url: https://github.com/nurikk/z2m-frontend/issues
about: Issues related to the frontend.
- name: Home Assistant addon issues
url: https://github.com/zigbee2mqtt/hassio-zigbee2mqtt/issues
about: Issues related to the Home Assistant addon.
- name: FAQ
url: https://www.zigbee2mqtt.io/guide/faq
about: Frequently asked questions.
- name: Support Chat
url: https://discord.gg/NyseBeK
about: Chat for feedback, questions and troubleshooting.
- name: 'IMPORTANT: Check development branch changelog first!!'
url: https://gist.github.com/Koenkk/bfd4c3d1725a2cccacc11d6ba51008ba
about: "Before submitting an issue, check that the issue hasn't already been solved in the development branch. Click 'Open' to see the release notes of the development branch. In case it is, you can read here how to switch to the development branch: https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html"
- name: Questions/discussion
url: https://github.com/Koenkk/zigbee2mqtt/discussions/new
about: Ask questions, discuss about devices or show things you made
- name: Frontend issues
url: https://github.com/nurikk/z2m-frontend/issues
about: Issues related to the frontend.
- name: Home Assistant addon issues
url: https://github.com/zigbee2mqtt/hassio-zigbee2mqtt/issues
about: Issues related to the Home Assistant addon.
- name: FAQ
url: https://www.zigbee2mqtt.io/guide/faq
about: Frequently asked questions.
- name: Support Chat
url: https://discord.gg/NyseBeK
about: Chat for feedback, questions and troubleshooting.
+34 -34
View File
@@ -1,38 +1,38 @@
name: Feature request
description: Suggest an idea for this project
title: "[Feature request]: "
title: '[Feature request]: '
labels: [feature request]
body:
- type: markdown
attributes:
value: |
**IMPORTANT:** Before submitting:
- Is your feature request related to the frontend? Then click [here](https://github.com/nurikk/zigbee2mqtt-frontend/issues/new?assignees=&labels=&template=feature_request.md&title=)
- type: textarea
id: textarea1
attributes:
label: Is your feature request related to a problem? Please describe
placeholder: A clear and concise description of what the problem is. Eg. I'm always frustrated when [...]
validations:
required: true
- type: textarea
id: textarea2
attributes:
label: Describe the solution you'd like
placeholder: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: textarea3
attributes:
label: Describe alternatives you've considered
placeholder: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
id: textarea4
attributes:
label: Additional context
placeholder: Add any other context or screenshots about the feature request here.
validations:
required: true
- type: markdown
attributes:
value: |
**IMPORTANT:** Before submitting:
- Is your feature request related to the frontend? Then click [here](https://github.com/nurikk/zigbee2mqtt-frontend/issues/new?assignees=&labels=&template=feature_request.md&title=)
- type: textarea
id: textarea1
attributes:
label: Is your feature request related to a problem? Please describe
placeholder: A clear and concise description of what the problem is. Eg. I'm always frustrated when [...]
validations:
required: true
- type: textarea
id: textarea2
attributes:
label: Describe the solution you'd like
placeholder: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: textarea3
attributes:
label: Describe alternatives you've considered
placeholder: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
id: textarea4
attributes:
label: Additional context
placeholder: Add any other context or screenshots about the feature request here.
validations:
required: true
+53 -39
View File
@@ -1,43 +1,57 @@
name: New device support request
description: Request support for a new device
title: "[New device support]: "
title: '[New device support]: '
labels: [new device support]
body:
- type: markdown
attributes:
value: |
**IMPORTANT:** Before submitting:
- Make sure this device is not already supported in the dev branch by checking the [dev branch changelog](https://gist.github.com/Koenkk/bfd4c3d1725a2cccacc11d6ba51008ba#new-supported-devices)
- Make sure there is no existing issue or PR for this device already, search for your device here: https://github.com/Koenkk/zigbee2mqtt/issues
- Follow this [guide](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html)
- If you are using the Home Assistant addon and are still on 1.18.1, check the first point of [this](https://github.com/Koenkk/zigbee2mqtt/releases/tag/1.19.0)
- type: input
id: link
attributes:
label: Link
description: Link of this device (product page)
placeholder: https://www.linktomydevice.org
validations:
required: true
- type: input
id: database
attributes:
label: Database entry
description: Entry of this device in `data/database.db` after pairing it
placeholder: '{"id":53,"type":"Router","ieeeAddr":"0x10458d00024284f69","nwkAddr":10148,"manufId":4151,"manufName":"LUMI","powerSource":"DC Source","modelId":"lumi.relay.c2acn01","epList":[1,2],"endpoints":{"1":{"profId":260,"epId":1,"devId":257,"inClusterList":[0,3,4,5,1,2,10,6,16,2820,12],"outClusterList":[25,10],"clusters":{"genBasic":{"attributes":{"modelId":"lumi.relay.c2acn01","appVersion":1,"manufacturerName":"LUMI","powerSource":4,"zclVersion":0,"stackVersion":2,"hwVersion":18,"dateCode":"8-6-2020"}},"genAnalogInput":{"attributes":{"presentValue":129.04425048828125}},"genOnOff":{"attributes":{"61440":117440715,"onOff":1}}},"binds":[],"configuredReportings":[],"meta":{}},"2":{"profId":260,"epId":2,"devId":257,"inClusterList":[6,16,4,5],"outClusterList":[],"clusters":{"genOnOff":{"attributes":{"61440":237478966,"onOff":0}}},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":1,"stackVersion":2,"hwVersion":18,"dateCode":"8-6-2020","zclVersion":0,"interviewCompleted":true,"meta":{},"lastSeen":1640285631405}'
validations:
required: true
- type: textarea
id: notes
attributes:
label: Comments
placeholder: I tried to follow the supporting new device page but got stuck at...
validations:
required: true
- type: textarea
attributes:
label: External definition
description: See [Creating the external definition](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html#_2-creating-the-external-definition)
render: shell
validations:
required: true
- type: markdown
attributes:
value: |
**IMPORTANT:** Before submitting:
- Make sure this device is not already supported in the dev branch by checking the [dev branch changelog](https://gist.github.com/Koenkk/bfd4c3d1725a2cccacc11d6ba51008ba#new-supported-devices)
- Make sure there is no existing issue or PR for this device already, search for your device here: https://github.com/Koenkk/zigbee2mqtt/issues
- Follow this [guide](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html)
- If you are using the Home Assistant addon and are still on 1.18.1, check the first point of [this](https://github.com/Koenkk/zigbee2mqtt/releases/tag/1.19.0)
- type: input
id: link
attributes:
label: Link
description: Link of this device (product page)
placeholder: https://www.linktomydevice.org
validations:
required: true
- type: input
id: database
attributes:
label: Database entry
description: Entry of this device in `data/database.db` after pairing it
placeholder: '{"id":53,"type":"Router","ieeeAddr":"0x10458d00024284f69","nwkAddr":10148,"manufId":4151,"manufName":"LUMI","powerSource":"DC Source","modelId":"lumi.relay.c2acn01","epList":[1,2],"endpoints":{"1":{"profId":260,"epId":1,"devId":257,"inClusterList":[0,3,4,5,1,2,10,6,16,2820,12],"outClusterList":[25,10],"clusters":{"genBasic":{"attributes":{"modelId":"lumi.relay.c2acn01","appVersion":1,"manufacturerName":"LUMI","powerSource":4,"zclVersion":0,"stackVersion":2,"hwVersion":18,"dateCode":"8-6-2020"}},"genAnalogInput":{"attributes":{"presentValue":129.04425048828125}},"genOnOff":{"attributes":{"61440":117440715,"onOff":1}}},"binds":[],"configuredReportings":[],"meta":{}},"2":{"profId":260,"epId":2,"devId":257,"inClusterList":[6,16,4,5],"outClusterList":[],"clusters":{"genOnOff":{"attributes":{"61440":237478966,"onOff":0}}},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":1,"stackVersion":2,"hwVersion":18,"dateCode":"8-6-2020","zclVersion":0,"interviewCompleted":true,"meta":{},"lastSeen":1640285631405}'
validations:
required: true
- type: input
id: z2m_version
attributes:
label: Zigbee2MQTT version
description: Can be found in the frontend -> settings -> about -> Zigbee2MQTT version. Are you running Zigbee2MQTT 1.18.1? Then read [this](https://github.com/Koenkk/zigbee2mqtt/releases/tag/1.19.0).
placeholder: '1.22.1'
validations:
required: true
- type: textarea
id: notes
attributes:
label: Comments
placeholder: I tried to follow the supporting new device page but got stuck at...
validations:
required: true
- type: textarea
attributes:
label: External definition
description: See [Creating the external definition](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html#_2-creating-the-external-definition)
render: shell
validations:
required: true
- type: textarea
attributes:
label: What does/doesn't work with the external definition?
description: Indicate what works and what doesn't work with the external definition.
validations:
required: true
+72 -72
View File
@@ -2,75 +2,75 @@ name: Problem report
description: Create a report to help us improve
labels: [problem]
body:
- type: markdown
attributes:
value: |
**IMPORTANT:** Before submitting:
- You read the [FAQ](https://www.zigbee2mqtt.io/guide/faq/)
- Are you using an EZSP adapter (e.g. Dongle-E/SkyConnect)? Try the [new driver](https://github.com/Koenkk/zigbee2mqtt/discussions/21462)
- Zigbee2MQTT fails to start? Read [this](https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start.html)
- Make sure the bug also occurs in the [dev branch](https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html)
- Make sure you are using the [latest firmware](https://www.zigbee2mqtt.io/guide/adapters/#recommended) on your adapter
- The issue has not been [reported already](https://github.com/Koenkk/zigbee2mqtt/issues)
- Is your issue related to the frontend? Then click [here](https://github.com/nurikk/zigbee2mqtt-frontend/issues/new?assignees=&labels=bug%2Ctriage&template=bug_report.yaml&title=%5BBug%5D%3A+)
- If you are using the Home Assistant addon and are still on 1.18.1, check the first point of [this](https://github.com/Koenkk/zigbee2mqtt/releases/tag/1.19.0)
- type: textarea
id: what_happend
attributes:
label: What happened?
validations:
required: true
- type: textarea
id: expect_to_happen
attributes:
label: What did you expect to happen?
placeholder: I expected that ...
validations:
required: false
- type: textarea
id: reproduce
attributes:
label: How to reproduce it (minimal and precise)
placeholder: First do this, then this..
validations:
required: false
- type: input
id: z2m_version
attributes:
label: Zigbee2MQTT version
description: Can be found in the frontend -> settings -> about -> Zigbee2MQTT version. Are you running Zigbee2MQTT 1.18.1? Then read [this](https://github.com/Koenkk/zigbee2mqtt/releases/tag/1.19.0).
placeholder: '1.22.1'
validations:
required: true
- type: input
id: adapter_fwversion
attributes:
label: Adapter firmware version
description: Can be found in the frontend -> settings -> about -> coordinator revision
placeholder: '20211210'
validations:
required: true
- type: input
id: adapter
attributes:
label: Adapter
description: The adapter you are using. In case of EZSP, try the [new `ember` driver](https://github.com/Koenkk/zigbee2mqtt/discussions/21462) first.
placeholder: Electrolama zig-a-zig-ah! (zzh!), Slaesh's CC2652RB stick, ...
validations:
required: true
- type: input
id: setup
attributes:
label: Setup
description: How do you run Z2M (plain, add-on...) and on what machine (Pi, x86-64, containerized...)?
placeholder: Add-on on Home Assistant OS on Intel NUC, Plain on Docker container, ...
validations:
required: true
- type: textarea
id: log
attributes:
label: Debug log
description: After enabling [debug logging](https://www.zigbee2mqtt.io/guide/configuration/logging.html#debugging) the log can be found under `data/log`. Attach the file below
placeholder: Click here and drag the file into it or click on "Attach files by.." below
validations:
required: false
- type: markdown
attributes:
value: |
**IMPORTANT:** Before submitting:
- You read the [FAQ](https://www.zigbee2mqtt.io/guide/faq/)
- Are you using an EZSP adapter (e.g. Dongle-E/SkyConnect)? Try the [new driver](https://github.com/Koenkk/zigbee2mqtt/discussions/21462)
- Zigbee2MQTT fails to start? Read [this](https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start.html)
- Make sure the bug also occurs in the [dev branch](https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html)
- Make sure you are using the [latest firmware](https://www.zigbee2mqtt.io/guide/adapters/#recommended) on your adapter
- The issue has not been [reported already](https://github.com/Koenkk/zigbee2mqtt/issues)
- Is your issue related to the frontend? Then click [here](https://github.com/nurikk/zigbee2mqtt-frontend/issues/new?assignees=&labels=bug%2Ctriage&template=bug_report.yaml&title=%5BBug%5D%3A+)
- If you are using the Home Assistant addon and are still on 1.18.1, check the first point of [this](https://github.com/Koenkk/zigbee2mqtt/releases/tag/1.19.0)
- type: textarea
id: what_happend
attributes:
label: What happened?
validations:
required: true
- type: textarea
id: expect_to_happen
attributes:
label: What did you expect to happen?
placeholder: I expected that ...
validations:
required: false
- type: textarea
id: reproduce
attributes:
label: How to reproduce it (minimal and precise)
placeholder: First do this, then this..
validations:
required: false
- type: input
id: z2m_version
attributes:
label: Zigbee2MQTT version
description: Can be found in the frontend -> settings -> about -> Zigbee2MQTT version. Are you running Zigbee2MQTT 1.18.1? Then read [this](https://github.com/Koenkk/zigbee2mqtt/releases/tag/1.19.0).
placeholder: '1.22.1'
validations:
required: true
- type: input
id: adapter_fwversion
attributes:
label: Adapter firmware version
description: Can be found in the frontend -> settings -> about -> coordinator revision
placeholder: '20211210'
validations:
required: true
- type: input
id: adapter
attributes:
label: Adapter
description: The adapter you are using. In case of EZSP, try the [new `ember` driver](https://github.com/Koenkk/zigbee2mqtt/discussions/21462) first.
placeholder: Electrolama zig-a-zig-ah! (zzh!), Slaesh's CC2652RB stick, ...
validations:
required: true
- type: input
id: setup
attributes:
label: Setup
description: How do you run Z2M (plain, add-on...) and on what machine (Pi, x86-64, containerized...)?
placeholder: Add-on on Home Assistant OS on Intel NUC, Plain on Docker container, ...
validations:
required: true
- type: textarea
id: log
attributes:
label: Debug log
description: After enabling [debug logging](https://www.zigbee2mqtt.io/guide/configuration/logging.html#debugging) the log can be found under `data/log`. Attach the file below
placeholder: Click here and drag the file into it or click on "Attach files by.." below
validations:
required: false
+60 -60
View File
@@ -1,64 +1,64 @@
name: Wrong device picture/vendor/model/description
description: Use if device is detected as supported and is fully functional but has a wrong picture, vendor, model or description
title: "[Wrong device]: "
title: '[Wrong device]: '
labels: [wrong device]
body:
- type: markdown
attributes:
value: |
Only use this if the device is detected as **supported** and is **fully functional** but has a wrong picture, vendor, model or description.
- type: input
id: link
attributes:
label: Link
description: Link of this device (product page)
placeholder: https://www.linktomydevice.org
validations:
required: true
- type: input
id: model
attributes:
label: Model
description: Expected model, model that is printed on the device, for Tuya device this is NOT something like TS0601 or _TZE200_cf1sl3tj
placeholder: RTCGQ01LM
validations:
required: true
- type: input
id: description
attributes:
label: Description
description: Expected description
placeholder: Motion sensor
validations:
required: true
- type: input
id: vendor
attributes:
label: Vendor
description: Expected vendor
placeholder: Xiaomi
validations:
required: true
- type: input
id: picture
attributes:
label: Picture (link)
description: Expected picture
placeholder: https://www.linktomydevice.org/RTCGQ01LM.jpg
validations:
required: true
- type: input
id: database
attributes:
label: Database entry
description: Entry of this device in `data/database.db` after pairing it
placeholder: '{"id":53,"type":"Router","ieeeAddr":"0x10458d00024284f69","nwkAddr":10148,"manufId":4151,"manufName":"LUMI","powerSource":"DC Source","modelId":"lumi.relay.c2acn01","epList":[1,2],"endpoints":{"1":{"profId":260,"epId":1,"devId":257,"inClusterList":[0,3,4,5,1,2,10,6,16,2820,12],"outClusterList":[25,10],"clusters":{"genBasic":{"attributes":{"modelId":"lumi.relay.c2acn01","appVersion":1,"manufacturerName":"LUMI","powerSource":4,"zclVersion":0,"stackVersion":2,"hwVersion":18,"dateCode":"8-6-2020"}},"genAnalogInput":{"attributes":{"presentValue":129.04425048828125}},"genOnOff":{"attributes":{"61440":117440715,"onOff":1}}},"binds":[],"configuredReportings":[],"meta":{}},"2":{"profId":260,"epId":2,"devId":257,"inClusterList":[6,16,4,5],"outClusterList":[],"clusters":{"genOnOff":{"attributes":{"61440":237478966,"onOff":0}}},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":1,"stackVersion":2,"hwVersion":18,"dateCode":"8-6-2020","zclVersion":0,"interviewCompleted":true,"meta":{},"lastSeen":1640285631405}'
validations:
required: true
- type: textarea
id: notes
attributes:
label: Notes
placeholder: Some additional notes...
validations:
required: false
- type: markdown
attributes:
value: |
Only use this if the device is detected as **supported** and is **fully functional** but has a wrong picture, vendor, model or description.
- type: input
id: link
attributes:
label: Link
description: Link of this device (product page)
placeholder: https://www.linktomydevice.org
validations:
required: true
- type: input
id: model
attributes:
label: Model
description: Expected model, model that is printed on the device, for Tuya device this is NOT something like TS0601 or _TZE200_cf1sl3tj
placeholder: RTCGQ01LM
validations:
required: true
- type: input
id: description
attributes:
label: Description
description: Expected description
placeholder: Motion sensor
validations:
required: true
- type: input
id: vendor
attributes:
label: Vendor
description: Expected vendor
placeholder: Xiaomi
validations:
required: true
- type: input
id: picture
attributes:
label: Picture (link)
description: Expected picture
placeholder: https://www.linktomydevice.org/RTCGQ01LM.jpg
validations:
required: true
- type: input
id: database
attributes:
label: Database entry
description: Entry of this device in `data/database.db` after pairing it
placeholder: '{"id":53,"type":"Router","ieeeAddr":"0x10458d00024284f69","nwkAddr":10148,"manufId":4151,"manufName":"LUMI","powerSource":"DC Source","modelId":"lumi.relay.c2acn01","epList":[1,2],"endpoints":{"1":{"profId":260,"epId":1,"devId":257,"inClusterList":[0,3,4,5,1,2,10,6,16,2820,12],"outClusterList":[25,10],"clusters":{"genBasic":{"attributes":{"modelId":"lumi.relay.c2acn01","appVersion":1,"manufacturerName":"LUMI","powerSource":4,"zclVersion":0,"stackVersion":2,"hwVersion":18,"dateCode":"8-6-2020"}},"genAnalogInput":{"attributes":{"presentValue":129.04425048828125}},"genOnOff":{"attributes":{"61440":117440715,"onOff":1}}},"binds":[],"configuredReportings":[],"meta":{}},"2":{"profId":260,"epId":2,"devId":257,"inClusterList":[6,16,4,5],"outClusterList":[],"clusters":{"genOnOff":{"attributes":{"61440":237478966,"onOff":0}}},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":1,"stackVersion":2,"hwVersion":18,"dateCode":"8-6-2020","zclVersion":0,"interviewCompleted":true,"meta":{},"lastSeen":1640285631405}'
validations:
required: true
- type: textarea
id: notes
attributes:
label: Notes
placeholder: Some additional notes...
validations:
required: false
+10 -10
View File
@@ -1,13 +1,13 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
target-branch: dev
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'daily'
target-branch: dev
- package-ecosystem: "docker"
directory: "/docker"
schedule:
interval: "daily"
target-branch: dev
- package-ecosystem: 'docker'
directory: '/docker'
schedule:
interval: 'daily'
target-branch: dev
+145 -133
View File
@@ -3,139 +3,151 @@ name: ci
on: [pull_request, push]
permissions:
contents: write
pull-requests: write
contents: write
pull-requests: write
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
with:
# Required for `release: merge dev -> master and promote dev`
token: ${{secrets.GH_TOKEN}}
- uses: actions/checkout@v4
if: ((github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push') == false
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org/
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Lint
run: |
npm run pretty:check
npm run eslint
- name: Test
run: npm run test-with-coverage
- name: Docker login
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
run: echo ${{ secrets.DOCKER_KEY }} | docker login -u koenkk --password-stdin
- name: Docker login ghcr.io
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
run: echo ${{ secrets.GH_TOKEN }} | docker login ghcr.io -u koenkk --password-stdin
- name: Docker setup - QEMU
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
uses: docker/setup-qemu-action@v3
with:
platforms: all
- name: Docker setup - Buildx
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
id: buildx
uses: docker/setup-buildx-action@v3
with:
version: latest
- name: "dev: Docker build"
if: github.ref == 'refs/heads/dev' && github.event_name == 'push'
run: |
docker buildx build \
--build-arg COMMIT=$(git rev-parse --short HEAD) \
--platform linux/arm64/v8,linux/386,linux/amd64,linux/arm/v6,linux/arm/v7 \
-f docker/Dockerfile \
--provenance=false \
--push \
-t koenkk/zigbee2mqtt:latest-dev -t ghcr.io/koenkk/zigbee2mqtt:latest-dev \
.
- name: "release: Docker build"
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
run: |
TAG="$(git describe --tags)"
docker buildx build \
--build-arg COMMIT=$(git rev-parse --short HEAD) \
--platform linux/arm64/v8,linux/386,linux/amd64,linux/arm/v6,linux/arm/v7 \
-f docker/Dockerfile \
--provenance=false \
--push \
-t koenkk/zigbee2mqtt:latest -t "koenkk/zigbee2mqtt:$TAG" -t ghcr.io/koenkk/zigbee2mqtt:latest -t "ghcr.io/koenkk/zigbee2mqtt:$TAG" \
.
- name: "release: Publish to npm"
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN }}
- name: "dev: Trigger zigbee2mqtt/hassio-zigbee2mqtt build"
if: github.ref == 'refs/heads/dev' && github.event_name == 'push'
run: |
curl \
-X POST \
-H "Authorization: token ${{ secrets.GH_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/zigbee2mqtt/hassio-zigbee2mqtt/actions/workflows/ci.yml/dispatches \
-d '{"ref":"master","inputs":{}}'
- name: "release: Trigger zigbee2mqtt/hassio-zigbee2mqtt build"
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
run: |
TAG=${GITHUB_REF#refs/*/}
echo "Triggering with tag '$TAG'"
curl \
-X POST \
-H "Authorization: token ${{ secrets.GH_TOKEN }}" \
-H "Accept: application/vnd.github.everest-preview+json" \
-H "Content-Type: application/json" \
https://api.github.com/repos/zigbee2mqtt/hassio-zigbee2mqtt/dispatches \
--data "{\"event_type\": \"release\", \"client_payload\": { \"version\": \"$TAG-1\"}}"
- name: "release: merge dev -> master and promote dev"
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
run: |
TAG=${GITHUB_REF#refs/*/}
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git fetch --unshallow
git fetch origin
git checkout master
git merge --ff-only origin/dev
git push origin master
git checkout dev
jq ".version = \"$TAG-dev\"" package.json > package.json.tmp
jq ".version = \"$TAG-dev\"" package-lock.json > package-lock.json.tmp
mv package.json.tmp package.json
mv package-lock.json.tmp package-lock.json
git add -A
git commit -m "chore: promote to dev"
git push origin dev
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
with:
# Required for `release: merge dev -> master and promote dev`
token: ${{secrets.GH_TOKEN}}
- uses: actions/checkout@v4
if: ((github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push') == false
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org/
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Lint
run: |
npm run pretty:check
npm run eslint
- name: Test
run: npm run test-with-coverage
- name: Docker login
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
run: echo ${{ secrets.DOCKER_KEY }} | docker login -u koenkk --password-stdin
- name: Docker login ghcr.io
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
run: echo ${{ secrets.GH_TOKEN }} | docker login ghcr.io -u koenkk --password-stdin
- name: Docker setup - QEMU
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
uses: docker/setup-qemu-action@v3
with:
platforms: all
- name: Docker setup - Buildx
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
id: buildx
uses: docker/setup-buildx-action@v3
with:
version: latest
- name: 'dev: Docker build'
if: github.ref == 'refs/heads/dev' && github.event_name == 'push'
run: |
docker buildx build \
--build-arg COMMIT=$(git rev-parse --short HEAD) \
--platform linux/arm64/v8,linux/386,linux/amd64,linux/arm/v6,linux/arm/v7 \
-f docker/Dockerfile \
--provenance=false \
--push \
-t koenkk/zigbee2mqtt:latest-dev -t ghcr.io/koenkk/zigbee2mqtt:latest-dev \
.
- name: 'release: Docker build'
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
run: |
TAG="$(git describe --tags)"
docker buildx build \
--build-arg COMMIT=$(git rev-parse --short HEAD) \
--platform linux/arm64/v8,linux/386,linux/amd64,linux/arm/v6,linux/arm/v7 \
-f docker/Dockerfile \
--provenance=false \
--push \
-t koenkk/zigbee2mqtt:latest -t "koenkk/zigbee2mqtt:$TAG" -t ghcr.io/koenkk/zigbee2mqtt:latest -t "ghcr.io/koenkk/zigbee2mqtt:$TAG" \
.
- name: 'release: Publish to npm'
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN }}
- name: 'dev: Trigger zigbee2mqtt/hassio-zigbee2mqtt build'
if: github.ref == 'refs/heads/dev' && github.event_name == 'push'
run: |
curl \
-X POST \
-H "Authorization: token ${{ secrets.GH_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/zigbee2mqtt/hassio-zigbee2mqtt/actions/workflows/ci.yml/dispatches \
-d '{"ref":"master","inputs":{}}'
- name: 'release: Trigger zigbee2mqtt/hassio-zigbee2mqtt build'
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
run: |
TAG=${GITHUB_REF#refs/*/}
echo "Triggering with tag '$TAG'"
curl \
-X POST \
-H "Authorization: token ${{ secrets.GH_TOKEN }}" \
-H "Accept: application/vnd.github.everest-preview+json" \
-H "Content-Type: application/json" \
https://api.github.com/repos/zigbee2mqtt/hassio-zigbee2mqtt/dispatches \
--data "{\"event_type\": \"release\", \"client_payload\": { \"version\": \"$TAG-1\"}}"
- name: 'release: Trigger zigbee2mqtt-chart image update'
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
run: |
TAG=${GITHUB_REF#refs/*/}
echo "Triggering with tag '$TAG'"
curl \
-X POST \
-H "Authorization: token ${{ secrets.GH_TOKEN }}" \
-H "Accept: application/vnd.github.everest-preview+json" \
-H "Content-Type: application/json" \
https://api.github.com/repos/Koenkk/zigbee2mqtt-chart/actions/workflows/OnZigbee2MQTTRelease/dispatches \
-d '{"ref":"main","inputs":{"zigbee2mqtt_version":"$TAG" }}'
- name: 'release: merge dev -> master and promote dev'
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
run: |
TAG=${GITHUB_REF#refs/*/}
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git fetch --unshallow
git fetch origin
git checkout master
git merge --ff-only origin/dev
git push origin master
git checkout dev
jq --indent 4 ".version = \"$TAG-dev\"" package.json > package.json.tmp
jq --indent 4 ".version = \"$TAG-dev\"" package-lock.json > package-lock.json.tmp
mv package.json.tmp package.json
mv package-lock.json.tmp package-lock.json
git add -A
git commit -m "chore: promote to dev"
git push origin dev
tests:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
runs-on: ${{ matrix.os }}
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
registry-url: https://registry.npmjs.org/
cache: 'npm'
- name: Install dependencies
# --ignore-scripts prevents the serialport build which often fails on Windows
run: npm ci --ignore-scripts
- name: Build
run: npm run build
- name: Test
run: npm run test-with-coverage
tests:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
runs-on: ${{ matrix.os }}
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
registry-url: https://registry.npmjs.org/
cache: 'npm'
- name: Install dependencies
# --ignore-scripts prevents the serialport build which often fails on Windows
run: npm ci --ignore-scripts
- name: Build
run: npm run build
- name: Test
run: npm run test-with-coverage
+16 -16
View File
@@ -1,23 +1,23 @@
name: codeql
on:
push:
branches:
- dev
push:
branches:
- dev
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
CodeQL-Build:
runs-on: ubuntu-latest
permissions:
security-events: write
permissions:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
+8 -8
View File
@@ -2,13 +2,13 @@ name: deps-review
on: [pull_request]
permissions:
contents: read
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout repository'
uses: actions/checkout@v4
- name: 'Dependency review'
uses: actions/dependency-review-action@v4
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout repository'
uses: actions/checkout@v4
- name: 'Dependency review'
uses: actions/dependency-review-action@v4
+19
View File
@@ -0,0 +1,19 @@
name: Fail PR to master
on:
pull_request:
branches:
- master
jobs:
fail_pr:
runs-on: ubuntu-latest
steps:
- name: Fail PR to master
run: |
if [[ "${{ github.event.pull_request.title }}" == "chore(dev): release"* ]]; then
echo "PR title starts with 'chore(dev): release', allowing PR"
else
echo "Pull requests to the master branch are not allowed, target dev branch"
exit 1
fi
+27 -27
View File
@@ -1,34 +1,34 @@
on:
workflow_dispatch:
workflow_dispatch:
name: ghcr-cleanup
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
build:
runs-on: ubuntu-latest
steps:
- name: Delete untagged images
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const response = await github.request("GET /${{ env.OWNER }}/packages/container/${{ env.PACKAGE_NAME }}/versions",
{ per_page: ${{ env.PER_PAGE }}
});
for(version of response.data) {
if (version.metadata.container.tags.length == 0) {
try {
console.log("delete " + version.id)
const deleteResponse = await github.request("DELETE /${{ env.OWNER }}/packages/container/${{ env.PACKAGE_NAME }}/versions/" + version.id, { });
console.log("status " + deleteResponse.status)
} catch (e) {
console.log("failed")
}
}
}
env:
OWNER: user
PACKAGE_NAME: zigbee2mqtt
PER_PAGE: 2000
steps:
- name: Delete untagged images
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const response = await github.request("GET /${{ env.OWNER }}/packages/container/${{ env.PACKAGE_NAME }}/versions",
{ per_page: ${{ env.PER_PAGE }}
});
for(version of response.data) {
if (version.metadata.container.tags.length == 0) {
try {
console.log("delete " + version.id)
const deleteResponse = await github.request("DELETE /${{ env.OWNER }}/packages/container/${{ env.PACKAGE_NAME }}/versions/" + version.id, { });
console.log("status " + deleteResponse.status)
} catch (e) {
console.log("failed")
}
}
}
env:
OWNER: user
PACKAGE_NAME: zigbee2mqtt
PER_PAGE: 2000
+14 -14
View File
@@ -1,19 +1,19 @@
name: merge-master-to-dev
on:
push:
branches:
- master
push:
branches:
- master
jobs:
merge_master_to_dev:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: devmasx/merge-branch@master
with:
type: now
head_to_merge: master
target_branch: dev
message: "chore: merge master to dev"
github_token: ${{ secrets.GH_TOKEN }}
merge_master_to_dev:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: devmasx/merge-branch@master
with:
type: now
head_to_merge: master
target_branch: dev
message: 'chore: merge master to dev'
github_token: ${{ secrets.GH_TOKEN }}
+59 -59
View File
@@ -1,69 +1,69 @@
on:
push:
branches:
- dev
push:
branches:
- dev
permissions:
contents: write
pull-requests: write
contents: write
pull-requests: write
name: release-please
jobs:
release-please:
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
version: "${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}}"
steps:
- uses: actions/setup-node@v4
with:
node-version: 20
release-please:
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
version: '${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}}'
steps:
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: googleapis/release-please-action@v4
id: release
with:
target-branch: dev
token: ${{secrets.GH_TOKEN}}
- uses: googleapis/release-please-action@v4
id: release
with:
target-branch: dev
token: ${{secrets.GH_TOKEN}}
# Checkout repos
- uses: actions/checkout@v4
with:
repository: koenkk/zigbee2mqtt
path: ./z2m
- uses: actions/checkout@v4
with:
repository: koenkk/zigbee2mqtt
path: ./z2m-master
ref: master
# Checkout repos
- uses: actions/checkout@v4
with:
repository: koenkk/zigbee2mqtt
path: ./z2m
- uses: actions/checkout@v4
with:
repository: koenkk/zigbee2mqtt
path: ./z2m-master
ref: master
- name: Restore cache commit-user-lookup.json
uses: actions/cache/restore@v4
with:
path: z2m/scripts/commit-user-lookup.json
key: commit-user-lookup-dummy
restore-keys: |
commit-user-lookup-
- name: Generate changelog
run: |
MASTER_Z2M_VERSION=$(cat z2m-master/package.json | jq -r '.version')
MASTER_ZHC_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee-herdsman-converters"')
MASTER_ZH_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee-herdsman"')
MASTER_FRONTEND_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee2mqtt-frontend"')
wget -q -O - https://raw.githubusercontent.com/Koenkk/zigbee2mqtt/release-please--branches--dev--components--zigbee2mqtt/CHANGELOG.md > z2m/CHANGELOG.md
cd z2m
npm ci
node scripts/generateChangelog.js $MASTER_Z2M_VERSION $MASTER_ZHC_VERSION $MASTER_ZH_VERSION $MASTER_FRONTEND_VERSION >> ../changelog.md
env:
GH_TOKEN: ${{secrets.GH_TOKEN}}
- name: Update changelog gist
run: |
gh gist edit bfd4c3d1725a2cccacc11d6ba51008ba -a changelog.md
env:
GH_TOKEN: ${{secrets.GH_TOKEN}}
- name: Save cache commit-user-lookup.json
uses: actions/cache/save@v4
if: always()
with:
path: z2m/scripts/commit-user-lookup.json
key: commit-user-lookup-${{ hashFiles('z2m/scripts/commit-user-lookup.json') }}
- name: Restore cache commit-user-lookup.json
uses: actions/cache/restore@v4
with:
path: z2m/scripts/commit-user-lookup.json
key: commit-user-lookup-dummy
restore-keys: |
commit-user-lookup-
- name: Generate changelog
run: |
MASTER_Z2M_VERSION=$(cat z2m-master/package.json | jq -r '.version')
MASTER_ZHC_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee-herdsman-converters"')
MASTER_ZH_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee-herdsman"')
MASTER_FRONTEND_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee2mqtt-frontend"')
wget -q -O - https://raw.githubusercontent.com/Koenkk/zigbee2mqtt/release-please--branches--dev--components--zigbee2mqtt/CHANGELOG.md > z2m/CHANGELOG.md
cd z2m
npm ci
node scripts/generateChangelog.js $MASTER_Z2M_VERSION $MASTER_ZHC_VERSION $MASTER_ZH_VERSION $MASTER_FRONTEND_VERSION >> ../changelog.md
env:
GH_TOKEN: ${{secrets.GH_TOKEN}}
- name: Update changelog gist
run: |
gh gist edit bfd4c3d1725a2cccacc11d6ba51008ba -a changelog.md
env:
GH_TOKEN: ${{secrets.GH_TOKEN}}
- name: Save cache commit-user-lookup.json
uses: actions/cache/save@v4
if: always()
with:
path: z2m/scripts/commit-user-lookup.json
key: commit-user-lookup-${{ hashFiles('z2m/scripts/commit-user-lookup.json') }}
+15 -15
View File
@@ -1,20 +1,20 @@
name: stale
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 30 days'
stale-pr-message: 'This pull request is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 30 days'
days-before-stale: 180
days-before-close: 30
exempt-issue-labels: dont-stale
operations-per-run: 500
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 30 days'
stale-pr-message: 'This pull request is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 30 days'
days-before-stale: 180
days-before-close: 30
exempt-issue-labels: dont-stale
operations-per-run: 500
+29 -29
View File
@@ -1,36 +1,36 @@
on:
repository_dispatch:
types: update_dep
repository_dispatch:
types: update_dep
name: update-dep
permissions: {}
jobs:
update_dep:
permissions:
contents: write
pull-requests: write
update_dep:
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: dev
token: ${{ secrets.GH_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm install ${{ github.event.client_payload.package }}@${{ github.event.client_payload.version }} --save-exact
- uses: peter-evans/create-pull-request@v6
id: cpr
with:
commit-message: "fix(ignore): update ${{ github.event.client_payload.package }} to ${{ github.event.client_payload.version }}"
branch: "deps/${{ github.event.client_payload.package }}"
title: Update ${{ github.event.client_payload.package }} to ${{ github.event.client_payload.version }}
token: ${{ secrets.GH_TOKEN }}
- run: sleep 5 # Otherwise pull request may not exist yet causing automerge to fail
- run: gh pr merge --squash --auto "${{ steps.cpr.outputs.pull-request-number }}"
if: steps.cpr.outputs.pull-request-operation == 'created'
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: dev
token: ${{ secrets.GH_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm install ${{ github.event.client_payload.package }}@${{ github.event.client_payload.version }} --save-exact
- uses: peter-evans/create-pull-request@v7
id: cpr
with:
commit-message: 'fix(ignore): update ${{ github.event.client_payload.package }} to ${{ github.event.client_payload.version }}'
branch: 'deps/${{ github.event.client_payload.package }}'
title: Update ${{ github.event.client_payload.package }} to ${{ github.event.client_payload.version }}
token: ${{ secrets.GH_TOKEN }}
- run: sleep 5 # Otherwise pull request may not exist yet causing automerge to fail
- run: gh pr merge --squash --auto "${{ steps.cpr.outputs.pull-request-number }}"
if: steps.cpr.outputs.pull-request-operation == 'created'
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
+29 -29
View File
@@ -1,35 +1,35 @@
name: Update dependencies
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * 0"
workflow_dispatch:
schedule:
- cron: '0 0 * * 0'
permissions:
contents: read
contents: read
jobs:
update_deps:
permissions:
contents: write # for peter-evans/create-pull-request to create branch
pull-requests: write # for peter-evans/create-pull-request to create a PR
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: dev
token: ${{ secrets.GH_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
# connect-gzip-static@4.0.0 requires Node 20 >=
# eslint: https://github.com/typescript-eslint/typescript-eslint/issues/8211
- run: npx npm-check-updates -u -x connect-gzip-static -x eslint
- run: rm -f package-lock.json
- run: npm install
- uses: peter-evans/create-pull-request@v6
with:
commit-message: "fix(ignore): update dependencies"
branch: "deps/all"
title: Update dependencies
token: ${{ secrets.GH_TOKEN }}
update_deps:
permissions:
contents: write # for peter-evans/create-pull-request to create branch
pull-requests: write # for peter-evans/create-pull-request to create a PR
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: dev
token: ${{ secrets.GH_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
# connect-gzip-static@4.0.0 requires Node 20 >=
# eslint: https://github.com/typescript-eslint/typescript-eslint/issues/8211
- run: npx npm-check-updates -u -x connect-gzip-static -x eslint
- run: rm -f package-lock.json
- run: npm install
- uses: peter-evans/create-pull-request@v7
with:
commit-message: 'fix(ignore): update dependencies'
branch: 'deps/all'
title: Update dependencies
token: ${{ secrets.GH_TOKEN }}
+2
View File
@@ -0,0 +1,2 @@
package-lock.json
CHANGELOG.md
+1 -1
View File
@@ -6,4 +6,4 @@
"bracketSpacing": false,
"endOfLine": "lf",
"tabWidth": 4
}
}
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "1.39.0"
".": "1.40.1"
}
+723 -648
View File
File diff suppressed because it is too large Load Diff
+16 -17
View File
@@ -1,4 +1,3 @@
# Contributor Covenant Code of Conduct
## Our Pledge
@@ -18,24 +17,24 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@@ -106,7 +105,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
+4 -4
View File
@@ -2,7 +2,7 @@
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.
- 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 `npm test`. Zigbee2MQTT enforces 100% code coverage, in case you add new code check if your code is covered by running `npm run test-with-coverage`. The coverage report can be found under `coverage/lcov-report/index.html`. Linting is also enforced and can be run with `npm 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).
- 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 `npm test`. Zigbee2MQTT enforces 100% code coverage, in case you add new code check if your code is covered by running `npm run test-with-coverage`. The coverage report can be found under `coverage/lcov-report/index.html`. Linting is also enforced and can be run with `npm 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).
+21 -9
View File
@@ -43,63 +43,75 @@
</div>
## [Getting started](https://www.zigbee2mqtt.io/guide/getting-started)
The [documentation](https://www.zigbee2mqtt.io/) provides you all the information needed to get up and running! Make sure you don't skip sections if this is your first visit, as there might be important details in there for you.
If you aren't familiar with **Zigbee** terminology make sure you [read this](https://www.zigbee2mqtt.io/advanced/zigbee/01_zigbee_network.html) to help you out.
## [Integrations](https://www.zigbee2mqtt.io/guide/usage/integrations.html)
Zigbee2MQTT integrates well with (almost) every home automation solution because it uses MQTT. However the following integrations are worth mentioning:
<img align="left" height="100px" width="100px" src="https://user-images.githubusercontent.com/7738048/40914297-49e6e560-6800-11e8-8904-36cce896e5a8.png">
### [Home Assistant](https://www.home-assistant.io/)
- [Home Assistant OS](https://www.home-assistant.io/installation/): Using [the official addon](https://github.com/zigbee2mqtt/hassio-zigbee2mqtt)
- Other installation: using instructions [here](https://www.zigbee2mqtt.io/guide/usage/integrations/home_assistant.html)
- [Home Assistant OS](https://www.home-assistant.io/installation/): Using [the official addon](https://github.com/zigbee2mqtt/hassio-zigbee2mqtt)
- Other installation: using instructions [here](https://www.zigbee2mqtt.io/guide/usage/integrations/home_assistant.html)
<br>
<img align="left" height="100px" width="100px" src="https://etc.athom.com/logo/white/256.png">
### [Homey](https://homey.app/)
- Integration implemented in the [Homey App](https://homey.app/nl-nl/app/com.gruijter.zigbee2mqtt/)
- Documentation and support in the [Homey Forum](https://community.homey.app/t/83214)
- Integration implemented in the [Homey App](https://homey.app/nl-nl/app/com.gruijter.zigbee2mqtt/)
- Documentation and support in the [Homey Forum](https://community.homey.app/t/83214)
<br>
<img align="left" height="100px" width="100px" src="https://user-images.githubusercontent.com/2734836/47615848-b8dd8700-dabd-11e8-9d77-175002dd8987.png">
### [Domoticz](https://www.domoticz.com/)
- Integration implemented in Domoticz ([documentation](https://www.domoticz.com/wiki/Zigbee2MQTT)).
- Integration implemented in Domoticz ([documentation](https://www.domoticz.com/wiki/Zigbee2MQTT)).
<br>
<img align="left" height="100px" width="100px" src="./images/gladys-assistant-logo.jpg">
### [Gladys Assistant](https://gladysassistant.com/)
- Integration implemented natively in Gladys Assistant ([documentation](https://gladysassistant.com/docs/integrations/zigbee2mqtt/)).
- Integration implemented natively in Gladys Assistant ([documentation](https://gladysassistant.com/docs/integrations/zigbee2mqtt/)).
<br>
<img align="left" height="100px" width="100px" src="https://forum.iobroker.net/assets/uploads/system/site-logo.png">
### [IoBroker](https://www.iobroker.net/)
- Integration implemented in IoBroker ([documentation](https://github.com/o0shojo0o/ioBroker.zigbee2mqtt)).
- Integration implemented in IoBroker ([documentation](https://github.com/o0shojo0o/ioBroker.zigbee2mqtt)).
<br>
## Architecture
![Architecture](images/architecture.png)
### Internal Architecture
Zigbee2MQTT is made up of three modules, each developed in its own Github project. Starting from the hardware (adapter) and moving up; [zigbee-herdsman](https://github.com/koenkk/zigbee-herdsman) connects to your Zigbee adapter and makes an API available to the higher levels of the stack. For e.g. Texas Instruments hardware, zigbee-herdsman uses the [TI zStack monitoring and test API](https://github.com/koenkk/zigbee-herdsman/raw/master/docs/Z-Stack%20Monitor%20and%20Test%20API.pdf) to communicate with the adapter. Zigbee-herdsman handles the core Zigbee communication. The module [zigbee-herdsman-converters](https://github.com/koenkk/zigbee-herdsman-converters) handles the mapping from individual device models to the Zigbee clusters they support. [Zigbee clusters](https://github.com/Koenkk/zigbee-herdsman/blob/master/docs/07-5123-08-Zigbee-Cluster-Library.pdf) are the layers of the Zigbee protocol on top of the base protocol that define things like how lights, sensors and switches talk to each other over the Zigbee network. Finally, the Zigbee2MQTT module drives zigbee-herdsman and maps the zigbee messages to MQTT messages. Zigbee2MQTT also keeps track of the state of the system. It uses a `database.db` file to store this state; a text file with a JSON database of connected devices and their capabilities. Zigbee2MQTT provides a [web-based interface](https://github.com/nurikk/zigbee2mqtt-frontend) that allows monitoring and configuration.
Zigbee2MQTT is made up of three modules, each developed in its own Github project. Starting from the hardware (adapter) and moving up; [zigbee-herdsman](https://github.com/koenkk/zigbee-herdsman) connects to your Zigbee adapter and makes an API available to the higher levels of the stack. For e.g. Texas Instruments hardware, zigbee-herdsman uses the [TI zStack monitoring and test API](https://github.com/koenkk/zigbee-herdsman/raw/master/docs/Z-Stack%20Monitor%20and%20Test%20API.pdf) to communicate with the adapter. Zigbee-herdsman handles the core Zigbee communication. The module [zigbee-herdsman-converters](https://github.com/koenkk/zigbee-herdsman-converters) handles the mapping from individual device models to the Zigbee clusters they support. [Zigbee clusters](https://github.com/Koenkk/zigbee-herdsman/blob/master/docs/07-5123-08-Zigbee-Cluster-Library.pdf) are the layers of the Zigbee protocol on top of the base protocol that define things like how lights, sensors and switches talk to each other over the Zigbee network. Finally, the Zigbee2MQTT module drives zigbee-herdsman and maps the zigbee messages to MQTT messages. Zigbee2MQTT also keeps track of the state of the system. It uses a `database.db` file to store this state; a text file with a JSON database of connected devices and their capabilities. Zigbee2MQTT provides a [web-based interface](https://github.com/nurikk/zigbee2mqtt-frontend) that allows monitoring and configuration.
### Developing
Zigbee2MQTT uses TypeScript (partially for now). Therefore after making changes to files in the `lib/` directory you need to recompile Zigbee2MQTT. This can be done by executing `npm run build`. For faster development instead of running `npm run build` you can run `npm run build-watch` in another terminal session, this will recompile as you change files.
## Supported devices
See [Supported devices](https://www.zigbee2mqtt.io/supported-devices) to check whether your device is supported. There is quite an extensive list, including devices from vendors like [Xiaomi](https://www.zigbee2mqtt.io/supported-devices/#v=Xiaomi), [Ikea](https://www.zigbee2mqtt.io/supported-devices/#v=IKEA), [Philips](https://www.zigbee2mqtt.io/supported-devices/#v=Philips), [OSRAM](https://www.zigbee2mqtt.io/supported-devices/#v=OSRAM) and more.
If it's not listed in [Supported devices](https://www.zigbee2mqtt.io/supported-devices), support can be added (fairly) easily, see [How to support new devices](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html).
## Support & help
If you need assistance you can check [opened issues](https://github.com/Koenkk/zigbee2mqtt/issues). Feel free to help with Pull Requests when you were able to fix things or add new devices or just share the love on social media.
+2 -7
View File
@@ -1,10 +1,5 @@
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
presets: [['@babel/preset-env', {targets: {node: 'current'}}], '@babel/preset-typescript'],
assumptions: {setPublicClassFields: true},
plugins: [
['@babel/plugin-proposal-decorators', {'legacy': true}],
],
plugins: [['@babel/plugin-proposal-decorators', {legacy: true}]],
};
+15 -15
View File
@@ -6,24 +6,24 @@ frontend: true
# MQTT settings
mqtt:
# MQTT base topic for zigbee2mqtt MQTT messages
base_topic: zigbee2mqtt
# MQTT server URL
server: 'mqtt://localhost'
# MQTT server authentication, uncomment if required:
# user: my_user
# password: my_password
# MQTT base topic for zigbee2mqtt MQTT messages
base_topic: zigbee2mqtt
# MQTT server URL
server: 'mqtt://localhost'
# MQTT server authentication, uncomment if required:
# user: my_user
# password: my_password
# Serial settings
serial:
# Location of CC2531 USB sniffer
port: /dev/ttyACM0
# Location of CC2531 USB sniffer
port: /dev/ttyACM0
# Advanced settings
advanced:
# Let Zigbee2MQTT generate a network key on first start
network_key: GENERATE
# Let Zigbee2MQTT generate a pan_id on first start
pan_id: GENERATE
# Let Zigbee2MQTT generate a ext_pan_id on first start
ext_pan_id: GENERATE
# Let Zigbee2MQTT generate a network key on first start
network_key: GENERATE
# Let Zigbee2MQTT generate a pan_id on first start
pan_id: GENERATE
# Let Zigbee2MQTT generate a ext_pan_id on first start
ext_pan_id: GENERATE
+7
View File
@@ -18,6 +18,13 @@ RUN apk add --no-cache --virtual .buildtools make gcc g++ python3 linux-headers
# Release
FROM base as release
LABEL org.opencontainers.image.authors="Koen Kanters"
LABEL org.opencontainers.image.title="zigbee2mqtt"
LABEL org.opencontainers.image.description="Zigbee to MQTT bridge using Zigbee-herdsman"
LABEL org.opencontainers.image.url="https://github.com/Koenkk/zigbee2mqtt"
LABEL org.opencontainers.image.documentation="https://www.zigbee2mqtt.io/"
LABEL org.opencontainers.image.source="https://github.com/Koenkk/zigbee2mqtt"
COPY --from=dependencies_and_build /app/node_modules ./node_modules
COPY dist ./dist
COPY package.json LICENSE index.js data/configuration.yaml data/configuration.example.yaml ./
+13 -13
View File
@@ -1,14 +1,14 @@
version: '3'
services:
version: '3'
services:
zigbee2mqtt:
container_name: zigbee2mqtt
image: koenkk/zigbee2mqtt
volumes:
- ./data:/app/data
devices:
# CC251
#- /dev/ttyUSB_cc2531:/dev/ttyACM0
# CC2530 / GBAN GB2530S
#- /dev/ttyUSB_cc2530:/dev/ttyACM0
restart: always
network_mode: host
container_name: zigbee2mqtt
image: koenkk/zigbee2mqtt
volumes:
- ./data:/app/data
devices:
# CC251
#- /dev/ttyUSB_cc2531:/dev/ttyACM0
# CC2530 / GBAN GB2530S
#- /dev/ttyUSB_cc2530:/dev/ttyACM0
restart: always
network_mode: host
+4 -4
View File
@@ -13,7 +13,7 @@ let watchdog = process.env.Z2M_WATCHDOG != undefined;
let watchdogCount = 0;
let unsolicitedStop = false;
// csv in minutes, default: 1min, 5min, 15min, 30min, 60min
let watchdogDelays = [60000, 300000, 900000, 1800000, 3600000];
let watchdogDelays = [2000, 60000, 300000, 900000, 1800000, 3600000];
if (watchdog && process.env.Z2M_WATCHDOG !== 'default') {
if (/^(?:(?:[0-9]*[.])?[0-9]+)+(?:,?(?:[0-9]*[.])?[0-9]+)*$/.test(process.env.Z2M_WATCHDOG)) {
@@ -34,7 +34,7 @@ async function triggerWatchdog(code) {
// garbage collector
controller = undefined;
console.log(`WATCHDOG: Waiting ${delay/60000}min before next start try.`);
console.log(`WATCHDOG: Waiting ${delay / 60000}min before next start try.`);
await new Promise((resolve) => setTimeout(resolve, delay));
await start();
} else {
@@ -61,7 +61,7 @@ async function currentHash() {
const git = require('git-last-commit');
return new Promise((resolve) => {
git.getLastCommit((err, commit) => err ? resolve('unknown') : resolve(commit.shortHash));
git.getLastCommit((err, commit) => (err ? resolve('unknown') : resolve(commit.shortHash)));
});
}
@@ -155,7 +155,7 @@ async function start() {
// consider next controller.stop() call as unsolicited, only after successful first start
unsolicitedStop = true;
watchdogCount = 0;// reset
watchdogCount = 0; // reset
}
async function stop(restart) {
+12 -7
View File
@@ -66,6 +66,7 @@ type ExtensionArgs = [
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let sdNotify: any = null;
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
sdNotify = process.env.NOTIFY_SOCKET ? require('sd-notify') : null;
} catch {
// sd-notify is optional
@@ -156,15 +157,19 @@ export class Controller {
}
// Log zigbee clients on startup
const devices = this.zigbee.devices(false);
logger.info(`Currently ${devices.length} devices are joined:`);
for (const device of devices) {
let deviceCount = 0;
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
const model = device.isSupported
? `${device.definition.model} - ${device.definition.vendor} ${device.definition.description}`
: 'Not supported';
logger.info(`${device.name} (${device.ieeeAddr}): ${model} (${device.zh.type})`);
deviceCount++;
}
logger.info(`Currently ${deviceCount} devices are joined.`);
// Enable zigbee join
try {
if (settings.get().permit_join) {
@@ -175,14 +180,14 @@ export class Controller {
await this.zigbee.permitJoin(settings.get().permit_join);
} catch (error) {
logger.error(`Failed to set permit join to ${settings.get().permit_join}`);
logger.error(`Failed to set permit join to ${settings.get().permit_join} (${error.message})`);
}
// MQTT
try {
await this.mqtt.connect();
} catch (error) {
logger.error(`MQTT failed to connect, exiting...`);
logger.error(`MQTT failed to connect, exiting... (${error.message})`);
await this.zigbee.stop();
return this.exit(1);
}
@@ -192,7 +197,7 @@ export class Controller {
// Send all cached states.
if (settings.get().advanced.cache_state_send_on_startup && settings.get().advanced.cache_state) {
for (const entity of [...devices, ...this.zigbee.groups()]) {
for (const entity of this.zigbee.devicesAndGroupsIterator()) {
if (this.state.exists(entity)) {
await this.publishEntityState(entity, this.state.get(entity), 'publishCached');
}
@@ -247,7 +252,7 @@ export class Controller {
await this.zigbee.stop();
logger.info('Stopped Zigbee2MQTT');
} catch (error) {
logger.error('Failed to stop Zigbee2MQTT');
logger.error(`Failed to stop Zigbee2MQTT (${error.message})`);
code = 1;
}
+2 -2
View File
@@ -41,10 +41,10 @@ export default class EventBus {
this.on('deviceRenamed', callback, key);
}
public emitDeviceRemoved(data: eventdata.DeviceRemoved): void {
public emitEntityRemoved(data: eventdata.EntityRemoved): void {
this.emitter.emit('deviceRemoved', data);
}
public onDeviceRemoved(key: ListenerKey, callback: (data: eventdata.DeviceRemoved) => void): void {
public onEntityRemoved(key: ListenerKey, callback: (data: eventdata.EntityRemoved) => void): void {
this.on('deviceRemoved', callback, key);
}
+24 -18
View File
@@ -48,8 +48,9 @@ export default class Availability extends Extension {
private resetTimer(device: Device): void {
clearTimeout(this.timers[device.ieeeAddr]);
this.removeFromPingQueue(device);
// If the timer triggers, the device is not available anymore otherwise resetTimer already have been called
// If the timer triggers, the device is not available anymore otherwise resetTimer already has been called
if (this.isActiveDevice(device)) {
// If device did not check in, ping it, if that fails it will be marked as offline
this.timers[device.ieeeAddr] = setTimeout(() => this.addToPingQueue(device), this.getTimeout(device) + utils.seconds(1));
@@ -58,14 +59,16 @@ export default class Availability extends Extension {
}
}
private async addToPingQueue(device: Device): Promise<void> {
private addToPingQueue(device: Device): void {
this.pingQueue.push(device);
await this.pingQueueExecuteNext();
this.pingQueueExecuteNext().catch(utils.noop);
}
private removeFromPingQueue(device: Device): void {
const index = this.pingQueue.findIndex((d) => d.ieeeAddr === device.ieeeAddr);
index != -1 && this.pingQueue.splice(index, 1);
if (index != -1) {
this.pingQueue.splice(index, 1);
}
}
private async pingQueueExecuteNext(): Promise<void> {
@@ -127,29 +130,32 @@ export default class Availability extends Extension {
}
});
this.eventBus.onDeviceRemoved(this, (data) => clearTimeout(this.timers[data.ieeeAddr]));
this.eventBus.onEntityRemoved(this, (data) => data.type == 'device' && clearTimeout(this.timers[data.id]));
this.eventBus.onDeviceLeave(this, (data) => clearTimeout(this.timers[data.ieeeAddr]));
this.eventBus.onDeviceAnnounce(this, (data) => this.retrieveState(data.device));
this.eventBus.onLastSeenChanged(this, this.onLastSeenChanged);
this.eventBus.onPublishAvailability(this, this.publishAvailabilityForAllEntities);
this.eventBus.onGroupMembersChanged(this, (data) => this.publishAvailability(data.group, false));
// Publish initial availability
await this.publishAvailabilityForAllEntities();
// Start availability for the devices
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
if (utils.isAvailabilityEnabledForEntity(device, settings.get())) {
this.resetTimer(device);
// If an active device is unavailable on start, add it to the pingqueue immediately.
if (this.isActiveDevice(device) && !this.isAvailable(device)) {
this.addToPingQueue(device);
}
}
}
}
@bind private async publishAvailabilityForAllEntities(): Promise<void> {
for (const entity of [...this.zigbee.devices(false), ...this.zigbee.groups()]) {
for (const entity of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
if (utils.isAvailabilityEnabledForEntity(entity, settings.get())) {
// Publish initial availability
await this.publishAvailability(entity, true, false, true);
if (entity.isDevice()) {
this.resetTimer(entity);
// If an active device is initially unavailable, ping it.
if (this.isActiveDevice(entity) && !this.isAvailable(entity)) {
await this.addToPingQueue(entity);
}
}
}
}
}
@@ -181,7 +187,7 @@ export default class Availability extends Extension {
await this.mqtt.publish(topic, payload, {retain: true, qos: 1});
if (!skipGroups && entity.isDevice()) {
for (const group of this.zigbee.groups()) {
for (const group of this.zigbee.groupsIterator()) {
if (group.hasMember(entity) && utils.isAvailabilityEnabledForEntity(group, settings.get())) {
await this.publishAvailability(group, false, forcePublish);
}
@@ -224,7 +230,7 @@ export default class Availability extends Extension {
continue;
}
const converter = device.definition.toZigbee.find((c) => c.key.find((k) => item.keys.includes(k)));
const converter = device.definition.toZigbee.find((c) => !c.key || c.key.find((k) => item.keys.includes(k)));
const options: KeyValue = device.options;
const state = this.state.get(device);
const meta: zhc.Tz.Meta = {
+7 -5
View File
@@ -363,7 +363,7 @@ export default class Bind extends Extension {
if (data.action === 'add') {
const bindsToGroup: zh.Bind[] = [];
for (const device of this.zigbee.devices(false)) {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
for (const endpoint of device.zh.endpoints) {
for (const bind of endpoint.binds) {
if (bind.target === data.group.zh) {
@@ -429,7 +429,7 @@ export default class Bind extends Extension {
await endpoint.configureReporting(bind.cluster.name, items);
logger.info(`Successfully setup reporting for '${entity}' cluster '${bind.cluster.name}'`);
} catch (error) {
logger.warning(`Failed to setup reporting for '${entity}' cluster '${bind.cluster.name}'`);
logger.warning(`Failed to setup reporting for '${entity}' cluster '${bind.cluster.name}' (${error.message})`);
}
}
}
@@ -443,7 +443,7 @@ export default class Bind extends Extension {
const endpoints = utils.isEndpoint(target) ? [target] : target.members;
const allBinds: zh.Bind[] = [];
for (const device of this.zigbee.devices(false)) {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
for (const endpoint of device.zh.endpoints) {
for (const bind of endpoint.binds) {
allBinds.push(bind);
@@ -489,7 +489,7 @@ export default class Bind extends Extension {
await endpoint.configureReporting(cluster, items);
logger.info(`Successfully disabled reporting for '${entity}' cluster '${cluster}'`);
} catch (error) {
logger.warning(`Failed to disable reporting for '${entity}' cluster '${cluster}'`);
logger.warning(`Failed to disable reporting for '${entity}' cluster '${cluster}' (${error.message})`);
}
}
@@ -555,7 +555,9 @@ export default class Bind extends Extension {
try {
await endpoint.read(poll.read.cluster, readAttrs);
} catch (error) {
logger.error(`Failed to poll ${readAttrs} from ${this.zigbee.resolveEntity(endpoint.getDevice()).name}`);
logger.error(
`Failed to poll ${readAttrs} from ${this.zigbee.resolveEntity(endpoint.getDevice()).name} (${error.message})`,
);
}
}, 1000);
}
+47 -27
View File
@@ -534,14 +534,16 @@ export default class Bridge extends Extension {
logger.info(`Interviewing '${device.name}'`);
try {
await device.zh.interview();
await device.zh.interview(true);
logger.info(`Successfully interviewed '${device.name}'`);
} catch (error) {
throw new Error(`interview of '${device.name}' (${device.ieeeAddr}) failed: ${error}`, {cause: error});
}
// publish devices so that the front-end has up-to-date info.
await this.publishDevices();
// A re-interview can for example result in a different modelId, therefore reconsider the definition.
await device.resolveDefinition(true);
this.eventBus.emitDevicesChanged();
this.eventBus.emitExposesChanged({device});
return utils.getResponse(message, {id: message.id}, null);
}
@@ -616,7 +618,6 @@ export default class Bridge extends Extension {
try {
logger.info(`Removing ${entityType} '${entity.name}'${blockForceLog}`);
const ieeeAddr = entity.isDevice() && entity.ieeeAddr;
const name = entity.name;
if (entity instanceof Device) {
@@ -639,7 +640,9 @@ export default class Bridge extends Extension {
// Fire event
if (entity instanceof Device) {
this.eventBus.emitDeviceRemoved({ieeeAddr, name});
this.eventBus.emitEntityRemoved({id: entityID, name, type: 'device'});
} else {
this.eventBus.emitEntityRemoved({id: entityID, name, type: 'group'});
}
// Remove from configuration.yaml
@@ -684,7 +687,9 @@ export default class Bridge extends Extension {
const config = objectAssignDeep({}, settings.get());
delete config.advanced.network_key;
delete config.mqtt.password;
config.frontend && delete config.frontend.auth_token;
if (config.frontend) {
delete config.frontend.auth_token;
}
const payload = {
version: this.zigbee2mqttVersion.version,
commit: this.zigbee2mqttVersion.commitHash,
@@ -720,8 +725,12 @@ export default class Bridge extends Extension {
scenes: Scene[];
}
const devices = this.zigbee.devices().map((device) => {
// XXX: definition<>DefinitionPayload don't match to use `Device[]` type here
const devices: KeyValue[] = [];
for (const device of this.zigbee.devicesIterator()) {
const endpoints: {[s: number]: Data} = {};
for (const endpoint of device.zh.endpoints) {
const data: Data = {
scenes: utils.getScenes(endpoint),
@@ -753,7 +762,7 @@ export default class Bridge extends Extension {
endpoints[endpoint.ID] = data;
}
return {
devices.push({
ieee_address: device.ieeeAddr,
type: device.zh.type,
network_address: device.zh.networkAddress,
@@ -770,24 +779,32 @@ export default class Bridge extends Extension {
interview_completed: device.zh.interviewCompleted,
manufacturer: device.zh.manufacturerName,
endpoints,
};
});
});
}
await this.mqtt.publish('bridge/devices', stringify(devices), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
}
async publishGroups(): Promise<void> {
const groups = this.zigbee.groups().map((g) => {
return {
id: g.ID,
friendly_name: g.ID === 901 ? 'default_bind_group' : g.name,
description: g.options.description,
scenes: utils.getScenes(g.zh),
members: g.zh.members.map((e) => {
return {ieee_address: e.getDevice().ieeeAddr, endpoint: e.ID};
}),
};
});
// XXX: id<>ID can't use `Group[]` type
const groups: KeyValue[] = [];
for (const group of this.zigbee.groupsIterator()) {
const members = [];
for (const member of group.zh.members) {
members.push({ieee_address: member.getDevice().ieeeAddr, endpoint: member.ID});
}
groups.push({
id: group.ID,
friendly_name: group.ID === 901 ? 'default_bind_group' : group.name,
description: group.options.description,
scenes: utils.getScenes(group.zh),
members,
});
}
await this.mqtt.publish('bridge/groups', stringify(groups), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
}
@@ -802,20 +819,23 @@ export default class Bridge extends Extension {
custom_clusters: {},
};
for (const device of this.zigbee.devices()) {
if (Object.keys(device.customClusters).length !== 0) {
data.custom_clusters[device.ieeeAddr] = device.customClusters;
}
for (const device of this.zigbee.devicesIterator((d) => !utils.objectIsEmpty(d.customClusters))) {
data.custom_clusters[device.ieeeAddr] = device.customClusters;
}
await this.mqtt.publish('bridge/definitions', stringify(data), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
}
getDefinitionPayload(device: Device): DefinitionPayload {
if (!device.definition) return null;
getDefinitionPayload(device: Device): DefinitionPayload | null {
if (!device.definition) {
return null;
}
// TODO: better typing to avoid @ts-expect-error
// @ts-expect-error icon is valid for external definitions
const definitionIcon = device.definition.icon;
let icon = device.options.icon ?? definitionIcon;
if (icon) {
icon = icon.replace('${zigbeeModel}', utils.sanitizeImageParameter(device.zh.modelID));
icon = icon.replace('${model}', utils.sanitizeImageParameter(device.definition.model));
+2 -3
View File
@@ -68,9 +68,8 @@ export default class Configure extends Extension {
setImmediate(async () => {
// Only configure routers on startup, end devices are likely sleeping and
// will reconfigure once they send a message
for (const device of this.zigbee.devices(false).filter((d) => d.zh.type === 'Router')) {
// Sleep 10 seconds between configuring on startup to not DDoS the coordinator
// when many devices have to be configured.
for (const device of this.zigbee.devicesIterator((d) => d.type === 'Router')) {
// Sleep 10 seconds between configuring on startup to not DDoS the coordinator when many devices have to be configured.
await utils.sleep(10);
await this.configure(device, 'started');
}
+1
View File
@@ -25,6 +25,7 @@ export default class ExternalConverters extends Extension {
delete toAdd['homeassistant'];
zhc.addDefinition(toAdd);
}
logger.info(`Loaded external converter '${file}'`);
} catch (error) {
logger.error(`Failed to load external converter file '${file}' (${error.message})`);
logger.error(
+18 -27
View File
@@ -25,26 +25,11 @@ export default class Frontend extends Extension {
private sslCert = settings.get().frontend.ssl_cert;
private sslKey = settings.get().frontend.ssl_key;
private authToken = settings.get().frontend.auth_token;
private retainedMessages = new Map();
private server: http.Server;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private fileServer: RequestHandler;
private wss: WebSocket.Server = null;
constructor(
zigbee: Zigbee,
mqtt: MQTT,
state: State,
publishEntityState: PublishEntityState,
eventBus: EventBus,
enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
restartCallback: () => Promise<void>,
addExtension: (extension: Extension) => Promise<void>,
) {
super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension);
this.eventBus.onMQTTMessagePublished(this, this.onMQTTPublishMessage);
}
private isHttpsConfigured(): boolean {
if (this.sslCert && this.sslKey) {
if (!fs.existsSync(this.sslCert) || !fs.existsSync(this.sslKey)) {
@@ -82,6 +67,8 @@ export default class Frontend extends Extension {
this.wss = new WebSocket.Server({noServer: true});
this.wss.on('connection', this.onWebSocketConnection);
this.eventBus.onMQTTMessagePublished(this, this.onMQTTPublishMessage);
if (!this.host) {
this.server.listen(this.port);
logger.info(`Started frontend on port ${this.port}`);
@@ -139,11 +126,20 @@ export default class Frontend extends Extension {
}
});
for (const [key, value] of this.retainedMessages) {
ws.send(stringify({topic: key, payload: value}));
for (const [topic, payload] of Object.entries(this.mqtt.retainedMessages)) {
/* istanbul ignore else */
if (topic.startsWith(`${this.mqttBaseTopic}/`)) {
ws.send(
stringify({
// Send topic without base_topic
topic: topic.substring(this.mqttBaseTopic.length + 1),
payload: utils.parseJSON(payload.payload, payload.payload),
}),
);
}
}
for (const device of this.zigbee.devices(false)) {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
const payload = this.state.get(device);
const lastSeen = settings.get().advanced.last_seen;
/* istanbul ignore if */
@@ -165,16 +161,11 @@ export default class Frontend extends Extension {
// Send topic without base_topic
const topic = data.topic.substring(this.mqttBaseTopic.length + 1);
const payload = utils.parseJSON(data.payload, data.payload);
if (data.options.retain) {
this.retainedMessages.set(topic, payload);
}
if (this.wss) {
for (const client of this.wss.clients) {
/* istanbul ignore else */
if (client.readyState === WebSocket.OPEN) {
client.send(stringify({topic, payload}));
}
for (const client of this.wss.clients) {
/* istanbul ignore else */
if (client.readyState === WebSocket.OPEN) {
client.send(stringify({topic, payload}));
}
}
}
+18 -11
View File
@@ -51,7 +51,6 @@ export default class Groups extends Extension {
private async syncGroupsWithSettings(): Promise<void> {
const settingsGroups = settings.getGroups();
const zigbeeGroups = this.zigbee.groups();
const addRemoveFromGroup = async (
action: 'add' | 'remove',
@@ -76,7 +75,7 @@ export default class Groups extends Extension {
for (const settingGroup of settingsGroups) {
const groupID = settingGroup.ID;
const zigbeeGroup = zigbeeGroups.find((g) => g.ID === groupID) || this.zigbee.createGroup(groupID);
const zigbeeGroup = this.zigbee.groupsIterator((g) => g.groupID === groupID).next().value || this.zigbee.createGroup(groupID);
const settingsEndpoints: zh.Endpoint[] = [];
for (const d of settingGroup.devices) {
@@ -113,13 +112,11 @@ export default class Groups extends Extension {
}
}
for (const zigbeeGroup of zigbeeGroups) {
if (!settingsGroups.some((g) => g.ID === zigbeeGroup.ID)) {
for (const endpoint of zigbeeGroup.zh.members) {
const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr).friendly_name;
for (const zigbeeGroup of this.zigbee.groupsIterator((zg) => !settingsGroups.some((sg) => sg.ID === zg.groupID))) {
for (const endpoint of zigbeeGroup.zh.members) {
const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr).friendly_name;
await addRemoveFromGroup('remove', deviceName, zigbeeGroup.ID, endpoint, zigbeeGroup);
}
await addRemoveFromGroup('remove', deviceName, zigbeeGroup.ID, endpoint, zigbeeGroup);
}
}
}
@@ -153,7 +150,13 @@ export default class Groups extends Extension {
if (payloadKeys.length) {
const entity = data.entity;
const groups = this.zigbee.groups().filter((g) => g.options && (g.options.optimistic == undefined || g.options.optimistic));
const groups = [];
for (const group of this.zigbee.groupsIterator()) {
if (group.options && (group.options.optimistic == undefined || group.options.optimistic)) {
groups.push(group);
}
}
if (entity instanceof Device) {
for (const group of groups) {
@@ -359,7 +362,7 @@ export default class Groups extends Extension {
resolvedEntityEndpoint,
} = parsed;
let error = parsed.error;
let changedGroups: Group[] = [];
const changedGroups: Group[] = [];
if (!error) {
try {
@@ -406,7 +409,11 @@ export default class Groups extends Extension {
} else {
// remove_all
logger.info(`Removing '${resolvedEntityDevice.name}' from all groups`);
changedGroups = this.zigbee.groups().filter((g) => g.zh.members.includes(resolvedEntityEndpoint));
for (const group of this.zigbee.groupsIterator((g) => g.members.includes(resolvedEntityEndpoint))) {
changedGroups.push(group);
}
await resolvedEntityEndpoint.removeFromAllGroups();
for (const settingsGroup of settings.getGroups()) {
+26 -18
View File
@@ -201,7 +201,7 @@ export default class HomeAssistant extends Extension {
this.discoveryOrigin = {name: 'Zigbee2MQTT', sw: this.zigbee2MQTTVersion, url: 'https://www.zigbee2mqtt.io'};
this.bridge = this.getBridgeEntity(await this.zigbee.getCoordinatorVersion());
this.bridgeIdentifier = this.getDevicePayload(this.bridge).identifiers[0];
this.eventBus.onDeviceRemoved(this, this.onDeviceRemoved);
this.eventBus.onEntityRemoved(this, this.onEntityRemoved);
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
this.eventBus.onEntityRenamed(this, this.onEntityRenamed);
this.eventBus.onPublishEntityState(this, this.onPublishEntityState);
@@ -225,7 +225,9 @@ export default class HomeAssistant extends Extension {
const discoverWait = 5;
// Discover with `published = false`, this will populate `this.discovered` without publishing the discoveries.
// This is needed for clearing outdated entries in `this.onMQTTMessage()`
for (const e of [this.bridge, ...this.zigbee.devices(false), ...this.zigbee.groups()]) {
await this.discover(this.bridge, false);
for (const e of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
await this.discover(e, false);
}
@@ -235,7 +237,9 @@ export default class HomeAssistant extends Extension {
this.mqtt.unsubscribe(`${this.discoveryTopic}/#`);
logger.debug(`Discovering entities to Home Assistant`);
for (const e of [this.bridge, ...this.zigbee.devices(false), ...this.zigbee.groups()]) {
await this.discover(this.bridge);
for (const e of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
await this.discover(e);
}
}, utils.seconds(discoverWait));
@@ -244,8 +248,8 @@ export default class HomeAssistant extends Extension {
this.eventBus.emitPublishAvailability();
}
private getDiscovered(entity: Device | Group | Bridge | string): Discovered {
const ID = typeof entity === 'string' ? entity : entity.ID;
private getDiscovered(entity: Device | Group | Bridge | string | number): Discovered {
const ID = typeof entity === 'string' || typeof entity === 'number' ? entity : entity.ID;
if (!(ID in this.discovered)) {
this.discovered[ID] = {messages: {}, triggers: new Set(), mockProperties: new Set(), discovered: false};
}
@@ -596,8 +600,8 @@ export default class HomeAssistant extends Extension {
// 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.state_open = 'OPEN');
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';
}
@@ -684,8 +688,7 @@ export default class HomeAssistant extends Extension {
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.${speed.property} if value_json.${speed.property} in [${presetList}]` + ` else 'None' | default('None') }}`;
discoveryEntry.discovery_payload.preset_mode_value_template = `{{ value_json.${speed.property} if value_json.${speed.property} in [${presetList}] else 'None' | default('None') }}`;
discoveryEntry.discovery_payload.preset_modes = presets;
}
@@ -811,7 +814,7 @@ export default class HomeAssistant extends Extension {
ballast_minimum_level: {entity_category: 'config'},
ballast_physical_maximum_level: {entity_category: 'diagnostic'},
ballast_physical_minimum_level: {entity_category: 'diagnostic'},
battery: {device_class: 'battery', entity_category: 'diagnostic', state_class: 'measurement'},
battery: {device_class: 'battery', state_class: 'measurement'},
battery2: {device_class: 'battery', entity_category: 'diagnostic', state_class: 'measurement'},
battery_voltage: {device_class: 'voltage', entity_category: 'diagnostic', state_class: 'measurement', enabled_by_default: true},
boost_heating_countdown: {device_class: 'duration'},
@@ -1015,7 +1018,7 @@ export default class HomeAssistant extends Extension {
};
if (lookup[firstExpose.name]?.device_class === 'temperature') {
discoveryEntry.discovery_payload.device_class == lookup[firstExpose.name]?.device_class;
discoveryEntry.discovery_payload.device_class = lookup[firstExpose.name]?.device_class;
} else {
delete discoveryEntry.discovery_payload.device_class;
}
@@ -1138,7 +1141,6 @@ export default class HomeAssistant extends Extension {
color_options: {icon: 'mdi:palette'},
level_config: {entity_category: 'diagnostic'},
programming_mode: {icon: 'mdi:calendar-clock'},
program: {value_template: `{{ value_json.${firstExpose.property}|default('',True) ` + `| truncate(254, True, '', 0) }}`},
schedule_settings: {icon: 'mdi:calendar-clock'},
};
if (firstExpose.access & ACCESS_STATE) {
@@ -1148,7 +1150,9 @@ export default class HomeAssistant extends Extension {
mockProperties: [{property: firstExpose.property, value: null}],
discovery_payload: {
name: endpoint ? `${firstExpose.label} ${endpoint}` : firstExpose.label,
value_template: `{{ value_json.${firstExpose.property} }}`,
// Truncate text if it's too long
// https://github.com/Koenkk/zigbee2mqtt/issues/23199
value_template: `{{ value_json.${firstExpose.property}|default('',True) | truncate(254, True, '', 0) }}`,
enabled_by_default: !settableText,
...lookup[firstExpose.name],
},
@@ -1202,15 +1206,15 @@ export default class HomeAssistant extends Extension {
return discoveryEntries;
}
@bind async onDeviceRemoved(data: eventdata.DeviceRemoved): Promise<void> {
@bind async onEntityRemoved(data: eventdata.EntityRemoved): Promise<void> {
logger.debug(`Clearing Home Assistant discovery for '${data.name}'`);
const discovered = this.getDiscovered(data.ieeeAddr);
const discovered = this.getDiscovered(data.id);
for (const topic of Object.keys(discovered.messages)) {
await this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false);
}
delete this.discovered[data.ieeeAddr];
delete this.discovered[data.id];
}
@bind async onGroupMembersChanged(data: eventdata.GroupMembersChanged): Promise<void> {
@@ -1700,6 +1704,10 @@ export default class HomeAssistant extends Extension {
}
}
if (entity.isDevice()) {
entity.definition.meta?.overrideHaDiscoveryPayload?.(payload);
}
const topic = this.getDiscoveryTopic(config, entity);
const payloadStr = stringify(payload);
newDiscoveredTopics.add(topic);
@@ -1741,7 +1749,7 @@ export default class HomeAssistant extends Extension {
if (!isDeviceAutomation && (!message.availability || !message.availability[0].topic.startsWith(baseTopic))) {
return;
}
} catch (e) {
} catch {
return;
}
@@ -1776,7 +1784,7 @@ export default class HomeAssistant extends Extension {
} else if ((data.topic === this.statusTopic || data.topic === defaultStatusTopic) && data.message.toLowerCase() === 'online') {
const timer = setTimeout(async () => {
// Publish all device states.
for (const entity of [...this.zigbee.devices(false), ...this.zigbee.groups()]) {
for (const entity of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
if (this.state.exists(entity)) {
await this.publishEntityState(entity, this.state.get(entity), 'publishCached');
}
+11 -9
View File
@@ -61,7 +61,7 @@ export default class BridgeLegacy extends Extension {
let json = null;
try {
json = JSON.parse(message);
} catch (e) {
} catch {
logger.error('Failed to parse message as JSON');
return;
}
@@ -86,7 +86,7 @@ export default class BridgeLegacy extends Extension {
try {
await this.zigbee.reset('soft');
logger.info('Soft reset ZNP');
} catch (error) {
} catch {
logger.error('Soft reset failed');
}
}
@@ -127,7 +127,9 @@ export default class BridgeLegacy extends Extension {
@bind async devices(topic: string): Promise<void> {
const coordinator = await this.zigbee.getCoordinatorVersion();
const devices = this.zigbee.devices().map((device) => {
const devices: KeyValue[] = [];
for (const device of this.zigbee.devicesIterator()) {
const payload: KeyValue = {
ieeeAddr: device.ieeeAddr,
type: device.zh.type,
@@ -155,8 +157,8 @@ export default class BridgeLegacy extends Extension {
payload.lastSeen = Date.now();
}
return payload;
});
devices.push(payload);
}
if (topic.split('/').pop() == 'get') {
await this.mqtt.publish(`bridge/config/devices`, stringify(devices), {}, settings.get().mqtt.base_topic, false, false);
@@ -179,7 +181,7 @@ export default class BridgeLegacy extends Extension {
let json = null;
try {
json = JSON.parse(message);
} catch (e) {
} catch {
logger.error(invalid);
return;
}
@@ -213,7 +215,7 @@ export default class BridgeLegacy extends Extension {
}
await this.mqtt.publish('bridge/log', stringify({type: `${isGroup ? 'group' : 'device'}_renamed`, message: {from, to}}));
} catch (error) {
} catch {
logger.error(`Failed to rename - ${from} to ${to}`);
}
}
@@ -231,7 +233,7 @@ export default class BridgeLegacy extends Extension {
if (json.hasOwnProperty('friendly_name')) {
name = json.friendly_name;
}
} catch (e) {
} catch {
// just friendly_name
name = message;
}
@@ -295,7 +297,7 @@ export default class BridgeLegacy extends Extension {
const cleanup = async (): Promise<void> => {
// Fire event
this.eventBus.emitDeviceRemoved({ieeeAddr, name});
this.eventBus.emitEntityRemoved({id: ieeeAddr, name: name, type: 'device'});
// Remove from configuration.yaml
settings.removeDevice(entity.ieeeAddr);
+7 -4
View File
@@ -2,6 +2,7 @@ import * as zhc from 'zigbee-herdsman-converters';
import logger from '../../util/logger';
import * as settings from '../../util/settings';
import utils from '../../util/utils';
import Extension from '../extension';
const defaultConfiguration = {
@@ -106,9 +107,11 @@ export default class Report extends Extension {
}
}
this.enabled
? await ep.bind(cluster, this.zigbee.firstCoordinatorEndpoint())
: await ep.unbind(cluster, this.zigbee.firstCoordinatorEndpoint());
if (this.enabled) {
await ep.bind(cluster, this.zigbee.firstCoordinatorEndpoint());
} else {
await ep.unbind(cluster, this.zigbee.firstCoordinatorEndpoint());
}
await ep.configureReporting(cluster, items);
logger.info(`Successfully ${term2} reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`);
@@ -176,7 +179,7 @@ export default class Report extends Extension {
}
override async start(): Promise<void> {
for (const device of this.zigbee.devices(false)) {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
if (this.shouldSetupReporting(device, null)) {
await this.setupReporting(device);
}
+2 -2
View File
@@ -45,7 +45,7 @@ export default class SoftReset extends Extension {
await this.zigbee.reset('soft');
logger.warning('Soft reset ZNP due to timeout');
} catch (error) {
logger.warning('Soft reset failed, trying stop/start');
logger.warning(`Soft reset failed, trying stop/start (${error.message})`);
await this.zigbee.stop();
logger.warning('Zigbee stopped');
@@ -53,7 +53,7 @@ export default class SoftReset extends Extension {
try {
await this.zigbee.start();
} catch (error) {
logger.error('Failed to restart!');
logger.error(`Failed to restart! (${error.message})`);
}
}
+45 -29
View File
@@ -121,11 +121,11 @@ export default class NetworkMap extends Extension {
// Shape the record according to device type
if (node.type == 'Coordinator') {
style = `style="bold, filled", fillcolor="${colors.fill.coordinator}", ` + `fontcolor="${colors.font.coordinator}"`;
style = `style="bold, filled", fillcolor="${colors.fill.coordinator}", fontcolor="${colors.font.coordinator}"`;
} else if (node.type == 'Router') {
style = `style="rounded, filled", fillcolor="${colors.fill.router}", ` + `fontcolor="${colors.font.router}"`;
style = `style="rounded, filled", fillcolor="${colors.fill.router}", fontcolor="${colors.font.router}"`;
} else {
style = `style="rounded, dashed, filled", fillcolor="${colors.fill.enddevice}", ` + `fontcolor="${colors.font.enddevice}"`;
style = `style="rounded, dashed, filled", fillcolor="${colors.fill.enddevice}", fontcolor="${colors.font.enddevice}"`;
}
// Add the device with its labels to the graph as a node.
@@ -210,31 +210,31 @@ export default class NetworkMap extends Extension {
async networkScan(includeRoutes: boolean): Promise<Topology> {
logger.info(`Starting network scan (includeRoutes '${includeRoutes}')`);
const devices = this.zigbee.devices().filter((d) => d.zh.type !== 'GreenPower' && !d.options.disabled);
const lqis: Map<Device, zh.LQI> = new Map();
const routingTables: Map<Device, zh.RoutingTable> = new Map();
const failed: Map<Device, string[]> = new Map();
const requestWithRetry = async <T>(request: () => Promise<T>): Promise<T> => {
try {
const result = await request();
return result;
} catch {
// Network is possibly congested, sleep 5 seconds to let the network settle.
await utils.sleep(5);
return request();
}
};
for (const device of this.zigbee.devicesIterator((d) => d.type !== 'GreenPower' && d.type !== 'EndDevice')) {
if (device.options.disabled) {
continue;
}
for (const device of devices.filter((d) => d.zh.type != 'EndDevice')) {
failed.set(device, []);
await utils.sleep(1); // sleep 1 second between each scan to reduce stress on network.
const doRequest = async <T>(request: () => Promise<T>, firstAttempt = true): Promise<T> => {
try {
return await request();
} catch (error) {
if (!firstAttempt) {
throw error;
} else {
// Network is possibly congested, sleep 5 seconds to let the network settle.
await utils.sleep(5);
return doRequest(request, false);
}
}
};
try {
const result = await doRequest<zh.LQI>(async () => device.zh.lqi());
const result = await requestWithRetry<zh.LQI>(async () => device.zh.lqi());
lqis.set(device, result);
logger.debug(`LQI succeeded for '${device.name}'`);
} catch (error) {
@@ -245,12 +245,13 @@ export default class NetworkMap extends Extension {
if (includeRoutes) {
try {
const result = await doRequest(async () => device.zh.routingTable());
const result = await requestWithRetry<zh.RoutingTable>(async () => device.zh.routingTable());
routingTables.set(device, result);
logger.debug(`Routing table succeeded for '${device.name}'`);
} catch (error) {
failed.get(device).push('routingTable');
logger.error(`Failed to execute routing table for '${device.name}'`);
logger.debug(error.stack);
}
}
}
@@ -258,8 +259,14 @@ export default class NetworkMap extends Extension {
logger.info(`Network scan finished`);
const topology: Topology = {nodes: [], links: []};
// Add nodes
for (const device of devices) {
// XXX: display GP/disabled devices in the map, better feedback than just hiding them?
for (const device of this.zigbee.devicesIterator((d) => d.type !== 'GreenPower')) {
if (device.options.disabled) {
continue;
}
// Add nodes
const definition = device.definition
? {
model: device.definition.model,
@@ -289,7 +296,7 @@ export default class NetworkMap extends Extension {
}
// Add links
lqis.forEach((lqi, device) => {
for (const [device, lqi] of lqis) {
for (const neighbor of lqi.neighbors) {
if (neighbor.relationship > 3) {
// Relationship is not active, skip it
@@ -298,9 +305,13 @@ export default class NetworkMap extends Extension {
// Some Xiaomi devices return 0x00 as the neighbor ieeeAddr (obviously not correct).
// Determine the correct ieeeAddr based on the networkAddress.
const neighborDevice = this.zigbee.deviceByNetworkAddress(neighbor.networkAddress);
if (neighbor.ieeeAddr === '0x0000000000000000' && neighborDevice) {
neighbor.ieeeAddr = neighborDevice.ieeeAddr;
if (neighbor.ieeeAddr === '0x0000000000000000') {
const neighborDevice = this.zigbee.deviceByNetworkAddress(neighbor.networkAddress);
/* istanbul ignore else */
if (neighborDevice) {
neighbor.ieeeAddr = neighborDevice.ieeeAddr;
}
}
const link: Link = {
@@ -318,13 +329,18 @@ export default class NetworkMap extends Extension {
};
const routingTable = routingTables.get(device);
if (routingTable) {
link.routes = routingTable.table.filter((t) => t.status === 'ACTIVE' && t.nextHop === neighbor.networkAddress);
for (const entry of routingTable.table) {
if (entry.status === 'ACTIVE' && entry.nextHop === neighbor.networkAddress) {
link.routes.push(entry);
}
}
}
topology.links.push(link);
}
});
}
return topology;
}
+4 -2
View File
@@ -1,5 +1,6 @@
import * as zhc from 'zigbee-herdsman-converters';
import utils from '../util/utils';
import Extension from './extension';
/**
@@ -7,7 +8,7 @@ import Extension from './extension';
*/
export default class OnEvent extends Extension {
override async start(): Promise<void> {
for (const device of this.zigbee.devices(false)) {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
await this.callOnEvent(device, 'start', {});
}
@@ -31,7 +32,8 @@ export default class OnEvent extends Extension {
override async stop(): Promise<void> {
await super.stop();
for (const device of this.zigbee.devices(false)) {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
await this.callOnEvent(device, 'stop', {});
}
}
+9 -6
View File
@@ -16,7 +16,7 @@ function isValidUrl(url: string): boolean {
let parsed;
try {
parsed = URI.parse(url);
} catch (_) {
} catch {
// istanbul ignore next
return false;
}
@@ -65,10 +65,10 @@ export default class OTAUpdate extends Extension {
// In order to support local firmware files we need to let zigbeeOTA know where the data directory is
zhc.ota.setDataDir(dataDir.getPath());
// In case Zigbee2MQTT is restared during an update, progress and remaining values are still in state.
// remove them.
for (const device of this.zigbee.devices(false)) {
// In case Zigbee2MQTT is restared during an update, progress and remaining values are still in state, remove them.
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
this.removeProgressAndRemainingFromState(device);
// Reset update state, e.g. when Z2M restarted during update.
if (this.state.get(device).update?.state === 'updating') {
this.state.get(device).update.state = 'available';
@@ -104,6 +104,7 @@ export default class OTAUpdate extends Extension {
} catch (e) {
supportsOTA = false;
logger.debug(`Failed to check if update available for '${data.device.name}' (${e.message})`);
logger.debug(e.stack);
}
const payload = this.getEntityPublishPayload(data.device, availableResult ?? 'idle');
@@ -138,7 +139,7 @@ export default class OTAUpdate extends Extension {
const endpoint = device.zh.endpoints.find((e) => e.supportsInputCluster('genBasic'));
const result = await endpoint.read('genBasic', ['dateCode', 'swBuildId'], {sendPolicy});
return {softwareBuildID: result.swBuildId, dateCode: result.dateCode};
} catch (e) {
} catch {
return null;
}
}
@@ -319,7 +320,9 @@ export default class OTAUpdate extends Extension {
if (error) {
logger.error(error);
errorStack && logger.debug(errorStack);
if (errorStack) {
logger.debug(errorStack);
}
}
}
}
+6 -5
View File
@@ -74,13 +74,13 @@ export default class Publish extends Extension {
if (parsedTopic.attribute) {
try {
return {[parsedTopic.attribute]: JSON.parse(data.message)};
} catch (e) {
} catch {
return {[parsedTopic.attribute]: data.message};
}
} else {
try {
return JSON.parse(data.message);
} catch (e) {
} catch {
if (stateValues.includes(data.message.toLowerCase())) {
return {state: data.message};
} else {
@@ -229,7 +229,8 @@ export default class Publish extends Extension {
if (!usedConverters.hasOwnProperty(endpointOrGroupID)) usedConverters[endpointOrGroupID] = [];
/* istanbul ignore next */
const converter = converters.find((c) => c.key.includes(key) && (!c.endpoint || c.endpoint == endpointName));
// Match any key if the toZigbee converter defines no key.
const converter = converters.find((c) => (!c.key || c.key.includes(key)) && (!c.endpoint || c.endpoint == endpointName));
if (parsedTopic.type === 'set' && usedConverters[endpointOrGroupID].includes(converter)) {
// Use a converter for set only once
@@ -317,12 +318,12 @@ export default class Publish extends Extension {
}
for (const [ID, payload] of Object.entries(toPublish)) {
if (Object.keys(payload).length != 0) {
if (!utils.objectIsEmpty(payload)) {
await this.publishEntityState(toPublishEntity[ID], payload);
}
}
const scenesChanged = Object.values(usedConverters).some((cl) => cl.some((c) => c.key.some((k) => sceneConverterKeys.includes(k))));
const scenesChanged = Object.values(usedConverters).some((cl) => cl.some((c) => c.key?.some((k) => sceneConverterKeys.includes(k))));
if (scenesChanged) {
this.eventBus.emitScenesChanged({entity: re});
}
+1 -1
View File
@@ -163,7 +163,7 @@ export default class Receive extends Extension {
}
}
if (Object.keys(payload).length) {
if (!utils.objectIsEmpty(payload)) {
await publish(payload);
} else {
await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, settings.get(), true, this.publishEntityState);
+2 -2
View File
@@ -42,8 +42,8 @@ export default class Device {
}
}
async resolveDefinition(): Promise<void> {
if (!this.zh.interviewing && (!this.definition || this._definitionModelID !== this.zh.modelID)) {
async resolveDefinition(ignoreCache = false): Promise<void> {
if (!this.zh.interviewing && (!this.definition || this._definitionModelID !== this.zh.modelID || ignoreCache)) {
this.definition = await zhc.findByDevice(this.zh, true);
this._definitionModelID = this.zh.modelID;
}
+1 -1
View File
@@ -17,7 +17,7 @@ export default class MQTT {
private eventBus: EventBus;
private initialConnect = true;
private republishRetainedTimer: NodeJS.Timeout;
private retainedMessages: {
public retainedMessages: {
[s: string]: {payload: string; options: MQTTOptions; skipLog: boolean; skipReceive: boolean; topic: string; base: string};
} = {};
+2 -2
View File
@@ -65,8 +65,8 @@ class State {
try {
this.state = JSON.parse(fs.readFileSync(this.file, 'utf8'));
logger.debug(`Loaded state from file ${this.file}`);
} catch (e) {
logger.debug(`Failed to load state from file ${this.file} (corrupt file?)`);
} catch (error) {
logger.debug(`Failed to load state from file ${this.file} (corrupt file?) (${error.message})`);
}
} else {
logger.debug(`Can't load state from file ${this.file} (doesn't exist)`);
+1 -1
View File
@@ -73,7 +73,7 @@ declare global {
namespace eventdata {
type EntityRenamed = {entity: Device | Group; homeAssisantRename: boolean; from: string; to: string};
type DeviceRemoved = {ieeeAddr: string; name: string};
type EntityRemoved = {id: number | string; name: string; type: 'device' | 'group'};
type MQTTMessage = {topic: string; message: string};
type MQTTMessagePublished = {topic: string; payload: string; options: {retain: boolean; qos: number}};
type StateChange = {
+1
View File
@@ -106,6 +106,7 @@ class Logger {
const options: KeyValue = {
app_name: 'Zigbee2MQTT',
format: winston.format.printf((info) => info.message),
...settings.get().advanced.log_syslog,
};
+11 -5
View File
@@ -227,7 +227,7 @@
},
"adapter": {
"type": ["string"],
"enum": ["deconz", "zstack", "zigate", "ezsp", "auto", "ember"],
"enum": ["deconz", "zstack", "zigate", "ezsp", "auto", "ember", "zboss"],
"title": "Adapter",
"default": "auto",
"requiresRestart": true,
@@ -609,7 +609,7 @@
],
"title": "Pan ID",
"requiresRestart": true,
"description": "ZigBee pan ID, changing requires repairing all devices!"
"description": "ZigBee pan ID, changing requires re-pairing all devices!"
},
"ext_pan_id": {
"oneOf": [
@@ -627,7 +627,7 @@
],
"title": "Ext Pan ID",
"requiresRestart": true,
"description": "Zigbee extended pan ID, changing requires repairing all devices!"
"description": "Zigbee extended pan ID, changing requires re-pairing all devices!"
},
"channel": {
"type": "number",
@@ -636,19 +636,23 @@
"default": 11,
"title": "ZigBee channel",
"requiresRestart": true,
"description": "Zigbee channel, changing requires repairing all devices! (Note: use a ZLL channel: 11, 15, 20, or 25 to avoid Problems)",
"description": "Zigbee channel, changing might require re-pairing some devices! (Note: use a ZLL channel: 11, 15, 20, or 25 to avoid problems)",
"examples": [15, 20, 25]
},
"adapter_concurrent": {
"title": "Adapter concurrency",
"requiresRestart": true,
"type": ["number", "null"],
"minimum": 1,
"maximum": 64,
"description": "Adapter concurrency (e.g. 2 for CC2531 or 16 for CC26X2R1) (default: null, uses recommended value)"
},
"adapter_delay": {
"type": ["number", "null"],
"requiresRestart": true,
"title": "Adapter delay",
"minimum": 0,
"maximum": 1000,
"description": "Adapter delay"
},
"cache_state": {
@@ -698,7 +702,7 @@
],
"title": "Network key",
"requiresRestart": true,
"description": "Network encryption key, changing requires repairing all devices!"
"description": "Network encryption key, changing requires re-pairing all devices!"
},
"timestamp_format": {
"type": "string",
@@ -711,6 +715,8 @@
"type": ["number", "null"],
"title": "Transmit power",
"requiresRestart": true,
"minimum": -128,
"maximum": 127,
"description": "Transmit power of adapter, only available for Z-Stack (CC253*/CC2652/CC1352) adapters, CC2652 = 5dbm, CC1352 max is = 20dbm (5dbm default)"
},
"output": {
+32 -26
View File
@@ -7,7 +7,7 @@ import schemaJson from './settings.schema.json';
import utils from './utils';
import yaml from './yaml';
export let schema = schemaJson;
// @ts-ignore
// @ts-expect-error
schema = {};
objectAssignDeep(schema, schemaJson);
@@ -140,16 +140,16 @@ function loadSettingsWithDefaults(): void {
'homeassistant_legacy_entity_attributes',
'homeassistant_status_topic',
]) {
// @ts-ignore
// @ts-expect-error
if (_settingsWithDefaults.advanced[key] !== undefined) {
// @ts-ignore
// @ts-expect-error
sLegacy[key.replace('homeassistant_', '')] = _settingsWithDefaults.advanced[key];
}
}
}
const s = typeof _settingsWithDefaults.homeassistant === 'object' ? _settingsWithDefaults.homeassistant : {};
// @ts-ignore
// @ts-expect-error
_settingsWithDefaults.homeassistant = {};
objectAssignDeep(_settingsWithDefaults.homeassistant, defaults, sLegacy, s);
}
@@ -157,7 +157,7 @@ function loadSettingsWithDefaults(): void {
if (_settingsWithDefaults.availability || _settingsWithDefaults.advanced?.availability_timeout) {
const defaults = {};
const s = typeof _settingsWithDefaults.availability === 'object' ? _settingsWithDefaults.availability : {};
// @ts-ignore
// @ts-expect-error
_settingsWithDefaults.availability = {};
objectAssignDeep(_settingsWithDefaults.availability, defaults, s);
}
@@ -165,35 +165,34 @@ function loadSettingsWithDefaults(): void {
if (_settingsWithDefaults.frontend) {
const defaults = {port: 8080, auth_token: false};
const s = typeof _settingsWithDefaults.frontend === 'object' ? _settingsWithDefaults.frontend : {};
// @ts-ignore
_settingsWithDefaults.frontend = {};
objectAssignDeep(_settingsWithDefaults.frontend, defaults, s);
}
if (_settings.advanced?.hasOwnProperty('baudrate') && _settings.serial?.baudrate == null) {
// @ts-ignore
// @ts-expect-error
_settingsWithDefaults.serial.baudrate = _settings.advanced.baudrate;
}
if (_settings.advanced?.hasOwnProperty('rtscts') && _settings.serial?.rtscts == null) {
// @ts-ignore
// @ts-expect-error
_settingsWithDefaults.serial.rtscts = _settings.advanced.rtscts;
}
if (_settings.advanced?.hasOwnProperty('ikea_ota_use_test_url') && _settings.ota?.ikea_ota_use_test_url == null) {
// @ts-ignore
// @ts-expect-error
_settingsWithDefaults.ota.ikea_ota_use_test_url = _settings.advanced.ikea_ota_use_test_url;
}
// @ts-ignore
// @ts-expect-error
if (_settings.experimental?.hasOwnProperty('transmit_power') && _settings.advanced?.transmit_power == null) {
// @ts-ignore
// @ts-expect-error
_settingsWithDefaults.advanced.transmit_power = _settings.experimental.transmit_power;
}
// @ts-ignore
// @ts-expect-error
if (_settings.experimental?.hasOwnProperty('output') && _settings.advanced?.output == null) {
// @ts-ignore
// @ts-expect-error
_settingsWithDefaults.advanced.output = _settings.experimental.output;
}
@@ -201,10 +200,17 @@ function loadSettingsWithDefaults(): void {
_settingsWithDefaults.advanced.log_level = 'warning';
}
// @ts-ignore
_settingsWithDefaults.ban && _settingsWithDefaults.blocklist.push(..._settingsWithDefaults.ban);
// @ts-ignore
_settingsWithDefaults.whitelist && _settingsWithDefaults.passlist.push(..._settingsWithDefaults.whitelist);
// @ts-expect-error
if (_settingsWithDefaults.ban) {
// @ts-expect-error
_settingsWithDefaults.blocklist.push(..._settingsWithDefaults.ban);
}
// @ts-expect-error
if (_settingsWithDefaults.whitelist) {
// @ts-expect-error
_settingsWithDefaults.passlist.push(..._settingsWithDefaults.whitelist);
}
}
function parseValueRef(text: string): {filename: string; key: string} | null {
@@ -257,7 +263,7 @@ function write(): void {
.filter((f: string, i: number) => i !== 0)
.map((f: string) => yaml.readIfExists(data.joinPath(f), {}))
.map((c: KeyValue) => Object.keys(c))
// @ts-ignore
// @ts-expect-error
.forEach((k: string) => delete content[k]);
}
@@ -280,7 +286,7 @@ export function validate(): string[] {
getInternalSettings();
} catch (error) {
if (error.name === 'YAMLException') {
return [`Your YAML file: '${error.file}' is invalid ` + `(use https://jsonformatter.org/yaml-validator to find and fix the issue)`];
return [`Your YAML file: '${error.file}' is invalid (use https://jsonformatter.org/yaml-validator to find and fix the issue)`];
}
return [error.message];
@@ -395,12 +401,12 @@ function read(): Settings {
// Read devices/groups configuration from separate file if specified.
const readDevicesOrGroups = (type: 'devices' | 'groups'): void => {
if (typeof s[type] === 'string' || (Array.isArray(s[type]) && Array(s[type]).length > 0)) {
/* eslint-disable-line */ // @ts-ignore
/* eslint-disable-line */
const files: string[] = Array.isArray(s[type]) ? s[type] : [s[type]];
s[type] = {};
for (const file of files) {
const content = yaml.readIfExists(data.joinPath(file), {});
/* eslint-disable-line */ // @ts-ignore
/* eslint-disable-line */ // @ts-expect-error
s[type] = objectAssignDeep.noMutate(s[type], content);
}
}
@@ -422,20 +428,20 @@ function applyEnvironmentVariables(settings: Partial<Settings>): void {
const envVariableName = `ZIGBEE2MQTT_CONFIG_${envPart}${key}`.toUpperCase();
if (process.env[envVariableName]) {
const setting = path.reduce((acc, val) => {
/* eslint-disable-line */ // @ts-ignore
/* eslint-disable-line */ // @ts-expect-error
acc[val] = acc[val] || {};
/* eslint-disable-line */ // @ts-ignore
/* eslint-disable-line */ // @ts-expect-error
return acc[val];
}, settings);
if (type.indexOf('object') >= 0 || type.indexOf('array') >= 0) {
try {
setting[key] = JSON.parse(process.env[envVariableName]);
} catch (error) {
} catch {
setting[key] = process.env[envVariableName];
}
} else if (type.indexOf('number') >= 0) {
/* eslint-disable-line */ // @ts-ignore
/* eslint-disable-line */ // @ts-expect-error
setting[key] = process.env[envVariableName] * 1;
} else if (type.indexOf('boolean') >= 0) {
setting[key] = process.env[envVariableName].toLowerCase() === 'true';
@@ -499,7 +505,7 @@ export function set(path: string[], value: string | number | boolean | KeyValue)
export function apply(settings: Record<string, unknown>): boolean {
getInternalSettings(); // Ensure _settings is initialized.
/* eslint-disable-line */ // @ts-ignore
/* eslint-disable-line */ // @ts-expect-error
const newSettings = objectAssignDeep.noMutate(_settings, settings);
utils.removeNullPropertiesFromObject(newSettings, NULLABLE_SETTINGS);
ajvSetting(newSettings);
+22 -5
View File
@@ -60,7 +60,7 @@ async function getZigbee2MQTTVersion(includeCommitHash = true): Promise<{commitH
if (err) {
try {
commitHash = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', '.hash'), 'utf-8');
} catch (error) {
} catch {
/* istanbul ignore next */
commitHash = 'unknown';
}
@@ -92,6 +92,12 @@ function formatDate(time: number, type: 'ISO_8601' | 'ISO_8601_local' | 'epoch'
}
}
function objectIsEmpty(object: object): boolean {
// much faster than checking `Object.keys(object).length`
for (const k in object) return false;
return true;
}
function objectHasProperties(object: {[s: string]: unknown}, properties: string[]): boolean {
for (const property of properties) {
if (!object.hasOwnProperty(property)) {
@@ -128,7 +134,7 @@ function getResponse(request: KeyValue | string, data: KeyValue, error: string):
function parseJSON(value: string, fallback: string): KeyValue | string {
try {
return JSON.parse(value);
} catch (e) {
} catch {
return fallback;
}
}
@@ -162,6 +168,7 @@ export function* loadExternalConverter(moduleName: string): Generator<ExternalDe
if (moduleName.endsWith('.js')) {
converter = loadModuleFromFile(data.joinPath(moduleName));
} else {
// eslint-disable-next-line @typescript-eslint/no-require-imports
converter = require(moduleName);
}
@@ -365,15 +372,15 @@ function filterProperties(filter: string[], data: KeyValue): void {
}
}
export function isNumericExposeFeature(feature: zhc.Feature): feature is zhc.Numeric {
export function isNumericExposeFeature(feature: zhc.Expose): feature is zhc.Numeric {
return feature?.type === 'numeric';
}
export function isEnumExposeFeature(feature: zhc.Feature): feature is zhc.Enum {
export function isEnumExposeFeature(feature: zhc.Expose): feature is zhc.Enum {
return feature?.type === 'enum';
}
export function isBinaryExposeFeature(feature: zhc.Feature): feature is zhc.Binary {
export function isBinaryExposeFeature(feature: zhc.Expose): feature is zhc.Binary {
return feature?.type === 'binary';
}
@@ -396,11 +403,19 @@ function getScenes(entity: zh.Endpoint | zh.Group): Scene[] {
return Object.values(scenes);
}
function deviceNotCoordinator(device: zh.Device): boolean {
return device.type !== 'Coordinator';
}
/* istanbul ignore next */
const noop = (): void => {};
export default {
capitalize,
getZigbee2MQTTVersion,
getDependencyVersion,
formatDate,
objectIsEmpty,
objectHasProperties,
equalsPartial,
getObjectProperty,
@@ -427,4 +442,6 @@ export default {
flatten,
arrayUnique,
getScenes,
deviceNotCoordinator,
noop,
};
+30 -19
View File
@@ -50,6 +50,7 @@ export default class Zigbee {
concurrent: settings.get().advanced.adapter_concurrent,
delay: settings.get().advanced.adapter_delay,
disableLED: settings.get().serial.disable_led,
transmitPower: settings.get().advanced.transmit_power,
},
acceptJoiningDeviceHandler: this.acceptJoiningDeviceHandler,
};
@@ -66,7 +67,7 @@ export default class Zigbee {
throw error;
}
for (const device of this.devices(false)) {
for (const device of this.devicesIterator(utils.deviceNotCoordinator)) {
await device.resolveDefinition();
}
@@ -124,7 +125,7 @@ export default class Zigbee {
logger.info(`Coordinator firmware version: '${stringify(await this.getCoordinatorVersion())}'`);
logger.debug(`Zigbee network parameters: ${stringify(await this.herdsman.getNetworkParameters())}`);
for (const device of this.devices(false)) {
for (const device of this.devicesIterator(utils.deviceNotCoordinator)) {
// If a passlist is used, all other device will be removed from the network.
const passlist = settings.get().passlist;
const blocklist = settings.get().blocklist;
@@ -135,24 +136,18 @@ export default class Zigbee {
logger.error(`Failed to remove '${device.ieeeAddr}' (${error.message})`);
}
};
if (passlist.length > 0) {
if (!passlist.includes(device.ieeeAddr)) {
logger.warning(`Device which is not on passlist connected (${device.ieeeAddr}), removing...`);
logger.warning(`Device not on passlist currently connected (${device.ieeeAddr}), removing...`);
await remove(device);
}
} else if (blocklist.includes(device.ieeeAddr)) {
logger.warning(`Device on blocklist is connected (${device.ieeeAddr}), removing...`);
logger.warning(`Device on blocklist currently connected (${device.ieeeAddr}), removing...`);
await remove(device);
}
}
// Check if we have to set a transmit power
if (settings.get().advanced.hasOwnProperty('transmit_power')) {
const transmitPower = settings.get().advanced.transmit_power;
await this.herdsman.setTransmitPower(transmitPower);
logger.info(`Set transmit power to '${transmitPower}'`);
}
return startResult;
}
@@ -253,7 +248,9 @@ export default class Zigbee {
@bind private resolveDevice(ieeeAddr: string): Device {
if (!this.deviceLookup[ieeeAddr]) {
const device = this.herdsman.getDeviceByIeeeAddr(ieeeAddr);
device && (this.deviceLookup[ieeeAddr] = new Device(device));
if (device) {
this.deviceLookup[ieeeAddr] = new Device(device);
}
}
const device = this.deviceLookup[ieeeAddr];
@@ -325,15 +322,29 @@ export default class Zigbee {
return this.herdsman.getDevicesByType('Coordinator')[0].endpoints[0];
}
groups(): Group[] {
return this.herdsman.getGroups().map((g) => this.resolveGroup(g.groupID));
*devicesAndGroupsIterator(
devicePredicate?: (value: zh.Device) => boolean,
groupPredicate?: (value: zh.Group) => boolean,
): Generator<Device | Group> {
for (const device of this.herdsman.getDevicesIterator(devicePredicate)) {
yield this.resolveDevice(device.ieeeAddr);
}
for (const group of this.herdsman.getGroupsIterator(groupPredicate)) {
yield this.resolveGroup(group.groupID);
}
}
devices(includeCoordinator = true): Device[] {
return this.herdsman
.getDevices()
.map((d) => this.resolveDevice(d.ieeeAddr))
.filter((d) => includeCoordinator || d.zh.type !== 'Coordinator');
*groupsIterator(predicate?: (value: zh.Group) => boolean): Generator<Group> {
for (const group of this.herdsman.getGroupsIterator(predicate)) {
yield this.resolveGroup(group.groupID);
}
}
*devicesIterator(predicate?: (value: zh.Device) => boolean): Generator<Device> {
for (const device of this.herdsman.getDevicesIterator(predicate)) {
yield this.resolveDevice(device.ieeeAddr);
}
}
@bind private async acceptJoiningDeviceHandler(ieeeAddr: string): Promise<boolean> {
+10476 -9764
View File
File diff suppressed because it is too large Load Diff
+114 -114
View File
@@ -1,117 +1,117 @@
{
"name": "zigbee2mqtt",
"version": "1.39.0",
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
"main": "index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
},
"engines": {
"node": "^18 || ^20 || ^22"
},
"keywords": [
"xiaomi",
"tradfri",
"hue",
"bridge",
"zigbee",
"mqtt",
"cc2531"
],
"scripts": {
"build": "tsc && node index.js writehash",
"build-watch": "tsc --watch",
"eslint": "eslint lib/ --max-warnings=0",
"pretty:write": "prettier --write lib test",
"pretty:check": "prettier --check lib test",
"start": "node index.js",
"test-with-coverage": "jest test --silent --maxWorkers=50% --coverage",
"test": "jest test --silent --maxWorkers=50%",
"test-watch": "jest test --silent --maxWorkers=25% --watch"
},
"author": "Koen Kanters",
"license": "GPL-3.0",
"bugs": {
"url": "https://github.com/Koenkk/zigbee2mqtt/issues"
},
"homepage": "https://koenkk.github.io/zigbee2mqtt",
"dependencies": {
"ajv": "^8.16.0",
"bind-decorator": "^1.0.11",
"connect-gzip-static": "3.0.1",
"debounce": "^2.1.0",
"fast-deep-equal": "^3.1.3",
"finalhandler": "^1.2.0",
"git-last-commit": "^1.0.1",
"humanize-duration": "^3.32.1",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"jszip": "^3.10.1",
"mkdir-recursive": "^0.4.0",
"moment": "^2.30.1",
"mqtt": "^5.7.3",
"object-assign-deep": "^0.4.0",
"rimraf": "^5.0.7",
"semver": "^7.6.2",
"source-map-support": "^0.5.21",
"uri-js": "^4.4.1",
"winston": "^3.13.0",
"winston-syslog": "^2.7.0",
"winston-transport": "^4.7.0",
"ws": "^8.17.1",
"zigbee-herdsman": "0.50.1",
"zigbee-herdsman-converters": "19.62.0",
"zigbee2mqtt-frontend": "0.7.2"
},
"devDependencies": {
"@babel/core": "^7.24.7",
"@babel/plugin-proposal-decorators": "^7.24.7",
"@babel/preset-env": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@types/finalhandler": "^1.2.3",
"@types/humanize-duration": "^3.27.4",
"@types/jest": "^29.5.12",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.14.9",
"@types/object-assign-deep": "^0.4.3",
"@types/readable-stream": "4.0.14",
"@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.14.1",
"babel-jest": "^29.7.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-perfectionist": "^2.11.0",
"jest": "^29.7.0",
"prettier": "^3.3.2",
"tmp": "^0.2.3",
"typescript": "^5.5.2"
},
"overrides": {
"zigbee-herdsman-converters": {
"zigbee-herdsman": "$zigbee-herdsman"
}
},
"jest": {
"coverageThreshold": {
"global": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
"name": "zigbee2mqtt",
"version": "1.40.1",
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
"main": "index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
},
"collectCoverageFrom": [
"lib/**/*.js",
"lib/**/*.ts"
]
},
"bin": {
"zigbee2mqtt": "cli.js"
},
"optionalDependencies": {
"sd-notify": "^2.8.0"
}
"engines": {
"node": "^18 || ^20 || ^22"
},
"keywords": [
"xiaomi",
"tradfri",
"hue",
"bridge",
"zigbee",
"mqtt",
"cc2531"
],
"scripts": {
"build": "tsc && node index.js writehash",
"build-watch": "tsc --watch",
"eslint": "eslint lib/ --max-warnings=0",
"pretty:write": "prettier --write .",
"pretty:check": "prettier --check .",
"start": "node index.js",
"test-with-coverage": "jest test --silent --maxWorkers=50% --coverage",
"test": "jest test --silent --maxWorkers=50%",
"test-watch": "jest test --silent --maxWorkers=25% --watch"
},
"author": "Koen Kanters",
"license": "GPL-3.0",
"bugs": {
"url": "https://github.com/Koenkk/zigbee2mqtt/issues"
},
"homepage": "https://koenkk.github.io/zigbee2mqtt",
"dependencies": {
"ajv": "^8.17.1",
"bind-decorator": "^1.0.11",
"connect-gzip-static": "3.0.1",
"debounce": "^2.1.0",
"fast-deep-equal": "^3.1.3",
"finalhandler": "^1.2.0",
"git-last-commit": "^1.0.1",
"humanize-duration": "^3.32.1",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"jszip": "^3.10.1",
"mkdir-recursive": "^0.4.0",
"moment": "^2.30.1",
"mqtt": "^5.10.1",
"object-assign-deep": "^0.4.0",
"rimraf": "^6.0.1",
"semver": "^7.6.3",
"source-map-support": "^0.5.21",
"uri-js": "^4.4.1",
"winston": "^3.14.2",
"winston-syslog": "^2.7.1",
"winston-transport": "^4.7.1",
"ws": "^8.18.0",
"zigbee-herdsman": "0.57.3",
"zigbee-herdsman-converters": "20.12.1",
"zigbee2mqtt-frontend": "0.7.4"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/plugin-proposal-decorators": "^7.24.7",
"@babel/preset-env": "^7.25.4",
"@babel/preset-typescript": "^7.24.7",
"@types/finalhandler": "^1.2.3",
"@types/humanize-duration": "^3.27.4",
"@types/jest": "^29.5.12",
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.5.1",
"@types/object-assign-deep": "^0.4.3",
"@types/readable-stream": "4.0.15",
"@types/ws": "8.5.12",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"babel-jest": "^29.7.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jest": "^28.8.1",
"eslint-plugin-perfectionist": "^3.3.0",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"tmp": "^0.2.3",
"typescript": "^5.5.4"
},
"overrides": {
"zigbee-herdsman-converters": {
"zigbee-herdsman": "$zigbee-herdsman"
}
},
"jest": {
"coverageThreshold": {
"global": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
},
"collectCoverageFrom": [
"lib/**/*.js",
"lib/**/*.ts"
]
},
"bin": {
"zigbee2mqtt": "cli.js"
},
"optionalDependencies": {
"sd-notify": "^2.8.0"
}
}
+11 -11
View File
@@ -1,12 +1,12 @@
{
"packages": {
".": {
"release-type": "node",
"include-component-in-tag": false,
"include-v-in-tag": false,
"draft": true
}
},
"pull-request-title-pattern": "chore${scope}: release ${version}",
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
}
"packages": {
".": {
"release-type": "node",
"include-component-in-tag": false,
"include-v-in-tag": false,
"draft": true
}
},
"pull-request-title-pattern": "chore${scope}: release ${version}",
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
}
+32 -13
View File
@@ -10,14 +10,27 @@ const zhTillVersion = process.argv[4];
const frontendTillVersion = process.argv[5];
const changelogs = [
{tillVersion: z2mTillVersion, project: 'koenkk/zigbee2mqtt',
contents: fs.readFileSync(path.join(__dirname, '..', 'CHANGELOG.md'), 'utf-8').split('\n')},
{tillVersion: zhcTillVersion, project: 'koenkk/zigbee-herdsman-converters',
contents: fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'zigbee-herdsman-converters', 'CHANGELOG.md'), 'utf-8').split('\n')},
{tillVersion: zhTillVersion, project: 'koenkk/zigbee-herdsman',
contents: fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'zigbee-herdsman', 'CHANGELOG.md'), 'utf-8').split('\n')},
{tillVersion: frontendTillVersion, project: 'nurikk/zigbee2mqtt-frontend', isFrontend: true,
contents: fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'zigbee2mqtt-frontend', 'CHANGELOG.md'), 'utf-8').split('\n')},
{
tillVersion: z2mTillVersion,
project: 'koenkk/zigbee2mqtt',
contents: fs.readFileSync(path.join(__dirname, '..', 'CHANGELOG.md'), 'utf-8').split('\n'),
},
{
tillVersion: zhcTillVersion,
project: 'koenkk/zigbee-herdsman-converters',
contents: fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'zigbee-herdsman-converters', 'CHANGELOG.md'), 'utf-8').split('\n'),
},
{
tillVersion: zhTillVersion,
project: 'koenkk/zigbee-herdsman',
contents: fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'zigbee-herdsman', 'CHANGELOG.md'), 'utf-8').split('\n'),
},
{
tillVersion: frontendTillVersion,
project: 'nurikk/zigbee2mqtt-frontend',
isFrontend: true,
contents: fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'zigbee2mqtt-frontend', 'CHANGELOG.md'), 'utf-8').split('\n'),
},
];
const releaseRe = /## \[(.+)\]/;
@@ -54,20 +67,26 @@ for (const changelog of changelogs) {
} else if (line.startsWith('* **ignore:**')) {
continue;
} else if (changeMatch) {
let localContext = changelog.isFrontend ? 'frontend' : (changeMatch[2] ? changeMatch[2] : context);
let localContext = changelog.isFrontend ? 'frontend' : changeMatch[2] ? changeMatch[2] : context;
if (!changes[localContext]) localContext = 'error';
const commit = changeMatch[5];
const commitUserKey = `${changelog.project}-${commit} `;
let user = commitUserKey in commitUserLookup ? commitUserLookup[commitUserKey] :
execSync(`curl -s https://api.github.com/repos/${changelog.project}/commits/${commit} | jq -r '.author.login'`).toString().trim();
let user =
commitUserKey in commitUserLookup
? commitUserLookup[commitUserKey]
: execSync(`curl -s https://api.github.com/repos/${changelog.project}/commits/${commit} | jq -r '.author.login'`)
.toString()
.trim();
if (user !== 'null') commitUserLookup[commitUserKey] = user;
const messages = [];
let message = changeMatch[3].trim();
if (message.endsWith('.')) message = message.substring(0, message.length - 1);
if (changelog.isFrontend) {
changes[localContext].push(`- [${commit.slice(0, 7)}](https://github.com/${changelog.project}/commit/${commit}) ${message} (@${user})`);
changes[localContext].push(
`- [${commit.slice(0, 7)}](https://github.com/${changelog.project}/commit/${commit}) ${message} (@${user})`,
);
messages.push(capitalizeFirstChar(message));
} else {
const otherUser = message.match(/\[@(.+)\]\(https:\/\/github.com\/.+\)/) || message.match(/@(.+)/);
@@ -122,7 +141,7 @@ for (const name of names) {
if (name[0] === 'add') {
result += `This release adds support for ${changes['add'].length} devices: \n`;
}
changes[name[0]].forEach((e) => result += `${e}\n`);
changes[name[0]].forEach((e) => (result += `${e}\n`));
result += '\n';
}
+20 -15
View File
@@ -3,7 +3,6 @@ const {ZnpVersion} = require('zigbee-herdsman/dist/adapter/z-stack/adapter/tstyp
const {Subsystem} = require('zigbee-herdsman/dist/adapter/z-stack/unpi/constants');
const {Znp} = require('zigbee-herdsman/dist/adapter/z-stack/znp');
class ZStackNvMemEraser {
constructor(device) {
this.znp = new Znp(device, 115200, false);
@@ -27,8 +26,7 @@ class ZStackNvMemEraser {
this.version = (await this.znp.request(Subsystem.SYS, 'version', {})).payload;
} catch (e) {
console.log(`Failed to get zStack version, assuming 1.2`);
this.version = {'transportrev': 2, 'product': 0, 'majorrel': 2,
'minorrel': 0, 'maintrel': 0, 'revision': ''};
this.version = {transportrev: 2, product: 0, majorrel: 2, minorrel: 0, maintrel: 0, revision: ''};
}
console.log(`Detected znp version '${ZnpVersion[this.version.product]}' (${JSON.stringify(this.version)})`);
@@ -41,34 +39,41 @@ class ZStackNvMemEraser {
async clearAllNvMemItems() {
let maxNvMemId;
switch (this.version.product) {
case ZnpVersion.zStack12: maxNvMemId = 0x0302; break;
case ZnpVersion.zStack30x: maxNvMemId = 0x033F; break;
case ZnpVersion.zStack3x0: maxNvMemId = 0x032F; break;
case ZnpVersion.zStack12:
maxNvMemId = 0x0302;
break;
case ZnpVersion.zStack30x:
maxNvMemId = 0x033f;
break;
case ZnpVersion.zStack3x0:
maxNvMemId = 0x032f;
break;
}
let deletedCount = 0;
console.log(`Clearing all NVMEM items, from 0 to ${maxNvMemId}`);
for (let id=0; id<=maxNvMemId; id++) {
for (let id = 0; id <= maxNvMemId; id++) {
let len;
const needOsal = !(this.version.product == ZnpVersion.zStack3x0 && id <= 7);
if (needOsal) {
const lengthRes = await this.znp.request(Subsystem.SYS, 'osalNvLength', {id: id});
len = lengthRes.payload['length'];
} else {
const lengthRes = await this.znp.request(Subsystem.SYS, 'nvLength',
{sysid: NvSystemIds.ZSTACK, itemid: id, subid: 0});
const lengthRes = await this.znp.request(Subsystem.SYS, 'nvLength', {sysid: NvSystemIds.ZSTACK, itemid: id, subid: 0});
len = lengthRes.payload['len'];
}
if (len != 0) {
console.log(`NVMEM item #${id} - deleting, size: ${len}`);
if (needOsal) {
await this.znp.request(Subsystem.SYS, 'osalNvDelete',
{id: id, len: len},
null, null, [ZnpCommandStatus.SUCCESS, ZnpCommandStatus.NV_ITEM_INITIALIZED]);
await this.znp.request(Subsystem.SYS, 'osalNvDelete', {id: id, len: len}, null, null, [
ZnpCommandStatus.SUCCESS,
ZnpCommandStatus.NV_ITEM_INITIALIZED,
]);
} else {
await this.znp.request(Subsystem.SYS, 'nvDelete',
{sysid: NvSystemIds.ZSTACK, itemid: id, subid: 0},
null, null, [ZnpCommandStatus.SUCCESS, ZnpCommandStatus.NV_ITEM_INITIALIZED]);
await this.znp.request(Subsystem.SYS, 'nvDelete', {sysid: NvSystemIds.ZSTACK, itemid: id, subid: 0}, null, null, [
ZnpCommandStatus.SUCCESS,
ZnpCommandStatus.NV_ITEM_INITIALIZED,
]);
}
deletedCount++;
}
+29 -24
View File
@@ -89,12 +89,9 @@ describe('Bridge', () => {
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/info',
stringify({
restart_required: false,
commit: version.commitHash,
config: {
advanced: {
legacy_availability_payload: true,
output: 'json',
adapter_concurrent: null,
adapter_delay: null,
availability_blacklist: [],
@@ -109,6 +106,9 @@ describe('Bridge', () => {
ext_pan_id: [221, 221, 221, 221, 221, 221, 221, 221],
last_seen: 'disable',
legacy_api: false,
legacy_availability_payload: true,
log_debug_namespace_ignore: '',
log_debug_to_mqtt_frontend: false,
log_directory: directory,
log_file: 'log.log',
log_level: 'info',
@@ -117,8 +117,7 @@ describe('Bridge', () => {
log_rotation: true,
log_symlink_current: false,
log_syslog: {},
log_debug_namespace_ignore: '',
log_debug_to_mqtt_frontend: false,
output: 'json',
pan_id: 6754,
report: false,
soft_reset_timeout: 0,
@@ -127,7 +126,7 @@ describe('Bridge', () => {
blocklist: [],
device_options: {},
devices: {
'0x000b57fffec6a5b2': {friendly_name: 'bulb', description: 'this is my bulb', retain: true},
'0x000b57fffec6a5b2': {description: 'this is my bulb', friendly_name: 'bulb', retain: true},
'0x000b57fffec6a5b3': {friendly_name: 'bulb_color', retain: false},
'0x000b57fffec6a5b4': {friendly_name: 'bulb_color_2', retain: false},
'0x000b57fffec6a5b7': {friendly_name: 'bulb_2', retain: false},
@@ -158,7 +157,20 @@ describe('Bridge', () => {
'0x0017880104e45553': {friendly_name: 'bulb_enddevice', retain: false},
'0x0017880104e45559': {friendly_name: 'cc2530_router', retain: false},
'0x0017880104e45560': {friendly_name: 'livolo', retain: false},
'0x0017880104e45561': {friendly_name: 'temperature_sensor'},
'0x0017880104e45562': {friendly_name: 'heating_actuator'},
'0x0017880104e45724': {friendly_name: 'GLEDOPTO_2ID'},
'0x0017882104a44559': {friendly_name: 'TS0601_thermostat'},
'0x0017882104a44560': {friendly_name: 'TS0601_switch'},
'0x0017882104a44562': {friendly_name: 'TS0601_cover_switch'},
'0x0017882194e45543': {friendly_name: 'QS-Zigbee-D02-TRIAC-2C-LN'},
'0x18fc2600000d7ae2': {friendly_name: 'bosch_radiator'},
'0x90fd9ffffe4b64aa': {friendly_name: 'SP600_OLD'},
'0x90fd9ffffe4b64ab': {friendly_name: 'SP600_NEW'},
'0x90fd9ffffe4b64ac': {friendly_name: 'MKS-CM-W5'},
'0x90fd9ffffe4b64ae': {friendly_name: 'tradfri_remote', retain: false},
'0x90fd9ffffe4b64af': {friendly_name: 'roller_shutter'},
'0x90fd9ffffe4b64ax': {friendly_name: 'ZNLDP12LM'},
'0xf4ce368a38be56a1': {
cover_1_enabled: 'true',
cover_1_tilt_enabled: 'true',
@@ -176,29 +188,17 @@ describe('Bridge', () => {
front_surface_enabled: 'true',
retain: false,
},
'0x0017882104a44559': {friendly_name: 'TS0601_thermostat'},
'0x0017882104a44560': {friendly_name: 'TS0601_switch'},
'0x0017882104a44562': {friendly_name: 'TS0601_cover_switch'},
'0x0017882194e45543': {friendly_name: 'QS-Zigbee-D02-TRIAC-2C-LN'},
'0x90fd9ffffe4b64aa': {friendly_name: 'SP600_OLD'},
'0x90fd9ffffe4b64ab': {friendly_name: 'SP600_NEW'},
'0x90fd9ffffe4b64ac': {friendly_name: 'MKS-CM-W5'},
'0x90fd9ffffe4b64ae': {friendly_name: 'tradfri_remote', retain: false},
'0x90fd9ffffe4b64af': {friendly_name: 'roller_shutter'},
'0x90fd9ffffe4b64ax': {friendly_name: 'ZNLDP12LM'},
'0x0017880104e45561': {friendly_name: 'temperature_sensor'},
'0x0017880104e45562': {friendly_name: 'heating_actuator'},
},
external_converters: [],
groups: {
1: {friendly_name: 'group_1', retain: false},
9: {devices: ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'], friendly_name: 'ha_discovery_group'},
11: {devices: ['bulb_2'], friendly_name: 'group_with_tradfri', retain: false},
14: {devices: ['power_plug', 'bulb_2'], friendly_name: 'switch_group', retain: false},
12: {devices: ['TS0601_thermostat'], friendly_name: 'thermostat_group', retain: false},
14: {devices: ['power_plug', 'bulb_2'], friendly_name: 'switch_group', retain: false},
15071: {devices: ['bulb_color_2', 'bulb_2'], friendly_name: 'group_tradfri_remote', retain: false},
2: {friendly_name: 'group_2', retain: false},
21: {devices: ['GLEDOPTO_2ID/cct'], friendly_name: 'gledopto_group'},
9: {devices: ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'], friendly_name: 'ha_discovery_group'},
},
homeassistant: false,
map_options: {
@@ -210,20 +210,21 @@ describe('Bridge', () => {
},
},
},
mqtt: {base_topic: 'zigbee2mqtt', include_device_information: false, server: 'mqtt://localhost', force_disable_retain: false},
mqtt: {base_topic: 'zigbee2mqtt', force_disable_retain: false, include_device_information: false, server: 'mqtt://localhost'},
ota: {disable_automatic_update_check: false, update_check_interval: 1440},
passlist: [],
permit_join: true,
serial: {disable_led: false, port: '/dev/dummy'},
},
config_schema: settings.schema,
coordinator: {ieee_address: '0x00124b00120144ae', meta: {revision: 20190425, version: 1}, type: 'z-Stack'},
log_level: 'info',
network: {channel: 15, extended_pan_id: [0, 11, 22], pan_id: 5674},
permit_join: false,
restart_required: false,
version: version.version,
zigbee_herdsman_converters: zhcVersion,
zigbee_herdsman: zhVersion,
config_schema: settings.schema,
zigbee_herdsman_converters: zhcVersion,
}),
{retain: true, qos: 0},
expect.any(Function),
@@ -2989,11 +2990,15 @@ describe('Bridge', () => {
});
it('Should allow interviewing a device by ieeeAddr', async () => {
const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb);
device.resolveDefinition = jest.fn();
MQTT.publish.mockClear();
zigbeeHerdsman.devices.bulb.interview.mockClear();
expect(device.resolveDefinition).toHaveBeenCalledTimes(0);
MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: '0x000b57fffec6a5b2'}));
await flushPromises();
expect(zigbeeHerdsman.devices.bulb.interview).toHaveBeenCalled();
expect(zigbeeHerdsman.devices.bulb.interview).toHaveBeenCalledWith(true);
expect(device.resolveDefinition).toHaveBeenCalledWith(true);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/interview',
stringify({data: {id: '0x000b57fffec6a5b2'}, status: 'ok'}),
+4 -10
View File
@@ -66,6 +66,7 @@ describe('Controller', () => {
});
it('Start controller', async () => {
settings.set(['advanced', 'transmit_power'], 14);
await controller.start();
expect(zigbeeHerdsman.constructor).toHaveBeenCalledWith({
network: {
@@ -78,14 +79,14 @@ describe('Controller', () => {
databaseBackupPath: path.join(data.mockDir, 'database.db.backup'),
backupPath: path.join(data.mockDir, 'coordinator_backup.json'),
acceptJoiningDeviceHandler: expect.any(Function),
adapter: {concurrent: null, delay: null, disableLED: false},
adapter: {concurrent: null, delay: null, disableLED: false, transmitPower: 14},
serialPort: {baudRate: undefined, rtscts: undefined, path: '/dev/dummy'},
});
expect(zigbeeHerdsman.start).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(0);
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, undefined, undefined);
expect(logger.info).toHaveBeenCalledWith(`Currently ${Object.values(zigbeeHerdsman.devices).length - 1} devices are joined:`);
expect(logger.info).toHaveBeenCalledWith(`Currently ${Object.values(zigbeeHerdsman.devices).length - 1} devices are joined.`);
expect(logger.info).toHaveBeenCalledWith(
'bulb (0x000b57fffec6a5b2): LED1545G12 - IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (Router)',
);
@@ -277,7 +278,7 @@ describe('Controller', () => {
await controller.start();
await flushPromises();
expect(logger.error).toHaveBeenCalledWith('MQTT error: addr not found');
expect(logger.error).toHaveBeenCalledWith('MQTT failed to connect, exiting...');
expect(logger.error).toHaveBeenCalledWith('MQTT failed to connect, exiting... (addr not found)');
expect(mockExit).toHaveBeenCalledTimes(1);
expect(mockExit).toHaveBeenCalledWith(1, false);
});
@@ -289,13 +290,6 @@ describe('Controller', () => {
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, undefined);
});
it('Start controller with transmit power', async () => {
settings.set(['experimental', 'transmit_power'], 14);
await controller.start();
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledWith(14);
});
it('Start controller and stop with restart', async () => {
await controller.start();
await controller.stop(true);
+4
View File
@@ -243,6 +243,10 @@ describe('Frontend', () => {
mockWS.implementation.clients.push(mockWSClient.implementation);
await mockWS.events.connection(mockWSClient.implementation);
const allTopics = mockWSClient.implementation.send.mock.calls.map((m) => JSON.parse(m).topic);
expect(allTopics).toContain('bridge/devices');
expect(allTopics).toContain('bridge/info');
expect(allTopics).toContain('bridge/config');
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bridge/state', payload: 'online'}));
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'remote', payload: {brightness: 255}}));
+58 -3
View File
@@ -854,9 +854,7 @@ describe('HomeAssistant extension', () => {
});
it('Should discover thermostat devices', async () => {
let payload;
payload = {
const payload = {
action_template:
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}",
action_topic: 'zigbee2mqtt/TS0601_thermostat',
@@ -905,6 +903,50 @@ describe('HomeAssistant extension', () => {
);
});
it('Should discover Bosch BTH-RA with a compatibility mapping', async () => {
const payload = {
action_template:
"{% 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'}],
current_temperature_template: '{{ value_json.local_temperature }}',
current_temperature_topic: 'zigbee2mqtt/bosch_radiator',
device: {
identifiers: ['zigbee2mqtt_0x18fc2600000d7ae2'],
manufacturer: 'Bosch',
model: 'Radiator thermostat II (BTH-RA)',
name: 'bosch_radiator',
sw_version: '3.05.09',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
json_attributes_topic: 'zigbee2mqtt/bosch_radiator',
max_temp: '30',
min_temp: '5',
mode_command_template: `{% set values = { 'auto':'schedule','heat':'manual','off':'pause'} %}{"operating_mode": "{{ values[value] if value in values.keys() else 'pause' }}"}`,
mode_command_topic: 'zigbee2mqtt/bosch_radiator/set',
mode_state_template:
"{% set values = {'schedule':'auto','manual':'heat','pause':'off'} %}{% set value = value_json.operating_mode %}{{ values[value] if value in values.keys() else 'off' }}",
mode_state_topic: 'zigbee2mqtt/bosch_radiator',
modes: ['off', 'heat', 'auto'],
name: null,
object_id: 'bosch_radiator',
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_topic: 'zigbee2mqtt/bosch_radiator',
temperature_unit: 'C',
unique_id: '0x18fc2600000d7ae2_climate_zigbee2mqtt',
};
expect(MQTT.publish).toHaveBeenCalledWith(
'homeassistant/climate/0x18fc2600000d7ae2/climate/config',
stringify(payload),
{qos: 1, retain: true},
expect.any(Function),
);
});
it('Should discover devices with cover_position', async () => {
let payload;
@@ -1401,6 +1443,19 @@ describe('HomeAssistant extension', () => {
);
});
it('Should clear discovery when group is removed', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/group/remove', stringify({id: 'ha_discovery_group'}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'homeassistant/light/1221051039810110150109113116116_9/light/config',
null,
{retain: true, qos: 1},
expect.any(Function),
);
});
it('Should refresh discovery when device is renamed', async () => {
await MQTT.events.message(
'homeassistant/device_automation/0x0017880104e45522/action_double/config',
+190 -56
View File
@@ -66,62 +66,40 @@ describe('Networkmap', () => {
* | -> CC2530_ROUTER -> WXKG02LM_rev1
*
*/
coordinator.lqi = () => {
return {
neighbors: [
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 2, depth: 1, linkquality: 120},
{ieeeAddr: bulb.ieeeAddr, networkAddress: bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92},
{
ieeeAddr: external_converter_device.ieeeAddr,
networkAddress: external_converter_device.networkAddress,
relationship: 2,
depth: 1,
linkquality: 92,
},
],
};
};
coordinator.routingTable = () => {
return {table: [{destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress}]};
};
bulb.lqi = () => {
return {
neighbors: [
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 1, depth: 2, linkquality: 110},
{ieeeAddr: CC2530_ROUTER.ieeeAddr, networkAddress: CC2530_ROUTER.networkAddress, relationship: 1, depth: 2, linkquality: 100},
],
};
};
bulb.routingTable = () => {
return {table: []};
};
bulb_color.lqi = () => {
return {neighbors: []};
};
bulb_color.routingTable = () => {
return {table: []};
};
CC2530_ROUTER.lqi = () => {
return {
neighbors: [
{ieeeAddr: '0x0000000000000000', networkAddress: WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, linkquality: 130},
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 4, depth: 2, linkquality: 130},
],
};
};
CC2530_ROUTER.routingTable = () => {
return {table: []};
};
unsupported_router.lqi = () => {
throw new Error('failed');
};
unsupported_router.routingTable = () => {
throw new Error('failed');
};
coordinator.lqi = jest.fn().mockResolvedValue({
neighbors: [
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 2, depth: 1, linkquality: 120},
{ieeeAddr: bulb.ieeeAddr, networkAddress: bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92},
{
ieeeAddr: external_converter_device.ieeeAddr,
networkAddress: external_converter_device.networkAddress,
relationship: 2,
depth: 1,
linkquality: 92,
},
],
});
coordinator.routingTable = jest.fn().mockResolvedValue({
table: [{destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress}],
});
bulb.lqi = jest.fn().mockResolvedValue({
neighbors: [
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 1, depth: 2, linkquality: 110},
{ieeeAddr: CC2530_ROUTER.ieeeAddr, networkAddress: CC2530_ROUTER.networkAddress, relationship: 1, depth: 2, linkquality: 100},
],
});
bulb.routingTable = jest.fn().mockResolvedValue({table: []});
bulb_color.lqi = jest.fn().mockResolvedValue({neighbors: []});
bulb_color.routingTable = jest.fn().mockResolvedValue({table: []});
CC2530_ROUTER.lqi = jest.fn().mockResolvedValue({
neighbors: [
{ieeeAddr: '0x0000000000000000', networkAddress: WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, linkquality: 130},
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 4, depth: 2, linkquality: 130},
],
});
CC2530_ROUTER.routingTable = jest.fn().mockResolvedValue({table: []});
unsupported_router.lqi = jest.fn().mockRejectedValue(new Error('failed'));
unsupported_router.routingTable = jest.fn().mockRejectedValue(new Error('failed'));
}
it('Output raw networkmap legacy api', async () => {
@@ -826,4 +804,160 @@ describe('Networkmap', () => {
const actual = JSON.parse(call[1]);
expect(actual).toStrictEqual(expected);
});
it('Handles retrying request when first attempt fails', async () => {
settings.set(['devices', '0x000b57fffec6a5b2', 'disabled'], true);
mock();
bulb.lqi.mockRejectedValueOnce(new Error('failed'));
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
let call = MQTT.publish.mock.calls[0];
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
const expected = {
data: {
routes: true,
type: 'raw',
value: {
links: [
{
depth: 1,
linkquality: 120,
lqi: 120,
relationship: 2,
routes: [],
source: {ieeeAddr: '0x000b57fffec6a5b3', networkAddress: 40399},
sourceIeeeAddr: '0x000b57fffec6a5b3',
sourceNwkAddr: 40399,
target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0},
targetIeeeAddr: '0x00124b00120144ae',
},
{
depth: 1,
linkquality: 92,
lqi: 92,
relationship: 2,
routes: [{destinationAddress: 6540, nextHop: 40369, status: 'ACTIVE'}],
source: {ieeeAddr: '0x000b57fffec6a5b2', networkAddress: 40369},
sourceIeeeAddr: '0x000b57fffec6a5b2',
sourceNwkAddr: 40369,
target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0},
targetIeeeAddr: '0x00124b00120144ae',
},
{
depth: 1,
linkquality: 92,
lqi: 92,
relationship: 2,
routes: [],
source: {ieeeAddr: '0x0017880104e45511', networkAddress: 1114},
sourceIeeeAddr: '0x0017880104e45511',
sourceNwkAddr: 1114,
target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0},
targetIeeeAddr: '0x00124b00120144ae',
},
{
depth: 2,
linkquality: 130,
lqi: 130,
relationship: 1,
routes: [],
source: {ieeeAddr: '0x0017880104e45521', networkAddress: 6538},
sourceIeeeAddr: '0x0017880104e45521',
sourceNwkAddr: 6538,
target: {ieeeAddr: '0x0017880104e45559', networkAddress: 6540},
targetIeeeAddr: '0x0017880104e45559',
},
],
nodes: [
{
definition: null,
failed: [],
friendlyName: 'Coordinator',
ieeeAddr: '0x00124b00120144ae',
lastSeen: 1000,
modelID: null,
networkAddress: 0,
type: 'Coordinator',
},
{
definition: {
description: 'Hue Go',
model: '7146060PH',
supports:
'light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality',
vendor: 'Philips',
},
failed: [],
friendlyName: 'bulb_color',
ieeeAddr: '0x000b57fffec6a5b3',
lastSeen: 1000,
modelID: 'LLC020',
networkAddress: 40399,
type: 'Router',
},
{
definition: {
description: 'Wireless remote switch (double rocker), 2016 model',
model: 'WXKG02LM_rev1',
supports: 'battery, voltage, power_outage_count, action, linkquality',
vendor: 'Aqara',
},
friendlyName: 'button_double_key',
ieeeAddr: '0x0017880104e45521',
lastSeen: 1000,
modelID: 'lumi.sensor_86sw2.es1',
networkAddress: 6538,
type: 'EndDevice',
},
{
definition: {
description: 'Automatically generated definition',
model: 'notSupportedModelID',
supports: 'action, linkquality',
vendor: 'Boef',
},
failed: ['lqi', 'routingTable'],
friendlyName: '0x0017880104e45525',
ieeeAddr: '0x0017880104e45525',
lastSeen: 1000,
manufacturerName: 'Boef',
modelID: 'notSupportedModelID',
networkAddress: 6536,
type: 'Router',
},
{
definition: {
description: 'CC2530 router',
model: 'CC2530.ROUTER',
supports: 'led, linkquality',
vendor: 'Custom devices (DiY)',
},
failed: [],
friendlyName: 'cc2530_router',
ieeeAddr: '0x0017880104e45559',
lastSeen: 1000,
modelID: 'lumi.router',
networkAddress: 6540,
type: 'Router',
},
{
definition: {description: 'external', model: 'external_converter_device', supports: 'linkquality', vendor: 'external'},
friendlyName: '0x0017880104e45511',
ieeeAddr: '0x0017880104e45511',
lastSeen: 1000,
modelID: 'external_converter_device',
networkAddress: 1114,
type: 'EndDevice',
},
],
},
},
status: 'ok',
};
const actual = JSON.parse(call[1]);
expect(actual).toStrictEqual(expected);
});
});
+3
View File
@@ -19,6 +19,9 @@ function writeDefaultConfiguration() {
port: '/dev/dummy',
},
devices: {
'0x18fc2600000d7ae2': {
friendly_name: 'bosch_radiator',
},
'0x000b57fffec6a5b2': {
retain: true,
friendly_name: 'bulb',
+58 -4
View File
@@ -48,6 +48,33 @@ const custom_clusters = {
},
};
const customClusterBTHRA = {
custom_1: {
ID: 513,
attributes: {
attribute_0: {ID: 16391, type: 48, manufacturerCode: 4617},
attribute_1: {ID: 16416, type: 48, manufacturerCode: 4617},
attribute_2: {ID: 16418, type: 48, manufacturerCode: 4617},
attribute_3: {ID: 16448, type: 41, manufacturerCode: 4617},
attribute_4: {ID: 16450, type: 48, manufacturerCode: 4617},
attribute_5: {ID: 16451, type: 48, manufacturerCode: 4617},
},
commands: {},
commandsResponse: {},
},
custom_2: {
ID: 516,
attributes: {
attribute_0: {ID: 16395, type: 32, manufacturerCode: 4617},
attribute_1: {ID: 16441, type: 48, manufacturerCode: 4617},
attribute_2: {ID: 16442, type: 48, manufacturerCode: 4617},
attribute_3: {ID: 16443, type: 48, manufacturerCode: 4617},
},
commands: {},
commandsResponse: {},
},
};
class Endpoint {
constructor(
ID,
@@ -365,6 +392,21 @@ const devices = {
'Mains (single phase)',
'TRADFRI bulb E27 WS opal 980lm',
),
'RBSH-TRV0-ZB-EU': new Device(
'EndDevice',
'0x18fc2600000d7ae2',
35902,
4617, // 0x1209,
[new Endpoint(1, [0, 1, 3, 4, 32, 513, 516, 2821], [10, 25], '0x18fc2600000d7ae2')],
true,
'Battery',
'RBSH-TRV0-ZB-EU',
false,
'BOSCH',
'20231122',
'3.05.09',
customClusterBTHRA,
),
bulb_color: bulb_color,
bulb_2: bulb_2,
bulb_color_2: bulb_color_2,
@@ -815,8 +857,14 @@ const mock = {
events[type] = handler;
},
stop: jest.fn(),
getDevices: jest.fn().mockImplementation(() => {
return Object.values(devices).filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr));
getDevicesIterator: jest.fn().mockImplementation(function* (predicate) {
for (const key in devices) {
const device = devices[key];
if ((returnDevices.length === 0 || returnDevices.includes(device.ieeeAddr)) && (!predicate || predicate(device))) {
yield device;
}
}
}),
getDevicesByType: jest.fn().mockImplementation((type) => {
return Object.values(devices)
@@ -833,8 +881,14 @@ const mock = {
.filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr))
.find((d) => d.networkAddress === networkAddress);
}),
getGroups: jest.fn().mockImplementation((query) => {
return Object.values(groups);
getGroupsIterator: jest.fn().mockImplementation(function* (predicate) {
for (const key in groups) {
const group = groups[key];
if (!predicate || predicate(group)) {
yield group;
}
}
}),
getGroupByID: jest.fn().mockImplementation((groupID) => {
return Object.values(groups).find((d) => d.groupID === groupID);
+5
View File
@@ -4,6 +4,11 @@ const versionHerdsman = require('../node_modules/zigbee-herdsman/package.json').
const versionHerdsmanConverters = require('../node_modules/zigbee-herdsman-converters/package.json').version;
describe('Utils', () => {
it('Object is empty', () => {
expect(utils.objectIsEmpty({})).toBeTruthy();
expect(utils.objectIsEmpty({a: 1})).toBeFalsy();
});
it('Object has properties', () => {
expect(utils.objectHasProperties({a: 1, b: 2, c: 3}, ['a', 'b'])).toBeTruthy();
expect(utils.objectHasProperties({a: 1, b: 2, c: 3}, ['a', 'b', 'd'])).toBeFalsy();
+3 -8
View File
@@ -18,11 +18,6 @@
"resolveJsonModule": true,
"experimentalDecorators": true
},
"include": [
"lib/**/*",
"lib/util/settings.schema.json"
],
"exclude": [
"node_modules"
],
}
"include": ["lib/**/*", "lib/util/settings.schema.json"],
"exclude": ["node_modules"]
}