GitHub

ESPHome (LibreTiny) for the Sonoff S61

esphome.yaml is the ESPHome the conversion installs on an S61, built on LibreTiny for its BK7238. It is the S60’s config (devices/sonoff-s60/esphome.yaml) with the S61’s wiring and storage: the same entities, actions, calibration handling and energy totals, so the two plugs look alike in Home Assistant.

How it differs from the S60’s

Nothing in the S61 route needs a LibreTiny or ESPHome change: ESPHome 2026.10.0 pins LibreTiny 1.13.0, which has the BK7238.

Adopted into Home Assistant

Home Assistant’s ESPHome builder can adopt a converted plug. It uses this config, at the release the plug’s firmware came from, and your plug keeps that config until you choose a newer release: how it works, how to take a fix and how to customise it.

Building

You build it as you would the S60’s image: in ESPHome’s own Docker image, with the config alone in a directory. The first build fetches LibreTiny, its Beken SDK and the toolchain.

mkdir build && cp devices/sonoff-s61/esphome.yaml build/ && cd build
docker run --rm -u "$(id -u):$(id -g)" -e HOME=/config -v "$PWD":/config \
    ghcr.io/esphome/esphome:2026.10.0 compile esphome.yaml

The outputs, in .esphome/build/esphome/.pioenvs/esphome/:

Reverting to stock, and to OpenBeken

A plug on ESPHome takes updates through ESPHome’s OTA as UF2 files, and LibreTiny writes a UF2’s download part to the download slot. So a UF2 that carries the stock app RBL (rebuilt from a dump with ../stock_app.py and the SDK packer, as the OpenBeken README says) as its download part, sent through the ordinary ESPHome OTA, is a reversion: the stock bootloader applies it on the reboot that follows. ../uf2.py writes one (uf2.write(rbl, name="sonoff-stock", version="1.0.2")), byte for byte what ltchiptool (LibreTiny’s tool) writes for the S61’s board:

ltchiptool uf2 write -o stock_revert.uf2 -b board.json -L 1.13.0 \
    -F "sonoff-stock:1.0.2" --legacy "stock_app_1.0.2.rbl=device:download"

The same with OpenBeken’s 4 MB-table .rbl moves the plug to OpenBeken, and OpenBeken’s ordinary ota_http <url of the ESPHome .rbl> brings it back: its 4 MB-table build downloads into the same slot. The stock EasyFlash store is never touched, so whichever firmware comes up reads its Wi-Fi, calibration and wiring from it on first boot - OpenBeken through its driver, ESPHome through its boot lambdas - and nothing is typed or copied across. Each firmware’s own settings beyond those (names, thresholds, the relay’s last state) are its own and start afresh; ESPHome’s live in the 32 KB at 0x3ee000, OpenBeken’s in the sector at 0x3e1000, and neither touches the other’s.

Validated in the emulator

All in the BekenEmulator fork on a 4 MB flash shaped like the project owner’s plug (stock firmware, provisioned with a test network), with no bypass of any check the plug makes:

  1. The conversion, the way the tool drives it: the plug on the local eWeLink stub is pushed an upgrade command; it fetches the FOTA-wrapped .rbl in ranged requests and stages it verbatim at 0x286000.
  2. The apply and first boot: booted from that flash, the stock bootloader logs upgrade(1.0.2->26.10.29-bk7238), decompresses (_STREAM_END, the raw size matching the build), erases and writes app, then go os_addr(0x10000); LibreTiny 1.13.0 starts, ESPHome logs Calibration: stock store and Wi-Fi from the stock store: 'MyTestNet' (11-char password), completes the four-way handshake, gets a DHCP lease, announces itself over mDNS as esphome-xxxxxx, asks for NTP, and answers through the emulator’s port forwards: the REST the tool reads (/switch/Relay ON, /text_sensor/Calibration stock store, /sensor/WiFi Signal), and the native API, which lists every entity and both actions, reports the relay restored on, and answers autoflash_wifi with the stock network’s name and password.
  3. Reversion: the stock UF2 above sent through ESPHome’s OTA (on LibreTiny it listens on port 8892, not 3232; esphome upload and espota2 know): OTA successful, Rebooting safely, then the stock bootloader’s upgrade(26.10.29-bk7238->1.0.2), erase and write. The flash afterwards holds the stock app byte for byte over its length and the EasyFlash store with the same keys, Wi-Fi and calibration; loaded afresh it boots the stock firmware (SDK 3.0.70, EasyFlash V4.1.99 is initialize success). As with OpenBeken, the emulator does not run the applied app in the same process after a firmware-initiated reboot; the persisted flash is what counts.
  4. To OpenBeken and back: a UF2 carrying OpenBeken’s 4 MB-table .rbl through ESPHome’s OTA (OTA successful, upgrade(26.10.29-bk7238->s61v6)); the fresh boot imports the stock Wi-Fi, calibration and pin template and joins in 8 s; ota_http of the ESPHome .rbl served from the host (init OTA, startaddr 0x286000, upgrade(s61v6->26.10.29-bk7238)); and the fresh boot of that is ESPHome again, Calibration: stock store, joined, the API answering autoflash_wifi with the stock network.

One emulator change was needed, in the fork: LibreTiny keeps its FreeRTOS tick on hardware Timer 0 (the 26 MHz group, for a fine micros()), where OpenBeken and the stock firmware use Timer 3; the emulator raised only Timer 3, so under it LibreTiny’s tick never advanced and every delay waited forever. It now models the 26 MHz group too, each enabled channel firing at its own period - the period matters: OpenBeken’s BK7238 build feeds its interrupt watchdog from a 10 s channel of that group, and a first version that raised every channel on every tick ran that feed back to back and starved OpenBeken instead.

ESPHome 2026.10.0 built the current image, artifacts/esphome-sonoff-s61-1.2.0.rbl, with tools/repo/update_firmware.py esphome 2026.10.0 --plug sonoff-s61 (docs/updating-firmware.md).

This has not yet been done on a real plug. The tool’s route runs end to end in the emulator (its fault suite).