mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 15:18:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
414c51f5b2 | ||
|
|
0c42067006 | ||
|
|
5741904e75 | ||
|
|
c60fdfd39b | ||
|
|
a7682498ef | ||
|
|
c6a3b90c2f | ||
|
|
ebd35052c0 | ||
|
|
2849f30417 | ||
|
|
e8526b7b58 | ||
|
|
3166d37096 | ||
|
|
2d2c01981e | ||
|
|
58b702e8ef | ||
|
|
6efa20fa1d | ||
|
|
bf72347468 | ||
|
|
73a6ad4cf8 | ||
|
|
1e75dd5628 | ||
|
|
bc92bd73e6 | ||
|
|
8756367666 | ||
|
|
79e5f0844a | ||
|
|
b0585172f4 | ||
|
|
89996835ed | ||
|
|
4f2a1e55e5 | ||
|
|
d488563f2a | ||
|
|
4310171abc | ||
|
|
36d7b4b022 | ||
|
|
50a6ed14d4 | ||
|
|
ee623c2484 | ||
|
|
2e6588719b | ||
|
|
fb35b52ca6 | ||
|
|
3306b63299 | ||
|
|
7a4ecadb6a | ||
|
|
511f3b09af | ||
|
|
828982d36c | ||
|
|
61bd95f8b8 | ||
|
|
74cff6a983 | ||
|
|
fdf7d10a41 | ||
|
|
1145e4b81b | ||
|
|
93f01d545e | ||
|
|
30e7385cac | ||
|
|
513314963c | ||
|
|
3c0cd8ebfa | ||
|
|
5bedc044d6 | ||
|
|
2a8a98f9a3 | ||
|
|
03f24a63d5 | ||
|
|
53003b3d0d | ||
|
|
cd5dd39971 | ||
|
|
41b150af82 | ||
|
|
3ba1f8cad0 | ||
|
|
f9aa525048 | ||
|
|
8b4be3d6ad | ||
|
|
0efe869fcb | ||
|
|
18cfbae9ca | ||
|
|
7625d261a5 | ||
|
|
6d8eea2142 | ||
|
|
2a9d22e2b3 | ||
|
|
656446c828 | ||
|
|
f1ffc1d265 | ||
|
|
2d1df8f0cb | ||
|
|
253e985388 | ||
|
|
1dd1163288 | ||
|
|
bb5627d33b | ||
|
|
38f42bb3c8 | ||
|
|
5f37145a02 | ||
|
|
a56061b36d | ||
|
|
b890b1bcb7 | ||
|
|
aa84a140e1 | ||
|
|
d875b297ad | ||
|
|
1c6ca6519c | ||
|
|
900c9f302e | ||
|
|
ea04021438 | ||
|
|
5a624ae188 | ||
|
|
42569d6c39 | ||
|
|
46e06dbbee | ||
|
|
a282221e40 | ||
|
|
bb22216d09 | ||
|
|
28cb8b08fb | ||
|
|
594a463f8b | ||
|
|
9c1a15465d | ||
|
|
22836f224d | ||
|
|
aafd937f8a | ||
|
|
d0195d0ff9 | ||
|
|
7592c627c2 | ||
|
|
98a57b4540 | ||
|
|
70b9d29ddf | ||
|
|
ac16d7ce69 | ||
|
|
d4f560d198 | ||
|
|
4136b1e539 | ||
|
|
b4af8329e7 | ||
|
|
ac0aca59a3 | ||
|
|
bc8274b9fd | ||
|
|
e5321d3f20 | ||
|
|
7a81a04b2f | ||
|
|
3b3d886121 | ||
|
|
48b790cbf3 | ||
|
|
9c93103fb7 | ||
|
|
33b35c1e61 | ||
|
|
073c7aedf7 | ||
|
|
e789514777 | ||
|
|
fd762fcf10 | ||
|
|
45b49bac0a | ||
|
|
055fba259a | ||
|
|
b206dc910f | ||
|
|
70db18f3ab | ||
|
|
cb2ef51df8 | ||
|
|
68e5b39172 | ||
|
|
b17ea2e4d9 | ||
|
|
41f9361922 | ||
|
|
dae66fd3fc | ||
|
|
d6ea54b8df | ||
|
|
c47b9c7c9b |
@@ -1,46 +0,0 @@
|
||||
<!--
|
||||
Before submitting an issue make sure you've searched for a similar issue and read the documentation: https://www.zigbee2mqtt.io/.
|
||||
|
||||
Rules (don't ignore these, your issue will be closed without further notice):
|
||||
- English only
|
||||
- Make sure you are on the latest Zigbee2MQTT version
|
||||
- Provide a clear description of the problem
|
||||
- DON'T copy logs directly here, post a link to https://hastebin.com/ or https://pastebin.com/.
|
||||
- Make sure you are running the latest firmware https://github.com/koenkk/z-stack-firmware.
|
||||
|
||||
Did you read the FAQ?
|
||||
- https://www.zigbee2mqtt.io/guide/faq
|
||||
|
||||
Zigbee2MQTT fails to start?
|
||||
- https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start.html
|
||||
|
||||
Having issues when using a CC2531?
|
||||
- Make sure the CC2531 is connected through a USB extension cable
|
||||
- Try the source routing firmware: https://github.com/Koenkk/Z-Stack-firmware/tree/master/coordinator/Z-Stack_Home_1.2/bin/source_routing
|
||||
- With larger networks (30/40+) devices, your CC2531 might not be powerful enough. This will cause weird issues, in this case it's advised to use a more powerful adapter: https://www.zigbee2mqtt.io/guide/adapters/
|
||||
|
||||
Unsupported device?
|
||||
- https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html
|
||||
|
||||
Device does not pair or interview fails?
|
||||
- https://www.zigbee2mqtt.io/guide/faq/#why-does-my-device-not-or-fail-to-pair
|
||||
|
||||
Bug report?
|
||||
- If applicable, provide steps how to reproduce the problem.
|
||||
- Provide the herdsman debug logging: https://www.zigbee2mqtt.io/guide/usage/debug.html
|
||||
|
||||
-->
|
||||
|
||||
<!--Start your bug report below this line-->
|
||||
|
||||
# Bug Report
|
||||
## What happened
|
||||
|
||||
## What did you expect to happen
|
||||
|
||||
## How to reproduce it (minimal and precise)
|
||||
|
||||
## Debug Info
|
||||
Zigbee2MQTT version:
|
||||
Adapter hardware: CC2531, CC2530, CC26X2R1 or CC1352P-2
|
||||
Adapter firmware version:
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Is your feature request related to a problem? Please describe
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
## Describe the solution you'd like
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
## Describe alternatives you've considered
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
## Additional context
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
title: "[Feature request]: "
|
||||
labels: [feature request]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**IMPORTANT:** Before submitting:
|
||||
- Is your feature request related to the fronted? 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. Ex. 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,21 +0,0 @@
|
||||
---
|
||||
name: New device support request
|
||||
about: Request support for a new device
|
||||
title: ''
|
||||
labels: new device support
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
VERY IMPORTANT: Before submitting a request, first (try to) follow: https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html
|
||||
|
||||
Dumping just the info about this device without actually attemping adding support yourself will likely lead into this issue being ignored.
|
||||
Clearly indicate what you tried and where you got stuck.
|
||||
-->
|
||||
|
||||
## Information about the device + link
|
||||
..
|
||||
|
||||
## data/database.db entry of the device
|
||||
..
|
||||
@@ -0,0 +1,54 @@
|
||||
name: New device support request
|
||||
description: Request support for a new device
|
||||
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](https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html)
|
||||
- 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 converter
|
||||
description: In case you tried to create an external converter, put it below
|
||||
render: shell
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Lights
|
||||
In case this device is a light, fill in the fields below:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Supported color modes
|
||||
placeholder: color temperature, color (rgb)
|
||||
- type: input
|
||||
attributes:
|
||||
label: Color temperature range
|
||||
description: In case this light supports color temperature, provide the color temperature range. [How to do this](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html#_3-1-retrieving-color-temperature-range-only-required-for-lights-which-support-color-temperature). This currently requires the latest [dev branch](https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html).
|
||||
placeholder: '{"colorTempPhysicalMin":153,"colorTempPhysicalMax":454}'
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
name: Problem report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: problem
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Before submitting an issue make sure you've searched for a similar issue and read the documentation: https://www.zigbee2mqtt.io/.
|
||||
|
||||
Rules (don't ignore these, your issue will be closed without further notice):
|
||||
- English only
|
||||
- Make sure you are on the latest Zigbee2MQTT version
|
||||
- Provide a clear description of the problem
|
||||
- DON'T copy logs directly here, post a link to https://hastebin.com/ or https://pastebin.com/.
|
||||
- Make sure you are running the latest firmware https://github.com/koenkk/z-stack-firmware.
|
||||
|
||||
Did you read the FAQ?
|
||||
- https://www.zigbee2mqtt.io/guide/faq/
|
||||
|
||||
Zigbee2MQTT fails to start?
|
||||
- https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start.html
|
||||
|
||||
Having issues when using a CC2531?
|
||||
- Make sure the CC2531 is connected through a USB extension cable
|
||||
- Try the source routing firmware: https://github.com/Koenkk/Z-Stack-firmware/tree/master/coordinator/Z-Stack_Home_1.2/bin/source_routing
|
||||
- With larger networks (30/40+) devices, your CC2531 might not be powerful enough. This will cause weird issues, in this case it's advised to use a more powerful adapter: https://www.zigbee2mqtt.io/guide/adapters/
|
||||
|
||||
Unsupported device?
|
||||
- https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html
|
||||
|
||||
Device does not pair or interview fails?
|
||||
- https://www.zigbee2mqtt.io/guide/faq/#why-does-my-device-not-or-fail-to-pair
|
||||
|
||||
Bug report?
|
||||
- If applicable, provide steps how to reproduce the problem.
|
||||
- Provide the herdsman debug logging: https://www.zigbee2mqtt.io/guide/usage/debug.html
|
||||
|
||||
-->
|
||||
|
||||
<!--Start your bug report below this line-->
|
||||
|
||||
## What happened
|
||||
|
||||
## What did you expect to happen
|
||||
|
||||
## How to reproduce it (minimal and precise)
|
||||
|
||||
## Debug info
|
||||
Zigbee2MQTT version:
|
||||
Adapter hardware: CC2531, CC2530, CC26X2R1, CC1352P-2, Conbee II
|
||||
Adapter firmware version:
|
||||
@@ -0,0 +1,68 @@
|
||||
name: Problem report
|
||||
description: Create a report to help us improve
|
||||
title: "[Problem]: "
|
||||
labels: [problem]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**IMPORTANT:** Before submitting:
|
||||
- You read the [FAQ](https://www.zigbee2mqtt.io/guide/faq/)
|
||||
- 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 fronted? 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: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: How to reproduce it (minimal and precise)
|
||||
placeholder: First do this, than this..
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: z2m_version
|
||||
attributes:
|
||||
label: Zigbee2MQTT version
|
||||
description: Can be found in the frontend -> settings -> about -> Zigbee2MQTT version
|
||||
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
|
||||
placeholder: Electrolama zig-a-zig-ah! (zzh!), Slaesh's CC2652RB stick, ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Debug log
|
||||
description: After enabling [debug logging](https://www.zigbee2mqtt.io/guide/usage/debug.html) 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: true
|
||||
@@ -1 +0,0 @@
|
||||
preferredBranch: dev
|
||||
+1
-1
@@ -4,5 +4,5 @@ Everybody is invited and welcome to contribute to Zigbee2MQTT. Zigbee2MQTT is wr
|
||||
|
||||
- Pull requests are always created against the [**dev**](https://github.com/Koenkk/zigbee2mqtt/tree/dev) branch.
|
||||
- Easiest way to start developing Zigbee2MQTT is by setting up a development environment (aka bare-metal installation). You can follow this [guide](https://www.zigbee2mqtt.io/guide/installation/01_linux.html) to do this.
|
||||
- You can run the tests locally by executing `npm test`. Zigbee2MQTT enforces 100% code coverage, in case you add new code check if you 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`.
|
||||
- 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 devices 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).
|
||||
|
||||
@@ -67,7 +67,7 @@ Zigbee2MQTT integrates well with (almost) every home automation solution because
|
||||

|
||||
|
||||
### 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 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.
|
||||
|
||||
+4
-4
@@ -254,15 +254,15 @@ class Controller {
|
||||
message.linkquality = entity.zh.linkquality;
|
||||
}
|
||||
|
||||
for (const extension of this.extensions) {
|
||||
extension.adjustMessageBeforePublish?.(entity, message);
|
||||
}
|
||||
|
||||
// filter mqtt message attributes
|
||||
if (entity.settings.filtered_attributes) {
|
||||
entity.settings.filtered_attributes.forEach((a) => delete message[a]);
|
||||
}
|
||||
|
||||
for (const extension of this.extensions) {
|
||||
extension.adjustMessageBeforePublish?.(entity, message);
|
||||
}
|
||||
|
||||
if (Object.entries(message).length) {
|
||||
const output = settings.get().experimental.output;
|
||||
if (output === 'attribute_and_json' || output === 'json') {
|
||||
|
||||
+11
-4
@@ -77,6 +77,13 @@ export default class EventBus {
|
||||
this.on('deviceJoined', callback, key);
|
||||
}
|
||||
|
||||
public emitEntityOptionsChanged(data: eventdata.EntityOptionsChanged): void {
|
||||
this.emitter.emit('entityOptionsChanged', data);
|
||||
}
|
||||
public onEntityOptionsChanged(key: ListenerKey, callback: (data: eventdata.EntityOptionsChanged) => void): void {
|
||||
this.on('entityOptionsChanged', callback, key);
|
||||
}
|
||||
|
||||
public emitDeviceLeave(data: eventdata.DeviceLeave): void {
|
||||
this.emitter.emit('deviceLeave', data);
|
||||
}
|
||||
@@ -133,11 +140,11 @@ export default class EventBus {
|
||||
this.on('scenesChanged', callback, key);
|
||||
}
|
||||
|
||||
public emitReportingDisabled(data: eventdata.ReportingDisabled): void {
|
||||
this.emitter.emit('reportingDisabled', data);
|
||||
public emitReconfigure(data: eventdata.Reconfigure): void {
|
||||
this.emitter.emit('reconfigure', data);
|
||||
}
|
||||
public onReportingDisabled(key: ListenerKey, callback: (data: eventdata.ReportingDisabled) => void): void {
|
||||
this.on('reportingDisabled', callback, key);
|
||||
public onReconfigure(key: ListenerKey, callback: (data: eventdata.Reconfigure) => void): void {
|
||||
this.on('reconfigure', callback, key);
|
||||
}
|
||||
|
||||
public emitStateChange(data: eventdata.StateChange): void {
|
||||
|
||||
@@ -95,8 +95,6 @@ export default class Availability extends Extension {
|
||||
}
|
||||
|
||||
override async start(): Promise<void> {
|
||||
logger.warn('Using experimental new availability feature');
|
||||
|
||||
this.eventBus.onDeviceRenamed(this, (data) => this.publishAvailability(data.device, false, true));
|
||||
this.eventBus.onDeviceRemoved(this, (data) => clearTimeout(this.timers[data.ieeeAddr]));
|
||||
this.eventBus.onDeviceLeave(this, (data) => clearTimeout(this.timers[data.ieeeAddr]));
|
||||
@@ -167,18 +165,17 @@ export default class Availability extends Extension {
|
||||
* device can send multiple times after each other.
|
||||
*/
|
||||
if (device.definition && !device.zh.interviewing && !this.retrieveStateDebouncers[device.ieeeAddr]) {
|
||||
this.retrieveStateDebouncers[device.ieeeAddr] = debounce(async () => {
|
||||
try {
|
||||
logger.debug(`Retrieving state of '${device.name}' after reconnect`);
|
||||
// Color and color temperature converters do both, only needs to be called once.
|
||||
const keySet = [['state'], ['brightness'], ['color', 'color_temp']];
|
||||
for (const keys of keySet) {
|
||||
const converter = device.definition.toZigbee.find((c) => c.key.find((k) => keys.includes(k)));
|
||||
await converter?.convertGet?.(device.endpoint(), keys[0],
|
||||
{message: this.state.get(device) || {}, mapped: device.definition});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to read state of '${device.name}' after reconnect (${error.message})`);
|
||||
this.retrieveStateDebouncers[device.ieeeAddr] = debounce(() => {
|
||||
logger.debug(`Retrieving state of '${device.name}' after reconnect`);
|
||||
// Color and color temperature converters do both, only needs to be called once.
|
||||
const keySet = [['state'], ['brightness'], ['color', 'color_temp']];
|
||||
for (const keys of keySet) {
|
||||
const converter = device.definition.toZigbee.find((c) => c.key.find((k) => keys.includes(k)));
|
||||
converter?.convertGet?.(device.endpoint(), keys[0],
|
||||
{message: this.state.get(device) || {}, mapped: device.definition})
|
||||
.catch((e) => {
|
||||
logger.error(`Failed to read state of '${device.name}' after reconnect (${e.message})`);
|
||||
});
|
||||
}
|
||||
}, utils.seconds(2));
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ const pollOnMessage: PollOnMessage = [
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.ATMEL,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.TELINK,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -129,6 +130,7 @@ const pollOnMessage: PollOnMessage = [
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.ATMEL,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.TELINK,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -155,6 +157,7 @@ const pollOnMessage: PollOnMessage = [
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.ATMEL,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.TELINK,
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -417,7 +420,7 @@ export default class Bind extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
this.eventBus.emitReportingDisabled({device});
|
||||
this.eventBus.emitReconfigure({device});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -399,6 +399,7 @@ export default class Bridge extends Extension {
|
||||
|
||||
logger.info(`Changed config for ${entityType} ${ID}`);
|
||||
|
||||
this.eventBus.emitEntityOptionsChanged({from: oldOptions, to: newOptions, entity});
|
||||
return utils.getResponse(message, {from: oldOptions, to: newOptions, id: ID}, null);
|
||||
}
|
||||
|
||||
@@ -565,7 +566,10 @@ export default class Bridge extends Extension {
|
||||
const payload = {
|
||||
version: this.zigbee2mqttVersion.version,
|
||||
commit: this.zigbee2mqttVersion.commitHash,
|
||||
coordinator: this.coordinatorVersion,
|
||||
coordinator: {
|
||||
ieee_address: this.zigbee.firstCoordinatorEndpoint().getDevice().ieeeAddr,
|
||||
...this.coordinatorVersion,
|
||||
},
|
||||
network: utils.toSnakeCase(await this.zigbee.getNetworkParameters()),
|
||||
log_level: logger.getLevel(),
|
||||
permit_join: this.zigbee.getPermitJoin(),
|
||||
@@ -688,7 +692,7 @@ export default class Bridge extends Extension {
|
||||
model: device.definition.model,
|
||||
vendor: device.definition.vendor,
|
||||
description: device.definition.description,
|
||||
exposes: device.definition.exposes,
|
||||
exposes: device.exposes(),
|
||||
supports_ota: !!device.definition.ota,
|
||||
options: device.definition.options,
|
||||
icon,
|
||||
|
||||
@@ -16,7 +16,7 @@ export default class Configure extends Extension {
|
||||
private topic = `${settings.get().mqtt.base_topic}/bridge/request/device/configure`;
|
||||
private legacyTopic = `${settings.get().mqtt.base_topic}/bridge/configure`;
|
||||
|
||||
@bind private async onReportingDisabled(data: eventdata.ReportingDisabled): Promise<void> {
|
||||
@bind private async onReconfigure(data: eventdata.Reconfigure): Promise<void> {
|
||||
// Disabling reporting unbinds some cluster which could be bound by configure, re-setup.
|
||||
if (data.device.zh.meta?.hasOwnProperty('configured')) {
|
||||
delete data.device.zh.meta.configured;
|
||||
@@ -79,7 +79,7 @@ export default class Configure extends Extension {
|
||||
this.eventBus.onDeviceInterview(this, (data) => this.configure(data.device, 'zigbee_event'));
|
||||
this.eventBus.onLastSeenChanged(this, (data) => this.configure(data.device, 'zigbee_event'));
|
||||
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
|
||||
this.eventBus.onReportingDisabled(this, this.onReportingDisabled);
|
||||
this.eventBus.onReconfigure(this, this.onReconfigure);
|
||||
}
|
||||
|
||||
private async configure(device: Device, event: 'started' | 'zigbee_event' | 'reporting_disabled' | 'mqtt_message',
|
||||
|
||||
+11
-10
@@ -13,17 +13,17 @@ const topicRegex =
|
||||
const legacyTopicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(.+)/(remove|add|remove_all)$`);
|
||||
const legacyTopicRegexRemoveAll = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/remove_all$`);
|
||||
|
||||
const stateProperties: {[s: string]: (value: string, definition: zhc.Definition) => boolean} = {
|
||||
const stateProperties: {[s: string]: (value: string, exposes: zhc.DefinitionExpose[]) => boolean} = {
|
||||
'state': () => true,
|
||||
'brightness': (value, definition) =>
|
||||
!!definition.exposes.find((e) => e.type === 'light' && e.features.find((f) => f.name === 'brightness')),
|
||||
'color_temp': (value, definition) =>
|
||||
!!definition.exposes.find((e) => e.type === 'light' && e.features.find((f) => f.name === 'color_temp')),
|
||||
'color': (value, definition) =>
|
||||
!!definition.exposes.find((e) => e.type === 'light' &&
|
||||
'brightness': (value, exposes) =>
|
||||
!!exposes.find((e) => e.type === 'light' && e.features.find((f) => f.name === 'brightness')),
|
||||
'color_temp': (value, exposes) =>
|
||||
!!exposes.find((e) => e.type === 'light' && e.features.find((f) => f.name === 'color_temp')),
|
||||
'color': (value, exposes) =>
|
||||
!!exposes.find((e) => e.type === 'light' &&
|
||||
e.features.find((f) => f.name === 'color_xy' || f.name === 'color_hs')),
|
||||
'color_mode': (value, definition) =>
|
||||
!!definition.exposes.find((e) => e.type === 'light' && (
|
||||
'color_mode': (value, exposes) =>
|
||||
!!exposes.find((e) => e.type === 'light' && (
|
||||
(e.features.find((f) => f.name === `color_${value}`)) ||
|
||||
(value === 'color_temp' && e.features.find((f) => f.name === 'color_temp')) )),
|
||||
};
|
||||
@@ -143,9 +143,10 @@ export default class Groups extends Extension {
|
||||
const groupsToPublish: Set<Group> = new Set();
|
||||
for (const member of entity.zh.members) {
|
||||
const device = this.zigbee.resolveEntity(member.getDevice()) as Device;
|
||||
const exposes = device.exposes();
|
||||
const memberPayload: KeyValue = {};
|
||||
Object.keys(payload).forEach((key) => {
|
||||
if (stateProperties[key](payload[key], device.definition)) {
|
||||
if (stateProperties[key](payload[key], exposes)) {
|
||||
memberPayload[key] = payload[key];
|
||||
}
|
||||
});
|
||||
|
||||
+111
-108
@@ -2,18 +2,19 @@ import * as settings from '../util/settings';
|
||||
import logger from '../util/logger';
|
||||
import utils from '../util/utils';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import zigbeeHerdsmanConverters from 'zigbee-herdsman-converters';
|
||||
import assert from 'assert';
|
||||
import Extension from './extension';
|
||||
import bind from 'bind-decorator';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
interface DiscoveryEntry {mockProperties: string[], type: string, object_id: string, discovery_payload: KeyValue}
|
||||
interface MockProperty {property: string, value: KeyValue | string}
|
||||
|
||||
const sensorClick = {
|
||||
// eslint-disable-next-line camelcase
|
||||
interface DiscoveryEntry {mockProperties: MockProperty[], type: string, object_id: string, discovery_payload: KeyValue}
|
||||
|
||||
const sensorClick: DiscoveryEntry = {
|
||||
type: 'sensor',
|
||||
object_id: 'click',
|
||||
mockProperties: ['click'],
|
||||
mockProperties: [{property: 'click', value: null}],
|
||||
discovery_payload: {
|
||||
icon: 'mdi:toggle-switch',
|
||||
value_template: '{{ value_json.click }}',
|
||||
@@ -25,6 +26,30 @@ const ACCESS_SET = 0b010;
|
||||
const groupSupportedTypes = ['light', 'switch', 'lock', 'cover'];
|
||||
const defaultStatusTopic = 'homeassistant/status';
|
||||
|
||||
const legacyMapping = [
|
||||
{
|
||||
models: ['WXKG01LM', 'HS1EB/HS1EB-E', 'ICZB-KPD14S', 'TERNCY-SD01', 'TERNCY-PP01', 'ICZB-KPD18S',
|
||||
'E1766', 'ZWallRemote0', 'ptvo.switch', '2AJZ4KPKEY', 'ZGRC-KEY-013', 'HGZB-02S', 'HGZB-045',
|
||||
'HGZB-1S', 'AV2010/34', 'IM6001-BTP01', 'WXKG11LM', 'WXKG03LM', 'WXKG02LM_rev1', 'WXKG02LM_rev2',
|
||||
'QBKG04LM', 'QBKG03LM', 'QBKG11LM', 'QBKG21LM', 'QBKG22LM', 'WXKG12LM', 'QBKG12LM',
|
||||
'E1743'],
|
||||
discovery: sensorClick,
|
||||
},
|
||||
{
|
||||
models: ['ICTC-G-1'],
|
||||
discovery: {
|
||||
type: 'sensor',
|
||||
mockProperties: [{property: 'brightness', value: null}],
|
||||
object_id: 'brightness',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'brightness',
|
||||
icon: 'mdi:brightness-5',
|
||||
value_template: '{{ value_json.brightness }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const featurePropertyWithoutEndpoint = (feature: zhc.DefinitionExposeFeature): string => {
|
||||
if (feature.endpoint) {
|
||||
return feature.property.slice(0, -1 + -1 * feature.endpoint.length);
|
||||
@@ -37,8 +62,8 @@ const featurePropertyWithoutEndpoint = (feature: zhc.DefinitionExposeFeature): s
|
||||
* This extensions handles integration with HomeAssistant
|
||||
*/
|
||||
export default class HomeAssistant extends Extension {
|
||||
private discovered: {[s: string]: {topics: Set<string>, mockProperties: Set<string>}} = {};
|
||||
private mapping: {[s: string]: DiscoveryEntry[]} = {};
|
||||
private discovered: {[s: string]:
|
||||
{topics: Set<string>, mockProperties: Set<MockProperty>, objectIDs: Set<string>}} = {};
|
||||
private discoveredTriggers : {[s: string]: Set<string>}= {};
|
||||
private discoveryTopic = settings.get().advanced.homeassistant_discovery_topic;
|
||||
private statusTopic = settings.get().advanced.homeassistant_status_topic;
|
||||
@@ -60,7 +85,6 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
this.zigbee2MQTTVersion = (await utils.getZigbee2MQTTVersion(false)).version;
|
||||
this.populateMapping();
|
||||
|
||||
this.eventBus.onDeviceRemoved(this, this.onDeviceRemoved);
|
||||
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
|
||||
@@ -83,7 +107,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
private exposeToConfig(exposes: zhc.DefinitionExpose[], entityType: 'device' | 'group',
|
||||
definition?: zhc.Definition): DiscoveryEntry[] {
|
||||
definition?: zhc.Definition, definitionExposes?: zhc.DefinitionExpose[]): DiscoveryEntry[] {
|
||||
// For groups an array of exposes (of the same type) is passed, this is to determine e.g. what features
|
||||
// to use for a bulb (e.g. color_xy/color_temp)
|
||||
assert(entityType === 'group' || exposes.length === 1, 'Multiple exposes for device not allowed');
|
||||
@@ -107,7 +131,7 @@ export default class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'light',
|
||||
object_id: endpoint ? `light_${endpoint}` : 'light',
|
||||
mockProperties: [state.property],
|
||||
mockProperties: [{property: state.property, value: null}],
|
||||
discovery_payload: {
|
||||
brightness: !!hasBrightness,
|
||||
schema: 'json',
|
||||
@@ -138,7 +162,7 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntry.discovery_payload.min_mireds = min;
|
||||
}
|
||||
|
||||
const effect = definition && definition.exposes.find((e) => e.type === 'enum' && e.name === 'effect');
|
||||
const effect = definitionExposes?.find((e) => e.type === 'enum' && e.name === 'effect');
|
||||
if (effect) {
|
||||
discoveryEntry.discovery_payload.effect = true;
|
||||
discoveryEntry.discovery_payload.effect_list = effect.values;
|
||||
@@ -151,7 +175,7 @@ export default class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'switch',
|
||||
object_id: endpoint ? `switch_${endpoint}` : 'switch',
|
||||
mockProperties: [property],
|
||||
mockProperties: [{property: property, value: null}],
|
||||
discovery_payload: {
|
||||
payload_off: state.value_off,
|
||||
payload_on: state.value_on,
|
||||
@@ -216,7 +240,7 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
const state = firstExpose.features.find((f) => f.name === 'running_state');
|
||||
if (state) {
|
||||
discoveryEntry.mockProperties.push(state.property);
|
||||
discoveryEntry.mockProperties.push({property: state.property, value: null});
|
||||
discoveryEntry.discovery_payload.action_topic = true;
|
||||
discoveryEntry.discovery_payload.action_template = `{% set values = ` +
|
||||
`{None:None,'idle':'off','heat':'heating','cool':'cooling','fan_only':'fan'}` +
|
||||
@@ -271,7 +295,7 @@ export default class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'number',
|
||||
object_id: endpoint ? `${tempCalibration.name}_${endpoint}` : `${tempCalibration.name}`,
|
||||
mockProperties: [tempCalibration.property],
|
||||
mockProperties: [{property: tempCalibration.property, value: null}],
|
||||
discovery_payload: {
|
||||
value_template: `{{ value_json.${tempCalibration.property} }}`,
|
||||
command_topic: true,
|
||||
@@ -290,10 +314,10 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
const piHeatingDemand = firstExpose.features.find((f) => f.name === 'pi_heating_demand');
|
||||
if (piHeatingDemand) {
|
||||
const discoveryEntry = {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'sensor',
|
||||
object_id: endpoint ? `${piHeatingDemand.name}_${endpoint}` : `${piHeatingDemand.name}`,
|
||||
mockProperties: [piHeatingDemand.property],
|
||||
mockProperties: [{property: piHeatingDemand.property, value: null}],
|
||||
discovery_payload: {
|
||||
value_template: `{{ value_json.${piHeatingDemand.property} }}`,
|
||||
...(piHeatingDemand.unit && {unit_of_measurement: piHeatingDemand.unit}),
|
||||
@@ -313,7 +337,7 @@ export default class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'lock',
|
||||
object_id: 'lock',
|
||||
mockProperties: [state.property],
|
||||
mockProperties: [{property: state.property, value: null}],
|
||||
discovery_payload: {
|
||||
command_topic: true,
|
||||
value_template: `{{ value_json.${state.property} }}`,
|
||||
@@ -396,7 +420,7 @@ export default class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'fan',
|
||||
object_id: 'fan',
|
||||
mockProperties: ['fan_state'],
|
||||
mockProperties: [{property: 'fan_state', value: null}],
|
||||
discovery_payload: {
|
||||
state_topic: true,
|
||||
state_value_template: '{{ value_json.fan_state }}',
|
||||
@@ -461,6 +485,7 @@ export default class HomeAssistant extends Extension {
|
||||
battery_low: {entity_category: 'diagnostic', device_class: 'battery'},
|
||||
button_lock: {entity_category: 'config', icon: 'mdi:lock'},
|
||||
carbon_monoxide: {device_class: 'safety'},
|
||||
card: {entity_category: 'config', icon: 'mdi:clipboard-check'},
|
||||
child_lock: {entity_category: 'config', icon: 'mdi:account-lock'},
|
||||
color_sync: {entity_category: 'config', icon: 'mdi:sync-circle'},
|
||||
consumer_connected: {entity_category: 'diagnostic', device_class: 'connectivity'},
|
||||
@@ -494,7 +519,7 @@ export default class HomeAssistant extends Extension {
|
||||
if (firstExpose.access & ACCESS_SET) {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'switch',
|
||||
mockProperties: [firstExpose.property],
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
object_id: endpoint ?
|
||||
`switch_${firstExpose.name}_${endpoint}` :
|
||||
`switch_${firstExpose.name}`,
|
||||
@@ -512,10 +537,10 @@ export default class HomeAssistant extends Extension {
|
||||
};
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
} else {
|
||||
const discoveryEntry = {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'binary_sensor',
|
||||
object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`,
|
||||
mockProperties: [firstExpose.property],
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
value_template: `{{ value_json.${firstExpose.property} }}`,
|
||||
payload_on: firstExpose.value_on,
|
||||
@@ -630,10 +655,10 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
const allowsSet = firstExpose.access & ACCESS_SET;
|
||||
|
||||
const discoveryEntry = {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'sensor',
|
||||
object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`,
|
||||
mockProperties: [firstExpose.property],
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
value_template: `{{ value_json.${firstExpose.property} }}`,
|
||||
enabled_by_default: !allowsSet,
|
||||
@@ -652,7 +677,7 @@ export default class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'number',
|
||||
object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`,
|
||||
mockProperties: [firstExpose.property],
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
value_template: `{{ value_json.${firstExpose.property} }}`,
|
||||
command_topic: true,
|
||||
@@ -699,7 +724,7 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntries.push({
|
||||
type: 'sensor',
|
||||
object_id: firstExpose.property,
|
||||
mockProperties: [firstExpose.property],
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
value_template: `{{ value_json.${firstExpose.property} }}`,
|
||||
enabled_by_default: !(firstExpose.access & ACCESS_SET),
|
||||
@@ -716,7 +741,7 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntries.push({
|
||||
type: 'select',
|
||||
object_id: firstExpose.property,
|
||||
mockProperties: [firstExpose.property],
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
value_template: `{{ value_json.${firstExpose.property} }}`,
|
||||
state_topic: true,
|
||||
@@ -735,10 +760,10 @@ export default class HomeAssistant extends Extension {
|
||||
action: {icon: 'mdi:gesture-double-tap'},
|
||||
};
|
||||
|
||||
const discoveryEntry = {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'sensor',
|
||||
object_id: firstExpose.property,
|
||||
mockProperties: [firstExpose.property],
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
value_template: `{{ value_json.${firstExpose.property} }}`,
|
||||
...lookup[firstExpose.name],
|
||||
@@ -753,46 +778,6 @@ export default class HomeAssistant extends Extension {
|
||||
return discoveryEntries;
|
||||
}
|
||||
|
||||
private populateMapping(): void {
|
||||
for (const def of zigbeeHerdsmanConverters.definitions) {
|
||||
this.mapping[def.model] = [];
|
||||
|
||||
if (['WXKG01LM', 'HS1EB/HS1EB-E', 'ICZB-KPD14S', 'TERNCY-SD01', 'TERNCY-PP01', 'ICZB-KPD18S',
|
||||
'E1766', 'ZWallRemote0', 'ptvo.switch', '2AJZ4KPKEY', 'ZGRC-KEY-013', 'HGZB-02S', 'HGZB-045',
|
||||
'HGZB-1S', 'AV2010/34', 'IM6001-BTP01', 'WXKG11LM', 'WXKG03LM', 'WXKG02LM_rev1', 'WXKG02LM_rev2',
|
||||
'QBKG04LM', 'QBKG03LM', 'QBKG11LM', 'QBKG21LM', 'QBKG22LM', 'WXKG12LM', 'QBKG12LM',
|
||||
'E1743'].includes(def.model)) {
|
||||
// deprecated
|
||||
this.mapping[def.model].push(sensorClick);
|
||||
}
|
||||
|
||||
if (['ICTC-G-1'].includes(def.model)) {
|
||||
// deprecated
|
||||
this.mapping[def.model].push({
|
||||
type: 'sensor',
|
||||
mockProperties: ['brightness'],
|
||||
object_id: 'brightness',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'brightness',
|
||||
icon: 'mdi:brightness-5',
|
||||
value_template: '{{ value_json.brightness }}',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const expose of def.exposes) {
|
||||
this.mapping[def.model].push(...this.exposeToConfig([expose], 'device', def));
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated in favour of exposes
|
||||
for (const definition of utils.getExternalConvertersDefinitions(settings.get())) {
|
||||
if (definition.hasOwnProperty('homeassistant')) {
|
||||
this.mapping[definition.model] = definition.homeassistant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bind onDeviceRemoved(data: eventdata.DeviceRemoved): void {
|
||||
logger.debug(`Clearing Home Assistant discovery topic for '${data.name}'`);
|
||||
this.discovered[data.ieeeAddr]?.topics.forEach((topic) => {
|
||||
@@ -816,9 +801,9 @@ export default class HomeAssistant extends Extension {
|
||||
* zigbee2mqtt/mydevice/l1.
|
||||
*/
|
||||
const entity = this.zigbee.resolveEntity(data.entity.name);
|
||||
if (entity.isDevice() && this.mapping[entity.definition?.model]) {
|
||||
for (const config of this.mapping[entity.definition.model]) {
|
||||
const match = /light_(.*)/.exec(config['object_id']);
|
||||
if (entity.isDevice() && this.discovered[entity.ieeeAddr]) {
|
||||
for (const objectID of this.discovered[entity.ieeeAddr].objectIDs) {
|
||||
const match = /light_(.*)/.exec(objectID);
|
||||
if (match) {
|
||||
const endpoint = match[1];
|
||||
const endpointRegExp = new RegExp(`(.*)_${endpoint}`);
|
||||
@@ -891,54 +876,75 @@ export default class HomeAssistant extends Extension {
|
||||
private getConfigs(entity: Device | Group): DiscoveryEntry[] {
|
||||
const isDevice = entity.isDevice();
|
||||
/* istanbul ignore next */
|
||||
if (!entity || (isDevice && !entity.definition) ||
|
||||
(isDevice && !this.mapping[entity.definition.model])) return [];
|
||||
if (!entity || (isDevice && !entity.definition)) return [];
|
||||
|
||||
let configs: DiscoveryEntry[];
|
||||
let configs: DiscoveryEntry[] = [];
|
||||
if (isDevice) {
|
||||
configs = this.mapping[entity.definition.model].slice();
|
||||
for (const expose of entity.exposes()) {
|
||||
configs.push(...this.exposeToConfig([expose], 'device', entity.definition, entity.exposes()));
|
||||
}
|
||||
|
||||
for (const mapping of legacyMapping) {
|
||||
if (mapping.models.includes(entity.definition.model)) {
|
||||
configs.push(mapping.discovery);
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated in favour of exposes
|
||||
/* istanbul ignore if */
|
||||
if (entity.definition.hasOwnProperty('homeassistant')) {
|
||||
// @ts-ignore
|
||||
configs.push(entity.definition.homeassistant);
|
||||
}
|
||||
} else { // group
|
||||
const exposesByType: {[s: string]: zhc.DefinitionExpose[]} = {};
|
||||
|
||||
entity.membersDefinitions().forEach((definition) => {
|
||||
for (const expose of definition.exposes.filter((e) => groupSupportedTypes.includes(e.type))) {
|
||||
let key = expose.type;
|
||||
if (['switch', 'lock', 'cover'].includes(expose.type) && expose.endpoint) {
|
||||
// A device can have multiple of these types which have to discovered seperately.
|
||||
// e.g. switch with property state and valve_detection.
|
||||
const state = expose.features.find((f) => f.name === 'state');
|
||||
key += featurePropertyWithoutEndpoint(state);
|
||||
}
|
||||
entity.zh.members.map((e) => this.zigbee.resolveEntity(e.getDevice()) as Device)
|
||||
.filter((d) => d.definition).forEach((device) => {
|
||||
for (const expose of device.exposes().filter((e) => groupSupportedTypes.includes(e.type))) {
|
||||
let key = expose.type;
|
||||
if (['switch', 'lock', 'cover'].includes(expose.type) && expose.endpoint) {
|
||||
// A device can have multiple of these types which have to discovered seperately.
|
||||
// e.g. switch with property state and valve_detection.
|
||||
const state = expose.features.find((f) => f.name === 'state');
|
||||
key += featurePropertyWithoutEndpoint(state);
|
||||
}
|
||||
|
||||
if (!exposesByType[key]) exposesByType[key] = [];
|
||||
exposesByType[key].push(expose);
|
||||
}
|
||||
});
|
||||
if (!exposesByType[key]) exposesByType[key] = [];
|
||||
exposesByType[key].push(expose);
|
||||
}
|
||||
});
|
||||
|
||||
configs = [].concat(...Object.values(exposesByType)
|
||||
.map((exposes) => this.exposeToConfig(exposes, 'group')));
|
||||
}
|
||||
|
||||
if (isDevice && settings.get().advanced.last_seen !== 'disable') {
|
||||
configs.push({
|
||||
const config: DiscoveryEntry = {
|
||||
type: 'sensor',
|
||||
object_id: 'last_seen',
|
||||
mockProperties: ['last_seen'],
|
||||
mockProperties: [{property: 'last_seen', value: null}],
|
||||
discovery_payload: {
|
||||
value_template: '{{ value_json.last_seen }}',
|
||||
icon: 'mdi:clock',
|
||||
enabled_by_default: false,
|
||||
entity_category: 'diagnostic',
|
||||
device_class: 'timestamp',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.last_seen.startsWith('ISO_8601')) {
|
||||
config.discovery_payload.device_class = 'timestamp';
|
||||
}
|
||||
|
||||
configs.push(config);
|
||||
}
|
||||
|
||||
if (isDevice && entity.definition.hasOwnProperty('ota')) {
|
||||
const updateStateSensor: DiscoveryEntry = {
|
||||
type: 'sensor',
|
||||
object_id: 'update_state',
|
||||
mockProperties: [],
|
||||
mockProperties: [{property: 'update', value: {state: null}}],
|
||||
discovery_payload: {
|
||||
icon: 'mdi:update',
|
||||
value_template: `{{ value_json['update']['state'] }}`,
|
||||
@@ -948,10 +954,10 @@ export default class HomeAssistant extends Extension {
|
||||
};
|
||||
|
||||
configs.push(updateStateSensor);
|
||||
const updateAvailableSensor = {
|
||||
const updateAvailableSensor: DiscoveryEntry = {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'update_available',
|
||||
mockProperties: ['update_available'],
|
||||
mockProperties: [{property: 'update_available', value: null}],
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
@@ -1001,13 +1007,12 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
if (entity.isGroup()) {
|
||||
if (!discover || entity.zh.members.length === 0) return;
|
||||
} else if (!discover || !entity.definition || !this.mapping[entity.definition.model] ||
|
||||
entity.zh.interviewing ||
|
||||
} else if (!discover || !entity.definition || entity.zh.interviewing ||
|
||||
(entity.settings.hasOwnProperty('homeassistant') && !entity.settings.homeassistant)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.discovered[discoverKey] = {topics: new Set(), mockProperties: new Set()};
|
||||
this.discovered[discoverKey] = {topics: new Set(), mockProperties: new Set(), objectIDs: new Set()};
|
||||
this.getConfigs(entity).forEach((config) => {
|
||||
const payload = {...config.discovery_payload};
|
||||
const baseTopic = `${settings.get().mqtt.base_topic}/${entity.name}`;
|
||||
@@ -1171,7 +1176,8 @@ export default class HomeAssistant extends Extension {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (['type', 'object_id'].includes(key)) {
|
||||
return;
|
||||
} else if (['number', 'string', 'boolean'].includes(typeof obj[key])) {
|
||||
} else if (['number', 'string', 'boolean'].includes(typeof obj[key]) ||
|
||||
Array.isArray(obj[key])) {
|
||||
payload[key] = obj[key];
|
||||
} else if (obj[key] === null) {
|
||||
delete payload[key];
|
||||
@@ -1193,7 +1199,9 @@ export default class HomeAssistant extends Extension {
|
||||
const topic = this.getDiscoveryTopic(config, entity);
|
||||
this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 0}, this.discoveryTopic, false, false);
|
||||
this.discovered[discoverKey].topics.add(topic);
|
||||
config.mockProperties?.forEach((property) => this.discovered[discoverKey].mockProperties.add(property));
|
||||
this.discovered[discoverKey].objectIDs.add(config.object_id);
|
||||
config.mockProperties?.forEach((mockProperty) =>
|
||||
this.discovered[discoverKey].mockProperties.add(mockProperty));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1296,9 +1304,9 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
override adjustMessageBeforePublish(entity: Device | Group, message: KeyValue): void {
|
||||
const discoverKey = this.getDiscoverKey(entity);
|
||||
this.discovered[discoverKey]?.mockProperties?.forEach((property) => {
|
||||
if (!message.hasOwnProperty(property)) {
|
||||
message[property] = null;
|
||||
this.discovered[discoverKey]?.mockProperties?.forEach((mockProperty) => {
|
||||
if (!message.hasOwnProperty(mockProperty.property)) {
|
||||
message[mockProperty.property] = mockProperty.value;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1362,11 +1370,6 @@ export default class HomeAssistant extends Extension {
|
||||
this.discoveredTriggers[device.ieeeAddr].add(discoveredKey);
|
||||
}
|
||||
|
||||
// Only for homeassistant.test.js
|
||||
_getMapping(): {[s: string]: DiscoveryEntry[]} {
|
||||
return this.mapping;
|
||||
}
|
||||
|
||||
_clearDiscoveredTrigger(): void {
|
||||
this.discoveredTriggers = {};
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ export default class Report extends Extension {
|
||||
device.zh.meta.reporting = reportKey;
|
||||
} else {
|
||||
delete device.zh.meta.reporting;
|
||||
this.eventBus.emitReportingDisabled({device});
|
||||
this.eventBus.emitReconfigure({device});
|
||||
}
|
||||
|
||||
this.eventBus.emitDevicesChanged();
|
||||
|
||||
@@ -267,7 +267,7 @@ export default class NetworkMap extends Extension {
|
||||
model: device.definition.model,
|
||||
vendor: device.definition.vendor,
|
||||
description: device.definition.description,
|
||||
supports: Array.from(new Set((device.definition.exposes).map((e) => {
|
||||
supports: Array.from(new Set((device.exposes()).map((e) => {
|
||||
return e.hasOwnProperty('name') ? e.name :
|
||||
`${e.type} (${e.features.map((f) => f.name).join(', ')})`;
|
||||
}))).join(', '),
|
||||
|
||||
@@ -19,6 +19,13 @@ export default class OnEvent extends Extension {
|
||||
(data) => this.callOnEvent(data.device, 'deviceAnnounce', this.convertData(data)));
|
||||
this.eventBus.onDeviceNetworkAddressChanged(this,
|
||||
(data) => this.callOnEvent(data.device, 'deviceNetworkAddressChanged', this.convertData(data)));
|
||||
this.eventBus.onEntityOptionsChanged(this,
|
||||
(data) => {
|
||||
if (data.entity.isDevice()) {
|
||||
this.callOnEvent(data.entity, 'deviceOptionsChanged', data)
|
||||
.then(() => this.eventBus.emitDevicesChanged());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private convertData(data: KeyValue): KeyValue {
|
||||
|
||||
@@ -92,18 +92,11 @@ export default class OTAUpdate extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
private async readSoftwareBuildIDAndDateCode(device: Device, update: boolean):
|
||||
private async readSoftwareBuildIDAndDateCode(device: Device, sendWhenActive: boolean):
|
||||
Promise<{softwareBuildID: string, dateCode: string}> {
|
||||
try {
|
||||
const endpoint = device.zh.endpoints.find((e) => e.supportsInputCluster('genBasic'));
|
||||
const result = await endpoint.read('genBasic', ['dateCode', 'swBuildId']);
|
||||
|
||||
if (update) {
|
||||
device.zh.softwareBuildID = result.swBuildId;
|
||||
device.zh.dateCode = result.dateCode;
|
||||
device.zh.save();
|
||||
}
|
||||
|
||||
const result = await endpoint.read('genBasic', ['dateCode', 'swBuildId'], {sendWhenActive});
|
||||
return {softwareBuildID: result.swBuildId, dateCode: result.dateCode};
|
||||
} catch (e) {
|
||||
return null;
|
||||
@@ -232,8 +225,9 @@ export default class OTAUpdate extends Extension {
|
||||
|
||||
const from_ = await this.readSoftwareBuildIDAndDateCode(device, false);
|
||||
await device.definition.ota.updateToLatest(device.zh, logger, onProgress);
|
||||
const to = await this.readSoftwareBuildIDAndDateCode(device, true);
|
||||
const to = await this.readSoftwareBuildIDAndDateCode(device, device.zh.type === 'EndDevice');
|
||||
const [fromS, toS] = [stringify(from_), stringify(to)];
|
||||
this.eventBus.emitReconfigure({device});
|
||||
const msg = `Finished update of '${device.name}'` +
|
||||
(to ? `, from '${fromS}' to '${toS}'` : ``);
|
||||
logger.info(msg);
|
||||
|
||||
@@ -10,7 +10,7 @@ import Device from '../model/device';
|
||||
import bind from 'bind-decorator';
|
||||
|
||||
const topicRegex = new RegExp(`^(.+?)(?:/(${utils.endpointNames.join('|')}|\\d+))?/(get|set)(?:/(.+))?`);
|
||||
const propertyEndpointRegex = new RegExp(`^(.*)_(${utils.endpointNames.join('|')}|\\d+)$`);
|
||||
const propertyEndpointRegex = new RegExp(`^(.*)_(${utils.endpointNames.join('|')})$`);
|
||||
const stateValues = ['on', 'off', 'toggle', 'open', 'close', 'stop', 'lock', 'unlock'];
|
||||
const sceneConverterKeys = ['scene_store', 'scene_add', 'scene_remove', 'scene_remove_all'];
|
||||
|
||||
@@ -128,10 +128,6 @@ export default class Publish extends Extension {
|
||||
|
||||
// Get entity details
|
||||
const definition = re instanceof Device ? re.definition : re.membersDefinitions();
|
||||
if (definition == null) {
|
||||
logger.error(`Cannot publish to unsupported device '${re.name}'`);
|
||||
return;
|
||||
}
|
||||
const target = re instanceof Group ? re.zh : re.endpoint(parsedTopic.endpoint);
|
||||
if (target == null) {
|
||||
logger.error(`Device '${re.name}' has no endpoint '${parsedTopic.endpoint}'`);
|
||||
@@ -149,8 +145,11 @@ export default class Publish extends Extension {
|
||||
const c = new Set(definition.map((d) => d.toZigbee).flat());
|
||||
if (c.size == 0) converters = defaultGroupConverters;
|
||||
else converters = Array.from(c);
|
||||
} else {
|
||||
} else if (definition) {
|
||||
converters = definition.toZigbee;
|
||||
} else {
|
||||
converters = [zigbeeHerdsmanConverters.toZigbeeConverters.read,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.write];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +215,7 @@ export default class Publish extends Extension {
|
||||
}
|
||||
|
||||
if (!converter) {
|
||||
logger.error(`No converter available for '${key}' (${message[key]})`);
|
||||
logger.error(`No converter available for '${key}' (${stringify(message[key])})`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,8 @@ export default class Receive extends Extension {
|
||||
if (converters.length == 0 && !ignoreClusters.includes(data.cluster)) {
|
||||
logger.debug(`No converter available for '${data.device.definition.model}' with ` +
|
||||
`cluster '${data.cluster}' and type '${data.type}' and data '${stringify(data.data)}'`);
|
||||
utils.publishLastSeen({device: data.device, reason: 'messageEmitted'},
|
||||
settings.get(), true, this.publishEntityState);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,15 @@ export default class Device {
|
||||
this.zh = device;
|
||||
}
|
||||
|
||||
exposes(): zhc.DefinitionExpose[] {
|
||||
/* istanbul ignore if */
|
||||
if (typeof this.definition.exposes == 'function') {
|
||||
return this.definition.exposes(this.zh, this.settings);
|
||||
} else {
|
||||
return this.definition.exposes;
|
||||
}
|
||||
}
|
||||
|
||||
ensureInSettings(): void {
|
||||
if (this.zh.type !== 'Coordinator' && !settings.getDevice(this.zh.ieeeAddr)) {
|
||||
settings.addDevice(this.zh.ieeeAddr);
|
||||
|
||||
Vendored
+5
-2
@@ -114,7 +114,7 @@ declare global {
|
||||
description: string
|
||||
options: zhc.DefinitionExpose[],
|
||||
vendor: string
|
||||
exposes: DefinitionExpose[]
|
||||
exposes: DefinitionExpose[] | ((device: zh.Device, options: KeyValue) => DefinitionExpose[])
|
||||
configure?: (device: zh.Device, coordinatorEndpoint: zh.Endpoint, logger: Logger) => Promise<void>;
|
||||
onEvent?: (type: string, data: KeyValue, device: zh.Device, settings: KeyValue) => Promise<void>;
|
||||
ota?: {
|
||||
@@ -142,7 +142,8 @@ declare global {
|
||||
type DeviceAnnounce = { device: Device };
|
||||
type DeviceInterview = { device: Device, status: 'started' | 'successful' | 'failed' };
|
||||
type DeviceJoined = { device: Device };
|
||||
type ReportingDisabled = { device: Device };
|
||||
type EntityOptionsChanged = { entity: Device | Group, from: KeyValue, to: KeyValue };
|
||||
type Reconfigure = { device: Device };
|
||||
type DeviceLeave = { ieeeAddr: string, name: string };
|
||||
type GroupMembersChanged = {group: Group, action: 'remove' | 'add' | 'remove_all',
|
||||
endpoint: zh.Endpoint, skipDisableReporting: boolean };
|
||||
@@ -284,6 +285,7 @@ declare global {
|
||||
legacy?: boolean,
|
||||
filtered_attributes?: string[],
|
||||
friendly_name: string,
|
||||
qos?: 0 | 1 | 2,
|
||||
}
|
||||
|
||||
interface GroupSettings {
|
||||
@@ -295,6 +297,7 @@ declare global {
|
||||
homeassistant?: KeyValue,
|
||||
filtered_attributes?: string[],
|
||||
friendly_name: string,
|
||||
qos?: 0 | 1 | 2,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
},
|
||||
"passive": {
|
||||
"type": "object",
|
||||
"title": "Active",
|
||||
"title": "Passive",
|
||||
"requiresRestart": true,
|
||||
"description": "Options for passive devices (mostly battery powered)",
|
||||
"properties": {
|
||||
|
||||
@@ -247,15 +247,18 @@ export function validate(): string[] {
|
||||
|
||||
// Verify that all friendly names are unique
|
||||
const names: string[] = [];
|
||||
const check = (name: string): void => {
|
||||
if (names.includes(name)) errors.push(`Duplicate friendly_name '${name}' found`);
|
||||
errors.push(...utils.validateFriendlyName(name));
|
||||
names.push(name);
|
||||
const check = (e: DeviceSettings | GroupSettings): void => {
|
||||
if (names.includes(e.friendly_name)) errors.push(`Duplicate friendly_name '${e.friendly_name}' found`);
|
||||
errors.push(...utils.validateFriendlyName(e.friendly_name));
|
||||
names.push(e.friendly_name);
|
||||
if (e.qos != null && ![0, 1, 2].includes(e.qos)) {
|
||||
errors.push(`QOS for '${e.friendly_name}' not valid, should be 0, 1 or 2 got ${e.qos}`);
|
||||
}
|
||||
};
|
||||
|
||||
const settingsWithDefaults = get();
|
||||
Object.values(settingsWithDefaults.devices).forEach((d) => check(d.friendly_name));
|
||||
Object.values(settingsWithDefaults.groups).forEach((g) => check(g.friendly_name));
|
||||
Object.values(settingsWithDefaults.devices).forEach((d) => check(d));
|
||||
Object.values(settingsWithDefaults.groups).forEach((g) => check(g));
|
||||
|
||||
if (settingsWithDefaults.mqtt.version !== 5) {
|
||||
for (const device of Object.values(settingsWithDefaults.devices)) {
|
||||
|
||||
@@ -80,12 +80,14 @@ export default class Zigbee {
|
||||
});
|
||||
this.herdsman.on('deviceInterview', (data: ZHEvents.DeviceInterviewPayload) => {
|
||||
const device = this.resolveDevice(data.device.ieeeAddr);
|
||||
/* istanbul ignore if */ if (!device) return; // Prevent potential race
|
||||
const d = {device, status: data.status};
|
||||
this.logDeviceInterview(d);
|
||||
this.eventBus.emitDeviceInterview(d);
|
||||
});
|
||||
this.herdsman.on('deviceJoined', (data: ZHEvents.DeviceJoinedPayload) => {
|
||||
const device = this.resolveDevice(data.device.ieeeAddr);
|
||||
/* istanbul ignore if */ if (!device) return; // Prevent potential race
|
||||
logger.info(`Device '${device.name}' joined`);
|
||||
this.eventBus.emitDeviceJoined({device});
|
||||
});
|
||||
|
||||
Generated
+2714
-2708
File diff suppressed because it is too large
Load Diff
+20
-20
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "1.22.1",
|
||||
"version": "1.22.2",
|
||||
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -38,51 +38,51 @@
|
||||
"ajv": "^8.8.2",
|
||||
"bind-decorator": "^1.0.11",
|
||||
"connect-gzip-static": "^2.1.1",
|
||||
"core-js": "^3.19.1",
|
||||
"core-js": "^3.20.1",
|
||||
"debounce": "^1.2.1",
|
||||
"deep-object-diff": "^1.1.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"finalhandler": "^1.1.2",
|
||||
"git-last-commit": "^1.0.1",
|
||||
"humanize-duration": "^3.27.0",
|
||||
"humanize-duration": "^3.27.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"mkdir-recursive": "^0.4.0",
|
||||
"moment": "^2.29.1",
|
||||
"mqtt": "4.2.8",
|
||||
"mqtt": "4.3.1",
|
||||
"object-assign-deep": "^0.4.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.5",
|
||||
"source-map-support": "^0.5.21",
|
||||
"winston": "^3.3.3",
|
||||
"winston-syslog": "^2.4.4",
|
||||
"ws": "^8.3.0",
|
||||
"zigbee-herdsman": "0.13.176",
|
||||
"zigbee-herdsman-converters": "14.0.336",
|
||||
"zigbee2mqtt-frontend": "0.6.46"
|
||||
"ws": "^8.4.0",
|
||||
"zigbee-herdsman": "0.13.188",
|
||||
"zigbee-herdsman-converters": "14.0.366",
|
||||
"zigbee2mqtt-frontend": "0.6.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.16.4",
|
||||
"@babel/preset-env": "^7.16.4",
|
||||
"@babel/preset-typescript": "^7.16.0",
|
||||
"@babel/core": "^7.16.5",
|
||||
"@babel/plugin-proposal-decorators": "^7.16.5",
|
||||
"@babel/preset-env": "^7.16.5",
|
||||
"@babel/preset-typescript": "^7.16.5",
|
||||
"@types/debounce": "^1.2.1",
|
||||
"@types/finalhandler": "^1.1.1",
|
||||
"@types/humanize-duration": "^3.27.0",
|
||||
"@types/humanize-duration": "^3.27.1",
|
||||
"@types/jest": "^27.0.3",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/object-assign-deep": "^0.4.0",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/ws": "^8.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"@typescript-eslint/parser": "^5.4.0",
|
||||
"babel-jest": "^27.3.1",
|
||||
"eslint": "^8.3.0",
|
||||
"@types/ws": "^8.2.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.8.0",
|
||||
"@typescript-eslint/parser": "^5.8.0",
|
||||
"babel-jest": "^27.4.5",
|
||||
"eslint": "^8.5.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-jest": "^25.3.0",
|
||||
"jest": "^27.3.1",
|
||||
"jest": "^27.4.5",
|
||||
"tmp": "^0.2.1",
|
||||
"typescript": "^4.5.2"
|
||||
"typescript": "^4.5.4"
|
||||
},
|
||||
"jest": {
|
||||
"coverageThreshold": {
|
||||
|
||||
@@ -248,13 +248,12 @@ describe('Availability', () => {
|
||||
expect(endpoint.read).toHaveBeenCalledWith('lightingColorCtrl',
|
||||
['colorMode', 'currentX', 'currentY', 'enhancedCurrentHue', 'currentSaturation', 'colorTemperature']);
|
||||
|
||||
// Should stop when one request fails
|
||||
endpoint.read.mockClear();
|
||||
await zigbeeHerdsman.events.deviceAnnounce({device: devices.bulb_color});
|
||||
await flushPromises();
|
||||
endpoint.read.mockImplementationOnce(() => {throw new Error('')});
|
||||
await advancedTime(utils.seconds(3));
|
||||
expect(endpoint.read).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.read).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('Should republish availability when device is renamed', async () => {
|
||||
|
||||
+8
-8
File diff suppressed because one or more lines are too long
@@ -100,12 +100,12 @@ describe('Configure', () => {
|
||||
expectBulbConfigured();
|
||||
});
|
||||
|
||||
it('Should reconfigure reporting on reportingDisabled event', async () => {
|
||||
it('Should reconfigure reporting on reconfigure event', async () => {
|
||||
expectBulbConfigured();
|
||||
const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb);
|
||||
mockClear(device.zh);
|
||||
expectBulbNotConfigured();
|
||||
controller.eventBus.emitReportingDisabled({device});
|
||||
controller.eventBus.emitReconfigure({device});
|
||||
await flushPromises();
|
||||
expectBulbConfigured();
|
||||
});
|
||||
|
||||
@@ -140,7 +140,7 @@ describe('Frontend', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({state: 'ON', linkquality: null, update_available: null}),
|
||||
stringify({state: 'ON', linkquality: null, update_available: null, update: {state: null}}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
@@ -151,7 +151,7 @@ describe('Frontend', () => {
|
||||
|
||||
// Received message on socket
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledTimes(1);
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bulb_color', payload: {state: 'ON', linkquality: null, update_available: null}}));
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bulb_color', payload: {state: 'ON', linkquality: null, update_available: null, update: {state: null}}}));
|
||||
|
||||
// Shouldnt set when not ready
|
||||
mockWSClient.implementation.send.mockClear();
|
||||
|
||||
+15
-30
@@ -49,14 +49,17 @@ describe('HomeAssistant extension', () => {
|
||||
it('Should not have duplicate type/object_ids in a mapping', () => {
|
||||
const duplicated = [];
|
||||
require('zigbee-herdsman-converters').devices.forEach((d) => {
|
||||
const mapping = extension._getMapping()[d.model];
|
||||
const exposes = typeof d.exposes == 'function' ? d.exposes() : d.exposes;
|
||||
const device = {definition: d, isDevice: () => true, settings: {}, exposes: () => exposes};
|
||||
const configs = extension.getConfigs(device);
|
||||
const cfg_type_object_ids = [];
|
||||
|
||||
mapping.forEach((c) => {
|
||||
if (cfg_type_object_ids.includes(c['type'] + '/' + c['object_id'])) {
|
||||
configs.forEach((c) => {
|
||||
const id = c['type'] + '/' + c['object_id'];
|
||||
if (cfg_type_object_ids.includes(id)) {
|
||||
duplicated.push(d.model);
|
||||
} else {
|
||||
cfg_type_object_ids.push(c['type'] + '/' + c['object_id']);
|
||||
cfg_type_object_ids.push(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -827,7 +830,7 @@ describe('HomeAssistant extension', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({"color":{"hue": 0, "saturation": 100, "h": 0, "s": 100}, "color_mode": "hs", "linkquality": null, "state": null, "update_available": null}),
|
||||
stringify({"color":{"hue": 0, "saturation": 100, "h": 0, "s": 100}, "color_mode": "hs", "linkquality": null, "state": null, "update_available": null, "update": {"state": null}}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
@@ -843,7 +846,7 @@ describe('HomeAssistant extension', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({"color": {"x": 0.4576,"y": 0.41}, "color_mode": "xy", "linkquality": null,"state": null, "update_available": null}),
|
||||
stringify({"color": {"x": 0.4576,"y": 0.41}, "color_mode": "xy", "linkquality": null,"state": null, "update_available": null, "update": {"state": null}}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
@@ -859,7 +862,7 @@ describe('HomeAssistant extension', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({"linkquality": null,"state": "ON", "update_available": null}),
|
||||
stringify({"linkquality": null,"state": "ON", "update_available": null, "update": {"state": null}}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
@@ -934,13 +937,13 @@ describe('HomeAssistant extension', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null}),
|
||||
stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null, "update": {"state": null}}),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/remote',
|
||||
stringify({"action":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null}),
|
||||
stringify({"action":null,"action_duration":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null, "update": {"state": null}}),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
@@ -958,13 +961,13 @@ describe('HomeAssistant extension', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null}),
|
||||
stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null, "update": {"state": null}}),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/remote',
|
||||
stringify({"action":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null}),
|
||||
stringify({"action":null,"action_duration":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null, "update": {"state": null}}),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
@@ -1490,24 +1493,6 @@ describe('HomeAssistant extension', () => {
|
||||
expect(MQTT.publish.mock.calls[3][0]).toStrictEqual('zigbee2mqtt/button/action');
|
||||
});
|
||||
|
||||
it('Load Home Assistant mapping from external converters', async () => {
|
||||
fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter-multiple.js'), path.join(data.mockDir, 'mock-external-converter-multiple.js'));
|
||||
settings.set(['external_converters'], ['mock-external-converter-multiple.js']);
|
||||
await resetExtension();
|
||||
|
||||
const homeassistantSwitch = {
|
||||
type: 'switch',
|
||||
object_id: 'switch',
|
||||
discovery_payload: {
|
||||
payload_off: 'OFF',
|
||||
payload_on: 'ON',
|
||||
value_template: '{{ value_json.state }}',
|
||||
command_topic: true,
|
||||
},
|
||||
};
|
||||
expect(extension._getMapping()['external_converters_device_1']).toEqual([homeassistantSwitch]);
|
||||
});
|
||||
|
||||
it('Should clear outdated configs', async () => {
|
||||
// Non-existing group -> clear
|
||||
MQTT.publish.mockClear();
|
||||
@@ -1666,7 +1651,7 @@ describe('HomeAssistant extension', () => {
|
||||
});
|
||||
|
||||
it('Should discover last_seen when enabled', async () => {
|
||||
settings.set(['advanced', 'last_seen'], ' epoch');
|
||||
settings.set(['advanced', 'last_seen'], 'ISO_8601');
|
||||
await resetExtension();
|
||||
|
||||
const payload = {
|
||||
|
||||
@@ -71,9 +71,8 @@ describe('OTA update', () => {
|
||||
expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0.00%`);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10.00%, ≈ 60 minutes remaining`);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb', from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190102","softwareBuildID":2}'`);
|
||||
expect(device.save).toHaveBeenCalledTimes(1);
|
||||
expect(device.dateCode).toBe('20190102');
|
||||
expect(device.softwareBuildID).toBe(2);
|
||||
expect(device.save).toHaveBeenCalledTimes(2);
|
||||
expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {'sendWhenActive': false});
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({"update_available":false,"update":{"state":"updating","progress":0}}),
|
||||
@@ -372,9 +371,8 @@ describe('OTA update', () => {
|
||||
expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10.00%, ≈ 60 minutes remaining`);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb', from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190102","softwareBuildID":2}'`);
|
||||
expect(logger.error).toHaveBeenCalledTimes(0);
|
||||
expect(device.save).toHaveBeenCalledTimes(1);
|
||||
expect(device.dateCode).toBe('20190102');
|
||||
expect(device.softwareBuildID).toBe(2);
|
||||
expect(device.save).toHaveBeenCalledTimes(2);
|
||||
expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {'sendWhenActive': false});
|
||||
});
|
||||
|
||||
it('Legacy api: Should handle when OTA update fails', async () => {
|
||||
|
||||
+29
-2
@@ -367,7 +367,7 @@ describe('Publish', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/thermostat_group/set', stringify({child_lock: 'LOCK'}));
|
||||
await flushPromises();
|
||||
expect(group.command).toHaveBeenCalledTimes(1);
|
||||
expect(group.command).toHaveBeenCalledWith("manuSpecificTuya", "setData", {data: [1], datatype: 1, dp: 7, length_hi: 0, length_lo: 1, status: 0, transid: expect.any(Number)}, {disableDefaultResponse: true});
|
||||
expect(group.command).toHaveBeenCalledWith("manuSpecificTuya", "dataRequest", {data: [1], datatype: 1, dp: 7, length_hi: 0, length_lo: 1, seq: expect.any(Number)}, {disableDefaultResponse: true});
|
||||
});
|
||||
|
||||
it('Should publish messages to groups with on and brightness', async () => {
|
||||
@@ -907,6 +907,33 @@ describe('Publish', () => {
|
||||
expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColor", {"colorx": 45940, "colory": 19595, "transtime": 30}, {}]);
|
||||
});
|
||||
|
||||
it('Should correctly toggle when transition is used', async () => {
|
||||
settings.set(['devices', zigbeeHerdsman.devices.bulb_color.ieeeAddr, 'transition'], 20);
|
||||
const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 163}));
|
||||
await flushPromises();
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'}));
|
||||
await flushPromises();
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'TOGGLE',}));
|
||||
await flushPromises();
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'TOGGLE',}));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(4);
|
||||
expect(endpoint.command).toHaveBeenNthCalledWith(1, "genLevelCtrl", "moveToLevelWithOnOff", {"level": 163, "transtime": 200}, {})
|
||||
expect(endpoint.command).toHaveBeenNthCalledWith(2, "genLevelCtrl", "moveToLevelWithOnOff", {"level": 0, "transtime": 200}, {})
|
||||
expect(endpoint.command).toHaveBeenNthCalledWith(3, "genLevelCtrl", "moveToLevelWithOnOff", {"level": 163, "transtime": 200}, {})
|
||||
expect(endpoint.command).toHaveBeenNthCalledWith(4, "genLevelCtrl", "moveToLevelWithOnOff", {"level": 0, "transtime": 200}, {})
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(4);
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(1, 'zigbee2mqtt/bulb_color', stringify({"brightness":163,"state":"ON"}),
|
||||
{ retain: false, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(2, 'zigbee2mqtt/bulb_color', stringify({"brightness":163,"state":"OFF"}),
|
||||
{ retain: false, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(3, 'zigbee2mqtt/bulb_color', stringify({"brightness":163,"state":"ON"}),
|
||||
{ retain: false, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(4, 'zigbee2mqtt/bulb_color', stringify({"brightness":163,"state":"OFF"}),
|
||||
{ retain: false, qos: 0 }, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should turn device off when brightness 0 is send', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
@@ -1294,7 +1321,7 @@ describe('Publish', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/MKS-CM-W5/l3/set', stringify({state: 'ON'}));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("manuSpecificTuya", "setData", {data: [1], datatype: 1, dp: 3, length_hi: 0, length_lo: 1, status: 0, transid: expect.any(Number)}, {disableDefaultResponse: true});
|
||||
expect(endpoint.command).toHaveBeenCalledWith("manuSpecificTuya", "dataRequest", {data: [1], datatype: 1, dp: 3, length_hi: 0, length_lo: 1, seq: expect.any(Number)}, {disableDefaultResponse: true});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/MKS-CM-W5');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state_l3: 'ON'});
|
||||
|
||||
@@ -421,7 +421,7 @@ describe('Receive', () => {
|
||||
it('Should handle a command', async () => {
|
||||
const device = zigbeeHerdsman.devices.E1743;
|
||||
const data = {};
|
||||
const payload = {data, cluster: 'genLevelCtrl', device, endpoint: device.getEndpoint(1), type: 'commandStopWithOnOff', linkquality: 10};
|
||||
const payload = {data, cluster: 'genLevelCtrl', device, endpoint: device.getEndpoint(1), type: 'commandStopWithOnOff', linkquality: 10, meta: {zclTransactionSequenceNumber: 1}};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
@@ -437,10 +437,10 @@ describe('Receive', () => {
|
||||
const oldNow = Date.now;
|
||||
Date.now = jest.fn()
|
||||
Date.now.mockReturnValue(new Date(150));
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await zigbeeHerdsman.events.message({...payload, meta: {zclTransactionSequenceNumber: 2}});
|
||||
await flushPromises();
|
||||
Date.now.mockReturnValue(new Date(200));
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await zigbeeHerdsman.events.message({...payload, meta: {zclTransactionSequenceNumber: 3}});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff');
|
||||
|
||||
@@ -705,6 +705,18 @@ describe('Settings', () => {
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
it('Configuration shouldnt be valid when invalid QOS value is used', async () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
devices: {'0x0017880104e45519': {friendly_name: 'myname', retain: false, qos: 3}},
|
||||
});
|
||||
|
||||
settings.reRead();
|
||||
|
||||
const error = `QOS for 'myname' not valid, should be 0, 1 or 2 got 3`;
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
it('Configuration shouldnt be valid when duplicate friendly_name are used', async () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
|
||||
Reference in New Issue
Block a user