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.

324 lines
12 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 YeelightBS2LightOutput
  23. /// class.
  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 translates LightColorValues into GPIO duty cycles
  36. /// for representing a requested light color.
  37. class ColorTranslator : public DutyCycles {
  38. public:
  39. void set_light_color_values(light::LightColorValues values) {
  40. // The light is turned off.
  41. if (values.get_state() == 0.0f || values.get_brightness() == 0.0f) {
  42. red = 0.0f;
  43. green = 0.0f;
  44. blue = 0.0f;
  45. white = 0.0f;
  46. return;
  47. }
  48. // At the lowest brightness setting, switch to night light mode.
  49. // In the Yeelight integration in Home Assistant, this feature is
  50. // exposed trough a separate switch. I have found that the switch
  51. // is both confusing and made me run into issues when automating
  52. // the lights.
  53. // I don't simply check for a brightness at or below 0.01 (1%),
  54. // because the lowest brightness setting from Home Assistant
  55. // turns up as 0.011765 in here (which is 3/255).
  56. if (values.get_brightness() < 0.012f) {
  57. // TODO make the color implementations return a DutyCycles object?
  58. // TODO Use polymorphic color classes?
  59. red = night_light_.red;
  60. green = night_light_.green;
  61. blue = night_light_.blue;
  62. white = night_light_.white;
  63. return;
  64. }
  65. // White light mode: temperature + brightness.
  66. if (values.get_white() > 0.0f) {
  67. auto temperature = values.get_color_temperature();
  68. white_light_.set_color(temperature, values.get_brightness());
  69. red = white_light_.red;
  70. green = white_light_.green;
  71. blue = white_light_.blue;
  72. white = white_light_.white;
  73. return;
  74. }
  75. // RGB color mode: red, green, blue + brightness.
  76. rgb_light_.set_color(
  77. values.get_red(), values.get_green(), values.get_blue(),
  78. values.get_brightness());
  79. red = rgb_light_.red;
  80. green = rgb_light_.green;
  81. blue = rgb_light_.blue;
  82. white = rgb_light_.white;
  83. }
  84. protected:
  85. ColorWhiteLight white_light_;
  86. ColorRGBLight rgb_light_;
  87. ColorNightLight night_light_;
  88. };
  89. /// This class is used to handle color transition requirements.
  90. ///
  91. /// When using the default ESPHome logic, transitioning is done by
  92. /// transitioning all light properties linearly from the original
  93. /// values to the new values, and letting the light output object
  94. /// translate these properties into light outputs on every step of the
  95. /// way. While this does work, it does not work nicely.
  96. ///
  97. /// For example, when transitioning from warm to cold white light,
  98. /// the color temperature would be transitioned from the old value to
  99. /// the new value. While doing so, the transition hits the middle
  100. /// white light setting, which shows up as a bright flash in the
  101. /// middle of the transition. The original firmware however, shows a
  102. /// smooth transition from warm to cold white light, without any flash.
  103. ///
  104. /// This class handles transitions by not varying the light properties
  105. /// over time, but by transitioning the LEDC duty cycle output levels
  106. /// over time. This matches the behavior of the original firmware.
  107. class TransitionHandler {
  108. public:
  109. TransitionHandler(LightStateDataExposer *exposer) : exposer_(exposer) {}
  110. bool handle() {
  111. if (!do_handle_()) {
  112. active_ = false;
  113. return false;
  114. }
  115. if (is_fresh_transition_()) {
  116. auto start = exposer_->get_transformer_values();
  117. auto end = exposer_->get_transformer_end_values();
  118. active_ = true;
  119. }
  120. return true;
  121. }
  122. protected:
  123. LightStateDataExposer *exposer_;
  124. bool active_ = false;
  125. DutyCycles start_;
  126. DutyCycles end_;
  127. /// Checks if this class will handle the light output logic.
  128. /// This is the case when a transformer is active and this
  129. /// transformer does implement a transitioning effect.
  130. bool do_handle_() {
  131. if (!exposer_->has_active_transformer())
  132. return false;
  133. if (!exposer_->transformer_is_transition())
  134. return false;
  135. return true;
  136. }
  137. /// Checks if a fresh transitioning is started.
  138. /// A transitioning is fresh when either no transition is known to
  139. /// be in progress or when a new end state is found during an
  140. /// ongoing transition.
  141. bool is_fresh_transition_() {
  142. bool is_fresh = false;
  143. if (active_ == false) {
  144. is_fresh = true;
  145. }
  146. return is_fresh;
  147. }
  148. };
  149. /// An implementation of the LightOutput interface for the Yeelight
  150. /// Bedside Lamp 2. The function of this class is to translate a
  151. /// required light state into actual physicial GPIO output signals
  152. /// to drive the device's LED circuitry.
  153. class YeelightBS2LightOutput : public Component, public light::LightOutput {
  154. public:
  155. /// Set the LEDC output for the red LED circuitry channel.
  156. void set_red_output(ledc::LEDCOutput *red) {
  157. red_ = red;
  158. }
  159. /// Set the LEDC output for the green LED circuitry channel.
  160. void set_green_output(ledc::LEDCOutput *green) {
  161. green_ = green;
  162. }
  163. /// Set the LEDC output for the blue LED circuitry channel.
  164. void set_blue_output(ledc::LEDCOutput *blue) {
  165. blue_ = blue;
  166. }
  167. /// Set the LEDC output for the white LED circuitry channel.
  168. void set_white_output(ledc::LEDCOutput *white) {
  169. white_ = white;
  170. }
  171. /// Set the first GPIO binary output, used as internal master
  172. /// switch for the LED light circuitry.
  173. void set_master1_output(gpio::GPIOBinaryOutput *master1) {
  174. master1_ = master1;
  175. }
  176. /// Set the second GPIO binary output, used as internal master
  177. /// switch for the LED light circuitry.
  178. void set_master2_output(gpio::GPIOBinaryOutput *master2) {
  179. master2_ = master2;
  180. }
  181. /// Returns a LightTraits object, which is used to explain to the
  182. /// outside world (e.g. Home Assistant) what features are supported
  183. /// by this device.
  184. light::LightTraits get_traits() override
  185. {
  186. auto traits = light::LightTraits();
  187. traits.set_supports_rgb(true);
  188. traits.set_supports_color_temperature(true);
  189. traits.set_supports_brightness(true);
  190. traits.set_supports_rgb_white_value(false);
  191. traits.set_supports_color_interlock(true);
  192. traits.set_min_mireds(MIRED_MIN);
  193. traits.set_max_mireds(MIRED_MAX);
  194. return traits;
  195. }
  196. /// Tranlates a requested light state into physicial GPIO outputs.
  197. void write_state(light::LightState *state)
  198. {
  199. auto values = state->current_values;
  200. // Power down the light when its state is 'off'.
  201. if (values.get_state() == 0)
  202. {
  203. red_->set_level(1.0f);
  204. green_->set_level(1.0f);
  205. blue_->set_level(1.0f);
  206. white_->set_level(0.0f);
  207. master2_->turn_off();
  208. master1_->turn_off();
  209. #ifdef TRANSITION_TO_OFF_BUGFIX
  210. previous_state_ = -1;
  211. previous_brightness_ = 0;
  212. #endif
  213. return;
  214. }
  215. if (transition_->handle()) {
  216. ESP_LOGD(TAG, "HANDLE transition!");
  217. }
  218. #ifdef TRANSITION_TO_OFF_BUGFIX
  219. // Remember the brightness that is used when the light is fully ON.
  220. auto brightness = values.get_brightness();
  221. if (values.get_state() == 1) {
  222. previous_brightness_ = brightness;
  223. }
  224. // When transitioning towards zero brightness ...
  225. else if (values.get_state() < previous_state_) {
  226. // ... check if the prevous brightness is the same as the current
  227. // brightness. If yes, then the brightness isn't being scaled ...
  228. if (previous_brightness_ == brightness) {
  229. // ... and we need to do that ourselves.
  230. brightness = values.get_state() * brightness;
  231. }
  232. }
  233. previous_state_ = values.get_state();
  234. #endif
  235. duty_cycles_.set_light_color_values(values);
  236. master2_->turn_on();
  237. master1_->turn_on();
  238. red_->set_level(duty_cycles_.red);
  239. green_->set_level(duty_cycles_.green);
  240. blue_->set_level(duty_cycles_.blue);
  241. white_->set_level(duty_cycles_.white);
  242. }
  243. protected:
  244. ledc::LEDCOutput *red_;
  245. ledc::LEDCOutput *green_;
  246. ledc::LEDCOutput *blue_;
  247. ledc::LEDCOutput *white_;
  248. esphome::gpio::GPIOBinaryOutput *master1_;
  249. esphome::gpio::GPIOBinaryOutput *master2_;
  250. TransitionHandler *transition_;
  251. ColorTranslator duty_cycles_; // TODO move to own class DefaultHandler
  252. #ifdef TRANSITION_TO_OFF_BUGFIX
  253. float previous_state_ = 1;
  254. float previous_brightness_ = -1;
  255. #endif
  256. friend class YeelightBS2LightState;
  257. /// Called by the YeelightBS2LightState class, to set the object that
  258. /// can be used to access protected data from the light state object.
  259. void set_light_state_data_exposer(LightStateDataExposer *exposer) {
  260. transition_ = new TransitionHandler(exposer);
  261. }
  262. };
  263. class YeelightBS2LightState : public light::LightState, public LightStateDataExposer
  264. {
  265. public:
  266. YeelightBS2LightState(const std::string &name, YeelightBS2LightOutput *output) : light::LightState(name, output) {
  267. output->set_light_state_data_exposer(this);
  268. }
  269. bool has_active_transformer() {
  270. return this->transformer_ != nullptr;
  271. }
  272. bool transformer_is_transition() {
  273. return this->transformer_->is_transition();
  274. }
  275. light::LightColorValues get_transformer_values() {
  276. return this->transformer_->get_values();
  277. }
  278. light::LightColorValues get_transformer_end_values() {
  279. return this->transformer_->get_end_values();
  280. }
  281. float get_transformer_progress() {
  282. return this->transformer_->get_progress();
  283. }
  284. };
  285. } // namespace bs2
  286. } // namespace yeelight
  287. } // namespace esphome