/* xdrv_04_light.ino - PWM, WS2812 and sonoff led support for Tasmota Copyright (C) 2021 Theo Arends This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_LIGHT /*********************************************************************************************\ * PWM, WS2812, Sonoff B1, AiLight, Sonoff Led and BN-SZ01, H801, MagicHome and Arilux * * light_type Module Color ColorTemp Modules * ---------- --------- ----- --------- ---------------------------- * 0 - no (Sonoff Basic) * 1 PWM1 W no (Sonoff BN-SZ) * 2 PWM2 CW yes (Sonoff Led) * 3 PWM3 RGB no (H801, MagicHome and Arilux LC01) * 4 PWM4 RGBW no (H801, MagicHome and Arilux) * 5 PWM5 RGBCW yes (H801, Arilux LC11) * 6 PWM6 * 7 PWM7 * 8 reserved * 9 SERIAL1 no * 10 SERIAL2 yes * 11 +WS2812 RGB no (One WS2812 RGB or RGBW ledstrip) * 12 AiLight RGBW no * 13 Sonoff B1 RGBCW yes * 14 reserved * 15 reserved * * light_scheme WS2812 3+ Colors 1+2 Colors Effect * ------------ ------ --------- ---------- ----------------- * 0 yes yes yes Color On/Off * 1 yes yes yes Wakeup light * 2 yes yes no Color cycle RGB * 3 yes yes no Color cycle RBG * 4 yes yes no Random RGB colors * 5 yes no no Clock * 6 yes no no Incandescent * 7 yes no no RGB * 8 yes no no Christmas * 9 yes no no Hanukkah * 10 yes no no Kwanzaa * 11 yes no no Rainbow * 12 yes no no Fire * 13 yes no no Stairs * 14 yes no no Clear (= Berry) \*********************************************************************************************/ /*********************************************************************************************\ * * Light management has been refactored to provide a cleaner class-based interface. * Also, now all values are stored as integer, no more floats that could generate * rounding errors. * * Two singletons are now used to control the state of the light. * - light_state (LightStateClass) stores the color / white temperature and * brightness. Use this object to READ only. * - light_controller (LightControllerClass) is used to change light state * and adjust all Settings and levels accordingly. * Always use this object to change light status. * * As there have been lots of changes in light control, here is a summary out * the whole flow from setting colors to drving the PMW pins. * * 1. To change colors, always use 'light_controller' object. * 'light_state' is only to be used to read current state. * .a For color bulbs, set color via changeRGB() or changeHS() for Hue/Sat. * Set the overall brightness changeBri(0..255) or changeDimmer(0..100%) * RGB and Hue/Sat are always kept in sync. Internally, RGB are stored at * full range (max brightness) so that when you reduce brightness and * raise it back again, colors don't change due to rounding errors. * .b For white bulbs with Cold/Warm colortone, use changeCW() or changeCT() * to change color-tone. Set overall brightness separately. * Color-tone temperature can range from 153 (Cold) to 500 (Warm). * SetOption82 can expand the rendering from 200-380 due to Alexa reduced range. * CW channels are stored at full brightness to avoid rounding errors. * .c Alternatively, you can set all 5 channels at once with changeChannels(), * in this case it will also set the corresponding brightness. * * 2.a After any change, the Settings object is updated so that changes * survive a reboot and can be stored in flash - in saveSettings() * .b Actual channel values are computed from RGB or CT combined with brightness. * Range is still 0..255 (8 bits) - in getActualRGBCW() * .c The 5 internal channels RGBWC are mapped to the actual channels supported * by the light_type: in calcLevels() * 1 channel - 0:Brightness * 2 channels - 0:Coldwhite 1:Warmwhite * 3 channels - 0:Red 1:Green 2:Blue * 4 channels - 0:Red 1:Green 2:Blue 3:White * 5 channels - 0:Red 1:Green 2:Blue 3:ColdWhite 4:Warmwhite * * 3. In LightAnimate(), final PWM values are computed at next tick. * .a If color did not change since last tick - ignore. * .b Extend resolution from 8 bits to 10 bits, which makes a significant * difference when applying gamma correction at low brightness. * .c Apply Gamma Correction if LedTable==1 (by default). * Gamma Correction uses an adaptative resolution table from 11 to 8 bits. * .d For Warm/Cold-white channels, Gamma correction is calculated in combined mode. * Ie. total white brightness (C+W) is used for Gamma correction and gives * the overall light power required. Then this light power is split among * Wamr/Cold channels. * .e Gamma correction is still applied to 8 bits channels for compatibility * with other non-PMW modules. * .f Apply color balance correction from rgbwwTable[]. * Note: correction is done after Gamma correction, it is meant * to adjust leds with different power * .g If rgbwwTable[4] is zero, blend RGB with White and adjust the level of * White channel according to rgbwwTable[3] * .h Scale ranges from 10 bits to 0..PWMRange (by default 1023) so no change * by default. * .i Apply port remapping from Option37 * .j Invert PWM value if port is of type PMWxi instead of PMWx * .k Apply PWM value with analogWrite() - if pin is configured * \*********************************************************************************************/ #define XDRV_04 4 // #define DEBUG_LIGHT #ifdef USE_NETWORK_LIGHT_SCHEMES enum LightSchemes { LS_POWER, LS_WAKEUP, LS_CYCLEUP, LS_CYCLEDN, LS_RANDOM, LS_DDP, LS_MAX }; #else enum LightSchemes { LS_POWER, LS_WAKEUP, LS_CYCLEUP, LS_CYCLEDN, LS_RANDOM, LS_MAX }; #endif const uint8_t LIGHT_COLOR_SIZE = 25; // Char array scolor size const char kLightCommands[] PROGMEM = "|" // No prefix // SetOptions synonyms D_SO_CHANNELREMAP "|" D_SO_MULTIPWM "|" D_SO_ALEXACTRANGE "|" D_SO_POWERONFADE "|" D_SO_PWMCT "|" D_SO_WHITEBLEND "|" D_SO_ARTNET_AUTORUN "|" // Other commands D_CMND_COLOR "|" D_CMND_COLORTEMPERATURE "|" D_CMND_DIMMER "|" D_CMND_DIMMER_RANGE "|" D_CMND_DIMMER_STEP "|" D_CMND_LEDTABLE "|" D_CMND_FADE "|" D_CMND_RGBWWTABLE "|" D_CMND_SCHEME "|" D_CMND_SPEED "|" D_CMND_WAKEUP "|" D_CMND_WAKEUPDURATION "|" D_CMND_WHITE "|" D_CMND_CHANNEL "|" D_CMND_HSBCOLOR "|" D_CMND_CTRANGE #ifdef USE_LIGHT_VIRTUAL_CT "|" D_CMND_VIRTUALCT #endif // USE_LIGHT_VIRTUAL_CT #ifdef USE_LIGHT_PALETTE "|" D_CMND_PALETTE #endif // USE_LIGHT_PALETTE #ifdef USE_DGR_LIGHT_SEQUENCE "|" D_CMND_SEQUENCE_OFFSET #endif // USE_DGR_LIGHT_SEQUENCE #ifdef USE_LIGHT_ARTNET "|" D_CMND_ARTNET "|" D_CMND_ARTNET_CONFIG #endif "|UNDOCA" ; SO_SYNONYMS(kLightSynonyms, 37, 68, 82, 91, 92, 105, 148, ); void (* const LightCommand[])(void) PROGMEM = { &CmndColor, &CmndColorTemperature, &CmndDimmer, &CmndDimmerRange, &CmndDimmerStep, &CmndLedTable, &CmndFade, &CmndRgbwwTable, &CmndScheme, &CmndSpeed, &CmndWakeup, &CmndWakeupDuration, &CmndWhite, &CmndChannel, &CmndHsbColor, &CmndCTRange, #ifdef USE_LIGHT_VIRTUAL_CT &CmndVirtualCT, #endif // USE_LIGHT_VIRTUAL_CT #ifdef USE_LIGHT_PALETTE &CmndPalette, #endif // USE_LIGHT_PALETTE #ifdef USE_DGR_LIGHT_SEQUENCE &CmndSequenceOffset, #endif // USE_DGR_LIGHT_SEQUENCE #ifdef USE_LIGHT_ARTNET &CmndArtNet, &CmndArtNetConfig, #endif &CmndUndocA }; // Light color mode, either RGB alone, or white-CT alone, or both only available if ct_rgb_linked is false enum LightColorModes { LCM_RGB = 1, LCM_CT = 2, LCM_BOTH = 3 }; struct LRgbColor { uint8_t R, G, B; }; const uint8_t MAX_FIXED_COLOR = 12; const LRgbColor kFixedColor[MAX_FIXED_COLOR] PROGMEM = { 255,0,0, 0,255,0, 0,0,255, 228,32,0, 0,228,32, 0,32,228, 188,64,0, 0,160,96, 160,32,240, 255,255,0, 255,0,170, 255,255,255 }; struct LWColor { uint8_t W; }; const uint8_t MAX_FIXED_WHITE = 4; const LWColor kFixedWhite[MAX_FIXED_WHITE] PROGMEM = { 0, 255, 128, 32 }; struct LCwColor { uint8_t C, W; }; const uint8_t MAX_FIXED_COLD_WARM = 4; const LCwColor kFixedColdWarm[MAX_FIXED_COLD_WARM] PROGMEM = { 0,0, 255,0, 0,255, 128,128 }; // CT min and max const uint16_t CT_MIN = 153; // 6500K const uint16_t CT_MAX = 500; // 2000K // Ranges used for Alexa const uint16_t CT_MIN_ALEXA = 200; // also 5000K const uint16_t CT_MAX_ALEXA = 380; // also 2600K // Virtual CT default values typedef uint8_t vct_pivot_t[LST_MAX]; const size_t CT_PIVOTS = LIGHT_VIRTUAL_CT_POINTS; const vct_pivot_t CT_PIVOTS_RGB PROGMEM = { 255, 255, 255, 0, 0 }; const vct_pivot_t CT_PIVOTS_CWW PROGMEM = { 0, 0, 0, 255, 0 }; const vct_pivot_t CT_PIVOTS_WWW PROGMEM = { 0, 0, 0, 0, 255 }; struct LIGHT { uint32_t strip_timer_counter = 0; // Bars and Gradient power_t power = 0; // Power for each channel if SetOption68, or boolean if single light uint8_t entry_color[LST_MAX]; uint8_t current_color[LST_MAX]; uint8_t new_color[LST_MAX]; uint8_t last_color[LST_MAX]; uint8_t color_remap[LST_MAX]; uint8_t wheel = 0; uint8_t random = 0; uint8_t subtype = 0; // LST_ subtype uint8_t device = 0; uint8_t old_power = 1; uint8_t wakeup_active = 0; // 0=inctive, 1=on-going, 2=about to start, 3=will be triggered next cycle uint8_t fixed_color_index = 1; uint8_t pwm_offset = 0; // Offset in color buffer, used by sm16716 to drive itself RGB, and PWM for CCT (value is 0 or 3) uint8_t max_scheme = LS_MAX -1; uint8_t last_scheme; uint32_t wakeup_start_time = 0; bool update = true; bool pwm_multi_channels = false; // SetOption68, treat each PWM channel as an independant dimmer bool virtual_ct = false; // SetOption106, add a 5th virtual channel, only if SO106 = 1, SO68 = 0, Light is RGBW (4 channels), SO37 < 128 bool fade_initialized = false; // dont't fade at startup bool fade_running = false; #ifdef USE_DEVICE_GROUPS uint8_t last_dgr_scheme = 0; bool devgrp_no_channels_out = false; // don't share channels with device group (e.g. if scheme set by other device) #ifdef USE_DGR_LIGHT_SEQUENCE uint8_t sequence_offset = 0; // number of channel changes this light is behind the master uint8_t * channels_fifo; #endif // USE_DGR_LIGHT_SEQUENCE #endif // USE_DEVICE_GROUPS #ifdef USE_LIGHT_PALETTE uint8_t palette_count = 0; // palette entry count uint8_t * palette; // dynamically allocated palette color array #endif // USE_LIGHT_PALETTE uint16_t fade_start_10[LST_MAX] = {0,0,0,0,0}; uint16_t fade_cur_10[LST_MAX]; uint16_t fade_end_10[LST_MAX]; // 10 bits resolution target channel values uint16_t fade_duration = 0; // duration of fade in milliseconds uint32_t fade_start = 0; // fade start time in milliseconds, compared to millis() bool fade_once_enabled = false; // override fade a single time bool fade_once_value = false; // override fade a single time bool speed_once_enabled = false; // override speed a single time uint8_t speed_once_value = 0; // override speed a single time uint16_t pwm_min = 0; // minimum value for PWM, from DimmerRange, 0..1023 uint16_t pwm_max = 1023; // maxumum value for PWM, from DimmerRange, 0..1023 // Virtual CT uint16_t vct_ct[CT_PIVOTS]; // CT value for each segment #ifdef USE_LIGHT_VIRTUAL_CT vct_pivot_t vct_color[CT_PIVOTS]; // array of 3 colors each with 5 values #endif } Light; power_t LightPower(void) { return Light.power; // Make external } uint8_t LightDevice(void) { return Light.device; // Make external } uint32_t LightDevices(void) { if (0 == Light.device) { return 0; } return TasmotaGlobal.devices_present - Light.device +1; // Make external } static uint32_t min3(uint32_t a, uint32_t b, uint32_t c) { return (a < b && a < c) ? a : (b < c) ? b : c; } #ifdef USE_NETWORK_LIGHT_SCHEMES WiFiUDP ddp_udp; uint8_t ddp_udp_up = 0; #endif // // LightStateClass // This class is an abstraction of the current light state. // It allows for b/w, full colors, or white colortone // // This class has 2 independant slots // 1/ Brightness 0.255, dimmer controls both RGB and WC (warm-cold) // 1/ RGB and Hue/Sat - always kept in sync and stored at full brightness, // i.e. R G or B are 255 // briRGB specifies the brightness for the RGB slot. // If Brightness is 0, it is equivalent to Off (for compatibility) // Dimmer is Brightness converted to range 0..100 // 2/ White with colortone - or CW (Cold / Warm) // ct is 153..500 temperature (153=cold, 500=warm) // briCT specifies the brightness for white channel // // Dimmer (0.100) is automatically derived from brightness // // INVARIANTS: // 1. RGB components are always stored at full brightness and modulated with briRGB // ((R == 255) || (G == 255) || (B == 255)) // 2. RGB and Hue/Sat are always kept in sync whether you use setRGB() or setHS() // 3. Warm/Cold white channels are always stored at full brightness // ((WW == 255) || (WC == 255)) // 4. WC/WW and CT are always kept in sync. // Note: if you use setCT() then WC+WW == 255 (both channels are linked) // but if you use setCW() both channels can be set independantly // 5. If RGB or CT channels are deactivated, then corresponding brightness is zero // if (colot_tone == LCM_RGB) then briCT = 0 // if (color_tone == LCM_CT) then briRGB = 0 // if (colot_tone == LCM_BOTH) then briRGB and briCT can have any values // // Note: If you want the actual RGB, you need to multiply with Bri, or use getActualRGBCW() // Note: all values are stored as unsigned integer, no floats. // Note: you can query values from this singleton. But to change values, // use the LightController - changing this object will have no effect on actual light. // class LightStateClass { private: uint16_t _hue = 0; // 0..359 uint16_t _hue16 = 0; // 0..65535 - high resolution hue necessary for Alexa/Hue integration uint8_t _sat = 255; // 0..255 uint8_t _briRGB = 255; // 0..255 uint8_t _briRGB_orig = 255; // 0..255 // dimmer is same as _bri but with a range of 0%-100% uint8_t _r = 255; // 0..255 uint8_t _g = 255; // 0..255 uint8_t _b = 255; // 0..255 uint8_t _subtype = 0; // local copy of Light.subtype, if we need multiple lights uint16_t _ct = CT_MIN; // 153..500, default to 153 (cold white) uint8_t _wc = 255; // white cold channel uint8_t _ww = 0; // white warm channel uint8_t _briCT = 255; uint8_t _briCT_orig = 255; uint8_t _color_mode = LCM_RGB; // RGB by default bool _power = false; // power indicator, used only in virtual lights, Tasmota tracks power separately bool _reachable = true; // reachable indicator, used only in vritual lights. Not used in internal light public: LightStateClass() { //AddLog(LOG_LEVEL_DEBUG_MORE, "LightStateClass::Constructor RGB raw (%d %d %d) HS (%d %d) bri (%d)", _r, _g, _b, _hue, _sat, _bri); } void setSubType(uint8_t sub_type) { _subtype = sub_type; // set sub_type at initialization, shoudln't be changed afterwards } uint8_t getSubType(void) const { return _subtype; } // This function is a bit hairy, it will try to match the required // colormode with the features of the device: // LST_NONE: LCM_RGB // LST_SINGLE: LCM_RGB // LST_COLDWARM: LCM_CT // LST_RGB: LCM_RGB // LST_RGBW: LCM_RGB, LCM_CT or LCM_BOTH // LST_RGBCW: LCM_RGB, LCM_CT or LCM_BOTH uint8_t setColorMode(uint8_t cm) { uint8_t prev_cm = _color_mode; if (cm < LCM_RGB) { cm = LCM_RGB; } if (cm > LCM_BOTH) { cm = LCM_BOTH; } uint8_t maxbri = (_briRGB >= _briCT) ? _briRGB : _briCT; switch (_subtype) { case LST_COLDWARM: _color_mode = LCM_CT; break; case LST_NONE: case LST_SINGLE: case LST_RGB: default: _color_mode = LCM_RGB; break; case LST_RGBW: case LST_RGBCW: _color_mode = cm; break; } if (LCM_RGB == _color_mode) { _briCT = 0; if (0 == _briRGB) { _briRGB = maxbri; } } if (LCM_CT == _color_mode) { _briRGB = 0; if (0 == _briCT) { _briCT = maxbri; } } #ifdef DEBUG_LIGHT AddLog(LOG_LEVEL_DEBUG_MORE, "LightStateClass::setColorMode prev_cm (%d) req_cm (%d) new_cm (%d)", prev_cm, cm, _color_mode); #endif return prev_cm; } inline uint8_t getColorMode() const { return _color_mode; } void addRGBMode(void) { setColorMode(_color_mode | LCM_RGB); } void addCTMode(void) { setColorMode(_color_mode | LCM_CT); } // power accessors, for virtual lights only (has no effect on Tasmota lights) bool getPower(void) const { return _power; } void setPower(bool pow) { _power = pow; } // reachable accessors, for virtual lights only (has no effect on Tasmota lights) bool getReachable(void) const { return _reachable; } void setReachable(bool reachable) { _reachable = reachable; } // Get RGB color, always at full brightness (ie. one of the components is 255) void getRGB(uint8_t *r, uint8_t *g, uint8_t *b) const { if (r) { *r = _r; } if (g) { *g = _g; } if (b) { *b = _b; } } // get full brightness values for warm and cold channels. // either w=c=0 (off) or w+c >= 255 void getCW(uint8_t *rc, uint8_t *rw) const { if (rc) { *rc = _wc; } if (rw) { *rw = _ww; } } // Get the actual values for each channel, ie multiply with brightness void getActualRGBCW(uint8_t *r, uint8_t *g, uint8_t *b, uint8_t *c, uint8_t *w) const { bool rgb_channels_on = _color_mode & LCM_RGB; bool ct_channels_on = _color_mode & LCM_CT; if (r) { *r = rgb_channels_on ? changeUIntScale(_r, 0, 255, 0, _briRGB) : 0; } if (g) { *g = rgb_channels_on ? changeUIntScale(_g, 0, 255, 0, _briRGB) : 0; } if (b) { *b = rgb_channels_on ? changeUIntScale(_b, 0, 255, 0, _briRGB) : 0; } if (c) { *c = ct_channels_on ? changeUIntScale(_wc, 0, 255, 0, _briCT) : 0; } if (w) { *w = ct_channels_on ? changeUIntScale(_ww, 0, 255, 0, _briCT) : 0; } } void getChannels(uint8_t *channels) const { getActualRGBCW(&channels[0], &channels[1], &channels[2], &channels[3], &channels[4]); } void getChannelsRaw(uint8_t *channels) const { channels[0] = _r; channels[1] = _g; channels[2] = _b; channels[3] = _wc; channels[4] = _ww; } void getHSB(uint16_t *hue, uint8_t *sat, uint8_t *bri) const { if (hue) { *hue = _hue; } if (sat) { *sat = _sat; } if (bri) { *bri = _briRGB; } } // getBri() is guaranteed to give the same result as setBri() - no rounding errors. uint8_t getBri(void) const { // return the max of _briCT and _briRGB return (_briRGB >= _briCT) ? _briRGB : _briCT; } // get the white Brightness inline uint8_t getBriCT(void) const { return _briCT; } inline uint8_t getBriCTOrig(void) const { return _briCT_orig; } static inline uint8_t DimmerToBri(uint8_t dimmer) { return changeUIntScale(dimmer, 0, 100, 0, 255); // 0..255 } static uint8_t BriToDimmer(uint8_t bri) { uint8_t dimmer = changeUIntScale(bri, 0, 255, 0, 100); // if brightness is non zero, force dimmer to be non-zero too if ((dimmer == 0) && (bri > 0)) { dimmer = 1; } return dimmer; } uint8_t getDimmer(uint32_t mode = 0) const { uint8_t bri; switch (mode) { case 1: bri = getBriRGB(); break; case 2: bri = getBriCT(); break; default: bri = getBri(); break; } return BriToDimmer(bri); } inline uint16_t getCT(void) const { return _ct; // 153..500, or CT_MIN..CT_MAX } // get current color in XY format void getXY(float *x, float *y) const { RgbToXy(_r, _g, _b, x, y); } // setters -- do not use directly, use the light_controller instead // sets both master Bri and whiteBri void setBri(uint8_t bri) { setBriRGB(_color_mode & LCM_RGB ? bri : 0); setBriCT(_color_mode & LCM_CT ? bri : 0); #ifdef DEBUG_LIGHT AddLog(LOG_LEVEL_DEBUG_MORE, "LightStateClass::setBri RGB raw (%d %d %d) HS (%d %d) bri (%d)", _r, _g, _b, _hue, _sat, _briRGB); #endif } // changes the RGB brightness alone uint8_t setBriRGB(uint8_t bri_rgb) { uint8_t prev_bri = _briRGB; _briRGB = bri_rgb; if (bri_rgb > 0) { addRGBMode(); } return prev_bri; } // changes the white brightness alone uint8_t setBriCT(uint8_t bri_ct) { uint8_t prev_bri = _briCT; _briCT = bri_ct; if (bri_ct > 0) { addCTMode(); } return prev_bri; } inline uint8_t getBriRGB(void) const { return _briRGB; } inline uint8_t getBriRGBOrig(void) const { return _briRGB_orig; } void setDimmer(uint8_t dimmer) { setBri(DimmerToBri(dimmer)); } void setCT(uint16_t ct) { if (0 == ct) { // disable ct mode setColorMode(LCM_RGB); // try deactivating CT mode, setColorMode() will check which is legal } else { ct = (ct < CT_MIN ? CT_MIN : (ct > CT_MAX ? CT_MAX : ct)); _ww = changeUIntScale(ct, Light.vct_ct[0], Light.vct_ct[CT_PIVOTS-1], 0, 255); _wc = 255 - _ww; _ct = ct; addCTMode(); } #ifdef DEBUG_LIGHT AddLog(LOG_LEVEL_DEBUG_MORE, "LightStateClass::setCT RGB raw (%d %d %d) HS (%d %d) briRGB (%d) briCT (%d) CT (%d)", _r, _g, _b, _hue, _sat, _briRGB, _briCT, _ct); #endif } // Manually set Cold/Warm channels. // There are two modes: // 1. (free_range == false, default) // In this mode there is only one virtual white channel with color temperature // As a side effect, WC+WW = 255. It means also that the sum of light power // from white LEDs is always equal to briCT. It is not possible here // to set both white LEDs at full power, hence protecting power supplies // from overlaoding. // 2. (free_range == true) // In this mode, values of WC and WW are free -- both channels can be set // at full power. // In this mode, we always scale both channels so that one at least is 255. // // We automatically adjust briCT to have the right values of channels void setCW(uint8_t c, uint8_t w, bool free_range = false) { uint16_t max = (w > c) ? w : c; // 0..255 uint16_t sum = c + w; if (sum <= 257) { free_range = false; } // if we don't allow free range or if sum is below 255 (with tolerance of 2) if (0 == max) { _briCT = 0; // brightness set to null setColorMode(LCM_RGB); // try deactivating CT mode, setColorMode() will check which is legal } else { if (!free_range) { // we need to normalize to sum = 255 _ww = changeUIntScale(w, 0, sum, 0, 255); _wc = 255 - _ww; } else { // we normalize to max = 255 _ww = changeUIntScale(w, 0, max, 0, 255); _wc = changeUIntScale(c, 0, max, 0, 255); } _ct = changeUIntScale(w, 0, sum, CT_MIN, CT_MAX); addCTMode(); // activate CT mode if needed if (_color_mode & LCM_CT) { _briCT = free_range ? max : (sum > 255 ? 255 : sum); } } #ifdef DEBUG_LIGHT AddLog(LOG_LEVEL_DEBUG_MORE, "LightStateClass::setCW CW (%d %d) CT (%d) briCT (%d)", c, w, _ct, _briCT); #endif } // sets RGB and returns the Brightness. Bri is updated unless keep_bri is true uint8_t setRGB(uint8_t r, uint8_t g, uint8_t b, bool keep_bri = false) { uint16_t hue; uint8_t sat; #ifdef DEBUG_LIGHT AddLog(LOG_LEVEL_DEBUG_MORE, "LightStateClass::setRGB RGB input (%d %d %d)", r, g, b); #endif uint32_t max = (r > g && r > b) ? r : (g > b) ? g : b; // 0..255 if (0 == max) { r = g = b = 255; setColorMode(LCM_CT); // try deactivating RGB, setColorMode() will check if this is legal } else { if (255 > max) { // we need to normalize rgb r = changeUIntScale(r, 0, max, 0, 255); g = changeUIntScale(g, 0, max, 0, 255); b = changeUIntScale(b, 0, max, 0, 255); } addRGBMode(); } if (!keep_bri) { _briRGB = (_color_mode & LCM_RGB) ? max : 0; } RgbToHsb(r, g, b, &hue, &sat, nullptr); _r = r; _g = g; _b = b; _hue = hue; _hue16 = changeUIntScale(_hue, 0, 360, 0, 65535); _sat = sat; #ifdef DEBUG_LIGHT AddLog(LOG_LEVEL_DEBUG_MORE, "LightStateClass::setRGB RGB raw (%d %d %d) HS (%d %d) bri (%d)", _r, _g, _b, _hue, _sat, _briRGB); #endif return max; } void setHS(uint16_t hue, uint8_t sat) { uint8_t r, g, b; HsToRgb(hue, sat, &r, &g, &b); _r = r; _g = g; _b = b; _hue = hue; _hue16 = changeUIntScale(_hue, 0, 360, 0, 65535); _sat = sat; addRGBMode(); #ifdef DEBUG_LIGHT AddLog(LOG_LEVEL_DEBUG_MORE, "LightStateClass::setHS HS (%d %d) rgb (%d %d %d)", hue, sat, r, g, b); AddLog(LOG_LEVEL_DEBUG_MORE, "LightStateClass::setHS RGB raw (%d %d %d) HS (%d %d) bri (%d)", _r, _g, _b, _hue, _sat, _briRGB); #endif } // version taking Hue over 0..65535 as sent by Zigbee or Hue void setH16S(uint16_t hue16, uint8_t sat) { uint16_t hue360 = changeUIntScale(hue16, 0, 65535, 0, 360); setHS(hue360, sat); _hue16 = hue16; // keep a higher resolution version in memory } uint16_t getHue16(void) const { return _hue16; } // set all 5 channels at once, don't modify the values in ANY way // Channels are: R G B CW WW void setChannelsRaw(uint8_t *channels) { _r = channels[0]; _g = channels[1]; _b = channels[2]; _wc = channels[3]; _ww = channels[4]; } // set all 5 channels at once. // Channels are: R G B CW WW // Brightness is automatically recalculated to adjust channels to the desired values void setChannels(uint8_t *channels) { setRGB(channels[0], channels[1], channels[2]); setCW(channels[3], channels[4], true); // free range for WC and WW uint8_t r = channels[0]; uint8_t g = channels[1]; uint8_t b = channels[2]; uint8_t cw = channels[3]; uint8_t ww = channels[4]; _briRGB_orig = (r > g && r > b) ? r : (g > b) ? g : b; _briCT_orig = (cw > ww) ? cw : ww; #ifdef DEBUG_LIGHT AddLog(LOG_LEVEL_DEBUG_MORE, "LightStateClass::setChannels (%d %d %d %d %d)", channels[0], channels[1], channels[2], channels[3], channels[4]); AddLog(LOG_LEVEL_DEBUG_MORE, "LightStateClass::setChannels CT (%d) briRGB (%d) briCT (%d)", _ct, _briRGB, _briCT); AddLog(LOG_LEVEL_DEBUG_MORE, "LightStateClass::setChannels Actuals (%d %d %d %d %d)", _r, _g, _b, _wc, _ww); #endif } }; /*********************************************************************************************\ * LightStateClass implementation \*********************************************************************************************/ class LightControllerClass { private: LightStateClass *_state; // are RGB and CT linked, i.e. if we set CT then RGB channels are off bool _ct_rgb_linked = true; bool _pwm_multi_channels = false; // treat each channel as independant dimmer public: LightControllerClass(LightStateClass& state) { _state = &state; } void setSubType(uint8_t sub_type) { _state->setSubType(sub_type); } inline bool setCTRGBLinked(bool ct_rgb_linked) { bool prev = _ct_rgb_linked; if (_pwm_multi_channels) { _ct_rgb_linked = false; // force to false if _pwm_multi_channels is set } else { _ct_rgb_linked = ct_rgb_linked; } return prev; } inline bool isCTRGBLinked() { return _ct_rgb_linked; } inline bool setPWMMultiChannel(bool pwm_multi_channels) { bool prev = _pwm_multi_channels; _pwm_multi_channels = pwm_multi_channels; if (pwm_multi_channels) setCTRGBLinked(false); // if pwm multi channel, then unlink RGB and CT return prev; } inline bool isPWMMultiChannel(void) { return _pwm_multi_channels; } #ifdef DEBUG_LIGHT void debugLogs() { uint8_t r,g,b,c,w; _state->getActualRGBCW(&r,&g,&b,&c,&w); AddLog(LOG_LEVEL_DEBUG_MORE, "LightControllerClass::debugLogs rgb (%d %d %d) cw (%d %d)", r, g, b, c, w); AddLog(LOG_LEVEL_DEBUG_MORE, "LightControllerClass::debugLogs lightCurrent (%d %d %d %d %d)", Light.current_color[0], Light.current_color[1], Light.current_color[2], Light.current_color[3], Light.current_color[4]); } #endif void loadSettings() { #ifdef DEBUG_LIGHT AddLog(LOG_LEVEL_DEBUG_MORE, "LightControllerClass::loadSettings Settings->light_color (%d %d %d %d %d - %d)", Settings->light_color[0], Settings->light_color[1], Settings->light_color[2], Settings->light_color[3], Settings->light_color[4], Settings->light_dimmer); AddLog(LOG_LEVEL_DEBUG_MORE, "LightControllerClass::loadSettings light_type/sub (%d %d)", TasmotaGlobal.light_type, Light.subtype); #endif if (_pwm_multi_channels) { _state->setChannelsRaw(Settings->light_color); } else { // first try setting CW, if zero, it select RGB mode _state->setCW(Settings->light_color[3], Settings->light_color[4], true); _state->setRGB(Settings->light_color[0], Settings->light_color[1], Settings->light_color[2]); // only if non-multi channel // We apply dimmer in priority to RGB uint8_t bri = _state->DimmerToBri(Settings->light_dimmer); // The default values are #FFFFFFFFFF, in this case we avoid setting all channels // at the same time, see #6534 and #8120 if ((DEFAULT_LIGHT_COMPONENT == Settings->light_color[0]) && (DEFAULT_LIGHT_COMPONENT == Settings->light_color[1]) && (DEFAULT_LIGHT_COMPONENT == Settings->light_color[2]) && (DEFAULT_LIGHT_COMPONENT == Settings->light_color[3]) && (DEFAULT_LIGHT_COMPONENT == Settings->light_color[4]) && (DEFAULT_LIGHT_DIMMER == Settings->light_dimmer) ) { if ((LST_COLDWARM == Light.subtype) || (LST_RGBCW == Light.subtype)) { _state->setCW(255, 0); // avoid having both white channels at 100%, zero second channel (#see 8120) } _state->setBriCT(bri); _state->setBriRGB(bri); _state->setColorMode(LCM_RGB); } if (Settings->light_color[0] + Settings->light_color[1] + Settings->light_color[2] > 0) { _state->setBriRGB(bri); } else { _state->setBriCT(bri); } } } void changeCTB(uint16_t new_ct, uint8_t briCT) { /* Color Temperature (https://developers.meethue.com/documentation/core-concepts) * * ct = 153 = 6500K = Cold = CCWW = FF00 * ct = 500 = 2000K = Warm = CCWW = 00FF */ // don't set CT if not supported if ((LST_COLDWARM != Light.subtype) && (LST_RGBW > Light.subtype)) { return; } _state->setCT(new_ct); _state->setBriCT(briCT); if (_ct_rgb_linked) { _state->setColorMode(LCM_CT); } // try to force CT saveSettings(); calcLevels(); //debugLogs(); } void changeDimmer(uint8_t dimmer, uint32_t mode = 0) { uint8_t bri = changeUIntScale(dimmer, 0, 100, 0, 255); switch (mode) { case 1: changeBriRGB(bri); if (_ct_rgb_linked) { _state->setColorMode(LCM_RGB); } // try to force CT break; case 2: changeBriCT(bri); if (_ct_rgb_linked) { _state->setColorMode(LCM_CT); } // try to force CT break; default: changeBri(bri); break; } } void changeBri(uint8_t bri) { _state->setBri(bri); saveSettings(); calcLevels(); } void changeBriRGB(uint8_t bri) { _state->setBriRGB(bri); saveSettings(); calcLevels(); } void changeBriCT(uint8_t bri) { _state->setBriCT(bri); saveSettings(); calcLevels(); } void changeRGB(uint8_t r, uint8_t g, uint8_t b, bool keep_bri = false) { _state->setRGB(r, g, b, keep_bri); if (_ct_rgb_linked) { _state->setColorMode(LCM_RGB); } // try to force RGB saveSettings(); calcLevels(); } // calculate the levels for each channel // if no parameter, results are stored in Light.current_color void calcLevels(uint8_t *current_color = nullptr) { uint8_t r,g,b,c,w,briRGB,briCT; if (current_color == nullptr) { current_color = Light.current_color; } if (_pwm_multi_channels) { // if PWM multi channel, no more transformation required _state->getChannelsRaw(current_color); return; } _state->getActualRGBCW(&r,&g,&b,&c,&w); briRGB = _state->getBriRGB(); briCT = _state->getBriCT(); current_color[0] = current_color[1] = current_color[2] = 0; current_color[3] = current_color[4] = 0; switch (Light.subtype) { case LST_NONE: current_color[0] = 255; break; case LST_SINGLE: current_color[0] = briRGB; break; case LST_COLDWARM: current_color[0] = c; current_color[1] = w; break; case LST_RGBW: case LST_RGBCW: if (LST_RGBCW == Light.subtype) { current_color[3] = c; current_color[4] = w; } else { current_color[3] = briCT; } // continue case LST_RGB: current_color[0] = r; current_color[1] = g; current_color[2] = b; break; } } void changeHSB(uint16_t hue, uint8_t sat, uint8_t briRGB) { _state->setHS(hue, sat); _state->setBriRGB(briRGB); if (_ct_rgb_linked) { _state->setColorMode(LCM_RGB); } // try to force RGB saveSettings(); calcLevels(); } /* special version for Alexa Hue maintaining a 16 bits Hue value */ void changeH16SB(uint16_t hue, uint8_t sat, uint8_t briRGB) { _state->setH16S(hue, sat); _state->setBriRGB(briRGB); if (_ct_rgb_linked) { _state->setColorMode(LCM_RGB); } // try to force RGB saveSettings(); calcLevels(); } // save the current light state to Settings void saveSettings() { if (Light.pwm_multi_channels) { // simply save each channel _state->getChannelsRaw(Settings->light_color); Settings->light_dimmer = 100; // arbitrary value, unused in this mode } else { uint8_t cm = _state->getColorMode(); memset(&Settings->light_color[0], 0, sizeof(Settings->light_color)); // clear all channels if (LCM_RGB & cm) { // can be either LCM_RGB or LCM_BOTH _state->getRGB(&Settings->light_color[0], &Settings->light_color[1], &Settings->light_color[2]); Settings->light_dimmer = _state->BriToDimmer(_state->getBriRGB()); // anyways we always store RGB with BrightnessRGB if (LCM_BOTH == cm) { // then store at actual brightness CW/WW if dual mode _state->getActualRGBCW(nullptr, nullptr, nullptr, &Settings->light_color[3], &Settings->light_color[4]); } } else if (LCM_CT == cm) { // cm can only be LCM_CT _state->getCW(&Settings->light_color[3], &Settings->light_color[4]); Settings->light_dimmer = _state->BriToDimmer(_state->getBriCT()); } } #ifdef DEBUG_LIGHT AddLog(LOG_LEVEL_DEBUG_MORE, "LightControllerClass::saveSettings Settings->light_color (%d %d %d %d %d - %d)", Settings->light_color[0], Settings->light_color[1], Settings->light_color[2], Settings->light_color[3], Settings->light_color[4], Settings->light_dimmer); #endif } // set all 5 channels at once. // Channels are: R G B CW WW // Brightness is automatically recalculated to adjust channels to the desired values // if channelsmapped is true, CCT/RGB mapping has already been done void changeChannels(uint8_t *channels, bool channelsmapped=false) { if (Light.pwm_multi_channels) { _state->setChannelsRaw(channels); } else if (LST_COLDWARM == Light.subtype && ! channelsmapped) { // remap channels 0-1 to 3-4 if cold/warm uint8_t remapped_channels[5] = {0,0,0,channels[0],channels[1]}; _state->setChannels(remapped_channels); } else { _state->setChannels(channels); } saveSettings(); calcLevels(); } }; // the singletons for light state and Light Controller LightStateClass light_state = LightStateClass(); LightControllerClass light_controller = LightControllerClass(light_state); /*********************************************************************************************\ * CT (White Color Temperature) \*********************************************************************************************/ // // Ensure that invariants for Virtual CT are good: // - CT_MIN <= ct[0] <= ct[1] <= ct[2] <= CT_MAX #ifdef USE_LIGHT_VIRTUAL_CT void checkVirtualCT(void) { if (Light.vct_ct[0] < CT_MIN) { Light.vct_ct[0] = CT_MIN; } if (Light.vct_ct[CT_PIVOTS-1] > CT_MAX) { Light.vct_ct[CT_PIVOTS-1] = CT_MAX; } for (uint32_t i = 0; i < CT_PIVOTS-1; i++) { if (Light.vct_ct[i+1] < Light.vct_ct[i]) { Light.vct_ct[i+1] = Light.vct_ct[i]; } } } #endif // USE_LIGHT_VIRTUAL_CT #ifdef USE_LIGHT_VIRTUAL_CT // Init default values for virtual CT, depending on the number of channels void initCTRange(uint32_t channels) { if (channels == 4) { if (Settings->flag4.virtual_ct_cw) { // Hardware White is Cold White memcpy_P(Light.vct_color[0], CT_PIVOTS_CWW, sizeof(Light.vct_color[0])); // Cold white memcpy_P(Light.vct_color[1], CT_PIVOTS_RGB, sizeof(Light.vct_color[1])); // Warm white } else { // Hardware White is Warm White memcpy_P(Light.vct_color[0], CT_PIVOTS_RGB, sizeof(Light.vct_color[0])); // Cold white memcpy_P(Light.vct_color[1], CT_PIVOTS_CWW, sizeof(Light.vct_color[1])); // Warm white } } else if (channels == 5) { memcpy_P(Light.vct_color[0], CT_PIVOTS_CWW, sizeof(Light.vct_color[0])); // Cold white memcpy_P(Light.vct_color[1], CT_PIVOTS_WWW, sizeof(Light.vct_color[1])); // Warm white } else { memcpy_P(Light.vct_color[0], CT_PIVOTS_RGB, sizeof(Light.vct_color[0])); // Cold white memcpy_P(Light.vct_color[1], CT_PIVOTS_RGB, sizeof(Light.vct_color[1])); // Warm white } for (uint32_t i = 1; i < CT_PIVOTS-1; i++) { memcpy_P(Light.vct_color[i+1], Light.vct_color[i], sizeof(Light.vct_color[0])); // Copy slot 1 into slot 2 (slot 2 in unused) } checkVirtualCT(); } #endif // USE_LIGHT_VIRTUAL_CT void getCTRange(uint16_t * min_ct, uint16_t * max_ct) { if (min_ct != nullptr) { *min_ct = Light.vct_ct[0]; } if (max_ct != nullptr) { *max_ct = Light.vct_ct[CT_PIVOTS-1]; } } void setCTRange(uint16_t ct_min, uint16_t ct_max) { Light.vct_ct[0] = ct_min; for (uint32_t i = 1; i < CT_PIVOTS; i++) { Light.vct_ct[i] = ct_max; // all slots above [1] are not used } } void setAlexaCTRange(void) { // depending on SetOption82, full or limited CT range if (Settings->flag4.alexa_ct_range) { setCTRange(CT_MIN_ALEXA, CT_MAX_ALEXA); } else { setCTRange(CT_MIN, CT_MAX); } } /********************************************************************************************/ void LightPwmOffset(uint32_t offset) { Light.pwm_offset = offset; } bool LightModuleInit(void) { TasmotaGlobal.light_type = LT_BASIC; // Use basic PWM control if SetOption15 = 0 if (Settings->flag.pwm_control) { // SetOption15 - Switch between commands PWM or COLOR/DIMMER/CT/CHANNEL for (uint32_t i = 0; i < LST_MAX; i++) { if (PinUsed(GPIO_PWM1, i)) { TasmotaGlobal.light_type++; } // Use Dimmer/Color control for all PWM as SetOption15 = 1 } } TasmotaGlobal.light_driver = 0; if (XlgtCall(FUNC_MODULE_INIT)) { // serviced } #ifdef ESP8266 else if (SONOFF_BN == TasmotaGlobal.module_type) { // PWM Single color led (White) TasmotaGlobal.light_type = LT_PWM1; } else if (SONOFF_LED == TasmotaGlobal.module_type) { // PWM Dual color led (White warm and cold) if (!TasmotaGlobal.my_module.io[4]) { // Fix Sonoff Led instabilities pinMode(4, OUTPUT); // Stop floating outputs digitalWrite(4, LOW); } if (!TasmotaGlobal.my_module.io[5]) { pinMode(5, OUTPUT); // Stop floating outputs digitalWrite(5, LOW); } if (!TasmotaGlobal.my_module.io[14]) { pinMode(14, OUTPUT); // Stop floating outputs digitalWrite(14, LOW); } TasmotaGlobal.light_type = LT_PWM2; } #endif // ESP8266 #ifdef USE_PWM_DIMMER else if (PWM_DIMMER == TasmotaGlobal.module_type) { TasmotaGlobal.light_type = Settings->pwm_dimmer_cfg.pwm_count + 1; } #endif // USE_PWM_DIMMER if (TasmotaGlobal.light_type > LT_BASIC) { UpdateDevicesPresent(1); } // post-process for lights uint32_t pwm_channels = (TasmotaGlobal.light_type & 7) > LST_MAX ? LST_MAX : (TasmotaGlobal.light_type & 7); if (Settings->flag3.pwm_multi_channels) { // SetOption68 - Enable multi-channels PWM instead of Color PWM if (0 == pwm_channels) { pwm_channels = 1; } UpdateDevicesPresent(pwm_channels - 1); // add the pwm channels controls at the end } else if ((Settings->param[P_RGB_REMAP] & 128) && (LST_RGBW <= pwm_channels)) { // SetOption37 // if RGBW or RGBCW, and SetOption37 >= 128, we manage RGB and W separately, hence adding a device UpdateDevicesPresent(1); } else { #ifdef USE_LIGHT_VIRTUAL_CT initCTRange(pwm_channels); if ((Settings->flag4.virtual_ct) && (LST_RGB <= pwm_channels)) { Light.virtual_ct = true; // enabled TasmotaGlobal.light_type += 5 - pwm_channels; // pretend it is a 5 channels bulb } #endif // USE_LIGHT_VIRTUAL_CT } return (TasmotaGlobal.light_type > LT_BASIC); } // compute actual PWM min/max values from DimmerRange // must be called when DimmerRange is changed or LedTable void LightCalcPWMRange(void) { uint16_t pwm_min, pwm_max; pwm_min = change8to10(LightStateClass::DimmerToBri(Settings->dimmer_hw_min)); // default 0 pwm_max = change8to10(LightStateClass::DimmerToBri(Settings->dimmer_hw_max)); // default 100 if (Settings->light_correction) { pwm_min = ledGamma10_10(pwm_min); // apply gamma correction pwm_max = ledGamma10_10(pwm_max); // 0..1023 } pwm_min = pwm_min > 0 ? changeUIntScale(pwm_min, 1, 1023, 1, Settings->pwm_range) : 0; // adapt range but keep zero and non-zero values pwm_max = changeUIntScale(pwm_max, 1, 1023, 1, Settings->pwm_range); // pwm_max cannot be zero Light.pwm_min = pwm_min; Light.pwm_max = pwm_max; //AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("LightCalcPWMRange %d %d - %d %d"), Settings->dimmer_hw_min, Settings->dimmer_hw_max, Light.pwm_min, Light.pwm_max); } void LightSetScheme(uint32_t scheme) { if (!scheme && Settings->light_scheme) { Light.update = true; } Settings->light_scheme = scheme; } void LightInit(void) { // move white blend mode from deprecated `RGBWWTable` to `SetOption105` if (0 == Settings->rgbwwTable[4]) { Settings->flag4.white_blend_mode = true; Settings->rgbwwTable[4] = 255; // set RGBWWTable value to its default } Light.device = TasmotaGlobal.devices_present; Light.subtype = (TasmotaGlobal.light_type & 7) > LST_MAX ? LST_MAX : (TasmotaGlobal.light_type & 7); // Always 0 - LST_MAX (5) Light.pwm_multi_channels = Settings->flag3.pwm_multi_channels; // SetOption68 - Enable multi-channels PWM instead of Color PWM if (LST_RGBW <= Light.subtype) { // only change if RGBW or RGBCW // do not allow independant RGB and WC colors bool ct_rgb_linked = !(Settings->param[P_RGB_REMAP] & 128); // SetOption37 light_controller.setCTRGBLinked(ct_rgb_linked); } if ((LST_SINGLE <= Light.subtype) && Light.pwm_multi_channels) { // we treat each PWM channel as an independant one, hence we switch to light_controller.setPWMMultiChannel(true); Light.device = TasmotaGlobal.devices_present - Light.subtype + 1; // adjust if we also have relays } else if (!light_controller.isCTRGBLinked()) { // if RGBW or RGBCW, and SetOption37 >= 128, we manage RGB and W separately Light.device--; // we take the last two devices as lights } LightCalcPWMRange(); #ifdef DEBUG_LIGHT AddLog(LOG_LEVEL_DEBUG_MORE, "LightInit Light.pwm_multi_channels=%d Light.subtype=%d Light.device=%d TasmotaGlobal.devices_present=%d", Light.pwm_multi_channels, Light.subtype, Light.device, TasmotaGlobal.devices_present); #endif light_controller.setSubType(Light.subtype); light_controller.loadSettings(); setAlexaCTRange(); light_controller.calcLevels(); // calculate the initial values (#8058) if (LST_SINGLE == Light.subtype) { Settings->light_color[0] = 255; // One channel only supports Dimmer but needs max color } if (TasmotaGlobal.light_type < LT_PWM6) { // PWM for (uint32_t i = 0; i < TasmotaGlobal.light_type; i++) { Settings->pwm_value[i] = 0; // Disable direct PWM control if (PinUsed(GPIO_PWM1, i)) { #ifdef ESP8266 pinMode(Pin(GPIO_PWM1, i), OUTPUT); #endif // ESP8266 // For ESP32, the PWM are already attached by GpioInit() - GpioInitPwm() } } if (PinUsed(GPIO_ARIRFRCV)) { if (PinUsed(GPIO_ARIRFSEL)) { pinMode(Pin(GPIO_ARIRFSEL), OUTPUT); digitalWrite(Pin(GPIO_ARIRFSEL), 1); // Turn off RF } } } uint32_t max_scheme = Light.max_scheme; if (Light.subtype < LST_RGB) { max_scheme = LS_POWER; } if ((LS_WAKEUP == Settings->light_scheme) || (Settings->light_scheme > max_scheme)) { LightSetScheme(LS_POWER); } Light.power = 0; Light.update = true; Light.wakeup_active = 0; if (0 == Settings->light_wakeup) { Settings->light_wakeup = 60; // Fix divide by zero exception 0 in Animate } if (Settings->flag4.fade_at_startup) { Light.fade_initialized = true; // consider fade intialized starting from black } LightUpdateColorMapping(); } void LightUpdateColorMapping(void) { uint8_t param = Settings->param[P_RGB_REMAP] & 127; // SetOption37 if (param > 119){ param = 0; } uint8_t tmp[] = {0,1,2,3,4}; Light.color_remap[0] = tmp[param / 24]; for (uint32_t i = param / 24; i<4; ++i){ tmp[i] = tmp[i+1]; } param = param % 24; Light.color_remap[1] = tmp[(param / 6)]; for (uint32_t i = param / 6; i<3; ++i){ tmp[i] = tmp[i+1]; } param = param % 6; Light.color_remap[2] = tmp[(param / 2)]; for (uint32_t i = param / 2; i<2; ++i){ tmp[i] = tmp[i+1]; } param = param % 2; Light.color_remap[3] = tmp[param]; Light.color_remap[4] = tmp[1-param]; Light.update = true; //AddLog(LOG_LEVEL_DEBUG, PSTR("%d colors: %d %d %d %d %d") ,Settings->param[P_RGB_REMAP], Light.color_remap[0],Light.color_remap[1],Light.color_remap[2],Light.color_remap[3],Light.color_remap[4]); } uint8_t LightGetDimmer(uint8_t dimmer) { return light_state.getDimmer(dimmer); } void LightSetDimmer(uint8_t dimmer) { light_controller.changeDimmer(dimmer); } void LightGetHSB(uint16_t *hue, uint8_t *sat, uint8_t *bri) { light_state.getHSB(hue, sat, bri); } void LightGetXY(float *X, float *Y) { light_state.getXY(X, Y); } // If SetOption68 is set, get the brightness for a specific device uint8_t LightGetBri(uint8_t device) { uint8_t bri = 254; // default value if relay if (Light.pwm_multi_channels) { if ((device >= Light.device) && (device < Light.device + LST_MAX) && (device <= TasmotaGlobal.devices_present)) { bri = Light.current_color[device - Light.device]; } } else if (light_controller.isCTRGBLinked()) { // standard behavior if (device == Light.device) { bri = light_state.getBri(); } } else { // unlinked if (device == Light.device) { bri = light_state.getBriRGB(); } else if (device == Light.device + 1) { bri = light_state.getBriCT(); } } return bri; } // If SetOption68 is set, set the brightness for a specific device void LightSetBri(uint8_t device, uint8_t bri) { if (Light.pwm_multi_channels) { if ((device >= Light.device) && (device < Light.device + LST_MAX) && (device <= TasmotaGlobal.devices_present)) { Light.current_color[device - Light.device] = bri; light_controller.changeChannels(Light.current_color); } } else if (light_controller.isCTRGBLinked()) { // standard if (device == Light.device) { light_controller.changeBri(bri); } } else { // unlinked if (device == Light.device) { light_controller.changeBriRGB(bri); } else if (device == Light.device + 1) { light_controller.changeBriCT(bri); } } } void LightSetBriScaled(uint8_t bri) { // change both dimmers, retain ratio between white and color channels uint32_t bri_rgb = light_state.getBriRGBOrig(); uint32_t bri_ct = light_state.getBriCTOrig(); #ifdef DEBUG_LIGHT AddLog(LOG_LEVEL_DEBUG, "LightSetBri bri:%d, bri_rgb:%d, bri_ct: %d", bri, bri_rgb, bri_ct); #endif uint32_t max_bri = bri_rgb > bri_ct ? bri_rgb : bri_ct; if (max_bri == 0) { bri_rgb = bri; bri_ct = bri; } else { bri_rgb = changeUIntScale(bri_rgb, 0, max_bri, 0, bri); bri_ct = changeUIntScale(bri_ct, 0, max_bri, 0, bri); } #ifdef DEBUG_LIGHT AddLog(LOG_LEVEL_DEBUG, "LightSetBri new bri_rgb:%d, new bri_ct: %d", bri_rgb, bri_ct); #endif light_controller.changeBriRGB(bri_rgb); light_controller.changeBriCT(bri_ct); } void LightColorOffset(int32_t offset) { uint16_t hue; uint8_t sat; light_state.getHSB(&hue, &sat, nullptr); // Allow user control over Saturation int16_t hue_new = hue + offset; if (hue_new < 0) { hue_new += 359; } if (hue_new > 359) { hue_new -= 359; } hue = hue_new; if (!Light.pwm_multi_channels) { light_state.setHS(hue, sat); } else { light_state.setHS(hue, 255); light_state.setBri(255); // If multi-channel, force bri to max, it will be later dimmed to correct value } light_controller.calcLevels(Light.new_color); } bool LightColorTempOffset(int32_t offset) { int32_t ct = LightGetColorTemp(); if (0 == ct) { return false; } // CT not supported ct += offset; if (ct < CT_MIN) { ct = CT_MIN; } else if (ct > CT_MAX) { ct = CT_MAX; } LightSetColorTemp(ct); return true; } void LightSetColorTemp(uint16_t ct) { /* Color Temperature (https://developers.meethue.com/documentation/core-concepts) * * ct = 153 mirek = 6500K = Cold = CCWW = FF00 * ct = 500 mirek = 2000K = Warm = CCWW = 00FF */ // don't set CT if not supported if ((LST_COLDWARM != Light.subtype) && (LST_RGBCW != Light.subtype)) { return; } light_controller.changeCTB(ct, light_state.getBriCT()); } uint16_t LightGetColorTemp(void) { // don't calculate CT for unsupported devices if ((LST_COLDWARM != Light.subtype) && (LST_RGBCW != Light.subtype)) { return 0; } return (light_state.getColorMode() & LCM_CT) ? light_state.getCT() : 0; } void LightSetSignal(uint16_t lo, uint16_t hi, uint16_t value) { /* lo - below lo is green hi - above hi is red */ if (Settings->flag.light_signal) { // SetOption18 - Pair light signal with CO2 sensor uint16_t signal = changeUIntScale(value, lo, hi, 0, 255); // 0..255 // AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "Light signal %d"), signal); light_controller.changeRGB(signal, 255 - signal, 0, true); // keep bri LightSetScheme(LS_POWER); if (0 == light_state.getBri()) { light_controller.changeBri(50); } } } // convert channels to string, use Option 17 to foce decimal, unless force_hex char* LightGetColor(char* scolor, boolean force_hex = false) { if ((0 == Settings->light_scheme) || (!Light.pwm_multi_channels)) { light_controller.calcLevels(); // recalculate levels only if Scheme 0, otherwise we mess up levels } scolor[0] = '\0'; for (uint32_t i = 0; i < Light.subtype; i++) { if (!force_hex && Settings->flag.decimal_text) { // SetOption17 - Switch between decimal or hexadecimal output snprintf_P(scolor, LIGHT_COLOR_SIZE, PSTR("%s%s%d"), scolor, (i > 0) ? "," : "", Light.current_color[i]); } else { snprintf_P(scolor, LIGHT_COLOR_SIZE, PSTR("%s%02X"), scolor, Light.current_color[i]); } } return scolor; } void LightPowerOn(void) { if (light_state.getBri() && !(Light.power)) { ExecuteCommandPower(Light.device, POWER_ON, SRC_LIGHT); } } void ResponseLightState(uint8_t append) { char scolor[LIGHT_COLOR_SIZE]; char scommand[33]; bool unlinked = !light_controller.isCTRGBLinked() && (Light.subtype >= LST_RGBW); // there are 2 power and dimmers for RGB and White if (append) { ResponseAppend_P(PSTR(",")); } else { Response_P(PSTR("{")); } if (!Light.pwm_multi_channels) { if (unlinked) { // RGB and W are unlinked, we display the second Power/Dimmer ResponseAppend_P(PSTR("\"" D_RSLT_POWER "%d\":\"%s\",\"" D_CMND_DIMMER "1\":%d" ",\"" D_RSLT_POWER "%d\":\"%s\",\"" D_CMND_DIMMER "2\":%d"), Light.device, GetStateText(Light.power & 1), light_state.getDimmer(1), Light.device + 1, GetStateText(Light.power & 2 ? 1 : 0), light_state.getDimmer(2)); } else { GetPowerDevice(scommand, Light.device, sizeof(scommand), Settings->flag.device_index_enable); // SetOption26 - Switch between POWER or POWER1 ResponseAppend_P(PSTR("\"%s\":\"%s\",\"" D_CMND_DIMMER "\":%d"), scommand, GetStateText(Light.power & 1), light_state.getDimmer()); } if (Light.subtype > LST_SINGLE) { ResponseAppend_P(PSTR(",\"" D_CMND_COLOR "\":\"%s\""), LightGetColor(scolor)); if (LST_RGB <= Light.subtype) { uint16_t hue; uint8_t sat, bri; light_state.getHSB(&hue, &sat, &bri); sat = changeUIntScale(sat, 0, 255, 0, 100); bri = changeUIntScale(bri, 0, 255, 0, 100); ResponseAppend_P(PSTR(",\"" D_CMND_HSBCOLOR "\":\"%d,%d,%d\""), hue,sat,bri); } // Add White level if ((LST_COLDWARM == Light.subtype) || (LST_RGBW <= Light.subtype)) { ResponseAppend_P(PSTR(",\"" D_CMND_WHITE "\":%d"), light_state.getDimmer(2)); } // Add CT if ((LST_COLDWARM == Light.subtype) || (LST_RGBCW == Light.subtype)) { ResponseAppend_P(PSTR(",\"" D_CMND_COLORTEMPERATURE "\":%d"), light_state.getCT()); } // Add status for each channel ResponseAppend_P(PSTR(",\"" D_CMND_CHANNEL "\":[" )); for (uint32_t i = 0; i < Light.subtype; i++) { uint8_t channel_raw = Light.current_color[i]; uint8_t channel = changeUIntScale(channel_raw,0,255,0,100); // if non null, force to be at least 1 if ((0 == channel) && (channel_raw > 0)) { channel = 1; } ResponseAppend_P(PSTR("%s%d" ), (i > 0 ? "," : ""), channel); } ResponseAppend_P(PSTR("]")); } if (append) { if (Light.subtype >= LST_RGB) { ResponseAppend_P(PSTR(",\"" D_CMND_SCHEME "\":%d"), Settings->light_scheme); } if (Light.max_scheme > LS_MAX) { ResponseAppend_P(PSTR(",\"" D_CMND_WIDTH "\":%d"), Settings->light_width); } ResponseAppend_P(PSTR(",\"" D_CMND_FADE "\":\"%s\",\"" D_CMND_SPEED "\":%d,\"" D_CMND_LEDTABLE "\":\"%s\""), GetStateText(Settings->light_fade), Settings->light_speed, GetStateText(Settings->light_correction)); } } else { // Light.pwm_multi_channels for (uint32_t i = 0; i < Light.subtype; i++) { GetPowerDevice(scommand, Light.device + i, sizeof(scommand), 1); uint32_t light_power_masked = Light.power & (1 << i); // the Light.power value for this device light_power_masked = light_power_masked ? 1 : 0; // convert to on/off ResponseAppend_P(PSTR("\"%s\":\"%s\",\"" D_CMND_CHANNEL "%d\":%d,"), scommand, GetStateText(light_power_masked), Light.device + i, changeUIntScale(Light.current_color[i], 0, 255, 0, 100)); } ResponseAppend_P(PSTR("\"" D_CMND_COLOR "\":\"%s\""), LightGetColor(scolor)); } // Light.pwm_multi_channels if (!append) { ResponseJsonEnd(); } } void LightPreparePower(power_t channels = 0xFFFFFFFF) { // 1 = only RGB, 2 = only CT, 3 = both RGB and CT #ifdef DEBUG_LIGHT AddLog(LOG_LEVEL_DEBUG, "LightPreparePower power=%d Light.power=%d", TasmotaGlobal.power, Light.power); #endif // If multi-channels, then we only switch off channels with a value of zero if (Light.pwm_multi_channels) { for (uint32_t i = 0; i < Light.subtype; i++) { if (bitRead(channels, i)) { // if channel is non-null, channel is supposed to be on, but it is off, do Power On if ((Light.current_color[i]) && (!bitRead(Light.power, i))) { if (!Settings->flag.not_power_linked) { // SetOption20 - Control power in relation to Dimmer/Color/Ct changes ExecuteCommandPower(Light.device + i, POWER_ON_NO_STATE, SRC_LIGHT); } } else { // if channel is zero and channel is on, set it off if ((0 == Light.current_color[i]) && bitRead(Light.power, i)) { ExecuteCommandPower(Light.device + i, POWER_OFF_NO_STATE, SRC_LIGHT); } } #ifdef USE_DOMOTICZ DomoticzUpdatePowerState(Light.device + i); #endif // USE_DOMOTICZ #ifdef USE_KNX KnxUpdateLight(); #endif } } } else { if (light_controller.isCTRGBLinked()) { // linked, standard if (light_state.getBri() && !(Light.power)) { if (!Settings->flag.not_power_linked) { // SetOption20 - Control power in relation to Dimmer/Color/Ct changes ExecuteCommandPower(Light.device, POWER_ON_NO_STATE, SRC_LIGHT); } } else if (!light_state.getBri() && Light.power) { ExecuteCommandPower(Light.device, POWER_OFF_NO_STATE, SRC_LIGHT); } } else { // RGB if (channels & 1) { if (light_state.getBriRGB() && !(Light.power & 1)) { if (!Settings->flag.not_power_linked) { // SetOption20 - Control power in relation to Dimmer/Color/Ct changes ExecuteCommandPower(Light.device, POWER_ON_NO_STATE, SRC_LIGHT); } } else if (!light_state.getBriRGB() && (Light.power & 1)) { ExecuteCommandPower(Light.device, POWER_OFF_NO_STATE, SRC_LIGHT); } } // White CT if (channels & 2) { if (light_state.getBriCT() && !(Light.power & 2)) { if (!Settings->flag.not_power_linked) { // SetOption20 - Control power in relation to Dimmer/Color/Ct changes ExecuteCommandPower(Light.device + 1, POWER_ON_NO_STATE, SRC_LIGHT); } } else if (!light_state.getBriCT() && (Light.power & 2)) { ExecuteCommandPower(Light.device + 1, POWER_OFF_NO_STATE, SRC_LIGHT); } } } #ifdef USE_DOMOTICZ DomoticzUpdatePowerState(Light.device); #endif // USE_DOMOTICZ #ifdef USE_KNX KnxUpdateLight(); #endif } if (Settings->flag3.hass_tele_on_power) { // SetOption59 - Send tele/%topic%/STATE in addition to stat/%topic%/RESULT MqttPublishTeleState(); } #ifdef DEBUG_LIGHT AddLog(LOG_LEVEL_DEBUG, "LightPreparePower End power=%d Light.power=%d", TasmotaGlobal.power, Light.power); #endif Light.power = TasmotaGlobal.power >> (Light.device - 1); // reset next state, works also with unlinked RGB/CT ResponseLightState(0); } #ifdef USE_LIGHT_PALETTE void LightSetPaletteEntry(void) { uint8_t bri = light_state.getBri(); uint8_t * palette_entry = &Light.palette[Light.wheel * Light.subtype]; for (int i = 0; i < Light.subtype; i++) { Light.new_color[i] = changeUIntScale(palette_entry[i], 0, 255, 0, bri); } light_state.setChannelsRaw(Light.new_color); if (!Light.pwm_multi_channels) { light_state.setCW(Light.new_color[3], Light.new_color[4], true); if (Light.new_color[0] || Light.new_color[1] || Light.new_color[2]) light_state.addRGBMode(); } } #endif // USE_LIGHT_PALETTE void LightCycleColor(int8_t direction) { // if (Light.strip_timer_counter % (Settings->light_speed * 2)) { return; } // Speed 1: 24sec, 2: 48sec, 3: 72sec, etc if (Settings->light_speed > 3) { if (Light.strip_timer_counter % (Settings->light_speed - 2)) { return; } // Speed 4: 24sec, 5: 36sec, 6: 48sec, etc } #ifdef USE_LIGHT_PALETTE if (Light.palette_count) { if (!Light.fade_running) { if (0 == direction) { Light.wheel = random(Light.palette_count); } else { Light.wheel += direction; if (Light.wheel >= Light.palette_count) { Light.wheel = 0; if (direction < 0) Light.wheel = Light.palette_count - 1; } } LightSetPaletteEntry(); } return; } #endif // USE_LIGHT_PALETTE if (0 == direction) { if (Light.random == Light.wheel) { Light.random = random(255); uint8_t my_dir = (Light.random < Light.wheel -128) ? 1 : (Light.random < Light.wheel ) ? 0 : (Light.random > Light.wheel +128) ? 0 : 1; // Increment or Decrement and roll-over Light.random = (Light.random & 0xFE) | my_dir; // AddLog(LOG_LEVEL_DEBUG, PSTR("LGT: random %d"), Light.random); } // direction = (Light.random < Light.wheel) ? -1 : 1; direction = (Light.random &0x01) ? 1 : -1; } // if (Settings->light_speed < 3) { direction <<= (3 - Settings->light_speed); } // Speed 1: 12/4=3sec, 2: 12/2=6sec, 3: 12sec if (Settings->light_speed < 3) { direction *= (4 - Settings->light_speed); } // Speed 1: 12/3=4sec, 2: 12/2=6sec, 3: 12sec Light.wheel += direction; uint16_t hue = changeUIntScale(Light.wheel, 0, 255, 0, 359); // Scale to hue to keep amount of steps low (max 255 instead of 359) // AddLog(LOG_LEVEL_DEBUG, PSTR("LGT: random %d, wheel %d, hue %d"), Light.random, Light.wheel, hue); if (!Light.pwm_multi_channels) { uint8_t sat; light_state.getHSB(nullptr, &sat, nullptr); // Allow user control over Saturation light_state.setHS(hue, sat); } else { light_state.setHS(hue, 255); light_state.setBri(255); // If multi-channel, force bri to max, it will be later dimmed to correct value } light_controller.calcLevels(Light.new_color); } #ifdef USE_NETWORK_LIGHT_SCHEMES void LightListenDDP() { // Light channels gets completely controlled over DDP. So, we don't really check other settings. // To enter this scheme, we are already assured the light is at least RGB static uint8_t ddp_color[5] = { 0, 0, 0, 0, 0 }; // Can't be trying to initialize UDP too early. if (TasmotaGlobal.restart_flag || TasmotaGlobal.global_state.network_down) { light_state.setChannels(ddp_color); light_controller.calcLevels(Light.new_color); return; } // Start DDP listener, if fail, just set last ddp_color if (!ddp_udp_up) { if (!ddp_udp.begin(4048)) { light_state.setChannels(ddp_color); light_controller.calcLevels(Light.new_color); return; } ddp_udp_up = 1; AddLog(LOG_LEVEL_DEBUG_MORE, "DDP: UDP Listener Started: Normal Scheme"); } // Get the DDP payload over UDP std::vector payload; while (uint16_t packet_size = ddp_udp.parsePacket()) { payload.resize(packet_size); if (!ddp_udp.read(&payload[0], payload.size())) { continue; } } // No verification checks performed against packet besides length if (payload.size() > 12) { ddp_color[0] = payload[10]; ddp_color[1] = payload[11]; ddp_color[2] = payload[12]; } light_state.setChannels(ddp_color); light_controller.calcLevels(Light.new_color); } #endif void LightSetPower(void) { // Light.power = XdrvMailbox.index; Light.old_power = Light.power; //Light.power = bitRead(XdrvMailbox.index, Light.device -1); uint32_t mask = 1; // default mask if (Light.pwm_multi_channels) { mask = (1 << Light.subtype) - 1; // wider mask } else if (!light_controller.isCTRGBLinked()) { mask = 3; // we got 2 devices, for RGB and White } uint32_t shift = Light.device - 1; // If PWM multi_channels // Ex: 3 Relays and 4 PWM - TasmotaGlobal.devices_present = 7, Light.device = 4, Light.subtype = 4 // Result: mask = 0b00001111 = 0x0F, shift = 3. // Power bits we consider are: 0b01111000 = 0x78 // If regular situation: TasmotaGlobal.devices_present == Light.subtype Light.power = (XdrvMailbox.index & (mask << shift)) >> shift; if (Light.wakeup_active) { Light.wakeup_active--; } #ifdef DEBUG_LIGHT AddLog(LOG_LEVEL_DEBUG_MORE, "LightSetPower XdrvMailbox.index=%d Light.old_power=%d Light.power=%d mask=%d shift=%d", XdrvMailbox.index, Light.old_power, Light.power, mask, shift); #endif if (Light.power != Light.old_power) { Light.update = true; } LightAnimate(); } bool LightGetFadeSetting(void) { if (Light.fade_once_enabled) return Light.fade_once_value; return Settings->light_fade; } uint8_t LightGetSpeedSetting(void) { if (Light.speed_once_enabled) return Light.speed_once_value; return Settings->light_speed; } // Force to reapply color, for example when PWM Frequency changed void LightReapplyColor(void) { for (uint32_t i = 0; i < LST_MAX; i++) { Light.last_color[i] = 0; } } // On entry Light.new_color[5] contains the color to be displayed // and Light.last_color[5] the color currently displayed // Light.power tells which lights or channels (SetOption68) are on/off void LightAnimate(void) { bool power_off = false; static int32_t sleep_previous = -1; // previous value of sleep before changing it to PWM_MAX_SLEEP, -1 means unchanged // make sure we update CT range in case SetOption82 was changed Light.strip_timer_counter++; // Set a maximum sleep of PWM_MAX_SLEEP if Fade is running, or if light is on and // a frequently updating light scheme is in use. This is to allow smooth transitions // between light levels and colors. if ((Settings->light_scheme > LS_POWER && Light.power) || Light.fade_running) { if (TasmotaGlobal.sleep > PWM_MAX_SLEEP) { sleep_previous = TasmotaGlobal.sleep; // save previous value of sleep TasmotaGlobal.sleep = PWM_MAX_SLEEP; // set a maximum value (in milliseconds) to sleep to ensure that animations are smooth } } else { if (sleep_previous > 0) { TasmotaGlobal.sleep = sleep_previous; sleep_previous = -1; // rearm } } if (!Light.power) { // All channels powered off Light.strip_timer_counter = 0; if (Settings->light_scheme >= LS_MAX) { power_off = true; } #ifdef USE_NETWORK_LIGHT_SCHEMES if (ddp_udp_up) { ddp_udp.stop(); ddp_udp_up = 0; AddLog(LOG_LEVEL_DEBUG_MORE, "DDP: UDP Stopped: Power Off"); } #endif } else { #ifdef USE_NETWORK_LIGHT_SCHEMES if ((Settings->light_scheme < LS_MAX) && (Settings->light_scheme != LS_DDP) && (ddp_udp_up)) { ddp_udp.stop(); ddp_udp_up = 0; AddLog(LOG_LEVEL_DEBUG_MORE, "DDP: UDP Stopped: Normal Scheme not DDP"); } #endif switch (Settings->light_scheme) { case LS_POWER: light_controller.calcLevels(Light.new_color); break; case LS_WAKEUP: { if (2 == Light.wakeup_active) { Light.wakeup_active = 1; for (uint32_t i = 0; i < Light.subtype; i++) { Light.new_color[i] = 0; } Light.wakeup_start_time = millis(); } // which step are we in a range 0..1023 uint32_t step_10 = ((millis() - Light.wakeup_start_time) * 1023) / (Settings->light_wakeup * 1000); if (step_10 > 1023) { step_10 = 1023; } // sanity check uint8_t wakeup_bri = changeUIntScale(step_10, 0, 1023, 0, LightStateClass::DimmerToBri(Settings->light_dimmer)); if (wakeup_bri != light_state.getBri()) { light_state.setBri(wakeup_bri); light_controller.calcLevels(); for (uint32_t i = 0; i < Light.subtype; i++) { Light.new_color[i] = Light.current_color[i]; } } if (1023 == step_10) { Response_P(PSTR("{\"" D_CMND_WAKEUP "\":\"" D_JSON_DONE "\"")); ResponseLightState(1); ResponseJsonEnd(); MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_STAT, PSTR(D_CMND_WAKEUP)); Light.wakeup_active = 0; LightSetScheme(LS_POWER); } } break; case LS_CYCLEUP: case LS_CYCLEDN: case LS_RANDOM: if (LS_CYCLEUP == Settings->light_scheme) { LightCycleColor(1); } else if (LS_CYCLEDN == Settings->light_scheme) { LightCycleColor(-1); } else { LightCycleColor(0); } if (Light.pwm_multi_channels) { // See #8058 Light.new_color[0] = changeUIntScale(Light.new_color[0], 0, 255, 0, Settings->light_color[0]); Light.new_color[1] = changeUIntScale(Light.new_color[1], 0, 255, 0, Settings->light_color[1]); Light.new_color[2] = changeUIntScale(Light.new_color[2], 0, 255, 0, Settings->light_color[2]); } break; #ifdef USE_NETWORK_LIGHT_SCHEMES case LS_DDP: LightListenDDP(); break; #endif default: XlgtCall(FUNC_SET_SCHEME); } #ifdef USE_DEVICE_GROUPS if (Settings->light_scheme != Light.last_dgr_scheme) { Light.last_dgr_scheme = Settings->light_scheme; SendDeviceGroupMessage(Light.device, DGR_MSGTYP_UPDATE, DGR_ITEM_LIGHT_SCHEME, Settings->light_scheme); Light.devgrp_no_channels_out = false; } #endif // USE_DEVICE_GROUPS Light.last_scheme = Settings->light_scheme; } if ((Settings->light_scheme < LS_MAX) || power_off) { // exclude WS281X Neopixel schemes // Apply power modifiers to Light.new_color LightApplyPower(Light.new_color, Light.power); // AddLog(LOG_LEVEL_INFO, PSTR("last_color (%02X%02X%02X%02X%02X) new_color (%02X%02X%02X%02X%02X) power %d"), // Light.last_color[0], Light.last_color[1], Light.last_color[2], Light.last_color[3], Light.last_color[4], // Light.new_color[0], Light.new_color[1], Light.new_color[2], Light.new_color[3], Light.new_color[4], // Light.power // ); if (memcmp(Light.last_color, Light.new_color, Light.subtype)) { Light.update = true; } if (Light.update) { #ifdef USE_DEVICE_GROUPS if (Light.power && !Light.devgrp_no_channels_out) LightSendDeviceGroupStatus(); #endif // USE_DEVICE_GROUPS uint16_t cur_col_10[LST_MAX]; // 10 bits resolution Light.update = false; // first set 8 and 10 bits channels for (uint32_t i = 0; i < LST_MAX; i++) { Light.last_color[i] = Light.new_color[i]; // Extend from 8 to 10 bits if no correction (in case no gamma correction is required) cur_col_10[i] = change8to10(Light.new_color[i]); } if (Light.pwm_multi_channels) { calcGammaMultiChannels(cur_col_10); } else { // AddLog(LOG_LEVEL_INFO, PSTR(">>> calcGammaBulbs In %03X,%03X,%03X,%03X,%03X"), cur_col_10[0], cur_col_10[1], cur_col_10[2], cur_col_10[3], cur_col_10[4]); calcGammaBulbs(cur_col_10); // AddLog(LOG_LEVEL_INFO, PSTR(">>> calcGammaBulbs Out %03X,%03X,%03X,%03X,%03X"), cur_col_10[0], cur_col_10[1], cur_col_10[2], cur_col_10[3], cur_col_10[4]); } // final adjusments for PMW ranges, post-gamma correction for (uint32_t i = 0; i < LST_MAX; i++) { // scale from 0..1023 to 0..pwm_range, but keep any non-zero value to at least 1 cur_col_10[i] = (cur_col_10[i] > 0) ? changeUIntScale(cur_col_10[i], 1, 1023, 1, Settings->pwm_range) : 0; } // apply port remapping on both 8 bits and 10 bits versions uint16_t orig_col_10bits[LST_MAX]; memcpy(orig_col_10bits, cur_col_10, sizeof(orig_col_10bits)); for (uint32_t i = 0; i < LST_MAX; i++) { cur_col_10[i] = orig_col_10bits[Light.color_remap[i]]; } if (!LightGetFadeSetting() || TasmotaGlobal.skip_light_fade || power_off || (!Light.fade_initialized)) { // no fade // record the current value for a future Fade memcpy(Light.fade_start_10, cur_col_10, sizeof(Light.fade_start_10)); // push the final values at 8 and 10 bits resolution to the PWMs LightSetOutputs(cur_col_10); LightStopFade(); Light.fade_initialized = true; // it is now ok to fade Light.fade_once_enabled = false; // light has been set, reset fade_once_enabled Light.speed_once_enabled = false; // light has been set, reset speed_once_enabled } else { // fade on if (Light.fade_running) { // if fade is running, we take the curring value as the start for the next fade memcpy(Light.fade_start_10, Light.fade_cur_10, sizeof(Light.fade_start_10)); } memcpy(Light.fade_end_10, cur_col_10, sizeof(Light.fade_start_10)); // check if PWM CT is enabled, we need a special handling of CT #16454 int32_t channel_ct = ChannelCT(); int32_t channel_white = ChannelWhite_when_PWMCT(); if (channel_ct >= 0 && channel_white >= 0) { if (Light.fade_start_10[channel_white] == 0) { // if fading from black, change the start CT to the target, otherwise we will have a wrong fade Light.fade_start_10[channel_ct] = Light.fade_end_10[channel_ct]; } } Light.fade_running = true; Light.fade_duration = 0; // set the value to zero to force a recompute Light.fade_start = 0; Light.fade_once_enabled = false; // fade will be applied, reset fade_once_enabled // Fade will applied immediately below } } if (Light.fade_running) { if (LightApplyFade()) { // AddLog(LOG_LEVEL_INFO, PSTR("LightApplyFade %d %d %d %d %d"), // Light.fade_cur_10[0], Light.fade_cur_10[1], Light.fade_cur_10[2], Light.fade_cur_10[3], Light.fade_cur_10[4]); LightSetOutputs(Light.fade_cur_10); } } // For WYZE bulbs we must set the CT pin (PWM2) to INPUT to fully turn it off if (TasmotaGlobal.gpio_optiona.pwm1_input && !Light.power && !Light.fade_running) { // GPIO Option_A1 if (PinUsed(GPIO_PWM1, 1)) { pinMode(Pin(GPIO_PWM1, 1), INPUT); } } } } bool isChannelGammaCorrected(uint32_t channel) { if (!Settings->light_correction) { return false; } // Gamma correction not activated if (channel >= Light.subtype) { return false; } // Out of range #ifdef ESP8266 if ((PHILIPS == TasmotaGlobal.module_type) || (Settings->flag4.pwm_ct_mode)) { #else if (Settings->flag4.pwm_ct_mode) { #endif // ESP8266 if ((LST_COLDWARM == Light.subtype) && (1 == channel)) { return false; } // PMW reserved for CT if ((LST_RGBCW == Light.subtype) && (4 == channel)) { return false; } // PMW reserved for CT } return true; } // Returns the channel number for PWM CT if any, or -1 if none int32_t ChannelCT(void) { #ifdef ESP8266 if ((PHILIPS == TasmotaGlobal.module_type) || (Settings->flag4.pwm_ct_mode)) { #else if (Settings->flag4.pwm_ct_mode) { #endif // ESP8266 if (LST_COLDWARM == Light.subtype) { return 1; } // PMW reserved for CT if (LST_RGBCW == Light.subtype) { return 4; } // PMW reserved for CT } return -1; } // Returns the white channel when PWM CT is enabled -- needed to check for brightness #16454 int32_t ChannelWhite_when_PWMCT(void) { #ifdef ESP8266 if ((PHILIPS == TasmotaGlobal.module_type) || (Settings->flag4.pwm_ct_mode)) { #else if (Settings->flag4.pwm_ct_mode) { #endif // ESP8266 if (LST_COLDWARM == Light.subtype) { return 0; } if (LST_RGBCW == Light.subtype) { return 3; } } return -1; } // Calculate the Gamma correction, if any, for fading, using the fast Gamma curve (10 bits in+out) uint16_t fadeGamma(uint32_t channel, uint16_t v) { if (isChannelGammaCorrected(channel)) { return ledGammaFast(v); } else { return v; } } uint16_t fadeGammaReverse(uint32_t channel, uint16_t vg) { if (isChannelGammaCorrected(channel)) { return leddGammaReverseFast(vg); } else { return vg; } } uint8_t LightGetCurFadeBri(void) { uint8_t max_bri = 0; uint8_t bri_i = 0; for (uint8_t i = 0; i < LST_MAX; i++) { bri_i = changeUIntScale(fadeGammaReverse(i, Light.fade_cur_10[i]), 4, 1023, 1, 100); if (bri_i > max_bri) max_bri = bri_i ; } return max_bri; } void LightStopFade(void) { Light.fade_running = false; #ifdef USE_PWM_DIMMER // If the power is off and the fade is done, turn the relay off. if (PWM_DIMMER == TasmotaGlobal.module_type && !Light.power) PWMDimmerSetPower(); #endif // USE_PWM_DIMMER } bool LightApplyFade(void) { // did the value chanegd and needs to be applied static uint32_t last_millis = 0; uint32_t now = millis(); if ((now - last_millis) <= 5) { return false; // the value was not changed in the last 5 milliseconds, ignore } last_millis = now; // Check if we need to calculate the duration if (0 == Light.fade_duration) { Light.fade_start = now; // compute the distance between start and and color (max of distance for each channel) uint32_t distance = 0; for (uint32_t i = 0; i < Light.subtype; i++) { int32_t channel_distance = fadeGammaReverse(i, Light.fade_end_10[i]) - fadeGammaReverse(i, Light.fade_start_10[i]); if (channel_distance < 0) { channel_distance = - channel_distance; } if (channel_distance > distance) { distance = channel_distance; } } if (distance > 0) { // compute the duration of the animation // Note: Settings->light_speed is the number of half-seconds for a 100% fade, // i.e. light_speed=1 means 1024 steps in 500ms Light.fade_duration = LightGetSpeedSetting() * 500; Light.speed_once_enabled = false; // The once off speed value has been read, reset it if (!Settings->flag5.fade_fixed_duration) { Light.fade_duration = (distance * Light.fade_duration) / 1023 + 1 /* make sure value is not zero */; // time is proportional to distance, except with SO117 } if (Settings->save_data) { // Also postpone the save_data for the duration of the Fade (in seconds) uint32_t delay_seconds = 1 + (Light.fade_duration + 999) / 1000; // add one more second // AddLog(LOG_LEVEL_INFO, PSTR("delay_seconds %d, save_data_counter %d"), delay_seconds, TasmotaGlobal.save_data_counter); if (TasmotaGlobal.save_data_counter < delay_seconds) { TasmotaGlobal.save_data_counter = delay_seconds; // pospone } } } else { // no fade needed, we keep the duration at zero, it will fallback directly to end of fade LightStopFade(); } } uint16_t fade_current = now - Light.fade_start; // number of milliseconds since start of fade if (fade_current <= Light.fade_duration) { // fade not finished //Serial.printf("Fade: %d / %d - ", fade_current, Light.fade_duration); for (uint32_t i = 0; i < Light.subtype; i++) { Light.fade_cur_10[i] = fadeGamma(i, changeUIntScale(fadeGammaReverse(i, fade_current), 0, Light.fade_duration, fadeGammaReverse(i, Light.fade_start_10[i]), fadeGammaReverse(i, Light.fade_end_10[i]))); // Light.fade_cur_10[i] = changeUIntScale(fade_current, // 0, Light.fade_duration, // Light.fade_start_10[i], Light.fade_end_10[i]); } } else { // stop fade //AddLop_P2(LOG_LEVEL_DEBUG, PSTR("Stop fade")); LightStopFade(); Light.fade_start = 0; Light.fade_duration = 0; // set light to target value memcpy(Light.fade_cur_10, Light.fade_end_10, sizeof(Light.fade_end_10)); // record the last value for next start memcpy(Light.fade_start_10, Light.fade_end_10, sizeof(Light.fade_start_10)); } return true; } // On entry we take the 5 channels 8 bits entry, and we apply Power modifiers // I.e. shut down channels that are powered down void LightApplyPower(uint8_t new_color[LST_MAX], power_t power) { // If SetOption68, multi_channels if (Light.pwm_multi_channels) { // if multi-channels, specifically apply the Light.power bits for (uint32_t i = 0; i < LST_MAX; i++) { if (0 == bitRead(power,i)) { // if power down bit is zero new_color[i] = 0; // shut down this channel } } // #ifdef DEBUG_LIGHT // AddLog(LOG_LEVEL_DEBUG_MORE, "Animate>> Light.power=%d Light.new_color=[%d,%d,%d,%d,%d]", // Light.power, Light.new_color[0], Light.new_color[1], Light.new_color[2], // Light.new_color[3], Light.new_color[4]); // #endif } else { if (!light_controller.isCTRGBLinked()) { // we have 2 power bits for RGB and White if (0 == (power & 1)) { new_color[0] = new_color[1] = new_color[2] = 0; } if (0 == (power & 2)) { new_color[3] = new_color[4] = 0; } } else if (!power) { for (uint32_t i = 0; i < LST_MAX; i++) { new_color[i] = 0; } } } } void LightSetOutputs(const uint16_t *cur_col_10) { // now apply the actual PWM values, adjusted and remapped 10-bits range if (TasmotaGlobal.light_type < LT_PWM6) { // only for direct PWM lights, not for Tuya, Armtronix... int32_t channel_ct = ChannelCT(); // Channel for PWM CT or -1 if no CT or regular CT #ifdef USE_PWM_DIMMER uint16_t max_col = 0; #ifdef USE_I2C if (TasmotaGlobal.gpio_optiona.linkind_support) { // Option_A6 uint8_t val = change10to8(cur_col_10[Light.pwm_offset] > 0 ? changeUIntScale(cur_col_10[Light.pwm_offset], 0, Settings->pwm_range, Light.pwm_min, Light.pwm_max) : 0); max_col = val; uint16_t chk = 65403 - val; uint8_t buf[] = { 0x09, 0x50, 0x01, val, 0x00, 0x00, (uint8_t)(chk >> 8), (uint8_t)(chk & 0xff) }; I2cWriteBuffer(0x50, 0x2A, buf, sizeof(buf)); } else #endif // USE_I2C #endif // USE_PWM_DIMMER for (uint32_t i = 0; i < (Light.subtype - Light.pwm_offset); i++) { uint16_t cur_col = cur_col_10[i + Light.pwm_offset]; #ifdef USE_PWM_DIMMER if (cur_col > max_col) max_col = cur_col; #endif // USE_PWM_DIMMER if (PinUsed(GPIO_PWM1, i)) { //AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_APPLICATION "Cur_Col%d 10 bits %d"), i, cur_col_10[i]); uint16_t cur_col = cur_col_10[i + Light.pwm_offset]; if (i != channel_ct) { // if CT don't use pwm_min and pwm_max cur_col = cur_col > 0 ? changeUIntScale(cur_col, 0, Settings->pwm_range, Light.pwm_min, Light.pwm_max) : 0; // shrink to the range of pwm_min..pwm_max } #ifdef ESP32 TasmotaGlobal.pwm_value[i] = cur_col; // mark the new expected value // AddLog(LOG_LEVEL_DEBUG_MORE, "analogWrite-%i 0x%03X", i, cur_col); #else // ESP32 if (!Settings->flag4.zerocross_dimmer) { AnalogWrite(Pin(GPIO_PWM1, i), bitRead(TasmotaGlobal.pwm_inverted, i) ? Settings->pwm_range - cur_col : cur_col); // AddLog(LOG_LEVEL_DEBUG_MORE, "analogWrite-%i 0x%03X", bitRead(TasmotaGlobal.pwm_inverted, i) ? Settings->pwm_range - cur_col : cur_col); } #endif // ESP32 } } #ifdef ESP32 PwmApplyGPIO(false); #endif // ESP32 #ifdef USE_PWM_DIMMER // Animate brightness LEDs to follow PWM dimmer brightness if (PWM_DIMMER == TasmotaGlobal.module_type) { TasmotaGlobal.pwm_dimmer_led_bri = change10to8(max_col); PWMDimmerSetBrightnessLeds(-1); } #endif // USE_PWM_DIMMER } // char msg[24]; // AddLog(LOG_LEVEL_DEBUG, PSTR("LGT: Channels %s"), ToHex_P((const unsigned char *)cur_col_10, 10, msg, sizeof(msg))); // Some devices need scaled RGB like Sonoff L1 uint32_t max = (cur_col_10[0] > cur_col_10[1] && cur_col_10[0] > cur_col_10[2]) ? cur_col_10[0] : (cur_col_10[1] > cur_col_10[2]) ? cur_col_10[1] : cur_col_10[2]; // 0..1023 uint8_t scale_col[3]; for (uint32_t i = 0; i < 3; i++) { scale_col[i] = (0 == max) ? 255 : changeUIntScale(cur_col_10[i], 0, max, 0, 255); } // AddLog(LOG_LEVEL_DEBUG, PSTR("LGT: CurCol %03X %03X %03X, ScaleCol %02X %02X %02X, Max %02X"), // cur_col_10[0], cur_col_10[1], cur_col_10[2], scale_col[0], scale_col[1], scale_col[2], max); uint8_t cur_col[LST_MAX]; for (uint32_t i = 0; i < LST_MAX; i++) { cur_col[i] = change10to8(cur_col_10[i]); } char *tmp_data = XdrvMailbox.data; char *tmp_topic = XdrvMailbox.topic; char *tmp_command = XdrvMailbox.command; XdrvMailbox.data = (char*)cur_col; XdrvMailbox.topic = (char*)scale_col; XdrvMailbox.command = (char*)cur_col_10; #ifdef USE_LIGHT_ARTNET if (ArtNetSetChannels()) { /* Serviced */} else #endif if (XlgtCall(FUNC_SET_CHANNELS)) { /* Serviced */ } else if (XdrvCall(FUNC_SET_CHANNELS)) { /* Serviced */ } XdrvMailbox.data = tmp_data; XdrvMailbox.topic = tmp_topic; XdrvMailbox.command = tmp_command; } // Just apply basic Gamma to each channel void calcGammaMultiChannels(uint16_t cur_col_10[5]) { // Apply gamma correction for 8 and 10 bits resolutions, if needed if (Settings->light_correction) { for (uint32_t i = 0; i < LST_MAX; i++) { cur_col_10[i] = ledGamma10_10(cur_col_10[i]); } } } // // Compute the Gamma correction for CW/WW // Can be used for 2-channels (channels 0,1) or 5 channels (channels 3,4) // // It is implicitly called by calcGammaBulb5Channels() // // In: // - 2 channels CW/WW in 10 bits format (0..1023) // Out: // - 2 channels CW/WW in 10 bits format, with Gamma corretion (if enabled), replaced in place // - white_bri10: global brightness of white channel, split over CW/WW (basically the sum of CW+WW, but it's easier to compute on this basis) // - white_free_cw: signals that CW/WW are free mode, and not linked via CT. This is used when channels are manually set on a channel per channel basis. CT is ignored // void calcGammaBulbCW(uint16_t cw10[2], uint16_t *white_bri10_out, bool *white_free_cw_out) { uint16_t white_bri10 = cw10[0] + cw10[1]; // cumulated brightness bool white_free_cw = (white_bri10 > 1031); // take a margin of 8 above 1023 to account for rounding errors white_bri10 = (white_bri10 > 1023) ? 1023 : white_bri10; // max 1023 if (Settings->light_correction) { if (white_free_cw) { cw10[0] = ledGamma10_10(cw10[0]); cw10[1] = ledGamma10_10(cw10[1]); } else { uint16_t white_bri10_gamma = ledGamma10_10(white_bri10); // gamma corrected white // now distributed among both channels cw10[0] = changeUIntScale(cw10[0], 0, white_bri10, 0, white_bri10_gamma); cw10[1] = changeUIntScale(cw10[1], 0, white_bri10, 0, white_bri10_gamma); // now use white_bri10_gamma as a reference white_bri10 = white_bri10_gamma; } } if (white_bri10_out != nullptr) { *white_bri10_out = white_bri10; } if (white_free_cw_out != nullptr) { *white_free_cw_out = white_free_cw; } } // // Calculate the gamma correction for all 5 channels RGBCW // Computation is valid for 1,3,4,5 channels // 2-channels bulbs must be handled separately // // In: // - 5 channels RGBCW in 10 bits format (0..1023) // Out: // - 5 channels RGBCW in 10 bits format, with Gamma corretion (if enabled), replaced in place // - white_bri10: global brightness of white channel, split over CW/WW (basically the sum of CW+WW, but it's easier to compute on this basis) // - white_free_cw: signals that CW/WW are free mode, and not linked via CT. This is used when channels are manually set on a channel per channel basis. CT is ignored // void calcGammaBulb5Channels(uint16_t col10[LST_MAX], uint16_t *white_bri10_out, bool *white_free_cw) { for (uint32_t i = 0; i < 3; i++) { if (Settings->light_correction) { col10[i] = ledGamma10_10(col10[i]); } } calcGammaBulbCW(&col10[3], white_bri10_out, white_free_cw); } // sale but converts from 8 bits to 10 bits first void calcGammaBulb5Channels_8(uint8_t in8[LST_MAX], uint16_t col10[LST_MAX]) { for (uint32_t i = 0; i < LST_MAX; i++) { col10[i] = change8to10(in8[i]); } calcGammaBulb5Channels(col10, nullptr, nullptr); } void calcGammaBulbs(uint16_t cur_col_10[5]) { bool rgbwwtable_applied_white = false; bool white_free_cw = false; // true if White channels are uncorrelated. Happens when CW+WW>255, i.e. manually setting white channels to exceed to total power of a single channel (may harm the power supply) // Various values needed for accurate White calculation // CT value streteched to 0..1023 (from within CT range, so not necessarily from 153 to 500). 0=Cold, 1023=Warm uint16_t ct = light_state.getCT(); uint16_t ct_10 = changeUIntScale(ct, Light.vct_ct[0], Light.vct_ct[CT_PIVOTS-1], 0, 1023); uint16_t white_bri10 = 0; // White total brightness normalized to 0..1023 // uint32_t cw1 = Light.subtype - 1; // address for the ColorTone PWM uint32_t cw0 = Light.subtype - 2; // address for the White Brightness PWM // calc basic gamma correction for all types if ((LST_SINGLE == Light.subtype) || (LST_RGB <= Light.subtype)) { calcGammaBulb5Channels(cur_col_10, &white_bri10, &white_free_cw); } else if (LST_COLDWARM == Light.subtype) { calcGammaBulbCW(cur_col_10, &white_bri10, &white_free_cw); } // Now see if we need to mix RGB and White // Valid only for LST_RGBW, LST_RGBCW, SetOption105 1, and white is zero (see doc) if ((LST_RGBW <= Light.subtype) && (Settings->flag4.white_blend_mode) && (0 == cur_col_10[3]+cur_col_10[4])) { uint32_t min_rgb_10 = min3(cur_col_10[0], cur_col_10[1], cur_col_10[2]); cur_col_10[0] -= min_rgb_10; cur_col_10[1] -= min_rgb_10; cur_col_10[2] -= min_rgb_10; // Add to white level uint32_t adjust_w_10 = change8to10(Settings->rgbwwTable[3]); // take the correction factor, bought back to 10 bits white_bri10 += changeUIntScale(min_rgb_10, 0, 1023, 0, adjust_w_10); // set white power down corrected with rgbwwTable[3] white_bri10 = (white_bri10 > 1023) ? 1023 : white_bri10; // max 1023 rgbwwtable_applied_white = true; } #ifdef USE_LIGHT_VIRTUAL_CT // compute virtual CT, which is suppsed to be compatible with white_blend_mode if (Light.virtual_ct && (!white_free_cw) && (LST_RGBW <= Light.subtype)) { // any light with a white channel vct_pivot_t *pivot = &Light.vct_color[0]; uint16_t *from_ct = &Light.vct_ct[0]; for (uint32_t i = 1; i < CT_PIVOTS-1; i++) { if (ct > Light.vct_ct[i]) { // if above mid-point, take range [1]..[2] instead of [0]..[1] pivot++; from_ct++; } } uint16_t from10[LST_MAX]; uint16_t to10[LST_MAX]; calcGammaBulb5Channels_8(*pivot, from10); calcGammaBulb5Channels_8(*(pivot+1), to10); // vct_pivot_t *pivot1 = pivot + 1; // AddLog(LOG_LEVEL_INFO, PSTR("+++ from_ct %d, to_ct %d [%03X,%03X,%03X,%03X,%03X] - [%03X,%03X,%03X,%03X,%03X]"), // *from_ct, *(from_ct+1), (*pivot)[0], (*pivot)[1], (*pivot)[2], (*pivot)[3], (*pivot)[4], // (*pivot1)[0], (*pivot1)[1], (*pivot1)[2], (*pivot1)[3], (*pivot1)[4]); // AddLog(LOG_LEVEL_INFO, PSTR("+++ from10 [%03X,%03X,%03X,%03X,%03X] - to 10 [%03X,%03X,%03X,%03X,%03X]"), // from10[0],from10[0],from10[0],from10[0],from10[4], // to10[0],to10[0],to10[0],to10[0],to10[4]); // set both CW/WW to zero since their previous value don't count anymore cur_col_10[3] = 0; cur_col_10[4] = 0; // Add the interpolated point to each component for (uint32_t i = 0; i < LST_MAX; i++) { cur_col_10[i] += changeUIntScale(changeUIntScale(ct, *from_ct, *(from_ct+1), from10[i], to10[i]), 0, 1023, 0, white_bri10); if (cur_col_10[i] > 1023) { cur_col_10[i] = 1023; } } } else #endif // USE_LIGHT_VIRTUAL_CT // compute the actual levels for CW/WW // We know ct_10 and white_bri_10 (which may be Gamma corrected) // cur_col_10[cw0] and cur_col_10[cw1] were unmodified up to now if (LST_RGBW == Light.subtype) { cur_col_10[3] = white_bri10; // simple case, we set the White level to the required brightness } else if ((LST_COLDWARM == Light.subtype) || (LST_RGBCW == Light.subtype)) { // if sum of both channels is > 255, then channels are probably uncorrelated if (!white_free_cw) { // then we split the total energy among the cold and warm leds cur_col_10[cw0+1] = changeUIntScale(ct_10, 0, 1023, 0, white_bri10); cur_col_10[cw0] = white_bri10 - cur_col_10[cw0+1]; } } // Apply RGBWWTable (RGB: always, CW: only if white_blend_mode is not engaged) for (uint32_t i = 0; i < (rgbwwtable_applied_white ? 3 : Light.subtype); i++) { uint32_t adjust = change8to10(Settings->rgbwwTable[i]); cur_col_10[i] = changeUIntScale(cur_col_10[i], 0, 1023, 0, adjust); } // Implement SO92: Some lights like Xiaomi Philips bulbs follow the scheme // cw0=intensity, cw0+1=temperature if (ChannelCT() >= 0) { // Need to compute white_bri10 and ct_10 from cur_col_10[] for compatibility with VirtualCT white_bri10 = cur_col_10[cw0] + cur_col_10[cw0+1]; if (white_bri10 > 1023) { // In white_free_cw mode, the combined brightness of cw and ww may be larger than 1023. // This cannot be represented in pwm_ct_mode, so we set the maximum brightness instead. white_bri10 = 1023; } cur_col_10[cw0] = white_bri10; cur_col_10[cw0+1] = ct_10; } } #ifdef USE_DEVICE_GROUPS void LightSendDeviceGroupStatus() { static uint8_t last_bri; uint8_t bri = light_state.getBri(); bool send_bri_update = (building_status_message || bri != last_bri); if (Light.subtype > LST_SINGLE) { static uint8_t last_channels[LST_MAX + 1] = { 0, 0, 0, 0, 0, 0 }; uint8_t channels[LST_MAX]; light_state.getChannelsRaw(channels); uint8_t color_mode = light_state.getColorMode(); if (!(color_mode & LCM_RGB)) channels[0] = channels[1] = channels[2] = 0; if (!(color_mode & LCM_CT)) channels[3] = channels[4] = 0; if (building_status_message || memcmp(channels, last_channels, LST_MAX)) { memcpy(last_channels, channels, LST_MAX); last_channels[LST_MAX]++; SendDeviceGroupMessage(Light.device, (send_bri_update ? DGR_MSGTYP_PARTIAL_UPDATE : DGR_MSGTYP_UPDATE), DGR_ITEM_LIGHT_CHANNELS, last_channels); } } if (send_bri_update) { last_bri = bri; SendDeviceGroupMessage(Light.device, DGR_MSGTYP_UPDATE, DGR_ITEM_LIGHT_BRI, light_state.getBri()); } } void LightHandleDevGroupItem(void) { static bool send_state = false; static bool restore_power = false; if (Settings->flag4.multiple_device_groups ? Settings->device_group_tie[*XdrvMailbox.topic] != Light.device : !(XdrvMailbox.index & DGR_FLAG_LOCAL)) return; bool more_to_come; uint32_t value = XdrvMailbox.payload; switch (XdrvMailbox.command_code) { case DGR_ITEM_EOL: more_to_come = (XdrvMailbox.index & DGR_FLAG_MORE_TO_COME); if (more_to_come) { TasmotaGlobal.skip_light_fade = true; } else if (restore_power) { restore_power = false; Light.power = Light.old_power; } LightAnimate(); TasmotaGlobal.skip_light_fade = false; if (send_state && !more_to_come) { light_controller.saveSettings(); if (Settings->flag3.hass_tele_on_power) { // SetOption59 - Send tele/%topic%/STATE in addition to stat/%topic%/RESULT MqttPublishTeleState(); } send_state = false; } break; case DGR_ITEM_LIGHT_BRI: if (light_state.getBri() != value) { light_state.setBri(value); Settings->light_dimmer = light_state.BriToDimmer(value); send_state = true; } break; case DGR_ITEM_LIGHT_SCHEME: if (Settings->light_scheme != value) { Light.last_dgr_scheme = Settings->light_scheme = value; Light.devgrp_no_channels_out = (value != 0); send_state = true; } break; case DGR_ITEM_LIGHT_CHANNELS: { uint8_t bri = light_state.getBri(); #ifdef USE_DGR_LIGHT_SEQUENCE static uint8_t last_sequence = 0; // If a sequence offset is set, set the channels to the ones we received // changes ago. if (Light.sequence_offset) { light_controller.changeChannels(Light.channels_fifo, true); // Shift the fifo down and load the newly received channels at the end for this update and // any updates we missed. int last_entry = (Light.sequence_offset - 1) * LST_MAX; for (uint8_t sequence = (uint8_t)XdrvMailbox.data[LST_MAX]; (uint8_t)(sequence - last_sequence) > 0; last_sequence++) { memmove(Light.channels_fifo, &Light.channels_fifo[LST_MAX], last_entry); memcpy(&Light.channels_fifo[last_entry], XdrvMailbox.data, LST_MAX); } } else #endif // USE_DGR_LIGHT_SEQUENCE light_controller.changeChannels((uint8_t *)XdrvMailbox.data, true); light_controller.changeBri(bri); } send_state = true; break; case DGR_ITEM_LIGHT_FIXED_COLOR: if (Light.subtype >= LST_COLDWARM) { send_state = true; #ifdef USE_LIGHT_PALETTE if (Light.palette_count) { Light.wheel = value % Light.palette_count; LightSetPaletteEntry(); break; } #endif // !USE_LIGHT_PALETTE if (Light.subtype <= LST_COLDWARM) { value = value % (MAX_FIXED_COLD_WARM - 1) + 201; } else { uint32_t max = MAX_FIXED_COLOR; if (Light.subtype >= LST_RGB) { max++; if (Light.subtype >= LST_RGBCW) max += (MAX_FIXED_COLD_WARM - 2); } value = value % max + 1; if (value > MAX_FIXED_COLOR) value += 200 - MAX_FIXED_COLOR; } Light.fixed_color_index = value; bool save_decimal_text = Settings->flag.decimal_text; char str[16]; LightColorEntry(str, sprintf_P(str, PSTR("%u"), value)); Settings->flag.decimal_text = save_decimal_text; uint32_t old_bri = light_state.getBri(); light_controller.changeChannels(Light.entry_color); light_controller.changeBri(old_bri); LightSetScheme(LS_POWER); if (!restore_power && !Light.power) { Light.old_power = Light.power; Light.power = 0xff; restore_power = true; } } break; case DGR_ITEM_LIGHT_FADE: if (Settings->light_fade != value) { Settings->light_fade = value; send_state = true; } break; case DGR_ITEM_LIGHT_SPEED: if (Settings->light_speed != value && value > 0 && value <= 40) { Settings->light_speed = value; send_state = true; } break; case DGR_ITEM_STATUS: SendDeviceGroupMessage(Light.device, DGR_MSGTYP_PARTIAL_UPDATE, DGR_ITEM_LIGHT_FADE, Settings->light_fade, DGR_ITEM_LIGHT_SPEED, Settings->light_speed, DGR_ITEM_LIGHT_SCHEME, Settings->light_scheme); LightSendDeviceGroupStatus(); break; } } #endif // USE_DEVICE_GROUPS /*********************************************************************************************\ * Commands \*********************************************************************************************/ bool LightColorEntry(char *buffer, uint32_t buffer_length) { char scolor[10]; char *p; char *str; uint32_t entry_type = 0; // Invalid uint8_t value = Light.fixed_color_index; #ifdef USE_LIGHT_PALETTE if (Light.palette_count) value = Light.wheel; #endif // USE_LIGHT_PALETTE if (buffer[0] == '#') { // Optional hexadecimal entry buffer++; buffer_length--; } if (Light.subtype >= LST_RGB) { char option = (1 == buffer_length) ? buffer[0] : '\0'; if ('+' == option) { #ifdef USE_LIGHT_PALETTE if (Light.palette_count || Light.fixed_color_index < MAX_FIXED_COLOR) { #else // USE_LIGHT_PALETTE if (Light.fixed_color_index < MAX_FIXED_COLOR) { #endif // !USE_LIGHT_PALETTE value++; } } else if ('-' == option) { #ifdef USE_LIGHT_PALETTE if (Light.palette_count || Light.fixed_color_index > 1) { #else // USE_LIGHT_PALETTE if (Light.fixed_color_index > 1) { #endif // !USE_LIGHT_PALETTE value--; } } else { value = atoi(buffer); } #ifdef USE_LIGHT_PALETTE if (Light.palette_count) value = value % Light.palette_count; #endif // USE_LIGHT_PALETTE } memset(&Light.entry_color, 0x00, sizeof(Light.entry_color)); // erase all channels except if the last character is '=', #6799 while ((buffer_length > 0) && ('=' == buffer[buffer_length - 1])) { buffer_length--; // remove all trailing '=' memcpy(&Light.entry_color, &Light.current_color, sizeof(Light.entry_color)); } if (strchr(buffer, ',') != nullptr) { // Decimal entry int8_t i = 0; for (str = strtok_r(buffer, ",", &p); str && i < 6; str = strtok_r(nullptr, ",", &p)) { if (i < LST_MAX) { Light.entry_color[i++] = atoi(str); } } entry_type = 2; // Decimal } else if (((2 * Light.subtype) == buffer_length) || (buffer_length > 3)) { // Hexadecimal entry for (uint32_t i = 0; i < tmin((uint)(buffer_length / 2), sizeof(Light.entry_color)); i++) { strlcpy(scolor, buffer + (i *2), 3); Light.entry_color[i] = (uint8_t)strtol(scolor, &p, 16); } entry_type = 1; // Hexadecimal } #ifdef USE_LIGHT_PALETTE else if (Light.palette_count) { Light.wheel = value; memcpy_P(&Light.entry_color, &Light.palette[value * Light.subtype], Light.subtype); entry_type = 1; // Hexadecimal } #endif // USE_LIGHT_PALETTE else if ((Light.subtype >= LST_RGB) && (value > 0) && (value <= MAX_FIXED_COLOR)) { Light.fixed_color_index = value; memcpy_P(&Light.entry_color, &kFixedColor[value -1], 3); entry_type = 1; // Hexadecimal } else if ((value > 199) && (value <= 199 + MAX_FIXED_COLD_WARM)) { if (LST_RGBW == Light.subtype) { memcpy_P(&Light.entry_color[3], &kFixedWhite[value -200], 1); entry_type = 1; // Hexadecimal } else if (LST_COLDWARM == Light.subtype) { memcpy_P(&Light.entry_color, &kFixedColdWarm[value -200], 2); entry_type = 1; // Hexadecimal } else if (LST_RGBCW == Light.subtype) { memcpy_P(&Light.entry_color[3], &kFixedColdWarm[value -200], 2); entry_type = 1; // Hexadecimal } } // Too much magic so removed since 9.0.0.3 // if (entry_type) { // Settings->flag.decimal_text = entry_type -1; // SetOption17 - Switch between decimal or hexadecimal output // } return (entry_type); } /********************************************************************************************/ void CmndSupportColor(void) { bool valid_entry = false; bool coldim = false; if (XdrvMailbox.data_len > 0) { valid_entry = LightColorEntry(XdrvMailbox.data, XdrvMailbox.data_len); if (valid_entry) { if (XdrvMailbox.index <= 2) { // Color(1), 2 #ifdef USE_LIGHT_PALETTE if (Light.palette_count && XdrvMailbox.index == 2) { LightSetPaletteEntry(); } else { #endif // USE_LIGHT_PALETTE uint32_t old_bri = light_state.getBri(); uint32_t old_bri_rgb = light_state.getBriRGB(); // change all channels to specified values light_controller.changeChannels(Light.entry_color); if (2 == XdrvMailbox.index) { // If Color2, set back old brightness if (light_controller.isCTRGBLinked()) { // RGB and white are linked, adjust brightness of all channels LightSetBriScaled(old_bri); } else { // RGB and white are unlinked, adjust brightness only of RGB channels LightSetBri(Light.device, old_bri_rgb); } } #ifdef USE_LIGHT_PALETTE } #endif // USE_LIGHT_PALETTE LightSetScheme(LS_POWER); coldim = true; } else { // Color3, 4, 5 and 6 for (uint32_t i = 0; i < LST_RGB; i++) { Settings->ws_color[XdrvMailbox.index -3][i] = Light.entry_color[i]; } } } } char scolor[LIGHT_COLOR_SIZE]; if (!valid_entry && (XdrvMailbox.index <= 2)) { ResponseCmndChar(LightGetColor(scolor)); } if (XdrvMailbox.index >= 3) { scolor[0] = '\0'; for (uint32_t i = 0; i < LST_RGB; i++) { if (Settings->flag.decimal_text) { // SetOption17 - Switch between decimal or hexadecimal output snprintf_P(scolor, sizeof(scolor), PSTR("%s%s%d"), scolor, (i > 0) ? "," : "", Settings->ws_color[XdrvMailbox.index -3][i]); } else { snprintf_P(scolor, sizeof(scolor), PSTR("%s%02X"), scolor, Settings->ws_color[XdrvMailbox.index -3][i]); } } ResponseCmndIdxChar(scolor); } if (coldim) { LightPreparePower(); // no parameter, recalculate Power for all channels } } void CmndColor(void) { // Color - Show current RGBWW color state // Color1 - Change color to RGBWW // Color2 - Change color to RGBWW but retain brightness (=dimmer) // Color3 - Change color to RGB of WS2812 Clock Second // Color4 - Change color to RGB of WS2812 Clock Minute // Color5 - Change color to RGB of WS2812 Clock Hour // Color6 - Change color to RGB of WS2812 Clock Marker if ((Light.subtype > LST_SINGLE) && (XdrvMailbox.index > 0) && (XdrvMailbox.index <= 6)) { CmndSupportColor(); } } void CmndWhite(void) { // White - Show current White (=Dimmer2) state // White 0..100 - Set White colors dimmer state if (Light.pwm_multi_channels) { return; } if ( ((Light.subtype >= LST_RGBW) || (LST_COLDWARM == Light.subtype)) && (XdrvMailbox.index == 1)) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 100)) { light_controller.changeDimmer(XdrvMailbox.payload, 2); LightPreparePower(2); } else { ResponseCmndNumber(light_state.getDimmer(2)); } } } void CmndChannel(void) { // Channel - Show current Channel state // Channel 0..100 - Set Channel dimmer state // Channel + - Incerement Channel in steps of 10 // Channel - - Decrement Channel in steps of 10 if ((XdrvMailbox.index >= Light.device) && (XdrvMailbox.index < Light.device + Light.subtype )) { uint32_t light_index = XdrvMailbox.index - Light.device; power_t coldim = 0; // bit flag to update // Handle +/- special command if (1 == XdrvMailbox.data_len) { uint8_t channel = changeUIntScale(Light.current_color[light_index],0,255,0,100); if ('+' == XdrvMailbox.data[0]) { XdrvMailbox.payload = (channel > 89) ? 100 : channel + 10; } else if ('-' == XdrvMailbox.data[0]) { XdrvMailbox.payload = (channel < 11) ? 1 : channel - 10; } } // Set "Channel" directly - this allows Color and Direct PWM control to coexist if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 100)) { Light.current_color[light_index] = changeUIntScale(XdrvMailbox.payload,0,100,0,255); if (Light.pwm_multi_channels) { coldim = 1 << light_index; // change the specified channel } else { if (light_controller.isCTRGBLinked()) { // if we change channels 1,2,3 then turn off CT mode (unless non-linked) if ((light_index < 3) && (light_controller.isCTRGBLinked())) { Light.current_color[3] = Light.current_color[4] = 0; } else { Light.current_color[0] = Light.current_color[1] = Light.current_color[2] = 0; } coldim = 1; } else { if (light_index < 3) { coldim = 1; } // RGB else { coldim = 2; } // CT } } light_controller.changeChannels(Light.current_color); } ResponseCmndIdxNumber(changeUIntScale(Light.current_color[light_index],0,255,0,100)); if (coldim) { LightPreparePower(coldim); } } } void CmndHsbColor(void) { // HsbColor - Show current HSB // HsbColor 360,100,100 - Set Hue, Saturation and Brighthness // HsbColor 360,100 - Set Hue and Saturation // HsbColor 360 - Set Hue // HsbColor1 360 - Set Hue // HsbColor2 100 - Set Saturation // HsbColor3 100 - Set Brightness if (Light.subtype >= LST_RGB) { if (XdrvMailbox.data_len > 0) { uint16_t c_hue; uint8_t c_sat; light_state.getHSB(&c_hue, &c_sat, nullptr); uint32_t HSB[3]; HSB[0] = c_hue; HSB[1] = c_sat; HSB[2] = light_state.getBriRGB(); if ((2 == XdrvMailbox.index) || (3 == XdrvMailbox.index)) { if ((uint32_t)XdrvMailbox.payload > 100) { XdrvMailbox.payload = 100; } HSB[XdrvMailbox.index-1] = changeUIntScale(XdrvMailbox.payload, 0, 100, 0, 255); } else { uint32_t paramcount = ParseParameters(3, HSB); if (HSB[0] > 360) { HSB[0] = 360; } for (uint32_t i = 1; i < paramcount; i++) { if (HSB[i] > 100) { HSB[i] = 100; } HSB[i] = changeUIntScale(HSB[i], 0, 100, 0, 255); // change sat and bri to 0..255 } } light_controller.changeHSB(HSB[0], HSB[1], HSB[2]); LightPreparePower(1); } else { ResponseLightState(0); } } } void CmndScheme(void) { // Scheme 0..14 - Select one of schemes 0 to 14 // Scheme 2 - Select scheme 2 // Scheme 2,0 - Select scheme 2 with color wheel set to 0 (HSB Red) // Scheme + - Select next scheme // Scheme - - Select previous scheme if (Light.subtype >= LST_RGB) { uint32_t max_scheme = Light.max_scheme; if (1 == XdrvMailbox.data_len) { if (('+' == XdrvMailbox.data[0]) && (Settings->light_scheme < max_scheme)) { XdrvMailbox.payload = Settings->light_scheme + ((0 == Settings->light_scheme) ? 2 : 1); // Skip wakeup } else if (('-' == XdrvMailbox.data[0]) && (Settings->light_scheme > 0)) { XdrvMailbox.payload = Settings->light_scheme - ((2 == Settings->light_scheme) ? 2 : 1); // Skip wakeup } } if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= max_scheme)) { uint32_t parm[2]; if (ParseParameters(2, parm) > 1) { Light.wheel = parm[1]; #ifdef USE_LIGHT_PALETTE Light.wheel--; #endif // USE_LIGHT_PALETTE } LightSetScheme(XdrvMailbox.payload); if (LS_WAKEUP == Settings->light_scheme) { Light.wakeup_active = 3; } LightPowerOn(); Light.strip_timer_counter = 0; // Publish state message for Hass if (Settings->flag3.hass_tele_on_power) { // SetOption59 - Send tele/%topic%/STATE in addition to stat/%topic%/RESULT MqttPublishTeleState(); } } ResponseCmndNumber(Settings->light_scheme); } } void CmndWakeup(void) { // Wakeup - Start wakeup light // Wakeup 0..100 - Start wakeup light to dimmer value 0..100 if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 100)) { light_controller.changeDimmer(XdrvMailbox.payload); } Light.wakeup_active = 3; LightSetScheme(LS_WAKEUP); LightPowerOn(); ResponseCmndChar(PSTR(D_JSON_STARTED)); } void CmndColorTemperature(void) { // CT - Show current color temperature // CT 153..500 - Set color temperature // CT + - Incerement color temperature in steps of 34 // CT - - Decrement color temperature in steps of 34 if (Light.pwm_multi_channels) { return; } if ((LST_COLDWARM == Light.subtype) || (LST_RGBCW == Light.subtype)) { // ColorTemp uint32_t ct = light_state.getCT(); if (1 == XdrvMailbox.data_len) { if ('+' == XdrvMailbox.data[0]) { XdrvMailbox.payload = (ct > (CT_MAX-34)) ? CT_MAX : ct + 34; } else if ('-' == XdrvMailbox.data[0]) { XdrvMailbox.payload = (ct < (CT_MIN+34)) ? CT_MIN : ct - 34; } } if ((XdrvMailbox.payload >= CT_MIN) && (XdrvMailbox.payload <= CT_MAX)) { // https://developers.meethue.com/documentation/core-concepts light_controller.changeCTB(XdrvMailbox.payload, light_state.getBriCT()); LightPreparePower(2); } else { ResponseCmndNumber(ct); } } } void LightDimmerOffset(uint32_t index, int32_t offset) { int32_t dimmer = light_state.getDimmer(index) + offset; if (dimmer < 1) { dimmer = Settings->flag3.slider_dimmer_stay_on; } // SetOption77 - Do not power off if slider moved to far left if (dimmer > 100) { dimmer = 100; } XdrvMailbox.index = index; XdrvMailbox.payload = dimmer; CmndDimmer(); } static int DimmerStep(int dimmer) { int step; int plus = 1; int payload; if (0 == XdrvMailbox.data_len) { return -1; } switch (XdrvMailbox.data[0]) { case '-': plus = 0; case '+': break; default: return -1; } if (1 == XdrvMailbox.data_len) { step = Settings->dimmer_step; } else { char *ep; step = strtoul(XdrvMailbox.data + 1, &ep, 10); if ('\0' != *ep || 1 > step) { return -1; } } if (plus) { payload = min(100, dimmer + step); } else { payload = max(1, dimmer - step); } return payload; } void CmndDimmer(void) { // Dimmer - Show current Dimmer state // Dimmer0 0..100 - Change both RGB and W(W) Dimmers // Dimmer1 0..100 - Change RGB Dimmer // Dimmer2 0..100 - Change W(W) Dimmer // Dimmer3 0..100 - Change both RGB and W(W) Dimmers with no fading // Dimmer + - Incerement Dimmer in steps of DimmerStep // Dimmer - - Decrement Dimmer in steps of DimmerStep uint32_t dimmer; if (XdrvMailbox.index == 3) { TasmotaGlobal.skip_light_fade = true; XdrvMailbox.index = 0; } else if (XdrvMailbox.index > 4) { XdrvMailbox.index = 1; } if ((light_controller.isCTRGBLinked()) || (0 == XdrvMailbox.index)) { dimmer = light_state.getDimmer(); } else { dimmer = light_state.getDimmer(XdrvMailbox.index); } // Handle +/-/!/ special commands int payload = DimmerStep(dimmer); if (-1 != payload) { XdrvMailbox.payload = payload; } else if (1 == XdrvMailbox.data_len) { if ('!' == XdrvMailbox.data[0] && Light.fade_running) { XdrvMailbox.payload = LightGetCurFadeBri(); } else if ('<' == XdrvMailbox.data[0] ) { XdrvMailbox.payload = 1; } else if ('>' == XdrvMailbox.data[0] ) { XdrvMailbox.payload = 100; } } // If value is ok, change it, otherwise report old value if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 100)) { if (light_controller.isCTRGBLinked()) { // normal state, linked RGB and CW if (4 == XdrvMailbox.index) { // change both dimmers, retain ratio between white and color channels uint32_t new_bri = changeUIntScale(XdrvMailbox.payload, 0, 100, 0, 255); LightSetBriScaled(new_bri); } else { // change both dimmers light_controller.changeDimmer(XdrvMailbox.payload); } LightPreparePower(); } else { if (0 != XdrvMailbox.index) { light_controller.changeDimmer(XdrvMailbox.payload, XdrvMailbox.index); LightPreparePower(1 << (XdrvMailbox.index - 1)); // recalculate only the target dimmer } else { // change both dimmers light_controller.changeDimmer(XdrvMailbox.payload, 1); light_controller.changeDimmer(XdrvMailbox.payload, 2); LightPreparePower(); } } #if defined(USE_PWM_DIMMER) && defined(USE_DEVICE_GROUPS) uint8_t bri = light_state.getBri(); if (bri != Settings->bri_power_on) { Settings->bri_power_on = bri; SendDeviceGroupMessage(Light.device, DGR_MSGTYP_PARTIAL_UPDATE, DGR_ITEM_BRI_POWER_ON, Settings->bri_power_on); } #endif // USE_PWM_DIMMER && USE_DEVICE_GROUPS Light.update = true; if (TasmotaGlobal.skip_light_fade) LightAnimate(); } else { ResponseCmndNumber(dimmer); } TasmotaGlobal.skip_light_fade = false; } void CmndDimmerRange(void) { // DimmerRange - Show current dimmer range as used by Tuya and PS16DZ Dimmers // DimmerRange 0,100 - Set dimmer hardware range from 0 to 100 and restart if (XdrvMailbox.data_len > 0) { uint32_t parm[2]; parm[0] = Settings->dimmer_hw_min; parm[1] = Settings->dimmer_hw_max; ParseParameters(2, parm); if (parm[0] < parm[1]) { Settings->dimmer_hw_min = parm[0]; Settings->dimmer_hw_max = parm[1]; } else { Settings->dimmer_hw_min = parm[1]; Settings->dimmer_hw_max = parm[0]; } LightCalcPWMRange(); Light.update = true; } Response_P(PSTR("{\"" D_CMND_DIMMER_RANGE "\":{\"Min\":%d,\"Max\":%d}}"), Settings->dimmer_hw_min, Settings->dimmer_hw_max); } void CmndDimmerStep(void) { // DimmerStep - Show current dimmer step as used by Dimmer +/- // DimmerStep 1..50 - Set dimmer step if (XdrvMailbox.data_len > 0) { if (XdrvMailbox.payload < 1) { Settings->dimmer_step = 1; } else if (XdrvMailbox.payload > 50) { Settings->dimmer_step = 50; } else { Settings->dimmer_step = XdrvMailbox.payload; } } ResponseCmndNumber(Settings->dimmer_step); } void CmndLedTable(void) { // LedTable - Show current LedTable state // LedTable 0 - Turn LedTable Off // LedTable On - Turn LedTable On // LedTable Toggle - Toggle LedTable state if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 2)) { switch (XdrvMailbox.payload) { case 0: // Off case 1: // On Settings->light_correction = XdrvMailbox.payload; break; case 2: // Toggle Settings->light_correction ^= 1; break; } LightCalcPWMRange(); Light.update = true; } ResponseCmndStateText(Settings->light_correction); } void CmndRgbwwTable(void) { // RgbWwTable - Show current RGBWW State // RgbWwTable 255,255,255,255,255 - Set RGBWW state to maximum if ((XdrvMailbox.data_len > 0)) { uint32_t parm[LST_RGBCW -1]; uint32_t parmcount = ParseParameters(LST_RGBCW, parm); for (uint32_t i = 0; i < parmcount; i++) { Settings->rgbwwTable[i] = parm[i]; } Light.update = true; } char scolor[LIGHT_COLOR_SIZE]; scolor[0] = '\0'; for (uint32_t i = 0; i < LST_RGBCW; i++) { snprintf_P(scolor, sizeof(scolor), PSTR("%s%s%d"), scolor, (i > 0) ? "," : "", Settings->rgbwwTable[i]); } ResponseCmndChar(scolor); } void CmndFade(void) { if (2 == XdrvMailbox.index) { // Home Assistant backwards compatibility, can be removed mid 2021 } else { // Fade - Show current Fade state // Fade 0 - Turn Fade Off // Fade On - Turn Fade On // Fade Toggle - Toggle Fade state switch (XdrvMailbox.payload) { case 0: // Off case 1: // On Settings->light_fade = XdrvMailbox.payload; break; case 2: // Toggle Settings->light_fade ^= 1; break; } #ifdef USE_DEVICE_GROUPS if (XdrvMailbox.payload >= 0 && XdrvMailbox.payload <= 2) SendDeviceGroupMessage(Light.device, DGR_MSGTYP_UPDATE, DGR_ITEM_LIGHT_FADE, Settings->light_fade); #endif // USE_DEVICE_GROUPS if (!Settings->light_fade) { LightStopFade(); } } ResponseCmndStateText(Settings->light_fade); } void CmndSpeed(void) { if (2 == XdrvMailbox.index) { // Speed2 ! cancels use of Speed2 in the future if ((1 == XdrvMailbox.data_len) && ('!' == XdrvMailbox.data[0])) { Light.fade_once_enabled = false; Light.speed_once_enabled = false; ResponseCmndDone(); return; } // Speed2 setting will be used only once, then revert to fade/speed if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 40)) { Light.fade_once_enabled = true; Light.fade_once_value = (XdrvMailbox.payload > 0); Light.speed_once_enabled = true; Light.speed_once_value = XdrvMailbox.payload; if (!Light.fade_once_value) { LightStopFade(); } } ResponseCmndIdxNumber(Light.speed_once_value); } else { // Speed 1 - Fast // Speed 40 - Very slow // Speed + - Increment Speed // Speed - - Decrement Speed if (1 == XdrvMailbox.data_len) { if (('+' == XdrvMailbox.data[0]) && (Settings->light_speed > 1)) { XdrvMailbox.payload = Settings->light_speed - 1; } else if (('-' == XdrvMailbox.data[0]) && (Settings->light_speed < 40)) { XdrvMailbox.payload = Settings->light_speed + 1; } } if ((XdrvMailbox.payload > 0) && (XdrvMailbox.payload <= 40)) { Settings->light_speed = XdrvMailbox.payload; #ifdef USE_DEVICE_GROUPS SendDeviceGroupMessage(Light.device, DGR_MSGTYP_UPDATE, DGR_ITEM_LIGHT_SPEED, Settings->light_speed); #endif // USE_DEVICE_GROUPS } ResponseCmndNumber(Settings->light_speed); } } void CmndWakeupDuration(void) { // WakeUpDuration - Show current Wake Up duration in seconds // WakeUpDuration 60 - Set Wake Up duration to 60 seconds if ((XdrvMailbox.payload > 0) && (XdrvMailbox.payload < 3001)) { Settings->light_wakeup = XdrvMailbox.payload; Light.wakeup_active = 0; } ResponseCmndNumber(Settings->light_wakeup); } void CmndCTRange(void) { // Format is "CTRange ctmin,ctmax" // Ex: // CTRange 153,500 // CTRange // CTRange 200,350 char *p; strtok_r(XdrvMailbox.data, ",", &p); if (p != nullptr) { int32_t ct_min = strtol(XdrvMailbox.data, nullptr, 0); int32_t ct_max = strtol(p, nullptr, 0); if ( (ct_min >= CT_MIN) && (ct_min <= CT_MAX) && (ct_max >= CT_MIN) && (ct_max <= CT_MAX) && (ct_min <= ct_max) ) { setCTRange(ct_min, ct_max); } else { return; // error } } Response_P(PSTR("{\"%s\":\"%d,%d\"}"), XdrvMailbox.command, Light.vct_ct[0], Light.vct_ct[CT_PIVOTS-1]); } #ifdef USE_LIGHT_VIRTUAL_CT void CmndVirtualCT(void) { if (!Settings->flag4.virtual_ct) { ResponseCmndChar_P(PSTR("You need to enable `SetOption106 1`")); return; } if (XdrvMailbox.data[0] == ('{')) { // parse JSON JsonParser parser(XdrvMailbox.data); JsonParserObject root = parser.getRootObject(); if (!root) { return; } uint32_t idx = 0; for (auto key : root) { if (idx >= CT_PIVOTS) { ResponseCmndChar_P(PSTR("Too many points")); return; } int32_t ct_val = strtol(key.getStr(), nullptr, 0); if ((ct_val < CT_MIN) || (ct_val > CT_MAX)) { ResponseCmndChar_P(PSTR("CT out of range")); return; } char * color = (char*) key.getValue().getStr(); // call color parser Light.vct_ct[idx] = ct_val; if (LightColorEntry(color, strlen(color))) { memcpy(&Light.vct_color[idx], Light.entry_color, sizeof(Light.vct_color[idx])); } idx++; } for (uint32_t i = idx-1; i < CT_PIVOTS-1; i++) { Light.vct_ct[i+1] = Light.vct_ct[i]; memcpy(&Light.vct_color[i+1], &Light.vct_color[i], sizeof(Light.vct_color[0])); } } checkVirtualCT(); Light.update = true; Response_P(PSTR("{\"%s\":{"), XdrvMailbox.command); uint32_t pivot_len = CT_PIVOTS; vct_pivot_t * pivot = &Light.vct_color[0]; if (Light.vct_ct[1] >= Light.vct_ct[2]) { pivot_len = 2; } // only 2 points are valid bool end = false; for (uint32_t i = 0; (i < CT_PIVOTS) && !end; i++) { if ((i >= CT_PIVOTS-1) || (Light.vct_ct[i] >= Light.vct_ct[i+1])) { end = true; } ResponseAppend_P(PSTR("\"%d\":\"%02X%02X%02X%02X%02X\"%c"), Light.vct_ct[i], (*pivot)[0], (*pivot)[1], (*pivot)[2], (*pivot)[3], (*pivot)[4], end ? '}' : ','); pivot++; } ResponseJsonEnd(); } #endif // USE_LIGHT_VIRTUAL_CT #ifdef USE_LIGHT_PALETTE void CmndPalette(void) { uint8_t * palette_entry; char * p; // Palette Color[ ...] if (XdrvMailbox.data_len) { Light.wheel = 0; Light.palette_count = 0; if (Light.palette) { free(Light.palette); Light.palette = nullptr; } if (XdrvMailbox.data_len > 1 || XdrvMailbox.data[0] != '0') { uint8_t palette_count = 0; char * color = XdrvMailbox.data; if (!(Light.palette = (uint8_t *)malloc(255 * Light.subtype))) return; palette_entry = Light.palette; for (;;) { p = strchr(color, ' '); if (p) *p = 0; color = Trim(color); if (*color && LightColorEntry(color, strlen(color))) { memcpy(palette_entry, Light.entry_color, Light.subtype); palette_entry += Light.subtype; palette_count++; } if (!p) break; color = p + 1; } if (!(Light.palette = (uint8_t *)realloc(Light.palette, palette_count * Light.subtype))) return; Light.palette_count = palette_count; } } char palette_str[5 * Light.subtype * Light.palette_count + 3]; p = palette_str; *p++ = '['; if (Light.palette_count) { palette_entry = Light.palette; for (int entry = 0; entry < Light.palette_count; entry++) { if (Settings->flag.decimal_text) { // SetOption17 - Switch between decimal or hexadecimal output *p++ = '"'; for (uint32_t i = 0; i < Light.subtype; i++) { p += sprintf_P(p, PSTR("%d,"), *palette_entry++); } *(p - 1) = '"'; } else { for (uint32_t i = 0; i < Light.subtype; i++) { p += sprintf_P(p, PSTR("%02X"), *palette_entry++); } } *p++ = ','; } p--; } *p++ = ']'; *p = 0; ResponseCmndChar(palette_str); } #endif // USE_LIGHT_PALETTE #ifdef USE_DGR_LIGHT_SEQUENCE void CmndSequenceOffset(void) { // SequenceOffset , x: 0=offset, 1=Friendly name 1 ending digits + offset [-1] // 2=MQTT topic ending digits + offset [-1] int32_t offset = XdrvMailbox.payload; if (XdrvMailbox.usridx && XdrvMailbox.index > 0) { uint32_t index = SET_FRIENDLYNAME1; if (XdrvMailbox.index == 2) index = SET_MQTT_TOPIC; char * name = SettingsText(index); char * ptr = name + strlen(name); while (--ptr >= name && isdigit(*ptr)); if (!XdrvMailbox.data_len) offset = -1; offset += atoi(ptr + 1); } if (offset >= 0 && offset <= 255) { if (offset != Light.sequence_offset) { if (Light.sequence_offset) free(Light.channels_fifo); Light.sequence_offset = offset; if (Light.sequence_offset) Light.channels_fifo = (uint8_t *)calloc(Light.sequence_offset, LST_MAX); } } ResponseCmndNumber(Light.sequence_offset); } #endif // USE_DGR_LIGHT_SEQUENCE void CmndUndocA(void) { // Theos legacy status char scolor[LIGHT_COLOR_SIZE]; LightGetColor(scolor, true); // force hex whatever Option 17 scolor[6] = '\0'; // RGB only Response_P(PSTR("%s,%d,%d,%d,%d,%d"), scolor, Settings->light_fade, Settings->light_correction, Settings->light_scheme, Settings->light_speed, Settings->light_width); MqttPublishPrefixTopicRulesProcess_P(STAT, XdrvMailbox.topic); ResponseClear(); } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xdrv04(uint32_t function) { bool result = false; if (FUNC_MODULE_INIT == function) { return LightModuleInit(); } else if (TasmotaGlobal.light_type) { switch (function) { case FUNC_SERIAL: result = XlgtCall(FUNC_SERIAL); break; case FUNC_LOOP: if (Light.fade_running) { if (LightApplyFade()) { LightSetOutputs(Light.fade_cur_10); } } #ifdef USE_LIGHT_ARTNET ArtNetLoop(); #endif // USE_LIGHT_ARTNET break; case FUNC_EVERY_50_MSECOND: LightAnimate(); break; #ifdef USE_DEVICE_GROUPS case FUNC_DEVICE_GROUP_ITEM: LightHandleDevGroupItem(); break; #endif // USE_DEVICE_GROUPS case FUNC_SET_POWER: LightSetPower(); break; case FUNC_BUTTON_MULTI_PRESSED: result = XlgtCall(FUNC_BUTTON_MULTI_PRESSED); break; #ifdef USE_WEBSERVER case FUNC_WEB_ADD_MAIN_BUTTON: XlgtCall(FUNC_WEB_ADD_MAIN_BUTTON); break; case FUNC_WEB_GET_ARG: XlgtCall(FUNC_WEB_GET_ARG); break; #endif // USE_WEBSERVER case FUNC_COMMAND: result = DecodeCommand(kLightCommands, LightCommand, kLightSynonyms); if (!result) { result = XlgtCall(FUNC_COMMAND); } break; case FUNC_PRE_INIT: LightInit(); break; #ifdef USE_LIGHT_ARTNET case FUNC_JSON_APPEND: ArtNetJSONAppend(); break; case FUNC_NETWORK_UP: ArtNetFuncNetworkUp(); break; case FUNC_NETWORK_DOWN: ArtNetFuncNetworkDown(); break; #endif // USE_LIGHT_ARTNET case FUNC_ACTIVE: result = true; break; } } return result; } #endif // USE_LIGHT