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();
              }
5 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

This 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)