#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