diff --git a/MODULES.md b/MODULES.md index bb02bfe73..be1d5d110 100644 --- a/MODULES.md +++ b/MODULES.md @@ -76,5 +76,6 @@ Module | Description 70 Sonoff L1 | Sonoff L1 light strip 71 Sonoff iFan03 | Sonoff iFan03 Wifi Smart Ceiling Fan with Light 72 EXS Dimmer | EXS Wifi Dimmer v4 +73 PWM Dimmer | Martin Jerry/acenx/Tessan/NTONPOWER SD0x PWM Dimmer Switches Over 600 additional devices are supported using [templates](TEMPLATES.md). diff --git a/PWM_Dimmer.md b/PWM_Dimmer.md new file mode 100644 index 000000000..da687bcf2 --- /dev/null +++ b/PWM_Dimmer.md @@ -0,0 +1,229 @@ +# PWM Dimmer + +The PWM Dimmer module adds support for Martin Jerry/acenx/Tessan/NTONPOWER SD0x PWM dimmer switches. The brightness of the load for these dimmers is controlled by a PWM GPIO pin. They typically have power, up and down buttons, a powered-on LED, five brightness LEDs and another status LED. Examples are:[ https://www.amazon.com/dp/B07FXYSVR1](https://www.amazon.com/dp/B07FXYSVR1),[ https://www.amazon.com/dp/B07V26Q3VD](https://www.amazon.com/dp/B07V26Q3VD),[ https://www.amazon.com/dp/B07K67D43J](https://www.amazon.com/dp/B07K67D43J),[ https://www.amazon.com/dp/B07TTGFWFM](https://www.amazon.com/dp/B07TTGFWFM) + +To include PWM dimmer support in the build, define USE_PWM_DIMMER in your user_config_override. This adds 4.5K to the code size. The light module is not required for PWM dimmer operation so you can #undef USE_LIGHT to reduce the firmware bin size. + +To enable PWM dimmer operation, select the PWM Dimmer module. + + +## PWM Dimmer Operation + +Pressing and releasing the power button toggles the power on/off. If the toggle turns the power on, the load is returned to the last brightness it was adjusted to. If Fade is enabled, the load is faded on/off at the rate defined by the Speed setting. + +When the power is on, holding the down or up button decreases/increases the brightness (PWM value). The brightness is changed faster at higher brightnesses. The BriMin command defines the lowest value the brightness can be decreased to. + +The brightness can also be changed using just the power button. When the power is on, holding the power button alternately increases or decreases the brightness. Initially, holding the power button increases the brightness. Releasing and then holding the power button again decreases the brightness. + +When the power is off, holding the down or up button turns the power on at a temporary brightness of the low/high levels set by the BriPreset command (default =10,255). Turning the power on at the low preset can also be accomplished by holding the power button while the power is off. The brightness presets are intended to enable quickly turning on a light to a dim or bright level without changing the normal desired brightness. Turning the power on to a preset does not change the brightness the load will be set to when the switch is turned on the next time. For example, if the light is on and you adjust the brightness to 80 and then turn the light off, when you turn it back on, the brightness will return to 80. If you turn the power off again and then press the down button, the light will be turned on with a brightness of the low preset. If you then turn the light off and on again, the brightness will return to 80. + +If there are LED’s defined in the template, they are turned on to indicate the current brightness. More LEDs are turned on at higher brightnesses. The LedTimeout command enables/disables an LED timeout. If LED timeout is enabled, the LED’s turn off five seconds after the last change in brightness. Note that the lowest LED and the blue power LED are always on when the power is on. + +The LEDLink LED can be used as a nightlight/powered-off indicator. The PoweredOffLed command enables/disables turning the LEDLink LED on when the power is off. + +Tapping (pressing and releasing quickly) the down or up buttons a given number of times and then holding the down or up button decreases or increases settings according to the table below. For example, tapping the down button once and then holding the up button sets all RGB lights in the device group to the next fixed color. Tapping the up button three times and then holding the down button decreases the high brightness preset. + + + + + + + + + + + + + + + + + + + + + + + +
Taps + Down Button + Up Button +
1 + Set fixed color1 + Publish MQTT event2 +
2 + Adjust minimum brightness + Adjust fade speed +
3 + Adjust low brightness preset + Adjust high brightness preset +
+ + +1. Setting the previous/next color only functions when remote device mode is enabled (see below) and only when the switch is in a device group with an RGB light. The color sequence as defined by the Light module is red, green, blue, orange, light green, light blue, amber, cyan, purple, yellow, pink, white using RGB channels, white using CT channels. + +2. The MQTT topic has the format %group-topic%/cmnd/Event, where %group-topic% is the group topic set by the GroupTopic command. The MQTT payload is SwitchTrigger#, where # is 1 if the down button is held or 2 if the up button is held. These triggers can be used in rules on remote devices (ON Event#SwitchTrigger1) or by automation software to trigger automations such as scene changes. For example, the Event topic SwitchTrigger1 payload could trigger the automation software to turn on the previous scene in a list and the SwitchTrigger2 payload could trigger the automation software to turn on the next scene in a list. + +Holding the power button, pressing the down/up buttons a given number of times and then releasing the power button toggles options according to the table below. Note that you must press a down or up button within 0.5 seconds to prevent the power button hold action from taking place. + + + + + + + + + + + + + + + + + + +
Presses + Down Button + Up Button +
1 + Toggle powered-off LED + Toggle brightness LED timeout +
2 + Toggle fading + +
+ + +Holding any button for over 10 seconds executes the WiFiConfig 2 command. + +Pressing and releasing a button publishes an MQTT TOGGLE command. Holding a button publishes an MQTT HOLD command followed by an MQTT OFF command when the button is released. + +When Device Groups are enabled, the PWM Dimmer minimum brightness, brightness presets, fade and speed settings are kept in sync across all switches in the group. The powered-off LED and LED timeout settings are specific to each switch. Changing them does not replicate the change to the other switches in the group. + + +### Commands + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Command + Parameters +
BriMin + 1..255 = set minimum brightness +

++ = increase minimum brightness +

+- = decrease minimum brightness +

BriPreset + <low>,<high> = set brightness low and high presets +

+1..255 = set brightness preset +

++ = increase brightness preset +

+- = decrease brightness preset +

Dimmer + 0..100 = set dimmer value from 0 to 100% +

++ = increase by 10 +

+- = decrease by 10 +

Fade + 0 = do not use fade (default) +

+1 = use fade +

LedTimeout + 0 = disable LED timeout +

+1 = enable LED timeout +

PoweredOffLed + 0 = disable powered-off LED +

+1 = disable powered-off LED +

Speed + 1..20 = set fade speed from fast 1 to very slow 20 +

++ = increase speed +

+- = decrease speed +

SetOption86 + Set brightness LED timeout +

+0 = disable timeout (default) +

+1 = enable timeout +

SetOption87 + Set powered-off LED (nightlight) +

+0 = disable powered-off LED (default) +

+1 = enable powered-off LED +

SetOption88 + Set remote device mode +

+0 = disable remote device mode(default) +

+1 = enable remote device mode +

