You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

282 lines
11 KiB

3 years ago
3 years ago
  1. #pragma once
  2. // What seems to be a bug in ESPHome transitioning: when turning on
  3. // the device, the brightness is scaled along with the state (which
  4. // runs from 0 to 1), but when turning off the device, the brightness
  5. // is kept the same while the state goes down from 1 to 0. As a result
  6. // when turning off the lamp with a transition time of 1s, the light
  7. // stays on for 1s and then turn itself off abruptly.
  8. //
  9. // Reported the issue + fix at:
  10. // https://github.com/esphome/esphome/pull/1643
  11. //
  12. // A work-around for this issue can be enabled using the following
  13. // define. Note that the code provides a forward-compatible fix, so
  14. // having this define active with a fixed ESPHome version should
  15. // not be a problem.
  16. #define TRANSITION_TO_OFF_BUGFIX
  17. namespace esphome {
  18. namespace yeelight {
  19. namespace bs2 {
  20. /// This is an interface definition that is used to extend the
  21. /// YeelightBS2LightOutput class with methods to access properties
  22. /// of an active LightTranformer from the TransitionHandler class.
  23. ///
  24. /// The transformer is protected in the light output class, making
  25. /// it impossible to access these properties directly from the
  26. /// light output class.
  27. class LightStateDataExposer {
  28. public:
  29. virtual bool has_active_transformer() = 0;
  30. virtual bool transformer_is_transition() = 0;
  31. virtual light::LightColorValues get_transformer_values() = 0;
  32. virtual light::LightColorValues get_transformer_end_values() = 0;
  33. virtual float get_transformer_progress() = 0;
  34. };
  35. /// This class is used to handle color transition requirements.
  36. ///
  37. /// When using the default ESPHome logic, transitioning is done by
  38. /// transitioning all light properties linearly from the original
  39. /// values to the new values, and letting the light output object
  40. /// translate these properties into light outputs on every step of the
  41. /// way. While this does work, it does not work nicely.
  42. ///
  43. /// For example, when transitioning from warm to cold white light,
  44. /// the color temperature would be transitioned from the old value to
  45. /// the new value. While doing so, the transition hits the middle
  46. /// white light setting, which shows up as a bright flash in the
  47. /// middle of the transition. The original firmware however, shows a
  48. /// smooth transition from warm to cold white light, without any flash.
  49. ///
  50. /// This class handles transitions by not varying the light properties
  51. /// over time, but by transitioning the LEDC duty cycle output levels
  52. /// over time. This matches the behavior of the original firmware.
  53. class TransitionHandler : public GPIOOutputs {
  54. public:
  55. TransitionHandler(LightStateDataExposer *exposer) : exposer_(exposer) {}
  56. bool set_light_color_values(light::LightColorValues values) {
  57. if (!has_active_transition_()) {
  58. start_values = values;
  59. active_ = false;
  60. return false;
  61. }
  62. if (is_fresh_transition_()) {
  63. start_->set_light_color_values(start_values);
  64. end_->set_light_color_values(exposer_->get_transformer_end_values());
  65. active_ = true;
  66. }
  67. auto progress = exposer_->get_transformer_progress();
  68. red = esphome::lerp(progress, start_->red, end_->red);
  69. green = esphome::lerp(progress, start_->green, end_->green);
  70. blue = esphome::lerp(progress, start_->blue, end_->blue);
  71. white = esphome::lerp(progress, start_->white, end_->white);
  72. return true;
  73. }
  74. protected:
  75. bool active_ = false;
  76. LightStateDataExposer *exposer_;
  77. light::LightColorValues start_values;
  78. GPIOOutputs *start_ = new ColorTranslator();
  79. GPIOOutputs *end_ = new ColorTranslator();
  80. /// Checks if this class will handle the light output logic.
  81. /// This is the case when a transformer is active and this
  82. /// transformer does implement a transitioning effect.
  83. bool has_active_transition_() {
  84. if (!exposer_->has_active_transformer())
  85. return false;
  86. if (!exposer_->transformer_is_transition())
  87. return false;
  88. return true;
  89. }
  90. /// Checks if a fresh transitioning is started.
  91. /// A transitioning is fresh when either no transition is known to
  92. /// be in progress or when a new end state is found during an
  93. /// ongoing transition.
  94. bool is_fresh_transition_() {
  95. if (active_ == false) {
  96. return true;
  97. }
  98. auto new_end_values = exposer_->get_transformer_end_values();
  99. if (new_end_values != end_->values) {
  100. return true;
  101. }
  102. return false;
  103. }
  104. };
  105. /// An implementation of the LightOutput interface for the Yeelight
  106. /// Bedside Lamp 2. The function of this class is to translate a
  107. /// required light state into actual physicial GPIO output signals
  108. /// to drive the device's LED circuitry.
  109. class YeelightBS2LightOutput : public Component, public light::LightOutput {
  110. public:
  111. /// Set the LEDC output for the red LED circuitry channel.
  112. void set_red_output(ledc::LEDCOutput *red) {
  113. red_ = red;
  114. }
  115. /// Set the LEDC output for the green LED circuitry channel.
  116. void set_green_output(ledc::LEDCOutput *green) {
  117. green_ = green;
  118. }
  119. /// Set the LEDC output for the blue LED circuitry channel.
  120. void set_blue_output(ledc::LEDCOutput *blue) {
  121. blue_ = blue;
  122. }
  123. /// Set the LEDC output for the white LED circuitry channel.
  124. void set_white_output(ledc::LEDCOutput *white) {
  125. white_ = white;
  126. }
  127. /// Set the first GPIO binary output, used as internal master
  128. /// switch for the LED light circuitry.
  129. void set_master1_output(gpio::GPIOBinaryOutput *master1) {
  130. master1_ = master1;
  131. }
  132. /// Set the second GPIO binary output, used as internal master
  133. /// switch for the LED light circuitry.
  134. void set_master2_output(gpio::GPIOBinaryOutput *master2) {
  135. master2_ = master2;
  136. }
  137. /// Returns a LightTraits object, which is used to explain to the
  138. /// outside world (e.g. Home Assistant) what features are supported
  139. /// by this device.
  140. light::LightTraits get_traits() override
  141. {
  142. auto traits = light::LightTraits();
  143. traits.set_supports_rgb(true);
  144. traits.set_supports_color_temperature(true);
  145. traits.set_supports_brightness(true);
  146. traits.set_supports_rgb_white_value(false);
  147. traits.set_supports_color_interlock(true);
  148. traits.set_min_mireds(MIRED_MIN);
  149. traits.set_max_mireds(MIRED_MAX);
  150. return traits;
  151. }
  152. /// Applies a requested light state to the physicial GPIO outputs.
  153. void write_state(light::LightState *state)
  154. {
  155. auto values = state->current_values;
  156. // Power down the light when its state is 'off'.
  157. if (values.get_state() == 0)
  158. {
  159. red_->set_level(1.0f);
  160. green_->set_level(1.0f);
  161. blue_->set_level(1.0f);
  162. white_->set_level(0.0f);
  163. master2_->turn_off();
  164. master1_->turn_off();
  165. #ifdef TRANSITION_TO_OFF_BUGFIX
  166. previous_state_ = -1;
  167. previous_brightness_ = 0;
  168. #endif
  169. return;
  170. }
  171. if (transition_handler_->set_light_color_values(values)) {
  172. master2_->turn_on();
  173. master1_->turn_on();
  174. red_->set_level(transition_handler_->red);
  175. green_->set_level(transition_handler_->green);
  176. blue_->set_level(transition_handler_->blue);
  177. white_->set_level(transition_handler_->white);
  178. return;
  179. }
  180. #ifdef TRANSITION_TO_OFF_BUGFIX
  181. // Remember the brightness that is used when the light is fully ON.
  182. auto brightness = values.get_brightness();
  183. if (values.get_state() == 1) {
  184. previous_brightness_ = brightness;
  185. }
  186. // When transitioning towards zero brightness ...
  187. else if (values.get_state() < previous_state_) {
  188. // ... check if the prevous brightness is the same as the current
  189. // brightness. If yes, then the brightness isn't being scaled ...
  190. if (previous_brightness_ == brightness) {
  191. // ... and we need to do that ourselves.
  192. brightness = values.get_state() * brightness;
  193. }
  194. }
  195. previous_state_ = values.get_state();
  196. #endif
  197. instant_handler_->set_light_color_values(values);
  198. master2_->turn_on();
  199. master1_->turn_on();
  200. red_->set_level(instant_handler_->red);
  201. green_->set_level(instant_handler_->green);
  202. blue_->set_level(instant_handler_->blue);
  203. white_->set_level(instant_handler_->white);
  204. }
  205. protected:
  206. ledc::LEDCOutput *red_;
  207. ledc::LEDCOutput *green_;
  208. ledc::LEDCOutput *blue_;
  209. ledc::LEDCOutput *white_;
  210. esphome::gpio::GPIOBinaryOutput *master1_;
  211. esphome::gpio::GPIOBinaryOutput *master2_;
  212. TransitionHandler *transition_handler_;
  213. ColorTranslator *instant_handler_ = new ColorTranslator();
  214. #ifdef TRANSITION_TO_OFF_BUGFIX
  215. float previous_state_ = 1;
  216. float previous_brightness_ = -1;
  217. #endif
  218. friend class YeelightBS2LightState;
  219. /// Called by the YeelightBS2LightState class, to set the object that
  220. /// can be used to access protected data from the light state object.
  221. void set_light_state_data_exposer(LightStateDataExposer *exposer) {
  222. transition_handler_ = new TransitionHandler(exposer);
  223. }
  224. };
  225. class YeelightBS2LightState : public light::LightState, public LightStateDataExposer
  226. {
  227. public:
  228. YeelightBS2LightState(const std::string &name, YeelightBS2LightOutput *output) : light::LightState(name, output) {
  229. output->set_light_state_data_exposer(this);
  230. }
  231. bool has_active_transformer() {
  232. return this->transformer_ != nullptr;
  233. }
  234. bool transformer_is_transition() {
  235. return this->transformer_->is_transition();
  236. }
  237. light::LightColorValues get_transformer_values() {
  238. return this->transformer_->get_values();
  239. }
  240. light::LightColorValues get_transformer_end_values() {
  241. return this->transformer_->get_end_values();
  242. }
  243. float get_transformer_progress() {
  244. return this->transformer_->get_progress();
  245. }
  246. };
  247. } // namespace bs2
  248. } // namespace yeelight
  249. } // namespace esphome