How to make a dumb 3 way bulb lamp smart

This tread provides details for converting a three way bulb lamp into a smart touch lamp. A less than one second touch turns the lamp on and off. A 1 to 10 second touch cycles through the three brightness levels.

The end product is controlled via an integrated esp32 board that includes two relays. If I could get the touch function working on the esp32-S3 I’d probable use one of the M5 products with a separate 2 relay board because it would be much smaller. I previously provided some input here about this effort as I was figuring out how to modify the lamp. This is the details for the first lamp I modified. I did figure out the esp32-S3 and built subsequent lamps with it. Details about that implementation are in a subsequent posted :

Originally, I was going to try and fit the electronics within the lamp but decided that was too big of a pain. In the end product the electronics are currently housed as part of the cord. Since the lamp needs both 110v and 5v, I’m currently feeding the product with two cords. One is a 110v line, the second is from a USB phone adaptor. This feeds power to the electronics box. Then from the electronics box I have a cord with three stands of 18 gauge wire and a small wire for the touch sensor.

The items used for this project are:

  • ESP32 board with integrated relays.
  • A replacement light socket that doesn’t have a knob
  • Wire with 3 stands 18 gauge
  • A box to hold the electronics
  • The previous power cord from the lamp
  • Low gauge wire that can be soldered to the ESP board and screwed to the lamp. I just used some old stuff I had sitting around.
  • Wrap to make the 3 stand wire and touch sensor wire look like one wire
  • Wire connectors

While the link for the ESP board is form amazon I actually found the board cheaper on both ebay, walmart and aliexpress. I got three feet of the 3 strand 18 gauge cord from home depot for $.75 per foot.

The first challenge was figuring out how to flash the ESP board. I started trying to do it via ESP home that is integrated with home assistant. This failed and only provided the error failed to connect. I was using a windows laptop with a USB TTL adaptor, attempting to use the install from the “Plug into this computer” option. To do the initial flash I installed the command line version of ESP home on my windows laptop, which worked without issue.

To put the ESP board in flash mode you have to hold the IO0 button when you apply power (plug in the USB TTL board). While still holding the IO0 button you tap the reset button (labeled EN). Finally, you release the IO0 button and the board should be ready to flash.

To use the esphome tool you need a text file with basic configuration. You can build this file in notepad. It should look something like the following:

substitutions:
  esphome_name: touch-lamp

esphome:
  name: "${esphome_name}"
  platform: ESP32
  board: esp32dev

# Enable Home Assistant API
api:
  encryption:
    key: "XXXXXXXXXXXXXXXXXXXXXXXXX"

ota:
  password: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

wifi:
  ssid: "XXXXXXX"
  password: "XXXXXXXXXXXXXXXXXXXXXXX"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "${esphome_name} hotspot"
    password: "whatever"

captive_portal:

time:
  - platform: homeassistant

# Enable Web server.
web_server:
  port: 80

logger:

# Text sensors with general information.
text_sensor:
  # Expose ESPHome version as sensor.
  - platform: version
    name: "${esphome_name} ESPhome ver"
  # Expose WiFi information as sensors.
  - platform: wifi_info
    ip_address:
      name: "${esphome_name} IP"
    ssid:
      name: "${esphome_name} SSID"

sensor:
  # General
  - platform: uptime
    name: "${esphome_name} - Uptime Sensor"
  - platform: wifi_signal
    name: "${esphome_name} - WiFi Signal"
    update_interval: 60s

You need to replace the Xs above with a good encryption key, a good ota password, the ssid for you wifi and the password for your wifi. You will also use this same configuration in the HA integrated version of esphome. When using the stand alone version of ESPhome you will flash the board via the com port. This is the USB TTL adaptor simlar to this as it gives access to 5V and 3.3v. Once you’ve done the initial flashing you can then update the esp program via wifi from the integrated version of esphome.

