mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 07:08:56 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2b5229f76 | ||
|
|
0c605688af | ||
|
|
6876e19008 | ||
|
|
437b1dfb3e | ||
|
|
fda1f23829 | ||
|
|
ed9d09a629 | ||
|
|
d298412a1d | ||
|
|
e7447cf1e3 | ||
|
|
c90171d2cc | ||
|
|
d2cf0dd8aa | ||
|
|
d415f57e30 | ||
|
|
56998f72f5 | ||
|
|
97bd090ad6 | ||
|
|
df182be21f | ||
|
|
290a0312d4 | ||
|
|
4d432658e9 | ||
|
|
c2d9ffa7ad | ||
|
|
82f5ddb748 | ||
|
|
1069ec2a1a | ||
|
|
cd4ac0c174 | ||
|
|
134b80ccf6 | ||
|
|
b07450d413 | ||
|
|
65f7fc0532 | ||
|
|
0a7a477281 | ||
|
|
a170f304c7 | ||
|
|
35ed5154c8 | ||
|
|
641853e6d5 | ||
|
|
2af9f21961 | ||
|
|
51321affcf | ||
|
|
5fca6fbd09 | ||
|
|
070065a248 | ||
|
|
9a8f204683 | ||
|
|
167c05681e | ||
|
|
01d7bd25be | ||
|
|
372ec315b5 | ||
|
|
df0543d4b7 | ||
|
|
a9e3fb3358 | ||
|
|
678e19fd7e | ||
|
|
9128838534 | ||
|
|
35876616ef | ||
|
|
0bc3129b7a | ||
|
|
5e3b18a053 | ||
|
|
ad3ff7fa40 | ||
|
|
008d6f9aa0 | ||
|
|
854353fb71 | ||
|
|
e35ba0f83c | ||
|
|
a154e18326 | ||
|
|
c793cc83ad | ||
|
|
a51626e357 | ||
|
|
0f3315063f | ||
|
|
1065916a01 | ||
|
|
a980b4dff5 | ||
|
|
825c7465ae | ||
|
|
97521478c6 | ||
|
|
6279c83e65 | ||
|
|
91479fc0ec | ||
|
|
9dbe6f0a6c | ||
|
|
5d415aa299 | ||
|
|
6a75e0d4ee | ||
|
|
13e9616c92 | ||
|
|
cc03be46df | ||
|
|
64154e0cb2 | ||
|
|
b2eaa3141e | ||
|
|
d192bbd28f | ||
|
|
9582808b09 | ||
|
|
623f999ec8 | ||
|
|
3141a14fec | ||
|
|
7e2a2bdd34 | ||
|
|
2f503bf39a | ||
|
|
945b72b990 | ||
|
|
3bfc7f31cf | ||
|
|
1f60edff99 | ||
|
|
3249ab00ab | ||
|
|
8c8f907fd3 | ||
|
|
170ef40982 | ||
|
|
e547062702 | ||
|
|
6ca8d35183 | ||
|
|
420bae4259 | ||
|
|
97dacd7623 | ||
|
|
8ef82fe23f | ||
|
|
cd3df10a54 | ||
|
|
d5ccd9221c | ||
|
|
fc9abfe53b | ||
|
|
01ea46b280 | ||
|
|
ab629d52ea | ||
|
|
9e216011d4 | ||
|
|
91d90bdcf1 | ||
|
|
6ba743cbed | ||
|
|
2399ec040f | ||
|
|
f151e1d371 | ||
|
|
55802ef648 | ||
|
|
89af770d96 | ||
|
|
0efd0cfe17 | ||
|
|
c852434dde | ||
|
|
57e93a48ff | ||
|
|
181944d2ff | ||
|
|
a09aa4243e | ||
|
|
932ee433c4 | ||
|
|
20b6fd42d3 | ||
|
|
178da2db1d | ||
|
|
e504da33d4 | ||
|
|
fa1775d8b4 | ||
|
|
2612de3c34 | ||
|
|
416f4fa548 | ||
|
|
ee57b0ce0b | ||
|
|
9729a72d33 | ||
|
|
23719df7ac | ||
|
|
ef6bf0a8f9 | ||
|
|
b3e9afaf3e | ||
|
|
5709898c94 | ||
|
|
68b5be0ce7 | ||
|
|
62bf76c11f | ||
|
|
243d333526 | ||
|
|
09383877b0 | ||
|
|
359af6f9ff | ||
|
|
cf68507ec3 | ||
|
|
78a3e4a371 | ||
|
|
11753a7078 | ||
|
|
512b785427 |
@@ -9,25 +9,25 @@ Rules (don't ignore these, your issue will be closed without further notice):
|
||||
- Make sure you are running the latest firmware https://github.com/koenkk/z-stack-firmware.
|
||||
|
||||
Did you read the FAQ?
|
||||
- https://www.zigbee2mqtt.io/information/FAQ.html
|
||||
- https://www.zigbee2mqtt.io/guide/faq
|
||||
|
||||
Zigbee2MQTT fails to start?
|
||||
- https://www.zigbee2mqtt.io/information/FAQ.html#help-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/information/supported_adapters.html#texas-instruments-cc26x2r1
|
||||
- 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/how_tos/how_to_support_new_devices.html
|
||||
- https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html
|
||||
|
||||
Device does not pair or interview fails?
|
||||
- https://www.zigbee2mqtt.io/information/FAQ.html#why-does-my-device-not-or-fail-to-pair
|
||||
- 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/information/debug.html#zigbee-herdsman-debug-logging
|
||||
- Provide the herdsman debug logging: https://www.zigbee2mqtt.io/guide/usage/debug.html
|
||||
|
||||
-->
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ contact_links:
|
||||
url: https://github.com/zigbee2mqtt/hassio-zigbee2mqtt/issues
|
||||
about: Issues related to the Home Assistant addon.
|
||||
- name: FAQ
|
||||
url: https://www.zigbee2mqtt.io/information/FAQ.html
|
||||
url: https://www.zigbee2mqtt.io/guide/faq
|
||||
about: Frequently asked questions.
|
||||
- name: Support Chat
|
||||
url: https://discord.gg/NyseBeK
|
||||
|
||||
@@ -8,7 +8,7 @@ assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
VERY IMPORTANT: Before submitting a request, first (try to) follow: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html
|
||||
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.
|
||||
|
||||
@@ -18,25 +18,25 @@ Rules (don't ignore these, your issue will be closed without further notice):
|
||||
- Make sure you are running the latest firmware https://github.com/koenkk/z-stack-firmware.
|
||||
|
||||
Did you read the FAQ?
|
||||
- https://www.zigbee2mqtt.io/information/FAQ.html
|
||||
- https://www.zigbee2mqtt.io/guide/faq/
|
||||
|
||||
Zigbee2MQTT fails to start?
|
||||
- https://www.zigbee2mqtt.io/information/FAQ.html#help-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/information/supported_adapters.html#texas-instruments-cc26x2r1
|
||||
- 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/how_tos/how_to_support_new_devices.html
|
||||
- https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html
|
||||
|
||||
Device does not pair or interview fails?
|
||||
- https://www.zigbee2mqtt.io/information/FAQ.html#why-does-my-device-not-or-fail-to-pair
|
||||
- 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/information/debug.html#zigbee-herdsman-debug-logging
|
||||
- Provide the herdsman debug logging: https://www.zigbee2mqtt.io/guide/usage/debug.html
|
||||
|
||||
-->
|
||||
|
||||
|
||||
@@ -16,7 +16,15 @@ jobs:
|
||||
node-version: 14
|
||||
- run: npm install zigbee2mqtt-frontend@${{ github.event.client_payload.version }} --save-exact
|
||||
- uses: peter-evans/create-pull-request@v3
|
||||
id: cpr
|
||||
with:
|
||||
commit-message: Update zigbee2mqtt-frontend to ${{ github.event.client_payload.version }}
|
||||
branch: "update_frontend"
|
||||
title: Update zigbee-frontend to ${{ github.event.client_payload.version }}
|
||||
- run: sleep 5 # Otherwise pull request may not exist yet causing automerge to fail
|
||||
- uses: peter-evans/enable-pull-request-automerge@v1
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.CR_PAT }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
merge-method: squash
|
||||
|
||||
@@ -16,7 +16,15 @@ jobs:
|
||||
node-version: 14
|
||||
- run: npm install zigbee-herdsman@${{ github.event.client_payload.version }} --save-exact
|
||||
- uses: peter-evans/create-pull-request@v3
|
||||
id: cpr
|
||||
with:
|
||||
commit-message: Update zigbee-herdsman to ${{ github.event.client_payload.version }}
|
||||
branch: "update_zh"
|
||||
title: Update zigbee-herdsman to ${{ github.event.client_payload.version }}
|
||||
- run: sleep 5 # Otherwise pull request may not exist yet causing automerge to fail
|
||||
- uses: peter-evans/enable-pull-request-automerge@v1
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.CR_PAT }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
merge-method: squash
|
||||
@@ -16,7 +16,15 @@ jobs:
|
||||
node-version: 14
|
||||
- run: npm install zigbee-herdsman-converters@${{ github.event.client_payload.version }} --save-exact
|
||||
- uses: peter-evans/create-pull-request@v3
|
||||
id: cpr
|
||||
with:
|
||||
commit-message: Update zigbee-herdsman-converters to ${{ github.event.client_payload.version }}
|
||||
branch: "update_zhc"
|
||||
title: Update zigbee-herdsman-converters to ${{ github.event.client_payload.version }}
|
||||
- run: sleep 5 # Otherwise pull request may not exist yet causing automerge to fail
|
||||
- uses: peter-evans/enable-pull-request-automerge@v1
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.CR_PAT }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
merge-method: squash
|
||||
+2
-2
@@ -3,6 +3,6 @@
|
||||
Everybody is invited and welcome to contribute to Zigbee2MQTT. Zigbee2MQTT is writen 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 definition, 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/getting_started/running_zigbee2mqtt.html#running-zigbee2mqtt) to do this.
|
||||
- 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`.
|
||||
- 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/how_tos/how_to_support_new_devices.html).
|
||||
- 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).
|
||||
|
||||
@@ -35,26 +35,26 @@
|
||||
</div>
|
||||
<h1>Zigbee2MQTT 🌉 🐝</h1>
|
||||
<p>
|
||||
Allows you to use your Zigbee devices <b>without</b> the vendors bridge or gateway.
|
||||
Allows you to use your Zigbee devices <b>without</b> the vendor's bridge or gateway.
|
||||
</p>
|
||||
<p>
|
||||
It bridges events and allows you to control your Zigbee devices via MQTT. In this way you can integrate your Zigbee devices with whatever smart home infrastructure you are using.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## [Getting started](https://www.zigbee2mqtt.io/#getting-started)
|
||||
## [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/information/zigbee_network.html) to help you out.
|
||||
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/#integration)
|
||||
## [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](https://www.home-assistant.io/hassio/): Using [the official addon](https://github.com/zigbee2mqtt/hassio-zigbee2mqtt)
|
||||
- Generic install or Hassbian: Using instructions [here](https://www.zigbee2mqtt.io/integration/home_assistant.html)
|
||||
- Generic install or Hassbian: Using instructions [here](https://www.zigbee2mqtt.io/guide/usage/integrations/home_assistant.html)
|
||||
|
||||
<img align="left" height="100px" width="100px" src="https://user-images.githubusercontent.com/2734836/47615848-b8dd8700-dabd-11e8-9d77-175002dd8987.png">
|
||||
|
||||
@@ -67,15 +67,15 @@ 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 an 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/raw/master/docs/Zigbee%20Cluster%20Library%20Specification%20v7.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.
|
||||
|
||||
### 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/information/supported_devices.html) to check whether your device is supported. There is quite an extensive list, including devices from vendors like Xiaomi, Ikea, Philips, OSRAM and more.
|
||||
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, Ikea, Philips, OSRAM and more.
|
||||
|
||||
If it's not listed in [Supported devices](https://www.zigbee2mqtt.io/information/supported_devices.html), support can be added (fairly) easy, see [How to support new devices](https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html).
|
||||
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.
|
||||
|
||||
@@ -87,7 +87,7 @@ async function start() {
|
||||
for (const error of errors) {
|
||||
console.log(`- ${error}`);
|
||||
}
|
||||
console.log(`\nIf you don't know how to solve this, read https://www.zigbee2mqtt.io/information/configuration.html`); // eslint-disable-line
|
||||
console.log(`\nIf you don't know how to solve this, read https://www.zigbee2mqtt.io/guide/configuration`); // eslint-disable-line
|
||||
console.log(`\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n`);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
+5
-7
@@ -37,7 +37,7 @@ const AllExtensions = [
|
||||
];
|
||||
|
||||
type ExtensionArgs = [Zigbee, MQTT, State, PublishEntityState, EventBus,
|
||||
(enable: boolean, name: string) => Promise<void>, () => void, (extension: Extension) => void];
|
||||
(enable: boolean, name: string) => Promise<void>, () => void, (extension: Extension) => Promise<void>];
|
||||
|
||||
class Controller {
|
||||
private eventBus: EventBus;
|
||||
@@ -101,7 +101,7 @@ class Controller {
|
||||
this.eventBus.onAdapterDisconnected(this, this.onZigbeeAdapterDisconnected);
|
||||
} catch (error) {
|
||||
logger.error('Failed to start zigbee');
|
||||
logger.error('Check https://www.zigbee2mqtt.io/information/FAQ.html#help-zigbee2mqtt-fails-to-start for possible solutions'); /* eslint-disable-line max-len */
|
||||
logger.error('Check https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start.html for possible solutions'); /* eslint-disable-line max-len */
|
||||
logger.error('Exiting...');
|
||||
logger.error(error.stack);
|
||||
this.exitCallback(1);
|
||||
@@ -160,10 +160,8 @@ class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.get().advanced.last_seen && settings.get().advanced.last_seen !== 'disable') {
|
||||
this.eventBus.onLastSeenChanged(this, (data) =>
|
||||
this.publishEntityState(data.device, {}, 'lastSeenChanged'));
|
||||
}
|
||||
this.eventBus.onLastSeenChanged(this,
|
||||
(data) => utils.publishLastSeen(data, settings.get(), false, this.publishEntityState));
|
||||
}
|
||||
|
||||
@bind async enableDisableExtension(enable: boolean, name: string): Promise<void> {
|
||||
@@ -276,7 +274,7 @@ class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
this.eventBus.emitPublishEntityState({entity, message, stateChangeReason});
|
||||
this.eventBus.emitPublishEntityState({entity, message, stateChangeReason, payload});
|
||||
}
|
||||
|
||||
async iteratePayloadAttributeOutput(topicRoot: string, payload: KeyValue, options: MQTTOptions): Promise<void> {
|
||||
|
||||
@@ -77,7 +77,7 @@ export default class Availability extends Extension {
|
||||
logger.debug(`Succesfully pinged '${device.name}' (attempt ${i + 1}/${attempts})`);
|
||||
break;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to ping '${device.name}' (attempt ${i + 1}/${attempts}, ${error.message})`);
|
||||
logger.warn(`Failed to ping '${device.name}' (attempt ${i + 1}/${attempts}, ${error.message})`);
|
||||
// Try again in 3 seconds.
|
||||
const lastAttempt = i - 1 === attempts;
|
||||
!lastAttempt && await utils.sleep(3);
|
||||
@@ -97,10 +97,10 @@ export default class Availability extends Extension {
|
||||
override async start(): Promise<void> {
|
||||
logger.warn('Using experimental new availability feature');
|
||||
|
||||
this.eventBus.onDeviceRenamed(this, (data: eventdata.DeviceRenamed) =>
|
||||
this.publishAvailability(data.device, false, true));
|
||||
this.eventBus.onDeviceLeave(this, (data: eventdata.DeviceLeave) => clearTimeout(this.timers[data.ieeeAddr]));
|
||||
this.eventBus.onDeviceAnnounce(this, (data: eventdata.DeviceAnnounce) => this.retrieveState(data.device));
|
||||
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]));
|
||||
this.eventBus.onDeviceAnnounce(this, (data) => this.retrieveState(data.device));
|
||||
this.eventBus.onLastSeenChanged(this, this.onLastSeenChanged);
|
||||
|
||||
for (const device of this.zigbee.devices(false)) {
|
||||
@@ -157,8 +157,8 @@ export default class Availability extends Extension {
|
||||
}
|
||||
|
||||
override async stop(): Promise<void> {
|
||||
super.stop();
|
||||
Object.values(this.timers).forEach((t) => clearTimeout(t));
|
||||
super.stop();
|
||||
}
|
||||
|
||||
private retrieveState(device: Device): void {
|
||||
|
||||
@@ -6,7 +6,7 @@ abstract class Extension {
|
||||
protected eventBus: EventBus;
|
||||
protected enableDisableExtension: (enable: boolean, name: string) => Promise<void>;
|
||||
protected restartCallback: () => void;
|
||||
protected addExtension: (extension: Extension) => void;
|
||||
protected addExtension: (extension: Extension) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Besides intializing variables, the constructor should do nothing!
|
||||
@@ -22,7 +22,7 @@ abstract class Extension {
|
||||
*/
|
||||
constructor(zigbee: Zigbee, mqtt: MQTT, state: State, publishEntityState: PublishEntityState,
|
||||
eventBus: EventBus, enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
|
||||
restartCallback: () => void, addExtension: (extension: Extension) => void) {
|
||||
restartCallback: () => void, addExtension: (extension: Extension) => Promise<void>) {
|
||||
this.zigbee = zigbee;
|
||||
this.mqtt = mqtt;
|
||||
this.state = state;
|
||||
|
||||
@@ -6,7 +6,7 @@ import Extension from './extension';
|
||||
export default class ExternalConverters extends Extension {
|
||||
constructor(zigbee: Zigbee, mqtt: MQTT, state: State, publishEntityState: PublishEntityState,
|
||||
eventBus: EventBus, enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
|
||||
restartCallback: () => void, addExtension: (extension: Extension) => void) {
|
||||
restartCallback: () => void, addExtension: (extension: Extension) => Promise<void>) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension);
|
||||
|
||||
for (const definition of utils.getExternalConvertersDefinitions(settings.get())) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import Extension from './extension';
|
||||
const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/extension/(save|remove)`);
|
||||
|
||||
export default class ExternalExtension extends Extension {
|
||||
private requestLookup: {[s: string]: (message: KeyValue) => MQTTResponse};
|
||||
private requestLookup: {[s: string]: (message: KeyValue) => Promise<MQTTResponse>};
|
||||
|
||||
override async start(): Promise<void> {
|
||||
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
|
||||
@@ -36,13 +36,13 @@ export default class ExternalExtension extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
@bind private removeExtension(message: KeyValue): MQTTResponse {
|
||||
@bind private async removeExtension(message: KeyValue): Promise<MQTTResponse> {
|
||||
const {name} = message;
|
||||
const extensions = this.getListOfUserDefinedExtensions();
|
||||
const extensionToBeRemoved = extensions.find((e) => e.name === name);
|
||||
|
||||
if (extensionToBeRemoved) {
|
||||
this.enableDisableExtension(false, extensionToBeRemoved.name);
|
||||
await this.enableDisableExtension(false, extensionToBeRemoved.name);
|
||||
const basePath = this.getExtensionsBasePath();
|
||||
const extensionFilePath = path.join(basePath, path.basename(name));
|
||||
fs.unlinkSync(extensionFilePath);
|
||||
@@ -54,10 +54,10 @@ export default class ExternalExtension extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
@bind private saveExtension(message: KeyValue): MQTTResponse {
|
||||
@bind private async saveExtension(message: KeyValue): Promise<MQTTResponse> {
|
||||
const {name, code} = message;
|
||||
const ModuleConstructor = utils.loadModuleFromText(code) as Extension;
|
||||
this.loadExtension(ModuleConstructor);
|
||||
const ModuleConstructor = utils.loadModuleFromText(code) as typeof Extension;
|
||||
await this.loadExtension(ModuleConstructor);
|
||||
const basePath = this.getExtensionsBasePath();
|
||||
/* istanbul ignore else */
|
||||
if (!fs.existsSync(basePath)) {
|
||||
@@ -75,7 +75,7 @@ export default class ExternalExtension extends Extension {
|
||||
if (match && this.requestLookup[match[1].toLowerCase()]) {
|
||||
const message = utils.parseJSON(data.message, data.message) as KeyValue;
|
||||
try {
|
||||
const response = this.requestLookup[match[1].toLowerCase()](message);
|
||||
const response = await this.requestLookup[match[1].toLowerCase()](message);
|
||||
await this.mqtt.publish(`bridge/response/extension/${match[1]}`, stringify(response));
|
||||
} catch (error) {
|
||||
logger.error(`Request '${data.topic}' failed with error: '${error.message}'`);
|
||||
@@ -85,11 +85,11 @@ export default class ExternalExtension extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
@bind private loadExtension(ConstructorClass: Extension): void {
|
||||
this.enableDisableExtension(false, ConstructorClass.constructor.name);
|
||||
@bind private async loadExtension(ConstructorClass: typeof Extension): Promise<void> {
|
||||
await this.enableDisableExtension(false, ConstructorClass.name);
|
||||
// @ts-ignore
|
||||
this.addExtension(new ConstructorClass(
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus, settings, logger));
|
||||
await this.addExtension(new ConstructorClass(this.zigbee, this.mqtt, this.state, this.publishEntityState,
|
||||
this.eventBus, settings, logger));
|
||||
}
|
||||
|
||||
private loadUserDefinedExtensions(): void {
|
||||
|
||||
+13
-11
@@ -1,5 +1,5 @@
|
||||
import http from 'http';
|
||||
import serveStatic from 'serve-static';
|
||||
import gzipStatic, {RequestHandler} from 'connect-gzip-static';
|
||||
import finalhandler from 'finalhandler';
|
||||
import logger from '../util/logger';
|
||||
import frontend from 'zigbee2mqtt-frontend';
|
||||
@@ -23,12 +23,12 @@ export default class Frontend extends Extension {
|
||||
private retainedMessages = new Map();
|
||||
private server: http.Server;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private fileServer: serveStatic.RequestHandler<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: () => void, addExtension: (extension: Extension) => void) {
|
||||
restartCallback: () => void, addExtension: (extension: Extension) => Promise<void>) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension);
|
||||
this.eventBus.onMQTTMessagePublished(this, this.onMQTTPublishMessage);
|
||||
}
|
||||
@@ -37,14 +37,16 @@ export default class Frontend extends Extension {
|
||||
this.server = http.createServer(this.onRequest);
|
||||
this.server.on('upgrade', this.onUpgrade);
|
||||
|
||||
/* istanbul ignore next */ // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const options = {setHeaders: (res: any, path: any): void => {
|
||||
if (path.endsWith('index.html')) {
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
}
|
||||
}};
|
||||
|
||||
this.fileServer = serveStatic(frontend.getPath(), options);
|
||||
/* istanbul ignore next */
|
||||
const options = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
setHeaders: (res: any, path: string): void => {
|
||||
if (path.endsWith('index.html')) {
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
}
|
||||
},
|
||||
};
|
||||
this.fileServer = gzipStatic(frontend.getPath(), options);
|
||||
this.wss = new WebSocket.Server({noServer: true});
|
||||
this.wss.on('connection', this.onWebSocketConnection);
|
||||
|
||||
|
||||
+233
-107
@@ -40,7 +40,6 @@ export default class HomeAssistant extends Extension {
|
||||
private discovered: {[s: string]: {topics: Set<string>, mockProperties: Set<string>}} = {};
|
||||
private mapping: {[s: string]: DiscoveryEntry[]} = {};
|
||||
private discoveredTriggers : {[s: string]: Set<string>}= {};
|
||||
private legacyApi = settings.get().advanced.legacy_api;
|
||||
private discoveryTopic = settings.get().advanced.homeassistant_discovery_topic;
|
||||
private statusTopic = settings.get().advanced.homeassistant_status_topic;
|
||||
private entityAttributes = settings.get().advanced.homeassistant_legacy_entity_attributes;
|
||||
@@ -48,7 +47,7 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
constructor(zigbee: Zigbee, mqtt: MQTT, state: State, publishEntityState: PublishEntityState,
|
||||
eventBus: EventBus, enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
|
||||
restartCallback: () => void, addExtension: (extension: Extension) => void) {
|
||||
restartCallback: () => void, addExtension: (extension: Extension) => Promise<void>) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension);
|
||||
if (settings.get().experimental.output === 'attribute') {
|
||||
throw new Error('Home Assistant integration is not possible with attribute output!');
|
||||
@@ -197,6 +196,7 @@ export default class HomeAssistant extends Extension {
|
||||
// Temperature
|
||||
current_temperature_topic: true,
|
||||
current_temperature_template: `{{ value_json.${temperature.property} }}`,
|
||||
command_topic_prefix: endpoint,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -219,7 +219,7 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntry.mockProperties.push(state.property);
|
||||
discoveryEntry.discovery_payload.action_topic = true;
|
||||
discoveryEntry.discovery_payload.action_template = `{% set values = ` +
|
||||
`{None:None,'idle':'off','heat':'heating','cool':'cooling','fan only':'fan'}` +
|
||||
`{None:None,'idle':'off','heat':'heating','cool':'cooling','fan_only':'fan'}` +
|
||||
` %}{{ values[value_json.${state.property}] }}`;
|
||||
}
|
||||
|
||||
@@ -266,6 +266,45 @@ export default class HomeAssistant extends Extension {
|
||||
`{{ value_json.${awayMode.property} }}`;
|
||||
}
|
||||
|
||||
const tempCalibration = firstExpose.features.find((f) => f.name === 'local_temperature_calibration');
|
||||
if (tempCalibration) {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'number',
|
||||
object_id: endpoint ? `${tempCalibration.name}_${endpoint}` : `${tempCalibration.name}`,
|
||||
mockProperties: [tempCalibration.property],
|
||||
discovery_payload: {
|
||||
value_template: `{{ value_json.${tempCalibration.property} }}`,
|
||||
command_topic: true,
|
||||
command_topic_prefix: endpoint,
|
||||
command_topic_postfix: tempCalibration.property,
|
||||
entity_category: 'config',
|
||||
icon: 'mdi:math-compass',
|
||||
...(tempCalibration.unit && {unit_of_measurement: tempCalibration.unit}),
|
||||
},
|
||||
};
|
||||
|
||||
if (tempCalibration.value_min != null) discoveryEntry.discovery_payload.min = tempCalibration.value_min;
|
||||
if (tempCalibration.value_max != null) discoveryEntry.discovery_payload.max = tempCalibration.value_max;
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
}
|
||||
|
||||
const piHeatingDemand = firstExpose.features.find((f) => f.name === 'pi_heating_demand');
|
||||
if (piHeatingDemand) {
|
||||
const discoveryEntry = {
|
||||
type: 'sensor',
|
||||
object_id: endpoint ? `${piHeatingDemand.name}_${endpoint}` : `${piHeatingDemand.name}`,
|
||||
mockProperties: [piHeatingDemand.property],
|
||||
discovery_payload: {
|
||||
value_template: `{{ value_json.${piHeatingDemand.property} }}`,
|
||||
...(piHeatingDemand.unit && {unit_of_measurement: piHeatingDemand.unit}),
|
||||
entity_category: 'diagnostic',
|
||||
icon: 'mdi:radiator',
|
||||
},
|
||||
};
|
||||
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
}
|
||||
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
} else if (firstExpose.type === 'lock') {
|
||||
assert(!endpoint, `Endpoint not supported for lock type`);
|
||||
@@ -307,26 +346,28 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
} else if (firstExpose.type === 'cover') {
|
||||
const position = exposes.find((expose) => expose.features.find((e) => e.name === 'position'));
|
||||
const hasTilt = exposes.find((expose) => expose.features.find((e) => e.name === 'tilt'));
|
||||
const tilt = exposes.find((expose) => expose.features.find((e) => e.name === 'tilt'));
|
||||
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'cover',
|
||||
mockProperties: [],
|
||||
object_id: endpoint ? `cover_${endpoint}` : 'cover',
|
||||
discovery_payload: {},
|
||||
discovery_payload: {
|
||||
command_topic_prefix: endpoint,
|
||||
},
|
||||
};
|
||||
|
||||
// For covers only supporting tilt don't discover the command/state_topic, otherwise
|
||||
// HA does not correctly reflect the state
|
||||
// - https://github.com/home-assistant/core/issues/51793
|
||||
// - https://github.com/Koenkk/zigbee-herdsman-converters/pull/2663
|
||||
if (!hasTilt || (hasTilt && position)) {
|
||||
if (!tilt || (tilt && position)) {
|
||||
discoveryEntry.discovery_payload.command_topic = true;
|
||||
discoveryEntry.discovery_payload.state_topic = !position;
|
||||
discoveryEntry.discovery_payload.command_topic_prefix = endpoint;
|
||||
}
|
||||
|
||||
if (!position && !hasTilt) {
|
||||
if (!position && !tilt) {
|
||||
discoveryEntry.discovery_payload.optimistic = true;
|
||||
}
|
||||
|
||||
@@ -340,12 +381,12 @@ export default class HomeAssistant extends Extension {
|
||||
};
|
||||
}
|
||||
|
||||
if (hasTilt) {
|
||||
assert(!endpoint, `Endpoint with tilt not supported for cover type`);
|
||||
if (tilt) {
|
||||
const t = tilt.features.find((f) => f.name === 'tilt');
|
||||
discoveryEntry.discovery_payload = {...discoveryEntry.discovery_payload,
|
||||
tilt_command_topic: true,
|
||||
tilt_status_topic: true,
|
||||
tilt_status_template: '{{ value_json.tilt }}',
|
||||
tilt_status_template: `{{ value_json.${getProperty(t)} }}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -378,8 +419,8 @@ export default class HomeAssistant extends Extension {
|
||||
// presets "on", "auto" and "smart" to cover the remaining modes in
|
||||
// ZCL. This supports a generic ZCL HVAC Fan Control fan. "Off" is
|
||||
// always a valid speed.
|
||||
let speeds =
|
||||
['off'].concat(['low', 'medium', 'high'].filter((s) => speed.values.includes(s)));
|
||||
let speeds = ['off'].concat(['low', 'medium', 'high', '1', '2', '3', '4', '5',
|
||||
'6', '7', '8', '9'].filter((s) => speed.values.includes(s)));
|
||||
let presets = ['on', 'auto', 'smart'].filter((s) => speed.values.includes(s));
|
||||
|
||||
if (['99432'].includes(definition.model)) {
|
||||
@@ -405,6 +446,7 @@ export default class HomeAssistant extends Extension {
|
||||
`{{ {${percentCommands}}[value] | default('') }}`;
|
||||
discoveryEntry.discovery_payload.speed_range_min = 1;
|
||||
discoveryEntry.discovery_payload.speed_range_max = speeds.length - 1;
|
||||
assert(presets.length !== 0);
|
||||
discoveryEntry.discovery_payload.preset_mode_state_topic = true;
|
||||
discoveryEntry.discovery_payload.preset_mode_command_topic = true;
|
||||
discoveryEntry.discovery_payload.preset_mode_value_template =
|
||||
@@ -416,15 +458,31 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
} else if (firstExpose.type === 'binary') {
|
||||
const lookup: {[s: string]: KeyValue}= {
|
||||
occupancy: {device_class: 'motion'},
|
||||
battery_low: {device_class: 'battery'},
|
||||
water_leak: {device_class: 'moisture'},
|
||||
vibration: {device_class: 'vibration'},
|
||||
contact: {device_class: 'door'},
|
||||
smoke: {device_class: 'smoke'},
|
||||
gas: {device_class: 'gas'},
|
||||
battery_low: {entity_category: 'diagnostic', device_class: 'battery'},
|
||||
button_lock: {entity_category: 'config', icon: 'mdi:lock'},
|
||||
carbon_monoxide: {device_class: 'safety'},
|
||||
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'},
|
||||
contact: {device_class: 'door'},
|
||||
eco_mode: {entity_category: 'config', icon: 'mdi:leaf'},
|
||||
expose_pin: {entity_category: 'config', icon: 'mdi:pin'},
|
||||
gas: {device_class: 'gas'},
|
||||
invert_cover: {entity_category: 'config', icon: 'mdi:arrow-left-right'},
|
||||
led_disabled_night: {entity_category: 'config', icon: 'mdi:led-off'},
|
||||
led_indication: {entity_category: 'config', icon: 'mdi:led-on'},
|
||||
legacy: {entity_category: 'config', icon: 'mdi:cog'},
|
||||
moving: {device_class: 'moving'},
|
||||
no_position_support: {entity_category: 'config', icon: 'mdi:minus-circle-outline'},
|
||||
occupancy: {device_class: 'motion'},
|
||||
power_outage_memory: {entity_category: 'config', icon: 'mdi:memory'},
|
||||
presence: {device_class: 'presence'},
|
||||
smoke: {device_class: 'smoke'},
|
||||
sos: {device_class: 'safety'},
|
||||
tamper: {device_class: 'tamper'},
|
||||
test: {entity_category: 'diagnostic', icon: 'mdi:test-tube'},
|
||||
vibration: {device_class: 'vibration'},
|
||||
water_leak: {device_class: 'moisture'},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -469,44 +527,102 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
} else if (firstExpose.type === 'numeric') {
|
||||
const lookup: {[s: string]: KeyValue} = {
|
||||
battery: {device_class: 'battery', state_class: 'measurement'},
|
||||
temperature: {device_class: 'temperature', state_class: 'measurement'},
|
||||
angle: {icon: 'angle-acute'},
|
||||
angle_axis: {icon: 'angle-acute'},
|
||||
aqi: {device_class: 'aqi', state_class: 'measurement'},
|
||||
auto_relock_time: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
away_preset_days: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
away_preset_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
battery: {device_class: 'battery', entity_category: 'diagnostic', state_class: 'measurement'},
|
||||
battery_voltage: {device_class: 'voltage', entity_category: 'diagnostic', state_class: 'measurement'},
|
||||
boost_time: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
calibration: {entity_category: 'config'},
|
||||
co2: {device_class: 'carbon_dioxide', state_class: 'measurement'},
|
||||
comfort_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
cpu_temperature: {
|
||||
device_class: 'temperature', entity_category: 'diagnostic', state_class: 'measurement',
|
||||
},
|
||||
cube_side: {icon: 'mdi:cube'},
|
||||
current: {
|
||||
device_class: 'current',
|
||||
enabled_by_default: false,
|
||||
entity_category: 'diagnostic',
|
||||
state_class: 'measurement',
|
||||
},
|
||||
current_phase_b: {
|
||||
device_class: 'current',
|
||||
enabled_by_default: false,
|
||||
entity_category: 'diagnostic',
|
||||
state_class: 'measurement',
|
||||
},
|
||||
current_phase_c: {
|
||||
device_class: 'current',
|
||||
enabled_by_default: false,
|
||||
entity_category: 'diagnostic',
|
||||
state_class: 'measurement',
|
||||
},
|
||||
deadzone_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
device_temperature: {
|
||||
device_class: 'temperature', entity_category: 'diagnostic', state_class: 'measurement',
|
||||
},
|
||||
eco2: {device_class: 'carbon_dioxide', state_class: 'measurement'},
|
||||
eco_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
energy: {device_class: 'energy', state_class: 'total_increasing'},
|
||||
formaldehyd: {state_class: 'measurement'},
|
||||
gas_density: {icon: 'mdi:google-circles-communities', state_class: 'measurement'},
|
||||
hcho: {icon: 'mdi:air-filter', state_class: 'measurement'},
|
||||
humidity: {device_class: 'humidity', state_class: 'measurement'},
|
||||
illuminance_lux: {device_class: 'illuminance', state_class: 'measurement'},
|
||||
illuminance: {
|
||||
device_class: 'illuminance', enabled_by_default: false, state_class: 'measurement',
|
||||
illuminance: {device_class: 'illuminance', enabled_by_default: false, state_class: 'measurement'},
|
||||
linkquality: {
|
||||
enabled_by_default: false,
|
||||
entity_category: 'diagnostic',
|
||||
icon: 'mdi:signal',
|
||||
state_class: 'measurement',
|
||||
},
|
||||
soil_moisture: {icon: 'mdi:water-percent', state_class: 'measurement'},
|
||||
local_temperature: {device_class: 'temperature', state_class: 'measurement'},
|
||||
max_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
max_temperature_limit: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
min_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
measurement_poll_interval: {entity_category: 'config', icon: 'mdi:clock-out'},
|
||||
occupancy_timeout: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
pm10: {device_class: 'pm10', state_class: 'measurement'},
|
||||
pm25: {device_class: 'pm25', state_class: 'measurement'},
|
||||
position: {icon: 'mdi:valve', state_class: 'measurement'},
|
||||
power: {device_class: 'power', entity_category: 'diagnostic', state_class: 'measurement'},
|
||||
precision: {entity_category: 'config', icon: 'mdi:decimal-comma-increase'},
|
||||
pressure: {device_class: 'pressure', state_class: 'measurement'},
|
||||
power: {device_class: 'power', state_class: 'measurement'},
|
||||
linkquality: {enabled_by_default: false, icon: 'mdi:signal', state_class: 'measurement'},
|
||||
current: {device_class: 'current', state_class: 'measurement'},
|
||||
voltage: {device_class: 'voltage', enabled_by_default: false, state_class: 'measurement'},
|
||||
current_phase_b: {device_class: 'current', state_class: 'measurement'},
|
||||
voltage_phase_b: {
|
||||
device_class: 'voltage', enabled_by_default: false, state_class: 'measurement',
|
||||
presence_timeout: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
requested_brightness_level: {
|
||||
enabled_by_default: false, entity_category: 'diagnostic', icon: 'mdi:brightness-5',
|
||||
},
|
||||
current_phase_c: {device_class: 'current', state_class: 'measurement'},
|
||||
voltage_phase_c: {
|
||||
device_class: 'voltage', enabled_by_default: false, state_class: 'measurement',
|
||||
},
|
||||
energy: {
|
||||
device_class: 'energy',
|
||||
state_class: 'total_increasing',
|
||||
requested_brightness_percent: {
|
||||
enabled_by_default: false, entity_category: 'diagnostic', icon: 'mdi:brightness-5',
|
||||
},
|
||||
smoke_density: {icon: 'mdi:google-circles-communities', state_class: 'measurement'},
|
||||
gas_density: {icon: 'mdi:google-circles-communities', state_class: 'measurement'},
|
||||
pm25: {device_class: 'pm25', state_class: 'measurement'},
|
||||
pm10: {device_class: 'pm10', state_class: 'measurement'},
|
||||
voc: {icon: 'mdi:air-filter', state_class: 'measurement'},
|
||||
aqi: {device_class: 'aqi', state_class: 'measurement'},
|
||||
hcho: {icon: 'mdi:air-filter', state_class: 'measurement'},
|
||||
requested_brightness_level: {enabled_by_default: false, icon: 'mdi:brightness-5'},
|
||||
requested_brightness_percent: {enabled_by_default: false, icon: 'mdi:brightness-5'},
|
||||
eco2: {device_class: 'carbon_dioxide', state_class: 'measurement'},
|
||||
co2: {device_class: 'carbon_dioxide', state_class: 'measurement'},
|
||||
local_temperature: {device_class: 'temperature', state_class: 'measurement'},
|
||||
soil_moisture: {icon: 'mdi:water-percent', state_class: 'measurement'},
|
||||
temperature: {device_class: 'temperature', state_class: 'measurement'},
|
||||
transition: {entity_category: 'config', icon: 'mdi:transition'},
|
||||
voc: {device_class: 'volatile_organic_compounds', state_class: 'measurement'},
|
||||
vibration_timeout: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
voltage: {
|
||||
device_class: 'voltage',
|
||||
enabled_by_default: false,
|
||||
entity_category: 'diagnostic',
|
||||
state_class: 'measurement',
|
||||
},
|
||||
voltage_phase_b: {
|
||||
device_class: 'voltage',
|
||||
enabled_by_default: false,
|
||||
entity_category: 'diagnostic',
|
||||
state_class: 'measurement',
|
||||
},
|
||||
voltage_phase_c: {
|
||||
device_class: 'voltage',
|
||||
enabled_by_default: false,
|
||||
entity_category: 'diagnostic',
|
||||
state_class: 'measurement',
|
||||
},
|
||||
x_axis: {icon: 'mdi:axis-x-arrow'},
|
||||
y_axis: {icon: 'mdi:axis-y-arrow'},
|
||||
z_axis: {icon: 'mdi:axis-z-arrow'},
|
||||
@@ -533,7 +649,7 @@ export default class HomeAssistant extends Extension {
|
||||
* breaking changes for sensors already existing in HA (legacy).
|
||||
*/
|
||||
if (allowsSet) {
|
||||
const discoveryEntry = {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'number',
|
||||
object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`,
|
||||
mockProperties: [firstExpose.property],
|
||||
@@ -542,32 +658,41 @@ export default class HomeAssistant extends Extension {
|
||||
command_topic: true,
|
||||
command_topic_prefix: endpoint,
|
||||
command_topic_postfix: firstExpose.property,
|
||||
min: firstExpose.value_min != null ? firstExpose.value_min : -65535,
|
||||
max: firstExpose.value_max != null ? firstExpose.value_max : 65535,
|
||||
...(firstExpose.unit && {unit_of_measurement: firstExpose.unit}),
|
||||
...lookup[firstExpose.name],
|
||||
},
|
||||
};
|
||||
|
||||
if (firstExpose.value_min != null) discoveryEntry.discovery_payload.min = firstExpose.value_min;
|
||||
if (firstExpose.value_max != null) discoveryEntry.discovery_payload.max = firstExpose.value_max;
|
||||
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
}
|
||||
} else if (firstExpose.type === 'enum') {
|
||||
const lookup: {[s: string]: KeyValue} = {
|
||||
action: {icon: 'mdi:gesture-double-tap'},
|
||||
backlight_auto_dim: {enabled_by_default: false, icon: 'mdi:brightness-auto'},
|
||||
backlight_mode: {enabled_by_default: false, icon: 'mdi:lightbulb'},
|
||||
color_power_on_behavior: {enabled_by_default: false, icon: 'mdi:palette'},
|
||||
device_mode: {enabled_by_default: false, icon: 'mdi:tune'},
|
||||
keep_time: {enabled_by_default: false, icon: 'mdi:av-timer'},
|
||||
melody: {icon: 'mdi:music-note'},
|
||||
mode_phase_control: {enabled_by_default: false, icon: 'mdi:tune'},
|
||||
mode: {enabled_by_default: false, icon: 'mdi:tune'},
|
||||
motion_sensitivity: {enabled_by_default: false, icon: 'mdi:tune'},
|
||||
operation_mode: {enabled_by_default: false, icon: 'mdi:tune'},
|
||||
power_on_behavior: {enabled_by_default: false, icon: 'mdi:power-settings'},
|
||||
power_outage_memory: {enabled_by_default: false, icon: 'mdi:power-settings'},
|
||||
sensitivity: {enabled_by_default: false, icon: 'mdi:tune'},
|
||||
sensors_type: {enabled_by_default: false, icon: 'mdi:tune'},
|
||||
switch_type: {enabled_by_default: false, icon: 'mdi:tune'},
|
||||
volume: {icon: 'mdi: volume-high'},
|
||||
backlight_auto_dim: {entity_category: 'config', icon: 'mdi:brightness-auto'},
|
||||
backlight_mode: {entity_category: 'config', icon: 'mdi:lightbulb'},
|
||||
color_power_on_behavior: {entity_category: 'config', icon: 'mdi:palette'},
|
||||
device_mode: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
effect: {enabled_by_default: false, icon: 'mdi:palette'},
|
||||
force: {enabled_by_default: false, icon: 'mdi:valve'},
|
||||
keep_time: {entity_category: 'config', icon: 'mdi:av-timer'},
|
||||
keypad_lockout: {entity_category: 'config', icon: 'mdi:lock'},
|
||||
melody: {entity_category: 'config', icon: 'mdi:music-note'},
|
||||
mode_phase_control: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
mode: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
motion_sensitivity: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
operation_mode: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
power_on_behavior: {entity_category: 'config', icon: 'mdi:power-settings'},
|
||||
power_outage_memory: {entity_category: 'config', icon: 'mdi:power-settings'},
|
||||
sensitivity: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
sensors_type: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
sound_volume: {entity_category: 'config', icon: 'mdi:volume-high'},
|
||||
switch_type: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
thermostat_unit: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
volume: {entity_category: 'config', icon: 'mdi: volume-high'},
|
||||
week: {entity_category: 'config', icon: 'mdi:calendar-clock'},
|
||||
};
|
||||
|
||||
if (firstExpose.access & ACCESS_STATE) {
|
||||
@@ -800,9 +925,11 @@ export default class HomeAssistant extends Extension {
|
||||
object_id: 'last_seen',
|
||||
mockProperties: ['last_seen'],
|
||||
discovery_payload: {
|
||||
icon: 'mdi:clock',
|
||||
value_template: '{{ value_json.last_seen }}',
|
||||
icon: 'mdi:clock',
|
||||
enabled_by_default: false,
|
||||
entity_category: 'diagnostic',
|
||||
device_class: 'timestamp',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -816,24 +943,25 @@ export default class HomeAssistant extends Extension {
|
||||
icon: 'mdi:update',
|
||||
value_template: `{{ value_json['update']['state'] }}`,
|
||||
enabled_by_default: false,
|
||||
entity_category: 'diagnostic',
|
||||
},
|
||||
};
|
||||
|
||||
configs.push(updateStateSensor);
|
||||
if (this.legacyApi) {
|
||||
const updateAvailableSensor = {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'update_available',
|
||||
mockProperties: ['update_available'],
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.update_available}}',
|
||||
enabled_by_default: false,
|
||||
},
|
||||
};
|
||||
configs.push(updateAvailableSensor);
|
||||
}
|
||||
const updateAvailableSensor = {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'update_available',
|
||||
mockProperties: ['update_available'],
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: `{{ value_json['update']['state'] == "available" }}`,
|
||||
enabled_by_default: true,
|
||||
device_class: 'update',
|
||||
entity_category: 'diagnostic',
|
||||
},
|
||||
};
|
||||
configs.push(updateAvailableSensor);
|
||||
}
|
||||
|
||||
if (isDevice && entity.settings.hasOwnProperty('legacy') && !entity.settings.legacy) {
|
||||
@@ -882,7 +1010,8 @@ export default class HomeAssistant extends Extension {
|
||||
this.discovered[discoverKey] = {topics: new Set(), mockProperties: new Set()};
|
||||
this.getConfigs(entity).forEach((config) => {
|
||||
const payload = {...config.discovery_payload};
|
||||
let stateTopic = `${settings.get().mqtt.base_topic}/${entity.name}`;
|
||||
const baseTopic = `${settings.get().mqtt.base_topic}/${entity.name}`;
|
||||
let stateTopic = baseTopic;
|
||||
if (payload.state_topic_postfix) {
|
||||
stateTopic += `/${payload.state_topic_postfix}`;
|
||||
delete payload.state_topic_postfix;
|
||||
@@ -932,19 +1061,14 @@ export default class HomeAssistant extends Extension {
|
||||
/* istanbul ignore next */
|
||||
if (availabilityEnabled) {
|
||||
payload.availability_mode = 'all';
|
||||
payload.availability.push({topic: `${settings.get().mqtt.base_topic}/${entity.name}/availability`});
|
||||
payload.availability.push({topic: `${baseTopic}/availability`});
|
||||
}
|
||||
|
||||
let commandTopic = `${settings.get().mqtt.base_topic}/${entity.name}/`;
|
||||
if (payload.command_topic_prefix) {
|
||||
commandTopic += `${payload.command_topic_prefix}/`;
|
||||
delete payload.command_topic_prefix;
|
||||
}
|
||||
commandTopic += 'set';
|
||||
if (payload.command_topic_postfix) {
|
||||
commandTopic += `/${payload.command_topic_postfix}`;
|
||||
delete payload.command_topic_postfix;
|
||||
}
|
||||
const commandTopicPrefix = payload.command_topic_prefix ? `${payload.command_topic_prefix}/` : '';
|
||||
delete payload.command_topic_prefix;
|
||||
const commandTopicPostfix = payload.command_topic_postfix ? `/${payload.command_topic_postfix}` : '';
|
||||
delete payload.command_topic_postfix;
|
||||
const commandTopic = `${baseTopic}/${commandTopicPrefix}set${commandTopicPostfix}`;
|
||||
|
||||
if (payload.command_topic) {
|
||||
payload.command_topic = commandTopic;
|
||||
@@ -955,9 +1079,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
if (payload.tilt_command_topic) {
|
||||
// Home Assistant does not support templates to set tilt (as of 2019-08-17),
|
||||
// so we (have to) use a subtopic.
|
||||
payload.tilt_command_topic = commandTopic + '/tilt';
|
||||
payload.tilt_command_topic = `${baseTopic}/${commandTopicPrefix}set/tilt`;
|
||||
}
|
||||
|
||||
if (payload.mode_state_topic) {
|
||||
@@ -965,11 +1087,11 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
if (payload.mode_command_topic) {
|
||||
payload.mode_command_topic = `${stateTopic}/set/system_mode`;
|
||||
payload.mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/system_mode`;
|
||||
}
|
||||
|
||||
if (payload.hold_command_topic) {
|
||||
payload.hold_command_topic = `${stateTopic}/set/preset`;
|
||||
payload.hold_command_topic = `${baseTopic}/${commandTopicPrefix}set/preset`;
|
||||
}
|
||||
|
||||
if (payload.hold_state_topic) {
|
||||
@@ -981,7 +1103,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
if (payload.away_mode_command_topic) {
|
||||
payload.away_mode_command_topic = `${stateTopic}/set/away_mode`;
|
||||
payload.away_mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/away_mode`;
|
||||
}
|
||||
|
||||
if (payload.current_temperature_topic) {
|
||||
@@ -1001,15 +1123,18 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
if (payload.temperature_command_topic) {
|
||||
payload.temperature_command_topic = `${stateTopic}/set/${payload.temperature_command_topic}`;
|
||||
payload.temperature_command_topic =
|
||||
`${baseTopic}/${commandTopicPrefix}set/${payload.temperature_command_topic}`;
|
||||
}
|
||||
|
||||
if (payload.temperature_low_command_topic) {
|
||||
payload.temperature_low_command_topic = `${stateTopic}/set/${payload.temperature_low_command_topic}`;
|
||||
payload.temperature_low_command_topic =
|
||||
`${baseTopic}/${commandTopicPrefix}set/${payload.temperature_low_command_topic}`;
|
||||
}
|
||||
|
||||
if (payload.temperature_high_command_topic) {
|
||||
payload.temperature_high_command_topic = `${stateTopic}/set/${payload.temperature_high_command_topic}`;
|
||||
payload.temperature_high_command_topic =
|
||||
`${baseTopic}/${commandTopicPrefix}set/${payload.temperature_high_command_topic}`;
|
||||
}
|
||||
|
||||
if (payload.fan_mode_state_topic) {
|
||||
@@ -1017,7 +1142,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
if (payload.fan_mode_command_topic) {
|
||||
payload.fan_mode_command_topic = `${stateTopic}/set/fan_mode`;
|
||||
payload.fan_mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/fan_mode`;
|
||||
}
|
||||
|
||||
if (payload.percentage_state_topic) {
|
||||
@@ -1025,7 +1150,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
if (payload.percentage_command_topic) {
|
||||
payload.percentage_command_topic = `${stateTopic}/set/fan_mode`;
|
||||
payload.percentage_command_topic = `${baseTopic}/${commandTopicPrefix}set/fan_mode`;
|
||||
}
|
||||
|
||||
if (payload.preset_mode_state_topic) {
|
||||
@@ -1033,7 +1158,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
if (payload.preset_mode_command_topic) {
|
||||
payload.preset_mode_command_topic = `${stateTopic}/set/fan_mode`;
|
||||
payload.preset_mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/fan_mode`;
|
||||
}
|
||||
|
||||
if (payload.action_topic) {
|
||||
@@ -1157,6 +1282,7 @@ export default class HomeAssistant extends Extension {
|
||||
if (entity.isDevice()) {
|
||||
payload.model = `${entity.definition.description} (${entity.definition.model})`;
|
||||
payload.manufacturer = entity.definition.vendor;
|
||||
payload.sw_version = entity.zh.softwareBuildID;
|
||||
}
|
||||
|
||||
if (settings.get().frontend?.url) {
|
||||
|
||||
@@ -330,7 +330,7 @@ export default class BridgeLegacy extends Extension {
|
||||
} catch (error) {
|
||||
logger.error(`Failed to ${lookup[action][2]} ${entity.name} (${error})`);
|
||||
// eslint-disable-next-line
|
||||
logger.error(`See https://www.zigbee2mqtt.io/information/mqtt_topics_and_message_structure.html#zigbee2mqttbridgeconfigremove for more info`);
|
||||
logger.error(`See https://www.zigbee2mqtt.io/guide/usage/mqtt_topics_and_messages.html#zigbee2mqtt-bridge-request for more info`);
|
||||
|
||||
this.mqtt.publish('bridge/log', stringify({type: `device_${lookup[action][0]}_failed`, message}));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import Extension from './extension';
|
||||
export default class OnEvent extends Extension {
|
||||
override async start(): Promise<void> {
|
||||
for (const device of this.zigbee.devices(false)) {
|
||||
await this.callOnEvent(device, 'start', {});
|
||||
this.callOnEvent(device, 'start', {});
|
||||
}
|
||||
|
||||
this.eventBus.onDeviceMessage(this, (data) => this.callOnEvent(data.device, 'message', this.convertData(data)));
|
||||
|
||||
@@ -45,7 +45,7 @@ export default class OTAUpdate extends Extension {
|
||||
if (data.type !== 'commandQueryNextImageRequest' || !data.device.definition) return;
|
||||
logger.debug(`Device '${data.device.name}' requested OTA`);
|
||||
|
||||
const supportsOTA = data.device.definition.hasOwnProperty('ota');
|
||||
let supportsOTA = data.device.definition.hasOwnProperty('ota');
|
||||
if (supportsOTA) {
|
||||
// When a device does a next image request, it will usually do it a few times after each other
|
||||
// with only 10 - 60 seconds inbetween. It doesn't make sense to check for a new update
|
||||
@@ -56,8 +56,14 @@ export default class OTAUpdate extends Extension {
|
||||
if (!check || this.inProgress.has(data.device.ieeeAddr)) return;
|
||||
|
||||
this.lastChecked[data.device.ieeeAddr] = Date.now();
|
||||
const available = await data.device.definition.ota.isUpdateAvailable(
|
||||
data.device.zh, logger, data.data);
|
||||
let available = false;
|
||||
try {
|
||||
available = await data.device.definition.ota.isUpdateAvailable(data.device.zh, logger, data.data);
|
||||
} catch (e) {
|
||||
supportsOTA = false;
|
||||
logger.debug(`Failed to check if update available for '${data.device.name}' (${e.message})`);
|
||||
}
|
||||
|
||||
const payload = this.getEntityPublishPayload(available ? 'available' : 'idle');
|
||||
this.publishEntityState(data.device, payload);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import debounce from 'debounce';
|
||||
import Extension from './extension';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import bind from 'bind-decorator';
|
||||
import utils from '../util/utils';
|
||||
|
||||
type DebounceFunction = (() => void) & { clear(): void; } & { flush(): void; };
|
||||
|
||||
@@ -24,7 +25,7 @@ export default class Receive extends Extension {
|
||||
*/
|
||||
if (data.entity.isDevice() && this.debouncers[data.entity.ieeeAddr] &&
|
||||
data.stateChangeReason !== 'publishDebounce' && data.stateChangeReason !== 'lastSeenChanged') {
|
||||
for (const key of Object.keys(data.message)) {
|
||||
for (const key of Object.keys(data.payload)) {
|
||||
delete this.debouncers[data.entity.ieeeAddr].payload[key];
|
||||
}
|
||||
}
|
||||
@@ -76,7 +77,8 @@ export default class Receive extends Extension {
|
||||
logger.warn(
|
||||
`Received message from unsupported device with Zigbee model '${data.device.zh.modelID}' ` +
|
||||
`and manufacturer name '${data.device.zh.manufacturerName}'`);
|
||||
logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html.`);
|
||||
// eslint-disable-next-line max-len
|
||||
logger.warn(`Please see: https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html`);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -106,7 +108,11 @@ export default class Receive extends Extension {
|
||||
data = {...data, device: this.zigbee.deviceByNetworkAddress(data.groupID)};
|
||||
}
|
||||
|
||||
if (!this.shouldProcess(data)) return;
|
||||
if (!this.shouldProcess(data)) {
|
||||
utils.publishLastSeen({device: data.device, reason: 'messageEmitted'},
|
||||
settings.get(), true, this.publishEntityState);
|
||||
return;
|
||||
}
|
||||
|
||||
const converters = data.device.definition.fromZigbee.filter((c) => {
|
||||
const type = Array.isArray(c.type) ? c.type.includes(data.type) : c.type === data.type;
|
||||
@@ -114,7 +120,7 @@ export default class Receive extends Extension {
|
||||
});
|
||||
|
||||
// Check if there is an available converter, genOta messages are not interesting.
|
||||
const ignoreClusters: (string | number)[] = ['genOta', 'genTime', 'genBasic'];
|
||||
const ignoreClusters: (string | number)[] = ['genOta', 'genTime', 'genBasic', 'genPollCtrl'];
|
||||
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)}'`);
|
||||
@@ -162,6 +168,9 @@ export default class Receive extends Extension {
|
||||
|
||||
if (Object.keys(payload).length) {
|
||||
publish(payload);
|
||||
} else {
|
||||
utils.publishLastSeen({device: data.device, reason: 'messageEmitted'},
|
||||
settings.get(), true, this.publishEntityState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@ export default class MQTT {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client = mqtt.connect(mqttSettings.server, options);
|
||||
// @ts-ignore https://github.com/Koenkk/zigbee2mqtt/issues/9822
|
||||
this.client.stream.setMaxListeners(0);
|
||||
|
||||
const onConnect = this.onConnect;
|
||||
this.client.on('connect', async () => {
|
||||
|
||||
Vendored
+5
-3
@@ -95,7 +95,7 @@ declare global {
|
||||
}
|
||||
|
||||
interface DefinitionExposeFeature {name: string, endpoint?: string,
|
||||
property: string, value_max?: number, value_min?: number,
|
||||
property: string, value_max?: number, value_min?: number, unit?: string,
|
||||
value_off?: string, value_on?: string, value_step?: number, values: string[], access: number}
|
||||
|
||||
interface DefinitionExpose {
|
||||
@@ -136,7 +136,8 @@ declare global {
|
||||
type StateChange = {
|
||||
entity: Device | Group, from: KeyValue, to: KeyValue, reason: string | null, update: KeyValue };
|
||||
type PermitJoinChanged = ZHEvents.PermitJoinChangedPayload;
|
||||
type LastSeenChanged = { device: Device };
|
||||
type LastSeenChanged = { device: Device,
|
||||
reason: 'deviceAnnounce' | 'networkAddress' | 'deviceJoined' | 'messageEmitted' | 'messageNonEmitted'; };
|
||||
type DeviceNetworkAddressChanged = { device: Device };
|
||||
type DeviceAnnounce = { device: Device };
|
||||
type DeviceInterview = { device: Device, status: 'started' | 'successful' | 'failed' };
|
||||
@@ -145,7 +146,8 @@ declare global {
|
||||
type DeviceLeave = { ieeeAddr: string, name: string };
|
||||
type GroupMembersChanged = {group: Group, action: 'remove' | 'add' | 'remove_all',
|
||||
endpoint: zh.Endpoint, skipDisableReporting: boolean };
|
||||
type PublishEntityState = {entity: Group | Device, message: KeyValue, stateChangeReason: StateChangeReason };
|
||||
type PublishEntityState = {entity: Group | Device, message: KeyValue, stateChangeReason: StateChangeReason,
|
||||
payload: KeyValue};
|
||||
type DeviceMessage = {
|
||||
type: ZHEvents.MessagePayloadType;
|
||||
device: Device;
|
||||
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
declare module 'zigbee2mqtt-frontend' {
|
||||
export function getPath(): string;
|
||||
}
|
||||
|
||||
declare module 'connect-gzip-static' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type RequestHandler = (req: any, res: any) => void;
|
||||
export default function gzipStatic(root: string, options?: Record<string, unknown>): RequestHandler;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
declare module 'zigbee2mqtt-frontend' {
|
||||
export function getPath(): string;
|
||||
}
|
||||
+2
-2
@@ -22,8 +22,8 @@ function getPath(): string {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
function __testingOnly_reload(): void {
|
||||
function testingOnlyReload(): void {
|
||||
load();
|
||||
}
|
||||
|
||||
export default {joinPath, getPath, __testingOnly_reload};
|
||||
export default {joinPath, getPath, testingOnlyReload};
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"type": "object",
|
||||
"title": "Active",
|
||||
"requiresRestart": true,
|
||||
"description": "Options for passive devices (routers/mains powered)",
|
||||
"description": "Options for passive devices (mostly battery powered)",
|
||||
"properties": {
|
||||
"timeout": {
|
||||
"type": "number",
|
||||
@@ -71,7 +71,7 @@
|
||||
],
|
||||
"title": "Availability",
|
||||
"requiresRestart": true,
|
||||
"description": "Checks wether devices are online/offline"
|
||||
"description": "Checks whether devices are online/offline"
|
||||
},
|
||||
"mqtt": {
|
||||
"type": "object",
|
||||
@@ -652,7 +652,7 @@
|
||||
"type": "string",
|
||||
"title": "Bind host",
|
||||
"description": "Frontend binding host",
|
||||
"default":" 0.0.0.0",
|
||||
"default": "0.0.0.0",
|
||||
"requiresRestart": true
|
||||
},
|
||||
"auth_token": {
|
||||
|
||||
@@ -85,7 +85,7 @@ const defaults: RecursivePartial<Settings> = {
|
||||
*
|
||||
* Therefore Zigbee2MQTT BY DEFAULT caches all values and resend it with every message.
|
||||
* advanced.cache_state in configuration.yaml allows to configure this.
|
||||
* https://www.zigbee2mqtt.io/configuration/configuration.html
|
||||
* https://www.zigbee2mqtt.io/guide/configuration/
|
||||
*/
|
||||
cache_state: true,
|
||||
cache_state_persistent: true,
|
||||
|
||||
+17
-1
@@ -70,6 +70,7 @@ async function getZigbee2MQTTVersion(includeCommitHash=true): Promise<{commitHas
|
||||
commitHash = commit.shortHash;
|
||||
}
|
||||
|
||||
commitHash = commitHash.trim();
|
||||
resolve({commitHash, version});
|
||||
});
|
||||
});
|
||||
@@ -280,11 +281,26 @@ const hours = (hours: number): number => 1000 * 60 * 60 * hours;
|
||||
const minutes = (minutes: number): number => 1000 * 60 * minutes;
|
||||
const seconds = (seconds: number): number => 1000 * seconds;
|
||||
|
||||
function publishLastSeen(data: eventdata.LastSeenChanged, settings: Settings, allowMessageEmitted: boolean,
|
||||
publishEntityState: PublishEntityState): void {
|
||||
/**
|
||||
* Prevent 2 MQTT publishes when 1 message event is received;
|
||||
* - In case reason == messageEmitted, receive.ts will only call this when it did not publish a
|
||||
* message based on the received zigbee message. In this case allowMessageEmitted has to be true.
|
||||
* - In case reason !== messageEmitted, controller.ts will call this based on the zigbee-herdsman
|
||||
* lastSeenChanged event.
|
||||
*/
|
||||
const allow = data.reason !== 'messageEmitted' || (data.reason === 'messageEmitted' && allowMessageEmitted);
|
||||
if (settings.advanced.last_seen && settings.advanced.last_seen !== 'disable' && allow) {
|
||||
publishEntityState(data.device, {}, 'lastSeenChanged');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
endpointNames, capitalize, getZigbee2MQTTVersion, getDependencyVersion, formatDate, objectHasProperties,
|
||||
equalsPartial, getObjectProperty, getResponse, parseJSON, loadModuleFromText, loadModuleFromFile,
|
||||
getExternalConvertersDefinitions, removeNullPropertiesFromObject, toNetworkAddressHex, toSnakeCase,
|
||||
parseEntityID, isEndpoint, isZHGroup, hours, minutes, seconds, validateFriendlyName, sleep,
|
||||
sanitizeImageParameter, isAvailabilityEnabledForDevice,
|
||||
sanitizeImageParameter, isAvailabilityEnabledForDevice, publishLastSeen,
|
||||
};
|
||||
|
||||
+6
-8
@@ -63,7 +63,7 @@ export default class Zigbee {
|
||||
|
||||
this.herdsman.on('adapterDisconnected', () => this.eventBus.emitAdapterDisconnected());
|
||||
this.herdsman.on('lastSeenChanged', (data: ZHEvents.LastSeenChangedPayload) => {
|
||||
this.eventBus.emitLastSeenChanged({device: this.resolveDevice(data.device.ieeeAddr)});
|
||||
this.eventBus.emitLastSeenChanged({device: this.resolveDevice(data.device.ieeeAddr), reason: data.reason});
|
||||
});
|
||||
this.herdsman.on('permitJoinChanged', (data: ZHEvents.PermitJoinChangedPayload) => {
|
||||
this.eventBus.emitPermitJoinChanged(data);
|
||||
@@ -98,7 +98,9 @@ export default class Zigbee {
|
||||
const device = this.resolveDevice(data.device.ieeeAddr);
|
||||
logger.debug(`Received Zigbee message from '${device.name}', type '${data.type}', ` +
|
||||
`cluster '${data.cluster}', data '${stringify(data.data)}' from endpoint ${data.endpoint.ID}` +
|
||||
(data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``));
|
||||
(data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``) +
|
||||
(device.zh.type === 'Coordinator' ? `, ignoring since it is from coordinator` : ``));
|
||||
if (device.zh.type === 'Coordinator') return;
|
||||
this.eventBus.emitDeviceMessage({...data, device});
|
||||
});
|
||||
|
||||
@@ -128,11 +130,6 @@ export default class Zigbee {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have to turn off the led
|
||||
if (settings.get().serial.disable_led) {
|
||||
await this.herdsman.setLED(false);
|
||||
}
|
||||
|
||||
// Check if we have to set a transmit power
|
||||
if (settings.get().experimental.hasOwnProperty('transmit_power')) {
|
||||
const transmitPower = settings.get().experimental.transmit_power;
|
||||
@@ -154,7 +151,8 @@ export default class Zigbee {
|
||||
} else {
|
||||
logger.warn(`Device '${name}' with Zigbee model '${data.device.zh.modelID}' and manufacturer name ` +
|
||||
`'${data.device.zh.manufacturerName}' is NOT supported, ` +
|
||||
`please follow https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`);
|
||||
// eslint-disable-next-line max-len
|
||||
`please follow https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html`);
|
||||
}
|
||||
} else if (data.status === 'failed') {
|
||||
logger.error(`Failed to interview '${name}', device has not successfully been paired`);
|
||||
|
||||
Generated
+4029
-4156
File diff suppressed because it is too large
Load Diff
+23
-24
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "1.22.0",
|
||||
"version": "1.22.1",
|
||||
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -35,9 +35,10 @@
|
||||
},
|
||||
"homepage": "https://koenkk.github.io/zigbee2mqtt",
|
||||
"dependencies": {
|
||||
"ajv": "^8.6.3",
|
||||
"ajv": "^8.8.2",
|
||||
"bind-decorator": "^1.0.11",
|
||||
"core-js": "^3.18.2",
|
||||
"connect-gzip-static": "^2.1.1",
|
||||
"core-js": "^3.19.1",
|
||||
"debounce": "^1.2.1",
|
||||
"deep-object-diff": "^1.1.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -52,38 +53,36 @@
|
||||
"object-assign-deep": "^0.4.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.5",
|
||||
"serve-static": "^1.14.1",
|
||||
"source-map-support": "^0.5.20",
|
||||
"source-map-support": "^0.5.21",
|
||||
"winston": "^3.3.3",
|
||||
"winston-syslog": "^2.4.4",
|
||||
"ws": "^8.2.3",
|
||||
"zigbee-herdsman": "0.13.164",
|
||||
"zigbee-herdsman-converters": "14.0.303",
|
||||
"zigbee2mqtt-frontend": "0.6.30"
|
||||
"ws": "^8.3.0",
|
||||
"zigbee-herdsman": "0.13.176",
|
||||
"zigbee-herdsman-converters": "14.0.336",
|
||||
"zigbee2mqtt-frontend": "0.6.46"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.5",
|
||||
"@babel/plugin-proposal-decorators": "^7.15.4",
|
||||
"@babel/preset-env": "^7.15.6",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.16.4",
|
||||
"@babel/preset-env": "^7.16.4",
|
||||
"@babel/preset-typescript": "^7.16.0",
|
||||
"@types/debounce": "^1.2.1",
|
||||
"@types/finalhandler": "^1.1.1",
|
||||
"@types/humanize-duration": "^3.25.1",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/js-yaml": "^4.0.3",
|
||||
"@types/humanize-duration": "^3.27.0",
|
||||
"@types/jest": "^27.0.3",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/object-assign-deep": "^0.4.0",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/serve-static": "^1.13.10",
|
||||
"@types/ws": "^8.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.32.0",
|
||||
"@typescript-eslint/parser": "^4.32.0",
|
||||
"babel-jest": "^27.2.4",
|
||||
"eslint": "^7.32.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"@typescript-eslint/parser": "^5.4.0",
|
||||
"babel-jest": "^27.3.1",
|
||||
"eslint": "^8.3.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-jest": "^24.5.0",
|
||||
"jest": "^27.2.4",
|
||||
"eslint-plugin-jest": "^25.3.0",
|
||||
"jest": "^27.3.1",
|
||||
"tmp": "^0.2.1",
|
||||
"typescript": "^4.4.3"
|
||||
"typescript": "^4.5.2"
|
||||
},
|
||||
"jest": {
|
||||
"coverageThreshold": {
|
||||
|
||||
@@ -205,6 +205,20 @@ describe('Availability', () => {
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should stop pinging device when it is removed', async () => {
|
||||
await resetExtension();
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
await advancedTime(utils.minutes(9));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: "bulb_color"}));
|
||||
await flushPromises();
|
||||
|
||||
await advancedTime(utils.minutes(3));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should allow to be disabled', async () => {
|
||||
settings.set(['availability'], false);
|
||||
await resetExtension();
|
||||
|
||||
+21
-9
@@ -43,7 +43,6 @@ describe('Controller', () => {
|
||||
await controller.start();
|
||||
expect(zigbeeHerdsman.constructor).toHaveBeenCalledWith({"network":{"panID":6754,"extendedPanID":[221,221,221,221,221,221,221,221],"channelList":[11],"networkKey":[1,3,5,7,9,11,13,15,0,2,4,6,8,10,12,13]},"databasePath":path.join(data.mockDir, "database.db"), "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}, "serialPort":{"baudRate":undefined,"rtscts":undefined,"path":"/dev/dummy"}}, logger);
|
||||
expect(zigbeeHerdsman.start).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.setLED).toHaveBeenCalledTimes(0);
|
||||
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(0);
|
||||
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, undefined, undefined);
|
||||
@@ -229,13 +228,6 @@ describe('Controller', () => {
|
||||
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, undefined);
|
||||
});
|
||||
|
||||
it('Start controller with disable_led', async () => {
|
||||
settings.set(['serial', 'disable_led'], true);
|
||||
await controller.start();
|
||||
expect(zigbeeHerdsman.setLED).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.setLED).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('Start controller with transmit power', async () => {
|
||||
settings.set(['experimental', 'transmit_power'], 14);
|
||||
await controller.start();
|
||||
@@ -652,9 +644,29 @@ describe('Controller', () => {
|
||||
await flushPromises();
|
||||
MQTT.publish.mockClear();
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
await zigbeeHerdsman.events.lastSeenChanged({device});
|
||||
await zigbeeHerdsman.events.lastSeenChanged({device, reason: 'deviceAnnounce'});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/remote', stringify({"brightness":255,"last_seen":1000}), { qos: 0, retain: true }, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should not publish last seen changes when reason is messageEmitted', async () => {
|
||||
settings.set(['advanced', 'last_seen'], 'epoch');
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
MQTT.publish.mockClear();
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
await zigbeeHerdsman.events.lastSeenChanged({device, reason: 'messageEmitted'});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Ignore messages from coordinator', async () => {
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/9218
|
||||
await controller.start();
|
||||
const device = zigbeeHerdsman.devices.coordinator;
|
||||
const payload = {device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10, cluster: 'genBasic', data: {modelId: device.modelID}};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(logger.debug).toHaveBeenCalledWith(`Received Zigbee message from 'Coordinator', type 'attributeReport', cluster 'genBasic', data '{"modelId":null}' from endpoint 1, ignoring since it is from coordinator`);
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -15,12 +15,12 @@ describe('Data', () => {
|
||||
it('Should return correct path when ZIGBEE2MQTT_DATA set', () => {
|
||||
const expected = tmp.dirSync().name;
|
||||
process.env.ZIGBEE2MQTT_DATA = expected;
|
||||
data.__testingOnly_reload();
|
||||
data.testingOnlyReload();
|
||||
const actual = data.getPath();
|
||||
expect(actual).toBe(expected);
|
||||
expect(data.joinPath('test')).toStrictEqual(path.join(expected, 'test'));
|
||||
delete process.env.ZIGBEE2MQTT_DATA;
|
||||
data.__testingOnly_reload();
|
||||
data.testingOnlyReload();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ jest.mock('http', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("serve-static", () =>
|
||||
jest.mock("connect-gzip-static", () =>
|
||||
jest.fn().mockImplementation((path) => {
|
||||
mockNodeStatic.variables.path = path
|
||||
return mockNodeStatic.implementation
|
||||
|
||||
+41
-35
@@ -77,7 +77,7 @@ describe('HomeAssistant extension', () => {
|
||||
"device":{
|
||||
"identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"],
|
||||
"name":"ha_discovery_group",
|
||||
"sw_version":version,
|
||||
"sw_version": version,
|
||||
},
|
||||
"max_mireds": 454,
|
||||
"min_mireds": 250,
|
||||
@@ -105,7 +105,7 @@ describe('HomeAssistant extension', () => {
|
||||
"device":{
|
||||
"identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"],
|
||||
"name":"ha_discovery_group",
|
||||
"sw_version":version,
|
||||
"sw_version": version,
|
||||
},
|
||||
"json_attributes_topic":"zigbee2mqtt/ha_discovery_group",
|
||||
"name":"ha_discovery_group",
|
||||
@@ -135,7 +135,7 @@ describe('HomeAssistant extension', () => {
|
||||
'device': {
|
||||
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
|
||||
'name': 'weather_sensor',
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
@@ -163,7 +163,7 @@ describe('HomeAssistant extension', () => {
|
||||
'device': {
|
||||
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
|
||||
'name': 'weather_sensor',
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
@@ -190,7 +190,7 @@ describe('HomeAssistant extension', () => {
|
||||
'device': {
|
||||
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
|
||||
'name': 'weather_sensor',
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
@@ -214,10 +214,11 @@ describe('HomeAssistant extension', () => {
|
||||
'name': 'weather_sensor_battery',
|
||||
'unique_id': '0x0017880104e45522_battery_zigbee2mqtt',
|
||||
'enabled_by_default': true,
|
||||
'entity_category': 'diagnostic',
|
||||
'device': {
|
||||
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
|
||||
'name': 'weather_sensor',
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
@@ -234,6 +235,7 @@ describe('HomeAssistant extension', () => {
|
||||
payload = {
|
||||
'icon': 'mdi:signal',
|
||||
'enabled_by_default': false,
|
||||
'entity_category': 'diagnostic',
|
||||
'unit_of_measurement': 'lqi',
|
||||
'state_class': 'measurement',
|
||||
'value_template': '{{ value_json.linkquality }}',
|
||||
@@ -244,7 +246,7 @@ describe('HomeAssistant extension', () => {
|
||||
'device': {
|
||||
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
|
||||
'name': 'weather_sensor',
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
@@ -268,7 +270,7 @@ describe('HomeAssistant extension', () => {
|
||||
"manufacturer":"Xiaomi",
|
||||
"model":"Aqara double key wired wall switch without neutral wire. Doesn't work as a router and doesn't support power meter (QBKG03LM)",
|
||||
"name":"wall_switch_double",
|
||||
"sw_version":version
|
||||
"sw_version": null
|
||||
},
|
||||
"json_attributes_topic":"zigbee2mqtt/wall_switch_double",
|
||||
"name":"wall_switch_double_left",
|
||||
@@ -296,7 +298,7 @@ describe('HomeAssistant extension', () => {
|
||||
"manufacturer":"Xiaomi",
|
||||
"model":"Aqara double key wired wall switch without neutral wire. Doesn't work as a router and doesn't support power meter (QBKG03LM)",
|
||||
"name":"wall_switch_double",
|
||||
"sw_version":version
|
||||
"sw_version": null
|
||||
},
|
||||
"json_attributes_topic":"zigbee2mqtt/wall_switch_double",
|
||||
"name":"wall_switch_double_right",
|
||||
@@ -330,7 +332,7 @@ describe('HomeAssistant extension', () => {
|
||||
"manufacturer":"IKEA",
|
||||
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
|
||||
"name":"bulb",
|
||||
"sw_version":version,
|
||||
"sw_version": null,
|
||||
},
|
||||
"effect":true,
|
||||
"effect_list":[
|
||||
@@ -383,7 +385,7 @@ describe('HomeAssistant extension', () => {
|
||||
'device': {
|
||||
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
|
||||
'name': 'weather_sensor',
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
@@ -410,7 +412,7 @@ describe('HomeAssistant extension', () => {
|
||||
'device': {
|
||||
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
|
||||
'name': 'weather_sensor',
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
@@ -437,7 +439,7 @@ describe('HomeAssistant extension', () => {
|
||||
'device': {
|
||||
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
|
||||
'name': 'weather_sensor',
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
@@ -522,7 +524,7 @@ describe('HomeAssistant extension', () => {
|
||||
'device': {
|
||||
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
|
||||
'name': 'weather_sensor',
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
'model': 'custom model',
|
||||
'manufacturer': 'Not from Xiaomi',
|
||||
},
|
||||
@@ -570,7 +572,7 @@ describe('HomeAssistant extension', () => {
|
||||
"manufacturer": "Xiaomi",
|
||||
"model": "Aqara single key wired wall switch without neutral wire. Doesn't work as a router and doesn't support power meter (QBKG04LM)",
|
||||
"name": "my_switch",
|
||||
"sw_version": version
|
||||
"sw_version": null
|
||||
},
|
||||
"json_attributes_topic": "zigbee2mqtt/my_switch",
|
||||
"name": "my_light_name_override",
|
||||
@@ -647,7 +649,7 @@ describe('HomeAssistant extension', () => {
|
||||
"zigbee2mqtt_0x0017880104e45548"
|
||||
],
|
||||
"name":"fan",
|
||||
"sw_version":version,
|
||||
"sw_version": null,
|
||||
"model":"Universal wink enabled white ceiling fan premier remote control (99432)",
|
||||
"manufacturer":"Hampton Bay"
|
||||
},
|
||||
@@ -679,7 +681,7 @@ describe('HomeAssistant extension', () => {
|
||||
"manufacturer":"TuYa",
|
||||
"model":"Radiator valve with thermostat (TS0601_thermostat)",
|
||||
"name":"TS0601_thermostat",
|
||||
"sw_version": version,
|
||||
"sw_version": null,
|
||||
},
|
||||
"hold_command_topic":"zigbee2mqtt/TS0601_thermostat/set/preset",
|
||||
"hold_modes":[
|
||||
@@ -734,7 +736,7 @@ describe('HomeAssistant extension', () => {
|
||||
{
|
||||
identifiers: [ 'zigbee2mqtt_0x0017880104e45551' ],
|
||||
name: 'smart vent',
|
||||
sw_version: version,
|
||||
sw_version: null,
|
||||
model: 'Smart vent (SV01)',
|
||||
manufacturer: 'Keen Home'
|
||||
},
|
||||
@@ -768,7 +770,7 @@ describe('HomeAssistant extension', () => {
|
||||
'device': {
|
||||
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
|
||||
'name': 'weather_sensor',
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
@@ -895,7 +897,7 @@ describe('HomeAssistant extension', () => {
|
||||
'device': {
|
||||
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
|
||||
'name': 'weather_sensor',
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
@@ -1012,7 +1014,7 @@ describe('HomeAssistant extension', () => {
|
||||
'device': {
|
||||
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
|
||||
'name': 'weather_sensor',
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
@@ -1092,7 +1094,7 @@ describe('HomeAssistant extension', () => {
|
||||
'device': {
|
||||
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
|
||||
'name': 'weather_sensor_renamed',
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
@@ -1126,7 +1128,7 @@ describe('HomeAssistant extension', () => {
|
||||
"zigbee2mqtt_0x0017880104e45522"
|
||||
],
|
||||
"name":"weather_sensor_renamed",
|
||||
"sw_version": version,
|
||||
"sw_version": null,
|
||||
"model":"Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)",
|
||||
"manufacturer":"Xiaomi"
|
||||
}
|
||||
@@ -1161,7 +1163,7 @@ describe('HomeAssistant extension', () => {
|
||||
'device': {
|
||||
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
|
||||
'name': 'weather_sensor_renamed',
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
@@ -1180,8 +1182,8 @@ describe('HomeAssistant extension', () => {
|
||||
const payload = {
|
||||
"payload_on":true,
|
||||
"payload_off":false,
|
||||
"value_template":"{{ value_json.update_available}}",
|
||||
"enabled_by_default": false,
|
||||
"value_template":`{{ value_json['update']['state'] == "available" }}`,
|
||||
"enabled_by_default": true,
|
||||
"state_topic":"zigbee2mqtt/bulb",
|
||||
"json_attributes_topic":"zigbee2mqtt/bulb",
|
||||
"name":"bulb update available",
|
||||
@@ -1191,11 +1193,13 @@ describe('HomeAssistant extension', () => {
|
||||
"zigbee2mqtt_0x000b57fffec6a5b2"
|
||||
],
|
||||
"name":"bulb",
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
|
||||
"manufacturer":"IKEA"
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
'device_class': 'update',
|
||||
'entity_category': 'diagnostic'
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -1230,7 +1234,7 @@ describe('HomeAssistant extension', () => {
|
||||
"zigbee2mqtt_0x0017880104e45520"
|
||||
],
|
||||
"name":"button",
|
||||
"sw_version": version,
|
||||
"sw_version": null,
|
||||
"model":"Aqara wireless switch (WXKG11LM)",
|
||||
"manufacturer":"Xiaomi"
|
||||
}
|
||||
@@ -1254,7 +1258,7 @@ describe('HomeAssistant extension', () => {
|
||||
"zigbee2mqtt_0x0017880104e45520"
|
||||
],
|
||||
"name":"button",
|
||||
"sw_version": version,
|
||||
"sw_version": null,
|
||||
"model":"Aqara wireless switch (WXKG11LM)",
|
||||
"manufacturer":"Xiaomi"
|
||||
}
|
||||
@@ -1418,7 +1422,7 @@ describe('HomeAssistant extension', () => {
|
||||
"zigbee2mqtt_0x0017880104e45520"
|
||||
],
|
||||
"name":"button",
|
||||
"sw_version": version,
|
||||
"sw_version": null,
|
||||
"model":"Aqara wireless switch (WXKG11LM)",
|
||||
"manufacturer":"Xiaomi"
|
||||
}
|
||||
@@ -1609,7 +1613,7 @@ describe('HomeAssistant extension', () => {
|
||||
'device': {
|
||||
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
|
||||
'name': 'weather_sensor',
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
@@ -1638,7 +1642,7 @@ describe('HomeAssistant extension', () => {
|
||||
"device":{
|
||||
"identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"],
|
||||
"name":"ha_discovery_group",
|
||||
"sw_version":version,
|
||||
"sw_version": version,
|
||||
},
|
||||
"json_attributes_topic":"zigbee2mqtt/ha_discovery_group",
|
||||
"max_mireds": 454,
|
||||
@@ -1678,7 +1682,7 @@ describe('HomeAssistant extension', () => {
|
||||
"manufacturer":"IKEA",
|
||||
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
|
||||
"name":"bulb",
|
||||
"sw_version":version
|
||||
"sw_version": null
|
||||
},
|
||||
"enabled_by_default":false,
|
||||
"icon":"mdi:clock",
|
||||
@@ -1686,7 +1690,9 @@ describe('HomeAssistant extension', () => {
|
||||
"name":"bulb last seen",
|
||||
"state_topic":"zigbee2mqtt/bulb",
|
||||
"unique_id":"0x000b57fffec6a5b2_last_seen_zigbee2mqtt",
|
||||
"value_template":"{{ value_json.last_seen }}"
|
||||
"value_template":"{{ value_json.last_seen }}",
|
||||
"device_class": "timestamp",
|
||||
"entity_category": "diagnostic"
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -1718,7 +1724,7 @@ describe('HomeAssistant extension', () => {
|
||||
'device': {
|
||||
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
|
||||
'name': 'weather_sensor',
|
||||
'sw_version': version,
|
||||
'sw_version': null,
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
'configuration_url': 'http://zigbee.mqtt/#/device/0x0017880104e45522/info'
|
||||
|
||||
@@ -265,6 +265,28 @@ describe('OTA update', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Should respond with NO_IMAGE_AVAILABLE when update available request fails', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
device.endpoints[0].commandResponse.mockClear();
|
||||
const data = {imageType: 12382};
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {throw new Error('Nothing to find here')})
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10};
|
||||
logger.info.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, logger, {"imageType": 12382});
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 0x98});
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({"update_available":false,"update":{"state":"idle"}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should check for update when device requests it and it is not available', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
device.endpoints[0].commandResponse.mockClear();
|
||||
|
||||
+1
-20
@@ -242,25 +242,6 @@ describe('Receive', () => {
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
|
||||
});
|
||||
|
||||
it('WSDCGQ11LM pressure precision from non ZCL properties', async () => {
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 1);
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
let payload = {data: {"65281":{"1":2985,"4":5032,"5":9,"6":[0,1],"10":0,"100":2345,"101":4608,"102":91552}}, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({"battery":91,"voltage":2985,"temperature":23.5,"humidity":46.08,"pressure":915.5});
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
payload = {data: {"16":9354,"20":-1,"measuredValue":915}, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({"battery":91,"voltage":2985,"temperature":23.5,"humidity":46.08,"pressure":935.4});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with voltage 3010', async () => {
|
||||
const device = zigbeeHerdsman.devices.WXKG02LM_rev1;
|
||||
const data = {'65281': {'1': 3010}}
|
||||
@@ -422,7 +403,7 @@ describe('Receive', () => {
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(logger.warn).toHaveBeenCalledWith(`Received message from unsupported device with Zigbee model 'notSupportedModelID' and manufacturer name 'notSupportedMfg'`);
|
||||
expect(logger.warn).toHaveBeenCalledWith(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html.`);
|
||||
expect(logger.warn).toHaveBeenCalledWith(`Please see: https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html`);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const mock = {
|
||||
subscribe: jest.fn(),
|
||||
reconnecting: false,
|
||||
on: jest.fn(),
|
||||
stream: {setMaxListeners: jest.fn()}
|
||||
};
|
||||
|
||||
const mockConnect = jest.fn().mockReturnValue(mock);
|
||||
|
||||
@@ -215,7 +215,6 @@ const mock = {
|
||||
events[type] = handler;
|
||||
},
|
||||
stop: jest.fn(),
|
||||
setLED: jest.fn(),
|
||||
getDevices: jest.fn().mockImplementation(() => {
|
||||
return Object.values(devices).filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr));
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user