diff --git a/CHANGELOG.md b/CHANGELOG.md index d09a957..fa2622a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [Unreleased] +## [2.1.0] +### Added +- Added ability to update validate communicaton with device and update state topic by issuing { "schema": true } command +- Added support for protocol 3.3 either via automatic device discovery or manual specification when using IP address + +### Changed +- Can specify "discover" instead of IP address to automatically find device (only works if device on same IP subnet as system running this script). This mode will also automatically detect 3.1 and 3.3 protocol devices +- Can manually specific protocol via ver3.1/ver3.3 in topic line after tuya/ +- Bump Tuyapi version to v5.3.x +- Bump MQTT version to v4.x.x +- Moved openHAB config to it's own document since many users use this with other tools +- Verious other fixes and cleanups ## [2.0.1] ### Added - Added capability to set multiple dps values over MQTT-Command -- - Custom Set-Function for TuyAPI-Class (added error handling for "index [1] not found" error) ### Changed diff --git a/README.md b/README.md index 47e298c..de70d25 100644 --- a/README.md +++ b/README.md @@ -1,266 +1,135 @@ -# tuyAPI-MQTT Client -MQTT interface for Tuya home automation devices sold under various names. -This is a wrapper script for the Project codetheweb/tuyapi. https://github.com/codetheweb/tuyapi - -This project provides an MQTT client for communication with the home automation devices. - -:exclamation: There is a greate Step-By-Step guide from user HolgiHab at openhab community ([Step-By-Step Guide]( -https://community.openhab.org/t/step-by-step-guide-for-adding-tuya-bulbs-smart-life-to-oh2-using-tuya-mqtt-js-by-agentk/59371)). This guide is not only for light bulbs, but also applies to sockets. :exclamation: - -## Instructions: - -Download this project to your openhab2-script-folder "/etc/openhab2/scripts" and install tuyapi from the same folder that the tuya-mqtt.js is in -``` -cd /etc/openhab2/scripts - -// clone this project -git clone git@github.com:TheAgentK/tuya-mqtt.git - -// change directory to the project directory -cd tuya-mqtt - -//installs this project along with codetheweb/tuyapi project -npm install -``` - -See the setup instructions found here: https://github.com/codetheweb/tuyapi/blob/master/docs/SETUP.md - - -## Basic Usage - -### Create your configuration file: -``` -cp config.json.sample config.json - -// edit the configuration file -nano config.json -``` - -### Start command -``` -node tuya-mqtt.js - -// For debugging purpose, to use DEBUG : https://www.npmjs.com/package/debug - -//on Linux machines at the bash command prompt: -DEBUG=* tuya-mqtt.js - - -// on Windows machines at the cmd.exe command prompt: -Set DEBUG=* tuya-mqtt.js -``` -URL to [DEBUG](https://www.npmjs.com/package/debug) - - - -### MQTT Topic's (send data) -``` -Change device state (by topic): - tuya////command/ - - Example: - - tuya////command/on - - tuya////command/off - - tuya////command/ON - - tuya////command/OFF - - tuya////command/1 - - tuya////command/0 - - tuya////command/toggle - - tuya////command/TOGGLE - - tuya////command/{ "dps": 1, "set": true } - - tuya////command/{ "multiple": true, "data": { "1": true, "7": true } } - -Change device state (by payload) -Use with OpenHAB 2.X MQTT bindings or others where only a single command topic is preferred: -NOTE: notice that nothing follows the word command, DO NOT but a "/" in after command. - - tuya////command - - Example: - "ON" - "OFF" - "on" - "off" - "1" - "0" - "toggle" - "TOGGLE" - "{ \"dps\": 1, \"set\": true }" - "{ \"multiple\": true, \"data\": { \"1\": true, \"7\": true } }" - -Change color of lightbulb (payload as HSB-Color) - tuya////color - - Example: - 64,0,100 -``` - -### MQTT Topic's (read data) -``` -Current device state (allways DPS[1]-Value): - tuya////state - -Device DPS-Values: - // returns JSON.stringify(dps) values, use with care, does not always contain all dps values - tuya////dps - - // return single dps data value - tuya////dps/ -``` - -## Issues -There are some reliability issues with tuyapi. Latest changes changed the syntax but still getting error maybe at an even higher rate. - -All questions regarding the tuyAPI please ask in the project https://github.com/codetheweb/tuyapi . - - -## Example items for OpenHAB 1.x Bindings (still works with OH > 2.4 but only if legacy 1.x MQTT bindings are enabled) -### simple switch on/off -``` - -Switch tuya_kitchen_coffeemachine_mqtt "Steckdose Kaffeemaschine" () ["Switchable"] { - mqtt="<[broker:tuya////state:state:default:.*], - >[broker:tuya////command/on:command:ON:true], - >[broker:tuya////command/off:command:OFF:false]" -} - -Switch tuya_livingroom_ledstrip_tv "LED Regal" () ["Lighting"] { - mqtt="<[broker:tuya/lightbulb////state:state:default:.*], - >[broker:tuya/lightbulb////command/on:command:ON:true], - >[broker:tuya/lightbulb////command/off:command:OFF:false]" -} - -``` - -### change color of lightbulb -``` - -# .items -Group gTuyaLivingColor "Tuya color group" -Color tuya_livingroom_colorpicker "Stehlampe farbe" (LivingDining) - -String tuya_livingroom_ledstrip_tv_color "Set color [%s]" (gTuyaLivingColor, LivingDining) { - mqtt=">[broker:tuya/lightbulb////color:command:*:default]" -} - - - -# .rules -import org.openhab.core.library.types.HSBType; - -rule "Set HSB value of item RGBLed to RGB color value" - when - Item tuya_livingroom_colorpicker received command - then - var appName = "Colorpicker.livingroom" - var color = receivedCommand.toString; - - // get all colors and send it via mqtt if light ist enabled - gTuyaLivingColor.members.forEach[ i | - var name = i.name; - var stateName = name.toString.split("_color").get(0); - var stateItem = gTuyaLights.allMembers.filter [ conf | conf.name.contains(stateName.toString) ].head; - - if(stateItem.state == ON){ - logInfo(appName, name + " change to color: " + color); - i.sendCommand(color); - Thread::sleep(400); - } - ] - end - -``` - -## Example items for OpenHAB 2.4 Bindings -### simple switch on/off - -With OpenHAB 2.X MQTT bindings you can add devices using a generic MQTT Thing via PaperUI or -configuration files. For PaperUI simply at the generic MQTT Thing and set the state and -command topics as follows: -``` - - tuya////state - - tuya////command - -``` - -If you prefer using configuration files vs PaperUI, it should look something like this: -See also OpenHAB 2.X MQTT binding [documentation](https://www.openhab.org/v2.4/addons/bindings/mqtt.generic/) - -``` - -Bridge mqtt:broker:myUnsecureBroker [ host="localhost", secure=false ] -{ - - Thing mqtt:topic:myCustomMQTT { - Channels: - Type switch : tuya_kitchen_coffeemachine_mqtt "Kitchen Coffee Machine MQTT Channel" [ - stateTopic="tuya////state", - commandTopic="tuya////command", - - // optional custom mqtt-payloads for ON and OFF - on="{ \"dps\": 1, \"set\": true }", - off="0" - ] - } - -} - -# *.item Example -Switch tuya_kitchen_coffeemachine_mqtt "Kitchen Coffee Machine Switch" (gKitchen, gTuya) ["Switchable"] { - channel="mqtt:topic:myMosquitto:tuya:coffeemachine" -} - -``` - -For one RGB bulb you would need a separate channel with the command topic set to -`tuya////color` and link that to your color item. - -``` - -Bridge mqtt:broker:myUnsecureBroker [ host="localhost", secure=false ] -{ - - Type colorHSB : livingroom_floorlamp_1_color "Livingroom floorlamp color MQTT Channel" [ - stateTopic="tuya/lightbulb/05200399bcddc2e02ec9/b58cf92e8bc5c899/192.168.178.49/state", - commandTopic="tuya/lightbulb/05200399bcddc2e02ec9/b58cf92e8bc5c899/192.168.178.49/color" - ] - -} - -# *.item Example -Color tuya_livingroom_colorpicker "Floorlamp colorpicker" (gLivingroom){ - channel="mqtt:topic:myMosquitto:tuya:livingroom_floorlamp_1_color" -} - -``` - -#### Basic UI sitemap -``` - -Switch item=tuya_kitchen_coffeemachine_mqtt - - -# Colorpicker for Lightbulbs -Colorpicker item=tuya_livingroom_colorpicker label="RGB lamp color" sendFrequency=30000 - -``` - -## Contributors -- [TheAgentK](https://github.com/TheAgentK) -- [tsightler](https://github.com/tsightler) -- [Tycale](https://github.com/Tycale) -- [crashdummymch](https://github.com/crashdummymch) -- [GadgetAngel](https://github.com/GadgetAngel) - - -## Related Projects: -- https://github.com/codetheweb/tuyapi -- https://github.com/unparagoned/njsTuya -- https://github.com/clach04/python-tuya -- https://github.com/Marcus-L/m4rcus.TuyaCore -- Specs: https://docs.tuya.com/en/cloudapi/cloud_access.html - -[![forthebadge](https://forthebadge.com/images/badges/made-with-javascript.svg)](https://forthebadge.com) -[![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg)](https://forthebadge.com) +# tuyAPI-MQTT Client +MQTT interface for Tuya home automation devices sold under various names. +This is a wrapper script for the Project codetheweb/tuyapi. https://github.com/codetheweb/tuyapi + +This project provides an MQTT gateway for locally controlling home automation devices made by Tuya Inc. To use this script you will need to obtain the device ID and local keys for each of your devices after they are configured via the Tuya/Smart Life or other Tuya compatible app (there are many). With this information it is possible to communicate locally with Tuya devices using protocol 3.1 and 3.3, without using the Tuya Cloud service, however, getting the keys requires signing up for a Tuya IOT developer account or using one of several other alternative methods (such as dumping the memory of a Tuya based app running on Andriod). Acquiring keys is not part of this project, please see the instructions at the TuyAPI project (on which this script is based) available at the TuyAPI project site: + +https://github.com/codetheweb/tuyapi/blob/master/docs/SETUP.md. + +## Instructions: +Download this project to your system into any directory (example below uses /opt/tuya-mqtt) and install tuyapi from the same folder that the tuya-mqtt.js is in +``` +// switch to opt directory +cd /opt + +// clone this project +git clone https://github.com/TheAgentK/tuya-mqtt + +// change directory to the project directory +cd tuya-mqtt + +//installs this project along with codetheweb/tuyapi project +npm install +``` + + +## Basic Usage +### Create your configuration file: +``` +cp config.json.sample config.json + +// edit the configuration file +nano config.json +``` + +### Start command +``` +node tuya-mqtt.js + +// For debugging purpose, to use DEBUG : https://www.npmjs.com/package/debug + +//on Linux machines at the bash command prompt, to turn ON DEBUG: +DEBUG=* tuya-mqtt.js + +//on Linux machines at the bash command prompt, to turn OFF DEBUG: +DEBUG=-* tuya-mqtt.js + +// on Windows machines at the cmd.exe command prompt, to turn ON DEBUG: +Set DEBUG=* & node c:/openhab2/userdata/etc/scripts/tuya-mqtt.js + +// on Windows machines at the cmd.exe command prompt, to turn OFF DEBUG: +Set DEBUG=-* & node c:/openhab2/userdata/etc/scripts/tuya-mqtt.js +``` + +### MQTT Topic's (send data) +**It's possible to replace the device IP address \ with the word "discover" to have the API attempt to automatically discover the device IP address. This allows support for 3.3 protocol devices transparently, without additional configuraiton, but does require the system running this script to be on the same IP subnet as the Tuya device since the discovery protocol relies on UDP broadcast packets from the devices.** +``` + tuya///discover/state + tuya///discover/command +``` +**If discovery will not work for your case you can still use the IP address, but, to use protocol 3.3 you must specify it in the topic explicitly** +``` + tuya/ver3.3//////command +``` +### Example command topic to set the device state: +``` + tuya////command +``` +### Example MQTT message payload for basic commands (default controls DPS[1] value, assumes true/false state control): +``` + "ON" + "OFF" + "on" + "off" + "1" + "0" + "toggle" + "TOGGLE" +``` +### Example MQTT message payload for advanced commands (set any DPS value): +``` + "{ \"dps\": 1, \"set\": true }" + "{ \"dps\": 7, \"set\": true }" + "{ \"multiple\": true, \"data\": { \"1\": true, \"7\": true } }" + "{ \"schema\": true }" + "{ \"multiple\": true, \"data\": { \"1\": true, \"2\": \"scene_4\" } }" + "{ \"multiple\": true, \"data\": { \"1\": true, \"2\": \"scene\", \"6\": \"c479000025ffc3\" } }" +``` +### Example command topic for color change of lightbulb +``` + tuya////color + + Example MQTT message payload: + 64,0,100 + 0,0,89 +``` + +### Example state topics (get device data) +### Get current device state (always DPS[1] value): + tuya////state + +### Get all available device DPS values +Returns JSON.stringify(dps) values, use with care, does not always contain all dps values +``` + tuya////dps +``` + +### Get any single DPS data value +``` + tuya////dps/ +``` + +## Issues +Not all Tuya protocols are supported. For example, some devices use protocol 3.2 which currently remains unsupported by the TuyAPI project due to lack of enough information to reverse engineer the protcol. If you are unable to control your devices with tuya-mqtt please verify that you can query and control them with tuya-cli first. If tuya-cli works, then this script should also work, if it doesn't then this script will not work either. + +## Integration with other Home Automation tools +openHAB examples are [here](docs/openHAB.md). + +## Contributors +- [TheAgentK](https://github.com/TheAgentK) +- [tsightler](https://github.com/tsightler) +- [Tycale](https://github.com/Tycale) +- [crashdummymch](https://github.com/crashdummymch) +- [GadgetAngel](https://github.com/GadgetAngel) + + +## Related Projects: +- https://github.com/codetheweb/tuyapi +- https://github.com/unparagoned/njsTuya +- https://github.com/clach04/python-tuya +- https://github.com/Marcus-L/m4rcus.TuyaCore +- Specs: https://docs.tuya.com/en/cloudapi/cloud_access.html + +[![forthebadge](https://forthebadge.com/images/badges/made-with-javascript.svg)](https://forthebadge.com) +[![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg)](https://forthebadge.com) diff --git a/docs/openHAB.md b/docs/openHAB.md new file mode 100644 index 0000000..2c0510f --- /dev/null +++ b/docs/openHAB.md @@ -0,0 +1,142 @@ +:exclamation: There is a greate Step-By-Step guide from user HolgiHab at openhab community ([Step-By-Step Guide]( +https://community.openhab.org/t/step-by-step-guide-for-adding-tuya-bulbs-smart-life-to-oh2-using-tuya-mqtt-js-by-agentk/59371)). This guide is not only for light bulbs, but also applies to sockets. :exclamation: + +## Example items for OpenHAB 1.x Bindings (still works with OH > 2.4 but only if legacy 1.x MQTT bindings are enabled) +### simple switch on/off +``` + +Switch tuya_kitchen_coffeemachine_mqtt "Steckdose Kaffeemaschine" () ["Switchable"] { + mqtt="<[broker:tuya////state:state:default:.*], + >[broker:tuya////command/on:command:ON:true], + >[broker:tuya////command/off:command:OFF:false]" +} + +Switch tuya_livingroom_ledstrip_tv "LED Regal" () ["Lighting"] { + mqtt="<[broker:tuya////state:state:default:.*], + >[broker:tuya////command/on:command:ON:true], + >[broker:tuya////command/off:command:OFF:false]" +} + +``` + +### change color of lightbulb +``` + +# .items +Group gTuyaLivingColor "Tuya color group" +Color tuya_livingroom_colorpicker "Stehlampe farbe" (LivingDining) + +String tuya_livingroom_ledstrip_tv_color "Set color [%s]" (gTuyaLivingColor, LivingDining) { + mqtt=">[broker:tuya////color:command:*:default]" +} + + + +# .rules +import org.openhab.core.library.types.HSBType; + +rule "Set HSB value of item RGBLed to RGB color value" + when + Item tuya_livingroom_colorpicker received command + then + var appName = "Colorpicker.livingroom" + var color = receivedCommand.toString; + + // get all colors and send it via mqtt if light ist enabled + gTuyaLivingColor.members.forEach[ i | + var name = i.name; + var stateName = name.toString.split("_color").get(0); + var stateItem = gTuyaLights.allMembers.filter [ conf | conf.name.contains(stateName.toString) ].head; + + if(stateItem.state == ON){ + logInfo(appName, name + " change to color: " + color); + i.sendCommand(color); + Thread::sleep(400); + } + ] + end + +``` + +## Example items for OpenHAB 2.4 Bindings +### simple switch on/off + +With OpenHAB 2.X MQTT bindings you can add devices using a generic MQTT Thing via PaperUI or +configuration files. For PaperUI simply at the generic MQTT Thing and set the state and +command topics as follows: +``` + + tuya////state + + tuya////command + +``` + +If you prefer using configuration files vs PaperUI, it should look something like this: +See also OpenHAB 2.X MQTT binding [documentation](https://www.openhab.org/v2.4/addons/bindings/mqtt.generic/) + +``` + +Bridge mqtt:broker:myUnsecureBroker [ host="localhost", secure=false ] +{ + + Thing mqtt:topic:myCustomMQTT { + Channels: + Type switch : tuya_kitchen_coffeemachine_mqtt_channel "Kitchen Coffee Machine MQTT Channel" [ + stateTopic="tuya////state", + commandTopic="tuya////command", + + // optional custom mqtt-payloads for ON and OFF + on="{ \"dps\": 1, \"set\": true }", + off="0" + ] + } + +} + +# *.item Example +Switch tuya_kitchen_coffeemachine_mqtt "Kitchen Coffee Machine Switch" (gKitchen, gTuya) ["Switchable"] { + channel="mqtt:topic:myUnsecureBroker:myCustomMQTT:tuya_kitchen_coffeemachine_mqtt_channel" +} + +``` + +For one RGB bulb you would need a separate channel with the command topic set to +`tuya////color` and link that to your color item. + +``` + +Bridge mqtt:broker:myUnsecureBroker [ host="localhost", secure=false ] +{ + Thing mqtt:topic:myCustomMQTT { + Channels: + Type colorHSB : livingroom_floorlamp_1_color "Livingroom floorlamp color MQTT Channel" [ + stateTopic="tuya/05200399bcddc2e02ec9/b58cf92e8bc5c899/192.168.178.49/state", + commandTopic="tuya/05200399bcddc2e02ec9/b58cf92e8bc5c899/192.168.178.49/color" + ] + } +} + +# *.item Example +Color tuya_livingroom_colorpicker "Floorlamp colorpicker" (gLivingroom){ + channel="mqtt:topic:myUnsecureBroker:myCustomMQTT:livingroom_floorlamp_1_color" +} + +``` + +#### Basic UI sitemap +``` + +Switch item=tuya_kitchen_coffeemachine_mqtt + +# turn the color bulb off or on +Switch item=tuya_livingroom_colorpicker label="RGB lamp [%s]" + +# pick the color level to send to the color bulb via MQTT color Channel +Slider item=tuya_livingroom_colorpicker label="RGB lamp level [%s]" minValue=0 maxValue=100 step=1 + +# color picked and sent via MQTT Color channel +Colorpicker item=tuya_livingroom_colorpicker label="RGB lamp color [%s]" icon="colorpicker" sendFrequency=30000 + + +``` diff --git a/package-lock.json b/package-lock.json index fb5398d..9a4d42f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "tuya-api", - "version": "1.0.0", + "name": "tuya-mqtt", + "version": "2.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -9,23 +9,36 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "brace-expansion": { @@ -37,6 +50,15 @@ "concat-map": "0.0.1" } }, + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -52,24 +74,24 @@ } }, "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "requires": { - "color-name": "1.1.3" + "color-name": "~1.1.4" } }, "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "commist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/commist/-/commist-1.0.0.tgz", - "integrity": "sha1-wMNSUBz29S6RJOPvicmAbiAi6+8=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", + "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==", "requires": { - "leven": "^1.0.0", + "leven": "^2.1.0", "minimist": "^1.1.0" } }, @@ -95,25 +117,26 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", "requires": { - "es5-ext": "^0.10.9" + "es5-ext": "^0.10.50", + "type": "^1.0.1" } }, "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "requires": { "ms": "^2.1.1" } }, "duplexify": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz", - "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", "requires": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", @@ -122,21 +145,21 @@ } }, "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "requires": { "once": "^1.4.0" } }, "es5-ext": { - "version": "0.10.46", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz", - "integrity": "sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw==", + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", "requires": { "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.1", - "next-tick": "1" + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" } }, "es6-iterator": { @@ -172,15 +195,26 @@ "es6-iterator": "~2.0.1", "es6-symbol": "3.1.1", "event-emitter": "~0.3.5" + }, + "dependencies": { + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + } } }, "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", "requires": { - "d": "1", - "es5-ext": "~0.10.14" + "d": "^1.0.1", + "ext": "^1.1.2" } }, "event-emitter": { @@ -192,6 +226,21 @@ "es5-ext": "~0.10.14" } }, + "ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "requires": { + "type": "^2.0.0" + }, + "dependencies": { + "type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", + "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==" + } + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -203,9 +252,9 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -252,6 +301,11 @@ "xtend": "^4.0.0" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -262,9 +316,9 @@ } }, "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "is-absolute": { "version": "1.0.0", @@ -319,23 +373,15 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, - "json-stable-stringify": { + "json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "requires": { - "jsonify": "~0.0.0" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" }, "leven": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/leven/-/leven-1.0.2.tgz", - "integrity": "sha1-kUS27ryl8dBoAWnxpncNzqYLdcM=" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=" }, "minimatch": { "version": "3.0.4", @@ -346,46 +392,47 @@ } }, "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "mqtt": { - "version": "2.18.8", - "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-2.18.8.tgz", - "integrity": "sha512-3h6oHlPY/yWwtC2J3geraYRtVVoRM6wdI+uchF4nvSSafXPZnaKqF8xnX+S22SU/FcgEAgockVIlOaAX3fkMpA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.2.1.tgz", + "integrity": "sha512-Iv893r+jWlo5GkNcPOfCGwW8M49IixwHiKLFFYTociEymSibUVCORVEjPXWPGzSxhn7BdlUeHicbRmWiv0Crkg==", "requires": { + "base64-js": "^1.3.0", "commist": "^1.0.0", "concat-stream": "^1.6.2", + "debug": "^4.1.1", "end-of-stream": "^1.4.1", "es6-map": "^0.1.5", "help-me": "^1.0.1", "inherits": "^2.0.3", - "minimist": "^1.2.0", - "mqtt-packet": "^5.6.0", + "minimist": "^1.2.5", + "mqtt-packet": "^6.3.2", "pump": "^3.0.0", "readable-stream": "^2.3.6", "reinterval": "^1.1.0", - "split2": "^2.1.1", - "websocket-stream": "^5.1.2", + "split2": "^3.1.0", + "ws": "^7.3.1", "xtend": "^4.0.1" } }, "mqtt-packet": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-5.6.0.tgz", - "integrity": "sha512-QECe2ivqcR1LRsPobRsjenEKAC3i1a5gmm+jNKJLrsiq9PaSQ18LlKFuxvhGxWkvGEPadWv6rKd31O4ICqS1Xw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.6.0.tgz", + "integrity": "sha512-LvghnKMFC70hKWMVykmhJarlO5e7lT3t9s9A2qPCUx+lazL3Mq55U+eCV0eLi7/nRRQYvEUWo/2tTo89EjnCJQ==", "requires": { - "bl": "^1.2.1", - "inherits": "^2.0.3", - "process-nextick-args": "^2.0.0", - "safe-buffer": "^5.1.0" + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "next-tick": { "version": "1.0.0", @@ -414,18 +461,18 @@ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, "p-retry": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.1.0.tgz", - "integrity": "sha512-oepllyG9gX1qH4Sm20YAKxg1GA7L7puhvGnTfimi31P07zSIj7SDV6YtuAx9nbJF51DES+2CIIRkXs8GKqWJxA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.2.0.tgz", + "integrity": "sha512-jPH38/MRh263KKcq0wBNOGFJbm+U6784RilTmHjB/HM9kH9V8WlCpVUcdOmip9cjXOh6MxZ5yk1z2SjDUJfWmA==", "requires": { "@types/retry": "^0.12.0", "retry": "^0.12.0" } }, "p-timeout": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.0.0.tgz", - "integrity": "sha512-HKUsVzU+2A+CcItUxgZ4Q1th5Hh2DHtSsh7gLTMkrL8Ki4Ss736nFp+yqb9M/ZKSKb5il0IXeLzBmUqD3k3mzQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", "requires": { "p-finally": "^1.0.0" } @@ -441,9 +488,9 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "pump": { "version": "3.0.0", @@ -476,9 +523,9 @@ } }, "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -510,17 +557,29 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "split2": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", - "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", "requires": { - "through2": "^2.0.2" + "readable-stream": "^3.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, "string_decoder": { "version": "1.1.1", @@ -531,18 +590,18 @@ } }, "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "requires": { - "readable-stream": "^2.1.5", + "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "through2-filter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", - "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", "requires": { "through2": "~2.0.0", "xtend": "~4.0.0" @@ -558,47 +617,37 @@ } }, "tuyapi": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tuyapi/-/tuyapi-4.0.4.tgz", - "integrity": "sha512-0gU6um3Imj3jHNm1cUuP1mXJdf0Z7H+kjhqlQfxuqUwwaB4hAmENatF6mmRhpF6NDzq181rXxU89hJpmw37Lmg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/tuyapi/-/tuyapi-5.3.1.tgz", + "integrity": "sha512-l0bbWxe4L8J7/bAQn0bJtBVbVDAEglC1T3a/YKYM3UvDXaKgFQUDVKhfQfHFAt0bzXVq1TeqU0zG4WIrxgiTHg==", "requires": { "debug": "4.1.1", - "p-retry": "4.1.0", - "p-timeout": "3.0.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - } + "p-retry": "4.2.0", + "p-timeout": "3.2.0" } }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" - }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" }, "unique-stream": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", - "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", "requires": { - "json-stable-stringify": "^1.0.0", - "through2-filter": "^2.0.0" + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" } }, "util-deprecate": { @@ -606,38 +655,20 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, - "websocket-stream": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/websocket-stream/-/websocket-stream-5.1.2.tgz", - "integrity": "sha512-lchLOk435iDWs0jNuL+hiU14i3ERSrMA0IKSiJh7z6X/i4XNsutBZrtqu2CPOZuA4G/zabiqVAos0vW+S7GEVw==", - "requires": { - "duplexify": "^3.5.1", - "inherits": "^2.0.1", - "readable-stream": "^2.3.3", - "safe-buffer": "^5.1.1", - "ws": "^3.2.0", - "xtend": "^4.0.0" - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", - "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0", - "ultron": "~1.1.0" - } + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==" }, "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" } } } diff --git a/package.json b/package.json index 75417cc..f889d3b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "tuya-mqtt", - "version": "2.0.1", - "description": "", + "version": "2.1.0", + "description": "Control Tuya devices locally via MQTT", "homepage": "https://github.com/TheAgentK/tuya-mqtt#readme", "main": "tuya-mqtt.js", "scripts": { @@ -9,17 +9,17 @@ }, "author": { "name": "TheAgentK", - "email": "lulatsch22@googlemail.com" + "email": "lulattsch22@googlemail.com" }, "license": "ISC", "dependencies": { - "color-convert": "^1.9.3", - "debug": "^3.2.6", - "mqtt": "^2.18.8", - "tuyapi": "^4.0.4" + "color-convert": "^2.0.1", + "debug": "^4.1.1", + "mqtt": "^4.2.1", + "tuyapi": "^5.3.1" }, "repository": { "type": "git", "url": "git://github.com/TheAgentK/tuya-mqtt.git" } -} \ No newline at end of file +} diff --git a/tuya-color.js b/tuya-color.js index d9d41f2..ea2374b 100644 --- a/tuya-color.js +++ b/tuya-color.js @@ -26,6 +26,58 @@ function TuyaColorLight() { this.dps = {}; } +/** + * calculate color value from given brightness percentage + * @param (Integer) percentage 0-100 percentage value + * @returns (Integer) color value from 25 - 255 + * @private + */ +TuyaColorLight.prototype._convertBrightnessPercentageToVal = function(brt_percentage){ + // the brightness scale does not start at 0 but starts at 25 - 255 + // this linear equation is a better fit to the conversion to 255 scale + var tmp = Math.round(2.3206*brt_percentage+22.56); + debug('Converted brightness percentage ' + brt_percentage + ' to: ' + tmp); + return tmp; +} + +/** + * calculate percentage from brightness color value + * @param brt_val 25 - 255 brightness color value + * @returns {Integer} 0 - 100 integer percent + * @private + */ +TuyaColorLight.prototype._convertValtoBrightnessPercentage = function(brt_val){ + var tmp = Math.round( (brt_val-22.56)/2.3206); + debug('Converted brightness value ' + brt_val + ' to: ' + tmp); + return tmp; +} + +/** + * calculate color value from given saturation percentage OR color temperature percentage + * @param (Integer) temp_percentage 0-100 percentage value + * @returns {Integer} saturation or color temperature value from 0 - 255 + * @private + */ +TuyaColorLight.prototype._convertSATorColorTempPercentageToVal = function(temp_percentage){ + // the saturation OR temperature scale does start at 0 - 255 + // this is a perfect linear equation fit for the saturation OR temperature scale conversion + var tmp = Math.round(((2.5498*temp_percentage)-0.4601)); + debug('Converted saturation OR temperature percentage ' + temp_percentage + ' to: ' + tmp); + return tmp; +} + +/** + * calculate percentage from saturation value OR color temperature value + * @param temp_val 0 - 255 saturation or color temperature value + * @returns {Integer} 0 - 100 integer percent + * @private + */ +TuyaColorLight.prototype._convertValtoSATorColorTempPercentage = function(temp_val){ + var tmp = Math.round( (temp_val+0.4601/2.5498)); + debug('Converted saturation OR temperature value ' + temp_val + ' to: ' + tmp); + return tmp; +} + /** * calculate color value from given percentage * @param {Integer} percentage 0-100 percentage value @@ -105,6 +157,18 @@ TuyaColorLight.prototype._ValIsHex = function (h) { return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(h) }; +/** + * get width Hex digits from given value + * @param (Integer) value, decimal value to convert to hex string + * @param (Integer) width, the number of hex digits to return + * @returns {string} value as HEX containing (width) number of hex digits + * @private + */ +TuyaColorLight.prototype._getHex = function (value,width){ + var hex = (value+Math.pow(16, width)).toString(16).slice(-width).toLowerCase(); + debug('value: ' + value + ' hex: ' + hex); + return hex; +} /** * get AlphaHex from percentage brightness * @param {Integer} brightness @@ -138,7 +202,8 @@ TuyaColorLight.prototype.setSaturation = function (value) { */ TuyaColorLight.prototype.setBrightness = function (value) { this.brightness = value; - var newValue = this._convertPercentageToVal(value); + //var newValue = this._convertPercentageToVal(value); + var newValue = this._convertBrightnessPercentageToVal(value); debug("BRIGHTNESS from UI: " + value + ' Converted from 100 to 255 scale: ' + newValue); } @@ -219,29 +284,65 @@ TuyaColorLight.prototype.getDps = function () { var lightness = Math.round(this.brightness / 2); var brightness = this.brightness; - var apiBrightness = this._convertPercentageToVal(brightness); - var alphaBrightness = this._getAlphaHex(brightness); + //var apiBrightness = this._convertPercentageToVal(brightness); + var apiBrightness = this._convertBrightnessPercentageToVal(brightness); + + //var alphaBrightness = this._getAlphaHex(brightness); + var alphaBrightness = this._getHex(apiBrightness,2); var hexColor1 = convert.hsl.hex(color.H, color.S, lightness); - var hexColor2 = convert.hsl.hex(0, 0, lightness); + //var hexColor2 = convert.hsl.hex(0, 0, lightness); + var hexColor2 = this._getHex(color.H,4); + hexColor2 = hexColor2 + this._getHex(this._convertSATorColorTempPercentageToVal(color.S),2); var colorTemperature = this.colorTemperature; var lightColor = (hexColor1 + hexColor2 + alphaBrightness).toLowerCase(); - var temperature = (this.colorMode === 'colour') ? 255 : this._convertColorTemperature(colorTemperature); + //var temperature = (this.colorMode === 'colour') ? 255 : this._convertColorTemperature(colorTemperature); + // color temperature percentage is at a fixed 51% + var temperature = this._convertSATorColorTempPercentageToVal(51); + + // if the bulb is in colour mode than the dps 3 and dps 4 are ignored by the bulb but if you set it now + // some tuya bulbs will ignore dps 5 because you set dps 3 or dps 4 + // FOR colour mode the bulb looks at dps 1, dps 2, and dps 5. + // DPS 5 is in the following format: + // HSL to HEX format are the leftmost hex digits (hex digits 14 - 9) + // hex digits 8 - 5 are the HSB/HSL Hue value in HEX format + // hex digits 4 - 3 are the HSB/HSL Saturation percentage as a value (converted to 0-255 scale) in HEX format + // hex digits 2 - 1 are the HSB Brightness percentage as a value (converted to 25-255 scale) in HEX format + + if (this.colorMode === 'colour') { + dpsTmp = { + '1': true, + '2': this.colorMode, + //'3': apiBrightness, + //'4': temperature, + '5': lightColor + // '6' : hexColor + hexColor + 'ff' + }; + debug("dps", dpsTmp); + return dpsTmp; + } - dpsTmp = { - '1': true, - '2': this.colorMode, - '3': apiBrightness, - '4': temperature, - '5': lightColor - // '6' : hexColor + hexColor + 'ff' - }; - debug("dps", dpsTmp); - return dpsTmp; + // if the bulb is in white mode then the dps 5 value is ignored by the bulb but if you set dps 5 value now + // you may not get a response back from the bulb on the dps values + // FOR white mode the bulb looks at dps 1, dps 2, dps 3 and dps 4 + // DPS 3 is the HSB/HSL Brightness percentage converted to a value from 25 to 255 in decimal format + // DPS 4 is the HSB/HSL Saturation percentage converted to a value from 0 to 255 in decimal format + if (this.colorMode === 'white'){ + dpsTmp = { + '1': true, + '2': this.colorMode, + '3': apiBrightness, + '4': temperature, + //'5': lightColor + // '6' : hexColor + hexColor + 'ff' + }; + debug("dps", dpsTmp); + return dpsTmp; + } } module.exports = TuyaColorLight; \ No newline at end of file diff --git a/tuya-device.js b/tuya-device.js index 66c21cd..c0ae76d 100644 --- a/tuya-device.js +++ b/tuya-device.js @@ -10,71 +10,10 @@ const debugColor = require('debug')('TuyAPI:device:color'); id: '03200240600194781244', key: 'b8bdebab418f5b55', ip: '192.168.178.45', - type: "socket" + type: "ver33" }); */ -// Helpers -const Parser = require('tuyapi/lib/message-parser'); - -/** - * Extends default TuyAPI-Class to add some more error handlers - */ -class CustomTuyAPI extends TuyAPI { - get(options) { - // Set empty object as default - options = options ? options : {}; - - const payload = { - gwId: this.device.gwID, - devId: this.device.id - }; - - debug('GET Payload:'); - debug(payload); - - // Create byte buffer - const buffer = Parser.encode({ - data: payload, - commandByte: 10 // 0x0a - }); - - // Send request and parse response - return new Promise((resolve, reject) => { - try { - // Send request - this._send(buffer).then(() => { - // Runs when data event is emitted - const resolveGet = data => { - // Remove self listener - this.removeListener('data', resolveGet); - - try { - if (options.schema === true) { - // Return whole response - resolve(data); - } else if (options.dps) { - // Return specific property - resolve(data.dps[options.dps]); - } else { - // Return first property by default - resolve(data.dps['1']); - } - } catch (error) { - reject(error); - } - }; - - // Add listener - this.on('data', resolveGet); - }); - } catch (error) { - reject(error); - } - }); - } -} - var TuyaDevice = (function () { var devices = []; var events = {}; @@ -125,7 +64,7 @@ var TuyaDevice = (function () { this.options = options; Object.defineProperty(this, 'device', { - value: new CustomTuyAPI(JSON.parse(JSON.stringify(this.options))) + value: new TuyAPI(JSON.parse(JSON.stringify(this.options))) }); this.device.on('data', data => { @@ -182,7 +121,11 @@ var TuyaDevice = (function () { } TuyaDevice.prototype.toString = function () { - return this.type + " (" + this.options.ip + ", " + this.options.id + ", " + this.options.key + ")"; + if (typeof this.type != "undefined") { + return this.type + " (" + this.options.ip + ", " + this.options.id + ", " + this.options.key + ")"; + } else { + return " (" + this.options.ip + ", " + this.options.id + ", " + this.options.key + ")"; + } } TuyaDevice.prototype.triggerAll = function (name, argument) { @@ -265,6 +208,12 @@ var TuyaDevice = (function () { }); } + TuyaDevice.prototype.schema = function(obj){ + return this.get(obj).then((status) => { + debug("get", obj); + }); + } + TuyaDevice.prototype.setColor = function (hexColor) { if (!this.connected) return; debugColor("Set color to: ", hexColor); @@ -318,4 +267,4 @@ var TuyaDevice = (function () { return TuyaDevice; }()); -module.exports = TuyaDevice; \ No newline at end of file +module.exports = TuyaDevice; diff --git a/tuya-mqtt.js b/tuya-mqtt.js index 9056b67..ca802c2 100644 --- a/tuya-mqtt.js +++ b/tuya-mqtt.js @@ -1,358 +1,375 @@ -const mqtt = require('mqtt'); -const TuyaDevice = require('./tuya-device'); -const debug = require('debug')('TuyAPI:mqtt'); -const debugColor = require('debug')('TuyAPI:mqtt:color'); -const debugTuya = require('debug')('TuyAPI:mqtt:device'); -const debugError = require('debug')('TuyAPI:mqtt:error'); -var cleanup = require('./cleanup').Cleanup(onExit); - -function bmap(istate) { - return istate ? 'ON' : "OFF"; -} - -function boolToString(istate) { - return istate ? 'true' : "false"; -} - -var connected = undefined; -var CONFIG = undefined; - -try { - CONFIG = require("./config"); -} catch (e) { - console.error("Configuration file not found") - debugError(e) - process.exit(1) -} - -if (typeof CONFIG.qos == "undefined") { - CONFIG.qos = 2; -} -if (typeof CONFIG.retain == "undefined") { - CONFIG.retain = false; -} - -const mqtt_client = mqtt.connect({ - host: CONFIG.host, - port: CONFIG.port, - username: CONFIG.mqtt_user, - password: CONFIG.mqtt_pass, -}); - -mqtt_client.on('connect', function (err) { - debug("Verbindung mit MQTT-Server hergestellt"); - connected = true; - var topic = CONFIG.topic + '#'; - mqtt_client.subscribe(topic, { - retain: CONFIG.retain, - qos: CONFIG.qos - }); -}); - -mqtt_client.on("reconnect", function (error) { - if (connected) { - debug("Verbindung mit MQTT-Server wurde unterbrochen. Erneuter Verbindungsversuch!"); - } else { - debug("Verbindung mit MQTT-Server konnte nicht herrgestellt werden."); - } - connected = false; -}); - -mqtt_client.on("error", function (error) { - debug("Verbindung mit MQTT-Server konnte nicht herrgestellt werden.", error); - connected = false; -}); - -/** - * execute function on topic message - */ - -function IsJsonString(text) { - if (/^[\],:{}\s]*$/.test(text.replace(/\\["\\\/bfnrtu]/g, '@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { - //the json is ok - return true; - } - return false; -} - -/** - * check mqtt-topic string for old notation with included device type - * @param {String} topic - */ -function checkTopicForOldNotation(_topic) { - var topic = _topic.split("/"); - var type = topic[1]; - var result = (type == "socket" || type == "lightbulb"); - return result; -} - -/** - * get action from mqtt-topic string - * @param {String} topic - * @returns {String} action type - */ -function getActionFromTopic(_topic) { - var topic = _topic.split("/"); - - if (checkTopicForOldNotation(_topic)) { - return topic[5]; - } else { - return topic[4]; - } -} - -/** - * get device informations from mqtt-topic string - * @param {String} topic - * @returns {String} object.id - * @returns {String} object.key - * @returns {String} object.ip - */ -function getDeviceFromTopic(_topic) { - var topic = _topic.split("/"); - - if (checkTopicForOldNotation(_topic)) { - return { - id: topic[2], - key: topic[3], - ip: topic[4], - type: topic[1] - }; - } else { - return { - id: topic[1], - key: topic[2], - ip: topic[3] - }; - } -} - -/** - * get command from mqtt - topic string - * converts simple commands to TuyAPI JSON commands - * @param {String} topic - * @returns {Object} - */ -function getCommandFromTopic(_topic, _message) { - var topic = _topic.split("/"); - var command = null; - - if (checkTopicForOldNotation(_topic)) { - command = topic[6]; - } else { - command = topic[5]; - } - - if (command == null) { - command = _message; - } - - if (command != "1" && command != "0" && IsJsonString(command)) { - debug("command is JSON"); - command = JSON.parse(command); - } else { - if (command.toLowerCase() != "toggle") { - // convert simple commands (on, off, 1, 0) to TuyAPI-Commands - var convertString = command.toLowerCase() == "on" || command == "1" || command == 1 ? true : false; - command = { - set: convertString - } - } else { - command = command.toLowerCase(); - } - } - - return command; -} - -mqtt_client.on('message', function (topic, message) { - try { - message = message.toString(); - var action = getActionFromTopic(topic); - var options = getDeviceFromTopic(topic); - - debug("receive settings", JSON.stringify({ - topic: topic, - action: action, - message: message, - options: options - })); - - var device = new TuyaDevice(options); - device.then(function (params) { - var device = params.device; - - switch (action) { - case "command": - var command = getCommandFromTopic(topic, message); - debug("receive command", command); - if (command == "toggle") { - device.switch(command).then((data) => { - debug("set device status completed", data); - }); - } else { - device.set(command).then((data) => { - debug("set device status completed", data); - }); - } - break; - case "color": - var color = message.toLowerCase(); - debugColor("set color: ", color); - device.setColor(color).then((data) => { - debug("set device color completed", data); - }); - break; - } - - }).catch((err) => { - debugError(err); - }); - } catch (e) { - debugError(e); - } -}); - -/** - * Publish current TuyaDevice state to MQTT-Topic - * @param {TuyaDevice} device - * @param {boolean} status - */ -function publishStatus(device, status) { - if (mqtt_client.connected == true) { - try { - var type = device.type; - var tuyaID = device.options.id; - var tuyaKey = device.options.key; - var tuyaIP = device.options.ip; - - if (typeof tuyaID != "undefined" && typeof tuyaKey != "undefined" && typeof tuyaIP != "undefined") { - var topic = CONFIG.topic; - if (typeof type != "undefined") { - topic += type + "/"; - } - topic += tuyaID + "/" + tuyaKey + "/" + tuyaIP + "/state"; - - mqtt_client.publish(topic, status, { - retain: CONFIG.retain, - qos: CONFIG.qos - }); - debugTuya("mqtt status updated to:" + topic + " -> " + status); - } else { - debugTuya("mqtt status not updated"); - } - } catch (e) { - debugError(e); - } - } -} - -function publishColorState(device, state) { - -} - -/** - * publish all dps-values to topic - * @param {TuyaDevice} device - * @param {Object} dps - */ -function publishDPS(device, dps) { - if (mqtt_client.connected == true) { - try { - var type = device.type; - var tuyaID = device.options.id; - var tuyaKey = device.options.key; - var tuyaIP = device.options.ip; - - if (typeof tuyaID != "undefined" && typeof tuyaKey != "undefined" && typeof tuyaIP != "undefined") { - var baseTopic = CONFIG.topic; - if (typeof type != "undefined") { - baseTopic += type + "/"; - } - baseTopic += tuyaID + "/" + tuyaKey + "/" + tuyaIP + "/dps"; - - var topic = baseTopic; - var data = JSON.stringify(dps); - debugTuya("mqtt dps updated to:" + topic + " -> ", data); - mqtt_client.publish(topic, data, { - retain: CONFIG.retain, - qos: CONFIG.qos - }); - - Object.keys(dps).forEach(function (key) { - var topic = baseTopic + "/" + key; - var data = JSON.stringify(dps[key]); - debugTuya("mqtt dps updated to:" + topic + " -> dps[" + key + "]", data); - mqtt_client.publish(topic, data, { - retain: CONFIG.retain, - qos: CONFIG.qos - }); - }); - } else { - debugTuya("mqtt dps not updated"); - } - } catch (e) { - debugError(e); - } - } -} - -/** - * event fires if TuyaDevice sends data - * @see TuyAPI (https://github.com/codetheweb/tuyapi) - */ -TuyaDevice.onAll('data', function (data) { - try { - if (typeof data.dps != "undefined") { - debugTuya('Data from device ' + this.type + ' :', data); - var status = data.dps['1']; - if (typeof status != "undefined") { - publishStatus(this, bmap(status)); - } - publishDPS(this, data.dps); - } - } catch (e) { - debugError(e); - } -}); - -/** - * MQTT connection tester - */ -function MQTT_Tester() { - this.interval = null; - - function mqttConnectionTest() { - if (mqtt_client.connected != connected) { - connected = mqtt_client.connected; - if (connected) { - debug('MQTT-Server verbunden.'); - } else { - debug('MQTT-Server nicht verbunden.'); - } - } - } - - this.destroy = function () { - clearInterval(this.interval); - this.interval = undefined; - } - - this.connect = function () { - this.interval = setInterval(mqttConnectionTest, 1500); - mqttConnectionTest(); - } - - var constructor = (function (that) { - that.connect.call(that); - })(this); -} -var tester = new MQTT_Tester(); - -/** - * Function call on script exit - */ -function onExit() { - TuyaDevice.disconnectAll(); - if (tester) tester.destroy(); -}; \ No newline at end of file +const mqtt = require('mqtt'); +const TuyaDevice = require('./tuya-device'); +const debug = require('debug')('TuyAPI:mqtt'); +const debugColor = require('debug')('TuyAPI:mqtt:color'); +const debugTuya = require('debug')('TuyAPI:mqtt:device'); +const debugError = require('debug')('TuyAPI:mqtt:error'); +var cleanup = require('./cleanup').Cleanup(onExit); + +var CONFIG = undefined; +var mqtt_client = undefined; + +function bmap(istate) { + return istate ? 'ON' : "OFF"; +} + +function boolToString(istate) { + return istate ? 'true' : "false"; +} + +/* + * execute function on topic message + */ + +function IsJsonString(text) { + if (/^[\],:{}\s]*$/.test(text.replace(/\\["\\\/bfnrtu]/g, '@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + //the json is ok + return true; + } + return false; +} + +/** + * check mqtt-topic string for old notation with included device type + * @param {String} topic + */ +function checkTopicNotation(_topic) { + var topic = _topic.split("/"); + var type = topic[1]; + var result = (type == "socket" || type == "lightbulb" || type == "ver3.1" || type == "ver3.3"); + return result; +} + +/** + * get action from mqtt-topic string + * @param {String} topic + * @returns {String} action type + */ +function getActionFromTopic(_topic) { + var topic = _topic.split("/"); + + if (checkTopicNotation(_topic)) { + return topic[5]; + } else { + return topic[4]; + } +} + +/** + * get device informations from mqtt-topic string + * @param {String} topic + * @returns {String} object.id + * @returns {String} object.key + * @returns {String} object.ip + */ +function getDeviceFromTopic(_topic) { + var topic = _topic.split("/"); + + if (checkTopicNotation(_topic)) { + // When there are 5 topic levels + // topic 2 is id, and topic 3 is key + var options = { + id: topic[2], + key: topic[3] + }; + + // 4th topic is IP address or "discover" keyword + if (topic[4] !== "discover") { + options.ip = topic[4] + // If IP is manually specified check if topic 1 + // is protocol version and set accordingly + if (topic[1] == "ver3.3") { + options.version = "3.3" + } else if (topic[1] == "ver3.1") { + options.version = "3.1" + } else { + // If topic is not version then it's device type + // Not used anymore but still supported for legacy setups + options.type = topic[1] + }; + }; + + return options; + } else { + // When there are 4 topic levels + // topic 1 is id, topic 2 is key + var options = { + id: topic[1], + key: topic[2] + }; + + // If topic 3 is not discover assume it is IP address + // Todo: Validate it is an IP address + if (topic[3] !== "discover") { + options.ip = topic[3] + }; + + return options; + } +} + +/** + * get command from mqtt - topic string + * converts simple commands to TuyAPI JSON commands + * @param {String} topic + * @returns {Object} + */ +function getCommandFromTopic(_topic, _message) { + var topic = _topic.split("/"); + var command = null; + + if (checkTopicNotation(_topic)) { + command = topic[6]; + } else { + command = topic[5]; + } + + if (command == null) { + command = _message; + } + + if (command != "1" && command != "0" && IsJsonString(command)) { + debug("command is JSON"); + command = JSON.parse(command); + } else { + if (command.toLowerCase() != "toggle") { + // convert simple commands (on, off, 1, 0) to TuyAPI-Commands + var convertString = command.toLowerCase() == "on" || command == "1" || command == 1 ? true : false; + command = { + set: convertString + } + } else { + command = command.toLowerCase(); + } + } + + return command; +} + +/** + * Publish current TuyaDevice state to MQTT-Topic + * @param {TuyaDevice} device + * @param {boolean} status + */ +function publishStatus(device, status) { + if (mqtt_client.connected == true) { + try { + var type = device.type; + var tuyaID = device.options.id; + var tuyaKey = device.options.key; + var tuyaIP = device.options.ip; + + if (typeof tuyaIP == "undefined") { + tuyaIP = "discover" + } + + if (typeof tuyaID != "undefined" && typeof tuyaKey != "undefined") { + var topic = CONFIG.topic; + if (typeof type != "undefined") { + topic += type + "/"; + } + topic += tuyaID + "/" + tuyaKey + "/" + tuyaIP + "/state"; + + mqtt_client.publish(topic, status, { + retain: CONFIG.retain, + qos: CONFIG.qos + }); + debugTuya("mqtt status updated to:" + topic + " -> " + status); + } else { + debugTuya("mqtt status not updated"); + } + } catch (e) { + debugError(e); + } + } +} + +function publishColorState(device, state) { + +} + +/** + * publish all dps-values to topic + * @param {TuyaDevice} device + * @param {Object} dps + */ +function publishDPS(device, dps) { + if (mqtt_client.connected == true) { + try { + var type = device.type; + var tuyaID = device.options.id; + var tuyaKey = device.options.key; + var tuyaIP = device.options.ip; + + if (typeof tuyaIP == "undefined") { + tuyaIP = "discover" + } + + if (typeof tuyaID != "undefined" && typeof tuyaKey != "undefined") { + var baseTopic = CONFIG.topic; + if (typeof type != "undefined") { + baseTopic += type + "/"; + } + baseTopic += tuyaID + "/" + tuyaKey + "/" + tuyaIP + "/dps"; + + var topic = baseTopic; + var data = JSON.stringify(dps); + debugTuya("mqtt dps updated to:" + topic + " -> ", data); + mqtt_client.publish(topic, data, { + retain: CONFIG.retain, + qos: CONFIG.qos + }); + + Object.keys(dps).forEach(function (key) { + var topic = baseTopic + "/" + key; + var data = JSON.stringify(dps[key]); + debugTuya("mqtt dps updated to:" + topic + " -> dps[" + key + "]", data); + mqtt_client.publish(topic, data, { + retain: CONFIG.retain, + qos: CONFIG.qos + }); + }); + } else { + debugTuya("mqtt dps not updated"); + } + } catch (e) { + debugError(e); + } + } +} + +/** + * event fires if TuyaDevice sends data + * @see TuyAPI (https://github.com/codetheweb/tuyapi) + */ +TuyaDevice.onAll('data', function (data) { + try { + if (typeof data.dps != "undefined") { + debugTuya('Data from device ' + this.tuyID + ' :', data); + var status = data.dps['1']; + if (typeof status != "undefined") { + publishStatus(this, bmap(status)); + } + publishDPS(this, data.dps); + } + } catch (e) { + debugError(e); + } +}); + +/** + * Function call on script exit + */ +function onExit() { + TuyaDevice.disconnectAll(); +}; + +// Simple sleep to pause in async functions +function sleep(sec) { + return new Promise(res => setTimeout(res, sec*1000)); +} + +// Main code function +const main = async() => { + + try { + CONFIG = require("./config"); + } catch (e) { + console.error("Configuration file not found") + debugError(e) + process.exit(1) + } + + if (typeof CONFIG.qos == "undefined") { + CONFIG.qos = 2; + } + if (typeof CONFIG.retain == "undefined") { + CONFIG.retain = false; + } + + mqtt_client = mqtt.connect({ + host: CONFIG.host, + port: CONFIG.port, + username: CONFIG.mqtt_user, + password: CONFIG.mqtt_pass, + }); + + mqtt_client.on('connect', function (err) { + debug("Connection established to MQTT server"); + var topic = CONFIG.topic + '#'; + mqtt_client.subscribe(topic, { + retain: CONFIG.retain, + qos: CONFIG.qos + }); + }); + + mqtt_client.on("reconnect", function (error) { + if (mqtt_client.connected) { + debug("Connection to MQTT server lost. Attempting to reconnect..."); + } else { + debug("Unable to connect to MQTT server"); + } + }); + + mqtt_client.on("error", function (error) { + debug("Unable to connect to MQTT server", error); + }); + + mqtt_client.on('message', function (topic, message) { + try { + message = message.toString(); + var action = getActionFromTopic(topic); + var options = getDeviceFromTopic(topic); + + debug("receive settings", JSON.stringify({ + topic: topic, + action: action, + message: message, + options: options + })); + + var device = new TuyaDevice(options); + + device.then(function (params) { + var device = params.device; + + switch (action) { + case "command": + var command = getCommandFromTopic(topic, message); + debug("Received command: ", command); + if (command == "toggle") { + device.switch(command).then((data) => { + debug("Set device status completed: ", data); + }); + } + if (command.schema === true) { + // Trigger device schema update to update state + device.schema(command).then((data) => { + }); + debug("Get schema status command complete"); + } else { + device.set(command).then((data) => { + debug("Set device status completed: ", data); + }); + } + break; + case "color": + var color = message.toLowerCase(); + debugColor("Set color: ", color); + device.setColor(color).then((data) => { + debug("Set device color completed: ", data); + }); + break; + } + + }).catch((err) => { + debugError(err); + }); + } catch (e) { + debugError(e); + } + }); +} + +// Call the main code +main() \ No newline at end of file