Compare commits

...
216 Commits
Author SHA1 Message Date
Koen Kanters 31e56785e4 1.11.0 2020-02-26 18:25:39 +01:00
Koen Kanters 962282b38c Undev 2020-02-26 18:25:36 +01:00
Koen Kanters e241d5ac8a Update converters. 2020-02-26 18:22:36 +01:00
Koen Kanters 1a973d0cd7 Update converters. 2020-02-26 17:30:38 +01:00
Koen Kanters 11ec191580 Don't discover illuminance_lux for RTCGQ11LM. https://github.com/Koenkk/zigbee-herdsman-converters/issues/1017 2020-02-26 17:20:31 +01:00
Koen Kanters 95788b8847 Discover illuminance_lux sensor. 2020-02-25 20:37:42 +01:00
Koen Kanters a36f11a4fb Update converters. 2020-02-25 20:20:52 +01:00
Koen Kanters 9cc84feb67 Allow to rename groups through zigbee2mqtt/bridge/config/rename. https://github.com/Koenkk/zigbee2mqtt/issues/2991 2020-02-23 21:51:30 +01:00
Koen Kanters dd82b29629 Update converters. https://github.com/Koenkk/zigbee2mqtt/issues/2967 2020-02-23 20:47:38 +01:00
Koen Kanters 2a9d0d1875 Update converters. https://github.com/Koenkk/zigbee2mqtt/issues/2921 2020-02-23 20:26:13 +01:00
Koen Kanters 66173ff689 Update herdsman. 2020-02-23 20:02:29 +01:00
Koen Kanters 645da4e3c0 Update herdsman. 2020-02-23 19:01:02 +01:00
shades684 409df4c193 Matches this pull request: https://github.com/Koenkk/zigbee-herdsman-converters/pull/1010 (#2988) 2020-02-23 13:33:30 +01:00
Koen Kanters aca32e2f02 Update converters. 2020-02-22 19:03:06 +01:00
Koen Kanters 79116a1158 Fix Hue bulbs in a group controlled by a Hue dimmer not reporting off state. #2882 2020-02-22 18:45:30 +01:00
Koen Kanters ad59ff57bc Also remove group from herdsman on remove. #2265 2020-02-22 15:53:05 +01:00
Koen Kanters df67c429bf Undiscover ota_update sensor on device remove. 2020-02-22 00:32:15 +01:00
Koen Kanters 6b5b4ab433 Log when update is availabe. https://github.com/Koenkk/zigbee2mqtt/commit/58d987b52328a40b6ac92f343f3e8ceeae3f2a10#r37363068 2020-02-20 20:01:26 +01:00
Koen Kanters c25c32f1be Update converters. 2020-02-19 22:12:25 +01:00
Koen Kanters 9b644ff82d Update herdsman. 2020-02-19 21:46:25 +01:00
Koen Kanters 0289a6c30d Update converters. 2020-02-18 22:07:34 +01:00
Koen Kanters 2b3cdebe41 Fix AC08559 missing Home Assistant colorxy mapping. #2966 2020-02-18 22:05:32 +01:00
Takisg 6ef4fac30a GL-C-007S (#2955)
Gledopto GL-C-007S

Add support for Gledopto GL-C-007S
Zigbee LED controller RGBW plus model
2020-02-17 20:15:57 +01:00
Koen Kanters f0903cd02c Discover action sensor for WXKG11LM. #2953 2020-02-16 20:55:40 +01:00
Koen Kanters 58d987b523 Implement update_available attribute and discover Home Assistant sensor. #2948 2020-02-16 16:00:15 +01:00
Koen Kanters 9a87abd0f4 Update herdsman and converters. 2020-02-15 16:39:35 +01:00
timstanley1985andKoen Kanters b379830e36 Log ota message to MQTT (#2936)
* Log ota message to MQTT

* Fix lint

* Amend MQTT log messages

* Fix typo

* Fix lint

* some changes.

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-02-14 23:41:34 +01:00
Koen Kanters 97af26b340 Also poll hue bulbs on button 1/2 press of Hue dimmer switch. #2882 2020-02-14 22:41:04 +01:00
rbrack42 7de51340a0 Adding support for Sylvania 72567 Flushmount Light (#2943) 2020-02-14 16:52:07 +01:00
Koen Kanters bbf432eae5 Update herdsman and herdsman-converters. 2020-02-13 21:35:37 +01:00
Koen Kanters eac15a748a npm ci. https://github.com/Koenkk/zigbee2mqtt/issues/2918 2020-02-13 21:31:14 +01:00
Koen Kanters 87faaa1817 Don't crash when devices doesn't respond to read modelID after update. https://github.com/Koenkk/zigbee2mqtt/issues/2921 2020-02-13 21:10:44 +01:00
Koen Kanters 984bad4cb3 Major OTA improvements. https://github.com/Koenkk/zigbee2mqtt/issues/2921 2020-02-09 20:44:37 +01:00
Koen Kanters 13b996eb70 Update converters. 2020-02-08 23:41:22 +01:00
Ricardo Pereira fb7c3a31dd Adding availability support to Commercial Electric Downlight model 53170161 (#2922) 2020-02-08 23:23:44 +01:00
Boris PrüßmannandKoen Kanters 06b6d66090 Feedback requested: add polling for commandOn & commandOff (#2730)
* Add polling for commandOn & commandOf.

When using groups together with devices that do not support reporting, this is needed
to have the new state reflected.

* Update deviceReport.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-02-08 20:53:16 +01:00
Koen Kanters 3d897c55e2 v2.7.0 rocks. 2020-02-08 20:50:07 +01:00
Koen Kanters c5fb651481 Try with v2.8.0 2020-02-08 20:46:26 +01:00
Koen Kanters 7d52a013ad Something wrong with the alpine 12 image? 2020-02-08 20:27:28 +01:00
Koen Kanters e278bb0145 Downgrade node. 2020-02-08 20:20:27 +01:00
Koen Kanters b36da25d68 Attempt to fix Docker build. 2020-02-08 20:17:13 +01:00
Koen Kanters 83291e242e OTA https://github.com/Koenkk/zigbee-herdsman/issues/60 (#2920)
* ota

* Bla

* Update converters.
2020-02-08 19:55:26 +01:00
Koen Kanters 99dc214fbe Update herdsman. 2020-02-07 19:50:48 +01:00
Koen Kanters 5e152f2ce4 Promote to dev. 2020-02-05 21:45:47 +01:00
Koen Kanters 8df6be8813 1.10.0 2020-02-05 21:41:31 +01:00
Koen Kanters 415e764592 Undev 2020-02-05 21:40:58 +01:00
Koen Kanters e8d9cbc19b Update converters. 2020-02-05 21:38:42 +01:00
svh1985 e473590dd9 Added log_file name setting (#2894)
* Added log_file name setting

Added log_file to schema while preserving the default log.txt setting.
Added option to logger to use the filename and support for the %timestamp% var.

* Update logger.js

Added the filename to the console/log output.
2020-02-05 19:37:13 +01:00
Koen Kanters 558153ebd6 Update converters. 2020-02-04 20:28:47 +01:00
Koen Kanters a07a28cb65 Update herdsman. 2020-02-02 21:05:25 +01:00
Koen Kanters 6090f124c0 Update converters. 2020-02-02 14:30:53 +01:00
Koen Kanters 5d8a9a3e51 Update converters. 2020-02-01 17:35:20 +01:00
Koen Kanters 313edcc445 Keep latest log in log.txt (not in log1.txt, etc.). https://github.com/Koenkk/zigbee2mqtt/issues/2855 2020-01-31 23:13:15 +01:00
Koen Kanters 4926fb85ae Await permit join before continuing with startup. https://github.com/Koenkk/zigbee2mqtt/issues/2617 2020-01-31 22:52:39 +01:00
Koen Kanters 0111681cb7 Update converters. https://github.com/Koenkk/zigbee2mqtt/issues/2850 2020-01-31 22:42:24 +01:00
Koen Kanters 9bc427e32f Update converters. 2020-01-31 21:05:13 +01:00
Koen Kanters 36e39e2f02 Update herdsman. 2020-01-31 20:31:02 +01:00
Rob Long 58e08d1443 Update homeassistant.js (#2861)
Added 5900131C5
2020-01-31 16:27:46 +01:00
Koen Kanters 2e19a8f916 Update converters. 2020-01-30 20:08:31 +01:00
Koen Kanters 5488504809 Update converters. 2020-01-29 21:38:37 +01:00
Michal Szymura 82fb79820a Added support for Sengled E11-U2E (#2853) 2020-01-29 21:21:22 +01:00
Koen Kanters c87ccea9ba Update converters. 2020-01-29 18:34:40 +01:00
Koen Kanters b6cb7acf7e Update converters. 2020-01-27 22:20:48 +01:00
Markus Peter ad286cbbc6 Home assistant config for Philips Hue Discover 17435/30/P7 (#2834) 2020-01-27 21:48:04 +01:00
Koen Kanters fe62ceb0f9 Add unbind for default_bind_group. https://github.com/Koenkk/zigbee2mqtt/issues/2772 2020-01-27 20:56:11 +01:00
Koen Kanters 61a3f0e839 Update converters and fix tests for real. 2020-01-26 21:38:04 +01:00
Koen Kanters b58cbd4883 Fix tests. 2020-01-26 19:57:15 +01:00
a-bailey 833bfcd81a added home assistant extension for LS12128 (#2825) 2020-01-26 19:44:44 +01:00
Philipp Bender 9e413bf35a Added homeassistant mapping for OSRAM A60 DIM Z3 (#2824) 2020-01-26 19:39:43 +01:00
Koen Kanters 3c28ed8f0b Update herdsman. https://github.com/Koenkk/zigbee2mqtt/issues/2749 2020-01-26 19:21:30 +01:00
Koen Kanters 20ba6a5274 Add icon and unit to Home Assistant linkquality sensor. #2806 2020-01-25 20:43:51 +01:00
Koen Kanters bd30b0b4a3 Update converters. 2020-01-25 20:00:22 +01:00
Koen Kanters 93e96a2e5e Update converters. 2020-01-23 21:00:50 +01:00
Koen Kanters e1d51e6171 Fix error when configure section of device is removed. 2020-01-23 20:42:10 +01:00
Marco AddarioandKoen Kanters cb855373ac Added support for Linkind Smart Zigbee LED (#2804)
* Added support for Linkind Smart Zigbee LED

Added support for Linkind Smart Zigbee LED bulb
( https://www.linkind.com/product-page/smart-zigbee-led-9w-a19-bulb-dimmable-tunable-hub-required-alexa-control )

* Update homeassistant.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-01-23 19:51:41 +01:00
Koen Kanters 145f8a577d Update converters. 2020-01-22 21:34:15 +01:00
Koen Kanters 0d3fa83ab1 Update herdsman. 2020-01-22 20:52:12 +01:00
Koen Kanters dae52a22d7 Update converters. 2020-01-21 21:19:11 +01:00
Koen Kanters e94b34caad Update converters. 2020-01-20 21:36:19 +01:00
wrcvr4andKoen Kanters 43c745df1d Support NUET56-DL27LX1.2 (#2784)
* Support DL27LX1.2

* Update homeassistant.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-01-20 17:48:33 +01:00
Koen Kanters 2e958f863f Update herdsman. 2020-01-18 23:05:05 +01:00
Koen Kanters 3b1ef5179b Update herdsman. 2020-01-18 20:27:53 +01:00
Koen Kanters ae5330d090 Update converters. 2020-01-17 22:51:37 +01:00
Twan Coenraad dd0ca2a392 Add keepalive option for MQTT (#2767) 2020-01-17 21:38:46 +01:00
Koen Kanters 39ed95371e Log when using non-valid log_level. #2706 2020-01-16 23:33:17 +01:00
Koen Kanters 3f6a95bc0b Don't setup reporting for BASICZBR3 as it doesn't support it. #2740 2020-01-16 22:52:13 +01:00
Koen Kanters 42f1a37499 Promote to dev 2020-01-16 22:44:11 +01:00
Koen Kanters 287f843bbb 1.9.0 2020-01-16 22:32:17 +01:00
Koen Kanters cd4319d281 Remove dev from version. 2020-01-16 22:32:13 +01:00
Viet DzungandKoen Kanters 2188937eea Home Assistant 0.104 breaking change: support new HA separate lock state (#2753)
* Support new HA separate lock state for Xiaomi ZNMS12LM and ZNMS12LM

https://github.com/home-assistant/home-assistant/pull/29808

* Update homeassistant.js

* Update homeassistant.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-01-16 22:25:58 +01:00
Koen Kanters a9cce7c222 Update converters. 2020-01-16 21:27:28 +01:00
jungmonam f9d85df51b Update homeassistant.js (#2758) 2020-01-16 21:17:55 +01:00
Ashton Kemerling 64315d0fc9 Home assistant bindings for Sylvania 72569 (#2754)
* Home assistant bindings for Sylvania 75269

Related to https://github.com/Koenkk/zigbee-herdsman-converters/pull/913.

* Fix Typo (Again)

Replace 75269 with 72569
2020-01-16 21:15:19 +01:00
MrNickllandKoen Kanters eba84647c1 Update TH1123ZB Home Assistant config (#2752)
* Update homeassistant.js

Added config parameters for Sinope TH1123ZB line voltage thermostat:
- Keypad lockout using the code for Stelpro thermostats.
- Power sensor.

* Update homeassistant.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-01-16 21:08:14 +01:00
Boris Prüßmann 7d268732de Let ubisys devices expose power consumption via Home Assistant (#2743) 2020-01-15 12:58:03 +01:00
Boris PrüßmannandKoen Kanters a79b2bae81 Add support for Philips Fair Hue ceiling light (#2742)
* Added endpoints for Busch-Jaeger

* Added RM01 to homeassistant.js

* Updated mapping to reflect latest change in converter

* Renamed endpoint according to converted PR

* Support for Philips Hue Fair

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-01-15 12:57:08 +01:00
Koen Kanters 5c18734e68 Update converters. 2020-01-14 19:06:35 +01:00
Boris PrüßmannandKoen Kanters 2234fd1e8a Added endpoints for Busch-Jaeger (#2723)
* Added endpoints for Busch-Jaeger

* Added RM01 to homeassistant.js

* Updated mapping to reflect latest change in converter

* Renamed endpoint according to converted PR

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-01-14 18:47:32 +01:00
Koen Kanters 80838bd248 Update converters. 2020-01-13 22:09:21 +01:00
lukeIamandKoen Kanters 4a0059afcd Allow attribute and json in parallel (#2712)
* Allow attribute and json in parallel

* Fix tests

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-01-12 15:07:06 +01:00
Martin Raatz 4a14706638 adding support for LUPUS - 1 chanel relay with ZigBee (#2711) 2020-01-12 14:31:06 +01:00
Koen Kanters 9890fb3ae2 Update herdsman and converters. 2020-01-11 18:04:30 +01:00
jungmonam 15d984e21b Update homeassistant.js (#2720) 2020-01-11 16:20:45 +01:00
Koen Kanters 57a5253203 Discovery battery sensor for E1744. #2715 2020-01-11 16:17:22 +01:00
TilmanK 7a8bef3e60 Fixed check if entity is of type "device" in deviceConfigure.js. (#2713)
The current statement (!entity.type === 'device') will always evaluate to false since it compares a boolean to a string.
2020-01-11 15:50:00 +01:00
Daniel Chesterton 77fc851236 Update Docker to Node 12 and use multistage builds to reduce image size (#2703) 2020-01-11 15:11:27 +01:00
Koen Kanters e76633519f Update converters. 2020-01-09 22:10:16 +01:00
Roberto Barbero a652df6ed7 Added Sinope TH1400ZB and TH1500ZB (#2708)
Added for Home Assistant MQTT autodiscovery
2020-01-09 21:56:03 +01:00
Koen Kanters cb0b0b5af9 Clear Home Assistant MQTT discovery on device remove. #2678 2020-01-09 21:47:19 +01:00
Koen Kanters 3a463b0782 Update converters. 2020-01-08 20:06:01 +01:00
TilmanKandKoen Kanters 0df966755c Corrected several typos in "Successfully" within log messages (#2697)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-01-08 19:29:22 +01:00
Daniel Chesterton 94d0253f27 Update availability_blacklist and availability_whitelist to allow for friendly names (#2684) 2020-01-08 18:02:49 +01:00
Jevgeni Kiski 4ee34fd637 LLKZMK11LM Supports power, temperature and consumption (#2657)
* LLKZMK11LM Supports power, temperature and consumption

LLKZMK11LM already sends power, temperature and consumption in mqtt json.

```
info  2020-01-02 20:06:20: MQTT publish: topic 'zigbee2mqtt/front_outdoor_lights', payload '{"state_l1":"ON","linkquality":54,"state_l2":"OFF","power":35.1,"consumption":2.89,"temperature":33}'
```

Not sure in which units `consumption` is. Not adding right now.

* LLKZMK11LM power consumption

* Linter

* mdi-flash
2020-01-08 17:57:40 +01:00
Carl de Billy cabb1f3979 Added <operation>_failed logging (#2636)
* Added <operation>_failed logging
for external tools (like Zigbee2MqttAssistant), this will allow the tool to know when an operation is failed.
https://github.com/Koenkk/zigbee2mqtt/issues/2223

* Added _failed logging to binding operations too

* Added _failed to group operations
2020-01-07 21:06:48 +01:00
Koen Kanters 8a72916d7d Update converters. 2020-01-07 20:08:12 +01:00
Koen Kanters c041f0cddc Ignore setup reporting for closuresWindowCovering ZNLDP12LM. #2611 2020-01-07 19:59:43 +01:00
Koen Kanters 6245c729cf Update converters. 2020-01-06 21:55:43 +01:00
Carl de BillyandKoen Kanters e5b2f71fcd Added support for Stelpro Ki & Stelpro Maestro (#2628)
* fix potential null exception

* Added support for Stelpro Ki

* Stelpro Ki: Added support for action_mode

(cherry picked from commit 0f740f1aedbedb28d64245531f10a9bfd985e49b)

* Stelpro Ki: fix "lock" status update

(cherry picked from commit 6ac94411183d92d1f658a7d17a724e50587b8571)

* Fixed lint issues

* Stelpro: Fixed local_temperature sensor

* Added support for Stelpro Maestro

* Fix lint

* HASS: Fixed support for action_topic/action_template

* Prevent a non supported device from causing a null-ref

* Update naming.

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-01-06 21:36:45 +01:00
Koen Kanters d0e9889f7d Fix race condition when Zigbee starts. #2591 2020-01-03 23:43:04 +01:00
Koen Kanters d3aba2689c Update converters. 2020-01-03 23:29:28 +01:00
jasperroandKoen Kanters 7a6d5f9a42 Added hue/saturation mode and tamper to home assistant (#2603)
* Added hue/saturation mode to home assistant extension

* Added tamper to homeassistant

* Fix ESlint stuff

* Update homeassistant.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-01-03 22:28:21 +01:00
AThomsenandKoen Kanters 89fb54d5c8 Add Nordtronic Box Dimmer 2.0 98425031 (#2629)
* Add Nordtronic Box Dimmer 2.0 98425031

* Update homeassistant.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-01-03 18:55:57 +01:00
Samuel Gaus afaed3f783 Support new IKEA GUNNARP round panel (#2653)
See: https://github.com/Koenkk/zigbee-herdsman-converters/pull/865
2020-01-03 09:09:28 +01:00
Kryzek dc6bf51b8e Update homeassistant.js (#2651)
Expose battery information to Home Assistant with couple of Trust battery-powered sensors.
Related to https://github.com/Koenkk/zigbee-herdsman-converters/pull/856
2020-01-03 09:04:21 +01:00
Koen Kanters 5f97f19d7f Add brightness_move for groups. #2620 2019-12-31 13:52:23 +01:00
Jack Wilsdon f04e01ff54 Use correct Home Assistant configuration for GL-S-007Z (#2616) 2019-12-27 15:56:24 +01:00
Koen Kanters f069a7e58e Update herdsman and converters. 2019-12-26 22:46:44 +01:00
Phi Dong 2197d4127d Update homeassistant.js (#2606)
Add support for Ecosmart-ZBT-A19-CCT-Bulb (A9A19A60WESDZ02) to Home Assistant
2019-12-26 21:32:34 +01:00
Martin Brooksbank 89fe701197 Added Ajax Online LED Strip HomeAssistant Mapping (#2592) 2019-12-23 21:08:40 +01:00
Koen Kanters e63d196966 Skip configure when device does not require it. https://github.com/Koenkk/zigbee2mqtt/issues/2588 2019-12-23 20:48:37 +01:00
fb22 9db1467ed8 Add 4023330P7 (#2574)
Add support for Philips Hue white suspension Amaze
2019-12-23 20:08:43 +01:00
Anthony Lavado 3bab32c1d7 Add support for the EcoSmart D1533 (#2570) 2019-12-23 19:56:55 +01:00
Koen Kanters 66b3bcb639 Update herdsman. 2019-12-19 21:52:45 +01:00
Koen Kanters 6f7d29392e Update herdsman and converters. 2019-12-17 21:35:47 +01:00
Koen Kanters 83e10f446e Update converters. 2019-12-17 19:59:22 +01:00
Koen Kanters af4184e3ff Always log 'No converter available...` to debug. #2543 2019-12-17 19:19:57 +01:00
Koen Kanters 316755a5c8 Fix get device group membership with friendly_name containing slashes. #2546 2019-12-17 19:02:11 +01:00
Wilmar den Ouden 5083bbcc16 feat: create environment variable for the config file (#2519)
Signed-off-by: wilmardo <info@wilmardenouden.nl>
2019-12-16 20:49:25 +01:00
Koen Kanters 981612c64c Support tint_scene for group. https://github.com/Koenkk/zigbee-herdsman-converters/issues/281 2019-12-16 20:17:58 +01:00
Koen Kanters 960f9e44ad Update herdsman. 2019-12-15 20:11:22 +01:00
Koen Kanters 8545a341c9 Promote to dev 2019-12-15 20:04:10 +01:00
Koen Kanters da4d26af62 1.8.0 2019-12-15 17:33:54 +01:00
Koen Kanters f64bf16f6f Update converters. 2019-12-15 16:37:00 +01:00
Koen Kanters 0b9997b7a5 Update converters. 2019-12-15 15:55:10 +01:00
mailq 6f33529c79 Add GL-S-005Z RGBW GU5.3 MR16 (#2533) 2019-12-15 15:07:31 +01:00
Koen Kanters 1972eb450c Update converters. 2019-12-13 19:25:22 +01:00
Markus Pfeiffer f4f910f014 Support for Hive Thermostatic Radiator Valve (#2517) 2019-12-13 19:18:34 +01:00
Wilmar den Ouden 087d1b8761 fix: avoid creating the log/ directory when file not enabled (#2516)
Signed-off-by: wilmardo <info@wilmardenouden.nl>
2019-12-13 19:09:18 +01:00
Timo S 4334e01759 temp_step attribute for climate entities (#2515) 2019-12-13 19:00:02 +01:00
Koen Kanters 6fd059967a Support setting cover position and thermostat occupied heating setpoint via group. #2505 2019-12-13 18:26:03 +01:00
Koen Kanters 61f5776310 Update converters. 2019-12-12 20:55:57 +01:00
stumps444 3f2d50900d Added support for Lockwood keyless deadbolt (#2506) 2019-12-12 20:02:41 +01:00
Koen Kanters be601cffb0 Update herdsman and converters. 2019-12-11 21:54:16 +01:00
Koen Kanters 6b7bd9da37 Re-add model to device information. #2497 2019-12-11 20:15:42 +01:00
Koen Kanters 3a9dfb04a7 Bind closuresWindowCovering. https://github.com/Koenkk/zigbee2mqtt/issues/2496 2019-12-11 20:00:09 +01:00
Thomas Jager 67f8e97b12 Add namron dimmer - homeassistant extension (#2494)
https://github.com/Koenkk/zigbee-herdsman-converters/pull/808
2019-12-11 19:41:06 +01:00
Koen Kanters 37089e440a Update herdsman. 2019-12-11 19:08:22 +01:00
Koen Kanters d17eec5310 Don't allow postfix to be a friendly_name. #2486 2019-12-09 18:27:39 +01:00
Jorge Schrauwen bcf79a7e34 Device Availability: Allow battery devices to report offline/online based on device.lastSeen (#2464)
* Remove execution bits for deviceReceive.js

* deviceAvailability.isPingable should return true if 'availability_lastseen_timeout' is set for device

* deviceAvailability.handleInterval should know how to handle lastseen_timeout setting

* Allow availability_lastseen_timeout for all devices

* Use a default lastseen timeout of 24h

* Updates

* Updates

* Updates

* 25 hours instead of 24.
2019-12-09 18:12:13 +01:00
Koen Kanters 6bd9e74e0c Fix tests 2019-12-08 16:52:23 +01:00
dusanmsk 6aad019328 mqtt listener to rename last paired device. (#2478)
* mqtt listener to rename last paired device.
send zigbee2mqtt/bridge/config/rename_last with payload containing new name and last paired device will be renamed

* Updates
2019-12-08 16:50:18 +01:00
Sven Lankes d05a3a066a add support for Mueller Licht 44435 Tint LED Stripe (#2483) 2019-12-08 14:11:27 +01:00
Koen Kanters 5440aadec0 Log remove docs on failure. #2477 2019-12-07 17:57:39 +01:00
Koen Kanters be30067a39 Let devices only report when there is a change. #2424 2019-12-07 17:54:40 +01:00
Koen Kanters 17560ca885 Check if state is not null. #2450 2019-12-07 17:45:00 +01:00
Marius e00dbbf4a9 Support Hue Go with Bluetooth LCT026 (7602031P7) (#2474) 2019-12-07 17:29:37 +01:00
Koen Kanters 33201367f1 Update herdsman and converters. 2019-12-05 21:33:28 +01:00
Jaron Viëtor 60627ab6e3 Added support for accepting hex color on deviceName/set/color MQTT topic (#2441)
* Added support for accepting hex color on deviceName/set/color MQTT topic

* Update entityPublish.js
2019-12-04 21:23:46 +01:00
Koen Kanters 88a1c2884a Update herdsman and converters. 2019-12-04 21:12:27 +01:00
Koen Kanters a53a19026c Add extra logging when publish to device fails. #2450 2019-12-04 20:46:19 +01:00
Koen Kanters 1459ffea7a Publish availabilty when non pinagable device joins. #2452 2019-12-04 20:21:14 +01:00
Koen Kanters ba26887ea5 Update converters and herdsman. 2019-12-03 20:11:45 +01:00
Wilmar den Ouden 694c45fd96 Make logging transports configurable (#2231)
* feat: Make logging optional

Signed-off-by: wilmardo <info@wilmardenouden.nl>

* feat: be more verbose about what logging is enabled

Signed-off-by: wilmardo <info@wilmardenouden.nl>

* feat: adds tests

Signed-off-by: wilmardo <info@wilmardenouden.nl>

* Updates.

* Updates

* fix tests.

* Remove onlythis.

* Remove another onlythis

* Improve test stability

* Remove another onlythis
2019-11-29 23:36:57 +01:00
Koen Kanters 5e165810ec Allow to configure manually. https://github.com/Koenkk/zigbee2mqtt/issues/2365 https://github.com/Koenkk/zigbee-herdsman-converters/issues/779#issuecomment-559598591 2019-11-29 09:43:59 +01:00
Koen Kanters 99fd93f9a7 Allow setting transmit power. https://github.com/Koenkk/zigbee2mqtt/issues/2253 2019-11-27 22:02:49 +01:00
Koen Kanters c2be95777f Mark nodejs 10, 12 and 13 as supported. #2423 2019-11-27 20:02:18 +01:00
Koen Kanters acce25737f Update converters. 2019-11-27 19:57:25 +01:00
Koen Kanters 134f86a580 Update herdsman. 2019-11-27 18:49:41 +01:00
Koen Kanters 2a81a54609 Stop pinging device when removed. #2419 2019-11-27 18:14:39 +01:00
Koen Kanters 2fcddb5ad5 Update herdsman. 2019-11-26 21:24:41 +01:00
Koen Kanters ac0291f09c Update herdsman. 2019-11-26 20:08:45 +01:00
Koen Kanters 7d1635aff8 Update converters. 2019-11-25 18:39:12 +01:00
Koen Kanters 6c56c95e3f Always make endpoint 1 the coordinator endpoint. 2019-11-25 17:44:44 +01:00
Koen Kanters d37d9ee859 Factory reset device through Touchlink. 2019-11-23 20:44:23 +01:00
Koen Kanters e179e053ce Update herdsman and converters. 2019-11-23 20:19:38 +01:00
Koen Kanters 546a72ff53 Update dependencies. 2019-11-23 12:07:23 +01:00
Koen Kanters c9122eecda Update gitignore. 2019-11-23 12:07:07 +01:00
Kryzek 46d7176976 availability_whitelist (#2387)
* availability_whitelist

* Tests

* Cleanup

* Update deviceAvailability.js
2019-11-23 11:47:37 +01:00
Hans-Wilhelm Warlo 2df1acf532 Return false when mappedDevice is falsy (#2383) 2019-11-22 07:48:11 +01:00
himpich c5bd3a2450 Add support for Innr FL 130 C Color LED Strip - homeassistant extension (#2377) 2019-11-21 20:26:10 +01:00
frontend89 260d2904f3 Add device specific configuration option: debounceIndexes (#2370)
* Add device specific configuration option: debounceIndexes

* Rename debounceIndexes to debounce_ignore

* Lint
2019-11-20 21:21:23 +01:00
Koen Kanters 8ae57e7ef0 Update herdsman and converters. 2019-11-20 20:08:30 +01:00
Koen Kanters b23276f7f3 Re-configure when device rejoins. https://github.com/Koenkk/zigbee-herdsman-converters/issues/758 2019-11-20 19:06:04 +01:00
Koen Kanters fdc8fcc02b Allow to specify endpoint via property. #2356 2019-11-20 17:36:36 +01:00
Koen Kanters ef16fcd505 Fix tests. 2019-11-20 17:35:49 +01:00
Koen Kanters c5898598ae Fix json_attributes_topic discovery for climate. https://github.com/Koenkk/zigbee2mqtt/issues/2321 2019-11-19 20:50:55 +01:00
Koen Kanters 461544ed6a Update converters. 2019-11-18 21:34:23 +01:00
Koen Kanters 661b2f1cc0 Home Assistant: discover json_attributes_topic for all MQTT device types. https://github.com/Koenkk/zigbee2mqtt/issues/2321 2019-11-18 18:14:22 +01:00
Koen Kanters eea660374f Use correct endpoint for binding dimmer. https://github.com/Koenkk/zigbee2mqtt/issues/2343 2019-11-18 17:54:28 +01:00
Kryzek 878a88632b Customizable timestamp (#2357)
* Customizable timestamp

Add a customizable timestamp

* Revert "Customizable timestamp"

This reverts commit 7ae06e8e2c.

* Customizable timestamp

* Update settings.js
2019-11-17 21:29:53 +01:00
Koen Kanters 22c8f14659 Update herdsman and converters. 2019-11-17 21:22:24 +01:00
Koen Kanters d2c4133e56 Update herdsman. 2019-11-16 13:52:49 +01:00
Koen Kanters 44db557a0c Log in local timezone. https://github.com/Koenkk/zigbee2mqtt/issues/2341 2019-11-15 23:04:23 +01:00
胡明春 31246efc64 Add UI support to 'S31 Lite zb' and SA-003-Zigbee (#2340)
* Add UI support to 'S31 Lite zb' and SA-003-Zigbee

* Update homeassistant.js
2019-11-15 22:55:49 +01:00
Koen Kanters a7486d723f Update herdsman converters. 2019-11-14 20:43:20 +01:00
Koen Kanters e33b49e2ff Update herdsman and converters. 2019-11-12 19:40:52 +01:00
Koen Kanters e51ed89641 Promote to dev. 2019-11-12 18:50:38 +01:00
Koen Kanters b459c35219 1.7.1 2019-11-12 18:47:59 +01:00
Koen Kanters d8512c73df Fail gracefully when state retrieval fails after device reconnects. #2318 2019-11-11 19:25:11 +01:00
Koen Kanters fdcaaee955 Fix typo. 2019-11-11 17:45:11 +01:00
Koenkk e2858dc90f Fix topic postfix detection having number. #2309 2019-11-11 17:36:33 +01:00
Jorge Schrauwen 3dbf454d07 Fix minor type in entityPublish.js (#2311) 2019-11-11 17:27:26 +01:00
Koen Kanters fc98b951ff Update herdsman. 2019-11-10 22:31:37 +01:00
Koen Kanters 0b0a9bca4d Update converters. 2019-11-10 19:53:37 +01:00
Jorge Schrauwen be1c81a778 #2296 only publish /availability if the state has changed (#2303) 2019-11-10 17:56:01 +01:00
Koen Kanters b61723923e Promote to 1.7.0+dev. https://github.com/Koenkk/zigbee2mqtt/issues/2263 2019-11-09 21:04:38 +01:00
44 changed files with 13379 additions and 3981 deletions
+5 -1
View File
@@ -65,4 +65,8 @@ data/state.json
data/log
data-backup/
data/coordinator_backup.json
data/.storage
data/.storage
data/database.db.backup
# MacOS indexing file
.DS_Store
+3 -3
View File
@@ -16,7 +16,7 @@ steps:
inputs:
versionSpec: '10.x'
displayName: 'Install Node.js'
- bash: npm install
- bash: npm ci
displayName: 'Install dependencies'
# Verify
@@ -30,7 +30,7 @@ steps:
displayName: Docker install
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
inputs:
dockerVersion: 19.03.2
dockerVersion: 19.03.5
releaseType: stable
- task: Docker@2
displayName: Docker registry login
@@ -41,7 +41,7 @@ steps:
- bash: |
sudo wget -O /usr/local/bin/buildx https://github.com/docker/buildx/releases/download/v0.3.1/buildx-v0.3.1.linux-amd64
sudo chmod a+x /usr/local/bin/buildx
docker run --rm --privileged hypriot/qemu-register
docker run --rm --privileged hypriot/qemu-register:v2.7.0
buildx create --use
buildx ls
displayName: 'Docker setup'
+23 -14
View File
@@ -1,21 +1,30 @@
FROM node:10-alpine
FROM node:12-alpine as base
# Copy files
ADD . /app
RUN cp /app/data/configuration.yaml /app
RUN cp /app/docker/run.sh /app
RUN chmod +x /app/run.sh
WORKDIR /app
RUN apk add --no-cache tzdata eudev
COPY package.json .
# Dependencies
FROM base as dependencies
COPY npm-shrinkwrap.json .
RUN apk add --no-cache --virtual .buildtools make gcc g++ python linux-headers git && \
npm ci --production && \
apk del .buildtools
# Release
FROM base as release
COPY --from=dependencies /app/node_modules ./node_modules
COPY lib ./lib
COPY index.js docker/run.sh data/configuration.yaml ./
RUN chmod +x /app/run.sh
RUN mkdir /app/data
# Write .hash.json
ARG COMMIT
RUN echo "{\"hash\": \"$COMMIT\"}" > .hash.json
# Install dependencies
RUN apk add --update --no-cache tzdata udev eudev && \
apk add --update --no-cache --virtual .buildtools make gcc g++ python linux-headers udev git && \
npm install --unsafe-perm --production && \
apk del .buildtools
# Entrypoint
ENTRYPOINT ["./run.sh"]
+31 -22
View File
@@ -1,5 +1,6 @@
const MQTT = require('./mqtt');
const Zigbee = require('./zigbee');
const EventBus = require('./eventBus');
const State = require('./state');
const logger = require('./util/logger');
const settings = require('./util/settings');
@@ -20,11 +21,13 @@ const ExtensionDeviceAvailability = require('./extension/deviceAvailability');
const ExtensionDeviceBind = require('./extension/deviceBind');
const ExtensionDeviceReport = require('./extension/deviceReport');
const ExtensionDeviceEvent = require('./extension/deviceEvent');
const ExtensionOTAUpdate = require('./extension/otaUpdate');
class Controller {
constructor() {
this.zigbee = new Zigbee();
this.mqtt = new MQTT();
this.eventBus = new EventBus();
this.state = new State();
this.publishEntityState = this.publishEntityState.bind(this);
@@ -32,39 +35,42 @@ class Controller {
// Initialize extensions.
this.extensions = [
new ExtensionEntityPublish(this.zigbee, this.mqtt, this.state, this.publishEntityState),
new ExtensionDeviceReceive(this.zigbee, this.mqtt, this.state, this.publishEntityState),
new ExtensionDeviceGroupMembership(this.zigbee, this.mqtt, this.state, this.publishEntityState),
new ExtensionDeviceConfigure(this.zigbee, this.mqtt, this.state, this.publishEntityState),
new ExtensionNetworkMap(this.zigbee, this.mqtt, this.state, this.publishEntityState),
new ExtensionBridgeConfig(this.zigbee, this.mqtt, this.state, this.publishEntityState),
new ExtensionGroups(this.zigbee, this.mqtt, this.state, this.publishEntityState),
new ExtensionDeviceBind(this.zigbee, this.mqtt, this.state, this.publishEntityState),
new ExtensionDeviceEvent(this.zigbee, this.mqtt, this.state, this.publishEntityState),
new ExtensionEntityPublish(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
new ExtensionDeviceReceive(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
new ExtensionDeviceGroupMembership(
this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus,
),
new ExtensionDeviceConfigure(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
new ExtensionNetworkMap(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
new ExtensionBridgeConfig(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
new ExtensionGroups(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
new ExtensionDeviceBind(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
new ExtensionDeviceEvent(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
new ExtensionOTAUpdate(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
];
if (settings.get().advanced.report) {
this.extensions.push(new ExtensionDeviceReport(
this.zigbee, this.mqtt, this.state, this.publishEntityState,
this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus,
));
}
if (settings.get().homeassistant) {
this.extensions.push(new ExtensionHomeAssistant(
this.zigbee, this.mqtt, this.state, this.publishEntityState,
this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus,
));
}
/* istanbul ignore next */
if (settings.get().advanced.soft_reset_timeout !== 0) {
this.extensions.push(new ExtensionSoftReset(
this.zigbee, this.mqtt, this.state, this.publishEntityState,
this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus,
));
}
if (settings.get().advanced.availability_timeout) {
this.extensions.push(new ExtensionDeviceAvailability(
this.zigbee, this.mqtt, this.state, this.publishEntityState,
this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus,
));
}
}
@@ -82,8 +88,6 @@ class Controller {
process.exit(1);
}
logger.info(`Logging to directory: '${logger.directory}'`);
logger.cleanup();
this.state.start();
const info = await utils.getZigbee2mqttVersion();
@@ -91,9 +95,10 @@ class Controller {
// Start zigbee
try {
await this.zigbee.start();
this.callExtensionMethod('onZigbeeStarted', []);
this.zigbee.on('event', this.onZigbeeEvent.bind(this));
this.zigbee.on('adapterDisconnected', this.onZigbeeAdapterDisconnected);
await this.zigbee.start();
} catch (error) {
logger.error('Failed to start zigbee');
logger.error('Exiting...');
@@ -123,10 +128,7 @@ class Controller {
logger.warn('Set `permit_join` to `false` once you joined all devices.');
}
this.zigbee.permitJoin(settings.get().permit_join);
// Call extensions
this.callExtensionMethod('onZigbeeStarted', []);
await this.zigbee.permitJoin(settings.get().permit_join);
// MQTT
this.mqtt.on('message', this.onMQTTMessage.bind(this));
@@ -274,12 +276,19 @@ class Controller {
'applicationVersion', 'stackVersion', 'zclVersion', 'hardwareVersion', 'dateCode', 'softwareBuildID',
];
messagePayload.device = {friendlyName: entity.name};
messagePayload.device = {
friendlyName: entity.name,
model: entity.mapped ? entity.mapped.model : 'unknown',
};
attributes.forEach((a) => messagePayload.device[a] = device[a]);
}
if (Object.entries(messagePayload).length) {
if (settings.get().experimental.output === 'json') {
if (settings.get().experimental.output === 'attribute_and_json') {
await this.mqtt.publish(entity.name, JSON.stringify(messagePayload), options);
await this.iteratePayloadAttributeOutput(`${entity.name}/`, messagePayload, options);
} else if (settings.get().experimental.output === 'json') {
await this.mqtt.publish(entity.name, JSON.stringify(messagePayload), options);
} else {
/* istanbul ignore else */
+20
View File
@@ -0,0 +1,20 @@
const events = require('events');
const assert = require('assert');
const allowedEvents = [
'deviceRemoved', // Device has been removed
];
class EventBus extends events.EventEmitter {
emit(event, data) {
assert(allowedEvents.includes(event), `Event '${event}' not supported`);
super.emit(event, data);
}
on(event, callback) {
assert(allowedEvents.includes(event), `Event '${event}' not supported`);
super.on(event, callback);
}
}
module.exports = EventBus;
+3 -1
View File
@@ -6,12 +6,14 @@ class BaseExtension {
* @param {MQTT} mqtt MQTT controller
* @param {State} state State controller
* @param {Function} publishEntityState Method to publish device state to MQTT.
* @param {EventBus} eventBus The event bus
*/
constructor(zigbee, mqtt, state, publishEntityState) {
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
this.zigbee = zigbee;
this.mqtt = mqtt;
this.state = state;
this.publishEntityState = publishEntityState;
this.eventBus = eventBus;
}
/**
+60 -9
View File
@@ -5,12 +5,13 @@ const utils = require('../util/utils');
const assert = require('assert');
const BaseExtension = require('./baseExtension');
const configRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/((?:\\w+/get)|(?:\\w+))`);
const configRegex =
new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/((?:\\w+/get)|(?:\\w+/factory_reset)|(?:\\w+))`);
const allowedLogLevels = ['error', 'warn', 'info', 'debug'];
class BridgeConfig extends BaseExtension {
constructor(zigbee, mqtt, state, publishEntityState) {
super(zigbee, mqtt, state, publishEntityState);
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
super(zigbee, mqtt, state, publishEntityState, eventBus);
// Bind functions
this.permitJoin = this.permitJoin.bind(this);
@@ -21,13 +22,17 @@ class BridgeConfig extends BaseExtension {
this.devices = this.devices.bind(this);
this.groups = this.groups.bind(this);
this.rename = this.rename.bind(this);
this.renameLast = this.renameLast.bind(this);
this.remove = this.remove.bind(this);
this.forceRemove = this.forceRemove.bind(this);
this.ban = this.ban.bind(this);
this.deviceOptions = this.deviceOptions.bind(this);
this.addGroup = this.addGroup.bind(this);
this.removeGroup = this.removeGroup.bind(this);
this.whitelist= this.whitelist.bind(this);
this.whitelist = this.whitelist.bind(this);
this.touchlinkFactoryReset = this.touchlinkFactoryReset.bind(this);
this.lastJoinedDeviceName = null;
// Set supported options
this.supportedOptions = {
@@ -40,6 +45,7 @@ class BridgeConfig extends BaseExtension {
'groups': this.groups,
'devices/get': this.devices,
'rename': this.rename,
'rename_last': this.renameLast,
'remove': this.remove,
'force_remove': this.forceRemove,
'ban': this.ban,
@@ -47,6 +53,7 @@ class BridgeConfig extends BaseExtension {
'add_group': this.addGroup,
'remove_group': this.removeGroup,
'whitelist': this.whitelist,
'touchlink/factory_reset': this.touchlinkFactoryReset,
};
}
@@ -194,12 +201,26 @@ class BridgeConfig extends BaseExtension {
return;
}
this._renameInternal(json.old, json.new);
}
renameLast(topic, message) {
if (!this.lastJoinedDeviceName) {
logger.error(`Cannot rename last joined device, no device has joined during this session`);
return;
}
this._renameInternal(this.lastJoinedDeviceName, message);
}
_renameInternal(from, to) {
try {
settings.changeFriendlyName(json.old, json.new);
logger.info(`Successfully renamed - ${json.old} to ${json.new} `);
this.mqtt.log('device_renamed', {from: json.old, to: json.new});
const isGroup = settings.getGroup(from) !== null;
settings.changeFriendlyName(from, to);
logger.info(`Successfully renamed - ${from} to ${to} `);
this.mqtt.log(`${isGroup ? 'group' : 'device'}_renamed`, {from, to});
} catch (error) {
logger.error(`Failed to rename - ${json.old} to ${json.new}`);
logger.error(`Failed to rename - ${from} to ${to}`);
}
}
@@ -233,7 +254,10 @@ class BridgeConfig extends BaseExtension {
removeGroup(topic, message) {
const name = message;
settings.removeGroup(name);
const entity = this.zigbee.resolveEntity(message);
assert(entity && entity.type === 'group', `Group '${message}' does not exist`);
settings.removeGroup(message);
entity.group.removeFromDatabase();
logger.info(`Removed group '${name}'`);
}
@@ -259,10 +283,15 @@ class BridgeConfig extends BaseExtension {
if (!entity) {
logger.error(`Cannot ${lookup[action][2]}, device '${message}' does not exist`);
this.mqtt.log(`device_${lookup[action][0]}_failed`, message);
return;
}
const cleanup = () => {
// Fire event
this.eventBus.emit('deviceRemoved', {device: entity.device});
// Remove from configuration.yaml
settings.removeDevice(entity.settings.ID);
@@ -284,6 +313,10 @@ class BridgeConfig extends BaseExtension {
cleanup();
} catch (error) {
logger.error(`Failed to ${lookup[action][2]} ${entity.settings.friendlyName} (${error})`);
// eslint-disable-next-line
logger.error(`See https://www.zigbee2mqtt.io/information/mqtt_topics_and_message_structure.html#zigbee2mqttbridgeconfigremove for more info`);
this.mqtt.log(`device_${lookup[action][0]}_failed`, message);
}
if (action === 'ban') {
@@ -293,6 +326,7 @@ class BridgeConfig extends BaseExtension {
async onMQTTConnected() {
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/config/+`);
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/config/+/+`);
await this.publish();
}
@@ -326,6 +360,23 @@ class BridgeConfig extends BaseExtension {
await this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0});
}
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
if (type === 'deviceJoined') {
this.lastJoinedDeviceName = settingsDevice.friendlyName;
}
}
async touchlinkFactoryReset() {
logger.info('Starting touchlink factory reset...');
const result = await this.zigbee.touchlinkFactoryReset();
if (result) {
logger.info('Successfully factory reset device through Touchlink');
} else {
logger.warn('Failed to factory reset device through Touchlink');
}
}
}
module.exports = BridgeConfig;
+94 -42
View File
@@ -6,18 +6,24 @@ const BaseExtension = require('./baseExtension');
// Some EndDevices should be pinged
// e.g. E11-G13 https://github.com/Koenkk/zigbee2mqtt/issues/775#issuecomment-453683846
const forcedPingable = [
zigbeeHerdsmanConverters.devices.find((d) => d.model === 'E11-G13'),
const pingableModels = [
'E11-G13',
'53170161',
];
const forcedPingable =
zigbeeHerdsmanConverters.devices.filter((d) => pingableModels.includes(d.model));
const toZigbeeCandidates = ['state', 'brightness', 'color', 'color_temp'];
const Hours25 = 1000 * 60 * 60 * 25;
/**
* This extensions pings devices to check if they are online.
*/
class DeviceAvailability extends BaseExtension {
constructor(zigbee, mqtt, state, publishEntityState) {
super(zigbee, mqtt, state, publishEntityState);
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
super(zigbee, mqtt, state, publishEntityState, eventBus);
this.availability_timeout = settings.get().advanced.availability_timeout;
this.timers = {};
@@ -27,39 +33,71 @@ class DeviceAvailability extends BaseExtension {
this.blacklist = settings.get().advanced.availability_blacklist.map((e) => {
return settings.getEntity(e).ID;
});
// Initialize whitelist
this.whitelist = settings.get().advanced.availability_whitelist.map((e) => {
return settings.getEntity(e).ID;
});
}
isPingable(device) {
if (this.blacklist.includes(device.ieeeAddr)) {
isAllowed(device) {
const ieeeAddr = device.ieeeAddr;
const deviceSettings = settings.getDevice(ieeeAddr);
const name = deviceSettings ? deviceSettings.friendly_name : null;
// Whitelist is not empty and device is in it, enable availability
if (this.whitelist.length > 0) {
return this.whitelist.includes(ieeeAddr) || (name && this.whitelist.includes(name));
}
// Device is on blacklist, disable availability
if (this.blacklist.includes(ieeeAddr) || (name && this.blacklist.includes(name))) {
return false;
}
return true;
}
isPingable(device) {
// Device is on forcedPingable-list, enable availability
if (forcedPingable.find((d) => d.zigbeeModel.includes(device.modelID))) {
return true;
}
// Device is a mains powered router
const result = utils.isRouter(device) && !utils.isBatteryPowered(device);
return result;
}
getAllPingableDevices() {
return this.zigbee.getClients().filter((d) => this.isPingable(d));
}
onMQTTConnected() {
// As some devices are not checked for availability (e.g. battery powered devices)
// we mark all device as online by default.
this.zigbee.getClients().forEach((device) => this.publishAvailability(device, true));
for (const device of this.zigbee.getClients()) {
// Mark all devices as online on start
this.publishAvailability(device, true);
// Start timers for all devices
this.getAllPingableDevices().forEach((device) => this.setTimer(device));
if (this.isAllowed(device)) {
if (this.isPingable(device)) {
this.setTimerPingable(device);
} else {
this.timers[device.ieeeAddr] = setInterval(() => {
this.handleIntervalNotPingable(device);
}, utils.secondsToMilliseconds(300));
}
}
}
}
async handleInterval(device) {
async handleIntervalPingable(device) {
// When a device is already unavailable, log the ping failed on 'debug' instead of 'error'.
const entity = this.zigbee.resolveEntity(device.ieeeAddr);
if (!entity) {
logger.debug(`Stop pinging '${device.ieeeAddr}', device is not known anymore`);
return;
}
const ieeeAddr = device.ieeeAddr;
const level = this.state.hasOwnProperty(ieeeAddr) && !this.state[ieeeAddr] ? 'debug' : 'error';
const entity = this.zigbee.resolveEntity(device.ieeeAddr);
try {
await device.ping();
this.publishAvailability(device, true);
@@ -68,17 +106,31 @@ class DeviceAvailability extends BaseExtension {
this.publishAvailability(device, false);
logger[level](`Failed to ping '${entity.name}'`);
} finally {
this.setTimer(device);
this.setTimerPingable(device);
}
}
setTimer(device) {
async handleIntervalNotPingable(device) {
const ago = Date.now() - device.lastSeen;
const entity = this.zigbee.resolveEntity(device.ieeeAddr);
if (!entity || !device.lastSeen) {
return;
}
logger.debug(`Non-pingable device '${entity.name}' was last seen '${ago / 1000}' seconds ago.`);
if (ago > Hours25) {
this.publishAvailability(device, false);
}
}
setTimerPingable(device) {
if (this.timers[device.ieeeAddr]) {
clearTimeout(this.timers[device.ieeeAddr]);
}
this.timers[device.ieeeAddr] = setTimeout(async () => {
await this.handleInterval(device);
await this.handleIntervalPingable(device);
}, utils.secondsToMilliseconds(this.availability_timeout));
}
@@ -96,12 +148,17 @@ class DeviceAvailability extends BaseExtension {
if (mappedDevice) {
const used = [];
for (const key of toZigbeeCandidates) {
const converter = mappedDevice.toZigbee.find((tz) => tz.key.includes(key));
if (converter && !used.includes(converter)) {
converter.convertGet(device.endpoints[0], key, {});
used.push(converter);
try {
for (const key of toZigbeeCandidates) {
const converter = mappedDevice.toZigbee.find((tz) => tz.key.includes(key));
if (converter && !used.includes(converter)) {
await converter.convertGet(device.endpoints[0], key, {});
used.push(converter);
}
}
} catch (error) {
const entity = this.zigbee.resolveEntity(device.ieeeAddr);
logger.error(`Failed to read state of '${entity.name}' after reconnect`);
}
}
}
@@ -113,12 +170,14 @@ class DeviceAvailability extends BaseExtension {
this.onReconnect(device);
}
this.state[ieeeAddr] = available;
const deviceSettings = settings.getDevice(ieeeAddr);
const name = deviceSettings ? deviceSettings.friendly_name : ieeeAddr;
const topic = `${name}/availability`;
const payload = available ? 'online' : 'offline';
this.mqtt.publish(topic, payload, {retain: true, qos: 0});
if (this.state[ieeeAddr] !== available) {
this.state[ieeeAddr] = available;
this.mqtt.publish(topic, payload, {retain: true, qos: 0});
}
}
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
@@ -127,29 +186,22 @@ class DeviceAvailability extends BaseExtension {
return;
}
if (this.isPingable(device)) {
// When a zigbee message from a device is received we know the device is still alive.
// => reset the timer.
this.setTimer(device);
if (this.isAllowed(device)) {
this.publishAvailability(data.device, true);
const online = this.state.hasOwnProperty(device.ieeeAddr) && this.state[device.ieeeAddr];
const offline = this.state.hasOwnProperty(device.ieeeAddr) && !this.state[device.ieeeAddr];
if (this.isPingable(device)) {
// When a zigbee message from a device is received we know the device is still alive.
// => reset the timer.
this.setTimerPingable(device);
if (!online && !offline) {
// A new device has been connected
this.publishAvailability(device, true);
} else if (offline) {
// When a message is received and the device is marked as offline, mark it online.
this.publishAvailability(device, true);
} else {
/* istanbul ignore else */
const online = this.state.hasOwnProperty(device.ieeeAddr) && this.state[device.ieeeAddr];
if (online && type === 'deviceAnnounce' && !utils.isIkeaTradfriDevice(device)) {
/**
* In case the device is powered off AND on within the availability timeout,
* zigbee2qmtt does not detect the device as offline (device is still marked online).
* When a device is turned on again the state could be out of sync.
* https://github.com/Koenkk/zigbee2mqtt/issues/1383#issuecomment-489412168
* endDeviceAnnce is typically send when a device comes online.
* deviceAnnounce is typically send when a device comes online.
*
* This isn't needed for TRADFRI devices as they already send the state themself.
*/
+16 -5
View File
@@ -4,7 +4,10 @@ const assert = require('assert');
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/(bind|unbind)/.+$`);
const BaseExtension = require('./baseExtension');
const clusters = ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'];
const clusters = ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl', 'closuresWindowCovering'];
// See zigbee-herdsman-converters devices.js
const defaultBindGroup = {type: 'group_number', ID: 901};
class DeviceBind extends BaseExtension {
onMQTTConnected() {
@@ -28,22 +31,26 @@ class DeviceBind extends BaseExtension {
// Find source; can only be a device and target
const source = this.zigbee.resolveEntity(sourceKey);
assert(source != null && source.type === 'device', 'Source undefined or not a device');
const target = this.zigbee.resolveEntity(targetKey);
const target = targetKey === 'default_bind_group' ? defaultBindGroup : this.zigbee.resolveEntity(targetKey);
assert(target != null, 'Target is unknown');
const sourceName = source.settings.friendlyName;
const targetName = target.settings.friendlyName;
const targetName = targetKey === 'default_bind_group' ? targetKey : target.settings.friendlyName;
// Find which clusters are supported by both the source and target.
// Groups are assumed to support all clusters.
for (const cluster of clusters) {
const targetValid = target.type === 'group' ||
const targetValid = target.type === 'group' || target.type === 'group_number' ||
target.device.type === 'Coordinator' || target.endpoint.supportsInputCluster(cluster);
if (source.endpoint.supportsOutputCluster(cluster) && targetValid) {
logger.debug(`${type}ing cluster '${cluster}' from '${sourceName}' to '${targetName}'`);
try {
const bindTarget = target.type === 'group' ? target.group : target.endpoint;
let bindTarget = null;
if (target.type === 'group') bindTarget = target.group;
else if (target.type === 'group_number') bindTarget = target.ID;
else bindTarget = target.endpoint;
if (type === 'bind') {
await source.endpoint.bind(cluster, bindTarget);
} else {
@@ -63,6 +70,10 @@ class DeviceBind extends BaseExtension {
`Failed to ${type} cluster '${cluster}' from '${sourceName}' to ` +
`'${targetName}' (${error})`,
);
this.mqtt.log(
`device_${type}_failed`,
{from: sourceName, to: targetName, cluster},
);
}
}
}
+41 -12
View File
@@ -4,23 +4,22 @@ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
const BaseExtension = require('./baseExtension');
class DeviceConfigure extends BaseExtension {
constructor(zigbee, mqtt, state, publishEntityState) {
super(zigbee, mqtt, state, publishEntityState);
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
super(zigbee, mqtt, state, publishEntityState, eventBus);
this.configuring = new Set();
this.attempts = {};
this.topic = `${settings.get().mqtt.base_topic}/bridge/configure`;
}
shouldConfigure(device, mappedDevice) {
if (!device) {
if (!device || !mappedDevice || !mappedDevice.configure) {
return false;
}
if (device.meta.hasOwnProperty('configured') && device.meta.configured === mappedDevice.meta.configureKey) {
return false;
}
if (!mappedDevice || !mappedDevice.configure) {
if (device.meta &&
device.meta.hasOwnProperty('configured') &&
device.meta.configured === mappedDevice.meta.configureKey) {
return false;
}
@@ -31,8 +30,32 @@ class DeviceConfigure extends BaseExtension {
return true;
}
onMQTTConnected() {
this.mqtt.subscribe(this.topic);
}
async onMQTTMessage(topic, message) {
if (topic !== this.topic) {
return;
}
const entity = this.zigbee.resolveEntity(message);
if (!entity || entity.type !== 'device') {
logger.error(`Device '${message}' does not exist`);
return;
}
const mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(entity.device.modelID);
if (!mappedDevice.configure) {
logger.warn(`Skipping configure of '${entity.name}', device does not require this.`);
return;
}
this.configure(entity.device, mappedDevice, entity.settings, true);
}
async onZigbeeStarted() {
this.coordinatorEndpoint = this.zigbee.getDevicesByType('Coordinator')[0].endpoints[0];
this.coordinatorEndpoint = this.zigbee.getDevicesByType('Coordinator')[0].getEndpoint(1);
for (const device of this.zigbee.getClients()) {
const mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
@@ -45,13 +68,19 @@ class DeviceConfigure extends BaseExtension {
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
const device = data.device;
if (type === 'deviceJoined' && device.meta.hasOwnProperty('configured')) {
delete device.meta.configured;
device.save();
}
if (this.shouldConfigure(device, mappedDevice)) {
this.configure(device, mappedDevice, settingsDevice);
}
}
async configure(device, mappedDevice, settingsDevice) {
if (this.configuring.has(device.ieeeAddr) || this.attempts[device.ieeeAddr] >= 3) {
async configure(device, mappedDevice, settingsDevice, force=false) {
if (this.configuring.has(device.ieeeAddr) || (this.attempts[device.ieeeAddr] >= 3 && !force)) {
return false;
}
@@ -64,7 +93,7 @@ class DeviceConfigure extends BaseExtension {
logger.info(`Configuring '${settingsDevice.friendlyName}'`);
try {
await mappedDevice.configure(device, this.coordinatorEndpoint);
logger.info(`Succesfully configured '${settingsDevice.friendlyName}'`);
logger.info(`Successfully configured '${settingsDevice.friendlyName}'`);
// eslint-disable-next-line
device.meta.configured = mappedDevice.meta.configureKey;
device.save();
+2
View File
@@ -21,6 +21,8 @@ class DeviceEvent extends BaseExtension {
}
callOnEvent(device, type, data, mappedDevice) {
zigbeeHerdsmanConverters.onEvent(type, data, device);
if (!mappedDevice) {
mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
}
+4 -1
View File
@@ -7,7 +7,10 @@ const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/device/
class DeviceGroupMembership extends BaseExtension {
onMQTTConnected() {
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/device/+/get_group_membership`);
for (let step = 1; step < 20; step++) {
const topic = `${settings.get().mqtt.base_topic}/bridge/device/${'+/'.repeat(step)}get_group_membership`;
this.mqtt.subscribe(topic);
}
}
async onMQTTMessage(topic, message) {
Regular → Executable
+35 -8
View File
@@ -5,8 +5,8 @@ const debounce = require('debounce');
const BaseExtension = require('./baseExtension');
class DeviceReceive extends BaseExtension {
constructor(zigbee, mqtt, state, publishEntityState) {
super(zigbee, mqtt, state, publishEntityState);
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
super(zigbee, mqtt, state, publishEntityState, eventBus);
this.coordinator = null;
this.elapsed = {};
this.debouncers = {};
@@ -16,7 +16,7 @@ class DeviceReceive extends BaseExtension {
this.coordinator = this.zigbee.getDevicesByType('Coordinator')[0];
}
publishDebounce(ieeeAddr, payload, time) {
publishDebounce(ieeeAddr, payload, time, debounceIgnore) {
if (!this.debouncers[ieeeAddr]) {
this.debouncers[ieeeAddr] = {
payload: {},
@@ -27,10 +27,33 @@ class DeviceReceive extends BaseExtension {
};
}
if (this.isPayloadConflicted(payload, this.debouncers[ieeeAddr].payload, debounceIgnore)) {
// publish previous payload immediately
this.debouncers[ieeeAddr].publish.flush();
}
// extend debounced payload with current
this.debouncers[ieeeAddr].payload = {...this.debouncers[ieeeAddr].payload, ...payload};
this.debouncers[ieeeAddr].publish();
}
// if debounce_ignore are specified (Array of strings)
// then all newPayload values with key present in debounce_ignore
// should equal or be undefined in oldPayload
// otherwise payload is conflicted
isPayloadConflicted(newPayload, oldPayload, debounceIgnore) {
let result = false;
Object.keys(oldPayload)
.filter((key) => (debounceIgnore || []).includes(key))
.forEach((key) => {
if (typeof newPayload[key] !== 'undefined' && newPayload[key] !== oldPayload[key]) {
result = true;
}
});
return result;
}
canHandleEvent(type, data, mappedDevice) {
if (type !== 'message') {
return false;
@@ -86,9 +109,7 @@ class DeviceReceive extends BaseExtension {
// Check if there is an available converter
if (!converters.length) {
// Don't log readRsp and raw as warn messages, they are not interesting most of the time.
const level = ['readResponse', 'raw'].includes(data.type) ? 'debug' : 'warn';
logger[level](
logger.debug(
`No converter available for '${mappedDevice.model}' with cluster '${data.cluster}' ` +
`and type '${data.type}' and data '${JSON.stringify(data.data)}'`,
);
@@ -117,7 +138,12 @@ class DeviceReceive extends BaseExtension {
// Check if we have to debounce
if (settingsDevice && settingsDevice.hasOwnProperty('debounce')) {
this.publishDebounce(data.device.ieeeAddr, payload, settingsDevice.debounce);
this.publishDebounce(
data.device.ieeeAddr,
payload,
settingsDevice.debounce,
settingsDevice.debounce_ignore,
);
} else {
this.publishEntityState(data.device.ieeeAddr, payload);
@@ -138,10 +164,11 @@ class DeviceReceive extends BaseExtension {
}
};
const meta = {device: data.device};
let payload = {};
converters.forEach((converter) => {
const options = {...settings.get().device_options, ...settings.getDevice(data.device.ieeeAddr)};
const converted = converter.convert(mappedDevice, data, publish, options);
const converted = converter.convert(mappedDevice, data, publish, options, meta);
if (converted) {
payload = {...payload, ...converted};
}
+45 -15
View File
@@ -1,15 +1,19 @@
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
const logger = require('../util/logger');
const CC2530Router = zigbeeHerdsmanConverters.devices.find((d) => d.model === 'CC2530.ROUTER');
const BASICZBR3 = zigbeeHerdsmanConverters.devices.find((d) => d.model === 'BASICZBR3');
const ZNLDP12LM = zigbeeHerdsmanConverters.devices.find((d) => d.model === 'ZNLDP12LM');
const utils = require('../util/utils');
const BaseExtension = require('./baseExtension');
const debounce = require('debounce');
const ZigbeeHerdsman = require('zigbee-herdsman');
const defaultConfiguration = {
minimumReportInterval: 3, maximumReportInterval: 300, reportableChange: 0,
minimumReportInterval: 3, maximumReportInterval: 300, reportableChange: 1,
};
const devicesNotSupportingReporting = [CC2530Router, BASICZBR3];
const reportKey = 1;
const clusters = {
@@ -54,29 +58,55 @@ const pollOnMessage = [
// When the bound devices/members of group have the following manufacturerID
manufacturerID: ZigbeeHerdsman.Zcl.ManufacturerCode.Philips,
},
{
key: 2,
cluster: {
genOnOff: [
{type: 'commandOn', data: {}},
{type: 'commandOff', data: {}},
{type: 'commandOffWithEffect', data: {}},
],
manuSpecificPhilips: [
{type: 'commandHueNotification', data: {button: 1}},
{type: 'commandHueNotification', data: {button: 4}},
],
},
read: {cluster: 'genOnOff', attributes: ['onOff']},
manufacturerID: ZigbeeHerdsman.Zcl.ManufacturerCode.Philips,
},
];
class DeviceReport extends BaseExtension {
constructor(zigbee, mqtt, state, publishEntityState) {
super(zigbee, mqtt, state, publishEntityState);
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
super(zigbee, mqtt, state, publishEntityState, eventBus);
this.configuring = new Set();
this.failed = new Set();
this.pollDebouncers = {};
}
async setupReporting(device) {
shouldIgnoreClusterForDevice(cluster, mappedDevice) {
if (mappedDevice === ZNLDP12LM && cluster === 'closuresWindowCovering') {
// Device announces it but doesn't support it
// https://github.com/Koenkk/zigbee2mqtt/issues/2611
return true;
}
return false;
}
async setupReporting(device, mappedDevice) {
if (this.configuring.has(device.ieeeAddr) || this.failed.has(device.ieeeAddr)) return;
this.configuring.add(device.ieeeAddr);
try {
for (const endpoint of device.endpoints) {
for (const ep of device.endpoints) {
for (const [cluster, configuration] of Object.entries(clusters)) {
if (endpoint.supportsInputCluster(cluster)) {
logger.debug(`Setup reporting for '${device.ieeeAddr}' - ${endpoint.ID} - ${cluster}`);
await endpoint.bind(cluster, this.coordinatorEndpoint);
await endpoint.configureReporting(cluster, configuration);
if (ep.supportsInputCluster(cluster) && !this.shouldIgnoreClusterForDevice(cluster, mappedDevice)) {
logger.debug(`Setup reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`);
await ep.bind(cluster, this.coordinatorEndpoint);
await ep.configureReporting(cluster, configuration);
logger.info(
`Succesfully setup reporting for '${device.ieeeAddr}' - ${endpoint.ID} - ${cluster}`,
`Successfully setup reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`,
);
}
}
@@ -97,7 +127,7 @@ class DeviceReport extends BaseExtension {
}
shouldSetupReporting(mappedDevice, device, messageType) {
if (!device) return false;
if (!device || !mappedDevice) return false;
// Handle messages of type endDeviceAnnce and devIncoming.
// This message is typically send when a device comes online after being powered off
@@ -109,24 +139,24 @@ class DeviceReport extends BaseExtension {
if (device.meta.hasOwnProperty('reporting') && device.meta.reporting === reportKey) return false;
if (!utils.isRouter(device) || utils.isBatteryPowered(device)) return false;
// Gledopto devices don't support reporting.
if (mappedDevice === CC2530Router || mappedDevice.vendor === 'Gledopto') return false;
if (devicesNotSupportingReporting.includes(mappedDevice) || mappedDevice.vendor === 'Gledopto') return false;
return true;
}
async onZigbeeStarted() {
this.coordinatorEndpoint = this.zigbee.getDevicesByType('Coordinator')[0].endpoints[0];
this.coordinatorEndpoint = this.zigbee.getDevicesByType('Coordinator')[0].getEndpoint(1);
for (const device of this.zigbee.getClients()) {
const mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
if (this.shouldSetupReporting(mappedDevice, device, null)) {
this.setupReporting(device);
this.setupReporting(device, mappedDevice);
}
}
}
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
if (this.shouldSetupReporting(mappedDevice, data.device, type)) {
this.setupReporting(data.device);
this.setupReporting(data.device, mappedDevice);
}
if (type === 'message') {
+31 -11
View File
@@ -15,6 +15,10 @@ const groupConverters = [
zigbeeHerdsmanConverters.toZigbeeConverters.light_color,
zigbeeHerdsmanConverters.toZigbeeConverters.light_alert,
zigbeeHerdsmanConverters.toZigbeeConverters.ignore_transition,
zigbeeHerdsmanConverters.toZigbeeConverters.cover_position_tilt,
zigbeeHerdsmanConverters.toZigbeeConverters.thermostat_occupied_heating_setpoint,
zigbeeHerdsmanConverters.toZigbeeConverters.tint_scene,
zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_move,
];
class EntityPublish extends BaseExtension {
@@ -37,7 +41,7 @@ class EntityPublish extends BaseExtension {
}
const ID = match[1].replace(`${settings.get().mqtt.base_topic}/`, '');
// If we didn't repalce base_topic we received something we don't care about
// If we didn't replace base_topic we received something we don't care about
if (ID === match[1] || ID.match(/bridge/)) {
return null;
}
@@ -124,7 +128,22 @@ class EntityPublish extends BaseExtension {
// For each attribute call the corresponding converter
const usedConverters = [];
for (const [key, value] of entries) {
for (let [key, value] of entries) {
let postfix = topic.postfix;
let actualTarget = target;
// When the key has a postfix included (e.g. state_right), this will override the target.
if (entity.type === 'device' && key.includes('_')) {
const underscoreIndex = key.lastIndexOf('_');
const possiblePostfix = key.substring(underscoreIndex + 1, key.length);
if (utils.getPostfixes().includes(possiblePostfix)) {
postfix = possiblePostfix;
key = key.substring(0, underscoreIndex);
const device = target.getDevice();
actualTarget = device.getEndpoint(mapped.endpoint(device)[postfix]);
}
}
const converter = converters.find((c) => c.key.includes(key));
if (usedConverters.includes(converter)) {
@@ -139,7 +158,7 @@ class EntityPublish extends BaseExtension {
// Converter didn't return a result, skip
const meta = {
endpoint_name: topic.postfix,
endpoint_name: postfix,
options,
message: json,
logger,
@@ -149,14 +168,14 @@ class EntityPublish extends BaseExtension {
};
try {
if (topic.type === 'set' && converter && converter.convertSet) {
if (topic.type === 'set' && converter.convertSet) {
logger.debug(`Publishing '${topic.type}' '${key}' to '${entity.name}'`);
const result = await converter.convertSet(target, key, value, meta);
if (result && result.hasOwnProperty('state')) {
const result = await converter.convertSet(actualTarget, key, value, meta);
if (result && result.state) {
const msg = result.state;
if (topic.postfix) {
msg[`state_${topic.postfix}`] = msg.state;
if (postfix) {
msg[`state_${postfix}`] = msg.state;
delete msg.state;
}
@@ -174,11 +193,11 @@ class EntityPublish extends BaseExtension {
entity.type === 'device' && result && result.hasOwnProperty('readAfterWriteTime') &&
entity.settings.retrieve_state
) {
setTimeout(() => converter.convertGet(target, key, meta), result.readAfterWriteTime);
setTimeout(() => converter.convertGet(actualTarget, key, meta), result.readAfterWriteTime);
}
} else if (topic.type === 'get' && converter && converter.convertGet) {
} else if (topic.type === 'get' && converter.convertGet) {
logger.debug(`Publishing get '${topic.type}' '${key}' to '${entity.name}'`);
await converter.convertGet(target, key, meta);
await converter.convertGet(actualTarget, key, meta);
} else {
logger.error(`No converter available for '${topic.type}' '${key}' (${json[key]})`);
continue;
@@ -187,6 +206,7 @@ class EntityPublish extends BaseExtension {
const message =
`Publish '${topic.type}' '${key}' to '${entity.name}' failed: '${error}'`;
logger.error(message);
logger.debug(error.stack);
this.mqtt.log('zigbee_publish_error', message, {friendly_name: entity.name});
}
+8 -2
View File
@@ -6,8 +6,8 @@ const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(
const topicRegexRemoveAll = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/remove_all$`);
class Groups extends BaseExtension {
constructor(zigbee, mqtt, state, publishEntityState) {
super(zigbee, mqtt, state, publishEntityState);
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
super(zigbee, mqtt, state, publishEntityState, eventBus);
this.onStateChange = this.onStateChange.bind(this);
}
@@ -142,6 +142,9 @@ class Groups extends BaseExtension {
if (!group || group.type !== 'group') {
logger.error(`Group '${topicMatch[1]}' does not exist`);
this.mqtt.log(
`device_group_${type}_failed`,
{friendly_name: message, group: topicMatch[1], error: 'group doesn\'t exists'});
return;
}
} else if (topic.match(topicRegexRemoveAll)) {
@@ -153,6 +156,9 @@ class Groups extends BaseExtension {
const entity = this.zigbee.resolveEntity(message);
if (!entity || !entity.type === 'device') {
logger.error(`Device '${message}' does not exist`);
this.mqtt.log(
`device_group_${type}_failed`,
{friendly_name: message, group: topicMatch[1], error: 'entity doesn\'t exists'});
return;
}
+363 -50
View File
@@ -26,6 +26,15 @@ const cfg = {
device_class: 'presence',
},
},
'binary_sensor_tamper': {
type: 'binary_sensor',
object_id: 'tamper',
discovery_payload: {
payload_on: false,
payload_off: true,
value_template: '{{ value_json.tamper }}',
},
},
'binary_sensor_contact': {
type: 'binary_sensor',
object_id: 'contact',
@@ -96,6 +105,15 @@ const cfg = {
device_class: 'battery',
},
},
'binary_sensor_update_available': {
type: 'binary_sensor',
object_id: 'update_available',
discovery_payload: {
payload_on: true,
payload_off: false,
value_template: '{{ value_json.update_available}}',
},
},
'binary_sensor_lock': {
type: 'binary_sensor',
object_id: 'lock',
@@ -122,11 +140,20 @@ const cfg = {
type: 'sensor',
object_id: 'illuminance',
discovery_payload: {
unit_of_measurement: 'lx',
unit_of_measurement: '-',
device_class: 'illuminance',
value_template: '{{ value_json.illuminance }}',
},
},
'sensor_illuminance_lux': {
type: 'sensor',
object_id: 'illuminance',
discovery_payload: {
unit_of_measurement: 'lx',
device_class: 'illuminance',
value_template: '{{ value_json.illuminance_lux }}',
},
},
'sensor_humidity': {
type: 'sensor',
object_id: 'humidity',
@@ -145,6 +172,15 @@ const cfg = {
value_template: '{{ value_json.temperature }}',
},
},
'sensor_local_temperature': {
type: 'sensor',
object_id: 'local_temperature',
discovery_payload: {
unit_of_measurement: '°C',
device_class: 'temperature',
value_template: '{{ value_json.local_temperature }}',
},
},
'sensor_pressure': {
type: 'sensor',
object_id: 'pressure',
@@ -227,7 +263,8 @@ const cfg = {
type: 'sensor',
object_id: 'linkquality',
discovery_payload: {
unit_of_measurement: '-',
icon: 'mdi:signal',
unit_of_measurement: 'lqi',
value_template: '{{ value_json.linkquality }}',
},
},
@@ -255,6 +292,15 @@ const cfg = {
icon: 'mdi:view-array',
},
},
'sensor_consumption': {
type: 'sensor',
object_id: 'consumption',
discovery_payload: {
unit_of_measurement: 'kWh',
value_template: '{{ value_json.consumption }}',
icon: 'mdi:flash',
},
},
'sensor_sensitivity': {
type: 'sensor',
object_id: 'sensitivity',
@@ -263,6 +309,14 @@ const cfg = {
icon: 'mdi:filter-variant',
},
},
'sensor_strength': {
type: 'sensor',
object_id: 'strength',
discovery_payload: {
value_template: '{{ value_json.strength }}',
icon: 'mdi:weight',
},
},
// Light
'light_brightness_colorxy_white': {
@@ -276,6 +330,18 @@ const cfg = {
command_topic: true,
},
},
'light_brightness_colortemp_colorxy_white': {
type: 'light',
object_id: 'light',
discovery_payload: {
brightness: true,
xy: true,
white_value: true,
schema: 'json',
command_topic: true,
color_temp: true,
},
},
'light_brightness_colortemp_colorxy': {
type: 'light',
object_id: 'light',
@@ -297,6 +363,17 @@ const cfg = {
command_topic: true,
},
},
'light_brightness_colortemp_colorhs': {
type: 'light',
object_id: 'light',
discovery_payload: {
brightness: true,
color_temp: true,
hs: true,
schema: 'json',
command_topic: true,
},
},
'light_brightness_colortemp': {
type: 'light',
object_id: 'light',
@@ -373,11 +450,25 @@ const cfg = {
discovery_payload: {
command_topic: true,
value_template: '{{ value_json.state }}',
state_locked: 'LOCK',
state_unlocked: 'UNLOCK',
},
},
'lock_keypad_lockout': {
type: 'lock',
object_id: 'keypad_lock',
discovery_payload: {
state_topic: true,
command_topic: true,
command_topic_postfix: 'keypad_lockout',
payload_unlock: '0',
payload_lock: '1',
value_template: '{{ value_json.keypad_lockout }}',
},
},
// Thermostat/HVAC
'thermostat': (minTemp=7, maxTemp=30, temperatureStateProperty='occupied_heating_setpoint') => {
'thermostat': (minTemp=7, maxTemp=30, temperatureStateProperty='occupied_heating_setpoint', tempStep=1) => {
return {
type: 'climate',
object_id: 'climate',
@@ -394,6 +485,9 @@ const cfg = {
temperature_state_topic: true,
temperature_state_template: `{{ value_json.${temperatureStateProperty} }}`,
temperature_command_topic: temperatureStateProperty,
temp_step: tempStep,
action_topic: true,
action_template: '{{ value_json.operation }}',
},
};
},
@@ -432,7 +526,7 @@ const switchWithPostfix = (postfix) => {
// Map homeassitant configurations to devices.
const mapping = {
'WXKG01LM': [cfg.sensor_click, cfg.sensor_battery],
'WXKG11LM': [cfg.sensor_click, cfg.sensor_battery],
'WXKG11LM': [cfg.sensor_click, cfg.sensor_battery, cfg.sensor_action],
'WXKG12LM': [cfg.sensor_click, cfg.sensor_battery, cfg.sensor_action],
// DEPRECATED; BREAKING_IMPROVEMENT: only use sensor_click for WXKG03LM (action hold -> click hold)
'WXKG03LM': [cfg.sensor_click, cfg.sensor_battery, cfg.sensor_action],
@@ -457,6 +551,7 @@ const mapping = {
'LED1536G5': [cfg.light_brightness_colortemp],
'7299760PH': [cfg.light_brightness_colorxy],
'7146060PH': [cfg.light_brightness_colortemp_colorxy],
'7602031P7': [cfg.light_brightness_colortemp_colorxy],
'046677476816': [cfg.light_brightness],
'F7C033': [cfg.light_brightness],
'JTYJ-GD-01LM/BW': [cfg.binary_sensor_smoke, cfg.sensor_battery, cfg.sensor_sensitivity, cfg.sensor_smoke_density],
@@ -464,6 +559,7 @@ const mapping = {
'SWITCH EDP RE:DY': [cfg.switch],
'CC2530.ROUTER': [cfg.binary_sensor_router],
'AA70155': [cfg.light_brightness_colortemp],
'A9A19A60WESDZ02': [cfg.light_brightness_colortemp],
'4058075816718': [cfg.light_brightness_colortemp_colorxy],
'AA69697': [cfg.light_brightness_colortemp_colorxy],
'HALIGHTDIMWWE27': [cfg.light_brightness],
@@ -479,6 +575,8 @@ const mapping = {
'73742': [cfg.light_brightness_colortemp],
'73740': [cfg.light_brightness_colortemp],
'73739': [cfg.light_brightness_colortemp_colorxy],
'72569': [cfg.light_brightness_colortemp],
'72567': [cfg.light_brightness_colortemp],
'22670': [cfg.light_brightness],
'ICTC-G-1': [cfg.sensor_brightness, cfg.sensor_battery, cfg.sensor_action],
'ICPSHC24-30EU-IL-1': [cfg.light_brightness],
@@ -513,26 +611,32 @@ const mapping = {
'DNCKATSW001': [cfg.switch],
'Z809A': [cfg.switch, cfg.sensor_power],
'NL08-0800': [cfg.light_brightness],
'98425031': [cfg.light_brightness],
'915005106701': [cfg.light_brightness_colortemp_colorxy],
'Aj_Zigbee_Led_Strip': [cfg.light_brightness_colortemp_colorxy],
'AB32840': [cfg.light_brightness_colortemp],
'8718696485880': [cfg.light_brightness_colortemp_colorxy],
'8718696598283': [cfg.light_brightness_colortemp],
'8718696695203': [cfg.light_brightness_colortemp],
'73693': [cfg.light_brightness_colortemp_colorxy],
'324131092621': [cfg.sensor_action, cfg.sensor_battery],
'9290012607': [cfg.binary_sensor_occupancy, cfg.sensor_temperature, cfg.sensor_illuminance, cfg.sensor_battery],
'9290012607': [
cfg.binary_sensor_occupancy, cfg.sensor_temperature, cfg.sensor_illuminance, cfg.sensor_illuminance_lux,
cfg.sensor_battery,
],
'GL-C-008': [cfg.light_brightness_colortemp_colorxy],
'GL-C-009': [cfg.light_brightness],
'STSS-MULT-001': [cfg.binary_sensor_contact, cfg.sensor_battery],
'E11-G23/E11-G33': [cfg.light_brightness],
'E1ACA4ABE38A': [cfg.light_brightness],
'AC03645': [cfg.light_brightness_colortemp_colorxy],
'AC03645': [cfg.light_brightness_colortemp_colorhs],
'AC03641': [cfg.light_brightness],
'AC03648': [cfg.light_brightness_colortemp],
'FB56+ZSW05HG1.2': [cfg.switch],
'72922-A': [cfg.switch],
'AC03642': [cfg.light_brightness_colortemp],
'AC08560': [cfg.light_brightness],
'AC10786-DIM': [cfg.light_brightness],
'DNCKATSW002': [switchWithPostfix('left'), switchWithPostfix('right')],
'DNCKATSW003': [switchWithPostfix('left'), switchWithPostfix('right'), switchWithPostfix('center')],
'DNCKATSW004': [
@@ -543,13 +647,15 @@ const mapping = {
'ZLED-2709': [cfg.light_brightness],
'8718696548738': [cfg.light_brightness_colortemp],
'915005587401': [cfg.light_brightness_colortemp],
'3435011P7': [cfg.light_brightness_colortemp],
'4052899926110': [cfg.light_brightness_colortemp_colorxy],
'Z01-CIA19NAE26': [cfg.light_brightness],
'E11-N1EA': [cfg.light_brightness_colortemp_colorxy],
'E11-U2E': [cfg.light_brightness_colortemp_colorxy],
'74283': [cfg.light_brightness],
'JTQJ-BF-01LM/BW': [cfg.binary_sensor_gas, cfg.sensor_gas_density, cfg.sensor_sensitivity],
'50043': [cfg.switch],
'50045': [cfg.light_brightness],
'50044/50045': [cfg.light_brightness],
'AV2010/22': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
'3210-L': [cfg.switch, cfg.sensor_power, cfg.sensor_current, cfg.sensor_voltage],
'3320-L': [cfg.binary_sensor_contact, cfg.sensor_temperature, cfg.sensor_battery],
@@ -560,7 +666,7 @@ const mapping = {
'433714': [cfg.light_brightness],
'3261030P7': [cfg.light_brightness_colortemp],
'3216431P5': [cfg.light_brightness_colortemp],
'DJT11LM': [cfg.sensor_action, cfg.sensor_battery, cfg.sensor_sensitivity],
'DJT11LM': [cfg.sensor_action, cfg.sensor_battery, cfg.sensor_sensitivity, cfg.sensor_strength],
'E1603/E1702': [cfg.switch],
'7199960PH': [cfg.light_brightness_colorxy],
'74696': [cfg.light_brightness],
@@ -587,10 +693,13 @@ const mapping = {
'900008-WW': [cfg.light_brightness],
'Mega23M12': [cfg.light_brightness_colortemp_colorxy],
'PSS-23ZBS': [cfg.switch],
'HS1SA': [cfg.binary_sensor_smoke, cfg.binary_sensor_battery_low],
'HS1SA-M': [cfg.binary_sensor_smoke, cfg.binary_sensor_battery_low],
'Z01-A19NAE26': [cfg.light_brightness_colortemp],
'Z01-A60EAE27': [cfg.light_brightness_colortemp],
'AC01353010G': [cfg.binary_sensor_occupancy, cfg.sensor_temperature, cfg.binary_sensor_battery_low],
'AC01353010G': [
cfg.binary_sensor_occupancy, cfg.binary_sensor_tamper,
cfg.sensor_temperature, cfg.binary_sensor_battery_low,
],
'SP 120': [cfg.switch, cfg.sensor_power],
'SP 222': [cfg.switch],
'RB 248 T': [cfg.light_brightness_colortemp],
@@ -601,18 +710,19 @@ const mapping = {
'421786': [cfg.light_brightness],
'ICZB-IW11D': [cfg.light_brightness],
'3321-S': [cfg.binary_sensor_contact, cfg.sensor_temperature],
'ZPIR-8000': [cfg.binary_sensor_occupancy],
'ZPIR-8000': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
'ZCTS-808': [cfg.binary_sensor_contact, cfg.sensor_battery],
'ZNLDP12LM': [cfg.light_brightness_colortemp],
'D1821': [cfg.light_brightness_colortemp_colorxy],
'ZNCLDJ11LM': [cfg.cover_position, cfg.sensor_cover],
'LTFY004': [cfg.light_brightness_colorxy],
'GL-S-007Z': [cfg.light_brightness_colorxy_white],
'GL-S-007Z': [cfg.light_brightness_colortemp_colorxy],
'3325-S': [cfg.sensor_temperature, cfg.binary_sensor_occupancy],
'4713407': [cfg.light_brightness],
'464800': [cfg.light_brightness_colortemp],
'3261331P7': [cfg.light_brightness_colortemp],
'4033930P7': [cfg.light_brightness_colortemp],
'4023330P7': [cfg.light_brightness_colortemp],
'GL-B-008Z': [cfg.light_brightness_colortemp_colorxy],
'AV2010/25': [cfg.switch, cfg.sensor_power],
'E12-N14': [cfg.light_brightness],
@@ -626,18 +736,23 @@ const mapping = {
'HGZB-20-DE': [cfg.switch],
'D1531': [cfg.light_brightness],
'D1532': [cfg.light_brightness],
'D1533': [cfg.light_brightness],
'AV2010/32': [],
'HGZB-07A': [cfg.light_brightness_colortemp_colorxy],
'E1524/E1810': [cfg.sensor_action, cfg.sensor_battery],
'GL-C-006': [cfg.light_brightness_colortemp],
'GL-C-007': [cfg.light_brightness_colorxy_white],
'GL-C-007S': [cfg.light_brightness_colorxy_white],
'GL-C-007/GL-C-008': [cfg.light_brightness_colortemp_colorxy_white],
'100.424.11': [cfg.light_brightness_colortemp],
'AC0251100NJ': [cfg.sensor_action, cfg.sensor_battery],
'AC0251100NJ/AC0251700NJ': [cfg.sensor_action, cfg.sensor_battery],
'71831': [cfg.light_brightness_colortemp],
'404000/404005/404012': [cfg.light_brightness_colortemp_colorxy],
'44435': [cfg.light_brightness_colortemp_colorxy],
'404006/404008/404004': [cfg.light_brightness_colortemp],
'MLI-404011': [cfg.sensor_action],
'GL-S-003Z': [cfg.light_brightness_colorxy_white],
'GL-S-005Z': [cfg.light_brightness_colortemp_colorxy],
'HS1DS-E': [cfg.binary_sensor_contact],
'SP600': [cfg.switch, cfg.sensor_power],
'1613V': [cfg.switch, cfg.sensor_power],
@@ -647,14 +762,15 @@ const mapping = {
'4090130P7': [cfg.light_brightness_colortemp_colorxy],
'100.110.39': [cfg.light_brightness_colortemp_colorxy],
'TI0001': [switchWithPostfix('left'), switchWithPostfix('right')],
'SPZB0001': [cfg.thermostat(5, 30, 'current_heating_setpoint'), cfg.sensor_battery],
'SPZB0001': [cfg.thermostat(5, 30, 'current_heating_setpoint', 0.5), cfg.sensor_battery],
'HS3CG': [cfg.binary_sensor_gas],
'81825': [cfg.sensor_action],
'Z809AF': [cfg.switch, cfg.sensor_power],
'RADON TriTech ZB': [
cfg.binary_sensor_occupancy, cfg.sensor_temperature, cfg.sensor_battery, cfg.binary_sensor_battery_low,
],
'IM-Z3.0-DIM': [cfg.light_brightness],
'07005B': [cfg.light_brightness],
'07004D': [cfg.light_brightness_colortemp_colorxy],
'E1746': [],
'LED1836G9': [cfg.light_brightness],
'YRD426NRSC': [cfg.lock, cfg.sensor_battery],
@@ -664,7 +780,7 @@ const mapping = {
'RB 265': [cfg.light_brightness],
'9290019758': [
cfg.binary_sensor_occupancy, cfg.sensor_temperature,
cfg.sensor_illuminance, cfg.sensor_battery,
cfg.sensor_illuminance, cfg.sensor_illuminance_lux, cfg.sensor_battery,
],
'HGZB-042': [switchWithPostfix('top'), switchWithPostfix('bottom')],
'HGZB-42': [switchWithPostfix('top'), switchWithPostfix('bottom')],
@@ -681,11 +797,11 @@ const mapping = {
'316GLEDRF': [cfg.light_brightness],
'LVS-ZB500D': [cfg.light_brightness],
'ST218': [],
'E1525': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
'E1525/E1745': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
'ZYCT-202': [cfg.sensor_action],
'GR-ZB01-W': [cfg.cover_position],
'4090531P7': [cfg.light_brightness_colortemp_colorxy],
'HGZB-42-UK / HGZB-41': [cfg.switch],
'HGZB-42-UK / HGZB-41 / HGZB-41-UK': [cfg.switch],
'ISW-ZPR1-WP13': [
cfg.binary_sensor_occupancy, cfg.sensor_temperature,
cfg.sensor_battery, cfg.binary_sensor_battery_low,
@@ -717,7 +833,7 @@ const mapping = {
cfg.sensor_temperature, cfg.binary_sensor_contact,
cfg.sensor_battery, cfg.binary_sensor_battery_low,
],
'IM6001-BTP01': [cfg.sensor_click, cfg.sensor_temperature],
'IM6001-BTP01': [cfg.sensor_click, cfg.sensor_temperature, cfg.sensor_battery],
'AV2010/34': [cfg.sensor_click],
'PP-WHT-US': [
cfg.switch, cfg.sensor_power,
@@ -759,7 +875,11 @@ const mapping = {
'BY 285 C': [cfg.light_brightness_colortemp_colorxy],
'HS1RC-M': [cfg.sensor_action, cfg.sensor_battery],
'SWO-WDS1PA': [cfg.binary_sensor_contact],
'LLKZMK11LM': [switchWithPostfix('l1'), switchWithPostfix('l2')],
'LLKZMK11LM': [
switchWithPostfix('l1'), switchWithPostfix('l2'),
cfg.sensor_power, cfg.sensor_temperature, cfg.sensor_consumption,
],
'T18W3Z': [switchWithPostfix('l1'), switchWithPostfix('l2'), switchWithPostfix('l3')],
'LVS-SM10ZW': [cfg.binary_sensor_contact, cfg.binary_sensor_battery_low],
'HS2SK': [cfg.switch, cfg.sensor_power],
'45853GE': [cfg.switch, cfg.sensor_power],
@@ -767,7 +887,7 @@ const mapping = {
'9290011998B': [cfg.light_brightness_colortemp],
'9290022167': [cfg.light_brightness_colortemp],
'4096730U7': [cfg.light_brightness_colortemp],
'RB 278 T': [cfg.light_brightness],
'RB 278 T': [cfg.light_brightness_colortemp],
'3315-G': [
cfg.sensor_temperature, cfg.sensor_battery, cfg.binary_sensor_water_leak,
],
@@ -826,8 +946,9 @@ const mapping = {
'7099860PH': [cfg.light_brightness_colorxy],
'HV-GSCXZB269': [cfg.light_brightness_colortemp],
'3216231P5': [cfg.light_brightness_colortemp],
'AC03647': [cfg.light_brightness_colortemp_colorxy],
'AC03647': [cfg.light_brightness_colortemp_colorhs],
'12031': [cfg.cover_position],
'LS12128': [cfg.cover_position],
'421792': [cfg.light_brightness_colortemp_colorxy],
'HGZB-06A': [cfg.light_brightness_colortemp_colorxy],
'LED1733G7': [cfg.light_brightness_colortemp],
@@ -843,8 +964,12 @@ const mapping = {
'ZNMS13LM': [
cfg.sensor_action, cfg.binary_sensor_lock, cfg.binary_sensor_lock_reverse,
],
'ZNMS11LM': [
cfg.sensor_action, cfg.binary_sensor_lock, cfg.binary_sensor_lock_reverse,
],
'12050': [cfg.switch, cfg.sensor_power],
'ROB_200-004-0': [cfg.light_brightness],
'4512700': [cfg.light_brightness],
'RH3040': [cfg.sensor_battery, cfg.binary_sensor_occupancy],
'DZ4743-00B': [cfg.switch],
'GLSK3ZB-1711': [cfg.switch],
@@ -863,7 +988,7 @@ const mapping = {
switchWithPostfix('top_right'), switchWithPostfix('center_right'), switchWithPostfix('bottom_right'),
],
'3306431P7': [cfg.light_brightness_colortemp],
'AC08559': [cfg.light_brightness_colortemp],
'AC08559': [cfg.light_brightness_colortemp_colorxy],
'LVS-ZB15S': [cfg.switch],
'LZL4BWHL01': [cfg.sensor_action],
'2AJZ4KPKEY': [cfg.sensor_click, cfg.sensor_battery],
@@ -872,10 +997,18 @@ const mapping = {
'ZM-CSW002-D': [switchWithPostfix('l1'), switchWithPostfix('l2'), cfg.sensor_power],
'LVS-SN10ZW': [cfg.sensor_battery, cfg.binary_sensor_occupancy],
'LVS-ZB15R': [cfg.switch],
'TH1123ZB': [cfg.thermostat()],
'TH1123ZB': [
cfg.thermostat(7, 30, 'occupied_heating_setpoint', 1.0), cfg.sensor_local_temperature,
cfg.lock_keypad_lockout, cfg.sensor_power,
],
'TH1124ZB': [cfg.thermostat()],
'TH1400ZB': [cfg.thermostat()],
'TH1500ZB': [cfg.thermostat()],
'Zen-01-W': [cfg.thermostat()],
'9290022166': [cfg.light_brightness_colortemp_colorxy],
'PM-C140-ZB': [cfg.sensor_power, cfg.switch],
'PM-B530-ZB': [cfg.sensor_power, cfg.switch],
'PM-B430-ZB': [cfg.sensor_power, cfg.switch],
'ptvo.switch': [
switchWithPostfix('bottom_left'), switchWithPostfix('bottom_right'), switchWithPostfix('top_left'),
switchWithPostfix('top_right'), switchWithPostfix('center'), cfg.sensor_click,
@@ -886,26 +1019,25 @@ const mapping = {
],
'DIYRuZ_KEYPAD20': [],
'DTB190502A1': [],
'FL 130 C': [cfg.light_brightness_colortemp_colorxy],
'BF 263': [cfg.light_brightness],
'RF 263': [cfg.light_brightness],
'F-WTR-UK-V2': [
cfg.sensor_temperature, cfg.binary_sensor_water_leak, cfg.sensor_battery,
],
'HS1CG-M': [cfg.binary_sensor_gas],
'HS1CG_M': [cfg.binary_sensor_gas],
'LVS-SN10ZW_SN11': [cfg.sensor_battery, cfg.binary_sensor_occupancy],
'B00TN589ZG': [cfg.light_brightness],
'PSB19-SW27': [cfg.light_brightness],
'S1': [cfg.switch],
'S2': [switchWithPostfix('l1'), switchWithPostfix('l2')],
'S1': [cfg.switch, cfg.sensor_power],
'S2': [switchWithPostfix('l1'), switchWithPostfix('l2'), cfg.sensor_power],
'ZWallRemote0': [cfg.sensor_click],
'D1': [cfg.light_brightness],
'J1': [cfg.cover_position_tilt],
'D1': [cfg.light_brightness, cfg.sensor_power],
'J1': [cfg.cover_position_tilt, cfg.sensor_power],
'73741': [cfg.light_brightness_colortemp_colorxy],
'ZA806SQ1TCF': [cfg.light_brightness_colortemp],
'RF 265': [cfg.light_brightness],
'ZNCZ03LM': [cfg.switch, cfg.sensor_power],
'17436/30/P7': [cfg.light_brightness],
'17435/30/P7': [cfg.light_brightness_colorxy],
'9290018187B': [cfg.light_brightness_colortemp_colorxy],
'1741830P7': [cfg.light_brightness_colortemp_colorxy],
'Z3-1BRL': [cfg.sensor_action, cfg.sensor_brightness],
@@ -920,7 +1052,7 @@ const mapping = {
'2AJZ4KPBS': [cfg.sensor_battery, cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
'2AJZ4KPDR': [cfg.sensor_battery, cfg.binary_sensor_contact, cfg.binary_sensor_battery_low],
'6717-84': [cfg.switch],
'ICZB-KPD18S': [cfg.sensor_click],
'ICZB-KPD18S': [cfg.sensor_battery, cfg.sensor_click, cfg.sensor_action],
'100.469.65': [cfg.light_brightness_colortemp],
'E1757': [cfg.cover_position, cfg.sensor_battery],
'E1926': [cfg.cover_position, cfg.sensor_battery],
@@ -933,6 +1065,7 @@ const mapping = {
'8718699673147': [cfg.light_brightness],
'3300-P': [cfg.sensor_temperature, cfg.binary_sensor_contact, cfg.sensor_battery],
'GL-B-008ZS': [cfg.light_brightness_colortemp_colorxy],
'T1828': [cfg.light_brightness_colortemp],
'T1829': [cfg.light_brightness_colortemp],
'929002240401': [cfg.switch],
'HGZB-20-UK': [cfg.switch],
@@ -945,18 +1078,19 @@ const mapping = {
'9290020399': [cfg.light_brightness],
'929002241201': [cfg.light_brightness],
'YRD210-HA-605': [cfg.lock, cfg.sensor_battery],
'YRD220/240 TSDB': [cfg.lock, cfg.sensor_battery],
'ZM-CSW032-D': [cfg.cover],
'LH-32ZB': [cfg.sensor_humidity, cfg.sensor_temperature, cfg.sensor_battery],
'511.201': [cfg.light_brightness],
'ZNCLDJ12LM': [cfg.cover_position],
'046677552343': [cfg.switch],
'3115331PH': [cfg.light_brightness_colortemp_colorxy],
'ZWLD-100': [cfg.binary_sensor_water_leak, cfg.binary_sensor_battery_low],
'ZWLD-100': [cfg.binary_sensor_water_leak, cfg.binary_sensor_battery_low, cfg.sensor_battery],
'GL-MC-001': [cfg.light_brightness_colortemp_colorxy],
'YRD226/246 TSDB': [cfg.lock, cfg.sensor_battery],
'T1820': [cfg.light_brightness_colortemp],
'BASICZBR3': [cfg.switch],
'E1744': [cfg.sensor_action],
'E1744': [cfg.sensor_action, cfg.sensor_battery],
'TS0201': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_battery],
'LH07321': [cfg.binary_sensor_water_leak, cfg.binary_sensor_battery_low],
'GL-C-008S': [cfg.light_brightness_colortemp_colorxy],
@@ -964,9 +1098,163 @@ const mapping = {
'8718699688882': [cfg.light_brightness],
'LED1738G7': [cfg.light_brightness_colortemp],
'9290022169': [cfg.light_brightness_colortemp],
'TERNCY-PP01': [cfg.sensor_temperature, cfg.binary_sensor_occupancy, cfg.sensor_illuminance, cfg.sensor_click],
'TERNCY-PP01': [
cfg.sensor_temperature, cfg.binary_sensor_occupancy, cfg.sensor_illuminance, cfg.sensor_illuminance_lux,
cfg.sensor_click,
],
'CR11S8UZ': [cfg.sensor_action],
'RB 148 T': [cfg.light_brightness_colortemp],
'STS-OUT-US-2': [cfg.switch, cfg.sensor_power],
'UK7004240': [cfg.thermostat(), cfg.sensor_battery],
'S31ZB': [cfg.switch],
'SA-003-Zigbee': [cfg.switch],
'SZ-DWS04': [cfg.binary_sensor_contact, cfg.sensor_temperature, cfg.sensor_battery],
'ICZB-B1FC60/B3FC64/B2FC95/B2FC125': [cfg.light_brightness_colortemp],
'TS0203': [cfg.sensor_battery, cfg.binary_sensor_contact],
'TS0204': [cfg.binary_sensor_gas],
'TS0205': [cfg.binary_sensor_smoke, cfg.sensor_battery],
'TS0111': [cfg.switch],
'TS0001': [cfg.switch],
'TS0207': [cfg.binary_sensor_water_leak, cfg.sensor_battery],
'iL07_1': [cfg.binary_sensor_occupancy, cfg.binary_sensor_tamper, cfg.binary_sensor_battery_low],
'S31 Lite zb': [cfg.switch],
'LH-992ZB': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
'548727': [cfg.light_brightness_colortemp_colorxy],
'TS0202': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
'TS0218': [cfg.sensor_action, cfg.sensor_battery],
'404021': [cfg.switch],
'Eco-Dim.07': [cfg.light_brightness],
'DIYRuZ_rspm': [cfg.switch, cfg.sensor_action, cfg.sensor_power, cfg.sensor_current],
'ZG9101SAC-HP-Switch': [cfg.switch],
'ZNCZ04LM': [cfg.switch, cfg.sensor_power],
'ZNCZ12LM': [cfg.switch, cfg.sensor_power],
'GL-S-007ZS': [cfg.light_brightness_colortemp_colorxy],
'4058075816732': [cfg.light_brightness_colortemp_colorxy],
'GL-B-007ZS': [cfg.light_brightness_colortemp_colorxy],
'GL-G-007Z': [cfg.light_brightness_colortemp_colorxy],
'WXCJKG11LM': [cfg.sensor_action, cfg.sensor_battery],
'WXCJKG12LM': [cfg.sensor_action, cfg.sensor_battery],
'WXCJKG13LM': [cfg.sensor_action, cfg.sensor_battery],
'8718699693985': [cfg.sensor_action, cfg.sensor_battery],
'GL-D-004ZS': [cfg.light_brightness_colortemp_colorxy],
'AC10787': [cfg.light_brightness_colortemp],
'F-APP-UK-V2': [cfg.switch],
'S9TSZGB': [cfg.sensor_action],
'SP-EUC01': [cfg.switch],
'511.012': [cfg.light_brightness],
'GL-S-008Z': [cfg.light_brightness_colortemp_colorxy],
'TZSW22FW-L4': [switchWithPostfix('top'), switchWithPostfix('bottom')],
'GDKES-01TZXD': [cfg.switch],
'GDKES-02TZXD': [switchWithPostfix('left'), switchWithPostfix('right')],
'GDKES-03TZXD': [switchWithPostfix('left'), switchWithPostfix('center'), switchWithPostfix('right')],
'6735/6736/6737': [cfg.switch, cfg.sensor_action],
'4034031P7': [cfg.light_brightness_colortemp],
'5900131C5': [cfg.light_brightness_colortemp],
'SZ-SRN12N': [],
'ML-ST-D200': [cfg.light_brightness],
'7099930PH': [cfg.light_brightness_colorxy],
'9GED18000-009': [cfg.lock, cfg.sensor_battery],
'9GED21500-005': [cfg.lock, cfg.sensor_battery],
'MP-841': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
'MCT-370 SMA': [cfg.binary_sensor_contact, cfg.binary_sensor_battery_low],
'RB 162': [cfg.light_brightness],
'SOHM-I1': [cfg.binary_sensor_contact, cfg.binary_sensor_battery_low],
'SWHM-I1': [cfg.binary_sensor_water_leak, cfg.binary_sensor_battery_low],
'SMHM-I1': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
'SKHMP30-I1': [cfg.switch, cfg.sensor_power],
'404028': [cfg.light_brightness_colortemp_colorxy],
'595UGR22': [cfg.light_brightness_colortemp],
'6ARCZABZH': [cfg.sensor_battery, cfg.sensor_action],
'ZK-EU-2U': [cfg.switch],
'511.202': [cfg.switch],
'SP 224': [cfg.switch],
'9290022411': [cfg.light_brightness],
'E1C-NB6': [cfg.switch],
'LVS-SC7': [cfg.sensor_action],
'1742930P7': [cfg.light_brightness_colortemp_colorxy],
'ZM-L03E-Z': [switchWithPostfix('left'), switchWithPostfix('center'), switchWithPostfix('right')],
'DL15S-1BZ': [cfg.switch],
'E1D-G73WNA': [cfg.binary_sensor_contact, cfg.binary_sensor_battery_low],
'WV704R0A0902': [cfg.thermostat()],
'067776': [cfg.cover_position],
'067773': [cfg.sensor_action, cfg.sensor_battery],
'067771': [cfg.switch],
'064873': [cfg.sensor_action],
'K4003C': [cfg.switch],
'STZB402': [
cfg.thermostat(5, 30, 'occupied_heating_setpoint', 0.5),
cfg.sensor_local_temperature,
cfg.lock_keypad_lockout,
],
'SMT402': [
cfg.thermostat(5, 30, 'occupied_heating_setpoint', 0.5),
cfg.sensor_local_temperature,
cfg.lock_keypad_lockout,
],
'046677551780': [cfg.light_brightness],
'798.15': [cfg.light_brightness],
'12126': [cfg.switch],
'067775': [cfg.switch, cfg.sensor_power],
'064888': [cfg.switch],
'gq8b1uv': [cfg.light_brightness],
'GZCGQ01LM': [cfg.sensor_battery, cfg.sensor_illuminance, cfg.sensor_illuminance_lux],
'9290018215': [cfg.light_brightness],
'1743230P7': [cfg.light_brightness_colortemp_colorxy],
'100.110.51': [cfg.light_brightness_colortemp],
'ZL1000100-CCT-US-V1A02': [cfg.light_brightness],
'HGZB-DLC4-N12B': [cfg.light_brightness_colortemp_colorxy],
'U86KCJ-ZP': [cfg.sensor_action],
'HS1HT': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_battery],
'HS2ESK-E': [cfg.switch, cfg.sensor_power],
'B01M7Y8BP9': [cfg.sensor_action],
'GP-WOU019BBDWG': [cfg.switch, cfg.sensor_power],
'AV2010/21A': [cfg.binary_sensor_battery_low, cfg.binary_sensor_contact, cfg.binary_sensor_tamper],
'AL-PIR02': [cfg.binary_sensor_occupancy, cfg.binary_sensor_tamper, cfg.sensor_battery],
'MKS-CM-W5': [
switchWithPostfix('l1'), switchWithPostfix('l2'),
switchWithPostfix('l3'), switchWithPostfix('l4'),
],
'STS-WTR-250': [cfg.binary_sensor_water_leak, cfg.sensor_battery],
'ZG2835RAC': [cfg.light_brightness],
'BW-IS2': [cfg.binary_sensor_contact, cfg.sensor_battery],
'BW-IS3': [cfg.binary_sensor_occupancy],
'SLR1b': [cfg.thermostat()],
'WPT1': [],
'4058075047853': [cfg.light_brightness_colortemp_colorxy],
'ROB_200-003-0': [cfg.switch],
'4512704': [cfg.switch],
'AV2010/24A': [cfg.binary_sensor_smoke, cfg.binary_sensor_battery_low, cfg.binary_sensor_tamper],
'902010/24': [cfg.binary_sensor_smoke, cfg.binary_sensor_battery_low, cfg.binary_sensor_tamper],
'ROB_200-014-0': [cfg.light_brightness],
'4090631P7': [cfg.light_brightness_colortemp_colorxy],
'SGMHM-I1': [cfg.binary_sensor_gas, cfg.binary_sensor_battery_low, cfg.binary_sensor_tamper],
'STHM-I1H': [cfg.sensor_humidity, cfg.sensor_temperature, cfg.sensor_battery],
'BDHM8E27W70-I1': [cfg.light_brightness_colortemp],
'M420': [cfg.sensor_battery],
'8718696167991': [cfg.light_brightness_colortemp_colorxy],
'GP-LBU019BBAWU': [cfg.light_brightness],
'371000001': [cfg.light_brightness_colortemp],
'10011725': [cfg.light_brightness_colortemp_colorxy],
'929002277501': [cfg.light_brightness],
'RS 230 C': [cfg.light_brightness_colortemp_colorxy],
'LED1903C5': [cfg.light_brightness_colortemp],
'1402755': [cfg.light_brightness],
'4503848C5': [cfg.light_brightness_colortemp],
'500.48': [cfg.light_brightness],
'TS0042': [cfg.sensor_action],
'RHK06': [cfg.binary_sensor_contact, cfg.sensor_battery],
'RHK07': [cfg.sensor_action, cfg.sensor_battery],
'RHK08': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_battery],
'RHK09': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
'07046L': [cfg.sensor_action],
'07045L': [cfg.binary_sensor_contact, cfg.binary_sensor_tamper, cfg.binary_sensor_battery_low],
'3402831P7': [cfg.light_brightness_colortemp],
'TERNCY-SD01': [cfg.sensor_click, cfg.sensor_battery],
'07048L': [cfg.switch, cfg.sensor_power],
'ICZB-KPD14S': [cfg.sensor_battery, cfg.sensor_click, cfg.sensor_action],
'73743': [cfg.sensor_action, cfg.sensor_battery],
'C4': [cfg.sensor_action],
'GL-D-003ZS': [cfg.light_brightness_colortemp_colorxy],
};
Object.keys(mapping).forEach((key) => {
@@ -977,8 +1265,8 @@ Object.keys(mapping).forEach((key) => {
* This extensions handles integration with HomeAssistant
*/
class HomeAssistant extends BaseExtension {
constructor(zigbee, mqtt, state, publishEntityState) {
super(zigbee, mqtt, state, publishEntityState);
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
super(zigbee, mqtt, state, publishEntityState, eventBus);
// A map of all discoverd devices
this.discovered = {};
@@ -993,6 +1281,19 @@ class HomeAssistant extends BaseExtension {
this.discoveryTopic = settings.get().advanced.homeassistant_discovery_topic;
this.statusTopic = settings.get().advanced.homeassistant_status_topic;
this.eventBus.on('deviceRemoved', (data) => this.onDeviceRemoved(data.device));
}
onDeviceRemoved(device) {
const mappedModel = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
if (mappedModel) {
logger.info(`Clearing Home Assistant discovery topic for '${device.ieeeAddr}'`);
this.getConfigs(mappedModel).forEach((config) => {
const topic = this.getDiscoveryTopic(config, device);
this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic);
});
}
}
async onMQTTConnected() {
@@ -1002,26 +1303,34 @@ class HomeAssistant extends BaseExtension {
for (const device of this.zigbee.getClients()) {
const mappedModel = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
if (mappedModel) {
this.discover(device.ieeeAddr, mappedModel, true);
this.discover(device, mappedModel, true);
}
}
}
discover(entityID, mappedModel, force=false) {
getConfigs(mappedModel) {
const configs = mapping[mappedModel.model].slice();
if (mappedModel.hasOwnProperty('ota')) {
configs.push(cfg.binary_sensor_update_available);
}
return configs;
}
discover(device, mappedModel, force=false) {
// Check if already discoverd and check if there are configs.
const discover = force || !this.discovered[entityID];
const discover = force || !this.discovered[device.ieeeAddr];
if (!discover) {
return;
}
const entity = settings.getEntity(entityID);
const entity = settings.getEntity(device.ieeeAddr);
if (!entity || (entity.type === 'device' && !mapping[mappedModel.model]) ||
(entity.hasOwnProperty('homeassistant') && !entity.homeassistant)) {
return;
}
mapping[mappedModel.model].forEach((config) => {
const topic = `${config.type}/${entity.ID}/${config.object_id}/config`;
this.getConfigs(mappedModel).forEach((config) => {
const topic = this.getDiscoveryTopic(config, device);
const payload = {...config.discovery_payload};
const stateTopic = `${settings.get().mqtt.base_topic}/${entity.friendlyName}`;
@@ -1042,11 +1351,7 @@ class HomeAssistant extends BaseExtension {
payload.tilt_status_topic = stateTopic;
}
// Set json_attributes_topic for types which support this
// https://github.com/Koenkk/zigbee2mqtt/issues/840
if (['binary_sensor', 'sensor', 'lock', 'climate', 'fan'].includes(config.type)) {
payload.json_attributes_topic = payload.state_topic;
}
payload.json_attributes_topic = stateTopic;
// Set (unique) name
payload.name = `${entity.friendlyName}_${config.object_id}`;
@@ -1135,6 +1440,10 @@ class HomeAssistant extends BaseExtension {
payload.speed_command_topic = `${stateTopic}/set/fan_mode`;
}
if (payload.action_topic) {
payload.action_topic = stateTopic;
}
// Override configuration with user settings.
if (entity.hasOwnProperty('homeassistant')) {
const add = (obj) => {
@@ -1161,7 +1470,7 @@ class HomeAssistant extends BaseExtension {
this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, this.discoveryTopic);
});
this.discovered[entityID] = true;
this.discovered[device.ieeeAddr] = true;
}
onMQTTMessage(topic, message) {
@@ -1186,10 +1495,14 @@ class HomeAssistant extends BaseExtension {
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
const device = data.device;
if (device && mappedDevice) {
this.discover(device.ieeeAddr, mappedDevice);
this.discover(device, mappedDevice);
}
}
getDiscoveryTopic(config, device) {
return `${config.type}/${device.ieeeAddr}/${config.object_id}/config`;
}
// Only for homeassistant.test.js
_getMapping() {
return mapping;
+2 -2
View File
@@ -5,8 +5,8 @@ const logger = require('../util/logger');
const BaseExtension = require('./baseExtension');
class NetworkMap extends BaseExtension {
constructor(zigbee, mqtt, state, publishEntityState) {
super(zigbee, mqtt, state, publishEntityState);
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
super(zigbee, mqtt, state, publishEntityState, eventBus);
// Subscribe to topic.
this.topic = `${settings.get().mqtt.base_topic}/bridge/networkmap`;
+132
View File
@@ -0,0 +1,132 @@
const settings = require('../util/settings');
const logger = require('../util/logger');
const assert = require('assert');
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/ota_update/.+$`);
const BaseExtension = require('./baseExtension');
const MINUTES_10 = 1000 * 60 * 10;
class OTAUpdate extends BaseExtension {
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
super(zigbee, mqtt, state, publishEntityState, eventBus);
this.inProgress = new Set();
this.lastChecked = {};
}
onMQTTConnected() {
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/ota_update/check`);
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/ota_update/update`);
}
async onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
if (data.type !== 'commandQueryNextImageRequest' || !mappedDevice.hasOwnProperty('ota')) return;
// 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' make sense to check for a new update
// each time.
const check = this.lastChecked.hasOwnProperty(data.device.ieeeAddr) ?
(Date.now() - this.lastChecked[data.device.ieeeAddr]) > MINUTES_10 : true;
if (!check || this.inProgress.has(data.device.ieeeAddr)) return;
this.lastChecked[data.device.ieeeAddr] = Date.now();
const available = await mappedDevice.ota.isUpdateAvailable(data.device, logger, data.data);
this.publishEntityState(data.device.ieeeAddr, {update_available: available});
if (available) {
const message = `Update available for '${settingsDevice.friendly_name}'`;
logger.info(message);
this.mqtt.log('ota_update', message, {status: 'available', device: settingsDevice.friendly_name});
}
}
async readSoftwareBuildIDAndDateCode(device, update) {
try {
const endpoint = device.endpoints.find((e) => e.supportsInputCluster('genBasic'));
const result = await endpoint.read('genBasic', ['dateCode', 'swBuildId']);
if (update) {
device.softwareBuildID = result.swBuildId;
device.dateCode = result.dateCode;
device.save();
}
return {softwareBuildID: result.swBuildId, dateCode: result.dateCode};
} catch (e) {
return null;
}
}
async onMQTTMessage(topic, message) {
if (!topic.match(topicRegex)) {
return null;
}
const device = this.zigbee.resolveEntity(message);
assert(device != null && device.type === 'device', 'Device not found or not a device');
if (!device.mapped || !device.mapped.ota) {
const message = `Device '${device.name}' does not support OTA updates`;
logger.error(message);
this.mqtt.log('ota_update', message, {status: `not_supported`, device: device.name});
return;
}
if (this.inProgress.has(device.device.ieeeAddr)) {
logger.error(`Update or check already in progress for '${device.name}', skipping...`);
return;
}
this.inProgress.add(device.device.ieeeAddr);
const type = topic.split('/')[3];
if (type === 'check') {
const message = `Checking if update available for '${device.name}'`;
logger.info(message);
this.mqtt.log('ota_update', message, {status: `checking_if_available`, device: device.name});
try {
const available = await device.mapped.ota.isUpdateAvailable(device.device, logger);
const message=(available ?
`Update available for '${device.name}'` : `No update available for '${device.name}'`);
logger.info(message);
const meta = {status: available ? 'available' : 'not_available', device: device.name};
this.mqtt.log('ota_update', message, meta);
this.publishEntityState(device.device.ieeeAddr, {update_available: available});
this.lastChecked[device.device.ieeeAddr] = Date.now();
} catch (error) {
const message = `Failed to check if update available for '${device.name}' (${error.message})`;
logger.error(message);
this.mqtt.log('ota_update', message, {status: `check_failed`, device: device.name});
}
} else { // type === 'update'
const message = `Updating '${device.name}' to latest firmware`;
logger.info(message);
this.mqtt.log('ota_update', message, {status: `update_in_progress`, device: device.name});
try {
const onProgress = (progress, remaining) => {
let message = `Update of '${device.name}' at ${progress}%`;
if (remaining) {
message += `, +- ${Math.round(remaining / 60)} minutes remaining`;
}
logger.info(message);
this.mqtt.log('ota_update', message, {status: `update_progress`, device: device.name, progress});
};
const from_ = await this.readSoftwareBuildIDAndDateCode(device.device, false);
await device.mapped.ota.updateToLatest(device.device, logger, onProgress);
const to = await this.readSoftwareBuildIDAndDateCode(device.device, true);
const [fromS, toS] = [JSON.stringify(from_), JSON.stringify(to)];
const message = `Finished update of '${device.name}'` + (to ? `, from '${fromS}' to '${toS}'` : ``);
logger.info(message);
const meta = {status: `update_succeeded`, device: device.name, from: from_, to};
this.mqtt.log('ota_update', message, meta);
this.publishEntityState(device.device.ieeeAddr, {update_available: false});
} catch (error) {
const message = `Update of '${device.name}' failed (${error.message})`;
logger.error(message);
this.mqtt.log('ota_update', message, {status: `update_failed`, device: device.name});
}
}
this.inProgress.delete(device.device.ieeeAddr);
}
}
module.exports = OTAUpdate;
+2 -2
View File
@@ -9,8 +9,8 @@ const BaseExtension = require('./baseExtension');
* This extensions soft resets the ZNP after a certain timeout.
*/
class SoftReset extends BaseExtension {
constructor(zigbee, mqtt, state, publishEntityState) {
super(zigbee, mqtt, state, publishEntityState);
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
super(zigbee, mqtt, state, publishEntityState, eventBus);
this.timer = null;
this.timeout = utils.secondsToMilliseconds(settings.get().advanced.soft_reset_timeout);
}
+5
View File
@@ -22,6 +22,11 @@ class MQTT extends events.EventEmitter {
},
};
if (mqttSettings.keepalive) {
logger.debug(`Using MQTT keepalive: ${mqttSettings.keepalive}`);
options.keepalive = mqttSettings.keepalive;
}
if (mqttSettings.ca) {
logger.debug(`MQTT SSL/TLS: Path to CA certificate = ${mqttSettings.ca}`);
options.ca = fs.readFileSync(mqttSettings.ca);
+57 -32
View File
@@ -6,16 +6,25 @@ const fs = require('fs');
const fx = require('mkdir-recursive');
const rimraf = require('rimraf');
const colorizer = winston.format.colorize();
const assert = require('assert');
// Directoy to log to
// What transports to enable
const output = settings.get().advanced.log_output;
// Directory to log to
const timestamp = moment(Date.now()).format('YYYY-MM-DD.HH-mm-ss');
const directory = settings.get().advanced.log_directory.replace('%TIMESTAMP%', timestamp);
const logFilename = settings.get().advanced.log_file.replace('%TIMESTAMP%', timestamp);
// Make sure that log directoy exsists
fx.mkdirSync(directory);
// Make sure that log directoy exsists when not logging to stdout only
if (output.includes('file')) {
fx.mkdirSync(directory);
}
// Determine the log level.
const level = settings.get().advanced.log_level;
const validLevels = ['info', 'error', 'warn', 'debug'];
assert(validLevels.includes(level), `'${level}' is not a valid log_level, use one of '${validLevels.join(', ')}'`);
const levelWithCompensatedLength = {
'info': 'info ',
@@ -24,26 +33,16 @@ const levelWithCompensatedLength = {
'debug': 'debug',
};
// Create logger
const transports = {
file: new winston.transports.File({
filename: path.join(directory, 'log.txt'),
json: false,
/* istanbul ignore next */
const timestampFormat = () => moment().format(settings.get().advanced.timestamp_format);
// Setup default console logger
const transportsToUse = [
new winston.transports.Console({
level,
maxFiles: 3, // Keep last 3 files
maxsize: 10000000, // 10MB
silent: !output.includes('console'),
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(/* istanbul ignore next */(info) => {
const {timestamp, level, message} = info;
return `${levelWithCompensatedLength[level]} ${timestamp.split('.')[0]}: ${message}`;
}),
),
}),
console: new winston.transports.Console({
level,
format: winston.format.combine(
winston.format.timestamp(),
winston.format.timestamp({format: timestampFormat}),
winston.format.printf(/* istanbul ignore next */(info) => {
const {timestamp, level, message} = info;
const prefix = colorizer.colorize(level, `zigbee2mqtt:${levelWithCompensatedLength[level]}`);
@@ -51,14 +50,30 @@ const transports = {
}),
),
}),
};
];
const logger = winston.createLogger({
transports: [
transports.console,
transports.file,
],
});
// Add file logger when enabled
// NOTE: the initiation of the logger, even when not added as transport tries to create the logging directory
if (output.includes('file')) {
transportsToUse.push(new winston.transports.File({
filename: path.join(directory, logFilename),
json: false,
level,
tailable: true,
maxFiles: 3, // Keep last 3 files
maxsize: 10000000, // 10MB
format: winston.format.combine(
winston.format.timestamp({format: timestampFormat}),
winston.format.printf(/* istanbul ignore next */(info) => {
const {timestamp, level, message} = info;
return `${levelWithCompensatedLength[level]} ${timestamp.split('.')[0]}: ${message}`;
}),
),
}));
}
// Create logger
const logger = winston.createLogger({transports: transportsToUse});
// Cleanup any old log directory.
function cleanup() {
@@ -80,11 +95,21 @@ function cleanup() {
}
logger.cleanup = cleanup;
logger.directory = directory;
logger.getLevel = () => transports.console.level;
logger.getLevel = () => transportsToUse[0].level;
logger.setLevel = (level) => {
transports.console.level = level;
transports.file.level = level;
transportsToUse.forEach((transport) => transport.level = level);
};
// Print to user what logging is enabled
if (output.includes('file')) {
if (output.includes('console')) {
logger.info(`Logging to console and directory: '${directory}' filename: ${logFilename}`);
} else {
logger.info(`Logging to directory: '${directory}' filename: ${logFilename}`);
}
logger.cleanup();
} else if (output.includes('console')) {
logger.info(`Logging to console only'`);
}
module.exports = logger;
+33 -4
View File
@@ -1,5 +1,6 @@
const data = require('./data');
const file = data.joinPath('configuration.yaml');
const utils = require('./utils');
const file = process.env.ZIGBEE2MQTT_CONFIG || data.joinPath('configuration.yaml');
const objectAssignDeep = require(`object-assign-deep`);
const path = require('path');
const yaml = require('./yaml');
@@ -38,11 +39,13 @@ const defaults = {
},
},
experimental: {
// json or attribute
// json or attribute or attribute_and_json
output: 'json',
},
advanced: {
log_output: ['console', 'file'],
log_directory: path.join(data.getPath(), 'log', '%TIMESTAMP%'),
log_file: 'log.txt',
log_level: /* istanbul ignore next */ process.env.DEBUG ? 'debug' : 'info',
soft_reset_timeout: 0,
pan_id: 0x1a62,
@@ -54,6 +57,7 @@ const defaults = {
// Availability timeout in seconds, disabled by default.
availability_timeout: 0,
availability_blacklist: [],
availability_whitelist: [],
/**
* Home Assistant requires ALL attributes to be present in ALL MQTT messages send by the device.
@@ -99,6 +103,12 @@ const defaults = {
* Home Assistant status topic
*/
homeassistant_status_topic: 'hass/status',
/**
* Configurable timestampFormat
* https://github.com/Koenkk/zigbee2mqtt/commit/44db557a0c83f419d66755d14e460cd78bd6204e
*/
timestamp_format: 'YYYY-MM-DD HH:mm:ss',
},
};
@@ -112,6 +122,7 @@ const schema = {
properties: {
base_topic: {type: 'string'},
server: {type: 'string'},
keepalive: {type: 'number'},
ca: {type: 'string'},
key: {type: 'string'},
cert: {type: 'string'},
@@ -132,6 +143,12 @@ const schema = {
},
ban: {type: 'array', items: {type: 'string'}},
whitelist: {type: 'array', items: {type: 'string'}},
experimental: {
type: 'object',
properties: {
transmit_power: {type: 'number'},
},
},
advanced: {
type: 'object',
properties: {
@@ -140,7 +157,9 @@ const schema = {
channel: {type: 'number', minimum: 11, maximum: 26},
cache_state: {type: 'boolean'},
log_level: {type: 'string', enum: ['info', 'warn', 'error', 'debug']},
log_output: {type: 'array', items: {type: 'string'}},
log_directory: {type: 'string'},
log_file: {type: 'string'},
baudrate: {type: 'number'},
rtscts: {type: 'boolean'},
soft_reset_timeout: {type: 'number', minimum: 0},
@@ -149,9 +168,11 @@ const schema = {
elapsed: {type: 'boolean'},
availability_timeout: {type: 'number', minimum: 0},
availability_blacklist: {type: 'array', items: {type: 'string'}},
availability_whitelist: {type: 'array', items: {type: 'string'}},
report: {type: 'boolean'},
homeassistant_discovery_topic: {type: 'string'},
homeassistant_status_topic: {type: 'string'},
timestamp_format: {type: 'string'},
},
},
map_options: {
@@ -270,11 +291,13 @@ function write() {
function validate() {
const validate = ajv.compile(schema);
const valid = validate(_settings);
const postfixes = utils.getPostfixes();
// Verify that all friendly names are unique
const names = [];
const check = (name) => {
if (names.includes(name)) throw new Error(`Duplicate friendly_name '${name}' found`);
if (postfixes.includes(name)) throw new Error(`Following friendly_name are not allowed: '${postfixes}'`);
names.push(name);
};
Object.values(_settingsWithDefaults.devices).forEach((d) => check(d.friendly_name));
@@ -577,9 +600,15 @@ function changeFriendlyName(IDorName, newName) {
throw new Error(`friendly_name '${newName}' is already in use`);
}
const device = getDeviceThrowIfNotExists(IDorName);
const settings = get();
settings.devices[device.ID].friendly_name = newName;
if (getDevice(IDorName)) {
settings.devices[getDevice(IDorName).ID].friendly_name = newName;
} else if (getGroup(IDorName)) {
settings.groups[getGroup(IDorName).ID].friendly_name = newName;
} else {
throw new Error(`Device or group '${IDorName}' does not exist`);
}
write();
}
+2 -2
View File
@@ -28,9 +28,9 @@ function toLocalISOString(dDate) {
}
const postfixes = [
'left', 'right', 'center', 'bottom_left', 'bottom_right', 'l1', 'l2', 'default',
'left', 'right', 'center', 'bottom_left', 'bottom_right', 'l1', 'l2', 'l3', 'l4', 'default',
'top_left', 'top_right', 'white', 'rgb', 'system', 'top', 'bottom', 'center_left', 'center_right',
'ep1', 'ep2',
'ep1', 'ep2', 'row_1', 'row_2', 'row_3', 'row_4', 'relay',
];
function flatten(arr) {
+13 -2
View File
@@ -9,7 +9,7 @@ const objectAssignDeep = require('object-assign-deep');
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
const postfixes = utils.getPostfixes();
const keyEndpointByNumber = new RegExp(`.*/([0-9].*)$`);
const keyEndpointByNumber = new RegExp(`.*/([0-9]*)$`);
const herdsmanSettings = {
network: {
@@ -75,7 +75,14 @@ class Zigbee extends events.EventEmitter {
// Check if we have to turn off the led
if (settings.get().serial.disable_led) {
this.herdsman.setLED(false);
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;
await this.herdsman.setTransmitPower(transmitPower);
logger.info(`Set transmit power to '${transmitPower}'`);
}
}
@@ -237,6 +244,10 @@ class Zigbee extends events.EventEmitter {
return true;
}
}
async touchlinkFactoryReset() {
return this.herdsman.touchlinkFactoryReset();
}
}
module.exports = Zigbee;
+11361 -3669
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "zigbee2mqtt",
"version": "1.7.0",
"version": "1.11.0",
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
"main": "index.js",
"repository": {
@@ -8,7 +8,7 @@
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
},
"engines": {
"node": "10"
"node": "^10 || ^12 || ^13"
},
"keywords": [
"xiaomi",
@@ -45,8 +45,8 @@
"rimraf": "*",
"semver": "*",
"winston": "*",
"zigbee-herdsman": "0.12.3",
"zigbee-herdsman-converters": "11.1.40"
"zigbee-herdsman": "0.12.55",
"zigbee-herdsman-converters": "12.0.32"
},
"devDependencies": {
"eslint": "*",
+2 -2
View File
@@ -26,7 +26,7 @@ chown -R pi:pi /opt/zigbee2mqtt
echo "Running install. This might take a while and can produce som expected errors"
cd /opt/zigbee2mqtt || exit
su pi -c "npm install"
su pi -c "npm ci"
echo "Creating service file zigbee2mqtt.service"
service_path="/etc/systemd/system/zigbee2mqtt.service"
@@ -70,4 +70,4 @@ fi
return 0
}
[[ "$_" == "$0" ]] && zigbee2mqtt-install-package
[[ "$_" == "$0" ]] && zigbee2mqtt-install-package
+71 -5
View File
@@ -22,6 +22,8 @@ describe('Bridge config', () => {
data.writeDefaultConfiguration();
settings._reRead();
data.writeDefaultState();
logger.info.mockClear();
logger.warn.mockClear();
});
it('Should publish bridge configuration on startup', async () => {
@@ -160,7 +162,7 @@ describe('Bridge config', () => {
const payload = JSON.parse(MQTT.publish.mock.calls[0][1]);
expect(payload.length).toStrictEqual(Object.values(zigbeeHerdsman.devices).length);
expect(payload[0]).toStrictEqual({"ieeeAddr": "0x00124b00120144ae", "type": "Coordinator", "dateCode": "20190425", "friendly_name": "Coordinator", networkAddress: 0, softwareBuildID: "z-Stack", lastSeen: 100});
expect(payload[1]).toStrictEqual({"friendly_name": "bulb", "ieeeAddr": "0x000b57fffec6a5b2", "lastSeen": 1000, "manufacturerID": 4476, "model": "LED1545G12", "modelID": "TRADFRI bulb E27 WS opal 980lm", "networkAddress": 40369, "powerSource": "Mains (single phase)", "type": "Router"});
expect(payload[1]).toStrictEqual({"dateCode": null, "friendly_name": "bulb", "ieeeAddr": "0x000b57fffec6a5b2", "lastSeen": 1000, "manufacturerID": 4476, "model": "LED1545G12", "modelID": "TRADFRI bulb E27 WS opal 980lm", "networkAddress": 40369, "powerSource": "Mains (single phase)", "type": "Router"});
Date.now = now;
});
@@ -201,6 +203,47 @@ describe('Bridge config', () => {
expect(settings.getDevice('bulb_color2')).toStrictEqual(bulb_color2);
});
it('Should allow rename groups', async () => {
MQTT.publish.mockClear();
expect(settings.getGroup(1)).toStrictEqual({"ID": 1, devices: [], friendlyName: "group_1", "friendly_name": "group_1", optimistic: true, retain: false});
MQTT.events.message('zigbee2mqtt/bridge/config/rename', JSON.stringify({old: 'group_1', new: 'group_1_renamed'}));
await flushPromises();
expect(settings.getGroup(1)).toStrictEqual({"ID": 1, devices: [], friendlyName: "group_1_renamed", "friendly_name": "group_1_renamed", optimistic: true, retain: false});
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
JSON.stringify({type: 'group_renamed', message: {from: 'group_1', to: 'group_1_renamed'}}),
{qos: 0, retain: false},
expect.any(Function)
);
});
it('Should allow to rename last joined device', async () => {
const device = zigbeeHerdsman.devices.bulb;
const payload = {device};
await zigbeeHerdsman.events.deviceJoined(payload);
await flushPromises();
expect(settings.getDevice('0x000b57fffec6a5b2').friendlyName).toStrictEqual('bulb');
MQTT.events.message('zigbee2mqtt/bridge/config/rename_last', 'bulb_new_name');
await flushPromises();
expect(settings.getDevice('0x000b57fffec6a5b2').friendlyName).toStrictEqual('bulb_new_name');
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
JSON.stringify({type: 'device_renamed', message: {from: 'bulb', to: 'bulb_new_name'}}),
{qos: 0, retain: false},
expect.any(Function)
);
});
it('Shouldnt rename when no device has been joined', async () => {
controller = new Controller();
await controller.start();
await flushPromises();
expect(settings.getDevice('0x000b57fffec6a5b2').friendlyName).toStrictEqual('bulb');
MQTT.events.message('zigbee2mqtt/bridge/config/rename_last', 'bulb_new_name');
await flushPromises();
expect(settings.getDevice('0x000b57fffec6a5b2').friendlyName).toStrictEqual('bulb');
});
it('Should allow to add groups', async () => {
zigbeeHerdsman.createGroup.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/add_group', 'new_group');
@@ -238,10 +281,11 @@ describe('Bridge config', () => {
});
it('Should allow to remove groups', async () => {
settings.addGroup('to_be_removed')
MQTT.events.message('zigbee2mqtt/bridge/config/remove_group', 'to_be_removed');
const group = zigbeeHerdsman.groups.group_1;
MQTT.events.message('zigbee2mqtt/bridge/config/remove_group', 'group_1');
await flushPromises();
expect(settings.getGroup('to_be_removed')).toStrictEqual(null);
expect(group.removeFromDatabase).toHaveBeenCalledTimes(1);
});
it('Shouldnt allow add groups without id or friendly_name in json', async () => {
@@ -339,8 +383,9 @@ describe('Bridge config', () => {
MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'bulb_color');
await flushPromises();
expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(settings.getDevice('bulb_color')).toStrictEqual({"ID": "0x000b57fffec6a5b3", "friendlyName": "bulb_color", "friendly_name": "bulb_color", "retain": false})
expect(MQTT.publish).toHaveBeenCalledTimes(0);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
});
it('Should handle when ban fails', async () => {
@@ -352,7 +397,28 @@ describe('Bridge config', () => {
MQTT.events.message('zigbee2mqtt/bridge/config/ban', 'bulb_color');
await flushPromises();
expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(settings.getDevice('bulb_color')).toStrictEqual({"ID": "0x000b57fffec6a5b3", "friendlyName": "bulb_color", "friendly_name": "bulb_color", "retain": false})
expect(MQTT.publish).toHaveBeenCalledTimes(0);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
});
it('Should allow to touchlink factory reset (OK)', async () => {
zigbeeHerdsman.touchlinkFactoryReset.mockClear();
zigbeeHerdsman.touchlinkFactoryReset.mockReturnValueOnce(true);
MQTT.events.message('zigbee2mqtt/bridge/config/touchlink/factory_reset', '');
await flushPromises();
expect(zigbeeHerdsman.touchlinkFactoryReset).toHaveBeenCalledTimes(1);
expect(logger.info).toHaveBeenCalledWith('Successfully factory reset device through Touchlink');
});
it('Should allow to touchlink factory reset (FAILS)', async () => {
zigbeeHerdsman.touchlinkFactoryReset.mockClear();
zigbeeHerdsman.touchlinkFactoryReset.mockReturnValueOnce(false);
MQTT.events.message('zigbee2mqtt/bridge/config/touchlink/factory_reset', '');
await flushPromises();
expect(zigbeeHerdsman.touchlinkFactoryReset).toHaveBeenCalledTimes(1);
expect(logger.warn).toHaveBeenCalledWith('Failed to factory reset device through Touchlink');
});
});
+30 -2
View File
@@ -30,10 +30,10 @@ describe('Controller', () => {
it('Start controller', async () => {
await controller.start();
expect(logger.cleanup).toHaveBeenCalledTimes(1);
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),"serialPort":{"baudRate":115200,"rtscts":true,"path":"/dev/dummy"}});
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);
expect(logger.info).toHaveBeenCalledWith(`Currently ${Object.values(zigbeeHerdsman.devices).length - 1} devices are joined:`)
@@ -57,6 +57,7 @@ describe('Controller', () => {
const configuration = {
base_topic: "zigbee2mqtt",
server: "mqtt://localhost",
keepalive: 30,
ca, cert, key,
password: 'pass',
user: 'user1',
@@ -69,6 +70,7 @@ describe('Controller', () => {
expect(MQTT.connect).toHaveBeenCalledTimes(1);
const expected = {
"will": {"payload": "offline", "retain": true, "topic": "zigbee2mqtt/bridge/state"},
keepalive: 30,
ca: Buffer.from([99, 97]),
key: Buffer.from([107, 101, 121]),
cert: Buffer.from([99, 101, 114, 116]),
@@ -176,6 +178,13 @@ describe('Controller', () => {
expect(zigbeeHerdsman.setLED).toHaveBeenCalledWith(false);
});
it('Start controller with transmit power', async () => {
settings.set(['experimental', 'transmit_power'], 14);
await controller.start();
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledWith(14);
});
it('Start controller and stop', async () => {
await controller.start();
await controller.stop();
@@ -390,13 +399,32 @@ describe('Controller', () => {
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/dummy-2", 'no', {"qos": 0, "retain": true}, expect.any(Function));
});
it('Publish entity state attribute_json output', async () => {
await controller.start();
settings.set(['experimental', 'output'], 'attribute_and_json');
MQTT.publish.mockClear();
await controller.publishEntityState('bulb', {state: 'ON', brightness: 200, color_temp: 370, linkquality: 99});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(5);
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "200", {"qos": 0, "retain": true}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/color_temp", "370", {"qos": 0, "retain": true}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/linkquality", "99", {"qos": 0, "retain": true}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", '{"state":"ON","brightness":200,"color_temp":370,"linkquality":99}', {"qos": 0, "retain": true}, expect.any(Function));
});
it('Publish entity state with device information', async () => {
await controller.start();
settings.set(['mqtt', 'include_device_information'], true);
MQTT.publish.mockClear();
await controller.publishEntityState('bulb', {state: 'ON'});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', '{"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"device":{"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369,"type":"Router","manufacturerID":4476,"powerSource":"Mains (single phase)"}}', {"qos": 0, "retain": true}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', '{"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"device":{"friendlyName":"bulb","model":"LED1545G12","ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369,"type":"Router","manufacturerID":4476,"powerSource":"Mains (single phase)","dateCode":null}}', {"qos": 0, "retain": true}, expect.any(Function));
// Unsupported device should have model "unknown"
await controller.publishEntityState('unsupported2', {state: 'ON'});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/unsupported2', '{"state":"ON","device":{"friendlyName":"unsupported2","model":"unknown","ieeeAddr":"0x0017880104e45529","networkAddress":6536,"type":"EndDevice","manufacturerID":0,"powerSource":"Battery","dateCode":null}}', {"qos": 0, "retain": false}, expect.any(Function));
});
it('Publish entity state no empty messages', async () => {
+134 -14
View File
@@ -4,6 +4,8 @@ const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b3');
zigbeeHerdsman.returnDevices.push('0x00124b00120144ae');
zigbeeHerdsman.returnDevices.push('0x0017880104e45553');
zigbeeHerdsman.returnDevices.push('0x0017880104e45517');
const MQTT = require('./stub/mqtt');
const settings = require('../lib/util/settings');
const Controller = require('../lib/controller');
@@ -54,7 +56,7 @@ describe('Device availability', () => {
device.ping.mockImplementationOnce(() => {throw new Error('failed')});
jest.advanceTimersByTime(11 * 1000);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenNthCalledWith(1,
'zigbee2mqtt/bulb_color/availability',
'offline',
@@ -68,8 +70,8 @@ describe('Device availability', () => {
await flushPromises();
expect(logger.debug).toHaveBeenCalledTimes(3);
expect(logger.debug).toHaveBeenCalledWith("Failed to ping 'bulb_color'");
expect(MQTT.publish).toHaveBeenCalledTimes(4);
expect(MQTT.publish).toHaveBeenNthCalledWith(3,
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenNthCalledWith(1,
'zigbee2mqtt/bulb_color/availability',
'offline',
{ retain: true, qos: 0 },
@@ -99,6 +101,21 @@ describe('Device availability', () => {
expect(endpoint.read).toHaveBeenCalledWith('lightingColorCtrl', ['currentX', 'currentY', 'colorTemperature']);
});
it('Should fail gracefully when quering state after reconnect fails', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
endpoint.read.mockClear();
endpoint.read.mockImplementationOnce(() => {throw new Error('Device timedout')});
device.ping.mockImplementationOnce(() => {throw new Error('failed')});
logger.debug.mockClear();
jest.advanceTimersByTime(11 * 2000);
await flushPromises();
expect(endpoint.read).toHaveBeenCalledTimes(1);
expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']);
expect(logger.error).toHaveBeenCalledWith(`Failed to read state of 'bulb_color' after reconnect`);
});
it('Shouldnt ping again when still pinging', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
device.ping.mockClear();
@@ -171,15 +188,6 @@ describe('Device availability', () => {
expect(endpoint.read).toHaveBeenCalledTimes(0);
});
it('Should do nothing when receiving message from non-pingable device', async () => {
MQTT.publish.mockClear();
const device = zigbeeHerdsman.devices.WXKG11LM;
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(MQTT.publish).toHaveBeenCalledTimes(0);
});
it('Should not do anything when message has no device', async () => {
MQTT.publish.mockClear();
const device = zigbeeHerdsman.devices.bulb_color;
@@ -189,9 +197,23 @@ describe('Device availability', () => {
expect(MQTT.publish.mock.calls.find((c) => c[0].includes('availability'))).toBeUndefined();
});
it('Should not ping blacklisted devices', async () => {
it('Should not ping devices blacklisted by friendly name', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
settings.set(['advanced', 'availability_blacklist'], [device.ieeeAddr])
settings.set(['advanced', 'availability_blacklist'], ['bulb_color'])
await controller.stop();
await flushPromises();
controller = new Controller();
await controller.start();
await flushPromises();
device.ping.mockClear();
jest.advanceTimersByTime(11 * 1000);
await flushPromises();
expect(device.ping).toHaveBeenCalledTimes(0);
});
it('Should not ping devices blacklisted by IEEE address', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
settings.set(['advanced', 'availability_blacklist'], [device.ieeeAddr]);
await controller.stop();
await flushPromises();
controller = new Controller();
@@ -216,6 +238,51 @@ describe('Device availability', () => {
expect(device.ping).toHaveBeenCalledTimes(1);
});
it('Should ping devices whitelisted by friendly name if availability_whitelist is set', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
settings.set(['advanced', 'availability_whitelist'], ['bulb_color']);
await controller.stop();
await flushPromises();
controller = new Controller();
await controller.start();
await flushPromises();
device.ping.mockClear();
jest.advanceTimersByTime(11 * 1000);
await flushPromises();
expect(device.ping).toHaveBeenCalledTimes(1);
});
it('Should ping devices whitelisted by IEEE address if availability_whitelist is set', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
settings.set(['advanced', 'availability_whitelist'], [device.ieeeAddr]);
await controller.stop();
await flushPromises();
controller = new Controller();
await controller.start();
await flushPromises();
device.ping.mockClear();
jest.advanceTimersByTime(11 * 1000);
await flushPromises();
expect(device.ping).toHaveBeenCalledTimes(1);
});
it('Should not ping non-whitelisted devices if availability_whitelist is set', async () => {
const device = zigbeeHerdsman.devices.bulb;
getExtension().state[device.ieeeAddr] = false;
settings.set(['advanced', 'availability_whitelist'], ['0x000b57fffec6a5b3'])
await controller.stop();
await flushPromises();
controller = new Controller();
await controller.start();
await flushPromises();
jest.advanceTimersByTime(11 * 1000);
await flushPromises();
expect(device.ping).toHaveBeenCalledTimes(0);
MQTT.publish.mockClear();
await zigbeeHerdsman.events.deviceAnnounce({device});
expect(MQTT.publish).toHaveBeenCalledTimes(0);
});
it('Should not read when device has no modelID and reconnects', async () => {
const device = zigbeeHerdsman.devices.nomodel;
getExtension().state[device.ieeeAddr] = true;
@@ -233,4 +300,57 @@ describe('Device availability', () => {
await flushPromises();
expect(endpoint.read).toHaveBeenCalledTimes(0);
});
it('Should stop pinging device when removed', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
device.ping.mockClear();
const endpoint = device.getEndpoint(1);
jest.advanceTimersByTime(11 * 1000);
await flushPromises();
expect(device.ping).toHaveBeenCalledTimes(1);
settings.removeDevice(device.ieeeAddr);
jest.advanceTimersByTime(11 * 1000);
await flushPromises();
expect(device.ping).toHaveBeenCalledTimes(1);
});
it('Should publish availability when end device joins', async () => {
const device = zigbeeHerdsman.devices.WXKG02LM;
const payload = {device};
MQTT.publish.mockClear();
await zigbeeHerdsman.events.deviceJoined(payload);
await flushPromises();
expect(device.ping).toHaveBeenCalledTimes(0);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/button_double_key/availability',
'online',
{ retain: true, qos: 0 },
expect.any(Function)
);
});
it('Should mark non-pingable device as non-available when offline for longer than 24 hours', async () => {
const device = zigbeeHerdsman.devices.remote;
const defaultLastSeen = device.lastSeen;
device.lastSeen = Date.now();
MQTT.publish.mockClear();
jest.advanceTimersByTime(1000 * 60 * 60 * 1); // 1 hours
expect(MQTT.publish).toHaveBeenCalledTimes(0);
device.lastSeen = device.lastSeen - (1000 * 60 * 60 * 25);
jest.advanceTimersByTime(1000 * 60 * 60 * 1); // 1 hours
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/remote/availability',
'offline',
{ retain: true, qos: 0 },
expect.any(Function)
);
// Shouldn't do anything more when device is removed
settings.removeDevice(device.ieeeAddr);
jest.advanceTimersByTime(1000 * 60 * 60 * 1); // 1 hours
expect(MQTT.publish).toHaveBeenCalledTimes(1);
device.lastSeen = defaultLastSeen;
});
});
+21
View File
@@ -15,6 +15,7 @@ describe('Device bind', () => {
endpoint.write.mockClear();
endpoint.configureReporting.mockClear();
endpoint.bind.mockClear();
endpoint.unbind.mockClear();
}
}
@@ -171,4 +172,24 @@ describe('Device bind', () => {
expect(endpoint.bind).toHaveBeenCalledTimes(1);
expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target);
});
it('Should unbind from default_bind_group', async () => {
const device = zigbeeHerdsman.devices.remote;
const target = 'default_bind_group';
const endpoint = device.getEndpoint(1);
mockClear(device);
MQTT.events.message('zigbee2mqtt/bridge/unbind/remote', target);
await flushPromises();
expect(endpoint.unbind).toHaveBeenCalledTimes(3);
expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", 901);
expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", 901);
expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", 901);
expect(MQTT.publish).toHaveBeenCalledTimes(3);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'default_bind_group', cluster: 'genScenes'}});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'default_bind_group', cluster: 'genOnOff'}});
expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'default_bind_group', cluster: 'genLevelCtrl'}});
});
});
+36 -1
View File
@@ -9,7 +9,7 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const mocksClear = [MQTT.publish, logger.warn, logger.debug];
describe('Device receive', () => {
describe('Device configure', () => {
let controller;
expectRemoteConfigured = () => {
@@ -17,6 +17,8 @@ describe('Device receive', () => {
const endpoint1 = device.getEndpoint(1);
expect(endpoint1.bind).toHaveBeenCalledTimes(2);
expect(endpoint1.bind).toHaveBeenCalledWith('genOnOff', this.coordinatorEndoint);
expect(endpoint1.bind).toHaveBeenCalledWith('genLevelCtrl', this.coordinatorEndoint);
const endpoint2 = device.getEndpoint(2);
expect(endpoint2.write).toHaveBeenCalledTimes(1);
expect(endpoint2.write).toHaveBeenCalledWith("genBasic", {"49": {"type": 25, "value": 11}}, {"disableDefaultResponse": true, "manufacturerCode": 4107});
@@ -54,6 +56,18 @@ describe('Device receive', () => {
expectRemoteConfigured();
});
it('Should re-configure when device rejoins', async () => {
expectRemoteConfigured();
const device = zigbeeHerdsman.devices.remote;
const endpoint = device.getEndpoint(1);
await flushPromises();
mockClear(device);
const payload = {device};
zigbeeHerdsman.events.deviceJoined(payload);
await flushPromises();
expectRemoteConfigured();
});
it('Should not configure twice', async () => {
expectRemoteConfigured();
const device = zigbeeHerdsman.devices.remote;
@@ -76,6 +90,27 @@ describe('Device receive', () => {
expectRemoteConfigured();
});
it('Should allow to reconfigure manually', async () => {
expectRemoteConfigured();
mockClear(zigbeeHerdsman.devices.remote);
expectRemoteNotConfigured();
await MQTT.events.message('zigbee2mqtt/bridge/configure', 'remote');
await flushPromises();
expectRemoteConfigured();
});
it('Shouldnt manually reconfigure when device does not exist', async () => {
await MQTT.events.message('zigbee2mqtt/bridge/configure', 'remote_random_non_existing');
await flushPromises();
expect(logger.error).toHaveBeenCalledWith(`Device 'remote_random_non_existing' does not exist`);
});
it('Should skip reconfigure when device does not require this', async () => {
await MQTT.events.message('zigbee2mqtt/bridge/configure', 'bulb');
await flushPromises();
expect(logger.warn).toHaveBeenCalledWith(`Skipping configure of 'bulb', device does not require this.`)
});
it('Should not configure when interviewing', async () => {
const device = zigbeeHerdsman.devices.remote;
delete device.meta.configured;
+13
View File
@@ -15,6 +15,7 @@ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
const mockOnEvent = jest.fn();
const mappedLivolo = zigbeeHerdsmanConverters.findByZigbeeModel(zigbeeHerdsman.devices.LIVOLO.modelID);
mappedLivolo.onEvent = mockOnEvent;
zigbeeHerdsmanConverters.onEvent = jest.fn();
describe('Device event', () => {
let controller;
@@ -27,6 +28,7 @@ describe('Device event', () => {
controller = new Controller();
await controller.start();
mocksClear.forEach((m) => m.mockClear());
zigbeeHerdsmanConverters.onEvent.mockClear();
await flushPromises();
});
@@ -59,4 +61,15 @@ describe('Device event', () => {
expect(call[1]).toStrictEqual({device})
expect(call[2]).toBe(device);
});
it('Should call index onEvent with zigbee event', async () => {
zigbeeHerdsmanConverters.onEvent.mockClear();
await zigbeeHerdsman.events.deviceAnnounce({device});
await flushPromises();
expect(zigbeeHerdsmanConverters.onEvent).toHaveBeenCalledTimes(1);
const call = zigbeeHerdsmanConverters.onEvent.mock.calls[0];
expect(call[0]).toBe('deviceAnnounce')
expect(call[1]).toStrictEqual({device})
expect(call[2]).toBe(device);
});
});
Regular → Executable
+60 -20
View File
@@ -110,6 +110,29 @@ describe('Device receive', () => {
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
});
it('Should debounce messages only with the same payload values for provided debounce_ignore keys', async () => {
jest.useFakeTimers();
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1);
settings.set(['devices', device.ieeeAddr, 'debounce_ignore'], ['temperature']);
const tempMsg = {data: {measuredValue: 8}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 13};
await zigbeeHerdsman.events.message(tempMsg);
const pressureMsg = {data: {measuredValue: 2}, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 13};
await zigbeeHerdsman.events.message(pressureMsg);
const tempMsg2 = {data: {measuredValue: 7}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 13};
await zigbeeHerdsman.events.message(tempMsg2);
const humidityMsg = {data: {measuredValue: 3}, cluster: 'msRelativeHumidity', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 13};
await zigbeeHerdsman.events.message(humidityMsg);
await flushPromises();
jest.advanceTimersByTime(50);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, pressure: 2, linkquality: 13});
jest.runAllTimers();
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.07, pressure: 2, humidity: 0.03, linkquality: 13});
});
it('Should handle a zigbee message with 1 precision', async () => {
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 1);
@@ -163,6 +186,25 @@ describe('Device 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,"linkquality":10});
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,"linkquality":10});
});
it('Should handle a zigbee message with voltage 3010', async () => {
const device = zigbeeHerdsman.devices.WXKG02LM;
const data = {'65281': {'1': 3010}}
@@ -219,7 +261,7 @@ describe('Device receive', () => {
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/occupancy_sensor');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'battery': 100, 'illuminance': 381, 'voltage': 3045, linkquality: 10});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'battery': 100, 'illuminance': 381, "illuminance_lux": 381, 'voltage': 3045, linkquality: 10});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
});
@@ -362,29 +404,27 @@ describe('Device receive', () => {
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(0);
expect(logger.warn).toHaveBeenCalledWith('No converter available for \'ZNCZ02LM\' with cluster \'genBinaryOutput\' and type \'attributeReport\' and data \'{"inactiveText":"hello"}\'');
expect(logger.debug).toHaveBeenCalledWith('No converter available for \'ZNCZ02LM\' with cluster \'genBinaryOutput\' and type \'attributeReport\' and data \'{"inactiveText":"hello"}\'');
});
it('Should not log when message is from supported device but has no converters is readResponse', async () => {
const device = zigbeeHerdsman.devices.ZNCZ02LM;
const data = {inactiveText: 'hello'};
const payload = {data, cluster: 'genBinaryOutput', device, endpoint: device.getEndpoint(1), type: 'readResponse', linkquality: 20};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(0);
expect(logger.warn).toBeCalledTimes(0);
});
it('Should report correct energy and power values for different versions of SP600', async () => {
// https://github.com/Koenkk/zigbee-herdsman-converters/issues/915, OLD and NEW use different date code
// divisor of OLD is not correct and therefore underreports by factor 10.
const data = {instantaneousDemand:496,currentSummDelivered:[0,6648]}
it('Should allow to calibrate temperature values', async () => {
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
settings.set(['devices', device.ieeeAddr, 'temperature_calibration'], 2);
const data = {measuredValue: -85}
const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
const SP600_NEW = zigbeeHerdsman.devices.SP600_NEW;
await zigbeeHerdsman.events.message({data, cluster: 'seMetering', device: SP600_NEW, endpoint: SP600_NEW.getEndpoint(1), type: 'attributeReport', linkquality: 10});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 1.15, linkquality: 10});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_NEW');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({energy: 0.66, power: 49.6, linkquality: 10});
MQTT.publish.mockClear();
const SP600_OLD = zigbeeHerdsman.devices.SP600_OLD;
await zigbeeHerdsman.events.message({data, cluster: 'seMetering', device: SP600_OLD, endpoint: SP600_OLD.getEndpoint(1), type: 'attributeReport', linkquality: 10});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_OLD');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({energy: 6.648, power: 496, linkquality: 10});
});
});
+16 -2
View File
@@ -8,6 +8,7 @@ zigbeeHerdsman.returnDevices.push('0x0017880104e45553');
zigbeeHerdsman.returnDevices.push('0x0017880104e45559');
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b4');
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b7');
zigbeeHerdsman.returnDevices.push('0x90fd9ffffe4b64ax');
const MQTT = require('./stub/mqtt');
const settings = require('../lib/util/settings');
const Controller = require('../lib/controller');
@@ -29,8 +30,8 @@ describe('Device report', () => {
expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint);
expect(endpoint.configureReporting).toHaveBeenCalledTimes(3);
expect(endpoint.configureReporting).toHaveBeenCalledWith('genOnOff', [{"attribute": "onOff", "maximumReportInterval": 300, "minimumReportInterval": 0, "reportableChange": 0}]);
expect(endpoint.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [{"attribute": "currentLevel", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 0}]);
expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [{"attribute": "colorTemperature", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 0}, {"attribute": "currentX", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 0}, {"attribute": "currentY", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 0}]);
expect(endpoint.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [{"attribute": "currentLevel", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 1}]);
expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [{"attribute": "colorTemperature", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 1}, {"attribute": "currentX", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 1}, {"attribute": "currentY", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 1}]);
}
mockClear = (device) => {
@@ -197,4 +198,17 @@ describe('Device report', () => {
// Should only call Hue bulb, not e.g. tradfri
expect(zigbeeHerdsman.devices.bulb.getEndpoint(1).read).toHaveBeenCalledTimes(0);
});
it('Should not configure reporting for the ZNLDP12LM closuresWindowCovering as it is ignored', async () => {
const device = zigbeeHerdsman.devices.ZNLDP12LM;
const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
const endpoint = device.getEndpoint(1);
delete device.meta.reporting;
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(3);
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint);
expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint);
expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint);
expect(endpoint.configureReporting).toHaveBeenCalledTimes(3);
});
});
+190 -7
View File
@@ -168,6 +168,19 @@ describe('Entity publish', () => {
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
});
it('Should publish messages to zigbee devices to non default-ep with state_[EP]', async () => {
const device = zigbeeHerdsman.devices.QBKG03LM;
const endpoint = device.getEndpoint(3);
await MQTT.events.message('zigbee2mqtt/wall_switch_double/set', JSON.stringify({state_right: 'OFF'}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "off", {}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wall_switch_double');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state_right: 'OFF'});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
});
it('Should publish messages to zigbee devices with color xy', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
@@ -458,6 +471,18 @@ describe('Entity publish', () => {
expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {});
});
it('Should parse set with color attribute topic', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
await MQTT.events.message('zigbee2mqtt/bulb_color/set/color', '#64C80A');
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17806, colory: 43155, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 156, color: {x: 0.2717, y: 0.6585}});
});
it('Should parse set with ieeeAddr topic', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
@@ -493,6 +518,16 @@ describe('Entity publish', () => {
expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {});
});
it('Should parse set with number at the end of friendly_name and postfix', async () => {
const device = zigbeeHerdsman.devices.QBKG03LM;
settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'ground_floor/kitchen/wall_switch/2');
const endpoint = device.getEndpoint(2);
await MQTT.events.message('zigbee2mqtt/ground_floor/kitchen/wall_switch/2/left/set', JSON.stringify({state: 'ON'}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {});
});
it('Should not publish messages to zigbee devices when payload is invalid', async () => {
const device = zigbeeHerdsman.devices.QBKG03LM;
const endpoint = device.getEndpoint(2);
@@ -626,6 +661,19 @@ describe('Entity publish', () => {
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "off", {}, {}]);
});
it('Should allow to set color via hue and saturation', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
const payload = {"color":{"hue":250, "saturation":50}};
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "enhancedMoveToHueAndSaturation", {"direction": 0, "enhancehue": 45510.416666666664, "saturation": 127, "transtime": 0,}, {}]);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({"color":{"hue":250,"saturation":50}});
});
it('ZNCLDJ11LM open', async () => {
const device = zigbeeHerdsman.devices.ZNCLDJ11LM;
const endpoint = device.getEndpoint(1);
@@ -670,11 +718,21 @@ describe('Entity publish', () => {
it('Should turn device on with on/off when transition is provided', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
const payload = {state: 'ON', transition: 1};
const payload = {state: 'ON', transition: 3};
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 255, transtime: 30}, {}]);
});
it('Should turn device on with on/off with transition when transition 0 is provided', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
const payload = {state: 'ON', transition: 0};
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 255, transtime: 0}, {}]);
});
it('Should turn device off with onOff on off with transition', async () => {
@@ -684,12 +742,44 @@ describe('Entity publish', () => {
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "off", {}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 10}, {}]);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'});
});
it('When device is turned off and on with transition with report enabled it should restore correct brightness', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
// Set initial brightness in state
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({brightness: 200}));
await flushPromises();
endpoint.command.mockClear();
MQTT.publish.mockClear();
// Turn off
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'OFF', transition: 3}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 30}, {}]);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0]).toEqual(["zigbee2mqtt/bulb_color", JSON.stringify({state: 'OFF', brightness: 200}), {"qos": 0, "retain": false}, expect.any(Function)]);
// Bulb reports brightness while decreasing brightness
await zigbeeHerdsman.events.message({data: {currentLevel: 1}, cluster: 'genLevelCtrl', device, endpoint, type: 'attributeReport', linkquality: 10});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(MQTT.publish.mock.calls[1]).toEqual(["zigbee2mqtt/bulb_color", JSON.stringify({state: 'OFF', brightness: 1, linkquality: 10}), {"qos": 0, "retain": false}, expect.any(Function)]);
// Turn on again
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'ON', transition: 3}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 30}, {}]);
expect(MQTT.publish).toHaveBeenCalledTimes(3);
expect(MQTT.publish.mock.calls[2]).toEqual(["zigbee2mqtt/bulb_color", JSON.stringify({state: 'ON', brightness: 200, linkquality: 10}), {"qos": 0, "retain": false}, expect.any(Function)]);
});
it('Home Assistant: should set state', async () => {
settings.set(['homeassistant'], true);
const device = zigbeeHerdsman.devices.bulb_color;
@@ -752,6 +842,30 @@ describe('Entity publish', () => {
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.41, y: 0.25}, color_temp: 150, state: 'ON'});
});
it('Should publish correct state on toggle command to zigbee bulb', async () => {
const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'TOGGLE'}));
await flushPromises();
// At this point the bulb has no state yet, so we cannot determine the next state and therefore shouldn't publish it to MQTT.
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "toggle", {}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(0);
// Turn bulb off so that the bulb gets a state.
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'OFF'}));
await flushPromises();
// Toggle again, now that we have state it should publish state ON
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'TOGGLE'}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(3);
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON'});
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false});
});
it('Should publish messages with options disableDefaultResponse', async () => {
const device = zigbeeHerdsman.devices.GLEDOPTO1112;
const endpoint = device.getEndpoint(11);
@@ -776,13 +890,29 @@ describe('Entity publish', () => {
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
});
it('Should publish brightness_move up to zigbee devices', async () => {
const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({brightness_move: -40}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "move", {"movemode": 1, "rate": 40}, {});
});
it('Should publish brightness_move down to zigbee devices', async () => {
const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({brightness_move: 30}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "move", {"movemode": 0, "rate": 30}, {});
});
it('HS2WD-E burglar warning', async () => {
const endpoint = zigbeeHerdsman.devices.HS2WD.getEndpoint(1);
const payload = {warning: {duration: 100, mode: 'burglar', strobe: true, level: 'high'}};
await MQTT.events.message('zigbee2mqtt/siren/set', JSON.stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 22, "warningduration": 100}, {});
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 22, "warningduration": 100}, {disableDefaultResponse: true});
});
it('HS2WD-E emergency warning', async () => {
@@ -791,7 +921,7 @@ describe('Entity publish', () => {
await MQTT.events.message('zigbee2mqtt/siren/set', JSON.stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 51, "warningduration": 10}, {});
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 51, "warningduration": 10}, {disableDefaultResponse: true});
});
it('HS2WD-E emergency without level', async () => {
@@ -800,7 +930,7 @@ describe('Entity publish', () => {
await MQTT.events.message('zigbee2mqtt/siren/set', JSON.stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 49, "warningduration": 10}, {});
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 49, "warningduration": 10}, {disableDefaultResponse: true});
});
it('HS2WD-E wrong payload (should use defaults)', async () => {
@@ -809,7 +939,7 @@ describe('Entity publish', () => {
await MQTT.events.message('zigbee2mqtt/siren/set', JSON.stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 53, "warningduration": 10}, {});
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 53, "warningduration": 10}, {disableDefaultResponse: true});
});
it('Shouldnt do anythign when device is not supported', async () => {
@@ -818,4 +948,57 @@ describe('Entity publish', () => {
await flushPromises();
expectNothingPublished();
});
it('Should publish state to roller shutter', async () => {
const endpoint = zigbeeHerdsman.devices.roller_shutter.getEndpoint(1);
await MQTT.events.message('zigbee2mqtt/roller_shutter/set', JSON.stringify({state: 'OPEN'}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevel", {"level": "255", "transtime": 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/roller_shutter');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({position: 100});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
});
it('Should publish to MKS-CM-W5', async () => {
const device = zigbeeHerdsman.devices['MKS-CM-W5'];
const endpoint = device.getEndpoint(1);
await MQTT.events.message('zigbee2mqtt/MKS-CM-W5/l3/set', JSON.stringify({state: 'ON'}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("manuSpecificTuyaDimmer", "setData", {data: [1,1], dp: 259, fn: 0, status: 0, transid: 16}, {disableDefaultResponse: true});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/MKS-CM-W5');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state_l3: 'ON'});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
});
it('Should publish separate genOnOff to GL-S-007ZS when setting state and brightness as bulb doesnt turn on with moveToLevelWithOnOff', async () => {
// https://github.com/Koenkk/zigbee2mqtt/issues/2757
jest.useFakeTimers();
const device = zigbeeHerdsman.devices['GL-S-007ZS'];
const endpoint = device.getEndpoint(1);
await MQTT.events.message('zigbee2mqtt/GL-S-007ZS/set', JSON.stringify({state: 'ON', brightness: 20}));
await flushPromises();
jest.runAllTimers();
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual([ 'genOnOff', 'on', {}, {} ]);
expect(endpoint.command.mock.calls[1]).toEqual([ 'genLevelCtrl', 'moveToLevelWithOnOff', { level: 20, transtime: 0 }, {} ]);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0]).toEqual([ 'zigbee2mqtt/GL-S-007ZS', '{"state":"ON","brightness":20}', { qos: 0, retain: false }, expect.any(Function)]);
jest.useRealTimers();
});
it('Should log as error when setting property with no defined converter', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
const payload = {'brightness_move': 20};
logger.error.mockClear();
await MQTT.events.message('zigbee2mqtt/bulb_color/get', JSON.stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(0);
expect(logger.error).toHaveBeenCalledWith("No converter available for 'get' 'brightness_move' (20)");
});
});
+86 -3
View File
@@ -5,7 +5,6 @@ const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
const flushPromises = () => new Promise(setImmediate);
const MQTT = require('./stub/mqtt');
const Controller = require('../lib/controller');
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
describe('HomeAssistant extension', () => {
beforeEach(async () => {
@@ -22,7 +21,7 @@ describe('HomeAssistant extension', () => {
it('Should have mapping for all devices supported by zigbee-herdsman-converters', () => {
const missing = [];
const HomeAssistant = require('../lib/extension/homeassistant');
const ha = new HomeAssistant(null, null, null, null);
const ha = new HomeAssistant(null, null, null, null, {on: () => {}});
require('zigbee-herdsman-converters').devices.forEach((d) => {
if (!ha._getMapping()[d.model]) {
@@ -141,7 +140,8 @@ describe('HomeAssistant extension', () => {
);
payload = {
'unit_of_measurement': '-',
'icon': 'mdi:signal',
'unit_of_measurement': 'lqi',
'value_template': '{{ value_json.linkquality }}',
'state_topic': 'zigbee2mqtt/weather_sensor',
'json_attributes_topic': 'zigbee2mqtt/weather_sensor',
@@ -416,6 +416,7 @@ describe('HomeAssistant extension', () => {
set_position_topic: 'zigbee2mqtt/smart_vent/set',
set_position_template: '{ "position": {{ position }} }',
value_template: '{{ value_json.position }}',
json_attributes_topic: 'zigbee2mqtt/smart_vent',
name: 'smart_vent_cover',
unique_id: '0x0017880104e45551_cover_zigbee2mqtt',
device:
@@ -636,4 +637,86 @@ describe('HomeAssistant extension', () => {
expect.any(Function),
);
});
it('Should clear discovery when device is removed', async () => {
controller = new Controller(false);
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'weather_sensor');
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'homeassistant/sensor/0x0017880104e45522/temperature/config',
null,
{ retain: true, qos: 0 },
expect.any(Function),
);
expect(MQTT.publish).toHaveBeenCalledWith(
'homeassistant/sensor/0x0017880104e45522/humidity/config',
null,
{ retain: true, qos: 0 },
expect.any(Function),
);
expect(MQTT.publish).toHaveBeenCalledWith(
'homeassistant/sensor/0x0017880104e45522/pressure/config',
null,
{ retain: true, qos: 0 },
expect.any(Function),
);
expect(MQTT.publish).toHaveBeenCalledWith(
'homeassistant/sensor/0x0017880104e45522/battery/config',
null,
{ retain: true, qos: 0 },
expect.any(Function),
);
expect(MQTT.publish).toHaveBeenCalledWith(
'homeassistant/sensor/0x0017880104e45522/linkquality/config',
null,
{ retain: true, qos: 0 },
expect.any(Function),
);
});
it('Should not clear discovery when unsupported device is removed', async () => {
controller = new Controller(false);
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'unsupported2');
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
});
it('Should discover update_available sensor when device supports it', async () => {
controller = new Controller(false);
await controller.start();
await flushPromises();
const payload = {
"payload_on":true,
"payload_off":false,
"value_template":"{{ value_json.update_available}}",
"state_topic":"zigbee2mqtt/bulb",
"json_attributes_topic":"zigbee2mqtt/bulb",
"name":"bulb_update_available",
"unique_id":"0x000b57fffec6a5b2_update_available_zigbee2mqtt",
"device":{
"identifiers":[
"zigbee2mqtt_0x000b57fffec6a5b2"
],
"name":"bulb",
'sw_version': this.version,
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
"manufacturer":"IKEA"
},
"availability_topic":"zigbee2mqtt/bridge/state"
};
expect(MQTT.publish).toHaveBeenCalledWith(
'homeassistant/binary_sensor/0x000b57fffec6a5b2/update_available/config',
JSON.stringify(payload),
{ retain: true, qos: 0 },
expect.any(Function),
);
});
});
+65 -5
View File
@@ -1,20 +1,41 @@
const tmp = require('tmp');
const data = require('./stub/data');
const dir = tmp.dirSync();
const settings = require('../lib/util/settings');
settings.set(['advanced', 'log_directory'], dir.name + '/%TIMESTAMP%');
const logger = require('../lib/util/logger.js');
let settings;
const fs = require('fs');
const path = require('path');
const data = require('./stub/data');
let stdOutWriteOriginal;
const rimraf = require('rimraf');
describe('Logger', () => {
beforeEach(async () => {
data.writeDefaultConfiguration();
jest.resetModules();
settings = require('../lib/util/settings');
settings.set(['advanced', 'log_directory'], dir.name + '/%TIMESTAMP%');
settings._reRead();
stdOutWriteOriginal = console._stdout.write;
console._stdout.write = () => {};
});
afterEach(async () => {
console._stdout.write = stdOutWriteOriginal;
});
it('Create log directory', () => {
const logger = require('../lib/util/logger.js');
const dirs = fs.readdirSync(dir.name);
expect(dirs.length).toBe(1);
});
it('Should cleanup', () => {
for (let i = 0; i < 20; i++) {
const logger = require('../lib/util/logger.js');
for (const d of fs.readdirSync(dir.name)) {
rimraf.sync(path.join(dir.name, d));
}
for (let i = 0; i < 21; i++) {
fs.mkdirSync(path.join(dir.name, `log_${i}`));
}
@@ -24,6 +45,7 @@ describe('Logger', () => {
})
it('Should not cleanup when there is no timestamp set', () => {
const logger = require('../lib/util/logger.js');
for (let i = 30; i < 40; i++) {
fs.mkdirSync(path.join(dir.name, `log_${i}`));
}
@@ -35,7 +57,45 @@ describe('Logger', () => {
})
it('Set and get log level', () => {
const logger = require('../lib/util/logger.js');
logger.setLevel('debug');
expect(logger.getLevel()).toBe('debug');
});
it('Logger should be console and file by default', () => {
const logger = require('../lib/util/logger.js');
const pipes = logger._readableState.pipes;
expect(pipes.length).toBe(2);
expect(pipes[0].constructor.name).toBe('Console');
expect(pipes[0].silent).toBe(false);
expect(pipes[1].constructor.name).toBe('File');
expect(pipes[1].dirname.startsWith(dir.name)).toBeTruthy();
});
it('Logger can be file only', () => {
settings.set(['advanced', 'log_output'], ['file']);
const logger = require('../lib/util/logger.js');
const pipes = logger._readableState.pipes;
expect(pipes.length).toBe(2);
expect(pipes[0].constructor.name).toBe('Console');
expect(pipes[0].silent).toBe(true);
expect(pipes[1].constructor.name).toBe('File');
expect(pipes[1].dirname.startsWith(dir.name)).toBeTruthy();
});
it('Logger can be console only', () => {
settings.set(['advanced', 'log_output'], ['console']);
const logger = require('../lib/util/logger.js');
const pipes = logger._readableState.pipes;
expect(pipes.constructor.name).toBe('Console');
expect(pipes.silent).toBe(false);
});
it('Logger can be nothing', () => {
settings.set(['advanced', 'log_output'], []);
const logger = require('../lib/util/logger.js');
const pipes = logger._readableState.pipes;
expect(pipes.constructor.name).toBe('Console');
expect(pipes.silent).toBe(true);
});
});
+192
View File
@@ -0,0 +1,192 @@
const data = require('./stub/data');
const logger = require('./stub/logger');
const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
const MQTT = require('./stub/mqtt');
const settings = require('../lib/util/settings');
const Controller = require('../lib/controller');
const flushPromises = () => new Promise(setImmediate);
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
describe('OTA update', () => {
let controller;
mockClear = (mapped) => {
mapped.ota.updateToLatest = jest.fn();
mapped.ota.isUpdateAvailable = jest.fn();
}
beforeEach(async () => {
data.writeDefaultConfiguration();
settings._reRead();
data.writeEmptyState();
controller = new Controller();
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
});
it('Should subscribe to nested topics', async () => {
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/ota_update/check');
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/ota_update/update');
});
it('Should OTA update a device', async () => {
const device = zigbeeHerdsman.devices.bulb;
const endpoint = device.endpoints[0];
let count = 0;
endpoint.read.mockImplementation(() => {
count++;
return {swBuildId: count, dateCode: '2019010' + count}
});
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
mockClear(mapped);
logger.info.mockClear();
logger.error.mockClear();
device.save.mockClear();
mapped.ota.updateToLatest.mockImplementationOnce((a, b, onUpdate) => {
onUpdate(0, null);
onUpdate(10, 3600);
});
MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb');
await flushPromises();
expect(logger.info).toHaveBeenCalledWith(`Updating 'bulb' to latest firmware`);
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(0);
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(1);
expect(mapped.ota.updateToLatest).toHaveBeenCalledWith(device, logger, expect.any(Function));
expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0%`);
expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10%, +- 60 minutes remaining`);
expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb', from '{"softwareBuildID":1,"dateCode":"20190101"}' to '{"softwareBuildID":2,"dateCode":"20190102"}'`);
expect(logger.error).toHaveBeenCalledTimes(0);
expect(device.save).toHaveBeenCalledTimes(1);
expect(device.dateCode).toBe('20190102');
expect(device.softwareBuildID).toBe(2);
});
it('Should handle when OTA update fails', async () => {
const device = zigbeeHerdsman.devices.bulb;
const endpoint = device.endpoints[0];
endpoint.read.mockImplementation(() => {return {swBuildId: 1, dateCode: '2019010'}});
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
mockClear(mapped);
logger.info.mockClear();
logger.error.mockClear();
device.save.mockClear();
mapped.ota.updateToLatest.mockImplementationOnce((a, b, onUpdate) => {
throw new Error('Update failed');
});
MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb');
await flushPromises();
expect(logger.error).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith(`Update of 'bulb' failed (Update failed)`);
});
it('Should be able to check if OTA update is available', async () => {
const device = zigbeeHerdsman.devices.bulb;
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
mockClear(mapped);
logger.info.mockClear();
mapped.ota.isUpdateAvailable.mockReturnValueOnce(false);
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
await flushPromises();
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0);
expect(logger.info).toHaveBeenCalledWith(`No update available for 'bulb'`);
logger.info.mockClear();
mapped.ota.isUpdateAvailable.mockReturnValueOnce(true);
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
await flushPromises();
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(2);
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0);
expect(logger.info).toHaveBeenCalledWith(`Update available for 'bulb'`);
});
it('Should handle if OTA update check fails', async () => {
const device = zigbeeHerdsman.devices.bulb;
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
mockClear(mapped);
logger.error.mockClear();
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {throw new Error('RF singals disturbed because of dogs barking')});
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
await flushPromises();
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0);
expect(logger.error).toHaveBeenCalledWith(`Failed to check if update available for 'bulb' (RF singals disturbed because of dogs barking)`);
});
it('Should not check for OTA when device does not support it', async () => {
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb_color_2');
await flushPromises();
expect(logger.error).toHaveBeenCalledWith(`Device 'bulb_color_2' does not support OTA updates`);
});
it('Should refuse to check/update when already in progress', async () => {
jest.useFakeTimers();
const device = zigbeeHerdsman.devices.bulb;
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
mockClear(mapped);
logger.info.mockClear();
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {
return new Promise((resolve, reject) => {setTimeout(() => resolve(), 99999)})
});
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
await flushPromises();
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
await flushPromises();
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith(`Update or check already in progress for 'bulb', skipping...`);
jest.runAllTimers();
await flushPromises();
});
it('Shouldnt crash when read modelID after OTA update fails', async () => {
const device = zigbeeHerdsman.devices.bulb;
const endpoint = device.endpoints[0];
let count = 0;
endpoint.read.mockImplementation(() => {
if (count === 1) throw new Error('Failed!')
count++;
return {swBuildId: 1, dateCode: '2019010'}
});
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
mockClear(mapped);
logger.info.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb');
await flushPromises();
expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`);
});
it('Should check for update when device requests it', async () => {
const device = zigbeeHerdsman.devices.bulb;
const data = {imageType: 12382};
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
mockClear(mapped);
mapped.ota.isUpdateAvailable.mockReturnValueOnce(true);
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(logger.info).toHaveBeenCalledWith(`Update available for 'bulb'`)
// Should not request again when device asks again after a short time
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
const extension = controller.extensions.find((e) => e.constructor.name === 'OTAUpdate');
extension.lastChecked = {};
logger.info.mockClear();
mapped.ota.isUpdateAvailable.mockReturnValueOnce(false);
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(logger.info).not.toHaveBeenCalledWith(`Update available for 'bulb'`)
});
});
+29 -1
View File
@@ -1,6 +1,7 @@
require('./stub/logger');
require('./stub/data');
const data = require('../lib/util/data.js');
const utils = require('../lib/util/utils.js');
const settings = require('../lib/util/settings.js');
const fs = require('fs');
const configurationFile = data.joinPath('configuration.yaml');
@@ -91,7 +92,7 @@ describe('Settings', () => {
settings.getDevice('0x12345678');
});
it('onlythis Should read MQTT username asn password form a separate file', () => {
it('Should read MQTT username and password form a separate file', () => {
const contentConfiguration = {
mqtt: {
server: 'my.mqtt.server',
@@ -511,6 +512,18 @@ describe('Settings', () => {
}).toThrowError(`Duplicate friendly_name 'myname' found`);
});
it('Configuration shouldnt be valid when friendly_name is a postfix', async () => {
write(configurationFile, {
devices: {'0x0017880104e45519': {friendly_name: 'left', retain: false}},
});
settings._reRead();
expect(() => {
settings.validate();
}).toThrowError(`Following friendly_name are not allowed: '${utils.getPostfixes()}'`);
});
it('Configuration shouldnt be valid when duplicate friendly_name are used', async () => {
write(configurationFile, {
devices: {
@@ -526,6 +539,21 @@ describe('Settings', () => {
}).toThrowError(`friendly_name 'myname' is already in use`);
});
it('Should throw when removing device which doesnt exist', async () => {
write(configurationFile, {
devices: {
'0x0017880104e45519': {friendly_name: 'myname', retain: false},
'0x0017880104e45511': {friendly_name: 'myname1', retain: false}
},
});
settings._reRead();
expect(() => {
settings.removeDevice('myname33');
}).toThrowError(`Device 'myname33' does not exist`);
});
it('Shouldnt write to configuration.yaml when there are no changes in it', () => {
const contentConfiguration = {devices: 'devices.yaml'};
const contentDevices = {};
+18
View File
@@ -129,6 +129,24 @@ function writeDefaultConfiguration() {
'0x90fd9ffffe4b64ae': {
retain: false,
friendly_name: 'tradfri_remote',
},
'0x90fd9ffffe4b64af': {
friendly_name: 'roller_shutter',
},
'0x90fd9ffffe4b64ax': {
friendly_name: 'ZNLDP12LM',
},
'0x90fd9ffffe4b64aa': {
friendly_name: 'SP600_OLD',
},
'0x90fd9ffffe4b64ab': {
friendly_name: 'SP600_NEW',
},
'0x90fd9ffffe4b64ac': {
friendly_name: 'MKS-CM-W5',
},
'0x0017880104e45526': {
friendly_name: 'GL-S-007ZS',
}
},
groups: {
+20 -4
View File
@@ -7,11 +7,13 @@ class Group {
this.command = jest.fn();
this.meta = {};
this.members = members;
this.removeFromDatabase = jest.fn();
this.hasMember = (endpoint) => this.members.includes(endpoint);
}
}
const clusters = {
'genBasic': 0,
'genScenes': 5,
'genOnOff': 6,
'genLevelCtrl': 8,
@@ -20,8 +22,9 @@ const clusters = {
}
class Endpoint {
constructor(ID, inputClusters, outputClusters, deviceIeeeAddress, binds=[]) {
constructor(ID, inputClusters, outputClusters, deviceIeeeAddress, binds=[], clusterValues={}) {
this.deviceIeeeAddress = deviceIeeeAddress;
this.clusterValues = clusterValues;
this.ID = ID;
this.inputClusters = inputClusters;
this.outputClusters = outputClusters;
@@ -33,7 +36,7 @@ class Endpoint {
this.configureReporting = jest.fn();
this.binds = binds;
this.supportsInputCluster = (cluster) => {
assert(clusters[cluster], `Undefined '${cluster}'`);
assert(clusters[cluster] !== undefined, `Undefined '${cluster}'`);
return this.inputClusters.includes(clusters[cluster]);
}
@@ -60,13 +63,18 @@ class Endpoint {
this.removeFromAllGroups = () => {
Object.values(groups).forEach((g) => this.removeFromGroup(g))
}
this.getClusterAttributeValue = (cluster, value) => {
return this.clusterValues[cluster][value];
}
}
}
class Device {
constructor(type, ieeeAddr, networkAddress, manufacturerID, endpoints, interviewCompleted, powerSource = null, modelID = null, interviewing=false, manufacturerName) {
constructor(type, ieeeAddr, networkAddress, manufacturerID, endpoints, interviewCompleted, powerSource = null, modelID = null, interviewing=false, manufacturerName, dateCode= null) {
this.type = type;
this.ieeeAddr = ieeeAddr;
this.dateCode = dateCode;
this.networkAddress = networkAddress;
this.manufacturerID = manufacturerID;
this.endpoints = endpoints;
@@ -113,7 +121,7 @@ const devices = {
'E1743': new Device('Router', '0x0017880104e45540', 6540,4476, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'TRADFRI on/off switch'),
'QBKG04LM': new Device('Router', '0x0017880104e45541', 6549,4151, [new Endpoint(1, [0], []), new Endpoint(2, [0, 6], [])], true, "Mains (single phase)", 'lumi.ctrl_neutral1'),
'QBKG03LM':new Device('Router', '0x0017880104e45542', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45542'), new Endpoint(2, [0, 6], [], '0x0017880104e45542'), new Endpoint(3, [0, 6], [], '0x0017880104e45542')], true, "Mains (single phase)", 'lumi.ctrl_neutral2'),
'GLEDOPTO1112': new Device('Router', '0x0017880104e45543', 6540,4151, [new Endpoint(11, [0], []), new Endpoint(13, [0], [])], true, "Mains (single phase)", 'GL-C-008'),
'GLEDOPTO1112': new Device('Router', '0x0017880104e45543', 6540, 4151, [new Endpoint(11, [0], [], '0x0017880104e45543'), new Endpoint(13, [0], [], '0x0017880104e45543')], true, "Mains (single phase)", 'GL-C-008'),
'GLEDOPTO111213': new Device('Router', '0x0017880104e45544', 6540,4151, [new Endpoint(11, [0], []), new Endpoint(13, [0], []), new Endpoint(12, [0], [])], true, "Mains (single phase)", 'GL-C-008'),
'HGZB04D': new Device('Router', '0x0017880104e45545', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'FB56+ZSC05HG1.0'),
'ZNCLDJ11LM': new Device('Router', '0x0017880104e45547', 6540,4151, [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], true, "Mains (single phase)", 'lumi.curtain'),
@@ -128,6 +136,12 @@ const devices = {
'CC2530_ROUTER': new Device('Router', '0x0017880104e45559', 6540,4151, [new Endpoint(1, [0, 6], [])], true, "Mains (single phase)", 'lumi.router'),
'LIVOLO': new Device('Router', '0x0017880104e45560', 6541,4152, [new Endpoint(6, [0, 6], [])], true, "Mains (single phase)", 'TI0001 '),
'tradfri_remote': new Device('EndDevice', '0x90fd9ffffe4b64ae', 33906, 4476, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x90fd9ffffe4b64ae')], true, "Battery", "TRADFRI remote control"),
'roller_shutter': new Device('EndDevice', '0x90fd9ffffe4b64af', 33906, 4476, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x90fd9ffffe4b64af')], true, "Battery", "SCM-R_00.00.03.15TC"),
'ZNLDP12LM': new Device('Router', '0x90fd9ffffe4b64ax', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64ax')], true, "Mains (single phase)", "lumi.light.aqcn02"),
'SP600_OLD': new Device('Router', '0x90fd9ffffe4b64aa', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64aa', [], {seMetering: {"multiplier":1,"divisor":10000}})], true, "Mains (single phase)", "SP600", false, 'Salus', '20160120'),
'SP600_NEW': new Device('Router', '0x90fd9ffffe4b64ab', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64aa', [], {seMetering: {"multiplier":1,"divisor":10000}})], true, "Mains (single phase)", "SP600", false, 'Salus', '20170220'),
'MKS-CM-W5': new Device('Router', '0x90fd9ffffe4b64ac', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64aa', [], {})], true, "Mains (single phase)", "qnazj70", false),
'GL-S-007ZS': new Device('Router', '0x0017880104e45526', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45526')], true, "Mains (single phase)", 'GL-S-007ZS'),
}
const groups = {
@@ -138,6 +152,8 @@ const groups = {
}
const mock = {
setTransmitPower: jest.fn(),
touchlinkFactoryReset: jest.fn(),
start: jest.fn(),
permitJoin: jest.fn(),
getCoordinatorVersion: jest.fn().mockReturnValue({type: 'z-Stack', meta: {version: 1, revision: 20190425}}),
+1 -2
View File
@@ -11,8 +11,7 @@ git checkout HEAD -- npm-shrinkwrap.json
git pull
echo "Installing dependencies..."
rm -rf node_modules
npm install
npm ci
echo "Restore configuration..."
cp -R data-backup/* data