This picture shows the USB to TTL adaptor connected to the board. I set the board in the orientation showed in the picture to help ensure I had a good connection on the pins. You need to connect 5V, ground, transmit data and receive data from the TTL adaptor to the ESP board. Transmit data from the TTL board goes to Receive data on the ESP board. These 4 pins are clearly labeled on the bottom of the board.

With the TTL board plugged into your windows machine and connected to the esp board you flash it running something like the following in a windows command prompt:

esphome run "Documents\touch_lamp.txt"

you need to update the file name touch_lamp.txt to the name of the file where you stored the initial configuration. The board will flash and then hang waiting for the board to reset. You have to tap the reset button (EN) for the board to boot the just installed program. When the board resets you will see the associated ESP log in your windows command prompt. Look at this output to find the IP address assigned to your board. Us the IP address in your web browser to access the web server provided by the ESP board. Assuming this all works then the board should be discovered by HA. I reset HA to kickoff the discovery action.

At this point you can fire up esphome within HA. You add a device with the same configuration above. Then try and install that configuration over wifi. Assuming that works you can then install a more complete version of the esp code via HA’s esphome interface. The code here is the code I originally used on my lamp, obviously your device name will be different. In a post below I’ve provided alternative code that looks to address the fact that the range of touch values seem to change over time. I’m now testing that new code and it appears to work better in handling the changing touch events.

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

esp32:
  board: esp32dev
  #framework:
  #  type: arduino

# Enable logging
logger:

# 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: "Touch Readings"
    id: "touch_readings"
    update_interval: 10s
    accuracy_decimals: 0
    lambda: |-
      return ((uint32_t) id(touch_sensor)->get_value());

esp32_touch:
  setup_mode: True
  iir_filter: 10ms
  low_voltage_reference: 0.5V
  high_voltage_reference: 2.7V
  voltage_attenuation: 1.5V

binary_sensor:
  - platform: esp32_touch
    name: "esp32 touch sensor"
    id: touch_sensor
    #pin: GPIO32
    pin: GPIO27
    threshold: 900
    filters:
      # Small filter, to debounce the spurious events.
      - delayed_on: 10ms
      - delayed_off: 10ms
    #on_press:
    on_click:
    - min_length: 10ms
      max_length: 500ms
      # Short touch to turn light on and off
      then:
        if:
          # test to ignore random event on boot
          condition:
            - lambda: 'return  id(time_since_boot).raw_state > 10;'
          then:
            switch.turn_off: power
    - min_length: 500ms
      max_length: 10s
      # longer touch to change brightness
      then:
        - button.press: cycle_brightness

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"

The esp32 web page should how look like this:

The configuration is set to print out the touch sensors value as you’ll need this information to set a threshold in the esp code appropriate for your lamp.

In HA this will look like this:

At this point it’s probably time to power the ESP board via the screw down power connectors on the board. I cut the end off a USB cable. Normally a USB cable contain 4 wires (some only have two power lines). The +5V DC is normally red and -DC is normally black. You connect these two wires to the 5V and GND screw down connectors on the board. Now you can power your board from you USB power adaptor. Make sure the board boots by verifying you can access the ESP web page.

For 110v you need to connect the 3 stand cord to your socket. If you look at the socket the screw in the center is neutral, while the screws to the right and left are hot. The two hot lines from this cord get connected to the ESP board at the NO1 and NO2 screw down connectors. The neutral stand from the 3 stand cord should be connected to the neutral side of the wall plug cord you’re using. In the US the larger prong of the cord is neutral. The hot side of the wall plug cord needs to be connected with two short wires that then get connected to the two screw down pins labeled COM1 and COM2 on the ESP board.

At this point if you plug in the 110v line you should be able to turn on the light from the ESP web page.

To control the lamp via touch you need to solder a thin gauge wire to one of the ESP capable touch pins (GPIO). I believe you have 10 pins you could use. The confirmation file I’ve provided uses GPIO pin 27. On the bottom of the ESP board this pin is labeled as G27. The other end of this wire need to be attached to your lamp. I drilled a small whole in the base of the lamp, under where the cord goes into the lamp and attached the wire with a screw here.

