mirror of
https://github.com/lehanspb/tuya-mqtt.git
synced 2025-12-16 09:44:36 +00:00
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:
@@ -1,5 +1,5 @@
|
||||
const TuyaDevice = require('./tuya-device')
|
||||
const debug = require('debug')('tuya-mqtt:tuya')
|
||||
const debug = require('debug')('tuya-mqtt:device')
|
||||
const utils = require('../lib/utils')
|
||||
|
||||
class GenericDevice extends TuyaDevice {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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')
|
||||
|
||||
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.whiteValueScale = this.config.whiteValueScale ? this.config.whiteValueScale : this.guess.whiteValueScale
|
||||
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.colorType = this.config.colorType ? this.config.colorType : this.guess.colorType
|
||||
this.config.colorType = 'hsb'
|
||||
|
||||
this.deviceData.mdl = 'RGBTW Light'
|
||||
|
||||
@@ -32,8 +35,8 @@ class RGBTWLight extends TuyaDevice {
|
||||
min: 1,
|
||||
max: 100,
|
||||
scale: this.config.whiteValueScale,
|
||||
stateMath: (this.config.whiteValueScale == 1000) ? '/10' : '/2.55',
|
||||
commandMath: (this.config.whiteValueScale == 1000) ? '*10' : '*2.55'
|
||||
stateMath: '/('+this.config.whiteValueScale+'/100)',
|
||||
commandMath: '*('+this.config.whiteValueScale+'/100)'
|
||||
},
|
||||
hs_state: {
|
||||
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
|
||||
this.initDiscovery()
|
||||
await utils.sleep(1)
|
||||
@@ -83,44 +103,46 @@ class RGBTWLight extends TuyaDevice {
|
||||
device: this.deviceData
|
||||
}
|
||||
|
||||
debug('Home Assistant config topic: '+configTopic)
|
||||
debug(discoveryData)
|
||||
if (this.config.dpsColorTemp) {
|
||||
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))
|
||||
}
|
||||
|
||||
async guessLightInfo() {
|
||||
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})
|
||||
if (mode && (mode === 'white' || mode === 'colour')) {
|
||||
this.guess.dpsPower = 1
|
||||
this.guess.dpsMode = 2
|
||||
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'
|
||||
if (mode && (mode === 'white' || mode === 'colour' || mode.toString().includes('scene'))) {
|
||||
debug('Detected probably Tuya color bulb at DPS 1-5, checking more details...')
|
||||
this.guess = {'dpsPower': 1, 'dpsMode': 2, 'dpsWhiteValue': 3, 'whiteValueScale': 255, 'dpsColorTemp': 4, 'colorTempScale': 255, 'dpsColor': 5}
|
||||
} else {
|
||||
mode = await this.device.get({"dps": 20})
|
||||
this.guess.dpsPower = 20
|
||||
this.guess.dpsMode = 21
|
||||
this.guess.dpsWhiteValue = 22
|
||||
this.guess.whiteValueScale = 1000
|
||||
const colorTemp = await this.device.get({"dps": 23})
|
||||
if (colorTemp) {
|
||||
this.guess.dpsColorTemp = 23
|
||||
debug('Detected likely Tuya color bulb at DPS 20-24, checking more details...')
|
||||
this.guess = {'dpsPower': 20, 'dpsMode': 21, 'dpsWhiteValue': 22, 'whiteValueScale': 1000, 'dpsColorTemp': 23, 'colorTempScale': 1000, 'dpsColor': 24}
|
||||
}
|
||||
if (this.guess.dpsPower) {
|
||||
debug('Attempting to detect if bulb supports color temperature...')
|
||||
const colorTemp = await this.device.get({"dps": this.guess.dpsColorTemp})
|
||||
if (colorTemp !== '' && colorTemp >= 0 && colorTemp <= this.guess.colorTempScale) {
|
||||
debug('Detected likely color temerature support')
|
||||
} else {
|
||||
debug('No color temperature support detected')
|
||||
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})
|
||||
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.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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')
|
||||
|
||||
class SimpleDimmer extends TuyaDevice {
|
||||
@@ -47,8 +48,8 @@ class SimpleDimmer extends TuyaDevice {
|
||||
device: this.deviceData
|
||||
}
|
||||
|
||||
debug('Home Assistant config topic: '+configTopic)
|
||||
debug(discoveryData)
|
||||
debugDiscovery('Home Assistant config topic: '+configTopic)
|
||||
debugDiscovery(discoveryData)
|
||||
this.publishMqtt(configTopic, JSON.stringify(discoveryData))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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')
|
||||
|
||||
class SimpleSwitch extends TuyaDevice {
|
||||
@@ -36,8 +37,8 @@ class SimpleSwitch extends TuyaDevice {
|
||||
device: this.deviceData
|
||||
}
|
||||
|
||||
debug('Home Assistant config topic: '+configTopic)
|
||||
debug(discoveryData)
|
||||
debugDiscovery('Home Assistant config topic: '+configTopic)
|
||||
debugDiscovery(discoveryData)
|
||||
this.publishMqtt(configTopic, JSON.stringify(discoveryData))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
const TuyAPI = require('tuyapi')
|
||||
const { evaluate } = require('mathjs')
|
||||
const utils = require('../lib/utils')
|
||||
const debug = require('debug')('tuya-mqtt:tuya')
|
||||
const debugMqtt = require('debug')('tuya-mqtt:mqtt')
|
||||
const { msSleep } = require('../lib/utils')
|
||||
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')
|
||||
|
||||
class TuyaDevice {
|
||||
@@ -39,15 +42,15 @@ class TuyaDevice {
|
||||
"color": {'h': 0, 's': 0, 'b': 0}
|
||||
}
|
||||
|
||||
// Property to hold friendly topics template
|
||||
this.deviceTopics = {}
|
||||
this.deviceTopics = {} // Property to hold friendly topics template
|
||||
this.heartbeatsMissed = 0 // Used to monitor heartbeat status to detect offline device
|
||||
|
||||
// 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)))
|
||||
@@ -55,7 +58,7 @@ class TuyaDevice {
|
||||
// Listen for device data and call update DPS function if valid
|
||||
this.device.on('data', (data) => {
|
||||
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)
|
||||
} else {
|
||||
if (data !== 'json obj data unvalid') {
|
||||
@@ -64,36 +67,46 @@ class TuyaDevice {
|
||||
}
|
||||
})
|
||||
|
||||
// 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()
|
||||
})
|
||||
// Attempt to find/connect to device and start heartbeat monitor
|
||||
this.connectDevice()
|
||||
this.monitorHeartbeat()
|
||||
|
||||
// On connect perform device specific init
|
||||
this.device.on('connected', () => {
|
||||
debug('Connected to device ' + this.toString())
|
||||
this.init()
|
||||
this.device.on('connected', async () => {
|
||||
// Sometimes TuyAPI reports connection even on socket error
|
||||
// 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
|
||||
this.device.on('disconnected', () => {
|
||||
this.connected = false
|
||||
this.publishMqtt(this.baseTopic+'status', 'offline')
|
||||
debug('Disconnected from device ' + this.toString())
|
||||
})
|
||||
|
||||
// On connect error call reconnect
|
||||
this.device.on('error', (err) => {
|
||||
this.device.on('error', async (err) => {
|
||||
debugError(err)
|
||||
if (err.message === 'Error from socket') {
|
||||
await utils.sleep(1)
|
||||
if (!this.device.isConnected()) {
|
||||
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) {
|
||||
if (typeof data.dps != 'undefined') {
|
||||
// Update cached device state data
|
||||
@@ -148,7 +161,7 @@ class TuyaDevice {
|
||||
}
|
||||
data = JSON.stringify(data)
|
||||
const dpsStateTopic = dpsTopic + '/state'
|
||||
debugMqtt('MQTT DPS JSON: ' + dpsStateTopic + ' -> ', data)
|
||||
debugState('MQTT DPS JSON: ' + dpsStateTopic + ' -> ', data)
|
||||
this.publishMqtt(dpsStateTopic, data, false)
|
||||
|
||||
// Publish dps/<#>/state value for each device DPS
|
||||
@@ -156,7 +169,7 @@ class TuyaDevice {
|
||||
if (this.state.dps[key].updated) {
|
||||
const dpsKeyTopic = dpsTopic + '/' + key + '/state'
|
||||
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.state.dps[key].updated = false
|
||||
}
|
||||
@@ -193,7 +206,7 @@ class TuyaDevice {
|
||||
return state
|
||||
}
|
||||
|
||||
// Parse the received state value based on deviceTopic config
|
||||
// Parse the received state numeric value based on deviceTopic rules
|
||||
parseStateNumber(value, deviceTopic) {
|
||||
// Check if it's a number and it's not outside of defined range
|
||||
if (isNaN(value)) {
|
||||
@@ -204,14 +217,14 @@ class TuyaDevice {
|
||||
switch (deviceTopic.type) {
|
||||
case 'int':
|
||||
if (deviceTopic.stateMath) {
|
||||
value = parseInt(Math.round(eval(value+deviceTopic.stateMath)))
|
||||
value = parseInt(Math.round(evaluate(value+deviceTopic.stateMath)))
|
||||
} else {
|
||||
value = parseInt(value)
|
||||
}
|
||||
break;
|
||||
case 'float':
|
||||
if (deviceTopic.stateMath) {
|
||||
value = parseFloat(eval(value+deviceTopic.stateMath))
|
||||
value = parseFloat(evaluate(value+deviceTopic.stateMath))
|
||||
} else {
|
||||
value = parseFloat(value)
|
||||
}
|
||||
@@ -222,12 +235,12 @@ class TuyaDevice {
|
||||
}
|
||||
|
||||
// Process MQTT commands for all command topics at device level
|
||||
async processCommand(message, commandTopic) {
|
||||
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()
|
||||
debugCommand('Received command: ', command)
|
||||
this.getStates()
|
||||
} else {
|
||||
// Call device specific command topic handler
|
||||
this.processDeviceCommand(message, commandTopic)
|
||||
@@ -241,14 +254,14 @@ class TuyaDevice {
|
||||
const deviceTopic = this.deviceTopics.hasOwnProperty(stateTopic) ? this.deviceTopics[stateTopic] : ''
|
||||
|
||||
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)
|
||||
let commandResult = this.sendTuyaCommand(command, deviceTopic)
|
||||
if (!commandResult) {
|
||||
debug('Command topic '+this.baseTopic+commandTopic+' received invalid value: '+command)
|
||||
debugCommand('Command topic '+this.baseTopic+commandTopic+' received invalid value: '+command)
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
||||
@@ -258,7 +271,7 @@ class TuyaDevice {
|
||||
let command
|
||||
|
||||
if (message != '1' && message != '0' && utils.isJsonString(message)) {
|
||||
debugMqtt('MQTT message is JSON')
|
||||
debugCommand('MQTT message is JSON')
|
||||
command = JSON.parse(message);
|
||||
} else {
|
||||
switch(message.toLowerCase()) {
|
||||
@@ -284,20 +297,20 @@ class TuyaDevice {
|
||||
processDpsCommand(message) {
|
||||
if (utils.isJsonString(message)) {
|
||||
const tuyaCommand = this.getCommandFromMessage(message)
|
||||
debugMqtt('Received command: '+tuyaCommand)
|
||||
debugCommand('Received command: '+tuyaCommand)
|
||||
this.set(tuyaCommand)
|
||||
} 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
|
||||
processDpsKeyCommand(message, dpsKey) {
|
||||
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 {
|
||||
const dpsMessage = this.parseDpsMessage(message)
|
||||
debugMqtt('Received command for DPS'+dpsKey+': ', message)
|
||||
debugCommand('Received command for DPS'+dpsKey+': ', message)
|
||||
const tuyaCommand = {
|
||||
dps: dpsKey,
|
||||
set: dpsMessage
|
||||
@@ -325,7 +338,13 @@ class TuyaDevice {
|
||||
this.connected = false
|
||||
for (let topic in this.deviceTopics) {
|
||||
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
|
||||
// Force topic update now that all states are fully in sync
|
||||
@@ -390,14 +409,14 @@ class TuyaDevice {
|
||||
switch (deviceTopic.type) {
|
||||
case 'int':
|
||||
if (deviceTopic.commandMath) {
|
||||
value = parseInt(Math.round(eval(command+deviceTopic.commandMath)))
|
||||
value = parseInt(Math.round(evaluate(command+deviceTopic.commandMath)))
|
||||
} else {
|
||||
value = parseInt(command)
|
||||
}
|
||||
break;
|
||||
case 'float':
|
||||
if (deviceTopic.commandMath) {
|
||||
value = parseFloat(eval(command+deviceTopic.commandMath))
|
||||
value = parseFloat(evaluate(command+deviceTopic.commandMath))
|
||||
} else {
|
||||
value = parseFloat(command)
|
||||
}
|
||||
@@ -421,7 +440,7 @@ class TuyaDevice {
|
||||
// Convert from Hex to Decimal and cache values
|
||||
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.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
|
||||
@@ -491,12 +510,16 @@ class TuyaDevice {
|
||||
|
||||
// Set white/colour mode based on
|
||||
async setLight(topic, command) {
|
||||
const currentMode = this.state.dps[this.config.dpsMode].val
|
||||
|
||||
let targetMode = undefined
|
||||
const currentMode = this.config.dpsMode.val
|
||||
|
||||
if (topic.key === this.config.dpsWhiteValue || topic.key === this.config.dpsColorTemp) {
|
||||
// If setting white level or color temperature, light should be in white mode
|
||||
targetMode = 'white'
|
||||
} 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 saturation is < 10 then white mode
|
||||
targetMode = 'white'
|
||||
@@ -505,17 +528,18 @@ class TuyaDevice {
|
||||
targetMode = 'colour'
|
||||
}
|
||||
}
|
||||
// If mode change required, add it to the set command
|
||||
if (targetMode && currentMode !== targetMode) {
|
||||
command = {
|
||||
multiple: true,
|
||||
data: {
|
||||
[command.dps]: command.set,
|
||||
[this.config.dpsMode]: targetMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the proper value
|
||||
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
|
||||
@@ -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
|
||||
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)
|
||||
if (this.connected) { return }
|
||||
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
|
||||
publishMqtt(topic, message, isDebug) {
|
||||
if (isDebug) { debugMqtt(topic, message) }
|
||||
if (isDebug) { debugState(topic, message) }
|
||||
this.mqttClient.publish(topic, message, { qos: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@ class Utils
|
||||
return new Promise(res => setTimeout(res, sec*1000))
|
||||
}
|
||||
|
||||
msSleep(ms) {
|
||||
return new Promise(res => setTimeout(res, ms))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = new Utils()
|
||||
68
package-lock.json
generated
68
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuya-mqtt",
|
||||
"version": "3.0.0-beta3",
|
||||
"version": "3.0.0-beta4",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -148,9 +148,9 @@
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.5.tgz",
|
||||
"integrity": "sha512-jVFzDV6NTbrLMxm4xDSIW/gKnk8rQLF9wAzLWIOg+5nU6ACrIMndeBdXci0FGtqJbP9tQvm6V39eshc96TO2wQ=="
|
||||
"version": "14.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.8.tgz",
|
||||
"integrity": "sha512-KPcKqKm5UKDkaYPTuXSx8wEP7vE9GnuaXIZKijwRYcePpZFDVuy2a57LarFKiORbHOuTOOwYzxVxcUzsh2P2Pw=="
|
||||
},
|
||||
"@types/responselike": {
|
||||
"version": "1.0.0",
|
||||
@@ -487,6 +487,11 @@
|
||||
"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": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -543,6 +548,11 @@
|
||||
"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": {
|
||||
"version": "5.0.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.5",
|
||||
"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": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
@@ -1271,6 +1291,11 @@
|
||||
"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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
@@ -1387,6 +1412,21 @@
|
||||
"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": {
|
||||
"version": "1.6.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz",
|
||||
@@ -2178,6 +2223,11 @@
|
||||
"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": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
||||
@@ -2211,8 +2261,9 @@
|
||||
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw=="
|
||||
},
|
||||
"tuyapi": {
|
||||
"version": "github:tsightler/tuyapi#5e99e36a41be43768451ff844375abfb6061ad5e",
|
||||
"from": "github:tsightler/tuyapi#bugfix-null-get",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tuyapi/-/tuyapi-6.0.1.tgz",
|
||||
"integrity": "sha512-2Qg0/avg3gtsDLRIktssFtxUA/WT6fvB+GCmQwb1uRQq6KoTsS9he2vDGzmExwaek8Fz3vgAACH7WNIRemCgDw==",
|
||||
"requires": {
|
||||
"debug": "4.1.1",
|
||||
"p-queue": "6.6.1",
|
||||
@@ -2240,6 +2291,11 @@
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.10.0.tgz",
|
||||
"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": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuya-mqtt",
|
||||
"version": "3.0.0-beta3",
|
||||
"version": "3.0.0-beta4",
|
||||
"description": "Control Tuya devices locally via MQTT",
|
||||
"homepage": "https://github.com/TheAgentK/tuya-mqtt#readme",
|
||||
"main": "tuya-mqtt.js",
|
||||
@@ -19,7 +19,8 @@
|
||||
"json5": "^2.1.3",
|
||||
"mqtt": "^4.2.1",
|
||||
"supports-color": "^7.2.0",
|
||||
"tuyapi": "github:tsightler/tuyapi#bugfix-null-get"
|
||||
"tuyapi": "^6.0.1",
|
||||
"mathjs": "7.5.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
22
tuya-mqtt.js
22
tuya-mqtt.js
@@ -2,7 +2,8 @@
|
||||
const fs = require('fs')
|
||||
const mqtt = require('mqtt')
|
||||
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 SimpleSwitch = require('./devices/simple-switch')
|
||||
const SimpleDimmer = require('./devices/simple-dimmer')
|
||||
@@ -28,23 +29,14 @@ function getDevice(configDevice, mqttClient) {
|
||||
case 'RGBTWLight':
|
||||
return new RGBTWLight(deviceInfo)
|
||||
break;
|
||||
case 'GenericDevice':
|
||||
return new GenericDevice(deviceInfo)
|
||||
break;
|
||||
}
|
||||
return null
|
||||
return new GenericDevice(deviceInfo)
|
||||
}
|
||||
|
||||
function initDevices(configDevices, mqttClient) {
|
||||
for (let configDevice of configDevices) {
|
||||
if (!configDevice.type) {
|
||||
debug('Device type not specified, skipping creation of this device')
|
||||
} else {
|
||||
const newDevice = getDevice(configDevice, mqttClient)
|
||||
if (newDevice) {
|
||||
tuyaDevices.push(newDevice)
|
||||
}
|
||||
}
|
||||
const newDevice = getDevice(configDevice, mqttClient)
|
||||
tuyaDevices.push(newDevice)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +54,7 @@ const main = async() => {
|
||||
}
|
||||
|
||||
if (typeof CONFIG.qos == 'undefined') {
|
||||
CONFIG.qos = 2
|
||||
CONFIG.qos = 1
|
||||
}
|
||||
if (typeof CONFIG.retain == 'undefined') {
|
||||
CONFIG.retain = false
|
||||
@@ -121,7 +113,7 @@ const main = async() => {
|
||||
|
||||
// If it looks like a valid command topic try to process it
|
||||
if (commandTopic.includes('command')) {
|
||||
debug('Received MQTT message -> ', JSON.stringify({
|
||||
debugCommand('Received MQTT message -> ', JSON.stringify({
|
||||
topic: topic,
|
||||
message: message
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user