QS-WiFi-D01-TRIAC Dimmer

I 1st grabbed a couple of these some months ago - and had limited success as at the time Tuya-convert wasnt working. I got one flashed - but had to do it via Serial and it never worked properly - though I was able to sniff the Serial comms to the MCU that controls the dimmer.

HTB1OeaAXLfsK1RjSszgq6yXzpXao

I have just bought a fresh couple to try again. Tuya-convert worked like a dream and I now have a working yaml for ESPHome including dimming via the push button. They are a perfect replacement like this for old X10 LD11 dimmer modules that also use a hard wired momentary switch.

There’s lots of very good info here :-

Original tasmota thread :-

related thread on ESPHome git

My Yaml…

key features…

  • Exposes the light to HA
  • Push Button - Single Click = Toggle Light
  • Push Button - Hold = Cycles through Dim->Bright->Dim etc.
  • No custom components required
esphome:
  name: esp_dim01
  platform: ESP8266
  board: esp01_1m

wifi:
  domain: .local
  ssid: "xxxxxx"
  password: "xxxxx"
  manual_ip:
    static_ip: 192.168.1.69
    gateway: 192.168.1.254
    subnet: 255.255.255.0

# Enable logging
logger:
  baud_rate: 0
  level: DEBUG
  logs:
    sensor: ERROR
    duty_cycle: ERROR
    binary_sensor: ERROR
    light: ERROR

# level: VERBOSE
# Enable Home Assistant API
api:

ota:

web_server:

time:
  - platform: homeassistant

substitutions:
  switch_id: "dim_01"

# globals:
# Dummy light brightness tracker Global

globals:
  # Dim direction for Switch 1: 0=Up (brighten) 1=down (dim)
  - id: g_direction_1
    type: int
    restore_value: no
    initial_value: "1"
  # Counter for time pressed for switch 1
  - id: g_counter_1
    type: int
    restore_value: no
    initial_value: "0"
  # initial brightness

# Uart definition to talk to MCU dimmer
uart:
  tx_pin: GPIO1
  rx_pin: GPIO3
  stop_bits: 1
  baud_rate: 9600

sensor:
  - platform: wifi_signal
    name: "${switch_id} WiFi Signal Sensor"
    update_interval: 60s
  # Primary template sensor to track Brightness of light object for "on_value" sending to MCU dimmer
  - platform: template
    name: "${switch_id} Brightness Sensor"
    id: sensor_g_bright
    internal: true
    update_interval: 20ms
    # Ensure on_value only triggered when brightness (0-255) changes
    filters:
      delta: 0.8
    # Read brightness (0 - 1) from light , convert to (0-255) for MCU
    lambda: |-
      if (id(light_main).remote_values.is_on()) {
        return (int(id(light_main).remote_values.get_brightness() * 255));
      }
      else {
        return 0;
      }
    # On Change send to MCU via UART
    on_value:
      then:
        - uart.write: !lambda |-
            return {0xFF, 0x55, (char) id(sensor_g_bright).state, 0x05, 0xDC, 0x0A};
        - logger.log:
            level: INFO
            format: "Sensor Value Change sent to UART %3.1f"
            args: ["id(sensor_g_bright).state"]
  # Sensor to detect button push (via duty_cycle of 50hz mains signal)
  - platform: duty_cycle
    pin: GPIO13
    internal: true
    id: sensor_push_switch
    name: "${switch_id} Sensor Push Switch"
    update_interval: 20ms

binary_sensor:
  #Binary sensor (on/off) which reads duty_cyle sensor readings.
  - platform: template
    id: switch1
    internal: true
    name: "${switch_id} Switch Binary Sensor"
    # read duty_cycle, convert to on/off
    lambda: |-
      if (id(sensor_push_switch).state < 95.0) {
        return true;
      } else {
        return false;
      }
    # Short Click - toggle light only
    on_click:
      max_length: 300ms
      then:
        light.toggle: light_main
    # Generic On_Press - log press, toggle DIM Direction and reset press interval counter
    on_press:
      then:
        - logger.log: "Switch 1 Press"
        - lambda: |-
            if (id(g_direction_1) == 0) {
              id(g_direction_1) = 1;
            } else {
              id(g_direction_1) = 0;
            }
            id(g_counter_1) = 0;

