node-red-contrib-tuya-local
Version:
Provides local control of tuya based devices
248 lines (214 loc) • 6.75 kB
JavaScript
const TuyaDev = require('tuyapi');
const {getHumanTimeStamp} = require('./lib/utils');
const formats = require('./formatvalues.js');
module.exports = function(RED) {
function TuyaNovostellaNode(config) {
RED.nodes.createNode(this,config);
var node = this;
var set_timeout = true
this.Name = config.devName;
this.Id = config.devId;
this.Key = config.devKey;
this.Ip = config.devIp;
this.version = config.protocolVer;
this.topicDelim = '/';
this.states = {
on: false,
mode: "n/a",
brightness: -1,
temperature: -1
};
//this.renameSchema = config.renameSchema;
//this.filterCB = config.filterCB;
const dev_info = {name:this.Name, ip:this.Ip, id:this.Id};
const device = new TuyaDev({
id: this.Id,
key: this.Key,
ip: this.Ip,
version: this.version});
function connectToDevice(timeout,req) {
device.find({'options': {'timeout':timeout}}).then( () => {
node.status({fill:"yellow",shape:"dot",text:"connecting"});
node.log(req);
device.connect().then( () => {
}, (reason) => {
node.status({fill:"red",shape:"ring",text:"failed: " + reason});
});
});
}
function disconnectDevice(deleted) {
set_timeout = deleted ? false : true;
device.disconnect();
}
function setDevice(req) {
if ( req == "request" ) {
device.get({"schema":true});
} else if ( req == "connect" ) {
// node.log('Connection requested by input');
connectToDevice(10,'Connection requested by input for device: ' + this.Name );
} else if ( req == "disconnect" ) {
node.log("Disconnection requested by input for device: " + this.Name)
device.disconnect();
} else if (req == "toggle") {
device.toggle();
} else if ( typeof req == "boolean" ) {
device.set({set: req}).then( () => {
node.status({fill:"green",shape:"dot",text: 'set success at:' + getHumanTimeStamp()});
}, (reason) => {
node.status({fill:"red",shape:"dot",text: 'set state failed:' + reason});
});
} else if ( "dps" in req ) {
console.log(req)
device.set(req);
} else if ( "multiple" in req) {
device.set({
multiple:true,
data: req.data
});
}
}
function parseDPS(dps) {
if (dps.hasOwnProperty('20')) {
node.states.on = dps[20];
}
if (dps.hasOwnProperty('21')) {
node.states.mode = dps[21];
}
if (dps.hasOwnProperty('22')) {
node.states.brightness = dps[22] / 10;
}
if (dps.hasOwnProperty('23')) {
node.states.temperature = dps[23];
}
}
connectToDevice(10,'Deploy connection request for device ' + this.Name);
setInterval(() => {
if (!device.isConnected()) {
connectToDevice(10, 'Re-connect');
}
}, 1 * 60 * 1000);
device.on('disconnected', () => {
this.status({fill:"red",shape:"ring",text:"disconnected from device"});
dev_info.available = false
msg = {data:dev_info}
node.send(msg);
//if (set_timeout) {
// timeout = setTimeout(connectToDevice, 10000, 10, 'set timeout for re-connect');
//}
});
device.on('connected', () => {
this.status({fill:"green",shape:"dot",text: this.Ip + " at " + getHumanTimeStamp()});
//try {
// clearTimeout(timeout)
//} catch(e) {
// node.log("No timeout defined for " + this.Name + ", probably NodeRED starting")
//}
});
device.on('error', error => {
this.status({fill:"red",shape:"ring",text:"error: " + error});
node.warn(error + " device: " + this.Name);
if (error.toString().includes("Error from socket")){
try {
node.log("error: Trying to clear a possible timeout timer for device " + this.Name )
clearTimeout(timeout)
} catch(e) {
node.log("error: No timeout defined, device " + this.Name + " is probably not powered")
}
}
});
device.on('data', (data,commandByte) => {
try {
if ("commandByte" !== null ) {
dev_info.available = true;
if (data.hasOwnProperty('dps')) {
parseDPS(data.dps);
if (node.states.on) {
node.status({fill:"green", shape:"dot", text:"ON"});
} else {
node.status({fill:"red", shape:"dot", text:"OFF"});
}
}
msg = {
data: dev_info,
commandByte: commandByte,
raw: data,
topic: "update",
payload: node.states
};
node.send(msg);
}
} catch (err) {
RED.log.error(err);
}
});
node.on('input', function(msg) {
let topicArr = String(msg.topic).split(node.topicDelim);
let topic = topicArr[topicArr.length - 1]; // get last part of topic
try {
if (topic.toUpperCase() === 'ON') {
if (!device.isConnected()) {
node.log("Device not connected (ON)");
return;
}
let v = formats.FormatValue(formats.Formats.BOOL, 'on', msg.payload);
let d = {
set: v,
dps: 20
};
//try {
device.set(d);
//} catch(err) {
// RED.log.error(err);
//}
if (v) {
node.status({fill:"green", shape:"dot", text:"ON"});
} else {
node.status({fill:"red", shape:"dot", text:"OFF"});
}
} else if (topic.toUpperCase() === 'WHITE') {
if (!device.isConnected()) {
node.log("Device not connected (WHITE)");
return;
}
let d = {
"20": true,
"21": "white",
"22": 1000, // brightness [10:1000]
"23": 0 // temperature [0:1000]
};
if (msg.payload.hasOwnProperty('brightness')) {
let v = formats.FormatValue(formats.Formats.INT, 'brightness', msg.payload.brightness);
d[22] = v * 10;
}
if (msg.payload.hasOwnProperty('temperature')) {
let v = formats.FormatValue(formats.Formats.INT, 'temperature', msg.payload.temperature);
d[23] = v;
}
device.set({
multiple: true,
data: d
});
node.status({fill:"green", shape:"dot", text:"ON"});
} else if (topic.toUpperCase() === 'COLOR') {
} else {
setDevice(msg.payload);
}
} catch (err) {
RED.log.error(err);
}
});
this.on('close', function(removed, done) {
if (removed) {
// This node has been deleted disconnect device and not set a timeout for reconnection
node.log("Node removal, gracefully disconnect device: " + this.Name);
device.isConnected() ? disconnectDevice(true) : node.log("Device " + this.Name + "not connected on removal");
} else {
// this node is being restarted, disconnect the device gracefully or connection will fail. Do not set a timeout
node.log("Node re-deploy, gracefully disconnect device: " + this.Name);
device.isConnected() ? disconnectDevice(true) : node.log("Device " + this.Name + "not connected on re-deploy");
}
done();
});
}
RED.nodes.registerType("tuya-novostella", TuyaNovostellaNode);
}