3.0.0-beta3

* Improve RGBTW white/color logic
* Rebase MQTT topic brightess to 100 scale (vs 255/1000)
* Added based auto-discovery for RGBTW light
* Added math functions
This commit is contained in:
tsightler
2020-10-07 08:26:13 -04:00
parent 38d3092af3
commit d5217ce237
4 changed files with 1865 additions and 63 deletions

View File

@@ -4,14 +4,17 @@ const utils = require('../lib/utils')
class RGBTWLight extends TuyaDevice { class RGBTWLight extends TuyaDevice {
async init() { async init() {
await this.guessLightInfo()
// Set device specific variables // Set device specific variables
this.config.dpsPower = this.config.dpsPower ? this.config.dpsPower : 1 this.config.dpsPower = this.config.dpsPower ? this.config.dpsPower : this.guess.dpsPower
this.config.dpsMode = this.config.dpsMode ? this.config.dpsMode : 2 this.config.dpsMode = this.config.dpsMode ? this.config.dpsMode : this.guess.dpsMode
this.config.dpsWhiteValue = this.config.dpsWhiteValue ? this.config.dpsWhiteValue : 3 this.config.dpsWhiteValue = this.config.dpsWhiteValue ? this.config.dpsWhiteValue : this.guess.dpsWhiteValue
this.config.whiteValueScale = this.config.whiteValueScale ? this.config.whiteValueScale : 1000 this.config.whiteValueScale = this.config.whiteValueScale ? this.config.whiteValueScale : this.guess.whiteValueScale
this.config.dpsColorTemp = this.config.dpsColorTemp ? this.config.dpsColorTemp : 4 this.config.dpsColorTemp = this.config.dpsColorTemp ? this.config.dpsColorTemp : this.guess.dpsColorTemp
this.config.dpsColor = this.config.dpsColor ? this.config.dpsColor : 5 this.config.dpsColor = this.config.dpsColor ? this.config.dpsColor : this.guess.dpsColor
this.config.colorType = this.config.colorType ? this.config.colorType : 'hsbhex' 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'
@@ -26,9 +29,11 @@ class RGBTWLight extends TuyaDevice {
white_value_state: { white_value_state: {
key: this.config.dpsWhiteValue, key: this.config.dpsWhiteValue,
type: 'int', type: 'int',
min: (this.config.whiteValueScale == 1000) ? 10 : 1, min: 1,
max: this.config.whiteValueScale, max: 100,
scale: this.config.whiteValueScale scale: this.config.whiteValueScale,
stateMath: (this.config.whiteValueScale == 1000) ? '/10' : '/2.55',
commandMath: (this.config.whiteValueScale == 1000) ? '*10' : '*2.55'
}, },
hs_state: { hs_state: {
key: this.config.dpsColor, key: this.config.dpsColor,
@@ -68,12 +73,12 @@ class RGBTWLight extends TuyaDevice {
command_topic: this.baseTopic+'command', command_topic: this.baseTopic+'command',
brightness_state_topic: this.baseTopic+'brightness_state', brightness_state_topic: this.baseTopic+'brightness_state',
brightness_command_topic: this.baseTopic+'brightness_command', brightness_command_topic: this.baseTopic+'brightness_command',
brightness_scale: 1000, brightness_scale: 100,
hs_state_topic: this.baseTopic+'hs_state', hs_state_topic: this.baseTopic+'hs_state',
hs_command_topic: this.baseTopic+'hs_command', hs_command_topic: this.baseTopic+'hs_command',
white_value_state_topic: this.baseTopic+'white_value_state', white_value_state_topic: this.baseTopic+'white_value_state',
white_value_command_topic: this.baseTopic+'white_value_command', white_value_command_topic: this.baseTopic+'white_value_command',
white_value_scale: 1000, white_value_scale: 100,
unique_id: this.config.id, unique_id: this.config.id,
device: this.deviceData device: this.deviceData
} }
@@ -82,6 +87,41 @@ class RGBTWLight extends TuyaDevice {
debug(discoveryData) debug(discoveryData)
this.publishMqtt(configTopic, JSON.stringify(discoveryData)) this.publishMqtt(configTopic, JSON.stringify(discoveryData))
} }
async guessLightInfo() {
this.guess = new Object()
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'
} 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
} else {
this.guess.dpsColorTemp = 0
}
this.guess.dpsColor = 24
const color = await this.device.get({"dps": this.guess.dpsColor})
this.guess.colorType = (color && color.length === 12) ? 'hsb' : 'hsbhex'
}
}
} }
module.exports = RGBTWLight module.exports = RGBTWLight

