From 8f129617b0aca64d9403eaef7cd14c6d1cf96a92 Mon Sep 17 00:00:00 2001 From: tsightler Date: Tue, 13 Oct 2020 00:39:56 -0400 Subject: [PATCH] Color Tweaks/Cleanups Minor tweaks and misc cleanups preparing for 3.0.0 release --- devices/rgbtw-light.js | 39 ++++++---- devices/tuya-device.js | 165 ++++++++++++++++++++--------------------- 2 files changed, 104 insertions(+), 100 deletions(-) diff --git a/devices/rgbtw-light.js b/devices/rgbtw-light.js index 4cae050..1d13620 100644 --- a/devices/rgbtw-light.js +++ b/devices/rgbtw-light.js @@ -7,14 +7,19 @@ class RGBTWLight extends TuyaDevice { async init() { await this.guessLightInfo() + if (!this.guess.dpsPower && !this.config.dpsPower) { + debug('Automatic discovery of Tuya bulb settings failed and no manual configuration') + return + } + // Set device specific variables 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.minColorTemp = this.config.minColorTemp ? this.config.minColorTemp : 165 - this.config.maxColorTemp = this.config.maxColorTemp ? this.config.maxColorTemp : 375 + this.config.minColorTemp = this.config.minColorTemp ? this.config.minColorTemp : 160 + this.config.maxColorTemp = this.config.maxColorTemp ? this.config.maxColorTemp : 385 this.config.colorTempScale = this.config.colorTempScale ? this.config.colorTempScale : this.guess.colorTempScale this.config.dpsColor = this.config.dpsColor ? this.config.dpsColor : this.guess.dpsColor this.config.colorType = this.config.colorType ? this.config.colorType : this.guess.colorType @@ -29,7 +34,7 @@ class RGBTWLight extends TuyaDevice { key: this.config.dpsPower, type: 'bool' }, - white_value_state: { + white_brightness_state: { key: this.config.dpsWhiteValue, type: 'int', min: 1, @@ -43,7 +48,7 @@ class RGBTWLight extends TuyaDevice { type: this.config.colorType, components: 'h,s' }, - brightness_state: { + color_brightness_state: { key: this.config.dpsColor, type: this.config.colorType, components: 'b' @@ -91,13 +96,13 @@ class RGBTWLight extends TuyaDevice { name: (this.config.name) ? this.config.name : this.config.id, state_topic: this.baseTopic+'state', command_topic: this.baseTopic+'command', - brightness_state_topic: this.baseTopic+'brightness_state', - brightness_command_topic: this.baseTopic+'brightness_command', + brightness_state_topic: this.baseTopic+'color_brightness_state', + brightness_command_topic: this.baseTopic+'color_brightness_command', 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_state_topic: this.baseTopic+'white_brightness_state', + white_value_command_topic: this.baseTopic+'white_brightness_command', white_value_scale: 100, unique_id: this.config.id, device: this.deviceData @@ -119,14 +124,18 @@ class RGBTWLight extends TuyaDevice { this.guess = new Object() debug('Attempting to detect light capabilites and DPS values...') debug('Querying DPS 2 for white/color mode setting...') - let mode = await this.device.get({"dps": 2}) - if (mode && (mode === 'white' || mode === 'colour' || mode.toString().includes('scene'))) { + + // Check if DPS 2 contains typical values for RGBTW light + const mode2 = await this.device.get({"dps": 2}) + const mode21 = await this.device.get({"dps": 21}) + if (mode2 && (mode2 === 'white' || mode2 === 'colour' || mode2.toString().includes('scene'))) { debug('Detected probably Tuya color bulb at DPS 1-5, checking more details...') this.guess = {'dpsPower': 1, 'dpsMode': 2, 'dpsWhiteValue': 3, 'whiteValueScale': 255, 'dpsColorTemp': 4, 'colorTempScale': 255, 'dpsColor': 5} - } else { - debug('Detected likely Tuya color bulb at DPS 20-24, checking more details...') - this.guess = {'dpsPower': 20, 'dpsMode': 21, 'dpsWhiteValue': 22, 'whiteValueScale': 1000, 'dpsColorTemp': 23, 'colorTempScale': 1000, 'dpsColor': 24} + } else if (mode21 && (mode21 === 'white' || mode21 === 'colour' || mode21.toString().includes('scene'))) { + debug('Detected likely Tuya color bulb at DPS 20-24, checking more details...') + this.guess = {'dpsPower': 20, 'dpsMode': 21, 'dpsWhiteValue': 22, 'whiteValueScale': 1000, 'dpsColorTemp': 23, 'colorTempScale': 1000, 'dpsColor': 24} } + if (this.guess.dpsPower) { debug('Attempting to detect if bulb supports color temperature...') const colorTemp = await this.device.get({"dps": this.guess.dpsColorTemp}) @@ -140,9 +149,7 @@ class RGBTWLight extends TuyaDevice { const color = await this.device.get({"dps": this.guess.dpsColor}) this.guess.colorType = (color && color.length === 12) ? 'hsb' : 'hsbhex' debug ('Detected Tuya color format '+this.guess.colorType.toUpperCase()) - } else { - debug('No Tuya color bulb detected, if this device is definitely a Tuya bulb please manually specify settings.') - } + } } } diff --git a/devices/tuya-device.js b/devices/tuya-device.js index 2db749f..c4b3ce2 100644 --- a/devices/tuya-device.js +++ b/devices/tuya-device.js @@ -106,6 +106,25 @@ class TuyaDevice { }) } + // 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 + try { + const result = await this.device.get({"dps": key}) + this.state.dps[key].val = result + this.state.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 in sync + this.publishTopics() + } + // Update cached DPS states on data updates updateState(data) { if (typeof data.dps != 'undefined') { @@ -188,7 +207,7 @@ class TuyaDevice { break; case 'int': case 'float': - state = this.parseStateNumber(value, deviceTopic) + state = this.parseNumberState(value, deviceTopic) break; case 'hsb': case 'hsbhex': @@ -207,7 +226,7 @@ class TuyaDevice { } // Parse the received state numeric value based on deviceTopic rules - parseStateNumber(value, deviceTopic) { + parseNumberState(value, deviceTopic) { // Check if it's a number and it's not outside of defined range if (isNaN(value)) { return '' @@ -216,18 +235,10 @@ class TuyaDevice { // Perform any required math transforms before returing command value switch (deviceTopic.type) { case 'int': - if (deviceTopic.stateMath) { - value = parseInt(Math.round(evaluate(value+deviceTopic.stateMath))) - } else { - value = parseInt(value) - } + value = (deviceTopic.stateMath) ? parseInt(Math.round(evaluate(value+deviceTopic.stateMath))) : value = parseInt(value) break; case 'float': - if (deviceTopic.stateMath) { - value = parseFloat(evaluate(value+deviceTopic.stateMath)) - } else { - value = parseFloat(value) - } + value = (deviceTopic.stateMath) ? parseFloat(evaluate(value+deviceTopic.stateMath)) : value = parseFloat(value) break; } @@ -236,69 +247,49 @@ class TuyaDevice { // Process MQTT commands for all command topics at device level processCommand(message, commandTopic) { - const command = this.getCommandFromMessage(message) + 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 (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(message, commandTopic) + this.processDeviceCommand(command, commandTopic) } } // Process MQTT commands for all command topics at device level - processDeviceCommand(message, commandTopic) { + 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: '+message) - const command = this.getCommandFromMessage(message) + 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: '+this.config.name) + debugCommand('Invalid command topic '+this.baseTopic+commandTopic+' for device id: '+this.config.id) return } } - - // Converts message to TuyAPI JSON commands - getCommandFromMessage(message) { - let command - - if (message != '1' && message != '0' && utils.isJsonString(message)) { - debugCommand('MQTT message is JSON') - command = JSON.parse(message); - } else { - switch(message.toLowerCase()) { - case 'on': - case 'off': - case '0': - case '1': - case 'true': - case 'false': - // convert simple messages (on, off, 1, 0) to TuyAPI commands - command = { - set: (message.toLowerCase() === 'on' || message === '1' || message === 'true' || message === 1) ? true : false - } - break; - default: - command = message.toLowerCase() - } - } - return command - } - + // Process Tuya JSON commands via DPS command topic processDpsCommand(message) { if (utils.isJsonString(message)) { - const tuyaCommand = this.getCommandFromMessage(message) - debugCommand('Received command: '+tuyaCommand) - this.set(tuyaCommand) + 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') } @@ -311,11 +302,11 @@ class TuyaDevice { } else { const dpsMessage = this.parseDpsMessage(message) debugCommand('Received command for DPS'+dpsKey+': ', message) - const tuyaCommand = { + const command = { dps: dpsKey, set: dpsMessage } - this.set(tuyaCommand) + this.set(command) } } @@ -332,27 +323,9 @@ class TuyaDevice { } } - // 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 - try { - const result = await this.device.get({"dps": key}) - this.state.dps[key].val = result - this.state.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 in sync - this.publishTopics() - } - // Set state based on command topic - sendTuyaCommand(command, deviceTopic) { + sendTuyaCommand(message, deviceTopic) { + let command = message.toLowerCase() const tuyaCommand = new Object() tuyaCommand.dps = deviceTopic.key switch (deviceTopic.type) { @@ -360,6 +333,7 @@ class TuyaDevice { if (command === 'toggle') { tuyaCommand.set = !this.state.dps[tuyaCommand.dps].val } else { + command = this.parseBoolCommand(command) if (typeof command.set === 'boolean') { tuyaCommand.set = command.set } else { @@ -369,16 +343,19 @@ class TuyaDevice { break; case 'int': case 'float': - tuyaCommand.set = this.parseCommandNumber(command, deviceTopic) + tuyaCommand.set = this.parseNumberCommand(command, deviceTopic) break; case 'hsb': this.updateSetColorState(command, deviceTopic.components) - tuyaCommand.set = this.getTuyaHsbColor() + tuyaCommand.set = this.parseTuyaHsbColor() break; case 'hsbhex': this.updateSetColorState(command, deviceTopic.components) - tuyaCommand.set = this.getTuyaHsbHexColor() + 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 @@ -391,18 +368,40 @@ class TuyaDevice { return true } } - + + // Convert simple bool commands to true/flase + 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 - parseCommandNumber(command, deviceTopic) { + 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('min') && command < deviceTopic.min) || - (deviceTopic.hasOwnProperty('max') && command > deviceTopic.max)) { - return invalid + } else if (deviceTopic.hasOwnProperty('min') && command < deviceTopic.min) { + debugError('Received command value "'+command+'" that is less than the configured minimum value') + debugError('Overriding command with minimum value '+deviceTopic.min) + command = deviceTopic.min + } else if (deviceTopic.hasOwnProperty('max') && command > deviceTopic.max) { + debugError('Received command value "'+command+'" that is greater than the configured maximum value') + debugError('Overriding command with maximum value: '+deviceTopic.max) + command = deviceTopic.max } // Perform any required math transforms before returing command value @@ -445,7 +444,7 @@ class TuyaDevice { // Initialize the set color values for first time. Used to conflicts // when mulitple HSB components are updated in quick succession - if (!this.state.setColor) { + if (!this.state.hasOwnProperty('setColor')) { this.state.setColor = { 'h': this.state.color.h, 's': this.state.color.s, @@ -467,7 +466,7 @@ class TuyaDevice { } // Returns Tuya HSB format value from current setColor HSB value - getTuyaHsbColor() { + parseTuyaHsbColor() { // 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') + (10 * b).toString(16).padStart(4, '0') @@ -475,7 +474,7 @@ class TuyaDevice { } // Returns Tuya HSBHEX format value from current setColor HSB value - getTuyaHsbHexColor() { + parseTuyaHsbHexColor() { 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(2.55 * b).toString(16).padStart(2, '0'); h /= 60; @@ -518,8 +517,6 @@ class TuyaDevice { // If setting white level or color temperature, light should be in white mode targetMode = 'white' } else if (topic.key === this.config.dpsColor) { - // Short sleep for cases where mulitple updates occur quickly - await msSleep(100) if (this.state.setColor.s < 10) { // If saturation is < 10 then white mode targetMode = 'white'