Updating the firmware the tool installs
A converted plug ends on official Tasmota, OpenBeken or this project’s
ESPHome. Tasmota and ESPHome follow their projects’ releases, a little
behind: the repository’s firmware-update workflow takes each once it has
settled, tests it and releases it, by itself (below). Doing it
by hand takes the same steps (after that).
What follows a release, and what does not
| Firmware | On the plug | Follows | Pinned in |
|---|---|---|---|
Official Tasmota: tasmota32c3.bin, tasmota32c3-safeboot.bin (and tasmota32c3.factory.bin, for its partition table) |
for good | every Tasmota release | firmware.json |
ESPHome for the S60 (artifacts/esphome-sonoff-s60-imprintable-*.bin) |
for good | every ESPHome release | firmware.json |
ESPHome’s bootloader (artifacts/esphome-sonoff-s60-bootloader-*.bin) |
for good | only when it has to change | firmware.json |
The mover (artifacts/autoflash-mover-*.bin) |
a minute or two | only when its config changes | firmware.json |
OpenBeken for the S61 (artifacts/openbeken-sonoff-s61-*.rbl) |
for good | by hand: OpenBeken has no releases to settle on | firmware.json |
ESPHome for the S61 (artifacts/esphome-sonoff-s61-*.rbl) |
for good | every ESPHome release, by hand so far | firmware.json |
| The Berry Tasmota, the recovery Safeboot, the settings template, the bridge | minutes | no | artifacts/README.md says where, and how each is changed |
A new pin reaches plugs already converted. Running the tool on a plug with an
earlier release of its firmware offers this one as an update, and the plug
takes it, so treat a pin bump as a change to plugs in use as well as to new
conversions (Changing firmware later).
A plug adopted into Home Assistant keeps to the release it names, and the tool
leaves it alone: firmware.json records when each published ESPHome image was
built (built, earlier_builds), and the tool updates only a plug that reports
one of those times.
The official Tasmota files are not kept in this repository: each run downloads
them from the release’s own page on GitHub and checks them against
firmware.json. The ESPHome builds are kept in it: the run writes the plug’s
Wi-Fi into a copy of each.
The interim firmware - everything a plug runs only part way through a
conversion - stays as it is. Each has been through every fault the suite puts
in its way, a change to one means all of that again, and none of it is left
on a plug. The Tasmota they are made from is INTERIM_TASMOTA_COMMIT in
tools/espressif/safeboot/safeboot_migration.py, which need not be the official release’s: the
recovery Safeboot saves the Wi-Fi in its release’s settings record, and the
official release takes that record and moves it on to its own layout, as
Tasmota does on any upgrade - as long as CFG_HOLDER is the same in both,
which the update checks. The S61’s interim firmware is its OpenBeken image
itself, with a marker sent after it, so it changes with every OpenBeken
update (below).
The bootloader is written in about a second, and a power cut in that
second leaves a plug that has to be opened - the only step of a conversion
that can. So a new ESPHome release brings a new app, and the published
bootloader stays, unless the new one fixes something an S60 needs (an ESP32-C3
with no secure boot or flash encryption); the bootloader of ESP-IDF 5.x starts
the apps of later 5.x releases. Replacing it is a decision of its own:
--new-bootloader, then the whole fault suite and real plugs.
By itself
Every Monday .github/workflows/firmware-update.yml asks which release of each
project has settled (python3 tools/repo/update_firmware.py due). A release has
settled when it is
- a stable release - never a beta, a pre-release or a development build;
- the newest of its series (Tasmota’s
16.0.x, ESPHome’s month,2026.10.x): no later point release; and - at least 7 days old, and either a newer series has begun or it is at least
14 days old (
SETTLE_MIN_DAYS,SETTLE_DAYS).
A fix for a bad release has come within days or not at all - Tasmota 14.4.0
was followed by 14.4.1 in four days, 15.0.0 by 15.0.1 in one, each .0
withdrawn - so this lets it come first. For Tasmota, a release every two months
or so, it means 14 quiet days; for ESPHome, a series a month, the month
before’s last point release once the new month’s .0 is out: always a month
behind, on a series that has had its fixes. A release listed under hold in
firmware.json ("hold": {"tasmota": ["16.1.0"]}) is never taken.
The workflow makes what is due into a pull request: the update (steps 1 and 2
below, its docs and changelog line written, the version raised a minor version),
then the unit tests and the final-firmware emulator test (step 4). Nothing is
pushed if the leak scan finds anything in it. The pull request says which
releases, what each check found, and the tests’ results; the workflow then
starts the lint, leak-scan and emulator-check workflows on its branch, as a
pull request opened with a workflow’s own token starts none itself, so the
checks main requires are there. One whose tests passed merges itself 7
days after it was opened, which is the release, unless it is labelled
hold; one whose tests failed, or a check main requires, is labelled
tests-failed and never merged by the workflow. A pull request is merged only
if main has not moved on since it was made and tested; otherwise it is
closed, and the next week’s run makes it again.
The same run checks the pinned Tasmota release is still published - each
conversion downloads its files - and opens an issue if it has gone, taking its
series’ newest release at once where there is one. It also opens one once
nothing has reached main for 50 days: GitHub switches a public repository’s
schedules off after 60 days with no activity, this one with them.
By hand, from the Actions tab: tasmota or esphome takes a given release
whether settled or not, and dry_run makes and tests the update and opens
nothing. python3 tools/repo/update_firmware.py check says what is pinned and what
is latest.
By hand
Work on a branch from main. Each step below is one commit.
1. Tasmota
python3 tools/repo/update_firmware.py tasmota # the latest release
python3 tools/repo/update_firmware.py tasmota 16.0.0 # or that one
It reads the release from GitHub (refusing a draft or pre-release) and its tag’s commit, downloads its three ESP32-C3 files, and checks:
- the factory image’s partition table is, byte for byte, the one the
conversion writes (
TARGET_TABLE_SHA256), and the Safeboot and the app in it are the release’s own files, at the offsets the conversion writes them to; - the Safeboot fits its partition and the app fits app0, each a valid ESP32-C3 image;
CFG_HOLDERin the release’smy_user_config.his the one the recovery Safeboot’s settings record has (tools/autoflash/tasmota_settings.py).
Then it writes the release, its commit and each file’s size and SHA-256 into
firmware.json. --dry-run checks without writing.
Where it stops:
- Another partition table: Tasmota has changed its layout. The repartition
(
TARGET_PARTITIONS, the Berry phases intools/espressif/safeboot/repartition/berry/) was reviewed for this one; a new layout is a new design, not an update. - Another
CFG_HOLDER: the release would start without the Wi-Fi the recovery Safeboot saved, and every conversion would end on its setup network. The recovery Safeboot would have to be rebuilt from that release (artifacts/README.md). - A file too large: the Safeboot or app no longer fits Tasmota’s own layout - which Tasmota would have changed first.
2. ESPHome
python3 tools/repo/update_firmware.py esphome # the latest release
python3 tools/repo/update_firmware.py esphome 2026.10.0 --image-version 1.3.0
A change to devices/sonoff-s60/esphome.yaml is published the same way, naming the
release already pinned, so that only the config changes.
It needs Docker: the image is built in ESPHome’s own image for that release
(ghcr.io/esphome/esphome:<release>), pinned by its digest, so the build does
not depend on what is installed on the computer building it. It checks, then
builds:
- the parts of ESPHome that
devices/sonoff-s60/esphome.yamlreaches into beyond its options - the fixed key its laston_bootlambda saves the Wi-Fi under, and the layout of what it saves (devices/sonoff-s60/esphome.md says why) - are as they were; if not, the lambda must be checked first; - the config’s
projectversion is raised (the next minor version, or--image-version), itspackage_import_urlis set to the release it will come out in, the top ofCHANGELOG.mdor the next after it, and it is built. A plug adopted into Home Assistant keeps to that release until its owner names a newer one, and the release workflow refuses a version whose configs name a release that is not out: rebuild them if the version changes first; - the app is an ESP32-C3 app that fits Sonoff’s slot, says it is that ESPHome release, holds each Wi-Fi placeholder once and untouched and takes a Wi-Fi written in, and holds no path from the computer that built it;
- its bootloader is compared with the published one. If they match, nothing changes. If they differ (a new ESP-IDF), the published one is kept, and the command says so - see above.
Then it publishes the image in artifacts/ (removing the one before), and
updates its line in SHA256SUMS, the config’s version and firmware.json.
--mover builds and publishes the mover the same way, only for a change to
its config. --dry-run builds and checks, and publishes nothing. ESP-IDF and
its tools are downloaded once, to captures/esphome-build/.
An ESPHome build is not byte for byte repeatable - it holds the time it was
built and where - so what pins a build is the Docker image it was made in, in
firmware.json. The bootloader is repeatable: built again with the release
that made the published one, it comes out the same.
3. OpenBeken and ESPHome for the S61
A Beken plug ends on OpenBeken or on ESPHome built on LibreTiny, each an
rt_ota image the plug’s stock bootloader applies from its download slot
(devices/sonoff-s61/); the images are
pinned under beken in firmware.json, by plug.
A change to what the OpenBeken driver does on first boot reaches only plugs converted after it: an updated plug keeps its settings, so the import does not run again. A fix that updated plugs need goes in what the driver does on every start (openbeken.md).
python3 tools/repo/update_firmware.py openbeken # the pinned OpenBeken commit
python3 tools/repo/update_firmware.py openbeken --commit <sha> --image-version 1.1.0
python3 tools/repo/update_firmware.py esphome 2026.10.0 --plug sonoff-s61
openbeken clones OpenBK7231T_App
at the commit pinned (or --commit; with none pinned, main’s head) and its
SDK, applies this project’s patches
(tools/beken/openbeken/: the 4 MB table,
offered upstream, and the hook before an OTA’s restart the driver uses),
adds this project’s OpenBeken driver
(tools/beken/openbeken/drv_autoflash.c and drv_autoflash.h) and the
plug’s table for it (devices/sonoff-s61/drv_autoflash_sonoff_s61.c) to
the build and to start-up as openbeken.md
says, and builds OpenBK7238 with FLASH_SIZE=4M in this project’s build
image (tools/beken/openbeken/build.Dockerfile, Ubuntu 24.04: the SDK’s own
toolchain wants a newer GLIBC than OpenBeken’s image has). It checks the
image is an rt_ota app of the version asked, fits the download slot, and
holds each command the driver registers; then it publishes it, recording the
OpenBeken and SDK commits, the hashes of the driver, the table and the
patches, and the build image. OpenBeken has no releases, so nothing takes it
by itself: update it by hand, on a commit that has been out a while. The version is this
project’s for the image (--image-version, the next minor by default), not
OpenBeken’s.
esphome --plug sonoff-s61 builds devices/sonoff-s61/esphome.yaml in
ESPHome’s image for the release, as the S60’s is built, and takes
image_bk7238_app.ota.rbl: checked as an rt_ota app that fits the slot, says
its version, project and ESPHome release, and has the Wi-Fi action in it,
with the build’s download partition where the stock bootloader looks. Its
first boot reads the plug’s Wi-Fi and calibration from the stock store, so
the image holds no placeholders and nothing is written into it. The config’s
version is raised and the image published as the S60’s is. The docs for
either are written by hand (below).
4. The docs
Each update writes what it changed, and tests/repo/test_firmware_pins.py fails
while any of it is missing:
artifacts/README.md: the image’s row (its name and SHA-256), the ESPHome release that builds it, and a paragraph on the new build;NOTICES.md: the image’s name, ESPHome release and ESP-IDF version;devices/sonoff-s60/esphome.md: the ESPHome release that built the current image;- for the S61’s images, the same by hand: their rows and paragraphs in
artifacts/README.md,NOTICES.md(the OpenBeken and SDK commits; the ESPHome release and LibreTiny version),devices/sonoff-s61/esphome.md; CHANGELOG.md: a line for each firmware, with a link to its release notes, under the release to come - a new section, a minor version on, once the one above has been released, withtools/autoflash/__init__.pyraised to it.
For a change to the config, built with the release already pinned, they say
the config changed, and what the change does is left to write by hand in
CHANGELOG.md and artifacts/README.md.
Each passage is found and rewritten, and if one is not found nothing is
changed: --no-docs leaves them all to be written by hand, as a change to
the mover or the bootloader (--mover, --new-bootloader) needs anyway. The
README’s badges read firmware.json on main, and change by themselves.
5. Test
Unit tests, in a checkout with no emulator state linked in:
python3 tests/run.py
The emulator, from just before the final firmware: nothing before it changes, and it was tested by the fault suite when it last did. Two public plugs (emulator.md) - one after E3 on the way to Tasmota, one after D5 on the way to ESPHome - are taken to their final firmware (F2 to H1, and I1 and I2), and the plugs that makes are then taken each to the other (L1 to I2, and X1 to H1): every step that writes, starts, checks or runs code on the final firmware, in four or five minutes on two emulators - one for each way, each going on to its second run as soon as its first is done. The plugs hold nothing of Sonoff’s, so no cloud is needed, real or stub, nor an account:
python -I tools/espressif/emulator/public_plugs.py test
All four must convert. The repository’s emulator-check workflow runs the
same, with the unit tests, on GitHub’s own runners, from the published public
plugs and emulator image, whenever the tool or its firmware changes. The faults are not put in their way: they test the
tool’s own recovery, which a new final firmware does not change. Where the
recovery leans on the firmware’s own behaviour, a major release (16 to 17,
say) or release notes touching its Wi-Fi, settings, updates or Berry, run
those few scenarios of the fault suite too - F4’s reset, G1, seven-times,
and the network gone at L1 and X1 (testing.md).
The S61’s OpenBeken, built anew - above all on a new OpenBeken commit -
is run on the S61’s emulator before it is published. This project’s driver
and its hook before an OTA’s restart lean on OpenBeken’s own workings: where
it keeps its settings and its Wi-Fi’s (0x3e1000, and its flash variables
at 0x1e5000), how its HTTP OTA ends in a restart. A commit can move either
with the build still passing and the image still holding each command, and
the interim firmware would then leave its settings behind for ESPHome or a
later OpenBeken to find. The starting plugs are made again with the new
image, then six scenarios run, each doing once something the driver or the
hook does - the Wi-Fi taken from Sonoff’s firmware, the interim firmware
named, fetching ESPHome and erasing itself, kept as OpenBeken, each way back
to Sonoff’s firmware - where a pass also checks the names the plug asked
DHCP for and, after ESPHome, that the interim’s settings are gone
(testing.md):
tools/beken/emulator/fault_suite.py prep
tools/beken/emulator/fault_suite.py list openbeken > openbeken.txt
rm -rf captures/emulation/s61-openbeken # no pass on the image before counts
AUTOFLASH_S61_SUITE_DIR=captures/emulation/s61-openbeken AUTOFLASH_S61_WORKERS=2 \
tools/beken/emulator/fault_suite.py batch openbeken.txt
All six must pass.
A real plug, converted from Sonoff to each firmware and between them, before the release: an emulator does not show real flash, radios or timings (testing.md).
6. Release
The update raised the version in tools/autoflash/__init__.py to the
changelog’s; merging to main publishes the release
(.github/workflows/release.yml), once the tests, lint and leak scan pass on
main again, with the firmware files attached. Runs left
part way by the release before carry on with the new firmware by themselves:
the bundle is built again for the new pins - at A5, or as a run carried on
past A5 starts - and a step whose checks were made with the old bundle does
them again. A run that had already installed the official app (F2) finishes on the
release it began with, using its bundle - or, if this version must build that
again (1.2.0’s, from another startup firmware), a bundle of the same release.
It reads the app back against that release’s files as usual, and says so: firmware.json keeps each
release pinned before (earlier) by its files, and update_firmware.py tasmota adds the one it replaces.