Skip to content

Sonoff S31

The S31 is the smart plug worth buying, because there is a CSE7766 metering chip inside it — real voltage, current and wattage measured on the mains, not a number derived from what the plug thinks the load ought to be drawing. Flashed with ESPHome it reports all of that to Home Assistant locally, keeps switching when the internet is down, and never talks to a vendor server again.

It is also the easiest Sonoff to flash. Two screws, the board slides out, and the programming connection is four labelled holes in a row rather than test points you have to hold a probe against.

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 S31, 2-pack — make sure it is the plain S31. The S31 Lite is the same shell with the metering chip left out, and no amount of config brings it back.
  • FT232RL USB-to-TTL serial adapter — it has a voltage jumper. Move it to 3.3 V before you plug anything in. 5 V on an ESP8285 is a dead plug.
  • 2.54 mm header pins — snap a four-pin strip off and solder it into the board. This kit is mostly perfboard, but the header strips in it are what you want here.
  • Female-to-female Dupont jumpers — four of them, from the adapter’s pins to the header you just soldered.
  • Soldering station, lead-free solder and liquid flux — four through-holes is a two-minute job, but it is a soldering job.
  • Precision screwdriver set — the case screws are small and recessed under the label.
  • Plastic pry tools — this is an automotive trim kit, but the nylon wedges in it are exactly right for splitting a plastic shell without scarring it. A guitar pick works too.

You can hold four jumper wires in the holes at an angle and flash it that way. People do. Solder the header instead — it takes two minutes, it frees both hands for the button-and-USB timing in step 4, and it means the second flash isn’t another wrestling match. Every flash after the first one is over WiFi anyway, so you only pay for this once per plug.

If you want metering without opening anything, the Shelly Plus 1PM does the same job with firmware that already speaks to Home Assistant. It is a wall-box device, not a plug — you’re trading a soldering iron for wiring behind a switch.

This is a mains device. Unplug it from the wall before you open it, and never plug it into mains while the serial adapter is attached — the adapter’s ground is your computer’s ground. Flash it on the bench, close the case, then plug it in. There is no step in this guide where the board is both open and live.

Wiring

Four connections, and the data pair crosses. That is the whole job.

Serial adapterS31 boardNote
3V33V3Jumper the adapter to 3.3 V first
TXRXCrossed
RXTXCrossed
GNDGND

Flashing

  1. Open it. Peel back the label on the end cap, remove the two screws under it, and slide the board out of the shell.

  2. Solder a four-pin header into the row of holes marked 3V3, RX, TX, GND, then wire the adapter to the table above.

  3. Hold the S31’s button down while you connect the adapter to USB. That is what puts the ESP8285 into flash mode — GPIO0 pulled low at boot. Keep holding for a second or two after power arrives.

  4. Flash sonoff-s31.yaml from ESPHome, over the serial port. Read the configuration section below first; there are three edits worth making before the first flash rather than after it.

  5. Confirm it appears in Home Assistant while it is still on the bench and still on USB power. The switch, the LED and the WiFi signal sensor all work on 3.3 V alone.

  6. Close the case before it goes anywhere near a socket. The metering sensors read zero until there is mains on the board, so that is the one thing you cannot verify until it is back together.

Configuration

Two substitutions at the top name the device. Change both so each plug is distinct:

substitutions:
device_name: "Dryer Plug"
device_id: "dryer-plug"

The rest comes from 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 and not a preference. The S31 carries an ESP8285, whose flash is inside the chip package and only speaks DOUT. Delete that line and the board boot-loops the moment the flash finishes.

What the config gives you

FunctionPinEntity
RelayGPIO12Switch — the outlet itself
LEDGPIO13Monochromatic light, inverted, PWM, with flicker and strobe effects
ButtonGPIO0Binary sensor, pulled up and inverted — also the flash-mode button
CSE7766RXVoltage, current and power sensors, each averaged over 5 s

Plus a WiFi signal sensor every five minutes, a connection status sensor, a restart switch, and SNTP time. The relay’s restore_mode is RESTORE_DEFAULT_ON, so after a power cut it comes back to whatever it was doing and defaults to on if it can’t tell. That is right for a fridge and wrong for a space heater — change it to RESTORE_DEFAULT_OFF for anything that gets hot.

Three edits to make before you flash

Silence the serial logger. The CSE7766 streams its readings in on RX, which is the ESP8266’s hardware UART — the same UART the logger writes to by default. They cannot share it. Add this at the top of the file:

logger:
baud_rate: 0

That only turns off logging over the serial pins. ESPHome’s log stream over WiFi, which is the one you actually read, is unaffected.

