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.

336 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 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. /// This class will take care of transitioning LEDC light outputs
  44. /// instead, which is what the original firmware in the device does as
  45. /// well. This makes transitions a lot cleaner.
  46. class TransitionHandler {
  47. public:
  48. TransitionHandler(LightStateDataExposer *exposer) : exposer_(exposer) {}
  49. bool handle() {
  50. if (!do_handle_()) {
  51. active_ = false;
  52. return false;
  53. }
  54. if (is_fresh_transition_()) {
  55. auto start = exposer_->get_transformer_values();
  56. auto end = exposer_->get_transformer_end_values();
  57. active_ = true;
  58. }
  59. return true;
  60. }
  61. protected:
  62. LightStateDataExposer *exposer_;
  63. bool active_ = false;
  64. DutyCycles start_;
  65. DutyCycles end_;
  66. /// Checks if this class will handle the light output logic.
  67. /// This is the case when a transformer is active and this
  68. /// transformer does implement a transitioning effect.
  69. bool do_handle_() {
  70. if (!exposer_->has_active_transformer())
  71. return false;
  72. if (!exposer_->transformer_is_transition())
  73. return false;
  74. return true;
  75. }
  76. /// Checks if a fresh transitioning is started.
  77. /// A transitioning is fresh when either no transition is known to
  78. /// be in progress or when a new end state is found during an
  79. /// ongoing transition.
  80. bool is_fresh_transition_() {
  81. bool is_fresh = false;
  82. if (active_ == false) {
  83. is_fresh = true;
  84. }
  85. return is_fresh;
  86. }
  87. };
  88. /// An implementation of the LightOutput interface for the Yeelight
  89. /// Bedside Lamp 2.
  90. class YeelightBS2LightOutput : public Component, public light::LightOutput {
  91. public:
  92. light::LightTraits get_traits() override
  93. {
  94. auto traits = light::LightTraits();
  95. traits.set_supports_rgb(true);
  96. traits.set_supports_color_temperature(true);
  97. traits.set_supports_brightness(true);
  98. traits.set_supports_rgb_white_value(false);
  99. traits.set_supports_color_interlock(true);
  100. traits.set_min_mireds(MIRED_MIN);
  101. traits.set_max_mireds(MIRED_MAX);
  102. return traits;
  103. }
  104. /// Set the LEDC output for the red LED circuitry channel.
  105. void set_red_output(ledc::LEDCOutput *red) {
  106. red_ = red;
  107. }
  108. /// Set the LEDC output for the green LED circuitry channel.
  109. void set_green_output(ledc::LEDCOutput *green) {
  110. green_ = green;
  111. }
  112. /// Set the LEDC output for the blue LED circuitry channel.
  113. void set_blue_output(ledc::LEDCOutput *blue) {
  114. blue_ = blue;
  115. }
  116. /// Set the LEDC output for the white LED circuitry channel.
  117. void set_white_output(ledc::LEDCOutput *white) {
  118. white_ = white;
  119. }
  120. /// Set the first GPIO binary output, used as internal master
  121. /// switch for the LED light circuitry.
  122. void set_master1_output(gpio::GPIOBinaryOutput *master1) {
  123. master1_ = master1;
  124. }
  125. /// Set the second GPIO binary output, used as internal master
  126. /// switch for the LED light circuitry.
  127. void set_master2_output(gpio::GPIOBinaryOutput *master2) {
  128. master2_ = master2;
  129. }
  130. void write_state(light::LightState *state)
  131. {
  132. if (transition_->handle()) {
  133. ESP_LOGD(TAG, "HANDLE transition!");
  134. }
  135. auto values = state->current_values;
  136. // Power down the light when its state is 'off'.
  137. if (values.get_state() == 0)
  138. {
  139. turn_off_();
  140. #ifdef TRANSITION_TO_OFF_BUGFIX
  141. previous_state_ = -1;
  142. previous_brightness_ = 0;
  143. #endif
  144. return;
  145. }
  146. auto brightness = values.get_brightness();
  147. #ifdef TRANSITION_TO_OFF_BUGFIX
  148. // Remember the brightness that is used when the light is fully ON.
  149. if (values.get_state() == 1) {
  150. previous_brightness_ = brightness;
  151. }
  152. // When transitioning towards zero brightness ...
  153. else if (values.get_state() < previous_state_) {
  154. // ... check if the prevous brightness is the same as the current
  155. // brightness. If yes, then the brightness isn't being scaled ...
  156. if (previous_brightness_ == brightness) {
  157. // ... and we need to do that ourselves.
  158. brightness = values.get_state() * brightness;
  159. }
  160. }
  161. previous_state_ = values.get_state();
  162. #endif
  163. // At the lowest brightness setting, switch to night light mode.
  164. // In the Yeelight integration in Home Assistant, this feature is
  165. // exposed trough a separate switch. I have found that the switch
  166. // is both confusing and made me run into issues when automating
  167. // the lights.
  168. // I don't simply check for a brightness at or below 0.01 (1%),
  169. // because the lowest brightness setting from Home Assistant
  170. // turns up as 0.011765 in here (which is 3/255).
  171. if (brightness < 0.012f && values.get_state() == 1) {
  172. turn_on_in_night_light_mode_();
  173. return;
  174. }
  175. // Leave it to the default tooling to figure out the basics.
  176. // Because of color interlocking, there are two possible outcomes:
  177. // - red, green, blue zero -> white light color temperature mode
  178. // - cwhite, wwhite zero -> RGB mode
  179. float red, green, blue, cwhite, wwhite;
  180. state->current_values_as_rgbww(
  181. &red, &green, &blue, &cwhite, &wwhite, true, false);
  182. if (cwhite > 0 || wwhite > 0) {
  183. turn_on_in_white_mode_(
  184. values.get_color_temperature(), brightness);
  185. }
  186. else {
  187. turn_on_in_rgb_mode_(
  188. values.get_red(), values.get_green(), values.get_blue(),
  189. brightness, values.get_state());
  190. }
  191. }
  192. protected:
  193. ledc::LEDCOutput *red_;
  194. ledc::LEDCOutput *green_;
  195. ledc::LEDCOutput *blue_;
  196. ledc::LEDCOutput *white_;
  197. esphome::gpio::GPIOBinaryOutput *master1_;
  198. esphome::gpio::GPIOBinaryOutput *master2_;
  199. ColorWhiteLight white_light_;
  200. ColorRGBLight rgb_light_;
  201. ColorNightLight night_light_;
  202. TransitionHandler *transition_;
  203. #ifdef TRANSITION_TO_OFF_BUGFIX
  204. float previous_state_ = 1;
  205. float previous_brightness_ = -1;
  206. #endif
  207. friend class YeelightBS2LightState;
  208. /// Called by the YeelightBS2LightState class, to set the object that
  209. /// can be used to access protected data from the light state object.
  210. void set_light_state_data_exposer(LightStateDataExposer *exposer) {
  211. transition_ = new TransitionHandler(exposer);
  212. }
  213. void turn_off_()
  214. {
  215. red_->set_level(1);
  216. green_->set_level(1);
  217. blue_->set_level(1);
  218. white_->set_level(0);
  219. master2_->turn_off();
  220. master1_->turn_off();
  221. }
  222. void turn_on_in_night_light_mode_()
  223. {
  224. ESP_LOGD(TAG, "Activate Night light feature");
  225. night_light_.set_color(1, 1, 1, 0.01, 1);
  226. ESP_LOGD(TAG, "New LED state : RGBW %f, %f, %f, %f", night_light_.red, night_light_.green, night_light_.blue, night_light_.white);
  227. // Drive the LEDs.
  228. master2_->turn_on();
  229. master1_->turn_on();
  230. red_->set_level(night_light_.red);
  231. green_->set_level(night_light_.green);
  232. blue_->set_level(night_light_.blue);
  233. white_->set_level(night_light_.white);
  234. }
  235. void turn_on_in_rgb_mode_(float red, float green, float blue, float brightness, float state)
  236. {
  237. ESP_LOGD(TAG, "Activate RGB %f, %f, %f, BRIGHTNESS %f", red, green, blue, brightness);
  238. rgb_light_.set_color(red, green, blue, brightness, state);
  239. ESP_LOGD(TAG, "New LED state : RGBW %f, %f, %f, off", rgb_light_.red, rgb_light_.green, rgb_light_.blue);
  240. // Drive the LEDs.
  241. master2_->turn_on();
  242. master1_->turn_on();
  243. red_->set_level(rgb_light_.red);
  244. green_->set_level(rgb_light_.green);
  245. blue_->set_level(rgb_light_.blue);
  246. white_->turn_off();
  247. }
  248. void turn_on_in_white_mode_(float temperature, float brightness)
  249. {
  250. ESP_LOGD(TAG, "Activate TEMPERATURE %f, BRIGHTNESS %f",
  251. temperature, brightness);
  252. white_light_.set_color(temperature, brightness);
  253. ESP_LOGD(TAG, "New LED state : RGBW %f, %f, %f, %f",
  254. white_light_.red, white_light_.green, white_light_.blue,
  255. white_light_.white);
  256. master2_->turn_on();
  257. master1_->turn_on();
  258. red_->set_level(white_light_.red);
  259. green_->set_level(white_light_.green);
  260. blue_->set_level(white_light_.blue);
  261. white_->set_level(white_light_.white);
  262. }
  263. };
  264. class YeelightBS2LightState : public light::LightState, public LightStateDataExposer
  265. {
  266. public:
  267. YeelightBS2LightState(const std::string &name, YeelightBS2LightOutput *output) : light::LightState(name, output) {
  268. output->set_light_state_data_exposer(this);
  269. }
  270. bool has_active_transformer() {
  271. return this->transformer_ != nullptr;
  272. }
  273. bool transformer_is_transition() {
  274. return this->transformer_->is_transition();
  275. }
  276. light::LightColorValues get_transformer_values() {
  277. return this->transformer_->get_values();
  278. }
  279. light::LightColorValues get_transformer_end_values() {
  280. return this->transformer_->get_end_values();
  281. }
  282. float get_transformer_progress() {
  283. return this->transformer_->get_progress();
  284. }
  285. };
  286. } // namespace bs2
  287. } // namespace yeelight
  288. } // namespace esphome