ESPHome 2026.9.0: Faster builds and encrypted updates
Every month ESPHome ships a release like this, so you can catch up on what’s new, what’s fixed, and anything worth knowing about before you update.
Short on time? Here’s the quick version
Section titled “Short on time? Here’s the quick version”Here’s the plain-language version for anyone short on time or newer to ESPHome – the full technical writeup continues below. Most setups won’t need any changes, but a few will.
- Modbus Controller users: If your config uses
modbus_controllerwithcustom_command,register_count,force_new_range, orskip_updates, those options have been renamed or replaced and your config needs updating. - ESP32 devices with a custom MAC: If you’ve burned a custom MAC into an ESP32 and use Ethernet, Bluetooth, or Thread/Zigbee, those interfaces will get new addresses after updating – so DHCP reservations, Bluetooth pairings, and network filters keyed to the old MAC need updating too.
- Certain UART sensors: A handful of components (including
mhz19,daly_bms, andteleinfo) now reject unsupported UART settings at config time – if any of yours were on the wrong baud rate or parity, they’ll flag a clear error you can fix on the UART bus.
New features: Updates are quicker – ESPHome now downloads and sets up its build tools in parallel – and can be encrypted using the same key that already secures the link to Home Assistant.
Also in this release: Six new components, around twenty additions to existing ones, and a broad sweep of fixes covering audio, Bluetooth, WiFi provisioning, and memory safety.
If you’re running a typical config, aside from the items above there’s nothing else you need to check before updating.
Keep reading below for the full release overview and technical detail – especially if you write custom lambdas, use external components, or run a non-standard setup.
Components added in this release
Section titled “Components added in this release”Release overview
Section titled “Release overview”ESPHome 2026.9.0 focuses on foundational build system and platform work: the scaffolding for an ESP8266 native toolchain lands across eight infrastructure PRs, PlatformIO installs are parallelized end to end (the package phase drops from 36% of build time to 25% on CI benchmarks), and a sweep across 60 files pushes 211 conditional log string literals off ESP8266 RAM into flash.
Beyond the platform work, this release adds Noise (ChaCha20-Poly1305) encryption for OTA updates, cuts what encryption costs in flash by close to half on every platform, extends Improv serial beyond WiFi so Ethernet-only boards can report their status and web server URL to setup tools, teaches WiFi provisioning to shut down the AP and captive portal when the window closes, and rewrites IR transmission on the BK7238 and RTL8720C to run from a hardware timer interrupt instead of a busy-wait. The multi-release modbus overhaul continues with a heap-free write path, a consolidated range-join option, and continuous polling. A hardening pass stops the device from rebooting when the heap runs out during WiFi scans, stalled API writes or OTA verification. Six new components (ds1603l, d01, sfa40, mk2pvrouter, snapshot, noise) and around twenty feature additions to existing components round out the release. The bundled Device Builder masks credentials in the YAML diff view, boots ESP32-C6 boards after a browser flash, and lets paired build servers recover from an address change on their own.
Upgrade checklist
Section titled “Upgrade checklist”- If you have a device with a custom MAC burned into the ESP32 eFuses and use Ethernet, Bluetooth, or 802.15.4, expect a new MAC on those interfaces after upgrading; update DHCP reservations, ACLs and Bluetooth pairings
- If you use
modbus_controllerwithcustom_command:, rename it tocustom_pdu:and drop the leading device address byte from the payload - If you use
modbus_controllerwithregister_count:orforce_new_range:, migrate toreuse_previous_range:and re-verify any FP32 packed asregister_count: 1+response_size: 4 - If you use
modbus_controllerskip_updates:on a sensor, split the slow registers onto a secondmodbus_controllerwith the sameaddress:and a slowerupdate_interval: - If you use
modbus_controllerswitchoroutputwith a non-zerooffset:on holding registers, re-verify the target register (writes now land ataddress + offset/2instead ofaddress + offset) - If you have a
modbus_controllerswitchwrite_lambdathat inverts the returned bool to flip the displayed state, publish the state explicitly or invert in the readlambdainstead - If you use
homeassistant.eventvariables:with lambda source, tag the value with!lambdato silence the new deprecation warning - If you use
esp32_hosted, remove any pinned ESP-IDF version below 5.3 or bump the pin to 5.3 or newer - If you use one of the affected UART components (
cm1106,daly_bms,hrxl_maxsonar_wr,hydreon_rgxx,mhz19,pylontech,teleinfo,vbus,wl_134,cse7761) with non-default UART settings, expect a configuration error and adjust the UART bus to the component’s required baud rate and parity - If you use API user-defined actions with long variable names or descriptions on ESP8266, keep each action’s total name and metadata text under 384 bytes or split into smaller actions
- If you have lambdas calling
modbus_controllerModbusCommandItemfactories orqueue_command(), migrate to the entity write helpers ormodbus_clientactions before the 2027.3.0 removal
ESP8266 native toolchain foundation
Section titled “ESP8266 native toolchain foundation”Led by @bdraco, this release lays the entire scaffolding for building ESP8266 firmware
without PlatformIO. ESPHome now has a shared registry and download layer
(#18570), a linker-script surgery module and per-board build metadata
ported from platform-espressif8266 4.2.1 (#18555), a toolchain seam
that validates a --toolchain choice on every platform (#18556), a
resumable, sha256-verified installer for the Arduino ESP8266 core 3.1.2 and xtensa gcc 10.3
(#18557), and a native library backend that resolves
cg.add_library() entries against the framework tree (#18558).
Supporting pieces came with it: the idedata and size-summary helpers and the library converter moved into shared build
helpers (#18661,
#18685), and a rebuild now logs which source change triggered it
(#18552).
This infrastructure ships dormant, with no user-facing option calling it yet; it is the foundation, shipped ahead so the native toolchain itself can land in 2026.10.0. A significant number of ESPHome installs still run on ESP8266, and a native build path gives ESPHome direct control over downloads, caching and incremental builds, the same route the ESP32 took with native ESP-IDF in 2026.7.0.
Build system parallelization
Section titled “Build system parallelization”The install phase of a PlatformIO build was serial from top to bottom, which showed up most sharply on Raspberry Pi class hardware and inside the Home Assistant App. This release parallelizes it end to end:
- Registry library and ESP-IDF tool archives download a few at a time instead of one by one, under a single combined progress bar, cutting cold-start install time (#18662); when two builds want the same archive at once, the waiting one shows the other’s download progress instead of sitting at 0% (#18983)
- A prefetch subprocess resolves and downloads PlatformIO packages in parallel into PlatformIO’s own cache (#18769), and the install pass extracts them with one worker per usable core (#18775); CI benchmarks on the esp8266-arduino leg show the package phase dropping from 17.1s to 10.3s, 36% of the build down to 25%
- The docker image’s own PlatformIO library preinstall got the same treatment, cutting a ~100s step to ~46s on every arch leg (#18777), and the CI compile-test image compresses with multithreaded zstd instead of gzip, taking another 40s off the pipeline (#18812)
- ESP-IDF native builds cache the discovered component list inside the extracted framework directory, skipping a full CMake configure pass on every rebuild after an sdkconfig change, ~2.2s on desktop and several times more on SBCs (#18752)
- PlatformIO checks whether each registry library is a private package before looking at its own cache, and on any machine without a PlatformIO login that check sleeps half a second per library; ESPHome never uses private packages, so it now answers the check itself, cutting a no-op rebuild of a six-library config from 2.9s to 0.4s (#18823)
ESP8266 RAM savings
Section titled “ESP8266 RAM savings”A concerted sweep across the codebase pushes conditional log string literals off ESP8266 RAM and into flash,
where the rest of the log format string already lives. @bdraco scanned every
ESP_LOG* call site and converted 211 bare literals across 113 lines in 60 files, wrapping each in
LOG_STR_LITERAL so ESP8266 keeps them in flash while other platforms see a no-op
(#18907,
#18906). A d1_mini with dht, adc, resistance, wifi and
wireguard drops 80 bytes of RAM from this alone. In the same vein, str_contains_ignore_case now keeps its
needle in flash on ESP8266 via a PSTR wrapper and an _P-family implementation
(#18574), and the 65-byte base64 alphabet table is gone
entirely, replaced by arithmetic range mapping that saves 64 bytes of RAM and doubles decode speed on host
(#18454). Every ESP8266 device with API encryption benefits
from the base64 change automatically.
Framework memory and build efficiency
Section titled “Framework memory and build efficiency”Two coordinated efforts trim per-instance overhead and unused sources across the tree.
@bharvey88 and @bdraco drove a series of over 30
PRs moving trivial one-line accessors from .cpp files into headers so the compiler can inline them, covering
WiFi, Ethernet, logger, light, select, sensor, text sensor, climate, cover, fan, switch, valve, text,
datetime, thermostat, sprinkler, MQTT, WireGuard, display, safe_mode and more (representative measurements:
32 bytes saved on ESP8266 for wifi scan result accessors, 48 bytes for the remaining wifi accessors, similar
per-component savings across the tree). Separately, @bdraco extended the
FILTER_SOURCE_FILES pattern so binary_sensor click/multi-click sources, all three sensor filter files, and
esp32/gpio.cpp are only copied and compiled when the config actually uses them
(#18602), and picked up other unused sources across
light/JSON schemas, i2s_audio SPDIF, MQTT entity types, uart debug, esp32_ble advertising, time posix TZ and
uptime timestamps (#18675,
#18676,
#18677,
#18678,
#18679,
#18680,
#18746,
#18750). A few one-off wins round it out: APINoiseFrameHelper
is 104 bytes smaller per open encrypted connection (#18420),
the camera image reader is created lazily instead of per connection
(#18421), and API message-type storage widened to uint16_t
without adding per-connection RAM (#18526).
Out-of-memory hardening
Section titled “Out-of-memory hardening”On ESP-IDF, C++ exceptions are disabled, so an allocation that fails when the heap is exhausted aborts and reboots the device instead of returning null. A sweep across the tree removes that abort from the paths a busy device actually hits, where the device can likely recover instead of rebooting. A WiFi scan whose results cannot be allocated is now dropped with a warning and retried, and with a single configured network the ESP32 driver filters the scan by SSID and keeps only the strongest 12 records, so the common case also uses the least RAM (#19254, #19253). A stalled API write on a lossy link used to allocate and free about 1 KB per write; each connection now keeps one reusable overflow buffer with a capped backlog, ending the heap fragmentation that could take a low-heap device down during the 90 second OTA confirmation window, and encoding got about 8% faster along the way (#19093). The OTA noise session, auth buffer and signature block, the Nextion command queue and the W5500 SPI context all moved off aborting allocation paths as well, failing the operation instead of the device (#19245, #19249, #19251, #19246, #19248).
ESPHome Device Builder
Section titled “ESPHome Device Builder”This is a smaller cycle for the bundled ESPHome Device Builder, and most of it already reached you through the 2026.8.x patch releases; this section covers everything since the 2026.8.0 release notes.
In the dashboard and editor:
- The YAML diff view used to render credentials in plain text the moment you flipped the diff toggle; it now masks the same sensitive values the editor does, with a reveal toggle (frontend#1664)
- A WiFi password containing
#made every WiFi write append a secondwifi_password:line tosecrets.yaml; the writer now handles quoted values, heals an already duplicated file, and reports a brokensecrets.yamlas your file to fix rather than an internal error (#2624, frontend#1673) - Dot-prefixed top-level keys, the documented way to park YAML anchors, no longer render as bogus cards or get deleted when you edit the section above them through the structured form (frontend#1689, #2652)
- The add-component form stops demanding pins and dimensions that the selected display model already predefines (#2648), and @Daniel085 brought the device comment back to the tile card (frontend#1671)
- The catalog picker renders only while its dialog is open (frontend#1698, frontend#1710, frontend#1715), and platform-scoped component triggers resolve by the instance’s platform (#2671, frontend#1720)
Flashing and provisioning:
- On ESPHome Web, flashing an ESP32-C6, H2 or P4 left the chip in the ROM bootloader instead of booting the new firmware, so Improv never answered; the reset after flashing now boots the firmware (frontend#1679)
- Prepare for first use gained an Erase the device first option so a board that ran other firmware does not come up with stale WiFi credentials (frontend#1680), and the WiFi credentials dialog prefills the stored values (frontend#1739)
- Dismissing the install dialog keeps the compile running (frontend#1732)
Keys and build servers:
- Rewriting a static API key replaces a duplicated
ota: encryption: key:with the bare block that inherits it (#2665), devices that already offer OTA encryption are nudged towardota: encryption:(frontend#1714), the encryption key shows in the device menu for devices keyed throughota: encryption:(#2679, frontend#1722), and Home Assistant adoption and its API key handoff got a round of hardening (#2693, #2716, #2720) - A paired build server that moves to a new IP on another subnet used to be reachable again only by editing the endpoint by hand; after five minutes of silence it now dials back to announce its new address, so the pairing heals itself (#2650)
Under the hood, out-of-band edit detection uses a versioned whole-file fingerprint, so a future algorithm change can never again trigger a fleet-wide regeneration on upgrade (#2617), the Docker container reaps orphaned processes when running as PID 1 (#2636), version history ignores the fake executable bit Samba puts on files edited from Windows (#2646), and the JSON config endpoint serialises non-string keys (#2660).
Encrypted OTA updates
Section titled “Encrypted OTA updates”@bdraco adds Noise (ChaCha20-Poly1305) encryption to the esphome OTA platform
(#18489), using the same protocol the native API already
uses. Until now the firmware image travelled over the network in plaintext, so a passive listener on the LAN
could capture the WiFi credentials and API encryption key embedded in the image; the SHA256 password only
authenticated the client, it did not make the transfer confidential. A device with an api encryption key
now offers OTA encryption with that key and the CLI uses it whenever the device offers, so once a device runs
this release, enabling encryption is an ordinary update; the update that brings older firmware to this release
still travels in plaintext (#18979). An
encryption: block on the OTA platform is what makes the device require it: both the device and the CLI then
fail closed so an attacker cannot strip encryption and force a plaintext upload. A bare encryption: inherits
the api encryption key, and a password and encryption are mutually exclusive because the key already
authenticates the uploader. This work
sits on top of a new shared noise component (#18490) that
lifts the Noise plumbing out of the api component so both api and ota share it, from the noise-c dependency
and the hardware RNG that seeds the handshake to key validation and the responder role; there is no user
configuration for noise itself.
Smaller and faster encryption
Section titled “Smaller and faster encryption”The noise-c and libsodium forks behind API and OTA encryption had only been lightly adapted for embedded targets. @bdraco went through them in full, cut them by nearly half in size and made them faster in most cases (#18989, #19030, #19062, #18692). The ESP8266 and RP2040 now run X25519 on 13-bit limb arithmetic that needs no 64-bit products, the Ed25519 base point table shrank from 30 KB to 12 KB on every platform, the SHA256 transform is compact on every platform and, on the ESP32 and RP2040, two to three times faster when cold, and on the ESP8266 the ChaCha20-Poly1305 transport is a quarter cheaper per packet with its constants out of DRAM. What turning on API encryption costs in 2026.8.0 and in this release, static RAM being the image’s own data, not per-connection heap:
| Board | Flash 2026.8.0 | Flash 2026.9.0 | Static RAM 2026.8.0 | Static RAM 2026.9.0 |
|---|---|---|---|---|
| ESP8266 d1 mini | 80.9 KB | 41.9 KB | 1376 B | 48 B |
| ESP32 AtomU (IDF) | 65.9 KB | 37.7 KB | 56 B | 32 B |
| Pico W (RP2040) | 65.5 KB | 34.7 KB | 48 B | 24 B |
A Noise handshake drops from about 580ms to about 300ms on the ESP8266, from 63ms to 46ms on the ESP32 and from about 260ms to about 200ms on the Pico W; on the ESP8266 and the Pico W an encrypted API connect is almost entirely the handshake, so connects speed up by about the same amount.
Improv Serial beyond WiFi
Section titled “Improv Serial beyond WiFi”@kbx81 makes improv_serial aware of the whole network stack instead of only
WiFi, so it now works on Ethernet-only boards and other WiFi-less devices
(#17598). The component depends on network instead of
wifi, gates every WiFi call behind #ifdef USE_WIFI, and implements the new Improv Get Network State
RPC (0x07) that reports which interfaces are up and the web server URL for each one. The Improv state machine
still uses WiFi provisioning where WiFi exists; on WiFi-less builds it reports STOPPED so clients do not
offer a WiFi form that cannot succeed. @bdraco additionally adds an optional
uart_id: so Improv can run on a dedicated UART bus separate from the logger’s serial port
(#18794), which also makes the protocol end-to-end testable
via the new host integration test.
Provisioning shuts down when the window closes
Section titled “Provisioning shuts down when the window closes”Also from @kbx81: the WiFi access point and captive portal are now torn down when
the provisioning window closes, matching the treatment BLE Improv already had
(#17466). An unprovisioned device that times out is now fully
unprovisionable until it is power-cycled - BLE Improv stops, the captive portal stops, the access point
disappears, and new API clients are turned away. Because a device whose access point is its only network
connection becomes unreachable when the window closes, provisioning: now warns at config time when WiFi is
configured with an access point and no station credentials.
ISR-driven IR transmission on LibreTiny
Section titled “ISR-driven IR transmission on LibreTiny”@kbx81 rewrites remote_transmitter on the Beken BK7238 and Realtek RTL8720C
to pace the IR envelope with a hardware timer interrupt chain instead of a priority-boosted busy-wait
(#18648,
#18660). Interrupts stay enabled throughout, so WiFi and
lwIP tasks run normally during long transmissions and remote_receiver decodes the device’s own frames off
the air during a send. non_blocking: is now supported on those two chips (previously ESP32-RMT only): the
send returns immediately after arming the chain and on_complete fires from loop(). Bench measurements on
an RM4 Pro (RTL8720CF) show 25.01-25.09ms repeat gaps for a configured 25ms (tighter than the busy-wait’s
25.02-25.05ms) and 27 captured transmissions all bit-perfect; an FK UFO-R4 (BK7238) measured 16-30us
duty-apply latency and 39/39 bit-perfect NEC envelopes. The BK7231N keeps the bit-bang implementation it had
before, since its SDK snapshot lacks the PWM driver interface the interrupt chain needs
(#18958).
Modbus controller overhaul
Section titled “Modbus controller overhaul”The long-running modbus refactor from @exciton continues with a large batch of improvements:
- Persistent writer devices (#18082): switch, number, output and
select writers become their own hub devices with a heap-free write path that allocates nothing after
setup(), andwrite_lambdagets full access to the underlying device so a coil switch can drive a holding-register write - Unified range polling (#18071): ranges move onto the same
PollingDevicearchitecture, saving 1,756 bytes of flash on the ESP8266 test config with no RAM change - One range option (#18085):
register_countandforce_new_rangeare consolidated into a single tri-statereuse_previous_rangeoption that says how a sensor relates to the range built just before it custom_pdu(#18652):custom_commandis renamed tocustom_pdu, with auto-migration when the leading byte matches the controller address- Continuous reads (#18542,
#18080): a new
continuous:option on the client actions and the controller makes reads stream back-to-back to fill idle bus time - Write-path fixes, shipping as breaking changes: the switch and output write offset is now byte-accurate where it
was previously doubled (#18787), and a switch
write_lambdareturn value only changes the value written to the device, so an active-low relay finally displays the requested state instead of the inverted one (#18788)
Users of custom_command, register_count, force_new_range or skip_updates should review their configs.
New components and hardware support
Section titled “New components and hardware support”Six new components join the tree:
- ds1603l by @JakeLC15 - a UART ultrasonic liquid-level sensor with configurable min/max level and volume mapping (#13133)
- d01 by @ch604 - a PM2.5 particulate sensor that broadcasts its own packets every 1.4 seconds (#17788)
- sfa40 by @NoQuarrel - the Sensirion SFA40 formaldehyde sensor with temperature and humidity readings (#17815)
- mk2pvrouter by @FredM67 - Mk2PVRouter telemetry over UART, modelled on the TeleInfo component (#8487)
- snapshot by @clydebarrow - a display
platform that draws into memory and writes BMP files, works anywhere the
hostplatform runs, and pairs with a newheadless:option on the SDL display for documentation captures and CI golden-image tests (#17917) - noise by @bdraco - a shared internal Noise primitives component that api and the new encrypted OTA both consume; no user configuration (#18490)
New features in existing components
Section titled “New features in existing components”- zigbee time platform by @luar123 adds a Zigbee-sourced time provider on ESP32 (#18656)
- Package-relative file paths by @bdraco: a file or directory option whose
config-relative path does not exist now resolves against the YAML file that declares it, so a package or
!included file can ship its own assets next to its YAML (#19259) - tuya water_heater by
@iago-veiga exposes a Tuya-MCU electric water heater as a single
water_heaterentity, collapsing the previous template-plus-lambda pattern into one block (#17323) - template climate by @rsre builds a full
climate entity out of ESPHome primitives, with per-field set actions, an
on_controltrigger, an optimistic mode that applies commands locally, and aclimate.template.publishaction for reporting the device’s own state (#14455) - lvgl list widget by @clydebarrow
adds a scrollable container designed to be filled at runtime, with
add_text,add,removeandclearactions pluson_add/on_removetriggers (#18018) - lvgl table widget also by @clydebarrow adds a lightweight grid of text elements (#18422) and support for LVGL 9.5’s radial and conical gradients (#18818)
- online_image auto-detection by
@guillempages picks up the image format from the server’s
Content-Typeheader (#16337), plus QOI format support inruntime_image(#16945) and a decoder that stays allocated between images of the same format (#18488) - ethernet spi_id by @jesserockz lets a SPI
ethernet chip (W5500, DM9051, ENC28J60, CH390) attach to an existing
spi:bus so it can share pins with other SPI peripherals like a display on boards such as the M5Stack CoreS3 (#18530) - network tcp_send_buffer by @bdraco exposes lwIP’s
per-socket send buffer size on ESP-IDF, giving Bluetooth proxies a way to raise the TCP buffer without
paying the RAM cost of
enable_high_performance(#18610) - epaper_spi UC8179 by @jesserockz adds a monochrome UC8179 driver with three refresh modes and registers models for the Waveshare 7.5in v2 panel and the Seeed reTerminal E1001 (#17568)
- api user-defined action metadata by
@bharvey88 adds optional
descriptionandexamplefields on API actions and variables so Home Assistant renders a proper form; ESP8266 configs with unusually long action metadata may need trimming to fit the 384-byte-per-action limit (#18881) - sen6x algorithm tuning by @bharvey88 exposes Sensirion’s VOC/NOx tuning parameters (#18779)
- emontx AP and F tags by @FredM67 adds apparent power and frequency sensor tags with correct defaults (#18586)
- climate_ir_lg advanced commands by
@ireun adds an
advanced_commands_supportflag that fixes heat/cool temperature setting, decodes JET mode, adds swing settings and rejects previously misdecoded frames (#10875) - mitsubishi_cn105 Fahrenheit by @crnjan implements the non-linear Fahrenheit mapping the Mitsubishi A/C hardware actually uses (#15488)
- hoermann_hcp buttons by @zweckj adds venting and half-open buttons (#18544)
- audio_http persistent buffer by @n-IA-hane keeps the HTTP decoder ring buffer allocated between playback sessions to avoid heap fragmentation on memory-tight devices (#18708)
- rp2_ble_tracker automation by @Bl00d-B0b wires the shared BLE automation surface onto the RP2 tracker (#18717)
- esp32_hosted by @jesserockz gains ESP-NOW on
the ESP32-P4: the host forwards every
esp_now_*call to the co-processor over esp-hosted, soespnow:works unchanged on boards like the M5Stack Tab5 once the co-processor runs the matching esp-hosted firmware (#17712) - esp32_ble by @jesserockz reference counts
advertising, so a device that only uses
esp32_improvstops advertising once provisioning ends instead of showing up in every phone’s Bluetooth list forever (#18943) - sendspin media source by @kahrendt
adds a
codecsoption that sets which audio codecs the media source advertises, in order of preference (#19047)
Networking and diagnostics
Section titled “Networking and diagnostics”Several targeted changes for network-heavy devices. Bluetooth proxy congestion warnings now fire once per
transaction and once per connection instead of on every drop cycle, ending the log flood a bulk transfer to
opendisplay-style screens produced (#18605).
esp32_ble_tracker now warns at config time when the scan window exceeds 600ms while WiFi is configured,
because holding the shared radio for over a second at a time starves WiFi and causes disconnects
(#18725). espota2 now logs the erase-prepare window
separately from the upload window and reports total OTA time
(#18582), so users see why there is a silent pause after the
handshake instead of interpreting it as a stall. On a lossy link an OTA upload could give up moments before a
lost chunk acknowledgement was retransmitted; the device now waits 105s for data and the CLI 160s for an
acknowledgement, so a retry always finds the device free
(#19041).
On the ESP8266, LEAmDNS could free the same packet buffer twice when an mDNS packet arrived while a send was
failing, corrupting the heap right as Home Assistant connected; its main loop calls are now guarded against
that re-entrancy (#18990). The logger’s UART on the ESP32-S3
now runs from the XTAL clock, which stops the garbled stack traces the APB clock produced
(#17939), and the debug component reports the reboot source
after a watchdog-flavoured esp_restart() and no longer prints an empty source
(#17537).
After a ble.disable and ble.enable cycle, a Bluetooth proxy
connection slot could get stuck connecting forever and stop scanning, because an idle slot never registered with the
new stack; every client now re-registers when the stack cycles, and an enable issued while a disable is still pending
cancels it instead of being ignored (#19068). Starting an OTA while a
proxy connection was active left the shared radio biased toward Bluetooth for the whole transfer; the preference now
reverts to balanced when the OTA starts (#19082).
ESP32 custom eFuse MAC applied consistently
Section titled “ESP32 custom eFuse MAC applied consistently”@kbx81 fixes an inconsistency where a custom MAC burned into the ESP32’s eFuses
was only applied to WiFi (#18452). The apply now runs in
app_main() before any component sets up, so Ethernet, Bluetooth and 802.15.4 all derive their addresses
from the custom base MAC. Only devices with a burned custom eFuse MAC AND Ethernet/Bluetooth/802.15.4 are
affected; on those devices Ethernet, Bluetooth and Thread/Zigbee will change on-wire MAC addresses on
upgrade, so DHCP reservations, MAC filters and BLE pairings may need updating (set
ignore_efuse_custom_mac: true under esp32.advanced to keep the old behaviour).
Codebase type annotations
Section titled “Codebase type annotations”Led by @jesserockz, a repo-wide sweep in eleven parts added parameter and
return type annotations to every mid-sized component’s Python code (over 130 components in this batch,
plus a smaller follow-up in #18697), landing across
PRs #18338 through
#18348. Because component modules do not use
from __future__ import annotations, every annotation is evaluated at import time, so the sweep also had to
add every missing import for the names it introduced; an all-modules import check pins that. No behaviour
changes.
Other fixes
Section titled “Other fixes”Audio on ESP32 got a round of fixes:
- A speaker start request that arrived while the previous task was still stopping is kept instead of dropped (#19027)
- The I2S driver is no longer started a second time while the bus lock is still held, which cost a one second delay before playback (#19045)
- The speaker’s state changes now all run on the loop thread, so a stop can cancel a start the loop has not processed yet and the speaker no longer wedges in a restart loop (#19089)
- The MP3 decoder treats a mid-stream format notice as the recoverable status it is instead of ending playback (#19028)
- The audio components check their buffers through a locked pointer or a null test instead of
use_count()(#19046), and a runningStaticTaskis suspended before its stack is freed, closing a use-after-free (#19048)
And elsewhere:
- MQTT light discovery publishes
brightness: trueagain, so Home Assistant sends brightness and colour as separate fields instead of folding one into the other (#18950) - dallas_temp filters the 85 °C reading a DS18B20 returns after a reset (#17877)
- ble_client nodes that never read services now report established, so the client releases its discovered services (#17920)
- template water heater stops republishing on every loop iteration while a temperature lambda returns
NAN(#19013) - pmsa003i no longer logs garbage from an uninitialized buffer when the I2C read fails (#19053)
- tuya builds without a network component (#18948)
- LVGL
qrcode,keyboardandtabviewwidgets declare their label dependency (#18387) - rf_bridge bucket sniffing works with the Portisch firmware (#17683)
- anova re-asserts the temperature unit on every poll (#17141)
- atm90e32 verifies its offset calibration writes (#18701)
- usb_uart keeps working when the comm interface claim fails (#18968)
- The removed udp options moved to
packet_transportno longer appear in the generated schema (#19032)
Thank you, contributors
Section titled “Thank you, contributors”This release includes 275 pull requests from over 40 contributors. A huge thank you to everyone who made 2026.9.0 possible:
- @exciton - 25 PRs including the multi-release Modbus overhaul: PollingDevice-based controller polling, continuous polling, heap-free writer entities, a compile-time register decoder, and the switch to typed address-based read callbacks across sdm_meter, growatt_solar, havells_solar, kuntze, pzemac, pzemdc, and selec_meter
- @jesserockz - 20 PRs including the 12-part sweep adding Python type annotations
across the component tree, the
FINAL_VALIDATE_SCHEMAreturn-type migration, theuartcheck_uart_settingsmove to final validation, the new UC8179 e-paper driver, and theethernetspi_idoption - @clydebarrow - 15 PRs including the new SDL snapshot and headless display mode,
LVGL list and table widgets, LVGL radial and conical gradients, ESP32-S31 support in
mipi_rgb, and CodSpeed benchmark reliability fixes - @kbx81 - 9 PRs including the provisioning window shutdown behavior,
improv_serialsupport for Ethernet and non-WiFi interfaces, the ESP32 custom eFuse base-MAC fix, and ISR-drivenremote_transmitteron RTL8720C and BK7238 - @guillempages - 8 PRs including
runtime_imageformat auto-detection, QOI decoding, MIME types, decoder retention, and BMP dimension validation, plus the portablestr_contains_ignore_casehelper - @bharvey88 - 6 PRs including the WiFi scan list dedupe helper shared with
improv_serial,sen6xVOC/NOx algorithm tuning, description and example metadata on user-defined API actions, and BK72xxdeep_sleepwakeup validation - @swoboda1337 - 6 PRs including CI diff-size fallback and max-parallel removal,
external_componentsoverride logging, and ESP32-S31 ADC and GPIO support - @FredM67 - 4 PRs including the new
mk2pvroutercomponent andemontxapparent power and frequency sensor support - @p1ngb4ck - 4 PRs including the
USBUartChannelBaseextraction, movingCONF_SLOTandCONF_LABELinto the sharedconstmodule, and uncovering a silentsafe_modeerror - @crnjan - 3 PRs including
mitsubishi_cn105Fahrenheit support, deferred status requests, and marking configurable classes as final - @rwrozelle - 2 PRs including the
IntervalSyncerPollingComponentrefactor and anopenthreadshutdown fix - @n-IA-hane - 2 PRs including SPI PSRAM DMA for external buffers and an
audio_httppersistent ring buffer option - @Bl00d-B0b - 2 PRs including
rp2_ble_trackerautomation triggers and scan actions plus a LibreTiny WiFi STA state reset on synchronous connect failure - @JakeLC15 - the new DS1603L ultrasonic liquid-level sensor component
- @ch604 - the new D01 PM2.5 sensor component
- @NoQuarrel - the new SFA40 formaldehyde sensor component
Also thank you to @bdraco, @ireun, @iago-veiga, @leodrivera, @JoppyFurr, @DavidvtWout, @alaraun, @zweckj, @kahrendt, @luar123, @Gafielt, @MakerYuichi, @mfishma, @Zebble, @rsre, @mipa87, @ryan-ronnander, @AndreKR, @ssieb, @ptr727, @dmd79, @Gytisla, @btli, @raykholo, and @CircuitSetup for their contributions, and to everyone who reported issues, tested pre-releases, and helped in the community.
Breaking changes
Section titled “Breaking changes”Component changes
Section titled “Component changes”- Modbus Controller:
custom_commandrenamed tocustom_pduand now takes the PDU only (function code + data), not a full frame. The controller’saddress:and CRC are appended automatically. Configs whose first byte matched the controller’saddress:are auto-migrated with a deprecation warning until 2027.3.0; frames targeting a different unit address must be moved to a sensor on the correct controller. #18652 - Modbus Controller:
address: 0(Modbus broadcast) is now rejected with a validation error. A broadcast is never answered, so it could never be polled. #18652 - Modbus Controller: The per-sensor
skip_updatesoption is retired and has no effect. It logs a deprecation warning at configuration time (removed in 2027.3.0). To poll some registers less often, add a secondmodbus_controllerwith the sameaddress:and a slowerupdate_interval:, and attach the slow sensors to it.offline_skip_updates(controller-level) is unaffected. #18652 - Modbus Controller:
register_countandforce_new_rangeare replaced by a new tri-statereuse_previous_rangeoption (auto/true/false).force_new_range: truemigrates automatically toreuse_previous_range: falsewith a deprecation warning (removed in 2027.3.0). Aregister_countmatching the derived width warns as redundant; a divergent one now fails validation with migration instructions. The old vendor pattern ofregister_count: 1+response_size: 4for an FP32 no longer validates. Range splitting is now strictly address-ordered, so where isolated items were previously sorted first the range may now split at their position. ARAWvalue or text sensor with aresponse_sizenow spansceil(response_size / 2)registers, one more than before for odd values. #18085 - Modbus Controller: Holding-register
switchandoutputplatforms now apply a non-zerooffset/byte_offsetbyte-accurately (address + offset/2) instead of doubling it. Configs with a non-zero write offset on these platforms must re-verify the target register. Odd offsets on these two platforms are now rejected at validation, as a 16-bit register write cannot target half a register. Thenumberplatform’s odd offsets are unchanged. #18787 - Modbus Controller: A
switchwrite_lambdareturning a bool different fromxnow only changes the value written to the device; the switch reports the requested state. Lambdas that relied on the return value flipping the displayed state should publish explicitly or invert in the readlambdainstead. The lambda’spayloadparameter changes type fromstd::vectorto a fixed-capacityRegisterValues/PduBuffer;push_back,clear,assignand indexing still work, butresize,reserve,insertand passing tostd::vector&helpers no longer compile. A lambda that fillspayloadand returns{}now sends the buffer (as documented); previously that combination sent nothing. #18788, #18082 - Modbus Controller: For a
custom_pdupoll, theaddressargument delivered toon_command_sent,on_onlineandon_offlinetriggers is now-1(no decodable address) instead of the sensor’s synthesized address. Automations filtering on a custom sensor’s synthetic address must test for-1instead. Thefunction_codeargument for acustom_pduwhose first byte has the 0x80 bit set is now reported with that bit masked (0x83 reports as 0x03). #18071 - Runtime Image: The image decoder is now kept allocated between decodes to avoid memory churn when decoding multiple images of the same format. This adds a small persistent memory overhead for the decoder object; on memory-constrained devices, reducing runtime image usage or restarting less frequently may be needed. #18488
- API:
homeassistant.eventvariables are now compiled as lambdas when they use!lambdaor look like lambda source; a plain string that looks like lambda source now logs a deprecation warning and will be treated as static text in 2027.3.0. Tag lambda values with!lambdaexplicitly.homeassistant.actionnow accepts plain static strings (previously rejected) and sends them as text. Acv.returning_lambdavalue whose onlyreturnsits inside a comment, or only matches as a substring likethe_return_value, now fails validation instead of failing at C++ compile. #18759 - API: User-defined actions now accept optional
descriptionand per-variabledescriptionandexamplemetadata using a new mapping form forvariables:. On ESP8266 only, each action’s action name, variable names, description and example text must total at most 384 bytes; configs that exceed this fail validation with a message identifying the action and its size. Other platforms have no limit. #18881 - API: The legacy media player
supports_pausefield is no longer sent. Clients on API 1.11 or newer already usefeature_flags, so current Home Assistant is unaffected; only very old clients that never learnedfeature_flagswould lose pause capability. #18801 - Preferences:
IntervalSynceris now aPollingComponent, so its interval can be suspended and resumed withcomponent.suspendandcomponent.resume. No YAML changes are required; the previousflash_write_interval: 0loop-every-iteration special case was already dead code (coerced to1msby the validator). Lambdas that called the oldset_write_interval()still compile via a deprecated shim that forwards toset_update_interval(), removed in 2027.3.0. #14370
Platform changes
Section titled “Platform changes”- ESP32: When a custom MAC address is burned into the eFuses, ESPHome now applies it as the system base MAC
before any component runs, so every interface (Wi-Fi, Ethernet, Bluetooth, 802.15.4) derives its address from it.
Previously only Wi-Fi did. Devices with a custom eFuse MAC and Ethernet, Bluetooth, or 802.15.4 will appear on the
network with a new MAC after upgrading: DHCP reservations, ACLs and Bluetooth bonds keyed to the old MAC need to be
updated, and 802.15.4 (Thread/Zigbee) nodes may need to be re-commissioned. Devices without a custom eFuse MAC (the
vast majority) are unaffected; setting
ignore_efuse_custom_mac: trueunderesp32: advanced:disables the behavior. #18452 - ESP8266: Arduino framework versions before 3.0.0 are rejected at validation. Those builds have failed since
ESPHome moved to C++20 in 2025.7, so nothing that compiled changes, but the check also covers
config,logsandupload, so a YAML still pinned below 3.0.0 needs theframework: version:pin removed or moved to 3.1.2 before those commands work again. #18917 - ESP32 Hosted: Now requires ESP-IDF 5.3 or newer. The legacy fallback to esp_hosted 2.0.11 (which had a known
double-free crash on ESP32-P4 + C6) has been removed and older ESP-IDF versions now fail validation with a clear
error. Configs that pin an ESP-IDF version below 5.3 alongside
esp32_hostedmust remove the pin or bump it to 5.3 or newer. #18417 - UART: UART settings validation (
baud_rate,parity,data_bits,stop_bits) for components that require specific values now happens at configuration time instead of only being logged at runtime. Nine previously unchecked components (cm1106,daly_bms,hrxl_maxsonar_wr,hydreon_rgxx,mhz19,pylontech,teleinfo,vbus,wl_134) and twelve components with incomplete checks now enforce their required UART parameters. Users with non-default UART settings that a component never supported will now get a configuration error instead of a runtime log error. #18940
Undocumented API changes
Section titled “Undocumented API changes”Advanced users with lambdas that touch component internals should note the following C++ changes. These APIs are not covered by the formal breaking-change policy (they are undocumented public methods), but lambdas often depend on them:
-
Preferences:
IntervalSyncer::set_write_interval()is deprecated in favour ofset_update_interval()(the newPollingComponentbase’s method) and will be removed in 2027.3.0. Existing lambdas callingset_write_interval()continue to compile via a forwarding shim. #14370 -
Modbus: New compile-time helpers
registers_to_value<VALUE_TYPE>()andregisters_to_uint32()are available inmodbus_helpers.hfor lambdas that know the value type at compile time. The runtimeregisters_to_number()is unchanged. Prefer the templated form when the type is known, as it inlines to a handful of instructions and returns the value’s natural type instead ofint64_t. #18863 -
Modbus Controller:
ModbusCommandItem, its factories (create_read_command,create_write_single_command,create_custom_commandand friends),queue_command(),unqueue_command(),on_write_register_response()and theFunctionCode::CUSTOMalias are deprecated and removed in 2027.3.0. Migrate one-shot writes to the entity write helpers exposed throughWriterDevice(available asitem->inside awrite_lambda), or to themodbus_clientwrite actions. UseFunctionCode::INVALIDfor the sentinel value formerly namedFunctionCode::CUSTOM. #18071 -
Modbus Controller:
write_lambdahas a strongeritempointer now.itemis the entity’s own persistent modbus device (not a throwaway command object), and the write helpers plusitem->queue_pdu()are the entire API. A lambda can drive any register, coil, or custom-PDU write through it, regardless of the entity’s own register type. Before/after for a coil switch writing a holding register:// Before: throwaway ModbusCommandItemauto cmd = ModbusCommandItem::create_write_single_command(parent_, 0x30, x ? 1 : 0);parent_->queue_command(cmd);return {};// After: entity's own deviceitem->write_single_register(0x30, x ? 0x0001 : 0x0000);return {};The
payloadparameter’s type also changes fromstd::vector<uint16_t>to a fixed-capacityRegisterValues(writes) orPduBuffer(custom PDUs).push_back,clear,assignand indexing keep working;resize,reserve,insertand passing tostd::vector&helpers no longer compile. Fillingpayloadand returning{}now sends the buffer (as the docs have always described); previously that combination sent nothing. #18082 -
Modbus Controller:
SensorItem::register_countandSensorItem::force_new_rangeare removed. Lambdas that read a sensor item’s register span should callentity_count()instead; the join behaviour previously encoded inforce_new_rangeis now expressed by the tri-statereuse_previous_rangeoption on the following sensor. #18085 -
Time:
RealTimeClock::set_timezone()(the C++ string-argument overload) and the on-device POSIX TZ string parser (parse_posix_tz()and helpers) are removed. There is no string-based C++ replacement: settimezone:in YAML, or let Home Assistant push a pre-parsed timezone over the API. Home Assistant 2026.3.0 and newer send the pre-parsed struct; older clients keep the codegen timezone. #18383
Breaking changes for developers
Section titled “Breaking changes for developers”- Core: Deprecated
EntityBase::get_device_class_ref(),get_device_class(),get_unit_of_measurement(),get_icon_ref()andget_icon()removed. Useget_device_class_to(),get_unit_of_measurement_ref()andget_icon_to(). #18375 - Core: Deprecated free functions
gamma_correct()andgamma_uncorrect()removed. UseLightState::gamma_correct_lut()andLightState::gamma_uncorrect_lut(). #18376 - Core: Deprecated
esp_log_vprintf_()__FlashStringHelperoverload removed; theconst char*overload is unchanged. #18377 - Core:
make_name_with_suffix()std::stringoverloads removed (hard removal, no deprecation cycle). Usemake_name_with_suffix_to()with a stack buffer sized viaMAX_NAME_WITH_SUFFIX_SIZEfromhelpers.h. #18828 - WiFi: Deprecated
WiFiComponent::wifi_ssid()removed. Use the heap-freewifi_ssid_to(). #18378 - Ethernet: Deprecated
EthernetComponent::get_eth_mac_address_pretty()removed. Useget_eth_mac_address_pretty_into_buffer(). #18379 - Modbus: Deprecated
ModbusDevice::waiting_for_response()removed. Useready_for_immediate_send(). #18381 - Modbus:
queue_pdu()refuses PDUs with the exception bit (0x80) set; receive parsers treat every 0x80-set response as the 2-byte spec exception shape; the “no device accepted broadcast” warning is removed; management codes 0x07/0x0B/0x0C/0x11 may now be broadcast. #18847 - Modbus: New compile-time register decoder
registers_to_value<VALUE_TYPE>()andregisters_to_uint32()inmodbus_helpers.h;registers_to_number()unchanged. #18863 - Modbus Controller:
ModbusCommandItem, its factories,queue_command(),unqueue_command(),on_write_register_response()andFunctionCode::CUSTOMare deprecated (removed in 2027.3.0). Migrate one-shot writes to the entity write helpers (WriterDevice) or themodbus_clientactions. #18071 - Modbus Controller:
SensorItem::register_countandSensorItem::force_new_rangeare removed; every platform constructor loses itsregister_count/force_new_rangeparameters (replaced byentity_count()andreuse_previous_range). #18085 - Web Server IDF: Deprecated
AsyncWebServerRequest::url()removed. Useurl_to(). #18382 - Time: On-device POSIX TZ string parser removed;
set_timezone()overloads andRealTimeClock::apply_timezone_()are gone. TheGetTimeResponse.timezoneproto field is marked deprecated and is no longer decoded. Settimezone:in YAML or let Home Assistant push the pre-parsed timezone. #18383 - UART:
UARTDevice::check_uart_settings()is deprecated (removed in 2027.3.0). Useuart.final_validate_device_schema()in your component’s PythonFINAL_VALIDATE_SCHEMAinstead. #18940 - USB UART:
USBUartChannelis now afinalconcrete type deriving from a newUSBUartChannelBase; subclass the base if you need to extend channel behaviour. #17472 - Mitsubishi CN105: All leaf configurable classes and actions (
MitsubishiCN105Component,MitsubishiCN105Climate,MitsubishiCN105VerticalVaneDirectionSelect,SetRemoteTemperatureAction,ClearRemoteTemperatureAction,VaneControlAction,LegacySetRemoteTemperatureAction,LegacyClearRemoteTemperatureAction) are markedfinaland can no longer be subclassed. #18272 - ESP32 IDF: The default IDF component exclusion list grew significantly. External components that include
headers from
app_trace,console,esp-tls,esp_driver_cam,esp_driver_gptimer,esp_driver_i2c,esp_driver_ledc,esp_driver_sdio,esp_driver_sdm,esp_driver_sdmmc,esp_driver_sdspi,json,protobuf-c,rt,sdmmc,tcp_transport,bt,esp_coex,esp_hal_ieee802154,esp_phy,esp_wifi,ieee802154,wpa_supplicant,esp_gdbstub,esp_http_serverornvs_sec_providermust callesp32.include_builtin_idf_component("<name>")into_code(or users can add them underesp32: framework: advanced: include_builtin_idf_components:). #18536, #18599, #18604, #18748 - ESP32 IDF: The mbedTLS root certificate bundle is no longer built unless a component asks for it. External
components that call
esp_crt_bundle_attach()must callesp32.require_certificate_bundle()into_code(or guard the call with#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE). #18747
For detailed migration guides and API documentation, see the ESPHome Developers Documentation.
Full list of changes
Section titled “Full list of changes”For the complete list of every merged pull request in this release, see the full 2026.9.0 changelog.



Comments