mirror of
https://github.com/lehanspb/tuya-mqtt.git
synced 2025-12-16 09:44:36 +00:00
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:
@@ -4,14 +4,17 @@ const utils = require('../lib/utils')
|
||||
|
||||
class RGBTWLight extends TuyaDevice {
|
||||
async init() {
|
||||
await this.guessLightInfo()
|
||||
|
||||
// Set device specific variables
|
||||
this.config.dpsPower = this.config.dpsPower ? this.config.dpsPower : 1
|
||||
this.config.dpsMode = this.config.dpsMode ? this.config.dpsMode : 2
|
||||
this.config.dpsWhiteValue = this.config.dpsWhiteValue ? this.config.dpsWhiteValue : 3
|
||||
this.config.whiteValueScale = this.config.whiteValueScale ? this.config.whiteValueScale : 1000
|
||||
this.config.dpsColorTemp = this.config.dpsColorTemp ? this.config.dpsColorTemp : 4
|
||||
this.config.dpsColor = this.config.dpsColor ? this.config.dpsColor : 5
|
||||
this.config.colorType = this.config.colorType ? this.config.colorType : 'hsbhex'
|
||||
this.config.dpsPower = this.config.dpsPower ? this.config.dpsPower : this.guess.dpsPower
|
||||
this.config.dpsMode = this.config.dpsMode ? this.config.dpsMode : this.guess.dpsMode
|
||||
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.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'
|
||||
|
||||
@@ -26,9 +29,11 @@ class RGBTWLight extends TuyaDevice {
|
||||
white_value_state: {
|
||||
key: this.config.dpsWhiteValue,
|
||||
type: 'int',
|
||||
min: (this.config.whiteValueScale == 1000) ? 10 : 1,
|
||||
max: this.config.whiteValueScale,
|
||||
scale: this.config.whiteValueScale
|
||||
min: 1,
|
||||
max: 100,
|
||||
scale: this.config.whiteValueScale,
|
||||
stateMath: (this.config.whiteValueScale == 1000) ? '/10' : '/2.55',
|
||||
commandMath: (this.config.whiteValueScale == 1000) ? '*10' : '*2.55'
|
||||
},
|
||||
hs_state: {
|
||||
key: this.config.dpsColor,
|
||||
@@ -68,12 +73,12 @@ class RGBTWLight extends TuyaDevice {
|
||||
command_topic: this.baseTopic+'command',
|
||||
brightness_state_topic: this.baseTopic+'brightness_state',
|
||||
brightness_command_topic: this.baseTopic+'brightness_command',
|
||||
brightness_scale: 1000,
|
||||
brightness_scale: 100,
|
||||
hs_state_topic: this.baseTopic+'hs_state',
|
||||
hs_command_topic: this.baseTopic+'hs_command',
|
||||
white_value_state_topic: this.baseTopic+'white_value_state',
|
||||
white_value_command_topic: this.baseTopic+'white_value_command',
|
||||
white_value_scale: 1000,
|
||||
white_value_scale: 100,
|
||||
unique_id: this.config.id,
|
||||
device: this.deviceData
|
||||
}
|
||||
@@ -82,6 +87,41 @@ class RGBTWLight extends TuyaDevice {
|
||||
debug(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
|
||||
@@ -54,11 +54,13 @@ class TuyaDevice {
|
||||
|
||||
// Listen for device data and call update DPS function if valid
|
||||
this.device.on('data', (data) => {
|
||||
if (typeof data == 'string') {
|
||||
debug('Data from device not encrypted:', data.replace(/[^a-zA-Z0-9 ]/g, ''))
|
||||
} else {
|
||||
debug('Data from device '+this.options.id+' ->', data.dps)
|
||||
if (typeof data === 'object') {
|
||||
debug('Received JSON data from device '+this.options.id+' ->', 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, ''))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -173,7 +175,7 @@ class TuyaDevice {
|
||||
break;
|
||||
case 'int':
|
||||
case 'float':
|
||||
state = value ? value.toString() : ''
|
||||
state = this.parseStateNumber(value, deviceTopic)
|
||||
break;
|
||||
case 'hsb':
|
||||
case 'hsbhex':
|
||||
@@ -191,6 +193,34 @@ class TuyaDevice {
|
||||
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
|
||||
async processCommand(message, commandTopic) {
|
||||
const command = this.getCommandFromMessage(message)
|
||||
@@ -213,8 +243,8 @@ class TuyaDevice {
|
||||
if (deviceTopic) {
|
||||
debug('Device '+this.options.id+' received command topic: '+commandTopic+', message: '+message)
|
||||
const command = this.getCommandFromMessage(message)
|
||||
let setResult = this.setTuyaState(command, deviceTopic)
|
||||
if (!setResult) {
|
||||
let commandResult = this.sendTuyaCommand(command, deviceTopic)
|
||||
if (!commandResult) {
|
||||
debug('Command topic '+this.baseTopic+commandTopic+' received invalid value: '+command)
|
||||
}
|
||||
} else {
|
||||
@@ -303,7 +333,7 @@ class TuyaDevice {
|
||||
}
|
||||
|
||||
// Set state based on command topic
|
||||
setTuyaState(command, deviceTopic) {
|
||||
sendTuyaCommand(command, deviceTopic) {
|
||||
const tuyaCommand = new Object()
|
||||
tuyaCommand.dps = deviceTopic.key
|
||||
switch (deviceTopic.type) {
|
||||
@@ -320,17 +350,7 @@ class TuyaDevice {
|
||||
break;
|
||||
case 'int':
|
||||
case 'float':
|
||||
if (isNaN(command)) {
|
||||
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)
|
||||
}
|
||||
tuyaCommand.set = this.parseCommandNumber(command, deviceTopic)
|
||||
break;
|
||||
case 'hsb':
|
||||
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
|
||||
// updates cached HSB color state for device
|
||||
updateColorState(value) {
|
||||
@@ -361,13 +415,13 @@ class TuyaDevice {
|
||||
[, 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.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 {
|
||||
[, 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.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 = 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
|
||||
@@ -397,17 +451,17 @@ class TuyaDevice {
|
||||
getTuyaHsbColor() {
|
||||
// Convert new HSB color to Tuya style HSB format
|
||||
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
|
||||
}
|
||||
|
||||
// Returns Tuya HSBHEX format value from current setColor HSB value
|
||||
getTuyaHsbHexColor() {
|
||||
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;
|
||||
s /= 100;
|
||||
b *= .255;
|
||||
b *= 2.55;
|
||||
const
|
||||
i = Math.floor(h),
|
||||
f = h - i,
|
||||
@@ -435,21 +489,19 @@ class TuyaDevice {
|
||||
return hex + hsb;
|
||||
}
|
||||
|
||||
// Set white/colour mode based on target mode
|
||||
// Set white/colour mode based on
|
||||
async setLight(topic, command) {
|
||||
const currentMode = this.state.dps[this.config.dpsMode].val
|
||||
let targetMode = undefined
|
||||
if (topic.key === this.config.dpsWhiteValue) {
|
||||
// If setting white level, light should be in white mode
|
||||
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) {
|
||||
if (this.state.setColor.s === 0 && this.state.setColor.s !== this.state.color.s) {
|
||||
// If setting saturation to 0 and not already zero, target mode is 'white'
|
||||
if (this.state.setColor.s < 10) {
|
||||
// If saturation is < 10 then white mode
|
||||
targetMode = 'white'
|
||||
} else if ((this.state.setColor.s > 0 && this.state.setColor.s !== this.state.color.s) ||
|
||||
this.state.setColor.h !== this.state.color.h ||
|
||||
this.state.setColor.b !== this.state.color.b) {
|
||||
// If setting saturation > 0, or changing any other color value, target mode is 'colour'
|
||||
} else {
|
||||
// If saturation > 0 and changing hue, set color mode
|
||||
targetMode = 'colour'
|
||||
}
|
||||
}
|
||||
|
||||
1736
package-lock.json
generated
1736
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuya-mqtt",
|
||||
"version": "3.0.0-beta2",
|
||||
"version": "3.0.0-beta3",
|
||||
"description": "Control Tuya devices locally via MQTT",
|
||||
"homepage": "https://github.com/TheAgentK/tuya-mqtt#readme",
|
||||
"main": "tuya-mqtt.js",
|
||||
@@ -13,11 +13,13 @@
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@tuyapi/cli": "^1.13.4",
|
||||
"color-convert": "^2.0.1",
|
||||
"debug": "^4.1.1",
|
||||
"json5": "^2.1.3",
|
||||
"mqtt": "^4.2.1",
|
||||
"tuyapi": "github:tsightler/tuyAPI",
|
||||
"json5": "^2.1.3"
|
||||
"supports-color": "^7.2.0",
|
||||
"tuyapi": "github:tsightler/tuyapi#bugfix-null-get"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user