# Dummy light output to allow creation of light object
output:
  - platform: esp8266_pwm
    pin: GPIO14
    frequency: 800 Hz
    id: dummy_pwm

# Primary Light object exposed to HA
light:
  - platform: monochromatic
    default_transition_length: 20ms
    name: "${switch_id} Light"
    output: dummy_pwm
    id: light_main

switch:
  - platform: restart
    name: "${switch_id} Restart"

# Polling object for long press handling of switch for dim/brighten cycle
interval:
  - interval: 20ms
    then:
      - if:
          condition:
            binary_sensor.is_on: switch1
          then:
            # Ramp rate for dim is product of interval (20ms) * number of intervals
            # Every 20ms Dimmer is increased/decreased by 2/255
            # Lower limit = 10%
            # Upper limit = 100%
            # 100% - 10% = 90% = 230/255. Therefore 230/2 * 20ms = 2.3 seconds for full range
            # At full/min brightness - further 16x20ms = 0.32 Seconds "dwell" by resetting counter to 0
            # Initial pause for 16x20ms = 0.32s to allow "on_click" to be discounted 1st
            # g_direction_1 = 0 (Increasing brightness)
            # g_direction_1 = 1 (decreasing brightness)
            # g_counter_1 = Interval pulse counter

            lambda: |-
              float curr_bright = id(light_main).remote_values.get_brightness();
              id(g_counter_1) += 1; 

              // If max bright, change direction
              if (curr_bright >= 0.999 && id(g_direction_1) == 0) {
                id(g_direction_1) = 1;
                id(g_counter_1) = 0;
              }

              // If below min_bright, change direction
              if (curr_bright < 0.1 && id(g_direction_1) == 1) {
                id(g_direction_1) = 0;
                id(g_counter_1) = 0;
              }

              if (id(g_direction_1) == 0 && id(g_counter_1) > 15) {
                // Increase Bright
                auto call = id(light_main).turn_on();
                call.set_brightness(curr_bright + (2.0/255.0));
                call.perform();
              }

              else if(id(g_direction_1) == 1 && id(g_counter_1) > 15) {
                // Decrease Bright
                auto call = id(light_main).turn_on();
                call.set_brightness(curr_bright - (2.0/255.0));
                call.perform();
              }
6 Likes

It would be awesome if you could please add your code here

Amazing! I will try this yaml on one of my dimmers this week. Thank you SO much for sharing!!

