How to make a dumb 3 way bulb lamp smart

This is an alternative configuration for the ESP32 that looks to handle touch events better. It appears the touch hardware’s range of reported values will change for multiple reasons. This makes selecting a threshold that continues to work over time hard to do. This new code just looks for a significate change in the reported touch value from event to event in order to determine that the lamp is being touched. By significant I simply mean a change in value larger than might happen from random sampling of the hardware. Here’s the updated sensor code. While you need to set a threshold value, it’s not used for any thing.

esphome:
  name: "fr-lamp-touch-bs"
  friendly_name: fr brian side lamp

esp32:
  board: esp32dev

# Enable logging
logger:
  level: INFO
#  level: DEBUG

# Enable Home Assistant API
api:
  encryption:
    key: !secret api_key

ota:
  password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "FR-Lamp-Touch"
    password: !secret wifi_password

# Enable Web server.
web_server:
  port: 80
  
captive_portal:

sensor:
  # hack so the touch sensor doesn't kick off at power on
  - platform: uptime
    name: Uptime Sensor
    id: time_since_boot
    update_interval: 30s

  #Readings calculated from espTouch sensors
  - platform: template
#    name: "Handle Touch"
    id: "handle_touch"
    internal: True
    update_interval: 333ms # a third of a second between check of touch 
    accuracy_decimals: 0
    lambda: |-
      static uint32_t touch_cnt; 
      static uint32_t prev_val;
      static uint16_t first_time = 1;
      static uint16_t start_up_delay = 30; // about 10 seconds - 30*333ms
      uint32_t cur_val;

      cur_val = ((uint32_t) id(touch_sensor)->get_value());

      if( first_time ){
        start_up_delay -= 1;
        if( start_up_delay <= 0){
          ESP_LOGI("custom", "Initial touch value, cur is: %d", cur_val);
          first_time = 0;
          touch_cnt = 0;
        }
        else
        {
          ESP_LOGI("custom", "Establishing initial touch sensor value, cur is: %d", cur_val);
        }
      }
      else if( cur_val < (prev_val - 30)){
        ESP_LOGI("custom", "initial touch, value decreased from %d to %d", prev_val, cur_val);
        touch_cnt = 1;
      }
      else if( touch_cnt > 0 ){
        if( cur_val > (prev_val + 30)){
          ESP_LOGI("custom", "done touch %d, value increased from %d to %d", touch_cnt, prev_val, cur_val);
          if( touch_cnt > 10)
          {
            ESP_LOGI("custom", "ignore touch");
          }
          else if (touch_cnt > 2)
          {
            ESP_LOGI("custom", "change brightness");
            id(cycle_brightness).press();
          }
          else 
          {
            ESP_LOGI("custom", "push power button");
            id(power).toggle();
          }
          touch_cnt = 0;
        }
        else
        {
          if( touch_cnt < 100){
            touch_cnt += 1;
          }
          ESP_LOGD("custom", "multi touch %d, value %d to %d", touch_cnt, prev_val, cur_val);
        }
      }

      prev_val = cur_val;
      return ((uint32_t) id(touch_sensor)->get_value());

esp32_touch:
  setup_mode: False
  iir_filter: 15ms
#  low_voltage_reference: 0.5V
  high_voltage_reference: 2.4V
  voltage_attenuation: 1V

binary_sensor:
  - platform: esp32_touch
    name: "esp32 touch sensor"
    id: touch_sensor
    #pin: GPIO32
    pin: GPIO27
    threshold: 710
    filters:
      # Small filter, to debounce the spurious events.
      - delayed_on: 10ms
      - delayed_off: 10ms

switch:
  - platform: gpio
    name: "low_level_filament"
    pin: 17
    id: low_level
    internal: True
  - platform: gpio
    name: "high_level_filament"
    pin: 16
    id: high_level
    internal: True
  - platform: template
    name: power
    id: power
    restore_mode: RESTORE_DEFAULT_OFF
    lambda: |-
      if (id(low_level).state || id(high_level).state) {
        return true;
      } else {
        return false;
      }
    turn_on_action:
      - select.set:
          id: modus_mode
          option: "low"
      #- switch.turn_on: 
    turn_off_action:
      - select.set:
          id: modus_mode
          option: "off"

