From 77b697f5bb3466204d67eeb8b72411917106a5d6 Mon Sep 17 00:00:00 2001 From: tsightler Date: Thu, 24 Sep 2020 15:30:47 -0400 Subject: [PATCH] Update tuya-mqtt.js --- tuya-mqtt.js | 261 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 156 insertions(+), 105 deletions(-) diff --git a/tuya-mqtt.js b/tuya-mqtt.js index 714667d..c148050 100644 --- a/tuya-mqtt.js +++ b/tuya-mqtt.js @@ -35,17 +35,22 @@ function getCommandFromMessage(_message) { let command = _message if (command != "1" && command != "0" && isJsonString(command)) { - debug("command is JSON"); + debug("Received command is JSON"); command = JSON.parse(command); } else { - if (command.toLowerCase() != "toggle") { - // convert simple commands (on, off, 1, 0) to TuyAPI-Commands - const convertString = command.toLowerCase() == "on" || command == "1" || command == 1 ? true : false; - command = { - set: convertString - } - } else { - command = command.toLowerCase(); + switch(command.toLowerCase()) { + case "on": + case "off": + case "0": + case "1": + // convert simple commands (on, off, 1, 0) to TuyAPI-Commands + const convertString = command.toLowerCase() == "on" || command == "1" || command == 1 ? true : false; + command = { + set: convertString + } + break; + default: + command = command.toLowerCase(); } } return command; @@ -77,16 +82,29 @@ function guessDeviceType(device, dps) { if (typeof dps['1'] === "boolean" && dps['2'] >= 0 && dps['2'] <= 255) { // A "dimmer" is a switch/light with brightness control only device.options.type = "dimmer" + device.options.template = + { + "state": { "dpsKey": 1, "dpsType": "bool" }, + "brightness_state": { "dpsKey": 2, "dpsType": "int", "minVal": 0, "maxVal": 255 } + } } } else if (keys === 1) { if (typeof dps['1'] === "boolean") { // If it only has one value and it's a boolean, it's probably a switch/socket device.options.type = "switch" + device.options.template = + { + "state": { "dpsKey": 1, "dpsType": "bool" } + } } } if (!device.options.type) { device.options.type = "unknown" + device.options.template = + { + "state": { "dpsKey": 1, "dpsType": "bool" } + } } } @@ -95,31 +113,28 @@ function publishColorState(device, state) { } function publishDeviceTopics(device, dps) { - const baseTopic = CONFIG.topic + device.topicLevel - let state - let brightness_state - switch (device.options.type) { - case "switch": - case "unknown": - state = (dps['1']) ? 'ON' : 'OFF'; - topic = baseTopic+"/state" - debugTuya("MQTT state ("+device.options.type+"): "+topic+" -> ", state); + if (!device.options.template) { + debugTuya ("No device template found!") + return + } + const baseTopic = CONFIG.topic + device.topicLevel + "/" + for (let stateTopic in device.options.template) { + const template = device.options.template[stateTopic] + const topic = baseTopic + stateTopic + let state + switch (template.dpsType) { + case "bool": + state = (dps[template.dpsKey]) ? 'ON' : 'OFF'; + break; + case "int": + state = (dps[template.dpsKey]) + state = (state > template.minVal && state < template.maxVal) ? state.toString() : "" + break; + } + if (state) { + debugTuya("MQTT "+device.options.type+" "+topic+" -> ", state); publishMQTT(topic, state); - break; - case "dimmer": - if ('1' in dps) { - state = (dps['1']) ? 'ON' : 'OFF'; - topic = baseTopic+"/state" - debugTuya("MQTT state ("+device.options.type+"): "+topic+" -> ", state); - publishMQTT(topic, state); - } - if ('2' in dps) { - brightness_state = JSON.stringify(dps['2']); - topic = baseTopic+"/brightness_state" - debugTuya("MQTT brightness ("+device.options.type+"): "+topic+" -> ", brightness_state); - publishMQTT(topic, brightness_state); - } - break; + } } } @@ -166,7 +181,7 @@ function publishDPS(device, dps) { TuyaDevice.onAll('data', function (data) { try { if (typeof data.dps != "undefined") { - debugTuya('Data from device ' + this.tuyID + ' :', data); + debugTuya('Data from device Id ' + data.devId + ' ->', data.dps); publishDPS(this, data.dps); } } catch (e) { @@ -187,7 +202,7 @@ function sleep(sec) { } function initTuyaDevices(tuyaDevices) { - for (const tuyaDevice of tuyaDevices) { + for (let tuyaDevice of tuyaDevices) { let options = { id: tuyaDevice.id, key: tuyaDevice.key @@ -205,6 +220,100 @@ function initTuyaDevices(tuyaDevices) { } } +// Process MQTT commands for all command topics at device level +function processDeviceCommand(message, device, commandTopic) { + let command = getCommandFromMessage(message); + // If it's the color command topic handle it manually + if (commandTopic === "color_command") { + const color = message.toLowerCase(); + debugColor("Set color: ", color); + device.setColor(color).then((data) => { + debug("Set device color completed: ", data); + }); + } else if (commandTopic === "command" && (command === "toggle" || command === "schema" )) { + // Handle special commands "toggle" and "schema" to primary device command topic + debug("Received command: ", command); + switch(command) { + case "toggle": + device.switch(command).then((data) => { + debug("Set device status completed: ", data); + }); + break; + case "schema": + // Trigger device schema to update state + device.schema(command).then((data) => { + debug("Get schema status command complete."); + }); + break; + } + } else { + // Recevied command on device topic level, check for matching device template + // and process command accordingly + const stateTopic = commandTopic.replace("command", "state") + const template = device.options.template[stateTopic] + if (template) { + debug("Received device "+commandTopic.replace("_"," "), message); + const tuyaCommand = new Object() + tuyaCommand.dps = template.dpsKey + switch (template.dpsType) { + case "bool": + if (command === "true") { + tuyaCommand.set = true + } else if (command === "false") { + tuyaCommand.set = false + } else if (typeof command.set === "boolean") { + tuyaCommand.set = command.set + } else { + tuyaCommand.set = "!!!!!" + } + break; + case "int": + tuyaCommand.set = (command > template.minVal && command < template.maxVal ) ? parseInt(command) : "!!!!!" + break; + } + if (tuyaCommand.set === "!!!!!") { + debug("Received invalid value for ", commandTopic, ", value:", command) + } else { + device.set(tuyaCommand).then((data) => { + debug("Set device "+commandTopic.replace("_"," ")+": ", data); + }); + } + } else { + debug("Received unknown command topic for device: ", commandTopic) + } + } +} + +// Process raw Tuya JSON commands via DPS command topic +function processDpsCommand(message, device) { + if (isJsonString(message)) { + const command = getCommandFromMessage(message); + debug("Received command: ", command); + device.set(command).then((data) => { + debug("Set device status completed: ", data); + }); + } else { + debug("DPS command topic requires Tuya style JSON value") + } +} + +// Process text base Tuya command via DPS key command topics +function processDpsKeyCommand(message, device, dpsKey) { + if (isJsonString(message)) { + debug("Individual DPS command topics do not accept JSON values") + } else { + const dpsMessage = parseDpsMessage(message) + debug("Received command for DPS"+dpsKey+": ", message); + const command = { + dps: dpsKey, + set: dpsMessage + } + device.set(command).then((data) => { + debug("Set device status completed: ", data); + }); + } +} + // Main code function const main = async() => { let tuyaDevices @@ -272,91 +381,33 @@ const main = async() => { message = message.toString(); const splitTopic = topic.split("/"); const topicLength = splitTopic.length - const action = splitTopic[topicLength - 1]; + const commandTopic = splitTopic[topicLength - 1]; const options = { topicLevel: splitTopic[1] } // If it looks like a valid command topic try to process it - if (action.includes("command")) { + if (commandTopic.includes("command")) { debug("Receive settings", JSON.stringify({ topic: topic, - action: action, - message: message, - topicLevel: options.topicLevel + message: message })); // Uses device topic level to find matching device var device = new TuyaDevice(options); device.then(function (params) { - var device = params.device; - switch (action) { - case "command": - if (topicLength === 3) { - const command = getCommandFromMessage(message); - debug("Received command: ", command); - if (command == "toggle") { - device.switch(command).then((data) => { - debug("Set device status completed: ", data); - }); - } - if (command == "schema") { - // 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); - }); - } - } else if (topicLength === 4) { - if (isJsonString(message)) { - const command = getCommandFromMessage(message); - debug("Received command: ", command); - device.set(command).then((data) => { - debug("Set device status completed: ", data); - }); - } else { - debug("DPS command topic requires Tuya style JSON value") - } - } else if (topicLength === 5) { - if (isJsonString(message)) { - debug("Individual DPS command topics require string value") - } else { - const dpsMessage = parseDpsMessage(message) - debug("Received DPS "+splitTopic[topicLength-2]+" command: ", message); - const command = { - dps: splitTopic[topicLength-2], - set: dpsMessage - } - device.set(command).then((data) => { - debug("Set device status completed: ", data); - }); - } - } + let device = params.device; + switch (topicLength) { + case 3: + processDeviceCommand(message, device, commandTopic); break; - case "color": - const color = message.toLowerCase(); - debugColor("Set color: ", color); - device.setColor(color).then((data) => { - debug("Set device color completed: ", data); - }); + case 4: + processDpsCommand(message, device); break; - case "brightness_command": - if (message >= 0 && message <= 255) { - const brightness = { - dps: 2, - set: parseInt(message) - } - debug("Set brighness: ", message) - device.set(brightness).then((data) => { - debug("Set device brightness completed: ",data); - }); - } else { - debug("Received invalid brightness value: " + message) - } + case 5: + const dpsKey = splitTopic[topicLength-2] + processDpsKeyCommand(message, device, dpsKey); break; } }).catch((err) => {