I put the electronics in a box. I used a step bit to make different size holes for the four wires. For the smaller wires I tied knots in them so they wouldn’t be pulled from the board. For the three strand cord I put a tie wrap on it to keep it in place.

Here we have all of the parts connected:

The lamp shade back on with the box cover in place:

You need to configure values so the touch sensor works consistently. Hopefully you only have to select a threshold value. In the configuration I provided you should see this threshold line:

    threshold: 900

You pick the value by watching the log output from the ESP board. This shows typical output from my lamp’s esp log:

With out touching the lamp the value is above 900. When the lamp is touched it drops below 900. Thus I’ve selected 900 as my value. I had some problems where the touch would work while sitting on the couch, but if I was standing the touch didn’t work. I was going to look at the potential of trying to use an adaptive threshold. That’s why the esp code includes the sensor “Touch Readings”. I found some other suggestion in this thread for changing other touch related parameters. Setting the values:

  low_voltage_reference: 0.5V
  high_voltage_reference: 2.7V
  voltage_attenuation: 1.5V

made it so the touch sensor worked both sitting and standing. Once you get a good touch threshold you need to change

  setup_mode: True

to

  setup_mode: False

You might also want to remove these lines as they send a message every 10 seconds to HA:

  - platform: template
    name: "Touch Readings"
    id: "touch_readings"
    update_interval: 10s
    accuracy_decimals: 0
    lambda: |-
      return ((uint32_t) id(touch_sensor)->get_value());

I would really have liked to use the M5 StampS3 with a separate dual relay board as it’s a smaller solution. Sadly I couldn’t get the touch sensor to work with this esp32s3. If anyone has any insight into how to get touch to work with this version of the esp32 I welcome the input.

It was suggested that I set the light to present a monochromatic light so that it would work as a dimmer slider in HA. I’ll be looking at that next.

2 Likes

That is amazing! The only issue I see is it’s not practical - still need the original thickness cord going into the light only and no extra cord, no electronics outside the lamp - otherwise the WAF goes into big negative territory!

The electronics have to go some where. The bottom of the base houses a weight taking up all of the space. If you can remove part of the weight you could probably put the electronics inside the lamp and then the only difference in the lamp would be it has two cords. If I were to put the electronics under the base I would use an esp separated from the relays. To get a the smallest electronics package I was hoping to use this esp32-s3 esp32-s3 with the smallest relay board I could find. The only problem is I couldn’t figure out how to get the touch pins to work on the M5 esp.

For me the WAF is a big issue that holds back a number of smart upgrades. With the box being on a cord and all the lamps being on end tables this configuration meet the WAF. The electronics box sits under the table or behind other furniture so it’s not visible. With the box being on the cord it’s not something else to move during cleaning time.

The cord could be made slight smaller by simple using three independent stands of 18 gauge wire with a wrap around it like I’ve shown in the picture. The pre-packaged wire in the picture includes three stands of threads and paper to make the wire round and less likely to stretch, it’s designed to be used with power tools. To get the three stands up through the lamp I actually had to pull off the outer insulation and strip the wire down to just the three stands. My WAF is very high, thus the reason I’ve put the time into this. At this point I’ve received no push back. Buying the separate stands is what I plan to do on the next lamp I modify.

1 Like

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

I’ve been working on getting things smaller for you. Here are the items I put into this lamp.

  • M5Stamp ESP32S3 Module - I used this one but would suggest using this alternative with pins. The one I used does not come with pins and if you’re not an experts with the soldering iron it’s near impossible to get things connected.
  • SunFounder 2 Channel DC 5V Relay - This is the smallest I could find. If anyone has seen something smaller, please let me know.
  • A small box. Wish I had a 3D printer to make something just right. I had to cut out some plastic that reduced the space in the box. As you can see below this is a tight fit.
  • 18 Guage 3 conductor wire - After ordering this I found 25ft at Lowes for $11.

