1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-09 21:33:48 +01:00

Color mode implementation (#2012)

This commit is contained in:
Oxan van Leeuwen
2021-07-29 19:11:56 +02:00
committed by GitHub
parent de382b704c
commit 5983ccc55c
39 changed files with 1210 additions and 476 deletions

View File

@@ -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(

View File

@@ -27,6 +27,7 @@ template<typename... Ts> class LightControlAction : public Action<Ts...> {
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<typename... Ts> class LightControlAction : public Action<Ts...> {
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<typename... Ts> class LightControlAction : public Action<Ts...> {
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...));

View File

@@ -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_))

View File

@@ -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();

View File

@@ -0,0 +1,107 @@
#pragma once
#include <cstdint>
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<uint8_t>(val_); }
constexpr operator bool() const { return static_cast<uint8_t>(val_) != 0; }
protected:
ColorCapability val_;
};
constexpr ColorCapabilityHelper operator&(ColorCapability lhs, ColorCapability rhs) {
return static_cast<ColorCapability>(static_cast<uint8_t>(lhs) & static_cast<uint8_t>(rhs));
}
constexpr ColorCapabilityHelper operator&(ColorCapabilityHelper lhs, ColorCapability rhs) {
return static_cast<ColorCapability>(static_cast<uint8_t>(lhs) & static_cast<uint8_t>(rhs));
}
constexpr ColorCapabilityHelper operator|(ColorCapability lhs, ColorCapability rhs) {
return static_cast<ColorCapability>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs));
}
constexpr ColorCapabilityHelper operator|(ColorCapabilityHelper lhs, ColorCapability rhs) {
return static_cast<ColorCapability>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(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<uint8_t>(val_); }
constexpr operator bool() const { return static_cast<uint8_t>(val_) != 0; }
protected:
ColorMode val_;
};
constexpr ColorModeHelper operator&(ColorMode lhs, ColorMode rhs) {
return static_cast<ColorMode>(static_cast<uint8_t>(lhs) & static_cast<uint8_t>(rhs));
}
constexpr ColorModeHelper operator&(ColorMode lhs, ColorCapability rhs) {
return static_cast<ColorMode>(static_cast<uint8_t>(lhs) & static_cast<uint8_t>(rhs));
}
constexpr ColorModeHelper operator&(ColorModeHelper lhs, ColorMode rhs) {
return static_cast<ColorMode>(static_cast<uint8_t>(lhs) & static_cast<uint8_t>(rhs));
}
constexpr ColorModeHelper operator|(ColorMode lhs, ColorMode rhs) {
return static_cast<ColorMode>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs));
}
constexpr ColorModeHelper operator|(ColorMode lhs, ColorCapability rhs) {
return static_cast<ColorMode>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs));
}
constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) {
return static_cast<ColorMode>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs));
}
} // namespace light
} // namespace esphome

View File

@@ -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]),

View File

@@ -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<float>(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<float>(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<float>(0.0f);
} else if (output_white) {
this->color_brightness_ = optional<float>(0.0f);
} else if (output_color) {
this->white_ = optional<float>(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<float>(0.0f);
this->red_ = optional<float>(1.0f);
this->green_ = optional<float>(1.0f);
this->blue_ = optional<float>(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<float>(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<float>(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<ColorMode> 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<ColorMode> 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<uint8_t, std::set<ColorMode>>(KEY(white, ct, cwww, rgb), __VA_ARGS__)
// Flag order: white, color temperature, cwww, rgb
std::array<std::tuple<uint8_t, std::set<ColorMode>>, 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<bool> 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<ColorMode> 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<float> 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<float> 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<float> 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<std::string> effect) {
if (effect.has_value())
this->set_effect(*effect);

View File

@@ -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<ColorMode> 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<float> 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<float> 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<float> 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<std::string> 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<ColorMode> 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<bool> state_;
optional<uint32_t> transition_length_;
optional<uint32_t> flash_length_;
optional<ColorMode> color_mode_;
optional<float> brightness_;
optional<float> color_brightness_;
optional<float> red_;
@@ -158,6 +185,8 @@ class LightCall {
optional<float> blue_;
optional<float> white_;
optional<float> color_temperature_;
optional<float> cold_white_;
optional<float> warm_white_;
optional<uint32_t> effect_;
bool publish_{true};
bool save_{true};

View File

@@ -15,40 +15,56 @@ inline static uint8_t to_uint8_scale(float x) { return static_cast<uint8_t>(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

View File

@@ -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

View File

@@ -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

View File

@@ -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<void()> &&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<LightTransformer> 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);
}

View File

@@ -93,11 +93,6 @@ class LightState : public Nameable, public Component {
*/
void add_new_target_state_reached_callback(std::function<void()> &&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);

View File

@@ -1,5 +1,8 @@
#pragma once
#include "color_mode.h"
#include <set>
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<ColorMode> &get_supported_color_modes() const { return this->supported_color_modes_; }
void set_supported_color_modes(std::set<ColorMode> 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<ColorMode> supported_color_modes_{};
float min_mireds_{0};
float max_mireds_{0};
bool supports_color_interlock_{false};
};
} // namespace light

View File

@@ -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 {

View File

@@ -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)