diff --git a/color_night_light.h b/color_night_light.h index ef73e4a..d3fd5a9 100644 --- a/color_night_light.h +++ b/color_night_light.h @@ -3,21 +3,23 @@ #include #include +#include "common.h" + namespace esphome { namespace yeelight { namespace bs2 { -class ColorNightLight -{ +class ColorNightLight : public GPIOOutputs { public: - // Based on measurements using the original device firmware. - float red = 0.968f; - float green = 0.968f; - float blue = 0.972f; - float white = 0.0f; + bool set_light_color_values(light::LightColorValues v) { + values = v; + // Based on measurements using the original device firmware. + red = 0.968f; + green = 0.968f; + blue = 0.972f; + white = 0.0f; - void set_color(float red, float green, float blue, float brightness, float state) - { + return true; } }; diff --git a/color_off.h b/color_off.h new file mode 100644 index 0000000..ef0a955 --- /dev/null +++ b/color_off.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include "common.h" + +namespace esphome { +namespace yeelight { +namespace bs2 { + +class ColorOff : public GPIOOutputs { +public: + bool set_light_color_values(light::LightColorValues v) { + values = v; + red = 0.0f; + green = 0.0f; + blue = 0.0f; + white = 0.0f; + + return true; + } +}; + +} // namespace yeelight_bs2 +} // namespace yeelight +} // namespace bs2 diff --git a/color_rgb_light.h b/color_rgb_light.h index afe8a44..f008cb4 100644 --- a/color_rgb_light.h +++ b/color_rgb_light.h @@ -8,6 +8,8 @@ #include #include +#include "common.h" + namespace esphome { namespace yeelight { namespace bs2 { @@ -243,19 +245,15 @@ static const RGBCircle rgb_circle_ {{ }} }}; -class ColorRGBLight -{ +class ColorRGBLight : public GPIOOutputs { public: - float red = 0; - float green = 0; - float blue = 0; - float white = 0; + bool set_light_color_values(light::LightColorValues v) { + values = v; - void set_color(float red, float green, float blue, float brightness) { // Determine the ring level for the color. This is a value between // 0 and 7, determining in what ring of the RGB circle the requested // color resides. - auto rgb_min = min(min(red, green), blue); + auto rgb_min = min(min(v.get_red(), v.get_green()), v.get_blue()); auto level = 7.0f * rgb_min; // While the default color circle in Home Assistant presents only a @@ -266,19 +264,28 @@ public: // Determine duty cycle measurements for the outer ring. auto level_a = floor(level); - set_duty_cycles_(&rgbp_a_, level_a, red, green, blue, brightness, &rgb_a_); + set_duty_cycles_( + &rgbp_a_, level_a, v.get_red(), v.get_green(), v.get_blue(), + v.get_brightness(), &rgb_a_); // Determine duty cycle measurements for the inner ring. auto level_b = ceil(level); - set_duty_cycles_(&rgbp_b_, level_b, red, green, blue, brightness, &rgb_b_); + set_duty_cycles_( + &rgbp_b_, level_a, v.get_red(), v.get_green(), v.get_blue(), + v.get_brightness(), &rgb_b_); // Almost there! We now have the correct duty cycles for the // two rings that we were looking at. In this last step, the // two values are interpolated based on the ring level. auto d = level - level_a; - this->red = rgb_a_.red + d * (rgb_b_.red - rgb_a_.red); - this->green = rgb_a_.green + d * (rgb_b_.green - rgb_a_.green); - this->blue = rgb_a_.blue + d * (rgb_b_.blue - rgb_a_.blue); + red = rgb_a_.red + d * (rgb_b_.red - rgb_a_.red); + green = rgb_a_.green + d * (rgb_b_.green - rgb_a_.green); + blue = rgb_a_.blue + d * (rgb_b_.blue - rgb_a_.blue); + + // The white output channel will always be 0 for RGB. + white = 0.0f; + + return true; } protected: diff --git a/color_translator.h b/color_translator.h new file mode 100644 index 0000000..41d8916 --- /dev/null +++ b/color_translator.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +#include "common.h" +#include "color_off.h" +#include "color_night_light.h" +#include "color_white_light.h" +#include "color_rgb_light.h" + +namespace esphome { +namespace yeelight { +namespace bs2 { + +/// This class translates LightColorValues into GPIO duty cycles +/// for representing a requested light color. +/// +/// The code handles all known light modes for the device: +/// +/// - off: the light is off +/// - night light: activated when brightness is at its lowest +/// - white light: based on color temperature + brightness +/// - RGB light: based on RGB values + brightness +class ColorTranslator : public GPIOOutputs { +public: + bool set_light_color_values(light::LightColorValues v) { + values = v; + + GPIOOutputs *delegate = nullptr; + + // Well, not much light here! Use the off "color". + if (v.get_state() == 0.0f || v.get_brightness() == 0.0f) { + delegate = off_light_; + } + // At the lowest brightness setting, switch to night light mode. + // In the Yeelight integration in Home Assistant, this feature is + // exposed trough a separate switch. I have found that the switch + // is both confusing and made me run into issues when automating + // the lights. + // I don't simply check for a brightness at or below 0.01 (1%), + // because the lowest brightness setting from Home Assistant + // turns up as 0.011765 in here (which is 3/255). + else if (v.get_brightness() < 0.012f) { + delegate = night_light_; + } + // When white light is requested, then use the color temperature + // white light mode: temperature + brightness. + else if (v.get_white() > 0.0f) { + delegate = white_light_; + } + // Otherwise, use RGB color mode: red, green, blue + brightness. + else { + delegate = rgb_light_; + } + + delegate->set_light_color_values(v); + delegate->copy_to(this); + + return true; + } + +protected: + GPIOOutputs *off_light_ = new ColorOff(); + GPIOOutputs *rgb_light_ = new ColorRGBLight(); + GPIOOutputs *white_light_ = new ColorWhiteLight(); + GPIOOutputs *night_light_ = new ColorNightLight(); +}; + +} // namespace yeelight_bs2 +} // namespace yeelight +} // namespace bs2 diff --git a/color_white_light.h b/color_white_light.h index 46d85c8..cb902b6 100644 --- a/color_white_light.h +++ b/color_white_light.h @@ -59,18 +59,13 @@ static const RGBWLevelsTable rgbw_levels_100_ {{ { 153.0f, 1.000f, 0.000f, 0.187f, 0.335f } }}; -class ColorWhiteLight -{ +class ColorWhiteLight : public GPIOOutputs { public: - float red = 0; - float green = 0; - float blue = 0; - float white = 0; + bool set_light_color_values(light::LightColorValues v) { + values = v; - void set_color(float temperature, float brightness) - { - temperature = clamp_temperature_(temperature); - brightness = clamp_brightness_(brightness); + auto temperature = clamp_temperature_(v.get_color_temperature()); + auto brightness = clamp_brightness_(v.get_brightness()); auto levels_1 = lookup_in_table_(rgbw_levels_1_, temperature); auto levels_100 = lookup_in_table_(rgbw_levels_100_, temperature); @@ -79,6 +74,8 @@ public: green = interpolate_(levels_1.green, levels_100.green, brightness); blue = interpolate_(levels_1.blue, levels_100.blue, brightness); white = interpolate_(levels_1.white, levels_100.white, brightness); + + return true; } protected: @@ -108,6 +105,7 @@ protected: throw std::invalid_argument("received too low temperature"); } + // TODO Use esphome::lerp? float interpolate_(float level_1, float level_100, float brightness) { auto coefficient = (level_100 - level_1) / 0.99f; diff --git a/common.h b/common.h index feee466..2622cf8 100644 --- a/common.h +++ b/common.h @@ -15,12 +15,36 @@ namespace bs2 { /// the original Yeelight firmware. static const int MIRED_MAX = 588; - /// This struct is used to hold GPIO pin duty cycles. - struct DutyCycles { - float red; - float green; - float blue; - float white; + /// This abstract class is used for building classes that translate + /// LightColorValues into the required GPIO pin outputs to represent + /// the requested color on the device. + class GPIOOutputs { + public: + float red = 0.0f; + float green = 0.0f; + float blue = 0.0f; + float white = 0.0f; + light::LightColorValues values; + + /// Set the red, green, blue, white fields to the PWM duty cycles + /// that are required to represent the requested light color for + /// the provided LightColorValues input. + /// Returns true when the class can handle the input, false otherwise. + virtual bool set_light_color_values(light::LightColorValues v) = 0; + + /// Copy the output values to another GPIOOutputs object. + void copy_to(GPIOOutputs *other) { + other->red = red; + other->green = green; + other->blue = blue; + other->white = white; + other->values = values; + } + + void log(const char *prefix) { + ESP_LOGD(TAG, "%s: RGB=[%f,%f,%f], white=%f", + prefix, red, green, blue, white); + } }; } // namespace bs2 diff --git a/light.h b/light.h index 605b664..aef6643 100644 --- a/light.h +++ b/light.h @@ -22,8 +22,8 @@ namespace bs2 { /// This is an interface definition that is used to extend the /// YeelightBS2LightOutput class with methods to access properties - /// of an active LightTranformer from the YeelightBS2LightOutput - /// class. + /// of an active LightTranformer from the TransitionHandler class. + /// /// The transformer is protected in the light output class, making /// it impossible to access these properties directly from the /// light output class. @@ -36,65 +36,6 @@ namespace bs2 { virtual float get_transformer_progress() = 0; }; - /// This class translates LightColorValues into GPIO duty cycles - /// for representing a requested light color. - class ColorTranslator : public DutyCycles { - public: - void set_light_color_values(light::LightColorValues values) { - // The light is turned off. - if (values.get_state() == 0.0f || values.get_brightness() == 0.0f) { - red = 0.0f; - green = 0.0f; - blue = 0.0f; - white = 0.0f; - return; - } - - // At the lowest brightness setting, switch to night light mode. - // In the Yeelight integration in Home Assistant, this feature is - // exposed trough a separate switch. I have found that the switch - // is both confusing and made me run into issues when automating - // the lights. - // I don't simply check for a brightness at or below 0.01 (1%), - // because the lowest brightness setting from Home Assistant - // turns up as 0.011765 in here (which is 3/255). - if (values.get_brightness() < 0.012f) { - // TODO make the color implementations return a DutyCycles object? - // TODO Use polymorphic color classes? - red = night_light_.red; - green = night_light_.green; - blue = night_light_.blue; - white = night_light_.white; - return; - } - - // White light mode: temperature + brightness. - if (values.get_white() > 0.0f) { - auto temperature = values.get_color_temperature(); - white_light_.set_color(temperature, values.get_brightness()); - red = white_light_.red; - green = white_light_.green; - blue = white_light_.blue; - white = white_light_.white; - return; - } - - // RGB color mode: red, green, blue + brightness. - rgb_light_.set_color( - values.get_red(), values.get_green(), values.get_blue(), - values.get_brightness()); - red = rgb_light_.red; - green = rgb_light_.green; - blue = rgb_light_.blue; - white = rgb_light_.white; - } - - protected: - ColorWhiteLight white_light_; - ColorRGBLight rgb_light_; - ColorNightLight night_light_; - }; - /// This class is used to handle color transition requirements. /// /// When using the default ESPHome logic, transitioning is done by @@ -113,35 +54,43 @@ namespace bs2 { /// This class handles transitions by not varying the light properties /// over time, but by transitioning the LEDC duty cycle output levels /// over time. This matches the behavior of the original firmware. - class TransitionHandler { + class TransitionHandler : public GPIOOutputs { public: TransitionHandler(LightStateDataExposer *exposer) : exposer_(exposer) {} - bool handle() { - if (!do_handle_()) { + bool set_light_color_values(light::LightColorValues values) { + if (!has_active_transition_()) { + start_values = values; active_ = false; return false; } if (is_fresh_transition_()) { - auto start = exposer_->get_transformer_values(); - auto end = exposer_->get_transformer_end_values(); - active_ = true; + start_->set_light_color_values(start_values); + end_->set_light_color_values(exposer_->get_transformer_end_values()); + active_ = true; } + auto progress = exposer_->get_transformer_progress(); + red = esphome::lerp(progress, start_->red, end_->red); + green = esphome::lerp(progress, start_->green, end_->green); + blue = esphome::lerp(progress, start_->blue, end_->blue); + white = esphome::lerp(progress, start_->white, end_->white); + return true; } protected: - LightStateDataExposer *exposer_; bool active_ = false; - DutyCycles start_; - DutyCycles end_; + LightStateDataExposer *exposer_; + light::LightColorValues start_values; + GPIOOutputs *start_ = new ColorTranslator(); + GPIOOutputs *end_ = new ColorTranslator(); /// Checks if this class will handle the light output logic. /// This is the case when a transformer is active and this /// transformer does implement a transitioning effect. - bool do_handle_() { + bool has_active_transition_() { if (!exposer_->has_active_transformer()) return false; if (!exposer_->transformer_is_transition()) @@ -154,11 +103,14 @@ namespace bs2 { /// be in progress or when a new end state is found during an /// ongoing transition. bool is_fresh_transition_() { - bool is_fresh = false; if (active_ == false) { - is_fresh = true; + return true; } - return is_fresh; + auto new_end_values = exposer_->get_transformer_end_values(); + if (new_end_values != end_->values) { + return true; + } + return false; } }; @@ -216,7 +168,7 @@ namespace bs2 { return traits; } - /// Tranlates a requested light state into physicial GPIO outputs. + /// Applies a requested light state to the physicial GPIO outputs. void write_state(light::LightState *state) { auto values = state->current_values; @@ -237,8 +189,14 @@ namespace bs2 { return; } - if (transition_->handle()) { - ESP_LOGD(TAG, "HANDLE transition!"); + if (transition_handler_->set_light_color_values(values)) { + master2_->turn_on(); + master1_->turn_on(); + red_->set_level(transition_handler_->red); + green_->set_level(transition_handler_->green); + blue_->set_level(transition_handler_->blue); + white_->set_level(transition_handler_->white); + return; } #ifdef TRANSITION_TO_OFF_BUGFIX @@ -259,13 +217,13 @@ namespace bs2 { previous_state_ = values.get_state(); #endif - duty_cycles_.set_light_color_values(values); + instant_handler_->set_light_color_values(values); master2_->turn_on(); master1_->turn_on(); - red_->set_level(duty_cycles_.red); - green_->set_level(duty_cycles_.green); - blue_->set_level(duty_cycles_.blue); - white_->set_level(duty_cycles_.white); + red_->set_level(instant_handler_->red); + green_->set_level(instant_handler_->green); + blue_->set_level(instant_handler_->blue); + white_->set_level(instant_handler_->white); } protected: @@ -275,8 +233,8 @@ namespace bs2 { ledc::LEDCOutput *white_; esphome::gpio::GPIOBinaryOutput *master1_; esphome::gpio::GPIOBinaryOutput *master2_; - TransitionHandler *transition_; - ColorTranslator duty_cycles_; // TODO move to own class DefaultHandler + TransitionHandler *transition_handler_; + ColorTranslator *instant_handler_ = new ColorTranslator(); #ifdef TRANSITION_TO_OFF_BUGFIX float previous_state_ = 1; float previous_brightness_ = -1; @@ -287,7 +245,7 @@ namespace bs2 { /// Called by the YeelightBS2LightState class, to set the object that /// can be used to access protected data from the light state object. void set_light_state_data_exposer(LightStateDataExposer *exposer) { - transition_ = new TransitionHandler(exposer); + transition_handler_ = new TransitionHandler(exposer); } };