Pick one owner for GPIO13. The config declares it twice — once as the esp8266_pwm output behind the light, and again in the status_led: block at the bottom. Two components cannot own one pin. Delete status_led:. The dimmable light is the more useful of the two, and connection health is already reported by the platform: status binary sensor a few lines above it.

Decide what the button does. As shipped, pressing the plug’s button toggles the LED, not the outlet. That is a fine debugging behaviour and a surprising one to hand somebody standing at a wall socket. To make it switch the load:

on_press:
then:
- switch.toggle: relay

Worth adding

There is no ap: block, so a mistyped WiFi password means opening the case again. Add a fallback access point under wifi: using the ap_password key that’s already in the shared secrets file, and the plug puts up a hotspot instead of going silent.

The api: block has no encryption key. secrets.yaml.example carries an api_key for exactly this — add encryption: with key: !secret api_key and the link between plug and Home Assistant stops being plaintext on your network.

Keep it lean beyond that. esp01_1m means 1 MB of flash, and an over-the-air update needs room for the new image beside the running one. web_server is usually the component that pushes an S31 past the point where it can update itself over WiFi, and getting back from there means the screwdriver again.

Energy dashboard

The config reports power, not energy — instantaneous watts, nothing cumulative. Home Assistant’s Energy dashboard wants kilowatt-hours, so it will not see this device as shipped. Give the CSE7766’s power sensor an id:, then add a total_daily_energy sensor pointing at that id with unit_of_measurement: kWh. That’s the sensor the Energy dashboard picks up, and it is roughly four lines.

Calibration

Out of the box the readings are close enough to tell you the dryer finished. They are not close enough to argue with a utility bill.

Plug in a load you know — a resistive one, a heater or an incandescent lamp, because anything with a switching supply has a power factor that muddies the comparison — and compare what the plug reports against what the load is rated for. If it’s off, the voltage, current and power sensors each take a calibrate_linear filter alongside the throttle_average already there. Two points, measured on your plug, not copied from anyone else’s.

Do it per plug. The CSE7766’s shunt tolerance is the dominant error, and it varies unit to unit.

Troubleshooting

The flash never starts. The button has to be held as USB power arrives, not after it. Unplug the adapter from the computer, hold the button, plug it back in, keep holding for two more seconds. If it still fails, check the adapter is on 3.3 V and that TX and RX are crossed.

It flashes but the power readings are zero. Two candidates. Either it is an S31 Lite, which has no CSE7766 and never will report anything — the relay and button will work perfectly, which is what makes this confusing — or the plug is running on the serial adapter’s 3.3 V with nothing in the socket. The metering chip measures mains. No mains, no numbers.

Readings appear but the logs are garbage, or the sensors update erratically. That’s the logger and the CSE7766 fighting over the UART. Set logger: to baud_rate: 0.

It flashes, then won’t join WiFi. The antenna sits inside a plastic brick behind a mains transformer, which is about the worst place in the house for it. Check the signal at that socket with a phone before blaming the flash — and if the plug went quiet the moment it left the bench, the credentials are wrong and there is no fallback AP to catch it.

The LED does nothing, or fights itself. GPIO13 is claimed twice in the shipped config. See 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-s31.yaml 114 lines
sonoff-s31.yaml
# Sonoff S31 Smart Plug
# ────────────────────────────────────────────────────────────
# Sonoff S31 plug with real-time power monitoring via CSE7766
# power metering chip. Includes relay control, LED indicator,
# and physical button.
#
# Hardware:
# - Board: ESP8266 (esp01_1m)
# - Power Meter: CSE7766
# - Output: Relay on GPIO12
# - LED: GPIO13
# ────────────────────────────────────────────────────────────
substitutions:
device_name: "Sonoff S31"
device_id: "sonoff-s31"
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:
light.is_on: led
then:
- light.turn_off: led
else:
- light.turn_on: led
name: "${device_name} button"
- platform: status
name: "${device_name} Status"
uart:
rx_pin: RX
baud_rate: 4800
parity: EVEN
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: cse7766
current:
name: "${device_name} Outlet Current"
filters:
- throttle_average: 5s
voltage:
name: "${device_name} Outlet Voltage"
filters:
- throttle_average: 5s
power:
name: "${device_name} Outlet Power"
filters:
- throttle_average: 5s
- 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
time:
- platform: sntp
id: sntp_time
servers:
- 0.pool.ntp.org
- 1.pool.ntp.org
- 2.pool.ntp.org

View on GitHub · Download raw

Every file for this project on GitHub →