Two wire connectors. I couldn’t get two of the new ones I listed in the first post inside the box with everything. Fortunately I had an alternative that was slightly narrower to make everything fit.

Here’s the open box loaded:

Here’s everything buttoned up:

And here it sits in it’s normal place:

There was sensitivity with the touch sensor with this lamp. I’m wondering if the issue is my shoddy connections, because there are no pins on the esp32. I order a few of the second M5Stamp with the pins to see if it works better, like it did in the standard bulb lamp I built. To deal with the sensitivity I had to use the code I worked out earlier. Here is the code I used in lamp:

esphome:
  name: "master-bed-touch-lamp"
  friendly_name: "master-bed-touch-lamp"

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino

# 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: "Master-Touch-Lamp"
    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( !touch_cnt && (cur_val > (prev_val + 25000))){
        ESP_LOGI("custom", "initial touch, value increased from %d to %d", prev_val, cur_val);
        touch_cnt = 1;
      }
      else if( touch_cnt > 0 ){
        if( cur_val < (prev_val - 25000)){
          ESP_LOGI("custom", "done touch %d, value decreased 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;
          // force skipping a couple after we handle a touch
          start_up_delay = 2;
          first_time = 1;
        }
        else
        {
          if( touch_cnt < 30){
            touch_cnt += 1;
            ESP_LOGD("custom", "multi touch %d, value %d to %d", touch_cnt, prev_val, cur_val);
          }
          else {
            ESP_LOGD("custom", "Reset touch because count too high %d, value %d to %d", touch_cnt, prev_val, cur_val);
            touch_cnt = 0;
            // force skipping a couple after we handle a touch
            start_up_delay = 2;
            first_time = 1;
          }
        }
      }

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

esp32_touch:
  setup_mode: False
  measurement_duration: 0.25ms
#  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: GPIO5
    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: GPIO7
    id: low_level
    inverted: true
    internal: True

  - platform: gpio
    #name: "high_level_filament"
    pin: GPIO9
    id: high_level
    inverted: true
    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

Hopefully this packaging will address the WAF. This is my third post in a row on this thread so I will not be able to post again unless someone else response on the thread. Once I get the other M5SampS3 controllers I’ll provide an update.

1 Like

This is amazing! In my honest optinion this is the only way to do it, unless there was more minutuarization (which is probably impossible) to have all of the extra leads and cicuitry inside the lamp (including the USB dongle). If the lamp is not metal that’s still not an issue because the light socket part could always be metal.

My final implementation of the 3 way lamp utilized a M5StampS3 ESP32 processor, a project box, 18 gauge wire and the SunFounder relay. To get touch to work I had to learned a lot more than I had anticipated. Utilizing the touch pins in the manner I’m using them here isn’t really how they are intended to be used. This approach is subjected to a lot of interference. Things that have a major impact on performance is the length of the USB power cable, the length of the wire from the the touch pin to the lamp, the USB power adapter and the size of the metal surface area being used to detect touch. The USB power cable can sometime end up acting as part of the touch surface. If you power the ESP from a multi port power adapter, touching thing plug into other ports can end up triggering a touch event. While I was successful in utilizing the standard ESP touch threshold detection capability in the simple touch lamp I built, I failed to get that method to work in my three way touch lamps.

I attached the M5StampS3 to the top of the 2x2 box via a longer screw, replacing the one that holds down the the orange top on the ESP board:

This shows the ESP to relay pin connections. The final wire off the ESP is the touch wire:

To expose more of the area of the pins I removed the plastic peace that comes installed on the pins.

This is everything in the case. I decided to use these Wago connectors because they are thinner and can sit under the lid with the ESP:

As mentioned above the standard threshold detection didn’t work for me. One of the main reason was when the light was on the reported non touch level was significantly different then when the light was off. As a result I used the code below that resets the idle level after each touch event. This way it can handle the value variance that happens as a result of the on/off state of the light.

esphome:
  name: ms-side-touch-lamp
  friendly_name: ms-side-touch-lamp

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino

# 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: "Master-Touch-Lamp"
    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 
    update_interval: 250ms  # a quarter of a second between check of touch 
    accuracy_decimals: 0
    lambda: |-
      static const uint16_t trig_val = 25000;
      // static const uint16_t trig_val = 40000;
      static uint32_t prev_delta;
      static uint32_t prev_val;
      static uint16_t touch_cnt; 
      static uint16_t first_time = 1;
      // static uint16_t start_up_delay = 30; // about 10 seconds - 30*333ms
      static uint16_t start_up_delay = 40; // about 10 seconds - 40*250ms
      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", "Base level to detect new touch is: %d", cur_val);
          first_time = 0;
          touch_cnt = 0;
          prev_delta = 0;
        }
        else
        {
          ESP_LOGI("custom", "Establishing touch base level, cur is: %d prev_delta is: %d", cur_val, prev_delta);
        }
      }
      else if( !touch_cnt && 
                ((cur_val > (prev_val + trig_val)) || ((cur_val + prev_delta) > (prev_val + trig_val)) ) ){
        ESP_LOGI("custom", "Start touch, value increased prev = %d, cur = %d, prev_delta = %d", prev_val, cur_val,prev_delta);
        touch_cnt = 1;
      }
      else if( touch_cnt > 0 ){
        if( (cur_val < (prev_val - trig_val)) || ( (cur_val + prev_delta) < (prev_val - trig_val)) ){
          ESP_LOGI("custom", "End touch cnt = %d, value decreased prev_val = %d, cur_val = %d, prev_delta= %d", touch_cnt, prev_val, cur_val, prev_delta);
          if( touch_cnt > 10)
          {
            ESP_LOGI("custom", "ignore touchi because count to high");
          }
          else if (touch_cnt > 2 && !(id(touch_disabled).state))
          {
            ESP_LOGI("custom", "change brightness with touch cnt %d",touch_cnt);
            id(brightness_step).press();
          }
          else if( !(id(touch_disabled).state) )
          {
            ESP_LOGI("custom", "push power button with touch cnt %d",touch_cnt);
            id(power).toggle();
          }
          touch_cnt = 0;
          // force skipping a couple after we handle a touch
          start_up_delay = 2;
          first_time = 1;
        }
        else
        {
          if( touch_cnt < 30){
            touch_cnt += 1;
            ESP_LOGD("custom", "multi touch %d, value %d to %d", touch_cnt, prev_val, cur_val);
          }
          else {
            ESP_LOGI("custom", "Reset touch because count too high %d, value %d to %d", touch_cnt, prev_val, cur_val);
            touch_cnt = 0;
            // force skipping a couple after we handle a touch
            start_up_delay = 2;
            first_time = 1;
          }
        }
      }

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

