From 7246f42a8ec4ba2c15ff930c69dacc4f174f6269 Mon Sep 17 00:00:00 2001 From: irtimaled Date: Sun, 26 Sep 2021 01:34:06 -0700 Subject: [PATCH] Tuya rgb support (#2278) Co-authored-by: Oxan van Leeuwen Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/rgbct/light.py | 2 +- esphome/components/rgbw/light.py | 10 ++- esphome/components/rgbww/light.py | 2 +- esphome/components/tuya/light/__init__.py | 12 ++- esphome/components/tuya/light/tuya_light.cpp | 91 ++++++++++++++------ esphome/components/tuya/light/tuya_light.h | 5 ++ esphome/const.py | 1 + esphome/core/helpers.cpp | 33 +++++++ esphome/core/helpers.h | 3 +- 9 files changed, 129 insertions(+), 30 deletions(-) diff --git a/esphome/components/rgbct/light.py b/esphome/components/rgbct/light.py index e525c207c7..0565057316 100644 --- a/esphome/components/rgbct/light.py +++ b/esphome/components/rgbct/light.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.components import light, output from esphome.const import ( CONF_BLUE, + CONF_COLOR_INTERLOCK, CONF_COLOR_TEMPERATURE, CONF_GREEN, CONF_RED, @@ -16,7 +17,6 @@ CODEOWNERS = ["@jesserockz"] rgbct_ns = cg.esphome_ns.namespace("rgbct") RGBCTLightOutput = rgbct_ns.class_("RGBCTLightOutput", light.LightOutput) -CONF_COLOR_INTERLOCK = "color_interlock" CONF_WHITE_BRIGHTNESS = "white_brightness" CONFIG_SCHEMA = cv.All( diff --git a/esphome/components/rgbw/light.py b/esphome/components/rgbw/light.py index de26edf7d5..f747580f61 100644 --- a/esphome/components/rgbw/light.py +++ b/esphome/components/rgbw/light.py @@ -1,11 +1,17 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import light, output -from esphome.const import CONF_BLUE, CONF_GREEN, CONF_RED, CONF_OUTPUT_ID, CONF_WHITE +from esphome.const import ( + CONF_BLUE, + CONF_COLOR_INTERLOCK, + CONF_GREEN, + CONF_RED, + CONF_OUTPUT_ID, + CONF_WHITE, +) rgbw_ns = cg.esphome_ns.namespace("rgbw") RGBWLightOutput = rgbw_ns.class_("RGBWLightOutput", light.LightOutput) -CONF_COLOR_INTERLOCK = "color_interlock" CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( { diff --git a/esphome/components/rgbww/light.py b/esphome/components/rgbww/light.py index c0ce85e267..35f77b154b 100644 --- a/esphome/components/rgbww/light.py +++ b/esphome/components/rgbww/light.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.components import light, output from esphome.const import ( CONF_BLUE, + CONF_COLOR_INTERLOCK, CONF_CONSTANT_BRIGHTNESS, CONF_GREEN, CONF_RED, @@ -16,7 +17,6 @@ from esphome.const import ( rgbww_ns = cg.esphome_ns.namespace("rgbww") RGBWWLightOutput = rgbww_ns.class_("RGBWWLightOutput", light.LightOutput) -CONF_COLOR_INTERLOCK = "color_interlock" CONFIG_SCHEMA = cv.All( light.RGB_LIGHT_SCHEMA.extend( diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py index f43cc570ca..6678fc47d8 100644 --- a/esphome/components/tuya/light/__init__.py +++ b/esphome/components/tuya/light/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( CONF_SWITCH_DATAPOINT, CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_WARM_WHITE_COLOR_TEMPERATURE, + CONF_COLOR_INTERLOCK, ) from .. import tuya_ns, CONF_TUYA_ID, Tuya @@ -20,6 +21,7 @@ CONF_MIN_VALUE_DATAPOINT = "min_value_datapoint" CONF_COLOR_TEMPERATURE_DATAPOINT = "color_temperature_datapoint" CONF_COLOR_TEMPERATURE_INVERT = "color_temperature_invert" CONF_COLOR_TEMPERATURE_MAX_VALUE = "color_temperature_max_value" +CONF_RGB_DATAPOINT = "rgb_datapoint" TuyaLight = tuya_ns.class_("TuyaLight", light.LightOutput, cg.Component) @@ -31,6 +33,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DIMMER_DATAPOINT): cv.uint8_t, cv.Optional(CONF_MIN_VALUE_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_RGB_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, cv.Inclusive( CONF_COLOR_TEMPERATURE_DATAPOINT, "color_temperature" ): cv.uint8_t, @@ -52,7 +56,9 @@ CONFIG_SCHEMA = cv.All( ): cv.positive_time_period_milliseconds, } ).extend(cv.COMPONENT_SCHEMA), - cv.has_at_least_one_key(CONF_DIMMER_DATAPOINT, CONF_SWITCH_DATAPOINT), + cv.has_at_least_one_key( + CONF_DIMMER_DATAPOINT, CONF_SWITCH_DATAPOINT, CONF_RGB_DATAPOINT + ), ) @@ -67,6 +73,8 @@ async def to_code(config): cg.add(var.set_min_value_datapoint_id(config[CONF_MIN_VALUE_DATAPOINT])) if CONF_SWITCH_DATAPOINT in config: cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) + if CONF_RGB_DATAPOINT in config: + cg.add(var.set_rgb_id(config[CONF_RGB_DATAPOINT])) if CONF_COLOR_TEMPERATURE_DATAPOINT in config: cg.add(var.set_color_temperature_id(config[CONF_COLOR_TEMPERATURE_DATAPOINT])) cg.add(var.set_color_temperature_invert(config[CONF_COLOR_TEMPERATURE_INVERT])) @@ -87,5 +95,7 @@ async def to_code(config): config[CONF_COLOR_TEMPERATURE_MAX_VALUE] ) ) + + cg.add(var.set_color_interlock(config[CONF_COLOR_INTERLOCK])) paren = await cg.get_variable(config[CONF_TUYA_ID]) cg.add(var.set_tuya_parent(paren)) diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index 6f3adfcdfd..97f6de9bae 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -1,5 +1,6 @@ #include "esphome/core/log.h" #include "tuya_light.h" +#include "esphome/core/helpers.h" namespace esphome { namespace tuya { @@ -34,6 +35,18 @@ void TuyaLight::setup() { call.perform(); }); } + if (rgb_id_.has_value()) { + this->parent_->register_listener(*this->rgb_id_, [this](const TuyaDatapoint &datapoint) { + auto red = parse_hex(datapoint.value_string, 0, 2); + auto green = parse_hex(datapoint.value_string, 2, 2); + auto blue = parse_hex(datapoint.value_string, 4, 2); + if (red.has_value() && green.has_value() && blue.has_value()) { + auto call = this->state_->make_call(); + call.set_rgb(float(*red) / 255, float(*green) / 255, float(*blue) / 255); + call.perform(); + } + }); + } if (min_value_datapoint_id_.has_value()) { parent_->set_integer_datapoint_value(*this->min_value_datapoint_id_, this->min_value_); } @@ -45,14 +58,31 @@ void TuyaLight::dump_config() { ESP_LOGCONFIG(TAG, " Dimmer has datapoint ID %u", *this->dimmer_id_); if (this->switch_id_.has_value()) ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); + if (this->rgb_id_.has_value()) + ESP_LOGCONFIG(TAG, " RGB has datapoint ID %u", *this->rgb_id_); } light::LightTraits TuyaLight::get_traits() { auto traits = light::LightTraits(); if (this->color_temperature_id_.has_value() && this->dimmer_id_.has_value()) { - traits.set_supported_color_modes({light::ColorMode::COLOR_TEMPERATURE}); + if (this->rgb_id_.has_value()) { + if (this->color_interlock_) + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLOR_TEMPERATURE}); + else + traits.set_supported_color_modes( + {light::ColorMode::RGB_COLOR_TEMPERATURE, light::ColorMode::COLOR_TEMPERATURE}); + } else + traits.set_supported_color_modes({light::ColorMode::COLOR_TEMPERATURE}); traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); + } else if (this->rgb_id_.has_value()) { + if (this->dimmer_id_.has_value()) { + if (this->color_interlock_) + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::WHITE}); + else + traits.set_supported_color_modes({light::ColorMode::RGB_WHITE}); + } else + traits.set_supported_color_modes({light::ColorMode::RGB}); } else if (this->dimmer_id_.has_value()) { traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); } else { @@ -64,38 +94,51 @@ light::LightTraits TuyaLight::get_traits() { void TuyaLight::setup_state(light::LightState *state) { state_ = state; } void TuyaLight::write_state(light::LightState *state) { - float brightness; - state->current_values_as_brightness(&brightness); + float red = 0.0f, green = 0.0f, blue = 0.0f; + float color_temperature = 0.0f, brightness = 0.0f; - if (brightness == 0.0f) { - // turning off, first try via switch (if exists), then dimmer - if (switch_id_.has_value()) { - parent_->set_boolean_datapoint_value(*this->switch_id_, false); - } else if (dimmer_id_.has_value()) { - parent_->set_integer_datapoint_value(*this->dimmer_id_, 0); + if (this->rgb_id_.has_value()) { + if (this->color_temperature_id_.has_value()) { + state->current_values_as_rgbct(&red, &green, &blue, &color_temperature, &brightness); + } else if (this->dimmer_id_.has_value()) { + state->current_values_as_rgbw(&red, &green, &blue, &brightness); + } else { + state->current_values_as_rgb(&red, &green, &blue); } - return; + } else if (this->color_temperature_id_.has_value()) { + state->current_values_as_ct(&color_temperature, &brightness); + } else { + state->current_values_as_brightness(&brightness); } - if (this->color_temperature_id_.has_value()) { - uint32_t color_temp_int = - static_cast(this->color_temperature_max_value_ * - (state->current_values.get_color_temperature() - this->cold_white_temperature_) / - (this->warm_white_temperature_ - this->cold_white_temperature_)); - if (this->color_temperature_invert_) { - color_temp_int = this->color_temperature_max_value_ - color_temp_int; + if (brightness > 0.0f || !color_interlock_) { + if (this->color_temperature_id_.has_value()) { + uint32_t color_temp_int = static_cast(color_temperature * this->color_temperature_max_value_); + if (this->color_temperature_invert_) { + color_temp_int = this->color_temperature_max_value_ - color_temp_int; + } + parent_->set_integer_datapoint_value(*this->color_temperature_id_, color_temp_int); + } + + if (this->dimmer_id_.has_value()) { + auto brightness_int = static_cast(brightness * this->max_value_); + brightness_int = std::max(brightness_int, this->min_value_); + + parent_->set_integer_datapoint_value(*this->dimmer_id_, brightness_int); } - parent_->set_integer_datapoint_value(*this->color_temperature_id_, color_temp_int); } - auto brightness_int = static_cast(brightness * this->max_value_); - brightness_int = std::max(brightness_int, this->min_value_); - - if (this->dimmer_id_.has_value()) { - parent_->set_integer_datapoint_value(*this->dimmer_id_, brightness_int); + if (brightness == 0.0f || !color_interlock_) { + if (this->rgb_id_.has_value()) { + char buffer[7]; + sprintf(buffer, "%02X%02X%02X", int(red * 255), int(green * 255), int(blue * 255)); + std::string value = buffer; + this->parent_->set_string_datapoint_value(*this->rgb_id_, value); + } } + if (this->switch_id_.has_value()) { - parent_->set_boolean_datapoint_value(*this->switch_id_, true); + parent_->set_boolean_datapoint_value(*this->switch_id_, state->current_values.is_on()); } } diff --git a/esphome/components/tuya/light/tuya_light.h b/esphome/components/tuya/light/tuya_light.h index 20753fa90b..de9ec5e45f 100644 --- a/esphome/components/tuya/light/tuya_light.h +++ b/esphome/components/tuya/light/tuya_light.h @@ -16,6 +16,7 @@ class TuyaLight : public Component, public light::LightOutput { this->min_value_datapoint_id_ = min_value_datapoint_id; } void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + void set_rgb_id(uint8_t rgb_id) { this->rgb_id_ = rgb_id; } void set_color_temperature_id(uint8_t color_temperature_id) { this->color_temperature_id_ = color_temperature_id; } void set_color_temperature_invert(bool color_temperature_invert) { this->color_temperature_invert_ = color_temperature_invert; @@ -32,6 +33,8 @@ class TuyaLight : public Component, public light::LightOutput { void set_warm_white_temperature(float warm_white_temperature) { this->warm_white_temperature_ = warm_white_temperature; } + void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } + light::LightTraits get_traits() override; void setup_state(light::LightState *state) override; void write_state(light::LightState *state) override; @@ -44,6 +47,7 @@ class TuyaLight : public Component, public light::LightOutput { optional dimmer_id_{}; optional min_value_datapoint_id_{}; optional switch_id_{}; + optional rgb_id_{}; optional color_temperature_id_{}; uint32_t min_value_ = 0; uint32_t max_value_ = 255; @@ -51,6 +55,7 @@ class TuyaLight : public Component, public light::LightOutput { float cold_white_temperature_; float warm_white_temperature_; bool color_temperature_invert_{false}; + bool color_interlock_{false}; light::LightState *state_{nullptr}; }; diff --git a/esphome/const.py b/esphome/const.py index a52085fbb7..054f032da4 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -111,6 +111,7 @@ CONF_COLD_WHITE_COLOR_TEMPERATURE = "cold_white_color_temperature" CONF_COLOR = "color" CONF_COLOR_BRIGHTNESS = "color_brightness" CONF_COLOR_CORRECT = "color_correct" +CONF_COLOR_INTERLOCK = "color_interlock" CONF_COLOR_MODE = "color_mode" CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors" diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 2b77c5827a..a190566bea 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -270,6 +270,39 @@ optional parse_int(const std::string &str) { return {}; return value; } + +optional parse_hex(const char chr) { + int out = chr; + if (out >= '0' && out <= '9') + return (out - '0'); + if (out >= 'A' && out <= 'F') + return (10 + (out - 'A')); + if (out >= 'a' && out <= 'f') + return (10 + (out - 'a')); + return {}; +} + +optional parse_hex(const std::string &str, size_t start, size_t length) { + if (str.length() < start) { + return {}; + } + size_t end = start + length; + if (str.length() < end) { + return {}; + } + int out = 0; + for (size_t i = start; i < end; i++) { + char chr = str[i]; + auto digit = parse_hex(chr); + if (!digit.has_value()) { + ESP_LOGW(TAG, "Can't convert '%s' to number, invalid character %c!", str.substr(start, length).c_str(), chr); + return {}; + } + out = (out << 4) | *digit; + } + return out; +} + uint32_t fnv1_hash(const std::string &str) { uint32_t hash = 2166136261UL; for (char c : str) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 86cd3b086e..8118585db5 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -43,7 +43,8 @@ std::string to_string(double val); std::string to_string(long double val); optional parse_float(const std::string &str); optional parse_int(const std::string &str); - +optional parse_hex(const std::string &str, size_t start, size_t length); +optional parse_hex(char chr); /// Sanitize the hostname by removing characters that are not in the allowlist and truncating it to 63 chars. std::string sanitize_hostname(const std::string &hostname);