From f9797825ad03d2e692290ef4f337a58cad47fd62 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Thu, 8 Jul 2021 11:37:47 +0200 Subject: [PATCH] Change color model to fix white channel issues (#1895) --- esphome/components/api/api.proto | 3 + esphome/components/api/api_connection.cpp | 3 + esphome/components/api/api_pb2.cpp | 29 +++++ esphome/components/api/api_pb2.h | 3 + .../components/light/addressable_light.cpp | 8 +- esphome/components/light/automation.h | 12 +- esphome/components/light/automation.py | 5 + esphome/components/light/effects.py | 4 + .../components/light/esp_color_correction.h | 7 +- esphome/components/light/light_call.cpp | 91 +++++++++++---- esphome/components/light/light_call.h | 7 ++ esphome/components/light/light_color_values.h | 110 ++++++++++-------- esphome/const.py | 1 + 13 files changed, 200 insertions(+), 83 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index a5bd9aec6d..8034c980a4 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -378,6 +378,7 @@ message LightStateResponse { fixed32 key = 1; bool state = 2; float brightness = 3; + float color_brightness = 10; float red = 4; float green = 5; float blue = 6; @@ -396,6 +397,8 @@ message LightCommandRequest { bool state = 3; bool has_brightness = 4; float brightness = 5; + bool has_color_brightness = 20; + float color_brightness = 21; bool has_rgb = 6; float red = 7; float green = 8; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 00a8539112..c36d36a159 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -308,6 +308,7 @@ bool APIConnection::send_light_state(light::LightState *light) { if (traits.get_supports_brightness()) resp.brightness = values.get_brightness(); if (traits.get_supports_rgb()) { + resp.color_brightness = values.get_color_brightness(); resp.red = values.get_red(); resp.green = values.get_green(); resp.blue = values.get_blue(); @@ -352,6 +353,8 @@ void APIConnection::light_command(const LightCommandRequest &msg) { call.set_state(msg.state); if (msg.has_brightness) call.set_brightness(msg.brightness); + if (msg.has_color_brightness) + call.set_color_brightness(msg.color_brightness); if (msg.has_rgb) { call.set_red(msg.red); call.set_green(msg.green); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index e53ac2019a..83ebdd8b68 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1263,6 +1263,10 @@ bool LightStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { this->brightness = value.as_float(); return true; } + case 10: { + this->color_brightness = value.as_float(); + return true; + } case 4: { this->red = value.as_float(); return true; @@ -1291,6 +1295,7 @@ void LightStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_float(3, this->brightness); + buffer.encode_float(10, this->color_brightness); buffer.encode_float(4, this->red); buffer.encode_float(5, this->green); buffer.encode_float(6, this->blue); @@ -1315,6 +1320,11 @@ void LightStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" color_brightness: "); + sprintf(buffer, "%g", this->color_brightness); + out.append(buffer); + out.append("\n"); + out.append(" red: "); sprintf(buffer, "%g", this->red); out.append(buffer); @@ -1359,6 +1369,10 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->has_brightness = value.as_bool(); return true; } + case 20: { + this->has_color_brightness = value.as_bool(); + return true; + } case 6: { this->has_rgb = value.as_bool(); return true; @@ -1415,6 +1429,10 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { this->brightness = value.as_float(); return true; } + case 21: { + this->color_brightness = value.as_float(); + return true; + } case 7: { this->red = value.as_float(); return true; @@ -1445,6 +1463,8 @@ void LightCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(3, this->state); buffer.encode_bool(4, this->has_brightness); buffer.encode_float(5, this->brightness); + buffer.encode_bool(20, this->has_color_brightness); + buffer.encode_float(21, this->color_brightness); buffer.encode_bool(6, this->has_rgb); buffer.encode_float(7, this->red); buffer.encode_float(8, this->green); @@ -1485,6 +1505,15 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" has_color_brightness: "); + out.append(YESNO(this->has_color_brightness)); + out.append("\n"); + + out.append(" color_brightness: "); + sprintf(buffer, "%g", this->color_brightness); + out.append(buffer); + out.append("\n"); + out.append(" has_rgb: "); out.append(YESNO(this->has_rgb)); out.append("\n"); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 956cecdeb9..6873e0d54c 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -371,6 +371,7 @@ class LightStateResponse : public ProtoMessage { uint32_t key{0}; bool state{false}; float brightness{0.0f}; + float color_brightness{0.0f}; float red{0.0f}; float green{0.0f}; float blue{0.0f}; @@ -392,6 +393,8 @@ class LightCommandRequest : public ProtoMessage { bool state{false}; bool has_brightness{false}; float brightness{0.0f}; + bool has_color_brightness{false}; + float color_brightness{0.0f}; bool has_rgb{false}; float red{0.0f}; float green{0.0f}; diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index e3ab9596d9..ea24736c63 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -25,10 +25,10 @@ void AddressableLight::call_setup() { } Color esp_color_from_light_color_values(LightColorValues val) { - auto r = static_cast(roundf(val.get_red() * 255.0f)); - auto g = static_cast(roundf(val.get_green() * 255.0f)); - auto b = static_cast(roundf(val.get_blue() * 255.0f)); - auto w = static_cast(roundf(val.get_white() * val.get_state() * 255.0f)); + auto r = static_cast(roundf(val.get_color_brightness() * val.get_red() * 255.0f)); + auto g = static_cast(roundf(val.get_color_brightness() * val.get_green() * 255.0f)); + auto b = static_cast(roundf(val.get_color_brightness() * val.get_blue() * 255.0f)); + auto w = static_cast(roundf(val.get_white() * 255.0f)); return Color(r, g, b, w); } diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index d1fb2a0bcb..e99d5c58bd 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -31,6 +31,7 @@ template class LightControlAction : public Action { TEMPLATABLE_VALUE(uint32_t, transition_length) TEMPLATABLE_VALUE(uint32_t, flash_length) TEMPLATABLE_VALUE(float, brightness) + TEMPLATABLE_VALUE(float, color_brightness) TEMPLATABLE_VALUE(float, red) TEMPLATABLE_VALUE(float, green) TEMPLATABLE_VALUE(float, blue) @@ -42,6 +43,7 @@ template class LightControlAction : public Action { auto call = this->parent_->make_call(); call.set_state(this->state_.optional_value(x...)); call.set_brightness(this->brightness_.optional_value(x...)); + call.set_color_brightness(this->color_brightness_.optional_value(x...)); call.set_red(this->red_.optional_value(x...)); call.set_green(this->green_.optional_value(x...)); call.set_blue(this->blue_.optional_value(x...)); @@ -139,6 +141,7 @@ template class AddressableSet : public Action { TEMPLATABLE_VALUE(int32_t, range_from) TEMPLATABLE_VALUE(int32_t, range_to) + TEMPLATABLE_VALUE(uint8_t, color_brightness) TEMPLATABLE_VALUE(uint8_t, red) TEMPLATABLE_VALUE(uint8_t, green) TEMPLATABLE_VALUE(uint8_t, blue) @@ -148,13 +151,16 @@ template class AddressableSet : public Action { auto *out = (AddressableLight *) this->parent_->get_output(); int32_t range_from = this->range_from_.value_or(x..., 0); int32_t range_to = this->range_to_.value_or(x..., out->size() - 1) + 1; + uint8_t remote_color_brightness = + static_cast(roundf(this->parent_->remote_values.get_color_brightness() * 255.0f)); + uint8_t color_brightness = this->color_brightness_.value_or(x..., remote_color_brightness); auto range = out->range(range_from, range_to); if (this->red_.has_value()) - range.set_red(this->red_.value(x...)); + range.set_red(esp_scale8(this->red_.value(x...), color_brightness)); if (this->green_.has_value()) - range.set_green(this->green_.value(x...)); + range.set_green(esp_scale8(this->green_.value(x...), color_brightness)); if (this->blue_.has_value()) - range.set_blue(this->blue_.value(x...)); + range.set_blue(esp_scale8(this->blue_.value(x...), color_brightness)); if (this->white_.has_value()) range.set_white(this->white_.value(x...)); out->schedule_show(); diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 3fb3126f14..dd1148131a 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_FLASH_LENGTH, CONF_EFFECT, CONF_BRIGHTNESS, + CONF_COLOR_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, @@ -63,6 +64,7 @@ LIGHT_CONTROL_ACTION_SCHEMA = cv.Schema( ), cv.Exclusive(CONF_EFFECT, "transformer"): cv.templatable(cv.string), cv.Optional(CONF_BRIGHTNESS): cv.templatable(cv.percentage), + cv.Optional(CONF_COLOR_BRIGHTNESS): cv.templatable(cv.percentage), cv.Optional(CONF_RED): cv.templatable(cv.percentage), cv.Optional(CONF_GREEN): cv.templatable(cv.percentage), cv.Optional(CONF_BLUE): cv.templatable(cv.percentage), @@ -114,6 +116,9 @@ async def light_control_to_code(config, action_id, template_arg, args): if CONF_BRIGHTNESS in config: template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, float) cg.add(var.set_brightness(template_)) + if CONF_COLOR_BRIGHTNESS in config: + template_ = await cg.templatable(config[CONF_COLOR_BRIGHTNESS], args, float) + cg.add(var.set_color_brightness(template_)) if CONF_RED in config: template_ = await cg.templatable(config[CONF_RED], args, float) cg.add(var.set_red(template_)) diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index c213de0ae6..f6ce812c34 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_STATE, CONF_DURATION, CONF_BRIGHTNESS, + CONF_COLOR_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, @@ -211,6 +212,7 @@ async def random_effect_to_code(config, effect_id): { cv.Optional(CONF_STATE, default=True): cv.boolean, cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_COLOR_BRIGHTNESS, default=1.0): cv.percentage, cv.Optional(CONF_RED, default=1.0): cv.percentage, cv.Optional(CONF_GREEN, default=1.0): cv.percentage, cv.Optional(CONF_BLUE, default=1.0): cv.percentage, @@ -223,6 +225,7 @@ async def random_effect_to_code(config, effect_id): cv.has_at_least_one_key( CONF_STATE, CONF_BRIGHTNESS, + CONF_COLOR_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, @@ -245,6 +248,7 @@ async def strobe_effect_to_code(config, effect_id): LightColorValues( color[CONF_STATE], color[CONF_BRIGHTNESS], + color[CONF_COLOR_BRIGHTNESS], color[CONF_RED], color[CONF_GREEN], color[CONF_BLUE], diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index 32fee8c8ea..8788246cfc 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -29,8 +29,7 @@ class ESPColorCorrection { return this->gamma_table_[res]; } inline uint8_t color_correct_white(uint8_t white) const ALWAYS_INLINE { - // do not scale white value with brightness - uint8_t res = esp_scale8(white, this->max_brightness_.white); + uint8_t res = esp_scale8(esp_scale8(white, this->max_brightness_.white), this->local_brightness_); return this->gamma_table_[res]; } inline Color color_uncorrect(Color color) const ALWAYS_INLINE { @@ -60,10 +59,10 @@ class ESPColorCorrection { return res; } inline uint8_t color_uncorrect_white(uint8_t white) const ALWAYS_INLINE { - if (this->max_brightness_.white == 0) + if (this->max_brightness_.white == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL; - uint8_t res = uncorrected / this->max_brightness_.white; + uint8_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; return res; } diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 3fbe921aa1..557d001321 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -43,6 +43,10 @@ LightCall &LightCall::parse_color_json(JsonObject &root) { } } + if (root.containsKey("color_brightness")) { + this->set_color_brightness(float(root["color_brightness"]) / 255.0f); + } + if (root.containsKey("white_value")) { this->set_white(float(root["white_value"]) / 255.0f); } @@ -182,6 +186,12 @@ LightColorValues LightCall::validate_() { this->transition_length_.reset(); } + // Color brightness exists check + if (this->color_brightness_.has_value() && !traits.get_supports_rgb()) { + ESP_LOGW(TAG, "'%s' - This light does not support setting RGB brightness!", name); + this->color_brightness_.reset(); + } + // RGB exists check if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { if (!traits.get_supports_rgb()) { @@ -204,34 +214,48 @@ LightColorValues LightCall::validate_() { this->color_temperature_.reset(); } - // If white channel is specified, set RGB to white color (when interlock is enabled) - if (this->white_.has_value()) { - if (traits.get_supports_color_interlock()) { - if (!this->red_.has_value() && !this->green_.has_value() && !this->blue_.has_value()) { - this->red_ = optional(1.0f); - this->green_ = optional(1.0f); - this->blue_ = optional(1.0f); - } - // make white values binary aka 0.0f or 1.0f... this allows brightness to do its job - if (*this->white_ > 0.0f) { - this->white_ = optional(1.0f); - } else { - this->white_ = optional(0.0f); - } - } - } - // If only a color channel is specified, set white channel to 100% for white, otherwise 0% (when interlock is enabled) - else if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - if (traits.get_supports_color_interlock()) { - if (*this->red_ == 1.0f && *this->green_ == 1.0f && *this->blue_ == 1.0f) { - this->white_ = optional(1.0f); - } else { - this->white_ = optional(0.0f); - } + // Set color brightness to 100% if currently zero and a color is set. This is both for compatibility with older + // clients that don't know about color brightness, and it's intuitive UX anyway: if I set a color, it should show up. + if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { + if (!this->color_brightness_.has_value() && this->parent_->remote_values.get_color_brightness() == 0.0f) + this->color_brightness_ = optional(1.0f); + } + + // Handle interaction between RGB and white for color interlock + if (traits.get_supports_color_interlock()) { + // Find out which channel (white or color) the user wanted to enable + bool output_white = this->white_.has_value() && *this->white_ > 0.0f; + bool output_color = (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f) || + this->red_.has_value() || this->green_.has_value() || this->blue_.has_value(); + + // Interpret setting the color to white as setting the white channel. + if (output_color && *this->red_ == 1.0f && *this->green_ == 1.0f && *this->blue_ == 1.0f) { + output_white = true; + output_color = false; + + if (!this->white_.has_value()) + this->white_ = optional(this->color_brightness_.value_or(1.0f)); + } + + // Ensure either the white value or the color brightness is always zero. + if (output_white && output_color) { + ESP_LOGW(TAG, "'%s' - Cannot enable color and white channel simultaneously with interlock!", name); + // For compatibility with historic behaviour, prefer white channel in this case. + this->color_brightness_ = optional(0.0f); + } else if (output_white) { + this->color_brightness_ = optional(0.0f); + } else if (output_color) { + this->white_ = optional(0.0f); } } + // If only a color temperature is specified, change to white light - else if (this->color_temperature_.has_value()) { + if (this->color_temperature_.has_value() && !this->white_.has_value() && !this->red_.has_value() && + !this->green_.has_value() && !this->blue_.has_value()) { + // Disable color LEDs explicitly if not already set + if (traits.get_supports_rgb() && !this->color_brightness_.has_value()) + this->color_brightness_ = optional(0.0f); + this->red_ = optional(1.0f); this->green_ = optional(1.0f); this->blue_ = optional(1.0f); @@ -256,6 +280,7 @@ LightColorValues LightCall::validate_() { // Range checks VALIDATE_RANGE(brightness, "Brightness") + VALIDATE_RANGE(color_brightness, "Color brightness") VALIDATE_RANGE(red, "Red") VALIDATE_RANGE(green, "Green") VALIDATE_RANGE(blue, "Blue") @@ -267,6 +292,8 @@ LightColorValues LightCall::validate_() { if (this->brightness_.has_value()) v.set_brightness(*this->brightness_); + if (this->color_brightness_.has_value()) + v.set_color_brightness(*this->color_brightness_); if (this->red_.has_value()) v.set_red(*this->red_); if (this->green_.has_value()) @@ -371,6 +398,7 @@ LightCall &LightCall::set_effect(const std::string &effect) { LightCall &LightCall::from_light_color_values(const LightColorValues &values) { this->set_state(values.is_on()); this->set_brightness_if_supported(values.get_brightness()); + this->set_color_brightness_if_supported(values.get_color_brightness()); this->set_red_if_supported(values.get_red()); this->set_green_if_supported(values.get_green()); this->set_blue_if_supported(values.get_blue()); @@ -388,6 +416,11 @@ LightCall &LightCall::set_brightness_if_supported(float brightness) { this->set_brightness(brightness); return *this; } +LightCall &LightCall::set_color_brightness_if_supported(float brightness) { + if (this->parent_->get_traits().get_supports_rgb_white_value()) + this->set_brightness(brightness); + return *this; +} LightCall &LightCall::set_red_if_supported(float red) { if (this->parent_->get_traits().get_supports_rgb()) this->set_red(red); @@ -445,6 +478,14 @@ LightCall &LightCall::set_brightness(float brightness) { this->brightness_ = brightness; return *this; } +LightCall &LightCall::set_color_brightness(optional brightness) { + this->color_brightness_ = brightness; + return *this; +} +LightCall &LightCall::set_color_brightness(float brightness) { + this->color_brightness_ = brightness; + return *this; +} LightCall &LightCall::set_red(optional red) { this->red_ = red; return *this; diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index becea50004..c63b63bc54 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -44,6 +44,12 @@ class LightCall { LightCall &set_brightness(float brightness); /// Set the brightness property if the light supports brightness. LightCall &set_brightness_if_supported(float brightness); + /// Set the color brightness of the light from 0.0 (no color) to 1.0 (fully on) + LightCall &set_color_brightness(optional brightness); + /// Set the color brightness of the light from 0.0 (no color) to 1.0 (fully on) + LightCall &set_color_brightness(float brightness); + /// Set the color brightness property if the light supports RGBW. + LightCall &set_color_brightness_if_supported(float brightness); /** Set the red RGB value of the light from 0.0 to 1.0. * * Note that this only controls the color of the light, not its brightness. @@ -146,6 +152,7 @@ class LightCall { optional transition_length_; optional flash_length_; optional brightness_; + optional color_brightness_; optional red_; optional green_; optional blue_; diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index cdd05ae7b7..4b6ca9e576 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -13,17 +13,24 @@ namespace light { /** This class represents the color state for a light object. * - * All values in this class are represented using floats in the range from 0.0 (off) to 1.0 (on). - * Not all values have to be populated though, for example a simple monochromatic light only needs - * to access the state and brightness attributes. + * All values in this class (except color temperature) are represented using floats in the range + * from 0.0 (off) to 1.0 (on). Please note that all values are automatically clamped to this range. * - * Please note all float values are automatically clamped. + * This class has the following properties: + * - state: Whether the light should be on/off. Represented as a float for transitions. Used for + * all lights. + * - brightness: The master brightness of the light, applied to all channels. Used for all lights + * with brightness control. + * - color_brightness: The brightness of the color channels of the light. Used for RGB, RGBW and + * RGBWW lights. + * - red, green, blue: The RGB values of the current color. They are normalized, so at least one of + * them is always 1.0. + * - white: The brightness of the white channel of the light. Used for RGBW and RGBWW lights. + * - color_temperature: The color temperature of the white channel in mireds. Used for RGBWW and + * CWWW lights. * - * state - Whether the light should be on/off. Represented as a float for transitions. - * brightness - The brightness of the light. - * red, green, blue - RGB values. - * white - The white value for RGBW lights. - * color_temperature - Temperature of the white value, range from 0.0 (cold) to 1.0 (warm) + * For lights with a color interlock (RGB lights and white light cannot be on at the same time), a + * valid state has always either color_brightness or white (or both) set to zero. */ class LightColorValues { public: @@ -31,16 +38,18 @@ class LightColorValues { LightColorValues() : state_(0.0f), brightness_(1.0f), + color_brightness_(1.0f), red_(1.0f), green_(1.0f), blue_(1.0f), white_(1.0f), color_temperature_{1.0f} {} - LightColorValues(float state, float brightness, float red, float green, float blue, float white, - float color_temperature = 1.0f) { + LightColorValues(float state, float brightness, float color_brightness, float red, float green, float blue, + float white, float color_temperature = 1.0f) { this->set_state(state); this->set_brightness(brightness); + this->set_color_brightness(color_brightness); this->set_red(red); this->set_green(green); this->set_blue(blue); @@ -48,38 +57,46 @@ class LightColorValues { this->set_color_temperature(color_temperature); } - LightColorValues(bool state, float brightness, float red, float green, float blue, float white, - float color_temperature = 1.0f) - : LightColorValues(state ? 1.0f : 0.0f, brightness, red, green, blue, white, color_temperature) {} + LightColorValues(bool state, float brightness, float color_brightness, float red, float green, float blue, + float white, float color_temperature = 1.0f) + : LightColorValues(state ? 1.0f : 0.0f, brightness, color_brightness, red, green, blue, white, + color_temperature) {} /// Create light color values from a binary true/false state. - static LightColorValues from_binary(bool state) { return {state, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; } + static LightColorValues from_binary(bool state) { return {state, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; } /// Create light color values from a monochromatic brightness state. static LightColorValues from_monochromatic(float brightness) { if (brightness == 0.0f) - return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; + return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; else - return {1.0f, brightness, 1.0f, 1.0f, 1.0f, 1.0f}; + return {1.0f, brightness, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; } /// Create light color values from an RGB state. static LightColorValues from_rgb(float r, float g, float b) { float brightness = std::max(r, std::max(g, b)); if (brightness == 0.0f) { - return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; + return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; } else { - return {1.0f, brightness, r / brightness, g / brightness, b / brightness, 1.0f}; + return {1.0f, brightness, 1.0f, r / brightness, g / brightness, b / brightness, 1.0f}; } } /// Create light color values from an RGBW state. static LightColorValues from_rgbw(float r, float g, float b, float w) { - float brightness = std::max(r, std::max(g, std::max(b, w))); - if (brightness == 0.0f) { - return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; + float color_brightness = std::max(r, std::max(g, b)); + float master_brightness = std::max(color_brightness, w); + if (master_brightness == 0.0f) { + return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; } else { - return {1.0f, brightness, r / brightness, g / brightness, b / brightness, w / brightness}; + return {1.0f, + master_brightness, + color_brightness / master_brightness, + r / color_brightness, + g / color_brightness, + b / color_brightness, + w / master_brightness}; } } @@ -97,6 +114,7 @@ class LightColorValues { LightColorValues v; v.set_state(esphome::lerp(completion, start.get_state(), end.get_state())); v.set_brightness(esphome::lerp(completion, start.get_brightness(), end.get_brightness())); + v.set_color_brightness(esphome::lerp(completion, start.get_color_brightness(), end.get_color_brightness())); v.set_red(esphome::lerp(completion, start.get_red(), end.get_red())); v.set_green(esphome::lerp(completion, start.get_green(), end.get_green())); v.set_blue(esphome::lerp(completion, start.get_blue(), end.get_blue())); @@ -121,8 +139,10 @@ class LightColorValues { color["g"] = uint8_t(this->get_green() * 255); color["b"] = uint8_t(this->get_blue() * 255); } - if (traits.get_supports_rgb_white_value()) + if (traits.get_supports_rgb_white_value()) { + root["color_brightness"] = uint8_t(this->get_color_brightness() * 255); root["white_value"] = uint8_t(this->get_white() * 255); + } if (traits.get_supports_color_temperature()) root["color_temp"] = uint32_t(this->get_color_temperature()); } @@ -131,21 +151,15 @@ class LightColorValues { /** Normalize the color (RGB/W) component. * * Divides all color attributes by the maximum attribute, so effectively set at least one attribute to 1. - * For example: r=0.3, g=0.5, b=0.4 => r=0.6, g=1.0, b=0.8 + * For example: r=0.3, g=0.5, b=0.4 => r=0.6, g=1.0, b=0.8. + * + * Note that this does NOT retain the brightness information from the color attributes. * * @param traits Used for determining which attributes to consider. */ void normalize_color(const LightTraits &traits) { if (traits.get_supports_rgb()) { float max_value = fmaxf(this->get_red(), fmaxf(this->get_green(), this->get_blue())); - if (traits.get_supports_rgb_white_value()) { - max_value = fmaxf(max_value, this->get_white()); - if (max_value == 0.0f) { - this->set_white(1.0f); - } else { - this->set_white(this->get_white() / max_value); - } - } if (max_value == 0.0f) { this->set_red(1.0f); this->set_green(1.0f); @@ -158,15 +172,10 @@ class LightColorValues { } if (traits.get_supports_brightness() && this->get_brightness() == 0.0f) { - if (traits.get_supports_rgb_white_value()) { - // 0% brightness for RGBW[W] means no RGB channel, but white channel on. - // do nothing - } else { - // 0% brightness means off - this->set_state(false); - // reset brightness to 100% - this->set_brightness(1.0f); - } + // 0% brightness means off + this->set_state(false); + // reset brightness to 100% + this->set_brightness(1.0f); } } @@ -180,9 +189,9 @@ class LightColorValues { /// Convert these light color values to an RGB representation and write them to red, green, blue. void as_rgb(float *red, float *green, float *blue, float gamma = 0, bool color_interlock = false) const { - float brightness = this->state_ * this->brightness_; - if (color_interlock) { - brightness = brightness * (1.0f - this->white_); + float brightness = this->state_ * this->brightness_ * this->color_brightness_; + if (color_interlock && this->white_ > 0.0f) { + brightness = 0; } *red = gamma_correct(brightness * this->red_, gamma); *green = gamma_correct(brightness * this->green_, gamma); @@ -232,8 +241,9 @@ class LightColorValues { /// Compare this LightColorValues to rhs, return true if and only if all attributes match. bool operator==(const LightColorValues &rhs) const { - return state_ == rhs.state_ && brightness_ == rhs.brightness_ && red_ == rhs.red_ && green_ == rhs.green_ && - blue_ == rhs.blue_ && white_ == rhs.white_ && color_temperature_ == rhs.color_temperature_; + return state_ == rhs.state_ && brightness_ == rhs.brightness_ && color_brightness_ == rhs.color_brightness_ && + red_ == rhs.red_ && green_ == rhs.green_ && blue_ == rhs.blue_ && white_ == rhs.white_ && + color_temperature_ == rhs.color_temperature_; } bool operator!=(const LightColorValues &rhs) const { return !(rhs == *this); } @@ -251,6 +261,11 @@ class LightColorValues { /// Set the brightness property of these light color values. In range 0.0 to 1.0 void set_brightness(float brightness) { this->brightness_ = clamp(brightness, 0.0f, 1.0f); } + /// Get the color brightness property of these light color values. In range 0.0 to 1.0 + float get_color_brightness() const { return this->color_brightness_; } + /// Set the color brightness property of these light color values. In range 0.0 to 1.0 + void set_color_brightness(float brightness) { this->color_brightness_ = clamp(brightness, 0.0f, 1.0f); } + /// Get the red property of these light color values. In range 0.0 to 1.0 float get_red() const { return this->red_; } /// Set the red property of these light color values. In range 0.0 to 1.0 @@ -281,6 +296,7 @@ class LightColorValues { protected: float state_; ///< ON / OFF, float for transition float brightness_; + float color_brightness_; float red_; float green_; float blue_; diff --git a/esphome/const.py b/esphome/const.py index bb20c606ce..9b7490878d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -121,6 +121,7 @@ CONF_CODE = "code" CONF_COLD_WHITE = "cold_white" CONF_COLD_WHITE_COLOR_TEMPERATURE = "cold_white_color_temperature" CONF_COLOR = "color" +CONF_COLOR_BRIGHTNESS = "color_brightness" CONF_COLOR_CORRECT = "color_correct" CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors"