mirror of
https://github.com/lehanspb/tuya-mqtt.git
synced 2025-12-18 16:17:30 +00:00
Reorganize tuya-device
This commit is contained in:
@@ -36,7 +36,7 @@ class TuyaDevice {
|
|||||||
// Variables to hold device state data
|
// Variables to hold device state data
|
||||||
this.dps = {} // Current dps state data for device
|
this.dps = {} // Current dps state data for device
|
||||||
this.dpsPub = {} // Published dps state data for device
|
this.dpsPub = {} // Published dps state data for device
|
||||||
this.color = {h, s, b, t, w} // Current color values (Hue, Saturation, Brightness, White Temp, White Level)
|
this.color = {'h': 0, 's': 0, 'b': 0, 't': 0, 'w': 0} // Current color values (Hue, Saturation, Brightness, White Temp, White Level)
|
||||||
|
|
||||||
// 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) {
|
||||||
@@ -53,10 +53,8 @@ class TuyaDevice {
|
|||||||
if (typeof data == 'string') {
|
if (typeof data == 'string') {
|
||||||
debug('Data from device not encrypted:', data.replace(/[^a-zA-Z0-9 ]/g, ''))
|
debug('Data from device not encrypted:', data.replace(/[^a-zA-Z0-9 ]/g, ''))
|
||||||
} else {
|
} 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)
|
||||||
debug('Data from device '+this.options.id+' ->', data.dps)
|
this.updateDpsData(data)
|
||||||
this.updateDpsData(data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -82,81 +80,13 @@ class TuyaDevice {
|
|||||||
|
|
||||||
// On connect error call reconnect
|
// On connect error call reconnect
|
||||||
this.device.on('error', (err) => {
|
this.device.on('error', (err) => {
|
||||||
if (err !== 'json obj data unvalid') {
|
debugError(err)
|
||||||
debugError(err)
|
|
||||||
}
|
|
||||||
if (err.message === 'Error from socket') {
|
if (err.message === 'Error from socket') {
|
||||||
this.reconnect()
|
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
|
// Update dps properties with device data updates
|
||||||
updateDpsData(data) {
|
updateDpsData(data) {
|
||||||
try {
|
try {
|
||||||
@@ -174,17 +104,19 @@ class TuyaDevice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process MQTT commands for all command topics at device level
|
// Publish device specific state topics
|
||||||
async processCommand(message, commandTopic) {
|
publishTopics() {
|
||||||
const command = this.getCommandFromMessage(message)
|
// Don't publish if device is not connected
|
||||||
if (commandTopic === 'command' && command === 'get-states' ) {
|
if (!this.connected) return
|
||||||
// Handle "get-states" command to update device state
|
|
||||||
debug('Received command: ', command)
|
// Loop through and publish all device specific topics
|
||||||
await this.getStates()
|
for (let topic in this.deviceTopics) {
|
||||||
} else {
|
const state = this.getTopicState(topic)
|
||||||
// Call device specific command topic handler
|
this.publishMqtt(this.baseTopic + topic, state, true)
|
||||||
this.processDeviceCommand(message, commandTopic)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Publish Generic Dps Topics
|
||||||
|
this.publishDpsTopics()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish all dps-values to topic
|
// Publish all dps-values to topic
|
||||||
@@ -212,6 +144,7 @@ class TuyaDevice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the friedly topic state based on DPS value type
|
||||||
getTopicState(topic) {
|
getTopicState(topic) {
|
||||||
const deviceTopic = this.deviceTopics[topic]
|
const deviceTopic = this.deviceTopics[topic]
|
||||||
const key = deviceTopic.key
|
const key = deviceTopic.key
|
||||||
@@ -234,6 +167,118 @@ class TuyaDevice {
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
// Set state based on command topic
|
// Set state based on command topic
|
||||||
setState(command, deviceTopic) {
|
setState(command, deviceTopic) {
|
||||||
const tuyaCommand = new Object()
|
const tuyaCommand = new Object()
|
||||||
@@ -275,101 +320,19 @@ class TuyaDevice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// Takes the current Tuya color and splits it into component parts
|
||||||
// Returns decimal format comma delimeted string of components for selected topic
|
// Updates cached color state for device and returns decimal format
|
||||||
|
// comma delimeted string of components for selected topic
|
||||||
getColorState(value, 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 [, 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 = {
|
this.color.h = parseInt(h, 16)
|
||||||
h: parseInt(h, 16),
|
this.color.s = Math.round(parseInt(s, 16) / 10)
|
||||||
s: Math.round(parseInt(s, 16) / 10),
|
this.color.b = parseInt(b, 16)
|
||||||
b: parseInt(b, 16)
|
|
||||||
}
|
|
||||||
const color = new Array()
|
const color = new Array()
|
||||||
const components = this.deviceTopics[topic].components.split(',')
|
const components = this.deviceTopics[topic].components.split(',')
|
||||||
|
|
||||||
for (let i in components) {
|
for (let i in components) {
|
||||||
if (decimalColor.hasOwnProperty([components[i]])) {
|
if (components.hasOwnProperty([components[i]])) {
|
||||||
color.push(decimalColor[components[i]])
|
color.push(decimalColor[components[i]])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,6 +380,41 @@ class TuyaDevice {
|
|||||||
await this.set(tuyaCommand)
|
await this.set(tuyaCommand)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = TuyaDevice
|
module.exports = TuyaDevice
|
||||||
Reference in New Issue
Block a user