@Bram, did you have any luck with this dimmer? I buy similar model (this one https://aliexpress.com/item/33015604161.html) and want to integrate it.

They work like a charm. I was able to use the Tuya Convert 2.0 instructions at the top of this thread (blakadder) to flash alternative firmware (I chose tasmota basic) & afterwards flashed ESPHome. Integration into HA is a matter of one click in the Integrations panel. Good luck!

PS Not sure what model you have bought an whether this is compatible with the QS-WIFI-D01-TRIAC…

Those devices are not WiFi…but a proprietary RF on 2.4ghz. You can control them via the Wi-Fi box…but there is no feedback for changes made by the RF controllers.

I have some…they work well. I love the multi function wall switch/controller. I integrated through node red. Someone is working On a proper Native integration. The lack of feedback is starting to niggle compared to better integrated Wi-Fi Ones

Hello recently, I have 1 dimmer of 2 channels specifically the QS-WIFI-D02 model, after using Tuya-Convert I have flash your configuration of QS-WIFI-D01, but it does not work. After searching the network I found a tasmota script that does work for the 2 channels https://gist.github.com/thxthx0/ce7f72ea75ab82be2704c9536ea77bf7
From what I have understood, having 2 channels changes the communication configuration with the MCU.
I’m starting with homeassistant, I still don’t have the necessary knowledge to edit your configuration correctly, could you guide me?

I have been following the Tasmota thread on the 2 ch with interest. Unfortunately (!) I dont need any more or would have ordered one to test.

This is a quick & dirty (untested) conversion to 2 Switches/ 2 Channels

esphome:
  name: esp_dim_2ch_01
  platform: ESP8266
  board: esp01_1m

wifi:
  domain: .local
  ssid: "xxxxxx"
  password: "xxxxxxxx"
  manual_ip:
    static_ip: 192.168.1.69
    gateway: 192.168.1.254
    subnet: 255.255.255.0

# Enable logging
logger:
  baud_rate: 0
  level: DEBUG
  logs:
    sensor: ERROR
    duty_cycle: ERROR
    binary_sensor: ERROR
    light: ERROR

# level: VERBOSE
# Enable Home Assistant API
api:

ota:

web_server:

time:
  - platform: homeassistant

substitutions:
  switch_id: "dim_2ch_01"

# globals:
# Dummy light brightness tracker Global

globals:
  # Dim direction for Switch 1: 0=Up (brighten) 1=down (dim)
  - id: g_direction_1
    type: int
    restore_value: no
    initial_value: "1"
  # Counter for time pressed for switch 1
  - id: g_counter_1
    type: int
    restore_value: no
    initial_value: "0"
  # initial brightness
  # Dim direction for Switch 2: 0=Up (brighten) 1=down (dim)
  - id: g_direction_2
    type: int
    restore_value: no
    initial_value: "1"
  # Counter for time pressed for switch 2
  - id: g_counter_2
    type: int
    restore_value: no
    initial_value: "0"
  # initial brightness
  
# Uart definition to talk to MCU dimmer
uart:
  tx_pin: GPIO1
  rx_pin: GPIO3
  stop_bits: 1
  baud_rate: 9600

sensor:
  - platform: wifi_signal
    name: "${switch_id} WiFi Signal Sensor"
    update_interval: 60s
  # Primary template sensor to track Brightness of light object for "on_value" sending to MCU dimmer
  # CH1
  - platform: template
    name: "${switch_id} Brightness Sensor CH1"
    id: sensor_g_bright_1
    internal: true
    update_interval: 20ms
    # Ensure on_value only triggered when brightness (0-255) changes
    filters:
      delta: 0.8
    # Read brightness (0 - 1) from light , convert to (0-255) for MCU
    lambda: |-
      if (id(light_main_1).remote_values.is_on()) {
        return (int(id(light_main_1).remote_values.get_brightness() * 255));
      }
      else {
        return 0;
      }
    # On Change send to MCU via UART
    on_value:
      then:
        - uart.write: !lambda |-
            return {0xFF, 0x55, 0x01, (char) id(sensor_g_bright_1).state,0x00, 0x00, 0x00, 0x0A};
        - logger.log:
            level: INFO
            format: "CH1 Sensor Value Change sent to UART %3.1f"
            args: ["id(sensor_g_bright_1).state"]
  # Sensor to detect button push (via duty_cycle of 50hz mains signal)
  - platform: template
    name: "${switch_id} Brightness Sensor CH2"
    id: sensor_g_bright_2
    internal: true
    update_interval: 20ms
    # Ensure on_value only triggered when brightness (0-255) changes
    filters:
      delta: 0.8
    # Read brightness (0 - 1) from light , convert to (0-255) for MCU
    lambda: |-
      if (id(light_main_2).remote_values.is_on()) {
        return (int(id(light_main_2).remote_values.get_brightness() * 255));
      }
      else {
        return 0;
      }
    # On Change send to MCU via UART
    on_value:
      then:
        - uart.write: !lambda |-
            return {0xFF, 0x55, 0x02, 0x00, (char) id(sensor_g_bright_2).state, 0x00, 0x00, 0x0A};
        - logger.log:
            level: INFO
            format: "CH2 Sensor Value Change sent to UART %3.1f"
            args: ["id(sensor_g_bright_2).state"]
  # Sensor to detect button push (via duty_cycle of 50hz mains signal)
  - platform: duty_cycle
    pin: GPIO13
    internal: true
    id: sensor_push_switch_1
    name: "${switch_id} Sensor Push Switch 1"
    update_interval: 20ms
  - platform: duty_cycle
    pin: GPIO5
    internal: true
    id: sensor_push_switch_2
    name: "${switch_id} Sensor Push Switch 2"
    update_interval: 20ms

binary_sensor:
  #Binary sensor (on/off) which reads duty_cyle sensor readings. CH1
  - platform: template
    id: switch1
    internal: true
    name: "${switch_id} Switch Binary Sensor 1"
    # read duty_cycle, convert to on/off
    lambda: |-
      if (id(sensor_push_switch_1).state < 95.0) {
        return true;
      } else {
        return false;
      }
    # Short Click - toggle light only
    on_click:
      max_length: 300ms
      then:
        light.toggle: light_main_1
    # Generic On_Press - log press, toggle DIM Direction and reset press interval counter
    on_press:
      then:
        - logger.log: "Switch 1 Press"
        - lambda: |-
            if (id(g_direction_1) == 0) {
              id(g_direction_1) = 1;
            } else {
              id(g_direction_1) = 0;
            }
            id(g_counter_1) = 0;
  #Binary sensor (on/off) which reads duty_cyle sensor readings. CH2
  - platform: template
    id: switch2
    internal: true
    name: "${switch_id} Switch Binary Sensor 2"
    # read duty_cycle, convert to on/off
    lambda: |-
      if (id(sensor_push_switch_2).state < 95.0) {
        return true;
      } else {
        return false;
      }
    # Short Click - toggle light only
    on_click:
      max_length: 300ms
      then:
        light.toggle: light_main_2
    # Generic On_Press - log press, toggle DIM Direction and reset press interval counter
    on_press:
      then:
        - logger.log: "Switch 1 Press"
        - lambda: |-
            if (id(g_direction_2) == 0) {
              id(g_direction_2) = 1;
            } else {
              id(g_direction_2) = 0;
            }
            id(g_counter_2) = 0;

# Dummy light output to allow creation of light object
output:
  - platform: esp8266_pwm
    pin: GPIO14
    frequency: 800 Hz
    id: dummy_pwm1
  - platform: esp8266_pwm
    pin: GPIO16
    frequency: 800 Hz
    id: dummy_pwm2

# Primary Light object exposed to HA
light:
  - platform: monochromatic
    default_transition_length: 20ms
    name: "${switch_id} Light 1"
    output: dummy_pwm1
    id: light_main_1
  - platform: monochromatic
    default_transition_length: 20ms
    name: "${switch_id} Light 2"
    output: dummy_pwm2
    id: light_main_2

switch:
  - platform: restart
    name: "${switch_id} Restart"

# Polling object for long press handling of switch for dim/brighten cycle
interval:
  - interval: 20ms
    then:
      - if:
          condition:
            binary_sensor.is_on: switch1
          then:
            # Ramp rate for dim is product of interval (20ms) * number of intervals
            # Every 20ms Dimmer is increased/decreased by 2/255
            # Lower limit = 10%
            # Upper limit = 100%
            # 100% - 10% = 90% = 230/255. Therefore 230/2 * 20ms = 2.3 seconds for full range
            # At full/min brightness - further 16x20ms = 0.32 Seconds "dwell" by resetting counter to 0
            # Initial pause for 16x20ms = 0.32s to allow "on_click" to be discounted 1st
            # g_direction_1 = 0 (Increasing brightness)
            # g_direction_1 = 1 (decreasing brightness)
            # g_counter_1 = Interval pulse counter

            lambda: |-
              float curr_bright = id(light_main_1).remote_values.get_brightness();
              id(g_counter_1) += 1; 

              // If max bright, change direction
              if (curr_bright >= 0.999 && id(g_direction_1) == 0) {
                id(g_direction_1) = 1;
                id(g_counter_1) = 0;
              }

              // If below min_bright, change direction
              if (curr_bright < 0.1 && id(g_direction_1) == 1) {
                id(g_direction_1) = 0;
                id(g_counter_1) = 0;
              }

              if (id(g_direction_1) == 0 && id(g_counter_1) > 15) {
                // Increase Bright
                auto call = id(light_main_1).turn_on();
                call.set_brightness(curr_bright + (2.0/255.0));
                call.perform();
              }

              else if(id(g_direction_1) == 1 && id(g_counter_1) > 15) {
                // Decrease Bright
                auto call = id(light_main_1).turn_on();
                call.set_brightness(curr_bright - (2.0/255.0));
                call.perform();
              }
      - if:
          condition:
            binary_sensor.is_on: switch2
          then:
            # Ramp rate for dim is product of interval (20ms) * number of intervals
            # Every 20ms Dimmer is increased/decreased by 2/255
            # Lower limit = 10%
            # Upper limit = 100%
            # 100% - 10% = 90% = 230/255. Therefore 230/2 * 20ms = 2.3 seconds for full range
            # At full/min brightness - further 16x20ms = 0.32 Seconds "dwell" by resetting counter to 0
            # Initial pause for 16x20ms = 0.32s to allow "on_click" to be discounted 1st
            # g_direction_1 = 0 (Increasing brightness)
            # g_direction_1 = 1 (decreasing brightness)
            # g_counter_1 = Interval pulse counter

            lambda: |-
              float curr_bright = id(light_main_2).remote_values.get_brightness();
              id(g_counter_2) += 1; 

              // If max bright, change direction
              if (curr_bright >= 0.999 && id(g_direction_2) == 0) {
                id(g_direction_2) = 1;
                id(g_counter_2) = 0;
              }

              // If below min_bright, change direction
              if (curr_bright < 0.1 && id(g_direction_2) == 1) {
                id(g_direction_2) = 0;
                id(g_counter_2) = 0;
              }

              if (id(g_direction_2) == 0 && id(g_counter_2) > 15) {
                // Increase Bright
                auto call = id(light_main_2).turn_on();
                call.set_brightness(curr_bright + (2.0/255.0));
                call.perform();
              }

              else if(id(g_direction_2) == 1 && id(g_counter_2) > 15) {
                // Decrease Bright
                auto call = id(light_main_2).turn_on();
                call.set_brightness(curr_bright - (2.0/255.0));
                call.perform();
              }        

Thank you very much, I will try it and tell you something.

Hi Seve,
Any update on your trials?

The state testing, and the regulation by prolonged pressing works well, but the on-off by a short press, when pressing CH1 the 2 lights come on and when pressing again they turn off, the same happens when pressing CH2.

I have modified 2 things from the original code, the first one may cause the CH1 light to come on, but the second one is only for the log.

correc_2

correc_2-1

I’m checking the code to see if I find the problem

1 Like

This Tasmota work-around using MQTT instead of auto-detect (SetOption19=0) worked for me on the D02:

are these able to dim led lights? or will they flicker

Guess that depends on the load.

I use them with LED’s, no flickering, although one has startup problems when I set dimming below 10%

This is awesome, great job. I have been using the sonoff version for this but have all my switches on ESPHome. I had planned to convert but you saved me a lot of work. :smiley:

Works flawlessly, I only changed minimum bright to 0.067 as some of my led bulbs are already quite bright at 10%

Thanks again!

This is working perfectly with QS-WiFi-D02-TRIAC Dimmer too ! Big thanks.
I only edited the switch1 as non-interal (for future use)

This is an alternative way of doing it without attaching a fake output to a pin. In my case this caused some artifacts. No doubt there’s some better way to do Serial.write :thinking:

uart_output.h:

#include "esphome.h"
using namespace esphome;

class MyCustomFloatOutput : public Component, public FloatOutput {
 public:
  void setup() override {
    // This will be called by App.setup()
    
  }

  void write_state(float state) override {
    // state is the amount this output should be on, from 0.0 to 1.0
    // we need to convert it to an integer first
    float zero = 15.0;
    float range = 255.0 - zero;
    int value = (state==0.0) ?  0 :int((state * range) +zero);
    ESP_LOGD("Uart_output","Uart output setting %d", value);
    Serial.write(0xFF);
    Serial.write(0x55);
    Serial.write((char) int(value));
    Serial.write(0x05);
    Serial.write(0xDC);
    Serial.write(0x0A);

  }
};


yaml:

esphome:
  name: ${device_name}
  platform: ESP8266
  board: esp01_1m
  includes:
    - uart_output.h

Hola, Como hago para actualizar a tasmota scripting?

Hi.

Could you please post your full YAML? Whats should I change, beyond the esphome: section and the uart_output.h to remove the fake output?

Here you go, my device has been modified with a rotary encoder so that my girlfriend can work it:

substitutions:
  device_name: 'qs_d01_triac'
  resolution: '66'
  
esphome:
  name: ${device_name}
  platform: ESP8266
  board: esp01_1m
  includes:
    - uart_output.h

  on_boot:
    then:
      lambda: |-
              
              id(encoder1).set_value((int)(id(light1).remote_values.get_brightness()* id(resolution)));

globals:
   - id: resolution
     type: int
     restore_value: no
     initial_value: '${resolution}'
  
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

uart:
  tx_pin: GPIO1
  rx_pin: GPIO3
  stop_bits: 1
  baud_rate: 9600

# Enable logging
logger:
  level: WARN
  baud_rate: 0
#  logs:
#    sensor: WARN
#    duty_cycle: ERROR


# Enable Home Assistant API
api:

ota:
  password: !secret ota_password

time:
  - platform: homeassistant
    id: homeassistant_time

binary_sensor:
  - platform: gpio
    internal: True
    name: "Switch"
    pin: 
      number: GPIO0
      mode: INPUT_PULLUP
      inverted: True
    on_press:
      then:
        - light.toggle: light1
   
output:
  - platform: custom
    type: float
    lambda: |-
      auto my_custom_float_output = new MyCustomFloatOutput();
      App.register_component(my_custom_float_output);
      return {my_custom_float_output};
  
    outputs:
      id: uart_float_output

light:
  - platform: monochromatic
    name: "${device_name} Nursery Light"
    id: light1
    output: uart_float_output
    default_transition_length: 0 ms
    
sensor:
  - platform: rotary_encoder
    internal: True
    name: "Rotary Encoder"
    id: encoder1
    pin_a: 
      number: GPIO4
      mode: INPUT_PULLUP
      inverted: True
    pin_b: 
      number: GPIO2
      mode: INPUT_PULLUP
      inverted: True
    min_value: 0
    max_value: ${resolution}

# Synchronize the rotary encoder state with the light

interval: 
  - interval: 300ms
    then:
      - lambda: |-
            static float prev_bright = 0.0;  // previously set brightness
            float light_bright= (float)id(light1).remote_values.get_brightness();;
            float enc_bright_low = min(1.0f,(float(id(encoder1).state / ${resolution})));
            float enc_bright_high = min(1.0f,float(1.0 + id(encoder1).state ) / ${resolution});
            float enc_bright_mean = (enc_bright_high+enc_bright_low)/2.0;


            if ((enc_bright_low >  prev_bright) || (enc_bright_high < prev_bright)) {
              ESP_LOGD("Poll loop","Prev: %3.1f\n", prev_bright);
              ESP_LOGD("Poll loop","Light: %3.1\n", light_bright);
              ESP_LOGD("Poll loop","Enc: %3.1\n", enc_bright_high);

              auto call =  id(light1).turn_on();
              if (id(light1).remote_values.get_state() != 0.0){ //Light is on
                  ESP_LOGD("Poll loop","Setting brightness %3.1f\n",enc_bright_mean);
                  call.set_brightness(enc_bright_mean);
                  call.perform();
              } else { // Light is off
                  ESP_LOGD("Poll loop","Set brightness (off) %3.1f\n",enc_bright_high);
                  id(light1).remote_values.set_brightness(enc_bright_mean);
              } 
              prev_bright = enc_bright_mean;

            }else if (light_bright != prev_bright) {
              id(encoder1).set_value((int)(id(light1).remote_values.get_brightness()* id(resolution)));
              prev_bright = light_bright;
            }

1 Like