diff --git a/.gitignore b/.gitignore index a2070c6..6dbf473 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -devices/ +old/ test/ config.json diff --git a/devices/generic-device.js b/devices/generic-device.js new file mode 100644 index 0000000..9984b40 --- /dev/null +++ b/devices/generic-device.js @@ -0,0 +1,30 @@ +const TuyaDevice = require('./tuya-device') +const debug = require('debug')('tuya-mqtt:tuya') +const utils = require('../lib/utils') + +class GenericDevice extends TuyaDevice { + async init() { + this.deviceData.mdl = 'Generic Device' + + // Check if custom template in device config + if (this.config.hasOwnProperty('template')) { + // Map generic DPS topics to device specific topic names + this.deviceTopics = this.config.template + console.log(this.deviceTopics) + } else { + // Try to get schema to at least know what DPS keys to get initial update + const result = await this.device.get({"schema": true}) + if (!utils.isJsonString(result)) { + if (result === 'Schema for device not available') { + debug('Device id '+this.config.id+' failed schema discovery and no custom template defined') + debug('Cannot get initial DPS state data for device '+this.options.name+' but data updates will be publish') + } + } + } + + // Get initial states and start publishing topics + this.getStates() + } +} + +module.exports = GenericDevice \ No newline at end of file diff --git a/devices/rgbtw-light.js b/devices/rgbtw-light.js new file mode 100644 index 0000000..5f99179 --- /dev/null +++ b/devices/rgbtw-light.js @@ -0,0 +1,80 @@ +const TuyaDevice = require('./tuya-device') +const debug = require('debug')('tuya-mqtt:tuya') +const utils = require('../lib/utils') + +class RGBTWLight extends TuyaDevice { + async init() { + // Set device specific variables + this.config.dpsPower = this.config.dpsPower ? this.config.dpsPower : 1 + this.config.dpsMode = this.config.dpsMode ? this.config.dpsMode : 2 + this.config.dpsWhiteValue = this.config.dpsWhiteValue ? this.config.dpsWhiteValue : 3 + this.config.whiteValueScale = this.config.whiteValueScale ? this.config.whiteValueScale : 1000 + this.config.dpsColorTemp = this.config.dpsColorTemp ? this.config.dpsColorTemp : 4 + this.config.dpsColor = this.config.dpsColor ? this.config.dpsColor : 5 + this.config.colorType = this.config.colorType ? this.config.colorType : 'hsb' + + this.deviceData.mdl = 'RGBTW Light' + + // Map generic DPS topics to device specific topic names + this.deviceTopics = { + state: { + key: this.config.dpsPower, + type: 'bool' + }, + white_value_state: { + key: this.config.dpsWhiteValue, + type: 'int', + min: (this.config.whiteValueScale = 1000) ? 10 : 1, + max: this.config.whiteValueScale, + scale: this.config.whiteValueScale + }, + hs_state: { + key: this.config.dpsColor, + type: this.config.colorType, + components: 'h,s' + }, + brightness_state: { + key: this.config.dpsColor, + type: this.config.colorType, + components: 'b' + }, + mode_state: { + key: this.config.dpsMode, + type: 'str' + } + } + + // Send home assistant discovery data and give it a second before sending state updates + this.initDiscovery() + await utils.sleep(1) + + // Get initial states and start publishing topics + this.getStates() + } + + initDiscovery() { + const configTopic = 'homeassistant/light/'+this.config.id+'/config' + + const discoveryData = { + name: (this.config.name) ? this.config.name : this.config.id, + state_topic: this.baseTopic+'state', + command_topic: this.baseTopic+'command', + brightness_state_topic: this.baseTopic+'brightness_state', + brightness_command_topic: this.baseTopic+'brightness_command', + brightness_scale: 1000, + hs_state_topic: this.baseTopic+'hs_state', + hs_command_topic: this.baseTopic+'hs_command', + white_value_state_topic: this.baseTopic+'white_value_state', + white_value_command_topic: this.baseTopic+'white_value_command', + white_value_scale: 1000, + unique_id: this.config.id, + device: this.deviceData + } + + debug('Home Assistant config topic: '+configTopic) + debug(discoveryData) + this.publishMqtt(configTopic, JSON.stringify(discoveryData)) + } +} + +module.exports = RGBTWLight \ No newline at end of file diff --git a/devices/simple-dimmer.js b/devices/simple-dimmer.js new file mode 100644 index 0000000..d4d8448 --- /dev/null +++ b/devices/simple-dimmer.js @@ -0,0 +1,56 @@ +const TuyaDevice = require('./tuya-device') +const debug = require('debug')('tuya-mqtt:tuya') +const utils = require('../lib/utils') + +class SimpleDimmer extends TuyaDevice { + async init() { + // Set device specific variables + this.config.dpsPower = this.config.dpsPower ? this.config.dpsPower : 1 + this.config.dpsBrightness = this.config.dpsBrightness ? this.config.dpsBrightness : 2 + this.config.brightnessScale = this.config.brightnessScale ? this.config.brightnessScale : 255 + + this.deviceData.mdl = 'Dimmer Switch' + + // Map generic DPS topics to device specific topic names + this.deviceTopics = { + state: { + key: this.config.dpsPower, + type: 'bool' + }, + brightness_state: { + key: this.config.dpsBrightness, + type: 'int', + min: (this.config.brightnessScale = 1000) ? 10 : 1, + max: this.config.brightnessScale, + scale: this.config.brightnessScale + } + } + + // Send home assistant discovery data and give it a second before sending state updates + this.initDiscovery() + await utils.sleep(1) + + // Get initial states and start publishing topics + this.getStates() + } + + initDiscovery() { + const configTopic = 'homeassistant/light/'+this.config.id+'/config' + + const discoveryData = { + name: (this.config.name) ? this.config.name : this.config.id, + state_topic: this.baseTopic+'state', + command_topic: this.baseTopic+'command', + brightness_state_topic: this.baseTopic+'brightness_state', + brightness_command_topic: this.baseTopic+'brightness_command', + unique_id: this.config.id, + device: this.deviceData + } + + debug('Home Assistant config topic: '+configTopic) + debug(discoveryData) + this.publishMqtt(configTopic, JSON.stringify(discoveryData)) + } +} + +module.exports = SimpleDimmer \ No newline at end of file diff --git a/devices/simple-switch.js b/devices/simple-switch.js new file mode 100644 index 0000000..f6c92d4 --- /dev/null +++ b/devices/simple-switch.js @@ -0,0 +1,45 @@ +const TuyaDevice = require('./tuya-device') +const debug = require('debug')('tuya-mqtt:tuya') +const utils = require('../lib/utils') + +class SimpleSwitch extends TuyaDevice { + async init() { + // Set device specific variables + this.config.dpsPower = this.config.dpsPower ? this.config.dpsPower : 1 + + this.deviceData.mdl = 'Switch/Socket' + + // Map generic DPS topics to device specific topic names + this.deviceTopics = { + state: { + key: this.config.dpsPower, + type: 'bool' + } + } + + // Send home assistant discovery data and give it a second before sending state updates + this.initDiscovery() + await utils.sleep(1) + + // Get initial states and start publishing topics + this.getStates() + } + + initDiscovery() { + const configTopic = 'homeassistant/switch/'+this.config.id+'/config' + + const discoveryData = { + name: (this.config.name) ? this.config.name : this.config.id, + state_topic: this.baseTopic+'state', + command_topic: this.baseTopic+'command', + unique_id: this.config.id, + device: this.deviceData + } + + debug('Home Assistant config topic: '+configTopic) + debug(discoveryData) + this.publishMqtt(configTopic, JSON.stringify(discoveryData)) + } +} + +module.exports = SimpleSwitch \ No newline at end of file diff --git a/devices/tuya-device.js b/devices/tuya-device.js new file mode 100644 index 0000000..e23e0fd --- /dev/null +++ b/devices/tuya-device.js @@ -0,0 +1,420 @@ +const TuyAPI = require('tuyapi') +const utils = require('../lib/utils') +const debug = require('debug')('tuya-mqtt:tuya') +const debugMqtt = require('debug')('tuya-mqtt:mqtt') +const debugError = require('debug')('tuya-mqtt:error') + +class TuyaDevice { + constructor(deviceInfo) { + this.config = deviceInfo.configDevice + this.mqttClient = deviceInfo.mqttClient + this.topic = deviceInfo.topic + + // Build TuyAPI device options from device config info + this.options = { + id: this.config.id, + key: this.config.key + } + if (this.config.name) { this.options.name = this.config.name.toLowerCase().replace(/ /g,'_') } + if (this.config.ip) { + this.options.ip = this.config.ip + if (this.config.version) { + this.options.version = this.config.version + } else { + this.options.version = '3.1' + } + } + + // Set default device data for Home Assistant device registry + // Values may be overridden by individual devices + this.deviceData = { + ids: [ this.config.id ], + name: (this.config.name) ? this.config.name : this.config.id, + mf: 'Tuya' + } + + this.dps = {} // This will hold dps state data for device + this.prevDps = {} // This will hold previous dps value for device to avoid republish of non-changed states + + // Build the MQTT topic for this device (friendly name or device id) + if (this.options.name) { + this.baseTopic = this.topic + this.options.name + '/' + } else { + this.baseTopic = this.topic + this.options.id + '/' + } + + // Create the new Tuya Device + this.device = new TuyAPI(JSON.parse(JSON.stringify(this.options))) + + // Listen for device data and call update DPS function if valid + this.device.on('data', (data) => { + if (typeof data == 'string') { + debug('Data from device not encrypted:', data.replace(/[^a-zA-Z0-9 ]/g, '')) + } else { + if (!(data.dps['1'] === null && data.dps['2'] === null && data.dps['3'] === null && data.dps['101'] === null && data.dps['102'] === null && data.dps['103'] === null)) { + debug('Data from device '+this.options.id+' ->', data.dps) + this.updateDpsData(data) + } + } + }) + + // Find device on network + debug('Search for device id '+this.options.id) + this.device.find().then(() => { + debug('Found device id '+this.options.id) + // Attempt connection to device + this.device.connect() + }) + + // On connect perform device specific init + this.device.on('connected', () => { + debug('Connected to device ' + this.toString()) + this.init() + }) + + // On disconnect perform device specific disconnect + this.device.on('disconnected', () => { + this.connected = false + debug('Disconnected from device ' + this.toString()) + }) + + // On connect error call reconnect + this.device.on('error', (err) => { + if (err !== 'json obj data unvalid') { + debugError(err) + } + if (err.message === 'Error from socket') { + this.reconnect() + } + }) + } + + // Retry connection every 10 seconds if unable to connect + async reconnect() { + debug('Error connecting to device id '+this.options.id+'...retry in 10 seconds.') + await utils.sleep(10) + if (this.connected) { return } + debug('Search for device id '+this.options.id) + this.device.find().then(() => { + debug('Found device id '+this.options.id) + // Attempt connection to device + this.device.connect() + }) + } + + // Publish MQTT + publishMqtt(topic, message, isDebug) { + if (isDebug) { debugMqtt(topic, message) } + this.mqttClient.publish(topic, message, { qos: 1 }); + } + + // Publish device specific state topics + publishTopics() { + // Don't publish if device is not connected + if (!this.connected) return + + // Loop through and publish all device specific topics + for (let topic in this.deviceTopics) { + const state = this.getTopicState(topic) + this.publishMqtt(this.baseTopic + topic, state, true) + } + + // Publish Generic Dps Topics + this.publishDpsTopics() + } + + // Process MQTT commands for all command topics at device level + processDeviceCommand(message, commandTopic) { + // Determine state topic from command topic to find proper template + const stateTopic = commandTopic.replace('command', 'state') + const deviceTopic = this.deviceTopics.hasOwnProperty(stateTopic) ? this.deviceTopics[stateTopic] : '' + + if (deviceTopic) { + debug('Device '+this.options.id+' recieved command topic: '+commandTopic+', message: '+message) + const command = this.getCommandFromMessage(message) + let setResult = this.setState(command, deviceTopic) + if (!setResult) { + debug('Command topic '+this.baseTopic+commandTopic+' received invalid value: '+command) + } + } else { + debug('Invalid command topic '+this.baseTopic+commandTopic+' for device: '+this.config.name) + return + } + } + + // Get and update state of all dps properties for device + async getStates() { + // Suppress topic updates while syncing state + this.connected = false + for (let topic in this.deviceTopics) { + const key = this.deviceTopics[topic].key + const result = await this.device.get({"dps": key}) + } + this.connected = true + // Force topic update now that all states are fully in sync + this.publishTopics() + } + + // Update dps properties with device data updates + updateDpsData(data) { + try { + if (typeof data.dps != 'undefined') { + // Update device dps values + for (let key in data.dps) { + this.dps[key] = data.dps[key] + } + if (this.connected) { + this.publishTopics() + } + } + } catch (e) { + debugError(e); + } + } + + // Process MQTT commands for all command topics at device level + async processCommand(message, commandTopic) { + const command = this.getCommandFromMessage(message) + if (commandTopic === 'command' && command === 'get-states' ) { + // Handle "get-states" command to update device state + debug('Received command: ', command) + await this.getStates() + } else { + // Call device specific command topic handler + this.processDeviceCommand(message, commandTopic) + } + } + + // Publish all dps-values to topic + publishDpsTopics() { + try { + const dpsTopic = this.baseTopic + 'dps' + + // Publish DPS JSON data if not empty + if (Object.keys(this.dps).length) { + const data = JSON.stringify(this.dps) + const dpsStateTopic = dpsTopic + '/state' + debugMqtt('MQTT DPS JSON: ' + dpsStateTopic + ' -> ', data) + this.publishMqtt(dpsStateTopic, data, false) + } + + // Publish dps/<#>/state value for each device DPS + for (let key in this.dps) { + const dpsKeyTopic = dpsTopic + '/' + key + '/state' + const data = this.dps.hasOwnProperty(key) ? this.dps[key].toString() : 'None' + debugMqtt('MQTT DPS'+key+': '+dpsKeyTopic+' -> ', data) + this.publishMqtt(dpsKeyTopic, data, false) + } + } catch (e) { + debugError(e); + } + } + + getTopicState(topic) { + const deviceTopic = this.deviceTopics[topic] + const key = deviceTopic.key + let state = null + switch (deviceTopic.type) { + case 'bool': + state = this.dps[key] ? 'ON' : 'OFF' + break; + case 'int': + state = this.dps[key] ? this.dps[key].toString() : 'None' + break; + case 'hsb': + if (this.dps[key]) { + state = this.getColorState(this.dps[key], topic) + } + break; + case 'str': + state = this.dps[key] ? this.dps[key] : '' + } + return state + } + + // Set state based on command topic + setState(command, deviceTopic) { + const tuyaCommand = new Object() + tuyaCommand.dps = deviceTopic.key + switch (deviceTopic.type) { + case 'bool': + if (command === 'toggle') { + tuyaCommand.set = !this.dps[tuyaCommand.dps] + } else { + if (typeof command.set === 'boolean') { + tuyaCommand.set = command.set + } else { + tuyaCommand.set = '!!!INVALID!!!' + } + } + break; + case 'int': + if (isNaN(command)) { + tuyaCommand.set = '!!!INVALID!!!' + } else if (deviceTopic.hasOwnProperty('min') && deviceTopic.hasOwnProperty('max')) { + tuyaCommand.set = (command >= deviceTopic.min && command <= deviceTopic.max ) ? parseInt(command) : '!!!INVALID!!!' + } else { + tuyaCommand.set = parseInt(command) + } + break; + case 'hsb': + tuyaCommand.set = this.getColorCommand(command, deviceTopic) + this.setLightMode(deviceTopic) + break; + } + if (tuyaCommand.set === '!!!INVALID!!!') { + return false + } else { + if (this.config.dpsWhiteValue === deviceTopic.key) { + this.setLightMode(deviceTopic) + } + this.set(tuyaCommand) + return true + } + } + + // Converts message to TuyAPI JSON commands + getCommandFromMessage(_message) { + let command = _message + + if (command != '1' && command != '0' && utils.isJsonString(command)) { + debugMqtt('MQTT message is JSON'); + command = JSON.parse(command); + } else { + switch(command.toLowerCase()) { + case 'on': + case 'off': + case '0': + case '1': + case 'true': + case 'false': + // convert simple commands (on, off, 1, 0) to TuyAPI-Commands + const convertString = command.toLowerCase() === 'on' || command === '1' || command === 'true' || command === 1 ? true : false; + command = { + set: convertString + } + break; + default: + command = command.toLowerCase(); + } + } + return command; + } + + // Process Tuya JSON commands via DPS command topic + processDpsCommand(message) { + if (utils.isJsonString(message)) { + const tuyaCommand = this.getCommandFromMessage(message) + debugMqtt('Received command: '+tuyaCommand) + this.set(tuyaCommand) + } else { + debugError('DPS command topic requires Tuya style JSON value') + } + } + + // Process text base Tuya command via DPS key command topics + processDpsKeyCommand(message, dpsKey) { + if (utils.isJsonString(message)) { + debugError('Individual DPS command topics do not accept JSON values') + } else { + const dpsMessage = this.parseDpsMessage(message) + debugMqtt('Received command for DPS'+dpsKey+': ', message) + const tuyaCommand = { + dps: dpsKey, + set: dpsMessage + } + this.set(tuyaCommand) + } + } + + // Parse string message into boolean and number types + parseDpsMessage(message) { + if (typeof message === 'boolean' ) { + return message; + } else if (message === 'true' || message === 'false') { + return (message === 'true') ? true : false + } else if (!isNaN(message)) { + return Number(message) + } else { + return message + } + } + + // Simple function to help debug output + toString() { + return this.config.name+' (' +(this.options.ip ? this.options.ip+', ' : '')+this.options.id+', '+this.options.key+')' + } + + set(command) { + debug('Set device '+this.options.id+' -> '+command) + return new Promise((resolve, reject) => { + this.device.set(command).then((result) => { + debug(result) + resolve(result) + }) + }) + } + + // Takes the current Tuya color and splits it into component parts + // Returns decimal format comma delimeted string of components for selected topic + getColorState(value, topic) { + const [, h, s, b] = (value || '000003e803e8').match(/^([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})$/i) || [0, '0', '3e8', '3e8']; + const decimalColor = { + h: parseInt(h, 16), + s: Math.round(parseInt(s, 16) / 10), + b: parseInt(b, 16) + } + const color = new Array() + const components = this.deviceTopics[topic].components.split(',') + for (let i in components) { + if (decimalColor.hasOwnProperty([components[i]])) { + color.push(decimalColor[components[i]]) + } + } + return (color.join(',')) + } + + // Takes provided decimal HSB components from MQTT topic, combine with existing + // settings for unchanged values since brightness is sometimes sent separately + // Convert to Tuya hex format and return value + getColorCommand(value, topic) { + const [, h, s, b] = (this.dps[topic.key] || '000003e803e8').match(/^([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})$/i) || [0, '0', '3e8', '3e8']; + const decimalColor = { + h: parseInt(h, 16), + s: Math.round(parseInt(s, 16) / 10), + b: parseInt(b, 16) + } + const components = topic.components.split(',') + const values = value.split(',') + for (let i in components) { + decimalColor[components[i]] = Math.round(values[i]) + } + const hexColor = decimalColor.h.toString(16).padStart(4, '0') + (10 * decimalColor.s).toString(16).padStart(4, '0') + (decimalColor.b).toString(16).padStart(4, '0') + return hexColor + } + + // Set light mode based on received command + async setLightMode(topic) { + const currentMode = this.dps[this.config.dpsMode] + let targetMode + + if (this.config.dpsWhiteValue === topic.key) { + // If setting white level, switch to white mode + targetMode = 'white' + } else if (this.config.dpsColor === topic.key) { + // If setting an HSB value, switch to colour mode + targetMode = 'colour' + } + + // Set the correct light mode + if (targetMode && targetMode !== currentMode) { + const tuyaCommand = { + dps: this.config.dpsMode, + set: targetMode + } + await this.set(tuyaCommand) + } + } +} + +module.exports = TuyaDevice \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..0943f9f --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,24 @@ +class Utils +{ + + // Check if data is JSON or not + isJsonString(data) { + try { + const parsedData = JSON.parse(data) + if (parsedData && typeof parsedData === "object") { + return parsedData + } + } + catch (e) { } + + return false + } + + // Simple sleep function for various required delays + sleep(sec) { + return new Promise(res => setTimeout(res, sec*1000)) + } + +} + +module.exports = new Utils() \ No newline at end of file diff --git a/package.json b/package.json index 7e02573..de9f1d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tuya-mqtt", - "version": "3.0.0-beta1", + "version": "3.0.0-beta2", "description": "Control Tuya devices locally via MQTT", "homepage": "https://github.com/TheAgentK/tuya-mqtt#readme", "main": "tuya-mqtt.js", @@ -16,7 +16,7 @@ "color-convert": "^2.0.1", "debug": "^4.1.1", "mqtt": "^4.2.1", - "tuyapi": "^5.3.2", + "tuyapi": "github:tsightler/tuyAPI", "json5": "^2.1.3" }, "repository": { diff --git a/tuya-color.js b/tuya-color.js deleted file mode 100644 index ea2374b..0000000 --- a/tuya-color.js +++ /dev/null @@ -1,348 +0,0 @@ -const convert = require('color-convert'); -const debug = require('debug')('TuyaColor'); - -/** - * Class to calculate settings for Tuya colors - */ -function TuyaColorLight() { - - this.colorMode = 'white'; // or 'colour' - this.brightness = 100; // percentage value use _convertValToPercentage functions below. - - this.color = { - H: 130, - 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 = {}; -} - -/** - * 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 - * @returns {Integer} color value from 0-255 - */ -TuyaColorLight.prototype._convertPercentageToVal = function (percentage) { - var tmp = Math.round(255 * (percentage / 100)); - debug('Converted ' + percentage + ' to: ' + tmp); - return tmp; -}; - -/** - * calculate percentage from color value - * @param {Integer} val 0-255 color value - * @returns {Integer} HK-Value - */ -TuyaColorLight.prototype._convertValToPercentage = function (val) { - var tmp = Math.round((val / 255) * 100); - debug('Converted ' + val + ' to: ' + tmp); - return tmp; -}; - -/** - * converts color value to color temperature - * @param {Integer} val - * @returns {Integer} percentage from 0-100 - */ -TuyaColorLight.prototype._convertColorTemperature = function (val) { - var tmpRange = this.colorTempMax - this.colorTempMin; - var tmpCalc = Math.round((val / this.colorTempMax) * 100); - - debug('HK colorTemp Value: ' + val); - debug('HK colorTemp scale min : ' + this.colorTempMin); - debug('HK colorTemp scale max : ' + this.colorTempMax); - debug('HK colorTemp range (tmpRange): ' + tmpRange); - debug('HK colorTemp % tmpCalc: ' + tmpCalc); - - var tuyaColorTemp = this._convertPercentageToVal(tmpCalc); - - debug('HK tuyaColorTemp: ' + tuyaColorTemp); - - return tuyaColorTemp; -}; - -/** - * Convert color temperature to HK - * @param {Integer} val - * @returns {Integer} HK-Value - */ -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); - - debug('Tuya color Temperature : ' + val); - debug('Tuya color temp Percent of 255: ' + tuyaColorTempPercent + '%'); - - debug('HK colorTemp scale min : ' + this.colorTempMin); - debug('HK colorTemp scale max : ' + this.colorTempMax); - - debug('HK Color Temp Range: ' + tmpRange); - debug('HK range %: ' + tuyaColorTempPercent); - debug('HK Value: ' + hkValue); - - return hkValue; -}; - -/** - * check if given String is HEX - * @param {String} h - * @returns {boolean} - */ -TuyaColorLight.prototype._ValIsHex = function (h) { - debug("Check if value is hex", 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 - * @return {string} brightness as HEX value - */ -TuyaColorLight.prototype._getAlphaHex = function (brightness) { - var i = brightness / 100; - var alpha = Math.round(i * 255); - var hex = (alpha + 0x10000).toString(16).substr(-2); - var perc = Math.round(i * 100); - - debug('alpha percent: ' + perc + '% hex: ' + hex + ' alpha: ' + alpha); - return hex; -}; - -/** - * Set saturation from value - * @param {Integer} value - */ -TuyaColorLight.prototype.setSaturation = function (value) { - this.color.S = value; - this.saturation = value; - this.colorMode = 'colour'; - - debug('SET SATURATION: ' + value); -}; - -/** - * Set Brightness - * @param {Integer} value - */ -TuyaColorLight.prototype.setBrightness = function (value) { - this.brightness = value; - //var newValue = this._convertPercentageToVal(value); - var newValue = this._convertBrightnessPercentageToVal(value); - debug("BRIGHTNESS from UI: " + value + ' Converted from 100 to 255 scale: ' + newValue); -} - -/** - * @param {} value - */ -TuyaColorLight.prototype.setHue = function (value) { - debug('SET HUE: ' + value); - debug('Saturation Value: ' + this.color.S); - this.color.H = value; - - //check color and set colormode if necessary - debug("colormode", value, this.color.S); - if (value === 0 && this.color.S === 0) { - this.colorMode = 'white'; - debug('SET Color Mode: \'white\''); - } else { - this.colorMode = 'colour'; - debug('SET Color Mode: \'colour\' -- dahhhhhh british spelling \'coulour\' really is annoying... why you gotta be special?'); - } - - - return { - color: this.color, - colorMode: this.colorMode, - hue: this.color.H, - saturation: this.saturation - }; -}; - -/** - * Set HSL color - * @param {Integer} hue - * @param {Integer} saturation - * @param {Integer} brightness - */ -TuyaColorLight.prototype.setHSL = function (hue, saturation, brightness) { - this.setSaturation(saturation); - this.setBrightness(brightness); - this.setHue(hue); -} - -/** - * Set color from given string - * @param {String} colorValue could be HEX or HSL color type - * @returns {Object} dps settings for given color - */ -TuyaColorLight.prototype.setColor = function (colorValue) { - debug("Recieved color", colorValue); - - if (this._ValIsHex(colorValue)) { - debug("Color is Hex"); - var color = convert.hex.hsl(colorValue); - } else { - debug("Color is HSL"); - var color = colorValue.split(","); - // convert strings to numbers - color.forEach(function (element, key) { - color[key] = parseInt(element, 10); - }); - } - debug("Converted color as HSL", { - 0: color[0] + " - " + typeof color[0], - 1: color[1] + " - " + typeof color[1], - 2: color[2] + " - " + typeof color[2] - }) - - this.setHSL(color[0], color[1], color[2]); - return this.getDps(); -} - -/** - * get dps settings for current color - * @returns {Object} dps settings - */ -TuyaColorLight.prototype.getDps = function () { - var color = this.color; - - var lightness = Math.round(this.brightness / 2); - var brightness = this.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 = 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); - // color temperature percentage is at a fixed 51% - var temperature = this._convertSATorColorTempPercentageToVal(51); - - // 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; \ No newline at end of file diff --git a/tuya-device.js b/tuya-device.js deleted file mode 100644 index 0c526e1..0000000 --- a/tuya-device.js +++ /dev/null @@ -1,270 +0,0 @@ -const TuyAPI = require('tuyapi'); -const TuyColor = require('./tuya-color'); -const debug = require('debug')('TuyAPI:device'); -const debugError = require('debug')('TuyAPI:device:error'); -const debugColor = require('debug')('TuyAPI:device:color'); - -/** - * - var device = new TuyaDevice({ - id: '03200240600194781244', - key: 'b8bdebab418f5b55', - ip: '192.168.178.45', - version: "3.3", - type: "" <- "switch", "light", "dimmer", etc. Attempts autodetect if not defined - }); - */ - -var TuyaDevice = (function () { - var devices = []; - var events = {}; - - function checkExisiting(options) { - var existing = false; - // Check for existing instance - devices.forEach(device => { - if (device.topicLevel == options.topicLevel) { - existing = device; - } - }); - return existing; - } - - function deleteDevice(id) { - devices.forEach((device, key) => { - if (device.hasOwnProperty("options")) { - if (id === device.options.id) { - debug("delete Device", devices[key].toString()); - delete devices[key]; - } - } - }); - } - - function TuyaDevice(options) { - var device = this; - - // Check for existing instance by matching topicLevel value - if (existing = checkExisiting(options)) { - return new Promise((resolve, reject) => { - resolve({ - status: "connected", - device: existing - }); - }); - } - - if (!(this instanceof TuyaDevice)) { - return new TuyaDevice(options); - } - - this.options = options; - - if (this.options.name) { - this.topicLevel = this.options.name.toLowerCase().replace(/ /g,"_"); - } else { - this.topicLevel = this.options.id; - } - - Object.defineProperty(this, 'device', { - value: new TuyAPI(JSON.parse(JSON.stringify(this.options))) - }); - - this.device.on('data', data => { - if (typeof data == "string") { - debugError('Data from device not encrypted:', data.replace(/[^a-zA-Z0-9 ]/g, "")); - } else { - debug('Data from device:', data); - device.triggerAll('data', data); - } - }); - - devices.push(this); - - // Find device on network - debug("Search device in network"); - this.find().then(() => { - debug("Device found in network"); - // Connect to device - this.device.connect(); - }); - - /** - * @return Promise to wait for connection - */ - return new Promise((resolve, reject) => { - this.device.on('connected', () => { - device.triggerAll('connected'); - device.connected = true; - debug('Connected to device.', device.toString()); - resolve({ - status: "connected", - device: this - }); - }); - this.device.on('disconnected', () => { - device.triggerAll('disconnected'); - device.connected = false; - debug('Disconnected from device.', device.toString()); - deleteDevice(options.id); - return reject({ - status: "disconnect", - device: null - }); - }); - - this.device.on('error', (err) => { - debugError(err); - device.triggerAll('error', err); - return reject({ - error: err, - device: this - }); - }); - }); - } - - TuyaDevice.prototype.toString = function () { - return this.name + " (" + this.options.ip + ", " + this.options.id + ", " + this.options.key + ")"; - } - - TuyaDevice.prototype.triggerAll = function (name, argument) { - var device = this; - var e = events[name] || []; - e.forEach(event => { - event.call(device, argument); - }); - } - - TuyaDevice.prototype.on = function (name, callback) { - if (!this.connected) return; - var device = this; - this.device.on(name, function () { - callback.apply(device, arguments); - }); - } - - TuyaDevice.prototype.find = function () { - return this.device.find(); - } - - TuyaDevice.prototype.get = function () { - return this.device.get(); - } - - TuyaDevice.prototype.set = function (options) { - debug('set:', options); - return new Promise((resolve, reject) => { - this.device.set(options).then((result) => { - this.get().then(() => { - debug("Set completed "); - resolve(result); - }); - }); - }); - } - - TuyaDevice.prototype.switch = function (newStatus, callback) { - if (!this.connected) return; - newStatus = newStatus.toLowerCase(); - if (newStatus == "on") { - return this.switchOn(callback); - } - if (newStatus == "off") { - return this.switchOff(callback); - } - if (newStatus == "toggle") { - return this.toggle(callback); - } - } - - TuyaDevice.prototype.switchOn = function () { - if (!this.connected) return; - debug("switch -> ON"); - - return this.set({ - set: true - }); - } - - TuyaDevice.prototype.switchOff = function () { - if (!this.connected) return; - debug("switch -> OFF"); - - return this.set({ - set: false - }); - } - - TuyaDevice.prototype.toggle = function () { - if (!this.connected) return; - return new Promise((resolve, reject) => { - this.get().then((status) => { - debug("toogle state", status); - this.set({ - set: !status - }); - }); - }); - } - - 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); - var tuya = this.device; - var color = new TuyColor(tuya); - var dps = color.setColor(hexColor); - debugColor("dps values:", dps); - - return this.set({ - multiple: true, - data: dps - }); - } - - TuyaDevice.prototype.connect = function (callback) { - debug("Connect to TuyAPI Device"); - return this.device.connect(callback); - } - - TuyaDevice.prototype.disconnect = function (callback) { - debug("Disconnect from TuyAPI Device"); - return this.device.disconnect(callback); - } - - Object.defineProperty(TuyaDevice, 'devices', { - value: devices - }); - - TuyaDevice.connectAll = function () { - devices.forEach(device => { - device.connect(); - }); - } - - TuyaDevice.disconnectAll = function () { - devices.forEach(device => { - device.disconnect(); - }); - } - - TuyaDevice.onAll = function (name, callback) { - if (events[name] == undefined) { - events[name] = []; - } - events[name].push(callback); - devices.forEach(device => { - device.triggerAll(name); - }); - } - - return TuyaDevice; -}()); - -module.exports = TuyaDevice; diff --git a/tuya-mqtt.js b/tuya-mqtt.js index d85181c..bdd0e08 100644 --- a/tuya-mqtt.js +++ b/tuya-mqtt.js @@ -1,426 +1,150 @@ +#!/usr/bin/env node const fs = require('fs') -const mqtt = require('mqtt'); -const json5 = require('json5'); -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'); +const mqtt = require('mqtt') +const json5 = require('json5') +const debug = require('debug')('tuya-mqtt:mqtt') +const debugError = require('debug')('tuya-mqtt:error') +const SimpleSwitch = require('./devices/simple-switch') +const SimpleDimmer = require('./devices/simple-dimmer') +const RGBTWLight = require('./devices/rgbtw-light') +const GenericDevice = require('./devices/generic-device') -var CONFIG = undefined; -var mqtt_client = undefined; +var CONFIG = undefined +var tuyaDevices = new Array() -/* - * Check if data is JSON or not - */ -function isJsonString (data){ - try { - const parsedData = JSON.parse(data); - if (parsedData && typeof parsedData === "object") { - return parsedData; - } +function getDevice(configDevice, mqttClient) { + const deviceInfo = { + configDevice: configDevice, + mqttClient: mqttClient, + topic: CONFIG.topic } - catch (e) { } - - return false; -}; - -/** - * get command from mqtt message - * converts message to TuyAPI JSON commands - * @param {String} message - * @returns {Object} - */ -function getCommandFromMessage(_message) { - let command = _message - - if (command != "1" && command != "0" && isJsonString(command)) { - debug("Received command is JSON"); - command = JSON.parse(command); - } else { - 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(); - } + switch (configDevice.type) { + case 'SimpleSwitch': + return new SimpleSwitch(deviceInfo) + break; + case 'SimpleDimmer': + return new SimpleDimmer(deviceInfo) + break; + case 'RGBTWLight': + return new RGBTWLight(deviceInfo) + break; + case 'GenericDevice': + return new GenericDevice(deviceInfo) + break; } - return command; + return null } -// Parse message -function parseDpsMessage(message) { - if (typeof message === "boolean" ) { - return message; - } else if (message === "true" || message === "false") { - return (message === "true") ? true : false - } else if (!isNaN(message)) { - return Number(message) - } else { - return message - } -} - -function publishMQTT(topic, data) { - mqtt_client.publish(topic, data, { - retain: CONFIG.retain, - qos: CONFIG.qos - }); -} - -function guessDeviceType(device, dps) { - keys = Object.keys(dps).length - if (keys === 2) { - 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" } - } - } -} - -function publishColorState(device, state) { - -} - -function publishDeviceTopics(device, dps) { - 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 - // Only publish state updates for DPS values included in device data - if (dps.hasOwnProperty(template.dpsType)) { - 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); - } - } - } -} - -/** - * publish all dps-values to topic - * @param {TuyaDevice} device - * @param {Object} dps - */ -function publishDPS(device, dps) { - if (mqtt_client.connected == true) { - try { - if (!device.options.type) { - guessDeviceType(device, dps) - } - - const baseTopic = CONFIG.topic + device.topicLevel + "/dps"; - const topic = baseTopic + "/state" - const data = JSON.stringify(dps); - - // Publish raw DPS JSON data - debugTuya("MQTT DPS JSON (raw): " + topic + " -> ", data); - publishMQTT(topic, data); - - // Publish dps/<#>/state value for each DPS - Object.keys(dps).forEach(function (key) { - const topic = baseTopic + "/" + key + "/state"; - const data = JSON.stringify(dps[key]); - debugTuya("MQTT DPS"+key+": "+topic+" -> ", data); - publishMQTT(topic, data); - }); - - publishDeviceTopics(device, dps) - - } 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 Id ' + data.devId + ' ->', data.dps); - 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)); -} - -function initTuyaDevices(tuyaDevices) { - for (let tuyaDevice of tuyaDevices) { - let options = { - id: tuyaDevice.id, - key: tuyaDevice.key - } - if (tuyaDevice.name) { options.name = tuyaDevice.name } - if (tuyaDevice.ip) { - options.ip = tuyaDevice.ip - if (tuyaDevice.version) { - options.version = tuyaDevice.version - } else { - version = "3.1" - } - } - new TuyaDevice(options); - } -} - -// 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); - }); - } +function initDevices(configDevices, mqttClient) { + for (let configDevice of configDevices) { + if (!configDevice.type) { + debug('Device type not specified, skipping creation of this device') } else { - debug("Received unknown command topic for device: ", commandTopic) + const newDevice = getDevice(configDevice, mqttClient) + if (newDevice) { + tuyaDevices.push(newDevice) + } } } } -// 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 + let configDevices + let mqttClient try { - CONFIG = require("./config"); + CONFIG = require('./config') } catch (e) { - console.error("Configuration file not found!") + console.error('Configuration file not found!') debugError(e) process.exit(1) } - if (typeof CONFIG.qos == "undefined") { - CONFIG.qos = 2; + if (typeof CONFIG.qos == 'undefined') { + CONFIG.qos = 2 } - if (typeof CONFIG.retain == "undefined") { - CONFIG.retain = false; + if (typeof CONFIG.retain == 'undefined') { + CONFIG.retain = false } try { - tuyaDevices = fs.readFileSync('./devices.conf', 'utf8'); - tuyaDevices = json5.parse(tuyaDevices) + configDevices = fs.readFileSync('./devices.conf', 'utf8') + configDevices = json5.parse(configDevices) } catch (e) { - console.error("Devices file not found!") + console.error('Devices file not found!') debugError(e) process.exit(1) } - if (!tuyaDevices.length) { - console.error("No devices found in devices file!") + if (!configDevices.length) { + console.error('No devices found in devices file!') process.exit(1) } - mqtt_client = mqtt.connect({ + mqttClient = 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"); - let topic = CONFIG.topic + '#'; - mqtt_client.subscribe(topic, { + mqttClient.on('connect', function (err) { + debug('Connection established to MQTT server') + let topic = CONFIG.topic + '#' + mqttClient.subscribe(topic, { retain: CONFIG.retain, qos: CONFIG.qos - }); - initTuyaDevices(tuyaDevices) - }); + }) + initDevices(configDevices, mqttClient) + }) - mqtt_client.on("reconnect", function (error) { - if (mqtt_client.connected) { - debug("Connection to MQTT server lost. Attempting to reconnect..."); + mqttClient.on('reconnect', function (error) { + if (mqttClient.connected) { + debug('Connection to MQTT server lost. Attempting to reconnect...') } else { - debug("Unable to connect to MQTT server"); + debug('Unable to connect to MQTT server') } - }); + }) - mqtt_client.on("error", function (error) { - debug("Unable to connect to MQTT server", error); - }); + mqttClient.on('error', function (error) { + debug('Unable to connect to MQTT server', error) + }) - mqtt_client.on('message', function (topic, message) { + mqttClient.on('message', function (topic, message) { try { - message = message.toString(); - const splitTopic = topic.split("/"); + message = message.toString() + const splitTopic = topic.split('/') const topicLength = splitTopic.length - const commandTopic = splitTopic[topicLength - 1]; - const options = { - topicLevel: splitTopic[1] - } + const commandTopic = splitTopic[topicLength - 1] + const deviceTopicLevel = splitTopic[1] // If it looks like a valid command topic try to process it - if (commandTopic.includes("command")) { - debug("Receive settings", JSON.stringify({ + if (commandTopic.includes('command')) { + debug('Received MQTT message -> ', JSON.stringify({ topic: topic, message: message - })); + })) - // Uses device topic level to find matching device - var device = new TuyaDevice(options); - - device.then(function (params) { - let device = params.device; - switch (topicLength) { - case 3: - processDeviceCommand(message, device, commandTopic); - break; - case 4: - processDpsCommand(message, device); - break; - case 5: - const dpsKey = splitTopic[topicLength-2] - processDpsKeyCommand(message, device, dpsKey); - break; - } - }).catch((err) => { - debugError(err); - }); + // Use device topic level to find matching device + const device = tuyaDevices.find(d => d.options.name === deviceTopicLevel || d.options.id === deviceTopicLevel) + switch (topicLength) { + case 3: + device.processCommand(message, commandTopic) + break; + case 4: + device.processDpsCommand(message) + break; + case 5: + const dpsKey = splitTopic[topicLength-2] + device.processDpsKeyCommand(message, dpsKey) + break; + } } } catch (e) { - debugError(e); + debugError(e) } - }); + }) } // Call the main code