select:
  - platform: template
    name: "brightness"
    id: modus_mode
    optimistic: true
    options:
      - "off"
      - "low"
      - "medium"
      - "high"
    initial_option: "off"
    on_value:
      then:
        - lambda: |-
            if (id(modus_mode).active_index() == 0) {
              id(low_level).turn_off();
              id(high_level).turn_off();
            } else if (id(modus_mode).active_index() == 1) {
              id(low_level).turn_on();
              id(high_level).turn_off();
            } else if (id(modus_mode).active_index() == 2) {
              id(low_level).turn_off();
              id(high_level).turn_on();
            } else if (id(modus_mode).active_index() == 3) {
              id(low_level).turn_on();
              id(high_level).turn_on();
            }         

button:
  # button to cycle brightness
  - platform: template
    name: cycle brightness
    id: cycle_brightness
    on_press:
      then:
        if:
          condition:
            and:
              # if light is off 
              - switch.is_off: low_level
              - switch.is_off: high_level
          then:
            # set to lowest level
            - select.set:
                id: modus_mode
                option: "low"
          else:
            if:
              condition:
                and:
                  # if at low bright
                  - switch.is_on: low_level
                  - switch.is_off: high_level
              then:
                # go to medium bright
                - select.set:
                    id: modus_mode
                    option: "medium"
              else:
                if:
                  condition:
                    and:
                      # if at medium bright
                      - switch.is_off: low_level
                      - switch.is_on: high_level
                  then:
                    # go to high bright
                    - select.set:
                        id: modus_mode
                        option: "high"
                  else:
                    # finally if at high bright go to low bright
                    - select.set:
                        id: modus_mode
                        option: "low"

  # restart-button
  - platform: restart
    name: "restart-esp32-dim-touch"

One thing with this code is if you put the logging in DEBUG mode you will get a log entry every 333ms as the handle_touch method runs. I couldn’t figure out how to have the ESPHOME code stop reporting the value with each iteration. If anyone knows how to suppress those messages, please let me know. As these message do provide the touch value that can be useful for seeing all the touch values the code is handling.

I received a suggestion to presented the lamp as a monochromatic light. This gives an on/off button with a slider and is a standard light type handled via HA. The result is easier integration to HA with a standard control. Below is the ESP code to make that happen:

esphome:
  name: "fr-lamp-touch-bs"
  friendly_name: fr brian side lamp

esp32:
  board: esp32dev

# Enable logging
logger:
  level: INFO
#  level: DEBUG

# Enable Home Assistant API
api:
  encryption:
    key: !secret api_key

ota:
  password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "FR-Lamp-Touch"
    password: !secret wifi_password

# Enable Web server.
web_server:
  port: 80
  
captive_portal:

sensor:
  # hack so the touch sensor doesn't kick off at power on
  - platform: uptime
    name: Uptime Sensor
    id: time_since_boot
    update_interval: 30s

  #Readings calculated from espTouch sensors
  - platform: template
    id: "handle_touch"
    internal: True
    update_interval: 333ms # a third of a second between check of touch 
    accuracy_decimals: 0
    lambda: |-
      static uint32_t touch_cnt; 
      static uint32_t prev_val;
      static uint16_t first_time = 1;
      static uint16_t start_up_delay = 30; // about 10 seconds - 30*333ms
      uint32_t cur_val;

      cur_val = ((uint32_t) id(touch_sensor)->get_value());

      if( first_time ){
        start_up_delay -= 1;
        if( start_up_delay <= 0){
          ESP_LOGI("custom", "Initial touch value, cur is: %d", cur_val);
          first_time = 0;
          touch_cnt = 0;
        }
        else
        {
          ESP_LOGI("custom", "Establishing initial touch sensor value, cur is: %d", cur_val);
        }
      }
      else if( cur_val < (prev_val - 30)){
        ESP_LOGI("custom", "initial touch, value decreased from %d to %d", prev_val, cur_val);
        touch_cnt = 1;
      }
      else if( touch_cnt > 0 ){
        if( cur_val > (prev_val + 30)){
          ESP_LOGI("custom", "done touch %d, value increased from %d to %d", touch_cnt, prev_val, cur_val);
          if( touch_cnt > 10)
          {
            ESP_LOGI("custom", "ignore touch");
          }
          else if (touch_cnt > 2)
          {
            ESP_LOGI("custom", "change brightness");
            id(brightness_step).press();
          }
          else 
          {
            ESP_LOGI("custom", "push power button");
            id(power).toggle();
          }
          touch_cnt = 0;
        }
        else
        {
          if( touch_cnt < 100){
            touch_cnt += 1;
          }
          ESP_LOGD("custom", "multi touch %d, value %d to %d", touch_cnt, prev_val, cur_val);
        }
      }

      prev_val = cur_val;
      return ((uint32_t) id(touch_sensor)->get_value());

