mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 07:08:56 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
403d3c090d | ||
|
|
9283c4b497 | ||
|
|
0d46354c48 | ||
|
|
13a108ddbc | ||
|
|
5524e881e6 | ||
|
|
a33c29f10a | ||
|
|
fc02071c8f | ||
|
|
06556d44f3 | ||
|
|
13084d22a7 | ||
|
|
14787d3a6a | ||
|
|
e2da2fa8d3 | ||
|
|
6dade8b11f | ||
|
|
557b17ad5b | ||
|
|
64222d231f | ||
|
|
b7fa722109 | ||
|
|
4a1d983168 | ||
|
|
6fa1dabecb | ||
|
|
3b2ba0950e | ||
|
|
9f40936452 | ||
|
|
b2bae6ba94 | ||
|
|
406deaa9da | ||
|
|
65b1e60a3d | ||
|
|
6ed9b26657 | ||
|
|
cf3298bf6f | ||
|
|
00948c0be1 | ||
|
|
ed9de74f91 | ||
|
|
41587211e0 | ||
|
|
b4176d6fee | ||
|
|
ccdf6b3c6d | ||
|
|
8a05077792 | ||
|
|
df7772bfe0 | ||
|
|
6ae9970efd | ||
|
|
b92072fc6a | ||
|
|
5bdf90df9e | ||
|
|
22980177b6 | ||
|
|
837cbe5f26 | ||
|
|
e95116c773 | ||
|
|
27300ba8da | ||
|
|
801ab9066f | ||
|
|
2a631f3551 | ||
|
|
66fefaa827 | ||
|
|
c986fd6c66 | ||
|
|
29f436ab95 | ||
|
|
d827d954ed | ||
|
|
8ae2c4d42d | ||
|
|
9027c41fe8 | ||
|
|
a5748d620b | ||
|
|
d1734feb21 | ||
|
|
00b5e983d9 | ||
|
|
efd3d3dfb0 | ||
|
|
aeba93d0a3 | ||
|
|
7ca456d20e | ||
|
|
13ac8a0f53 | ||
|
|
fe6cacd87e | ||
|
|
00923685d2 | ||
|
|
bd7dec0f11 | ||
|
|
9bb21f486c | ||
|
|
d0f5733db3 | ||
|
|
5b610afa8f | ||
|
|
2cb8a8f5a5 | ||
|
|
3217332e46 | ||
|
|
a25ef81cf4 | ||
|
|
eac11a6439 | ||
|
|
a330830cde | ||
|
|
e132316ac4 | ||
|
|
3dc9a05b0c | ||
|
|
7cc5209af3 | ||
|
|
02dc8a7b87 | ||
|
|
3a573da83c | ||
|
|
26718eefea | ||
|
|
115b187c1a | ||
|
|
fd65b9a108 | ||
|
|
d9ba4524e4 | ||
|
|
5fcf295ab8 | ||
|
|
b7d9d9e142 | ||
|
|
1bf3032b98 | ||
|
|
85453091e7 | ||
|
|
26b1613948 | ||
|
|
c32c411f88 | ||
|
|
cda8927dc7 | ||
|
|
8c71afd93c | ||
|
|
712b920abb | ||
|
|
f795399e10 | ||
|
|
541221bbbd | ||
|
|
24c9159e19 | ||
|
|
5d05ebbfd2 | ||
|
|
bd78f56d5f | ||
|
|
af7467bf62 | ||
|
|
e3fa0bfb65 | ||
|
|
00661fac4b | ||
|
|
2fbb73a009 | ||
|
|
8397f47649 | ||
|
|
fe87b5dec7 | ||
|
|
2916d7ee52 | ||
|
|
eee1da620f | ||
|
|
5663afb27e | ||
|
|
1bf385c5ff | ||
|
|
6b9247c84d | ||
|
|
6cb62e53cf | ||
|
|
f97ad2e951 | ||
|
|
e917802f5f | ||
|
|
4210edf60e | ||
|
|
addc2385b6 | ||
|
|
2dbbed14f0 | ||
|
|
5c42883779 |
+53
-68
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
package-lock.json
|
||||
CHANGELOG.md
|
||||
+1
-1
@@ -6,4 +6,4 @@
|
||||
"bracketSpacing": false,
|
||||
"endOfLine": "lf",
|
||||
"tabWidth": 4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "1.39.0"
|
||||
".": "1.40.1"
|
||||
}
|
||||
|
||||
+723
-648
File diff suppressed because it is too large
Load Diff
+16
-17
@@ -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
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
### 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
@@ -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}]],
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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()) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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', {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)`);
|
||||
|
||||
Vendored
+1
-1
@@ -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 = {
|
||||
|
||||
@@ -106,6 +106,7 @@ class Logger {
|
||||
|
||||
const options: KeyValue = {
|
||||
app_name: 'Zigbee2MQTT',
|
||||
format: winston.format.printf((info) => info.message),
|
||||
...settings.get().advanced.log_syslog,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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> {
|
||||
|
||||
Generated
+10476
-9764
File diff suppressed because it is too large
Load Diff
+114
-114
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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}}));
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,9 @@ function writeDefaultConfiguration() {
|
||||
port: '/dev/dummy',
|
||||
},
|
||||
devices: {
|
||||
'0x18fc2600000d7ae2': {
|
||||
friendly_name: 'bosch_radiator',
|
||||
},
|
||||
'0x000b57fffec6a5b2': {
|
||||
retain: true,
|
||||
friendly_name: 'bulb',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user