ESPHome 2026.4.0: Faster ESP32 defaults and signed OTA

Release Overview
Section titled “Release Overview”ESPHome 2026.4.0 delivers major performance and reliability improvements across all platforms. ESP32 devices default to maximum CPU frequency (33% faster API operations), unlock 40KB extra IRAM, signed OTA verification, and custom partition tables. ESP8266 gets a crash handler matching ESP32/RP2040, and a new client-side state logging architecture achieves up to 46x faster sensor publishing by moving log formatting off the device. Bluetooth Proxy advertisement forwarding (the constant hot path) dropped to just ~1.8% of main loop time on ESP32-C3. 31 PRs prepare the codebase for ESP-IDF 6.0, and CodSpeed benchmarking infrastructure now catches performance regressions before they ship. The substitution system has been redesigned for up to 18x faster config loading with dynamic !include paths, Ethernet expands with 5 new chip types across ESP32 and RP2040, and GPIO expanders gain interrupt-driven operation cutting idle I2C traffic by 99.7%. This release also adds LVGL v9.5.0, native Mitsubishi CN105 climate control, 4 new sensor components, and a esphome bundle CLI command to assist with remote compilation.
Upgrade Checklist
Section titled “Upgrade Checklist”- If you use LVGL with
rotationconfigured on the display component, move therotationsetting to thelvgl:block, and remove touchscreentransform:. - If you use the
i2s_audiomedia player, migrate to thespeakermedia player component (thei2s_audiomedia player has been removed) - If you use
use_legacyin youri2s_audioconfig, remove it (the legacy I2S driver has been removed) - If you have battery-powered ESP32/S2/S3/C5 devices, add
cpu_frequency: 160MHZto your config (the default is now 240MHz) - If you have two YAML keys that resolve to the same substituted name, note that the later key now takes precedence (“last writer wins”)
- If you use
sen5xwithvoc_baseline, remove it (it was dead code that never worked; usestore_baseline: trueinstead) - If you use
rp2040_pio_led_stripwithchipset: CUSTOM, use explicitbit0_high/bit0_low/bit1_high/bit1_lowtiming parameters instead - If you use Nextion
get_wave_chan_id()in lambdas, switch toget_wave_channel_id()(the old method is deprecated, removal in 2026.10.0) - If you use
sensor.raw_statein lambdas, migrate to using filtered state oron_raw_valuetriggers (.raw_stateis deprecated) - If you maintain external components that call
TEMPLATABLE_VALUEmacro-generated setters with raw C++ constants, route values throughcg.templatable()in Python codegen - If you maintain external components using
ButtonPressTrigger,SensorStateTrigger, or similar trigger classes, migrate tobuild_callback_automation() - If you maintain external components using
FlushResult, rename toUARTFlushResultwithUART_FLUSH_RESULT_prefix
ESP32 Performance and Security Improvements
Section titled “ESP32 Performance and Security Improvements”Several ESP32 platform changes improve performance, security, and flexibility.
Default CPU Frequency Raised to Maximum (#15143):
ESP32, ESP32-S2, ESP32-S3, and ESP32-C5 now default to 240MHz instead of 160MHz. This restores the performance level that Arduino users had before May 2025 and delivers ~34% faster CPU-bound operations:
- API encryption handshake: 90ms to 64ms (29% faster)
- Protobuf encoding (BLE proxy): 34% faster
- Noise encryption: 33-34% faster across all payload sizes
Users on battery power or with thermal constraints can override with cpu_frequency: 160MHZ. This is a breaking change as it increases power consumption on affected variants.
ESP32 crash handler improvements: The existing crash handler now captures backtraces from both cores on dual-core ESP32 variants (#15559) and preserves crash data across OTA rollback reboots (#15578).
40KB extra IRAM on original ESP32 (#14874): A new option reclaims 40KB of previously reserved SRAM1 as IRAM, expanding the flash cache window and reducing cache misses for WiFi, BLE, and API operations, at no cost to heap. ESPHome will automatically detect your bootloader version at boot and suggest enabling this option only when it is safe to do so. Do not enable this option without the bootloader check confirming it is safe; enabling it with a pre-v5.1 bootloader will brick the device (requiring USB reflash to recover). See the ESP32 documentation for details.
Signed OTA verification (#15357): New signed_ota_verification option enables firmware signature verification during OTA updates without requiring hardware Secure Boot (eFuse burning). Two workflows are supported:
- Private key mode (
signing_key) - Firmware is automatically signed during build. Simplest setup. - Public key mode (
verification_key) - Only the public key is embedded; binaries are signed externally (e.g., in CI/CD with the private key in a secure vault).
esp32: framework: type: esp-idf advanced: signed_ota_verification: signing_key: secure_boot_signing_key.pem signing_scheme: rsa3072Custom partition tables (#7682): Components and YAML configs can now define custom data/app partitions that are appended to the flash layout. The partition table generation has been unified across Arduino and IDF, and NVS has been moved to the end of flash with increased size (20KB to 384KB on Arduino). See the ESP32 documentation for details.
ESP-IDF 5.5.4 and Arduino 3.3.8 (#15666, #15705): Platform bumped to pioarduino 55.03.38-1 with ESP-IDF 5.5.4 and Arduino 3.3.8. PlatformIO is now run in a subprocess to isolate its virtualenv from ESPHome, preventing import errors after build.
ADC crash fix (#15717): ADC oneshot control functions are now placed in IRAM for cache safety. Previously, ADC reads could crash when flash cache was disabled during background NVS writes by WiFi, BLE, Zigbee, Thread, or power management.
Web server OTA brick hazard fix (#15720): Fixed a long-standing brick hazard in the captive portal and web server OTA flow where an interrupted upload followed by a retry could write concatenated firmware to flash, producing an invalid image that soft-bricked the device and required a serial flash to recover. The OTA backend now resets on each new upload.
mDNS crash fix (#15670): Bumped espressif/mdns to 1.11.0, fixing heap corruption when processing sustained mDNS browse traffic that caused deterministic LoadProhibited crashes, and a null pointer exception in mdns_parse_packet.
ESP8266 Crash Handler, Without a Serial Cable
Section titled “ESP8266 Crash Handler, Without a Serial Cable”Completing the platform coverage started in 2026.3.0 (which added crash handlers for ESP32 and RP2040), ESP8266 devices now have the same post-mortem crash diagnostics (#15465). When a device crashes, the exception details and up to 16 stack-scanned backtrace addresses are saved to RTC memory and reported over the API on the next boot. No USB cable required; crash data appears in esphome logs (which automatically decodes addresses to source locations via addr2line) and the Home Assistant log viewer, entirely over WiFi.
[E][esp8266]: *** CRASH DETECTED ON PREVIOUS BOOT ***[E][esp8266]: Reason: Exception - StoreProhibit (exccause=29)[E][esp8266]: PC: 0x40212EC5[E][esp8266]: EXCVADDR: 0x00000000[E][esp8266]: BT0: 0x40212F5A[E][esp8266]: BT1: 0x40203B10The PC is the program counter where the crash occurred, EXCVADDR is the memory address that caused the fault, and BT0/BT1/etc. are the stack backtrace showing the call chain leading to the crash. When using esphome logs, these addresses are automatically decoded to source file and line numbers via addr2line, so you see the actual function names instead of raw hex.
No configuration needed; it’s automatically enabled for all ESP8266 devices. Resource cost is minimal: 0 bytes RAM, 119 bytes IRAM, ~1KB flash.
Client-Side State Logging
Section titled “Client-Side State Logging”State change logging has been fundamentally redesigned to eliminate the single biggest CPU bottleneck on every ESPHome device (#15155).
Every entity state change ('CO2' >> 420 ppm, 'Light' - Setting ON) previously ran vsnprintf on the device to format a log string and sent it over UART, even when nobody was watching the serial console (which is 99.9% of devices). CodSpeed benchmarks revealed this was consuming 95%+ of sensor publish time.
State change logs are now reconstructed on the client instead of the device. When esphome logs or the dashboard connects, it subscribes to compact protobuf state messages and formats them locally. The protobuf messages are a fraction of the size of formatted strings, and formatting happens on your computer instead of the microcontroller. This achieves up to 46x faster in the publish path, with flash savings of 1.4KB (ESP32-C3) to 2.2KB (ESP8266). State changes still appear in logs as [S] lines in bright cyan:
[18:03:14.677][S][sensor]: 'CO2' >> 420 ppm[18:03:14.677][S][sensor]: 'Temperature' >> 35.63 °C[18:03:51.747][S][light]: 'Light' >> ONA new --no-states flag (#15160) lets users suppress state change lines entirely for the first time, useful for debugging when the log is flooded with sensor readings. Users who need the old device-side state logging can opt back in by setting logger: level: VERBOSE.
Preliminary ESP-IDF 6.0 Compatibility
Section titled “Preliminary ESP-IDF 6.0 Compatibility”This release includes 31 pull requests preparing ESPHome for ESP-IDF 6.0, which brings GCC 15, picolibc, and significant API changes from Espressif. Led by @swoboda1337, this effort touches cryptography, drivers, and toolchain compatibility across the entire codebase. While ESP-IDF 6.0 is not yet the default framework version, these changes ensure ESPHome is ready when it ships. All related PRs are tagged with the idf-6 label.
PSA Crypto API migration:
IDF 6.0 deprecates the legacy mbedTLS crypto APIs. All ESPHome cryptographic operations have been migrated to the new PSA Crypto API:
- SHA-256 hashing (#14809)
- HMAC-SHA256 (#14814)
- BLE tracker PSA Crypto (#14811)
- BTHome/Xiaomi BLE CCM decryption (#14816)
- DLMS meter GCM decryption (#14817)
Driver and API compatibility fixes across 20+ components:
- WiFi, ADC, LEDC, RMT (LED strips, remote transmitter/receiver), MIPI DSI, PSRAM, I2S audio, I2C, deep sleep, MQTT, USB host, OpenTherm timer, ethernet, debug, and camera components all updated for IDF 6.0 API changes
Toolchain compatibility:
- PicoLibC
std::isnanconflict resolved (#14768) vfprintfprintf stub fixed for picolibc (#15172)- Implicit
int-to-gpio_num_tconversions fixed for GCC 15 (#14830) - Legacy I2S driver removed in preparation for IDF 6.0 where it no longer exists (#14932)
Substitution System Overhaul
Section titled “Substitution System Overhaul”Led by @jpeletier, the YAML configuration system receives two major improvements that make complex, multi-file configurations far more powerful and faster to load.
Substitutions in !include paths (#12213): You can now use substitution variables and Jinja expressions directly in !include filenames, enabling dynamic file selection:
substitutions: eth_model: LAN8720
packages: - !include network/${eth_model}/config.yamlThis works with command-line substitutions too, allowing the same YAML to target different hardware configurations without duplicating files.
18x faster config loading (#14918): The substitution engine has been redesigned to perform all variable substitutions in a single linear pass instead of the previous multi-pass approach. This achieves up to 18x faster config load times for large projects with many packages and includes. The rewrite also fixes several edge cases where cross-file variable references (${A * B} mixing local and root variables) were unreliable. Note that merge_config argument order for duplicate substituted keys has changed to “last writer wins.”
Ethernet Expansion
Section titled “Ethernet Expansion”Building on RP2040/RP2350’s first-class platform support in 2026.3.0, this release dramatically expands Ethernet support across platforms, adding 5 new chip types and bringing SPI Ethernet to the RP2040 platform for the first time. RP2040 boards like the WIZnet EVB-Pico series can now be used as wired IoT controllers without WiFi.
New RP2040 Ethernet Support:
- W5500 (#14820) - 100Mbps SPI Ethernet, stress tested with 344 successful API connections on a WIZnet W5500-EVB-Pico
- W5100/W5100S (#15131) - 10/100Mbps SPI Ethernet for boards like the W5100S-EVB-Pico
- W6100 and W6300 (#15543) - Next-generation WIZnet controllers with IPv6 support for boards like the W6300-EVB-Pico2
Cross-Platform Additions:
- ENC28J60 (#14945) - Microchip’s widely-available 10BASE-T controller now supported on both ESP32 (IDF) and RP2040
ESP32 Improvements:
- SPI interface selection (#10285) - New
interfaceconfig variable lets ESP32 users selectspi2orspi3host, resolving conflicts with display components on boards like the M5Stack CoreS3
GPIO Expander Interrupt Support
Section titled “GPIO Expander Interrupt Support”The PCF8574, PCA9554, MCP23008/MCP23017, MCP23S08/MCP23S17, PI4IOE5V6408, MCP23016, PCA6416A, and TCA9555 GPIO expander components now support an optional interrupt_pin that eliminates I2C/SPI polling entirely (#15444, #15445, #15616, #15614, #15613).
Supported Expanders:
- PCF8574, PCA9554, PCA6416A, TCA9555 - I2C GPIO expanders
- MCP23008, MCP23017, MCP23S08, MCP23S17, MCP23016 - I2C and SPI GPIO expander family
- PI4IOE5V6408 - I2C GPIO expander
Measured Impact (ESP32-IDF, PCF8574 + 3 binary sensors):
| Metric | Polling | Interrupt | Improvement |
|---|---|---|---|
| I2C reads/min (idle) | 4100ms | 12ms | 99.7% reduction |
| Loop iterations per gate cycle | 7477 | 16 | 468x fewer |
| Binary sensor read time | ~0.6ms (I2C) | ~0.002ms (cache) | 300x faster |
When configured, binary sensors return from cache (no bus reads) between interrupts, and the main loop is completely disabled until a pin change occurs. The MCP23x17 automatically enables the MIRROR bit so a single interrupt wire catches changes on all 16 pins.
pcf8574: id: my_pcf8574 interrupt_pin: GPIO16Performance Benchmarking with CodSpeed
Section titled “Performance Benchmarking with CodSpeed”This release introduces comprehensive C++ benchmarking infrastructure using CodSpeed, enabling continuous performance tracking and regression prevention. While initially added to protect the gains from 2026.3.0, the benchmarks quickly revealed remaining bottlenecks and drove a wave of targeted optimizations throughout this release cycle.
Benchmarks added:
- Protobuf encoding/decoding (API messages, BLE advertisements, ListEntities)
- Noise encryption handshakes and payload encryption
- Sensor, binary sensor, light, cover, climate, fan, number, select, switch, text_sensor, and button publish/call paths
- Scheduler interval and timeout operations
- Plaintext and encrypted API frame writes
API layer optimizations (the native API is the primary communication path between ESPHome devices and Home Assistant). The goal was to make Bluetooth Proxy lightweight enough that every ESP32-C3 can run it without worrying about performance impact, and this release delivers: combined with changes from 2026.3.0, the API and BLE proxy advertisement forwarding (the constant hot path) now consume only ~1.8% of main loop runtime on ESP32-C3, down from ~3.3% in 2026.3.0 and a fraction of what it was a year ago:
- Protobuf encoding: 17-20% faster with register-optimized write path (#15290)
- Precomputed tag bytes: Varint and length-delimited field tags computed at compile time (#15067)
- Frame write hot path: Peeled first write iteration, inlined socket writes, zero-gap batch encoding. Single sensor state writes 72% faster, batch writes 21% faster, stack frame reduced from 352 to 48 bytes (-86%) (#15063)
- Varint encoding optimized: Plaintext varint encoding and write_protobuf_packet devirtualized (#14758)
- Command dispatch devirtualized: Eliminates virtual call overhead on every incoming API message (#15044)
- Protobuf decode non-virtual: ProtoDecodableMessage::decode() devirtualized (#15076)
- Constant-size varint codegen:
max_valueproto option eliminates variable-length encoding for bounded fields (#15424) - Entity name/object_id optimized:
max_data_lengthproto option for known-bounded string fields (#15426) - Fixed32 decode: Uses memcpy on little-endian platforms instead of byte-shifting (#15292)
- Raw tag+value writes: Forced fixed32 key fields emit raw bytes (#15051)
- Float zero checks: Integer comparison instead of floating-point (#15490)
- Code size reduction: Buffer and nodelay optimizations (#14797)
- Overflow buffer extracted: Frame helper buffer management simplified (#14871)
- Noise encryption speedup: Continuing from the ChaCha20-Poly1305 optimization in 2026.3.0 (which achieved 8-32% faster encryption), this release enables
HAVE_WEAK_SYMBOLSandHAVE_INLINE_ASMfor libsodium, replacing a volatile byte-by-bytesodium_memzero()fallback with word-sizedmemset(). Small messages (the common case for sensor updates and service calls) see the biggest gains: 24% faster on ESP32, 11% on ESP8266, 10% on RP2040 (#15038) - Noise handshake: Split state_action_ to reduce stack pressure (#15464)
- PSK update: Avoided heap allocation in timeout lambda (#14921)
- Batch encoding: Simplified encode_to_buffer to single resize call, inlined DeferredBatch::add_item (#15355, #15353)
- Enum auto-derivation: Auto-derive max_value for enum fields in protobuf codegen (#15469)
- Sub-message inlining: New
(inline_encode)proto option inlines small sub-messages directly into parent encoders, eliminating function pointer indirection. Applied toBluetoothLERawAdvertisement(encoded up to 12 times per BLE batch): 40-55% faster BLE advertisement encoding (#15599) speed_optimizedhot paths: New proto option emits__attribute__((optimize("O2")))on specific encode paths so they remain inlined even under firmware’s default-Osflag. Applied toSensorStateResponse,BluetoothLERawAdvertisementsResponse, andSubscribeLogsResponse: BLE advertisementcalculate_sizeis an additional 41% faster, with similar gains for sensor state and log encoding (#15691, #15698)
Core framework optimizations:
- Scheduler: Integer math for interval offset calculation, reschedules fired intervals directly into the heap, early-exit cancel path, and inlined fast-path checks (#14755, #15516, #14902, #14905)
- Main loop inlined:
Application::loop()andcalculate_looping_components_inlined to eliminate stack frames (#15041, #14944) - std::bind eliminated: Replaced across 20+ components with lambdas to fit within small-buffer optimization (#14961 and related PRs)
- Automation call chain collapsed: The
Trigger::trigger()→Automation::trigger()→ActionList::play()forwarding chain is force-inlined, collapsing 3 stack frames into 1. Combined with ControllerRegistry dispatch inlining andcall_loop_removal, a button→lambda automation went from 16 stack frames to 10, significantly reducing stack overflow risk on devices with deep automation chains (#15042, #15173, #14931) - IfAction compile-time optimization:
HasElseparameter eliminates the else-branch check at compile time when no else block is configured (#15134) - Logger: Reduced per-message overhead by inlining hot path helpers (#14851)
- errno caching: Avoids duplicate
__errno()calls across API, async_tcp, captive_portal, and web_server_idf (#14751) - Loop elimination: Many components were running their
loop()every single main loop iteration just to check a condition or timestamp. These have been converted to event-driven patterns that only run when needed:web_server: Loop completely disabled when no SSE clients are connected; most devices have zero browser clients 99%+ of the time (#15428)total_daily_energy: Replaced loop() with a single timeout that fires at midnight (#15432)CronTrigger: Replaced loop() withset_interval(#15433)preferences: Loop compiled out entirely whenflash_write_intervalis non-zero (#14943)- GPIO expanders (PCF8574, PCA9554, MCP23xxx, PI4IOE5V6408): Loop disabled when all pins are outputs (#15455, #15460)
sx127x,cc1101,sx126x: Use GPIO interrupt to wake the loop instead of polling, with loop disabled entirely when packet mode is inactive (#15627, #15606)hbridgelight: Loop only runs when both cold and warm white channels are active and multiplexing is needed; disabled for single-channel or off states (#15615)
- Bluetooth Proxy: BLE event handler dispatch devirtualized, eliminating virtual call overhead on every BLE advertisement and GATT event (#15310). BLE
is_active/is_runninginlined and STL bloat removed (#14875). Advertisement flushing replacedloop()(which ran ~7,400 times/60s just to check a timestamp) withset_interval(100ms)so the flush logic only runs when needed (~600 times/60s), eliminating ~6,800 wasted iterations per minute (#15347) - Devirtualization: Preferences backend, PollingComponent::set_update_interval (#14825, #14938)
- Light: Reciprocal multiply in normalize_color, pass LightTraits to avoid redundant virtual calls (#15401, #15403)
CodSpeed runs on every pull request and push to dev, providing a continuous baseline that catches regressions before they ship. It was CodSpeed’s benchmark visualization that revealed vsnprintf was consuming 95%+ of sensor publish time, leading to the client-side state logging change that achieved a 46x speedup on the sensor publish path. Visit codspeed.io/esphome/esphome to explore the benchmark results. Thank you to CodSpeed for providing their service free to open source projects.
Memory Optimizations
Section titled “Memory Optimizations”Driven by the increased visibility from benchmarking and memory analysis tooling, this release includes significant RAM and flash savings across the framework.
Component allocations moved from heap to BSS (#15079): This is a major architectural change; all component instances (new_Pvariable) are now allocated via placement new into statically-sized BSS storage instead of individual heap allocations. Previously, every component, sensor, automation, and filter was a separate new call, fragmenting the heap over time. Now these allocations are packed into a single contiguous BSS region, freeing heap space for WiFi, BLE, and network buffers that genuinely need dynamic allocation. This eliminates hundreds of small heap allocations per device and significantly reduces heap fragmentation on long-running devices.
ESPHome’s CI automatically runs a home-grown memory impact analysis on every pull request, showing contributors the exact flash and RAM impact of their changes before merging. Previously, component memory was invisible to this tooling because it was all allocated at runtime on the heap. Once the BSS change landed, every component’s memory became a statically-visible symbol that the CI could measure and attribute. The analyze-memory tool was extended to attribute placement new storage symbols back to their owning components (#15092), making it easy to spot which components still had room for improvement. This visibility drove the wave of targeted optimizations below.
Core framework savings:
- Component base class: Shrunk from 12 to 8 bytes per instance, saving ~200-400 bytes across typical configs (#15103)
- CallbackManager: Replaced
std::function(16 bytes) with lightweightCallback(8 bytes) across all callback registrations. Small lambdas like[this]are now stored inline in the function pointer context with zero heap allocation, covering 96% of all callback registrations. Measured +488 bytes free heap on an ESP8266 config with MQTT + web_server + many entities (#14853). A follow-up replacedstd::vectorwith a trivial-copy container, eliminating per-signature template instantiations of vector reallocation machinery and saving 996 bytes of flash on ESP8266 plus 4 bytes of RAM per CallbackManager instance (#15272) - Trigger trampolines eliminated across 23 components: Previously, every
on_press,on_value,on_state, etc. automation instantiated a separate Trigger object just to forward a callback, a 4+ byte heap-allocated object that existed solely to hold a pointer. A newbuild_callback_automation()pattern collapses the trigger into a pointer-sized forwarder struct stored inline in the callback manager, eliminating the separate allocation entirely. The initial PR (#15174) migrated the core entity types (button, sensor, binary_sensor, switch, text_sensor, number, event) saving 88-208 bytes RAM, then 22 follow-up PRs migrated alarm_control_panel, lock, media_player, ld2450, rtttl, online_image, rotary_encoder, dfplayer, hlk_fm22x, pn532, pn7150/pn7160, sim800l, fingerprint_grow, ltr_als_ps, ltr501, nextion, ezo, haier, modbus_controller, rf_bridge, factory_reset, sml, and safe_mode - TemplatableValue → TemplatableFn: The function-pointer pattern was first proven on
LightControlAction, shrinking each instance from 128 to 72 bytes (44% reduction, saving 616 bytes RAM on an ESP8266 light config with 11 actions) (#15132). This was then generalized codebase-wide: ~340TEMPLATABLE_VALUEfields automatically dropped from 8 to 4 bytes each by storing function pointers instead of tagged unions (#15545) - ActionList: Removed
actions_end_pointer to save RAM per automation (#15283)
Entity and component savings:
- Binary sensor: Removed redundant
optional<bool>state, saving 8 bytes per instance (#15095) - Text sensor:
raw_callback_gated behindUSE_TEXT_SENSOR_FILTER, saving 4 bytes per instance when unused (#15097) - Light: Reordered LightState fields to eliminate padding (#15112)
- GPIO binary sensor: Packed fields and removed redundant
last_state_(#15113) - GPIO switch: Compiled out interlock fields when unused (#15111)
- Climate/Fan: Custom mode vectors stored on entity directly, eliminating heap allocation (#15206, #15209)
- WebServerBase: Reduced by 4 bytes (#15251)
- Runtime stats: Stored inline on Component, eliminating
std::maplookup (#15345)
Heap allocation elimination:
- Sensor filters:
OrFilter,CalibrateLinearFilter,CalibratePolynomialFilter,ValueList,FilterOut,ThrottleWithPrioritymigrated tostd::array(#15262, #15263, #15264, #15265) - Text sensor filters:
SubstituteFilterandMapFiltermigrated tostd::array(#15266, #15269) - Automation conditions:
And,Or,Xorconditions migrated tostd::array(#15282) - PID: Replaced
std::dequewithFixedRingBuffer(#14733) - Sensor sliding window: Replaced with
FixedRingBuffer(#14736)
ESP8266 RAM savings:
- API dump strings: Moved to PROGMEM (#14982)
- Logger: Log level lookup tables and task log buffer moved to PROGMEM/BSS (#15003, #15153)
Config Bundle for Remote Compilation
Section titled “Config Bundle for Remote Compilation”The new esphome bundle CLI command packages a YAML config and all its local dependencies (fonts, images, certificates, local external_components, secrets) into a self-contained .esphomebundle.tar.gz archive (#13791). This is the foundation for remote compilation support, enabling devices like the Home Assistant Green to offload compilation to a remote build server.
Key Features:
- Automatic dependency discovery - Finds all referenced files (YAML includes, fonts, images, animations, certificates, C++ includes, web assets)
- Secrets filtering - Only includes secrets actually referenced by bundled YAML files
- Deterministic archives - Sorted entries and zeroed timestamps for reproducibility
- Incremental builds - Preserves PlatformIO build caches across re-extractions
esphome bundle my_device.yaml # Create bundleesphome bundle my_device.yaml --list-only # Preview filesesphome compile my_device.esphomebundle.tar.gz # Compile from bundleLVGL v9 Upgrade
Section titled “LVGL v9 Upgrade”Led by @clydebarrow with 27 PRs, ESPHome’s LVGL integration has been upgraded from v8 to LVGL v9.5.0 (#12312). This is a major library migration that brings a new rendering pipeline, improved performance, and new widget capabilities. Existing configurations should compile and run, though some properties are deprecated.
Key changes:
- Native rotation with hardware acceleration - Rotation is now handled directly by LVGL instead of the display driver, with runtime rotation changes via
lvgl.display.set_rotationand automatic use of hardware rotation when available. On ESP32-P4, rotation uses the PPA (Pixel Processing Accelerator) for zero-CPU-cost transforms (#14955, #15453). - Automatic touchscreen rotation - Touch input is automatically rotated to match display orientation, eliminating the need for manual coordinate transforms in most cases.
Users will need to move their
rotationconfig from the display component to thelvgl:block, and remove or adjust thetransform:block in the touchscreen component. - Built-in dark theme - A single
dark_mode: trueundertheme:enables LVGL’s native dark default theme, replacing the tedious process of manually replicating dark theme properties (#15389) - All LVGL 9 events - Complete mapping of LVGL 9 events to ESPHome automation triggers (#15362)
- Drop shadow support - New
bitmap_mask_srcstyle property and A8 image format for drop shadow effects (#15334)
New Sensors and Hardware Support
Section titled “New Sensors and Hardware Support”This release adds support for several new sensors and hardware platforms:
- SPA06-003 by @danielkent-net - Goermicro digital pressure and temperature sensor with both I2C and SPI interfaces, available from Adafruit and Seeed Grove (#14521, #14522, #14523)
- HDC2080 by @G-Pereira - Texas Instruments temperature and humidity sensor (#9331)
- emonTx by @FredM67 - OpenEnergyMonitor emonTx/emonPi energy monitoring via UART serial bridge, with JSON triggers and command actions (#9027)
- BMP581 SPI support by @danielkent-net - The BMP581 pressure sensor now supports SPI in addition to I2C (#13124)
- BMP585 support by @nytaros - Adds the newer BMP585 ASIC ID to the BMP581 component (#15277)
- ESP32-C61 PSRAM - Quad mode PSRAM support at 40MHz and 80MHz (#14795)
- Internal temperature for nRF52 Zephyr by @Ardumine (#15297) and LN882H by @Bl00d-B0b (#15370)
Other Notable Features
Section titled “Other Notable Features”- Number-to-sensor and text-to-text_sensor bridges - New read-only sensor/text_sensor views of number/text components for use in automations and displays (#15125, #15090)
- ESP32 Hosted SPI transport - SPI transport and 1-bit SDIO bus width support for the
esp32_hostedWiFi offloading component (#15551) - Nextion custom protocol triggers -
on_custom_switch,on_custom_sensor,on_custom_text_sensor,on_custom_binary_sensortriggers for event-driven Nextion workflows without entities (#13248) - MQTT alarm panel JSON payloads - Alarm control panel command topic now accepts JSON payloads with PIN codes, e.g.
{"state": "DISARM", "code": "1234"}(#14731) - Media player enqueue action - New
media_player.enqueueaction for queueing media URLs from automations (#14775) - MIPI RGB optional sync pins -
hsync_pin/vsync_pinnow optional for displays like the BigTreeTech PandaTouch (#14870) - Brennenstuhl remote switches - New 433MHz rolling code protocol for Brennenstuhl comfort-line switches (#9407)
- DSMR thermal MBUS id - New
thermal_mbus_idoption for Warmtelink heat meters (#7519) - VBus DeltaSol CS4 - Support for Citrin Solar 1.3 controller (#12477)
- TM1637 buffer manipulation -
set_buffermethod for raw segment writes and custom glyphs (#13686) - HUB75 scan wiring - New
SCAN_1_8_32PX_FULLwiring option (#15130) */Ncron syntax - Time-based automations now support*/5style step expressions (#15434)- microFLAC audio decoding by @kahrendt - New FLAC decoder is 9% faster on ESP32-S3 with CRC validation re-enabled, plus true streaming support that eliminates unnecessary memory copies (#15372)
Mitsubishi CN105 Climate Component
Section titled “Mitsubishi CN105 Climate Component”Built by @crnjan across 5 PRs, the new mitsubishi_cn105 component brings native support for controlling Mitsubishi A/C units through the CN105 connector, one of the most requested climate integrations in ESPHome. The component implements the same CN105 protocol used by the popular SwiCago HeatPump project, ensuring compatibility with the same range of devices (#15315, #15358, #15437, #15462, #15483).
Key Features:
- Full climate control - Power, mode, target temperature, and fan speed can be controlled directly from Home Assistant
- Non-blocking protocol driver - Custom UART parser designed for performance and reliability
- Smart temperature polling - The
current_temperature_min_intervaloption rate-limits room temperature reads to prevent rapid oscillations near measurement boundaries while keeping a fastupdate_intervalfor responsive control - Broad platform support - Works on ESP32, ESP32 IDF, ESP8266, and RP2040/RP2350
climate: - platform: mitsubishi_cn105 name: "Air Condition" uart_id: ac_uart update_interval: 1s current_temperature_min_interval: 60sCodebase Correctness Sweep
Section titled “Codebase Correctness Sweep”Continuing the correctness sweep started in 2026.3.0, @swoboda1337 contributed another 99 bug fix and validation PRs in this release, addressing missing state_class and device_class on sensor schemas, incorrect config validation (wrong types, missing range checks, broken ensure_list usage), dead code removal, format specifier warnings, and codegen type mismatches across 100+ components. This sustained effort has significantly improved the reliability and correctness of the ESPHome codebase.
Thank You, Contributors
Section titled “Thank You, Contributors”This release includes 507 pull requests from over 40 contributors. A huge thank you to everyone who made 2026.4.0 possible:
- @swoboda1337 - 121 PRs including a correctness sweep across 100+ components, ESP-IDF 6.0 compatibility across dozens of components, and the ESP32 CPU frequency default change
- @clydebarrow - 27 PRs including the LVGL v9.5.0 migration, native rotation support, and display component improvements
- @edwardtfn - 14 PRs including Nextion custom protocol triggers, performance optimizations, and code quality improvements across the Nextion component
- @jpeletier - 10 PRs including the substitution system redesign for 18x faster config loading and
!includepath substitution support - @kbx81 - 10 PRs including signed OTA verification without hardware secure boot, IR receiver frequency metadata, and sensor code cleanups
- @kahrendt - 7 PRs including the media player enqueue action, microFLAC decoding, and audio pipeline improvements
- @danielkent-net - 5 PRs including the new SPA06-003 pressure/temperature sensor (I2C and SPI) and BMP581 SPI support
- @crnjan - 5 PRs building the new Mitsubishi CN105 climate component from scaffold to full read/write control
- @exciton - 4 PRs including modbus helper refactoring and integration tests
- @jesserockz - 4 PRs including CI improvements and unbounded percentage validators
- @diorcety - 4 PRs including git subpath support and compiler compatibility fixes
- @FredM67 - 2 PRs including the new emonTx energy monitoring component
- @tomaszduda23 - 2 PRs including debug peripherals status and Zigbee logging improvements
- @P4uLT - 2 PRs including PID FixedRingBuffer optimization
- @Kamilcuk - 2 PRs including placement new allocation for Pvariables
- @luar123 - custom ESP32 partition tables and partition table refactoring
- @G-Pereira - the new HDC2080 temperature and humidity sensor
Also thank you to @bdraco, @thomwiggers, @aanban, @rtyle, @Szewcson, @Pernotto, @mike1703, @tuct, @CFlix, @heythisisnate, @warthog618, @FWeinb, @droscy, @intcreator, @szupi-ipuzs, @fblaese, @glmnet, @nytaros, @Ardumine, @rguca, @Bl00d-B0b, @RAR, @Passific, and @Tomer27cz for their contributions, and to everyone who reported issues, tested pre-releases, and helped in the community.
Breaking Changes
Section titled “Breaking Changes”Platform Changes
Section titled “Platform Changes”-
ESP32 CPU frequency: Default CPU frequency changed to maximum supported per variant (ESP32/S2/S3/C5: 160MHz to 240MHz). This improves performance ~33% but increases power consumption. Battery-powered devices should add
cpu_frequency: 160MHZto their config. #15143 -
ESP32 partition table: Partition layout changed for both Arduino and IDF frameworks. NVS moved to end of flash with increased size (20KB to 384KB on Arduino). Existing devices will get the new partition table on next OTA update. NVS data is preserved since the library finds it by name, not position. #7682
-
ESP8266 RTC preferences: RTC preferences capacity reduced from 96 to 78 words to accommodate the new crash handler. Overflow goes to flash. Very unlikely to affect real configs. #15465
Component Changes
Section titled “Component Changes”-
LVGL rotation: The
rotationoption must now be configured underlvgl:instead of the display component. LVGL now handles rotation for the display and touchscreen directly, with support for runtime rotation changes vialvgl.display.set_rotation. Users must move theirrotationsetting from the display config to thelvgl:block. #14955 and remove or adjust thetransform:block in their touchscreen config if they have one. -
I2S Audio: The legacy I2S driver (
use_legacyoption) has been removed. The new I2S driver is always used. Thei2s_audiomedia player sub-component has been removed entirely; users should migrate to the speaker media player component. #14932 -
Binary Sensor multi_click:
on_multi_clicktiming sequences are now limited to 255 entries (previously unlimited). No real config would approach this limit. #15267 -
Binary Sensor autorepeat: Autorepeat timing lists are now limited to 254 entries (previously unlimited). No real config would approach this limit. #15268
Configuration System Changes
Section titled “Configuration System Changes”- Substitutions: When two YAML keys in the same dict resolve to the same substituted name, the later-appearing key’s value now takes precedence (“last writer wins”). Previously, the existing resolved value took precedence on dict merges. This only affects the edge case of duplicate substituted keys. #14918
Undocumented API Changes
Section titled “Undocumented API Changes”Several internal C++ APIs have changed in this release. While these are not part of the public API, they may affect users who write lambdas or maintain external components.
Component and Preferences Infrastructure
Section titled “Component and Preferences Infrastructure”-
Component:
set_component_source(const LogString *)removed and replaced with protectedset_component_source_(uint8_t). Thewarn_if_blocking_over_field changed fromuint16_t(milliseconds) touint8_t(centiseconds). Component base class shrunk from 12 to 8 bytes. #15103 -
Preferences:
ESPPreferenceBackendandESPPreferencesabstract base classes removed. They are now type aliases to concrete platform-specificfinalclasses. All existing code usingglobal_preferencesorESPPreferenceObjectcontinues to work unchanged. #14825
Automation Trigger Changes
Section titled “Automation Trigger Changes”- Trigger classes removed:
ButtonPressTrigger,SensorStateTrigger,SensorRawStateTrigger,PressTrigger,ReleaseTrigger,StateTrigger,StateChangeTrigger,SwitchStateTrigger,SwitchTurnOnTrigger,SwitchTurnOffTrigger,TextSensorStateTrigger,TextSensorStateRawTrigger,NumberStateTrigger, andEventTriggerare no longer instantiated. Lambdas usingtrigger_idto access these objects will break. UseScriptfor action control instead. TheAutomation::trigger_protected field was also removed. #15174
TemplatableValue Changes
Section titled “TemplatableValue Changes”- TemplatableValue for non-string types: No longer accepts stateful lambdas (lambdas with captures). No external usage was found. If you need a stateful callable, use
std::function<T(X...)>directly. TheTEMPLATABLE_VALUEmacro now usesTemplatableFn(4 bytes) for trivially copyable types. #15545
Component-Specific Changes
Section titled “Component-Specific Changes”-
Nextion:
get_wave_chan_id()deprecated in favor ofget_wave_channel_id()(removal in 2026.10.0). Getter methods now return byconstreference instead of by value.get_queue_type_string()returnsconst char *instead ofstd::string. Waveform-related methods are now gated behindUSE_NEXTION_WAVEFORMand may not be available if no waveform sensors are configured. #15204, #15273 -
ATM90E32:
get_phase_angle_()return type changed fromuint16_ttofloat(fixes precision loss).last_periodic_millismember removed (was never used). #15238 -
AS5600: Dead
angle_sensor_,raw_angle_sensor_,position_sensor_members and their setters removed (were never wired to config schema). #15254 -
SEN5x:
voc_baselineconfig option removed (was never wired to C++). Usestore_baseline: trueinstead. #15391 -
Graph:
legendconfig no longer accepts a list (only the first element was ever used). Single-value configs continue to work. #15522 -
Haier:
control_methodconfig no longer accepts a list. Single-value configs continue to work. #15523 -
RP2040 PIO LED Strip:
CUSTOMremoved fromchipsetenum (it crashed at compile time). Use explicit timing parameters (bit0_high,bit0_low,bit1_high,bit1_low) instead. #15537
Breaking Changes for Developers
Section titled “Breaking Changes for Developers”- CallbackManager:
std::functionreplaced with lightweightCallbacktype. Existing code continues to work but external components should migrate callback registration methods fromstd::functionparameters to templates for optimal performance (non-template methods fall back to a heap-allocation path). #14853 - PollingComponent:
set_update_interval()is now non-virtual. Subclasses overriding this method should use alternative patterns. #14938 - UART FlushResult:
FlushResultrenamed toUARTFlushResultwithUART_FLUSH_RESULT_prefix for enum values. #15101 - Sensor raw_state:
.raw_stateis deprecated.raw_callback_is now gated behindUSE_SENSOR_FILTER, saving RAM on every sensor instance when no filters are configured. #15094 - Trigger trampolines eliminated: Trigger objects for common entity automations (button, sensor, binary_sensor, switch, text_sensor, number, event) are no longer instantiated.
build_automation()and Trigger subclasses remain available. #15174 - TemplatableFn:
TEMPLATABLE_VALUEmacro now uses 4-byteTemplatableFnfor trivially copyable types. External components calling macro-generated setters with raw C++ constants instead of going throughcg.templatable()will fail to compile. #15545 - Climate/Fan custom modes: Custom mode/preset vectors are now stored on the entity instead of heap-allocated. Constructors for climate and fan traits have changed. #15206, #15209
- Trigger migrations to callback automation:
alarm_control_panel,lock, andmedia_playertriggers now use callback automation pattern. #15198, #15199, #15200 - BLE event dispatch: BLE event handler dispatch is devirtualized. #15310
- wake_loop: Moved from socket component into core. #15446
- Preferences devirtualized:
ESPPreferenceBackendandESPPreferencesare now type aliases to concrete platform-specific classes (no virtual base). #14825 - Component shrunk from 12 to 8 bytes:
set_component_source(const LogString *)removed and replaced withset_component_source_(uint8_t)(protected). #15103 - AS5600: Dead
angle,raw_angle, andpositionsensor code removed. #15254 - Modbus helpers: Shared helper functions refactored across modbus components. #15291, #14172
For detailed migration guides and API documentation, see the ESPHome Developers Documentation.
Release 2026.4.1 - April 20
Section titled “Release 2026.4.1 - April 20”- [core] Fix app_state_ status bits clobbered for non-looping components esphome#15658 by @bdraco
- [core] Inline feed_wdt hot path with out-of-line slow path esphome#15656 by @bdraco
- [st7789v] Fix swapped offset_width/offset_height in model presets esphome#15755 by @swoboda1337
- [sx126x][sx127x] Fix frequency precision loss from float32 codegen esphome#15753 by @swoboda1337
- Bump aioesphomeapi from 44.15.0 to 44.16.0 esphome#15757 by @dependabot[bot]
- [lvgl] Clean the build if lv_conf.h changes esphome#15777 by @clydebarrow
- [mitsubishi_cn105] use HEAT_COOL mode to enable temperature slider esphome#15748 by @crnjan
- [core] Diagnose missing cg.templatable in codegen for TEMPLATABLE_VALUE fields esphome#15758 by @bdraco
- [time] Fix RTC is_valid() rejecting valid times after day_of_year cleanup esphome#15763 by @bdraco
- [qmc5883l] Move per-update log line from DEBUG to VERBOSE esphome#15781 by @swoboda1337
- [bundle] Force-resolve nested IncludeFile during file discovery esphome#15762 by @bdraco
- [ili9xxx] Guard against null buffer in display_() when allocation fails esphome#15786 by @bdraco
- [esp32] Accept unquoted minimum_chip_revision values esphome#15785 by @swoboda1337
- [lvgl] Guard lv_image_set_src wrapper with LV_USE_IMAGE esphome#15789 by @swoboda1337
- [mipi_spi] Drawing fixes for native display esphome#15802 by @clydebarrow
- [image] Fix byte order handling esphome#15800 by @clydebarrow
- [core] coerce set_interval(0) / update_interval: 0ms to 1ms esphome#15799 by @bdraco
- [ethernet] Fix SPI3_HOST default breaking compile on variants without SPI3 esphome#15809 by @swoboda1337
- [core] Fix DelayAction compile error with non-const reference args esphome#15814 by @bdraco
- [esp32] Downgrade unneeded
ignore_pin_validation_errorto a warning esphome#15811 by @swoboda1337 - [runtime_image] Fix RGB order esphome#15813 by @clydebarrow
- [lvgl] Fix crash with snow on rotated display esphome#15822 by @clydebarrow
- [core] Feed WDT unconditionally in main loop to fix empty-config panic esphome#15830 by @bdraco
- Bump aioesphomeapi from 44.16.0 to 44.16.1 esphome#15836 by @dependabot[bot]
- [core] Default PollingComponent() to 1ms when codegen is bypassed esphome#15831 by @bdraco
- [substitutions] Fix
substitutions: !include file.yamlregression esphome#15850 by @bdraco - [packages] Improve error messages with include stack and fix missing path propagation esphome#15844 by @jpeletier
Release 2026.4.2 - April 23
Section titled “Release 2026.4.2 - April 23”- [lvgl] Fix format of hello world page esphome#15868 by @clydebarrow
- [lvgl] Fix overloads for setting images on styles esphome#15864 by @clydebarrow
- [lvgl] Fix update of textarea attached to keyboard esphome#15866 by @clydebarrow
- [image] Fix rodata bloat for multi-frame RGB565+alpha animations esphome#15873 by @swoboda1337
- [core] Fix Pvariable placement new losing subclass identity esphome#15881 by @swoboda1337
- [ld2412] Fix null deref in set_basic_config when entities unconfigured esphome#15893 by @swoboda1337
- [esp32] Add Secure Boot V1 ECDSA signing scheme for pre-rev-3.0 ESP32 esphome#15882 by @kbx81 (new-feature)
- [io_expanders] Self-heal interrupt-driven expanders when INT stays asserted across the read esphome#15923 by @swoboda1337
- [usb_uart] Derive TX output chunk count from
buffer_sizeconfig esphome#15909 by @kbx81
Release 2026.4.3 - April 28
Section titled “Release 2026.4.3 - April 28”- [lvgl] Triggers on tabview tabs fix esphome#15935 by @clydebarrow
- [time] Handle Windows EINVAL when validating POSIX TZ strings esphome#15934 by @jesserockz
- [deep_sleep] Fix sleep_duration codegen type to uint32_t esphome#15965 by @bdraco
- [wifi] Fix stale wifi.connected after state transition esphome#15966 by @bdraco
- [rotary_encoder][at581x] Fix templatable int field types esphome#16015 by @bdraco
- [esp32][wifi] Fix bootloop and WiFi connection issue if nvs partition is missing or has non-default label esphome#16025 by @Mat931
- [nextion] Unify TFT upload ack timeout to 5000ms esphome#15960 by @edwardtfn
- [esp32_touch] Feed wdt esphome#16066 by @jesserockz
- [image] Fix RGB565+alpha rendering for multi-frame animations esphome#16017 by @bdraco
Release 2026.4.4 - May 5
Section titled “Release 2026.4.4 - May 5”- [automation] Fix codegen type for component.resume update_interval esphome#16069 by @bharvey88
- [mcp23xxx_base] Reject unsupported interrupt_pin options (inverted, allow_other_uses) esphome#16149 by @bdraco
- [core] Strip \?\ prefix from sys.executable for PlatformIO subprocess esphome#16158 by @jesserockz
- [esp32] Replace 512B stack buffer in printf wraps with picolibc cookie FILE esphome#16170 by @bdraco
- [lvgl] Clamp values for meter line indicators esphome#16180 by @clydebarrow
- [esp32] Drop printf wrap on IDF 6.0+ (picolibc no longer needs it) esphome#16189 by @bdraco
- [api] Fall back to owning types for service array args used after a delay esphome#16140 by @bdraco
- [api] Use safe_print for log output and fix safe_print bytes-repr fallback esphome#16160 by @jesserockz
Release 2026.4.5 - May 7
Section titled “Release 2026.4.5 - May 7”- [ha-addon] Add opt-in toggle for the new ESPHome Device Builder esphome#16247 by @jesserockz
- [bundle] Include secrets.yaml when
!secretkeys are quoted esphome#16271 by @bdraco - [substitutions] Fix sibling references inside dict-valued substitutions esphome#16273 by @bdraco
- [core] Fix WiFi connection in safe mode esphome#16269 by @Mat931
- [nextion] Fix text sensor state not updated on string response esphome#16280 by @edwardtfn
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.4.0 changelog.





Comments