diff --git a/package-lock.json b/package-lock.json index ead8f89..aed0386 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, "bl": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", @@ -37,6 +42,15 @@ "concat-map": "0.0.1" } }, + "buffer": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.1.0.tgz", + "integrity": "sha512-YkIRgwsZwJWTnyQrsBTWefizHh+8GYj3kbL1BTiAQ/9pwpino0G7B2gp5tx/FUBqUlvtxV85KNR3mwfAtv15Yw==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, "buffer-from": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", @@ -71,6 +85,19 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "requires": { + "color-name": "1.1.1" + } + }, + "color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=" + }, "commist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/commist/-/commist-1.0.0.tgz", @@ -102,9 +129,12 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "crc": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.5.0.tgz", - "integrity": "sha1-mLi6fUiWZbo5efWbITgTdBAaGWQ=" + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "requires": { + "buffer": "^5.1.0" + } }, "cron": { "version": "1.3.0", @@ -124,10 +154,15 @@ "which": "^1.2.9" } }, + "crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==" + }, "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", "requires": { "ms": "2.0.0" } @@ -171,9 +206,9 @@ } }, "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "find-up": { "version": "2.1.0", @@ -189,9 +224,9 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "get-caller-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", - "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" }, "get-stream": { "version": "3.0.0", @@ -248,6 +283,11 @@ "xtend": "^4.0.0" } }, + "ieee754": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", + "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -419,9 +459,9 @@ } }, "mqtt": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-2.18.1.tgz", - "integrity": "sha512-p+RIMFsNb5z65/dy5beKgTnycd3+N8gQ+E2Jnx+0g0OoRza/LCXtUp/vEb3mgWJdljTU+5n4Lc3h0ya994zmVg==", + "version": "2.18.3", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-2.18.3.tgz", + "integrity": "sha512-BXCUugFgA6FOWJGxhvUWtVLOdt6hYTmiMGPksEyKuuF1FQ0ji7UJBJ/0kVRMUtUWCAtPGnt4mZZZgJpzNLcuQg==", "requires": { "commist": "^1.0.0", "concat-stream": "^1.6.2", @@ -457,7 +497,7 @@ "node-forge": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", - "integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==" + "integrity": "sha1-bBUsNFzhHFL0ZcKr2VfoY5zWdN8=" }, "npm-run-path": { "version": "2.0.2", @@ -522,7 +562,7 @@ "p-timeout": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", - "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", + "integrity": "sha1-2N0ZeVldLcATnh/ka4tkbLPN8Dg=", "requires": { "p-finally": "^1.0.0" } @@ -730,7 +770,7 @@ } }, "tuyapi": { - "version": "github:codetheweb/tuyapi#bfc7c414621b7ad5956f51bae6301a40ce88f113", + "version": "github:codetheweb/tuyapi#9b31d7c74b2e97ef30069ca0d9faabd59ef932b7", "from": "github:codetheweb/tuyapi", "requires": { "crc": "^3.5.0", @@ -868,9 +908,9 @@ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" }, "yargs": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.0.0.tgz", - "integrity": "sha512-Rjp+lMYQOWtgqojx1dEWorjCofi1YN7AoFvYV7b1gx/7dAAeuI4kN5SZiEvr0ZmsZTOpDRcCqrpI10L31tFkBw==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "requires": { "cliui": "^4.0.0", "decamelize": "^1.1.1", diff --git a/package.json b/package.json index 17fde24..eb6c03b 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,11 @@ "author": "", "license": "ISC", "dependencies": { + "color-convert": "^1.9.2", "cron": "^1.3.0", - "mqtt": "^2.18.1", + "crypto": "^1.0.1", + "mqtt": "^2.18.3", "tuyapi": "github:codetheweb/tuyapi", - "yargs": "^11.0.0" + "yargs": "^11.1.0" } } diff --git a/tuya-color.js b/tuya-color.js new file mode 100644 index 0000000..5077666 --- /dev/null +++ b/tuya-color.js @@ -0,0 +1,237 @@ +const convert = require('color-convert'); + +function TuyaColorLight(tuya) { + this.tuya = tuya; + + this.colorMode = 'white'; + this.brightness = 100; // percentage value use _convertValToPercentage functions below. + + this.color = { + H: 130, + S: 100, + L: 50 + }; + this.color2 = { + H: 0, + S: 100, + L: 50 + }; + + this.hue = this.color.H; + this.saturation = this.color.S; + this.lightness = this.color.L; + + this.colorTemperature = 255; + this.colorTempMin = 153; + this.colorTempMax = 500; + + this.dps = {}; + + this.debug = false; +} + +TuyaColorLight.prototype._convertPercentageToVal = function (percentage) { + var tmp = Math.round(255 * (percentage / 100)); + this.tuyaDebug('Converted ' + percentage + ' to: ' + tmp); + return tmp; +}; + +TuyaColorLight.prototype._convertValToPercentage = function (val) { + var tmp = Math.round((val / 255) * 100); + this.tuyaDebug('Converted ' + val + ' to: ' + tmp); + return tmp; +}; + +TuyaColorLight.prototype._convertColorTemperature = function (val) { + var tmpRange = this.colorTempMax - this.colorTempMin; + var tmpCalc = Math.round((val / this.colorTempMax) * 100); + + this.tuyaDebug('HK colorTemp Value: ' + val); + this.tuyaDebug('HK colorTemp scale min : ' + this.colorTempMin); + this.tuyaDebug('HK colorTemp scale max : ' + this.colorTempMax); + this.tuyaDebug('HK colorTemp range (tmpRange): ' + tmpRange); + this.tuyaDebug('HK colorTemp % tmpCalc: ' + tmpCalc); + + var tuyaColorTemp = this._convertPercentageToVal(tmpCalc); + + this.tuyaDebug('HK tuyaColorTemp: ' + tuyaColorTemp); + + return tuyaColorTemp; + +}; + +TuyaColorLight.prototype._convertColorTemperatureToHK = function (val) { + + var tuyaColorTempPercent = this._convertValToPercentage(this.colorTemperature); + var tmpRange = this.colorTempMax - this.colorTempMin; + var tmpCalc = Math.round((tmpRange * (tuyaColorTempPercent / 100)) + this.colorTempMin); + var hkValue = Math.round(tmpCalc); + + this.tuyaDebug('Tuya color Temperature : ' + val); + this.tuyaDebug('Tuya color temp Percent of 255: ' + tuyaColorTempPercent + '%'); + + this.tuyaDebug('HK colorTemp scale min : ' + this.colorTempMin); + this.tuyaDebug('HK colorTemp scale max : ' + this.colorTempMax); + + this.tuyaDebug('HK Color Temp Range: ' + tmpRange); + this.tuyaDebug('HK range %: ' + tuyaColorTempPercent); + this.tuyaDebug('HK Value: ' + hkValue); + + return hkValue; + +}; + + +TuyaColorLight.prototype.tuyaDebug = function (args) { + if (this.debug === true) { + console.log(args); + } +}; + +TuyaColorLight.prototype._getAlphaHex = function (brightness) { + // for (var i = 1; i >= 0; i -= 0.01) { + var i = brightness / 100; + this.tuyaDebug('input brightness: ' + brightness + ' and i is ' + i); + var alpha = Math.round(i * 255); + var hex = (alpha + 0x10000).toString(16).substr(-2); + var perc = Math.round(i * 100); + + this.tuyaDebug('alpha percent: ' + perc + '% hex: ' + hex + ' alpha: ' + alpha); + return hex; +}; + +TuyaColorLight.prototype.setSaturation = function (value, callback) { + var colorMode = 'colour'; + var saturation = value; + var color = this.color; + color.S = value; + + this.color = color; + this.colorMode = colorMode; + this.saturation = saturation; + + this.tuyaDebug(' SET SATURATION: ' + value); +}; + +TuyaColorLight.prototype.setBrightness = function (value, callback) { + this.brightness = value; + var newValue = this._convertPercentageToVal(value); + this.tuyaDebug(this.debugPrefix + " BRIGHTNESS from UI: " + value + ' Converted from 100 to 255 scale: ' + newValue); +} + +TuyaColorLight.prototype.setHue = function (value, callback) { + this.tuyaDebug('SET HUE: ' + value); + this.tuyaDebug('Saturation Value: ' + this.color.S); + this.color.H = value; + + if (value === 0 && this.color.S === 0) { + this.colorMode = 'white'; + this.tuyaDebug('SET Color Mode: \'white\''); + } else { + this.colorMode = 'colour'; + this.tuyaDebug('SET Color Mode: \'colour\' -- dahhhhhh british spelling \'coulour\' really is annoying... why you gotta be special?'); + } + + + var returnVal = {}; + + returnVal.color = this.color; + returnVal.colorMode = this.colorMode; + returnVal.hue = this.color.H; + returnVal.saturation = this.saturation; +}; + +TuyaColorLight.prototype.setHSL = function (hue, saturation, brightness) { + this.setBrightness(brightness); + this.setSaturation(saturation); + this.setHue(hue); +} + +TuyaColorLight.prototype.setColor = function (hexColor) { + var color = convert.hex.hsl(hexColor); + this.tuyaDebug(color); + this.setHSL(color[0], color[1], color[2]); + return this.getDps(); +} + +TuyaColorLight.prototype.getDps = function () { + var color = this.color; + var color2 = this.color2; + + var lightness = Math.round(this.brightness / 2); + var brightness = this.brightness; + var apiBrightness = this._convertPercentageToVal(brightness); + var alphaBrightness = this._getAlphaHex(brightness); + + var hexColorOriginal1 = convert.hsl.hex(color.H, color.S, color.L); + var rgbColorOriginal1 = convert.hsl.rgb(color.H, color.S, color.L); + + var hexColorOriginal2 = convert.hsl.hex(0, 0, 50); + var rgbColorOriginal2 = convert.hsl.rgb(0, 0, 50); + + var hexColor1 = convert.hsl.hex(color.H, color.S, lightness); + var rgbColor1 = convert.hsl.rgb(color.H, color.S, lightness); + + var hexColor2 = convert.hsl.hex(0, 0, lightness); + var rgbColor2 = convert.hsl.rgb(0, 0, lightness); + + var colorTemperature = this.colorTemperature; + + // var ww = Math.round((this.brightness * 255) / 100); + + var lightColor = (hexColor1 + hexColor2 + alphaBrightness).toLowerCase(); + + var temperature = (this.colorMode === 'colour') ? 255 : this._convertColorTemperature(colorTemperature); + + dpsTmp = { + '1': true, + '2': this.colorMode, + '3': apiBrightness, + '4': temperature, + '5': lightColor + // '6' : hexColor + hexColor + 'ff' + }; + this.tuyaDebug(dpsTmp); + return dpsTmp; +} + +module.exports = TuyaColorLight; +//module.exports.color.setColor('#ff2600'); +//module.exports.color.setColor('#ffffff'); + +// let tuya = new TuyaDevice({ +// id: '05200399bcddc2e02ec9', +// key: 'b58cf92e8bc5c899', +// ip: '192.168.178.49' +// }); + +// tuya.get().then(status => { +// console.log('Status:', status); +// var promisses = []; +// promisses.push( +// tuya.set({ +// dps: 2, +// set: "colour" +// }) +// ); +// promisses.push( +// tuya.set({ +// dps: 3, +// set: 0 +// }) +// ); +// promisses.push( +// tuya.set({ +// dps: 4, +// set: 255 +// }) +// ); +// Promise.all(promisses).then(result => { +// console.log('Result of setting status to ' + !status + ': ' + result); + +// tuya.get().then(status => { +// console.log('New status:', status); +// return; +// }); +// }); +// }); \ No newline at end of file diff --git a/tuya-connector.js b/tuya-connector.js index 40d9e52..de4dd01 100644 --- a/tuya-connector.js +++ b/tuya-connector.js @@ -2,7 +2,7 @@ const TuyaDevice = require('tuyapi'); const TuyaStatus = require('./tuya-status'); let tuya = undefined; -_DEBUG = false; +_DEBUG = true; exports.setDebug = function (debug) { _DEBUG = debug; @@ -59,6 +59,11 @@ exports.setStatus = function (newState, callback) { } } +exports.setColor = function (hexColor, callback) { + console.log("tuya-connector.setColor"); + TuyaStatus.setColor(hexColor, callback); +} + exports.getCurrent = function () { TuyaStatus.getCurrent(); } diff --git a/tuya-mqtt.js b/tuya-mqtt.js index 072f05a..b25479e 100644 --- a/tuya-mqtt.js +++ b/tuya-mqtt.js @@ -24,6 +24,7 @@ function bmap(istate) { client.on('connect', function () { var topic = 'tuya/#'; client.subscribe(topic); + console.log("MQTT Subscribed"); }) var knowDevice = function (tuyaID, tuyaKey, tuyaIP) { @@ -35,18 +36,19 @@ var knowDevice = function (tuyaID, tuyaKey, tuyaIP) { }); return isKnown; } -var addDevice = function (tuyaID, tuyaKey, tuyaIP) { +var addDevice = function (type, tuyaID, tuyaKey, tuyaIP) { var newDevice = { id: tuyaID, key: tuyaKey, - ip: tuyaIP + ip: tuyaIP, + type: type }; autoUpdate.push(newDevice); } -exports.publishStatus = function (tuyaID, tuyaKey, tuyaIP, status) { +exports.publishStatus = function (tuyaID, tuyaKey, tuyaIP, type, status) { if (tuyaID != undefined && tuyaKey != undefined && tuyaIP != undefined) { - var topic = "tuya/socket/" + tuyaID + "/" + tuyaKey + "/" + tuyaIP + "/state"; + var topic = "tuya/" + type + "/" + tuyaID + "/" + tuyaKey + "/" + tuyaIP + "/state"; client.publish(topic, status, { retain: true, qos: 2 @@ -54,38 +56,80 @@ exports.publishStatus = function (tuyaID, tuyaKey, tuyaIP, status) { } } -exports.setStatus = function (tuyaID, tuyaKey, tuyaIP, status) { +exports.setStatus = function (type, tuyaID, tuyaKey, tuyaIP, status) { if (tuyaID != undefined && tuyaKey != undefined && tuyaIP != undefined) { if (!knowDevice(tuyaID, tuyaKey, tuyaIP)) { - addDevice(tuyaID, tuyaKey, tuyaIP); + addDevice(type, tuyaID, tuyaKey, tuyaIP); } TuyaDevice.createDevice(tuyaID, tuyaKey, tuyaIP); if (TuyaDevice.hasDevice()) { TuyaDevice.setStatus(status, function (newStatus) { - module.exports.publishStatus(tuyaID, tuyaKey, tuyaIP, newStatus); + module.exports[type].publishStatus(tuyaID, tuyaKey, tuyaIP, newStatus); }); } } } -exports.getStatus = function (tuyaID, tuyaKey, tuyaIP) { +exports.getStatus = function (type, tuyaID, tuyaKey, tuyaIP) { if (tuyaID != undefined && tuyaKey != undefined && tuyaIP != undefined) { TuyaDevice.createDevice(tuyaID, tuyaKey, tuyaIP); if (TuyaDevice.hasDevice()) { TuyaDevice.getStatus(function (status) { - module.exports.publishStatus(tuyaID, tuyaKey, tuyaIP, bmap(status)); + module.exports[type].publishStatus(tuyaID, tuyaKey, tuyaIP, bmap(status)); }) } } } +exports.socket = {}; +exports.socket.publishStatus = function (tuyaID, tuyaKey, tuyaIP, status) { + return module.exports.publishStatus(tuyaID, tuyaKey, tuyaIP, "socket", status); +} + +exports.lightbulb = {}; +exports.lightbulb.publishStatus = function (tuyaID, tuyaKey, tuyaIP, status) { + return module.exports.publishStatus(tuyaID, tuyaKey, tuyaIP, "lightbulb", status); +} +exports.lightbulb.publishColor = function (tuyaID, tuyaKey, tuyaIP, color) { + if (tuyaID != undefined && tuyaKey != undefined && tuyaIP != undefined) { + var topic = "tuya/lightbulb/" + tuyaID + "/" + tuyaKey + "/" + tuyaIP + "/state/color"; + client.publish(topic, color, { + retain: true, + qos: 2 + }); + } +} +exports.lightbulb.setColor = function (tuyaID, tuyaKey, tuyaIP, color) { + if (tuyaID != undefined && tuyaKey != undefined && tuyaIP != undefined) { + if (!knowDevice(tuyaID, tuyaKey, tuyaIP)) { + //addDevice("lightbulb", tuyaID, tuyaKey, tuyaIP); + } + TuyaDevice.createDevice(tuyaID, tuyaKey, tuyaIP); + if (TuyaDevice.hasDevice()) { + console.log("tuya-mqtt.lightbulb.setColor: " + color); + TuyaDevice.setColor(color, function (newStatus) { + console.log(newStatus); + module.exports.lightbulb.publishColor(tuyaID, tuyaKey, tuyaIP, newStatus); + }); + } + } +} + client.on('message', function (topic, message) { try { var topic = topic.split("/"); var type = topic[1]; var exec = topic[5]; if (type == "socket" && exec == "command" && topic.length == 7) { - module.exports.setStatus(topic[2], topic[3], topic[4], topic[6]); + module.exports.setStatus(type, topic[2], topic[3], topic[4], topic[6]); + } + if (type == "lightbulb" && exec == "command" && topic.length == 7) { + module.exports.setStatus(type, topic[2], topic[3], topic[4], topic[6]); + } + if (type == "lightbulb" && exec == "color" && topic.length == 6) { + message = message.toString(); + message = message.toLowerCase(); + module.exports.lightbulb.setColor(topic[2], topic[3], topic[4], message); } } catch (e) { console.error(e); @@ -95,7 +139,7 @@ client.on('message', function (topic, message) { new CronJob('0 */1 * * * *', function () { try { autoUpdate.forEach(function (entry) { - module.exports.getStatus(entry.id, entry.key, entry.ip); + module.exports.getStatus(entry.type, entry.id, entry.key, entry.ip); }); } catch (e) { console.error(e); diff --git a/tuya-mqtt2.js b/tuya-mqtt2.js new file mode 100644 index 0000000..2391ad8 --- /dev/null +++ b/tuya-mqtt2.js @@ -0,0 +1,132 @@ +const mqtt = require('mqtt'); +const TuyaDevice = require('./tuyaapi-extended'); +const CronJob = require('cron').CronJob; +const crypto = require('crypto'); +const autoUpdate = {}; + +/** + * MQTT Settings + */ +var options = { + clientId: 'tuya_mqtt', + port: 1883, + keepalive: 60 +}; +const client = mqtt.connect({ + host: 'localhost', + port: options.port +}); + +function bmap(istate) { + return istate ? 'ON' : "OFF"; +} + +client.on('connect', function () { + var topic = 'tuya/#'; + client.subscribe(topic); + console.log("MQTT Subscribed"); + updateDeviceStatus(); +}) + +function createHash(tuyaID, tuyaKey, tuyaIP) { + return crypto.createHmac('sha256', "") + .update(tuyaID + tuyaKey + tuyaIP) + .digest('hex'); +} + +function isKnowDevice(tuyaID, tuyaKey, tuyaIP) { + var isKnown = false; + var searchKey = createHash(tuyaID, tuyaKey, tuyaIP); + if (autoUpdate[searchKey] != undefined) { + isKnown = true; + } + return isKnown; +} +function getKnownDevice(tuyaID, tuyaKey, tuyaIP) { + var searchKey = createHash(tuyaID, tuyaKey, tuyaIP); + return autoUpdate[searchKey]; +} +function addDevice(key, device) { + autoUpdate[key] = device; +} +function createDevice(tuyaID, tuyaKey, tuyaIP, tuyaType) { + if (tuyaID != undefined && tuyaKey != undefined) { + var tuya = undefined; + if (isKnowDevice(tuyaID, tuyaKey, tuyaIP)) { + tuya = getKnownDevice(tuyaID, tuyaKey, tuyaIP); + } else { + var key = createHash(tuyaID, tuyaKey, tuyaIP); + var tuya = new TuyaDevice({ + id: tuyaID, + key: tuyaKey, + ip: tuyaIP, + type: tuyaType + }); + addDevice(key, tuya); + } + return tuya; + } + return undefined; +}; + +client.on('message', function (topic, message) { + try { + var topic = topic.split("/"); + var type = topic[1]; + var exec = topic[5]; + if (type == "socket" && exec == "command" && topic.length == 7) { + var tuya = createDevice(topic[2], topic[3], topic[4], type); + tuya.onoff(topic[6], function (status) { + publishStatus(tuya, bmap(status)); + }); + } + if (type == "lightbulb" && exec == "command" && topic.length == 7) { + var tuya = createDevice(topic[2], topic[3], topic[4], type); + tuya.onoff(topic[6], function (status) { + publishStatus(tuya, bmap(status)); + }); + } + if (type == "lightbulb" && exec == "color" && topic.length == 6) { + message = message.toString(); + message = message.toLowerCase(); + var tuya = createDevice(topic[2], topic[3], topic[4]); + tuya.setColor(message); + } + } catch (e) { + console.error(e); + } +}); + + +function publishStatus(tuya, status) { + var device = tuya.getDevice(); + var type = device.type; + var tuyaID = device.id; + var tuyaKey = device.key; + var tuyaIP = device.ip; + + if (tuyaID != undefined && tuyaKey != undefined && tuyaIP != undefined) { + var topic = "tuya/" + type + "/" + tuyaID + "/" + tuyaKey + "/" + tuyaIP + "/state"; + client.publish(topic, status, { + retain: true, + qos: 2 + }); + } +} + +function updateDeviceStatus() { + try { + Object.keys(autoUpdate).forEach(function (k) { + var tuya = autoUpdate[k]; + tuya.getStatus(function (status) { + publishStatus(tuya, bmap(status)); + }) + }); + } catch (e) { + console.error(e); + } +} + +new CronJob('0 */10 * * * *', function () { + updateDeviceStatus(); +}, null, true, 'America/Los_Angeles'); \ No newline at end of file diff --git a/tuya-status.js b/tuya-status.js index 953efd9..ab3ed19 100644 --- a/tuya-status.js +++ b/tuya-status.js @@ -1,4 +1,5 @@ const TuyaDevice = require('tuyapi'); +const TuyaColor = require('./tuya-color'); let tuya = undefined; _DEBUG = true; @@ -8,10 +9,12 @@ function bmap(istate) { exports.setDebug = function (debug) { _DEBUG = debug; + TuyaColor.setDebug(debug); } exports.setDevice = function (newTuya) { tuya = newTuya; + TuyaColor.setDevice(tuya); } exports.hasDevice = function () { @@ -35,13 +38,11 @@ exports.get = function (callback) { } } -exports.set = function (newState, callback) { +exports.set = function (options, callback) { if (this.hasDevice()) { - tuya.set({ - set: newState - }).then(result => { + tuya.set(options).then(result => { if (_DEBUG) { - console.log('Result of setting status to ' + newState + ': ' + result); + console.log('Result of setting status to ' + options + ': ' + result); } tuya.get().then(status => { if (_DEBUG) { @@ -68,20 +69,31 @@ exports.getCurrent = function () { exports.toggle = function (callback) { var self = this; self.get(function (newStatus) { - self.set(!newStatus, callback); + self.set({ + set: !newState + }, callback); }) } +exports.setColor = function (hexColor, callback) { + var color = new TuyaColor.color(); + color.setColor(hexColor); +} + exports.on = function (callback) { var self = this; tuya.resolveId().then(() => { - self.set(true, callback); + self.set({ + set: true + }, callback); }); } exports.off = function (callback) { var self = this; tuya.resolveId().then(() => { - self.set(false, callback); + self.set({ + set: false + }, callback); }); } \ No newline at end of file diff --git a/tuyaapi-extended.js b/tuyaapi-extended.js new file mode 100644 index 0000000..22ec106 --- /dev/null +++ b/tuyaapi-extended.js @@ -0,0 +1,180 @@ +const TuyaDevice = require('tuyapi'); +const TuyaColor = require('./tuya-color'); +const debug = require('debug')('TuyAPI'); + +// Helpers +const Cipher = require('tuyapi/lib/cipher'); +const Parser = require('tuyapi/lib/message-parser') + +TuyaDevice.prototype.getDevice = function () { + return this.device; +} + +TuyaDevice.prototype.get = function (options) { + // Set empty object as default + options = options ? options : {}; + + const payload = { + gwId: this.device.id, + devId: this.device.id + }; + + debug('Payload: ', payload); + + // Create byte buffer + const buffer = Parser.encode({ + data: payload, + commandByte: '0a' + }); + + return new Promise((resolve, reject) => { + this._send(this.device.ip, buffer).then(data => { + var dps = data.dps; + if (options.schema === true) { + resolve(data); + } else if (options.dps) { + resolve(dps[options.dps]); + } else { + if (dps != undefined && dps["1"] != undefined) { + resolve(dps['1']); + } else { + resolve(dps); + } + } + }).catch(err => { + reject(err); + }); + }); +}; + +TuyaDevice.prototype.set = function (options) { + let dps = {}; + var count = Object.keys(options).length; + + if (options.dps != undefined || options.set != undefined) { + if (options.dps === undefined) { + dps = { + 1: options.set + }; + } else { + dps = { + [options.dps.toString()]: options.set + }; + } + } else { + dps = options; + } + + const now = new Date(); + const timeStamp = (parseInt(now.getTime() / 1000, 10)).toString(); + + const payload = { + devId: this.device.id, + uid: '', + t: timeStamp, + dps + }; + + debug('Payload:'); + debug(payload); + + // Encrypt data + const data = this.device.cipher.encrypt({ + data: JSON.stringify(payload) + }); + + // Create MD5 signature + const md5 = this.device.cipher.md5('data=' + data + + '||lpv=' + this.device.version + + '||' + this.device.key); + + // Create byte buffer from hex data + const thisData = Buffer.from(this.device.version + md5 + data); + const buffer = Parser.encode({ + data: thisData, + commandByte: '07' + }); + + // Send request to change status + return new Promise((resolve, reject) => { + this._send(this.device.ip, buffer).then(() => { + resolve(true); + }).catch(err => { + reject(err); + }); + }); +}; + +TuyaDevice.prototype.getStatus = function (callback) { + var tuya = this; + tuya.get().then(status => { + debug('Current Status: ' + status); + callback.call(this, status); + }); +} + +TuyaDevice.prototype.setStatus = function (options, callback) { + var tuya = this; + tuya.set(options).then(result => { + debug('Result of setting status to ' + options + ': ' + result); + tuya.get().then(status => { + debug('New status: ' + status); + if (callback != undefined) { + callback.call(this, status); + } else { + debug(status); + } + return; + }); + }); +} + +TuyaDevice.prototype.toggle = function (callback) { + var tuya = this; + tuya.get().then(status => { + tuya.setStatus({ + set: !status + }, callback); + }); +} + +TuyaDevice.prototype.onoff = function (newStatus, callback) { + newStatus = newStatus.toLowerCase(); + debug("onoff: " + newStatus); + if (newStatus == "on") { + this.on(callback); + } + if (newStatus == "off") { + this.off(callback); + } +} + +TuyaDevice.prototype.setColor = function (hexColor, callback) { + var tuya = this; + var color = new TuyaColor(tuya); + var dps = color.setColor(hexColor); + tuya.get().then(status => { + tuya.setStatus(dps, callback); + }); +} + +TuyaDevice.prototype.on = function (callback) { + var tuya = this; + tuya.get().then(status => { + tuya.setStatus({ + set: true + }, callback); + }); +} + +TuyaDevice.prototype.off = function (callback) { + debug("off: "); + var tuya = this; + tuya.get().then(status => { + tuya.setStatus({ + set: false + }, callback); + }); +} + +module.exports = TuyaDevice; \ No newline at end of file