3.0.0-beta2

* Implement basic template model
* Generic device can specify template in devices.conf
* Simple switch, dimmer, and RGBTW specific support files
This commit is contained in:
tsightler
2020-10-02 23:16:53 -04:00
parent 56b87aa27e
commit f598d246cf
11 changed files with 750 additions and 989 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
devices/
old/
test/
config.json

30
devices/generic-device.js Normal file
View File

@@ -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

80
devices/rgbtw-light.js Normal file
View File

@@ -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

56
devices/simple-dimmer.js Normal file
View File

@@ -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

45
devices/simple-switch.js Normal file
View File

@@ -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

420
devices/tuya-device.js Normal file
View File

@@ -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

24
lib/utils.js Normal file
View File

@@ -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()

View File

@@ -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": {

View File

@@ -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;

View File

@@ -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: "<device_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;

View File

@@ -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