esp32_touch:
  setup_mode: False
  #measurement_duration: 0.35ms
  measurement_duration: .5ms
  #  voltage_attenuation: 1V

binary_sensor:
  - platform: esp32_touch
#    name: "esp32 touch sensor"
    id: touch_sensor
    pin: GPIO5
    threshold: 10  # a value I never expect the thing to cross
    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: GPIO7
    id: low_level
    inverted: true
    internal: True

  - platform: gpio
    #name: "high_level_filament"
    pin: GPIO9
    id: high_level
    inverted: true
    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: 66%    
      - lambda: |-
          ESP_LOGI("custom", "Turn on power action");
      #- switch.turn_on: 
    turn_off_action:
      - light.turn_off:
          id: lamp_ctrl
      - lambda: |-
          ESP_LOGI("custom", "Turn off power action");

  - platform: template
    name: "touch disabled"
    id: touch_disabled
    restore_mode: ALWAYS_OFF
    optimistic: true

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

You might need to play with a few items to get things working. The variable trig_val represents the amount of change from the base value the reported touch level needs to vary to be considered the start of a touch. Above you’ll see it is set to 25000. Originally I had a 6 foot USB power cord connected to the M5StampS3 and the value was set to 40,000. I then switched to a 10 inch USB cord with a different power adapter and things no longer worked. I needed to drop trig_val down to 25,000 to get things working again.

