mirror of
https://github.com/lehanspb/tuya-mqtt.git
synced 2025-12-16 09:44:36 +00:00
Merge branch 'dev' into master
This commit is contained in:
62
README.md
62
README.md
@@ -52,14 +52,19 @@ node tuya-mqtt.js
|
||||
|
||||
// For debugging purpose, to use DEBUG : https://www.npmjs.com/package/debug
|
||||
|
||||
//on Linux machines at the bash command prompt:
|
||||
//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:
|
||||
Set 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
|
||||
```
|
||||
URL to [DEBUG](https://www.npmjs.com/package/debug)
|
||||
URL to install [DEBUG](https://www.npmjs.com/package/debug)
|
||||
|
||||
|
||||
|
||||
@@ -83,7 +88,12 @@ Change device state (by topic):
|
||||
- tuya/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/command/toggle
|
||||
- tuya/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/command/TOGGLE
|
||||
- tuya/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/command/{ "dps": 1, "set": true }
|
||||
- tuya/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/command/{ "dps": 7, "set": true }
|
||||
- tuya/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/command/{ "multiple": true, "data": { "1": true, "7": true } }
|
||||
- tuya/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/command/{ "schema": true }
|
||||
- tuya/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/command/{ "multiple": true, "data": { "1": true, "2": "scene_4" } }
|
||||
- tuya/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/command/{ "multiple": true, "data":
|
||||
{ "1": true, "2": "scene", "6": "c479000025ffc3" } }
|
||||
|
||||
Change device state (by payload)
|
||||
Use with OpenHAB 2.X MQTT bindings or others where only a single command topic is preferred:
|
||||
@@ -101,13 +111,18 @@ NOTE: notice that nothing follows the word command, DO NOT but a "/" in after co
|
||||
"toggle"
|
||||
"TOGGLE"
|
||||
"{ \"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\" } }"
|
||||
|
||||
Change color of lightbulb (payload as HSB-Color)
|
||||
tuya/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/color
|
||||
|
||||
Example:
|
||||
64,0,100
|
||||
0,0,89
|
||||
```
|
||||
|
||||
### MQTT Topic's (read data)
|
||||
@@ -140,9 +155,9 @@ Switch tuya_kitchen_coffeemachine_mqtt "Steckdose Kaffeemaschine" <socket> (<GRO
|
||||
}
|
||||
|
||||
Switch tuya_livingroom_ledstrip_tv "LED Regal" <lightbulb> (<GROUPS>) ["Lighting"] {
|
||||
mqtt="<[broker:tuya/lightbulb/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/state:state:default:.*],
|
||||
>[broker:tuya/lightbulb/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/command/on:command:ON:true],
|
||||
>[broker:tuya/lightbulb/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/command/off:command:OFF:false]"
|
||||
mqtt="<[broker:tuya/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/state:state:default:.*],
|
||||
>[broker:tuya/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/command/on:command:ON:true],
|
||||
>[broker:tuya/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/command/off:command:OFF:false]"
|
||||
}
|
||||
|
||||
```
|
||||
@@ -155,7 +170,7 @@ Group gTuyaLivingColor "Tuya color group" <lightbulb>
|
||||
Color tuya_livingroom_colorpicker "Stehlampe farbe" (LivingDining)
|
||||
|
||||
String tuya_livingroom_ledstrip_tv_color "Set color [%s]" (gTuyaLivingColor, LivingDining) {
|
||||
mqtt=">[broker:tuya/lightbulb/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/color:command:*:default]"
|
||||
mqtt=">[broker:tuya/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/color:command:*:default]"
|
||||
}
|
||||
|
||||
|
||||
@@ -210,12 +225,12 @@ Bridge mqtt:broker:myUnsecureBroker [ host="localhost", secure=false ]
|
||||
|
||||
Thing mqtt:topic:myCustomMQTT {
|
||||
Channels:
|
||||
Type switch : tuya_kitchen_coffeemachine_mqtt "Kitchen Coffee Machine MQTT Channel" [
|
||||
Type switch : tuya_kitchen_coffeemachine_mqtt_channel "Kitchen Coffee Machine MQTT Channel" [
|
||||
stateTopic="tuya/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/state",
|
||||
commandTopic="tuya/<tuyAPI-id>/<tuyAPI-key>/<tuyAPI-ip>/command",
|
||||
|
||||
// optional custom mqtt-payloads for ON and OFF
|
||||
on="{ \"dps": 1, \"set\": true },
|
||||
on="{ \"dps\": 1, \"set\": true }",
|
||||
off="0"
|
||||
]
|
||||
}
|
||||
@@ -224,7 +239,7 @@ Bridge mqtt:broker:myUnsecureBroker [ host="localhost", secure=false ]
|
||||
|
||||
# *.item Example
|
||||
Switch tuya_kitchen_coffeemachine_mqtt "Kitchen Coffee Machine Switch" <socket> (gKitchen, gTuya) ["Switchable"] {
|
||||
channel="mqtt:topic:myMosquitto:tuya:coffeemachine"
|
||||
channel="mqtt:topic:myUnsecureBroker:myCustomMQTT:tuya_kitchen_coffeemachine_mqtt_channel"
|
||||
}
|
||||
|
||||
```
|
||||
@@ -236,17 +251,18 @@ For one RGB bulb you would need a separate channel with the command topic set to
|
||||
|
||||
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"
|
||||
]
|
||||
|
||||
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:myMosquitto:tuya:livingroom_floorlamp_1_color"
|
||||
channel="mqtt:topic:myUnsecureBroker:myCustomMQTT:livingroom_floorlamp_1_color"
|
||||
}
|
||||
|
||||
```
|
||||
@@ -256,9 +272,15 @@ Color tuya_livingroom_colorpicker "Floorlamp colorpicker" (gLivingroom){
|
||||
|
||||
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
|
||||
|
||||
# Colorpicker for Lightbulbs
|
||||
Colorpicker item=tuya_livingroom_colorpicker label="RGB lamp color" sendFrequency=30000
|
||||
|
||||
```
|
||||
|
||||
|
||||
131
tuya-color.js
131
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);
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -208,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);
|
||||
|
||||
814
tuya-mqtt.js
814
tuya-mqtt.js
@@ -1,371 +1,443 @@
|
||||
'use strict'
|
||||
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 loop
|
||||
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("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);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Call the main code
|
||||
main()
|
||||
'use strict'
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
if (command.schema === true) {
|
||||
// this command is very useful. IT IS A COMMAND. It's place under the command topic.
|
||||
// It's the ONLY command that does not use device.set to get a result.
|
||||
// You have to use device.get and send the get method an exact JSON string of { schema: true }
|
||||
// This schema command does NOT
|
||||
// change the state of the device, all it does is query the device
|
||||
// as a confirmation that all communications are working properly.
|
||||
// Otherwise you have to physically change the state of the device just to
|
||||
// find out if you can talk to it. If this command returns no errors than
|
||||
// we know we are have an established communication channel. This is a native TuyAPI call that
|
||||
// the TuyAPI interface defines (its only available via the GET command.
|
||||
// this call returns a object of results
|
||||
device.schema(command).then((data) => {
|
||||
});
|
||||
debug("get (schema) device status completed");
|
||||
} 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 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 loop
|
||||
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("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);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Call the main code
|
||||
main()
|
||||
|
||||
/**
|
||||
* Function call on script exit
|
||||
*/
|
||||
function onExit() {
|
||||
TuyaDevice.disconnectAll();
|
||||
if (tester) tester.destroy();
|
||||
};
|
||||
Reference in New Issue
Block a user