From c39ac9edfebead943cc51fb6ac4fe9b039b5a5c4 Mon Sep 17 00:00:00 2001 From: irtimaled Date: Tue, 28 Sep 2021 13:19:17 -0700 Subject: [PATCH] Support HSV-based color support on tuya light (#2400) * fix: stop tuya light state getting reset * fix typo * Support for HSV color in Tuya * Clamp formatting --- esphome/components/tuya/light/__init__.py | 11 +++- esphome/components/tuya/light/tuya_light.cpp | 33 ++++++++-- esphome/components/tuya/light/tuya_light.h | 2 + esphome/core/helpers.cpp | 63 ++++++++++++++++++++ esphome/core/helpers.h | 5 ++ 5 files changed, 107 insertions(+), 7 deletions(-) diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py index 6678fc47d8..b983e3f84e 100644 --- a/esphome/components/tuya/light/__init__.py +++ b/esphome/components/tuya/light/__init__.py @@ -22,6 +22,7 @@ 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" +CONF_HSV_DATAPOINT = "hsv_datapoint" TuyaLight = tuya_ns.class_("TuyaLight", light.LightOutput, cg.Component) @@ -33,7 +34,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.Exclusive(CONF_RGB_DATAPOINT, "color"): cv.uint8_t, + cv.Exclusive(CONF_HSV_DATAPOINT, "color"): cv.uint8_t, cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, cv.Inclusive( CONF_COLOR_TEMPERATURE_DATAPOINT, "color_temperature" @@ -57,7 +59,10 @@ CONFIG_SCHEMA = cv.All( } ).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key( - CONF_DIMMER_DATAPOINT, CONF_SWITCH_DATAPOINT, CONF_RGB_DATAPOINT + CONF_DIMMER_DATAPOINT, + CONF_SWITCH_DATAPOINT, + CONF_RGB_DATAPOINT, + CONF_HSV_DATAPOINT, ), ) @@ -75,6 +80,8 @@ async def to_code(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])) + elif CONF_HSV_DATAPOINT in config: + cg.add(var.set_hsv_id(config[CONF_HSV_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])) diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index f75cc964aa..133ee1e557 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -46,6 +46,19 @@ void TuyaLight::setup() { call.perform(); } }); + } else if (hsv_id_.has_value()) { + this->parent_->register_listener(*this->hsv_id_, [this](const TuyaDatapoint &datapoint) { + auto hue = parse_hex(datapoint.value_string, 0, 4); + auto saturation = parse_hex(datapoint.value_string, 4, 4); + auto value = parse_hex(datapoint.value_string, 8, 4); + if (hue.has_value() && saturation.has_value() && value.has_value()) { + float red, green, blue; + hsv_to_rgb(*hue, float(*saturation) / 1000, float(*value) / 1000, red, green, blue); + auto call = this->state_->make_call(); + call.set_rgb(red, green, blue); + call.perform(); + } + }); } if (min_value_datapoint_id_.has_value()) { parent_->set_integer_datapoint_value(*this->min_value_datapoint_id_, this->min_value_); @@ -60,12 +73,14 @@ void TuyaLight::dump_config() { 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_); + else if (this->hsv_id_.has_value()) + ESP_LOGCONFIG(TAG, " HSV has datapoint ID %u", *this->hsv_id_); } light::LightTraits TuyaLight::get_traits() { auto traits = light::LightTraits(); if (this->color_temperature_id_.has_value() && this->dimmer_id_.has_value()) { - if (this->rgb_id_.has_value()) { + if (this->rgb_id_.has_value() || this->hsv_id_.has_value()) { if (this->color_interlock_) traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLOR_TEMPERATURE}); else @@ -75,7 +90,7 @@ light::LightTraits TuyaLight::get_traits() { 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()) { + } else if (this->rgb_id_.has_value() || this->hsv_id_.has_value()) { if (this->dimmer_id_.has_value()) { if (this->color_interlock_) traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::WHITE}); @@ -97,7 +112,7 @@ void TuyaLight::write_state(light::LightState *state) { float red = 0.0f, green = 0.0f, blue = 0.0f; float color_temperature = 0.0f, brightness = 0.0f; - if (this->rgb_id_.has_value()) { + if (this->rgb_id_.has_value() || this->hsv_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()) { @@ -137,8 +152,16 @@ void TuyaLight::write_state(light::LightState *state) { 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); + std::string rgb_value = buffer; + this->parent_->set_string_datapoint_value(*this->rgb_id_, rgb_value); + } else if (this->hsv_id_.has_value()) { + int hue; + float saturation, value; + rgb_to_hsv(red, green, blue, hue, saturation, value); + char buffer[13]; + sprintf(buffer, "%04X%04X%04X", hue, int(saturation * 1000), int(value * 1000)); + std::string hsv_value = buffer; + this->parent_->set_string_datapoint_value(*this->hsv_id_, hsv_value); } } diff --git a/esphome/components/tuya/light/tuya_light.h b/esphome/components/tuya/light/tuya_light.h index de9ec5e45f..3d9f25271c 100644 --- a/esphome/components/tuya/light/tuya_light.h +++ b/esphome/components/tuya/light/tuya_light.h @@ -17,6 +17,7 @@ class TuyaLight : public Component, public light::LightOutput { } 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_hsv_id(uint8_t hsv_id) { this->hsv_id_ = hsv_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; @@ -48,6 +49,7 @@ class TuyaLight : public Component, public light::LightOutput { optional min_value_datapoint_id_{}; optional switch_id_{}; optional rgb_id_{}; + optional hsv_id_{}; optional color_temperature_id_{}; uint32_t min_value_ = 0; uint32_t max_value_ = 255; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 0092d202c4..731ee6c8f5 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -394,6 +394,69 @@ std::string hexencode(const uint8_t *data, uint32_t len) { return res; } +void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value) { + float max_color_value = std::max(std::max(red, green), blue); + float min_color_value = std::min(std::min(red, green), blue); + float delta = max_color_value - min_color_value; + + if (delta == 0) + hue = 0; + else if (max_color_value == red) + hue = int(fmod(((60 * ((green - blue) / delta)) + 360), 360)); + else if (max_color_value == green) + hue = int(fmod(((60 * ((blue - red) / delta)) + 120), 360)); + else if (max_color_value == blue) + hue = int(fmod(((60 * ((red - green) / delta)) + 240), 360)); + + if (max_color_value == 0) + saturation = 0; + else + saturation = delta / max_color_value; + + value = max_color_value; +} + +void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green, float &blue) { + float chroma = value * saturation; + float hue_prime = fmod(hue / 60.0, 6); + float intermediate = chroma * (1 - fabs(fmod(hue_prime, 2) - 1)); + float delta = value - chroma; + + if (0 <= hue_prime && hue_prime < 1) { + red = chroma; + green = intermediate; + blue = 0; + } else if (1 <= hue_prime && hue_prime < 2) { + red = intermediate; + green = chroma; + blue = 0; + } else if (2 <= hue_prime && hue_prime < 3) { + red = 0; + green = chroma; + blue = intermediate; + } else if (3 <= hue_prime && hue_prime < 4) { + red = 0; + green = intermediate; + blue = chroma; + } else if (4 <= hue_prime && hue_prime < 5) { + red = intermediate; + green = 0; + blue = chroma; + } else if (5 <= hue_prime && hue_prime < 6) { + red = chroma; + green = 0; + blue = intermediate; + } else { + red = 0; + green = 0; + blue = 0; + } + + red += delta; + green += delta; + blue += delta; +} + #ifdef USE_ESP8266 IRAM_ATTR InterruptLock::InterruptLock() { xt_state_ = xt_rsil(15); } IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(xt_state_); } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index f5a7a197ca..24b55eade0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -149,6 +149,11 @@ std::array decode_uint16(uint16_t value); /// Encode a 32-bit unsigned integer given four bytes in MSB -> LSB order uint32_t encode_uint32(uint8_t msb, uint8_t byte2, uint8_t byte3, uint8_t lsb); +/// Convert RGB floats (0-1) to hue (0-360) & saturation/value percentage (0-1) +void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value); +/// Convert hue (0-360) & saturation/value percentage (0-1) to RGB floats (0-1) +void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green, float &blue); + /*** * An interrupt helper class. *