3.0.0-beta4

* Default to generic device
* Improved debugging granularity/increased categories
* Add heartbeat monitoring for availability
* Catch more failure cases with retry (still some missing I'd guess)
* Switch to MathJS evaluate for simple math transforms
* RGBTW: Switch base scale for all friendly topics to 100 (automatic conversion on backend)
* RGBTW: Add color temperature support
* RGBTW: Improve autodetection
* RGBTW: Improved white/color mode handling (still work to do here)
This commit is contained in:
tsightler
2020-10-12 16:14:22 -04:00
parent d5217ce237
commit 60b50c760e
9 changed files with 248 additions and 112 deletions

View File

@@ -1,5 +1,5 @@
const TuyaDevice = require('./tuya-device') const TuyaDevice = require('./tuya-device')
const debug = require('debug')('tuya-mqtt:tuya') const debug = require('debug')('tuya-mqtt:device')
const utils = require('../lib/utils') const utils = require('../lib/utils')
class GenericDevice extends TuyaDevice { class GenericDevice extends TuyaDevice {

View File

@@ -1,5 +1,6 @@
const TuyaDevice = require('./tuya-device') const TuyaDevice = require('./tuya-device')
const debug = require('debug')('tuya-mqtt:tuya') const debug = require('debug')('tuya-mqtt:device-detect')
const debugDiscovery = require('debug')('tuya-mqtt:discovery')
const utils = require('../lib/utils') const utils = require('../lib/utils')
class RGBTWLight extends TuyaDevice { class RGBTWLight extends TuyaDevice {
@@ -12,9 +13,11 @@ class RGBTWLight extends TuyaDevice {
this.config.dpsWhiteValue = this.config.dpsWhiteValue ? this.config.dpsWhiteValue : this.guess.dpsWhiteValue this.config.dpsWhiteValue = this.config.dpsWhiteValue ? this.config.dpsWhiteValue : this.guess.dpsWhiteValue
this.config.whiteValueScale = this.config.whiteValueScale ? this.config.whiteValueScale : this.guess.whiteValueScale this.config.whiteValueScale = this.config.whiteValueScale ? this.config.whiteValueScale : this.guess.whiteValueScale
this.config.dpsColorTemp = this.config.dpsColorTemp ? this.config.dpsColorTemp : this.guess.dpsColorTemp this.config.dpsColorTemp = this.config.dpsColorTemp ? this.config.dpsColorTemp : this.guess.dpsColorTemp
this.config.minColorTemp = this.config.minColorTemp ? this.config.minColorTemp : 165
this.config.maxColorTemp = this.config.maxColorTemp ? this.config.maxColorTemp : 375
this.config.colorTempScale = this.config.colorTempScale ? this.config.colorTempScale : this.guess.colorTempScale
this.config.dpsColor = this.config.dpsColor ? this.config.dpsColor : this.guess.dpsColor this.config.dpsColor = this.config.dpsColor ? this.config.dpsColor : this.guess.dpsColor
this.config.colorType = this.config.colorType ? this.config.colorType : this.guess.colorType this.config.colorType = this.config.colorType ? this.config.colorType : this.guess.colorType
this.config.colorType = 'hsb'
this.deviceData.mdl = 'RGBTW Light' this.deviceData.mdl = 'RGBTW Light'
@@ -32,8 +35,8 @@ class RGBTWLight extends TuyaDevice {
min: 1, min: 1,
max: 100, max: 100,
scale: this.config.whiteValueScale, scale: this.config.whiteValueScale,
stateMath: (this.config.whiteValueScale == 1000) ? '/10' : '/2.55', stateMath: '/('+this.config.whiteValueScale+'/100)',
commandMath: (this.config.whiteValueScale == 1000) ? '*10' : '*2.55' commandMath: '*('+this.config.whiteValueScale+'/100)'
}, },
hs_state: { hs_state: {
key: this.config.dpsColor, key: this.config.dpsColor,
@@ -56,6 +59,23 @@ class RGBTWLight extends TuyaDevice {
} }
} }
// If device supports Color Temperature add color temp device topic
if (this.config.dpsColorTemp) {
// Values used for tranform
const rangeFactor = (this.config.maxColorTemp-this.config.minColorTemp)/100
const scaleFactor = this.config.colorTempScale/100
const tuyaMaxColorTemp = this.config.maxColorTemp/rangeFactor*scaleFactor
this.deviceTopics.color_temp_state = {
key: this.config.dpsColorTemp,
type: 'int',
min: this.config.minColorTemp,
max: this.config.maxColorTemp,
stateMath: '/'+scaleFactor+'*-'+rangeFactor+'+'+this.config.maxColorTemp,
commandMath: '/'+rangeFactor+'*-'+scaleFactor+'+'+tuyaMaxColorTemp
}
}
// Send home assistant discovery data and give it a second before sending state updates // Send home assistant discovery data and give it a second before sending state updates
this.initDiscovery() this.initDiscovery()
await utils.sleep(1) await utils.sleep(1)
@@ -83,43 +103,45 @@ class RGBTWLight extends TuyaDevice {
device: this.deviceData device: this.deviceData
} }
debug('Home Assistant config topic: '+configTopic) if (this.config.dpsColorTemp) {
debug(discoveryData) discoveryData.color_temp_state_topic = this.baseTopic+'color_temp_state'
discoveryData.color_temp_command_topic = this.baseTopic+'color_temp_command'
discoveryData.min_mireds = this.config.minColorTemp
discoveryData.max_mireds = this.config.maxColorTemp
}
debugDiscovery('Home Assistant config topic: '+configTopic)
debugDiscovery(discoveryData)
this.publishMqtt(configTopic, JSON.stringify(discoveryData)) this.publishMqtt(configTopic, JSON.stringify(discoveryData))
} }
async guessLightInfo() { async guessLightInfo() {
this.guess = new Object() this.guess = new Object()
debug('Attempting to detect light capabilites and DPS values...')
debug('Querying DPS 2 for white/color mode setting...')
let mode = await this.device.get({"dps": 2}) let mode = await this.device.get({"dps": 2})
if (mode && (mode === 'white' || mode === 'colour')) { if (mode && (mode === 'white' || mode === 'colour' || mode.toString().includes('scene'))) {
this.guess.dpsPower = 1 debug('Detected probably Tuya color bulb at DPS 1-5, checking more details...')
this.guess.dpsMode = 2 this.guess = {'dpsPower': 1, 'dpsMode': 2, 'dpsWhiteValue': 3, 'whiteValueScale': 255, 'dpsColorTemp': 4, 'colorTempScale': 255, 'dpsColor': 5}
this.guess.dpsWhiteValue = 3
this.guess.whiteValueScale = 255
const colorTemp = await this.device.get({"dps": 4})
if (colorTemp) {
this.guess.dpsColorTemp = 4
} else {
this.guess.dpsColorTemp = 0
}
this.guess.dpsColor = 5
const color = await this.device.get({"dps": this.guess.dpsColor})
this.guess.colorType = (color && color.length === 14) ? 'hsbhex' : 'hsb'
} else { } else {
mode = await this.device.get({"dps": 20}) debug('Detected likely Tuya color bulb at DPS 20-24, checking more details...')
this.guess.dpsPower = 20 this.guess = {'dpsPower': 20, 'dpsMode': 21, 'dpsWhiteValue': 22, 'whiteValueScale': 1000, 'dpsColorTemp': 23, 'colorTempScale': 1000, 'dpsColor': 24}
this.guess.dpsMode = 21 }
this.guess.dpsWhiteValue = 22 if (this.guess.dpsPower) {
this.guess.whiteValueScale = 1000 debug('Attempting to detect if bulb supports color temperature...')
const colorTemp = await this.device.get({"dps": 23}) const colorTemp = await this.device.get({"dps": this.guess.dpsColorTemp})
if (colorTemp) { if (colorTemp !== '' && colorTemp >= 0 && colorTemp <= this.guess.colorTempScale) {
this.guess.dpsColorTemp = 23 debug('Detected likely color temerature support')
} else { } else {
debug('No color temperature support detected')
this.guess.dpsColorTemp = 0 this.guess.dpsColorTemp = 0
} }
this.guess.dpsColor = 24 debug('Attempting to detect Tuya color format used by device...')
const color = await this.device.get({"dps": this.guess.dpsColor}) const color = await this.device.get({"dps": this.guess.dpsColor})
this.guess.colorType = (color && color.length === 12) ? 'hsb' : 'hsbhex' this.guess.colorType = (color && color.length === 12) ? 'hsb' : 'hsbhex'
debug ('Detected Tuya color format '+this.guess.colorType.toUpperCase())
} else {
debug('No Tuya color bulb detected, if this device is definitely a Tuya bulb please manually specify settings.')
} }
} }
} }

