Getting Started with ESPHome on Shelly Smart Switches
I’ve been on another home automation kick recently. I’d heard of Shelly devices, but I hadn’t gotten around to investigating them until recently. To dip my feet in, I bought a Shelly 1 Mini gen3 and a Shelly 2PM gen3.
The larger Shelly devices have 1.27mm pitch UART programming headers, whereas the “mini” series do not. I was hoping to go the OTA update route to install ESPHome anyway, so that shouldn’t be an issue! As I soon discovered however, the OTA upgrade process from Shelly stock to ESPHome is fraught with issues (as of 2025-03).
Shelly 1 Mini gen3
My search engine chose this page to be my guide for flashing ESPHome over-the-air onto a Shelly 1 Mini. PLEASE DO NOT FOLLOW THE INSTRUCTIONS PROVIDED THERE, AS THEY WILL SOFT-BRICK YOUR DEVICE. If only the poster updated their post to say that…
Be aware that this guide has issues until PR has come through in esphome
To be clear, the “issues” described are that devices cannot accept further OTA updates for ESPHome after initial flashing. Of course, UART flashing is always an option.
Even with the aforementioned PR, the update process still doesn’t appear to work properly. This comment shows errors from attempting an OTA while using unprotected_writes
option in ESPHome’s ota
configuration.
I will use this device someday, when I feel like soldering the tiny little pads.
Shelly 2PM gen3
Shortly after I made a Shelly 1 mini device temporarily unusable, I moved to investigating my larger Shelly device. This guide showed a nice, simple way to make a pitch adapter for the programming header. However, I had issues with my connections being strong enough. I ended up buying a pitch changer and some 1.27mm pins from digikey to make a nice stable adapter.
I struggled to find any ESPHome configs for the Shelly 2PM gen3 until I found this forum post, which provided a bare-bones config. NOTE THAT THIS CONFIG DOES NOT PROVIDE THERMAL LIMITING. Nevertheless, it’s a great starting point.
After playing around with a functional Shelly 2PM running ESPHome, I made two configs:
- One for normal operation - switch 0 toggles relay 0, switch 1 toggles relay 1. However I copied Shelly’s default of having the switch only toggling the relay to match the switch position, so it’s more like a switch than a button.
- Another for toggling two separate loads from the same physical switch (eg. a switch that controls a fan/light combo)
The latter was easily achievable with the on_multi_click function (see my config below!).
These devices are just fantastic. I soon decided that I wanted to get a Shelly Dimmer 2 as well.
Shelly Dimmer 2
There’s not much to note regarding differences flashing this compared to other Shelly devices, except for how awesome ESPHome is, specifically in how this firmware is flashed. As noted on its ESPHome page, the Dimmer 2 uses an STM32 co-processor for interactions with mains power. ESPHome automatically flashes it with custom firmware from ESPHome with the following configuration snippet:
light:
- platform: shelly_dimmer
name: Shelly Dimmer 2 Light
firmware:
version: "51.6"
update: true
...
The following config for the Dimmer 2 is largely taken from this forum post.
Configs!
Shelly 2 PM gen3 - normal operation
esphome:
name: DEVICE_NAME
friendly_name: FRIENDLY_NAME
esp32:
board: esp32-c3-devkitm-1
flash_size: 8MB
framework:
type: esp-idf
version: recommended
sdkconfig_options:
COMPILER_OPTIMIZATION_SIZE: y
substitutions:
max_temp: "70" # °C
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: "ENCRYPTION KEY"
ota:
- platform: esphome
password: "OTA PASSWORD"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "SSID NAME"
password: "AP PASSWORD"
captive_portal:
i2c:
sda: GPIO6
scl: GPIO7
sensor:
- platform: ade7953_i2c
irq_pin: GPIO1
voltage:
name: "Voltage"
id: voltage
icon: mdi:alpha-v-circle-outline
device_class: voltage
frequency:
name: "Frequency"
id: frequency
accuracy_decimals: 2
icon: mdi:cosine-wave
device_class: frequency
current_a:
name: "Current A"
id: currenta
icon: mdi:alpha-a-circle-outline
device_class: current
current_b:
name: "Current B"
id: currentb
icon: mdi:alpha-a-circle-outline
device_class: current
active_power_a:
name: "Power A"
id: powera
icon: mdi:power
device_class: power
filters:
- multiply: -1
active_power_b:
name: "Power b"
id: powerb
icon: mdi:power
device_class: power
filters:
- multiply: -1
update_interval: 5s
# NTC Temperature
- platform: ntc
sensor: temp_resistance_reading
name: "Temperature"
id: temperature
icon: "mdi:thermometer"
calibration:
b_constant: 3350
reference_resistance: 10kOhm
reference_temperature: 298.15K
on_value:
then:
- if:
condition:
- sensor.in_range:
id: temperature
above: ${max_temp}
then:
- switch.turn_off:
id: relay0
- logger.log: "Switch turned off because temperature exceeded ${max_temp}°C"
on_value_range:
- above: ${max_temp}
then:
- logger.log: "Temperature exceeded ${max_temp}°C"
- platform: resistance
id: temp_resistance_reading
sensor: temp_analog_reading
configuration: DOWNSTREAM
resistor: 32kOhm
- platform: adc
id: temp_analog_reading
pin: 4
status_led:
pin:
number: 2
inverted: true
output:
- platform: gpio
id: "relay_output0"
pin: 5
- platform: gpio
id: "relay_output1"
pin: 3
switch:
- platform: output
id: "relay0"
name: "Relay0"
output: "relay_output0"
- platform: output
id: "relay1"
name: "Relay1"
output: "relay_output1"
binary_sensor:
- platform: gpio
name: "Switch 0"
id: switch0
pin: 18
on_state:
then:
- lambda: |-
if (id(switch0).state) {
id(relay0).turn_on();
} else {
id(relay0).turn_off();
}
filters:
- delayed_on_off: 50ms
- platform: gpio
name: "Switch 1"
id: switch1
pin: 10
on_state:
then:
- lambda: |-
if (id(switch1).state) {
id(relay1).turn_on();
} else {
id(relay1).turn_off();
}
filters:
- delayed_on_off: 50ms
- platform: gpio
name: "Button"
pin:
number: 19
inverted: yes
mode:
input: true
pullup: true
Shelly 2PM gen3 - single switch multiple loads
esphome:
name: DEVICE_NAME
friendly_name: FRIENDLY_NAME
esp32:
board: esp32-c3-devkitm-1
flash_size: 8MB
framework:
type: esp-idf
version: recommended
sdkconfig_options:
COMPILER_OPTIMIZATION_SIZE: y
substitutions:
max_temp: "70" # °C
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: "ENCRYPTSSIDION KEY"
ota:
- platform: esphome
password: "OTA PASSWORD"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "SSID NAME"
password: "AP PASSWORD"
captive_portal:
i2c:
sda: GPIO6
scl: GPIO7
sensor:
- platform: ade7953_i2c
irq_pin: GPIO1
voltage:
name: "Voltage"
id: voltage
icon: mdi:alpha-v-circle-outline
device_class: voltage
frequency:
name: "Frequency"
id: frequency
accuracy_decimals: 2
icon: mdi:cosine-wave
device_class: frequency
current_a:
name: "Current A"
id: currenta
icon: mdi:alpha-a-circle-outline
device_class: current
current_b:
name: "Current B"
id: currentb
icon: mdi:alpha-a-circle-outline
device_class: current
active_power_a:
name: "Power A"
id: powera
icon: mdi:power
device_class: power
filters:
- multiply: -1
active_power_b:
name: "Power b"
id: powerb
icon: mdi:power
device_class: power
filters:
- multiply: -1
update_interval: 5s
# NTC Temperature
- platform: ntc
sensor: temp_resistance_reading
name: "Temperature"
id: temperature
icon: "mdi:thermometer"
calibration:
b_constant: 3350
reference_resistance: 10kOhm
reference_temperature: 298.15K
on_value:
then:
- if:
condition:
- sensor.in_range:
id: temperature
above: ${max_temp}
then:
- switch.turn_off:
id: relay0
- logger.log: "Switch turned off because temperature exceeded ${max_temp}°C"
on_value_range:
- above: ${max_temp}
then:
- logger.log: "Temperature exceeded ${max_temp}°C"
- platform: resistance
id: temp_resistance_reading
sensor: temp_analog_reading
configuration: DOWNSTREAM
resistor: 32kOhm
- platform: adc
id: temp_analog_reading
pin: 4
status_led:
pin:
number: 2
inverted: true
output:
- platform: gpio
id: "relay_output0"
pin: 5
- platform: gpio
id: "relay_output1"
pin: 3
switch:
- platform: output
id: "relay0"
name: "Relay0"
output: "relay_output0"
- platform: output
id: "relay1"
name: "Relay1"
output: "relay_output1"
binary_sensor:
- platform: gpio
name: "Switch 0"
id: switch0
pin: 18
on_multi_click:
# toggle fan on quick ON->OFF
- timing:
- ON for at most 100ms
- OFF for at least 1ms
then:
- switch.toggle: relay1
# toggle fan on quick OFF->ON
- timing:
- OFF for at most 100ms
- ON for at least 1ms
then:
- switch.toggle: relay1
# toggle light ON after 100ms
- timing:
- ON for at least 100ms
then:
- switch.turn_on: relay0
# toggle light OFF after 100ms
- timing:
- OFF for at least 100ms
then:
- switch.turn_off: relay0
filters:
- delayed_on_off: 50ms
- platform: gpio
name: "Button"
pin:
number: 19
inverted: yes
mode:
input: true
pullup: true
Shelly Dimmer 2
This one is still in progress, but here’s what I have for now:
esphome:
name: DEVICE_NAME
friendly_name: FRIENDLY_NAME
substitutions:
max_temp: "70" # °C
esp8266:
board: esp01_1m
# Enable logging
logger:
baud_rate: 0
# Enable Home Assistant API
api:
encryption:
key: "ENCRYPTION KEY"
ota:
platform: esphome
password: "OTA PASSWORD"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "SSID NAME"
password: "AP PASSWORD"
captive_portal:
uart:
tx_pin: 1
rx_pin: 3
baud_rate: 115200
light:
- platform: shelly_dimmer
name: ${light_name}
id: dimmer
leading_edge: false #choose between leading edge and trailing edge (use trailing edge for led dimming)
min_brightness: 470
max_brightness: 1000
restore_mode: ALWAYS_OFF
default_transition_length: 1s
gamma_correct: 0 #change this to your liking. Default value is 2.8, but 0 prevents some brightness pops for me
firmware:
version: "51.6"
update: true
sensor: #Important: don't change this sensor-part unless you know what you are doing. These sensors will shut the light down when overheating temperature is reached.
# NTC Temperature
- platform: ntc
sensor: temp_resistance_reading
name: Temperature
id: temperature
unit_of_measurement: "°C"
accuracy_decimals: 1
icon: "mdi:thermometer"
calibration:
b_constant: 3350
reference_resistance: 10kOhm
reference_temperature: 298.15K
on_value:
then:
- if:
condition:
- sensor.in_range:
id: temperature
above: ${max_temp}
- light.is_on: dimmer
then:
- light.turn_off:
id: dimmer
- logger.log: "Switch turned off because temperature exceeded ${max_temp}°C"
on_value_range:
- above: ${max_temp}
then:
- logger.log: "Temperature exceeded ${max_temp}°C"
- platform: resistance
id: temp_resistance_reading
sensor: temp_analog_reading
configuration: DOWNSTREAM
resistor: 32kOhm
- platform: adc
id: temp_analog_reading
pin: A0
switch:
- platform: restart
name: Reboot
script:
- id: script_dim_down_timer
mode: restart # script will be kept running for 5 seconds sinces the latest time the script is executed
then:
- logger.log: "Dim-down timer script started"
- delay: 5s
- logger.log: "Dim-down timer script finished"
- id: script_dim_up_timer
mode: restart # script will be kept running for 5 seconds sinces the latest time the script is executed
then:
- logger.log: "Dim-up timer script started"
- delay: 5s
- logger.log: "Dim-up timer script finished"
- id: script_dim_down
mode: single # script will run once
then:
- logger.log: "Dim-down script started"
- while:
condition:
and:
- script.is_running: script_dim_down_timer #makes sure that dimming will stop after the set period
- light.is_on: dimmer #prevents dimming of a light that is off
- lambda: 'return(id(dimmer).remote_values.get_brightness() >= 0.01);' #prevents the light from going off and prevents the script from running unnecessary long (it stops at 1% brightness)
then:
- light.dim_relative:
id: dimmer
relative_brightness: -0.5%
transition_length: 0.01s
- delay: 0.01s
- logger.log: "Dim-down script finished"
- id: script_dim_up
mode: single # script will run once
then:
- logger.log: "Dim-up script started"
- while:
condition:
and:
- script.is_running: script_dim_up_timer #makes sure that dimming will stop after the set period
- light.is_on: dimmer #prevents dimming of a light that is off
- lambda: 'return(id(dimmer).remote_values.get_brightness() <= 0.999);' #prevents the script from running unnecessary long (it stops at 100% brightness)
then:
- light.dim_relative:
id: dimmer
relative_brightness: 0.5%
transition_length: 0.01s
- delay: 0.01s
- logger.log: "Dim-up script finished"
- id: script_turn_on_off
mode: single
then:
- logger.log: "Turn_on_off script started"
- if:
condition:
light.is_on:
id: dimmer
then:
- light.turn_off:
id: dimmer
- logger.log: "Light turned off"
else:
- light.turn_on:
id: dimmer
brightness: !lambda |-
return id(dimmer).remote_values.get_brightness();
- logger.log: "Light turned on with previous brightness setting"
binary_sensor:
- platform: gpio
name: Dim Down
id: sensor_dim_down
pin:
number: GPIO12
mode: INPUT
internal: false
on_multi_click:
- timing:
- ON for at most 300ms
then:
- logger.log: "Physical short press (dim_down) trigger"
- script.execute: script_turn_on_off
- timing:
- ON for at least 300ms
then:
- logger.log: "Physical long press (dim_down) trigger"
- script.execute: script_dim_down_timer
- script.execute: script_dim_down
on_release:
then:
- if:
condition:
light.is_on:
id: dimmer
then:
- logger.log: "Physical dim_down release trigger"
- script.stop: script_dim_down_timer
- logger.log: "Script_dim_down_timer stopped"
- platform: gpio
name: Dim Up
id: sensor_dim_up
pin:
number: GPIO14
mode: INPUT
internal: false
on_multi_click:
- timing:
- ON for at most 300ms
then:
- logger.log: "Physical short press (dim_up) trigger"
- script.execute: script_turn_on_off
- timing:
- ON for at least 300ms
then:
- logger.log: "Physical long press (dim_up) trigger"
- script.execute: script_dim_up_timer
- script.execute: script_dim_up
on_release:
then:
- if:
condition:
light.is_on:
id: dimmer
then:
- logger.log: "Physical dim_up release trigger"
- script.stop: script_dim_up_timer
- logger.log: "Script_dim_up_timer stopped"
button:
- platform: template
name: Dim Down
on_press:
then:
- script.execute: script_dim_down_timer
- script.execute: script_dim_down
- platform: template
name: Dim Up
on_press:
then:
- script.execute: script_dim_up_timer
- script.execute: script_dim_up
- platform: template
name: Dim Stop
on_press:
then:
- logger.log: "Stopping timer script"
- script.stop: script_dim_down_timer
- script.stop: script_dim_up_timer