esp32_touch:
  setup_mode: False
  iir_filter: 15ms
#  low_voltage_reference: 0.5V
  high_voltage_reference: 2.4V
  voltage_attenuation: 1V

binary_sensor:
    # touch actions handled above
  - platform: esp32_touch
    id: touch_sensor
    pin: GPIO27
    threshold: 10
    filters:
      # Small filter, to debounce the spurious events.
      - delayed_on: 10ms
      - delayed_off: 10ms
       
button:
  # button to cycle brightness
  - platform: template
    name: brightness step
    id: brightness_step
    on_press:
      then:
        if:
          condition:
            and:
              # if light is off 
              - switch.is_off: low_level
              - switch.is_off: high_level
          then:
            # set to lowest level
            - light.turn_on:
                id: lamp_ctrl
                brightness: 33%          
          else:
            if:
              condition:
                and:
                  # if at low bright
                  - switch.is_on: low_level
                  - switch.is_off: high_level
              then:
                # go to medium bright
                - light.turn_on:
                    id: lamp_ctrl
                    brightness: 66%
              else:
                if:
                  condition:
                    and:
                      # if at medium bright
                      - switch.is_off: low_level
                      - switch.is_on: high_level
                  then:
                    # go to high bright
                    - light.turn_on:
                        id: lamp_ctrl
                        brightness: 100%
                  else:
                    # finally if at high bright go to low bright
                    - light.turn_on:
                        id: lamp_ctrl
                        brightness: 33%

  # restart-button
  - platform: restart
    name: "restart-esp32-dim-touch"

switch:
  - platform: gpio
    #name: "low_level_filament"
    pin: 17
    id: low_level
    internal: True

  - platform: gpio
    #name: "high_level_filament"
    pin: 16
    id: high_level
    internal: True

  - platform: template
    #name: power
    id: power
    internal: True
    restore_mode: RESTORE_DEFAULT_OFF
    lambda: |-
      if (id(low_level).state || id(high_level).state) {
        return true;
      } else {
        return false;
      }
    turn_on_action:
      - light.turn_on:
          id: lamp_ctrl
          brightness: 33%    
      #- switch.turn_on: 
    turn_off_action:
      - light.turn_off:
          id: lamp_ctrl

output:
  - platform: template
    type: float
    id: output_comp
    write_action:
#      - lambda: |-
#          ESP_LOGI("custom", "Write action state = %f", state);
      - if:
          condition:
            lambda: return ((state > 0) && (state < .34));
          then:
            - switch.turn_on: low_level
            - switch.turn_off: high_level
          else:
            - if: 
                condition:
                  lambda: return ((state >= .34) && (state < .67));
                then:
#                  - lambda: |-
#                      ESP_LOGI("custom", "Low level state = %f", state);
                  - switch.turn_off: low_level
                  - switch.turn_on: high_level
                else:
                  - if: 
                      condition:
                        lambda: return ((state >= .67) && (state <= 1));
                      then:
#                        - lambda: |-
#                            ESP_LOGI("custom", "Low level state = %f", state);
                        - switch.turn_on: low_level
                        - switch.turn_on: high_level
                      else:
                        - if: 
                            condition:
                              lambda: return ((state == 0) );
                            then:
#                              - lambda: |-
#                                  ESP_LOGI("custom", "Turn light off = %f", state);
                              - switch.turn_off: low_level
                              - switch.turn_off: high_level
        
light:
  - platform: monochromatic
    name: "Lamp Control"
    id: lamp_ctrl
    output: output_comp
    gamma_correct: 1
    default_transition_length: 10ms
    restore_mode: RESTORE_DEFAULT_OFF