View File

@@ -1,5 +1,6 @@
const TuyaDevice = require('./tuya-device') const TuyaDevice = require('./tuya-device')
const debug = require('debug')('tuya-mqtt:tuya') const debug = require('debug')('tuya-mqtt:device')
const debugDiscovery = require('debug')('tuya-mqtt:discovery')
const utils = require('../lib/utils') const utils = require('../lib/utils')
class SimpleDimmer extends TuyaDevice { class SimpleDimmer extends TuyaDevice {
@@ -47,8 +48,8 @@ class SimpleDimmer extends TuyaDevice {
device: this.deviceData device: this.deviceData
} }
debug('Home Assistant config topic: '+configTopic) debugDiscovery('Home Assistant config topic: '+configTopic)
debug(discoveryData) debugDiscovery(discoveryData)
this.publishMqtt(configTopic, JSON.stringify(discoveryData)) this.publishMqtt(configTopic, JSON.stringify(discoveryData))
} }
} }

View File

@@ -1,5 +1,6 @@
const TuyaDevice = require('./tuya-device') const TuyaDevice = require('./tuya-device')
const debug = require('debug')('tuya-mqtt:tuya') const debug = require('debug')('tuya-mqtt:device')
const debugDiscovery = require('debug')('tuya-mqtt:discovery')
const utils = require('../lib/utils') const utils = require('../lib/utils')
class SimpleSwitch extends TuyaDevice { class SimpleSwitch extends TuyaDevice {
@@ -36,8 +37,8 @@ class SimpleSwitch extends TuyaDevice {
device: this.deviceData device: this.deviceData
} }
debug('Home Assistant config topic: '+configTopic) debugDiscovery('Home Assistant config topic: '+configTopic)
debug(discoveryData) debugDiscovery(discoveryData)
this.publishMqtt(configTopic, JSON.stringify(discoveryData)) this.publishMqtt(configTopic, JSON.stringify(discoveryData))
} }
} }

