diff --git a/doc/reverse_engineering/Original firmware boot messages.txt b/doc/reverse_engineering/Original Firmware/Original firmware boot messages.txt similarity index 100% rename from doc/reverse_engineering/Original firmware boot messages.txt rename to doc/reverse_engineering/Original Firmware/Original firmware boot messages.txt diff --git a/light.py b/light.py index 3495429..74ce17d 100644 --- a/light.py +++ b/light.py @@ -28,19 +28,19 @@ def to_code(config): yield light.register_light(var, config) led_red = yield cg.get_variable(config[CONF_RED]) - cg.add(var.set_red(led_red)) + cg.add(var.set_red_output(led_red)) led_green = yield cg.get_variable(config[CONF_GREEN]) - cg.add(var.set_green(led_green)) + cg.add(var.set_green_output(led_green)) led_blue = yield cg.get_variable(config[CONF_BLUE]) - cg.add(var.set_blue(led_blue)) + cg.add(var.set_blue_output(led_blue)) led_white = yield cg.get_variable(config[CONF_WHITE]) - cg.add(var.set_white(led_white)) + cg.add(var.set_white_output(led_white)) master1 = yield cg.get_variable(config[CONF_MASTER1]) - cg.add(var.set_master1(master1)) + cg.add(var.set_master1_output(master1)) master2 = yield cg.get_variable(config[CONF_MASTER2]) - cg.add(var.set_master2(master2)) + cg.add(var.set_master2_output(master2)) diff --git a/white_light.h b/white_light.h new file mode 100644 index 0000000..4285737 --- /dev/null +++ b/white_light.h @@ -0,0 +1,119 @@ +#pragma once + +#include +#include + +namespace esphome { +namespace rgbww { +namespace yeelight_bs2 { + +// Same range as supported by the original Yeelight firmware. +static const int MIRED_MAX = 153; +static const int MIRED_MIN = 588; + +struct RgbwLevelsByTemperature { + float from_temperature; + float red; + float green; + float blue; + float white; +}; + +using RgbwLevelsTable = std::array; + +static const RgbwLevelsTable rgbw_levels_1_ {{ + { 501.0f, 0.873f, 0.907f, 1.000f, 0.063f }, + { 455.0f, 0.873f, 0.896f, 1.000f, 0.063f }, + { 417.0f, 0.873f, 0.891f, 1.000f, 0.068f }, + { 371.0f, 0.873f, 0.880f, 1.000f, 0.070f }, + { 334.0f, 0.873f, 0.887f, 1.000f, 0.088f }, + { 313.0f, 0.882f, 0.904f, 1.000f, 0.128f }, + { 295.0f, 0.947f, 1.000f, 0.968f, 0.145f }, + { 251.0f, 0.999f, 1.000f, 1.000f, 0.155f }, + { 223.0f, 1.000f, 0.899f, 0.921f, 0.130f }, + { 201.0f, 1.000f, 0.873f, 0.908f, 0.115f }, + { 182.0f, 1.000f, 0.873f, 0.901f, 0.103f }, + { 173.0f, 1.000f, 0.873f, 0.904f, 0.094f }, + { 167.0f, 1.000f, 0.873f, 0.891f, 0.098f }, + { 154.0f, 1.000f, 0.873f, 0.894f, 0.090f }, + { 153.0f, 1.000f, 0.873f, 0.892f, 0.088f } +}}; + +static const RgbwLevelsTable rgbw_levels_100_ {{ + { 501.0f, 0.000f, 0.344f, 1.000f, 0.068f }, + { 455.0f, 0.000f, 0.237f, 1.000f, 0.093f }, + { 417.0f, 0.000f, 0.186f, 1.000f, 0.120f }, + { 371.0f, 0.000f, 0.149f, 1.000f, 0.167f }, + { 334.0f, 0.000f, 0.135f, 1.000f, 0.325f }, + { 313.0f, 0.097f, 0.314f, 1.000f, 0.740f }, + { 295.0f, 0.745f, 1.000f, 0.953f, 0.905f }, + { 251.0f, 1.000f, 1.000f, 1.000f, 1.000f }, + { 223.0f, 1.000f, 0.267f, 0.485f, 0.765f }, + { 201.0f, 1.000f, 0.000f, 0.355f, 0.609f }, + { 182.0f, 1.000f, 0.000f, 0.282f, 0.489f }, + { 173.0f, 1.000f, 0.000f, 0.313f, 0.392f }, + { 167.0f, 1.000f, 0.000f, 0.180f, 0.422f }, + { 154.0f, 1.000f, 0.000f, 0.218f, 0.368f }, + { 153.0f, 1.000f, 0.000f, 0.187f, 0.335f } +}}; + +class WhiteLight +{ +public: + float red = 0; + float green = 0; + float blue = 0; + float white = 0; + + void set_color(float temperature, float brightness) + { + temperature = clamp_temperature_(temperature); + brightness = clamp_brightness_(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); + } + +protected: + float clamp_temperature_(float temperature) + { + if (temperature < MIRED_MAX) + temperature = MIRED_MAX; + else if (temperature > MIRED_MIN) + temperature = MIRED_MIN; + return temperature; + } + + float clamp_brightness_(float brightness) + { + if (brightness < 0.01f) + brightness = 0.01f; + else if (brightness > 1.00f) + brightness = 1.00f; + return brightness; + } + + RgbwLevelsByTemperature lookup_in_table_(RgbwLevelsTable table, float temperature) + { + for (RgbwLevelsByTemperature& item : table) + if (temperature >= item.from_temperature) + return item; + throw std::invalid_argument("received too low temperature"); + } + + 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 yeelight_bs2 +} // namespace rgbww +} // namespace esphome diff --git a/yeelight_bs2_light_output.h b/yeelight_bs2_light_output.h index c9eb499..778ffd4 100644 --- a/yeelight_bs2_light_output.h +++ b/yeelight_bs2_light_output.h @@ -16,312 +16,230 @@ // Reported the issue + fix at: // https://github.com/esphome/esphome/pull/1643 // -// A work-around for this issue can be enabled using this define: +// A work-around for this issue can be enabled using the following +// define. Note that the code provides a forward-compatible fix, so +// having this define active with a fixed ESPHome version should +// not be a problem. #define TRANSITION_TO_OFF_BUGFIX -//#define YEELIGHT_DEBUG_LOG - - -namespace esphome -{ - namespace rgbww +namespace esphome { +namespace rgbww { + + static const char *TAG = "yeelight_bs2.light"; + + // Same range as supported by the original Yeelight firmware. + static const int HOME_ASSISTANT_MIRED_MIN = 153; + static const int HOME_ASSISTANT_MIRED_MAX = 588; + + // The PWM frequencies as used by the original device + // for driving the LED circuitry. + const float RGB_PWM_FREQUENCY = 3000.0f; + // I measured 10kHz for this channel, but making this 10000.0f results + // in the blue channel failing. So possibly this is the actual + // frequency to use (it's the frequency that provides a 13 bit + // bith depth to the PWM channel). + const float WHITE_PWM_FREQUENCY = 9765.0f; + + class YeelightBS2LightOutput : public Component, public light::LightOutput { - static const char *TAG = "yeelight_bs2.light"; - - // Same range as supported by the original Yeelight firmware. - static const int HOME_ASSISTANT_MIRED_MIN = 153; - static const int HOME_ASSISTANT_MIRED_MAX = 588; - - // The PWM frequency as used by the original device - // for driving the LED circuitry. - const float PWM_FREQUENCY = 3000.0f; - - class YeelightBS2LightOutput : public Component, public light::LightOutput + public: + light::LightTraits get_traits() override { - public: - void set_red(ledc::LEDCOutput *red) { red_ = red; red_->set_frequency(PWM_FREQUENCY); } - void set_green(ledc::LEDCOutput *green) { green_ = green; green_->set_frequency(PWM_FREQUENCY); } - void set_blue(ledc::LEDCOutput *blue) { blue_ = blue; blue_->set_frequency(PWM_FREQUENCY); } - void set_white(ledc::LEDCOutput *white) { white_ = white; white_->set_frequency(PWM_FREQUENCY); } - void set_master1(gpio::GPIOBinaryOutput *master1) { master1_ = master1; } - void set_master2(gpio::GPIOBinaryOutput *master2) { master2_ = master2; } - - 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(HOME_ASSISTANT_MIRED_MIN); - traits.set_max_mireds(HOME_ASSISTANT_MIRED_MAX); - return traits; - } + 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(HOME_ASSISTANT_MIRED_MIN); + traits.set_max_mireds(HOME_ASSISTANT_MIRED_MAX); + return traits; + } + + void set_red_output(ledc::LEDCOutput *red) { + red_ = red; + red_->set_frequency(RGB_PWM_FREQUENCY); + } + + void set_green_output(ledc::LEDCOutput *green) { + green_ = green; + green_->set_frequency(RGB_PWM_FREQUENCY); + } + + void set_blue_output(ledc::LEDCOutput *blue) { + blue_ = blue; + blue_->set_frequency(RGB_PWM_FREQUENCY); + } + + void set_white_output(ledc::LEDCOutput *white) { + white_ = white; + white_->set_frequency(WHITE_PWM_FREQUENCY); + } + + void set_master1_output(gpio::GPIOBinaryOutput *master1) { + master1_ = master1; + } + + void set_master2_output(gpio::GPIOBinaryOutput *master2) { + master2_ = master2; + } + + void write_state(light::LightState *state) override + { + auto values = state->current_values; - void write_state(light::LightState *state) override - { - auto values = state->current_values; + ESP_LOGD(TAG, "B = State %f, RGB %f %f %f, BRI %f, TEMP %f", + values.get_state(), + values.get_red(), values.get_green(), values.get_blue(), + values.get_brightness(), values.get_color_temperature()); -#ifdef YEELIGHT_DEBUG_LOG - ESP_LOGD(TAG, "B = State %f, RGB %f %f %f, BRI %f, TEMP %f", - values.get_state(), - values.get_red(), values.get_green(), values.get_blue(), - values.get_brightness(), values.get_color_temperature()); -#endif - - // Power down the light when its state is 'off'. - if (values.get_state() == 0) - { - this->turn_off_(); -#ifdef YEELIGHT_DEBUG_LOG - previous_state_ = -1; - previous_brightness_ = 0; + // Power down the light when its state is 'off'. + if (values.get_state() == 0) + { + turn_off_(); +#ifdef TRANSITION_TO_OFF_BUGFIX + previous_state_ = -1; + previous_brightness_ = 0; #endif - return; - } + return; + } - auto brightness = values.get_brightness(); + auto brightness = values.get_brightness(); #ifdef TRANSITION_TO_OFF_BUGFIX - // Remember the brightness that is used when the light is fully ON. - if (values.get_state() == 1) { - previous_brightness_ = brightness; - } - // When transitioning towards zero brightness ... - else if (values.get_state() < previous_state_) { - // ... check if the prevous brightness is the same as the current - // brightness. If yes, then the brightness isn't being scaled ... - if (previous_brightness_ == brightness) { - // ... and we need to do that ourselves. - brightness = values.get_state() * brightness; - } - } - previous_state_ = values.get_state(); -#endif - - // Leave it to the default tooling to figure out the basics. - // Because of the color interlocking, there are two possible outcomes: - // - red, green, blue zero -> the light is in color temperature mode - // - cwhite, wwhite zero -> the light is in RGB mode - float red, green, blue, cwhite, wwhite; - state->current_values_as_rgbww(&red, &green, &blue, &cwhite, &wwhite, true, false); - - if (cwhite > 0 || wwhite > 0) - { - this->turn_on_in_color_temperature_mode_( - values.get_color_temperature(), brightness); - } - else - { - // The RGB mode does not use the RGB values as determined by - // current_values_as_rgbww(). The device has LED driving circuitry - // that takes care of the required brightness curve while ramping up - // the brightness. Therefore, the actual RGB values are passed here. - this->turn_on_in_rgb_mode_( - values.get_red(), values.get_green(), values.get_blue(), - brightness, values.get_state()); + // Remember the brightness that is used when the light is fully ON. + if (values.get_state() == 1) { + previous_brightness_ = brightness; + } + // When transitioning towards zero brightness ... + else if (values.get_state() < previous_state_) { + // ... check if the prevous brightness is the same as the current + // brightness. If yes, then the brightness isn't being scaled ... + if (previous_brightness_ == brightness) { + // ... and we need to do that ourselves. + brightness = values.get_state() * brightness; } } - - protected: - ledc::LEDCOutput *red_; - ledc::LEDCOutput *green_; - ledc::LEDCOutput *blue_; - ledc::LEDCOutput *white_; - esphome::gpio::GPIOBinaryOutput *master1_; - esphome::gpio::GPIOBinaryOutput *master2_; -#ifdef TRANSITION_TO_OFF_BUGFIX - float previous_state_ = 1; - float previous_brightness_ = -1; + previous_state_ = values.get_state(); #endif - void turn_off_() - { - // Using set_level() calls for the RGB GPIOs, and not - // turn_off(), because turn_off() causes some unwanted - // flashing when powering off at low brightness. - red_->set_level(1); - green_->set_level(1); - blue_->set_level(1); - white_->turn_off(); - master1_->turn_off(); - master2_->turn_off(); - } + // Leave it to the default tooling to figure out the basics. + // Because of the color interlocking, there are two possible outcomes: + // - red, green, blue zero -> the light is in color temperature mode + // - cwhite, wwhite zero -> the light is in RGB mode + float red, green, blue, cwhite, wwhite; + state->current_values_as_rgbww(&red, &green, &blue, &cwhite, &wwhite, true, false); - void turn_on_in_rgb_mode_(float red, float green, float blue, float brightness, float state) + if (cwhite > 0 || wwhite > 0) { -#ifdef YEELIGHT_DEBUG_LOG - ESP_LOGD(TAG, "Activate RGB %f, %f, %f, BRIGHTNESS %f", red, green, blue, brightness); -#endif - - // The brightness must be at least 3/100 to light up the LEDs. - // During transitions (where state is a fraction between 0 and 1, - // indicating the transition progress) we don't apply this to - // get smoother transitioning when turning on the light. - if (state == 1 && brightness < 0.03f) - brightness = 0.03f; - - // Apply brightness. - red = red * brightness; - green = green * brightness; - blue = blue * brightness; - - // Inverse the signal. The LEDs in the lamp's circuit are brighter - // when the pwm levels on the GPIO pins are lower. - red = 1.0f - red; - green = 1.0f - green; - blue = 1.0f - blue; - -#ifdef YEELIGHT_DEBUG_LOG - ESP_LOGD(TAG, "New LED state : RGBW %f, %f, %f", red, green, blue); -#endif - - // Drive the LEDs. - red_->set_level(red); - green_->set_level(green); - blue_->set_level(blue); - white_->turn_off(); - master1_->turn_on(); - master2_->turn_on(); + turn_on_in_white_mode_(values.get_color_temperature(), brightness); } - - void turn_on_in_color_temperature_mode_(float temperature, float brightness) + else { -#ifdef YEELIGHT_DEBUG_LOG - ESP_LOGD(TAG, "Activate TEMPERATURE %f, BRIGHTNESS %f", temperature, brightness); -#endif - - // Empirically determined during programming the temperature GPIO output - // code from below, by checking how far my outputs were off from the - // original lamp firmeware's outputs. This scaler is used for correcting - // my output towards the original output. - float scaler; - - float red = 1.0; - float green = 1.0; - float blue = 1.0; - float white = 1.0; - - // Temperature band 370 - 588 - if (temperature <= HOME_ASSISTANT_MIRED_MAX && temperature >= 371) - { - scaler = 3.23f; - - float start = 371; - float end = 588; - float band = end - start; - - float red_volt = 2.86f * (1.0f - brightness); - red = red_volt / scaler; - - float green_1 = 2.90f + (temperature - start) * (2.97f - 2.90f) / band; - float green_100 = 0.45f + (temperature - start) * (1.13f - 0.45f) / band; - float green_volt = green_1 + brightness * (green_100 - green_1); - green = green_volt / scaler; - - float white_1 = 0.28f - (temperature - start) * (0.28f - 0.19f) / band; - float white_100 = 1.07f - (temperature - start) * (1.07f - 0.22f) / band; - float white_volt = white_1 + brightness * (white_100 - white_1); - white = white_volt / scaler; - } - // Temperature band 334 - 370 - else if (temperature >= 334) - { - scaler = 3.23f; - - float red_volt = (1.0f - brightness) * 2.86f; - red = red_volt / scaler; - - float green_volt = 2.9f - brightness * (2.9f - 0.45f); - green = green_volt / scaler; - - float white_volt = 0.28f + brightness * (1.07f - 0.28f); - white = white_volt / scaler; - } - // Temperature band 313 - 333 - // - // The light becomes noticably brighter when moving from temperature 334 to - // temperature 333. There's a little jump in the lighting output here. - // Possibly this is a switch from warm to cold lighting as imposed by the - // LED circuitry, making this unavoidable. However, it would be interesting - // to see if we can smoothen this out. - // BTW: This behavior is in sync with the original firmware. - else if (temperature >= 313) - { - scaler = 3.23f; - - float red_volt = 2.89f - brightness * (2.89f - 0.32f); - red = red_volt / scaler; - - float green_volt = 2.96f - brightness * (2.96f - 1.03f); - green = green_volt / scaler; - - float white_volt = 0.42f + brightness * (2.43f - 0.42f); - float scaler_white = 3.45f; - white = white_volt / scaler_white; - } - // Temperature band 251 - 312 - else if (temperature >= 251) - { - scaler = 3.48f; - - float white_correction = 1.061; - float white_volt = 0.5f + brightness * (3.28f * white_correction - 0.5f); - white = white_volt / scaler; - } - // Temperature band 223 - 250 - else if (temperature >= 223) - { - scaler = 3.25f; - - float green_volt = 2.94f - brightness * (2.94f - 0.88f); - green = green_volt / scaler; + // The RGB mode does not use the RGB values as determined by + // current_values_as_rgbww(). The device has LED driving circuitry + // that takes care of the required brightness curve while ramping up + // the brightness. Therefore, the actual RGB values are passed here. + turn_on_in_rgb_mode_( + values.get_red(), values.get_green(), values.get_blue(), + brightness, values.get_state()); + } + } + + protected: + ledc::LEDCOutput *red_; + ledc::LEDCOutput *green_; + ledc::LEDCOutput *blue_; + ledc::LEDCOutput *white_; + esphome::gpio::GPIOBinaryOutput *master1_; + esphome::gpio::GPIOBinaryOutput *master2_; + esphome::rgbww::yeelight_bs2::WhiteLight white_light_; +#ifdef TRANSITION_TO_OFF_BUGFIX + float previous_state_ = 1; + float previous_brightness_ = -1; +#endif - float blue_volt = 3.02f - brightness * (3.02f - 1.59f); - blue = blue_volt / scaler; + void turn_off_() + { + red_->set_level(1); + green_->set_level(1); + blue_->set_level(1); + white_->set_level(0); + master2_->turn_off(); + master1_->turn_off(); + } + + void turn_on_in_rgb_mode_(float red, float green, float blue, float brightness, float state) + { + ESP_LOGD(TAG, "Activate RGB %f, %f, %f, BRIGHTNESS %f", red, green, blue, brightness); + + // The brightness must be at least 3/100 to light up the LEDs. + // During transitions (where state is a fraction between 0 and 1, + // indicating the transition progress) we don't apply this to + // get smoother transitioning when turning on the light. + if (state == 1 && brightness < 0.03f) + brightness = 0.03f; + + // Apply proper color mixing around the RGB white point. + // Overall, the RGB colors are very usable when simply scaling the + // RGB channels with the brightness, but around the white point, + // the color is a bit on the red side of the spectrum. The following + // scaling was created to fix that. + auto red_w = (0.07f + brightness*(0.57f - 0.07f)) * red; + auto green_w = (0.13f + brightness*(1.00f - 0.13f)) * green; + auto blue_w = (0.06f + brightness*(0.45f - 0.06f)) * blue; + + // For other colors, we can simply scale the RGB channels with the + // requested brightness, resulting in a very usable color. Not 100% + // the same as the original firmware, but sometimes even better IMO. + auto red_c = red * brightness; + auto green_c = green * brightness; + auto blue_c = blue * brightness; + + // The actual RGB values are a weighed mix of the above two. + // The closer to the white point, the more the white point + // value applies. + auto level_red = (red_w * ((green+blue)/2)) + (red_c * (1-(green+blue)/2)); + auto level_green = (green_w * ((red+blue)/2)) + (green_c * (1-(red+blue)/2)); + auto level_blue = (blue_w * ((red+green)/2)) + (blue_c * (1-(red+green)/2)); + + // Invert the signal. The LEDs in the lamp's circuit are brighter + // when the pwm levels on the GPIO pins are lower. + level_red = 1.0f - level_red; + level_green = 1.0f - level_green; + level_blue = 1.0f - level_blue; + + ESP_LOGD(TAG, "New LED state : RGBW %f, %f, %f, off", level_red, level_green, level_blue); + + // Drive the LEDs. + master2_->turn_on(); + master1_->turn_on(); + red_->set_level(level_red); + green_->set_level(level_green); + blue_->set_level(level_blue); + white_->set_level(0); + + } + + void turn_on_in_white_mode_(float temperature, float brightness) + { + ESP_LOGD(TAG, "Activate TEMPERATURE %f, BRIGHTNESS %f", + temperature, brightness); - float white_correction = 1.024f; - float white_volt = 0.42f + brightness * (2.51f * white_correction - 0.42f); - float scaler_white = 3.36f; - white = white_volt / scaler_white; - } - // Temperature band 153 - 222 - else if (temperature >= HOME_ASSISTANT_MIRED_MIN) - { - float start = 153; - float end = 222; - float band = end - start; - - scaler = 3.23f; - - float green_volt = 2.86f - brightness * 2.86f; - green = green_volt / scaler; - - float blue_1 = 2.92f + (temperature - start) * (2.97f - 2.92f) / band; - float blue_100 = 0.62f + (temperature - start) * (1.17f - 0.62f) / band; - float blue_volt = blue_1 - brightness * (blue_1 - blue_100); - blue = blue_volt / scaler; - - float white_1 = 0.28f + (temperature - start) * (0.37f - 0.28f) / band; - float white_100 = 1.1f + (temperature - start) * (2.0f - 1.1f) / band; - float white_volt = white_1 + brightness * (white_100 - white_1); - float scaler_white = 3.27f; - white = white_volt / scaler_white; - } + white_light_.set_color(temperature, brightness); -#ifdef YEELIGHT_DEBUG_LOG - ESP_LOGD(TAG, "New LED state : RGBW %f, %f, %f, %f", red, green, blue, white); -#endif + ESP_LOGD(TAG, "New LED state : RGBW %f, %f, %f, %f", + white_light_.red, white_light_.green, white_light_.blue, + white_light_.white); - red_->set_level(red); - green_->set_level(green); - blue_->set_level(blue); - white_->set_level(white); - master2_->turn_on(); - master1_->turn_on(); - } - }; + master2_->turn_on(); + master1_->turn_on(); + red_->set_level(white_light_.red); + green_->set_level(white_light_.green); + blue_->set_level(white_light_.blue); + white_->set_level(white_light_.white); + } + }; - } // namespace rgbww +} // namespace rgbww } // namespace esphome