Fork of the espurna firmware for `mhsw` switches
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.

1299 lines
39 KiB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
  1. /*
  2. LIGHT MODULE
  3. Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
  4. */
  5. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  6. #include <Ticker.h>
  7. #include <ArduinoJson.h>
  8. #include <vector>
  9. extern "C" {
  10. #include "libs/fs_math.h"
  11. }
  12. #if LIGHT_PROVIDER == LIGHT_PROVIDER_DIMMER
  13. #define PWM_CHANNEL_NUM_MAX LIGHT_CHANNELS
  14. extern "C" {
  15. #include "libs/pwm.h"
  16. }
  17. #endif
  18. // -----------------------------------------------------------------------------
  19. Ticker _light_comms_ticker;
  20. Ticker _light_save_ticker;
  21. Ticker _light_transition_ticker;
  22. struct channel_t {
  23. unsigned char pin; // real GPIO pin
  24. bool reverse; // wether we should invert the value before using it
  25. bool state; // is the channel ON
  26. unsigned char inputValue; // raw value, without the brightness
  27. unsigned char value; // normalized value, including brightness
  28. unsigned char target; // target value
  29. double current; // transition value
  30. };
  31. std::vector<channel_t> _light_channel;
  32. bool _light_state = false;
  33. bool _light_use_transitions = false;
  34. unsigned int _light_transition_time = LIGHT_TRANSITION_TIME;
  35. bool _light_has_color = false;
  36. bool _light_use_white = false;
  37. bool _light_use_cct = false;
  38. bool _light_use_gamma = false;
  39. unsigned long _light_steps_left = 1;
  40. unsigned char _light_brightness = LIGHT_MAX_BRIGHTNESS;
  41. unsigned int _light_mireds = round((LIGHT_COLDWHITE_MIRED+LIGHT_WARMWHITE_MIRED)/2);
  42. #if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX
  43. #include <my92xx.h>
  44. my92xx * _my92xx;
  45. ARRAYINIT(unsigned char, _light_channel_map, MY92XX_MAPPING);
  46. #endif
  47. // UI hint about channel distribution
  48. const char _light_channel_desc[5][5] PROGMEM = {
  49. {'W', 0, 0, 0, 0},
  50. {'W', 'C', 0, 0, 0},
  51. {'R', 'G', 'B', 0, 0},
  52. {'R', 'G', 'B', 'W', 0},
  53. {'R', 'G', 'B', 'W', 'C'}
  54. };
  55. // Gamma Correction lookup table (8 bit)
  56. // TODO: move to PROGMEM
  57. const unsigned char _light_gamma_table[] = {
  58. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  59. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2,
  60. 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6,
  61. 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 11, 11, 11,
  62. 12, 12, 13, 13, 14, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19,
  63. 19, 20, 20, 21, 22, 22, 23, 23, 24, 25, 25, 26, 26, 27, 28, 28,
  64. 29, 30, 30, 31, 32, 33, 33, 34, 35, 35, 36, 37, 38, 39, 39, 40,
  65. 41, 42, 43, 43, 44, 45, 46, 47, 48, 49, 50, 50, 51, 52, 53, 54,
  66. 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 71,
  67. 72, 73, 74, 75, 76, 77, 78, 80, 81, 82, 83, 84, 86, 87, 88, 89,
  68. 91, 92, 93, 94, 96, 97, 98, 100, 101, 102, 104, 105, 106, 108, 109, 110,
  69. 112, 113, 115, 116, 118, 119, 121, 122, 123, 125, 126, 128, 130, 131, 133, 134,
  70. 136, 137, 139, 140, 142, 144, 145, 147, 149, 150, 152, 154, 155, 157, 159, 160,
  71. 162, 164, 166, 167, 169, 171, 173, 175, 176, 178, 180, 182, 184, 186, 187, 189,
  72. 191, 193, 195, 197, 199, 201, 203, 205, 207, 209, 211, 213, 215, 217, 219, 221,
  73. 223, 225, 227, 229, 231, 233, 235, 238, 240, 242, 244, 246, 248, 251, 253, 255
  74. };
  75. // -----------------------------------------------------------------------------
  76. // UTILS
  77. // -----------------------------------------------------------------------------
  78. void _setRGBInputValue(unsigned char red, unsigned char green, unsigned char blue) {
  79. _light_channel[0].inputValue = constrain(red, 0, LIGHT_MAX_VALUE);
  80. _light_channel[1].inputValue = constrain(green, 0, LIGHT_MAX_VALUE);;
  81. _light_channel[2].inputValue = constrain(blue, 0, LIGHT_MAX_VALUE);;
  82. }
  83. void _setCCTInputValue(unsigned char warm, unsigned char cold) {
  84. _light_channel[0].inputValue = constrain(warm, 0, LIGHT_MAX_VALUE);
  85. _light_channel[1].inputValue = constrain(cold, 0, LIGHT_MAX_VALUE);
  86. }
  87. void _generateBrightness() {
  88. double brightness = (double) _light_brightness / LIGHT_MAX_BRIGHTNESS;
  89. // Convert RGB to RGBW(W)
  90. if (_light_has_color && _light_use_white) {
  91. // Substract the common part from RGB channels and add it to white channel. So [250,150,50] -> [200,100,0,50]
  92. unsigned char white = std::min(_light_channel[0].inputValue, std::min(_light_channel[1].inputValue, _light_channel[2].inputValue));
  93. for (unsigned int i=0; i < 3; i++) {
  94. _light_channel[i].value = _light_channel[i].inputValue - white;
  95. }
  96. // Split the White Value across 2 White LED Strips.
  97. if (_light_use_cct) {
  98. // This change the range from 153-500 to 0-347 so we get a value between 0 and 1 in the end.
  99. double miredFactor = ((double) _light_mireds - (double) LIGHT_COLDWHITE_MIRED)/((double) LIGHT_WARMWHITE_MIRED - (double) LIGHT_COLDWHITE_MIRED);
  100. // set cold white
  101. _light_channel[3].inputValue = 0;
  102. _light_channel[3].value = round(((double) 1.0 - miredFactor) * white);
  103. // set warm white
  104. _light_channel[4].inputValue = 0;
  105. _light_channel[4].value = round(miredFactor * white);
  106. } else {
  107. _light_channel[3].inputValue = 0;
  108. _light_channel[3].value = white;
  109. }
  110. // Scale up to equal input values. So [250,150,50] -> [200,100,0,50] -> [250, 125, 0, 63]
  111. unsigned char max_in = std::max(_light_channel[0].inputValue, std::max(_light_channel[1].inputValue, _light_channel[2].inputValue));
  112. unsigned char max_out = std::max(std::max(_light_channel[0].value, _light_channel[1].value), std::max(_light_channel[2].value, _light_channel[3].value));
  113. unsigned char channelSize = _light_use_cct ? 5 : 4;
  114. if (_light_use_cct) {
  115. max_out = std::max(max_out, _light_channel[4].value);
  116. }
  117. double factor = (max_out > 0) ? (double) (max_in / max_out) : 0;
  118. for (unsigned char i=0; i < channelSize; i++) {
  119. _light_channel[i].value = round((double) _light_channel[i].value * factor * brightness);
  120. }
  121. // Scale white channel to match brightness
  122. for (unsigned char i=3; i < channelSize; i++) {
  123. _light_channel[i].value = constrain(_light_channel[i].value * LIGHT_WHITE_FACTOR, 0, LIGHT_MAX_BRIGHTNESS);
  124. }
  125. // For the rest of channels, don't apply brightness, it is already in the inputValue
  126. // i should be 4 when RGBW and 5 when RGBWW
  127. for (unsigned char i=channelSize; i < _light_channel.size(); i++) {
  128. _light_channel[i].value = _light_channel[i].inputValue;
  129. }
  130. } else {
  131. // Apply brightness equally to all channels
  132. for (unsigned char i=0; i < _light_channel.size(); i++) {
  133. _light_channel[i].value = _light_channel[i].inputValue * brightness;
  134. }
  135. String lightDesc(unsigned char id) {
  136. if (id >= _light_channel.size()) return F("UNKNOWN");
  137. const char tag = pgm_read_byte(&_light_channel_desc[_light_channel.size() - 1][id]);
  138. switch (tag) {
  139. case 'W': return F("WARM WHITE");
  140. case 'C': return F("COLD WHITE");
  141. case 'R': return F("RED");
  142. case 'G': return F("GREEN");
  143. case 'B': return F("BLUE");
  144. default: break;
  145. }
  146. return F("UNKNOWN");
  147. }
  148. // -----------------------------------------------------------------------------
  149. // Input Values
  150. // -----------------------------------------------------------------------------
  151. void _fromLong(unsigned long value, bool brightness) {
  152. if (brightness) {
  153. _setRGBInputValue((value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF);
  154. _light_brightness = (value & 0xFF) * LIGHT_MAX_BRIGHTNESS / 255;
  155. } else {
  156. _setRGBInputValue((value >> 16) & 0xFF, (value >> 8) & 0xFF, (value) & 0xFF);
  157. }
  158. }
  159. void _fromRGB(const char * rgb) {
  160. char * p = (char *) rgb;
  161. if (strlen(p) == 0) return;
  162. switch (p[0]) {
  163. case '#': // HEX Value
  164. if (_light_has_color) {
  165. ++p;
  166. unsigned long value = strtoul(p, NULL, 16);
  167. // RGBA values are interpreted like RGB + brightness
  168. _fromLong(value, strlen(p) > 7);
  169. }
  170. break;
  171. case 'M': // Mired Value
  172. _fromMireds(atol(p + 1));
  173. break;
  174. case 'K': // Kelvin Value
  175. _fromKelvin(atol(p + 1));
  176. break;
  177. default: // assume decimal values separated by commas
  178. char * tok;
  179. unsigned char count = 0;
  180. unsigned char channels = _light_channel.size();
  181. tok = strtok(p, ",");
  182. while (tok != NULL) {
  183. _light_channel[count].inputValue = atoi(tok);
  184. if (++count == channels) break;
  185. tok = strtok(NULL, ",");
  186. }
  187. // RGB but less than 3 values received, assume it is 0
  188. if (_light_has_color && (count < 3)) {
  189. // check channel 1 and 2:
  190. for (int i = 1; i <= 2; i++) {
  191. if (count < (i+1)) {
  192. _light_channel[i].inputValue = 0;
  193. }
  194. }
  195. }
  196. break;
  197. }
  198. }
  199. // HSV string is expected to be "H,S,V", where:
  200. // 0 <= H <= 360
  201. // 0 <= S <= 100
  202. // 0 <= V <= 100
  203. void _fromHSV(const char * hsv) {
  204. char * ptr = (char *) hsv;
  205. if (strlen(ptr) == 0) return;
  206. if (!_light_has_color) return;
  207. char * tok;
  208. unsigned char count = 0;
  209. unsigned int value[3] = {0};
  210. tok = strtok(ptr, ",");
  211. while (tok != NULL) {
  212. value[count] = atoi(tok);
  213. if (++count == 3) break;
  214. tok = strtok(NULL, ",");
  215. }
  216. if (count != 3) return;
  217. // HSV to RGB transformation -----------------------------------------------
  218. //INPUT: [0,100,57]
  219. //IS: [145,0,0]
  220. //SHOULD: [255,0,0]
  221. double h = (value[0] == 360) ? 0 : (double) value[0] / 60.0;
  222. double f = (h - floor(h));
  223. double s = (double) value[1] / 100.0;
  224. _light_brightness = round((double) value[2] * 2.55); // (255/100)
  225. unsigned char p = round(255 * (1.0 - s));
  226. unsigned char q = round(255 * (1.0 - s * f));
  227. unsigned char t = round(255 * (1.0 - s * (1.0 - f)));
  228. switch (int(h)) {
  229. case 0:
  230. _setRGBInputValue(255, t, p);
  231. break;
  232. case 1:
  233. _setRGBInputValue(q, 255, p);
  234. break;
  235. case 2:
  236. _setRGBInputValue(p, 255, t);
  237. break;
  238. case 3:
  239. _setRGBInputValue(p, q, 255);
  240. break;
  241. case 4:
  242. _setRGBInputValue(t, p, 255);
  243. break;
  244. case 5:
  245. _setRGBInputValue(255, p, q);
  246. break;
  247. default:
  248. _setRGBInputValue(0, 0, 0);
  249. break;
  250. }
  251. }
  252. // Thanks to Sacha Telgenhof for sharing this code in his AiLight library
  253. // https://github.com/stelgenhof/AiLight
  254. void _fromKelvin(unsigned long kelvin) {
  255. if (!_light_has_color) {
  256. if(!_light_use_cct) return;
  257. _light_mireds = constrain(round(1000000UL / kelvin), LIGHT_MIN_MIREDS, LIGHT_MAX_MIREDS);
  258. // This change the range from 153-500 to 0-347 so we get a value between 0 and 1 in the end.
  259. double factor = ((double) _light_mireds - (double) LIGHT_COLDWHITE_MIRED)/((double) LIGHT_WARMWHITE_MIRED - (double) LIGHT_COLDWHITE_MIRED);
  260. unsigned char warm = round(factor * LIGHT_MAX_VALUE);
  261. unsigned char cold = round(((double) 1.0 - factor) * LIGHT_MAX_VALUE);
  262. _setCCTInputValue(warm, cold);
  263. return;
  264. }
  265. _light_mireds = constrain(round(1000000UL / kelvin), LIGHT_MIN_MIREDS, LIGHT_MAX_MIREDS);
  266. if (_light_use_cct) {
  267. _setRGBInputValue(LIGHT_MAX_VALUE, LIGHT_MAX_VALUE, LIGHT_MAX_VALUE);
  268. return;
  269. }
  270. // Calculate colors
  271. kelvin /= 100;
  272. unsigned int red = (kelvin <= 66)
  273. ? LIGHT_MAX_VALUE
  274. : 329.698727446 * fs_pow((double) (kelvin - 60), -0.1332047592);
  275. unsigned int green = (kelvin <= 66)
  276. ? 99.4708025861 * fs_log(kelvin) - 161.1195681661
  277. : 288.1221695283 * fs_pow((double) kelvin, -0.0755148492);
  278. unsigned int blue = (kelvin >= 66)
  279. ? LIGHT_MAX_VALUE
  280. : ((kelvin <= 19)
  281. ? 0
  282. : 138.5177312231 * fs_log(kelvin - 10) - 305.0447927307);
  283. _setRGBInputValue(red, green, blue);
  284. }
  285. // Color temperature is measured in mireds (kelvin = 1e6/mired)
  286. void _fromMireds(unsigned long mireds) {
  287. unsigned long kelvin = constrain(1000000UL / mireds, 1000, 40000);
  288. _fromKelvin(kelvin);
  289. }
  290. // -----------------------------------------------------------------------------
  291. // Output Values
  292. // -----------------------------------------------------------------------------
  293. void _toRGB(char * rgb, size_t len, bool target) {
  294. unsigned long value = 0;
  295. value += target ? _light_channel[0].target : _light_channel[0].inputValue;
  296. value <<= 8;
  297. value += target ? _light_channel[1].target : _light_channel[1].inputValue;
  298. value <<= 8;
  299. value += target ? _light_channel[2].target : _light_channel[2].inputValue;
  300. snprintf_P(rgb, len, PSTR("#%06X"), value);
  301. }
  302. void _toRGB(char * rgb, size_t len) {
  303. _toRGB(rgb, len, false);
  304. }
  305. void _toHSV(char * hsv, size_t len, bool target) {
  306. double h, s, v;
  307. double brightness = (double) _light_brightness / LIGHT_MAX_BRIGHTNESS;
  308. double r = (double) ((target ? _light_channel[0].target : _light_channel[0].inputValue) * brightness) / 255.0;
  309. double g = (double) ((target ? _light_channel[1].target : _light_channel[1].inputValue) * brightness) / 255.0;
  310. double b = (double) ((target ? _light_channel[2].target : _light_channel[2].inputValue) * brightness) / 255.0;
  311. double min = std::min(r, std::min(g, b));
  312. double max = std::max(r, std::max(g, b));
  313. v = 100.0 * max;
  314. if (v == 0) {
  315. h = s = 0;
  316. } else {
  317. s = 100.0 * (max - min) / max;
  318. if (s == 0) {
  319. h = 0;
  320. } else {
  321. if (max == r) {
  322. if (g >= b) {
  323. h = 0.0 + 60.0 * (g - b) / (max - min);
  324. } else {
  325. h = 360.0 + 60.0 * (g - b) / (max - min);
  326. }
  327. } else if (max == g) {
  328. h = 120.0 + 60.0 * (b - r) / (max - min);
  329. } else {
  330. h = 240.0 + 60.0 * (r - g) / (max - min);
  331. }
  332. }
  333. }
  334. // String
  335. snprintf_P(hsv, len, PSTR("%d,%d,%d"), round(h), round(s), round(v));
  336. }
  337. void _toHSV(char * hsv, size_t len) {
  338. _toHSV(hsv, len, false);
  339. }
  340. void _toLong(char * color, size_t len, bool target) {
  341. if (!_light_has_color) return;
  342. snprintf_P(color, len, PSTR("%d,%d,%d"),
  343. (int) (target ? _light_channel[0].target : _light_channel[0].inputValue),
  344. (int) (target ? _light_channel[1].target : _light_channel[1].inputValue),
  345. (int) (target ? _light_channel[2].target : _light_channel[2].inputValue)
  346. );
  347. }
  348. void _toLong(char * color, size_t len) {
  349. _toLong(color, len, false);
  350. }
  351. void _toCSV(char * buffer, size_t len, bool applyBrightness, bool target) {
  352. char num[10];
  353. float b = applyBrightness ? (float) _light_brightness / LIGHT_MAX_BRIGHTNESS : 1;
  354. for (unsigned char i=0; i<_light_channel.size(); i++) {
  355. itoa((target ? _light_channel[i].target : _light_channel[i].inputValue) * b, num, 10);
  356. if (i>0) strncat(buffer, ",", len--);
  357. strncat(buffer, num, len);
  358. len = len - strlen(num);
  359. }
  360. }
  361. void _toCSV(char * buffer, size_t len, bool applyBrightness) {
  362. _toCSV(buffer, len, applyBrightness, false);
  363. }
  364. // -----------------------------------------------------------------------------
  365. // PROVIDER
  366. // -----------------------------------------------------------------------------
  367. unsigned int _toPWM(unsigned long value, bool gamma, bool reverse) {
  368. value = constrain(value, 0, LIGHT_MAX_VALUE);
  369. if (gamma) value = _light_gamma_table[value];
  370. if (LIGHT_MAX_VALUE != LIGHT_LIMIT_PWM) value = map(value, 0, LIGHT_MAX_VALUE, 0, LIGHT_LIMIT_PWM);
  371. if (reverse) value = LIGHT_LIMIT_PWM - value;
  372. return value;
  373. }
  374. // Returns a PWM value for the given channel ID
  375. unsigned int _toPWM(unsigned char id) {
  376. bool useGamma = _light_use_gamma && _light_has_color && (id < 3);
  377. return _toPWM(_light_channel[id].current, useGamma, _light_channel[id].reverse);
  378. }
  379. void _transition() {
  380. // Update transition ticker
  381. _light_steps_left--;
  382. if (_light_steps_left == 0) _light_transition_ticker.detach();
  383. // Transitions
  384. for (unsigned int i=0; i < _light_channel.size(); i++) {
  385. if (_light_steps_left == 0) {
  386. _light_channel[i].current = _light_channel[i].target;
  387. } else {
  388. double difference = (double) (_light_channel[i].target - _light_channel[i].current) / (_light_steps_left + 1);
  389. _light_channel[i].current = _light_channel[i].current + difference;
  390. }
  391. }
  392. }
  393. void _lightProviderUpdate() {
  394. _transition();
  395. #if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX
  396. for (unsigned char i=0; i<_light_channel.size(); i++) {
  397. _my92xx->setChannel(_light_channel_map[i], _toPWM(i));
  398. }
  399. _my92xx->setState(true);
  400. _my92xx->update();
  401. #endif
  402. #if LIGHT_PROVIDER == LIGHT_PROVIDER_DIMMER
  403. for (unsigned int i=0; i < _light_channel.size(); i++) {
  404. pwm_set_duty(_toPWM(i), i);
  405. }
  406. pwm_start();
  407. #endif
  408. }
  409. // -----------------------------------------------------------------------------
  410. // PERSISTANCE
  411. // -----------------------------------------------------------------------------
  412. union light_rtcmem_t {
  413. struct {
  414. uint8_t channels[5];
  415. uint8_t brightness;
  416. uint16_t mired;
  417. } packed;
  418. uint64_t value;
  419. };
  420. #define LIGHT_RTCMEM_CHANNELS_MAX sizeof(light_rtcmem_t().packed.channels)
  421. void _lightSaveRtcmem() {
  422. if (lightChannels() > LIGHT_RTCMEM_CHANNELS_MAX) return;
  423. light_rtcmem_t light;
  424. for (unsigned int i=0; i < lightChannels(); i++) {
  425. light.packed.channels[i] = _light_channel[i].inputValue;
  426. }
  427. light.packed.brightness = _light_brightness;
  428. light.packed.mired = _light_mireds;
  429. Rtcmem->light = light.value;
  430. }
  431. void _lightRestoreRtcmem() {
  432. if (lightChannels() > LIGHT_RTCMEM_CHANNELS_MAX) return;
  433. light_rtcmem_t light;
  434. light.value = Rtcmem->light;
  435. for (unsigned int i=0; i < lightChannels(); i++) {
  436. _light_channel[i].inputValue = light.packed.channels[i];
  437. }
  438. _light_brightness = light.packed.brightness;
  439. _light_mireds = light.packed.mired;
  440. }
  441. void _lightSaveSettings() {
  442. for (unsigned int i=0; i < _light_channel.size(); i++) {
  443. setSetting("ch", i, _light_channel[i].inputValue);
  444. }
  445. setSetting("brightness", _light_brightness);
  446. setSetting("mireds", _light_mireds);
  447. saveSettings();
  448. }
  449. void _lightRestoreSettings() {
  450. for (unsigned int i=0; i < _light_channel.size(); i++) {
  451. _light_channel[i].inputValue = getSetting("ch", i, i==0 ? 255 : 0).toInt();
  452. }
  453. _light_brightness = getSetting("brightness", LIGHT_MAX_BRIGHTNESS).toInt();
  454. _light_mireds = getSetting("mireds", _light_mireds).toInt();
  455. lightUpdate(false, false);
  456. }
  457. // -----------------------------------------------------------------------------
  458. // MQTT
  459. // -----------------------------------------------------------------------------
  460. #if MQTT_SUPPORT
  461. void _lightMQTTCallback(unsigned int type, const char * topic, const char * payload) {
  462. String mqtt_group_color = getSetting("mqttGroupColor");
  463. if (type == MQTT_CONNECT_EVENT) {
  464. mqttSubscribe(MQTT_TOPIC_BRIGHTNESS);
  465. if (_light_has_color) {
  466. mqttSubscribe(MQTT_TOPIC_COLOR_RGB);
  467. mqttSubscribe(MQTT_TOPIC_COLOR_HSV);
  468. mqttSubscribe(MQTT_TOPIC_TRANSITION);
  469. }
  470. if (_light_has_color || _light_use_cct) {
  471. mqttSubscribe(MQTT_TOPIC_MIRED);
  472. mqttSubscribe(MQTT_TOPIC_KELVIN);
  473. }
  474. // Group color
  475. if (mqtt_group_color.length() > 0) mqttSubscribeRaw(mqtt_group_color.c_str());
  476. // Channels
  477. char buffer[strlen(MQTT_TOPIC_CHANNEL) + 3];
  478. snprintf_P(buffer, sizeof(buffer), PSTR("%s/+"), MQTT_TOPIC_CHANNEL);
  479. mqttSubscribe(buffer);
  480. }
  481. if (type == MQTT_MESSAGE_EVENT) {
  482. // Group color
  483. if ((mqtt_group_color.length() > 0) & (mqtt_group_color.equals(topic))) {
  484. lightColor(payload, true);
  485. lightUpdate(true, mqttForward(), false);
  486. return;
  487. }
  488. // Match topic
  489. String t = mqttMagnitude((char *) topic);
  490. // Color temperature in mireds
  491. if (t.equals(MQTT_TOPIC_MIRED)) {
  492. _fromMireds(atol(payload));
  493. lightUpdate(true, mqttForward());
  494. return;
  495. }
  496. // Color temperature in kelvins
  497. if (t.equals(MQTT_TOPIC_KELVIN)) {
  498. _fromKelvin(atol(payload));
  499. lightUpdate(true, mqttForward());
  500. return;
  501. }
  502. // Color
  503. if (t.equals(MQTT_TOPIC_COLOR_RGB)) {
  504. lightColor(payload, true);
  505. lightUpdate(true, mqttForward());
  506. return;
  507. }
  508. if (t.equals(MQTT_TOPIC_COLOR_HSV)) {
  509. lightColor(payload, false);
  510. lightUpdate(true, mqttForward());
  511. return;
  512. }
  513. // Brightness
  514. if (t.equals(MQTT_TOPIC_BRIGHTNESS)) {
  515. _light_brightness = constrain(atoi(payload), 0, LIGHT_MAX_BRIGHTNESS);
  516. lightUpdate(true, mqttForward());
  517. return;
  518. }
  519. // Transitions
  520. if (t.equals(MQTT_TOPIC_TRANSITION)) {
  521. lightTransitionTime(atol(payload));
  522. return;
  523. }
  524. // Channel
  525. if (t.startsWith(MQTT_TOPIC_CHANNEL)) {
  526. unsigned int channelID = t.substring(strlen(MQTT_TOPIC_CHANNEL)+1).toInt();
  527. if (channelID >= _light_channel.size()) {
  528. DEBUG_MSG_P(PSTR("[LIGHT] Wrong channelID (%d)\n"), channelID);
  529. return;
  530. }
  531. lightChannel(channelID, atoi(payload));
  532. lightUpdate(true, mqttForward());
  533. return;
  534. }
  535. }
  536. }
  537. void lightMQTT() {
  538. char buffer[20];
  539. if (_light_has_color) {
  540. // Color
  541. if (getSetting("useCSS", LIGHT_USE_CSS).toInt() == 1) {
  542. _toRGB(buffer, sizeof(buffer), true);
  543. } else {
  544. _toLong(buffer, sizeof(buffer), true);
  545. }
  546. mqttSend(MQTT_TOPIC_COLOR_RGB, buffer);
  547. _toHSV(buffer, sizeof(buffer), true);
  548. mqttSend(MQTT_TOPIC_COLOR_HSV, buffer);
  549. }
  550. if (_light_has_color || _light_use_cct) {
  551. // Mireds
  552. snprintf_P(buffer, sizeof(buffer), PSTR("%d"), _light_mireds);
  553. mqttSend(MQTT_TOPIC_MIRED, buffer);
  554. }
  555. // Channels
  556. for (unsigned int i=0; i < _light_channel.size(); i++) {
  557. itoa(_light_channel[i].target, buffer, 10);
  558. mqttSend(MQTT_TOPIC_CHANNEL, i, buffer);
  559. }
  560. // Brightness
  561. snprintf_P(buffer, sizeof(buffer), PSTR("%d"), _light_brightness);
  562. mqttSend(MQTT_TOPIC_BRIGHTNESS, buffer);
  563. }
  564. void lightMQTTGroup() {
  565. String mqtt_group_color = getSetting("mqttGroupColor");
  566. if (mqtt_group_color.length()>0) {
  567. char buffer[20];
  568. _toCSV(buffer, sizeof(buffer), true);
  569. mqttSendRaw(mqtt_group_color.c_str(), buffer);
  570. }
  571. }
  572. #endif
  573. // -----------------------------------------------------------------------------
  574. // Broker
  575. // -----------------------------------------------------------------------------
  576. #if BROKER_SUPPORT
  577. void lightBroker() {
  578. char buffer[10];
  579. for (unsigned int i=0; i < _light_channel.size(); i++) {
  580. itoa(_light_channel[i].inputValue, buffer, 10);
  581. brokerPublish(BROKER_MSG_TYPE_STATUS, MQTT_TOPIC_CHANNEL, i, buffer);
  582. }
  583. }
  584. #endif
  585. // -----------------------------------------------------------------------------
  586. // API
  587. // -----------------------------------------------------------------------------
  588. unsigned char lightChannels() {
  589. return _light_channel.size();
  590. }
  591. bool lightHasColor() {
  592. return _light_has_color;
  593. }
  594. bool lightUseCCT() {
  595. return _light_use_cct;
  596. }
  597. void _lightComms(unsigned char mask) {
  598. // Report color & brightness to MQTT broker
  599. #if MQTT_SUPPORT
  600. if (mask & 0x01) lightMQTT();
  601. if (mask & 0x02) lightMQTTGroup();
  602. #endif
  603. // Report color to WS clients (using current brightness setting)
  604. #if WEB_SUPPORT
  605. wsSend(_lightWebSocketStatus);
  606. #endif
  607. // Report channels to local broker
  608. #if BROKER_SUPPORT
  609. lightBroker();
  610. #endif
  611. }
  612. void lightUpdate(bool save, bool forward, bool group_forward) {
  613. _generateBrightness();
  614. // Update channels
  615. for (unsigned int i=0; i < _light_channel.size(); i++) {
  616. _light_channel[i].target = _light_state && _light_channel[i].state ? _light_channel[i].value : 0;
  617. //DEBUG_MSG_P("[LIGHT] Channel #%u target value: %u\n", i, _light_channel[i].target);
  618. }
  619. // Configure color transition
  620. _light_steps_left = _light_use_transitions ? _light_transition_time / LIGHT_TRANSITION_STEP : 1;
  621. _light_transition_ticker.attach_ms(LIGHT_TRANSITION_STEP, _lightProviderUpdate);
  622. // Delay every communication 100ms to avoid jamming
  623. unsigned char mask = 0;
  624. if (forward) mask += 1;
  625. if (group_forward) mask += 2;
  626. _light_comms_ticker.once_ms(LIGHT_COMMS_DELAY, _lightComms, mask);
  627. _lightSaveRtcmem();
  628. #if LIGHT_SAVE_ENABLED
  629. // Delay saving to EEPROM 5 seconds to avoid wearing it out unnecessarily
  630. if (save) _light_save_ticker.once(LIGHT_SAVE_DELAY, _lightSaveSettings);
  631. #endif
  632. };
  633. void lightUpdate(bool save, bool forward) {
  634. lightUpdate(save, forward, true);
  635. }
  636. #if LIGHT_SAVE_ENABLED == 0
  637. void lightSave() {
  638. _lightSaveSettings();
  639. }
  640. #endif
  641. void lightState(unsigned char i, bool state) {
  642. _light_channel[i].state = state;
  643. }
  644. bool lightState(unsigned char i) {
  645. return _light_channel[i].state;
  646. }
  647. void lightState(bool state) {
  648. _light_state = state;
  649. }
  650. bool lightState() {
  651. return _light_state;
  652. }
  653. void lightColor(const char * color, bool rgb) {
  654. DEBUG_MSG_P(PSTR("[LIGHT] %s: %s\n"), rgb ? "RGB" : "HSV", color);
  655. if (rgb) {
  656. _fromRGB(color);
  657. } else {
  658. _fromHSV(color);
  659. }
  660. }
  661. void lightColor(const char * color) {
  662. lightColor(color, true);
  663. }
  664. void lightColor(unsigned long color) {
  665. _fromLong(color, false);
  666. }
  667. String lightColor(bool rgb) {
  668. char str[12];
  669. if (rgb) {
  670. _toRGB(str, sizeof(str));
  671. } else {
  672. _toHSV(str, sizeof(str));
  673. }
  674. return String(str);
  675. }
  676. String lightColor() {
  677. return lightColor(true);
  678. }
  679. unsigned int lightChannel(unsigned char id) {
  680. if (id <= _light_channel.size()) {
  681. return _light_channel[id].inputValue;
  682. }
  683. return 0;
  684. }
  685. void lightChannel(unsigned char id, int value) {
  686. if (id <= _light_channel.size()) {
  687. _light_channel[id].inputValue = constrain(value, 0, LIGHT_MAX_VALUE);
  688. }
  689. }
  690. void lightChannelStep(unsigned char id, int steps) {
  691. lightChannel(id, lightChannel(id) + steps * LIGHT_STEP);
  692. }
  693. unsigned int lightBrightness() {
  694. return _light_brightness;
  695. }
  696. void lightBrightness(int b) {
  697. _light_brightness = constrain(b, 0, LIGHT_MAX_BRIGHTNESS);
  698. }
  699. void lightBrightnessStep(int steps) {
  700. lightBrightness(_light_brightness + steps * LIGHT_STEP);
  701. }
  702. unsigned long lightTransitionTime() {
  703. if (_light_use_transitions) {
  704. return _light_transition_time;
  705. } else {
  706. return 0;
  707. }
  708. }
  709. void lightTransitionTime(unsigned long m) {
  710. if (0 == m) {
  711. _light_use_transitions = false;
  712. } else {
  713. _light_use_transitions = true;
  714. _light_transition_time = m;
  715. }
  716. setSetting("useTransitions", _light_use_transitions);
  717. setSetting("lightTime", _light_transition_time);
  718. saveSettings();
  719. }
  720. // -----------------------------------------------------------------------------
  721. // SETUP
  722. // -----------------------------------------------------------------------------
  723. #if WEB_SUPPORT
  724. bool _lightWebSocketOnReceive(const char * key, JsonVariant& value) {
  725. if (strncmp(key, "light", 5) == 0) return true;
  726. if (strncmp(key, "use", 3) == 0) return true;
  727. return false;
  728. }
  729. void _lightWebSocketStatus(JsonObject& root) {
  730. if (_light_has_color) {
  731. if (getSetting("useRGB", LIGHT_USE_RGB).toInt() == 1) {
  732. root["rgb"] = lightColor(true);
  733. } else {
  734. root["hsv"] = lightColor(false);
  735. }
  736. }
  737. if (_light_use_cct) {
  738. root["useCCT"] = _light_use_cct;
  739. root["mireds"] = _light_mireds;
  740. }
  741. JsonArray& channels = root.createNestedArray("channels");
  742. for (unsigned char id=0; id < _light_channel.size(); id++) {
  743. channels.add(lightChannel(id));
  744. }
  745. root["brightness"] = lightBrightness();
  746. }
  747. void _lightWebSocketOnSend(JsonObject& root) {
  748. root["colorVisible"] = 1;
  749. root["mqttGroupColor"] = getSetting("mqttGroupColor");
  750. root["useColor"] = _light_has_color;
  751. root["useWhite"] = _light_use_white;
  752. root["useGamma"] = _light_use_gamma;
  753. root["useTransitions"] = _light_use_transitions;
  754. root["useCSS"] = getSetting("useCSS", LIGHT_USE_CSS).toInt() == 1;
  755. root["useRGB"] = getSetting("useRGB", LIGHT_USE_RGB).toInt() == 1;
  756. root["lightTime"] = _light_transition_time;
  757. _lightWebSocketStatus(root);
  758. }
  759. void _lightWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
  760. if (_light_has_color) {
  761. if (strcmp(action, "color") == 0) {
  762. if (data.containsKey("rgb")) {
  763. lightColor(data["rgb"], true);
  764. lightUpdate(true, true);
  765. }
  766. if (data.containsKey("hsv")) {
  767. lightColor(data["hsv"], false);
  768. lightUpdate(true, true);
  769. }
  770. }
  771. }
  772. if (_light_use_cct) {
  773. if (strcmp(action, "mireds") == 0) {
  774. _fromMireds(data["mireds"]);
  775. lightUpdate(true, true);
  776. }
  777. }
  778. if (strcmp(action, "channel") == 0) {
  779. if (data.containsKey("id") && data.containsKey("value")) {
  780. lightChannel(data["id"], data["value"]);
  781. lightUpdate(true, true);
  782. }
  783. }
  784. if (strcmp(action, "brightness") == 0) {
  785. if (data.containsKey("value")) {
  786. lightBrightness(data["value"]);
  787. lightUpdate(true, true);
  788. }
  789. }
  790. }
  791. #endif
  792. #if API_SUPPORT
  793. void _lightAPISetup() {
  794. if (_light_has_color) {
  795. apiRegister(MQTT_TOPIC_COLOR_RGB,
  796. [](char * buffer, size_t len) {
  797. if (getSetting("useCSS", LIGHT_USE_CSS).toInt() == 1) {
  798. _toRGB(buffer, len, true);
  799. } else {
  800. _toLong(buffer, len, true);
  801. }
  802. },
  803. [](const char * payload) {
  804. lightColor(payload, true);
  805. lightUpdate(true, true);
  806. }
  807. );
  808. apiRegister(MQTT_TOPIC_COLOR_HSV,
  809. [](char * buffer, size_t len) {
  810. _toHSV(buffer, len, true);
  811. },
  812. [](const char * payload) {
  813. lightColor(payload, false);
  814. lightUpdate(true, true);
  815. }
  816. );
  817. apiRegister(MQTT_TOPIC_KELVIN,
  818. [](char * buffer, size_t len) {},
  819. [](const char * payload) {
  820. _fromKelvin(atol(payload));
  821. lightUpdate(true, true);
  822. }
  823. );
  824. apiRegister(MQTT_TOPIC_MIRED,
  825. [](char * buffer, size_t len) {},
  826. [](const char * payload) {
  827. _fromMireds(atol(payload));
  828. lightUpdate(true, true);
  829. }
  830. );
  831. }
  832. for (unsigned int id=0; id<_light_channel.size(); id++) {
  833. char key[15];
  834. snprintf_P(key, sizeof(key), PSTR("%s/%d"), MQTT_TOPIC_CHANNEL, id);
  835. apiRegister(key,
  836. [id](char * buffer, size_t len) {
  837. snprintf_P(buffer, len, PSTR("%d"), _light_channel[id].target);
  838. },
  839. [id](const char * payload) {
  840. lightChannel(id, atoi(payload));
  841. lightUpdate(true, true);
  842. }
  843. );
  844. }
  845. apiRegister(MQTT_TOPIC_TRANSITION,
  846. [](char * buffer, size_t len) {
  847. snprintf_P(buffer, len, PSTR("%d"), lightTransitionTime());
  848. },
  849. [](const char * payload) {
  850. lightTransitionTime(atol(payload));
  851. }
  852. );
  853. apiRegister(MQTT_TOPIC_BRIGHTNESS,
  854. [](char * buffer, size_t len) {
  855. snprintf_P(buffer, len, PSTR("%d"), _light_brightness);
  856. },
  857. [](const char * payload) {
  858. lightBrightness(atoi(payload));
  859. lightUpdate(true, true);
  860. }
  861. );
  862. }
  863. #endif // API_SUPPORT
  864. #if TERMINAL_SUPPORT
  865. void _lightInitCommands() {
  866. terminalRegisterCommand(F("BRIGHTNESS"), [](Embedis* e) {
  867. if (e->argc > 1) {
  868. const String value(e->argv[1]);
  869. if( value.length() > 0 ) {
  870. if( value[0] == '+' || value[0] == '-' ) {
  871. lightBrightness(lightBrightness()+String(e->argv[1]).toInt());
  872. } else {
  873. lightBrightness(String(e->argv[1]).toInt());
  874. }
  875. lightUpdate(true, true);
  876. }
  877. }
  878. DEBUG_MSG_P(PSTR("Brightness: %d\n"), lightBrightness());
  879. terminalOK();
  880. });
  881. terminalRegisterCommand(F("CHANNEL"), [](Embedis* e) {
  882. if (e->argc < 2) {
  883. terminalError(F("Wrong arguments"));
  884. }
  885. int id = String(e->argv[1]).toInt();
  886. if (e->argc > 2) {
  887. int value = String(e->argv[2]).toInt();
  888. lightChannel(id, value);
  889. lightUpdate(true, true);
  890. }
  891. DEBUG_MSG_P(PSTR("Channel #%d (%s): %d\n"), id, lightDesc(id).c_str(), lightChannel(id));
  892. terminalOK();
  893. });
  894. terminalRegisterCommand(F("COLOR"), [](Embedis* e) {
  895. if (e->argc > 1) {
  896. String color = String(e->argv[1]);
  897. lightColor(color.c_str());
  898. lightUpdate(true, true);
  899. }
  900. DEBUG_MSG_P(PSTR("Color: %s\n"), lightColor().c_str());
  901. terminalOK();
  902. });
  903. terminalRegisterCommand(F("KELVIN"), [](Embedis* e) {
  904. if (e->argc > 1) {
  905. String color = String("K") + String(e->argv[1]);
  906. lightColor(color.c_str());
  907. lightUpdate(true, true);
  908. }
  909. DEBUG_MSG_P(PSTR("Color: %s\n"), lightColor().c_str());
  910. terminalOK();
  911. });
  912. terminalRegisterCommand(F("MIRED"), [](Embedis* e) {
  913. if (e->argc > 1) {
  914. const String value(e->argv[1]);
  915. String color = String("M");
  916. if( value.length() > 0 ) {
  917. if( value[0] == '+' || value[0] == '-' ) {
  918. color += String(_light_mireds + String(e->argv[1]).toInt());
  919. } else {
  920. color += String(e->argv[1]);
  921. }
  922. lightColor(color.c_str());
  923. lightUpdate(true, true);
  924. }
  925. }
  926. DEBUG_MSG_P(PSTR("Color: %s\n"), lightColor().c_str());
  927. terminalOK();
  928. });
  929. }
  930. #endif // TERMINAL_SUPPORT
  931. #if LIGHT_PROVIDER == LIGHT_PROVIDER_DIMMER
  932. unsigned long getIOMux(unsigned long gpio) {
  933. unsigned long muxes[16] = {
  934. PERIPHS_IO_MUX_GPIO0_U, PERIPHS_IO_MUX_U0TXD_U, PERIPHS_IO_MUX_GPIO2_U, PERIPHS_IO_MUX_U0RXD_U,
  935. PERIPHS_IO_MUX_GPIO4_U, PERIPHS_IO_MUX_GPIO5_U, PERIPHS_IO_MUX_SD_CLK_U, PERIPHS_IO_MUX_SD_DATA0_U,
  936. PERIPHS_IO_MUX_SD_DATA1_U, PERIPHS_IO_MUX_SD_DATA2_U, PERIPHS_IO_MUX_SD_DATA3_U, PERIPHS_IO_MUX_SD_CMD_U,
  937. PERIPHS_IO_MUX_MTDI_U, PERIPHS_IO_MUX_MTCK_U, PERIPHS_IO_MUX_MTMS_U, PERIPHS_IO_MUX_MTDO_U
  938. };
  939. return muxes[gpio];
  940. }
  941. unsigned long getIOFunc(unsigned long gpio) {
  942. unsigned long funcs[16] = {
  943. FUNC_GPIO0, FUNC_GPIO1, FUNC_GPIO2, FUNC_GPIO3,
  944. FUNC_GPIO4, FUNC_GPIO5, FUNC_GPIO6, FUNC_GPIO7,
  945. FUNC_GPIO8, FUNC_GPIO9, FUNC_GPIO10, FUNC_GPIO11,
  946. FUNC_GPIO12, FUNC_GPIO13, FUNC_GPIO14, FUNC_GPIO15
  947. };
  948. return funcs[gpio];
  949. }
  950. #endif
  951. void _lightConfigure() {
  952. _light_has_color = getSetting("useColor", LIGHT_USE_COLOR).toInt() == 1;
  953. if (_light_has_color && (_light_channel.size() < 3)) {
  954. _light_has_color = false;
  955. setSetting("useColor", _light_has_color);
  956. }
  957. _light_use_white = getSetting("useWhite", LIGHT_USE_WHITE).toInt() == 1;
  958. if (_light_use_white && (_light_channel.size() < 4) && (_light_channel.size() != 2)) {
  959. _light_use_white = false;
  960. setSetting("useWhite", _light_use_white);
  961. }
  962. _light_use_cct = getSetting("useCCT", LIGHT_USE_CCT).toInt() == 1;
  963. if (_light_use_cct && (((_light_channel.size() < 5) && (_light_channel.size() != 2)) || !_light_use_white)) {
  964. _light_use_cct = false;
  965. setSetting("useCCT", _light_use_cct);
  966. }
  967. _light_use_gamma = getSetting("useGamma", LIGHT_USE_GAMMA).toInt() == 1;
  968. _light_use_transitions = getSetting("useTransitions", LIGHT_USE_TRANSITIONS).toInt() == 1;
  969. _light_transition_time = getSetting("lightTime", LIGHT_TRANSITION_TIME).toInt();
  970. }
  971. void lightSetup() {
  972. #ifdef LIGHT_ENABLE_PIN
  973. pinMode(LIGHT_ENABLE_PIN, OUTPUT);
  974. digitalWrite(LIGHT_ENABLE_PIN, HIGH);
  975. #endif
  976. #if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX
  977. _my92xx = new my92xx(MY92XX_MODEL, MY92XX_CHIPS, MY92XX_DI_PIN, MY92XX_DCKI_PIN, MY92XX_COMMAND);
  978. for (unsigned char i=0; i<LIGHT_CHANNELS; i++) {
  979. _light_channel.push_back((channel_t) {0, false, true, 0, 0, 0});
  980. }
  981. #endif
  982. #if LIGHT_PROVIDER == LIGHT_PROVIDER_DIMMER
  983. #ifdef LIGHT_CH1_PIN
  984. _light_channel.push_back((channel_t) {LIGHT_CH1_PIN, LIGHT_CH1_INVERSE, true, 0, 0, 0});
  985. #endif
  986. #ifdef LIGHT_CH2_PIN
  987. _light_channel.push_back((channel_t) {LIGHT_CH2_PIN, LIGHT_CH2_INVERSE, true, 0, 0, 0});
  988. #endif
  989. #ifdef LIGHT_CH3_PIN
  990. _light_channel.push_back((channel_t) {LIGHT_CH3_PIN, LIGHT_CH3_INVERSE, true, 0, 0, 0});
  991. #endif
  992. #ifdef LIGHT_CH4_PIN
  993. _light_channel.push_back((channel_t) {LIGHT_CH4_PIN, LIGHT_CH4_INVERSE, true, 0, 0, 0});
  994. #endif
  995. #ifdef LIGHT_CH5_PIN
  996. _light_channel.push_back((channel_t) {LIGHT_CH5_PIN, LIGHT_CH5_INVERSE, true, 0, 0, 0});
  997. #endif
  998. uint32 pwm_duty_init[PWM_CHANNEL_NUM_MAX];
  999. uint32 io_info[PWM_CHANNEL_NUM_MAX][3];
  1000. for (unsigned int i=0; i < _light_channel.size(); i++) {
  1001. pwm_duty_init[i] = 0;
  1002. io_info[i][0] = getIOMux(_light_channel[i].pin);
  1003. io_info[i][1] = getIOFunc(_light_channel[i].pin);
  1004. io_info[i][2] = _light_channel[i].pin;
  1005. pinMode(_light_channel[i].pin, OUTPUT);
  1006. }
  1007. pwm_init(LIGHT_MAX_PWM, pwm_duty_init, PWM_CHANNEL_NUM_MAX, io_info);
  1008. pwm_start();
  1009. #endif
  1010. DEBUG_MSG_P(PSTR("[LIGHT] LIGHT_PROVIDER = %d\n"), LIGHT_PROVIDER);
  1011. DEBUG_MSG_P(PSTR("[LIGHT] Number of channels: %d\n"), _light_channel.size());
  1012. _lightConfigure();
  1013. if (rtcmemStatus()) {
  1014. _lightRestoreRtcmem();
  1015. } else {
  1016. _lightRestoreSettings();
  1017. }
  1018. #if WEB_SUPPORT
  1019. wsOnSendRegister(_lightWebSocketOnSend);
  1020. wsOnActionRegister(_lightWebSocketOnAction);
  1021. wsOnReceiveRegister(_lightWebSocketOnReceive);
  1022. #endif
  1023. #if API_SUPPORT
  1024. _lightAPISetup();
  1025. #endif
  1026. #if MQTT_SUPPORT
  1027. mqttRegister(_lightMQTTCallback);
  1028. #endif
  1029. #if TERMINAL_SUPPORT
  1030. _lightInitCommands();
  1031. #endif
  1032. // Main callbacks
  1033. espurnaRegisterReload([]() {
  1034. #if LIGHT_SAVE_ENABLED == 0
  1035. lightSave();
  1036. #endif
  1037. _lightConfigure();
  1038. });
  1039. }
  1040. #endif // LIGHT_PROVIDER != LIGHT_PROVIDER_NONE