Compare commits

...
275 Commits
Author SHA1 Message Date
Koen Kanters 7d27a54f8f 1.12.2 2020-04-09 17:48:12 +02:00
Koen Kanters e1355aa2e0 Update herdsman 2020-04-09 17:48:08 +02:00
Koen Kanters 23805c25af 1.12.1 2020-04-08 17:54:22 +02:00
Koen Kanters 0cffae8118 Undev 2020-04-08 17:54:17 +02:00
Koen Kanters 864e8b26d5 Update converters. 2020-04-06 21:39:06 +02:00
Koen Kanters ad48ea2af6 Update herdsman. 2020-04-06 19:14:55 +02:00
ghostcodeandKoen Kanters 418b3d1e5d Action and click trigger for multiple endpoints and 511.344 / ZG2819S support (#3289)
* Home Assistant support for ZG2819S/511.344

* Added 'homeassistant_legacy_triggers' support for devices with multiple endpoints

* Changed code style to pass lint test

* Reverted fix for the action and click trigger to send an empty message for devices with multiple endpoints

* Home Assistant support for the updated ZG2819S converter

* Update homeassistant.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-04-05 21:53:18 +02:00
Koen Kanters 4970d1a927 Update herdsman. 2020-04-05 21:32:24 +02:00
Koen Kanters e71485cd50 Refactor 2020-04-05 20:36:09 +02:00
Koen Kanters 552eec7baa Isolate legacy MQTT log. #3281 2020-04-05 18:36:08 +02:00
Koen Kanters 9df1589361 Move stateChange event to event bus. 2020-04-05 15:48:23 +02:00
Koen Kanters 1e6f99d88f Don't cache properties which start with action_ 2020-04-05 15:41:48 +02:00
Koen Kanters 4fa9aaa0b2 Refactor 2020-04-05 15:41:24 +02:00
Koen Kanters 3ef7555ca2 Refactor 2020-04-05 00:05:05 +02:00
Koen Kanters e0076367d3 Start on legacy api isolation. https://github.com/Koenkk/zigbee2mqtt/issues/3281 2020-04-04 22:47:23 +02:00
Koen Kanters 903cec5760 Allow to disable log rotation. #3280 2020-04-04 19:46:43 +02:00
Koen Kanters 1d9d63062a Add missing devices to configuration. #3263 2020-04-04 19:15:24 +02:00
Jorge Schrauwen 1e28c4285b entityPublish now passes in meta.mapped as an array for groups (#3283) 2020-04-04 18:59:14 +02:00
Koen Kanters 98cf258489 Update converters. 2020-04-03 21:38:43 +02:00
Koen Kanters b15e4d1ad9 Update converters. 2020-04-03 19:19:52 +02:00
Kiall Mac Innes 1abb5b6a48 Fix conflict between illuminance and illuminance_lux (#3275)
* Fix conflict between illuminance and illuminance_lux

On certain devices, e.g. GZCGQ01LM, 9290012607, 9290019758, TERNCY-PP01 etc, both
cfg.sensor_illuminance and cfg.sensor_illuminance_lux are applied. As both of these
had the same object_id, only one would show in Home Assistant. Oddly, a mix of both
would apply - e.g. I would get the illuminance value, with the illuminance_lux unit
of 'lx'.

* Add a test for duplicated type/object_id in HA discovery configs
2020-04-03 11:12:58 +02:00
Koen Kanters 496c5329a2 Update converters. 2020-04-02 18:15:10 +02:00
Koen Kanters 3e1db1787d ZM-CSW032-D doesn't support reporting. https://github.com/Koenkk/zigbee2mqtt/issues/3216 2020-04-02 18:11:49 +02:00
Daniel ZegarraandKoen Kanters 86c3ce3705 Added HA config for radiator valve Siterwell GS361 (#3259)
* Added HA config for radiator valve Siterwell GS361

* Updated payload and state keywords for mqtt msgs

* Update homeassistant.js

* Update homeassistant.js

* Update homeassistant.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-04-02 14:29:39 +02:00
Koen Kanters da01a504e7 Update herdsman 2020-04-01 22:36:14 +02:00
Koen Kanters 379847d6d0 Update herdsman and converters. 2020-04-01 21:52:35 +02:00
presslab-us 5952a2a12a Fix definition of Nyce NCZ-3011-HA (#3267) 2020-04-01 21:42:48 +02:00
Koen Kanters f95eb4527a Only setup supported color capabilities. https://github.com/Koenkk/zigbee2mqtt/issues/3260 2020-04-01 20:33:04 +02:00
Koen Kanters defe6a5ec1 Update herdsman. 2020-03-31 21:36:19 +02:00
Koen Kanters 5b34e14a62 Update converters. 2020-03-31 19:59:12 +02:00
presslab-usandKoen Kanters 87d6587779 Support Orvibo RL804QZB (#3252)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-03-31 19:01:24 +02:00
presslab-usandKoen Kanters 92b6510175 Support Leviton Omnistat2 thermostat (#3249)
* Support Leviton Omnistat2 thermostat

* Update homeassistant.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-03-31 18:53:51 +02:00
Koen Kanters d6fccbd83e Fix tests. 2020-03-31 18:18:15 +02:00
Koen Kanters eeb85cf096 Update herdsman and converters. 2020-03-30 21:56:34 +02:00
Koen Kanters 7131c05255 DeviceReport: don't crash on binds to non-existing entities. #3245 2020-03-30 19:50:35 +02:00
danieledwardgeorgehitchcock 1ca34536de Added device support for Hue Beyond suspension light (#3246) 2020-03-30 19:45:08 +02:00
presslab-us 7e45ab05cf Fix colortemp ability for Paulmann RGBW Light (#3239) 2020-03-30 19:02:25 +02:00
Koen Kanters 545eca6708 Update converters. 2020-03-29 21:36:59 +02:00
presslab-usandKoen Kanters 6b01418a00 Support Centralite thermostat 3157100 (#3234)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-03-29 21:22:48 +02:00
presslab-usandKoen Kanters 3bd1f4dc4a Fix power reporting of Centralite 4257050-RZHAC (#3236)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-03-29 21:14:48 +02:00
presslab-us 00f0381d62 Support Iris hose faucet water timer 27087-03 (#3230) 2020-03-29 20:28:54 +02:00
Koen Kanters 3c9639048e Remove battery_low for HS1CG-E. https://github.com/Koenkk/zigbee2mqtt/issues/3041 2020-03-29 20:19:13 +02:00
Koen Kanters 78c7024980 Update herdsman. 2020-03-29 19:40:33 +02:00
Koen Kanters fb4d86cef5 Allow to change non-setted options via device_options. #3215 2020-03-28 19:48:05 +01:00
Koen Kanters e49b733fae Cover position for ZM-CSW032-D. https://github.com/Koenkk/zigbee2mqtt/issues/3216 2020-03-28 19:30:27 +01:00
Koen Kanters 5092a12298 Change unit of Home Assistant sensor_energy to kWh. https://github.com/Koenkk/zigbee-herdsman-converters/issues/974 2020-03-28 19:26:02 +01:00
presslab-us 15ee48c318 Support Iris smart button 3460-L (#3214) 2020-03-28 19:12:49 +01:00
Koen Kanters ac7ac1a464 Update converters. 2020-03-27 20:44:10 +01:00
presslab-usandKoen Kanters d73bcf3520 Support Sylvania iQBR30 (#3205)
* Support Sylvania iQBR30

* Update homeassistant.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-03-27 17:45:34 +01:00
Koen Kanters a444a01fa7 Update converters. 2020-03-26 22:24:22 +01:00
presslab-usandKoen Kanters f7b2dff37e Support Centralite Night Light Repeater (#3203)
* Support Centralite Night Light Repeater

* Update homeassistant.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-03-26 22:15:22 +01:00
presslab-us 676e7e08c6 Support wallyHOME Multi-Sensor (#3202) 2020-03-26 22:09:59 +01:00
Koen Kanters 1c8ba3d28e Mock water_leak value for Home Assistant when it's not available. #3146 2020-03-26 22:01:05 +01:00
presslab-us 4ce9370cc0 Add humidity support for SmartThings (Centralite) 3310-S (#3197) 2020-03-26 21:28:27 +01:00
Koen Kanters a7e38b6bb0 Update zigbee-herdsman. https://github.com/Koenkk/zigbee2mqtt/issues/3111 2020-03-26 20:20:59 +01:00
Koen Kanters 4fe23842cf Don't allow non existing entities on availability_blacklist or availability_whitelist. #3191 2020-03-25 20:46:20 +01:00
Koen Kanters a307747d8c Fix typo. 2020-03-25 20:05:18 +01:00
Koen Kanters 5cd1af569e Fix crash when removing device from group which has no devices in settings. #3185 2020-03-23 22:24:35 +01:00
Nico87LBS dfc897af1a Add Hue Impress outdoor Pedestal (#3186) 2020-03-23 21:51:07 +01:00
Koen Kanters a3b285d1ac Don't allow MQTT wildcard (# or +) in friendly_name. #3175 2020-03-23 19:59:49 +01:00
Koen Kanters 06c792f493 Update herdsman and converters. 2020-03-23 19:42:06 +01:00
Koen Kanters d52d520da8 Update herdsman. 2020-03-22 20:46:39 +01:00
Nico87LBS 5461b3c3f8 Added Hue Econic Outdoor Pedestal (#3174) 2020-03-22 16:36:14 +01:00
Koen Kanters 78c17f0a61 Update converters. 2020-03-21 21:59:10 +01:00
Jorge Schrauwen e226cd229e #3159 Fix incorrect unbind message (#3172) 2020-03-21 21:33:00 +01:00
Addison Lynch 2f557bd397 Add 75541 (#3167) 2020-03-21 15:37:43 +01:00
Koen Kanters ed37570ef0 Update converters. 2020-03-20 19:46:57 +01:00
Koen Kanters 6cc2689593 Log error when there is nothing to bind. #3159 2020-03-20 19:00:00 +01:00
Koen Kanters 92d8eb2b6b Fix unsupported device warnings when device is still being interviewed. #3158 2020-03-20 18:04:28 +01:00
Koen Kanters a30abba973 Make sure that there is always an update_available value for Home Assistant. https://github.com/Koenkk/zigbee2mqtt/issues/3146 2020-03-20 17:50:28 +01:00
Koen Kanters ac8a84feb2 Update converters. 2020-03-19 23:04:39 +01:00
Koen Kanters e3fafcdfed Update herdsman. 2020-03-19 19:39:36 +01:00
Koen Kanters 6b9baf0114 Allow to specify adapter type. https://github.com/Koenkk/zigbee-herdsman/issues/72 2020-03-19 19:34:11 +01:00
Koen Kanters 9b80b6785d Update converters. 2020-03-18 22:50:45 +01:00
Koen Kanters 73dab78c37 Promote to dev 2020-03-18 19:03:33 +01:00
Koen Kanters 840b9d966c 1.12.0 2020-03-18 19:01:45 +01:00
Koen Kanters d514c0b3c1 Undev 2020-03-18 19:01:41 +01:00
Koen Kanters dc856ff8bf Update herdsman and converters. 2020-03-17 18:31:50 +01:00
Koen Kanters 11ee61fb1c Update converters. 2020-03-16 20:20:00 +01:00
r1cht3r 43a2aa27cd changing 3323-G cfg.binary_sensor_contact to cfg.binary_sensor_occupancy (#3137) 2020-03-16 20:07:53 +01:00
Filip CornelissenandKoen Kanters 43c7ce8e8a add support for ROB_200-007-0 (#3125)
* add support for ROB_200-007-0

* Fix automatically discovery for Home assistant for ROB_200-007-0

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-03-15 22:42:39 +01:00
Koen Kanters 27b3fd2f03 Update converters. 2020-03-15 22:26:03 +01:00
Koen Kanters 66b68da318 Add vendor and description to 'zigbee2mqtt/bridge/config/devices'. #3133 2020-03-15 17:04:10 +01:00
Koen Kanters db57829068 Update converters. 2020-03-15 16:58:48 +01:00
Koen Kanters 15bef07328 Fix LifeControl_Door_Sensor Home Assistant discovery. #3108 2020-03-15 15:25:28 +01:00
Koen Kanters 4d3bbce687 Implement lightWithPostfix Home Assistant configuration. #3003 2020-03-15 09:38:39 +01:00
Koen Kanters cb87b31cdd Update herdsman. 2020-03-14 15:08:00 +01:00
Koen Kanters cee146b062 Update converters. 2020-03-13 19:24:20 +01:00
Kryzek cde5c9696d Update homeassistant.js (#3121)
Related to https://github.com/Koenkk/zigbee-herdsman-converters/pull/1063
2020-03-13 19:20:38 +01:00
Koen Kanters 882c725b13 Update converters. 2020-03-13 18:39:40 +01:00
Dustin SallingsandKoen Kanters 97a4b6b539 Add support for expiring retained messages. (#3082)
* Add support for expiring retained messages.

For most of my environmental monitoring use cases, I want the readings
retained so I can pick them up from clients at any time, but if the
sensor (or zigbee2mqtt) fails, I want the readings to go away so I can
tell the difference between a stale reading and a missing reading.

This is easily accomplished in MQTTv5 using the "message expiry
interval" property.  To add that to zigbee2mqtt, I added a 'version'
option to the mqtt section so I can specify to connect with version 5
and added a 'retention' property to devices allowing me to specify how
long items should be retained.

e.g.

    mqtt:
      base_topic: site/zigbee2mqtt
      server: 'mqtt://myserver'
      user: zigbee
      version: 5
    serial:
      port: /dev/ttyACM0
    devices:
      '0x00358d00022308da':
        friendly_name: someroom
        retain: true
        retention: 900

* Also get from deviceOptions

* Update settings.js

* Update controller.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-03-12 20:25:37 +01:00
Koen Kanters f1a685bea2 Reduce MQTT subscriptions in deviceBind. https://github.com/Koenkk/zigbee2mqtt/issues/3102 2020-03-11 21:30:01 +01:00
Koen Kanters a17d96c14a Revert "Reduce MQTT subscriptions. #3102"
This reverts commit 82080e3d22.
2020-03-11 21:24:59 +01:00
Koen Kanters 9b592d4546 Refuse to start when 'device_options: null' is set. #3107 2020-03-11 20:18:27 +01:00
Koen Kanters 444850f1bc Update converters. 2020-03-11 20:11:47 +01:00
Koen Kanters 82080e3d22 Reduce MQTT subscriptions. #3102 2020-03-11 19:02:24 +01:00
b59d761ca6 publish announces to log topic (#3101)
* publish announces to log topic

* publish announces to log topic #test fix

* Update controller.js

* Update controller.test.js

* Update deviceAvailability.test.js

Co-authored-by: Andrey Sergeew <avsergeev@neoflex.ru>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-03-11 18:21:06 +01:00
Koen Kanters 785079a712 Update herdsman. 2020-03-10 21:30:12 +01:00
Koen Kanters 59281fe1c2 Update herdsman. https://github.com/Koenkk/zigbee2mqtt/issues/2693 2020-03-10 20:29:46 +01:00
Koen Kanters ab85577132 Wait 30 seconds before publishing device states. https://github.com/Koenkk/zigbee2mqtt/issues/105 2020-03-10 19:32:44 +01:00
Koen Kanters da8b8aa373 Fix command_on_template for lightWithPostfix. https://github.com/Koenkk/zigbee2mqtt/issues/3003#issuecomment-596868209 2020-03-10 19:28:19 +01:00
Koen Kanters 052ccfa242 Update converters. 2020-03-10 18:40:16 +01:00
Koen Kanters 8e9729cbbd Update herdsman and converters. 2020-03-09 20:01:51 +01:00
netadmindaveandKoen Kanters 3f7d383ead added config for Third Reality Switch 3RSS007Z (#3089)
* added config for Third Reality Switch 3RSS007Z

* Update homeassistant.js

* Update homeassistant.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-03-09 18:59:48 +01:00
Koen Kanters b92e3c31d1 Update converters. 2020-03-09 18:09:15 +01:00
Koen Kanters 4d54e5038b Refuse to start when friendly_name ends with /DIGIT. #2645 2020-03-07 23:06:22 +01:00
Koen Kanters 407d329d4f Update herdsman and converters. 2020-03-06 23:34:36 +01:00
Koen Kanters 34b3e66fa5 Add l5 till l8 to postfixes. https://github.com/Koenkk/zigbee-herdsman-converters/pull/1041 2020-03-06 23:18:50 +01:00
Kiall Mac Innes 67b4bcf815 Update HA Discovery Info on Rename (#3040)
* Update HA Discovery Info on Rename

Update HA discovery info when a device is renamed. Adds a new deviceRenamed
event to the event bus, allowing the HA extension to react to the rename

Fixes #2440

* Handle Groups correctly wrt HA discovery data refresh

* Cleanup group handling

* Cleanup group handling
2020-03-04 12:55:08 +01:00
Koen Kanters 3b60a907c8 Fix error on OTA update message without mapped device. 2020-03-03 21:19:00 +01:00
Koen Kanters 2d10c15a94 Update herdsman and converters. 2020-03-03 20:33:10 +01:00
Jorge Schrauwen b17b9c76ef #3009 filtered_attributes should also work in device_options (#3057) 2020-03-03 18:30:54 +01:00
Magnus PernemarkandKoen Kanters 2454136b9b Added MQTT logging to TouchLink Reset process (#3053)
* MQTT logging for TouchLink reset process

* Changed status to be in meta

* Update bridgeConfig.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-03-03 18:22:28 +01:00
Koen Kanters dc8b6c5294 Update converters. 2020-03-02 21:11:54 +01:00
Jorge Schrauwen 658cc21b4d #3009 allow filtering of mqtt topics and payloads (#3037)
* Allow filtering of attributes in mqtt json payload or published attributes

* Add test for mqtt_attribute_filter

Just doing attribute_and_json should be fine as the code changes are run before this. Doing this tests lets us varify both json or seperate topic get properly filtered.

* Add test for mqtt_attribute_filter on group

* Rename mqtt_attribute_filter to filtered_attributes
2020-03-02 20:08:51 +01:00
Koen Kanters 68750fade0 Fix tests on node 12. https://github.com/Koenkk/zigbee2mqtt/issues/3031 2020-03-02 19:11:43 +01:00
Koen Kanters ed34422640 Merge branch 'master' into dev 2020-03-02 19:07:49 +01:00
Koen Kanters e140cf5de3 Add branch switcher. 2020-03-02 19:07:38 +01:00
Koen Kanters 43b744987b Add AV2010/32 Home Assistant discovery. #3038 2020-03-01 16:03:08 +01:00
Koen Kanters 9f1b8cf214 Allow to disable homeassistant legacy triggers. #3033 2020-03-01 15:55:20 +01:00
Koen Kanters ed75c85b69 Implement Home Assistant MQTT device trigger. #3033 2020-02-29 18:07:15 +01:00
Koen Kanters 2eb6f075dc Update converters. https://github.com/Koenkk/zigbee2mqtt/issues/3024 2020-02-29 13:53:35 +01:00
Koen Kanters a16e3542c0 Update herdsman. #3019 2020-02-29 12:43:53 +01:00
Koen Kanters dd3678f84b TEMP: disable OTA response. https://github.com/Koenkk/zigbee2mqtt/issues/3019 2020-02-28 23:52:30 +01:00
Koen Kanters a40ad0c52f Respond with ABORT when device checks for OTA. https://github.com/Koenkk/zigbee2mqtt/issues/3019 2020-02-28 23:42:59 +01:00
Koen Kanters f6aba5f64a Respond with image not available when OTA is requested but we don't support it. #3019 2020-02-28 23:30:33 +01:00
Koen Kanters 8f8180f84d Update converters and herdsman. 2020-02-28 23:27:12 +01:00
Cedric Girard 4803be2df9 Add necessary changes for NPM publishing (#3021) 2020-02-28 21:59:01 +01:00
Koen Kanters 05766fa39a Update dependencies. https://github.com/Koenkk/zigbee2mqtt/issues/3007 2020-02-28 20:56:32 +01:00
Koen Kanters 342d4c17b9 Fix debounce, debounce_ignore, retain and qos in device_options not honored. #3004 2020-02-27 21:06:27 +01:00
Christian Scheffler 69c477da7d added mqtt log events group_added and group_removed (#3016) 2020-02-27 21:03:56 +01:00
Koen Kanters 63be685201 Fix OTA update start on check with / as base_topic prefix. #3010 2020-02-27 20:33:04 +01:00
Koen Kanters 5c0bb33c1b Discover Home Assistant temperature sensor for QBKG12LM. https://github.com/Koenkk/zigbee-herdsman-converters/issues/1020 2020-02-26 22:02:04 +01:00
Koen Kanters 0036176098 Promote to dev. 2020-02-26 18:28:22 +01:00
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
48 changed files with 14837 additions and 4642 deletions
+1
View File
@@ -0,0 +1 @@
preferredBranch: dev
+25
View File
@@ -0,0 +1,25 @@
#tests
test
coverage
#build tools
.travis.yml
.jenkins.yml
.codeclimate.yml
.github
azure-pipelines.yaml
#linters
.jscsrc
.jshintrc
.eslintrc*
.dockerignore
.eslintignore
#editor settings
.vscode
#src
docs
docker
images
+3
View File
@@ -29,6 +29,9 @@
<a href="https://www.codacy.com/manual/Koenkk/zigbee2mqtt?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=Koenkk/zigbee2mqtt&amp;utm_campaign=Badge_Grade">
<img src="https://api.codacy.com/project/badge/Grade/24f1e0fe39f04daa810e8a1416693d3f">
</a>
<a href="https://www.npmjs.com/package/zigbee2mqtt">
<img src="https://img.shields.io/npm/v/zigbee2mqtt">
</a>
</div>
<h1>Zigbee2mqtt 🌉 🐝</h1>
<p>
+12 -4
View File
@@ -14,9 +14,9 @@ steps:
# Prepare
- task: NodeTool@0
inputs:
versionSpec: '10.x'
versionSpec: '12.x'
displayName: 'Install Node.js'
- bash: npm install
- bash: npm ci
displayName: 'Install dependencies'
# Verify
@@ -25,12 +25,20 @@ steps:
- bash: npm run eslint
displayName: 'Lint'
# NPM
- task: Npm@1
inputs:
command: publish
publishEndpoint: npm
displayName: 'Publish to npm'
condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'), ne(variables['Build.Reason'], 'PullRequest'))
# Docker
- task: DockerInstaller@0
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 +49,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"]
+81 -67
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');
@@ -14,58 +15,58 @@ const ExtensionSoftReset = require('./extension/softReset');
const ExtensionHomeAssistant = require('./extension/homeassistant');
const ExtensionDeviceConfigure = require('./extension/deviceConfigure');
const ExtensionDeviceGroupMembership = require('./extension/deviceGroupMembership');
const ExtensionBridgeConfig = require('./extension/bridgeConfig');
const ExtensionBridgeLegacy = require('./extension/bridgeLegacy');
const ExtensionGroups = require('./extension/groups');
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.state = new State();
this.eventBus = new EventBus();
this.state = new State(this.eventBus);
this.publishEntityState = this.publishEntityState.bind(this);
this.onZigbeeAdapterDisconnected = this.onZigbeeAdapterDisconnected.bind(this);
// Initialize extensions.
const args = [this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus];
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(...args),
new ExtensionDeviceReceive(...args),
new ExtensionDeviceGroupMembership(...args),
new ExtensionDeviceConfigure(...args),
new ExtensionNetworkMap(...args),
new ExtensionGroups(...args),
new ExtensionDeviceBind(...args),
new ExtensionDeviceEvent(...args),
new ExtensionOTAUpdate(...args),
];
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
this.extensions.push(new ExtensionBridgeLegacy(...args));
}
if (settings.get().advanced.report) {
this.extensions.push(new ExtensionDeviceReport(
this.zigbee, this.mqtt, this.state, this.publishEntityState,
));
this.extensions.push(new ExtensionDeviceReport(...args));
}
if (settings.get().homeassistant) {
this.extensions.push(new ExtensionHomeAssistant(
this.zigbee, this.mqtt, this.state, this.publishEntityState,
));
this.extensions.push(new ExtensionHomeAssistant(...args));
}
/* 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.extensions.push(new ExtensionSoftReset(...args));
}
if (settings.get().advanced.availability_timeout) {
this.extensions.push(new ExtensionDeviceAvailability(
this.zigbee, this.mqtt, this.state, this.publishEntityState,
));
this.extensions.push(new ExtensionDeviceAvailability(...args));
}
}
@@ -89,9 +90,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...');
@@ -107,8 +109,8 @@ class Controller {
logger.info(
(entity.settings ? entity.settings.friendlyName : entity.device.ieeeAddr) +
` (${entity.device.ieeeAddr}): ` +
(entity.mapped ?
`${entity.mapped.model} - ${entity.mapped.vendor} ${entity.mapped.description} ` :
(entity.definition ?
`${entity.definition.model} - ${entity.definition.vendor} ${entity.definition.description} ` :
'Not supported ') +
`(${entity.device.type})`,
);
@@ -121,10 +123,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));
@@ -139,6 +138,13 @@ class Controller {
}
}
// Add devices which are in database but not in configuration to configuration
for (const device of this.zigbee.getClients()) {
if (!settings.getDevice(device.ieeeAddr)) {
settings.addDevice(device.ieeeAddr);
}
}
// Call extensions
await this.callExtensionMethod('onMQTTConnected', []);
}
@@ -166,13 +172,13 @@ class Controller {
}
async onZigbeeEvent(type, data) {
const entity = this.zigbee.resolveEntity(data.device || data.ieeeAddr);
if (data.device && !entity.settings) {
const resolvedEntity = this.zigbee.resolveEntity(data.device || data.ieeeAddr);
if (data.device && !resolvedEntity.settings) {
// Only deviceLeave doesn't have a device (not interesting to add to settings)
entity.settings = settings.addDevice(data.device.ieeeAddr);
resolvedEntity.settings = settings.addDevice(data.device.ieeeAddr);
}
const name = entity && entity.settings ? entity.settings.friendlyName : null;
const name = resolvedEntity && resolvedEntity.settings ? resolvedEntity.settings.friendlyName : null;
if (type === 'message') {
logger.debug(
@@ -182,34 +188,25 @@ class Controller {
);
} else if (type === 'deviceJoined') {
logger.info(`Device '${name}' joined`);
this.mqtt.log('device_connected', {friendly_name: name});
} else if (type === 'deviceInterview') {
if (data.status === 'successful') {
logger.info(`Successfully interviewed '${name}', device has successfully been paired`);
if (entity.mapped) {
const {vendor, description, model} = entity.mapped;
logger.info(
`Device '${name}' is supported, identified as: ${vendor} ${description} (${model})`,
);
const log = {friendly_name: name, model, vendor, description, supported: true};
this.mqtt.log('pairing', 'interview_successful', log);
if (resolvedEntity.definition) {
const {vendor, description, model} = resolvedEntity.definition;
logger.info(`Device '${name}' is supported, identified as: ${vendor} ${description} (${model})`);
} else {
logger.warn(
`Device '${name}' with Zigbee model '${data.device.modelID}' is NOT supported, ` +
`please follow https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`,
);
this.mqtt.log('pairing', 'interview_successful', {friendly_name: name, supported: false});
}
} else if (data.status === 'failed') {
logger.error(`Failed to interview '${name}', device has not successfully been paired`);
this.mqtt.log('pairing', 'interview_failed', {friendly_name: name});
} else {
/* istanbul ignore else */
if (data.status === 'started') {
logger.info(`Starting interview of '${name}'`);
this.mqtt.log('pairing', 'interview_started', {friendly_name: name});
}
}
} else if (type === 'deviceAnnounce') {
@@ -218,15 +215,11 @@ class Controller {
/* istanbul ignore else */
if (type === 'deviceLeave') {
logger.warn(`Device '${name || data.ieeeAddr}' left the network`);
this.mqtt.log('device_removed', 'left_network', {friendly_name: name || data.ieeeAddr});
}
}
// Call extensions
this.callExtensionMethod(
'onZigbeeEvent',
[type, data, entity ? entity.mapped : null, entity ? entity.settings : null],
);
this.callExtensionMethod('onZigbeeEvent', [type, data, resolvedEntity]);
}
onMQTTMessage(payload) {
@@ -238,55 +231,76 @@ class Controller {
}
async publishEntityState(IDorName, payload, stateChangeReason=null) {
const entity = this.zigbee.resolveEntity(IDorName);
if (!entity || !entity.settings) {
const resolvedEntity = this.zigbee.resolveEntity(IDorName);
if (!resolvedEntity || !resolvedEntity.settings) {
logger.error(`'${IDorName}' does not exist, skipping publish`);
return;
}
if (entity.type === 'device' && settings.get().advanced.last_seen !== 'disable' && entity.device.lastSeen) {
payload.last_seen = utils.formatDate(entity.device.lastSeen, settings.get().advanced.last_seen);
const isDevice = resolvedEntity.type === 'device';
if (isDevice && settings.get().advanced.last_seen !== 'disable' && resolvedEntity.device.lastSeen) {
payload.last_seen = utils.formatDate(resolvedEntity.device.lastSeen, settings.get().advanced.last_seen);
}
let messagePayload = {...payload};
const currentState = this.state.exists(entity.settings.ID) ? this.state.get(entity.settings.ID) : {};
const currentState = this.state.exists(resolvedEntity.settings.ID) ?
this.state.get(resolvedEntity.settings.ID) : {};
const newState = objectAssignDeep.noMutate(currentState, payload);
// Update state cache with new state.
this.state.set(entity.settings.ID, newState, stateChangeReason);
this.state.set(resolvedEntity.settings.ID, newState, stateChangeReason);
if (settings.get().advanced.cache_state) {
// Add cached state to payload
messagePayload = newState;
}
const deviceOptions = settings.get().device_options;
const options = {
retain: entity.settings.hasOwnProperty('retain') ? entity.settings.retain : false,
qos: entity.settings.hasOwnProperty('qos') ? entity.settings.qos : 0,
retain: utils.getObjectsProperty([resolvedEntity.settings, deviceOptions], 'retain', false),
qos: utils.getObjectsProperty([resolvedEntity.settings, deviceOptions], 'qos', 0),
};
if (entity.type === 'device' && settings.get().mqtt.include_device_information) {
const device = this.zigbee.getDeviceByIeeeAddr(entity.device.ieeeAddr);
const retention = utils.getObjectsProperty([resolvedEntity.settings, deviceOptions], 'retention', false);
if (retention !== false) {
options.properties = {messageExpiryInterval: retention};
}
if (isDevice && settings.get().mqtt.include_device_information) {
const attributes = [
'ieeeAddr', 'networkAddress', 'type', 'manufacturerID', 'manufacturerName', 'powerSource',
'applicationVersion', 'stackVersion', 'zclVersion', 'hardwareVersion', 'dateCode', 'softwareBuildID',
];
messagePayload.device = {
friendlyName: entity.name,
model: entity.mapped ? entity.mapped.model : 'unknown',
friendlyName: resolvedEntity.name,
model: resolvedEntity.definition ? resolvedEntity.definition.model : 'unknown',
};
attributes.forEach((a) => messagePayload.device[a] = device[a]);
attributes.forEach((a) => messagePayload.device[a] = resolvedEntity.device[a]);
}
// filter mqtt message attributes
if (deviceOptions.filtered_attributes) {
deviceOptions.filtered_attributes.forEach((a) => delete messagePayload[a]);
}
if (resolvedEntity.settings.filtered_attributes) {
resolvedEntity.settings.filtered_attributes.forEach((a) => delete messagePayload[a]);
}
this.eventBus.emit('publishEntityState', {payload: messagePayload, entity: resolvedEntity});
if (Object.entries(messagePayload).length) {
if (settings.get().experimental.output === 'json') {
await this.mqtt.publish(entity.name, JSON.stringify(messagePayload), options);
if (settings.get().experimental.output === 'attribute_and_json') {
await this.mqtt.publish(resolvedEntity.name, JSON.stringify(messagePayload), options);
await this.iteratePayloadAttributeOutput(`${resolvedEntity.name}/`, messagePayload, options);
} else if (settings.get().experimental.output === 'json') {
await this.mqtt.publish(resolvedEntity.name, JSON.stringify(messagePayload), options);
} else {
/* istanbul ignore else */
if (settings.get().experimental.output === 'attribute') {
await this.iteratePayloadAttributeOutput(`${entity.name}/`, messagePayload, options);
await this.iteratePayloadAttributeOutput(`${resolvedEntity.name}/`, messagePayload, options);
}
}
}
+24
View File
@@ -0,0 +1,24 @@
const events = require('events');
const assert = require('assert');
const allowedEvents = [
'deviceRemoved', // Device has been removed
'deviceRenamed', // Device has been renamed
'groupRenamed', // Group has been renamed
'publishEntityState', // Entity state will be published
'stateChange', // Entity changes its state
];
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;
+5 -3
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;
}
/**
@@ -28,10 +30,10 @@ class BaseExtension {
* Is called when a Zigbee message from a device is received.
* @param {string} type Type of the message
* @param {Object} data Data of the message
* @param {Object?} mappedDevice The mapped device
* @param {Object?} resolvedEntity Resolved entity returned from this.zigbee.resolveEntity()
* @param {Object?} settingsDevice Device settings
*/
// onZigbeeEvent(type, data, mappedDevice, settingsDevice) {}
// onZigbeeEvent(type, data, resolvedEntity) {}
/**
* Is called when a MQTT message is received
@@ -9,9 +9,9 @@ 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);
class BridgeLegacy extends BaseExtension {
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
super(zigbee, mqtt, state, publishEntityState, eventBus);
// Bind functions
this.permitJoin = this.permitJoin.bind(this);
@@ -63,7 +63,10 @@ class BridgeConfig extends BaseExtension {
assert(entity, `Entity '${message}' does not exist`);
settings.whitelistDevice(entity.ID);
logger.info(`Whitelisted '${entity.friendlyName}'`);
this.mqtt.log('device_whitelisted', {friendly_name: entity.friendlyName});
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: 'device_whitelisted', message: {friendly_name: entity.friendlyName}}),
);
} catch (error) {
logger.error(`Failed to whitelist '${message}' '${error}'`);
}
@@ -147,9 +150,11 @@ class BridgeConfig extends BaseExtension {
};
if (device.type !== 'Coordinator') {
const mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
const definition = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
const friendlyDevice = settings.getDevice(device.ieeeAddr);
payload.model = mappedDevice ? mappedDevice.model : device.modelID;
payload.model = definition ? definition.model : device.modelID;
payload.vendor = definition ? definition.vendor : '-';
payload.description = definition ? definition.description : '-';
payload.friendly_name = friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr;
payload.manufacturerID = device.manufacturerID;
payload.manufacturerName = device.manufacturerName;
@@ -172,16 +177,18 @@ class BridgeConfig extends BaseExtension {
if (topic.split('/').pop() == 'get') {
this.mqtt.publish(`bridge/config/devices`, JSON.stringify(devices), {});
} else {
this.mqtt.log('devices', devices);
this.mqtt.publish('bridge/log', JSON.stringify({type: 'devices', message: devices}));
}
}
groups(topic, message) {
this.mqtt.log('groups', settings.getGroups().map((g) => {
const payload = settings.getGroups().map((g) => {
const group = {...g};
delete group.friendlyName;
return group;
}));
});
this.mqtt.publish('bridge/log', JSON.stringify({type: 'groups', message: payload}));
}
rename(topic, message) {
@@ -215,9 +222,17 @@ class BridgeConfig extends BaseExtension {
_renameInternal(from, to) {
try {
const isGroup = settings.getGroup(from) !== null;
settings.changeFriendlyName(from, to);
logger.info(`Successfully renamed - ${from} to ${to} `);
this.mqtt.log('device_renamed', {from, to});
const entity = this.zigbee.resolveEntity(to);
const eventData = isGroup ? {group: entity.group} : {device: entity.device};
this.eventBus.emit(`${isGroup ? 'group' : 'device'}Renamed`, eventData);
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `${isGroup ? 'group' : 'device'}_renamed`, message: {from, to}}),
);
} catch (error) {
logger.error(`Failed to rename - ${from} to ${to}`);
}
@@ -248,12 +263,17 @@ class BridgeConfig extends BaseExtension {
const group = settings.addGroup(name, id);
this.zigbee.createGroup(group.ID);
this.mqtt.publish('bridge/log', JSON.stringify({type: `group_added`, message: name}));
logger.info(`Added group '${name}'`);
}
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();
this.mqtt.publish('bridge/log', JSON.stringify({type: `group_removed`, message}));
logger.info(`Removed group '${name}'`);
}
@@ -279,10 +299,15 @@ class BridgeConfig extends BaseExtension {
if (!entity) {
logger.error(`Cannot ${lookup[action][2]}, device '${message}' does not exist`);
this.mqtt.publish('bridge/log', JSON.stringify({type: `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);
@@ -290,7 +315,7 @@ class BridgeConfig extends BaseExtension {
this.state.remove(entity.settings.ID);
logger.info(`Successfully ${lookup[action][0]} ${entity.settings.friendlyName}`);
this.mqtt.log(`device_${lookup[action][0]}`, message);
this.mqtt.publish('bridge/log', JSON.stringify({type: `device_${lookup[action][0]}`, message}));
};
try {
@@ -306,6 +331,8 @@ class BridgeConfig extends BaseExtension {
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.publish('bridge/log', JSON.stringify({type: `device_${lookup[action][0]}_failed`, message}));
}
if (action === 'ban') {
@@ -350,22 +377,86 @@ class BridgeConfig extends BaseExtension {
await this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0});
}
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
onZigbeeEvent(type, data, resolvedEntity) {
if (type === 'deviceJoined' && resolvedEntity) {
this.lastJoinedDeviceName = resolvedEntity.name;
}
if (type === 'deviceJoined') {
this.lastJoinedDeviceName = settingsDevice.friendlyName;
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `device_connected`, message: {friendly_name: resolvedEntity.name}}),
);
} else if (type === 'deviceInterview') {
if (data.status === 'successful') {
if (resolvedEntity.definition) {
const {vendor, description, model} = resolvedEntity.definition;
const log = {friendly_name: resolvedEntity.name, model, vendor, description, supported: true};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `pairing`, message: 'interview_successful', meta: log}),
);
} else {
const meta = {friendly_name: resolvedEntity.name, supported: false};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `pairing`, message: 'interview_successful', meta}),
);
}
} else if (data.status === 'failed') {
const meta = {friendly_name: resolvedEntity.name};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `pairing`, message: 'interview_failed', meta}),
);
} else {
/* istanbul ignore else */
if (data.status === 'started') {
const meta = {friendly_name: resolvedEntity.name};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `pairing`, message: 'interview_started', meta}),
);
}
}
} else if (type === 'deviceAnnounce') {
const meta = {friendly_name: resolvedEntity.name};
this.mqtt.publish('bridge/log', JSON.stringify({type: `device_announced`, message: 'announce', meta}));
} else {
/* istanbul ignore else */
if (type === 'deviceLeave') {
const name = resolvedEntity ? resolvedEntity.name : data.ieeeAddr;
const meta = {friendly_name: name};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `device_removed`, message: 'left_network', meta}),
);
}
}
}
async touchlinkFactoryReset() {
logger.info('Starting touchlink factory reset...');
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `touchlink`, message: 'reset_started', meta: {status: 'started'}}),
);
const result = await this.zigbee.touchlinkFactoryReset();
if (result) {
logger.info('Succesfully factory reset device through Touchlink');
logger.info('Successfully factory reset device through Touchlink');
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `touchlink`, message: 'reset_success', meta: {status: 'success'}}),
);
} else {
logger.warn('Failed to factory reset device through Touchlink');
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `touchlink`, message: 'reset_failed', meta: {status: 'failed'}}),
);
}
}
}
module.exports = BridgeConfig;
module.exports = BridgeLegacy;
+19 -10
View File
@@ -6,10 +6,14 @@ 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;
@@ -18,8 +22,8 @@ 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 = {};
@@ -37,13 +41,18 @@ class DeviceAvailability extends BaseExtension {
}
isAllowed(device) {
const ieeeAddr = device.ieeeAddr;
const deviceSettings = settings.getDevice(ieeeAddr);
const name = deviceSettings.friendly_name;
// Whitelist is not empty and device is in it, enable availability
if (this.whitelist.length > 0) {
return this.whitelist.includes(device.ieeeAddr);
return this.whitelist.includes(ieeeAddr) || (name && this.whitelist.includes(name));
}
// Device is on blacklist, disable availability
if (this.blacklist.includes(device.ieeeAddr)) {
if (this.blacklist.includes(ieeeAddr) || (name && this.blacklist.includes(name))) {
return false;
}
@@ -135,13 +144,13 @@ class DeviceAvailability extends BaseExtension {
async onReconnect(device) {
if (device && device.modelID) {
const mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
const definition = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
if (mappedDevice) {
if (definition) {
const used = [];
try {
for (const key of toZigbeeCandidates) {
const converter = mappedDevice.toZigbee.find((tz) => tz.key.includes(key));
const converter = definition.toZigbee.find((tz) => tz.key.includes(key));
if (converter && !used.includes(converter)) {
await converter.convertGet(device.endpoints[0], key, {});
used.push(converter);
@@ -171,7 +180,7 @@ class DeviceAvailability extends BaseExtension {
}
}
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
onZigbeeEvent(type, data, resolvedEntity) {
const device = data.device;
if (!device) {
return;
+46 -12
View File
@@ -6,12 +6,13 @@ const BaseExtension = require('./baseExtension');
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() {
for (let step = 1; step < 20; step++) {
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/bind${'/+'.repeat(step)}`);
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/unbind${'/+'.repeat(step)}`);
}
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/bind/#`);
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/unbind/#`);
}
async onMQTTMessage(topic, message) {
@@ -28,22 +29,28 @@ 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;
let attemptedToBindSomething = false;
// 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}'`);
attemptedToBindSomething = true;
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 {
@@ -54,18 +61,45 @@ class DeviceBind extends BaseExtension {
`Successfully ${type === 'bind' ? 'bound' : 'unbound'} cluster '${cluster}' from ` +
`'${sourceName}' to '${targetName}'`,
);
this.mqtt.log(
`device_${type}`,
{from: sourceName, to: targetName, cluster},
);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const message = {from: sourceName, to: targetName, cluster};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `device_${type}`, message}),
);
}
} catch (error) {
logger.error(
`Failed to ${type} cluster '${cluster}' from '${sourceName}' to ` +
`'${targetName}' (${error})`,
);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const message = {from: sourceName, to: targetName, cluster};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `device_${type}_failed`, message}),
);
}
}
}
}
if (!attemptedToBindSomething) {
logger.error(`Nothing to ${type} from '${sourceName}' to '${targetName}'`);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const message = {from: sourceName, to: targetName};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `device_${type}_failed`, message}),
);
}
}
}
}
+29 -29
View File
@@ -1,31 +1,28 @@
const settings = require('../util/settings');
const logger = require('../util/logger');
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) {
shouldConfigure(resolvedEntity) {
if (!resolvedEntity || !resolvedEntity.definition || !resolvedEntity.definition.configure) {
return false;
}
if (device.meta.hasOwnProperty('configured') && device.meta.configured === mappedDevice.meta.configureKey) {
if (resolvedEntity.device.meta &&
resolvedEntity.device.meta.hasOwnProperty('configured') &&
resolvedEntity.device.meta.configured === resolvedEntity.definition.meta.configureKey) {
return false;
}
if (!mappedDevice || !mappedDevice.configure) {
return false;
}
if (device.interviewing === true) {
if (resolvedEntity.device.interviewing === true) {
return false;
}
@@ -41,29 +38,32 @@ class DeviceConfigure extends BaseExtension {
return;
}
const entity = this.zigbee.resolveEntity(message);
if (!entity || !entity.type === 'device') {
const resolvedEntity = this.zigbee.resolveEntity(message);
if (!resolvedEntity || resolvedEntity.type !== 'device') {
logger.error(`Device '${message}' does not exist`);
return;
}
const mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(entity.device.modelID);
this.configure(entity.device, mappedDevice, entity.settings, true);
if (!resolvedEntity.definition || !resolvedEntity.definition.configure) {
logger.warn(`Skipping configure of '${resolvedEntity.name}', device does not require this.`);
return;
}
this.configure(resolvedEntity, true);
}
async onZigbeeStarted() {
this.coordinatorEndpoint = this.zigbee.getDevicesByType('Coordinator')[0].getEndpoint(1);
for (const device of this.zigbee.getClients()) {
const mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
const settingsDevice = settings.getDevice(device.ieeeAddr);
if (this.shouldConfigure(device, mappedDevice)) {
await this.configure(device, mappedDevice, settingsDevice);
const resolvedEntity = this.zigbee.resolveEntity(device);
if (this.shouldConfigure(resolvedEntity)) {
await this.configure(resolvedEntity);
}
}
}
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
onZigbeeEvent(type, data, resolvedEntity) {
const device = data.device;
if (type === 'deviceJoined' && device.meta.hasOwnProperty('configured')) {
@@ -71,12 +71,13 @@ class DeviceConfigure extends BaseExtension {
device.save();
}
if (this.shouldConfigure(device, mappedDevice)) {
this.configure(device, mappedDevice, settingsDevice);
if (this.shouldConfigure(resolvedEntity)) {
this.configure(resolvedEntity);
}
}
async configure(device, mappedDevice, settingsDevice, force=false) {
async configure(resolvedEntity, force=false) {
const device = resolvedEntity.device;
if (this.configuring.has(device.ieeeAddr) || (this.attempts[device.ieeeAddr] >= 3 && !force)) {
return false;
}
@@ -87,16 +88,15 @@ class DeviceConfigure extends BaseExtension {
this.attempts[device.ieeeAddr] = 0;
}
logger.info(`Configuring '${settingsDevice.friendlyName}'`);
logger.info(`Configuring '${resolvedEntity.name}'`);
try {
await mappedDevice.configure(device, this.coordinatorEndpoint);
logger.info(`Succesfully configured '${settingsDevice.friendlyName}'`);
// eslint-disable-next-line
device.meta.configured = mappedDevice.meta.configureKey;
await resolvedEntity.definition.configure(device, this.coordinatorEndpoint);
logger.info(`Successfully configured '${resolvedEntity.name}'`);
device.meta.configured = resolvedEntity.definition.meta.configureKey;
device.save();
} catch (error) {
logger.error(
`Failed to configure '${settingsDevice.friendlyName}', ` +
`Failed to configure '${resolvedEntity.name}', ` +
`attempt ${this.attempts[device.ieeeAddr] + 1} (${error.stack})`,
);
this.attempts[device.ieeeAddr]++;
+10 -8
View File
@@ -8,9 +8,9 @@ class DeviceEvent extends BaseExtension {
}
}
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
if (data.device) {
this.callOnEvent(data.device, type, data, mappedDevice);
onZigbeeEvent(type, data, resolvedEntity) {
if (data.device && resolvedEntity.definition) {
this.callOnEvent(data.device, type, data, resolvedEntity.definition);
}
}
@@ -20,13 +20,15 @@ class DeviceEvent extends BaseExtension {
}
}
callOnEvent(device, type, data, mappedDevice) {
if (!mappedDevice) {
mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
callOnEvent(device, type, data, definition) {
zigbeeHerdsmanConverters.onEvent(type, data, device);
if (!definition) {
definition = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
}
if (mappedDevice && mappedDevice.onEvent) {
mappedDevice.onEvent(type, data, device);
if (definition && definition.onEvent) {
definition.onEvent(type, data, device);
}
}
}
+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) {
+19 -22
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 = {};
@@ -54,8 +54,8 @@ class DeviceReceive extends BaseExtension {
return result;
}
canHandleEvent(type, data, mappedDevice) {
if (type !== 'message') {
canHandleEvent(type, data, resolvedEntity) {
if (type !== 'message' || !resolvedEntity) {
return false;
}
@@ -83,12 +83,12 @@ class DeviceReceive extends BaseExtension {
return false;
}
if (data.device.modelID === null && data.device.interviewing) {
if (!data.device.modelID && data.device.interviewing) {
logger.debug(`Skipping message, modelID is undefined and still interviewing`);
return false;
}
if (!mappedDevice) {
if (!resolvedEntity.definition) {
logger.warn(`Received message from unsupported device with Zigbee model '${data.device.modelID}'`);
logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html.`);
return false;
@@ -97,27 +97,28 @@ class DeviceReceive extends BaseExtension {
return true;
}
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
if (!this.canHandleEvent(type, data, mappedDevice)) {
onZigbeeEvent(type, data, resolvedEntity) {
if (!this.canHandleEvent(type, data, resolvedEntity)) {
return;
}
const converters = mappedDevice.fromZigbee.filter((c) => {
const converters = resolvedEntity.definition.fromZigbee.filter((c) => {
const type = Array.isArray(c.type) ? c.type.includes(data.type) : c.type === data.type;
return c.cluster === data.cluster && type;
});
// 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](
`No converter available for '${mappedDevice.model}' with cluster '${data.cluster}' ` +
logger.debug(
`No converter available for '${resolvedEntity.definition.model}' with cluster '${data.cluster}' ` +
`and type '${data.type}' and data '${JSON.stringify(data.data)}'`,
);
return;
}
const debounce = resolvedEntity.settings.debounce || settings.get().device_options.debounce;
const debounceIgnore = resolvedEntity.settings.debounce_ignore || settings.get().device_options.debounce_ignore;
// Convert this Zigbee message to a MQTT message.
// Get payload for the message.
// - If a payload is returned publish it to the MQTT broker
@@ -139,17 +140,12 @@ class DeviceReceive extends BaseExtension {
}
// Check if we have to debounce
if (settingsDevice && settingsDevice.hasOwnProperty('debounce')) {
this.publishDebounce(
data.device.ieeeAddr,
payload,
settingsDevice.debounce,
settingsDevice.debounce_ignore,
);
if (debounce) {
this.publishDebounce(data.device.ieeeAddr, payload, debounce, debounceIgnore);
} else {
this.publishEntityState(data.device.ieeeAddr, payload);
if (settings.get().homeassistant) {
if (settings.get().homeassistant && settings.get().advanced.homeassistant_legacy_triggers) {
/**
* Publish an empty value for click and action payload, in this way Home Assistant
* can use Home Assistant entities in automations.
@@ -166,10 +162,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(resolvedEntity.definition, data, publish, options, meta);
if (converted) {
payload = {...payload, ...converted};
}
+90 -23
View File
@@ -1,6 +1,6 @@
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
const logger = require('../util/logger');
const CC2530Router = zigbeeHerdsmanConverters.devices.find((d) => d.model === 'CC2530.ROUTER');
const ZNLDP12LM = zigbeeHerdsmanConverters.devices.find((d) => d.model === 'ZNLDP12LM');
const utils = require('../util/utils');
const BaseExtension = require('./baseExtension');
const debounce = require('debounce');
@@ -10,8 +10,26 @@ const defaultConfiguration = {
minimumReportInterval: 3, maximumReportInterval: 300, reportableChange: 1,
};
const devicesNotSupportingReporting = [
zigbeeHerdsmanConverters.devices.find((d) => d.model === 'CC2530.ROUTER'),
zigbeeHerdsmanConverters.devices.find((d) => d.model === 'BASICZBR3'),
zigbeeHerdsmanConverters.devices.find((d) => d.model === 'ZM-CSW032-D'),
];
const reportKey = 1;
const getColorCapabilities = async (endpoint) => {
if (endpoint.getClusterAttributeValue('lightingColorCtrl', 'colorCapabilities') === undefined) {
await endpoint.read('lightingColorCtrl', ['colorCapabilities']);
}
const value = endpoint.getClusterAttributeValue('lightingColorCtrl', 'colorCapabilities');
return {
colorTemperature: (value & 1<<4) > 0,
colorXY: (value & 1<<3) > 0,
};
};
const clusters = {
'genOnOff': [
{attribute: 'onOff', ...defaultConfiguration, minimumReportInterval: 0, reportableChange: 0},
@@ -20,9 +38,18 @@ const clusters = {
{attribute: 'currentLevel', ...defaultConfiguration},
],
'lightingColorCtrl': [
{attribute: 'colorTemperature', ...defaultConfiguration},
{attribute: 'currentX', ...defaultConfiguration},
{attribute: 'currentY', ...defaultConfiguration},
{
attribute: 'colorTemperature', ...defaultConfiguration,
condition: async (endpoint) => (await getColorCapabilities(endpoint)).colorTemperature,
},
{
attribute: 'currentX', ...defaultConfiguration,
condition: async (endpoint) => (await getColorCapabilities(endpoint)).colorXY,
},
{
attribute: 'currentY', ...defaultConfiguration,
condition: async (endpoint) => (await getColorCapabilities(endpoint)).colorXY,
},
],
'closuresWindowCovering': [
{attribute: 'currentPositionLiftPercentage', ...defaultConfiguration},
@@ -54,29 +81,66 @@ 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, definition) {
if (definition === ZNLDP12LM && cluster === 'closuresWindowCovering') {
// Device announces it but doesn't support it
// https://github.com/Koenkk/zigbee2mqtt/issues/2611
return true;
}
return false;
}
async setupReporting(resolvedEntity) {
const {device, definition} = resolvedEntity;
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, definition)) {
logger.debug(`Setup reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`);
const items = [];
for (const entry of configuration) {
if (!entry.hasOwnProperty('condition') || (await entry.condition(ep))) {
items.push({...entry});
delete items[items.length - 1].condition;
}
}
await ep.bind(cluster, this.coordinatorEndpoint);
await ep.configureReporting(cluster, items);
logger.info(
`Succesfully setup reporting for '${device.ieeeAddr}' - ${endpoint.ID} - ${cluster}`,
`Successfully setup reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`,
);
}
}
@@ -96,9 +160,11 @@ class DeviceReport extends BaseExtension {
this.configuring.delete(device.ieeeAddr);
}
shouldSetupReporting(mappedDevice, device, messageType) {
if (!device || !mappedDevice) return false;
shouldSetupReporting(resolvedEntity, messageType) {
if (!resolvedEntity || !resolvedEntity.device || !resolvedEntity.definition ||
messageType === 'deviceLeave') return false;
const {device, definition} = resolvedEntity;
// Handle messages of type endDeviceAnnce and devIncoming.
// This message is typically send when a device comes online after being powered off
// Ikea TRADFRI tend to forget their reporting after powered off.
@@ -109,7 +175,7 @@ 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(definition) || definition.vendor === 'Gledopto') return false;
return true;
}
@@ -117,16 +183,16 @@ class DeviceReport extends BaseExtension {
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);
const resolvedEntity = this.zigbee.resolveEntity(device);
if (this.shouldSetupReporting(resolvedEntity, null)) {
this.setupReporting(resolvedEntity);
}
}
}
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
if (this.shouldSetupReporting(mappedDevice, data.device, type)) {
this.setupReporting(data.device);
onZigbeeEvent(type, data, resolvedEntity) {
if (this.shouldSetupReporting(resolvedEntity, type)) {
this.setupReporting(resolvedEntity);
}
if (type === 'message') {
@@ -152,7 +218,8 @@ class DeviceReport extends BaseExtension {
let toPoll = [];
// Add bound devices
toPoll = toPoll.concat([].concat(...message.device.endpoints.map((e) => e.binds.map((e) => e))));
toPoll = toPoll.concat([].concat(...message.device.endpoints.map((e) =>
e.binds.map((e) => e).filter((e) => e.target))));
toPoll = toPoll.filter((e) => e.target.constructor.name === 'Endpoint');
toPoll = toPoll.filter((e) => e.target.getDevice().type !== 'Coordinator');
toPoll = toPoll.map((e) => e.target);
+35 -13
View File
@@ -6,7 +6,7 @@ const utils = require('../util/utils');
const assert = require('assert');
const BaseExtension = require('./baseExtension');
const postfixes = utils.getPostfixes();
const postfixes = utils.getEndpointNames();
const topicRegex = new RegExp(`^(.+?)(?:/(${postfixes.join('|')}))?/(get|set)(?:/(.+))?`);
const groupConverters = [
@@ -17,6 +17,8 @@ const groupConverters = [
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 {
@@ -57,7 +59,15 @@ class EntityPublish extends BaseExtension {
const entity = this.zigbee.resolveEntity(entityName);
if (!entity) {
this.mqtt.log('entity_not_found', {friendly_name: entityName});
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const message = {friendly_name: entityName};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `entity_not_found`, message}),
);
}
logger.error(`Entity '${entityName}' is unknown`);
return;
}
@@ -72,22 +82,22 @@ class EntityPublish extends BaseExtension {
assert(entity.type === 'device' || entity.type === 'group');
if (entity.type === 'device') {
// Map device to a model
if (!entity.mapped) {
if (!entity.definition) {
logger.warn(`Device with modelID '${entity.device.modelID}' is not supported.`);
logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`);
return;
}
device = entity.device;
mapped = entity.mapped;
mapped = entity.definition;
target = entity.endpoint;
converters = entity.mapped.toZigbee;
options = entity.mapped.meta && entity.mapped.meta.options ? entity.mapped.meta.options : {};
options = {...options, ...entity.settings};
converters = entity.definition.toZigbee;
options = entity.settings;
} else {
converters = groupConverters;
target = entity.group;
options = entity.settings;
mapped = entity.group.members.map((e) => zigbeeHerdsmanConverters.findByZigbeeModel(e.getDevice().modelID));
}
// Convert the MQTT message to a Zigbee message.
@@ -134,7 +144,7 @@ class EntityPublish extends BaseExtension {
if (entity.type === 'device' && key.includes('_')) {
const underscoreIndex = key.lastIndexOf('_');
const possiblePostfix = key.substring(underscoreIndex + 1, key.length);
if (utils.getPostfixes().includes(possiblePostfix)) {
if (utils.getEndpointNames().includes(possiblePostfix)) {
postfix = possiblePostfix;
key = key.substring(0, underscoreIndex);
const device = target.getDevice();
@@ -166,15 +176,19 @@ 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(actualTarget, key, value, meta);
if (result && result.state) {
const msg = result.state;
if (postfix) {
msg[`state_${postfix}`] = msg.state;
delete msg.state;
for (const key of ['state', 'brightness']) {
if (msg.hasOwnProperty(key)) {
msg[`${key}_${postfix}`] = msg[key];
delete msg[key];
}
}
}
this.publishEntityState(entity.settings.ID, msg);
@@ -193,7 +207,7 @@ class EntityPublish extends BaseExtension {
) {
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(actualTarget, key, meta);
} else {
@@ -205,7 +219,15 @@ class EntityPublish extends BaseExtension {
`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});
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {friendly_name: entity.name};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `zigbee_publish_error`, message, meta}),
);
}
}
usedConverters.push(converter);
+61 -10
View File
@@ -1,13 +1,15 @@
const settings = require('../util/settings');
const logger = require('../util/logger');
const BaseExtension = require('./baseExtension');
const utils = require('../util/utils');
const postfixes = utils.getEndpointNames();
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(.+)/(remove|add|remove_all)$`);
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);
}
@@ -24,7 +26,7 @@ class Groups extends BaseExtension {
async onZigbeeStarted() {
await this.syncGroupsWithSettings();
this.state.on('stateChange', this.onStateChange);
this.eventBus.on('stateChange', this.onStateChange);
}
async syncGroupsWithSettings() {
@@ -142,6 +144,16 @@ class Groups extends BaseExtension {
if (!group || group.type !== 'group') {
logger.error(`Group '${topicMatch[1]}' does not exist`);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const payload = {friendly_name: message, group: topicMatch[1], error: 'group doesn\'t exists'};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `device_group_${type}_failed`, message: payload}),
);
}
return;
}
} else if (topic.match(topicRegexRemoveAll)) {
@@ -153,6 +165,16 @@ class Groups extends BaseExtension {
const entity = this.zigbee.resolveEntity(message);
if (!entity || !entity.type === 'device') {
logger.error(`Device '${message}' does not exist`);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const payload = {friendly_name: message, group: topicMatch[1], error: 'entity doesn\'t exists'};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `device_group_${type}_failed`, message: payload}),
);
}
return;
}
@@ -161,12 +183,17 @@ class Groups extends BaseExtension {
`${entity.name}/${entity.endpoint.ID}`,
];
if (entity.endpointName) {
keys.push(`${entity.device.ieeeAddr}/${entity.endpointName}`);
keys.push(`${entity.name}/${entity.endpointName}`);
const definition = entity.definition;
const endpoints = definition && definition.endpoint ? definition.endpoint(entity.device) : null;
const endpointName = endpoints ? Object.entries(endpoints).find((e) => e[1] === entity.endpoint.ID)[0] : null;
if (endpointName) {
keys.push(`${entity.device.ieeeAddr}/${endpointName}`);
keys.push(`${entity.name}/${endpointName}`);
}
if (entity.isDefaultEndpoint) {
const hasEndpointName = postfixes.find((p) => message.endsWith(`/${p}`));
if (!hasEndpointName) {
keys.push(entity.name);
keys.push(entity.device.ieeeAddr);
}
@@ -175,18 +202,42 @@ class Groups extends BaseExtension {
logger.info(`Adding '${entity.name}' to '${group.name}'`);
await entity.endpoint.addToGroup(group.group);
settings.addDeviceToGroup(group.settings.ID, keys);
this.mqtt.log('device_group_add', {friendly_name: entity.name, group: group.name});
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const payload = {friendly_name: entity.name, group: group.name};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `device_group_add`, message: payload}),
);
}
} else if (type === 'remove') {
logger.info(`Removing '${entity.name}' from '${group.name}'`);
await entity.endpoint.removeFromGroup(group.group);
settings.removeDeviceFromGroup(group.settings.ID, keys);
this.mqtt.log('device_group_remove', {friendly_name: entity.name, group: group.name});
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const payload = {friendly_name: entity.name, group: group.name};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `device_group_remove`, message: payload}),
);
}
} else { // remove_all
logger.info(`Removing '${entity.name}' from all groups`);
await entity.endpoint.removeFromAllGroups();
for (const settingsGroup of settings.getGroups()) {
settings.removeDeviceFromGroup(settingsGroup.ID, keys);
this.mqtt.log('device_group_remove_all', {friendly_name: entity.name});
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const payload = {friendly_name: entity.name};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `device_group_remove_all`, message: payload}),
);
}
}
}
}
File diff suppressed because it is too large Load Diff
+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`;
+206
View File
@@ -0,0 +1,206 @@
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, resolvedEntity) {
if (data.type !== 'commandQueryNextImageRequest' || !resolvedEntity || !resolvedEntity.definition) return;
const supportsOTA = resolvedEntity.definition.hasOwnProperty('ota');
if (supportsOTA) {
// When a device does a next image request, it will usually do it a few times after each other
// with only 10 - 60 seconds inbetween. It doesn' 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 resolvedEntity.definition.ota.isUpdateAvailable(data.device, logger, data.data);
this.publishEntityState(data.device.ieeeAddr, {update_available: available});
if (available) {
const message = `Update available for '${resolvedEntity.settings.friendly_name}'`;
logger.info(message);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: 'available', device: resolvedEntity.settings.friendly_name};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `ota_update`, message, meta}),
);
}
}
}
// Respond to the OTA request:
// - In case we don't support OTA: respond with NO_IMAGE_AVAILABLE (0x98) (so the client stops requesting OTAs)
// - In case we do support OTA: respond with ABORT (0x95) as we don't want to update now.
const endpoint = data.device.endpoints.find((e) => e.supportsOutputCluster('genOta'));
await endpoint.commandResponse('genOta', 'queryNextImageResponse', {status: supportsOTA ? 0x95 : 0x98});
}
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.definition || !device.definition.ota) {
const message = `Device '${device.name}' does not support OTA updates`;
logger.error(message);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: `not_supported`, device: device.name};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `ota_update`, message, meta}),
);
}
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.substring(settings.get().mqtt.base_topic.length).split('/')[3];
if (type === 'check') {
const message = `Checking if update available for '${device.name}'`;
logger.info(message);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: `checking_if_available`, device: device.name};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `ota_update`, message, meta}),
);
}
try {
const available = await device.definition.ota.isUpdateAvailable(device.device, logger);
const message=(available ?
`Update available for '${device.name}'` : `No update available for '${device.name}'`);
logger.info(message);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: available ? 'available' : 'not_available', device: device.name};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `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);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: `check_failed`, device: device.name};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `ota_update`, message, meta}),
);
}
}
} else { // type === 'update'
const message = `Updating '${device.name}' to latest firmware`;
logger.info(message);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: `update_in_progress`, device: device.name};
this.mqtt.publish(
'bridge/log',
JSON.stringify({type: `ota_update`, message, meta}),
);
}
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);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: `update_progress`, device: device.name, progress};
this.mqtt.publish('bridge/log', JSON.stringify({type: `ota_update`, message, meta}));
}
};
const from_ = await this.readSoftwareBuildIDAndDateCode(device.device, false);
await device.definition.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);
this.publishEntityState(device.device.ieeeAddr, {update_available: false});
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: `update_succeeded`, device: device.name, from: from_, to};
this.mqtt.publish('bridge/log', JSON.stringify({type: `ota_update`, message, meta}));
}
} catch (error) {
const message = `Update of '${device.name}' failed (${error.message})`;
logger.error(message);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: `update_failed`, device: device.name};
this.mqtt.publish('bridge/log', JSON.stringify({type: `ota_update`, message, meta}));
}
}
}
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);
}
+9 -10
View File
@@ -22,6 +22,15 @@ class MQTT extends events.EventEmitter {
},
};
if (mqttSettings.version) {
options.protocolVersion = mqttSettings.version;
}
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);
@@ -102,16 +111,6 @@ class MQTT extends events.EventEmitter {
this.client.publish(topic, payload, options, () => resolve());
});
}
log(type, message, meta=null) {
const payload = {type, message};
if (meta) {
payload.meta = meta;
}
this.publish('bridge/log', JSON.stringify(payload), {retain: false});
}
}
module.exports = MQTT;
+10 -11
View File
@@ -2,23 +2,21 @@ const logger = require('./util/logger');
const data = require('./util/data');
const fs = require('fs');
const objectAssignDeep = require('object-assign-deep');
const events = require('events');
const saveInterval = 1000 * 60 * 5; // 5 minutes
const dontCacheProperties = [
'action', 'button', 'button_left', 'button_right', 'click', 'forgotten', 'keyerror',
'step_size', 'transition_time', 'action_color_temperature', 'action_color',
'action_group', 'group_list', 'group_capacity', 'no_occupancy_since',
'action', 'action_.*', 'button', 'button_left', 'button_right', 'click', 'forgotten', 'keyerror',
'step_size', 'transition_time', 'group_list', 'group_capacity', 'no_occupancy_since',
'step_mode', 'transition_time', 'duration', 'elapsed', 'from_side', 'to_side',
];
class State extends events.EventEmitter {
constructor() {
super();
class State {
constructor(eventBus) {
this.state = {};
this.file = data.joinPath('state.json');
this.timer = null;
this.eventBus = eventBus;
}
start() {
@@ -70,17 +68,18 @@ class State extends events.EventEmitter {
set(ID, state, reason=null) {
const toState = objectAssignDeep.noMutate(state);
dontCacheProperties.forEach((property) => {
if (toState.hasOwnProperty(property)) {
for (const property of Object.keys(toState)) {
if (dontCacheProperties.find((p) => property.match(p))) {
delete toState[property];
}
});
}
const fromState = this.state[ID];
this.state[ID] = toState;
this.emit('stateChange', {ID, from: fromState, to: toState, reason});
this.eventBus.emit('stateChange', {ID, from: fromState, to: toState, reason});
}
remove(ID) {
+26 -16
View File
@@ -6,6 +6,7 @@ const fs = require('fs');
const fx = require('mkdir-recursive');
const rimraf = require('rimraf');
const colorizer = winston.format.colorize();
const assert = require('assert');
// What transports to enable
const output = settings.get().advanced.log_output;
@@ -13,6 +14,7 @@ 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 when not logging to stdout only
if (output.includes('file')) {
@@ -21,6 +23,8 @@ if (output.includes('file')) {
// 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 ',
@@ -50,21 +54,27 @@ const transportsToUse = [
// Add file logger when enabled
// NOTE: the initiation of the logger, even when not added as transport tries to create the logging directory
const transportFileOptions = {
filename: path.join(directory, logFilename),
json: false,
level,
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}`;
}),
),
};
if (settings.get().advanced.log_rotation) {
transportFileOptions.tailable = true;
transportFileOptions.maxFiles = 3; // Keep last 3 files
transportFileOptions.maxsize = 10000000; // 10MB
}
if (output.includes('file')) {
transportsToUse.push(new winston.transports.File({
filename: path.join(directory, 'log.txt'),
json: false,
level,
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}`;
}),
),
}));
transportsToUse.push(new winston.transports.File(transportFileOptions));
}
// Create logger
@@ -98,9 +108,9 @@ logger.setLevel = (level) => {
// Print to user what logging is enabled
if (output.includes('file')) {
if (output.includes('console')) {
logger.info(`Logging to console and directory: '${directory}'`);
logger.info(`Logging to console and directory: '${directory}' filename: ${logFilename}`);
} else {
logger.info(`Logging to directory: '${directory}'`);
logger.info(`Logging to directory: '${directory}' filename: ${logFilename}`);
}
logger.cleanup();
} else if (output.includes('console')) {
+64 -15
View File
@@ -1,6 +1,6 @@
const data = require('./data');
const utils = require('./utils');
const file = data.joinPath('configuration.yaml');
const file = process.env.ZIGBEE2MQTT_CONFIG || data.joinPath('configuration.yaml');
const objectAssignDeep = require(`object-assign-deep`);
const path = require('path');
const yaml = require('./yaml');
@@ -39,12 +39,15 @@ const defaults = {
},
},
experimental: {
// json or attribute
// json or attribute or attribute_and_json
output: 'json',
},
advanced: {
legacy_api: true,
log_rotation: true,
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,
@@ -52,6 +55,7 @@ const defaults = {
channel: 11,
baudrate: 115200,
rtscts: true,
adapter_concurrent: null,
// Availability timeout in seconds, disabled by default.
availability_timeout: 0,
@@ -103,6 +107,13 @@ const defaults = {
*/
homeassistant_status_topic: 'hass/status',
/**
* Home Assistant legacy triggers, when enabled:
* - Zigbee2mqt will send an empty 'action' or 'click' after one has been send
* - A 'sensor_action' and 'sensor_click' will be discoverd
*/
homeassistant_legacy_triggers: true,
/**
* Configurable timestampFormat
* https://github.com/Koenkk/zigbee2mqtt/commit/44db557a0c83f419d66755d14e460cd78bd6204e
@@ -114,6 +125,7 @@ const defaults = {
const schema = {
type: 'object',
properties: {
device_options: {type: 'object'},
homeassistant: {type: 'boolean'},
permit_join: {type: 'boolean'},
mqtt: {
@@ -121,6 +133,7 @@ const schema = {
properties: {
base_topic: {type: 'string'},
server: {type: 'string'},
keepalive: {type: 'number'},
ca: {type: 'string'},
key: {type: 'string'},
cert: {type: 'string'},
@@ -129,6 +142,7 @@ const schema = {
client_id: {type: 'string'},
reject_unauthorized: {type: 'boolean'},
include_device_information: {type: 'boolean'},
version: {type: 'number'},
},
required: ['base_topic', 'server'],
},
@@ -137,6 +151,7 @@ const schema = {
properties: {
port: {type: ['string', 'null']},
disable_led: {type: 'boolean'},
adapter: {type: 'string', enum: ['deconz', 'zstack']},
},
},
ban: {type: 'array', items: {type: 'string'}},
@@ -150,13 +165,16 @@ const schema = {
advanced: {
type: 'object',
properties: {
legacy_api: {type: 'boolean'},
pan_id: {type: 'number'},
ext_pan_id: {type: 'array', items: {type: 'number'}},
channel: {type: 'number', minimum: 11, maximum: 26},
cache_state: {type: 'boolean'},
log_rotation: {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},
@@ -170,6 +188,7 @@ const schema = {
homeassistant_discovery_topic: {type: 'string'},
homeassistant_status_topic: {type: 'string'},
timestamp_format: {type: 'string'},
adapter_concurrent: {type: 'number'},
},
},
map_options: {
@@ -221,7 +240,9 @@ const schema = {
properties: {
friendly_name: {type: 'string'},
retain: {type: 'boolean'},
retention: {type: 'number'},
qos: {type: 'number'},
filtered_attributes: {type: 'array', items: {type: 'string'}},
},
required: ['friendly_name'],
},
@@ -241,6 +262,7 @@ const schema = {
devices: {type: 'array', items: {type: 'string'}},
optimistic: {type: 'boolean'},
qos: {type: 'number'},
filtered_attributes: {type: 'array', items: {type: 'string'}},
},
required: ['friendly_name'],
},
@@ -288,17 +310,42 @@ function write() {
function validate() {
const validate = ajv.compile(schema);
const valid = validate(_settings);
const postfixes = utils.getPostfixes();
const postfixes = utils.getEndpointNames();
// 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}'`);
if (name.match(/.*\/\d*$/)) throw new Error(`Friendly name cannot end with a "/DIGIT" ('${name}')`);
if (name.includes('#') || name.includes('+')) {
throw new Error(`MQTT wildcard (+ and #) not allowed in friendly_name ('${name}')`);
}
names.push(name);
};
Object.values(_settingsWithDefaults.devices).forEach((d) => check(d.friendly_name));
Object.values(_settingsWithDefaults.groups).forEach((g) => check(g.friendly_name));
const settingsWithDefaults = getWithDefaults();
Object.values(settingsWithDefaults.devices).forEach((d) => check(d.friendly_name));
Object.values(settingsWithDefaults.groups).forEach((g) => check(g.friendly_name));
if (settingsWithDefaults.mqtt.version !== 5) {
for (const device of Object.values(settingsWithDefaults.devices)) {
if (device.retention) {
throw new Error('MQTT retention requires protocol version 5');
}
}
}
const checkAvailabilityList = (list, type) => {
list.forEach((e) => {
if (!getEntity(e)) {
throw new Error(`Non-existing entity '${e}' specified in '${type}'`);
}
});
};
checkAvailabilityList(settingsWithDefaults.advanced.availability_blacklist, 'availability_blacklist');
checkAvailabilityList(settingsWithDefaults.advanced.availability_whitelist, 'availability_whitelist');
return !valid ? validate.errors.map((v) => `${v.dataPath.substring(1)} ${v.message}`) : null;
}
@@ -563,6 +610,9 @@ function removeDeviceFromGroup(groupIDorName, keys) {
const groupID = getGroupThrowIfNotExists(groupIDorName).ID;
const settings = get();
const group = settings.groups[groupID];
if (!group.devices) {
group.devices = [];
}
const key = groupHasDevice(group, keys);
if (key) {
@@ -581,14 +631,7 @@ function removeGroup(groupIDorName) {
function changeDeviceOptions(IDorName, newOptions) {
const device = getDeviceThrowIfNotExists(IDorName);
const settings = get();
const currentOptions = settings.devices[device.ID];
Object.keys(currentOptions).forEach((key) => {
if (newOptions[key]) {
currentOptions[key] = newOptions[key];
}
});
objectAssignDeep(settings.devices[device.ID], newOptions);
write();
}
@@ -597,9 +640,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();
}
+16 -4
View File
@@ -27,10 +27,11 @@ function toLocalISOString(dDate) {
':' + pad(tzOffset % 60);
}
const postfixes = [
'left', 'right', 'center', 'bottom_left', 'bottom_right', 'l1', 'l2', 'default',
const endpointNames = [
'left', 'right', 'center', 'bottom_left', 'bottom_right', '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',
'l1', 'l2', 'l3', 'l4', 'l5', 'l6', 'l7', 'l8',
];
function flatten(arr) {
@@ -108,12 +109,23 @@ function equalsPartial(object, expected) {
return true;
}
function getObjectsProperty(objects, key, defaultValue) {
for (const object of objects) {
if (object.hasOwnProperty(key)) {
return object[key];
}
}
return defaultValue;
}
module.exports = {
millisecondsToSeconds: (milliseconds) => milliseconds / 1000,
secondsToMilliseconds: (seconds) => seconds * 1000,
getZigbee2mqttVersion,
objectHasProperties,
getPostfixes: () => postfixes,
getObjectsProperty,
getEndpointNames: () => endpointNames,
isXiaomiDevice: (device) => {
return device.modelID !== 'lumi.router' && xiaomiManufacturerID.includes(device.manufacturerID) &&
(!device.manufacturerName || !device.manufacturerName.startsWith('Trust'));
+34 -22
View File
@@ -8,7 +8,7 @@ const events = require('events');
const objectAssignDeep = require('object-assign-deep');
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
const postfixes = utils.getPostfixes();
const endpointNames = utils.getEndpointNames();
const keyEndpointByNumber = new RegExp(`.*/([0-9]*)$`);
const herdsmanSettings = {
@@ -25,6 +25,10 @@ const herdsmanSettings = {
baudRate: settings.get().advanced.baudrate,
rtscts: settings.get().advanced.rtscts,
path: settings.get().serial.port,
adapter: settings.get().serial.adapter,
},
adapter: {
concurrent: settings.get().advanced.adapter_concurrent,
},
};
@@ -127,6 +131,17 @@ class Zigbee extends events.EventEmitter {
return this.herdsman.getDevicesByType(type);
}
/**
* @param {string} key
* @return {object} {
* type: device | coordinator
* device|group: zigbee-herdsman entity
* endpoint: selected endpoint (only if type === device)
* settings: from configuration.yaml
* name: name of the entity
* definition: zigbee-herdsman-converters definition (only if type === device)
* }
*/
resolveEntity(key) {
assert(
typeof key === 'string' || typeof key === 'number' ||
@@ -149,13 +164,13 @@ class Zigbee extends events.EventEmitter {
};
}
let postfix = postfixes.find((p) => key.endsWith(`/${p}`));
const postfixByNumber = key.match(keyEndpointByNumber);
if (!postfix && postfixByNumber) {
postfix = Number(postfixByNumber[1]);
let endpointKey = endpointNames.find((p) => key.endsWith(`/${p}`));
const endpointByNumber = key.match(keyEndpointByNumber);
if (!endpointKey && endpointByNumber) {
endpointKey = Number(endpointByNumber[1]);
}
if (postfix) {
key = key.replace(`/${postfix}`, '');
if (endpointKey) {
key = key.replace(`/${endpointKey}`, '');
}
const entity = settings.getEntity(key);
@@ -167,19 +182,17 @@ class Zigbee extends events.EventEmitter {
return null;
}
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
const endpoints = mapped && mapped.endpoint ? mapped.endpoint(device) : null;
let isDefaultEndpoint = true;
const definition = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
const endpoints = definition && definition.endpoint ? definition.endpoint(device) : null;
let endpoint;
if (postfix) {
isDefaultEndpoint = false;
if (postfixByNumber) {
endpoint = device.getEndpoint(postfix);
if (endpointKey) {
if (endpointByNumber) {
endpoint = device.getEndpoint(endpointKey);
} else {
assert(mapped != null, `Postfix '${postfix}' is given but device is unsupported`);
assert(endpoints != null, `Postfix '${postfix}' is given but device defines no endpoints`);
const endpointID = endpoints[postfix];
assert(endpointID, `Postfix '${postfix}' is given but device has no such endpoint`);
assert(definition != null, `Endpoint name '${endpointKey}' is given but device is unsupported`);
assert(endpoints != null, `Endpoint name '${endpointKey}' is given but no endpoints defined`);
const endpointID = endpoints[endpointKey];
assert(endpointID, `Endpoint name '${endpointKey}' is given but device has no such endpoint`);
endpoint = device.getEndpoint(endpointID);
}
} else if (endpoints && endpoints['default']) {
@@ -188,10 +201,8 @@ class Zigbee extends events.EventEmitter {
endpoint = device.endpoints[0];
}
const endpointName = endpoints ? Object.entries(endpoints).find((e) => e[1] === endpoint.ID)[0] : null;
return {
type: 'device', device, settings: entity, mapped, endpoint, name: entity.friendlyName,
isDefaultEndpoint, endpointName,
type: 'device', device, endpoint, settings: entity, name: entity.friendlyName, definition,
};
} else {
let group = this.getGroupByID(entity.ID);
@@ -203,9 +214,10 @@ class Zigbee extends events.EventEmitter {
return {
type: 'device',
device: key,
endpoint: key.endpoints[0],
settings: setting,
mapped: zigbeeHerdsmanConverters.findByZigbeeModel(key.modelID),
name: setting ? setting.friendlyName : (key.type === 'Coordinator' ? 'Coordinator' : key.ieeeAddr),
definition: zigbeeHerdsmanConverters.findByZigbeeModel(key.modelID),
};
}
}
+11947 -4169
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "zigbee2mqtt",
"version": "1.8.0",
"version": "1.12.2",
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
"main": "index.js",
"repository": {
@@ -45,8 +45,8 @@
"rimraf": "*",
"semver": "*",
"winston": "*",
"zigbee-herdsman": "0.12.19",
"zigbee-herdsman-converters": "11.1.61"
"zigbee-herdsman": "0.12.78",
"zigbee-herdsman-converters": "12.0.69"
},
"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
@@ -9,7 +9,7 @@ const Controller = require('../lib/controller');
const flushPromises = () => new Promise(setImmediate);
describe('Bridge config', () => {
describe('Bridge legacy', () => {
let controller;
beforeAll(async () => {
@@ -100,6 +100,21 @@ describe('Bridge config', () => {
expect(settings.getDevice('bulb_color')).toStrictEqual(
{"ID": "0x000b57fffec6a5b3", "friendlyName": "bulb_color", "friendly_name": "bulb_color", "retain": true}
);
MQTT.events.message('zigbee2mqtt/bridge/config/device_options', JSON.stringify({friendly_name: 'bulb_color', options: {random_setting: true}}));
await flushPromises();
expect(settings.getDevice('bulb_color')).toStrictEqual(
{"ID": "0x000b57fffec6a5b3", "friendlyName": "bulb_color", "friendly_name": "bulb_color", "random_setting": true, "retain": true}
);
MQTT.events.message('zigbee2mqtt/bridge/config/device_options', JSON.stringify({friendly_name: 'bulb_color', options: {options: {random_1: true}}}));
await flushPromises();
expect(settings.getDevice('bulb_color')).toStrictEqual(
{"ID": "0x000b57fffec6a5b3", "friendlyName": "bulb_color", "friendly_name": "bulb_color", "random_setting": true, "retain": true, options: {random_1: true}}
);
MQTT.events.message('zigbee2mqtt/bridge/config/device_options', JSON.stringify({friendly_name: 'bulb_color', options: {options: {random_2: false}}}));
await flushPromises();
expect(settings.getDevice('bulb_color')).toStrictEqual(
{"ID": "0x000b57fffec6a5b3", "friendlyName": "bulb_color", "friendly_name": "bulb_color", "random_setting": true, "retain": true, options: {random_1: true, random_2: false}}
);
});
it('Should allow permit join', async () => {
@@ -161,8 +176,8 @@ describe('Bridge config', () => {
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/config/devices');
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({"ieeeAddr": "0x00124b00120144ae", "type": "Coordinator", "dateCode": "20190425", "friendly_name": "Coordinator", networkAddress: 0, softwareBuildID: "z-Stack", lastSeen: 100});
expect(payload[2]).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", "description": "TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white", "vendor": "IKEA"});
Date.now = now;
});
@@ -203,6 +218,20 @@ 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};
@@ -234,9 +263,15 @@ describe('Bridge config', () => {
zigbeeHerdsman.createGroup.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/add_group', 'new_group');
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
JSON.stringify({type: 'group_added', message: 'new_group'}),
{qos: 0, retain: false},
expect.any(Function)
);
expect(settings.getGroup('new_group')).toStrictEqual({"ID": 3, "friendlyName": "new_group", "friendly_name": "new_group", devices: [], optimistic: true});
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(3)
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(3);
});
it('Should allow to add groups with json', async () => {
@@ -245,7 +280,13 @@ describe('Bridge config', () => {
await flushPromises();
expect(settings.getGroup('new_group')).toStrictEqual({"ID": 3, "friendlyName": "new_group", "friendly_name": "new_group", devices: [], optimistic: true});
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(3)
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(3);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
JSON.stringify({type: 'group_added', message: 'new_group'}),
{qos: 0, retain: false},
expect.any(Function)
);
});
it('Should allow to add groups with json specifying id', async () => {
@@ -254,7 +295,13 @@ describe('Bridge config', () => {
await flushPromises();
expect(settings.getGroup('new_group')).toStrictEqual({"ID": 42, "friendlyName": "new_group", "friendly_name": "new_group", devices: [], optimistic: true});
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(42)
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(42);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
JSON.stringify({type: 'group_added', message: 'new_group'}),
{qos: 0, retain: false},
expect.any(Function)
);
});
it('Should allow to add groups with json specifying only id', async () => {
@@ -267,10 +314,17 @@ 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);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
JSON.stringify({type: 'group_removed', message: 'group_1'}),
{qos: 0, retain: false},
expect.any(Function)
);
});
it('Shouldnt allow add groups without id or friendly_name in json', async () => {
@@ -368,8 +422,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 () => {
@@ -381,8 +436,9 @@ 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 () => {
@@ -392,7 +448,7 @@ describe('Bridge config', () => {
MQTT.events.message('zigbee2mqtt/bridge/config/touchlink/factory_reset', '');
await flushPromises();
expect(zigbeeHerdsman.touchlinkFactoryReset).toHaveBeenCalledTimes(1);
expect(logger.info).toHaveBeenCalledWith('Succesfully factory reset device through Touchlink');
expect(logger.info).toHaveBeenCalledWith('Successfully factory reset device through Touchlink');
});
it('Should allow to touchlink factory reset (FAILS)', async () => {
+85 -3
View File
@@ -30,7 +30,7 @@ describe('Controller', () => {
it('Start controller', async () => {
await controller.start();
expect(zigbeeHerdsman.constructor).toHaveBeenCalledWith({"network":{"panID":6754,"extendedPanID":[221,221,221,221,221,221,221,221],"channelList":[11],"networkKey":[1,3,5,7,9,11,13,15,0,2,4,6,8,10,12,13]},"databasePath":path.join(data.mockDir, "database.db"), "databaseBackupPath":path.join(data.mockDir, "database.db.backup"),"backupPath":path.join(data.mockDir, "coordinator_backup.json"),"acceptJoiningDeviceHandler": expect.any(Function),"serialPort":{"baudRate":115200,"rtscts":true,"path":"/dev/dummy"}});
expect(zigbeeHerdsman.constructor).toHaveBeenCalledWith({"network":{"panID":6754,"extendedPanID":[221,221,221,221,221,221,221,221],"channelList":[11],"networkKey":[1,3,5,7,9,11,13,15,0,2,4,6,8,10,12,13]},"databasePath":path.join(data.mockDir, "database.db"), "databaseBackupPath":path.join(data.mockDir, "database.db.backup"),"backupPath":path.join(data.mockDir, "coordinator_backup.json"),"acceptJoiningDeviceHandler": expect.any(Function),adapter: {concurrent: null}, "serialPort":{"baudRate":115200,"rtscts":true,"path":"/dev/dummy"}});
expect(zigbeeHerdsman.start).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.setLED).toHaveBeenCalledTimes(0);
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(0);
@@ -57,11 +57,13 @@ describe('Controller', () => {
const configuration = {
base_topic: "zigbee2mqtt",
server: "mqtt://localhost",
keepalive: 30,
ca, cert, key,
password: 'pass',
user: 'user1',
client_id: 'my_client_id',
reject_unauthorized: false,
version: 5,
}
settings.set(['mqtt'], configuration)
await controller.start();
@@ -69,6 +71,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]),
@@ -76,6 +79,7 @@ describe('Controller', () => {
username: 'user1',
clientId: 'my_client_id',
rejectUnauthorized: false,
protocolVersion: 5,
}
expect(MQTT.connect).toHaveBeenCalledWith("mqtt://localhost", expected);
@@ -238,9 +242,16 @@ describe('Controller', () => {
expect(logger.debug).toHaveBeenCalledWith(`Received Zigbee message from 'bulb', type 'attributeReport', cluster 'genBasic', data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1 with groupID 0`);
});
it('Should add entities which are missing from configuration but are in database to configuration', async () => {
await controller.start();
const device = zigbeeHerdsman.devices.notInSettings;
expect(settings.getDevice(device.ieeeAddr)).not.toBeNull();
});
it('On zigbee event message from unkown device should create it', async () => {
await controller.start();
const device = zigbeeHerdsman.devices.notInSettings;
settings.removeDevice(device.ieeeAddr);
expect(settings.getDevice(device.ieeeAddr)).toBeNull();
const payload = {device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10, groupID: 0, cluster: 'genBasic', data: {modelId: device.modelID}};
await zigbeeHerdsman.events.message(payload);
@@ -358,6 +369,7 @@ describe('Controller', () => {
await zigbeeHerdsman.events.deviceAnnounce(payload);
await flushPromises();
expect(logger.debug).toHaveBeenCalledWith(`Device 'bulb' announced itself`);
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', '{"type":"device_announced","message":"announce","meta":{"friendly_name":"bulb"}}', { retain: false, qos: 0}, expect.any(Function));
});
it('On zigbee event device leave (removed from database and settings)', async () => {
@@ -397,18 +409,88 @@ 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 attribute_json output filtered', async () => {
await controller.start();
settings.set(['experimental', 'output'], 'attribute_and_json');
settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'filtered_attributes'], ['color_temp', 'linkquality']);
MQTT.publish.mockClear();
await controller.publishEntityState('bulb', {state: 'ON', brightness: 200, color_temp: 370, linkquality: 99});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(3);
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", '{"state":"ON","brightness":200}', {"qos": 0, "retain": true}, expect.any(Function));
});
it('Publish entity state attribute_json output filtered (device_options)', async () => {
await controller.start();
settings.set(['experimental', 'output'], 'attribute_and_json');
settings.set(['device_options', 'filtered_attributes'], ['color_temp', 'linkquality']);
MQTT.publish.mockClear();
await controller.publishEntityState('bulb', {state: 'ON', brightness: 200, color_temp: 370, linkquality: 99});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(3);
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", '{"state":"ON","brightness":200}', {"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","model":"LED1545G12","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"}}', {"qos": 0, "retain": false}, expect.any(Function));
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('Should publish entity state without retain', async () => {
await controller.start();
settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'retain'], false);
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}', {"qos": 0, "retain": false}, expect.any(Function));
});
it('Should publish entity state with retain', async () => {
await controller.start();
settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'retain'], 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}', {"qos": 0, "retain": true}, expect.any(Function));
});
it('Should publish entity state with expiring retention', async () => {
await controller.start();
settings.set(['mqtt', 'version'], 5);
settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'retain'], true);
settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'retention'], 37);
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}', {"qos": 0, "retain": true, "properties": {messageExpiryInterval: 37}}, expect.any(Function));
});
it('Publish entity state no empty messages', async () => {
+32 -5
View File
@@ -197,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();
@@ -224,9 +238,23 @@ describe('Device availability', () => {
expect(device.ping).toHaveBeenCalledTimes(1);
});
it('Should ping whitelisted devices if availability_whitelist is set', async () => {
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'], [device.ieeeAddr])
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();
@@ -252,7 +280,6 @@ describe('Device availability', () => {
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 () => {
+34 -5
View File
@@ -15,6 +15,7 @@ describe('Device bind', () => {
endpoint.write.mockClear();
endpoint.configureReporting.mockClear();
endpoint.bind.mockClear();
endpoint.unbind.mockClear();
}
}
@@ -29,11 +30,8 @@ describe('Device bind', () => {
MQTT.publish.mockClear();
});
it('Should subscribe to nested topics', async () => {
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/bind/+');
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/bind/+/+');
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/bind/+/+/+');
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/bind/+/+/+/+/+');
it('Should subscribe to topics', async () => {
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/bind/#');
});
it('Should bind', async () => {
@@ -56,6 +54,17 @@ describe('Device bind', () => {
expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'bulb_color', cluster: 'genLevelCtrl'}});
});
it('Should log error when there is nothing to bind', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
mockClear(device);
logger.error.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/bind/remote', 'button');
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(0);
expect(logger.error).toHaveBeenCalledWith(`Nothing to bind from 'remote' to 'button'`);
});
it('Should unbind', async () => {
const device = zigbeeHerdsman.devices.remote;
const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
@@ -171,4 +180,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'}});
});
});
+6
View File
@@ -105,6 +105,12 @@ describe('Device configure', () => {
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);
});
});
+63 -20
View File
@@ -110,6 +110,32 @@ describe('Device receive', () => {
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
});
it('Should debounce and retain messages when set via device_options', async () => {
jest.useFakeTimers();
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
settings.set(['device_options', 'debounce'], 0.1);
settings.set(['device_options', 'retain'], true);
delete settings.get().devices['0x0017880104e45522']['retain'];
const data1 = {measuredValue: 8}
const payload1 = {data: data1, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload1);
const data2 = {measuredValue: 1}
const payload2 = {data: data2, cluster: 'msRelativeHumidity', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload2);
const data3 = {measuredValue: 2}
const payload3 = {data: data3, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload3);
await flushPromises();
jest.advanceTimersByTime(50);
expect(MQTT.publish).toHaveBeenCalledTimes(0);
jest.runAllTimers();
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: 0.08, humidity: 0.01, pressure: 2, linkquality: 10});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": true});
});
it('Should debounce messages only with the same payload values for provided debounce_ignore keys', async () => {
jest.useFakeTimers();
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
@@ -186,6 +212,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}}
@@ -242,7 +287,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});
});
@@ -385,29 +430,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});
});
});
+50 -5
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');
@@ -21,7 +22,7 @@ const mocksClear = [MQTT.publish, logger.warn, logger.debug, debounce];
describe('Device report', () => {
let controller;
function expectOnOffBrightnessColorReport(endpoint) {
function expectOnOffBrightnessColorReport(endpoint, colorXY) {
const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
expect(endpoint.bind).toHaveBeenCalledTimes(3);
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint);
@@ -30,7 +31,11 @@ describe('Device report', () => {
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": 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}]);
if (colorXY) {
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}]);
} else {
expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [{"attribute": "colorTemperature", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 1}]);
}
}
mockClear = (device) => {
@@ -56,7 +61,7 @@ describe('Device report', () => {
it('Should configure reporting on startup', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
expectOnOffBrightnessColorReport(endpoint);
expectOnOffBrightnessColorReport(endpoint, true);
});
it('Should configure reporting when receicing message from device which has not been setup yet', async () => {
@@ -69,7 +74,7 @@ describe('Device report', () => {
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expectOnOffBrightnessColorReport(endpoint);
expectOnOffBrightnessColorReport(endpoint, false);
expect(device.save).toHaveBeenCalledTimes(1);
});
@@ -129,7 +134,7 @@ describe('Device report', () => {
const payload = {device};
await zigbeeHerdsman.events.deviceAnnounce(payload);
await flushPromises();
expectOnOffBrightnessColorReport(endpoint);
expectOnOffBrightnessColorReport(endpoint, false);
});
it('Should not configure reporting on device leave', async () => {
@@ -197,4 +202,44 @@ 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);
});
it('Should not setup colorTemperature reporting when bulb does not support it and should read colorCapabilities when its not there yet ', async () => {
const device = zigbeeHerdsman.devices.bulb;
const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
const endpoint = device.getEndpoint(1);
delete device.meta.reporting;
mockClear(device);
endpoint.getClusterAttributeValue = jest.fn();
let count = 0;
endpoint.getClusterAttributeValue.mockImplementation((d) => {
count++;
if (count === 1) return undefined;
return 17;
});
const payload = {device};
await zigbeeHerdsman.events.deviceAnnounce(payload);
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.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities'])
expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [{"attribute": "colorTemperature", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 1}]);
expect(endpoint.configureReporting).toHaveBeenCalledTimes(3);
});
});
+164 -13
View File
@@ -5,7 +5,6 @@ const MQTT = require('./stub/mqtt');
const settings = require('../lib/util/settings');
const Controller = require('../lib/controller');
const flushPromises = () => new Promise(setImmediate);
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const mocksClear = [MQTT.publish, logger.warn, logger.debug];
@@ -30,7 +29,6 @@ describe('Entity publish', () => {
controller = new Controller();
await controller.start();
await flushPromises();
await wait(50);
});
beforeEach(async () => {
@@ -662,14 +660,16 @@ describe('Entity publish', () => {
});
it('Should allow to set color via hue and saturation', async () => {
const device = zigbeeHerdsman.devices.bulb_color_2;
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_2/set', JSON.stringify(payload));
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(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 () => {
@@ -712,15 +712,24 @@ describe('Entity publish', () => {
expect(endpoint.command).toHaveBeenCalledWith("closuresWindowCovering", "stop", {}, {});
});
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 () => {
@@ -730,12 +739,70 @@ 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('When device is turned off with transition and turned on WITHOUT transition it should restore the 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)]);
// Turn on again
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'ON'}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 0}, {}]);
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(MQTT.publish.mock.calls[1]).toEqual(["zigbee2mqtt/bulb_color", JSON.stringify({state: 'ON', brightness: 200}), {"qos": 0, "retain": false}, expect.any(Function)]);
});
it('Home Assistant: should set state', async () => {
settings.set(['homeassistant'], true);
const device = zigbeeHerdsman.devices.bulb_color;
@@ -846,13 +913,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 () => {
@@ -861,7 +944,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 () => {
@@ -870,7 +953,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 () => {
@@ -879,7 +962,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 () => {
@@ -900,4 +983,72 @@ describe('Entity publish', () => {
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)");
});
it('Should restore brightness when its turned off without transition and is turned on with', async () => {
// https://github.com/Koenkk/zigbee-herdsman-converters/issues/1097
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
endpoint.command.mockClear();
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'ON'}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]);
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({"state": "ON", "brightness": 123}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 123, transtime: 0}, {}]);
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({"state": "OFF"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(3);
expect(endpoint.command.mock.calls[2]).toEqual(["genOnOff", "off", {}, {}]);
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({"state": "ON", "transition": 1.0}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(4);
expect(endpoint.command.mock.calls[3]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 123, transtime: 10}, {}]);
});
});
+21 -4
View File
@@ -14,6 +14,7 @@ describe('Groups', () => {
let controller;
beforeEach(() => {
data.writeEmptyState();
controller = new Controller();
Object.values(zigbeeHerdsman.groups).forEach((g) => g.members = []);
data.writeDefaultConfiguration();
@@ -296,6 +297,23 @@ describe('Groups', () => {
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", '{"state":"ON"}', {"retain": false, qos: 0}, expect.any(Function));
});
it('Should publish state change of all members when a group changes its state, filtered', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
const group = zigbeeHerdsman.groups.group_1;
group.members.push(endpoint);
settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, filtered_attributes: ['brightness'], devices: [device.ieeeAddr]}});
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
await MQTT.events.message('zigbee2mqtt/group_1/set', JSON.stringify({state: 'ON', brightness: 100}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", '{"state":"ON","brightness":100}', {"retain": false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", '{"state":"ON"}', {"retain": false, qos: 0}, expect.any(Function));
});
it('Shouldnt publish group state change when a group is not optimistic', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
@@ -306,7 +324,6 @@ describe('Groups', () => {
await flushPromises();
MQTT.publish.mockClear();
controller.state.state = {};
const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint, type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
@@ -410,8 +427,8 @@ describe('Groups', () => {
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(3);
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", '{"state":"OFF"}', {"retain": false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", '{"state":"OFF","brightness":50,"color_temp":370,"linkquality":99}', {"retain": true, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", '{"state":"OFF","brightness":50,"color_temp":370}', {"retain": false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", '{"state":"OFF"}', {"retain": true, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", '{"state":"OFF"}', {"retain": false, qos: 0}, expect.any(Function));
});
it('Should publish state change off even when missing current state', async () => {
@@ -440,4 +457,4 @@ describe('Groups', () => {
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", '{"state":"OFF"}', {"retain": false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", '{"state":"OFF"}', {"retain": false, qos: 0}, expect.any(Function));
});
});
});
+307 -5
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]) {
@@ -33,6 +32,27 @@ describe('HomeAssistant extension', () => {
expect(missing).toHaveLength(0);
});
it('Should not have duplicate type/object_ids in a mapping', () => {
const duplicated = [];
const HomeAssistant = require('../lib/extension/homeassistant');
const ha = new HomeAssistant(null, null, null, null, {on: () => {}});
require('zigbee-herdsman-converters').devices.forEach((d) => {
const mapping = ha._getMapping()[d.model];
const cfg_type_object_ids = [];
mapping.forEach((c) => {
if (cfg_type_object_ids.includes(c['type'] + '/' + c['object_id'])) {
duplicated.push(d.model);
} else {
cfg_type_object_ids.push(c['type'] + '/' + c['object_id']);
}
});
});
expect(duplicated).toHaveLength(0);
});
it('Should discover devices', async () => {
controller = new Controller(false);
await controller.start();
@@ -141,7 +161,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',
@@ -564,13 +585,13 @@ describe('HomeAssistant extension', () => {
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
'{"state":"ON","brightness":50,"color_temp":370,"linkquality":99}',
'{"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"update_available":false}',
{ retain: true, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/remote',
'{"brightness":255}',
'{"brightness":255,"update_available":false}',
{ retain: true, qos: 0 },
expect.any(Function)
);
@@ -637,4 +658,285 @@ 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 refresh discovery when device is renamed', async () => {
controller = new Controller(false);
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/rename', '{"old": "weather_sensor", "new": "weather_sensor_renamed"}');
await flushPromises();
const payload = {
'unit_of_measurement': '°C',
'device_class': 'temperature',
'value_template': '{{ value_json.temperature }}',
'state_topic': 'zigbee2mqtt/weather_sensor_renamed',
'json_attributes_topic': 'zigbee2mqtt/weather_sensor_renamed',
'name': 'weather_sensor_renamed_temperature',
'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt',
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor_renamed',
'sw_version': this.version,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
'availability_topic': 'zigbee2mqtt/bridge/state',
};
expect(MQTT.publish).toHaveBeenCalledWith(
'homeassistant/sensor/0x0017880104e45522/temperature/config',
JSON.stringify(payload),
{ retain: true, qos: 0 },
expect.any(Function),
);
});
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),
);
});
it('Should discover trigger when click is published', async () => {
controller = new Controller(false);
await controller.start();
await flushPromises();
const discovered = MQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]);
expect(discovered.length).toBe(4);
expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/click/config');
expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/action/config');
MQTT.publish.mockClear();
const device = zigbeeHerdsman.devices.WXKG11LM;
const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
const discoverPayload = {
"automation_type":"trigger",
"type":"click",
"subtype":"single",
"payload":"single",
"topic":"zigbee2mqtt/button/click",
"device":{
"identifiers":[
"zigbee2mqtt_0x0017880104e45520"
],
"name":"button",
"sw_version": this.version,
"model":"Aqara wireless switch (WXKG11LM)",
"manufacturer":"Xiaomi"
}
};
expect(MQTT.publish).toHaveBeenCalledWith(
'homeassistant/device_automation/0x0017880104e45520/click_single/config',
JSON.stringify(discoverPayload),
{ retain: true, qos: 0 },
expect.any(Function),
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/button/click',
'single',
{ retain: false, qos: 0 },
expect.any(Function),
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/button',
JSON.stringify({click: "single", linkquality: 10}),
{ retain: false, qos: 0 },
expect.any(Function),
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/button',
JSON.stringify({linkquality: 10, click: ""}),
{ retain: false, qos: 0 },
expect.any(Function),
);
// Should only discover it once
MQTT.publish.mockClear();
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(MQTT.publish).not.toHaveBeenCalledWith(
'homeassistant/device_automation/0x0017880104e45520/click_single/config',
JSON.stringify(discoverPayload),
{ retain: true, qos: 0 },
expect.any(Function),
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/button/click',
'single',
{ retain: false, qos: 0 },
expect.any(Function),
);
});
it('Should disable Home Assistant legacy triggers', async () => {
settings.set(['advanced', 'homeassistant_legacy_triggers'], false);
controller = new Controller(false);
await controller.start();
await flushPromises();
const discovered = MQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]);
expect(discovered.length).toBe(2);
expect(discovered).not.toContain('homeassistant/sensor/0x0017880104e45520/click/config');
expect(discovered).not.toContain('homeassistant/sensor/0x0017880104e45520/action/config');
MQTT.publish.mockClear();
const device = zigbeeHerdsman.devices.WXKG11LM;
const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
const discoverPayload = {
"automation_type":"trigger",
"type":"click",
"subtype":"single",
"payload":"single",
"topic":"zigbee2mqtt/button/click",
"device":{
"identifiers":[
"zigbee2mqtt_0x0017880104e45520"
],
"name":"button",
"sw_version": this.version,
"model":"Aqara wireless switch (WXKG11LM)",
"manufacturer":"Xiaomi"
}
};
expect(MQTT.publish).toHaveBeenCalledWith(
'homeassistant/device_automation/0x0017880104e45520/click_single/config',
JSON.stringify(discoverPayload),
{ retain: true, qos: 0 },
expect.any(Function),
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/button/click',
'single',
{ retain: false, qos: 0 },
expect.any(Function),
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/button',
JSON.stringify({click: "single", linkquality: 10}),
{ retain: false, qos: 0 },
expect.any(Function),
);
expect(MQTT.publish).toHaveBeenCalledTimes(3);
});
it('Should republish payload to postfix topic with lightWithPostfix config', async () => {
controller = new Controller(false);
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
await MQTT.events.message('zigbee2mqtt/U202DST600ZB/l2/set', JSON.stringify({state: 'ON', brightness: 20}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/U202DST600ZB', JSON.stringify({state_l2:"ON", brightness_l2:20}), {"qos": 0, "retain": false}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/U202DST600ZB/l2', JSON.stringify({state:"ON", brightness:20}), {"qos": 0, "retain": false}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/U202DST600ZB/l1', JSON.stringify({}), {"qos": 0, "retain": false}, expect.any(Function));
});
it('Shouldnt crash in onPublishEntityState on group publish', async () => {
controller = new Controller(false);
await controller.start();
await flushPromises();
logger.error.mockClear();
MQTT.publish.mockClear();
await MQTT.events.message('zigbee2mqtt/group_1/set', JSON.stringify({state: 'ON'}));
await flushPromises();
expect(logger.error).toHaveBeenCalledTimes(0);
});
});
+10
View File
@@ -98,4 +98,14 @@ describe('Logger', () => {
expect(pipes.constructor.name).toBe('Console');
expect(pipes.silent).toBe(true);
});
it('Should allow to disable log rotation', () => {
settings.set(['advanced', 'log_rotation'], false);
const logger = require('../lib/util/logger.js');
const pipes = logger._readableState.pipes;
expect(pipes[1].constructor.name).toBe('File');
expect(pipes[1].maxFiles).toBeNull();
expect(pipes[1].tailable).toBeFalsy();
expect(pipes[1].maxsize).toBeNull();
});
});
+204
View File
@@ -0,0 +1,204 @@
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 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', 'ZNLDP12LM');
await flushPromises();
expect(logger.error).toHaveBeenCalledWith(`Device 'ZNLDP12LM' 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'`);
expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 0x95});
// 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'`)
});
it('Should respond with NO_IMAGE_AVAILABLE when not supporting OTA', async () => {
const device = zigbeeHerdsman.devices.QBKG04LM;
const data = {imageType: 12382};
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 152});
});
});
+90 -1
View File
@@ -434,6 +434,31 @@ describe('Settings', () => {
expect(settings.get().groups).toStrictEqual(expected);
});
it('Shouldnt crash when removing device from group when group has no devices', () => {
write(configurationFile, {
devices: {
'0x123': {
friendly_name: 'bulb',
retain: true,
}
},
groups: {
'1': {
friendly_name: 'test123',
}
}
});
settings.removeDeviceFromGroup('test123', ['0x123']);
const expected = {
'1': {
friendly_name: 'test123',
},
};
expect(settings.get().groups).toStrictEqual(expected);
});
it('Should throw when adding device to non-existing group', () => {
write(configurationFile, {
devices: {
@@ -464,6 +489,31 @@ describe('Settings', () => {
}).toThrow(new Error("Device '0x123' already exists"));
});
it('Should not allow retention configuration without MQTT v5', () => {
write(configurationFile, {
devices: {'0x0017880104e45519': {friendly_name: 'tain', retention: 900}},
});
settings._reRead();
expect(() => {
settings.validate();
}).toThrowError('MQTT retention requires protocol version 5');
});
it('Should not allow non-existing entities in availability blacklist', () => {
write(configurationFile, {
devices: {'0x0017880104e45519': {friendly_name: 'tain'}},
advanced: {availability_blacklist: ['0x0017880104e45519', 'non_existing']},
});
settings._reRead();
expect(() => {
settings.validate();
}).toThrowError(`Non-existing entity 'non_existing' specified in 'availability_blacklist'`);
});
it('Should ban devices', () => {
write(configurationFile, {});
settings.banDevice('0x123');
@@ -512,6 +562,30 @@ describe('Settings', () => {
}).toThrowError(`Duplicate friendly_name 'myname' found`);
});
it('Configuration shouldnt be valid when friendly_name ends with /DIGIT', async () => {
write(configurationFile, {
devices: {'0x0017880104e45519': {friendly_name: 'myname/123', retain: false}},
});
settings._reRead();
expect(() => {
settings.validate();
}).toThrowError(`Friendly name cannot end with a "/DIGIT" ('myname/123')`);
});
it('Configuration shouldnt be valid when friendly_name contains a MQTT wildcard', async () => {
write(configurationFile, {
devices: {'0x0017880104e45519': {friendly_name: 'myname#', retain: false}},
});
settings._reRead();
expect(() => {
settings.validate();
}).toThrowError(`MQTT wildcard (+ and #) not allowed in friendly_name ('myname#')`);
});
it('Configuration shouldnt be valid when friendly_name is a postfix', async () => {
write(configurationFile, {
devices: {'0x0017880104e45519': {friendly_name: 'left', retain: false}},
@@ -521,7 +595,7 @@ describe('Settings', () => {
expect(() => {
settings.validate();
}).toThrowError(`Following friendly_name are not allowed: '${utils.getPostfixes()}'`);
}).toThrowError(`Following friendly_name are not allowed: '${utils.getEndpointNames()}'`);
});
it('Configuration shouldnt be valid when duplicate friendly_name are used', async () => {
@@ -539,6 +613,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 = {};
+21
View File
@@ -132,6 +132,27 @@ function writeDefaultConfiguration() {
},
'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',
},
'0x0017880104e43559': {
friendly_name: 'U202DST600ZB'
},
'0x0017880104e44559': {
friendly_name: '3157100_thermostat',
}
},
groups: {
+24 -7
View File
@@ -7,11 +7,14 @@ 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,
'genOta': 25,
'genScenes': 5,
'genOnOff': 6,
'genLevelCtrl': 8,
@@ -20,12 +23,14 @@ 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;
this.command = jest.fn();
this.commandResponse = jest.fn();
this.read = jest.fn();
this.write = jest.fn();
this.bind = jest.fn();
@@ -33,7 +38,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 +65,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;
@@ -90,13 +100,13 @@ class Device {
const returnDevices = [];
const bulb_color = new Device('Router', '0x000b57fffec6a5b3', 40399, 4107, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b3')], true, "Mains (single phase)", "LLC020");
const bulb_color = new Device('Router', '0x000b57fffec6a5b3', 40399, 4107, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b3', [], {lightingColorCtrl: {colorCapabilities: 254}})], true, "Mains (single phase)", "LLC020");
const bulb_color_2 = new Device('Router', '0x000b57fffec6a5b4', 401292, 4107, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b4')], true, "Mains (single phase)", "LLC020");
const bulb_2 = new Device('Router', '0x000b57fffec6a5b7', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b7')], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm");
const devices = {
'coordinator': new Device('Coordinator', '0x00124b00120144ae', 0, 0, [new Endpoint(1, [], [])], false),
'bulb': new Device('Router', '0x000b57fffec6a5b2', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b2')], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm"),
'bulb': new Device('Router', '0x000b57fffec6a5b2', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b2', [], {lightingColorCtrl: {colorCapabilities: 17}})], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm"),
'bulb_color': bulb_color,
'bulb_2': bulb_2,
'bulb_color_2': bulb_color_2,
@@ -111,9 +121,9 @@ const devices = {
'RTCGQ11LM': new Device('EndDevice', '0x0017880104e45523', 6540,4151, [new Endpoint(1, [0], [])], true, "Battery", "lumi.sensor_motion.aq2"),
'ZNCZ02LM': new Device('Router', '0x0017880104e45524', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", "lumi.plug"),
'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'),
'QBKG04LM': new Device('Router', '0x0017880104e45541', 6549,4151, [new Endpoint(1, [0], [25]), 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'),
@@ -129,6 +139,13 @@ const devices = {
'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', [], {lightingColorCtrl: {colorCapabilities: 254}})], 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'),
'U202DST600ZB': new Device('Router', '0x0017880104e43559', 6540,4151, [new Endpoint(10, [0, 6], [], '0x0017880104e43559'), new Endpoint(11, [0, 6], [], '0x0017880104e43559')], true, "Mains (single phase)", 'U202DST600ZB'),
'3157100': new Device('Router', '0x0017880104e44559', 6542,4151, [new Endpoint(1, [], [], '0x0017880104e44559')], true, "Mains (single phase)", '3157100'),
}
const groups = {
+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