Compare commits

...
177 Commits
Author SHA1 Message Date
Koenkk b0d3c2f964 0.2.0 2018-11-14 19:18:31 +01:00
Koenkk f797a12250 Update zigbee-shepherd-converters. 2018-11-14 18:32:02 +01:00
Jonas Janz 081d3c3dfe update docs for Home Hssistant (#583)
This adds a hint that you 'need' to set "hass/state" as a topic if you want zigbee2mqtt to resend the cached values on restarts.
2018-11-11 20:04:34 +01:00
Koenkk 7ca966ce39 npm is not installed by default anymore in Alpine. 2018-11-07 20:25:38 +01:00
Koenkk fc664bdc68 Fix node version to >=8.11 10 and implement check on startup. https://github.com/Koenkk/zigbee2mqtt/issues/552 2018-11-07 20:05:14 +01:00
Koenkk 2cb27b14f9 Allow ‘/‘ in friendly_name. https://github.com/Koenkk/zigbee2mqtt/issues/470 2018-11-07 19:40:58 +01:00
Koenkk 86a102e081 Only mock state after set. #518 2018-11-06 21:20:03 +01:00
Koenkk 9119e54c2c Prepare for read commands. https://github.com/Koenkk/zigbee2mqtt/issues/518 2018-11-05 21:55:30 +01:00
Dustin Wyatt f379bc0391 add linkquality to json_attributes for HA discovery (#566) 2018-11-05 19:39:36 +01:00
Koenkk 2e736bf501 Update zigbee-shepherd-converters to 4.0.11 2018-10-31 17:10:28 +01:00
Stanislav Demydiuk 1e7f985d4e Support both DeviceID and FriendlyName in device control topic (#476) 2018-10-30 21:23:34 +01:00
Koenkk 61c4ff33e0 Update zigbee-shepherd-converters to 4.0.10 2018-10-29 21:34:48 +01:00
mmnpkf 73ae48f387 Add Sengled Element Classic (A60) - B22 fitting (#544)
* Add Sengled Element Classic (A60) - B22 fitting

The Sengled Element Classic with Edison fitting (E27) is already defined - model E11-G23.  The same bulb comes with a bayonet fitting (B22) with a slightly different model number - E11-G33.

* Update homeassistant.js

* Update homeassistant.js
2018-10-29 21:12:37 +01:00
Stanislav Demydiuk 0a1c60b2cf Add Link for Domoticz integration to Readme (#546) 2018-10-29 08:29:28 +01:00
Koenkk 4eb56319c4 Update zigbee-shepherd-converters to 4.0.9 2018-10-25 20:44:55 +02:00
mmnpkf d8ea36af29 Add Nue Double GPO (au version) to Home Assistant (#506)
* Update homeassistant.js

Add code to expose Nue double power outlet (au version) to Home Assistant

* Update homeassistant.js
2018-10-25 20:43:31 +02:00
Felix Triller d0cf61bf10 Use Home Assistant's device_class (#528)
* Use Home Assistant's device_class for sensors.

* Unit for pressure should be hPa

* There is no device_class lock
2018-10-24 21:10:42 +02:00
Koenkk ddb83cc6e4 Handle zigbee commands sequentially (and refactor it). #529 2018-10-23 20:39:48 +02:00
Jens Willmer e54de4bea7 Adding update script (#525)
* Adding update script

Fix #389

* Update update.sh :)

* Make executable & update update.sh
2018-10-22 18:08:03 +02:00
Koenkk 97cc56b691 0.1.8 2018-10-19 22:44:26 +02:00
Koenkk c9df5c8221 Update zigbee-shepherd-converters to 4.0.7. 2018-10-19 22:27:29 +02:00
dcshoes23 7f13e2c3e6 Add homeassistant auto-discovery support for CE LED potlight 4" (#505) 2018-10-19 22:20:11 +02:00
Koenkk 0aab565083 Update zigbee-shepherd-converters to 4.0.6 2018-10-17 21:17:45 +02:00
Kiall Mac Innes 153f8bcfb5 Add Philips Hue white ambiance E14 (LTW012) HA Support (#491)
Adds HA support for LTW012 devices - See https://github.com/Koenkk/zigbee-shepherd-converters/pull/100
2018-10-17 18:25:33 +02:00
Simon van der Veldt c04ed81adb Fix typo in logging messages (#486) 2018-10-16 17:51:54 +02:00
Koen Kanters a1cf75456b Fix state caching. #485 2018-10-16 17:34:46 +02:00
Koenkk b063e67e24 Update zigbee-shepherd-converters to 4.0.5. https://github.com/Koenkk/zigbee2mqtt/issues/467 2018-10-15 19:07:12 +02:00
Koenkk a4da482225 0.1.7 2018-10-15 17:37:06 +02:00
Koenkk 98302bebd1 Update zigbee-shepherd-converters to 4.0.3 2018-10-09 21:28:32 +02:00
AnhDuc85 2197328db5 add homeassistant config for Osram Outdoor Lantern W RGBW OSRAM (#464) 2018-10-09 20:47:29 +02:00
Koen Kanters c5b9176614 Fix unique_id being set when not all devices have a friendly_name. 2018-10-08 20:25:04 +02:00
Laurent 0322354c37 [RFC] graphviz network map, display all devices (#443)
* network map, more info

* add a get(All)Devices method to zigbee

* graphviz: display all devices

loop through all devices, display all devices even those that haven't
responded to the lqi scan.

* makes eslint happy :)

* remove null chars from network map graphviz output

makes graphviz happy :-)

* Improvements to graphviz network map

* Always add device type
2018-10-07 21:46:54 +02:00
Koenkk b692c94fe9 Update zigbee-shepherd. https://github.com/Koenkk/zigbee-shepherd/pull/6 2018-10-05 22:08:47 +02:00
Koenkk 1496861811 Fix https://github.com/Koenkk/zigbee2mqtt/issues/373 again. 2018-10-05 20:54:57 +02:00
Koen Kanters 5459152f01 Fix Koenkk. #451 2018-10-05 19:35:23 +02:00
Koenkk 1db926173d Refactor router polling & only poll Xiaomi routers, refactor soft reset timeout and disable by default. #274 2018-10-02 21:15:12 +02:00
Koenkk da9ee71d80 Fix crash when deviceSettings are missing. 2018-10-02 20:35:14 +02:00
Phil Hawthorne d9f02230a0 Add autodiscovery support for Sylvania MR16 (#435)
Adds auto discovery support for the MR16 smart bulbs from
Osram/Sylvania.
2018-10-01 16:39:23 +02:00
Koenkk bf68f4575f Update zigbee-shepherd-converters to 4.0.2 2018-09-29 23:05:18 +02:00
Koenkk 33ea08ad22 Update dependencies for. #388 2018-09-29 23:05:18 +02:00
UnrealKazu 3a17db7c79 Add support for TRADFRI Control Outlet (#413)
The control outlet is a switch, hence the switch configuration.
2018-09-29 23:05:18 +02:00
Koenkk e55252b9ff Mock state ON when moveToLevelWithOnOff succeeds. https://github.com/Koenkk/zigbee-shepherd-converters/pull/81 2018-09-29 23:05:18 +02:00
Koenkk 71d12c3c71 Revert "Allow joining through specified device."
This reverts commit bf85ea1494.
2018-09-29 23:05:18 +02:00
Koenkk bc7b2bd5e0 Allow joining through specified device. 2018-09-29 23:05:18 +02:00
Koenkk 50d58d3dff Always call zigbee shepherd permit join (in both true and false). 2018-09-29 23:05:18 +02:00
Koenkk 4180bd8933 Add friendly_name to graphviz network map. #252 2018-09-29 23:05:18 +02:00
Koenkk 172d11677b Update zigbee-shepherd-converters. 2018-09-29 23:05:18 +02:00
Plácido Revilla ab5537e18c Add support for GE 45857 in-wall smart dimmer (#394)
* Add support for homeassistant discovery of E11-N1EA

* Add support for GE 45857 in-wall smart dimmer
2018-09-29 23:05:18 +02:00
Viet Dzung b06e3b61f2 Support cfg in foundation and functional (#390)
* Support cfg in foundation and functional

* Update zigbee-shepherd-converters.
2018-09-29 23:05:18 +02:00
Stanislav Demydiuk 03d913defc Add ability to send device information in MQTT message payload (#374) 2018-09-29 23:05:18 +02:00
Koenkk e4bcac5411 Update zigbee-shepherd. https://github.com/Koenkk/zigbee-shepherd/pull/4 2018-09-29 23:05:18 +02:00
Koenkk 5ed29d045b 0.1.6 2018-09-23 11:21:24 +02:00
Koen Kanters 75acffa8c7 Fix topicPrefix parsing. #387 2018-09-21 11:12:51 +02:00
Koenkk ade84dc6c0 Fix MQTT command with prefix, introduced in #c22f8b3. #387 2018-09-20 16:16:57 +02:00
Koenkk 7d49e136a6 0.1.5 2018-09-19 21:00:03 +02:00
Koenkk 0c1dba7208 Fix functional command. #386 2018-09-19 20:59:30 +02:00
Koenkk 63f4f681c4 0.1.4 2018-09-19 18:59:52 +02:00
Unknown 1afbe05318 Update zigbee-shepherd-converters. 2018-09-18 22:24:17 +02:00
Plácido Revilla 8376b7c5f6 Add support for homeassistant discovery of E11-N1EA (#379) 2018-09-18 18:07:33 +02:00
Unknown b71a9920de Update zcl-packet. #373 2018-09-18 18:03:27 +02:00
Unknown 2fd59cb7ef Remove log_directory_timestamp option, implement default settings, no : in timestamp. #356 #381 2018-09-18 17:51:34 +02:00
Zhong Jianxin 60e72c612c Allow no timestamp in log directory (#356)
* Allow no timestamp in log directory

* Update log_directory_timestamp option
2018-09-17 22:55:17 +02:00
Koen Kanters fbbc1fad73 Merge pull request #371 from dzungpv/dev
Fix foundation error from #8433571
2018-09-14 08:34:58 +02:00
Koen Kanters d5de918247 Revert 2018-09-14 08:33:28 +02:00
Koen Kanters f916ec5f4c Update foundation publish 2018-09-14 08:31:11 +02:00
Viet Dzung 469a01ebb6 Fix foundation error from #8433571 2018-09-14 10:01:07 +07:00
Koenkk 6ede5a373e Update zigbee-shepherd/zcl-packet. 2018-09-12 22:16:59 +02:00
Koenkk 6e1af542e5 Update zigbee-shepherd-converter. 2018-09-11 22:04:37 +02:00
Koen Kanters 53051e9192 Merge pull request #351 from lolorc/linkquality0
don't ignore linkquality when its value is 0
2018-09-11 21:42:03 +02:00
Koenkk 542a5f1564 Update zigbee-shepherd-converters. #355 2018-09-11 21:41:24 +02:00
Koen Kanters b82f0e46aa Use hasOwnProperty for linkquality check. https://stackoverflow.com/questions/13632999/if-key-in-object-or-ifobject-hasownpropertykey 2018-09-11 21:38:01 +02:00
Koen Kanters f0f2dc847b Merge pull request #355 from dzungpv/dev
Support Foundation Command for use in zigbee-shepherd converter
2018-09-11 21:34:25 +02:00
Koenkk 8433571b1e Refactor multiple foundation. 2018-09-11 21:28:30 +02:00
Koenkk 2141d670bf Update dependencies. 2018-09-11 18:01:48 +02:00
Viet Dzung fccb4de565 Fix Lint error 2018-09-11 14:28:04 +07:00
Viet Dzung 236afaacaf Support Foundation Command for use in zigbee-shepherd converter https://github.com/zigbeer/zcl-packet/wiki/6.-Appendix#FoundCmdTbl 2018-09-11 13:59:49 +07:00
Koenkk f463ca68a5 Improve zigbee message debug logging. 2018-09-10 18:06:29 +02:00
Laurent 7502ba5312 don't ignore linkquality when its value is 0 2018-09-09 19:28:38 +02:00
Koen Kanters c05b70b30b Merge pull request #325 from sebastianheierhoff/master
Fix issue #306: Sending config msg works only with default topic
2018-09-03 16:24:23 +02:00
sebastianheierhoff c22f8b36ba Fix issue #306: Sending config msg works only with default topic 2018-08-31 12:34:59 +02:00
Koenkk 7276d116c2 Update dependencies. 2018-08-30 18:06:52 +02:00
Koen Kanters 1b7549dd73 Merge pull request #316 from sebastianheierhoff/master
Fix issue #229: router devices can't be managed by zigbee2mqtt
2018-08-30 17:59:22 +02:00
Koen Kanters 7e4fff8454 Merge pull request #313 from TheCellMC/master
Add support for Philips Hue motion sensor
2018-08-30 17:53:09 +02:00
sebastianheierhoff ad43d61f28 Fix issue #229: router devices can't be managed by zigbee2mqtt 2018-08-29 19:22:36 +02:00
sebastianheierhoff ed5c52654f Fix issue #229: router devices can't be managed by zigbee2mqtt 2018-08-29 19:19:54 +02:00
sebastianheierhoff 4f1b521bee Fix issue #229: router devices can't be managed by zigbee2mqtt 2018-08-29 18:49:01 +02:00
TheCellMC a46f06a312 Update homeassistant.js 2018-08-29 09:14:20 +02:00
TheCellMC 6311dfb0f1 Update homeassistant.js 2018-08-29 09:11:41 +02:00
TheCellMC ee82a885f7 Update homeassistant.js 2018-08-29 08:57:11 +02:00
TheCellMC 88e6c10fb2 Added support for hue motion sensor 2018-08-29 08:44:31 +02:00
TheCellMC 88524b7aed Added support for hue motion sensor 2018-08-29 08:44:22 +02:00
Koenkk b2a27dd64a Implement basic graphviz networkmap. #252 2018-08-28 22:07:57 +02:00
Koenkk 2a520b0e81 Raw implementation of network map. #252 2018-08-28 21:55:00 +02:00
Koenkk 0c32f25203 Update zigbee-shepherd-converters to 2.0.44 2018-08-25 00:11:42 +02:00
Koen Kanters 1aa020875a Merge pull request #218 from arontsang/master
Allow user to able use the same kind of click multiple times
2018-08-25 00:02:19 +02:00
Koen Kanters 9e5acdc94e Merge branch 'dev' into master 2018-08-24 23:59:39 +02:00
Koen Kanters 417879b6a8 Add force_update: true for action 2018-08-24 23:58:44 +02:00
Koen Kanters 4ee7845b2a Merge pull request #292 from carpenike/master
Added Self Signed CA Support
2018-08-17 18:41:36 +02:00
Koen Kanters fb9600d55d Change selfsigned to reject_unauthorized 2018-08-17 18:39:45 +02:00
Koen Kanters 47de3e20ae Remove selfsigned from configuration.yaml 2018-08-17 18:34:33 +02:00
Koenkk c8c8417192 0.1.3 2018-08-17 18:32:11 +02:00
Ryan Holt c00f091d7f removed trailing whitespace 2018-08-17 12:25:02 -04:00
Ryan Holt b0b2bd2444 adjusted whitespace to remove tabs and replace with spaces. 2018-08-17 12:22:27 -04:00
Ryan Holt 1e0c64caf5 made true default within commented section. 2018-08-17 12:17:18 -04:00
Ryan Holt db1fd6dd12 removed dockerfile 2018-08-17 12:16:47 -04:00
Ryan Holt 9ee0049248 adjusted to support self-signed certificates on mqtt 2018-08-17 12:15:44 -04:00
Koen Kanters 56a0f6256b Merge pull request #287 from tb-killa/logfile-extended
Logfile extended
2018-08-16 20:12:07 +02:00
Koenkk 197a3cdebc Update log file extension. 2018-08-16 20:04:29 +02:00
tb-killa c520985a4f Implement cleanup-function for logfiles 2018-08-16 19:07:32 +02:00
tb-killa 931926cd96 add fs and bring in advanced logname with dateformat 2018-08-16 19:07:32 +02:00
tb-killa 427c99cd83 add moment to logger.js 2018-08-16 19:06:48 +02:00
tb-killa c2cd02e790 add moment to package.json 2018-08-16 19:06:48 +02:00
Koenkk a921394240 Merge branch 'dev' of https://github.com/Koenkk/zigbee2mqtt into dev 2018-08-16 18:56:51 +02:00
Koenkk 21f047f4aa Use localtimestamp for file logging. #289 2018-08-16 18:56:33 +02:00
Koenkk 9a70c95672 Start adding tests. 2018-08-15 22:19:54 +02:00
Koenkk fc2e93d96f Update dependencies 2018-08-15 21:10:19 +02:00
Koenkk 494c15a731 Update dependencies. 2018-08-13 20:37:01 +02:00
Koen Kanters 23a2feb7cf Merge pull request #275 from redmusicxd/patch-1
Trust ZLED-2709
2018-08-13 19:22:39 +02:00
redmusicxd f4ceccaa9b Trust ZLED-2709 2018-08-13 19:22:24 +02:00
Koenkk b4398e21c3 Fix linting 2018-08-13 19:21:55 +02:00
tb-killa 3900d56d6e Extend ping function with friendlyName logging 2018-08-13 19:21:55 +02:00
tb-killa 08f64b96be remove arrow separator 2018-08-13 19:21:55 +02:00
tb-killa 8411c0b30d Change logger-formatter confirm to zigbee-shepherd debug 2018-08-13 19:21:55 +02:00
Koenkk 43260a3a83 Implement mqtt last will message. #267 2018-08-13 19:21:55 +02:00
Koenkk 6f4013fd94 Update zigbee-shepherd-converters. https://github.com/Koenkk/zigbee2mqtt/issues/204 2018-08-13 19:21:55 +02:00
Koenkk 426a89914f Update zigbee-shepherd. #252 2018-08-13 19:21:55 +02:00
tb-killa 4fa1a4c57a Extend configureDevice with friendlyName Output 2018-08-13 19:21:55 +02:00
Koenkk 5ddfcea8f3 Log coordinator firmware version on startup. 2018-08-13 19:21:55 +02:00
Koenkk 502c0cb852 Log error when zigbee command fails. #254 2018-08-13 19:21:55 +02:00
Koenkk 07d8fd5605 Update zigbee-shepherd. 2018-08-13 19:21:55 +02:00
Daniel Welch 445ccb5bbd Update trigger-downstream.sh to dev branch
https://github.com/Koenkk/zigbee-shepherd-converters/pull/46#issuecomment-410533163
@Koenkk
The problem here was that this script is triggered with pushes to `master`, not `dev`. I've updated the edge add-on version to use the `dev` branch rather than `master`. A new image is building now and should be available soon using the dev branch.
2018-08-13 19:21:55 +02:00
Koenkk 74c8758a3b Update zigbee-shepherd. 2018-08-13 19:21:55 +02:00
Koenkk 1f6f73e8d2 Allow to log to custom directory. #207 #182 2018-08-13 19:21:55 +02:00
Koenkk 04b2f24d6b Persist devices state across restarts. #249 2018-08-13 19:21:55 +02:00
Alex Mekkering 5e96c46a7a Update to serialport 6.2.2 2018-08-13 19:21:55 +02:00
Koenkk ec4f5e1472 Update dependencies. 2018-08-13 19:21:55 +02:00
Koenkk 32a512ea6b Update zcl-packet. #150 2018-08-13 19:21:55 +02:00
Koenkk 46a3461e76 Update dependencies 2018-08-13 19:21:55 +02:00
Koenkk fe92e9c421 Fix package.json 2018-08-13 19:21:55 +02:00
Koenkk 2f02168fcb Update to latest zcl-packet. #150 #198 2018-08-13 19:21:55 +02:00
Koenkk 2ba86a13eb Add linkquality to messages. #173 2018-08-13 19:21:55 +02:00
Koen Kanters 7dd1c75be0 Update issue template. #264 2018-08-09 17:56:51 +02:00
Koen Kanters b5f0e9b958 Update ISSUE_TEMPLATE.md 2018-08-08 19:05:22 +02:00
Koenkk bd901ec9dd Add issue template. #264 2018-08-08 19:04:24 +02:00
Koenkk a3d89c0137 0.1.2 2018-07-31 17:20:50 +02:00
Koenkk f1ae2194c9 Lock zigbee-shepherd version. #236 2018-07-31 17:18:19 +02:00
Koenkk 738c31c758 0.1.1 2018-07-27 18:06:57 +02:00
Koenkk f8fcf3a7c8 Temp: lock zcl-packet to older version. #198 2018-07-27 17:49:56 +02:00
Koen Kanters ae863ba5ab Merge pull request #219 from jaaps/hue-bloom
Add Hue Bloom support
2018-07-27 17:39:45 +02:00
Koenkk 5e66019390 Update zigbee-shepherd-converters. 2018-07-27 17:39:31 +02:00
jaaps 696e6b4787 Add Hue Bloom support 2018-07-27 10:09:13 +02:00
Aron Tsang 7403fc7ce4 Add force update 2018-07-27 03:17:09 +08:00
Aron Tsang 79d8cb69c0 Expire clicks after 1 second so you can click again 2018-07-27 03:00:07 +08:00
Koen Kanters b33878304f Merge pull request #214 from Koenkk/issue_148
Add support for MQTT client ID. #148
2018-07-24 18:34:02 +02:00
Koenkk 6e71c48108 Merge branch 'issue_148' of https://github.com/Koenkk/zigbee2mqtt into issue_148 2018-07-24 18:31:41 +02:00
Koenkk 2b7aeed4fb Add support for MQTT client ID. #148 2018-07-24 18:31:16 +02:00
Koen Kanters b3cedc4ad1 Merge pull request #206 from tb-killa/rename-devices
Rename devices (friendly_name) ->  (#177)
2018-07-24 18:27:04 +02:00
Koenkk 8ea62f535e Update change friendly name of device. 2018-07-24 18:25:16 +02:00
Koenkk de879858a5 Update zigbee-shepherd-converters to 2.0.33. 2018-07-22 18:49:45 +02:00
Koenkk d4ba3ae069 Update zigbee-shepherd-converters. 2018-07-21 21:28:42 +02:00
Koenkk 0e608c7b2e Add disable_led option. #133 2018-07-21 21:13:28 +02:00
tb-killa aab18de88d ADD Rename Function via MQTT Command 2018-07-21 18:19:11 +02:00
tb-killa 6d684b7c11 ADD RenameFriendlyName Function 2018-07-21 18:15:56 +02:00
Koenkk 396e4b1bed Update zigbee-shepherd-converters. 2018-07-18 22:50:28 +02:00
Koenkk 04d7a87765 Fix logging of unsupported cid. #199 2018-07-18 22:30:51 +02:00
Koenkk 49878e093d Log message on unsupported cid. 2018-07-14 21:22:50 +02:00
Koenkk 209624cbef Update zigbee-shepherd-converters to 2.0.28. 2018-07-14 21:10:46 +02:00
Koen Kanters 65a9645677 Merge pull request #191 from tomusher/gledopto-rgbcct-controller-support
Add mapping for Gledopto RGB+CCT controller for Home Assistant
2018-07-12 17:47:29 +02:00
Koen Kanters ad41ed6dcf Merge branch 'dev' into gledopto-rgbcct-controller-support 2018-07-12 17:43:44 +02:00
Tom Usher b494136c22 Add mapping for Gledopto RGB+CCT controller for Home Assistant 2018-07-11 13:19:01 +01:00
Koenkk 356ed9fa39 Update zigbee-shepherd and zigbee-shepherd-conveters for 324131092621 support. #36 2018-07-10 18:43:31 +02:00
Koenkk 8b5938f73f Update zigbee-shepherd-converters. 2018-07-07 23:39:30 +02:00
Koenkk 51d8c79ef2 Update zigbee-shepherd (includes firmware revision information). https://github.com/Koenkk/zigbee2mqtt/issues/178 2018-07-05 21:23:30 +02:00
Koenkk 3897541366 Log zigbee-shepherd information on startup. #178 2018-07-05 20:06:38 +02:00
Koenkk 0eee507edb Travis.yml check if TRAVIS_TAG is set to “”. 2018-07-05 20:00:12 +02:00
Koenkk caf87e805d Update zcl-packet. https://github.com/Koenkk/zigbee2mqtt/issues/150 2018-07-05 19:43:54 +02:00
Koenkk ecc734cff9 Add support for MQTT client ID. #148 2018-06-26 19:33:26 +02:00
30 changed files with 5091 additions and 2331 deletions
+9 -5
View File
@@ -1,17 +1,21 @@
{
"env": {
"mocha": true,
"es6": true,
"node": true
},
"extends": ["eslint:recommended", "google"],
"parserOptions": {
"ecmaFeatures": {
"experimentalObjectRestSpread": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
"require-jsdoc": "off",
"indent": ["error", 4],
"max-len": ["error", { "code": 120 }]
}
"max-len": ["error", { "code": 120 }],
"mocha/no-exclusive-tests": "error"
},
"plugins": [
"mocha"
]
}
+11
View File
@@ -0,0 +1,11 @@
Unsupported device
First check: https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices
Bug report
- First make sure your on the latest version of zigbee2mqtt.
- Make sure you are running the latest CC253X firmware from https://github.com/koenkk/z-stack-firmware.
- Provide a clear description of the problem.
- If applicable, provide steps how to reproduce the problem.
- Provide a log with `log_level` set to `debug`, see https://github.com/Koenkk/zigbee2mqtt/wiki/Configuration
- DON'T copy logs directly here, post a link to https://hastebin.com/ or https://pastebin.com/.
+4 -1
View File
@@ -60,4 +60,7 @@ typings/
# data
data/database.db
data/config.json
data/log*.txt
data/log*.txt
data/state.json
data/log
data-backup/
+4 -3
View File
@@ -6,16 +6,17 @@ services:
- docker
node_js:
- "8"
- "10"
install:
- npm install
script:
before_script:
- npm test
- npm run verify-homeassistant-mapping
- npm run eslint
after_success:
script:
- ".travis/docker.sh"
- ".travis/wiki.sh"
- ".travis/trigger-downstream.sh"
+1 -1
View File
@@ -17,7 +17,7 @@ build_and_push() {
# Only update Docker images for:
# - dev branch
# - version release
if [ "$TRAVIS_PULL_REQUEST" = "false" ] && [ ! -z ${TRAVIS_TAG+x} ]
if [ "$TRAVIS_PULL_REQUEST" = "false" ] && [ ! -z ${TRAVIS_TAG+x} ] && [ ! "$TRAVIS_TAG" = "" ]
then
echo "Updating docker images for master branch!"
login
+3 -3
View File
@@ -20,11 +20,11 @@ function travis_trigger() {
"https://api.travis-ci.org/repo/${org}%2F${repo}/requests"
}
# Only trigger downstream if on master branch and not pull request
if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]
# Only trigger downstream if on dev branch and not pull request
if [ "$TRAVIS_BRANCH" = "dev" -a "$TRAVIS_PULL_REQUEST" = "false" ]
then
echo "Triggering build of downstream projects!"
travis_trigger "danielwelch" "hassio-zigbee2mqtt" "master"
else
echo "Not triggering build of downstream projects, triggered by pull request or not on master branch"
echo "Not triggering build of downstream projects, triggered by pull request or not on dev branch"
fi
+6 -1
View File
@@ -24,6 +24,11 @@ Zigbee2mqtt integrates well with (almost) every home automation solution because
- [Hassio](https://www.home-assistant.io/hassio/): Using [the official addon](https://github.com/danielwelch/hassio-zigbee2mqtt) from [danielwelch](https://github.com/danielwelch)
- Generic install or Hassbian: Using instructions [here](https://github.com/Koenkk/zigbee2mqtt/wiki/Integrating-with-Home-Assistant)
<img align="left" height="100px" width="100px" src="https://user-images.githubusercontent.com/2734836/47615848-b8dd8700-dabd-11e8-9d77-175002dd8987.png">
### [Domoticz](https://www.domoticz.com/)
- Integration implemented in [domoticz-zigbee2mqtt-plugin](https://github.com/stas-demydiuk/domoticz-zigbee2mqtt-plugin)
## Architecture
![Architecture](architecture.png)
@@ -39,6 +44,6 @@ If you need assistance you can check [opened issues](https://github.com/Koenkk/z
* [AndrewLinden](https://github.com/AndrewLinden)
* [oskarn97](https://github.com/oskarn97)
* [dgomes](https://github.com/dgomes)
* [Koenkk](https://github.com/Koenk)
* [Koenkk](https://github.com/Koenkk)
* [kirovilya](https://github.com/kirovilya)
* [ciotlosm](https://github.com/ciotlosm)
+2 -2
View File
@@ -1,4 +1,4 @@
FROM alpine:3.7
FROM alpine:3.8
# Copy files
ADD . /app
@@ -12,7 +12,7 @@ ARG COMMIT
RUN echo "{\"hash\": \"$COMMIT\"}" > .hash.json
# Install dependencies
RUN apk add --update --no-cache make gcc g++ python linux-headers udev nodejs git && \
RUN apk add --update --no-cache make gcc g++ python linux-headers udev nodejs=8.11.4-r0 npm=8.11.4-r0 git && \
npm install --unsafe-perm && \
apk del make gcc g++ python linux-headers udev git
+2 -2
View File
@@ -1,4 +1,4 @@
FROM arm32v6/alpine:3.7
FROM arm32v6/alpine:3.8
# Setup cross-build
ENV QEMU_EXECVE 1
@@ -14,7 +14,7 @@ ADD . /app
RUN [ "qemu-arm-static", "/bin/sh", "-c", \
"cp /app/data/configuration.yaml /app && \
cp /app/docker/run.sh /app && chmod +x /app/run.sh && \
apk add --update --no-cache make gcc g++ python linux-headers udev nodejs git && \
apk add --update --no-cache make gcc g++ python linux-headers udev nodejs=8.11.4-r0 npm=8.11.4-r0 git && \
npm install --unsafe-perm && \
apk del make gcc g++ python linux-headers udev git"]
+2 -2
View File
@@ -1,4 +1,4 @@
FROM arm64v8/alpine:3.7
FROM arm64v8/alpine:3.8
# Setup cross-build
ENV QEMU_EXECVE 1
@@ -14,7 +14,7 @@ ADD . /app
RUN [ "qemu-aarch64-static", "/bin/sh", "-c", \
"cp /app/data/configuration.yaml /app && \
cp /app/docker/run.sh /app && chmod +x /app/run.sh && \
apk add --update --no-cache make gcc g++ python linux-headers udev nodejs git && \
apk add --update --no-cache make gcc g++ python linux-headers udev nodejs=8.11.4-r0 npm=8.11.4-r0 git && \
npm install --unsafe-perm && \
apk del make gcc g++ python linux-headers udev git"]
+8 -1
View File
@@ -1,5 +1,12 @@
const Controller = require('./lib/controller');
const semver = require('semver');
const engines = require('./package.json').engines;
const version = engines.node;
if (!semver.satisfies(process.version, version)) {
console.log(`\t\tZigbee2mqtt requires node version ${version}, you are running ${process.version}!\n`); // eslint-disable-line
}
const Controller = require('./lib/controller');
const controller = new Controller();
controller.start();
+134 -186
View File
@@ -1,17 +1,17 @@
const MQTT = require('./mqtt');
const Zigbee = require('./zigbee');
const State = require('./state');
const logger = require('./util/logger');
const settings = require('./util/settings');
const ExtensionNetworkMap = require('./extension/networkMap');
const ExtensionSoftReset = require('./extension/softReset');
const ExtensionRouterPollXiaomi = require('./extension/routerPollXiaomi');
const ExtensionDevicePublish = require('./extension/devicePublish');
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
const homeassistant = require('./homeassistant');
const objectAssignDeep = require(`object-assign-deep`);
const objectAssignDeep = require('object-assign-deep');
const mqttConfigRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/\\w+`, 'g');
const mqttDeviceRegex = new RegExp(`${settings.get().mqtt.base_topic}/[\\w\\s\\d]+/set`, 'g');
const mqttDevicePrefixRegex = new RegExp(`${settings.get().mqtt.base_topic}/[\\w\\s\\d]+/[\\w\\s\\d]+/set`, 'g');
const pollInterval = 60 * 1000; // seconds * 1000.
const softResetTimeout = 3600 * 1000; // seconds * 1000.
const allowedLogLevels = ['error', 'warn', 'info', 'debug'];
@@ -30,17 +30,19 @@ if (settings.get().homeassistant && !cacheState) {
class Controller {
constructor() {
this.zigbee = new Zigbee();
this.mqtt = new MQTT();
this.stateCache = {};
this.configured = [];
this.handleZigbeeMessage = this.handleZigbeeMessage.bind(this);
this.handleMQTTMessage = this.handleMQTTMessage.bind(this);
this.zigbee = new Zigbee(this.handleZigbeeMessage);
this.mqtt = new MQTT();
this.state = new State();
this.configured = [];
this.extensions = [];
}
start() {
this.startupLogVersion(() => {
this.zigbee.start(this.handleZigbeeMessage, (error) => {
this.zigbee.start((error) => {
if (error) {
logger.error('Failed to start', error);
} else {
@@ -57,17 +59,12 @@ class Controller {
logger.warn('`permit_join` set to `true` in configuration.yaml.');
logger.warn('Allowing new devices to join.');
logger.warn('Set `permit_join` to `false` once you joined all devices.');
this.zigbee.permitJoin(true);
}
// Start timers.
this.pollTimer(true);
this.softResetTimeout(true);
this.zigbee.permitJoin(settings.get().permit_join);
// Connect to MQTT broker
const subscriptions = [
`${settings.get().mqtt.base_topic}/+/set`,
`${settings.get().mqtt.base_topic}/+/+/set`,
`${settings.get().mqtt.base_topic}/bridge/config/+`,
];
@@ -93,73 +90,39 @@ class Controller {
});
}
// Initialize extensions.
this.extensions = [
new ExtensionDevicePublish(this.zigbee, this.mqtt, this.state, this.mqttPublishDeviceState),
new ExtensionNetworkMap(this.zigbee, this.mqtt, this.state, this.mqttPublishDeviceState),
new ExtensionSoftReset(this.zigbee, this.mqtt, this.state, this.mqttPublishDeviceState),
new ExtensionRouterPollXiaomi(this.zigbee, this.mqtt, this.state, this.mqttPublishDeviceState),
];
// Resend all cached states.
this.sendAllCachedStates();
}
sendAllCachedStates() {
this.zigbee.getAllClients().forEach((device) => {
if (this.stateCache.hasOwnProperty(device.ieeeAddr)) {
this.mqttPublishDeviceState(device.ieeeAddr, this.stateCache[device.ieeeAddr], false);
if (this.state.exists(device.ieeeAddr)) {
this.mqttPublishDeviceState(device, this.state.get(device.ieeeAddr), false);
}
});
}
softResetTimeout(start) {
if (this._softResetTimer) {
clearTimeout(this._softResetTimer);
this._softResetTimer = null;
}
if (start) {
this._softResetTimer = setTimeout(() => {
this.zigbee.softReset((error) => {
if (error) {
logger.warn('Soft reset error', error);
this.zigbee.stop((error) => {
logger.warn('Zigbee stopped');
this.zigbee.start(this.handleZigbeeMessage, (error) => {
if (error) {
logger.error('Failed to restart!');
}
});
});
} else {
logger.warn('Soft resetted zigbee');
}
this.softResetTimeout(true);
});
}, softResetTimeout);
}
}
pollTimer(start) {
// Some routers need polling to prevent them from sleeping.
if (start && !this._pollTimer) {
this._pollTimer = setInterval(() => {
const devices = this.zigbee.getAllClients().filter((d) => {
const power = d.powerSource ? d.powerSource.toLowerCase().split(' ')[0] : 'unknown';
return power !== 'battery' && power !== 'unknown' && d.type === 'Router';
});
devices.forEach((d) => this.zigbee.ping(d.ieeeAddr));
}, pollInterval);
} else if (!start && this._pollTimer) {
clearTimeout(this._pollTimer);
this._pollTimer = null;
}
}
stop(callback) {
this.extensions.filter((e) => e.stop).forEach((e) => e.stop());
this.state.save();
this.mqtt.disconnect();
this.pollTimer(false);
this.softResetTimeout(false);
this.zigbee.stop(callback);
}
configureDevice(device) {
let friendlyName = 'unknown';
const ieeeAddr = device.ieeeAddr;
if (settings.getDevice(ieeeAddr)) {
friendlyName = settings.getDevice(ieeeAddr).friendly_name;
}
if (ieeeAddr && device.modelId && !this.configured.includes(ieeeAddr)) {
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
@@ -167,9 +130,9 @@ class Controller {
if (mappedModel && mappedModel.configure) {
mappedModel.configure(ieeeAddr, this.zigbee.shepherd, this.zigbee.getCoordinator(), (ok, msg) => {
if (ok) {
logger.info(`Succesfully configured ${ieeeAddr}`);
logger.info(`Succesfully configured ${friendlyName} ${ieeeAddr}`);
} else {
logger.error(`Failed to configure ${ieeeAddr}`);
logger.error(`Failed to configure ${friendlyName} ${ieeeAddr}`);
}
});
}
@@ -204,11 +167,36 @@ class Controller {
`${friendlyDevice.vendor} ${friendlyDevice.description} (${type})`;
}
handleZigbeeMessage(message) {
// Zigbee message receieved, reset soft reset timeout.
this.softResetTimeout(true);
getDeviceInfoForMqtt(device) {
const {type, ieeeAddr, nwkAddr, manufId, manufName, powerSource, modelId, status} = device;
const deviceSettings = settings.getDevice(device.ieeeAddr);
return {
ieeeAddr,
friendlyName: deviceSettings.friendly_name || '',
type,
nwkAddr,
manufId,
manufName,
powerSource,
modelId,
status,
};
}
handleZigbeeMessage(message) {
// Call extensions.
this.extensions.filter((e) => e.handleZigbeeMessage).forEach((e) => e.handleZigbeeMessage(message));
// Log the message.
let logMessage = `Received zigbee message of type '${message.type}' ` +
`with data '${JSON.stringify(message.data)}'`;
if (message.endpoints && message.endpoints[0].device) {
const device = message.endpoints[0].device;
logMessage += ` of device '${device.modelId}' (${device.ieeeAddr})`;
}
logger.debug(logMessage);
logger.debug('Recieved zigbee message with data', JSON.stringify(message.data));
if (message.type == 'devInterview' && !settings.getDevice(message.data)) {
logger.info('Connecting with device...');
this.mqtt.log('pairing', 'connecting with device');
@@ -282,10 +270,14 @@ class Controller {
if (!converters.length) {
if (cid) {
logger.warn(
`No converter available for '${mappedModel.model}' with cid '${cid}' and type '${message.type}'`
`No converter available for '${mappedModel.model}' with cid '${cid}', ` +
`type '${message.type}' and data '${JSON.stringify(message.data)}'`
);
} else if (cmdId) {
logger.warn(`No converter available for '${mappedModel.model}' with cmd '${cmdId}'`);
logger.warn(
`No converter available for '${mappedModel.model}' with cmd '${cmdId}' ` +
`and data '${JSON.stringify(message.data)}'`
);
}
logger.warn(`Please see: https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices.`);
@@ -308,7 +300,12 @@ class Controller {
}
});
this.mqttPublishDeviceState(device.ieeeAddr, payload, cache);
// Add device linkquality.
if (message.hasOwnProperty('linkquality')) {
payload.linkquality = message.linkquality;
}
this.mqttPublishDeviceState(device, payload, cache);
};
const payload = converter.convert(mappedModel, message, publish, settings.getDevice(device.ieeeAddr));
@@ -320,12 +317,15 @@ class Controller {
}
handleMQTTMessage(topic, message) {
logger.debug(`Recieved mqtt message on topic '${topic}' with data '${message}'`);
logger.debug(`Received MQTT message on '${topic}' with data '${message}'`);
// Find extensions that can handle MQTT messages and get results
const results = this.extensions
.filter((e) => e.handleMQTTMessage)
.map((e) => e.handleMQTTMessage(topic, message));
if (topic.match(mqttConfigRegex)) {
this.handleMQTTMessageConfig(topic, message);
} else if (topic.match(mqttDeviceRegex) || topic.match(mqttDevicePrefixRegex)) {
this.handleMQTTMessageDevice(topic, message, topic.match(mqttDevicePrefixRegex));
} else if (topic === 'hass/status') {
if (message.toString().toLowerCase() === 'online') {
const timer = setTimeout(() => {
@@ -333,13 +333,13 @@ class Controller {
clearTimeout(timer);
}, 20000);
}
} else {
logger.warn(`Cannot handle MQTT message with topic '${topic}' and message '${message}'`);
} else if (!results.includes(true)) {
logger.warn(`Cannot handle MQTT message on '${topic}' with data '${message}'`);
}
}
handleMQTTMessageConfig(topic, message) {
const option = topic.split('/')[3];
const option = topic.split('/').slice(-1)[0];
if (option === 'permit_join') {
this.zigbee.permitJoin(message.toString().toLowerCase() === 'true');
@@ -384,6 +384,9 @@ class Controller {
// Remove from configuration.yaml
settings.removeDevice(deviceID);
// Remove from state
this.state.remove(deviceID);
logger.info(`Successfully removed ${deviceID}`);
this.mqtt.log('device_removed', message);
};
@@ -400,129 +403,74 @@ class Controller {
} else {
cleanup();
}
} else if (option === 'rename') {
const invalid = `Invalid rename message format expected {old: 'friendly_name', new: 'new_name} ` +
`got ${message.toString()}`;
let json = null;
try {
json = JSON.parse(message.toString());
} catch (e) {
logger.error(invalid);
return;
}
// Validate message
if (!json.new || !json.old) {
logger.error(invalid);
return;
}
if (settings.changeFriendlyName(json.old, json.new)) {
logger.info(`Successfully renamed - ${json.old} to ${json.new} `);
} else {
logger.error(`Failed to renamed - ${json.old} to ${json.new}`);
return;
}
// Homeassistant rediscover
if (settings.get().homeassistant) {
const ID = settings.getIDByFriendlyName(json.new);
const device = this.zigbee.getDevice(ID);
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
if (mappedModel) {
homeassistant.discover(device.ieeeAddr, mappedModel.model, this.mqtt, true);
}
}
} else {
logger.warn(`Cannot handle MQTT config option '${option}' with message '${message}'`);
}
}
handleMQTTMessageDevice(topic, message, withPrefix) {
const friendlyName = topic.split('/')[1];
const topicPrefix = withPrefix ? topic.split('/')[2] : '';
mqttPublishDeviceState(device, payload, cache) {
const deviceID = device.ieeeAddr;
const appSettings = settings.get();
let messagePayload = {...payload};
// Map friendlyName to deviceID.
const deviceID = settings.getIDByFriendlyName(friendlyName);
if (!deviceID) {
logger.error(`Cannot handle '${topic}' because deviceID of '${friendlyName}' cannot be found`);
return;
}
// Convert the MQTT message to a Zigbee message.
let json = null;
try {
json = JSON.parse(message);
} catch (e) {
// Cannot be parsed to JSON, assume state message.
json = {state: message.toString()};
}
// Find ep for this device
const device = this.zigbee.getDevice(deviceID);
if (!device) {
logger.error(`Failed to find device with deviceID ${deviceID}`);
return;
}
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
if (!mappedModel) {
logger.warn(`Device with modelID '${device.modelId}' is not supported.`);
logger.warn(`Please see: https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices`);
return;
}
const ep = mappedModel.ep && mappedModel.ep[topicPrefix] ? mappedModel.ep[topicPrefix] : null;
const published = [];
Object.keys(json).forEach((key) => {
// Find converter for this key.
const converter = mappedModel.toZigbee.find((c) => c.key === key);
if (!converter) {
logger.error(`No converter available for '${key}' (${json[key]})`);
return;
}
const message = converter.convert(json[key], json);
if (!message) {
return;
}
const callback = (error) => {
// Devices do not report when they go off, this ensures state (on/off) is always in sync.
if (!error && key.startsWith('state')) {
const msg = {};
const _key = topicPrefix ? `${key}_${topicPrefix}` : key;
msg[_key] = json[key];
this.mqttPublishDeviceState(deviceID, msg, true);
}
};
this.zigbee.publish(deviceID, message.cid, message.cmd, message.zclData, ep, callback);
published.push({message: message, converter: converter});
});
/**
* After publishing a command to a zigbee device we want to monitor the changed attribute(s) so that
* everything stays in sync.
*/
published.forEach((p) => {
let counter = 0;
let secondsToMonitor = 1;
// In case of a transition we need to monitor for the whole transition time.
if (p.message.zclData.hasOwnProperty('transtime')) {
// Note that: transtime 10 = 0.1 seconds, 100 = 1 seconds, etc.
secondsToMonitor = (p.message.zclData.transtime / 10) + 1;
}
const timer = setInterval(() => {
counter++;
// Doing a 'read' will result in the device sending a zigbee message with the current attribute value.
// which will be handled by this.handleZigbeeMessage.
p.converter.attr.forEach((attribute) => {
this.zigbee.read(deviceID, p.message.cid, attribute, ep, () => null);
});
if (counter >= secondsToMonitor) {
clearTimeout(timer);
}
}, 1000);
});
}
mqttPublishDeviceState(deviceID, payload, cache) {
if (cacheState) {
// Add cached state to payload
if (this.stateCache[deviceID]) {
payload = objectAssignDeep.noMutate(this.stateCache[deviceID], payload);
if (this.state.exists(deviceID)) {
messagePayload = objectAssignDeep.noMutate(this.state.get(deviceID), payload);
}
// Update state cache with new state.
if (cache) {
this.stateCache[deviceID] = payload;
this.state.set(deviceID, messagePayload);
}
}
const deviceSettings = settings.getDevice(deviceID);
const friendlyName = deviceSettings ? deviceSettings.friendly_name : deviceID;
const options = {
retain: deviceSettings.retain,
qos: deviceSettings.qos ? deviceSettings.qos : 0,
retain: deviceSettings ? deviceSettings.retain : false,
qos: deviceSettings && deviceSettings.qos ? deviceSettings.qos : 0,
};
this.mqtt.publish(deviceSettings.friendly_name, JSON.stringify(payload), options);
if (appSettings.mqtt.include_device_information) {
messagePayload.device = this.getDeviceInfoForMqtt(device);
}
this.mqtt.publish(friendlyName, JSON.stringify(messagePayload), options);
}
startupLogVersion(callback) {
+149
View File
@@ -0,0 +1,149 @@
const settings = require('../util/settings');
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
const Queue = require('queue');
const logger = require('../util/logger');
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/.+/(set|get)$`);
const postfixes = ['left', 'right', 'center', 'bottom_left', 'bottom_right', 'top_left', 'top_right'];
class DevicePublish {
constructor(zigbee, mqtt, state, mqttPublishDeviceState) {
this.zigbee = zigbee;
this.mqtt = mqtt;
this.state = state;
// TODO -> remove this; move to publish device state method to mqtt.js
this.mqttPublishDeviceState = mqttPublishDeviceState;
/**
* Setup command queue.
* The command queue ensures that only 1 command is executed at a time.
* When executing multiple commands at the same time, some commands may fail.
*/
this.queue = new Queue();
this.queue.concurrency = 1;
this.queue.autostart = true;
// Subscribe to topics.
const maxDepth = 20;
for (let step = 1; step < maxDepth; step++) {
const topic = `${settings.get().mqtt.base_topic}/${'+/'.repeat(step)}`;
this.mqtt.subscribe(`${topic}set`);
this.mqtt.subscribe(`${topic}get`);
}
}
stop() {
this.queue.stop();
}
parseTopic(topic) {
if (!topic.match(topicRegex)) {
return null;
}
// Remove base from topic
topic = topic.replace(`${settings.get().mqtt.base_topic}/`, '');
// Parse type from topic
const type = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
// Remove type from topic
topic = topic.replace(`/${type}`, '');
// Check if we have to deal with a postfix.
let postfix = null;
if (postfixes.find((p) => topic.endsWith(p))) {
postfix = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
// Remove postfix from topic
topic = topic.replace(`/${postfix}`, '');
}
const deviceID = topic;
return {type: type, deviceID: deviceID, postfix: postfix};
}
handleMQTTMessage(topic, message) {
topic = this.parseTopic(topic);
if (!topic) {
return false;
}
// Map friendlyName to ieeeAddr if possible.
const ieeeAddr = settings.getIeeAddrByFriendlyName(topic.deviceID) || topic.deviceID;
// Get device
const device = this.zigbee.getDevice(ieeeAddr);
if (!device) {
logger.error(`Failed to find device with ieeAddr: '${ieeeAddr}'`);
return;
}
// Map device to a model
const model = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
if (!model) {
logger.warn(`Device with modelID '${device.modelId}' is not supported.`);
logger.warn(`Please see: https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices`);
return;
}
// Convert the MQTT message to a Zigbee message.
let json = null;
try {
json = JSON.parse(message);
} catch (e) {
// Cannot be parsed to JSON, assume state message.
json = {state: message.toString()};
}
// Determine endpoint to publish to.
const endpoint = model.hasOwnProperty('ep') && model.ep.hasOwnProperty(topic.postfix) ?
model.ep[topic.postfix] : null;
// For each key in the JSON message find the matching converter.
Object.keys(json).forEach((key) => {
const converter = model.toZigbee.find((c) => c.key === key);
if (!converter) {
logger.error(`No converter available for '${key}' (${json[key]})`);
return;
}
// Converter didn't return a result, skip
const converted = converter.convert(json[key], json, topic.type);
if (!converted) {
return;
}
// Add job to queue
this.queue.push((queueCallback) => {
this.zigbee.publish(
ieeeAddr,
converted.cid,
converted.cmd,
converted.cmdType,
converted.zclData,
converted.cfg,
endpoint,
(error, rsp) => {
// Devices do not report when they go off, this ensures state (on/off) is always in sync.
if (topic.type === 'set' && !error && (key.startsWith('state') || key === 'brightness')) {
const msg = {};
const _key = topic.postfix ? `state_${topic.postfix}` : 'state';
msg[_key] = key === 'brightness' ? 'ON' : json['state'];
this.mqttPublishDeviceState(device, msg, true);
}
queueCallback();
}
);
});
});
return true;
}
}
module.exports = DevicePublish;
+89
View File
@@ -0,0 +1,89 @@
const settings = require('../util/settings');
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
class NetworkMap {
constructor(zigbee, mqtt, state) {
this.zigbee = zigbee;
this.mqtt = mqtt;
this.state = state;
// Subscribe to topic.
this.topic = `${settings.get().mqtt.base_topic}/bridge/networkmap`;
this.mqtt.subscribe(this.topic);
// Set supported formats
this.supportedFormats = {
'raw': this.raw,
'graphviz': this.graphviz,
};
}
handleMQTTMessage(topic, message) {
message = message.toString();
if (topic === this.topic && this.supportedFormats.hasOwnProperty(message)) {
this.zigbee.networkScan((result)=> {
const converted = this.supportedFormats[message](this.zigbee, result);
this.mqtt.publish(`bridge/networkmap/${message}`, converted, {});
});
return true;
}
return false;
}
raw(zigbee, topology) {
return JSON.stringify(topology);
}
graphviz(zigbee, topology) {
let text = 'digraph G {\nnode[shape=record];\n';
const lqiDevices = new Map(topology.map((d) => [d.ieeeAddr, d]));
zigbee.getDevices().forEach((device) => {
const labels = [];
const friendlyDevice = settings.getDevice(device.ieeeAddr);
const friendlyName = friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr;
// Add friendly name
labels.push(friendlyName);
// Add the device type
labels.push(device.type);
// Add the device model
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
if (mappedModel) {
labels.push(`${mappedModel.vendor} ${mappedModel.description} (${mappedModel.model})`);
} else {
// This model is not supported by zigbee-shepherd-converters, add zigbee model information, if available
const zigbeeModel = [device.manufName, device.modelId].filter((a) => a).join(' ');
labels.push(zigbeeModel ? zigbeeModel : 'No model information available');
}
// Add the device status (online/offline)
labels.push(device.status);
// Add the device with its labels to the graph as a node.
text += ` "${device.ieeeAddr}" [label="{${labels.join('|')}}"];\n`;
/**
* Add an edge between the device and its parent to the graph
* NOTE: There are situations where a device is NOT in the topology, this can be e.g.
* due to not responded to the lqi scan. In that case we do not add an edge for this device.
*/
const lqiDevice = lqiDevices.get(device.ieeeAddr);
if (lqiDevice != undefined) {
text += ` "${device.ieeeAddr}" -> "${lqiDevice.parent}" [label="${lqiDevice.lqi}"]\n`;
}
});
text += '}';
return text.replace(/\0/g, '');
}
}
module.exports = NetworkMap;
+40
View File
@@ -0,0 +1,40 @@
const utils = require('../util/utils');
const interval = utils.secondsToMilliseconds(60);
/**
* This extensions polls Xiaomi Zigbee routers to keep them awake.
*/
class RouterPollXiaomi {
constructor(zigbee, mqtt, state) {
this.zigbee = zigbee;
this.timer = null;
this.startTimer();
}
startTimer() {
this.clearTimer();
this.timer = setInterval(() => this.handleInterval(), interval);
}
clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
stop() {
this.clearTimer();
}
handleInterval() {
this.zigbee.getAllClients()
.filter((d) => utils.isXiaomiDevice(d)) // Filter Xiaomi devices
.filter((d) => d.type === 'Router') // Filter routers
.filter((d) => d.powerSource && d.powerSource !== 'Battery') // Remove battery powered devices
.forEach((d) => this.zigbee.ping(d.ieeeAddr)); // Ping devices.
}
}
module.exports = RouterPollXiaomi;
+66
View File
@@ -0,0 +1,66 @@
const settings = require('../util/settings');
const logger = require('../util/logger');
const utils = require('../util/utils');
/**
* This extensions soft resets the ZNP after a certain timeout.
*/
class SoftReset {
constructor(zigbee, mqtt, state) {
this.zigbee = zigbee;
this.timer = null;
this.timeout = utils.secondsToMilliseconds(settings.get().advanced.soft_reset_timeout);
if (this.timeout === 0) {
logger.debug(`Soft reset timeout disabled`);
} else {
logger.debug(`Soft reset timeout set to ${utils.millisecondsToSeconds(this.timeout)} seconds`);
}
this.resetTimer();
}
clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
resetTimer() {
if (this.timeout === 0) {
return;
}
this.clearTimer();
this.timer = setTimeout(() => this.handleTimeout(), this.timeout);
}
handleTimeout() {
logger.warn('Soft reset timeout triggered');
this.zigbee.softReset((error) => {
if (error) {
logger.warn('Soft reset failed, trying stop/start');
this.zigbee.stop((error) => {
logger.warn('Zigbee stopped');
this.zigbee.start((error) => {
if (error) {
logger.error('Failed to restart!');
}
});
});
} else {
logger.warn('Soft resetted ZNP due to timeout');
}
this.resetTimer();
});
}
handleZigbeeMessage(message) {
this.resetTimer();
}
}
module.exports = SoftReset;
+130 -38
View File
@@ -10,7 +10,18 @@ const configurations = {
payload_off: false,
value_template: '{{ value_json.occupancy }}',
device_class: 'motion',
json_attributes: ['battery', 'voltage'],
json_attributes: ['linkquality', 'battery', 'voltage'],
},
},
'binary_sensor_presence': {
type: 'binary_sensor',
object_id: 'presence',
discovery_payload: {
payload_on: true,
payload_off: false,
value_template: '{{ value_json.presence }}',
device_class: 'presence',
json_attributes: ['battery', 'voltage', 'action'],
},
},
'binary_sensor_contact': {
@@ -21,7 +32,7 @@ const configurations = {
payload_off: true,
value_template: '{{ value_json.contact }}',
device_class: 'door',
json_attributes: ['battery', 'voltage'],
json_attributes: ['linkquality', 'battery', 'voltage'],
},
},
'binary_sensor_water_leak': {
@@ -32,7 +43,7 @@ const configurations = {
payload_off: false,
value_template: '{{ value_json.water_leak }}',
device_class: 'moisture',
json_attributes: ['battery', 'voltage'],
json_attributes: ['linkquality', 'battery', 'voltage'],
},
},
'binary_sensor_smoke': {
@@ -43,7 +54,18 @@ const configurations = {
payload_off: false,
value_template: '{{ value_json.smoke }}',
device_class: 'smoke',
json_attributes: ['battery', 'voltage'],
json_attributes: ['linkquality', 'battery', 'voltage'],
},
},
'binary_sensor_gas': {
type: 'binary_sensor',
object_id: 'gas',
discovery_payload: {
payload_on: true,
payload_off: false,
value_template: '{{ value_json.gas }}',
device_class: 'gas',
json_attributes: ['linkquality', 'sensitivity'],
},
},
'binary_sensor_router': {
@@ -54,7 +76,7 @@ const configurations = {
payload_off: false,
value_template: '{{ value_json.state }}',
device_class: 'connectivity',
json_attributes: ['description', 'type', 'rssi'],
json_attributes: ['linkquality', 'description', 'type', 'rssi'],
},
},
@@ -64,9 +86,9 @@ const configurations = {
object_id: 'illuminance',
discovery_payload: {
unit_of_measurement: 'lx',
icon: 'mdi:theme-light-dark',
device_class: 'illuminance',
value_template: '{{ value_json.illuminance }}',
json_attributes: ['battery', 'voltage'],
json_attributes: ['linkquality', 'battery', 'voltage'],
},
},
'sensor_humidity': {
@@ -74,9 +96,9 @@ const configurations = {
object_id: 'humidity',
discovery_payload: {
unit_of_measurement: '%',
icon: 'mdi:water-percent',
device_class: 'humidity',
value_template: '{{ value_json.humidity }}',
json_attributes: ['battery', 'voltage'],
json_attributes: ['linkquality', 'battery', 'voltage'],
},
},
'sensor_temperature': {
@@ -84,19 +106,19 @@ const configurations = {
object_id: 'temperature',
discovery_payload: {
unit_of_measurement: '°C',
icon: 'mdi:temperature-celsius',
device_class: 'temperature',
value_template: '{{ value_json.temperature }}',
json_attributes: ['battery', 'voltage'],
json_attributes: ['linkquality', 'battery', 'voltage'],
},
},
'sensor_pressure': {
type: 'sensor',
object_id: 'pressure',
discovery_payload: {
unit_of_measurement: 'Pa',
icon: 'mdi:speedometer',
unit_of_measurement: 'hPa',
device_class: 'pressure',
value_template: '{{ value_json.pressure }}',
json_attributes: ['battery', 'voltage'],
json_attributes: ['linkquality', 'battery', 'voltage'],
},
},
'sensor_click': {
@@ -105,7 +127,8 @@ const configurations = {
discovery_payload: {
icon: 'mdi:toggle-switch',
value_template: '{{ value_json.click }}',
json_attributes: ['battery', 'voltage', 'action', 'duration'],
json_attributes: ['linkquality', 'battery', 'voltage', 'action', 'duration'],
force_update: true,
},
},
'sensor_power': {
@@ -115,16 +138,20 @@ const configurations = {
unit_of_measurement: 'Watt',
icon: 'mdi:flash',
value_template: '{{ value_json.power }}',
json_attributes: ['voltage', 'temperature', 'consumption', 'current', 'power_factor'],
json_attributes: ['linkquality', 'voltage', 'temperature', 'consumption', 'current', 'power_factor'],
},
},
'sensor_action': {
type: 'sensor',
object_id: 'action',
discovery_payload: {
icon: 'mdi:cube',
icon: 'mdi:gesture-double-tap',
value_template: '{{ value_json.action }}',
json_attributes: ['battery', 'voltage', 'angle', 'side', 'from_side', 'to_side'],
json_attributes: [
'linkquality', 'battery', 'voltage', 'angle', 'side', 'from_side', 'to_side', 'brightness',
'angle_x_absolute', 'angle_y_absolute', 'angle_z', 'angle_y', 'angle_x', 'unknown_data',
],
force_update: true,
},
},
'sensor_brightness': {
@@ -134,7 +161,16 @@ const configurations = {
unit_of_measurement: 'brightness',
icon: 'mdi:brightness-5',
value_template: '{{ value_json.brightness }}',
json_attributes: [],
json_attributes: ['linkquality'],
},
},
'sensor_lock': {
type: 'sensor',
object_id: 'lock',
discovery_payload: {
icon: 'mdi:lock',
value_template: '{{ value_json.inserted }}',
json_attributes: ['linkquality', 'forgotten', 'keyerror'],
},
},
@@ -191,28 +227,21 @@ const configurations = {
command_topic: true,
},
},
'switch_left': {
};
const switchWithPostfix = (postfix) => {
return {
type: 'switch',
object_id: 'switch_left',
object_id: `switch_${postfix}`,
discovery_payload: {
payload_off: 'OFF',
payload_on: 'ON',
value_template: '{{ value_json.state_left }}',
value_template: `{{ value_json.state_${postfix} }}`,
command_topic: true,
command_topic_prefix: 'left',
command_topic_prefix: postfix,
json_attributes: ['linkquality', `button_${postfix}`],
},
},
'switch_right': {
type: 'switch',
object_id: 'switch_right',
discovery_payload: {
payload_off: 'OFF',
payload_on: 'ON',
value_template: '{{ value_json.state_right }}',
command_topic: true,
command_topic_prefix: 'right',
},
},
};
};
// Map homeassitant configurations to devices.
@@ -223,7 +252,7 @@ const mapping = {
'WXKG03LM': [configurations.sensor_click],
'WXKG02LM': [configurations.sensor_click],
'QBKG04LM': [configurations.switch],
'QBKG03LM': [configurations.switch_left, configurations.switch_right],
'QBKG03LM': [switchWithPostfix('left'), switchWithPostfix('right')],
'WSDCGQ01LM': [configurations.sensor_temperature, configurations.sensor_humidity],
'WSDCGQ11LM': [configurations.sensor_temperature, configurations.sensor_humidity, configurations.sensor_pressure],
'RTCGQ01LM': [configurations.binary_sensor_occupancy],
@@ -240,12 +269,14 @@ const mapping = {
'LED1537R6': [configurations.light_brightness_colortemp],
'LED1650R5': [configurations.light_brightness],
'LED1536G5': [configurations.light_brightness_colortemp],
'7299760PH': [configurations.light_brightness_xy],
'7146060PH': [configurations.light_brightness_colortemp_xy],
'F7C033': [configurations.light_brightness],
'JTYJ-GD-01LM/BW': [configurations.binary_sensor_smoke],
'PLUG EDP RE:DY': [configurations.switch, configurations.sensor_power],
'CC2530.ROUTER': [configurations.binary_sensor_router],
'AA70155': [configurations.light_brightness_colortemp],
'4058075816718': [configurations.light_brightness_colortemp_xy],
'AA69697': [configurations.light_brightness_colortemp_xy],
'HALIGHTDIMWWE27': [configurations.light_brightness],
'AB3257001NJ': [configurations.switch],
@@ -280,7 +311,7 @@ const mapping = {
'SL 110 W': [configurations.light_brightness],
'AA68199': [configurations.light_brightness_colortemp],
'QBKG11LM': [configurations.switch, configurations.sensor_power],
'QBKG12LM': [configurations.switch_left, configurations.switch_right, configurations.sensor_power],
'QBKG12LM': [switchWithPostfix('left'), switchWithPostfix('right'), configurations.sensor_power],
'K2RGBW01': [configurations.light_brightness_colortemp_xy],
'9290011370': [configurations.light_brightness],
'DNCKATSW001': [configurations.switch],
@@ -288,6 +319,67 @@ const mapping = {
'NL08-0800': [configurations.light_brightness],
'915005106701': [configurations.light_brightness_colortemp_xy],
'AB32840': [configurations.light_brightness_colortemp],
'8718696485880': [configurations.light_brightness_colortemp_xy],
'8718696598283': [configurations.light_brightness_colortemp],
'8718696695203': [configurations.light_brightness_colortemp],
'73693': [configurations.light_brightness_colortemp_xy],
'324131092621': [configurations.sensor_action],
'9290012607': [
configurations.binary_sensor_occupancy, configurations.sensor_temperature,
configurations.sensor_illuminance,
],
'GL-C-008': [configurations.light_brightness_colortemp_xy],
'STSS-MULT-001': [configurations.binary_sensor_contact],
'E11-G23/E11-G33': [configurations.light_brightness],
'AC03645': [configurations.light_brightness_colortemp_xy],
'AC03641': [configurations.light_brightness],
'FB56+ZSW05HG1.2': [configurations.switch],
'72922-A': [configurations.switch],
'AC03642': [configurations.light_brightness_colortemp],
'DNCKATSW002': [switchWithPostfix('left'), switchWithPostfix('right')],
'DNCKATSW003': [switchWithPostfix('left'), switchWithPostfix('right'), switchWithPostfix('center')],
'DNCKATSW004': [
switchWithPostfix('bottom_left'), switchWithPostfix('bottom_right'),
switchWithPostfix('top_left'), switchWithPostfix('top_right'),
],
'BY 165': [configurations.light_brightness],
'ZLED-2709': [configurations.light_brightness],
'8718696548738': [configurations.light_brightness_colortemp],
'4052899926110': [configurations.light_brightness_colortemp_xy],
'Z01-CIA19NAE26': [configurations.light_brightness],
'E11-N1EA': [configurations.light_brightness_colortemp_xy],
'74283': [configurations.light_brightness],
'JTQJ-BF-01LM/BW': [configurations.binary_sensor_gas],
'50045': [configurations.light_brightness],
'AV2010/22': [configurations.binary_sensor_occupancy],
'3210-L': [configurations.switch],
'7299355PH': [configurations.light_brightness_xy],
'45857GE': [configurations.light_brightness],
'A6121': [configurations.sensor_lock],
'433714': [configurations.light_brightness],
'3261030P7': [configurations.light_brightness_colortemp],
'DJT11LM': [configurations.sensor_action],
'E1603': [configurations.switch],
'7199960PH': [configurations.light_brightness_xy],
'74696': [configurations.light_brightness],
'AB35996': [configurations.light_brightness_colortemp_xy],
'AB401130055': [configurations.light_brightness_colortemp],
'74282': [configurations.light_brightness_colortemp],
'RS 128 T': [configurations.light_brightness_colortemp],
'53170161': [configurations.light_brightness_colortemp],
'4058075036147': [configurations.light_brightness_colortemp_xy],
'KS-SM001': [configurations.switch],
'MG-AUWS01': [switchWithPostfix('left'), switchWithPostfix('right')],
'9290002579A': [configurations.light_brightness_colortemp_xy],
'4256251-RZHAC': [configurations.switch, configurations.sensor_power],
'STS-PRS-251': [configurations.binary_sensor_presence],
'4058075816794': [configurations.light_brightness_colortemp],
'4052899926158': [configurations.light_brightness],
'4058075036185': [configurations.light_brightness_colortemp_xy],
'50049': [configurations.light_brightness_xy],
'915005733701': [configurations.light_brightness_colortemp_xy],
'RB 285 C': [configurations.light_brightness_colortemp_xy],
'3216331P5': [configurations.light_brightness_colortemp],
};
// A map of all discoverd devices
@@ -304,7 +396,7 @@ function discover(deviceID, model, mqtt, force) {
mapping[model].forEach((config) => {
const topic = `${config.type}/${deviceID}/${config.object_id}/config`;
const payload = config.discovery_payload;
const payload = {...config.discovery_payload};
payload.state_topic = `${settings.get().mqtt.base_topic}/${friendlyName}`;
payload.availability_topic = `${settings.get().mqtt.base_topic}/bridge/state`;
+29 -4
View File
@@ -12,12 +12,29 @@ class MQTT {
const mqttSettings = settings.get().mqtt;
logger.info(`Connecting to MQTT server at ${mqttSettings.server}`);
const options = {};
const options = {
will: {
topic: this.prefixTopic('bridge/state'),
payload: 'offline',
retain: true,
},
};
if (mqttSettings.user && mqttSettings.password) {
options.username = mqttSettings.user;
options.password = mqttSettings.password;
}
if (mqttSettings.client_id) {
logger.debug(`Using MQTT client ID: '${mqttSettings.client_id}'`);
options.clientId = mqttSettings.client_id;
}
if (mqttSettings.hasOwnProperty('reject_unauthorized') && !mqttSettings.reject_unauthorized) {
logger.debug(`MQTT reject_unauthorized set false, ignoring certificate warnings.`);
options.rejectUnauthorized = false;
}
this.client = mqtt.connect(mqttSettings.server, options);
// Register callbacks.
@@ -53,7 +70,11 @@ class MQTT {
handleConnect() {
logger.info('Connected to MQTT server');
this.publish('bridge/state', 'online', {retain: true, qos: 0});
this.subscriptions.forEach((topic) => this.client.subscribe(topic));
this.subscriptions.forEach((topic) => this.subscribe(topic));
}
subscribe(topic) {
this.client.subscribe(topic);
}
handleMessage(topic, message) {
@@ -63,8 +84,7 @@ class MQTT {
}
publish(topic, payload, options, callback, baseTopic) {
baseTopic = baseTopic ? baseTopic : settings.get().mqtt.base_topic;
topic = `${baseTopic}/${topic}`;
topic = this.prefixTopic(topic, baseTopic);
if (!this.client || this.client.reconnecting) {
logger.error(`Not connected to MQTT server!`);
@@ -76,6 +96,11 @@ class MQTT {
this.client.publish(topic, payload, options, callback);
}
prefixTopic(topic, baseTopic) {
baseTopic = baseTopic ? baseTopic : settings.get().mqtt.base_topic;
return `${baseTopic}/${topic}`;
}
log(type, message) {
const payload = {type: type, message: message};
this.publish('bridge/log', JSON.stringify(payload), {retain: false});
+55
View File
@@ -0,0 +1,55 @@
const logger = require('./util/logger');
const data = require('./util/data');
const fs = require('fs');
const saveInterval = 1000 * 60 * 5; // 5 minutes
class State {
constructor() {
this.state = {};
this.file = data.joinPath('state.json');
this._load();
// Save the state on every interval
this.timer = setInterval(() => this.save(), saveInterval);
}
_load() {
if (fs.existsSync(this.file)) {
try {
this.state = JSON.parse(fs.readFileSync(this.file, 'utf8'));
logger.debug(`Loaded state from file ${this.file}`);
} catch (e) {
logger.debug(`Failed to load state from file ${this.file} (corrupt file?)`);
}
} else {
logger.debug(`Can't load state from file ${this.file} (doesn't exsist)`);
}
}
save() {
logger.debug(`Saving state to file ${this.file}`);
const json = JSON.stringify(this.state, null, 4);
fs.writeFileSync(this.file, json, 'utf8');
}
exists(ID) {
return this.state.hasOwnProperty(ID);
}
get(ID) {
return this.state[ID];
}
set(ID, state) {
this.state[ID] = state;
}
remove(ID) {
if (this.exists(ID)) {
delete this.state[ID];
}
}
}
module.exports = State;
+1
View File
@@ -11,4 +11,5 @@ if (process.env.ZIGBEE2MQTT_DATA) {
module.exports = {
joinPath: (file) => path.join(dataPath, file),
getPath: () => dataPath,
};
+42 -17
View File
@@ -1,37 +1,62 @@
const winston = require('winston');
const data = require('./data');
const moment = require('moment');
const settings = require('./settings');
const path = require('path');
const fs = require('fs');
const fx = require('mkdir-recursive');
const rimraf = require('rimraf');
let logLevel = '';
if (process.env.DEBUG) {
logLevel = 'debug';
} else if (settings.get().advanced && settings.get().advanced.log_level) {
logLevel = settings.get().advanced.log_level;
} else {
logLevel = winston.level.info;
}
// Determine the log level.
const level = settings.get().advanced.log_level;
// Directoy 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);
// Make sure that log directoy exsists
fx.mkdirSync(directory);
// Create logger
const logger = new (winston.Logger)({
transports: [
new (winston.transports.File)({
filename: data.joinPath('log.txt'),
filename: path.join(directory, 'log.txt'),
json: false,
level: logLevel,
maxFiles: 3,
level: level,
maxFiles: 3, // Keep last 3 files
maxsize: 10000000, // 10MB
timestamp: () => new Date().toLocaleString(),
}),
new (winston.transports.Console)({
timestamp: () => new Date().toLocaleString(),
formatter: function(options) {
return options.timestamp() + ' ' +
winston.config.colorize(options.level, options.level.toUpperCase()) + ' ' +
(options.message ? options.message : '') +
(options.meta && Object.keys(options.meta).length ? '\n\t'+ JSON.stringify(options.meta) : '' );
return winston.config.colorize(options.level, ' zigbee2mqtt:' + options.level.toLowerCase()) + ' ' +
options.timestamp() + ' ' + (options.message ? options.message : '') +
(options.meta && Object.keys(options.meta).length ? '\n\t'+ JSON.stringify(options.meta) : '' );
},
}),
],
});
logger.transports.console.level = logLevel;
logger.info(`Logging to directory: '${directory}'`);
logger.transports.console.level = level;
// Cleanup any old log directory.
if (settings.get().advanced.log_directory.includes('%TIMESTAMP%')) {
const rootDirectory = path.join(directory, '..');
let directories = fs.readdirSync(rootDirectory).map((d) => {
d = path.join(rootDirectory, d);
return {path: d, birth: fs.statSync(d).birthtimeMs};
});
directories.sort((a, b) => b.birth - a.birth);
directories = directories.slice(10, directories.length);
directories.forEach((dir) => {
logger.debug(`Removing old log directory '${dir.path}'`);
rimraf.sync(dir.path);
});
}
module.exports = logger;
+26 -1
View File
@@ -2,6 +2,17 @@ const yaml = require('js-yaml');
const fs = require('fs');
const data = require('./data');
const file = data.joinPath('configuration.yaml');
const objectAssignDeep = require(`object-assign-deep`);
const path = require('path');
const defaults = {
permit_join: false,
advanced: {
log_directory: path.join(data.getPath(), 'log', '%TIMESTAMP%'),
log_level: process.env.DEBUG ? 'debug' : 'info',
soft_reset_timeout: 0,
},
};
let settings = read();
@@ -44,11 +55,25 @@ function getIDByFriendlyName(friendlyName) {
);
}
function changeFriendlyName(old, new_) {
const ID = getIDByFriendlyName(old);
if (!ID) {
return false;
}
settings.devices[ID].friendly_name = new_;
writeRead();
return true;
}
module.exports = {
get: () => settings,
get: () => objectAssignDeep.noMutate(defaults, settings),
write: () => write(),
getDevice: (id) => settings.devices ? settings.devices[id] : null,
addDevice: (id) => addDevice(id),
removeDevice: (id) => removeDevice(id),
getIDByFriendlyName: (friendlyName) => getIDByFriendlyName(friendlyName),
changeFriendlyName: (old, new_) => changeFriendlyName(old, new_),
getIeeAddrByFriendlyName: (friendlyName) => getIDByFriendlyName(friendlyName),
};
+8
View File
@@ -0,0 +1,8 @@
// Xiaomi uses 4151 and 4447 (lumi.plug) as manufacturer ID.
const xiaomiManufacturerID = [4151, 4447];
module.exports = {
millisecondsToSeconds: (milliseconds) => milliseconds / 1000,
secondsToMilliseconds: (seconds) => seconds * 1000,
isXiaomiDevice: (device) => xiaomiManufacturerID.includes(device.manufId),
};
+68 -28
View File
@@ -3,6 +3,7 @@ const logger = require('./util/logger');
const settings = require('./util/settings');
const data = require('./util/data');
const zclPacket = require('zcl-packet');
const utils = require('./util/utils');
const advancedSettings = settings.get().advanced;
@@ -13,7 +14,7 @@ const shepherdSettings = {
},
dbPath: data.joinPath('database.db'),
sp: {
baudrate: advancedSettings && advancedSettings.baudrate ? advancedSettings.baudrate : 115200,
baudRate: advancedSettings && advancedSettings.baudrate ? advancedSettings.baudrate : 115200,
rtscts: advancedSettings && (typeof(advancedSettings.rtscts) === 'boolean') ? advancedSettings.rtscts : true,
},
};
@@ -21,13 +22,14 @@ const shepherdSettings = {
logger.debug(`Using zigbee-shepherd with settings: '${JSON.stringify(shepherdSettings)}'`);
class Zigbee {
constructor() {
constructor(onMessage) {
this.onMessage = onMessage;
this.handleReady = this.handleReady.bind(this);
this.handleMessage = this.handleMessage.bind(this);
this.handleError = this.handleError.bind(this);
}
start(onMessage, callback) {
start(callback) {
logger.info(`Starting zigbee-shepherd`);
this.shepherd = new ZShepherd(settings.get().serial.port, shepherdSettings);
@@ -47,13 +49,13 @@ class Zigbee {
);
callback(error);
} else {
logger.info('zigbee-shepherd started');
this._logStartupInfo();
callback(null);
}
});
}, 60 * 1000);
} else {
logger.info('zigbee-shepherd started');
this._logStartupInfo();
callback(null);
}
});
@@ -62,8 +64,12 @@ class Zigbee {
this.shepherd.on('ready', this.handleReady);
this.shepherd.on('ind', this.handleMessage);
this.shepherd.on('error', this.handleError);
}
this.onMessage = onMessage;
_logStartupInfo() {
logger.info('zigbee-shepherd started');
logger.info(`Coordinator firmware version: '${this.shepherd.info().firmware.revision}'`);
logger.debug(`zigbee-shepherd info: ${JSON.stringify(this.shepherd.info())}`);
}
softReset(callback) {
@@ -78,12 +84,11 @@ class Zigbee {
}
handleReady() {
// Set all Xiaomi devices (manufId === 4151) to be online, so shepherd won't try
// to query info from devices (which would fail because they go tosleep).
// Xiaomi lumi.plug has manufId === 4447 and can be in the sleep mode too
// Set all Xiaomi devices to be online, so shepherd won't try
// to query info from devices (which would fail because they go to sleep).
const devices = this.getAllClients();
devices.forEach((d) => {
if ((d.manufId === 4151) || (d.manufId === 4447)) {
if (utils.isXiaomiDevice(d)) {
const device = this.shepherd.find(d.ieeeAddr, 1);
if (device) {
device.getDevice().update({
@@ -94,6 +99,11 @@ class Zigbee {
}
});
// Check if we have to turn off the led
if (settings.get().serial.disable_led) {
this.shepherd.controller.request('UTIL', 'ledControl', {ledid: 3, mode: 0});
}
logger.info('zigbee-shepherd ready');
}
@@ -117,7 +127,7 @@ class Zigbee {
}
getAllClients() {
return this.shepherd.list().filter((device) => device.type !== 'Coordinator');
return this.getDevices().filter((device) => device.type !== 'Coordinator');
}
removeDevice(deviceID, callback) {
@@ -143,11 +153,16 @@ class Zigbee {
}
ping(deviceID) {
let friendlyName = 'unknown';
const device = this.shepherd._findDevByAddr(deviceID);
const ieeeAddr = device.ieeeAddr;
if (settings.getDevice(ieeeAddr)) {
friendlyName = settings.getDevice(ieeeAddr).friendly_name;
}
if (device) {
// Note: checkOnline has the callback argument but does not call callback
logger.debug(`Check online ${deviceID}`);
logger.debug(`Check online ${friendlyName} ${deviceID}`);
this.shepherd.controller.checkOnline(device);
}
}
@@ -158,34 +173,59 @@ class Zigbee {
}
}
getDevice(deviceID) {
return this.shepherd.list().find((d) => d.ieeeAddr === deviceID);
getDevices() {
return this.shepherd.list();
}
getDevice(ieeeAddr) {
return this.getDevices().find((d) => d.ieeeAddr === ieeeAddr);
}
getCoordinator() {
const device = this.shepherd.list().find((d) => d.type === 'Coordinator');
const device = this.getDevices().find((d) => d.type === 'Coordinator');
return this.shepherd.find(device.ieeeAddr, 1);
}
publish(deviceID, cid, cmd, zclData, ep, callback) {
const device = this._findDevice(deviceID, ep);
publish(ieeAddr, cid, cmd, cmdType, zclData, cfg, ep, callback) {
const device = this._findDevice(ieeAddr, ep);
if (!device) {
logger.error(`Zigbee cannot publish message to device because '${deviceID}' not known by zigbee-shepherd`);
logger.error(`Zigbee cannot publish message to device because '${ieeAddr}' not known by zigbee-shepherd`);
return;
}
logger.info(`Zigbee publish to '${deviceID}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} - ${ep}`);
device.functional(cid, cmd, zclData, callback);
logger.info(
`Zigbee publish to '${ieeAddr}', ${cid} - ${cmd} - ` +
`${JSON.stringify(zclData)} - ${JSON.stringify(cfg)} - ${ep}`
);
const callback_ = (error, rsp) => {
if (error) {
logger.error(
`Zigbee publish to '${ieeAddr}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} ` +
`- ${JSON.stringify(cfg)} - ${ep} ` +
`failed with error ${error}`);
}
callback(error, rsp);
// TODO: Send read reponse back to MQTT.
};
if (cmdType === 'functional') {
device.functional(cid, cmd, zclData, cfg, callback_);
} else if (cmdType === 'foundation') {
device.foundation(cid, cmd, zclData, cfg, callback_);
} else {
logger.error(`Unknown zigbee publish cmdType ${cmdType}`);
}
}
read(deviceID, cid, attr, ep, callback) {
const device = this._findDevice(deviceID, ep);
if (!device) {
logger.error(`Zigbee cannot read attribute from device because '${deviceID}' not known by zigbee-shepherd`);
return;
}
device.read(cid, attr, callback);
networkScan(callback) {
logger.info('Starting network scan...');
this.shepherd.lqiScan().then((result) => {
logger.info('Network scan completed');
callback(result);
});
}
registerOnAfIncomingMsg(ieeeAddr, ep) {
+3958 -2023
View File
File diff suppressed because it is too large Load Diff
+29 -13
View File
@@ -1,12 +1,15 @@
{
"name": "zigbee2mqtt",
"version": "0.1.0",
"version": "0.2.0",
"description": "Zigbee to MQTT bridge using zigbee-shepherd",
"main": "index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
},
"engines": {
"node": ">=8.11 10"
},
"keywords": [
"xiaomi",
"tradfri",
@@ -17,10 +20,12 @@
"cc2531"
],
"scripts": {
"start": "node index.js",
"coverage": "nyc --all report --reporter=text mocha --recursive --timeout=3000",
"docgen": "node support/docgen.js",
"verify-homeassistant-mapping": "node support/verify-homeassistant-mapping.js",
"eslint": "node_modules/.bin/eslint ."
"eslint": "node_modules/.bin/eslint .",
"start": "node index.js",
"test": "mocha --recursive",
"verify-homeassistant-mapping": "node support/verify-homeassistant-mapping.js"
},
"author": "Koen Kanters",
"license": "GPL-3.0",
@@ -29,18 +34,29 @@
},
"homepage": "https://github.com/Koenkk/zigbee2mqtt/wiki",
"dependencies": {
"object-assign-deep": "*",
"mqtt": "*",
"winston": "2.4.2",
"zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git",
"zigbee-shepherd-converters": "*",
"json2yaml": "*",
"zcl-packet": "git+https://github.com/Koenkk/zcl-packet.git",
"git-last-commit": "*",
"js-yaml": "*",
"git-last-commit": "*"
"json2yaml": "*",
"mkdir-recursive": "*",
"moment": "*",
"mqtt": "*",
"object-assign-deep": "*",
"queue": "*",
"rimraf": "*",
"winston": "2.4.2",
"zcl-packet": "git+https://github.com/Koenkk/zcl-packet.git#fbd8c936bbd4be0597ad3e934be0ca722b0128a6",
"zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git#c3fdef2c30267573aa5821a0f2c51f21a700eff2",
"zigbee-shepherd-converters": "5.0.1"
},
"devDependencies": {
"chai": "*",
"chai-spies": "*",
"eslint": "*",
"eslint-config-google": "*"
"eslint-config-google": "*",
"eslint-plugin-mocha": "*",
"mocha": "*",
"nyc": "*",
"proxyquire": "*",
"sinon": "*"
}
}
+3
View File
@@ -63,7 +63,10 @@ text += ' will_message:\n';
text += ` topic: 'hass/status'\n`;
text += ` payload: 'offline'\n`;
text += '```';
text += '\n\n'
text += 'Zigbee2mqtt is expecting Home Assistant to send it\'s birth/will messages to `hass/status`.'
text += 'Be sure to add this to your `configuration.yaml` if you want zigbee2mqtt to resend the cached values when Home Assistant restarts'
text += '\n\n'
text += 'To respond to button clicks (e.g. WXKG01LM) you can use the following Home Assistant configuration:\n'
+23
View File
@@ -0,0 +1,23 @@
/* global describe, it */
const chai = require('chai');
const proxyquire = require('proxyquire').noPreserveCache();
const data = () => proxyquire('../lib/util/data.js', {});
const path = require('path');
describe('Data', () => {
describe('Get path', () => {
it('Should return correct path', () => {
const expected = path.normalize(path.join(__dirname, '..', 'data'));
const actual = data().getPath();
chai.assert.strictEqual(actual, expected);
});
it('Should return correct path when ZIGBEE2MQTT_DATA set', () => {
const expected = path.join('var', 'zigbee2mqtt');
process.env.ZIGBEE2MQTT_DATA = expected;
const actual = data().getPath();
chai.assert.strictEqual(actual, expected);
});
});
});
+165
View File
@@ -0,0 +1,165 @@
/* global describe, it */
const chai = require('chai');
const sinon = require('sinon');
const DevicePublish = require('../lib/extension/devicePublish');
const settings = require('../lib/util/settings');
const mqtt = {
subscribe: (topic) => {},
};
describe('DevicePublish', () => {
let devicePublish;
beforeEach(() => {
devicePublish = new DevicePublish(null, mqtt, null, null);
});
describe('Parse topic', () => {
it('Should handle non-valid topics', () => {
const topic = 'zigbee2mqtt1/my_device_id/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed, null);
});
it('Should handle non-valid topics', () => {
const topic = 'zigbee2mqtt1/my_device_id/sett';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed, null);
});
it('Should handle non-valid topics', () => {
const topic = 'zigbee2mqtt/my_device_id/write';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed, null);
});
it('Should handle non-valid topics', () => {
const topic = 'zigbee2mqtt/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed, null);
});
it('Should handle non-valid topics', () => {
const topic = 'set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed, null);
});
it('Should parse set topic', () => {
const topic = 'zigbee2mqtt/my_device_id/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.deviceID, 'my_device_id');
chai.assert.strictEqual(parsed.postfix, null);
});
it('Should parse get topic', () => {
const topic = 'zigbee2mqtt/my_device_id2/get';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'get');
chai.assert.strictEqual(parsed.deviceID, 'my_device_id2');
chai.assert.strictEqual(parsed.postfix, null);
});
it('Should parse topic with when base topic has multiple slashes', () => {
const stub = sinon.stub(settings, 'get').callsFake(() => {
return {
mqtt: {
base_topic: 'zigbee2mqtt/at/my/home',
},
};
});
const topic = 'zigbee2mqtt/at/my/home/my_device_id2/get';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'get');
chai.assert.strictEqual(parsed.deviceID, 'my_device_id2');
chai.assert.strictEqual(parsed.postfix, null);
stub.restore();
});
it('Should parse topic with when deviceID has multiple slashes', () => {
const topic = 'zigbee2mqtt/floor0/basement/my_device_id2/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.deviceID, 'floor0/basement/my_device_id2');
chai.assert.strictEqual(parsed.postfix, null);
});
it('Should parse topic with when base and deviceID have multiple slashes', () => {
const stub = sinon.stub(settings, 'get').callsFake(() => {
return {
mqtt: {
base_topic: 'zigbee2mqtt/at/my/basement',
},
};
});
const topic = 'zigbee2mqtt/at/my/basement/floor0/basement/my_device_id2/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.deviceID, 'floor0/basement/my_device_id2');
chai.assert.strictEqual(parsed.postfix, null);
stub.restore();
});
it('Should parse set with ieeAddr topic', () => {
const topic = 'zigbee2mqtt/0x12345689/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.deviceID, '0x12345689');
chai.assert.strictEqual(parsed.postfix, null);
});
it('Should parse set with postfix topic', () => {
const topic = 'zigbee2mqtt/0x12345689/left/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.deviceID, '0x12345689');
chai.assert.strictEqual(parsed.postfix, 'left');
});
it('Should parse set with postfix topic', () => {
const topic = 'zigbee2mqtt/0x12345689/right/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.deviceID, '0x12345689');
chai.assert.strictEqual(parsed.postfix, 'right');
});
it('Should parse set with postfix topic', () => {
const topic = 'zigbee2mqtt/0x12345689/bottom_left/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.deviceID, '0x12345689');
chai.assert.strictEqual(parsed.postfix, 'bottom_left');
});
it('Shouldnt parse set with invalid postfix topic', () => {
const topic = 'zigbee2mqtt/0x12345689/invalid/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.deviceID, '0x12345689/invalid');
chai.assert.strictEqual(parsed.postfix, null);
});
it('Should parse set with and slashes in base and deviceID postfix topic', () => {
const stub = sinon.stub(settings, 'get').callsFake(() => {
return {
mqtt: {
base_topic: 'zigbee2mqtt/at/my/home',
},
};
});
const topic = 'zigbee2mqtt/at/my/home/my/device/in/basement/sensor/bottom_left/get';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.type, 'get');
chai.assert.strictEqual(parsed.deviceID, 'my/device/in/basement/sensor');
chai.assert.strictEqual(parsed.postfix, 'bottom_left');
stub.restore();
});
});
});
Executable
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash -e
echo "Stopping zigbee2mqtt..."
sudo systemctl stop zigbee2mqtt
echo "Creating backup of configuration..."
cp -R data data-backup
echo "Updating..."
git checkout HEAD -- npm-shrinkwrap.json
git pull
echo "Installing dependencies..."
rm -rf node_modules
npm install
echo "Restore configuration..."
cp -R data-backup/* data
rm -rf data-backup
echo "Starting zigbee2mqtt..."
sudo systemctl start zigbee2mqtt
echo "Done!"