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.