View File

@@ -54,11 +54,13 @@ 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 == 'string') { if (typeof data === 'object') {
debug('Data from device not encrypted:', data.replace(/[^a-zA-Z0-9 ]/g, '')) debug('Received JSON data from device '+this.options.id+' ->', data.dps)
} else {
debug('Data from device '+this.options.id+' ->', data.dps)
this.updateState(data) 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, ''))
}
} }
}) })
@@ -173,7 +175,7 @@ class TuyaDevice {
break; break;
case 'int': case 'int':
case 'float': case 'float':
state = value ? value.toString() : '' state = this.parseStateNumber(value, deviceTopic)
break; break;
case 'hsb': case 'hsb':
case 'hsbhex': case 'hsbhex':
@@ -191,6 +193,34 @@ class TuyaDevice {
return state return state
} }
// Parse the received state value based on deviceTopic config
parseStateNumber(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':
if (deviceTopic.stateMath) {
value = parseInt(Math.round(eval(value+deviceTopic.stateMath)))
} else {
value = parseInt(value)
}
break;
case 'float':
if (deviceTopic.stateMath) {
value = parseFloat(eval(value+deviceTopic.stateMath))
} else {
value = parseFloat(value)
}
break;
}
return value.toString()
}
// Process MQTT commands for all command topics at device level // Process MQTT commands for all command topics at device level
async processCommand(message, commandTopic) { async processCommand(message, commandTopic) {
const command = this.getCommandFromMessage(message) const command = this.getCommandFromMessage(message)
@@ -213,8 +243,8 @@ class TuyaDevice {
if (deviceTopic) { if (deviceTopic) {
debug('Device '+this.options.id+' received command topic: '+commandTopic+', message: '+message) debug('Device '+this.options.id+' received command topic: '+commandTopic+', message: '+message)
const command = this.getCommandFromMessage(message) const command = this.getCommandFromMessage(message)
let setResult = this.setTuyaState(command, deviceTopic) let commandResult = this.sendTuyaCommand(command, deviceTopic)
if (!setResult) { if (!commandResult) {
debug('Command topic '+this.baseTopic+commandTopic+' received invalid value: '+command) debug('Command topic '+this.baseTopic+commandTopic+' received invalid value: '+command)
} }
} else { } else {
@@ -303,7 +333,7 @@ class TuyaDevice {
} }
// Set state based on command topic // Set state based on command topic
setTuyaState(command, deviceTopic) { sendTuyaCommand(command, deviceTopic) {
const tuyaCommand = new Object() const tuyaCommand = new Object()
tuyaCommand.dps = deviceTopic.key tuyaCommand.dps = deviceTopic.key
switch (deviceTopic.type) { switch (deviceTopic.type) {
@@ -320,17 +350,7 @@ class TuyaDevice {
break; break;
case 'int': case 'int':
case 'float': case 'float':
if (isNaN(command)) { tuyaCommand.set = this.parseCommandNumber(command, deviceTopic)
tuyaCommand.set = '!!!INVALID!!!'
} else if (deviceTopic.hasOwnProperty('min') && deviceTopic.hasOwnProperty('max')) {
if (command >= deviceTopic.min && command <= deviceTopic.max ) {
tuyaCommand.set = deviceTopic.type === 'int' ? parseInt(command) : parseFloat(command)
} else {
tuyaCommand.set = '!!!INVALID!!!'
}
} else {
tuyaCommand.set = deviceTopic.type === 'int' ? parseInt(command) : parseFloat(command)
}
break; break;
case 'hsb': case 'hsb':
this.updateSetColorState(command, deviceTopic.components) this.updateSetColorState(command, deviceTopic.components)
@@ -353,6 +373,40 @@ class TuyaDevice {
} }
} }
// Validate/transform set interger values
parseCommandNumber(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('min') && command < deviceTopic.min) ||
(deviceTopic.hasOwnProperty('max') && command > deviceTopic.max)) {
return invalid
}
// Perform any required math transforms before returing command value
switch (deviceTopic.type) {
case 'int':
if (deviceTopic.commandMath) {
value = parseInt(Math.round(eval(command+deviceTopic.commandMath)))
} else {
value = parseInt(command)
}
break;
case 'float':
if (deviceTopic.commandMath) {
value = parseFloat(eval(command+deviceTopic.commandMath))
} else {
value = parseFloat(command)
}
break;
}
return value
}
// Takes Tuya color value in HSB or HSBHEX format and // Takes Tuya color value in HSB or HSBHEX format and
// updates cached HSB color state for device // updates cached HSB color state for device
updateColorState(value) { updateColorState(value) {
@@ -360,14 +414,14 @@ class TuyaDevice {
if (this.config.colorType === 'hsbhex') { 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']; [, 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.state.color.h = parseInt(h, 16) this.state.color.h = parseInt(h, 16)
this.state.color.s = Math.round(parseInt(s, 16) / 2.55) // Convert saturation to 100 scale this.state.color.s = Math.round(parseInt(s, 16) / 2.55) // Convert saturation to 100 scale
this.state.color.b = Math.round(parseInt(b, 16) / .255) // Convert brightness to 1000 scale this.state.color.b = Math.round(parseInt(b, 16) / 2.55) // Convert brightness to 100 scale
} else { } else {
[, h, s, b] = (value || '000003e803e8').match(/^([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})$/i) || [0, '0', '3e8', '3e8'] [, 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 // 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 = parseInt(b, 16) // Convert brightness to 1000 scale this.state.color.b = Math.round(parseInt(b, 16) / 10) // Convert brightness to 1000 scale
} }
// Initialize the set color values for first time. Used to conflicts // Initialize the set color values for first time. Used to conflicts
@@ -397,17 +451,17 @@ class TuyaDevice {
getTuyaHsbColor() { getTuyaHsbColor() {
// Convert new HSB color to Tuya style HSB format // Convert new HSB color to Tuya style HSB format
let {h, s, b} = this.state.setColor let {h, s, b} = this.state.setColor
const hexColor = h.toString(16).padStart(4, '0') + (10 * s).toString(16).padStart(4, '0') + (b).toString(16).padStart(4, '0') const hexColor = h.toString(16).padStart(4, '0') + (10 * s).toString(16).padStart(4, '0') + (10 * b).toString(16).padStart(4, '0')
return hexColor return hexColor
} }
// Returns Tuya HSBHEX format value from current setColor HSB value // Returns Tuya HSBHEX format value from current setColor HSB value
getTuyaHsbHexColor() { getTuyaHsbHexColor() {
let {h, s, b} = this.state.setColor let {h, s, b} = this.state.setColor
const hsb = h.toString(16).padStart(4, '0') + Math.round(2.55 * s).toString(16).padStart(2, '0') + Math.round(b * .255).toString(16).padStart(2, '0'); 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; h /= 60;
s /= 100; s /= 100;
b *= .255; b *= 2.55;
const const
i = Math.floor(h), i = Math.floor(h),
f = h - i, f = h - i,
@@ -435,21 +489,19 @@ class TuyaDevice {
return hex + hsb; return hex + hsb;
} }
// Set white/colour mode based on target mode // Set white/colour mode based on
async setLight(topic, command) { async setLight(topic, command) {
const currentMode = this.state.dps[this.config.dpsMode].val const currentMode = this.state.dps[this.config.dpsMode].val
let targetMode = undefined let targetMode = undefined
if (topic.key === this.config.dpsWhiteValue) { if (topic.key === this.config.dpsWhiteValue || topic.key === this.config.dpsColorTemp) {
// If setting white level, 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) {
if (this.state.setColor.s === 0 && this.state.setColor.s !== this.state.color.s) { if (this.state.setColor.s < 10) {
// If setting saturation to 0 and not already zero, target mode is 'white' // If saturation is < 10 then white mode
targetMode = 'white' targetMode = 'white'
} else if ((this.state.setColor.s > 0 && this.state.setColor.s !== this.state.color.s) || } else {
this.state.setColor.h !== this.state.color.h || // If saturation > 0 and changing hue, set color mode
this.state.setColor.b !== this.state.color.b) {
// If setting saturation > 0, or changing any other color value, target mode is 'colour'
targetMode = 'colour' targetMode = 'colour'
} }
} }

1736
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "tuya-mqtt", "name": "tuya-mqtt",
"version": "3.0.0-beta2", "version": "3.0.0-beta3",
"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",
@@ -13,11 +13,13 @@
}, },
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@tuyapi/cli": "^1.13.4",
"color-convert": "^2.0.1", "color-convert": "^2.0.1",
"debug": "^4.1.1", "debug": "^4.1.1",
"json5": "^2.1.3",
"mqtt": "^4.2.1", "mqtt": "^4.2.1",
"tuyapi": "github:tsightler/tuyAPI", "supports-color": "^7.2.0",
"json5": "^2.1.3" "tuyapi": "github:tsightler/tuyapi#bugfix-null-get"
}, },
"repository": { "repository": {
"type": "git", "type": "git",