From 5983ccc55ca722f3ddd676e6a6e59d1fcdd59f16 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Thu, 29 Jul 2021 19:11:56 +0200 Subject: [PATCH] Color mode implementation (#2012) --- esphome/components/api/api.proto | 31 +- esphome/components/api/api_connection.cpp | 39 +- esphome/components/api/api_pb2.cpp | 156 ++++++- esphome/components/api/api_pb2.h | 30 +- .../binary/light/binary_light_output.h | 2 +- esphome/components/cwww/cwww_light_output.h | 9 +- esphome/components/cwww/light.py | 19 +- esphome/components/demo/demo_light.h | 28 +- .../components/fastled_base/fastled_light.h | 3 +- .../components/hbridge/hbridge_light_output.h | 28 +- esphome/components/light/__init__.py | 4 +- esphome/components/light/automation.h | 6 + esphome/components/light/automation.py | 17 + esphome/components/light/base_light_effects.h | 39 +- esphome/components/light/color_mode.h | 107 +++++ esphome/components/light/effects.py | 20 + esphome/components/light/light_call.cpp | 429 +++++++++++------- esphome/components/light/light_call.h | 37 +- esphome/components/light/light_color_values.h | 224 ++++----- .../components/light/light_json_schema.cpp | 165 +++++++ esphome/components/light/light_json_schema.h | 25 + esphome/components/light/light_state.cpp | 28 +- esphome/components/light/light_state.h | 5 - esphome/components/light/light_traits.h | 53 ++- esphome/components/light/light_transformer.h | 35 +- esphome/components/light/types.py | 14 + .../monochromatic_light_output.h | 2 +- esphome/components/mqtt/mqtt_light.cpp | 34 +- .../neopixelbus/neopixelbus_light.h | 7 +- esphome/components/rgb/rgb_light_output.h | 3 +- esphome/components/rgbw/rgbw_light_output.h | 8 +- esphome/components/rgbww/light.py | 18 +- esphome/components/rgbww/rgbww_light_output.h | 17 +- esphome/components/tuya/light/tuya_light.cpp | 9 +- esphome/components/web_server/web_server.cpp | 6 +- esphome/config_validation.py | 17 + esphome/const.py | 1 + esphome/core/helpers.cpp | 9 + esphome/core/helpers.h | 2 + 39 files changed, 1210 insertions(+), 476 deletions(-) create mode 100644 esphome/components/light/color_mode.h create mode 100644 esphome/components/light/light_json_schema.cpp create mode 100644 esphome/components/light/light_json_schema.h diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 073775ed2e..c04e0a32f1 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -352,6 +352,18 @@ message FanCommandRequest { } // ==================== LIGHT ==================== +enum ColorMode { + COLOR_MODE_UNKNOWN = 0; + COLOR_MODE_ON_OFF = 1; + COLOR_MODE_BRIGHTNESS = 2; + COLOR_MODE_WHITE = 7; + COLOR_MODE_COLOR_TEMPERATURE = 11; + COLOR_MODE_COLD_WARM_WHITE = 19; + COLOR_MODE_RGB = 35; + COLOR_MODE_RGB_WHITE = 39; + COLOR_MODE_RGB_COLOR_TEMPERATURE = 47; + COLOR_MODE_RGB_COLD_WARM_WHITE = 51; +} message ListEntitiesLightResponse { option (id) = 15; option (source) = SOURCE_SERVER; @@ -362,10 +374,12 @@ message ListEntitiesLightResponse { string name = 3; string unique_id = 4; - bool supports_brightness = 5; - bool supports_rgb = 6; - bool supports_white_value = 7; - bool supports_color_temperature = 8; + repeated ColorMode supported_color_modes = 12; + // next four supports_* are for legacy clients, newer clients should use color modes + bool legacy_supports_brightness = 5 [deprecated=true]; + bool legacy_supports_rgb = 6 [deprecated=true]; + bool legacy_supports_white_value = 7 [deprecated=true]; + bool legacy_supports_color_temperature = 8 [deprecated=true]; float min_mireds = 9; float max_mireds = 10; repeated string effects = 11; @@ -379,12 +393,15 @@ message LightStateResponse { fixed32 key = 1; bool state = 2; float brightness = 3; + ColorMode color_mode = 11; float color_brightness = 10; float red = 4; float green = 5; float blue = 6; float white = 7; float color_temperature = 8; + float cold_white = 12; + float warm_white = 13; string effect = 9; } message LightCommandRequest { @@ -398,6 +415,8 @@ message LightCommandRequest { bool state = 3; bool has_brightness = 4; float brightness = 5; + bool has_color_mode = 22; + ColorMode color_mode = 23; bool has_color_brightness = 20; float color_brightness = 21; bool has_rgb = 6; @@ -408,6 +427,10 @@ message LightCommandRequest { float white = 11; bool has_color_temperature = 12; float color_temperature = 13; + bool has_cold_white = 24; + float cold_white = 25; + bool has_warm_white = 26; + float warm_white = 27; bool has_transition_length = 14; uint32 transition_length = 15; bool has_flash_length = 16; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index fb05772e5e..b92c67b042 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -301,22 +301,28 @@ bool APIConnection::send_light_state(light::LightState *light) { auto traits = light->get_traits(); auto values = light->remote_values; + auto color_mode = values.get_color_mode(); LightStateResponse resp{}; resp.key = light->get_object_id_hash(); resp.state = values.is_on(); - if (traits.get_supports_brightness()) + resp.color_mode = static_cast(color_mode); + if (color_mode & light::ColorCapability::BRIGHTNESS) resp.brightness = values.get_brightness(); - if (traits.get_supports_rgb()) { + if (color_mode & light::ColorCapability::RGB) { resp.color_brightness = values.get_color_brightness(); resp.red = values.get_red(); resp.green = values.get_green(); resp.blue = values.get_blue(); } - if (traits.get_supports_rgb_white_value()) + if (color_mode & light::ColorCapability::WHITE) resp.white = values.get_white(); - if (traits.get_supports_color_temperature()) + if (color_mode & light::ColorCapability::COLOR_TEMPERATURE) resp.color_temperature = values.get_color_temperature(); + if (color_mode & light::ColorCapability::COLD_WARM_WHITE) { + resp.cold_white = values.get_cold_white(); + resp.warm_white = values.get_warm_white(); + } if (light->supports_effects()) resp.effect = light->get_effect_name(); return this->send_light_state_response(resp); @@ -328,11 +334,18 @@ bool APIConnection::send_light_info(light::LightState *light) { msg.object_id = light->get_object_id(); msg.name = light->get_name(); msg.unique_id = get_default_unique_id("light", light); - msg.supports_brightness = traits.get_supports_brightness(); - msg.supports_rgb = traits.get_supports_rgb(); - msg.supports_white_value = traits.get_supports_rgb_white_value(); - msg.supports_color_temperature = traits.get_supports_color_temperature(); - if (msg.supports_color_temperature) { + for (auto mode : traits.get_supported_color_modes()) + msg.supported_color_modes.push_back(static_cast(mode)); + + msg.legacy_supports_brightness = traits.supports_color_capability(light::ColorCapability::BRIGHTNESS); + msg.legacy_supports_rgb = traits.supports_color_capability(light::ColorCapability::RGB); + msg.legacy_supports_white_value = + msg.legacy_supports_rgb && (traits.supports_color_capability(light::ColorCapability::WHITE) || + traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)); + msg.legacy_supports_color_temperature = traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || + traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE); + + if (msg.legacy_supports_color_temperature) { msg.min_mireds = traits.get_min_mireds(); msg.max_mireds = traits.get_max_mireds(); } @@ -353,6 +366,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_mode) + call.set_color_mode(static_cast(msg.color_mode)); if (msg.has_color_brightness) call.set_color_brightness(msg.color_brightness); if (msg.has_rgb) { @@ -364,6 +379,10 @@ void APIConnection::light_command(const LightCommandRequest &msg) { call.set_white(msg.white); if (msg.has_color_temperature) call.set_color_temperature(msg.color_temperature); + if (msg.has_cold_white) + call.set_cold_white(msg.cold_white); + if (msg.has_warm_white) + call.set_warm_white(msg.warm_white); if (msg.has_transition_length) call.set_transition_length(msg.transition_length); if (msg.has_flash_length) @@ -655,7 +674,7 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 5; + resp.api_version_minor = 6; resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; this->connection_state_ = ConnectionState::CONNECTED; return resp; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 057d71324f..79092cb511 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -62,6 +62,32 @@ template<> const char *proto_enum_to_string(enums::FanDirec return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::ColorMode value) { + switch (value) { + case enums::COLOR_MODE_UNKNOWN: + return "COLOR_MODE_UNKNOWN"; + case enums::COLOR_MODE_ON_OFF: + return "COLOR_MODE_ON_OFF"; + case enums::COLOR_MODE_BRIGHTNESS: + return "COLOR_MODE_BRIGHTNESS"; + case enums::COLOR_MODE_WHITE: + return "COLOR_MODE_WHITE"; + case enums::COLOR_MODE_COLOR_TEMPERATURE: + return "COLOR_MODE_COLOR_TEMPERATURE"; + case enums::COLOR_MODE_COLD_WARM_WHITE: + return "COLOR_MODE_COLD_WARM_WHITE"; + case enums::COLOR_MODE_RGB: + return "COLOR_MODE_RGB"; + case enums::COLOR_MODE_RGB_WHITE: + return "COLOR_MODE_RGB_WHITE"; + case enums::COLOR_MODE_RGB_COLOR_TEMPERATURE: + return "COLOR_MODE_RGB_COLOR_TEMPERATURE"; + case enums::COLOR_MODE_RGB_COLD_WARM_WHITE: + return "COLOR_MODE_RGB_COLD_WARM_WHITE"; + default: + return "UNKNOWN"; + } +} template<> const char *proto_enum_to_string(enums::SensorStateClass value) { switch (value) { case enums::STATE_CLASS_NONE: @@ -1117,20 +1143,24 @@ void FanCommandRequest::dump_to(std::string &out) const { } bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { + case 12: { + this->supported_color_modes.push_back(value.as_enum()); + return true; + } case 5: { - this->supports_brightness = value.as_bool(); + this->legacy_supports_brightness = value.as_bool(); return true; } case 6: { - this->supports_rgb = value.as_bool(); + this->legacy_supports_rgb = value.as_bool(); return true; } case 7: { - this->supports_white_value = value.as_bool(); + this->legacy_supports_white_value = value.as_bool(); return true; } case 8: { - this->supports_color_temperature = value.as_bool(); + this->legacy_supports_color_temperature = value.as_bool(); return true; } default: @@ -1182,10 +1212,13 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); buffer.encode_string(4, this->unique_id); - buffer.encode_bool(5, this->supports_brightness); - buffer.encode_bool(6, this->supports_rgb); - buffer.encode_bool(7, this->supports_white_value); - buffer.encode_bool(8, this->supports_color_temperature); + for (auto &it : this->supported_color_modes) { + buffer.encode_enum(12, it, true); + } + buffer.encode_bool(5, this->legacy_supports_brightness); + buffer.encode_bool(6, this->legacy_supports_rgb); + buffer.encode_bool(7, this->legacy_supports_white_value); + buffer.encode_bool(8, this->legacy_supports_color_temperature); buffer.encode_float(9, this->min_mireds); buffer.encode_float(10, this->max_mireds); for (auto &it : this->effects) { @@ -1212,20 +1245,26 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("'").append(this->unique_id).append("'"); out.append("\n"); - out.append(" supports_brightness: "); - out.append(YESNO(this->supports_brightness)); + for (const auto &it : this->supported_color_modes) { + out.append(" supported_color_modes: "); + out.append(proto_enum_to_string(it)); + out.append("\n"); + } + + out.append(" legacy_supports_brightness: "); + out.append(YESNO(this->legacy_supports_brightness)); out.append("\n"); - out.append(" supports_rgb: "); - out.append(YESNO(this->supports_rgb)); + out.append(" legacy_supports_rgb: "); + out.append(YESNO(this->legacy_supports_rgb)); out.append("\n"); - out.append(" supports_white_value: "); - out.append(YESNO(this->supports_white_value)); + out.append(" legacy_supports_white_value: "); + out.append(YESNO(this->legacy_supports_white_value)); out.append("\n"); - out.append(" supports_color_temperature: "); - out.append(YESNO(this->supports_color_temperature)); + out.append(" legacy_supports_color_temperature: "); + out.append(YESNO(this->legacy_supports_color_temperature)); out.append("\n"); out.append(" min_mireds: "); @@ -1251,6 +1290,10 @@ bool LightStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->state = value.as_bool(); return true; } + case 11: { + this->color_mode = value.as_enum(); + return true; + } default: return false; } @@ -1299,6 +1342,14 @@ bool LightStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { this->color_temperature = value.as_float(); return true; } + case 12: { + this->cold_white = value.as_float(); + return true; + } + case 13: { + this->warm_white = value.as_float(); + return true; + } default: return false; } @@ -1307,12 +1358,15 @@ 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_enum(11, this->color_mode); 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); buffer.encode_float(7, this->white); buffer.encode_float(8, this->color_temperature); + buffer.encode_float(12, this->cold_white); + buffer.encode_float(13, this->warm_white); buffer.encode_string(9, this->effect); } void LightStateResponse::dump_to(std::string &out) const { @@ -1332,6 +1386,10 @@ void LightStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" color_mode: "); + out.append(proto_enum_to_string(this->color_mode)); + out.append("\n"); + out.append(" color_brightness: "); sprintf(buffer, "%g", this->color_brightness); out.append(buffer); @@ -1362,6 +1420,16 @@ void LightStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" cold_white: "); + sprintf(buffer, "%g", this->cold_white); + out.append(buffer); + out.append("\n"); + + out.append(" warm_white: "); + sprintf(buffer, "%g", this->warm_white); + out.append(buffer); + out.append("\n"); + out.append(" effect: "); out.append("'").append(this->effect).append("'"); out.append("\n"); @@ -1381,6 +1449,14 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->has_brightness = value.as_bool(); return true; } + case 22: { + this->has_color_mode = value.as_bool(); + return true; + } + case 23: { + this->color_mode = value.as_enum(); + return true; + } case 20: { this->has_color_brightness = value.as_bool(); return true; @@ -1397,6 +1473,14 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->has_color_temperature = value.as_bool(); return true; } + case 24: { + this->has_cold_white = value.as_bool(); + return true; + } + case 26: { + this->has_warm_white = value.as_bool(); + return true; + } case 14: { this->has_transition_length = value.as_bool(); return true; @@ -1465,6 +1549,14 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { this->color_temperature = value.as_float(); return true; } + case 25: { + this->cold_white = value.as_float(); + return true; + } + case 27: { + this->warm_white = value.as_float(); + return true; + } default: return false; } @@ -1475,6 +1567,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(22, this->has_color_mode); + buffer.encode_enum(23, this->color_mode); buffer.encode_bool(20, this->has_color_brightness); buffer.encode_float(21, this->color_brightness); buffer.encode_bool(6, this->has_rgb); @@ -1485,6 +1579,10 @@ void LightCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(11, this->white); buffer.encode_bool(12, this->has_color_temperature); buffer.encode_float(13, this->color_temperature); + buffer.encode_bool(24, this->has_cold_white); + buffer.encode_float(25, this->cold_white); + buffer.encode_bool(26, this->has_warm_white); + buffer.encode_float(27, this->warm_white); buffer.encode_bool(14, this->has_transition_length); buffer.encode_uint32(15, this->transition_length); buffer.encode_bool(16, this->has_flash_length); @@ -1517,6 +1615,14 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" has_color_mode: "); + out.append(YESNO(this->has_color_mode)); + out.append("\n"); + + out.append(" color_mode: "); + out.append(proto_enum_to_string(this->color_mode)); + out.append("\n"); + out.append(" has_color_brightness: "); out.append(YESNO(this->has_color_brightness)); out.append("\n"); @@ -1563,6 +1669,24 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" has_cold_white: "); + out.append(YESNO(this->has_cold_white)); + out.append("\n"); + + out.append(" cold_white: "); + sprintf(buffer, "%g", this->cold_white); + out.append(buffer); + out.append("\n"); + + out.append(" has_warm_white: "); + out.append(YESNO(this->has_warm_white)); + out.append("\n"); + + out.append(" warm_white: "); + sprintf(buffer, "%g", this->warm_white); + out.append(buffer); + out.append("\n"); + out.append(" has_transition_length: "); out.append(YESNO(this->has_transition_length)); out.append("\n"); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 0551508b4b..4c92b7b00d 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -32,6 +32,18 @@ enum FanDirection : uint32_t { FAN_DIRECTION_FORWARD = 0, FAN_DIRECTION_REVERSE = 1, }; +enum ColorMode : uint32_t { + COLOR_MODE_UNKNOWN = 0, + COLOR_MODE_ON_OFF = 1, + COLOR_MODE_BRIGHTNESS = 2, + COLOR_MODE_WHITE = 7, + COLOR_MODE_COLOR_TEMPERATURE = 11, + COLOR_MODE_COLD_WARM_WHITE = 19, + COLOR_MODE_RGB = 35, + COLOR_MODE_RGB_WHITE = 39, + COLOR_MODE_RGB_COLOR_TEMPERATURE = 47, + COLOR_MODE_RGB_COLD_WARM_WHITE = 51, +}; enum SensorStateClass : uint32_t { STATE_CLASS_NONE = 0, STATE_CLASS_MEASUREMENT = 1, @@ -356,10 +368,11 @@ class ListEntitiesLightResponse : public ProtoMessage { uint32_t key{0}; std::string name{}; std::string unique_id{}; - bool supports_brightness{false}; - bool supports_rgb{false}; - bool supports_white_value{false}; - bool supports_color_temperature{false}; + std::vector supported_color_modes{}; + bool legacy_supports_brightness{false}; + bool legacy_supports_rgb{false}; + bool legacy_supports_white_value{false}; + bool legacy_supports_color_temperature{false}; float min_mireds{0.0f}; float max_mireds{0.0f}; std::vector effects{}; @@ -376,12 +389,15 @@ class LightStateResponse : public ProtoMessage { uint32_t key{0}; bool state{false}; float brightness{0.0f}; + enums::ColorMode color_mode{}; float color_brightness{0.0f}; float red{0.0f}; float green{0.0f}; float blue{0.0f}; float white{0.0f}; float color_temperature{0.0f}; + float cold_white{0.0f}; + float warm_white{0.0f}; std::string effect{}; void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -398,6 +414,8 @@ class LightCommandRequest : public ProtoMessage { bool state{false}; bool has_brightness{false}; float brightness{0.0f}; + bool has_color_mode{false}; + enums::ColorMode color_mode{}; bool has_color_brightness{false}; float color_brightness{0.0f}; bool has_rgb{false}; @@ -408,6 +426,10 @@ class LightCommandRequest : public ProtoMessage { float white{0.0f}; bool has_color_temperature{false}; float color_temperature{0.0f}; + bool has_cold_white{false}; + float cold_white{0.0f}; + bool has_warm_white{false}; + float warm_white{0.0f}; bool has_transition_length{false}; uint32_t transition_length{0}; bool has_flash_length{false}; diff --git a/esphome/components/binary/light/binary_light_output.h b/esphome/components/binary/light/binary_light_output.h index 731973bdad..86c83aff5c 100644 --- a/esphome/components/binary/light/binary_light_output.h +++ b/esphome/components/binary/light/binary_light_output.h @@ -12,7 +12,7 @@ class BinaryLightOutput : public light::LightOutput { void set_output(output::BinaryOutput *output) { output_ = output; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(false); + traits.set_supported_color_modes({light::ColorMode::ON_OFF}); return traits; } void write_state(light::LightState *state) override { diff --git a/esphome/components/cwww/cwww_light_output.h b/esphome/components/cwww/cwww_light_output.h index 3351a98d24..2b7698ce5a 100644 --- a/esphome/components/cwww/cwww_light_output.h +++ b/esphome/components/cwww/cwww_light_output.h @@ -16,10 +16,7 @@ class CWWWLightOutput : public light::LightOutput { void set_constant_brightness(bool constant_brightness) { constant_brightness_ = constant_brightness; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); - traits.set_supports_rgb(false); - traits.set_supports_rgb_white_value(false); - traits.set_supports_color_temperature(true); + traits.set_supported_color_modes({light::ColorMode::COLD_WARM_WHITE}); traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); return traits; @@ -34,8 +31,8 @@ class CWWWLightOutput : public light::LightOutput { protected: output::FloatOutput *cold_white_; output::FloatOutput *warm_white_; - float cold_white_temperature_; - float warm_white_temperature_; + float cold_white_temperature_{0}; + float warm_white_temperature_{0}; bool constant_brightness_; }; diff --git a/esphome/components/cwww/light.py b/esphome/components/cwww/light.py index 1a027a86b4..734f9aa1e7 100644 --- a/esphome/components/cwww/light.py +++ b/esphome/components/cwww/light.py @@ -20,11 +20,14 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(CWWWLightOutput), cv.Required(CONF_COLD_WHITE): cv.use_id(output.FloatOutput), cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), - cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, - cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, } ), + cv.has_none_or_all_keys( + [CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_WARM_WHITE_COLOR_TEMPERATURE] + ), light.validate_color_temperature_channels, ) @@ -32,11 +35,19 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) await light.register_light(var, config) + cwhite = await cg.get_variable(config[CONF_COLD_WHITE]) cg.add(var.set_cold_white(cwhite)) - cg.add(var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE])) + if CONF_COLD_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE]) + ) wwhite = await cg.get_variable(config[CONF_WARM_WHITE]) cg.add(var.set_warm_white(wwhite)) - cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) + if CONF_WARM_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE]) + ) + cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS])) diff --git a/esphome/components/demo/demo_light.h b/esphome/components/demo/demo_light.h index 989dbf3306..2007e9ff50 100644 --- a/esphome/components/demo/demo_light.h +++ b/esphome/components/demo/demo_light.h @@ -28,45 +28,31 @@ class DemoLight : public light::LightOutput, public Component { void set_type(DemoLightType type) { type_ = type; } light::LightTraits get_traits() override { light::LightTraits traits{}; - // brightness - // rgb - // rgb_white_value - // color_temperature, min_mireds, max_mireds - // color_interlock switch (type_) { case DemoLightType::TYPE_1: + traits.set_supported_color_modes({light::ColorMode::ON_OFF}); break; case DemoLightType::TYPE_2: - traits.set_supports_brightness(true); + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); break; case DemoLightType::TYPE_3: - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); + traits.set_supported_color_modes({light::ColorMode::RGB}); break; case DemoLightType::TYPE_4: - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); - traits.set_supports_rgb_white_value(true); + traits.set_supported_color_modes({light::ColorMode::RGB_WHITE}); break; case DemoLightType::TYPE_5: - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); - traits.set_supports_rgb_white_value(true); - traits.set_supports_color_temperature(true); + traits.set_supported_color_modes({light::ColorMode::RGB_COLOR_TEMPERATURE}); traits.set_min_mireds(153); traits.set_max_mireds(500); break; case DemoLightType::TYPE_6: - traits.set_supports_brightness(true); - traits.set_supports_color_temperature(true); + traits.set_supported_color_modes({light::ColorMode::COLD_WARM_WHITE}); traits.set_min_mireds(153); traits.set_max_mireds(500); break; case DemoLightType::TYPE_7: - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); - traits.set_supports_rgb_white_value(true); - traits.set_supports_color_interlock(true); + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::WHITE}); break; } return traits; diff --git a/esphome/components/fastled_base/fastled_light.h b/esphome/components/fastled_base/fastled_light.h index 59d143dbef..ac6acc95a5 100644 --- a/esphome/components/fastled_base/fastled_light.h +++ b/esphome/components/fastled_base/fastled_light.h @@ -208,8 +208,7 @@ class FastLEDLightOutput : public light::AddressableLight { // (In most use cases you won't need these) light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); + traits.set_supported_color_modes({light::ColorMode::RGB}); return traits; } void setup() override; diff --git a/esphome/components/hbridge/hbridge_light_output.h b/esphome/components/hbridge/hbridge_light_output.h index 03a5b3a88c..2f4f87134c 100644 --- a/esphome/components/hbridge/hbridge_light_output.h +++ b/esphome/components/hbridge/hbridge_light_output.h @@ -18,10 +18,7 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); // Dimming - traits.set_supports_rgb(false); - traits.set_supports_rgb_white_value(true); // hbridge color - traits.set_supports_color_temperature(false); + traits.set_supported_color_modes({light::ColorMode::COLD_WARM_WHITE}); return traits; } @@ -31,11 +28,11 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { // This method runs around 60 times per second // We cannot do the PWM ourselves so we are reliant on the hardware PWM if (!this->forward_direction_) { // First LED Direction - this->pinb_pin_->set_level(this->duty_off_); this->pina_pin_->set_level(this->pina_duty_); + this->pinb_pin_->set_level(0); this->forward_direction_ = true; } else { // Second LED Direction - this->pina_pin_->set_level(this->duty_off_); + this->pina_pin_->set_level(0); this->pinb_pin_->set_level(this->pinb_duty_); this->forward_direction_ = false; } @@ -44,23 +41,7 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { float get_setup_priority() const override { return setup_priority::HARDWARE; } void write_state(light::LightState *state) override { - float bright; - state->current_values_as_brightness(&bright); - - state->set_gamma_correct(0); - float red, green, blue, white; - state->current_values_as_rgbw(&red, &green, &blue, &white); - - if ((white / bright) > 0.55) { - this->pina_duty_ = (bright * (1 - (white / bright))); - this->pinb_duty_ = bright; - } else if (white < 0.45) { - this->pina_duty_ = bright; - this->pinb_duty_ = white; - } else { - this->pina_duty_ = bright; - this->pinb_duty_ = bright; - } + state->current_values_as_cwww(&this->pina_duty_, &this->pinb_duty_, false); } protected: @@ -68,7 +49,6 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { output::FloatOutput *pinb_pin_; float pina_duty_ = 0; float pinb_duty_ = 0; - float duty_off_ = 0; bool forward_direction_ = false; }; diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 119ff3703c..ec7d6bcbc0 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -108,7 +108,9 @@ ADDRESSABLE_LIGHT_SCHEMA = RGB_LIGHT_SCHEMA.extend( def validate_color_temperature_channels(value): if ( - value[CONF_COLD_WHITE_COLOR_TEMPERATURE] + CONF_COLD_WHITE_COLOR_TEMPERATURE in value + and CONF_WARM_WHITE_COLOR_TEMPERATURE in value + and value[CONF_COLD_WHITE_COLOR_TEMPERATURE] >= value[CONF_WARM_WHITE_COLOR_TEMPERATURE] ): raise cv.Invalid( diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 02c6163555..a816049f08 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -27,6 +27,7 @@ template class LightControlAction : public Action { public: explicit LightControlAction(LightState *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(ColorMode, color_mode) TEMPLATABLE_VALUE(bool, state) TEMPLATABLE_VALUE(uint32_t, transition_length) TEMPLATABLE_VALUE(uint32_t, flash_length) @@ -37,10 +38,13 @@ template class LightControlAction : public Action { TEMPLATABLE_VALUE(float, blue) TEMPLATABLE_VALUE(float, white) TEMPLATABLE_VALUE(float, color_temperature) + TEMPLATABLE_VALUE(float, cold_white) + TEMPLATABLE_VALUE(float, warm_white) TEMPLATABLE_VALUE(std::string, effect) void play(Ts... x) override { auto call = this->parent_->make_call(); + call.set_color_mode(this->color_mode_.optional_value(x...)); 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...)); @@ -49,6 +53,8 @@ template class LightControlAction : public Action { call.set_blue(this->blue_.optional_value(x...)); call.set_white(this->white_.optional_value(x...)); call.set_color_temperature(this->color_temperature_.optional_value(x...)); + call.set_cold_white(this->cold_white_.optional_value(x...)); + call.set_warm_white(this->warm_white_.optional_value(x...)); call.set_effect(this->effect_.optional_value(x...)); call.set_flash_length(this->flash_length_.optional_value(x...)); call.set_transition_length(this->transition_length_.optional_value(x...)); diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 85741f1ffd..cfba273565 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.const import ( CONF_ID, + CONF_COLOR_MODE, CONF_TRANSITION_LENGTH, CONF_STATE, CONF_FLASH_LENGTH, @@ -14,10 +15,14 @@ from esphome.const import ( CONF_BLUE, CONF_WHITE, CONF_COLOR_TEMPERATURE, + CONF_COLD_WHITE, + CONF_WARM_WHITE, CONF_RANGE_FROM, CONF_RANGE_TO, ) from .types import ( + ColorMode, + COLOR_MODES, DimRelativeAction, ToggleAction, LightState, @@ -55,6 +60,7 @@ async def light_toggle_to_code(config, action_id, template_arg, args): LIGHT_CONTROL_ACTION_SCHEMA = cv.Schema( { cv.Required(CONF_ID): cv.use_id(LightState), + cv.Optional(CONF_COLOR_MODE): cv.enum(COLOR_MODES, upper=True, space="_"), cv.Optional(CONF_STATE): cv.templatable(cv.boolean), cv.Exclusive(CONF_TRANSITION_LENGTH, "transformer"): cv.templatable( cv.positive_time_period_milliseconds @@ -70,6 +76,8 @@ LIGHT_CONTROL_ACTION_SCHEMA = cv.Schema( cv.Optional(CONF_BLUE): cv.templatable(cv.percentage), cv.Optional(CONF_WHITE): cv.templatable(cv.percentage), cv.Optional(CONF_COLOR_TEMPERATURE): cv.templatable(cv.color_temperature), + cv.Optional(CONF_COLD_WHITE): cv.templatable(cv.percentage), + cv.Optional(CONF_WARM_WHITE): cv.templatable(cv.percentage), } ) LIGHT_TURN_OFF_ACTION_SCHEMA = automation.maybe_simple_id( @@ -102,6 +110,9 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id( async def light_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) + if CONF_COLOR_MODE in config: + template_ = await cg.templatable(config[CONF_COLOR_MODE], args, ColorMode) + cg.add(var.set_color_mode(template_)) if CONF_STATE in config: template_ = await cg.templatable(config[CONF_STATE], args, bool) cg.add(var.set_state(template_)) @@ -134,6 +145,12 @@ async def light_control_to_code(config, action_id, template_arg, args): if CONF_COLOR_TEMPERATURE in config: template_ = await cg.templatable(config[CONF_COLOR_TEMPERATURE], args, float) cg.add(var.set_color_temperature(template_)) + if CONF_COLD_WHITE in config: + template_ = await cg.templatable(config[CONF_COLD_WHITE], args, float) + cg.add(var.set_cold_white(template_)) + if CONF_WARM_WHITE in config: + template_ = await cg.templatable(config[CONF_WARM_WHITE], args, float) + cg.add(var.set_warm_white(template_)) if CONF_EFFECT in config: template_ = await cg.templatable(config[CONF_EFFECT], args, cg.std_string) cg.add(var.set_effect(template_)) diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index eb92fec642..f66b90f665 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -57,16 +57,31 @@ class RandomLightEffect : public LightEffect { if (now - this->last_color_change_ < this->update_interval_) { return; } + + auto color_mode = this->state_->remote_values.get_color_mode(); auto call = this->state_->turn_on(); - if (this->state_->get_traits().get_supports_rgb()) { - call.set_red_if_supported(random_float()); - call.set_green_if_supported(random_float()); - call.set_blue_if_supported(random_float()); - call.set_white_if_supported(random_float()); - } else { - call.set_brightness_if_supported(random_float()); + bool changed = false; + if (color_mode & ColorCapability::RGB) { + call.set_red(random_float()); + call.set_green(random_float()); + call.set_blue(random_float()); + changed = true; + } + if (color_mode & ColorCapability::COLOR_TEMPERATURE) { + float min = this->state_->get_traits().get_min_mireds(); + float max = this->state_->get_traits().get_max_mireds(); + call.set_color_temperature(min + random_float() * (max - min)); + changed = true; + } + if (color_mode & ColorCapability::COLD_WARM_WHITE) { + call.set_cold_white(random_float()); + call.set_warm_white(random_float()); + changed = true; + } + if (!changed) { + // only randomize brightness if there's no colored option available + call.set_brightness(random_float()); } - call.set_color_temperature_if_supported(random_float()); call.set_transition_length_if_supported(this->transition_length_); call.set_publish(true); call.set_save(false); @@ -142,7 +157,6 @@ class StrobeLightEffect : public LightEffect { if (!color.is_on()) { // Don't turn the light off, otherwise the light effect will be stopped call.set_brightness_if_supported(0.0f); - call.set_white_if_supported(0.0f); call.set_state(true); } call.set_publish(false); @@ -177,13 +191,16 @@ class FlickerLightEffect : public LightEffect { out.set_green(remote.get_green() * beta + current.get_green() * alpha + (random_cubic_float() * this->intensity_)); out.set_blue(remote.get_blue() * beta + current.get_blue() * alpha + (random_cubic_float() * this->intensity_)); out.set_white(remote.get_white() * beta + current.get_white() * alpha + (random_cubic_float() * this->intensity_)); + out.set_cold_white(remote.get_cold_white() * beta + current.get_cold_white() * alpha + + (random_cubic_float() * this->intensity_)); + out.set_warm_white(remote.get_warm_white() * beta + current.get_warm_white() * alpha + + (random_cubic_float() * this->intensity_)); auto traits = this->state_->get_traits(); auto call = this->state_->make_call(); call.set_publish(false); call.set_save(false); - if (traits.get_supports_brightness()) - call.set_transition_length(0); + call.set_transition_length_if_supported(0); call.from_light_color_values(out); call.set_state(true); call.perform(); diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h new file mode 100644 index 0000000000..0f5b7b4b93 --- /dev/null +++ b/esphome/components/light/color_mode.h @@ -0,0 +1,107 @@ +#pragma once + +#include + +namespace esphome { +namespace light { + +/// Color capabilities are the various outputs that a light has and that can be independently controlled by the user. +enum class ColorCapability : uint8_t { + /// Light can be turned on/off. + ON_OFF = 1 << 0, + /// Master brightness of the light can be controlled. + BRIGHTNESS = 1 << 1, + /// Brightness of white channel can be controlled separately from other channels. + WHITE = 1 << 2, + /// Color temperature can be controlled. + COLOR_TEMPERATURE = 1 << 3, + /// Brightness of cold and warm white output can be controlled. + COLD_WARM_WHITE = 1 << 4, + /// Color can be controlled using RGB format (includes a brightness control for the color). + RGB = 1 << 5 +}; + +/// Helper class to allow bitwise operations on ColorCapability +class ColorCapabilityHelper { + public: + constexpr ColorCapabilityHelper(ColorCapability val) : val_(val) {} + constexpr operator ColorCapability() const { return val_; } + constexpr operator uint8_t() const { return static_cast(val_); } + constexpr operator bool() const { return static_cast(val_) != 0; } + + protected: + ColorCapability val_; +}; +constexpr ColorCapabilityHelper operator&(ColorCapability lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorCapabilityHelper operator&(ColorCapabilityHelper lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorCapabilityHelper operator|(ColorCapability lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} +constexpr ColorCapabilityHelper operator|(ColorCapabilityHelper lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +/// Color modes are a combination of color capabilities that can be used at the same time. +enum class ColorMode : uint8_t { + /// No color mode configured (cannot be a supported mode, only active when light is off). + UNKNOWN = 0, + /// Only on/off control. + ON_OFF = (uint8_t) ColorCapability::ON_OFF, + /// Dimmable light. + BRIGHTNESS = (uint8_t) ColorCapability::BRIGHTNESS, + /// White output only (use only if the light also has another color mode such as RGB). + WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::WHITE), + /// Controllable color temperature output. + COLOR_TEMPERATURE = + (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::COLOR_TEMPERATURE), + /// Cold and warm white output with individually controllable brightness. + COLD_WARM_WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::COLD_WARM_WHITE), + /// RGB color output. + RGB = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::RGB), + /// RGB color output and a separate white output. + RGB_WHITE = + (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::RGB | ColorCapability::WHITE), + /// RGB color output and a separate white output with controllable color temperature. + RGB_COLOR_TEMPERATURE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::RGB | + ColorCapability::WHITE | ColorCapability::COLOR_TEMPERATURE), + /// RGB color output, and separate cold and warm white outputs. + RGB_COLD_WARM_WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::RGB | + ColorCapability::COLD_WARM_WHITE), +}; + +/// Helper class to allow bitwise operations on ColorMode with ColorCapability +class ColorModeHelper { + public: + constexpr ColorModeHelper(ColorMode val) : val_(val) {} + constexpr operator ColorMode() const { return val_; } + constexpr operator uint8_t() const { return static_cast(val_); } + constexpr operator bool() const { return static_cast(val_) != 0; } + + protected: + ColorMode val_; +}; +constexpr ColorModeHelper operator&(ColorMode lhs, ColorMode rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorModeHelper operator&(ColorMode lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorModeHelper operator&(ColorModeHelper lhs, ColorMode rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorModeHelper operator|(ColorMode lhs, ColorMode rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} +constexpr ColorModeHelper operator|(ColorMode lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} +constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index f6ce812c34..be558a8140 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -12,11 +12,15 @@ from esphome.const import ( CONF_STATE, CONF_DURATION, CONF_BRIGHTNESS, + CONF_COLOR_MODE, CONF_COLOR_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, + CONF_COLOR_TEMPERATURE, + CONF_COLD_WHITE, + CONF_WARM_WHITE, CONF_ALPHA, CONF_INTENSITY, CONF_SPEED, @@ -27,6 +31,8 @@ from esphome.const import ( ) from esphome.util import Registry from .types import ( + ColorMode, + COLOR_MODES, LambdaLightEffect, PulseLightEffect, RandomLightEffect, @@ -212,11 +218,17 @@ 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_MODE): cv.enum( + COLOR_MODES, upper=True, space="_" + ), 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, cv.Optional(CONF_WHITE, default=1.0): cv.percentage, + cv.Optional(CONF_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_COLD_WHITE, default=1.0): cv.percentage, + cv.Optional(CONF_WARM_WHITE, default=1.0): cv.percentage, cv.Required( CONF_DURATION ): cv.positive_time_period_milliseconds, @@ -225,11 +237,15 @@ async def random_effect_to_code(config, effect_id): cv.has_at_least_one_key( CONF_STATE, CONF_BRIGHTNESS, + CONF_COLOR_MODE, CONF_COLOR_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, + CONF_COLOR_TEMPERATURE, + CONF_COLD_WHITE, + CONF_WARM_WHITE, ), ), cv.Length(min=2), @@ -246,6 +262,7 @@ async def strobe_effect_to_code(config, effect_id): ( "color", LightColorValues( + color.get(CONF_COLOR_MODE, ColorMode.UNKNOWN), color[CONF_STATE], color[CONF_BRIGHTNESS], color[CONF_COLOR_BRIGHTNESS], @@ -253,6 +270,9 @@ async def strobe_effect_to_code(config, effect_id): color[CONF_GREEN], color[CONF_BLUE], color[CONF_WHITE], + color.get(CONF_COLOR_TEMPERATURE, 0.0), + color[CONF_COLD_WHITE], + color[CONF_WARM_WHITE], ), ), ("duration", color[CONF_DURATION]), diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index d7e8ce6298..536018c33b 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -7,95 +7,42 @@ namespace light { static const char *const TAG = "light"; -#ifdef USE_JSON -LightCall &LightCall::parse_color_json(JsonObject &root) { - if (root.containsKey("state")) { - auto val = parse_on_off(root["state"]); - switch (val) { - case PARSE_ON: - this->set_state(true); - break; - case PARSE_OFF: - this->set_state(false); - break; - case PARSE_TOGGLE: - this->set_state(!this->parent_->remote_values.is_on()); - break; - case PARSE_NONE: - break; - } +static const char *color_mode_to_human(ColorMode color_mode) { + switch (color_mode) { + case ColorMode::UNKNOWN: + return "Unknown"; + case ColorMode::WHITE: + return "White"; + case ColorMode::COLOR_TEMPERATURE: + return "Color temperature"; + case ColorMode::COLD_WARM_WHITE: + return "Cold/warm white"; + case ColorMode::RGB: + return "RGB"; + case ColorMode::RGB_WHITE: + return "RGBW"; + case ColorMode::RGB_COLD_WARM_WHITE: + return "RGB + cold/warm white"; + case ColorMode::RGB_COLOR_TEMPERATURE: + return "RGB + color temperature"; + default: + return ""; } - - if (root.containsKey("brightness")) { - this->set_brightness(float(root["brightness"]) / 255.0f); - } - - if (root.containsKey("color")) { - JsonObject &color = root["color"]; - // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. - float max_rgb = 0.0f; - if (color.containsKey("r")) { - float r = float(color["r"]) / 255.0f; - max_rgb = fmaxf(max_rgb, r); - this->set_red(r); - } - if (color.containsKey("g")) { - float g = float(color["g"]) / 255.0f; - max_rgb = fmaxf(max_rgb, g); - this->set_green(g); - } - if (color.containsKey("b")) { - float b = float(color["b"]) / 255.0f; - max_rgb = fmaxf(max_rgb, b); - this->set_blue(b); - } - if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) { - this->set_color_brightness(max_rgb); - } - } - - if (root.containsKey("white_value")) { - this->set_white(float(root["white_value"]) / 255.0f); - } - - if (root.containsKey("color_temp")) { - this->set_color_temperature(float(root["color_temp"])); - } - - return *this; } -LightCall &LightCall::parse_json(JsonObject &root) { - this->parse_color_json(root); - - if (root.containsKey("flash")) { - auto length = uint32_t(float(root["flash"]) * 1000); - this->set_flash_length(length); - } - - if (root.containsKey("transition")) { - auto length = uint32_t(float(root["transition"]) * 1000); - this->set_transition_length(length); - } - - if (root.containsKey("effect")) { - const char *effect = root["effect"]; - this->set_effect(effect); - } - - return *this; -} -#endif void LightCall::perform() { - // use remote values for fallback const char *name = this->parent_->get_name().c_str(); - if (this->publish_) { - ESP_LOGD(TAG, "'%s' Setting:", name); - } - LightColorValues v = this->validate_(); if (this->publish_) { + ESP_LOGD(TAG, "'%s' Setting:", name); + + // Only print color mode when it's being changed + ColorMode current_color_mode = this->parent_->remote_values.get_color_mode(); + if (this->color_mode_.value_or(current_color_mode) != current_color_mode) { + ESP_LOGD(TAG, " Color mode: %s", color_mode_to_human(v.get_color_mode())); + } + // Only print state when it's being changed bool current_state = this->parent_->remote_values.is_on(); if (this->state_.value_or(current_state) != current_state) { @@ -106,30 +53,35 @@ void LightCall::perform() { ESP_LOGD(TAG, " Brightness: %.0f%%", v.get_brightness() * 100.0f); } - if (this->color_temperature_.has_value()) { - ESP_LOGD(TAG, " Color Temperature: %.1f mireds", v.get_color_temperature()); - } - if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - ESP_LOGD(TAG, " Red=%.0f%%, Green=%.0f%%, Blue=%.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, + ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, v.get_blue() * 100.0f); } + if (this->white_.has_value()) { - ESP_LOGD(TAG, " White Value: %.0f%%", v.get_white() * 100.0f); + ESP_LOGD(TAG, " White: %.0f%%", v.get_white() * 100.0f); + } + if (this->color_temperature_.has_value()) { + ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); + } + + if (this->cold_white_.has_value() || this->warm_white_.has_value()) { + ESP_LOGD(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f, + v.get_warm_white() * 100.0f); } } if (this->has_flash_()) { // FLASH if (this->publish_) { - ESP_LOGD(TAG, " Flash Length: %.1fs", *this->flash_length_ / 1e3f); + ESP_LOGD(TAG, " Flash length: %.1fs", *this->flash_length_ / 1e3f); } this->parent_->start_flash_(v, *this->flash_length_); } else if (this->has_transition_()) { // TRANSITION if (this->publish_) { - ESP_LOGD(TAG, " Transition Length: %.1fs", *this->transition_length_ / 1e3f); + ESP_LOGD(TAG, " Transition length: %.1fs", *this->transition_length_ / 1e3f); } // Special case: Transition and effect can be set when turning off @@ -177,32 +129,48 @@ void LightCall::perform() { } LightColorValues LightCall::validate_() { - // use remote values for fallback auto *name = this->parent_->get_name().c_str(); auto traits = this->parent_->get_traits(); + // Color mode check + if (this->color_mode_.has_value() && !traits.supports_color_mode(this->color_mode_.value())) { + ESP_LOGW(TAG, "'%s' - This light does not support color mode %s!", name, + color_mode_to_human(this->color_mode_.value())); + this->color_mode_.reset(); + } + + // Ensure there is always a color mode set + if (!this->color_mode_.has_value()) { + this->color_mode_ = this->compute_color_mode_(); + } + auto color_mode = *this->color_mode_; + + // Transform calls that use non-native parameters for the current mode. + this->transform_parameters_(); + // Brightness exists check - if (this->brightness_.has_value() && !traits.get_supports_brightness()) { + if (this->brightness_.has_value() && !(color_mode & ColorCapability::BRIGHTNESS)) { ESP_LOGW(TAG, "'%s' - This light does not support setting brightness!", name); this->brightness_.reset(); } // Transition length possible check - if (this->transition_length_.has_value() && *this->transition_length_ != 0 && !traits.get_supports_brightness()) { + if (this->transition_length_.has_value() && *this->transition_length_ != 0 && + !(color_mode & ColorCapability::BRIGHTNESS)) { ESP_LOGW(TAG, "'%s' - This light does not support transitions!", name); 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); + if (this->color_brightness_.has_value() && !(color_mode & ColorCapability::RGB)) { + ESP_LOGW(TAG, "'%s' - This color mode 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()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting RGB color!", name); + if (!(color_mode & ColorCapability::RGB)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting RGB color!", name); this->red_.reset(); this->green_.reset(); this->blue_.reset(); @@ -210,68 +178,25 @@ LightColorValues LightCall::validate_() { } // White value exists check - if (this->white_.has_value() && !traits.get_supports_rgb_white_value()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting white value!", name); + if (this->white_.has_value() && + !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting white value!", name); this->white_.reset(); } // Color temperature exists check - if (this->color_temperature_.has_value() && !traits.get_supports_color_temperature()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting color temperature!", name); + if (this->color_temperature_.has_value() && + !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting color temperature!", name); this->color_temperature_.reset(); } - // 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 - 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); - - // if setting color temperature from color (i.e. switching to white light), set White to 100% - auto cv = this->parent_->remote_values; - bool was_color = cv.get_red() != 1.0f || cv.get_blue() != 1.0f || cv.get_green() != 1.0f; - if (traits.get_supports_color_interlock() || was_color) { - this->white_ = optional(1.0f); + // Cold/warm white value exists check + if (this->cold_white_.has_value() || this->warm_white_.has_value()) { + if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting cold/warm white value!", name); + this->cold_white_.reset(); + this->warm_white_.reset(); } } @@ -292,13 +217,22 @@ LightColorValues LightCall::validate_() { VALIDATE_RANGE(green, "Green") VALIDATE_RANGE(blue, "Blue") VALIDATE_RANGE(white, "White") + VALIDATE_RANGE(cold_white, "Cold white") + VALIDATE_RANGE(warm_white, "Warm white") + + // Set color brightness to 100% if currently zero and a color is set. + 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); + } auto v = this->parent_->remote_values; + if (this->color_mode_.has_value()) + v.set_color_mode(*this->color_mode_); if (this->state_.has_value()) v.set_state(*this->state_); 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()) @@ -309,9 +243,12 @@ LightColorValues LightCall::validate_() { v.set_blue(*this->blue_); if (this->white_.has_value()) v.set_white(*this->white_); - if (this->color_temperature_.has_value()) v.set_color_temperature(*this->color_temperature_); + if (this->cold_white_.has_value()) + v.set_cold_white(*this->cold_white_); + if (this->warm_white_.has_value()) + v.set_warm_white(*this->warm_white_); v.normalize_color(traits); @@ -322,7 +259,7 @@ LightColorValues LightCall::validate_() { } // validate transition length/flash length/effect not used at the same time - bool supports_transition = traits.get_supports_brightness(); + bool supports_transition = color_mode & ColorCapability::BRIGHTNESS; // If effect is already active, remove effect start if (this->has_effect_() && *this->effect_ == this->parent_->active_effect_index_) { @@ -331,7 +268,7 @@ LightColorValues LightCall::validate_() { // validate effect index if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { - ESP_LOGW(TAG, "'%s' Invalid effect index %u", name, *this->effect_); + ESP_LOGW(TAG, "'%s' - Invalid effect index %u!", name, *this->effect_); this->effect_.reset(); } @@ -381,6 +318,127 @@ LightColorValues LightCall::validate_() { return v; } +void LightCall::transform_parameters_() { + auto traits = this->parent_->get_traits(); + + // Allow CWWW modes to be set with a white value and/or color temperature. This is used by HA, + // which doesn't support CWWW modes (yet?), and for compatibility with the pre-colormode model, + // as CWWW and RGBWW lights used to represent their values as white + color temperature. + if (((this->white_.has_value() && *this->white_ > 0.0f) || this->color_temperature_.has_value()) && // + (*this->color_mode_ & ColorCapability::COLD_WARM_WHITE) && // + !(*this->color_mode_ & ColorCapability::WHITE) && // + !(*this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // + traits.get_min_mireds() > 0.0f && traits.get_max_mireds() > 0.0f) { + ESP_LOGD(TAG, "'%s' - Setting cold/warm white channels using white/color temperature values.", + this->parent_->get_name().c_str()); + auto current_values = this->parent_->remote_values; + if (this->color_temperature_.has_value()) { + const float white = + this->white_.value_or(fmaxf(current_values.get_cold_white(), current_values.get_warm_white())); + const float color_temp = clamp(*this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds()); + const float ww_fraction = + (color_temp - traits.get_min_mireds()) / (traits.get_max_mireds() - traits.get_min_mireds()); + const float cw_fraction = 1.0f - ww_fraction; + const float max_cw_ww = std::max(ww_fraction, cw_fraction); + this->cold_white_ = white * gamma_uncorrect(cw_fraction / max_cw_ww, this->parent_->get_gamma_correct()); + this->warm_white_ = white * gamma_uncorrect(ww_fraction / max_cw_ww, this->parent_->get_gamma_correct()); + } else { + const float max_cw_ww = std::max(current_values.get_warm_white(), current_values.get_cold_white()); + this->cold_white_ = *this->white_ * current_values.get_cold_white() / max_cw_ww; + this->warm_white_ = *this->white_ * current_values.get_warm_white() / max_cw_ww; + } + } +} +ColorMode LightCall::compute_color_mode_() { + auto supported_modes = this->parent_->get_traits().get_supported_color_modes(); + int supported_count = supported_modes.size(); + + // Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown. + if (supported_count == 0) + return ColorMode::UNKNOWN; + + // In the common case of lights supporting only a single mode, use that one. + if (supported_count == 1) + return *supported_modes.begin(); + + // Don't change if the light is being turned off. + ColorMode current_mode = this->parent_->remote_values.get_color_mode(); + if (this->state_.has_value() && !*this->state_) + return current_mode; + + // If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to + // pre-colormode clients and automations, but also for the MQTT API, where HA doesn't let us know which color mode + // was used for some reason. + std::set suitable_modes = this->get_suitable_color_modes_(); + + // Don't change if the current mode is suitable. + if (suitable_modes.count(current_mode) > 0) { + ESP_LOGI(TAG, "'%s' - Keeping current color mode %s for call without color mode.", + this->parent_->get_name().c_str(), color_mode_to_human(current_mode)); + return current_mode; + } + + // Use the preferred suitable mode. + for (auto mode : suitable_modes) { + if (supported_modes.count(mode) == 0) + continue; + + ESP_LOGI(TAG, "'%s' - Using color mode %s for call without color mode.", this->parent_->get_name().c_str(), + color_mode_to_human(mode)); + return mode; + } + + // There's no supported mode for this call, so warn, use the current more or a mode at random and let validation strip + // out whatever we don't support. + auto color_mode = current_mode != ColorMode::UNKNOWN ? current_mode : *supported_modes.begin(); + ESP_LOGW(TAG, "'%s' - No color mode suitable for this call supported, defaulting to %s!", + this->parent_->get_name().c_str(), color_mode_to_human(color_mode)); + return color_mode; +} +std::set LightCall::get_suitable_color_modes_() { + bool has_white = this->white_.has_value() && *this->white_ > 0.0f; + bool has_ct = this->color_temperature_.has_value(); + bool has_cwww = (this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || + (this->warm_white_.has_value() && *this->warm_white_ > 0.0f); + bool has_rgb = (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f) || + (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()); + +#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3) +#define ENTRY(white, ct, cwww, rgb, ...) \ + std::make_tuple>(KEY(white, ct, cwww, rgb), __VA_ARGS__) + + // Flag order: white, color temperature, cwww, rgb + std::array>, 10> lookup_table{ + ENTRY(true, false, false, false, + {ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, + ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, true, false, false, + {ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, + ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(true, true, false, false, + {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, false, true, false, {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, false, false, false, + {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB, + ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE}), + ENTRY(true, false, false, true, + {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, true, false, true, {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(true, true, false, true, {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, false, true, true, {ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, false, false, true, + {ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + }; + + auto key = KEY(has_white, has_ct, has_cwww, has_rgb); + for (auto &item : lookup_table) + if (std::get<0>(item) == key) + return std::get<1>(item); + + // This happens if there are conflicting flags given. + return {}; +} + LightCall &LightCall::set_effect(const std::string &effect) { if (strcasecmp(effect.c_str(), "none") == 0) { this->set_effect(0); @@ -406,53 +464,74 @@ 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_color_mode_if_supported(values.get_color_mode()); this->set_red_if_supported(values.get_red()); this->set_green_if_supported(values.get_green()); this->set_blue_if_supported(values.get_blue()); this->set_white_if_supported(values.get_white()); this->set_color_temperature_if_supported(values.get_color_temperature()); + this->set_cold_white_if_supported(values.get_cold_white()); + this->set_warm_white_if_supported(values.get_warm_white()); return *this; } +ColorMode LightCall::get_active_color_mode_() { + return this->color_mode_.value_or(this->parent_->remote_values.get_color_mode()); +} LightCall &LightCall::set_transition_length_if_supported(uint32_t transition_length) { - if (this->parent_->get_traits().get_supports_brightness()) + if (this->get_active_color_mode_() & ColorCapability::BRIGHTNESS) this->set_transition_length(transition_length); return *this; } LightCall &LightCall::set_brightness_if_supported(float brightness) { - if (this->parent_->get_traits().get_supports_brightness()) + if (this->get_active_color_mode_() & ColorCapability::BRIGHTNESS) this->set_brightness(brightness); return *this; } +LightCall &LightCall::set_color_mode_if_supported(ColorMode color_mode) { + if (this->parent_->get_traits().supports_color_mode(color_mode)) + this->color_mode_ = color_mode; + return *this; +} LightCall &LightCall::set_color_brightness_if_supported(float brightness) { - if (this->parent_->get_traits().get_supports_rgb_white_value()) + if (this->get_active_color_mode_() & ColorCapability::RGB) this->set_color_brightness(brightness); return *this; } LightCall &LightCall::set_red_if_supported(float red) { - if (this->parent_->get_traits().get_supports_rgb()) + if (this->get_active_color_mode_() & ColorCapability::RGB) this->set_red(red); return *this; } LightCall &LightCall::set_green_if_supported(float green) { - if (this->parent_->get_traits().get_supports_rgb()) + if (this->get_active_color_mode_() & ColorCapability::RGB) this->set_green(green); return *this; } LightCall &LightCall::set_blue_if_supported(float blue) { - if (this->parent_->get_traits().get_supports_rgb()) + if (this->get_active_color_mode_() & ColorCapability::RGB) this->set_blue(blue); return *this; } LightCall &LightCall::set_white_if_supported(float white) { - if (this->parent_->get_traits().get_supports_rgb_white_value()) + if (this->get_active_color_mode_() & ColorCapability::WHITE) this->set_white(white); return *this; } LightCall &LightCall::set_color_temperature_if_supported(float color_temperature) { - if (this->parent_->get_traits().get_supports_color_temperature()) + if (this->get_active_color_mode_() & ColorCapability::COLOR_TEMPERATURE) this->set_color_temperature(color_temperature); return *this; } +LightCall &LightCall::set_cold_white_if_supported(float cold_white) { + if (this->get_active_color_mode_() & ColorCapability::COLD_WARM_WHITE) + this->set_cold_white(cold_white); + return *this; +} +LightCall &LightCall::set_warm_white_if_supported(float warm_white) { + if (this->get_active_color_mode_() & ColorCapability::COLD_WARM_WHITE) + this->set_warm_white(warm_white); + return *this; +} LightCall &LightCall::set_state(optional state) { this->state_ = state; return *this; @@ -485,6 +564,14 @@ LightCall &LightCall::set_brightness(float brightness) { this->brightness_ = brightness; return *this; } +LightCall &LightCall::set_color_mode(optional color_mode) { + this->color_mode_ = color_mode; + return *this; +} +LightCall &LightCall::set_color_mode(ColorMode color_mode) { + this->color_mode_ = color_mode; + return *this; +} LightCall &LightCall::set_color_brightness(optional brightness) { this->color_brightness_ = brightness; return *this; @@ -533,6 +620,22 @@ LightCall &LightCall::set_color_temperature(float color_temperature) { this->color_temperature_ = color_temperature; return *this; } +LightCall &LightCall::set_cold_white(optional cold_white) { + this->cold_white_ = cold_white; + return *this; +} +LightCall &LightCall::set_cold_white(float cold_white) { + this->cold_white_ = cold_white; + return *this; +} +LightCall &LightCall::set_warm_white(optional warm_white) { + this->warm_white_ = warm_white; + return *this; +} +LightCall &LightCall::set_warm_white(float warm_white) { + this->warm_white_ = warm_white; + return *this; +} LightCall &LightCall::set_effect(optional effect) { if (effect.has_value()) this->set_effect(*effect); diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index c63b63bc54..31dc47e55d 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -44,6 +44,14 @@ 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 mode of the light. + LightCall &set_color_mode(optional color_mode); + /// Set the color mode of the light. + LightCall &set_color_mode(ColorMode color_mode); + /// Set the color mode of the light, if this mode is supported. + LightCall &set_color_mode_if_supported(ColorMode color_mode); + /// 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) @@ -98,6 +106,18 @@ class LightCall { LightCall &set_color_temperature(float color_temperature); /// Set the color_temperature property if the light supports color temperature. LightCall &set_color_temperature_if_supported(float color_temperature); + /// Set the cold white value of the light from 0.0 to 1.0. + LightCall &set_cold_white(optional cold_white); + /// Set the cold white value of the light from 0.0 to 1.0. + LightCall &set_cold_white(float cold_white); + /// Set the cold white property if the light supports cold white output. + LightCall &set_cold_white_if_supported(float cold_white); + /// Set the warm white value of the light from 0.0 to 1.0. + LightCall &set_warm_white(optional warm_white); + /// Set the warm white value of the light from 0.0 to 1.0. + LightCall &set_warm_white(float warm_white); + /// Set the warm white property if the light supports cold white output. + LightCall &set_warm_white_if_supported(float warm_white); /// Set the effect of the light by its name. LightCall &set_effect(optional effect); /// Set the effect of the light by its name. @@ -131,18 +151,24 @@ class LightCall { * @return The light call for chaining setters. */ LightCall &set_rgbw(float red, float green, float blue, float white); -#ifdef USE_JSON - LightCall &parse_color_json(JsonObject &root); - LightCall &parse_json(JsonObject &root); -#endif LightCall &from_light_color_values(const LightColorValues &values); void perform(); protected: + /// Get the currently targeted, or active if none set, color mode. + ColorMode get_active_color_mode_(); + /// Validate all properties and return the target light color values. LightColorValues validate_(); + //// Compute the color mode that should be used for this call. + ColorMode compute_color_mode_(); + /// Get potential color modes for this light call. + std::set get_suitable_color_modes_(); + /// Some color modes also can be set using non-native parameters, transform those calls. + void transform_parameters_(); + bool has_transition_() { return this->transition_length_.has_value(); } bool has_flash_() { return this->flash_length_.has_value(); } bool has_effect_() { return this->effect_.has_value(); } @@ -151,6 +177,7 @@ class LightCall { optional state_; optional transition_length_; optional flash_length_; + optional color_mode_; optional brightness_; optional color_brightness_; optional red_; @@ -158,6 +185,8 @@ class LightCall { optional blue_; optional white_; optional color_temperature_; + optional cold_white_; + optional warm_white_; optional effect_; bool publish_{true}; bool save_{true}; diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 344411b75e..5a5a2f6e34 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -15,40 +15,56 @@ inline static uint8_t to_uint8_scale(float x) { return static_cast(roun /** This class represents the color state for a light object. * - * 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. + * The representation of the color state is dependent on the active color mode. A color mode consists of multiple + * color capabilities, and each color capability has its own representation in this class. The fields available are as + * follows: * - * 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. + * Always: + * - color_mode: The currently active color mode. * - * 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. + * For ON_OFF capability: + * - state: Whether the light should be on/off. Represented as a float for transitions. + * + * For BRIGHTNESS capability: + * - brightness: The master brightness of the light, should be applied to all channels. + * + * For RGB capability: + * - color_brightness: The brightness of the color channels of the light. + * - red, green, blue: The RGB values of the current color. They are normalized, so at least one of them is always 1.0. + * + * For WHITE capability: + * - white: The brightness of the white channel of the light. + * + * For COLOR_TEMPERATURE capability: + * - color_temperature: The color temperature of the white channel in mireds. Note that it is not clamped to the valid + * range as set in the traits, so the output needs to do this. + * + * For COLD_WARM_WHITE capability: + * - cold_white, warm_white: The brightness of the cald and warm white channels of the light. + * + * All values (except color temperature) are represented using floats in the range 0.0 (off) to 1.0 (on), and are + * automatically clamped to this range. Properties not used in the current color mode can still have (invalid) values + * and must not be accessed by the light output. */ class LightColorValues { public: - /// Construct the LightColorValues with all attributes enabled, but state set to 0.0 + /// Construct the LightColorValues with all attributes enabled, but state set to off. LightColorValues() - : state_(0.0f), + : color_mode_(ColorMode::UNKNOWN), + 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} {} + color_temperature_{0.0f}, + cold_white_{1.0f}, + warm_white_{1.0f} {} - LightColorValues(float state, float brightness, float color_brightness, float red, float green, float blue, - float white, float color_temperature = 1.0f) { + LightColorValues(ColorMode color_mode, float state, float brightness, float color_brightness, float red, float green, + float blue, float white, float color_temperature, float cold_white, float warm_white) { + this->set_color_mode(color_mode); this->set_state(state); this->set_brightness(brightness); this->set_color_brightness(color_brightness); @@ -57,49 +73,8 @@ class LightColorValues { this->set_blue(blue); this->set_white(white); this->set_color_temperature(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, 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, 1.0f}; - else - 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, 1.0f}; - } else { - 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 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, - master_brightness, - color_brightness / master_brightness, - r / color_brightness, - g / color_brightness, - b / color_brightness, - w / master_brightness}; - } + this->set_cold_white(cold_white); + this->set_warm_white(warm_white); } /** Linearly interpolate between the values in start to the values in end. @@ -114,6 +89,7 @@ class LightColorValues { */ static LightColorValues lerp(const LightColorValues &start, const LightColorValues &end, float completion) { LightColorValues v; + v.set_color_mode(end.color_mode_); 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())); @@ -122,33 +98,11 @@ class LightColorValues { v.set_blue(esphome::lerp(completion, start.get_blue(), end.get_blue())); v.set_white(esphome::lerp(completion, start.get_white(), end.get_white())); v.set_color_temperature(esphome::lerp(completion, start.get_color_temperature(), end.get_color_temperature())); + v.set_cold_white(esphome::lerp(completion, start.get_cold_white(), end.get_cold_white())); + v.set_warm_white(esphome::lerp(completion, start.get_warm_white(), end.get_warm_white())); return v; } -#ifdef USE_JSON - /** Dump this color into a JsonObject. Only dumps values if the corresponding traits are marked supported by traits. - * - * @param root The json root object. - * @param traits The traits object used for determining whether to include certain attributes. - */ - void dump_json(JsonObject &root, const LightTraits &traits) const { - root["state"] = (this->get_state() != 0.0f) ? "ON" : "OFF"; - if (traits.get_supports_brightness()) - root["brightness"] = uint8_t(this->get_brightness() * 255); - if (traits.get_supports_rgb()) { - JsonObject &color = root.createNestedObject("color"); - color["r"] = uint8_t(this->get_color_brightness() * this->get_red() * 255); - color["g"] = uint8_t(this->get_color_brightness() * this->get_green() * 255); - color["b"] = uint8_t(this->get_color_brightness() * this->get_blue() * 255); - } - if (traits.get_supports_rgb_white_value()) { - root["white_value"] = uint8_t(this->get_white() * 255); - } - if (traits.get_supports_color_temperature()) - root["color_temp"] = uint32_t(this->get_color_temperature()); - } -#endif - /** Normalize the color (RGB/W) component. * * Divides all color attributes by the maximum attribute, so effectively set at least one attribute to 1. @@ -159,7 +113,7 @@ class LightColorValues { * @param traits Used for determining which attributes to consider. */ void normalize_color(const LightTraits &traits) { - if (traits.get_supports_rgb()) { + if (this->color_mode_ & ColorCapability::RGB) { float max_value = fmaxf(this->get_red(), fmaxf(this->get_green(), this->get_blue())); if (max_value == 0.0f) { this->set_red(1.0f); @@ -172,7 +126,7 @@ class LightColorValues { } } - if (traits.get_supports_brightness() && this->get_brightness() == 0.0f) { + if (this->color_mode_ & ColorCapability::BRIGHTNESS && this->get_brightness() == 0.0f) { // 0% brightness means off this->set_state(false); // reset brightness to 100% @@ -180,6 +134,9 @@ class LightColorValues { } } + // Note that method signature of as_* methods is kept as-is for compatibility reasons, so not all parameters + // are always used or necessary. Methods will be deprecated later. + /// Convert these light color values to a binary representation and write them to binary. void as_binary(bool *binary) const { *binary = this->state_ == 1.0f; } @@ -190,64 +147,68 @@ 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_ * this->color_brightness_; - if (color_interlock && this->white_ > 0.0f) { - brightness = 0; + if (this->color_mode_ & ColorCapability::RGB) { + float brightness = this->state_ * this->brightness_ * this->color_brightness_; + *red = gamma_correct(brightness * this->red_, gamma); + *green = gamma_correct(brightness * this->green_, gamma); + *blue = gamma_correct(brightness * this->blue_, gamma); + } else { + *red = *green = *blue = 0; } - *red = gamma_correct(brightness * this->red_, gamma); - *green = gamma_correct(brightness * this->green_, gamma); - *blue = gamma_correct(brightness * this->blue_, gamma); } /// Convert these light color values to an RGBW representation and write them to red, green, blue, white. void as_rgbw(float *red, float *green, float *blue, float *white, float gamma = 0, bool color_interlock = false) const { - this->as_rgb(red, green, blue, gamma, color_interlock); - *white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); + this->as_rgb(red, green, blue, gamma); + if (this->color_mode_ & ColorCapability::WHITE) { + *white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); + } else { + *white = 0; + } } /// Convert these light color values to an RGBWW representation with the given parameters. void as_rgbww(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue, float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false, bool color_interlock = false) const { - this->as_rgb(red, green, blue, gamma, color_interlock); - const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); - const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); - const float cw_fraction = 1.0f - ww_fraction; - const float white_level = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); - *cold_white = white_level * cw_fraction; - *warm_white = white_level * ww_fraction; - if (!constant_brightness) { - const float max_cw_ww = std::max(ww_fraction, cw_fraction); - *cold_white /= max_cw_ww; - *warm_white /= max_cw_ww; - } + this->as_rgb(red, green, blue, gamma); + this->as_cwww(0, 0, cold_white, warm_white, gamma, constant_brightness); } /// Convert these light color values to an CWWW representation with the given parameters. void as_cwww(float color_temperature_cw, float color_temperature_ww, float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false) const { - const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); - const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); - const float cw_fraction = 1.0f - ww_fraction; - const float white_level = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); - *cold_white = white_level * cw_fraction; - *warm_white = white_level * ww_fraction; - if (!constant_brightness) { - const float max_cw_ww = std::max(ww_fraction, cw_fraction); - *cold_white /= max_cw_ww; - *warm_white /= max_cw_ww; + if (this->color_mode_ & ColorMode::COLD_WARM_WHITE) { + const float cw_level = gamma_correct(this->cold_white_, gamma); + const float ww_level = gamma_correct(this->warm_white_, gamma); + const float white_level = gamma_correct(this->state_ * this->brightness_, gamma); + *cold_white = white_level * cw_level; + *warm_white = white_level * ww_level; + if (constant_brightness && (cw_level > 0 || ww_level > 0)) { + const float sum = cw_level + ww_level; + *cold_white /= sum; + *warm_white /= sum; + } + } else { + *cold_white = *warm_white = 0; } } /// 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_ && color_brightness_ == rhs.color_brightness_ && - red_ == rhs.red_ && green_ == rhs.green_ && blue_ == rhs.blue_ && white_ == rhs.white_ && - color_temperature_ == rhs.color_temperature_; + return color_mode_ == rhs.color_mode_ && 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_ && + cold_white_ == rhs.cold_white_ && warm_white_ == rhs.warm_white_; } bool operator!=(const LightColorValues &rhs) const { return !(rhs == *this); } + /// Get the color mode of these light color values. + ColorMode get_color_mode() const { return this->color_mode_; } + /// Set the color mode of these light color values. + void set_color_mode(ColorMode color_mode) { this->color_mode_ = color_mode; } + /// Get the state of these light color values. In range from 0.0 (off) to 1.0 (on) float get_state() const { return this->state_; } /// Get the binary true/false state of these light color values. @@ -290,11 +251,20 @@ class LightColorValues { /// Get the color temperature property of these light color values in mired. float get_color_temperature() const { return this->color_temperature_; } /// Set the color temperature property of these light color values in mired. - void set_color_temperature(float color_temperature) { - this->color_temperature_ = std::max(0.000001f, color_temperature); - } + void set_color_temperature(float color_temperature) { this->color_temperature_ = color_temperature; } + + /// Get the cold white property of these light color values. In range 0.0 to 1.0. + float get_cold_white() const { return this->cold_white_; } + /// Set the cold white property of these light color values. In range 0.0 to 1.0. + void set_cold_white(float cold_white) { this->cold_white_ = clamp(cold_white, 0.0f, 1.0f); } + + /// Get the warm white property of these light color values. In range 0.0 to 1.0. + float get_warm_white() const { return this->warm_white_; } + /// Set the warm white property of these light color values. In range 0.0 to 1.0. + void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); } protected: + ColorMode color_mode_; float state_; ///< ON / OFF, float for transition float brightness_; float color_brightness_; @@ -303,6 +273,8 @@ class LightColorValues { float blue_; float white_; float color_temperature_; ///< Color Temperature in Mired + float cold_white_; + float warm_white_; }; } // namespace light diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp new file mode 100644 index 0000000000..2e07d91046 --- /dev/null +++ b/esphome/components/light/light_json_schema.cpp @@ -0,0 +1,165 @@ +#include "light_json_schema.h" +#include "light_output.h" + +#ifdef USE_JSON + +namespace esphome { +namespace light { + +// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema + +void LightJSONSchema::dump_json(LightState &state, JsonObject &root) { + if (state.supports_effects()) + root["effect"] = state.get_effect_name(); + + auto values = state.remote_values; + auto traits = state.get_output()->get_traits(); + + switch (values.get_color_mode()) { + case ColorMode::UNKNOWN: // don't need to set color mode if we don't know it + break; + case ColorMode::ON_OFF: + root["color_mode"] = "onoff"; + break; + case ColorMode::BRIGHTNESS: + root["color_mode"] = "brightness"; + break; + case ColorMode::WHITE: // not supported by HA in MQTT + root["color_mode"] = "white"; + break; + case ColorMode::COLOR_TEMPERATURE: + root["color_mode"] = "color_temp"; + break; + case ColorMode::COLD_WARM_WHITE: // not supported by HA + root["color_mode"] = "cwww"; + break; + case ColorMode::RGB: + root["color_mode"] = "rgb"; + break; + case ColorMode::RGB_WHITE: + root["color_mode"] = "rgbw"; + break; + case ColorMode::RGB_COLOR_TEMPERATURE: // not supported by HA + root["color_mode"] = "rgbct"; + break; + case ColorMode::RGB_COLD_WARM_WHITE: + root["color_mode"] = "rgbww"; + break; + } + + if (values.get_color_mode() & ColorCapability::ON_OFF) + root["state"] = (values.get_state() != 0.0f) ? "ON" : "OFF"; + if (values.get_color_mode() & ColorCapability::BRIGHTNESS) + root["brightness"] = uint8_t(values.get_brightness() * 255); + + JsonObject &color = root.createNestedObject("color"); + if (values.get_color_mode() & ColorCapability::RGB) { + color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255); + color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255); + color["b"] = uint8_t(values.get_color_brightness() * values.get_blue() * 255); + } + if (values.get_color_mode() & ColorCapability::WHITE) { + color["w"] = uint8_t(values.get_white() * 255); + root["white_value"] = uint8_t(values.get_white() * 255); // legacy API + } + if (values.get_color_mode() & ColorCapability::COLOR_TEMPERATURE) { + // this one isn't under the color subkey for some reason + root["color_temp"] = uint32_t(values.get_color_temperature()); + } + if (values.get_color_mode() & ColorCapability::COLD_WARM_WHITE) { + color["c"] = uint8_t(values.get_cold_white() * 255); + color["w"] = uint8_t(values.get_warm_white() * 255); + } +} + +void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject &root) { + if (root.containsKey("state")) { + auto val = parse_on_off(root["state"]); + switch (val) { + case PARSE_ON: + call.set_state(true); + break; + case PARSE_OFF: + call.set_state(false); + break; + case PARSE_TOGGLE: + call.set_state(!state.remote_values.is_on()); + break; + case PARSE_NONE: + break; + } + } + + if (root.containsKey("brightness")) { + call.set_brightness(float(root["brightness"]) / 255.0f); + } + + if (root.containsKey("color")) { + JsonObject &color = root["color"]; + // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. + float max_rgb = 0.0f; + if (color.containsKey("r")) { + float r = float(color["r"]) / 255.0f; + max_rgb = fmaxf(max_rgb, r); + call.set_red(r); + } + if (color.containsKey("g")) { + float g = float(color["g"]) / 255.0f; + max_rgb = fmaxf(max_rgb, g); + call.set_green(g); + } + if (color.containsKey("b")) { + float b = float(color["b"]) / 255.0f; + max_rgb = fmaxf(max_rgb, b); + call.set_blue(b); + } + if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) { + call.set_color_brightness(max_rgb); + } + + if (color.containsKey("c")) { + call.set_cold_white(float(color["c"]) / 255.0f); + } + if (color.containsKey("w")) { + // the HA scheme is ambigious here, the same key is used for white channel in RGBW and warm + // white channel in RGBWW. + if (color.containsKey("c")) { + call.set_warm_white(float(color["w"]) / 255.0f); + } else { + call.set_white(float(color["w"]) / 255.0f); + } + } + } + + if (root.containsKey("white_value")) { // legacy API + call.set_white(float(root["white_value"]) / 255.0f); + } + + if (root.containsKey("color_temp")) { + call.set_color_temperature(float(root["color_temp"])); + } +} + +void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject &root) { + LightJSONSchema::parse_color_json(state, call, root); + + if (root.containsKey("flash")) { + auto length = uint32_t(float(root["flash"]) * 1000); + call.set_flash_length(length); + } + + if (root.containsKey("transition")) { + auto length = uint32_t(float(root["transition"]) * 1000); + call.set_transition_length(length); + } + + if (root.containsKey("effect")) { + const char *effect = root["effect"]; + call.set_effect(effect); + } +} + +} // namespace light +} // namespace esphome + +#endif diff --git a/esphome/components/light/light_json_schema.h b/esphome/components/light/light_json_schema.h new file mode 100644 index 0000000000..bb7ff812ab --- /dev/null +++ b/esphome/components/light/light_json_schema.h @@ -0,0 +1,25 @@ +#pragma once + +#include "light_call.h" +#include "light_state.h" + +#ifdef USE_JSON + +namespace esphome { +namespace light { + +class LightJSONSchema { + public: + /// Dump the state of a light as JSON. + static void dump_json(LightState &state, JsonObject &root); + /// Parse the JSON state of a light to a LightCall. + static void parse_json(LightState &state, LightCall &call, JsonObject &root); + + protected: + static void parse_color_json(LightState &state, LightCall &call, JsonObject &root); +}; + +} // namespace light +} // namespace esphome + +#endif diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index e32b15daf1..d5bf720f61 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -16,6 +16,7 @@ LightCall LightState::toggle() { return this->make_call().set_state(!this->remot LightCall LightState::make_call() { return LightCall(this); } struct LightStateRTCState { + ColorMode color_mode{ColorMode::UNKNOWN}; bool state{false}; float brightness{1.0f}; float color_brightness{1.0f}; @@ -24,6 +25,8 @@ struct LightStateRTCState { float blue{1.0f}; float white{1.0f}; float color_temp{1.0f}; + float cold_white{1.0f}; + float warm_white{1.0f}; uint32_t effect{0}; }; @@ -64,6 +67,7 @@ void LightState::setup() { break; } + call.set_color_mode_if_supported(recovered.color_mode); call.set_state(recovered.state); call.set_brightness_if_supported(recovered.brightness); call.set_color_brightness_if_supported(recovered.color_brightness); @@ -72,6 +76,8 @@ void LightState::setup() { call.set_blue_if_supported(recovered.blue); call.set_white_if_supported(recovered.white); call.set_color_temperature_if_supported(recovered.color_temp); + call.set_cold_white_if_supported(recovered.cold_white); + call.set_warm_white_if_supported(recovered.warm_white); if (recovered.effect != 0) { call.set_effect(recovered.effect); } else { @@ -81,11 +87,11 @@ void LightState::setup() { } void LightState::dump_config() { ESP_LOGCONFIG(TAG, "Light '%s'", this->get_name().c_str()); - if (this->get_traits().get_supports_brightness()) { + if (this->get_traits().supports_color_capability(ColorCapability::BRIGHTNESS)) { ESP_LOGCONFIG(TAG, " Default Transition Length: %.1fs", this->default_transition_length_ / 1e3f); ESP_LOGCONFIG(TAG, " Gamma Correct: %.2f", this->gamma_correct_); } - if (this->get_traits().get_supports_color_temperature()) { + if (this->get_traits().supports_color_capability(ColorCapability::COLOR_TEMPERATURE)) { ESP_LOGCONFIG(TAG, " Min Mireds: %.1f", this->get_traits().get_min_mireds()); ESP_LOGCONFIG(TAG, " Max Mireds: %.1f", this->get_traits().get_max_mireds()); } @@ -141,14 +147,6 @@ void LightState::add_new_target_state_reached_callback(std::function &&s this->target_state_reached_callback_.add(std::move(send_callback)); } -#ifdef USE_JSON -void LightState::dump_json(JsonObject &root) { - if (this->supports_effects()) - root["effect"] = this->get_effect_name(); - this->remote_values.dump_json(root, this->output_->get_traits()); -} -#endif - void LightState::set_default_transition_length(uint32_t default_transition_length) { this->default_transition_length_ = default_transition_length; } @@ -169,18 +167,17 @@ void LightState::current_values_as_brightness(float *brightness) { } void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) { auto traits = this->get_traits(); - this->current_values.as_rgb(red, green, blue, this->gamma_correct_, traits.get_supports_color_interlock()); + this->current_values.as_rgb(red, green, blue, this->gamma_correct_, false); } void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) { auto traits = this->get_traits(); - this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, traits.get_supports_color_interlock()); + this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, false); } void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness, bool color_interlock) { auto traits = this->get_traits(); this->current_values.as_rgbww(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, cold_white, - warm_white, this->gamma_correct_, constant_brightness, - traits.get_supports_color_interlock()); + warm_white, this->gamma_correct_, constant_brightness, false); } void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { auto traits = this->get_traits(); @@ -241,6 +238,7 @@ void LightState::set_transformer_(std::unique_ptr transformer) void LightState::save_remote_values_() { LightStateRTCState saved; + saved.color_mode = this->remote_values.get_color_mode(); saved.state = this->remote_values.is_on(); saved.brightness = this->remote_values.get_brightness(); saved.color_brightness = this->remote_values.get_color_brightness(); @@ -249,6 +247,8 @@ void LightState::save_remote_values_() { saved.blue = this->remote_values.get_blue(); saved.white = this->remote_values.get_white(); saved.color_temp = this->remote_values.get_color_temperature(); + saved.cold_white = this->remote_values.get_cold_white(); + saved.warm_white = this->remote_values.get_warm_white(); saved.effect = this->active_effect_index_; this->rtc_.save(&saved); } diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index cd5e6ad1cb..fb34b76367 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -93,11 +93,6 @@ class LightState : public Nameable, public Component { */ void add_new_target_state_reached_callback(std::function &&send_callback); -#ifdef USE_JSON - /// Dump the state of this light as JSON. - void dump_json(JsonObject &root); -#endif - /// Set the default transition length, i.e. the transition length when no transition is provided. void set_default_transition_length(uint32_t default_transition_length); diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index ed9c0d44ea..cbade64d77 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -1,5 +1,8 @@ #pragma once +#include "color_mode.h" +#include + namespace esphome { namespace light { @@ -8,35 +11,49 @@ class LightTraits { public: LightTraits() = default; - bool get_supports_brightness() const { return this->supports_brightness_; } - void set_supports_brightness(bool supports_brightness) { this->supports_brightness_ = supports_brightness; } - bool get_supports_rgb() const { return this->supports_rgb_; } - void set_supports_rgb(bool supports_rgb) { this->supports_rgb_ = supports_rgb; } - bool get_supports_rgb_white_value() const { return this->supports_rgb_white_value_; } - void set_supports_rgb_white_value(bool supports_rgb_white_value) { - this->supports_rgb_white_value_ = supports_rgb_white_value; + const std::set &get_supported_color_modes() const { return this->supported_color_modes_; } + void set_supported_color_modes(std::set supported_color_modes) { + this->supported_color_modes_ = std::move(supported_color_modes); } - bool get_supports_color_temperature() const { return this->supports_color_temperature_; } - void set_supports_color_temperature(bool supports_color_temperature) { - this->supports_color_temperature_ = supports_color_temperature; + + bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode); } + bool supports_color_capability(ColorCapability color_capability) const { + for (auto mode : this->supported_color_modes_) { + if (mode & color_capability) + return true; + } + return false; } - bool get_supports_color_interlock() const { return this->supports_color_interlock_; } - void set_supports_color_interlock(bool supports_color_interlock) { - this->supports_color_interlock_ = supports_color_interlock; + + ESPDEPRECATED("get_supports_brightness() is deprecated, use color modes instead.") + bool get_supports_brightness() const { return this->supports_color_capability(ColorCapability::BRIGHTNESS); } + ESPDEPRECATED("get_supports_rgb() is deprecated, use color modes instead.") + bool get_supports_rgb() const { return this->supports_color_capability(ColorCapability::RGB); } + ESPDEPRECATED("get_supports_rgb_white_value() is deprecated, use color modes instead.") + bool get_supports_rgb_white_value() const { + return this->supports_color_mode(ColorMode::RGB_WHITE) || + this->supports_color_mode(ColorMode::RGB_COLOR_TEMPERATURE); } + ESPDEPRECATED("get_supports_color_temperature() is deprecated, use color modes instead.") + bool get_supports_color_temperature() const { + return this->supports_color_capability(ColorCapability::COLOR_TEMPERATURE); + } + ESPDEPRECATED("get_supports_color_interlock() is deprecated, use color modes instead.") + bool get_supports_color_interlock() const { + return this->supports_color_mode(ColorMode::RGB) && + (this->supports_color_mode(ColorMode::WHITE) || this->supports_color_mode(ColorMode::COLD_WARM_WHITE) || + this->supports_color_mode(ColorMode::COLOR_TEMPERATURE)); + } + float get_min_mireds() const { return this->min_mireds_; } void set_min_mireds(float min_mireds) { this->min_mireds_ = min_mireds; } float get_max_mireds() const { return this->max_mireds_; } void set_max_mireds(float max_mireds) { this->max_mireds_ = max_mireds; } protected: - bool supports_brightness_{false}; - bool supports_rgb_{false}; - bool supports_rgb_white_value_{false}; - bool supports_color_temperature_{false}; + std::set supported_color_modes_{}; float min_mireds_{0}; float max_mireds_{0}; - bool supports_color_interlock_{false}; }; } // namespace light diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index 222be7802c..ddd0f4dd22 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -51,24 +51,45 @@ class LightTransitionTransformer : public LightTransformer { : LightTransformer(start_time, length, start_values, target_values) { // When turning light on from off state, use colors from new. if (!this->start_values_.is_on() && this->target_values_.is_on()) { + this->start_values_ = LightColorValues(target_values); this->start_values_.set_brightness(0.0f); - this->start_values_.set_red(target_values.get_red()); - this->start_values_.set_green(target_values.get_green()); - this->start_values_.set_blue(target_values.get_blue()); - this->start_values_.set_white(target_values.get_white()); - this->start_values_.set_color_temperature(target_values.get_color_temperature()); + } + + // When changing color mode, go through off state, as color modes are orthogonal and there can't be two active. + if (this->start_values_.get_color_mode() != this->target_values_.get_color_mode()) { + this->changing_color_mode_ = true; + this->intermediate_values_ = LightColorValues(this->get_start_values_()); + this->intermediate_values_.set_state(false); } } LightColorValues get_values() override { - float v = LightTransitionTransformer::smoothed_progress(this->get_progress()); - return LightColorValues::lerp(this->get_start_values_(), this->get_target_values_(), v); + float p = this->get_progress(); + + // Halfway through, when intermediate state (off) is reached, flip it to the target, but remain off. + if (this->changing_color_mode_ && p > 0.5f && + this->intermediate_values_.get_color_mode() != this->target_values_.get_color_mode()) { + this->intermediate_values_ = LightColorValues(this->get_end_values()); + this->intermediate_values_.set_state(false); + } + + LightColorValues &start = this->changing_color_mode_ && p > 0.5f ? this->intermediate_values_ : this->start_values_; + LightColorValues &end = this->changing_color_mode_ && p < 0.5f ? this->intermediate_values_ : this->target_values_; + if (this->changing_color_mode_) + p = p < 0.5f ? p * 2 : (p - 0.5) * 2; + + float v = LightTransitionTransformer::smoothed_progress(p); + return LightColorValues::lerp(start, end, v); } bool publish_at_end() override { return false; } bool is_transition() override { return true; } static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } + + protected: + bool changing_color_mode_{false}; + LightColorValues intermediate_values_{}; }; class LightFlashTransformer : public LightTransformer { diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index 7c96cda7b1..66329f7cf9 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -13,6 +13,20 @@ AddressableLightRef = AddressableLight.operator("ref") Color = cg.esphome_ns.class_("Color") LightColorValues = light_ns.class_("LightColorValues") +# Color modes +ColorMode = light_ns.enum("ColorMode", is_class=True) +COLOR_MODES = { + "ON_OFF": ColorMode.ON_OFF, + "BRIGHTNESS": ColorMode.BRIGHTNESS, + "WHITE": ColorMode.WHITE, + "COLOR_TEMPERATURE": ColorMode.COLOR_TEMPERATURE, + "COLD_WARM_WHITE": ColorMode.COLD_WARM_WHITE, + "RGB": ColorMode.RGB, + "RGB_WHITE": ColorMode.RGB_WHITE, + "RGB_COLOR_TEMPERATURE": ColorMode.RGB_COLOR_TEMPERATURE, + "RGB_COLD_WARM_WHITE": ColorMode.RGB_COLD_WARM_WHITE, +} + # Actions ToggleAction = light_ns.class_("ToggleAction", automation.Action) LightControlAction = light_ns.class_("LightControlAction", automation.Action) diff --git a/esphome/components/monochromatic/monochromatic_light_output.h b/esphome/components/monochromatic/monochromatic_light_output.h index c3a015ff3c..f1708ae70b 100644 --- a/esphome/components/monochromatic/monochromatic_light_output.h +++ b/esphome/components/monochromatic/monochromatic_light_output.h @@ -12,7 +12,7 @@ class MonochromaticLightOutput : public light::LightOutput { void set_output(output::FloatOutput *output) { output_ = output; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); return traits; } void write_state(light::LightState *state) override { diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index 4bd0882b8c..bf483bf947 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -1,4 +1,5 @@ #include "mqtt_light.h" +#include "esphome/components/light/light_json_schema.h" #include "esphome/core/log.h" #ifdef USE_LIGHT @@ -14,7 +15,9 @@ std::string MQTTJSONLightComponent::component_type() const { return "light"; } void MQTTJSONLightComponent::setup() { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject &root) { - this->state_->make_call().parse_json(root).perform(); + LightCall call = this->state_->make_call(); + LightJSONSchema::parse_json(*this->state_, call, root); + call.perform(); }); auto f = std::bind(&MQTTJSONLightComponent::publish_state_, this); @@ -24,21 +27,40 @@ void MQTTJSONLightComponent::setup() { MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : MQTTComponent(), state_(state) {} bool MQTTJSONLightComponent::publish_state_() { - return this->publish_json(this->get_state_topic_(), [this](JsonObject &root) { this->state_->dump_json(root); }); + return this->publish_json(this->get_state_topic_(), + [this](JsonObject &root) { LightJSONSchema::dump_json(*this->state_, root); }); } LightState *MQTTJSONLightComponent::get_state() const { return this->state_; } std::string MQTTJSONLightComponent::friendly_name() const { return this->state_->get_name(); } void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { root["schema"] = "json"; auto traits = this->state_->get_traits(); - if (traits.get_supports_brightness()) + + root["color_mode"] = true; + JsonArray &color_modes = root.createNestedArray("supported_color_modes"); + if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE)) + color_modes.add("color_temp"); + if (traits.supports_color_mode(ColorMode::RGB)) + color_modes.add("rgb"); + if (traits.supports_color_mode(ColorMode::RGB_WHITE)) + color_modes.add("rgbw"); + if (traits.supports_color_mode(ColorMode::RGB_COLD_WARM_WHITE)) + color_modes.add("rgbww"); + if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) + color_modes.add("brightness"); + if (traits.supports_color_mode(ColorMode::ON_OFF)) + color_modes.add("onoff"); + + // legacy API + if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) root["brightness"] = true; - if (traits.get_supports_rgb()) + if (traits.supports_color_capability(ColorCapability::RGB)) root["rgb"] = true; - if (traits.get_supports_color_temperature()) + if (traits.supports_color_capability(ColorCapability::COLOR_TEMPERATURE)) root["color_temp"] = true; - if (traits.get_supports_rgb_white_value()) + if (traits.supports_color_capability(ColorCapability::WHITE)) root["white_value"] = true; + if (this->state_->supports_effects()) { root["effect"] = true; JsonArray &effect_list = root.createNestedArray("effect_list"); diff --git a/esphome/components/neopixelbus/neopixelbus_light.h b/esphome/components/neopixelbus/neopixelbus_light.h index 2f279e1c9b..c7f7badc5a 100644 --- a/esphome/components/neopixelbus/neopixelbus_light.h +++ b/esphome/components/neopixelbus/neopixelbus_light.h @@ -115,8 +115,7 @@ class NeoPixelRGBLightOutput : public NeoPixelBusLightOutputBasecolor_interlock_); - traits.set_supports_rgb(true); - traits.set_supports_rgb_white_value(true); + 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}); return traits; } void write_state(light::LightState *state) override { diff --git a/esphome/components/rgbww/light.py b/esphome/components/rgbww/light.py index 41cba1f7a1..37bc668215 100644 --- a/esphome/components/rgbww/light.py +++ b/esphome/components/rgbww/light.py @@ -27,12 +27,15 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_BLUE): cv.use_id(output.FloatOutput), cv.Required(CONF_COLD_WHITE): cv.use_id(output.FloatOutput), cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), - cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, - cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, } ), + cv.has_none_or_all_keys( + [CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_WARM_WHITE_COLOR_TEMPERATURE] + ), light.validate_color_temperature_channels, ) @@ -50,10 +53,17 @@ async def to_code(config): cwhite = await cg.get_variable(config[CONF_COLD_WHITE]) cg.add(var.set_cold_white(cwhite)) - cg.add(var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE])) + if CONF_COLD_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE]) + ) wwhite = await cg.get_variable(config[CONF_WARM_WHITE]) cg.add(var.set_warm_white(wwhite)) - cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) + if CONF_WARM_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE]) + ) + cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS])) cg.add(var.set_color_interlock(config[CONF_COLOR_INTERLOCK])) diff --git a/esphome/components/rgbww/rgbww_light_output.h b/esphome/components/rgbww/rgbww_light_output.h index e14b967530..2182cc201e 100644 --- a/esphome/components/rgbww/rgbww_light_output.h +++ b/esphome/components/rgbww/rgbww_light_output.h @@ -14,17 +14,16 @@ class RGBWWLightOutput : public light::LightOutput { void set_blue(output::FloatOutput *blue) { blue_ = blue; } void set_cold_white(output::FloatOutput *cold_white) { cold_white_ = cold_white; } void set_warm_white(output::FloatOutput *warm_white) { warm_white_ = warm_white; } - void set_cold_white_temperature(int cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } - void set_warm_white_temperature(int warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } + void set_cold_white_temperature(float cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } + void set_warm_white_temperature(float warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } void set_constant_brightness(bool constant_brightness) { constant_brightness_ = constant_brightness; } void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); - traits.set_supports_rgb_white_value(true); - traits.set_supports_color_temperature(true); - traits.set_supports_color_interlock(this->color_interlock_); + if (this->color_interlock_) + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLD_WARM_WHITE}); + else + traits.set_supported_color_modes({light::ColorMode::RGB_COLD_WARM_WHITE}); traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); return traits; @@ -46,8 +45,8 @@ class RGBWWLightOutput : public light::LightOutput { output::FloatOutput *blue_; output::FloatOutput *cold_white_; output::FloatOutput *warm_white_; - int cold_white_temperature_; - int warm_white_temperature_; + float cold_white_temperature_{0}; + float warm_white_temperature_{0}; bool constant_brightness_; bool color_interlock_{false}; }; diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index d7e3561328..e0ca026248 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -45,11 +45,14 @@ void TuyaLight::dump_config() { light::LightTraits TuyaLight::get_traits() { auto traits = light::LightTraits(); - traits.set_supports_brightness(this->dimmer_id_.has_value()); - traits.set_supports_color_temperature(this->color_temperature_id_.has_value()); - if (this->color_temperature_id_.has_value()) { + if (this->color_temperature_id_.has_value() && this->dimmer_id_.has_value()) { + 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->dimmer_id_.has_value()) { + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); + } else { + traits.set_supported_color_modes({light::ColorMode::ON_OFF}); } return traits; } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index b775d44211..5e45a87e26 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -8,6 +8,10 @@ #include +#ifdef USE_LIGHT +#include "esphome/components/light/light_json_schema.h" +#endif + #ifdef USE_LOGGER #include #endif @@ -528,7 +532,7 @@ std::string WebServer::light_json(light::LightState *obj) { return json::build_json([obj](JsonObject &root) { root["id"] = "light-" + obj->get_object_id(); root["state"] = obj->remote_values.is_on() ? "ON" : "OFF"; - obj->dump_json(root); + light::LightJSONSchema::dump_json(*obj, root); }); } #endif diff --git a/esphome/config_validation.py b/esphome/config_validation.py index aad147dbc9..9afed58f80 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -563,6 +563,23 @@ def has_at_most_one_key(*keys): return validate +def has_none_or_all_keys(*keys): + """Validate that none or all of the given keys exist in the config.""" + + def validate(obj): + if not isinstance(obj, dict): + raise Invalid("expected dictionary") + + number = sum(k in keys for k in obj) + if number != 0 and number != len(keys): + raise Invalid( + "Must specify either none or all of {}.".format(", ".join(keys)) + ) + return obj + + return validate + + TIME_PERIOD_ERROR = ( "Time period {} should be format number + unit, for example 5ms, 5s, 5min, 5h" ) diff --git a/esphome/const.py b/esphome/const.py index 6702e773c6..5c21836c7d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -120,6 +120,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_MODE = "color_mode" CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors" CONF_COMMAND = "command" diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index a6cf8b779c..9e9c775899 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -79,6 +79,15 @@ float gamma_correct(float value, float gamma) { return powf(value, gamma); } +float gamma_uncorrect(float value, float gamma) { + if (value <= 0.0f) + return 0.0f; + if (gamma <= 0.0f) + return value; + + return powf(value, 1 / gamma); +} + std::string to_lowercase_underscore(std::string s) { std::transform(s.begin(), s.end(), s.begin(), ::tolower); std::replace(s.begin(), s.end(), ' ', '_'); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 808f96d4b8..6f6281fd01 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -116,6 +116,8 @@ uint8_t fast_random_8(); /// Applies gamma correction with the provided gamma to value. float gamma_correct(float value, float gamma); +/// Reverts gamma correction with the provided gamma to value. +float gamma_uncorrect(float value, float gamma); /// Create a string from a value and an accuracy in decimals. std::string value_accuracy_to_string(float value, int8_t accuracy_decimals);