Files
tuya-mqtt/tuya-mqtt.js
GadgetAngel 61433e74a9 This update allows for an essential TuyAPI command to be implemented via the tuya-mqtt.exe MQTT server. {"schema": true} is the ONLY COMMAND that the TuyAPI GET method implements.
{"schema": true} allows the user to establish that proper communications with the tuya device can occur WITHOUT actually changing the present STATE of the device.  This is the only command that will query the tuya device.

The current documentation says that you can query the tuya device over the "dps" TOPIC but from what I see the present state of the software does not force the tuya device to respond when using the "dps" TOPIC.

Since {"schema": true} is a command that forces a response from the tuya device, this command has been implemented under the "command" TOPIC. So "command" is the action and '{"schema": true}' becomes the command.  The response is returned just like all other commands.  I, use this command to guarantee communications has been established with the tuya device.  If this "schema" command fails, tuya-mqtt will indicate the result in the openhab.log file and then I, can find out what is physically wrong with the communications.  If this command fails the first time due to "socket" error and then goes through on the second attempt then I know that the error was due to TCP communications problem on initial startup.  This command helps as a work-a-round for the "ERROR: socket problem"
2019-04-20 09:29:01 -04:00

375 lines
11 KiB
JavaScript

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);
});
}
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 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();
};