Skip to content

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.

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.

AdapterS20 padNote
3V33V3Jumper on 3.3 V. Do not use the adapter’s 5 V pin
GNDGND
TXRXCrossed
RXTXCrossed

Crossed data lines are the thing people get wrong. The adapter transmits into the board’s receiver, not into its transmitter.

Flashing

  1. 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.

  2. 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.

  3. Wire the adapter to the table above with the USB end unplugged.

  4. 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.

  5. 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.yamlwifi_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.

FunctionGPIONotes
RelayGPIO12The switch entity
LEDGPIO13Inverted, driven as a PWM output
ButtonGPIO0Inverted, 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 87 lines
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

View on GitHub · Download raw

Every file for this project on GitHub →