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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1210 additions and 476 deletions

View File

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

View File

@ -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<enums::ColorMode>(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<enums::ColorMode>(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<light::ColorMode>(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;

View File

@ -62,6 +62,32 @@ template<> const char *proto_enum_to_string<enums::FanDirection>(enums::FanDirec
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::ColorMode>(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>(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<enums::ColorMode>());
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<enums::ColorMode>(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<enums::ColorMode>(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<enums::ColorMode>();
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<enums::ColorMode>(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<enums::ColorMode>(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<enums::ColorMode>();
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<enums::ColorMode>(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<enums::ColorMode>(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");

View File

@ -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<enums::ColorMode> 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<std::string> 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};

View File

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

View File

@ -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_;
};

View File

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

View File

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

View File

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

View File

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

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 {
if (this->color_mode_ & ColorCapability::RGB) {
float brightness = this->state_ * this->brightness_ * this->color_brightness_;
if (color_interlock && this->white_ > 0.0f) {
brightness = 0;
}
*red = gamma_correct(brightness * this->red_, gamma);
*green = gamma_correct(brightness * this->green_, gamma);
*blue = gamma_correct(brightness * this->blue_, gamma);
} else {
*red = *green = *blue = 0;
}
}
/// 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);
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;
}
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;
return false;
}
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)

View File

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

View File

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

View File

@ -115,8 +115,7 @@ class NeoPixelRGBLightOutput : public NeoPixelBusLightOutputBase<T_METHOD, T_COL
public:
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;
}
@ -133,9 +132,7 @@ class NeoPixelRGBWLightOutput : public NeoPixelBusLightOutputBase<T_METHOD, T_CO
public:
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_supported_color_modes({light::ColorMode::RGB_WHITE});
return traits;
}

View File

@ -15,8 +15,7 @@ class RGBLightOutput : public light::LightOutput {
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 write_state(light::LightState *state) override {

View File

@ -16,10 +16,10 @@ class RGBWLightOutput : public light::LightOutput {
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_color_interlock(this->color_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 {

View File

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

View File

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

View File

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

View File

@ -8,6 +8,10 @@
#include <cstdlib>
#ifdef USE_LIGHT
#include "esphome/components/light/light_json_schema.h"
#endif
#ifdef USE_LOGGER
#include <esphome/components/logger/logger.h>
#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

View File

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

View File

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

View File

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

View File

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