View File

@@ -1,7 +1,10 @@
const TuyAPI = require('tuyapi') const TuyAPI = require('tuyapi')
const { evaluate } = require('mathjs')
const utils = require('../lib/utils') const utils = require('../lib/utils')
const debug = require('debug')('tuya-mqtt:tuya') const { msSleep } = require('../lib/utils')
const debugMqtt = require('debug')('tuya-mqtt:mqtt') const debug = require('debug')('tuya-mqtt:tuyapi')
const debugState = require('debug')('tuya-mqtt:state')
const debugCommand = require('debug')('tuya-mqtt:command')
const debugError = require('debug')('tuya-mqtt:error') const debugError = require('debug')('tuya-mqtt:error')
class TuyaDevice { class TuyaDevice {
@@ -39,8 +42,8 @@ class TuyaDevice {
"color": {'h': 0, 's': 0, 'b': 0} "color": {'h': 0, 's': 0, 'b': 0}
} }
// Property to hold friendly topics template this.deviceTopics = {} // Property to hold friendly topics template
this.deviceTopics = {} this.heartbeatsMissed = 0 // Used to monitor heartbeat status to detect offline device
// Build the MQTT topic for this device (friendly name or device id) // Build the MQTT topic for this device (friendly name or device id)
if (this.options.name) { if (this.options.name) {
@@ -55,7 +58,7 @@ class TuyaDevice {
// Listen for device data and call update DPS function if valid // Listen for device data and call update DPS function if valid
this.device.on('data', (data) => { this.device.on('data', (data) => {
if (typeof data === 'object') { if (typeof data === 'object') {
debug('Received JSON data from device '+this.options.id+' ->', data.dps) debug('Received JSON data from device '+this.options.id+' ->', JSON.stringify(data.dps))
this.updateState(data) this.updateState(data)
} else { } else {
if (data !== 'json obj data unvalid') { if (data !== 'json obj data unvalid') {
@@ -64,36 +67,46 @@ class TuyaDevice {
} }
}) })
// Find device on network // Attempt to find/connect to device and start heartbeat monitor
debug('Search for device id '+this.options.id) this.connectDevice()
this.device.find().then(() => { this.monitorHeartbeat()
debug('Found device id '+this.options.id)
// Attempt connection to device
this.device.connect()
})
// On connect perform device specific init // On connect perform device specific init
this.device.on('connected', () => { this.device.on('connected', async () => {
debug('Connected to device ' + this.toString()) // Sometimes TuyAPI reports connection even on socket error
this.init() // Wait one second to check if device is really connected before initializing
await utils.sleep(1)
if (this.device.isConnected()) {
debug('Connected to device ' + this.toString())
this.heartbeatsMissed = 0
this.publishMqtt(this.baseTopic+'status', 'online')
this.init()
}
}) })
// On disconnect perform device specific disconnect // On disconnect perform device specific disconnect
this.device.on('disconnected', () => { this.device.on('disconnected', () => {
this.connected = false this.connected = false
this.publishMqtt(this.baseTopic+'status', 'offline')
debug('Disconnected from device ' + this.toString()) debug('Disconnected from device ' + this.toString())
}) })
// On connect error call reconnect // On connect error call reconnect
this.device.on('error', (err) => { this.device.on('error', async (err) => {
debugError(err) debugError(err)
if (err.message === 'Error from socket') { await utils.sleep(1)
if (!this.device.isConnected()) {
this.reconnect() this.reconnect()
} }
}) })
// On heartbeat reset heartbeat timer
this.device.on('heartbeat', () => {
this.heartbeatsMissed = 0
})
} }
// Update dps properties with device data updates // Update cached DPS states on data updates
updateState(data) { updateState(data) {
if (typeof data.dps != 'undefined') { if (typeof data.dps != 'undefined') {
// Update cached device state data // Update cached device state data
@@ -148,7 +161,7 @@ class TuyaDevice {
} }
data = JSON.stringify(data) data = JSON.stringify(data)
const dpsStateTopic = dpsTopic + '/state' const dpsStateTopic = dpsTopic + '/state'
debugMqtt('MQTT DPS JSON: ' + dpsStateTopic + ' -> ', data) debugState('MQTT DPS JSON: ' + dpsStateTopic + ' -> ', data)
this.publishMqtt(dpsStateTopic, data, false) this.publishMqtt(dpsStateTopic, data, false)
// Publish dps/<#>/state value for each device DPS // Publish dps/<#>/state value for each device DPS
@@ -156,7 +169,7 @@ class TuyaDevice {
if (this.state.dps[key].updated) { if (this.state.dps[key].updated) {
const dpsKeyTopic = dpsTopic + '/' + key + '/state' const dpsKeyTopic = dpsTopic + '/' + key + '/state'
const data = this.state.dps.hasOwnProperty(key) ? this.state.dps[key].val.toString() : 'None' const data = this.state.dps.hasOwnProperty(key) ? this.state.dps[key].val.toString() : 'None'
debugMqtt('MQTT DPS'+key+': '+dpsKeyTopic+' -> ', data) debugState('MQTT DPS'+key+': '+dpsKeyTopic+' -> ', data)
this.publishMqtt(dpsKeyTopic, data, false) this.publishMqtt(dpsKeyTopic, data, false)
this.state.dps[key].updated = false this.state.dps[key].updated = false
} }
@@ -193,7 +206,7 @@ class TuyaDevice {
return state return state
} }
// Parse the received state value based on deviceTopic config // Parse the received state numeric value based on deviceTopic rules
parseStateNumber(value, deviceTopic) { parseStateNumber(value, deviceTopic) {
// Check if it's a number and it's not outside of defined range // Check if it's a number and it's not outside of defined range
if (isNaN(value)) { if (isNaN(value)) {
@@ -204,14 +217,14 @@ class TuyaDevice {
switch (deviceTopic.type) { switch (deviceTopic.type) {
case 'int': case 'int':
if (deviceTopic.stateMath) { if (deviceTopic.stateMath) {
value = parseInt(Math.round(eval(value+deviceTopic.stateMath))) value = parseInt(Math.round(evaluate(value+deviceTopic.stateMath)))
} else { } else {
value = parseInt(value) value = parseInt(value)
} }
break; break;
case 'float': case 'float':
if (deviceTopic.stateMath) { if (deviceTopic.stateMath) {
value = parseFloat(eval(value+deviceTopic.stateMath)) value = parseFloat(evaluate(value+deviceTopic.stateMath))
} else { } else {
value = parseFloat(value) value = parseFloat(value)
} }
@@ -222,12 +235,12 @@ class TuyaDevice {
} }
// Process MQTT commands for all command topics at device level // Process MQTT commands for all command topics at device level
async processCommand(message, commandTopic) { processCommand(message, commandTopic) {
const command = this.getCommandFromMessage(message) const command = this.getCommandFromMessage(message)
if (commandTopic === 'command' && command === 'get-states') { if (commandTopic === 'command' && command === 'get-states') {
// Handle "get-states" command to update device state // Handle "get-states" command to update device state
debug('Received command: ', command) debugCommand('Received command: ', command)
await this.getStates() this.getStates()
} else { } else {
// Call device specific command topic handler // Call device specific command topic handler
this.processDeviceCommand(message, commandTopic) this.processDeviceCommand(message, commandTopic)
@@ -241,14 +254,14 @@ class TuyaDevice {
const deviceTopic = this.deviceTopics.hasOwnProperty(stateTopic) ? this.deviceTopics[stateTopic] : '' const deviceTopic = this.deviceTopics.hasOwnProperty(stateTopic) ? this.deviceTopics[stateTopic] : ''
if (deviceTopic) { if (deviceTopic) {
debug('Device '+this.options.id+' received command topic: '+commandTopic+', message: '+message) debugCommand('Device '+this.options.id+' received command topic: '+commandTopic+', message: '+message)
const command = this.getCommandFromMessage(message) const command = this.getCommandFromMessage(message)
let commandResult = this.sendTuyaCommand(command, deviceTopic) let commandResult = this.sendTuyaCommand(command, deviceTopic)
if (!commandResult) { if (!commandResult) {
debug('Command topic '+this.baseTopic+commandTopic+' received invalid value: '+command) debugCommand('Command topic '+this.baseTopic+commandTopic+' received invalid value: '+command)
} }
} else { } else {
debug('Invalid command topic '+this.baseTopic+commandTopic+' for device: '+this.config.name) debugCommand('Invalid command topic '+this.baseTopic+commandTopic+' for device: '+this.config.name)
return return
} }
} }
@@ -258,7 +271,7 @@ class TuyaDevice {
let command let command
if (message != '1' && message != '0' && utils.isJsonString(message)) { if (message != '1' && message != '0' && utils.isJsonString(message)) {
debugMqtt('MQTT message is JSON') debugCommand('MQTT message is JSON')
command = JSON.parse(message); command = JSON.parse(message);
} else { } else {
switch(message.toLowerCase()) { switch(message.toLowerCase()) {
@@ -284,20 +297,20 @@ class TuyaDevice {
processDpsCommand(message) { processDpsCommand(message) {
if (utils.isJsonString(message)) { if (utils.isJsonString(message)) {
const tuyaCommand = this.getCommandFromMessage(message) const tuyaCommand = this.getCommandFromMessage(message)
debugMqtt('Received command: '+tuyaCommand) debugCommand('Received command: '+tuyaCommand)
this.set(tuyaCommand) this.set(tuyaCommand)
} else { } else {
debugError('DPS command topic requires Tuya style JSON value') debugCommand('DPS command topic requires Tuya style JSON value')
} }
} }
// Process text base Tuya command via DPS key command topics // Process text base Tuya command via DPS key command topics
processDpsKeyCommand(message, dpsKey) { processDpsKeyCommand(message, dpsKey) {
if (utils.isJsonString(message)) { if (utils.isJsonString(message)) {
debugError('Individual DPS command topics do not accept JSON values') debugCommand('Individual DPS command topics do not accept JSON values')
} else { } else {
const dpsMessage = this.parseDpsMessage(message) const dpsMessage = this.parseDpsMessage(message)
debugMqtt('Received command for DPS'+dpsKey+': ', message) debugCommand('Received command for DPS'+dpsKey+': ', message)
const tuyaCommand = { const tuyaCommand = {
dps: dpsKey, dps: dpsKey,
set: dpsMessage set: dpsMessage
@@ -325,7 +338,13 @@ class TuyaDevice {
this.connected = false this.connected = false
for (let topic in this.deviceTopics) { for (let topic in this.deviceTopics) {
const key = this.deviceTopics[topic].key const key = this.deviceTopics[topic].key
const result = await this.device.get({"dps": key}) try {
const result = await this.device.get({"dps": key})
this.state.dps[key].val = result
this.state.dps[key].updated = true
} catch {
debugError('Could not get value for device DPS key '+key)
}
} }
this.connected = true this.connected = true
// Force topic update now that all states are fully in sync // Force topic update now that all states are fully in sync
@@ -390,14 +409,14 @@ class TuyaDevice {
switch (deviceTopic.type) { switch (deviceTopic.type) {
case 'int': case 'int':
if (deviceTopic.commandMath) { if (deviceTopic.commandMath) {
value = parseInt(Math.round(eval(command+deviceTopic.commandMath))) value = parseInt(Math.round(evaluate(command+deviceTopic.commandMath)))
} else { } else {
value = parseInt(command) value = parseInt(command)
} }
break; break;
case 'float': case 'float':
if (deviceTopic.commandMath) { if (deviceTopic.commandMath) {
value = parseFloat(eval(command+deviceTopic.commandMath)) value = parseFloat(evaluate(command+deviceTopic.commandMath))
} else { } else {
value = parseFloat(command) value = parseFloat(command)
} }
@@ -421,7 +440,7 @@ class TuyaDevice {
// Convert from Hex to Decimal and cache values // Convert from Hex to Decimal and cache values
this.state.color.h = parseInt(h, 16) this.state.color.h = parseInt(h, 16)
this.state.color.s = Math.round(parseInt(s, 16) / 10) // Convert saturation to 100 Scale this.state.color.s = Math.round(parseInt(s, 16) / 10) // Convert saturation to 100 Scale
this.state.color.b = Math.round(parseInt(b, 16) / 10) // Convert brightness to 1000 scale this.state.color.b = Math.round(parseInt(b, 16) / 10) // Convert brightness to 100 scale
} }
// Initialize the set color values for first time. Used to conflicts // Initialize the set color values for first time. Used to conflicts
@@ -491,12 +510,16 @@ class TuyaDevice {
// Set white/colour mode based on // Set white/colour mode based on
async setLight(topic, command) { async setLight(topic, command) {
const currentMode = this.state.dps[this.config.dpsMode].val
let targetMode = undefined let targetMode = undefined
const currentMode = this.config.dpsMode.val
if (topic.key === this.config.dpsWhiteValue || topic.key === this.config.dpsColorTemp) { if (topic.key === this.config.dpsWhiteValue || topic.key === this.config.dpsColorTemp) {
// If setting white level or color temperature, light should be in white mode // If setting white level or color temperature, light should be in white mode
targetMode = 'white' targetMode = 'white'
} else if (topic.key === this.config.dpsColor) { } else if (topic.key === this.config.dpsColor) {
// Short sleep for cases where mulitple updates occur quickly
await msSleep(100)
if (this.state.setColor.s < 10) { if (this.state.setColor.s < 10) {
// If saturation is < 10 then white mode // If saturation is < 10 then white mode
targetMode = 'white' targetMode = 'white'
@@ -505,17 +528,18 @@ class TuyaDevice {
targetMode = 'colour' targetMode = 'colour'
} }
} }
// If mode change required, add it to the set command
if (targetMode && currentMode !== targetMode) { // Set the proper value
command = {
multiple: true,
data: {
[command.dps]: command.set,
[this.config.dpsMode]: targetMode
}
}
}
this.set(command) this.set(command)
// Put the bulb in the correct mode
if (targetMode) {
command = {
dps: this.config.dpsMode,
set: targetMode
}
this.set(command)
}
} }
// Simple function to help debug output // Simple function to help debug output
@@ -532,9 +556,27 @@ class TuyaDevice {
}) })
} }
connectDevice() {
// 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().catch((error) => {
debugError(error.message)
this.reconnect()
})
}).catch(async (error) => {
debugError(error.message)
debugError('Will attempt to find device again in 60 seconds')
await utils.sleep(60)
this.connectDevice()
})
}
// Retry connection every 10 seconds if unable to connect // Retry connection every 10 seconds if unable to connect
async reconnect() { async reconnect() {
debug('Error connecting to device id '+this.options.id+'...retry in 10 seconds.') debugError('Error connecting to device id '+this.options.id+'...retry in 10 seconds.')
await utils.sleep(10) await utils.sleep(10)
if (this.connected) { return } if (this.connected) { return }
debug('Search for device id '+this.options.id) debug('Search for device id '+this.options.id)
@@ -545,10 +587,27 @@ class TuyaDevice {
}) })
} }
// Simple function to monitor heartbeats to determine if
monitorHeartbeat() {
setInterval(async () => {
if (this.connected) {
if (this.heartbeatsMissed > 3) {
debugError('Device id '+this.options.id+' not responding to heartbeats...disconnecting')
this.device.disconnect()
await utils.sleep(1)
this.connectDevice()
} else if (this.heartbeatsMissed > 0) {
const errMessage = this.heartbeatsMissed > 1 ? " consecutive heartbeats" : " heartbeat"
debugError('Device id '+this.options.id+' has missed '+this.heartbeatsMissed+errMessage)
}
this.heartbeatsMissed++
}
}, 10000)
}
// Publish MQTT // Publish MQTT
publishMqtt(topic, message, isDebug) { publishMqtt(topic, message, isDebug) {
if (isDebug) { debugMqtt(topic, message) } if (isDebug) { debugState(topic, message) }
this.mqttClient.publish(topic, message, { qos: 1 }); this.mqttClient.publish(topic, message, { qos: 1 });
} }
} }

View File

@@ -19,6 +19,10 @@ class Utils
return new Promise(res => setTimeout(res, sec*1000)) return new Promise(res => setTimeout(res, sec*1000))
} }
msSleep(ms) {
return new Promise(res => setTimeout(res, ms))
}
} }
module.exports = new Utils() module.exports = new Utils()

68
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "tuya-mqtt", "name": "tuya-mqtt",
"version": "3.0.0-beta3", "version": "3.0.0-beta4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -148,9 +148,9 @@
} }
}, },
"@types/node": { "@types/node": {
"version": "14.11.5", "version": "14.11.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.8.tgz",
"integrity": "sha512-jVFzDV6NTbrLMxm4xDSIW/gKnk8rQLF9wAzLWIOg+5nU6ACrIMndeBdXci0FGtqJbP9tQvm6V39eshc96TO2wQ==" "integrity": "sha512-KPcKqKm5UKDkaYPTuXSx8wEP7vE9GnuaXIZKijwRYcePpZFDVuy2a57LarFKiORbHOuTOOwYzxVxcUzsh2P2Pw=="
}, },
"@types/responselike": { "@types/responselike": {
"version": "1.0.0", "version": "1.0.0",
@@ -487,6 +487,11 @@
"minimist": "^1.1.0" "minimist": "^1.1.0"
} }
}, },
"complex.js": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.0.11.tgz",
"integrity": "sha512-6IArJLApNtdg1P1dFtn3dnyzoZBEF0MwMnrfF1exSBRpZYoy4yieMkpZhQDC0uwctw48vii0CFVyHfpgZ/DfGw=="
},
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -543,6 +548,11 @@
"ms": "2.1.2" "ms": "2.1.2"
} }
}, },
"decimal.js": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz",
"integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw=="
},
"decompress-response": { "decompress-response": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-5.0.0.tgz", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-5.0.0.tgz",
@@ -773,6 +783,11 @@
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
}, },
"escape-latex": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
"integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw=="
},
"escape-string-regexp": { "escape-string-regexp": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@@ -864,6 +879,11 @@
} }
} }
}, },
"fraction.js": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.0.12.tgz",
"integrity": "sha512-8Z1K0VTG4hzYY7kA/1sj4/r1/RWLBD3xwReT/RCrUCbzPszjNQCCsy3ktkU/eaEqX3MYa4pY37a52eiBlPMlhA=="
},
"fresh": { "fresh": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@@ -1271,6 +1291,11 @@
"iterate-iterator": "^1.0.1" "iterate-iterator": "^1.0.1"
} }
}, },
"javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k="
},
"json-buffer": { "json-buffer": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -1387,6 +1412,21 @@
"semver": "^6.0.0" "semver": "^6.0.0"
} }
}, },
"mathjs": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-7.5.1.tgz",
"integrity": "sha512-H2q/Dq0qxBLMw+G84SSXmGqo/znihuxviGgAQwAcyeFLwK2HksvSGNx4f3dllZF51bWOnu2op60VZxH2Sb51Pw==",
"requires": {
"complex.js": "^2.0.11",
"decimal.js": "^10.2.1",
"escape-latex": "^1.2.0",
"fraction.js": "^4.0.12",
"javascript-natural-sort": "^0.7.1",
"seed-random": "^2.2.0",
"tiny-emitter": "^2.1.0",
"typed-function": "^2.0.0"
}
},
"mime": { "mime": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
@@ -1982,6 +2022,11 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"seed-random": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz",
"integrity": "sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ="
},
"semaphore": { "semaphore": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz",
@@ -2178,6 +2223,11 @@
"xtend": "~4.0.0" "xtend": "~4.0.0"
} }
}, },
"tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"tmp": { "tmp": {
"version": "0.0.33", "version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@@ -2211,8 +2261,9 @@
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw==" "integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw=="
}, },
"tuyapi": { "tuyapi": {
"version": "github:tsightler/tuyapi#5e99e36a41be43768451ff844375abfb6061ad5e", "version": "6.0.1",
"from": "github:tsightler/tuyapi#bugfix-null-get", "resolved": "https://registry.npmjs.org/tuyapi/-/tuyapi-6.0.1.tgz",
"integrity": "sha512-2Qg0/avg3gtsDLRIktssFtxUA/WT6fvB+GCmQwb1uRQq6KoTsS9he2vDGzmExwaek8Fz3vgAACH7WNIRemCgDw==",
"requires": { "requires": {
"debug": "4.1.1", "debug": "4.1.1",
"p-queue": "6.6.1", "p-queue": "6.6.1",
@@ -2240,6 +2291,11 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.10.0.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.10.0.tgz",
"integrity": "sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw==" "integrity": "sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw=="
}, },
"typed-function": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/typed-function/-/typed-function-2.0.0.tgz",
"integrity": "sha512-Hhy1Iwo/e4AtLZNK10ewVVcP2UEs408DS35ubP825w/YgSBK1KVLwALvvIG4yX75QJrxjCpcWkzkVRB0BwwYlA=="
},
"typedarray": { "typedarray": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "tuya-mqtt", "name": "tuya-mqtt",
"version": "3.0.0-beta3", "version": "3.0.0-beta4",
"description": "Control Tuya devices locally via MQTT", "description": "Control Tuya devices locally via MQTT",
"homepage": "https://github.com/TheAgentK/tuya-mqtt#readme", "homepage": "https://github.com/TheAgentK/tuya-mqtt#readme",
"main": "tuya-mqtt.js", "main": "tuya-mqtt.js",
@@ -19,7 +19,8 @@
"json5": "^2.1.3", "json5": "^2.1.3",
"mqtt": "^4.2.1", "mqtt": "^4.2.1",
"supports-color": "^7.2.0", "supports-color": "^7.2.0",
"tuyapi": "github:tsightler/tuyapi#bugfix-null-get" "tuyapi": "^6.0.1",
"mathjs": "7.5.1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -2,7 +2,8 @@
const fs = require('fs') const fs = require('fs')
const mqtt = require('mqtt') const mqtt = require('mqtt')
const json5 = require('json5') const json5 = require('json5')
const debug = require('debug')('tuya-mqtt:mqtt') const debug = require('debug')('tuya-mqtt:info')
const debugCommand = require('debug')('tuya-mqtt:command')
const debugError = require('debug')('tuya-mqtt:error') const debugError = require('debug')('tuya-mqtt:error')
const SimpleSwitch = require('./devices/simple-switch') const SimpleSwitch = require('./devices/simple-switch')
const SimpleDimmer = require('./devices/simple-dimmer') const SimpleDimmer = require('./devices/simple-dimmer')
@@ -28,23 +29,14 @@ function getDevice(configDevice, mqttClient) {
case 'RGBTWLight': case 'RGBTWLight':
return new RGBTWLight(deviceInfo) return new RGBTWLight(deviceInfo)
break; break;
case 'GenericDevice':
return new GenericDevice(deviceInfo)
break;
} }
return null return new GenericDevice(deviceInfo)
} }
function initDevices(configDevices, mqttClient) { function initDevices(configDevices, mqttClient) {
for (let configDevice of configDevices) { for (let configDevice of configDevices) {
if (!configDevice.type) { const newDevice = getDevice(configDevice, mqttClient)
debug('Device type not specified, skipping creation of this device') tuyaDevices.push(newDevice)
} else {
const newDevice = getDevice(configDevice, mqttClient)
if (newDevice) {
tuyaDevices.push(newDevice)
}
}
} }
} }
@@ -62,7 +54,7 @@ const main = async() => {
} }
if (typeof CONFIG.qos == 'undefined') { if (typeof CONFIG.qos == 'undefined') {
CONFIG.qos = 2 CONFIG.qos = 1
} }
if (typeof CONFIG.retain == 'undefined') { if (typeof CONFIG.retain == 'undefined') {
CONFIG.retain = false CONFIG.retain = false
@@ -121,7 +113,7 @@ const main = async() => {
// If it looks like a valid command topic try to process it // If it looks like a valid command topic try to process it
if (commandTopic.includes('command')) { if (commandTopic.includes('command')) {
debug('Received MQTT message -> ', JSON.stringify({ debugCommand('Received MQTT message -> ', JSON.stringify({
topic: topic, topic: topic,
message: message message: message
})) }))