From 81ca44dba216e9468a2e3a50fca6d197453812ac Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Tue, 6 Aug 2019 10:57:50 +0200 Subject: [PATCH] Add SetOption68 to enable multi-channel PWM instead of a single light (#6134) --- sonoff/_changelog.ino | 1 + sonoff/settings.h | 2 +- sonoff/sonoff.h | 3 +- sonoff/sonoff.ino | 7 + sonoff/support_command.ino | 3 + sonoff/xdrv_01_webserver.ino | 7 + sonoff/xdrv_04_light.ino | 346 ++++++++++++++++++++++++++--------- sonoff/xdrv_20_hue.ino | 131 +++++++++---- 8 files changed, 376 insertions(+), 124 deletions(-) diff --git a/sonoff/_changelog.ino b/sonoff/_changelog.ino index 5a1674254..6945cf6ea 100644 --- a/sonoff/_changelog.ino +++ b/sonoff/_changelog.ino @@ -9,6 +9,7 @@ * Add define USE_ENERGY_POWER_LIMIT to disable Energy Power Limit detection while Energy Margin detection is active * Add allow repeat/longpress for IRSend raw, introduced IRSend option (#6074) * Change Store AWS IoT Private Key and Certificate in SPI Flash avoiding device-specific compilations + * Add SetOption68 to enable multi-channel PWM instead of a single light (#6134) * * 6.6.0.2 20190714 * Change commands Var and Mem to show all parameters when no index is given (#6107) diff --git a/sonoff/settings.h b/sonoff/settings.h index d30a82ea9..c4c5001c2 100644 --- a/sonoff/settings.h +++ b/sonoff/settings.h @@ -81,7 +81,7 @@ typedef union { // Restricted by MISRA-C Rule 18.4 bu uint32_t tuya_show_dimmer : 1; // bit 15 (v6.5.0.15) - SetOption65 - Enable or Disable Dimmer slider control uint32_t tuya_dimmer_range_255 : 1; // bit 16 (v6.6.0.1) - SetOption66 - Enable or Disable Dimmer range 255 slider control uint32_t buzzer_enable : 1; // bit 17 (v6.6.0.1) - SetOption67 - Enable buzzer when available - uint32_t spare18 : 1; + uint32_t pmw_multi_channels : 1; // bit 18 (v6.6.0.3) - SetOption68 - Enable multi-channels PWM insteas of Color PWM uint32_t spare19 : 1; uint32_t spare20 : 1; uint32_t spare21 : 1; diff --git a/sonoff/sonoff.h b/sonoff/sonoff.h index 5a7d9c76a..d79724cc9 100644 --- a/sonoff/sonoff.h +++ b/sonoff/sonoff.h @@ -59,6 +59,7 @@ const uint8_t MAX_COUNTERS = 4; // Max number of counter sensors const uint8_t MAX_TIMERS = 16; // Max number of Timers const uint8_t MAX_PULSETIMERS = 8; // Max number of supported pulse timers const uint8_t MAX_FRIENDLYNAMES = 4; // Max number of Friendly names +const uint8_t MAX_HUE_DEVICES = 15; // Max number of Philips Hue device per emulation const uint8_t MAX_DOMOTICZ_IDX = 4; // Max number of Domoticz device, key and switch indices const uint8_t MAX_DOMOTICZ_SNS_IDX = 12; // Max number of Domoticz sensors indices const uint8_t MAX_KNX_GA = 10; // Max number of KNX Group Addresses to read that can be set @@ -264,7 +265,7 @@ const uint8_t kDefaultRfCode[9] PROGMEM = { 0x21, 0x16, 0x01, 0x0E, 0x03, 0x48, \*********************************************************************************************/ extern uint8_t light_device; // Light device number -extern uint8_t light_power; // Light power +extern power_t light_power; // Light power extern uint8_t rotary_changed; // Rotary switch changed #endif // _SONOFF_H_ diff --git a/sonoff/sonoff.ino b/sonoff/sonoff.ino index c6c5159ed..5e70b87ee 100755 --- a/sonoff/sonoff.ino +++ b/sonoff/sonoff.ino @@ -1451,6 +1451,13 @@ void GpioInit(void) light_type |= LT_SM16716; } #endif // USE_SM16716 + + // post-process for lights + if (Settings.flag3.pmw_multi_channels) { + uint32_t pwm_channels = (light_type & 7) > LST_MAX ? LST_MAX : (light_type & 7); + if (0 == pwm_channels) pwm_channels = 1; + devices_present += pwm_channels - 1; // add the pwm channels controls at the end + } #endif // USE_LIGHT if (!light_type) { for (uint32_t i = 0; i < MAX_PWMS; i++) { // Basic PWM control only diff --git a/sonoff/support_command.ino b/sonoff/support_command.ino index 1e84f9fce..2c2e7038f 100644 --- a/sonoff/support_command.ino +++ b/sonoff/support_command.ino @@ -585,6 +585,9 @@ void CmndSetoption(void) if (10 == pindex) { // SetOption60 enable or disable traditional sleep WiFiSetSleepMode(); // Update WiFi sleep mode accordingly } + if (18 == pindex) { // SetOption68 for multi-channel PWM, requires a reboot + restart_flag = 2; + } } } else { // SetOption32 .. 49 diff --git a/sonoff/xdrv_01_webserver.ino b/sonoff/xdrv_01_webserver.ino index 591fa8da2..c80dd7318 100644 --- a/sonoff/xdrv_01_webserver.ino +++ b/sonoff/xdrv_01_webserver.ino @@ -46,6 +46,8 @@ uint8_t *efm8bb1_update = nullptr; enum UploadTypes { UPL_TASMOTA, UPL_SETTINGS, UPL_EFM8BB1 }; +static const char * HEADER_KEYS[] = { "User-Agent", }; + const char HTTP_HEAD[] PROGMEM = "" "" @@ -543,6 +545,11 @@ void StartWebserver(int type, IPAddress ipweb) #endif // Not FIRMWARE_MINIMAL } reset_web_log_flag = false; + + // Collect User-Agent for Alexa Hue Emulation + // This is used in xdrv_20_hue.ino in function findEchoGeneration() + WebServer->collectHeaders(HEADER_KEYS, sizeof(HEADER_KEYS)/sizeof(char*)); + WebServer->begin(); // Web server start } if (webserver_state != type) { diff --git a/sonoff/xdrv_04_light.ino b/sonoff/xdrv_04_light.ino index 8728f8bff..40f0ff64d 100644 --- a/sonoff/xdrv_04_light.ino +++ b/sonoff/xdrv_04_light.ino @@ -126,7 +126,7 @@ \*********************************************************************************************/ #define XDRV_04 4 -//#define DEBUG_LIGHT +// #define DEBUG_LIGHT const uint8_t LIGHT_COLOR_SIZE = 25; // Char array scolor size const uint8_t WS2812_SCHEMES = 7; // Number of additional WS2812 schemes supported by xdrv_ws2812.ino @@ -241,10 +241,11 @@ uint8_t light_new_color[LST_MAX]; uint8_t light_last_color[LST_MAX]; uint8_t light_color_remap[LST_MAX]; +power_t light_power = 0; // Power for each channel if SetOption68, or boolean if single light uint8_t light_wheel = 0; uint8_t light_subtype = 0; // LST_ subtype +bool light_pwm_multi_channels = false; // SetOption68, treat each PWM channel as an independant dimmer uint8_t light_device = 0; -uint8_t light_power = 0; uint8_t light_old_power = 1; uint8_t light_update = 1; uint8_t light_wakeup_active = 0; @@ -772,6 +773,7 @@ private: // 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) { @@ -784,7 +786,11 @@ public: inline bool setCTRGBLinked(bool ct_rgb_linked) { bool prev = _ct_rgb_linked; - _ct_rgb_linked = 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; } @@ -792,6 +798,17 @@ public: 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; @@ -815,12 +832,15 @@ public: // 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]); - // We apply dimmer in priority to RGB - uint8_t bri = _state->DimmerToBri(Settings.light_dimmer); - if (Settings.light_color[0] + Settings.light_color[1] + Settings.light_color[2] > 0) { - _state->setBriRGB(bri); - } else { - _state->setBriCT(bri); + if (!_pwm_multi_channels) { + // only if non-multi channel + // We apply dimmer in priority to RGB + uint8_t bri = _state->DimmerToBri(Settings.light_dimmer); + if (Settings.light_color[0] + Settings.light_color[1] + Settings.light_color[2] > 0) { + _state->setBriRGB(bri); + } else { + _state->setBriCT(bri); + } } } @@ -864,6 +884,15 @@ public: void calcLevels() { uint8_t r,g,b,c,w,briRGB,briCT; _state->getActualRGBCW(&r,&g,&b,&c,&w); + + if (_pwm_multi_channels) { // if PWM multi channel, no more transformation required + light_current_color[0] = r; + light_current_color[1] = g; + light_current_color[2] = b; + light_current_color[3] = c; + light_current_color[4] = w; + return; + } briRGB = _state->getBriRGB(); briCT = _state->getBriCT(); @@ -907,20 +936,28 @@ public: // save the current light state to Settings. void saveSettings() { - uint8_t cm = _state->getColorMode(); + if (light_pwm_multi_channels) { + // simply save each channel + _state->getActualRGBCW(&Settings.light_color[0], &Settings.light_color[1], + &Settings.light_color[2], &Settings.light_color[3], + &Settings.light_color[4]); + 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)); - 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]); + memset(&Settings.light_color[0], 0, sizeof(Settings.light_color)); + 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()); } - } 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_P2(LOG_LEVEL_DEBUG_MORE, "LightControllerClass::saveSettings Settings.light_color (%d %d %d %d %d - %d)", @@ -1322,6 +1359,7 @@ void LightInit(void) light_device = devices_present; light_subtype = (light_type & 7) > LST_MAX ? LST_MAX : (light_type & 7); // Always 0 - LST_MAX (5) + light_pwm_multi_channels = Settings.flag3.pmw_multi_channels; #if defined(USE_WS2812) && (USE_WS2812_CTYPE > NEO_3LED) if (LT_WS2812 == light_type) { @@ -1329,6 +1367,16 @@ void LightInit(void) } #endif + 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 = devices_present - light_subtype + 1; // adjust if we also have relays + } +#ifdef DEBUG_LIGHT + AddLog_P2(LOG_LEVEL_DEBUG_MORE, "LightInit light_pwm_multi_channels=%d light_subtype=%d light_device=%d devices_present=%d", + light_pwm_multi_channels, light_subtype, light_device, devices_present); +#endif + light_controller.setSubType(light_subtype); light_controller.loadSettings(); @@ -1456,6 +1504,32 @@ void LightSetDimmer(uint8_t dimmer) { light_controller.changeDimmer(dimmer); } +// 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 <= devices_present)) { + bri = light_current_color[device - light_device]; + } + } else if (device == light_device) { + bri = light_state.getBri(); + } + return bri; +} + +// If SetOption68 is set, get 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 <= devices_present)) { + light_current_color[device - light_device] = bri; + light_controller.changeChannels(light_current_color); + } + } else if (device == light_device) { + light_controller.changeBri(bri); + } +} + void LightSetColorTemp(uint16_t ct) { /* Color Temperature (https://developers.meethue.com/documentation/core-concepts) @@ -1568,19 +1642,52 @@ void LightState(uint8_t append) void LightPreparePower(void) { - if (light_state.getBri() && !(light_power)) { - if (!Settings.flag.not_power_linked) { - ExecuteCommandPower(light_device, POWER_ON_NO_STATE, SRC_LIGHT); +#ifdef DEBUG_LIGHT + AddLog_P2(LOG_LEVEL_DEBUG, "LightPreparePower power=%d light_power=%d", 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 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)) && (0 == bitRead(power, i + light_device - 1))) { +// ExecuteCommandPower(light_device + i, POWER_ON_NO_STATE, SRC_LIGHT); +// //bitSet(Settings.power, i + light_device - 1); +// #ifdef DEBUG_LIGHT +// AddLog_P2(LOG_LEVEL_DEBUG, "ExecuteCommandPower ON device=%d", light_device + i); +// #endif +// } +// // if channel is zero and channel is on, set it off +// if ((0 == light_current_color[i]) && bitRead(power, i + light_device - 1)) { +// ExecuteCommandPower(light_device + i, POWER_OFF_NO_STATE, SRC_LIGHT); +// //bitClear(Settings.power, i + light_device - 1); +// #ifdef DEBUG_LIGHT +// AddLog_P2(LOG_LEVEL_DEBUG, "ExecuteCommandPower OFF device=%d", light_device + i); +// #endif +// } +// #ifdef USE_DOMOTICZ +// DomoticzUpdatePowerState(light_device + i); +// #endif // USE_DOMOTICZ +// } + } else { + if (light_state.getBri() && !(light_power)) { + if (!Settings.flag.not_power_linked) { + 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 if (!light_state.getBri() && light_power) { - ExecuteCommandPower(light_device, POWER_OFF_NO_STATE, SRC_LIGHT); - } #ifdef USE_DOMOTICZ - DomoticzUpdatePowerState(light_device); + DomoticzUpdatePowerState(light_device); #endif // USE_DOMOTICZ + } + if (Settings.flag3.hass_tele_on_power) { MqttPublishTeleState(); } +#ifdef DEBUG_LIGHT + AddLog_P2(LOG_LEVEL_DEBUG, "LightPreparePower End power=%d light_power=%d", power, light_power); +#endif + light_power = power >> (light_device - 1); // reset next state LightState(0); } @@ -1667,11 +1774,26 @@ void LightSetPower(void) { // light_power = XdrvMailbox.index; light_old_power = light_power; - light_power = bitRead(XdrvMailbox.index, light_device -1); + //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 + } + uint32_t shift = light_device - 1; + // If PWM multi_channels + // Ex: 3 Relays and 4 PWM - 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: devices_present == light_subtype + light_power = (XdrvMailbox.index & (mask << shift)) >> shift; if (light_wakeup_active) { light_wakeup_active--; } - if (light_power && !light_old_power) { +#ifdef DEBUG_LIGHT + AddLog_P2(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 = 1; } LightAnimate(); @@ -1762,6 +1884,22 @@ void LightAnimate(void) } if ((Settings.light_scheme < LS_MAX) || !light_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(light_power,i)) { // if power down bit is zero + light_new_color[i] = 0; // shut down this channel + } + } + // #ifdef DEBUG_LIGHT + // AddLog_P2(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 + } + if (memcmp(light_last_color, light_new_color, light_subtype)) { light_update = 1; } @@ -1777,62 +1915,11 @@ void LightAnimate(void) } if (PHILIPS == my_module_type) { - // Xiaomi Philips bulbs follow a different scheme: - uint8_t cold; // channel 1 is the color tone, mapped to cold channel (0..255) - light_state.getCW(&cold, nullptr); - cur_col[1] = cold; - cur_col_10bits[1] = changeUIntScale(cur_col[1], 0, 255, 0, 1023); - // now set channel 0 to overall brightness - uint8_t pxBri = light_state.getBriCT(); - // channel 0=intensity, channel1=temperature - if (Settings.light_correction) { // gamma correction - cur_col[0] = ledGamma(pxBri); - cur_col_10bits[0] = ledGamma(pxBri, 10); // 10 bits gamma correction - } else { - cur_col[0] = pxBri; - cur_col_10bits[0] = changeUIntScale(pxBri, 0, 255, 0, 1023); // no gamma, extend to 10 bits - } + calcGammaXiaomiBulbs(cur_col, cur_col_10bits); + } else if (light_pwm_multi_channels) { + calcGammaMultiChannels(cur_col, cur_col_10bits); } else { // PHILIPS != my_module_type - // Apply gamma correction for 8 and 10 bits resolutions, if needed - if (Settings.light_correction) { - // First apply combined correction to the overall white power - if ((LST_COLDWARM == light_subtype) || (LST_RGBWC == light_subtype)) { - uint8_t w_idx[2] = {0, 1}; // if LST_COLDWARM, channels 0 and 1 - if (LST_RGBWC == light_subtype) { // if LST_RGBWC, channels 3 and 4 - w_idx[0] = 3; - w_idx[1] = 4; - } - uint16_t white_bri = cur_col[w_idx[0]] + cur_col[w_idx[1]]; - // if sum of both channels is > 255, then channels are probablu uncorrelated - if (white_bri <= 255) { - // we calculate the gamma corrected sum of CW + WW - uint16_t white_bri_10bits = ledGamma(white_bri, 10); - uint8_t white_bri_8bits = ledGamma(white_bri); - // then we split the total energy among the cold and warm leds - cur_col_10bits[w_idx[0]] = changeUIntScale(cur_col[w_idx[0]], 0, white_bri, 0, white_bri_10bits); - cur_col_10bits[w_idx[1]] = changeUIntScale(cur_col[w_idx[1]], 0, white_bri, 0, white_bri_10bits); - cur_col[w_idx[0]] = changeUIntScale(cur_col[w_idx[0]], 0, white_bri, 0, white_bri_8bits); - cur_col[w_idx[1]] = changeUIntScale(cur_col[w_idx[1]], 0, white_bri, 0, white_bri_8bits); - } else { - cur_col_10bits[w_idx[0]] = ledGamma(cur_col[w_idx[0]], 10); - cur_col_10bits[w_idx[1]] = ledGamma(cur_col[w_idx[1]], 10); - cur_col[w_idx[0]] = ledGamma(cur_col[w_idx[0]]); - cur_col[w_idx[1]] = ledGamma(cur_col[w_idx[1]]); - } - } - // then apply gamma correction to RGB channels - if (LST_RGB <= light_subtype) { - for (uint32_t i = 0; i < 3; i++) { - cur_col_10bits[i] = ledGamma(cur_col[i], 10); - cur_col[i] = ledGamma(cur_col[i]); - } - } - // If RGBW or Single channel, also adjust White channel - if (LST_COLDWARM != light_subtype) { - cur_col_10bits[3] = ledGamma(cur_col[3], 10); - cur_col[3] = ledGamma(cur_col[3]); - } - } + calcGammaBulbs(cur_col, cur_col_10bits); // Now see if we need to mix RGB and True White // Valid only for LST_RGBW, LST_RGBWC, rgbwwTable[4] is zero, and white is zero (see doc) @@ -1931,6 +2018,79 @@ void LightAnimate(void) } } +// Do specific computation for Xiaomi Bulbs +void calcGammaXiaomiBulbs(uint8_t cur_col[5], uint16_t cur_col_10bits[5]) { + // Xiaomi Philips bulbs follow a different scheme: + uint8_t cold; // channel 1 is the color tone, mapped to cold channel (0..255) + light_state.getCW(&cold, nullptr); + cur_col[1] = cold; + cur_col_10bits[1] = changeUIntScale(cur_col[1], 0, 255, 0, 1023); + // now set channel 0 to overall brightness + uint8_t pxBri = light_state.getBriCT(); + // channel 0=intensity, channel1=temperature + if (Settings.light_correction) { // gamma correction + cur_col[0] = ledGamma(pxBri); + cur_col_10bits[0] = ledGamma(pxBri, 10); // 10 bits gamma correction + } else { + cur_col[0] = pxBri; + cur_col_10bits[0] = changeUIntScale(pxBri, 0, 255, 0, 1023); // no gamma, extend to 10 bits + } +} + +// Just apply basic Gamma to each channel +void calcGammaMultiChannels(uint8_t cur_col[5], uint16_t cur_col_10bits[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_10bits[i] = ledGamma(cur_col[i], 10); + cur_col[i] = ledGamma(cur_col[i]); + } + } +} + +void calcGammaBulbs(uint8_t cur_col[5], uint16_t cur_col_10bits[5]) { + // Apply gamma correction for 8 and 10 bits resolutions, if needed + if (Settings.light_correction) { + // First apply combined correction to the overall white power + if ((LST_COLDWARM == light_subtype) || (LST_RGBWC == light_subtype)) { + uint8_t w_idx[2] = {0, 1}; // if LST_COLDWARM, channels 0 and 1 + if (LST_RGBWC == light_subtype) { // if LST_RGBWC, channels 3 and 4 + w_idx[0] = 3; + w_idx[1] = 4; + } + uint16_t white_bri = cur_col[w_idx[0]] + cur_col[w_idx[1]]; + // if sum of both channels is > 255, then channels are probablu uncorrelated + if (white_bri <= 255) { + // we calculate the gamma corrected sum of CW + WW + uint16_t white_bri_10bits = ledGamma(white_bri, 10); + uint8_t white_bri_8bits = ledGamma(white_bri); + // then we split the total energy among the cold and warm leds + cur_col_10bits[w_idx[0]] = changeUIntScale(cur_col[w_idx[0]], 0, white_bri, 0, white_bri_10bits); + cur_col_10bits[w_idx[1]] = changeUIntScale(cur_col[w_idx[1]], 0, white_bri, 0, white_bri_10bits); + cur_col[w_idx[0]] = changeUIntScale(cur_col[w_idx[0]], 0, white_bri, 0, white_bri_8bits); + cur_col[w_idx[1]] = changeUIntScale(cur_col[w_idx[1]], 0, white_bri, 0, white_bri_8bits); + } else { + cur_col_10bits[w_idx[0]] = ledGamma(cur_col[w_idx[0]], 10); + cur_col_10bits[w_idx[1]] = ledGamma(cur_col[w_idx[1]], 10); + cur_col[w_idx[0]] = ledGamma(cur_col[w_idx[0]]); + cur_col[w_idx[1]] = ledGamma(cur_col[w_idx[1]]); + } + } + // then apply gamma correction to RGB channels + if (LST_RGB <= light_subtype) { + for (uint32_t i = 0; i < 3; i++) { + cur_col_10bits[i] = ledGamma(cur_col[i], 10); + cur_col[i] = ledGamma(cur_col[i]); + } + } + // If RGBW or Single channel, also adjust White channel + if (LST_COLDWARM != light_subtype) { + cur_col_10bits[3] = ledGamma(cur_col[3], 10); + cur_col[3] = ledGamma(cur_col[3]); + } + } +} + /*********************************************************************************************\ * Commands \*********************************************************************************************/ @@ -2080,10 +2240,16 @@ void CmndChannel(void) bool coldim = false; // Set "Channel" directly - this allows Color and Direct PWM control to coexist if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 100)) { - light_current_color[XdrvMailbox.index-1] = changeUIntScale(XdrvMailbox.payload,0,100,0,255); - // if we change channels 1,2,3 then turn off CT mode (unless non-linked) - if ((XdrvMailbox.index <= 3) && (light_controller.isCTRGBLinked())) { - light_current_color[3] = light_current_color[4] = 0; + light_current_color[XdrvMailbox.index - light_device] = changeUIntScale(XdrvMailbox.payload,0,100,0,255); + if (light_pwm_multi_channels) { + // if (!Settings.flag.not_power_linked) { // SetOption20 + // light_power = light_power | (1 << (XdrvMailbox.index - light_device)); // ask to turn on channel + // } + } else { + // if we change channels 1,2,3 then turn off CT mode (unless non-linked) + if ((XdrvMailbox.index <= 3) && (light_controller.isCTRGBLinked())) { + light_current_color[3] = light_current_color[4] = 0; + } } light_controller.changeChannels(light_current_color); coldim = true; diff --git a/sonoff/xdrv_20_hue.ino b/sonoff/xdrv_20_hue.ino index 5b2dece9a..42f471907 100644 --- a/sonoff/xdrv_20_hue.ino +++ b/sonoff/xdrv_20_hue.ino @@ -243,6 +243,22 @@ uint16_t prev_ct = 254; char prev_x_str[24] = "\0"; // store previously set xy by Alexa app char prev_y_str[24] = "\0"; +uint8_t getLocalLightSubtype(uint8_t device) { + if (light_type) { + if (device >= light_device) { + if (Settings.flag3.pmw_multi_channels) { + return LST_SINGLE; // If SetOption68, each channel acts like a dimmer + } else { + return light_subtype; // the actual light + } + } else { + return LST_NONE; // relays + } + } else { + return LST_NONE; + } +} + void HueLightStatus1(uint8_t device, String *response) { uint16_t ct = 0; @@ -251,14 +267,18 @@ void HueLightStatus1(uint8_t device, String *response) uint16_t hue = 0; uint8_t sat = 0; uint8_t bri = 254; + uint32_t echo_gen = findEchoGeneration(); // 1 for 1st gen =+ Echo Dot 2nd gen, 2 for 2nd gen and above + // local_light_subtype simulates the light_subtype for 'device' + // For relays LST_NONE, for dimmers LST_SINGLE + uint8_t local_light_subtype = getLocalLightSubtype(device); + bri = LightGetBri(device); // get Dimmer corrected with SetOption68 + if (bri > 254) bri = 254; // Philips Hue bri is between 1 and 254 + if (bri < 1) bri = 1; if (light_type) { light_state.getHSB(&hue, &sat, nullptr); - bri = light_state.getBri(); - if (bri > 254) bri = 254; // Philips Hue bri is between 1 and 254 - if (bri < 1) bri = 1; if ((bri > prev_bri ? bri - prev_bri : prev_bri - bri) < 1) bri = prev_bri; @@ -293,17 +313,17 @@ void HueLightStatus1(uint8_t device, String *response) *response += FPSTR(HUE_LIGHTS_STATUS_JSON1); response->replace("{state}", (power & (1 << (device-1))) ? "true" : "false"); // Brightness for all devices with PWM - //if (LST_SINGLE <= light_subtype) { - light_status += "\"bri\":"; - light_status += String(bri); - light_status += ","; - //} - if (LST_COLDWARM <= light_subtype) { + if ((1 == echo_gen) || (LST_SINGLE <= local_light_subtype)) { // force dimmer for 1st gen Echo + light_status += "\"bri\":"; + light_status += String(bri); + light_status += ","; + } + if (LST_COLDWARM <= local_light_subtype) { light_status += F("\"colormode\":\""); light_status += (g_gotct ? "ct" : "hs"); light_status += "\","; } - if (LST_RGB <= light_subtype) { // colors + if (LST_RGB <= local_light_subtype) { // colors if (prev_x_str[0] && prev_y_str[0]) { light_status += "\"xy\":["; light_status += prev_x_str; @@ -327,10 +347,10 @@ void HueLightStatus1(uint8_t device, String *response) light_status += String(sat); light_status += ","; } - if (LST_COLDWARM == light_subtype || LST_RGBW <= light_subtype) { // white temp + if (LST_COLDWARM == local_light_subtype || LST_RGBW <= local_light_subtype) { // white temp light_status += "\"ct\":"; - light_status += String(ct > 0 ? ct : 284); - light_status += ","; // if no ct, default to medium white + light_status += String(ct > 0 ? ct : 284); // if no ct, default to medium white + light_status += ","; } response->replace("{light_status}", light_status); } @@ -338,7 +358,20 @@ void HueLightStatus1(uint8_t device, String *response) void HueLightStatus2(uint8_t device, String *response) { *response += FPSTR(HUE_LIGHTS_STATUS_JSON2); - response->replace("{j1", Settings.friendlyname[device-1]); + if (device <= MAX_FRIENDLYNAMES) { + response->replace("{j1", Settings.friendlyname[device-1]); + } else { + char fname[33]; + strcpy(fname, Settings.friendlyname[MAX_FRIENDLYNAMES-1]); + uint32_t fname_len = strlen(fname); + if (fname_len >= 33-3) { + fname[33-3] = 0x00; + fname_len = 33-3; + } + fname[fname_len++] = '-'; + fname[fname_len++] = '0' + device - MAX_FRIENDLYNAMES; + response->replace("{j1", fname); + } response->replace("{j2", GetHueDeviceId(device)); } @@ -357,10 +390,32 @@ uint32_t DecodeLightId(uint32_t id) { return id & 0xF; } +static const char * FIRST_GEN_UA[] = { // list of User-Agents signature + "AEOBC", // Echo Dot 2ng Generation +}; + +// Check if the Echo device is of 1st generation, which triggers different results +uint32_t findEchoGeneration(void) { + // result is 1 for 1st gen, 2 for 2nd gen and further + String user_agent = WebServer->header("User-Agent"); + uint32_t gen = 2; + + for (uint32_t i = 0; i < sizeof(FIRST_GEN_UA)/sizeof(char*); i++) { + if (user_agent.indexOf(FIRST_GEN_UA[i]) >= 0) { // found + gen = 1; + break; + } + } + + AddLog_P2(LOG_LEVEL_DEBUG_MORE, D_LOG_HTTP D_HUE " User-Agent: %s, gen=%d", user_agent.c_str(), gen); // Header collection is set in xdrv_01_webserver.ino, in StartWebserver() + + return gen; +} + void HueGlobalConfig(String *path) { String response; - uint8_t maxhue = (devices_present > MAX_FRIENDLYNAMES) ? MAX_FRIENDLYNAMES : devices_present; + uint8_t maxhue = (devices_present > MAX_HUE_DEVICES) ? MAX_HUE_DEVICES : devices_present; path->remove(0,1); // cut leading / to get response = F("{\"lights\":{\""); @@ -403,7 +458,8 @@ void HueLights(String *path) bool on = false; bool change = false; // need to change a parameter to the light uint8_t device = 1; - uint8_t maxhue = (devices_present > MAX_FRIENDLYNAMES) ? MAX_FRIENDLYNAMES : devices_present; + uint8_t local_light_subtype = light_subtype; + uint8_t maxhue = (devices_present > MAX_HUE_DEVICES) ? MAX_HUE_DEVICES : devices_present; path->remove(0,path->indexOf("/lights")); // Remove until /lights if (path->endsWith("/lights")) { // Got /lights @@ -426,6 +482,8 @@ void HueLights(String *path) if ((device < 1) || (device > maxhue)) { device = 1; } + local_light_subtype = getLocalLightSubtype(device); // get the subtype for this device + if (WebServer->args()) { response = "["; @@ -452,14 +510,18 @@ void HueLights(String *path) resp = true; } - if (light_type) { - light_state.getHSB(&hue, &sat, nullptr); - bri = light_state.getBri(); // get the combined bri for CT and RGB, not only the RGB one - ct = light_state.getCT(); - uint8_t color_mode = light_state.getColorMode(); - if (LCM_RGB == color_mode) { g_gotct = false; } - if (LCM_CT == color_mode) { g_gotct = true; } - // If LCM_BOTH == color_mode, leave g_gotct unchanged + if (light_type && (local_light_subtype >= LST_SINGLE)) { + if (!Settings.flag3.pmw_multi_channels) { + light_state.getHSB(&hue, &sat, nullptr); + bri = light_state.getBri(); // get the combined bri for CT and RGB, not only the RGB one + ct = light_state.getCT(); + uint8_t color_mode = light_state.getColorMode(); + if (LCM_RGB == color_mode) { g_gotct = false; } + if (LCM_CT == color_mode) { g_gotct = true; } + // If LCM_BOTH == color_mode, leave g_gotct unchanged + } else { // treat each channel as simple dimmer + bri = LightGetBri(device); + } } prev_x_str[0] = prev_y_str[0] = 0; // reset xy string @@ -551,14 +613,18 @@ void HueLights(String *path) resp = true; } if (change) { - if (light_type) { - if (g_gotct) { - light_controller.changeCTB(ct, bri); - } else { - light_controller.changeHSB(hue, sat, bri); + if (light_type && (local_light_subtype > LST_NONE)) { // not relay + if (!Settings.flag3.pmw_multi_channels) { + if (g_gotct) { + light_controller.changeCTB(ct, bri); + } else { + light_controller.changeHSB(hue, sat, bri); + } + LightPreparePower(); + } else { // SetOption68 On, each channel is a dimmer + LightSetBri(device, bri); } - LightPreparePower(); - if (LST_COLDWARM <= light_subtype) { + if (LST_COLDWARM <= local_light_subtype) { MqttPublishPrefixTopic_P(RESULT_OR_STAT, PSTR(D_CMND_COLOR)); } else { MqttPublishPrefixTopic_P(RESULT_OR_STAT, PSTR(D_CMND_DIMMER)); @@ -577,6 +643,7 @@ void HueLights(String *path) } } else if(path->indexOf("/lights/") >= 0) { // Got /lights/ID + AddLog_P2(LOG_LEVEL_DEBUG_MORE, "/lights path=%s", path->c_str()); path->remove(0,8); // Remove /lights/ device = DecodeLightId(atoi(path->c_str())); if ((device < 1) || (device > maxhue)) { @@ -600,7 +667,7 @@ void HueGroups(String *path) * http://sonoff/api/username/groups?1={"name":"Woonkamer","lights":[],"type":"Room","class":"Living room"}) */ String response = "{}"; - uint8_t maxhue = (devices_present > MAX_FRIENDLYNAMES) ? MAX_FRIENDLYNAMES : devices_present; + uint8_t maxhue = (devices_present > MAX_HUE_DEVICES) ? MAX_HUE_DEVICES : devices_present; if (path->endsWith("/0")) { response = FPSTR(HUE_GROUP0_STATUS_JSON);