You can also see above that I have measurement_duration set .5ms. I did at one point have it set to .24ms and .35ms. The bigger this number the larger the reported touch values. I found the .5ms gave a good range of touch sensor values.

To have the system spit out a constant stream of touch sensor values, so you can figure how to set the values mentioned above, you changes setup_mode, under the esp32_touch section, to True and change Logging from INFO to DEBUG.

In the above code I also added a switch that can be used to disable the touch capability. This is so that if you want to move the lamps around and not worry about the lamp changing between on and off you can toggle the touch_disabled switch. I use the automation below, that is kicked off by a sonoff T0 2G wall switch running esphome, to toggle disable_touch on our 4 touch lamps. Toggling disable_touch happens on a long touch event from the wall switch. The automation flashes the light closes to the switch so you get visible notification that the disable_touch switch state has been changed.

alias: Family room bottom switch toggle bs lamp
description: ""
trigger:
  - platform: state
    entity_id:
      - sensor.family_room_lamps_ctrl_bottom_pad_event
condition: []
action:
  - if:
      - condition: state
        entity_id: sensor.family_room_lamps_ctrl_bottom_pad_event
        state: short
    then:
      - service: light.toggle
        metadata: {}
        data: {}
        target:
          entity_id: light.bs_family_room_lamp
    else:
      - if:
          - condition: state
            entity_id: sensor.family_room_lamps_ctrl_bottom_pad_event
            state: medium
        then:
          - service: button.press
            metadata: {}
            data: {}
            target:
              entity_id: button.fr_lamp_touch_bs_brightness_step
        else:
          - if:
              - condition: state
                entity_id: sensor.family_room_lamps_ctrl_bottom_pad_event
                state: long
            then:
              - service: light.toggle
                target:
                  entity_id: light.bs_family_room_lamp
                data: {}
              - delay:
                  hours: 0
                  minutes: 0
                  seconds: 1
                  milliseconds: 0
              - service: light.toggle
                target:
                  entity_id: light.bs_family_room_lamp
                data: {}
              - delay:
                  hours: 0
                  minutes: 0
                  seconds: 1
                  milliseconds: 0
              - service: light.toggle
                target:
                  entity_id: light.bs_family_room_lamp
                data: {}
              - delay:
                  hours: 0
                  minutes: 0
                  seconds: 1
                  milliseconds: 0
              - service: light.toggle
                target:
                  entity_id: light.bs_family_room_lamp
                data: {}
              - service: switch.toggle
                target:
                  entity_id: switch.fr_lamp_touch_bs_touch_disabled
                data: {}
              - service: switch.toggle
                metadata: {}
                data: {}
                target:
                  entity_id: switch.master_bed_touch_lamp_touch_disabled
              - service: switch.toggle
                metadata: {}
                data: {}
                target:
                  entity_id: switch.lamp_computer_room_touch_disabled
              - service: switch.toggle
                metadata: {}
                data: {}
                target:
                  entity_id: switch.ms_side_touch_lamp_touch_disabled
mode: single

In this last lamp I did connect the touch wire directly to the metal pole that runs through the lamp. This worked fine. In the event you’re not using a metal lamp then by connecting to the metal pole in the lamp you would be able to use the metal lamp socket as your touch pad.

I’ve also discovered that things like starting a high-power vacuum cleaner have the potential to cause a touch event. I believe I’ve successfully tuned most of these external inference items out, but only time will tell for sure.

This is imporessive. Are you still stuck with the two separate cables coming out of the lamp and the extra hardware outside of the lamp?

So the lamps I’ve converted don’t really have the room to place the electronics within the lamp. As such I’m still using the 2x2x1 inch box inline with the power cord. On this last lamp I have the cord and touch wire going together into the lamp. They are wrapped together to make them look like one cord.

The box sits under the desk and isn’t really exposed. I do believe with a bigger lamp you could probably hide the electronic within the lamp.

1 Like