Sonoff S20
The S20 is a relay, a button and an LED in a plastic shell. No metering chip, no sensors, nothing clever — which is exactly why it is the right first ESPHome flash. Four pads, a forty-line config, and a device that stops phoning home the moment it reboots.
If you want to know what the thing plugged into it is actually drawing, build the Sonoff S31 instead. Same flashing procedure, same GPIO map, real metering inside.
Parts
Some links below are Amazon affiliate links. As an Amazon Associate I earn from qualifying purchases — at no extra cost to you. It helps keep these guides free. Outside the US, they should send you to your own Amazon store; if one doesn't, searching the part number there will find it.
- Sonoff S20 smart plug — check the socket type matches your country before you order
- USB-to-serial adapter, FT232RL — move the voltage jumper to 3.3 V before you plug anything in. At 5 V you will kill the ESP8266 on the first connection
- Dupont jumper wires, all three genders — you want the female-to-female ones
- Pin header strip, 2.54 mm — snap four pins off a 40-pin strip. This kit is sold for its perfboard; the headers are what you’re after here
- Precision screwdriver set — the case screw is small and recessed
- Soldering iron and lead-free solder — four joints, once
Two optional things that make the job nicer:
- Spring-loaded test probes — the no-solder route. These are bare pogo pins, not a finished jig: you drill four holes in a scrap of acrylic or a printed block, press them in, and hold the whole thing against the pads. Worth building once if you plan to flash more than two of these
- Plastic pry tools — sold as a car trim kit, but the wide nylon wedges are the right shape for the case clips and won’t leave screwdriver scars in the shell
The S20 is not in production any more, so what you’ll find is old stock and clones. If you’re buying new today, buy the S31 — the two-pack is the one worth having, because it adds a CSE7766 metering chip for a couple of dollars more. It is a different device with a different config; the build is on the S31 page.
Unplug it from the wall before you open it, and never connect the serial adapter while the plug is in a socket. The board is mains-referenced in normal use. Flash it on the bench, close it up, then plug it in.
Wiring
The four pads on the board are 3V3, RX, TX and GND. On most revisions
they are bare holes with no header soldered in — that is the four-pin strip in
the parts list.
| Adapter | S20 pad | Note |
|---|---|---|
| 3V3 | 3V3 | Jumper on 3.3 V. Do not use the adapter’s 5 V pin |
| GND | GND | |
| TX | RX | Crossed |
| RX | TX | Crossed |
Crossed data lines are the thing people get wrong. The adapter transmits into the board’s receiver, not into its transmitter.
Flashing
-
Peel the label off the back and undo the screw underneath. Work the shell apart at the seam with a nylon wedge — the rest of it is clips.
-
Solder a four-pin header into the programming pads, or press your pogo-pin jig against them. If you’re holding jumper wires in the holes by hand, stop: one twitch mid-flash and you’re starting over.
-
Wire the adapter to the table above with the USB end unplugged.
-
Hold the button down, then plug the adapter into USB. The button is on GPIO0, and pulling GPIO0 low as power arrives is what selects flash mode. Keep holding for a second after. Nothing on the plug will tell you it worked — ESPHome failing to find a device is how you find out it didn’t.
-
Flash the config, then unplug, reassemble, and plug the closed plug into a socket. Every flash after this one is over WiFi.
Configuration
The config is esphome/sonoff-s20.yaml in the download section. Change the two
substitutions at the top so each plug gets its own name:
substitutions: device_name: "Fish Tank" device_id: "fish-tank"It reads three keys out of secrets.yaml — wifi_ssid, wifi_password and
ota_password. There is a secrets.yaml.example in the repo with every key
these configs use.
board_flash_mode: dout is not optional on Sonoff hardware. Leave it where it
is; a device flashed in the wrong mode boot-loops and has to be done again over
serial, which means opening the case a second time.
There is no ap: block and no encryption on the API. If you want the recovery
access point — the one a device puts up when it can’t reach your WiFi — add an
ap: under wifi: with ap_password. If you want the Home Assistant link
encrypted, add encryption: key: !secret api_key under api:. Both keys are
already in the secrets example.
What you get in Home Assistant
The board is an esp01_1m — 1 MB of flash, half of which has to stay free for
an OTA image to land in. That is the whole reason this config is short.
| Function | GPIO | Notes |
|---|---|---|
| Relay | GPIO12 | The switch entity |
| LED | GPIO13 | Inverted, driven as a PWM output |
| Button | GPIO0 | Inverted, INPUT_PULLUP; also the flash-mode button |
ESPHome devices are discovered, so there is nothing to write on the Home Assistant side. You get the relay as a switch, the LED as a dimmable light with flicker and strobe effects, the physical button as a binary sensor, a connectivity sensor, a WiFi signal reading every 300 seconds, and a restart switch.
The button toggles the relay on the device itself, not through an automation, so the plug still works with Home Assistant switched off. That is most of the reason to run it locally.
Two edits worth making
GPIO13 is claimed twice. It is the PWM output behind the led light, and it
is claimed again at the bottom of the file by status_led. They are fighting
over the same LED. Keep status_led while you’re bringing the plug up and the
LED reports WiFi and API state, which is what you want on the bench. Delete the
status_led: block once it’s on the wall and the LED becomes yours to drive from
an automation.
The LED only follows the relay when you press the button. The button’s
on_press toggles the relay and the light together; switching the relay from
Home Assistant doesn’t touch the light. If you want them locked together, put
light.turn_on / light.turn_off actions on the relay’s own on_turn_on and
on_turn_off and delete them from the button.
restore_mode: RESTORE_DEFAULT_ON means the plug comes back on after a power
cut if it has never stored a state. That is right for a fridge and wrong for a
space heater. Change it to RESTORE_DEFAULT_OFF on anything you would not want
to switch itself on while you’re away.
Troubleshooting
ESPHome never finds the device. The button has to be held as power arrives, not after. Unplug USB, hold the button, plug back in, and keep holding. If that doesn’t do it, check TX and RX — straight-through is the other common mistake.
It flashes, then never joins WiFi. 2.4 GHz only; the ESP8266 cannot see a 5 GHz network. If your router publishes both bands under one SSID, that is usually the cause. The S20’s antenna is also inside a plastic brick sitting behind a mains transformer, so a marginal corner of the house is a real possibility — check the WiFi signal sensor before blaming the flash.
The relay clicks but the socket is dead. That is a mains-side fault, not a firmware one. The firmware’s job ends at the relay coil. Unplug it and check your work before anything else goes into that socket.
The LED does something you didn’t ask for. See the status_led note above.
Files & downloads
ESPHome configuration
Copy this into your ESPHome directory and adjust the substitutions at the top.
Secrets are referenced by name — see secrets.yaml.example
.
sonoff-s20.yaml
# Sonoff S20 Smart Plug# ────────────────────────────────────────────────────────────# Basic Sonoff S20 smart plug without power monitoring.# Relay control, LED indicator, and physical button.## Hardware:# - Board: ESP8266 (esp01_1m)# - Output: Relay on GPIO12# - LED: GPIO13# ────────────────────────────────────────────────────────────
substitutions: device_name: "Sonoff S20" device_id: "sonoff-s20"
esphome: name: ${device_id}
esp8266: board: esp01_1m board_flash_mode: dout
wifi: ssid: !secret wifi_ssid password: !secret wifi_password
ota: password: !secret ota_password platform: esphome
api:
binary_sensor: - platform: gpio pin: number: GPIO0 mode: INPUT_PULLUP inverted: true on_press: then: - if: condition: switch.is_on: relay then: - switch.turn_off: relay - light.turn_off: led else: - switch.turn_on: relay - light.turn_on: led name: "${device_name} button" - platform: status name: "${device_name} Status"
light: - platform: monochromatic id: led name: "${device_name} LED" output: output_pin effects: - flicker: - strobe:
output: - platform: esp8266_pwm id: output_pin pin: number: GPIO13 inverted: true
sensor: - platform: wifi_signal name: "${device_name} WiFi Signal" update_interval: 300s
switch: - platform: gpio id: relay name: "${device_name} relay" pin: GPIO12 restore_mode: RESTORE_DEFAULT_ON - platform: restart name: "${device_name} Restart"
status_led: pin: number: GPIO13 inverted: true