QS-WiFi-D01-TRIAC Dimmer

Hi,
I’ve been enjoying the QS-WIFI-D01-TRIAC dimmer with your ESPhome YAML for quite some while now. I’m looking to tweak it a little bit, but unfortunately after many attempts I now resort to your original post.
I’m looking to add a light.turn_off command with a nice fade out instead of immediate turn off.
Somehow the above command doesn’t affect the module.
Probably has to do something with the fact that this dimmer module needs direct writes to some other module (MCU via UART?).

Is this possible, and if so could you give me a hint how to do so?

Thanks!!

If the fade off is the only time you want the effect, should be possible to add an additional template switch. Use the timer loop to detect when its turned off - just as if the button were held. Some extra logic required to turn fully off at min brightness and Ensure it’s turned on whenever the main light object is on.

Perhaps more elegant and would allow a generic “fade to any level” would be to create a second light that you actually expose to HA to set a target brightness…then use the timer to track the difference between the actual light and the target brightness and adjust accordingly with a maximum step each cycle to create the ramp.

I have found that different bulbs/lights need different blends of the timing frequency and brightness step to get a smooth dim. So would expect to have to tweak the fine details.

The 2nd approach sounds like an interesting enhancement so may that give a go myself. DM if you’d be interested in testing.

1 Like

Seems to work fine on QS-Wifi-D02-2C nowadays…
Finally found time and replaced my Tasmota script for this one, which works flawless for me:

esphome:
  name: qs-wifi-ds02-2c
  platform: ESP8266
  board: esp01_1m

wifi:
  ssid: "MyWiFi_SSID"
  password: "MyWiFi_pw"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Qs-Wifi-Ds02-2C"
    password: "MyFallBack_pw"

captive_portal:

# Enable Home Assistant API
api:
  password: "my-ha-api-password"

ota:

# Example configuration entry (optional web server)
web_server:
  port: 80

# Make sure logging is not using the serial port
logger:
  baud_rate: 0
  logs:
    sensor: ERROR
    duty_cycle: ERROR
    binary_sensor: ERROR
    light: ERROR

# level: VERBOSE
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 2 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();
              }        

Thanks!

Edit also seems to work on MS-105B-220 2-Channel Dimmer

1 Like

Dear aceindy,

Thanks for the information!

I found an error.
I want to set it to switch to the last state after a power failure, when there is power again. Or return to the off state by default.
Unfortunately, it now works by always turning on, that is, turning on CH1 and CH2.

How can it be set that, for example, CH1 can be dimmable, but CH2 acts as a smooth switch?

Please also share the code line described above.

I assume this would be possible with restore_state

restore_state: on

And you’ll have to explain what you mean by ‘smooth switch’ :woozy_face:

What exactly do you want me to share ??

PS: I’m not an ESPHome expert, I just managed to combine the yaml from here and there and made it work on 2 channels

Unfortunately I am a very beginner in the world of ESPHome and I have only minimal knowledge of English. :pensive:

Where should I enter the following?
restore_state: on

smooth switch = all you need to know is to turn it on and off. No dimmability.

Please share the full modified ESPhome code line with me!
Gang1: physical switch = pushbutton. If I press it briefly, turn on the dimmable bulb. If I press and hold for a long time, adjust the brightness. If I press it briefly, turn off the dimmable bulb.

Gang2: physical switch = pushbutton. If I press it briefly, turn on the bulb (no dimmable, simple bulb). If I press / hold for a long time, do nothing. If I press it briefly, turn off the bulb (no dimmable, simple bulb).

Thank you very much in advance!

Just a blind guess, commented out #on_press and changed brightness to fixed 1

Unfortunately, I can’t try it now because I’m not home :pensive: but it would be great to know if your suggestion works.

Another:
‘Restore_state: on’ in line ‘sensor’ is incorrect, indicates an error.

Can you check, can you try?

Sorry, I don’t have the time to do that…maybe someone else has ideas ?

Thanks @aceindy !
I ported my MS-105B-220 2-Channel Dimmer from Tasmota to ESPHome with your implementation and it works like a charme :smiley:

1 Like