mirror of
https://github.com/lehanspb/tuya-mqtt.git
synced 2025-12-18 00:10:20 +00:00
* Update 3.0.2 * Fix (hopefully) uninitialized key values for devices which block get requests for some DPS values based on operating mode (seen with at least one RGBTW light that will not return color DPS key while in white mode). * Modify HSB/HSBHEX guessing in RGBTW light to attempt to deal with issue above * Implement automatic reconnect when device disconnects socket on it's side * Update package dependencies to latest versions
651 lines
25 KiB
JavaScript
651 lines
25 KiB
JavaScript
const TuyAPI = require('tuyapi')
|
|
const { evaluate } = require('mathjs')
|
|
const utils = 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 {
|
|
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(/\s|\+|#|\//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'
|
|
}
|
|
|
|
// Initialize properties to hold cached device state data
|
|
this.dps = {}
|
|
this.color = {'h': 0, 's': 0, 'b': 0}
|
|
|
|
// Device friendly topics
|
|
this.deviceTopics = {}
|
|
|
|
// Missed heartbeat monitor
|
|
this.heartbeatsMissed = 0
|
|
this.reconnecting = false
|
|
|
|
// 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 === 'object') {
|
|
debug('Received JSON data from device '+this.options.id+' ->', JSON.stringify(data.dps))
|
|
this.updateState(data)
|
|
} else {
|
|
if (data !== 'json obj data unvalid') {
|
|
debug('Received string data from device '+this.options.id+' ->', data.replace(/[^a-zA-Z0-9 ]/g, ''))
|
|
}
|
|
}
|
|
})
|
|
|
|
// Attempt to find/connect to device and start heartbeat monitor
|
|
this.connectDevice()
|
|
this.monitorHeartbeat()
|
|
|
|
// On connect perform device specific 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', async () => {
|
|
this.connected = false
|
|
this.publishMqtt(this.baseTopic+'status', 'offline')
|
|
debug('Disconnected from device ' + this.toString())
|
|
await utils.sleep(5)
|
|
this.reconnect()
|
|
})
|
|
|
|
// On connect error call reconnect
|
|
this.device.on('error', async (err) => {
|
|
debugError(err)
|
|
await utils.sleep(1)
|
|
this.reconnect()
|
|
})
|
|
|
|
// On heartbeat reset heartbeat timer
|
|
this.device.on('heartbeat', () => {
|
|
this.heartbeatsMissed = 0
|
|
})
|
|
}
|
|
|
|
// Get and update cached values of all configured/known dps value for device
|
|
async getStates() {
|
|
// Suppress topic updates while syncing device state with cached state
|
|
this.connected = false
|
|
for (let topic in this.deviceTopics) {
|
|
const key = this.deviceTopics[topic].key
|
|
if (!this.dps[key]) { this.dps[key] = {} }
|
|
try {
|
|
this.dps[key].val = await this.device.get({"dps": key})
|
|
this.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 syncronized
|
|
this.publishTopics()
|
|
}
|
|
|
|
// Update cached DPS values on data updates
|
|
updateState(data) {
|
|
if (typeof data.dps != 'undefined') {
|
|
// Update cached device state data
|
|
for (let key in data.dps) {
|
|
// Only update if the received value is different from previous value
|
|
if (this.dps[key] !== data.dps[key]) {
|
|
this.dps[key] = {
|
|
'val': data.dps[key],
|
|
'updated': true
|
|
}
|
|
}
|
|
if (this.isRgbtwLight) {
|
|
if (this.config.hasOwnProperty('dpsColor') && this.config.dpsColor == key) {
|
|
this.updateColorState(data.dps[key])
|
|
} else if (this.config.hasOwnProperty('dpsMode') && this.config.dpsMode == key) {
|
|
// If color/white mode is changing, force sending color state
|
|
// Allows overriding saturation value to 0% for white mode for the HSB device topics
|
|
this.dps[this.config.dpsColor].updated = true
|
|
}
|
|
}
|
|
}
|
|
if (this.connected) {
|
|
this.publishTopics()
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 deviceTopic = this.deviceTopics[topic]
|
|
const key = deviceTopic.key
|
|
// Only publish values if different from previous value
|
|
if (this.dps[key] && this.dps[key].updated) {
|
|
const state = this.getTopicState(deviceTopic, this.dps[key].val)
|
|
if (state) {
|
|
this.publishMqtt(this.baseTopic + topic, state, true)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Publish Generic Dps Topics
|
|
this.publishDpsTopics()
|
|
}
|
|
|
|
// Publish all dps-values to topic
|
|
publishDpsTopics() {
|
|
try {
|
|
if (!Object.keys(this.dps).length) { return }
|
|
|
|
const dpsTopic = this.baseTopic + 'dps'
|
|
// Publish DPS JSON data if not empty
|
|
let data = {}
|
|
for (let key in this.dps) {
|
|
// Only publish values if different from previous value
|
|
if (this.dps[key].updated) {
|
|
data[key] = this.dps[key].val
|
|
}
|
|
}
|
|
data = JSON.stringify(data)
|
|
const dpsStateTopic = dpsTopic + '/state'
|
|
debugState('MQTT DPS JSON: ' + dpsStateTopic + ' -> ', data)
|
|
this.publishMqtt(dpsStateTopic, data, false)
|
|
|
|
// Publish dps/<#>/state value for each device DPS
|
|
for (let key in this.dps) {
|
|
// Only publish values if different from previous value
|
|
if (this.dps[key].updated) {
|
|
const dpsKeyTopic = dpsTopic + '/' + key + '/state'
|
|
const data = this.dps.hasOwnProperty(key) ? this.dps[key].val.toString() : 'None'
|
|
debugState('MQTT DPS'+key+': '+dpsKeyTopic+' -> ', data)
|
|
this.publishMqtt(dpsKeyTopic, data, false)
|
|
this.dps[key].updated = false
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debugError(e);
|
|
}
|
|
}
|
|
|
|
// Get the friendly topic state based on configured DPS value type
|
|
getTopicState(deviceTopic, value) {
|
|
let state
|
|
switch (deviceTopic.type) {
|
|
case 'bool':
|
|
state = value ? 'ON' : 'OFF'
|
|
break;
|
|
case 'int':
|
|
case 'float':
|
|
state = this.parseNumberState(value, deviceTopic)
|
|
break;
|
|
case 'hsb':
|
|
case 'hsbhex':
|
|
// Return comma separate array of component values for specific topic
|
|
state = new Array()
|
|
const components = deviceTopic.components.split(',')
|
|
for (let i in components) {
|
|
// If light is in white mode always report saturation 0%, otherwise report actual value
|
|
state.push((components[i] === 's' && this.dps[this.config.dpsMode].val === 'white') ? 0 : this.color[components[i]])
|
|
}
|
|
state = (state.join(','))
|
|
break;
|
|
case 'str':
|
|
state = value ? value : ''
|
|
break;
|
|
}
|
|
return state
|
|
}
|
|
|
|
// Parse the received state numeric value based on deviceTopic rules
|
|
parseNumberState(value, deviceTopic) {
|
|
// Check if it's a number and it's not outside of defined range
|
|
if (isNaN(value)) {
|
|
return ''
|
|
}
|
|
|
|
// Perform any required math transforms before returing command value
|
|
switch (deviceTopic.type) {
|
|
case 'int':
|
|
value = (deviceTopic.stateMath) ? parseInt(Math.round(evaluate(value+deviceTopic.stateMath))) : parseInt(value)
|
|
break;
|
|
case 'float':
|
|
value = (deviceTopic.stateMath) ? parseFloat(evaluate(value+deviceTopic.stateMath)) : parseFloat(value)
|
|
break;
|
|
}
|
|
|
|
return value.toString()
|
|
}
|
|
|
|
// Initial processing of MQTT commands for all command topics
|
|
processCommand(message, commandTopic) {
|
|
let command
|
|
if (utils.isJsonString(message)) {
|
|
debugCommand('Received MQTT command message is a JSON string')
|
|
command = JSON.parse(message);
|
|
} else {
|
|
debugCommand('Received MQTT command message is a text string')
|
|
command = message.toLowerCase()
|
|
}
|
|
|
|
// If get-states command, then updates all states and re-publish topics
|
|
if (commandTopic === 'command' && command === 'get-states') {
|
|
// Handle "get-states" command to update device state
|
|
debugCommand('Received command: ', command)
|
|
this.getStates()
|
|
} else {
|
|
// Call device specific command topic handler
|
|
this.processDeviceCommand(command, commandTopic)
|
|
}
|
|
}
|
|
|
|
// Process MQTT commands for all device command topics
|
|
processDeviceCommand(command, 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) {
|
|
debugCommand('Device '+this.options.id+' received command topic: '+commandTopic+', message: '+command)
|
|
let commandResult = this.sendTuyaCommand(command, deviceTopic)
|
|
if (!commandResult) {
|
|
debugCommand('Command topic '+this.baseTopic+commandTopic+' received invalid value: '+command)
|
|
}
|
|
} else {
|
|
debugCommand('Invalid command topic '+this.baseTopic+commandTopic+' for device id: '+this.config.id)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Process Tuya JSON commands via DPS command topic
|
|
processDpsCommand(message) {
|
|
if (utils.isJsonString(message)) {
|
|
const command = JSON.parse(message)
|
|
debugCommand('Parsed Tuya JSON command: '+JSON.stringify(command))
|
|
this.set(command)
|
|
} else {
|
|
debugCommand('DPS command topic requires Tuya style JSON value')
|
|
}
|
|
}
|
|
|
|
// Process text based Tuya commands via DPS key command topics
|
|
processDpsKeyCommand(message, dpsKey) {
|
|
if (utils.isJsonString(message)) {
|
|
debugCommand('Individual DPS command topics do not accept JSON values')
|
|
} else {
|
|
const dpsMessage = this.parseDpsMessage(message)
|
|
debugCommand('Received command for DPS'+dpsKey+': ', message)
|
|
const command = {
|
|
dps: dpsKey,
|
|
set: dpsMessage
|
|
}
|
|
this.set(command)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// Set state based on command topic
|
|
sendTuyaCommand(message, deviceTopic) {
|
|
let command = message.toLowerCase()
|
|
const tuyaCommand = new Object()
|
|
tuyaCommand.dps = deviceTopic.key
|
|
switch (deviceTopic.type) {
|
|
case 'bool':
|
|
if (command === 'toggle') {
|
|
tuyaCommand.set = !this.dps[tuyaCommand.dps].val
|
|
} else {
|
|
command = this.parseBoolCommand(command)
|
|
if (typeof command.set === 'boolean') {
|
|
tuyaCommand.set = command.set
|
|
} else {
|
|
tuyaCommand.set = '!!!INVALID!!!'
|
|
}
|
|
}
|
|
break;
|
|
case 'int':
|
|
case 'float':
|
|
tuyaCommand.set = this.parseNumberCommand(command, deviceTopic)
|
|
break;
|
|
case 'hsb':
|
|
this.updateCommandColor(command, deviceTopic.components)
|
|
tuyaCommand.set = this.parseTuyaHsbColor()
|
|
break;
|
|
case 'hsbhex':
|
|
this.updateCommandColor(command, deviceTopic.components)
|
|
tuyaCommand.set = this.parseTuyaHsbHexColor()
|
|
break;
|
|
default:
|
|
// If type is not one of the above just use the raw string as is
|
|
tuyaCommand.set = message
|
|
}
|
|
if (tuyaCommand.set === '!!!INVALID!!!') {
|
|
return false
|
|
} else {
|
|
if (this.isRgbtwLight) {
|
|
this.setLight(deviceTopic, tuyaCommand)
|
|
} else {
|
|
this.set(tuyaCommand)
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Convert simple bool commands to true/false
|
|
parseBoolCommand(command) {
|
|
switch(command) {
|
|
case 'on':
|
|
case 'off':
|
|
case '0':
|
|
case '1':
|
|
case 'true':
|
|
case 'false':
|
|
return {
|
|
set: (command === 'on' || command === '1' || command === 'true' || command === 1) ? true : false
|
|
}
|
|
default:
|
|
return command
|
|
}
|
|
}
|
|
|
|
// Validate/transform set interger values
|
|
parseNumberCommand(command, deviceTopic) {
|
|
let value = undefined
|
|
const invalid = '!!!INVALID!!!'
|
|
|
|
// Check if it's a number and it's not outside of defined range
|
|
if (isNaN(command)) {
|
|
return invalid
|
|
} else if (deviceTopic.hasOwnProperty('topicMin') && command < deviceTopic.topicMin) {
|
|
debugError('Received command value "'+command+'" that is less than the configured minimum value')
|
|
debugError('Overriding command with minimum value '+deviceTopic.topicMin)
|
|
command = deviceTopic.topicMin
|
|
} else if (deviceTopic.hasOwnProperty('topicMax') && command > deviceTopic.topicMax) {
|
|
debugError('Received command value "'+command+'" that is greater than the configured maximum value')
|
|
debugError('Overriding command with maximum value: '+deviceTopic.topicMax)
|
|
command = deviceTopic.topicMax
|
|
}
|
|
|
|
// Perform any required math transforms before returing command value
|
|
switch (deviceTopic.type) {
|
|
case 'int':
|
|
if (deviceTopic.commandMath) {
|
|
value = parseInt(Math.round(evaluate(command+deviceTopic.commandMath)))
|
|
} else {
|
|
value = parseInt(command)
|
|
}
|
|
break;
|
|
case 'float':
|
|
if (deviceTopic.commandMath) {
|
|
value = parseFloat(evaluate(command+deviceTopic.commandMath))
|
|
} else {
|
|
value = parseFloat(command)
|
|
}
|
|
break;
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
// Takes Tuya color value in HSB or HSBHEX format and updates cached HSB color state for device
|
|
// Credit homebridge-tuya project for HSB/HSBHEX conversion code
|
|
updateColorState(value) {
|
|
let h, s, b
|
|
if (this.config.colorType === 'hsbhex') {
|
|
[, h, s, b] = (value || '0000000000ffff').match(/^.{6}([0-9a-f]{4})([0-9a-f]{2})([0-9a-f]{2})$/i) || [0, '0', 'ff', 'ff'];
|
|
this.color.h = parseInt(h, 16)
|
|
this.color.s = Math.round(parseInt(s, 16) / 2.55) // Convert saturation to 100 scale
|
|
this.color.b = Math.round(parseInt(b, 16) / 2.55) // Convert brightness to 100 scale
|
|
} else {
|
|
[, h, s, b] = (value || '000003e803e8').match(/^([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})$/i) || [0, '0', '3e8', '3e8']
|
|
// Convert from Hex to Decimal and cache values
|
|
this.color.h = parseInt(h, 16)
|
|
this.color.s = Math.round(parseInt(s, 16) / 10) // Convert saturation to 100 Scale
|
|
this.color.b = Math.round(parseInt(b, 16) / 10) // Convert brightness to 100 scale
|
|
}
|
|
|
|
// Initialize the command color values with existing color state
|
|
if (!this.hasOwnProperty('cmdColor')) {
|
|
this.cmdColor = {
|
|
'h': this.color.h,
|
|
's': this.color.s,
|
|
'b': this.color.b
|
|
}
|
|
}
|
|
}
|
|
|
|
// Caches color updates when HSB components have separate device topics
|
|
// cmdColor property always contains the desired HSB color state based on received
|
|
// command topic messages vs actual device color state, which may be pending
|
|
updateCommandColor(value, components) {
|
|
// Update any HSB component with a changed value
|
|
components = components.split(',')
|
|
const values = value.split(',')
|
|
for (let i in components) {
|
|
this.cmdColor[components[i]] = Math.round(values[i])
|
|
}
|
|
}
|
|
|
|
// Returns Tuya HSB format value from current cmdColor HSB values
|
|
// Credit homebridge-tuya project for HSB conversion code
|
|
parseTuyaHsbColor() {
|
|
let {h, s, b} = this.cmdColor
|
|
const hexColor = h.toString(16).padStart(4, '0') + (10 * s).toString(16).padStart(4, '0') + (10 * b).toString(16).padStart(4, '0')
|
|
return hexColor
|
|
}
|
|
|
|
// Returns Tuya HSBHEX format value from current cmdColor HSB values
|
|
// Credit homebridge-tuya project for HSBHEX conversion code
|
|
parseTuyaHsbHexColor() {
|
|
let {h, s, b} = this.cmdColor
|
|
const hsb = h.toString(16).padStart(4, '0') + Math.round(2.55 * s).toString(16).padStart(2, '0') + Math.round(2.55 * b).toString(16).padStart(2, '0');
|
|
h /= 60;
|
|
s /= 100;
|
|
b *= 2.55;
|
|
const
|
|
i = Math.floor(h),
|
|
f = h - i,
|
|
p = b * (1 - s),
|
|
q = b * (1 - s * f),
|
|
t = b * (1 - s * (1 - f)),
|
|
rgb = (() => {
|
|
switch (i % 6) {
|
|
case 0:
|
|
return [b, t, p];
|
|
case 1:
|
|
return [q, b, p];
|
|
case 2:
|
|
return [p, b, t];
|
|
case 3:
|
|
return [p, q, b];
|
|
case 4:
|
|
return [t, p, b];
|
|
case 5:
|
|
return [b, p, q];
|
|
}
|
|
})().map(c => Math.round(c).toString(16).padStart(2, '0')),
|
|
hex = rgb.join('');
|
|
|
|
return hex + hsb;
|
|
}
|
|
|
|
// Set white/colour mode based on received commands
|
|
async setLight(topic, command) {
|
|
let targetMode = undefined
|
|
|
|
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) {
|
|
// Split device topic HSB components into array
|
|
const components = topic.components.split(',')
|
|
|
|
// If device topic inlucdes saturation check for changes
|
|
if (components.includes('s')) {
|
|
if (this.cmdColor.s < 10) {
|
|
// Saturation changed to < 10% = white mode
|
|
targetMode = 'white'
|
|
} else {
|
|
// Saturation changed to >= 10% = color mode
|
|
targetMode = 'colour'
|
|
}
|
|
} else {
|
|
// For other cases stay in existing mode
|
|
targetMode = this.dps[this.config.dpsMode].val
|
|
}
|
|
}
|
|
|
|
// Send the issued command
|
|
this.set(command)
|
|
|
|
// Make sure the bulb stays in the correct mode
|
|
if (targetMode) {
|
|
command = {
|
|
dps: this.config.dpsMode,
|
|
set: targetMode
|
|
}
|
|
this.set(command)
|
|
}
|
|
}
|
|
|
|
// 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+' -> '+JSON.stringify(command))
|
|
return new Promise((resolve, reject) => {
|
|
this.device.set(command).then((result) => {
|
|
resolve(result)
|
|
})
|
|
})
|
|
}
|
|
|
|
// Search for and connect to device
|
|
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() {
|
|
if (!this.device.isConnected() && !this.reconnecting) {
|
|
this.reconnecting = true
|
|
debugError('Error connecting to device id '+this.options.id+'...retry in 10 seconds.')
|
|
await utils.sleep(10)
|
|
if (this.connected) { return }
|
|
this.connectDevice()
|
|
this.reconnecting = false
|
|
}
|
|
}
|
|
|
|
// Republish device discovery/state data (used for Home Assistant state topic)
|
|
async republish() {
|
|
const status = (this.device.isConnected()) ? 'online' : 'offline'
|
|
this.publishMqtt(this.baseTopic+'status', status)
|
|
await utils.sleep(1)
|
|
this.init()
|
|
}
|
|
|
|
// Republish device discovery/state data (used for Home Assistant state topic)
|
|
async republish() {
|
|
const status = (this.device.isConnected()) ? 'online' : 'offline'
|
|
this.publishMqtt(this.baseTopic+'status', status)
|
|
await utils.sleep(1)
|
|
this.init()
|
|
}
|
|
|
|
// 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) { debugState(topic, message) }
|
|
this.mqttClient.publish(topic, message, { qos: 1 });
|
|
}
|
|
}
|
|
|
|
module.exports = TuyaDevice |