+ + + +### Remote Device Mode + +Remote device mode allows PWM Dimmer switches to control remote devices. With remote device mode enabled, each button controls a different device. Note that dimmer switches with toggle-style down/up buttons have limited functionality as remote device mode switches because you can not push the down and up buttons simultaneously. + +To include remote device mode support in the build, define USE_PWM_DIMMER_REMOTE in your user_config_override. Remote device mode support requires device group support so USE_DEVICE_GROUPS is automatically defined if USE_PWM_DIMMER_REMOTE is defined. Remote device mode support adds 0.7K to the code size in addition to the code size required for device groups support. + +To enable remote device mode, set Option88 to 1. Each remote device must be running firmware with device group support and have remote device support enabled. The remote devices do not need to be built with PWM dimmer support nor do they need to be switches. + +If a remote device is a PWM dimmer, the device acts like a 3-way dimmer switch would and may or may not have a load connected to it. It’s also possible to use a PWM dimmer switch without a load to act as a wall switch to control the power, brightness and color of one or more smart lights with Tasmota with device group support loaded on them. + +With remote device mode enabled, button 1 is the power button for the local device while buttons 2 and 3 are the power buttons for remote devices. Group names for buttons 2 and 3 are set by the GroupTopic2 and GroupTopic3 commands respectively. Note that the button numbers are defined by the module template and can be in any physical order on the switch (button 1 can be defined as the top button, the middle button or the bottom button). + +Pressing and releasing a power button toggles the power on all devices in the group assigned to the button. When the power is on, holding the button alternately increases or decreases the brightness. When the power is off, holding the button turns the power on at a temporary brightness of the low level set by the BriPreset command (default =10). + +While holding a power button, the other two buttons act like the down and up buttons for the remote device. All the functions performed by the down and up buttons in non-remote device mode are available in remote device mode. While holding button 1, button 2 performs the functions of the down button and button 3 performs the functions of the up button. While holding button 2, button 1 performs the functions of the down button and button 3 performs the functions of the up button. While holding button 3, button 1 performs the functions of the down button and button 2 performs the functions of the up button. diff --git a/TEMPLATES.md b/TEMPLATES.md index 2cb5b4773..d922a4174 100644 --- a/TEMPLATES.md +++ b/TEMPLATES.md @@ -130,6 +130,7 @@ Kingart Touch {"NAME":"PS-16-DZ","GPIO":[255,148,255,149,255,255,0,0,255 Moes DS01 {"NAME":"MOES - DS01","GPIO":[255,255,255,255,255,255,0,0,255,108,255,107,255],"FLAG":0,"BASE":54} Moes QS-WiFi-D01 Dimmer 150W {"NAME":"WiFi-Dimmer","GPIO":[0,148,0,149,0,0,0,0,0,42,37,0,0],"FLAG":0,"BASE":18} PS-16-DZ {"NAME":"PS-16-DZ","GPIO":[255,148,255,149,255,255,0,0,255,52,255,255,255],"FLAG":0,"BASE":58} +PWM Dimmer {"NAME":"PWM Dimmer","GPIO":[19,18,0,59,158,58,0,0,57,37,56,122,29],"FLAG":0,"BASE":73} Zemismart KS-7011 {"NAME":"KS-7011 Dimmer","GPIO":[255,107,255,108,255,255,0,0,255,255,255,255,255],"FLAG":0,"BASE":54} ``` diff --git a/tasmota/i18n.h b/tasmota/i18n.h index 0821afb6c..1e1e6b909 100644 --- a/tasmota/i18n.h +++ b/tasmota/i18n.h @@ -535,6 +535,14 @@ // Commands xdrv_32_hotplug.ino #define D_CMND_HOTPLUG "HotPlug" +// Commands xdrv_34_pwm_dimmer.ino +#ifdef USE_PWM_DIMMER +#define D_CMND_BRI_MIN "BriMin" +#define D_CMND_BRI_PRESET "BriPreset" +#define D_CMND_LED_TIMEOUT "LedTimeout" +#define D_CMND_POWERED_OFF_LED "PoweredOffLed" +#endif + // Commands xsns_02_analog.ino #define D_CMND_ADCPARAM "AdcParam" diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 29cb700ab..0ae6d4663 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -415,7 +415,9 @@ //#define USE_EXS_DIMMER // Add support for ES-Store WiFi Dimmer (+1k5 code) // #define EXS_MCU_CMNDS // Add command to send MCU commands (+0k8 code) //#define USE_HOTPLUG // Add support for sensor HotPlug -//#define USE_DEVICE_GROUPS // Add support for device groups (+5k6 code) +#define USE_DEVICE_GROUPS // Add support for device groups (+3k5 code) +#define USE_PWM_DIMMER // Add support for MJ-SD01/acenx/NTONPOWER PWM dimmers (+4k5 code) +#define USE_PWM_DIMMER_REMOTE // Add support for remote switches to PWM Dimmer, also adds device groups support (+0k7 code, also includes device groups) // -- Optional light modules ---------------------- #define USE_WS2812 // WS2812 Led string using library NeoPixelBus (+5k code, +1k mem, 232 iram) - Disable by // diff --git a/tasmota/settings.h b/tasmota/settings.h index 20000aa9f..da8da2f29 100644 --- a/tasmota/settings.h +++ b/tasmota/settings.h @@ -469,9 +469,8 @@ struct SYSCFG { uint8_t bri_min; // F05 uint8_t bri_preset_low; // F06 uint8_t bri_preset_high; // F07 - uint8_t button_devices; // F08 - uint8_t free_f05[195]; // F09 + uint8_t free_f05[196]; // F08 uint32_t device_group_share_in; // FCC - Bitmask of device group items imported uint32_t device_group_share_out; // FD0 - Bitmask of device group items exported diff --git a/tasmota/support_tasmota.ino b/tasmota/support_tasmota.ino index b4d699cb6..ad9b38165 100644 --- a/tasmota/support_tasmota.ino +++ b/tasmota/support_tasmota.ino @@ -640,6 +640,12 @@ void MqttShowState(void) break; } #endif // USE_SONOFF_IFAN +#ifdef USE_PWM_DIMMER + if (PWM_DIMMER == my_module_type) { + ResponseAppend_P(PSTR(",\"" D_CMND_DIMMER "\":%d,\"" D_CMND_FADE "\":\"%s\",\"" D_CMND_SPEED "\":%d"), + Settings.light_dimmer, GetStateText(Settings.light_fade), Settings.light_speed); + } +#endif // USE_PWM_DIMMER #ifdef USE_LIGHT } #endif diff --git a/tasmota/tasmota_post.h b/tasmota/tasmota_post.h index 448fcc6b2..d8a8ea5d1 100644 --- a/tasmota/tasmota_post.h +++ b/tasmota/tasmota_post.h @@ -73,6 +73,16 @@ extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack #define MODULE SONOFF_BASIC // [Module] Select default model #endif +#ifdef USE_PWM_DIMMER_REMOTE +#ifdef USE_PWM_DIMMER +#ifndef USE_DEVICE_GROUPS +#define USE_DEVICE_GROUPS +#endif // USE_DEVICE_GROUPS +#else // USE_PWM_DIMMER +#undef USE_PWM_DIMMER_REMOTE +#endif // USE_PWM_DIMMER +#endif // USE_PWM_DIMMER_REMOTE + /*********************************************************************************************\ * [tasmota-sensors.bin] * Provide an image with useful supported sensors enabled @@ -250,6 +260,7 @@ extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack #undef USE_EMULATION_HUE // Disable Hue Bridge emulation for Alexa (+14k code, +2k mem common) #undef USE_EMULATION_WEMO // Disable Belkin WeMo emulation for Alexa (+6k code, +2k mem common) #undef USE_DEVICE_GROUPS // Disable support for device groups (+3k5 code) +#undef USE_PWM_DIMMER_REMOTE // Disbale support for remote switches to PWM Dimmer #undef DEBUG_THEO // Disable debug code #undef USE_DEBUG_DRIVER // Disable debug code #endif // FIRMWARE_KNX_NO_EMULATION @@ -288,6 +299,7 @@ extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack #undef USE_EXS_DIMMER // Disable support for EX-Store WiFi Dimmer #undef USE_HOTPLUG // Disable support for HotPlug #undef USE_DEVICE_GROUPS // Disable support for device groups (+3k5 code) +#undef USE_PWM_DIMMER_REMOTE // Disbale support for remote switches to PWM Dimmer #undef USE_ENERGY_SENSOR // Disable energy sensors (-14k code) #undef USE_PZEM004T // Disable PZEM004T energy sensor @@ -365,6 +377,7 @@ extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack #undef USE_EXS_DIMMER // Disable support for EX-Store WiFi Dimmer #undef USE_HOTPLUG // Disable support for HotPlug #undef USE_DEVICE_GROUPS // Disable support for device groups (+3k5 code) +#undef USE_PWM_DIMMER_REMOTE // Disbale support for remote switches to PWM Dimmer // -- Optional light modules ---------------------- //#undef USE_LIGHT // Also disable all Dimmer/Light support @@ -587,6 +600,7 @@ extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack #undef USE_EXS_DIMMER // Disable support for EX-Store WiFi Dimmer #undef USE_HOTPLUG // Disable support for HotPlug #undef USE_DEVICE_GROUPS // Disable support for device groups (+3k5 code) +#undef USE_PWM_DIMMER_REMOTE // Disbale support for remote switches to PWM Dimmer // -- Optional light modules ---------------------- #undef USE_LIGHT // Disable support for lights diff --git a/tasmota/tasmota_template.h b/tasmota/tasmota_template.h index c4ee7b19d..6f5aa0f8c 100644 --- a/tasmota/tasmota_template.h +++ b/tasmota/tasmota_template.h @@ -418,6 +418,7 @@ enum SupportedModules { SONOFF_L1, SONOFF_IFAN03, EXS_DIMMER, + PWM_DIMMER, MAXMODULE}; #define USER_MODULE 255 @@ -864,6 +865,9 @@ const uint8_t kModuleNiceList[] PROGMEM = { #endif #ifdef USE_EXS_DIMMER EXS_DIMMER, +#endif +#ifdef USE_PWM_DIMMER + PWM_DIMMER, #endif H801, // Light Devices MAGICHOME, @@ -2204,6 +2208,33 @@ const mytmplt kModules[MAXMODULE] PROGMEM = { GPIO_USER, // GPIO14 0, // GPIO15 0, 0 + }, + { "PWM Dimmer", // PWM_DIMMER - Support for Martin Jerry/acenx/Tessan/NTONPOWER SD0x PWM + // dimmer switches. The brightness of the load for these dimmers is + // controlled by a PWM GPIO pin. There are typically power, up & down + // buttons and 4 LED's. Examples are: + // https://www.amazon.com/dp/B07FXYSVR1 + // https://www.amazon.com/dp/B07V26Q3VD + // https://www.amazon.com/dp/B07K67D43J + // https://www.amazon.com/dp/B07TTGFWFM + GPIO_KEY3, // GPIO00 Up button + GPIO_KEY2, // GPIO01 Down button + 0, // GPIO02 + GPIO_LED4_INV, // GPIO03 Level 5 LED + GPIO_LEDLNK_INV, // GPIO04 LED Link + GPIO_LED3_INV, // GPIO05 Level 4 LED + // GPIO06 (SD_CLK Flash) + // GPIO07 (SD_DATA0 Flash QIO/DIO/DOUT) + // GPIO08 (SD_DATA1 Flash QIO/DIO/DOUT) + 0, // GPIO09 (SD_DATA2 Flash QIO or ESP8285) + 0, // GPIO10 (SD_DATA3 Flash QIO or ESP8285) + // GPIO11 (SD_CMD Flash) + GPIO_LED2_INV, // GPIO12 Level 3 LED + GPIO_PWM1, // GPIO13 Dimmer PWM + GPIO_LED1_INV, // GPIO12 Level 2 LED + GPIO_KEY1_INV, // GPIO15 Power button + GPIO_REL1_INV, // GPIO16 Power relay/Level 1 LED + 0 } }; diff --git a/tasmota/xdrv_01_webserver.ino b/tasmota/xdrv_01_webserver.ino index 3b408cf66..7e7292a8c 100644 --- a/tasmota/xdrv_01_webserver.ino +++ b/tasmota/xdrv_01_webserver.ino @@ -1123,6 +1123,17 @@ void HandleRoot(void) } // Settings.flag3.pwm_multi_channels } #endif // USE_LIGHT +#ifdef USE_PWM_DIMMER + if (PWM_DIMMER == my_module_type) { + WSContentSend_P(HTTP_MSG_SLIDER_GRADIENT, // Brightness - Black to White + "c", // c - Unique HTML id + "#000", "#fff", // Black to White + 4, // sl4 - Unique range HTML id - Used as source for Saturation begin color + Settings.flag3.slider_dimmer_stay_on, 100, // Range 0/1 to 100% + Settings.light_dimmer, + 'd', 0); // d0 - Value id is related to lc("d0", value) and WebGetArg("d0", tmp, sizeof(tmp)); + } +#endif // USE_PWM_DIMMER #ifdef USE_SHUTTER if (Settings.flag3.shutter_mode) { // SetOption80 - Enable shutter support for (uint32_t i = 0; i < shutters_present; i++) { diff --git a/tasmota/xdrv_04_light.ino b/tasmota/xdrv_04_light.ino index baaf2b7a7..d54598077 100644 --- a/tasmota/xdrv_04_light.ino +++ b/tasmota/xdrv_04_light.ino @@ -2588,6 +2588,8 @@ void CmndRgbwwTable(void) ResponseCmndChar(scolor); } +#endif // USE_LIGHT +#if defined(USE_LIGHT) || defined(USE_PWM_DIMMER) void CmndFade(void) { // Fade - Show current Fade state @@ -2606,7 +2608,9 @@ void CmndFade(void) #ifdef USE_DEVICE_GROUPS if (XdrvMailbox.payload >= 0 && XdrvMailbox.payload <= 2) SendLocalDeviceGroupMessage(DGR_MSGTYP_UPDATE, DGR_ITEM_LIGHT_FADE, Settings.light_fade); #endif // USE_DEVICE_GROUPS +#ifdef USE_LIGHT if (!Settings.light_fade) { Light.fade_running = false; } +#endif // USE_LIGHT ResponseCmndStateText(Settings.light_fade); } @@ -2632,6 +2636,8 @@ void CmndSpeed(void) } ResponseCmndNumber(Settings.light_speed); } +#endif // #if defined(USE_LIGHT) || defined(USE_PWM_DIMMER) +#ifdef USE_LIGHT void CmndWakeupDuration(void) { @@ -2664,7 +2670,10 @@ bool Xdrv04(uint8_t function) bool result = false; if (FUNC_MODULE_INIT == function) { - return LightModuleInit(); +#ifdef USE_PWM_DIMMER + if (PWM_DIMMER != my_module_type) +#endif // USE_PWM_DIMMER + return LightModuleInit(); } else if (light_type) { switch (function) { diff --git a/tasmota/xdrv_12_home_assistant.ino b/tasmota/xdrv_12_home_assistant.ino index 7396694af..fde47a623 100644 --- a/tasmota/xdrv_12_home_assistant.ino +++ b/tasmota/xdrv_12_home_assistant.ino @@ -200,8 +200,8 @@ void HAssAnnounceRelayLight(void) TryResponseAppend_P(HASS_DISCOVER_RELAY, command_topic, value_template, SettingsText(SET_STATE_TXT1), SettingsText(SET_STATE_TXT2)); TryResponseAppend_P(HASS_DISCOVER_DEVICE_INFO_SHORT, unique_id, ESP.getChipId(), WiFi.macAddress().c_str()); -#ifdef USE_LIGHT - if (is_light) +#if defined(USE_LIGHT) || defined(USE_PWM_DIMMER) + if (is_light || PWM_DIMMER == my_module_type) { char *brightness_command_topic = stemp1; @@ -209,6 +209,7 @@ void HAssAnnounceRelayLight(void) strncpy_P(stemp3, Settings.flag.not_power_linked ? PSTR("last") : PSTR("brightness"), sizeof(stemp3)); // SetOption20 - Control power in relation to Dimmer/Color/Ct changes TryResponseAppend_P(HASS_DISCOVER_LIGHT_DIMMER, brightness_command_topic, state_topic, stemp3); +#ifdef USE_LIGHT if (Light.subtype >= LST_RGB) { char *rgb_command_topic = stemp1; @@ -234,8 +235,9 @@ void HAssAnnounceRelayLight(void) GetTopic_P(color_temp_command_topic, CMND, mqtt_topic, D_CMND_COLORTEMPERATURE); TryResponseAppend_P(HASS_DISCOVER_LIGHT_CT, color_temp_command_topic, state_topic); } +#endif // USE_LIGHT } -#endif // USE_LIGHT +#endif // defined(USE_LIGHT) || defined(USE_PWM_DIMMER) TryResponseAppend_P(PSTR("}")); } MqttPublish(stopic, true); diff --git a/tasmota/xdrv_35_pwm_dimmer.ino b/tasmota/xdrv_35_pwm_dimmer.ino new file mode 100644 index 000000000..94bc4ea38 --- /dev/null +++ b/tasmota/xdrv_35_pwm_dimmer.ino @@ -0,0 +1,1137 @@ +/* + xdrv_35_pwm_dimmer.ino - PWM Dimmer Switch support for Tasmota + + Copyright (C) 2020 Paul C Diem + + 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_PWM_DIMMER + +/*********************************************************************************************\ +* Support for Martin Jerry/acenx/Tessan/NTONPOWER SD0x PWM dimmer switches. The brightness of +* the load for these dimmers is controlled by a PWM GPIO pin. Examples are: +* +* https://www.amazon.com/dp/B07FXYSVR1 +* https://www.amazon.com/dp/B07V26Q3VD +* https://www.amazon.com/dp/B07K67D43J +* https://www.amazon.com/dp/B07TTGFWFM +* +\*********************************************************************************************/ + +#define XDRV_35 35 + +#ifndef MAX_FIXED_COLOR +#define MAX_FIXED_COLOR 12 +#endif // MAX_FIXED_COLOR + +const char kPWMDimmerCommands[] PROGMEM = "|" // No prefix + D_CMND_BRI_MIN "|" D_CMND_BRI_PRESET "|" D_CMND_DIMMER "|" D_CMND_FADE "|" D_CMND_LED_TIMEOUT "|" + D_CMND_POWERED_OFF_LED "|" D_CMND_SPEED; + +void (* const PWMDimmerCommand[])(void) PROGMEM = { + &CmndBriMin, &CmndBriPreset, &PWMDmmerCmndDimmer, &CmndFade, &CmndLedTimeout, + &CmndPoweredOffLed, &CmndSpeed }; + +#ifdef USE_PWM_DIMMER_REMOTE +struct remote_pwm_dimmer { + power_t power; + uint8_t light_speed; + uint8_t bri_power_on; + uint8_t bri_min; + uint8_t bri_preset_low; + uint8_t bri_preset_high; + uint8_t fixed_color_index; + uint8_t bri; + bool power_button_increases_bri; + bool light_fade; +}; +#endif // USE_PWM_DIMMER_REMOTE + +uint32_t led_timeout_time = 0; +uint32_t turn_off_brightness_leds_time = 0; +uint32_t button_hold_time[3]; +uint8_t current_bri; +uint8_t target_bri; +uint8_t restore_powered_off_led = 0; +uint8_t power_button_index = 0; +uint8_t down_button_index = 1; +uint8_t up_button_index = 2; +uint8_t fixed_color_index; +uint8_t button_press_count[3] = { 0, 0, 0 }; +bool relay_is_on = false; +bool ignore_power_button_hold; +bool ignore_power_button_release; +bool button_was_held = false; +bool power_button_increases_bri = true; +bool invert_power_button_bri_direction = false; +bool button_hold_sent[3]; +bool button_pressed[3] = { false, false, false }; +#ifdef USE_PWM_DIMMER_REMOTE +struct remote_pwm_dimmer * remote_pwm_dimmers; +struct remote_pwm_dimmer * active_remote_pwm_dimmer; +uint8_t buttons_pressed = 0; +bool active_device_is_local; +#endif // USE_PWM_DIMMER_REMOTE + +void PWMDimmerCheckBri(uint8_t * value_ptr) +{ + if (*value_ptr < Settings.bri_min) *value_ptr = Settings.bri_min; +} + +void PWMModuleInit() +{ + Settings.seriallog_level = 0; + Settings.flag.mqtt_serial = 0; // Disable serial logging + Settings.ledstate = 0; // Disable LED usage + Settings.flag.pwm_control = 0; // Use basic PWM control instead of Light + + if (Settings.last_module != Settings.module) { + Settings.bri_min = 25; + Settings.bri_power_on = 50; + Settings.bri_preset_low = 25; + Settings.bri_preset_high = 255; + Settings.last_module = Settings.module; + } + else { + if (Settings.bri_min < 1) Settings.bri_min = 25; + PWMDimmerCheckBri(&Settings.bri_power_on); + PWMDimmerCheckBri(&Settings.bri_preset_low); + PWMDimmerCheckBri(&Settings.bri_preset_high); + } + + if (Settings.light_speed < 1) Settings.light_speed = 1; + + target_bri = ((Settings.power & 1) ? changeUIntScale(Settings.light_dimmer, 0, 100, 0, 255) : 0); + PWMDimmerCheckBri(&target_bri); + current_bri = target_bri; + + if (pin[GPIO_PWM1] < 99) { + uint32_t pwm_value = changeUIntScale(current_bri, 0, 255, 0, Settings.pwm_range); + analogWrite(pin[GPIO_PWM1], bitRead(pwm_inverted, 0) ? Settings.pwm_range - pwm_value : pwm_value); + } + + relay_is_on = (current_bri > 0); + if (pin[GPIO_REL1] < 99) DigitalWrite(GPIO_REL1, bitRead(rel_inverted, 0) ? !relay_is_on : relay_is_on); + + PWMDimmerSetPoweredOffLed(); + PWMDimmerSetBrightnessLeds(0); +} + +void PWMDimmerInit(void) +{ +#ifdef USE_PWM_DIMMER_REMOTE + if (Settings.flag4.remote_device_mode) { + if (device_group_count > 1) { + if ((remote_pwm_dimmers = (struct remote_pwm_dimmer *) calloc(device_group_count - 1, sizeof(struct remote_pwm_dimmer))) == nullptr) { + AddLog_P2(LOG_LEVEL_ERROR, PSTR("PWMDimmer: error allocating PWM dimmer array")); + Settings.flag4.remote_device_mode = false; + } + } + } + + active_device_is_local = true; +#endif // USE_PWM_DIMMER_REMOTE +} + +// operation: 0 = normal, -1 = all off, 1 = all on +void PWMDimmerSetBrightnessLeds(int32_t operation) +{ + if (leds_present) { + uint32_t step = (!operation ? (255 - Settings.bri_min) / (leds_present + 1) : operation < 0 ? 255 : 0); + uint32_t level = step; + SetLedPowerIdx(0, current_bri >= level); + if (leds_present > 1) { + level += step; + SetLedPowerIdx(1, current_bri >= level); + if (leds_present > 2) { + level += step; + SetLedPowerIdx(2, current_bri >= level); + if (leds_present > 3) { + level += step; + SetLedPowerIdx(3, current_bri >= level); + } + } + } + + // If enabled, set the LED timeout. + if (!operation) led_timeout_time = (current_bri && Settings.flag4.led_timeout ? millis() + 5000 : 0); + } +} + +void PWMDimmerSetPoweredOffLed(void) +{ + // Set the powered-off LED state. + if (pin[GPIO_LEDLNK] < 99) { + bool power_off_led_on = !power && Settings.flag4.powered_off_led; + if (ledlnk_inverted) power_off_led_on ^= 1; + digitalWrite(pin[GPIO_LEDLNK], power_off_led_on); + } +} + +void PWMDimmerAnimate(bool no_fade) +{ + // We're only here if something changed. If this is no longer the case, uncomment the following + // line. + //if (current_bri == target_bri) return; + + // Advance the current brightness towards the target. + if (!no_fade && Settings.light_fade) { + if (current_bri < Settings.bri_min) { + current_bri = Settings.bri_min; + } + else { + uint8_t offset = current_bri / Settings.light_speed / 5 + 1; + uint8_t max_offset = abs(target_bri - current_bri); + if (offset > max_offset) offset = max_offset; + if (current_bri < target_bri) + current_bri += offset; + else + current_bri -= offset; + } + } + else { + current_bri = target_bri; + } + + // If the current brightness is now < bri_min, set the current and target brightness to 0. + if (current_bri < Settings.bri_min) current_bri = target_bri = 0; + + // Sset the new PWM value. + if (pin[GPIO_PWM1] < 99) { + uint32_t pwm_value = changeUIntScale(current_bri, 0, 255, 0, Settings.pwm_range); + analogWrite(pin[GPIO_PWM1], bitRead(pwm_inverted, 0) ? Settings.pwm_range - pwm_value : pwm_value); + } + + // Handle a power state change. + if (relay_is_on != (current_bri > 0)) { + bool power_is_on = ((power & 1) != 0); + if (power_is_on == relay_is_on) { + ExecuteCommandPower(1, (relay_is_on ? POWER_OFF : POWER_ON), SRC_SWITCH); + } + + relay_is_on = !relay_is_on; + if (pin[GPIO_REL1] < 99) DigitalWrite(GPIO_REL1, bitRead(rel_inverted, 0) ? !relay_is_on : relay_is_on); + + // Set the powered-off LED. + PWMDimmerSetPoweredOffLed(); + } + + // Set the brightness LED's. + PWMDimmerSetBrightnessLeds(0); +} + +void PWMDimmerSetBri(uint8_t bri) +{ + if (bri == target_bri) return; + target_bri = bri; + if (target_bri && target_bri < Settings.bri_min) target_bri = Settings.bri_min; + Settings.bri_power_on = target_bri; + Settings.light_dimmer = changeUIntScale(target_bri, 0, 255, 0, 100); + if (Settings.flag3.hass_tele_on_power) { // SetOption59 - Send tele/%topic%/STATE in addition to stat/%topic%/RESULT + MqttPublishTeleState(); + } + PWMDimmerAnimate(false); +} + +#ifdef USE_DEVICE_GROUPS +void PWMDimmerHandleDeviceGroupRequest() +{ + static bool send_state = false; + uint8_t value = XdrvMailbox.payload; + switch (XdrvMailbox.command_code) { + case DGR_ITEM_BRI_MIN: + Settings.bri_min = 1; + case DGR_ITEM_BRI_PRESET_LOW: + case DGR_ITEM_BRI_PRESET_HIGH: + case DGR_ITEM_BRI_POWER_ON: + PWMDimmerCheckBri(&value); + break; + case DGR_ITEM_LIGHT_BRI: + if (value) PWMDimmerCheckBri(&value); + break; + } + + switch (XdrvMailbox.command_code) { + case DGR_ITEM_EOL: + if (send_state && !(XdrvMailbox.index & DGR_FLAG_MORE_TO_COME)) { + 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 (target_bri != value) { + PWMDimmerSetBri(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_BRI_MIN: + if (Settings.bri_min != value) { + Settings.bri_min = value; + send_state = true; + } + break; + case DGR_ITEM_BRI_PRESET_LOW: + if (Settings.bri_preset_low != value) { + Settings.bri_preset_low = value; + send_state = true; + } + break; + case DGR_ITEM_BRI_PRESET_HIGH: + if (Settings.bri_preset_high != value) { + Settings.bri_preset_high = value; + send_state = true; + } + break; + case DGR_ITEM_BRI_POWER_ON: + if (Settings.bri_power_on != value) { + Settings.bri_power_on = value; + send_state = true; + } + break; + case DGR_ITEM_STATUS: + SendLocalDeviceGroupMessage(DGR_MSGTYP_UPDATE, DGR_ITEM_LIGHT_FADE, Settings.light_fade, + DGR_ITEM_LIGHT_SPEED, Settings.light_speed, DGR_ITEM_LIGHT_BRI, target_bri, + DGR_ITEM_BRI_MIN, Settings.bri_min, DGR_ITEM_BRI_PRESET_LOW, Settings.bri_preset_low, + DGR_ITEM_BRI_PRESET_HIGH, Settings.bri_preset_high, DGR_ITEM_BRI_POWER_ON, Settings.bri_power_on); + break; + } +} +#endif // USE_DEVICE_GROUPS + +void PWMDimmerHandleButton() +{ + /* + * Power Button Up/Down Buttons State Remote Mode Action + * -------------------- ---------------------- ----- ------------ ---------------------------- + * Press & release Released Any Any Toggle power + * Hold for hold time Released On Any Brighter/dimmer while held + * then reverse direction + * Hold for hold time Released Off Any Power on at bri preset low + * Hold while Press & release up Any Any Toggle/change options + * Hold while Press & release down Any Any Toggle/change options + * Hold while Press up On Yes Brighter + * Hold while Press down On Yes Dimmer + * Released Press up On No Brigther + * Released Press down On No Dimmer + * Released Press & release up Off No Power on at bri preset low + * Released Press & release down Off No Power on at bri preset high + * + * Holding any button for over 10 seconds executes the WiFiConfig 2 command. + * + * In remote mode, whichever button is pressed first becomes the power button and any buttons + * pressed while it is held affect the device associated with it. The up and down buttons change + * depeneding on which button is the current power button: + * + * Power Down Up + * ----- ---- -- + * 1 2 3 + * 2 1 3 + * 3 1 2 + */ + uint32_t button_index = XdrvMailbox.index; + uint32_t now = millis(); + bool toggle_power = false; + uint8_t dgr_item = 0; + uint8_t dgr_value; + + // If the button is pressed, ... + if (!XdrvMailbox.payload) { + int8_t bri_direction = 0; + + // If the button was just pressed, reset the press count if it was released for longer than 1 + // second, flag the button as pressed, clear the hold sent flag and increment the buttons + // pressed count. + if (!button_pressed[button_index]) { + if (now > button_hold_time[button_index] && now - button_hold_time[button_index] > 1000) button_press_count[button_index] = 0; + button_pressed[button_index] = true; + button_hold_sent[button_index] = false; +#ifdef USE_PWM_DIMMER_REMOTE + buttons_pressed++; + + // If there are no other buttons pressed right now and remote mode is enabled, make the device + // associated with this button the device we're going to control. + if (buttons_pressed == 1 && Settings.flag4.remote_device_mode) { + power_button_index = button_index; + up_button_index = (button_index == 2 ? 1 : 2); + down_button_index = (button_index ? 0 : 1); + active_device_is_local = device_groups[power_button_index].local; + if (!active_device_is_local) active_remote_pwm_dimmer = &remote_pwm_dimmers[power_button_index - 1]; + } + if (button_index == power_button_index) { +#else // USE_PWM_DIMMER_REMOTE + // If this is about the power button, initialize some variables. + if (!button_index) { +#endif // USE_PWM_DIMMER_REMOTE + button_hold_time[button_index] = now + 500; + ignore_power_button_hold = false; + ignore_power_button_release = false; + return; + } + + // If this is not about the power button, load the new hold time. Note that the hold time for + // the power button is longer than the hold time for the other buttons. + button_hold_time[button_index] = now + 250; + } + + // If the button is being held, send a button hold. + else if (button_hold_time[button_index] < now) { + if (!button_hold_sent[button_index]) { + button_hold_sent[button_index] = true; + SendKey(KEY_BUTTON, button_index + 1, POWER_HOLD); + } + + // If the button has been held for over 10 seconds, execute the WiFiConfig 2 command. + if (now - button_hold_time[button_index] > 10000) { + char scmnd[20]; + snprintf_P(scmnd, sizeof(scmnd), PSTR(D_CMND_WIFICONFIG " 2")); + ExecuteCommand(scmnd, SRC_BUTTON); + return; + } + } + + // If this is about the power button, ... +#ifdef USE_PWM_DIMMER_REMOTE + if (button_index == power_button_index) { +#else // USE_PWM_DIMMER_REMOTE + if (!button_index) { +#endif // USE_PWM_DIMMER_REMOTE + + // If the power button has been held with no other buttons pressed, ... + if (!ignore_power_button_hold && button_hold_time[button_index] < now) { + ignore_power_button_release = true; + + // If the device power is on, adjust the brightness. Set the direction based on the current + // direction for the device and then invert the direction when the power button is released. + // The new brightness will be calculated below. +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local ? active_remote_pwm_dimmer->power : power) { + bri_direction = (!active_device_is_local ? (active_remote_pwm_dimmer->power_button_increases_bri ? 1 : -1) : (power_button_increases_bri ? 1 : -1)); +#else // USE_PWM_DIMMER_REMOTE + if (power) { + bri_direction = (power_button_increases_bri ? 1 : -1); +#endif // USE_PWM_DIMMER_REMOTE + invert_power_button_bri_direction = true; + } + + // If the power is not on, turn it on using an initial brightness of bri_preset_low and set + // the power button hold dimmer direction to true so holding the power switch increases the + // brightness. + else { +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) { + active_remote_pwm_dimmer->bri = active_remote_pwm_dimmer->bri_preset_low; + active_remote_pwm_dimmer->power_button_increases_bri = true; + } + else { +#endif // USE_PWM_DIMMER_REMOTE + target_bri = Settings.bri_preset_low; + power_button_increases_bri = true; +#ifdef USE_PWM_DIMMER_REMOTE + } +#endif // USE_PWM_DIMMER_REMOTE + toggle_power = true; + + // Reset the power button hold time to delay before we start increasing the brightness. + button_hold_time[button_index] = now + 500; + } + } + } + + // If this is about the down or up buttons, ... + else { + + // If the power button is also pressed, set flags to ignore the power button being held and + // the next power button release. + if (button_pressed[power_button_index]) { + ignore_power_button_release = ignore_power_button_hold = true; + } + + // If the active device is local or the power button if also pressed, then if the down/up + // button has been held, handle the action based on the number of times the down or up button + // was pressed and released before holding it. +#ifdef USE_PWM_DIMMER_REMOTE + if (active_device_is_local == !button_pressed[power_button_index]) { +#endif // USE_PWM_DIMMER_REMOTE + if (button_hold_time[button_index] < now) { + + uint8_t uint8_value; + uint8_t min_uint8_value; + bool down_button_is_held = (button_index == down_button_index); + bool down_button_was_tapped = (button_press_count[down_button_index] > 0); + uint8_t tap_count = (down_button_was_tapped ? button_press_count[down_button_index] : button_press_count[up_button_index]); + switch (tap_count) { + case 0: + // If the power is on, adjust the brightness. Set the direction based on which button is + // pressed. The new brightness will be calculated below. +#ifdef USE_PWM_DIMMER_REMOTE + if ((!active_device_is_local ? active_remote_pwm_dimmer->power : power)) { +#else // USE_PWM_DIMMER_REMOTE + if (power) { +#endif // USE_PWM_DIMMER_REMOTE + bri_direction = (down_button_is_held ? -1 : 1); + } + + // If the power is not on, turn it on using a temporary brightness of bri_preset_low if + // the down button is pressed or bri_preset_low if the up button is pressed. + else { +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) + active_remote_pwm_dimmer->bri = (down_button_is_held ? active_remote_pwm_dimmer->bri_preset_low : active_remote_pwm_dimmer->bri_preset_high); + else +#endif // USE_PWM_DIMMER_REMOTE + target_bri = (down_button_is_held ? Settings.bri_preset_low : Settings.bri_preset_high); + toggle_power = true; + button_hold_time[button_index] = now + 1000; + + // If the power button is also pressed, set the power button hold dimmer direction + // so holding the power switch adjusts the brightness away from the brightness we + // just set. +#ifdef USE_PWM_DIMMER_REMOTE + if (button_pressed[power_button_index]) active_remote_pwm_dimmer->power_button_increases_bri = down_button_is_held; +#endif // USE_PWM_DIMMER_REMOTE + } + break; + + case 1: + if (down_button_was_tapped) { + // Select the previous/next color. +#ifdef USE_DEVICE_GROUPS +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) + uint8_value = active_remote_pwm_dimmer->fixed_color_index; + else +#endif // USE_PWM_DIMMER_REMOTE + uint8_value = fixed_color_index; + if (down_button_is_held) { + if (uint8_value) + uint8_value--; + else + uint8_value = MAX_FIXED_COLOR; + } + else { + if (uint8_value < MAX_FIXED_COLOR) + uint8_value++; + else + uint8_value = 0; + } +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) + active_remote_pwm_dimmer->fixed_color_index = uint8_value; + else +#endif // USE_PWM_DIMMER_REMOTE + fixed_color_index = uint8_value; + dgr_item = DGR_ITEM_LIGHT_FIXED_COLOR; + dgr_value = uint8_value; +#endif // USE_DEVICE_GROUPS + ; + } + else { + // Publish MQTT Event SwitchTrigger#. + char topic[TOPSZ]; +#ifdef USE_PWM_DIMMER_REMOTE + snprintf_P(topic, sizeof(topic), PSTR("%s/cmnd/Event"), device_groups[power_button_index].group_name); +#else // USE_PWM_DIMMER_REMOTE + snprintf_P(topic, sizeof(topic), PSTR("%s/cmnd/Event"), SettingsText(SET_MQTT_GRP_TOPIC)); +#endif // USE_PWM_DIMMER_REMOTE + sprintf_P(mqtt_data, PSTR("SwitchTrigger%u"), (down_button_is_held ? 1 : 2)); + MqttPublish(topic); + } + break; + + case 2: + if (down_button_was_tapped) { + // Decrease/increase the minimum brightness. +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) + uint8_value = active_remote_pwm_dimmer->bri_min; + else +#endif // USE_PWM_DIMMER_REMOTE + uint8_value = Settings.bri_min; + if (down_button_is_held) { + if (uint8_value > 4) uint8_value--; + } + else if (uint8_value < 255) { + uint8_value++; + } +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) { + active_remote_pwm_dimmer->bri_min = uint8_value; + if (active_remote_pwm_dimmer->bri_power_on < uint8_value) active_remote_pwm_dimmer->bri_power_on = uint8_value; + } + else { +#endif // USE_PWM_DIMMER_REMOTE + Settings.bri_min = uint8_value; + if (Settings.bri_power_on < uint8_value) Settings.bri_power_on = uint8_value; +#ifdef USE_PWM_DIMMER_REMOTE + } +#endif // USE_PWM_DIMMER_REMOTE + dgr_item = DGR_ITEM_BRI_MIN; + dgr_value = uint8_value; + } + else { + // Decrease/increase the fade speed. +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) + uint8_value = active_remote_pwm_dimmer->light_speed; + else +#endif // USE_PWM_DIMMER_REMOTE + uint8_value = Settings.light_speed; + if (down_button_is_held) { + if (uint8_value > 1) uint8_value--; + } + else if (uint8_value < 40) { + uint8_value++; + } +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) + active_remote_pwm_dimmer->light_speed = uint8_value; + else +#endif // USE_PWM_DIMMER_REMOTE + Settings.light_speed = uint8_value; + dgr_item = DGR_ITEM_LIGHT_SPEED; + dgr_value = uint8_value; + } + break; + + case 3: + if (down_button_was_tapped) { + // Decrease/increase the low brightness preset. +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) { + uint8_value = active_remote_pwm_dimmer->bri_preset_low; + min_uint8_value =active_remote_pwm_dimmer->bri_min; + } + else { +#endif // USE_PWM_DIMMER_REMOTE + uint8_value = Settings.bri_preset_low; + min_uint8_value = Settings.bri_min; +#ifdef USE_PWM_DIMMER_REMOTE + } +#endif // USE_PWM_DIMMER_REMOTE + if (down_button_is_held) { + if (uint8_value > min_uint8_value) uint8_value--; + } + else if (uint8_value < 255) { + uint8_value++; + } +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) + active_remote_pwm_dimmer->bri_preset_low = uint8_value; + else +#endif // USE_PWM_DIMMER_REMOTE + Settings.bri_preset_low = uint8_value; + dgr_item = DGR_ITEM_BRI_PRESET_LOW; + dgr_value = uint8_value; + } + else { + // Decrease/increase the high brightness preset. +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) + uint8_value = active_remote_pwm_dimmer->bri_preset_high; + else +#endif // USE_PWM_DIMMER_REMOTE + uint8_value = Settings.bri_preset_high; + if (down_button_is_held) { + if (uint8_value > 4) uint8_value--; + } + else if (uint8_value < 255) { + uint8_value++; + } +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) + active_remote_pwm_dimmer->bri_preset_high = uint8_value; + else +#endif // USE_PWM_DIMMER_REMOTE + Settings.bri_preset_high = uint8_value; + dgr_item = DGR_ITEM_BRI_PRESET_HIGH; + dgr_value = uint8_value; + } + break; + } + + button_was_held = true; + + // If the button was tapped before it was held, reset the button hold time to one second and + // turn all the brightness LEDs on for 250ms. + if (tap_count > 0) { + button_hold_time[button_index] = now + 1000; + turn_off_brightness_leds_time = now + 250; + PWMDimmerSetBrightnessLeds(1); + } + } + else if (turn_off_brightness_leds_time && turn_off_brightness_leds_time < millis()) { + turn_off_brightness_leds_time = 0; + PWMDimmerSetBrightnessLeds(-1); + } +#ifdef USE_PWM_DIMMER_REMOTE + } +#endif // USE_PWM_DIMMER_REMOTE + } + + // If we need to adjust the brightness, do it. The brightness is adjusted faster the longer the + // button is held. + if (bri_direction) { + int32_t bri; +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) + bri = active_remote_pwm_dimmer->bri; + else +#endif // USE_PWM_DIMMER_REMOTE + bri = target_bri; + int32_t new_bri; + int32_t offset = bri / 12 + 1; + if (bri_direction > 0) { + new_bri = bri + offset; + if (new_bri > 255) new_bri = 255; + } + else { + new_bri = bri - offset; + if (new_bri < Settings.bri_min) new_bri = Settings.bri_min; + } + if (new_bri != bri) { +#ifdef USE_DEVICE_GROUPS + SendDeviceGroupMessage(power_button_index, DGR_MSGTYP_UPDATE_MORE_TO_COME, DGR_ITEM_LIGHT_BRI, new_bri); +#endif // USE_DEVICE_GROUPS +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) + active_remote_pwm_dimmer->bri_power_on = active_remote_pwm_dimmer->bri = new_bri; + else { +#endif // USE_PWM_DIMMER_REMOTE + Settings.bri_power_on = target_bri = new_bri; + Settings.light_dimmer = changeUIntScale(target_bri, 0, 255, 0, 100); + PWMDimmerAnimate(true); +#ifdef USE_PWM_DIMMER_REMOTE + } +#endif // USE_PWM_DIMMER_REMOTE + } + } + } + + // If the button was just released, ... + else if (button_pressed[button_index]) { + + // If the button was held, send a button off; otherwise, send a button toggle. + SendKey(KEY_BUTTON, button_index + 1, (button_hold_sent[button_index] ? POWER_OFF : POWER_TOGGLE)); + + // If this is about the power button, ... +#ifdef USE_PWM_DIMMER_REMOTE + if (button_index == power_button_index) { +#else // USE_PWM_DIMMER_REMOTE + if (!button_index) { +#endif // USE_PWM_DIMMER_REMOTE + + // If we're ignoring the next power button released, ... + if (ignore_power_button_release) { + ignore_power_button_release = false; + + // If the power button was held with no other buttons pressed, we changed the brightness so + // invert the bri direction for the next time and send a final update. + if (invert_power_button_bri_direction) { + invert_power_button_bri_direction = false; + dgr_item = DGR_ITEM_LIGHT_BRI; +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) { + active_remote_pwm_dimmer->power_button_increases_bri ^= 1; + dgr_value = active_remote_pwm_dimmer->bri; + } + else { +#endif // USE_PWM_DIMMER_REMOTE + power_button_increases_bri ^= 1; + dgr_value = target_bri; +#ifdef USE_PWM_DIMMER_REMOTE + } +#endif // USE_PWM_DIMMER_REMOTE + } + + // The up and/or down buttons were pressed while the power button was pressed. Handle the + // options based on the number of times the buttons were pressed. + else { + switch (button_press_count[down_button_index]) { + case 1: + // Toggle the powered-off LED option. +#ifdef USE_PWM_DIMMER_REMOTE + if (active_device_is_local) { +#endif // USE_PWM_DIMMER_REMOTE + Settings.flag4.powered_off_led ^= 1; + PWMDimmerSetPoweredOffLed(); +#ifdef USE_PWM_DIMMER_REMOTE + } +#endif // USE_PWM_DIMMER_REMOTE + break; + case 2: + // Toggle fading. + dgr_item = DGR_ITEM_LIGHT_FADE; +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) { + active_remote_pwm_dimmer->light_fade ^= 1; + dgr_value = active_remote_pwm_dimmer->light_fade; + } + else { +#endif // USE_PWM_DIMMER_REMOTE + Settings.light_fade ^= 1; + dgr_value = Settings.light_fade; +#ifdef USE_PWM_DIMMER_REMOTE + } +#endif // USE_PWM_DIMMER_REMOTE + break; + } + button_press_count[down_button_index] = 0; + + switch (button_press_count[up_button_index]) { + case 1: + // Toggle the LED timeout. +#ifdef USE_PWM_DIMMER_REMOTE + if (active_device_is_local) { +#endif // USE_PWM_DIMMER_REMOTE + Settings.flag4.led_timeout ^= 1; + if (relay_is_on) PWMDimmerSetBrightnessLeds(Settings.flag4.led_timeout ? -1 : 0); +#ifdef USE_PWM_DIMMER_REMOTE + } +#endif // USE_PWM_DIMMER_REMOTE + break; + } + button_press_count[up_button_index] = 0; + } + } + + // If we're not ignoring the power button until it's released, toggle the power. + else { +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) + active_remote_pwm_dimmer->bri = active_remote_pwm_dimmer->bri_power_on; + else +#endif // USE_PWM_DIMMER_REMOTE + target_bri = Settings.bri_power_on; + toggle_power = true; + } + } + + // If this is about the up or down buttons, ... + else { + + // If the button was held, ... + if (button_was_held) { + button_was_held = false; + + // If the button was tapped before it was held, we used the brightness LEDs to inidcate the + // operation so reset the brightness LEDs. + if (button_press_count[down_button_index] > 0 || button_press_count[up_button_index]) { + turn_off_brightness_leds_time = 0; + PWMDimmerSetBrightnessLeds(0); + } + + // If the button was not tapped before it was held, we changed the brightness and sent + // updates with the more-to-come message type. Send a final update. + else { + dgr_item = DGR_ITEM_LIGHT_BRI; +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) + dgr_value = active_remote_pwm_dimmer->bri; + else +#endif // USE_PWM_DIMMER_REMOTE + dgr_value = target_bri; + } + + // Reset the the button press count. + button_press_count[button_index] = 0; + } + + // If the button was tapped (pressed and released quickly), increment the count of how many + // times this button has been pressed. + else if (button_hold_time[button_index] >= now) { + button_press_count[button_index]++; + } + } + + // Flag the button as released. + button_pressed[button_index] = false; +#ifdef USE_PWM_DIMMER_REMOTE + buttons_pressed--; +#endif // USE_PWM_DIMMER_REMOTE + } + + if (toggle_power) { + power_t new_power; + uint8_t new_bri; +#ifdef USE_DEVICE_GROUPS +#ifdef USE_PWM_DIMMER_REMOTE + if (!active_device_is_local) { + active_remote_pwm_dimmer->power ^= 1; + new_power = active_remote_pwm_dimmer->power; + new_bri = active_remote_pwm_dimmer->bri; + } + else { +#endif // USE_PWM_DIMMER_REMOTE + new_power = power ^ 1; + new_bri = target_bri; +#ifdef USE_PWM_DIMMER_REMOTE + } +#endif // USE_PWM_DIMMER_REMOTE + if (new_power) + SendDeviceGroupMessage(power_button_index, DGR_MSGTYP_UPDATE, DGR_ITEM_LIGHT_BRI, new_bri, DGR_ITEM_POWER, new_power); + else + SendDeviceGroupMessage(power_button_index, DGR_MSGTYP_UPDATE, DGR_ITEM_POWER, new_power); +#endif // USE_DEVICE_GROUPS + +#ifdef USE_PWM_DIMMER_REMOTE + // The target brightness has already been set. Execute the toggle power command with a source of + // SRC_RETRY. This will turn the power on using the current target brightness (see + // FUNC_SET_DEVICE_POWER below). + if (active_device_is_local) +#endif // USE_PWM_DIMMER_REMOTE + ExecuteCommandPower(1, POWER_TOGGLE, SRC_RETRY); + } + + // If we're not toggling the power and we made changes, send a group update. + else if (dgr_item) { +#ifdef USE_DEVICE_GROUPS + SendDeviceGroupMessage(power_button_index, DGR_MSGTYP_UPDATE_DIRECT, dgr_item, dgr_value); +#endif // USE_DEVICE_GROUPS + if (Settings.flag3.hass_tele_on_power) { // SetOption59 - Send tele/%topic%/STATE in addition to stat/%topic%/RESULT + MqttPublishTeleState(); + } + } +} + +/*********************************************************************************************\ + * Commands +\*********************************************************************************************/ + +void CmndBriMin(void) +{ + uint8_t new_bri_min = Settings.bri_min; + if (XdrvMailbox.payload > 0 && XdrvMailbox.payload <= 255) { + new_bri_min = (XdrvMailbox.payload < 4 ? 4 : XdrvMailbox.payload); + } + else if (1 == XdrvMailbox.data_len) { + if ('-' == XdrvMailbox.data[0]) { + if (new_bri_min > 4) new_bri_min--; + } + else if ('+' == XdrvMailbox.data[0] && new_bri_min < 255) + new_bri_min++; + } + + if (new_bri_min != Settings.bri_min) { + Settings.bri_min = new_bri_min; + PWMDimmerCheckBri(&Settings.bri_power_on); + PWMDimmerCheckBri(&Settings.bri_preset_low); + PWMDimmerCheckBri(&Settings.bri_preset_high); + +#ifdef DEVICE_GROUPS + SendLocalDeviceGroupMessage(DGR_MSGTYP_UPDATE, DGR_ITEM_BRI_MIN, Settings.bri_min, + DGR_ITEM_BRI_POWER_ON, Settings.bri_power_on, DGR_ITEM_BRI_PRESET_LOW, Settings.bri_preset_low, + DGR_ITEM_BRI_PRESET_HIGH, Settings.bri_preset_high); +#endif // DEVICE_GROUPS + } + + ResponseCmndNumber(Settings.bri_min); +} + +void CmndBriPreset(void) +{ + if (XdrvMailbox.data_len > 0) { + bool valid = true; + uint32_t value; + uint8_t parm[2]; + parm[0] = Settings.bri_preset_low; + parm[1] = Settings.bri_preset_high; + char * ptr = XdrvMailbox.data; + for (uint32_t i = 0; i < 2; i++) { + while (*ptr == ' ') ptr++; + if (*ptr == '+') { + if (parm[i] < 255) parm[i]++; + } + else if (*ptr == '-') { + if (parm[i] > Settings.bri_min) parm[i]--; + } + else { + value = strtoul(ptr, &ptr, 0); + if (value < 1 || parm[i] > 255) { + valid = false; + break; + } + parm[i] = value; + PWMDimmerCheckBri(&parm[i]); + if (*ptr != ',') break; + } + ptr++; + } + if (valid && !*ptr) { + if (parm[0] < parm[1]) { + Settings.bri_preset_low = parm[0]; + Settings.bri_preset_high = parm[1]; + } else + { + Settings.bri_preset_low = parm[1]; + Settings.bri_preset_high = parm[0]; + } +#ifdef DEVICE_GROUPS + SendLocalDeviceGroupMessage(DGR_MSGTYP_UPDATE, DGR_ITEM_BRI_PRESET_LOW, Settings.bri_preset_low, DGR_ITEM_BRI_PRESET_HIGH, Settings.bri_preset_high); +#endif // DEVICE_GROUPS + } + } + Response_P(PSTR("{\"" D_CMND_BRI_PRESET "\":{\"Low\":%d,\"High\":%d}}"), Settings.bri_preset_low, Settings.bri_preset_high); +} + +void PWMDmmerCmndDimmer(void) +{ + uint8_t dimmer = changeUIntScale(target_bri, 0, 255, 0, 100); + if (1 == XdrvMailbox.data_len) { + if ('+' == XdrvMailbox.data[0]) + XdrvMailbox.payload = (dimmer > 89) ? 100 : dimmer + 10; + else if ('-' == XdrvMailbox.data[0]) { + XdrvMailbox.payload = (dimmer < 11) ? 1 : dimmer - 10; + } + } + + if (XdrvMailbox.payload >= 0 && XdrvMailbox.payload <= 100) { + uint8_t bri = changeUIntScale(XdrvMailbox.payload, 0, 100, 0, 255); +#ifdef USE_DEVICE_GROUPS + SendLocalDeviceGroupMessage(DGR_MSGTYP_UPDATE, DGR_ITEM_LIGHT_BRI, bri); +#endif // USE_DEVICE_GROUPS + PWMDimmerSetBri(bri); + } + + ResponseCmndNumber(Settings.light_dimmer); +} + +void CmndLedTimeout(void) +{ + // Set the brightness LED timeout state. + switch (XdrvMailbox.payload) { + case 0: // Off + case 1: // On + Settings.flag4.led_timeout = XdrvMailbox.payload; + break; + case 2: // Toggle + Settings.flag4.led_timeout ^= 1; + break; + } + if (relay_is_on) PWMDimmerSetBrightnessLeds(0); + ResponseCmndStateText(Settings.flag4.led_timeout); +} + +void CmndPoweredOffLed(void) +{ + // Set the powered-off LED state. + switch (XdrvMailbox.payload) { + case 0: // Off + case 1: // On + Settings.flag4.powered_off_led = XdrvMailbox.payload; + break; + case 2: // Toggle + Settings.flag4.powered_off_led ^= 1; + break; + } + PWMDimmerSetPoweredOffLed(); + ResponseCmndStateText(Settings.flag4.powered_off_led); +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xdrv35(uint8_t function) +{ + bool result = false; + + if (PWM_DIMMER != my_module_type) return result; + + switch (function) { + case FUNC_LOOP: + if (current_bri != target_bri) PWMDimmerAnimate(false); + break; + + case FUNC_EVERY_SECOND: + // Turn off the brightness LED's if it's time. + if (led_timeout_time && led_timeout_time < millis()) { + for (uint32_t index = 0; index < leds_present; index++) SetLedPowerIdx(index, 0); + led_timeout_time = 0; + } + + // The powered-off LED is also the LedLink LED. If we lose the WiFi or MQTT server + // connection, the LED will be set to a blinking state and will be turned off when the + // connection is restored. If the state is blinking now, set a flag so we know that we need + // to restore it when it stops blinking. + if (global_state.data) + restore_powered_off_led = 5; + else if (restore_powered_off_led) { + PWMDimmerSetPoweredOffLed(); + restore_powered_off_led--; + } + break; + + case FUNC_BUTTON_PRESSED: + // Handle the button press/release instead of the button handler. + PWMDimmerHandleButton(); + result = true; + break; + +#ifdef USE_DEVICE_GROUPS + case FUNC_DEVICE_GROUP_REQUEST: + PWMDimmerHandleDeviceGroupRequest(); + break; +#endif // USE_DEVICE_GROUPS + + case FUNC_SET_DEVICE_POWER: + // Handle turning the power on/off here so we can fade off. Set the target brightness and let + // Animate handle turning the power off/on. If the source is SRC_RETRY, we're turning the + // power on with a brightness preset and the target brightness has already been set. + if (!XdrvMailbox.index) + target_bri = 0; + else if (SRC_RETRY != XdrvMailbox.payload) { + target_bri = Settings.bri_power_on; + Settings.light_dimmer = changeUIntScale(target_bri, 0, 255, 0, 100); + } + result = true; + break; + + case FUNC_COMMAND: + result = DecodeCommand(kPWMDimmerCommands, PWMDimmerCommand); + break; + + case FUNC_INIT: + PWMDimmerInit(); + break; + + case FUNC_PRE_INIT: +#ifdef USE_PWM_DIMMER_REMOTE + // If remote device mode is enabled, set the device group count to the number of buttons + // present. + if (Settings.flag4.remote_device_mode) { + for (uint32_t button_index = 0; button_index < MAX_KEYS; button_index++) { + if (pin[GPIO_KEY1 + button_index] < 99) device_group_count++; + } + } +#endif // USE_PWM_DIMMER_REMOTE + break; + + case FUNC_MODULE_INIT: + PWMModuleInit(); + result = true; + break; + } + return result; +} + +#endif // USE_PWM_DIMMER \ No newline at end of file