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 {
|
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
|
||||||
@@ -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':
|
||||||
@@ -190,6 +192,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) {
|
||||||
@@ -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
1736
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user