From 46e4b69c7c2659bca05a17f11f49a144dcdea5b6 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Thu, 8 Apr 2021 00:07:20 +0200 Subject: [PATCH] Another round of code cleanup. Almost done, one more pass and I'm good to continue with the next task: the touch panel. --- color_translator.h => color_instant_handler.h | 28 +- color_night_light.h | 27 +- color_off.h | 10 +- color_rgb_light.h | 57 ++- color_transition_handler.h | 139 +++++++ color_white_light.h | 44 +- common.h | 44 +- gpio_outputs.h | 46 +++ light.h | 380 +++++++----------- 9 files changed, 403 insertions(+), 372 deletions(-) rename color_translator.h => color_instant_handler.h (75%) create mode 100644 color_transition_handler.h create mode 100644 gpio_outputs.h diff --git a/color_translator.h b/color_instant_handler.h similarity index 75% rename from color_translator.h rename to color_instant_handler.h index 0e90fb9..7f733ec 100644 --- a/color_translator.h +++ b/color_instant_handler.h @@ -4,6 +4,7 @@ #include #include "common.h" +#include "gpio_outputs.h" #include "color_off.h" #include "color_night_light.h" #include "color_white_light.h" @@ -13,20 +14,20 @@ 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: +/** + * 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: based on RGB or white mode + lowest possible brightness + * - white light: based on color temperature + brightness + * - RGB light: based on RGB values + brightness + */ +class ColorInstantHandler : public GPIOOutputs { +protected: bool set_light_color_values(light::LightColorValues v) { - values = v; - GPIOOutputs *delegate; // The actual implementation of the various light modes is in @@ -48,7 +49,6 @@ public: return true; } -protected: GPIOOutputs *off_light_ = new ColorOff(); GPIOOutputs *rgb_light_ = new ColorRGBLight(); GPIOOutputs *white_light_ = new ColorWhiteLight(); diff --git a/color_night_light.h b/color_night_light.h index d3ad107..a3a1010 100644 --- a/color_night_light.h +++ b/color_night_light.h @@ -1,31 +1,32 @@ #pragma once #include "common.h" +#include "gpio_outputs.h" namespace esphome { namespace yeelight { namespace bs2 { +/** + * This class can handle the GPIO outputs for the night light mode. + * + * At the lowest brightness setting, the light will 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. + * Using the lowest brightness for triggering the night light feels a lot + * more natural. + */ class ColorNightLight : public GPIOOutputs { -public: +protected: bool set_light_color_values(light::LightColorValues v) { - // This class can handle the GPIO outputs for the night light mode. - // - // 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%), + // Note: I do not check for a brightness at or below 0.01 (1%) here, // because the lowest brightness setting from Home Assistant turns - // up as 0.011765 in here (which is 3/255). + // up as 0.011765 in here (which is 3/255 and not 1/100). if (v.get_brightness() >= 0.012f) { return false; } - values = v; - // This night light mode is activated when white light is selected. // Based on measurements using the original device firmware, so it // matches the night light of the original firmware. diff --git a/color_off.h b/color_off.h index 443d026..23c36d5 100644 --- a/color_off.h +++ b/color_off.h @@ -4,22 +4,22 @@ #include #include "common.h" +#include "gpio_outputs.h" namespace esphome { namespace yeelight { namespace bs2 { +/** + * This class can handle the GPIO outputs in case the light of turned off. + */ class ColorOff : public GPIOOutputs { -public: +protected: bool set_light_color_values(light::LightColorValues v) { - // This class can handle the light settings when the light is turned - // off or the brightness is set to zero. if (v.get_state() != 0.0f && v.get_brightness() != 0.0f) { return false; } - values = v; - red = 1.0f; green = 1.0f; blue = 1.0f; diff --git a/color_rgb_light.h b/color_rgb_light.h index 801ba5f..9de4b59 100644 --- a/color_rgb_light.h +++ b/color_rgb_light.h @@ -1,13 +1,10 @@ -/** - * This code implements the RGB light mode (based on RGB + brightness) - * for the Yeelight Bedside Lamp 2. - */ #pragma once #include #include #include "common.h" +#include "gpio_outputs.h" namespace esphome { namespace yeelight { @@ -29,28 +26,27 @@ using RGBRing = std::array; using RGBCircle = std::array; /** - * The following table contains GPIO PWM duty cycles as used for driving - * the LEDs in the device in RGB mode. + * The following table contains GPIO PWM duty cycles as used for driving the + * LEDs in the device in RGB mode. * * The base for this table are measurements against the original device * firmware, using the RGB color circle as used in Home Assistant as the * color space model. * - * This circle has 7 colored rings around a white center point. - * The outer ring, with the highest saturation, is numbered as 0. - * The inner ring around the white center point is numbered as 6. - * The white center point itself is numbered as 7, although this one - * cannot really be called "a ring". + * This circle has 7 colored rings around a white center point. The outer + * ring, with the highest saturation, is numbered as 0. The inner ring + * around the white center point is numbered as 6. The white center point + * itself is numbered as 7, although this one cannot really be called "a + * ring". * - * For each ring, there are 24 color positions, starting at the - * color red (0°), going around the circle clockwise via - * green (120°) and blue (240°). + * For each ring, there are 24 color positions, starting at the color red + * (0°), going around the circle clockwise via green (120°) and blue (240°). * * For each color position, two duty cycle measurements are registered: * - one defining the duty cycles at 1% brightness - * - one defining the duty cycles at 100% brightness - * Duty cycles for in-between brightnesses can be derived from these - * values by means of linear interpolation. + * - one defining the duty cycles at 100% brightness Duty cycles for + * in-between brightnesses can be derived from these values by means of + * linear interpolation. */ static const RGBCircle rgb_circle_ {{ // Ring 0, min value RGB component value = 0 @@ -244,19 +240,19 @@ static const RGBCircle rgb_circle_ {{ }} }}; +/** + * This class can handle the GPIO outputs for the RGB light mode, + * based on RGB color values + brightness. + */ class ColorRGBLight : public GPIOOutputs { -public: +protected: bool set_light_color_values(light::LightColorValues v) { - // This class can handle the GPIO outputs for RGB light, based - // on RGB color values + brightness. if (v.get_white() > 0.0f) { return false; } - values = v; - - // 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 + // 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(v.get_red(), v.get_green()), v.get_blue()); auto level = 7.0f * rgb_min; @@ -282,6 +278,7 @@ public: // 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. + // TODO use esphome::lerp ? auto d = level - level_a; red = rgb_a_.red + d * (rgb_b_.red - rgb_a_.red); green = rgb_a_.green + d * (rgb_b_.green - rgb_a_.green); @@ -293,7 +290,6 @@ public: return true; } -protected: RGBPoint rgbp_a_; RGBPoint rgbp_b_; RGB rgb_a_; @@ -311,8 +307,8 @@ protected: return; } - // Other ring levels are more complex. Start by retrieving the - // duty cycle measurement data for the ring at hand. + // Other ring levels are more complex. Start by retrieving the duty + // cycle measurement data for the ring at hand. auto ring = rgb_circle_[ring_level]; // Because we only have a subset of all colors in the RGB ring @@ -367,10 +363,9 @@ protected: } /** - * Apply brightness interpolation to the duty cycle measurements. - * We have the low (0.01) and high (1.00) brightness measurements - * in the data. Brightness can be applied by means of linear - * interpolation. + * Apply brightness interpolation to the duty cycle measurements. We + * have the low (0.01) and high (1.00) brightness measurements in the + * data. Brightness can be applied by means of linear interpolation. */ void apply_brightness_(RGBPoint *p, float brightness, RGB *rgb) { auto d = brightness - 0.01f; diff --git a/color_transition_handler.h b/color_transition_handler.h new file mode 100644 index 0000000..e6de4d9 --- /dev/null +++ b/color_transition_handler.h @@ -0,0 +1,139 @@ +#pragma once + +#include "common.h" +#include "gpio_outputs.h" +#include "color_instant_handler.h" + +namespace esphome { +namespace yeelight { +namespace bs2 { + +/** + * This is an interface definition that is used to extend the LightState + * class with functionality to inspect LightTransformer data from + * within other classes. + * + * This interface is required for the ColorTransitionHandler, so it can + * check whether or not a light color transition is in progress. + */ +class LightStateTransformerInspector { +public: + virtual bool is_active() = 0; + virtual bool is_transition() = 0; + virtual light::LightColorValues get_end_values() = 0; + virtual float get_progress() = 0; +}; + +/** + * This class is used to handle specific light color transition requirements + * for the device. + * + * When using the default ESPHome logic, transitioning is done by + * transitioning all light properties linearly from the original values to + * the new values, and letting the light output object translate these + * properties into light outputs on every step of the way. While this does + * work, it does not work nicely. + * + * For example, when transitioning from warm to cold white light, the color + * temperature would be transitioned from the old value to the new value. + * While doing so, the transition hits the middle white light setting, which + * shows up as a bright flash in the middle of the transition. The original + * firmware however, shows a smooth transition from warm to cold white + * light, without any flash. + * + * 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 ColorTransitionHandler : public GPIOOutputs { +public: + ColorTransitionHandler(LightStateTransformerInspector *inspector) : transformer_(inspector) {} + +protected: + bool set_light_color_values(light::LightColorValues values) { + if (!light_state_has_active_transition_()) { + // Remember the last active light color values. When a transition + // is detected, we'll use these as the starting point. It is not + // possible to use the current values at that point, because the + // transition is already in progress by the time the transition + // is detected. + start_light_values_ = values; + + active_ = false; + return false; + } + + // When a fresh transition is started, then compute the GPIO outputs + // to use for both the start and end point. This transition handler + // will then transition linearly between these two. + if (is_fresh_transition_()) { + start_->set_light_color_values(start_light_values_); + end_light_values_ = transformer_->get_end_values(); + end_->set_light_color_values(end_light_values_); + active_ = true; + } + // When a transition is modified, then use the current GPIO outputs + // as the new starting point. + else if (is_modified_transition_()) { + this->copy_to(start_); + end_light_values_ = transformer_->get_end_values(); + end_->set_light_color_values(end_light_values_); + } + + // Determine required GPIO outputs for current transition progress. + progress_ = transformer_->get_progress(); + auto smoothed = light::LightTransitionTransformer::smoothed_progress(progress_); + red = esphome::lerp(smoothed, start_->red, end_->red); + green = esphome::lerp(smoothed, start_->green, end_->green); + blue = esphome::lerp(smoothed, start_->blue, end_->blue); + white = esphome::lerp(smoothed, start_->white, end_->white); + + return true; + } + + bool active_ = false; + float progress_ = 0.0f; + LightStateTransformerInspector *transformer_; + light::LightColorValues start_light_values_; + light::LightColorValues end_light_values_; + GPIOOutputs *start_ = new ColorInstantHandler(); + GPIOOutputs *end_ = new ColorInstantHandler(); + + /** + * Checks if the LightState currently has an active LightTransformer. + */ + bool light_state_has_active_transition_() { + if (!transformer_->is_active()) + return false; + if (!transformer_->is_transition()) + return false; + return true; + } + + /** + * Checks if a fresh transitioning is started. + * A transitioning is fresh when no existing transition is active. + */ + bool is_fresh_transition_() { + return active_ == false; + } + + /** + * Checks if a new end state is set, while an existing transition + * is active. This might be detected in two ways: + * - the end color has been updated + * - the progress has been reverted + */ + bool is_modified_transition_() { + auto new_end_light_values = transformer_->get_end_values(); + auto new_progress = transformer_->get_progress(); + return ( + new_end_light_values != end_light_values_ || + new_progress < progress_ + ); + } +}; + +} // namespace yeelight_bs2 +} // namespace yeelight +} // namespace bs2 diff --git a/color_white_light.h b/color_white_light.h index c1fd7d0..69008e9 100644 --- a/color_white_light.h +++ b/color_white_light.h @@ -1,18 +1,27 @@ -/** - * This code implements the white light mode (based on temperature + - * brightness) for the Yeelight Bedside Lamp 2. - */ #pragma once #include #include #include "common.h" +#include "gpio_outputs.h" namespace esphome { namespace yeelight { namespace bs2 { +/** + * The minimum color temperature in mired. Same as supported by + * the original Yeelight firmware. + */ +static const int MIRED_MIN = 153; + +/** + * The maximum color temperature in mired. Same as supported by + * the original Yeelight firmware. + */ +static const int MIRED_MAX = 588; + struct RGBWLevelsByTemperature { float from_temperature; float red; @@ -59,32 +68,31 @@ static const RGBWLevelsTable rgbw_levels_100_ {{ { 153.0f, 1.000f, 0.000f, 0.187f, 0.335f } }}; +/** + * This class can handle the GPIO outputs for the white light mode, + * based on color temperature + brightness. + */ class ColorWhiteLight : public GPIOOutputs { -public: +protected: bool set_light_color_values(light::LightColorValues v) { - // This class can handle the light settings when white light is - // requested, based on color temperature + brightness. if (v.get_white() == 0.0f) { return false; } - values = v; - 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); - red = interpolate_(levels_1.red, levels_100.red, brightness); - 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); + red = esphome::lerp(brightness, levels_1.red, levels_100.red); + green = esphome::lerp(brightness, levels_1.green, levels_100.green); + blue = esphome::lerp(brightness, levels_1.blue, levels_100.blue); + white = esphome::lerp(brightness, levels_1.white, levels_100.white); return true; } -protected: float clamp_temperature_(float temperature) { if (temperature > MIRED_MAX) @@ -110,14 +118,6 @@ protected: return item; 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; - auto level = level_1 + (brightness - 0.01f) * coefficient; - return level; - } }; } // namespace bs2 diff --git a/common.h b/common.h index 2622cf8..b0aafb3 100644 --- a/common.h +++ b/common.h @@ -4,49 +4,9 @@ namespace esphome { namespace yeelight { namespace bs2 { - /// A tag, used for logging. - static const char *TAG = "yeelight_bs2"; +/** A tag, used for logging. */ +static const char *TAG = "yeelight_bs2"; - /// The minimum color temperature in mired. Same as supported by - /// the original Yeelight firmware. - static const int MIRED_MIN = 153; - - /// The maximum color temperature in mired. Same as supported by - /// the original Yeelight firmware. - static const int MIRED_MAX = 588; - - /// 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 } // namespace yeelight } // namespace esphome diff --git a/gpio_outputs.h b/gpio_outputs.h new file mode 100644 index 0000000..342fb7c --- /dev/null +++ b/gpio_outputs.h @@ -0,0 +1,46 @@ +#pragma once + +namespace esphome { +namespace yeelight { +namespace bs2 { + +/** + * This abstract class is used for implementing classes that translate + * LightColorValues into the required GPIO PWM duty cycle levels to represent + * the requested color on the physical device. + */ +class GPIOOutputs { +public: + float red = 0.0f; + float green = 0.0f; + float blue = 0.0f; + float white = 0.0f; + + /** + * Sets 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 input can be handled, false otherwise. + */ + virtual bool set_light_color_values(light::LightColorValues v) = 0; + + /** + * Copies the current output values to another GPIOOutputs object. + */ + void copy_to(GPIOOutputs *other) { + other->red = red; + other->green = green; + other->blue = blue; + other->white = white; + } + + void log(const char *prefix) { + ESP_LOGD(TAG, "%s: RGB=[%f,%f,%f], white=%f", + prefix, red, green, blue, white); + } +}; + +} // namespace bs2 +} // namespace yeelight +} // namespace esphome diff --git a/light.h b/light.h index c6e3a0f..3b8de4f 100644 --- a/light.h +++ b/light.h @@ -1,258 +1,148 @@ #pragma once +#include "common.h" +#include "color_instant_handler.h" +#include "color_transition_handler.h" + namespace esphome { namespace yeelight { 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 TransitionHandler class. - /// - /// The transformer is protected in the light output class, making - /// it impossible to access these properties directly from the - /// light output class. - class LightStateTransformerInspector { - public: - virtual bool is_active() = 0; - virtual bool is_transition() = 0; - virtual light::LightColorValues get_end_values() = 0; - virtual float get_progress() = 0; - }; - - /// This class is used to handle color transition requirements. - /// - /// When using the default ESPHome logic, transitioning is done by - /// transitioning all light properties linearly from the original - /// values to the new values, and letting the light output object - /// translate these properties into light outputs on every step of the - /// way. While this does work, it does not work nicely. - /// - /// For example, when transitioning from warm to cold white light, - /// the color temperature would be transitioned from the old value to - /// the new value. While doing so, the transition hits the middle - /// white light setting, which shows up as a bright flash in the - /// middle of the transition. The original firmware however, shows a - /// smooth transition from warm to cold white light, without any flash. - /// - /// 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 : public GPIOOutputs { - public: - TransitionHandler(LightStateTransformerInspector *inspector) : transformer_(inspector) {} - - bool set_light_color_values(light::LightColorValues values) { - if (!light_state_has_active_transition_()) { - // Remember the last active light color values. When a transition - // is detected, use these as the starting point. It is not possible - // to use the current values at that point, because the transition - // is already in progress by the time the transition is detected. - start_values = values; - - active_ = false; - return false; - } - - // When a fresh transition is started, then compute the GPIO outputs - // to use for both the start and end point. This transition handler - // will then transition linearly between these two. - if (is_fresh_transition_()) { - start_->set_light_color_values(start_values); - end_->set_light_color_values(transformer_->get_end_values()); - active_ = true; - } - // When a transition is modified, then use the current GPIO outputs - // as the new starting point. - else if (is_modified_transition_()) { - this->copy_to(start_); - end_->set_light_color_values(transformer_->get_end_values()); - } - - // Determine the required GPIO outputs for the current transition progress. - progress_ = transformer_->get_progress(); - auto smoothed = light::LightTransitionTransformer::smoothed_progress(progress_); - red = esphome::lerp(smoothed, start_->red, end_->red); - green = esphome::lerp(smoothed, start_->green, end_->green); - blue = esphome::lerp(smoothed, start_->blue, end_->blue); - white = esphome::lerp(smoothed, start_->white, end_->white); - - return true; - } - - protected: - bool active_ = false; - float progress_ = 0.0f; - LightStateTransformerInspector *transformer_; - light::LightColorValues start_values; - GPIOOutputs *start_ = new ColorTranslator(); - GPIOOutputs *end_ = new ColorTranslator(); - - /// Checks if the LightState object currently has an active LightTransformer. - bool light_state_has_active_transition_() { - if (!transformer_->is_active()) - return false; - if (!transformer_->is_transition()) - return false; - return true; - } - - /// Checks if a fresh transitioning is started. - /// A transitioning is fresh when no existing transition is active. - bool is_fresh_transition_() { - return active_ == false; - } - - /// Checks if a new end state is set, while an existing transition - /// is active. This might be detected in two ways: - /// - the end color has been updated - /// - the progress has been reverted - bool is_modified_transition_() { - auto new_end_values = transformer_->get_end_values(); - auto new_progress = transformer_->get_progress(); - return new_end_values != end_->values || new_progress < progress_; - } - }; - - /// An implementation of the LightOutput interface for the Yeelight - /// Bedside Lamp 2. The function of this class is to translate a - /// required light state into actual physicial GPIO output signals - /// to drive the device's LED circuitry. - class YeelightBS2LightOutput : public Component, public light::LightOutput { - public: - /// Set the LEDC output for the red LED circuitry channel. - void set_red_output(ledc::LEDCOutput *red) { - red_ = red; - } - - /// Set the LEDC output for the green LED circuitry channel. - void set_green_output(ledc::LEDCOutput *green) { - green_ = green; - } - - /// Set the LEDC output for the blue LED circuitry channel. - void set_blue_output(ledc::LEDCOutput *blue) { - blue_ = blue; - } - - /// Set the LEDC output for the white LED circuitry channel. - void set_white_output(ledc::LEDCOutput *white) { - white_ = white; - } - - /// Set the first GPIO binary output, used as internal master - /// switch for the LED light circuitry. - void set_master1_output(gpio::GPIOBinaryOutput *master1) { - master1_ = master1; - } - - /// Set the second GPIO binary output, used as internal master - /// switch for the LED light circuitry. - void set_master2_output(gpio::GPIOBinaryOutput *master2) { - master2_ = master2; - } - - /// Returns a LightTraits object, which is used to explain to the - /// outside world (e.g. Home Assistant) what features are supported - /// by this device. - light::LightTraits get_traits() override - { - auto traits = light::LightTraits(); - traits.set_supports_rgb(true); - traits.set_supports_color_temperature(true); - traits.set_supports_brightness(true); - traits.set_supports_rgb_white_value(false); - traits.set_supports_color_interlock(true); - traits.set_min_mireds(MIRED_MIN); - traits.set_max_mireds(MIRED_MAX); - return traits; - } - - /// Applies a requested light state to the physicial GPIO outputs. - void write_state(light::LightState *state) - { - auto values = state->current_values; - - // The color must either be set instantly, or the color is - // transitioning to an end color. The transition handler - // will do its own inspection to see if a transition is - // currently active or not. Based on the outcome, use either - // the instant or transition handler. - GPIOOutputs *delegate; - if (transition_handler_->set_light_color_values(values)) { - delegate = transition_handler_; - } else { - instant_handler_->set_light_color_values(values); - delegate = instant_handler_; - } - - // Note: one might think that it is more logical to turn on - // the LED circuitry master switch after setting the individual - // channels, but this is the order that was used by the original - // firmware. I tried to stay as close as possible to the original - // behavior, so that's why these GPIOs are turned on at this point. - if (values.get_state() != 0) - { - master2_->turn_on(); - master1_->turn_on(); - } - - // Apply the current GPIO output levels from the selected handler. - red_->set_level(delegate->red); - green_->set_level(delegate->green); - blue_->set_level(delegate->blue); - white_->set_level(delegate->white); - - if (values.get_state() == 0) - { - master2_->turn_off(); - master1_->turn_off(); - } - } - - protected: - ledc::LEDCOutput *red_; - ledc::LEDCOutput *green_; - ledc::LEDCOutput *blue_; - ledc::LEDCOutput *white_; - esphome::gpio::GPIOBinaryOutput *master1_; - esphome::gpio::GPIOBinaryOutput *master2_; - GPIOOutputs *transition_handler_; - GPIOOutputs *instant_handler_ = new ColorTranslator(); - - friend class YeelightBS2LightState; - - /// Called by the YeelightBS2LightState class, to set the object that - /// can be used to access protected data from the light state object. - void set_transformer_inspector(LightStateTransformerInspector *exposer) { - transition_handler_ = new TransitionHandler(exposer); - } - }; - - /// This custom LightState class is used to provide access to the - /// protected LightTranformer information in the LightState class. - class YeelightBS2LightState : public light::LightState, public LightStateTransformerInspector +/** + * A LightOutput class for the Yeelight Bedside Lamp 2. + * + * The function of this class is to translate a required light state + * into actual physicial GPIO output signals to drive the device's LED + * circuitry. It forms the glue between the physical device and the + * logical light color input. + */ +class YeelightBS2LightOutput : public Component, public light::LightOutput { +public: + /** Sets the LEDC output for the red LED circuitry channel. */ + void set_red_output(ledc::LEDCOutput *red) { red_ = red; } + + /** Sets the LEDC output for the green LED circuitry channel. */ + void set_green_output(ledc::LEDCOutput *green) { green_ = green; } + + /** Sets the LEDC output for the blue LED circuitry channel. */ + void set_blue_output(ledc::LEDCOutput *blue) { blue_ = blue; } + + /** Sets the LEDC output for the white LED circuitry channel. */ + void set_white_output(ledc::LEDCOutput *white) { white_ = white; } + + /** + * Sets the first GPIO binary output, used as internal master switch for + * the LED light circuitry. + */ + void set_master1_output(gpio::GPIOBinaryOutput *master1) { master1_ = master1; } + + /** + * Set the second GPIO binary output, used as internal master switch for + * the LED light circuitry. + */ + void set_master2_output(gpio::GPIOBinaryOutput *master2) { master2_ = master2; } + + /** + * Returns a LightTraits object, which is used to explain to the outside + * world (e.g. Home Assistant) what features are supported by this device. + */ + light::LightTraits get_traits() override { - public: - YeelightBS2LightState(const std::string &name, YeelightBS2LightOutput *output) : light::LightState(name, output) { - output->set_transformer_inspector(this); - } - - bool is_active() { - return this->transformer_ != nullptr; - } - - bool is_transition() { - return this->transformer_->is_transition(); + auto traits = light::LightTraits(); + traits.set_supports_rgb(true); + traits.set_supports_color_temperature(true); + traits.set_supports_brightness(true); + traits.set_supports_rgb_white_value(false); + traits.set_supports_color_interlock(true); + traits.set_min_mireds(MIRED_MIN); + traits.set_max_mireds(MIRED_MAX); + return traits; + } + + /** + * Applies a requested light state to the physicial GPIO outputs. + */ + void write_state(light::LightState *state) + { + auto values = state->current_values; + + // The color must either be set instantly, or the color is + // transitioning to an end color. The transition handler will do its + // own inspection to see if a transition is currently active or not. + // Based on the outcome, use either the instant or transition handler. + GPIOOutputs *delegate; + if (transition_handler_->set_light_color_values(values)) { + delegate = transition_handler_; + } else { + instant_handler_->set_light_color_values(values); + delegate = instant_handler_; + } + + // Note: one might think that it is more logical to turn on the LED + // circuitry master switch after setting the individual channels, + // but this is the order that was used by the original firmware. I + // tried to stay as close as possible to the original behavior, so + // that's why these GPIOs are turned on at this point. + if (values.get_state() != 0) + { + master2_->turn_on(); + master1_->turn_on(); } - light::LightColorValues get_end_values() { - return this->transformer_->get_end_values(); - } + // Apply the current GPIO output levels from the selected handler. + red_->set_level(delegate->red); + green_->set_level(delegate->green); + blue_->set_level(delegate->blue); + white_->set_level(delegate->white); - float get_progress() { - return this->transformer_->get_progress(); - } - }; + if (values.get_state() == 0) + { + master2_->turn_off(); + master1_->turn_off(); + } + } + +protected: + ledc::LEDCOutput *red_; + ledc::LEDCOutput *green_; + ledc::LEDCOutput *blue_; + ledc::LEDCOutput *white_; + esphome::gpio::GPIOBinaryOutput *master1_; + esphome::gpio::GPIOBinaryOutput *master2_; + GPIOOutputs *transition_handler_; + GPIOOutputs *instant_handler_ = new ColorInstantHandler(); + + friend class YeelightBS2LightState; + + /** + * Called by the YeelightBS2LightState class, to set the object that can be + * used to access the protected LightTransformer data from the LightState + * object. + */ + void set_transformer_inspector(LightStateTransformerInspector *exposer) { + transition_handler_ = new ColorTransitionHandler(exposer); + } +}; + +/** + * This custom LightState class is used to provide access to the protected + * LightTranformer information in the LightState class. + * + * This class is used by the ColorTransitionHandler class to inspect if + * an ongoing light color transition is active in a LightState object. + */ +class YeelightBS2LightState : public light::LightState, public LightStateTransformerInspector +{ +public: + YeelightBS2LightState(const std::string &name, YeelightBS2LightOutput *output) : light::LightState(name, output) { + output->set_transformer_inspector(this); + } + + bool is_active() { return this->transformer_ != nullptr; } + bool is_transition() { return this->transformer_->is_transition(); } + light::LightColorValues get_end_values() { return this->transformer_->get_end_values(); } + float get_progress() { return this->transformer_->get_progress(); } +}; } // namespace bs2 } // namespace yeelight