<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>ESPHome Developer Documentation | Blog</title><description>Documentation for developers contributing to and building components for ESPHome.</description><link>https://developers.esphome.io/</link><language>en</language><item><title>remote_base Protocol Methods Are No Longer Virtual and Receiver Lists Are Sized From Codegen</title><link>https://developers.esphome.io/blog/2026/10/14/remote_base-protocols-non-virtual-and-receiver-lists-sized-from-codegen/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/10/14/remote_base-protocols-non-virtual-and-receiver-lists-sized-from-codegen/</guid><pubDate>Wed, 14 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;RemoteProtocol&amp;#x3C;T&gt;&lt;/code&gt; no longer declares &lt;code dir=&quot;auto&quot;&gt;encode()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;decode()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;dump()&lt;/code&gt; as virtual, the receiver’s listener and dumper lists are fixed size &lt;code dir=&quot;auto&quot;&gt;StaticVector&lt;/code&gt;s sized during code generation, and a protocol’s source file is only compiled when the configuration uses it. External components that derive from &lt;code dir=&quot;auto&quot;&gt;RemoteProtocol&lt;/code&gt;, that register listeners or dumpers from C++, or that use a protocol class from their own C++ need small changes.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;developer breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.10.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/19084&quot;&gt;PR #19084&lt;/a&gt;: Make protocol methods non-virtual and size receiver lists from codegen&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every protocol class was derived from &lt;code dir=&quot;auto&quot;&gt;RemoteProtocol&amp;#x3C;T&gt;&lt;/code&gt;, but nothing ever used one through a base pointer: dumpers, triggers, binary sensors and transmit actions are all templates on the concrete protocol type. The virtual methods only cost a vtable per protocol and kept the linker from dropping the &lt;code dir=&quot;auto&quot;&gt;encode()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;decode()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;dump()&lt;/code&gt; bodies a build never calls. The receiver kept its listeners and dumpers in &lt;code dir=&quot;auto&quot;&gt;std::vector&lt;/code&gt;s that grew on the heap during setup, even though code generation knows exactly how many there are. The CI memory report on the PR measured 14.6 KB less flash on the ESP8266 test build with a receiver and the ir_rf_proxy platforms, and 11.9 KB less on the ESP32 IDF build; the rc_switch protocol table also moves from RAM into flash.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;why-a-clean-break&quot;&gt;Why a clean break&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Every protocol added to &lt;code dir=&quot;auto&quot;&gt;remote_base&lt;/code&gt; has cost every user of the component, whether their configuration used it or not: with the methods virtual and every source file always compiled, each protocol’s vtable, its &lt;code dir=&quot;auto&quot;&gt;encode()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;decode()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;dump()&lt;/code&gt; bodies and its dumper and trigger instantiations all stayed in the image. There are 35 protocols in tree and more arrive most releases, so that per-protocol tax kept growing for every &lt;code dir=&quot;auto&quot;&gt;remote_receiver&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;remote_transmitter&lt;/code&gt; user, most of whom use one or two protocols. Making the methods non-virtual and compiling only the requested sources removes the tax. There is no way to keep &lt;code dir=&quot;auto&quot;&gt;override&lt;/code&gt; compiling on a method that is no longer virtual, so a deprecation window is not possible and the change lands as a clean break with this guide.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Four things, each with its own migration step below:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;RemoteProtocol&amp;#x3C;T&gt;&lt;/code&gt; only supplies the &lt;code dir=&quot;auto&quot;&gt;ProtocolData&lt;/code&gt; alias and no longer declares the protocol methods. &lt;code dir=&quot;auto&quot;&gt;encode()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;decode()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;dump()&lt;/code&gt; are plain member functions, checked by C++20 concepts wherever a protocol is used.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;RemoteReceiverBase::register_listener()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;register_dumper()&lt;/code&gt; only accept a registration when code generation counted a slot for it; with no slot counted the call fails a &lt;code dir=&quot;auto&quot;&gt;static_assert&lt;/code&gt;. A registration from C++ &lt;code dir=&quot;auto&quot;&gt;setup()&lt;/code&gt; has no slot.&lt;/li&gt;
&lt;li&gt;A protocol’s &lt;code dir=&quot;auto&quot;&gt;*_protocol.cpp&lt;/code&gt; in &lt;code dir=&quot;auto&quot;&gt;remote_base&lt;/code&gt; is only compiled when something in the configuration requests it: a dumper, a trigger, a binary sensor, a transmit action, or an explicit request from a component’s &lt;code dir=&quot;auto&quot;&gt;to_code()&lt;/code&gt;. A protocol that ships in your own component is compiled as before.&lt;/li&gt;
&lt;li&gt;The protected &lt;code dir=&quot;auto&quot;&gt;call_listeners_()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;call_dumpers_()&lt;/code&gt; on &lt;code dir=&quot;auto&quot;&gt;RemoteReceiverBase&lt;/code&gt; are merged into &lt;code dir=&quot;auto&quot;&gt;call_listeners_dumpers_()&lt;/code&gt;, which already existed.&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;External components that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;derive a protocol class from &lt;code dir=&quot;auto&quot;&gt;remote_base::RemoteProtocol&amp;#x3C;T&gt;&lt;/code&gt; and mark its methods &lt;code dir=&quot;auto&quot;&gt;override&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;call &lt;code dir=&quot;auto&quot;&gt;register_listener()&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;register_dumper()&lt;/code&gt; on a remote receiver from C++&lt;/li&gt;
&lt;li&gt;use a protocol class bundled in &lt;code dir=&quot;auto&quot;&gt;remote_base&lt;/code&gt;, such as &lt;code dir=&quot;auto&quot;&gt;remote_base::NECProtocol&lt;/code&gt;, from their own C++ without a dumper, trigger, binary sensor or transmit action for it in the configuration&lt;/li&gt;
&lt;li&gt;call &lt;code dir=&quot;auto&quot;&gt;encode()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;decode()&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;dump()&lt;/code&gt; through a &lt;code dir=&quot;auto&quot;&gt;RemoteProtocol&amp;#x3C;T&gt;&lt;/code&gt; pointer or reference; the base no longer declares them, so the error is &lt;code dir=&quot;auto&quot;&gt;no member named &apos;decode&apos;&lt;/code&gt; rather than a message about &lt;code dir=&quot;auto&quot;&gt;override&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Standard YAML configurations are not affected&lt;/strong&gt;, with one exception: a lambda that calls &lt;code dir=&quot;auto&quot;&gt;id(tx).transmit&amp;#x3C;remote_base::NECProtocol&gt;(data)&lt;/code&gt; needs that protocol referenced somewhere else in the configuration, see below.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;protocol-classes-drop-override&quot;&gt;Protocol classes: drop &lt;code dir=&quot;auto&quot;&gt;override&lt;/code&gt;&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The methods are no longer virtual, so the compiler reports each &lt;code dir=&quot;auto&quot;&gt;override&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; MyProtocol : &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; RemoteProtocol&amp;#x3C;MyData&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;public:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;encode&lt;/span&gt;&lt;span&gt;(RemoteTransmitData &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;&lt;span&gt;dst&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; MyData &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;optional&amp;#x3C;MyData&gt; &lt;/span&gt;&lt;span&gt;decode&lt;/span&gt;&lt;span&gt;&lt;span&gt;(RemoteReceiveData &lt;/span&gt;&lt;span&gt;src&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dump&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; MyData &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; MyProtocol : &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; RemoteProtocol&amp;#x3C;MyData&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;public:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;encode&lt;/span&gt;&lt;span&gt;(RemoteTransmitData &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;&lt;span&gt;dst&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; MyData &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;optional&amp;#x3C;MyData&gt; &lt;/span&gt;&lt;span&gt;decode&lt;/span&gt;&lt;span&gt;&lt;span&gt;(RemoteReceiveData &lt;/span&gt;&lt;span&gt;src&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dump&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; MyData &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The signatures are unchanged. The &lt;code dir=&quot;auto&quot;&gt;RemoteProtocolDecoder&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;RemoteProtocolDumper&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;RemoteProtocolEncoder&lt;/code&gt; concepts check them wherever the protocol is used, so a mismatch is reported at the &lt;code dir=&quot;auto&quot;&gt;DECLARE_REMOTE_PROTOCOL(...)&lt;/code&gt; line, normally in the protocol’s own header; a protocol declared without that macro sees it at its first use.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;listeners-and-dumpers-register-from-python&quot;&gt;Listeners and dumpers: register from Python&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The receiver’s lists are sized during code generation. A component that registered itself from C++ has no slot:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before, in setup()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;receiver_&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;register_listener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Register from &lt;code dir=&quot;auto&quot;&gt;to_code()&lt;/code&gt; instead, so the slot is counted. For a component whose schema uses &lt;code dir=&quot;auto&quot;&gt;CONF_RECEIVER_ID&lt;/code&gt;, one call does both halves:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; esphome.components &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; remote_base&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;to_code&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;var &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cg.&lt;/span&gt;&lt;span&gt;new_Pvariable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;CONF_ID&lt;/span&gt;&lt;span&gt;])&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; cg.&lt;/span&gt;&lt;span&gt;register_component&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; config&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# sets the receiver on the entity and registers it as a listener&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; remote_base.&lt;/span&gt;&lt;span&gt;attach_receiver&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; config&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;attach_receiver(var, config, key=...)&lt;/code&gt; accepts a different config key, &lt;code dir=&quot;auto&quot;&gt;remote_base.register_listener(var, config)&lt;/code&gt; registers without calling &lt;code dir=&quot;auto&quot;&gt;set_receiver()&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;remote_base.add_listener(receiver, var)&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;remote_base.add_dumper(receiver, dumper)&lt;/code&gt; take the receiver object directly. The slot counting lives in these helpers: a hand written &lt;code dir=&quot;auto&quot;&gt;cg.add(receiver.register_listener(var))&lt;/code&gt; emits the call but counts no slot, so it fails the same way the C++ registration does.&lt;/p&gt;
&lt;p&gt;When a configuration counted no slot at all, a C++ registration fails the build with a message naming this fix; when the counted slots are already used, the registration is dropped and the same message is logged as an error at boot, so the listener or dumper never receives data. Check the boot log for &lt;code dir=&quot;auto&quot;&gt;No listener slot&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;No dumper slot&lt;/code&gt; after migrating.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;bundled-protocols-used-from-c-request-the-source-file&quot;&gt;Bundled protocols used from C++: request the source file&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;A component that uses one of the protocol classes bundled in &lt;code dir=&quot;auto&quot;&gt;remote_base&lt;/code&gt; directly in C++, with no dumper, trigger, binary sensor or transmit action for it in the configuration, must keep the source file in the build:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# in to_code()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;remote_base.&lt;/span&gt;&lt;span&gt;request_protocol&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;coolix&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The name is the protocol’s source file without the &lt;code dir=&quot;auto&quot;&gt;_protocol.cpp&lt;/code&gt; suffix: &lt;code dir=&quot;auto&quot;&gt;NECProtocol&lt;/code&gt; lives in &lt;code dir=&quot;auto&quot;&gt;nec_protocol.cpp&lt;/code&gt;, so it is &lt;code dir=&quot;auto&quot;&gt;&quot;nec&quot;&lt;/code&gt;; &lt;code dir=&quot;auto&quot;&gt;RCSwitchBase&lt;/code&gt; lives in &lt;code dir=&quot;auto&quot;&gt;rc_switch_protocol.cpp&lt;/code&gt;, so it is &lt;code dir=&quot;auto&quot;&gt;&quot;rc_switch&quot;&lt;/code&gt;. Otherwise the protocol’s &lt;code dir=&quot;auto&quot;&gt;.cpp&lt;/code&gt; is filtered out and the link fails with an undefined reference to its &lt;code dir=&quot;auto&quot;&gt;encode()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;decode()&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;dump()&lt;/code&gt;. Unknown names raise during code generation and list the valid ones.&lt;/p&gt;
&lt;p&gt;A protocol that ships in your own component needs no request. Only the source files in &lt;code dir=&quot;auto&quot;&gt;remote_base&lt;/code&gt; are filtered, so registering your protocol with &lt;code dir=&quot;auto&quot;&gt;register_trigger&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;register_dumper&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;register_binary_sensor&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;register_action&lt;/code&gt; works as before, and &lt;code dir=&quot;auto&quot;&gt;request_protocol&lt;/code&gt; only accepts the bundled names.&lt;/p&gt;
&lt;p&gt;The same applies to a YAML lambda that uses a protocol class, for example &lt;code dir=&quot;auto&quot;&gt;id(tx).transmit&amp;#x3C;remote_base::NECProtocol&gt;(data)&lt;/code&gt;: reference that protocol somewhere else in the configuration, with a &lt;code dir=&quot;auto&quot;&gt;dump&lt;/code&gt; entry, an &lt;code dir=&quot;auto&quot;&gt;on_nec&lt;/code&gt; trigger or a &lt;code dir=&quot;auto&quot;&gt;remote_transmitter.transmit_nec&lt;/code&gt; action.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;receiver-platforms-one-call-for-listeners-and-dumpers&quot;&gt;Receiver platforms: one call for listeners and dumpers&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;A platform deriving from &lt;code dir=&quot;auto&quot;&gt;RemoteReceiverBase&lt;/code&gt; that called the protected &lt;code dir=&quot;auto&quot;&gt;call_listeners_()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;call_dumpers_()&lt;/code&gt; separately now calls &lt;code dir=&quot;auto&quot;&gt;call_listeners_dumpers_()&lt;/code&gt;, which already existed as the combined entry point; the two were merged so each list can compile out on its own.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;supporting-multiple-esphome-versions&quot;&gt;Supporting Multiple ESPHome Versions&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;override&lt;/code&gt; on a method that is no longer virtual is a compile error, so a component that has to build on 2026.9 and 2026.10 needs a version guard around the declarations:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#include&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;esphome/core/version.h&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; MyProtocol : &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; RemoteProtocol&amp;#x3C;MyData&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;public:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ESPHOME_VERSION_CODE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;VERSION_CODE&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2026&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;encode&lt;/span&gt;&lt;span&gt;(RemoteTransmitData &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;&lt;span&gt;dst&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; MyData &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;optional&amp;#x3C;MyData&gt; &lt;/span&gt;&lt;span&gt;decode&lt;/span&gt;&lt;span&gt;&lt;span&gt;(RemoteReceiveData &lt;/span&gt;&lt;span&gt;src&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dump&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; MyData &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#else&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;encode&lt;/span&gt;&lt;span&gt;(RemoteTransmitData &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;&lt;span&gt;dst&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; MyData &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;optional&amp;#x3C;MyData&gt; &lt;/span&gt;&lt;span&gt;decode&lt;/span&gt;&lt;span&gt;&lt;span&gt;(RemoteReceiveData &lt;/span&gt;&lt;span&gt;src&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dump&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; MyData &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#endif&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For listeners, &lt;code dir=&quot;auto&quot;&gt;remote_base.register_listener(var, config)&lt;/code&gt; exists on both versions, so a component whose schema uses &lt;code dir=&quot;auto&quot;&gt;CONF_RECEIVER_ID&lt;/code&gt; can move the registration to Python once and drop the C++ call with no guard. The guard below is only for a component whose receiver option has another name, since &lt;code dir=&quot;auto&quot;&gt;attach_receiver&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;add_listener&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;add_dumper&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;request_protocol&lt;/code&gt; only exist from 2026.10.0; keep the C++ registration for older versions:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# in to_code()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; esphome.components.remote_base &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; attach_receiver&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;except&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ImportError&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# ESPHome &amp;#x3C; 2026.10.0 registers the listener from setup()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;receiver &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; cg.&lt;/span&gt;&lt;span&gt;get_variable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;CONF_REMOTE_RECEIVER_ID&lt;/span&gt;&lt;span&gt;])&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;cg.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;var.&lt;/span&gt;&lt;span&gt;set_receiver&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;receiver&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;attach_receiver&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; config&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;CONF_REMOTE_RECEIVER_ID&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; MyComponent::&lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ESPHOME_VERSION_CODE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;VERSION_CODE&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2026&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;receiver_&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;register_listener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#endif&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;request_protocol&lt;/code&gt; follows the same shape for a bundled protocol; before 2026.10.0 every protocol source file is compiled, so the failing import is safe to ignore.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.10.0 (October 2026):&lt;/strong&gt; protocol methods are no longer virtual, receiver lists are sized from codegen, unused protocol sources are not compiled. No deprecation period: &lt;code dir=&quot;auto&quot;&gt;override&lt;/code&gt; on a non-virtual method cannot be kept compiling.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Protocol classes that still mark the methods override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;RemoteProtocol&amp;#x3C;&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rnE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;(encode|decode|dump)\(.*\) override&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Listener or dumper registration from C++&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rnE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;register_listener|register_dumper&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Receiver platforms calling the split helpers&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rnE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;call_listeners_\(|call_dumpers_\(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Protocol classes used from C++ or lambdas&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rnE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;remote_base::([A-Za-z0-9]*Protocol|RCSwitchBase)&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_configs/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/19084&quot;&gt;PR #19084: Make protocol methods non-virtual and size receiver lists from codegen&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://esphome.io/components/remote_transmitter.html&quot;&gt;Remote Transmitter component&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://esphome.io/components/remote_receiver.html&quot;&gt;Remote Receiver component&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.esphome.io/blog/2026/01/13/listener-staticvector-migration-wifi-and-logger/&quot;&gt;Listener StaticVector Migration: WiFi and Logger&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.esphome.io/blog/2026/03/12/call_loop-mark_failed-and-call_dump_config-are-no-longer-virtual/&quot;&gt;call_loop(), mark_failed(), and call_dump_config() Are No Longer Virtual&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.esphome.io/blog/2026/04/09/ble-event-handler-dispatch-devirtualized/&quot;&gt;BLE Event Handler Dispatch Devirtualized&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>InternalGPIOPin Is the Platform Pin Class Instead of a Virtual Base</title><link>https://developers.esphome.io/blog/2026/10/09/internalgpiopin-is-the-platform-pin-class-instead-of-a-virtual-base/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/10/09/internalgpiopin-is-the-platform-pin-class-instead-of-a-virtual-base/</guid><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;InternalGPIOPin&lt;/code&gt; is no longer an abstract class. It is now a type alias for the one pin class each platform provides (&lt;code dir=&quot;auto&quot;&gt;esp32::ESP32InternalGPIOPin&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;esp8266::ESP8266GPIOPin&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;rp2::RP2GPIOPin&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;libretiny::ArduinoInternalGPIOPin&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;zephyr::ZephyrGPIOPin&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;host::HostGPIOPin&lt;/code&gt;), so every call through an &lt;code dir=&quot;auto&quot;&gt;InternalGPIOPin *&lt;/code&gt; is a direct call. &lt;code dir=&quot;auto&quot;&gt;GPIOPin&lt;/code&gt; keeps its virtual interface.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;developer breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.11.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/20455&quot;&gt;PR #20455&lt;/a&gt;: Bind InternalGPIOPin to the platform pin class instead of a virtual base&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every image contains exactly one internal pin implementation, chosen by the platform at build time. &lt;code dir=&quot;auto&quot;&gt;InternalGPIOPin&lt;/code&gt; was still an abstract class on top of it, so &lt;code dir=&quot;auto&quot;&gt;get_pin()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;is_inverted()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;to_isr()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;attach_interrupt()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;detach_interrupt()&lt;/code&gt; all went through a vtable for no reason, and the compiler could not inline the trivial getters. Preferences and the OTA backend had the same shape and were already converted to a concept-checked alias; this change applies the same pattern to pins.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;GPIOPin&lt;/code&gt; is a different case and stays virtual. Which class sits behind a &lt;code dir=&quot;auto&quot;&gt;GPIOPin *&lt;/code&gt; is decided per configuration between the internal pin and the I/O expander pins (pcf8574, mcp23xxx, sx1509 and others), so that polymorphism is real.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;flash-and-ram&quot;&gt;Flash and RAM&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Measured on esp32-idf builds of the test configurations, plus a timer-only deep_sleep configuration with no pins:&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Configuration&lt;/th&gt;&lt;th&gt;Flash before&lt;/th&gt;&lt;th&gt;Flash after&lt;/th&gt;&lt;th&gt;RAM&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;gpio switch, output and binary sensor&lt;/td&gt;&lt;td&gt;200439&lt;/td&gt;&lt;td&gt;200375&lt;/td&gt;&lt;td&gt;unchanged&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;pulse_counter&lt;/td&gt;&lt;td&gt;197927&lt;/td&gt;&lt;td&gt;197867&lt;/td&gt;&lt;td&gt;unchanged&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;dht&lt;/td&gt;&lt;td&gt;199351&lt;/td&gt;&lt;td&gt;197819&lt;/td&gt;&lt;td&gt;8 bytes less&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;deep_sleep, timer only, no pins&lt;/td&gt;&lt;td&gt;198079&lt;/td&gt;&lt;td&gt;198131&lt;/td&gt;&lt;td&gt;unchanged&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Components that drive a pin many times, like dht’s bit-banged read, gain the most. The last row is the one configuration that grows: ESP32 used to leave its pin source out of a build with no pins, and the direct calls now need those methods to link, so the file is always compiled and the linker keeps only what is referenced.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;esphome/core/gpio.h&lt;/code&gt; includes the platform’s pin header and declares &lt;code dir=&quot;auto&quot;&gt;using InternalGPIOPin = &amp;#x3C;platform class&gt;;&lt;/code&gt;, checked by a &lt;code dir=&quot;auto&quot;&gt;static_assert&lt;/code&gt; on the new &lt;code dir=&quot;auto&quot;&gt;InternalGPIOPinContract&lt;/code&gt; concept.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;GPIOPin&lt;/code&gt;, the &lt;code dir=&quot;auto&quot;&gt;gpio::Flags&lt;/code&gt; enum, &lt;code dir=&quot;auto&quot;&gt;ISRInternalGPIOPin&lt;/code&gt; and the concept live in the new &lt;code dir=&quot;auto&quot;&gt;esphome/core/gpio_pin.h&lt;/code&gt;. Including &lt;code dir=&quot;auto&quot;&gt;esphome/core/gpio.h&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;esphome/core/hal.h&lt;/code&gt; still gives you everything it did before.&lt;/li&gt;
&lt;li&gt;The platform pin classes, which were already &lt;code dir=&quot;auto&quot;&gt;final&lt;/code&gt;, derive from &lt;code dir=&quot;auto&quot;&gt;GPIOPin&lt;/code&gt; directly. The protected hook that takes a &lt;code dir=&quot;auto&quot;&gt;void (*)(void *)&lt;/code&gt; callback is now named &lt;code dir=&quot;auto&quot;&gt;attach_interrupt_()&lt;/code&gt;; the public &lt;code dir=&quot;auto&quot;&gt;attach_interrupt&amp;#x3C;T&gt;()&lt;/code&gt; template is unchanged.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;GPIOPin::is_internal()&lt;/code&gt; now carries a rule: only the platform pin class behind the alias may return &lt;code dir=&quot;auto&quot;&gt;true&lt;/code&gt;, because callers &lt;code dir=&quot;auto&quot;&gt;static_cast&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;InternalGPIOPin&lt;/code&gt; on it.&lt;/li&gt;
&lt;li&gt;The &lt;code dir=&quot;auto&quot;&gt;USE_ESP32_INTERNAL_GPIO&lt;/code&gt; define is gone.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;External components that:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Derive a class from &lt;code dir=&quot;auto&quot;&gt;InternalGPIOPin&lt;/code&gt;, for example a pin implementation for a platform ESPHome does not ship, or a dummy pin in a unit test.&lt;/li&gt;
&lt;li&gt;Derive from &lt;code dir=&quot;auto&quot;&gt;InternalGPIOPin&lt;/code&gt; only to reach the protected &lt;code dir=&quot;auto&quot;&gt;attach_interrupt&lt;/code&gt; overload that takes a &lt;code dir=&quot;auto&quot;&gt;void (*)(void *)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Derive from &lt;code dir=&quot;auto&quot;&gt;GPIOPin&lt;/code&gt; and return &lt;code dir=&quot;auto&quot;&gt;true&lt;/code&gt; from &lt;code dir=&quot;auto&quot;&gt;is_internal()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Forward declare it with &lt;code dir=&quot;auto&quot;&gt;class InternalGPIOPin;&lt;/code&gt; instead of including &lt;code dir=&quot;auto&quot;&gt;esphome/core/gpio.h&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Components that take an &lt;code dir=&quot;auto&quot;&gt;InternalGPIOPin *&lt;/code&gt; or a &lt;code dir=&quot;auto&quot;&gt;GPIOPin *&lt;/code&gt; are not affected&lt;/strong&gt; unless they forward declare it. Setters such as &lt;code dir=&quot;auto&quot;&gt;set_pin(InternalGPIOPin *pin)&lt;/code&gt;, calls like &lt;code dir=&quot;auto&quot;&gt;pin-&gt;get_pin()&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;pin-&gt;attach_interrupt(...)&lt;/code&gt;, and I/O expander pins that derive from &lt;code dir=&quot;auto&quot;&gt;GPIOPin&lt;/code&gt; compile unchanged. &lt;strong&gt;Standard YAML configurations are not affected.&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;a-subclass-of-internalgpiopin&quot;&gt;A subclass of InternalGPIOPin&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Before, &lt;code dir=&quot;auto&quot;&gt;InternalGPIOPin&lt;/code&gt; was an abstract class and an external component could provide its own pin implementation behind it. The name now resolves to the platform’s pin class, which was already &lt;code dir=&quot;auto&quot;&gt;final&lt;/code&gt;, so a subclass fails with an error such as &lt;code dir=&quot;auto&quot;&gt;base &apos;HostGPIOPin&apos; is marked &apos;final&apos;&lt;/code&gt;. Such a pin can derive from &lt;code dir=&quot;auto&quot;&gt;GPIOPin&lt;/code&gt; instead, which works everywhere a component takes a &lt;code dir=&quot;auto&quot;&gt;GPIOPin *&lt;/code&gt; but offers no interrupt API. A project that already overrides a platform component through &lt;code dir=&quot;auto&quot;&gt;external_components&lt;/code&gt; can instead supply its class as that platform’s pin class, which the alias then picks up, or the platform can be contributed to ESPHome itself.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;reaching-the-raw-attach_interrupt&quot;&gt;Reaching the raw attach_interrupt&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The public &lt;code dir=&quot;auto&quot;&gt;attach_interrupt&amp;#x3C;T&gt;()&lt;/code&gt; template and the protected hook that takes a &lt;code dir=&quot;auto&quot;&gt;void (*)(void *)&lt;/code&gt; shared one name. A call with a plain &lt;code dir=&quot;auto&quot;&gt;void *&lt;/code&gt; callback picked the protected overload and failed to compile, so some components derived from &lt;code dir=&quot;auto&quot;&gt;InternalGPIOPin&lt;/code&gt; only to make it visible:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt; ExposeInternalPin : &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; InternalGPIOPin {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;using&lt;/span&gt;&lt;span&gt; InternalGPIOPin::attach_interrupt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;static_cast&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;ExposeInternalPin &lt;/span&gt;&lt;span&gt;*&gt;&lt;/span&gt;&lt;span&gt;(pin)-&gt;&lt;/span&gt;&lt;span&gt;attach_interrupt&lt;/span&gt;&lt;span&gt;(my_isr, arg, gpio::INTERRUPT_ANY_EDGE);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The hook is now &lt;code dir=&quot;auto&quot;&gt;attach_interrupt_()&lt;/code&gt;, so the template is the only &lt;code dir=&quot;auto&quot;&gt;attach_interrupt&lt;/code&gt; and takes the &lt;code dir=&quot;auto&quot;&gt;void *&lt;/code&gt; form as well (&lt;code dir=&quot;auto&quot;&gt;T&lt;/code&gt; deduces to &lt;code dir=&quot;auto&quot;&gt;void&lt;/code&gt;). Call it directly:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pin&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;attach_interrupt&lt;/span&gt;&lt;span&gt;(my_isr, arg, gpio::INTERRUPT_ANY_EDGE);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;a-forward-declaration-of-internalgpiopin&quot;&gt;A forward declaration of InternalGPIOPin&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;A header that only holds pin pointers could declare the class instead of including its header. The name is now an alias, and declaring it as a class fails. On ESP8266, for example, GCC reports &lt;code dir=&quot;auto&quot;&gt;error: using typedef-name &apos;using InternalGPIOPin = class esphome::esp8266::ESP8266GPIOPin&apos; after &apos;class&apos;&lt;/code&gt; when &lt;code dir=&quot;auto&quot;&gt;esphome/core/gpio.h&lt;/code&gt; was already included, or &lt;code dir=&quot;auto&quot;&gt;error: conflicting declaration &apos;using InternalGPIOPin = class esphome::esp8266::ESP8266GPIOPin&apos;&lt;/code&gt; in &lt;code dir=&quot;auto&quot;&gt;gpio.h&lt;/code&gt; when the forward declaration comes first:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;namespace&lt;/span&gt;&lt;span&gt; esphome {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; InternalGPIOPin;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;  // namespace esphome&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Include the header instead; this works on every ESPHome version, so no version guard is needed:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#include&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;esphome/core/gpio.h&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;a-gpiopin-subclass-that-returns-true-from-is_internal&quot;&gt;A GPIOPin subclass that returns true from is_internal()&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Return &lt;code dir=&quot;auto&quot;&gt;false&lt;/code&gt;. Callers that see &lt;code dir=&quot;auto&quot;&gt;true&lt;/code&gt; cast the pointer to the platform pin class, so any other class returning &lt;code dir=&quot;auto&quot;&gt;true&lt;/code&gt; is undefined behavior.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;supporting-multiple-esphome-versions&quot;&gt;Supporting Multiple ESPHome Versions&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The raw &lt;code dir=&quot;auto&quot;&gt;attach_interrupt&lt;/code&gt; call is the one place a version guard is needed, and only when the callback must keep its &lt;code dir=&quot;auto&quot;&gt;void *&lt;/code&gt; signature:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ESPHOME_VERSION_CODE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;VERSION_CODE&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2026&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;11&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pin&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;attach_interrupt&lt;/span&gt;&lt;span&gt;(my_isr, arg, gpio::INTERRUPT_ANY_EDGE);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#else&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt; ExposeInternalPin : &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; InternalGPIOPin {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;using&lt;/span&gt;&lt;span&gt; InternalGPIOPin::attach_interrupt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;static_cast&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;ExposeInternalPin &lt;/span&gt;&lt;span&gt;*&gt;&lt;/span&gt;&lt;span&gt;(pin)-&gt;&lt;/span&gt;&lt;span&gt;attach_interrupt&lt;/span&gt;&lt;span&gt;(my_isr, arg, gpio::INTERRUPT_ANY_EDGE);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#endif&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A typed callback avoids the guard altogether, because the public template has accepted it on every version:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;my_isr&lt;/span&gt;&lt;span&gt;(MyComponent &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pin&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;attach_interrupt&lt;/span&gt;&lt;span&gt;(my_isr, &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;, gpio::INTERRUPT_ANY_EDGE);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.11.0 (November 2026):&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;InternalGPIOPin&lt;/code&gt; becomes the platform pin class&lt;/li&gt;
&lt;li&gt;No deprecation period: once the name refers to a &lt;code dir=&quot;auto&quot;&gt;final&lt;/code&gt; class, nothing can keep a subclass of it compiling&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# List every reference, then check the base lists: a class that names InternalGPIOPin&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# as any of its bases is affected, as is a GPIOPin subclass whose is_internal() returns true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# and any `class InternalGPIOPin;` forward declaration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;InternalGPIOPin\|is_internal() override&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/20455&quot;&gt;PR #20455: Bind InternalGPIOPin to the platform pin class instead of a virtual base&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Select Options and Light Effects Move to ConstVector</title><link>https://developers.esphome.io/blog/2026/10/05/select-options-and-light-effects-move-to-constvector/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/10/05/select-options-and-light-effects-move-to-constvector/</guid><pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;ESPHome 2026.10.0 stores select options and light effects in flash tables generated at build time instead of copying them onto the heap at boot. The getters now return a &lt;code dir=&quot;auto&quot;&gt;ConstVector&lt;/code&gt; view instead of a &lt;code dir=&quot;auto&quot;&gt;FixedVector&lt;/code&gt;. Code that binds the result with &lt;code dir=&quot;auto&quot;&gt;auto&lt;/code&gt; and uses the common read only members keeps working; code that names the old type, or calls &lt;code dir=&quot;auto&quot;&gt;front()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;back()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;capacity()&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;full()&lt;/code&gt;, needs a small change.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.10.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Select options and light effects are fixed when the firmware is built, but both were copied into a heap &lt;code dir=&quot;auto&quot;&gt;FixedVector&lt;/code&gt; during &lt;code dir=&quot;auto&quot;&gt;setup()&lt;/code&gt;. On ESP8266 the list literal that codegen passed in also sat in &lt;code dir=&quot;auto&quot;&gt;.rodata&lt;/code&gt;, which is RAM there, so each list cost RAM twice. The API also built a temporary heap vector of effect names every time Home Assistant listed a light.&lt;/p&gt;
&lt;p&gt;Codegen now emits each list as a table in flash, and the entity keeps a small &lt;code dir=&quot;auto&quot;&gt;ConstVector&lt;/code&gt; view of it (a pointer and a size). Selects with identical option lists share one table.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;changes&quot;&gt;Changes&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/20117&quot;&gt;PR #20117&lt;/a&gt;: select options are stored in a shared flash table; adds the core &lt;code dir=&quot;auto&quot;&gt;ConstVector&amp;#x3C;T, Owning&gt;&lt;/code&gt; type.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/20204&quot;&gt;PR #20204&lt;/a&gt;: light effects are stored in a flash table, and the API lists effect names straight from it.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;select&quot;&gt;Select&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// OLD&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; FixedVector&amp;#x3C;&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;char&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;get_options&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// NEW&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; SelectOptions &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;get_options&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt;  // SelectOptions = ConstVector&amp;#x3C;const char *, true&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;All &lt;code dir=&quot;auto&quot;&gt;set_options(...)&lt;/code&gt; overloads keep their behavior: they copy the list you pass, so runtime option lists from lambdas or external components work exactly as before. &lt;code dir=&quot;auto&quot;&gt;SelectTraits&lt;/code&gt; stays non copyable, as it was with &lt;code dir=&quot;auto&quot;&gt;FixedVector&lt;/code&gt;. A new &lt;code dir=&quot;auto&quot;&gt;set_options_static(table, count)&lt;/code&gt; is used by generated code only.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;light&quot;&gt;Light&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// OLD&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; FixedVector&amp;#x3C;LightEffect &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;get_effects&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add_effects&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; std::initializer_list&amp;#x3C;LightEffect &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;effects&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// NEW&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; ConstVector&amp;#x3C;LightEffect &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;get_effects&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add_effects&lt;/span&gt;&lt;span&gt;(LightEffect &lt;/span&gt;&lt;span&gt;*const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;&lt;span&gt;effects&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;size_t&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;span&gt;  // generated code only&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;add_effects()&lt;/code&gt; is only called by generated code; no external callers were found.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Standard YAML configurations&lt;/strong&gt; need no changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lambdas and external components&lt;/strong&gt; keep working if they use any of these, which covers every use found in a GitHub code search:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;const auto &amp;#x26;opts = id(my_select).traits.get_options();&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;const auto &amp;#x26;effects = id(my_light).get_effects();&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;range based &lt;code dir=&quot;auto&quot;&gt;for&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;size()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;empty()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;[]&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;at()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;data()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;std::find(list.begin(), list.end(), value)&lt;/code&gt;, including &lt;code dir=&quot;auto&quot;&gt;const auto *it = std::find(...)&lt;/code&gt;: the iterators are raw pointers, as they were with &lt;code dir=&quot;auto&quot;&gt;FixedVector&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Code that breaks&lt;/strong&gt; is code that spells out the old type:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// OLD - no longer compiles&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; FixedVector&lt;/span&gt;&lt;span&gt;&amp;#x3C;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;char&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;opts &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;(my_select).&lt;/span&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get_options&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; FixedVector&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;LightEffect &lt;/span&gt;&lt;span&gt;*&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;effects &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;(my_light).&lt;/span&gt;&lt;span&gt;get_effects&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// NEW - let the compiler pick the type&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;opts &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;(my_select).&lt;/span&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get_options&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;effects &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;(my_light).&lt;/span&gt;&lt;span&gt;get_effects&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;ConstVector&lt;/code&gt; also drops the &lt;code dir=&quot;auto&quot;&gt;front()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;back()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;capacity()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;full()&lt;/code&gt; members that &lt;code dir=&quot;auto&quot;&gt;FixedVector&lt;/code&gt; had, so calls to them fail even when the result is bound with &lt;code dir=&quot;auto&quot;&gt;auto&lt;/code&gt;. For a non empty list, use &lt;code dir=&quot;auto&quot;&gt;list[0]&lt;/code&gt; instead of &lt;code dir=&quot;auto&quot;&gt;front()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;list[list.size() - 1]&lt;/code&gt; instead of &lt;code dir=&quot;auto&quot;&gt;back()&lt;/code&gt;. The list never grows after it is set, so &lt;code dir=&quot;auto&quot;&gt;capacity()&lt;/code&gt; is the same as &lt;code dir=&quot;auto&quot;&gt;size()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;full()&lt;/code&gt; is always true.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// OLD - no longer compiles&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;char&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;first &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;(my_select).&lt;/span&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get_options&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;front&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// NEW&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;opts &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;(my_select).&lt;/span&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get_options&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;char&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;first &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;opts&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;span&gt;  // check opts.empty() first if the list can be empty&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Copying the light effect list into a local variable (&lt;code dir=&quot;auto&quot;&gt;const auto effects = id(my_light).get_effects();&lt;/code&gt;) failed to compile since &lt;code dir=&quot;auto&quot;&gt;FixedVector&lt;/code&gt; was introduced; it compiles again now, because the light effect view is a cheap, non owning copy of a flash table. Copying a select’s options is still not allowed, since a select may own a runtime copy.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;constvector&quot;&gt;ConstVector&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;ConstVector&amp;#x3C;T&gt;&lt;/code&gt; in &lt;code dir=&quot;auto&quot;&gt;esphome/core/helpers.h&lt;/code&gt; is a read only view of a table: a pointer and a size, with raw pointer iterators. The owning variant, &lt;code dir=&quot;auto&quot;&gt;ConstVector&amp;#x3C;T, true&gt;&lt;/code&gt;, can also hold a heap copy for lists set at runtime; it marks ownership in the top bit of the size, so it stays a pointer and a size (8 bytes on 32 bit targets). Element types must be whole words (pointers or other 32 bit values), because ESP8266 can only read flash with aligned 32 bit loads.&lt;/p&gt;
&lt;p&gt;Other entity lists set at build time can move to &lt;code dir=&quot;auto&quot;&gt;ConstVector&lt;/code&gt; the same way.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rnE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;FixedVector&amp;#x3C;[[:space:]]*(const char|LightEffect)[[:space:]]*\*[[:space:]]*&gt;|get_(options|effects)\(\)\.(front|back|capacity|full)\(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--include=&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;*.cpp&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--include=&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;*.h&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--include=&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;*.yaml&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The pattern also matches &lt;code dir=&quot;auto&quot;&gt;FixedVector&amp;#x3C;const char *&gt;&lt;/code&gt; uses unrelated to selects, such as event types; only the ones holding select options need changing. It does not catch &lt;code dir=&quot;auto&quot;&gt;front()&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;back()&lt;/code&gt; called on a variable that holds the list, so check those by hand.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.10.0:&lt;/strong&gt; select options (#20117) and light effects (#20204) use &lt;code dir=&quot;auto&quot;&gt;ConstVector&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about these changes or need help migrating your external component, please ask in the &lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; or open a &lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;discussion on GitHub&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title>nRF52: nRF Connect SDK Projects Fetched Only When Needed</title><link>https://developers.esphome.io/blog/2026/09/26/nrf52-sdk-projects-fetched-on-demand/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/09/26/nrf52-sdk-projects-fetched-on-demand/</guid><pubDate>Sat, 26 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;nRF52 builds with the native &lt;code dir=&quot;auto&quot;&gt;sdk-nrf&lt;/code&gt; toolchain now download only the nRF Connect SDK west projects a build needs,
instead of all of them. External components that use an SDK module outside the default set must request it with
&lt;code dir=&quot;auto&quot;&gt;include_west_project()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.10.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/19735&quot;&gt;PR #19735&lt;/a&gt;: Fetch only the nRF Connect SDK projects a build needs&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The nRF Connect SDK manifest lists about 50 west projects, 2.2 GB on disk, and most of them serve features ESPHome never
uses (Matter, TF-M, WiFi, LoRaWAN, Azure IoT and more). Matter and cmock also pull git submodules with their full
history. The first nRF52 build downloaded all of it.&lt;/p&gt;
&lt;p&gt;A fresh install now fetches four projects (five from SDK 3.1), about 1 GB on disk instead of 2.2 GB and roughly a
third of the previous download, and a component adds whatever else it needs.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;These west projects are always fetched:&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Project&lt;/th&gt;&lt;th&gt;Provides&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;zephyr&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Zephyr RTOS&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;hal_nordic&lt;/code&gt;&lt;/td&gt;&lt;td&gt;nrfx drivers and the MDK&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;nrfxlib&lt;/code&gt;&lt;/td&gt;&lt;td&gt;SoftDevice Controller, MPSL, ZBOSS and other Nordic libraries&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;cmsis&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Arm CMSIS headers&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;cmsis_6&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Arm CMSIS 6 core headers, SDK 3.1 and later&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Every other project in the manifest is left out unless a component asks for it. The install is shared by every config on
the machine, so a later build that needs another project fetches only what is missing; nothing is removed again.&lt;/p&gt;
&lt;p&gt;ESPHome’s built-in components already request what they use:&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Project&lt;/th&gt;&lt;th&gt;Requested by&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;tinycrypt&lt;/code&gt; up to SDK 3.1, &lt;code dir=&quot;auto&quot;&gt;mbedtls&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;oberon-psa-crypto&lt;/code&gt; from 3.2&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;zephyr_ble_server&lt;/code&gt; (and &lt;code dir=&quot;auto&quot;&gt;ble_nus&lt;/code&gt; through it)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;segger&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;debug&lt;/code&gt; (RTT logging)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;openthread&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;mbedtls&lt;/code&gt;; &lt;code dir=&quot;auto&quot;&gt;oberon-psa-crypto&lt;/code&gt; too from SDK 2.7&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;openthread&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;zcbor&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;mcuboot&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;zephyr_mcumgr&lt;/code&gt; OTA&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;mcuboot&lt;/code&gt;&lt;/td&gt;&lt;td&gt;any build with sysbuild on&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;An SDK installed before this change keeps every project and is not touched.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;External components that&lt;/strong&gt; use a Zephyr or nRF Connect SDK module from a project outside the default set, for example
CMSIS DSP, LittleFS or FatFs, without one of the built-in components above pulling it in.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Standard YAML configurations are not affected&lt;/strong&gt;; the built-in components request the projects they need.&lt;/p&gt;
&lt;p&gt;There is no YAML override for users: an external component that needs another project has to be updated to request it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Call &lt;code dir=&quot;auto&quot;&gt;include_west_project()&lt;/code&gt; from your component’s &lt;code dir=&quot;auto&quot;&gt;to_code()&lt;/code&gt; with the west project name from the SDK manifest
(&lt;code dir=&quot;auto&quot;&gt;nrf/west.yml&lt;/code&gt;, or the &lt;code dir=&quot;auto&quot;&gt;zephyr/west.yml&lt;/code&gt; it imports):&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# In your component&apos;s __init__.py&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; esphome.components.nrf52.framework &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; include_west_project&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;to_code&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;include_west_project&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;cmsis-dsp&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# ... rest of to_code&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The call only records the project; the download happens when the build installs the SDK. Project names change between
SDK versions (TinyCrypt left the manifest in SDK 3.2, for example), so check
&lt;code dir=&quot;auto&quot;&gt;CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]&lt;/code&gt; when a project only exists in some of them. A name the manifest does not
have fails the build with a message listing the requested projects.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;supporting-multiple-esphome-versions&quot;&gt;Supporting Multiple ESPHome Versions&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;to_code&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; esphome.components.nrf52.framework &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; include_west_project&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;except&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ImportError&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# ESPHome &amp;#x3C; 2026.10.0 fetches every project&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;pass&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;include_west_project&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;cmsis-dsp&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;build-errors&quot;&gt;Build Errors&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A missing project shows up as a Kconfig symbol that cannot be enabled or a header that cannot be found:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;warning: TINYCRYPT (defined at modules/Kconfig.tinycrypt:9) has direct dependencies ... with value n&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;fatal error: zcbor_common.h: No such file or directory&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The fix is to add the matching &lt;code dir=&quot;auto&quot;&gt;include_west_project()&lt;/code&gt; call. To see every project and which ones were left out, run
&lt;code dir=&quot;auto&quot;&gt;west list -a -f &quot;{name} {active}&quot;&lt;/code&gt; in the SDK folder, &lt;code dir=&quot;auto&quot;&gt;frameworks/&amp;#x3C;version&gt;&lt;/code&gt; under &lt;code dir=&quot;auto&quot;&gt;~/Library/Caches/esphome/sdk-nrf&lt;/code&gt;
on macOS, &lt;code dir=&quot;auto&quot;&gt;~/.cache/esphome/sdk-nrf&lt;/code&gt; on Linux and &lt;code dir=&quot;auto&quot;&gt;%LOCALAPPDATA%\esphome\Cache\sdk-nrf&lt;/code&gt; on Windows, or under
&lt;code dir=&quot;auto&quot;&gt;ESPHOME_SDK_NRF_PREFIX&lt;/code&gt; when that is set. The &lt;code dir=&quot;auto&quot;&gt;west&lt;/code&gt; module lives in the matching &lt;code dir=&quot;auto&quot;&gt;penvs/&amp;#x3C;version&gt;&lt;/code&gt; Python environment
next to it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.10.0 (October 2026):&lt;/strong&gt; only the default projects are fetched&lt;/li&gt;
&lt;li&gt;No deprecation period; behavior changed directly&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://esphome.io/components/nrf52/&quot;&gt;nRF52 Platform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/19735&quot;&gt;PR #19735: Fetch only the nRF Connect SDK projects a build needs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.zephyrproject.org/latest/develop/west/manifest.html#active-and-inactive-projects&quot;&gt;West manifest project filter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>ClimateTraits Boolean Accessors Removed in Favor of Feature Flags</title><link>https://developers.esphome.io/blog/2026/05/14/climatetraits-boolean-accessors-removed-in-favor-of-feature-flags/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/05/14/climatetraits-boolean-accessors-removed-in-favor-of-feature-flags/</guid><pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The ten deprecated &lt;code dir=&quot;auto&quot;&gt;ClimateTraits::get/set_supports_*&lt;/code&gt; accessors have been removed. External climate components must migrate to the &lt;code dir=&quot;auto&quot;&gt;add_feature_flags()&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;has_feature_flags()&lt;/code&gt; API introduced in 2025.11.0 — the 6-month deprecation window has elapsed.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.5.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/16289&quot;&gt;PR #16289&lt;/a&gt;: Remove deprecations scheduled for 2026.5.0&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In 2025.11.0 the old boolean getter/setter pairs were superseded by an &lt;code dir=&quot;auto&quot;&gt;add_feature_flags()&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;has_feature_flags()&lt;/code&gt; bitmask API on &lt;code dir=&quot;auto&quot;&gt;ClimateTraits&lt;/code&gt;. The booleans were kept as thin shims marked &lt;code dir=&quot;auto&quot;&gt;ESPDEPRECATED(&quot;...&quot;, &quot;2025.11.0&quot;)&lt;/code&gt; to give external components a 6-month window. That window has now closed and the shims are gone. (The same release also migrated other &lt;code dir=&quot;auto&quot;&gt;ClimateTraits&lt;/code&gt; enum sets to packed-bitmask storage in a separate change; see the &lt;a href=&quot;https://developers.esphome.io/blog/2025/11/07/climate-entity-class-finitesetmask-and-flash-storage-optimizations/&quot;&gt;climate entity class optimizations&lt;/a&gt; post for that adjacent story.)&lt;/p&gt;
&lt;p&gt;The replacement API has been the canonical way to express climate capabilities since 2025.11.0, and every in-tree climate component (heat-pump IR codecs, mini-split bridges, thermostat platform, etc.) has been on it for months.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The following ten methods have been removed from &lt;code dir=&quot;auto&quot;&gt;ClimateTraits&lt;/code&gt;:&lt;/p&gt;

















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Removed method&lt;/th&gt;&lt;th&gt;Replacement flag&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;set_supports_current_temperature(bool)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;get_supports_current_temperature()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;set_supports_current_humidity(bool)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;get_supports_current_humidity()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;set_supports_two_point_target_temperature(bool)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;add_feature_flags(climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;get_supports_two_point_target_temperature()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;has_feature_flags(climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;set_supports_target_humidity(bool)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;add_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;get_supports_target_humidity()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;set_supports_action(bool)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;get_supports_action()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;watch-for-two_point_target_temperature&quot;&gt;Watch for &lt;code dir=&quot;auto&quot;&gt;two_point_target_temperature&lt;/code&gt;&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;set_supports_two_point_target_temperature(true)&lt;/code&gt; maps to &lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE&lt;/code&gt;&lt;/strong&gt;, not &lt;code dir=&quot;auto&quot;&gt;CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE&lt;/code&gt;. The bitmask flag name reflects the actual semantic — when set, the climate device &lt;em&gt;requires&lt;/em&gt; both &lt;code dir=&quot;auto&quot;&gt;target_temperature_low&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;target_temperature_high&lt;/code&gt; to be specified instead of a single &lt;code dir=&quot;auto&quot;&gt;target_temperature&lt;/code&gt;. If your old code called &lt;code dir=&quot;auto&quot;&gt;set_supports_two_point_target_temperature(true)&lt;/code&gt;, this is the flag you want — but double-check that your device actually requires both setpoints (rare for single-setpoint heat-pump and IR-controlled units) rather than just optionally supporting them, since the new name reflects the stricter semantic.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;External climate components in C++&lt;/strong&gt; that still call the old accessors. A GitHub code search for the removed identifiers across public repositories turned up ~20+ such components — heat-pump IR drivers, mini-split bridges, brand-specific climate integrations (Haier, Daikin IR, Ecodan, Sanyou, Sharp, Mill Heat, …).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;YAML configurations are not affected.&lt;/strong&gt; Built-in climate platforms migrated in 2025.11.0 and any user-facing config keys are unchanged.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before (no longer compiles)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ClimateTraits traits;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set_supports_current_temperature&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set_supports_action&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set_supports_two_point_target_temperature&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get_supports_action&lt;/span&gt;&lt;span&gt;()) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ClimateTraits traits;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add_feature_flags&lt;/span&gt;&lt;span&gt;(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add_feature_flags&lt;/span&gt;&lt;span&gt;(climate::CLIMATE_SUPPORTS_ACTION);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add_feature_flags&lt;/span&gt;&lt;span&gt;(climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;has_feature_flags&lt;/span&gt;&lt;span&gt;(climate::CLIMATE_SUPPORTS_ACTION)) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;add_feature_flags()&lt;/code&gt; accepts a bitwise OR of multiple flags, so a long sequence of individual calls can be condensed:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add_feature_flags&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;climate::CLIMATE_SUPPORTS_ACTION);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;has_feature_flags()&lt;/code&gt; is a bitwise-AND check: it returns true when &lt;strong&gt;any&lt;/strong&gt; of the bits in the argument are set on the traits. For most capability checks you’ll be testing one flag at a time, which behaves exactly like the old &lt;code dir=&quot;auto&quot;&gt;get_supports_*()&lt;/code&gt; boolean — pass a single flag. If you pass several OR’d together, the result is “is at least one of these supported,” not “are all of these supported.”&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2025.11.0:&lt;/strong&gt; Feature-flag API added; boolean accessors marked &lt;code dir=&quot;auto&quot;&gt;ESPDEPRECATED&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026.5.0:&lt;/strong&gt; Boolean accessors removed.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Find any of the ten removed boolean accessors in one pass.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# The two alternation groups isolate the accessor side (set/get) and the trait name,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# so the output line tells you exactly which one to migrate.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rEn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;(set|get)_supports_(current_temperature|current_humidity|two_point_target_temperature|target_humidity|action)&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/16289&quot;&gt;PR #16289&lt;/a&gt; — Remove climate / ektf2232 deprecations&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.esphome.io/blog/2025/11/07/climate-entity-class-finitesetmask-and-flash-storage-optimizations/&quot;&gt;2025.11.0 Climate optimizations blog post&lt;/a&gt; — the deprecation that started the clock&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.esphome.io/blog/2026/04/09/climate-and-fan-custom-mode-vectors-moved-to-entity/&quot;&gt;2026.4.0 Climate fan custom mode storage blog post&lt;/a&gt; — related climate-API tightening&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Default-Constructed PollingComponent No Longer Polls</title><link>https://developers.esphome.io/blog/2026/05/14/default-constructed-pollingcomponent-no-longer-polls/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/05/14/default-constructed-pollingcomponent-no-longer-polls/</guid><pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The no-argument &lt;code dir=&quot;auto&quot;&gt;PollingComponent()&lt;/code&gt; constructor now initializes the update interval to &lt;code dir=&quot;auto&quot;&gt;SCHEDULER_DONT_RUN&lt;/code&gt; (&lt;code dir=&quot;auto&quot;&gt;UINT32_MAX&lt;/code&gt;) instead of &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt;. External components that subclass &lt;code dir=&quot;auto&quot;&gt;PollingComponent&lt;/code&gt;, instantiate it with no constructor argument, and never call &lt;code dir=&quot;auto&quot;&gt;set_update_interval()&lt;/code&gt; will stop polling. Pass an interval to the constructor or call &lt;code dir=&quot;auto&quot;&gt;set_update_interval()&lt;/code&gt; explicitly.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.5.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15832&quot;&gt;PR #15832&lt;/a&gt;: Default PollingComponent() to not run when codegen is bypassed&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Codegen-driven components always pass the user’s &lt;code dir=&quot;auto&quot;&gt;update_interval:&lt;/code&gt; through to the constructor (or call &lt;code dir=&quot;auto&quot;&gt;set_update_interval()&lt;/code&gt; explicitly), so they were never affected by the default value. The default only mattered when an external component bypassed codegen — wrote &lt;code dir=&quot;auto&quot;&gt;PollingComponent()&lt;/code&gt; directly in C++ and never set an interval.&lt;/p&gt;
&lt;p&gt;The old default of &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt; was a workaround. Before the recent scheduler rework (#15516), the inefficient scheduler informally throttled &lt;code dir=&quot;auto&quot;&gt;interval=0&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;interval=1&lt;/code&gt; into something close to plain &lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt; behavior, so components that forgot to set an interval got “some” polling. After the scheduler became efficient and #15799 / #15831 hardened the runtime coercion path, an explicit &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt; ms interval would have meant &lt;strong&gt;1000 polls per second&lt;/strong&gt; — starving the watchdog and masking any real symptoms.&lt;/p&gt;
&lt;p&gt;No author who forgets to set an interval actually wants 1000 polls per second. The correct behavior when an interval is genuinely missing is to &lt;strong&gt;fail loudly&lt;/strong&gt; — don’t poll at all, log &lt;code dir=&quot;auto&quot;&gt;Update Interval: never&lt;/code&gt;, and let the developer notice.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;PollingComponent&lt;/span&gt;&lt;span&gt;() : &lt;/span&gt;&lt;span&gt;update_interval_&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;) {}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// → polled every 1 ms when never configured&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;PollingComponent&lt;/span&gt;&lt;span&gt;() : &lt;/span&gt;&lt;span&gt;update_interval_&lt;/span&gt;&lt;span&gt;(SCHEDULER_DONT_RUN) {}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// → does not poll until set_update_interval() is called&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;SCHEDULER_DONT_RUN&lt;/code&gt; is &lt;code dir=&quot;auto&quot;&gt;UINT32_MAX&lt;/code&gt;. The scheduler already treats this value as disabled, and &lt;code dir=&quot;auto&quot;&gt;LOG_UPDATE_INTERVAL&lt;/code&gt; already prints &lt;code dir=&quot;auto&quot;&gt;Update Interval: never&lt;/code&gt; for this value — so the &lt;code dir=&quot;auto&quot;&gt;dump_config&lt;/code&gt; output is also self-describing. See &lt;a href=&quot;https://github.com/esphome/esphome/pull/15832&quot;&gt;PR #15832&lt;/a&gt; for the full picture of how the scheduler and &lt;code dir=&quot;auto&quot;&gt;Component&lt;/code&gt; handle this sentinel.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;External components written in C++ that meet &lt;strong&gt;all three&lt;/strong&gt; of these conditions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Inherit from &lt;code dir=&quot;auto&quot;&gt;PollingComponent&lt;/code&gt; in C++&lt;/li&gt;
&lt;li&gt;Construct the instance via the no-argument &lt;code dir=&quot;auto&quot;&gt;PollingComponent()&lt;/code&gt; constructor (not &lt;code dir=&quot;auto&quot;&gt;PollingComponent(interval)&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Never call &lt;code dir=&quot;auto&quot;&gt;set_update_interval()&lt;/code&gt; afterwards&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Components driven by ESPHome’s Python code generation (&lt;code dir=&quot;auto&quot;&gt;cv.polling_component_schema(...)&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;sensor.sensor_schema()&lt;/code&gt;, etc.) are unaffected — the codegen always passes the configured (or schema-default) interval through to the constructor, so the default value never matters for them.&lt;/p&gt;
&lt;p&gt;A known example is &lt;a href=&quot;https://github.com/psvanstrom/esphome-p1reader/pull/110&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;psvanstrom/esphome-p1reader&lt;/code&gt;&lt;/a&gt;, which independently switched to &lt;code dir=&quot;auto&quot;&gt;PollingComponent(10)&lt;/code&gt; ahead of this change.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Pick whichever fits your component:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Option 1: pass the interval to the constructor (recommended for fixed-interval components)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; MyComponent : &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; PollingComponent {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;public:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;MyComponent&lt;/span&gt;&lt;span&gt;() : &lt;/span&gt;&lt;span&gt;PollingComponent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;60000&lt;/span&gt;&lt;span&gt;) {}&lt;/span&gt;&lt;span&gt;  // 60 s&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Option 2: keep the no-arg constructor and let codegen set the interval before registration.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; MyComponent : &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; PollingComponent {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;public:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt;       // your periodic work&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dump_config&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// (no need to override setup() or pass an interval to the constructor —&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// the Python codegen below calls set_update_interval() for you)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Python side — use polling_component_schema; cg.register_component() handles set_update_interval&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# automatically when CONF_UPDATE_INTERVAL is in config.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CONFIG_SCHEMA&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cv.&lt;/span&gt;&lt;span&gt;Schema&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;cv.&lt;/span&gt;&lt;span&gt;GenerateID&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;: cv.&lt;/span&gt;&lt;span&gt;declare_id&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;MyComponent&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;cv.&lt;/span&gt;&lt;span&gt;polling_component_schema&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;60s&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;to_code&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;var &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cg.&lt;/span&gt;&lt;span&gt;new_Pvariable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;CONF_ID&lt;/span&gt;&lt;span&gt;])&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; cg.&lt;/span&gt;&lt;span&gt;register_component&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; config&lt;/span&gt;&lt;span&gt;)   &lt;/span&gt;&lt;span&gt;# ← this sets the update interval for you&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;cg.register_component()&lt;/code&gt; (in &lt;code dir=&quot;auto&quot;&gt;esphome/cpp_helpers.py&lt;/code&gt;) calls &lt;code dir=&quot;auto&quot;&gt;set_update_interval(config[CONF_UPDATE_INTERVAL])&lt;/code&gt; before it registers the variable with &lt;code dir=&quot;auto&quot;&gt;App&lt;/code&gt;, so by the time &lt;code dir=&quot;auto&quot;&gt;App::register_component()&lt;/code&gt; triggers &lt;code dir=&quot;auto&quot;&gt;call_setup()&lt;/code&gt; → &lt;code dir=&quot;auto&quot;&gt;start_poller()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;update_interval_&lt;/code&gt; already holds the user’s configured value.&lt;/p&gt;
&lt;div dir=&quot;auto&quot;&gt;
&lt;p dir=&quot;auto&quot;&gt;&lt;svg viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;WARNING&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Don’t call &lt;code dir=&quot;auto&quot;&gt;set_update_interval()&lt;/code&gt; from inside &lt;code dir=&quot;auto&quot;&gt;setup()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;PollingComponent::call_setup()&lt;/code&gt; runs &lt;code dir=&quot;auto&quot;&gt;start_poller()&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; the subclass’s &lt;code dir=&quot;auto&quot;&gt;setup()&lt;/code&gt; — see &lt;a href=&quot;https://github.com/esphome/esphome/blob/dev/esphome/core/component.cpp&quot;&gt;the
implementation&lt;/a&gt; (search for
&lt;code dir=&quot;auto&quot;&gt;PollingComponent::call_setup&lt;/code&gt;). By the time &lt;code dir=&quot;auto&quot;&gt;setup()&lt;/code&gt; executes, the scheduler interval has already been
registered with whatever &lt;code dir=&quot;auto&quot;&gt;update_interval_&lt;/code&gt; held at registration time (&lt;code dir=&quot;auto&quot;&gt;SCHEDULER_DONT_RUN&lt;/code&gt; if the default
constructor was used). Calling &lt;code dir=&quot;auto&quot;&gt;set_update_interval()&lt;/code&gt; from &lt;code dir=&quot;auto&quot;&gt;setup()&lt;/code&gt; only updates the field — it does &lt;strong&gt;not&lt;/strong&gt;
re-register the interval, so polling stays disabled.&lt;/p&gt;
&lt;p&gt;The header comment for &lt;code dir=&quot;auto&quot;&gt;call_setup&lt;/code&gt; explicitly says the poller starts first “allowing setup to cancel it if
desired.” So if you really need a runtime decision inside &lt;code dir=&quot;auto&quot;&gt;setup()&lt;/code&gt; to change the cadence, the supported
pattern is &lt;code dir=&quot;auto&quot;&gt;stop_poller()&lt;/code&gt; → &lt;code dir=&quot;auto&quot;&gt;set_update_interval(ms)&lt;/code&gt; → &lt;code dir=&quot;auto&quot;&gt;start_poller()&lt;/code&gt;. Setting the interval before
registration (Option 2 above, or via the constructor in Option 1) is much cleaner.&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Option 3: if you want the component to behave like a plain Component (no polling),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// inherit from Component instead — that&apos;s the new explicit way to express &quot;no polling.&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; MyComponent : &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; Component {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If you see &lt;code dir=&quot;auto&quot;&gt;Update Interval: never&lt;/code&gt; in your device’s startup log and the component used to poll, you’re hitting this case — pick option 1 or 2.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Find subclasses of PollingComponent (also catches forward declarations and&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# struct/class variations on the same line).&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rEn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;class .* PollingComponent|struct .* PollingComponent|: *public *PollingComponent&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Find no-arg PollingComponent constructor calls (likely affected)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rEn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;PollingComponent\(\)&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Find whether set_update_interval is ever called&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;set_update_interval&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If your component has &lt;code dir=&quot;auto&quot;&gt;PollingComponent()&lt;/code&gt; (no argument) and no &lt;code dir=&quot;auto&quot;&gt;set_update_interval()&lt;/code&gt; call anywhere, you’ll need to add one of the three options above before 2026.5.0.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15832&quot;&gt;PR #15832&lt;/a&gt; — Default PollingComponent() to not run when codegen is bypassed&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15831&quot;&gt;PR #15831&lt;/a&gt; — Prerequisite: encode the old runtime coerce as a compile-time no-op&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15799&quot;&gt;PR #15799&lt;/a&gt; — Earlier scheduler hardening that made the old default actually mean 1 ms&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>FloatOutput Power Scaling Fields Gated Behind USE_OUTPUT_FLOAT_POWER_SCALING</title><link>https://developers.esphome.io/blog/2026/05/14/floatoutput-power-scaling-fields-gated-behind-use_output_float_power_scaling/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/05/14/floatoutput-power-scaling-fields-gated-behind-use_output_float_power_scaling/</guid><pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;min_power_&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;max_power_&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;zero_means_zero_&lt;/code&gt; fields and their setters on &lt;code dir=&quot;auto&quot;&gt;FloatOutput&lt;/code&gt; are now gated behind a new &lt;code dir=&quot;auto&quot;&gt;USE_OUTPUT_FLOAT_POWER_SCALING&lt;/code&gt; build flag. The codegen turns the flag on automatically whenever a YAML config references the feature. Lambdas that call &lt;code dir=&quot;auto&quot;&gt;id(out).set_min_power(...)&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;set_max_power(...)&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;set_zero_means_zero(...)&lt;/code&gt; without any matching YAML key or action will now fail to compile with a clear &lt;code dir=&quot;auto&quot;&gt;static_assert&lt;/code&gt; pointing at the one-line YAML fix.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;breaking change&lt;/strong&gt; for external components and YAML configs that drive power scaling exclusively from lambdas in &lt;strong&gt;ESPHome 2026.5.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15998&quot;&gt;PR #15998&lt;/a&gt;: Gate FloatOutput power scaling fields behind USE_OUTPUT_FLOAT_POWER_SCALING&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;min_power_&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;max_power_&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;zero_means_zero_&lt;/code&gt; cost &lt;strong&gt;12 bytes per instance&lt;/strong&gt; on every PWM channel, DAC channel, LEDC output, and dimmer-chip channel — paid by every device with any &lt;code dir=&quot;auto&quot;&gt;output:&lt;/code&gt; block, regardless of whether the user touches the feature. Inside &lt;code dir=&quot;auto&quot;&gt;esphome/esphome&lt;/code&gt;, total YAML usage of &lt;code dir=&quot;auto&quot;&gt;min_power&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;max_power&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;zero_means_zero&lt;/code&gt; is on the order of 17 lines, mostly in test fixtures. The &lt;code dir=&quot;auto&quot;&gt;output.set_min_power&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;output.set_max_power&lt;/code&gt; runtime actions (added in #8934) have no callers outside the action’s own test fixture.&lt;/p&gt;
&lt;p&gt;This follows the same pattern the entity base classes already use — &lt;code dir=&quot;auto&quot;&gt;USE_POWER_SUPPLY&lt;/code&gt; gates &lt;code dir=&quot;auto&quot;&gt;BinaryOutput::power_&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;USE_ENTITY_ICON&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;USE_ENTITY_DEVICE_CLASS&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;USE_ENTITY_UNIT_OF_MEASUREMENT&lt;/code&gt; gate string members of &lt;code dir=&quot;auto&quot;&gt;EntityBase&lt;/code&gt;. Configs that opt in pay the cost; configs that don’t get it stripped.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;measured-savings&quot;&gt;Measured savings&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Built with the unmodified example configs from devices.esphome.io for two real ESP8266 PWM lights — neither uses &lt;code dir=&quot;auto&quot;&gt;min_power&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;max_power&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;zero_means_zero&lt;/code&gt; / scaling actions:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://devices.esphome.io/devices/H801-RGBW-LED-Controller/&quot;&gt;H801 RGBW LED Controller&lt;/a&gt;&lt;/strong&gt; — 5 PWM outputs (RGB + cold-white + warm-white, the “RGBWW” &lt;code dir=&quot;auto&quot;&gt;light:&lt;/code&gt; platform):&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Metric&lt;/th&gt;&lt;th&gt;Baseline&lt;/th&gt;&lt;th&gt;PR&lt;/th&gt;&lt;th&gt;Δ&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;RAM (.bss + .data)&lt;/td&gt;&lt;td&gt;30,416 B&lt;/td&gt;&lt;td&gt;30,352 B&lt;/td&gt;&lt;td&gt;&lt;strong&gt;-64 B&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Flash&lt;/td&gt;&lt;td&gt;346,047 B&lt;/td&gt;&lt;td&gt;345,799 B&lt;/td&gt;&lt;td&gt;&lt;strong&gt;-248 B&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;sizeof(ESP8266PWM)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;40 B&lt;/td&gt;&lt;td&gt;28 B&lt;/td&gt;&lt;td&gt;&lt;strong&gt;-12 B&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;H802 RGBW LED Controller&lt;/strong&gt; — 4 PWM outputs: -48 B RAM, -248 B flash, same 12 B per channel.&lt;/p&gt;
&lt;p&gt;Per-instance .bss savings scale linearly with channel count (12 B × N). Flash savings (~248 B per binary) come from the elided multiply/subtract in &lt;code dir=&quot;auto&quot;&gt;set_level()&lt;/code&gt; and the dropped scaling-related strings/branches in &lt;code dir=&quot;auto&quot;&gt;dump_config()&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;USE_OUTPUT_FLOAT_POWER_SCALING&lt;/code&gt; is a new &lt;code dir=&quot;auto&quot;&gt;cg.add_define()&lt;/code&gt; flag that controls four things at compile time on &lt;code dir=&quot;auto&quot;&gt;FloatOutput&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;min_power_&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;max_power_&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;zero_means_zero_&lt;/code&gt; member fields&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;set_min_power()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;set_max_power()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;set_zero_means_zero()&lt;/code&gt; setters&lt;/li&gt;
&lt;li&gt;The min/max/zero-means-zero math inside &lt;code dir=&quot;auto&quot;&gt;FloatOutput::set_level()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The &lt;code dir=&quot;auto&quot;&gt;SetMinPowerAction&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;SetMaxPowerAction&lt;/code&gt; action templates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Python codegen turns the flag on &lt;strong&gt;automatically&lt;/strong&gt; whenever:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code dir=&quot;auto&quot;&gt;min_power:&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;max_power:&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;zero_means_zero:&lt;/code&gt; key is set on &lt;strong&gt;any&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;output:&lt;/code&gt; entry, &lt;strong&gt;or&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;An &lt;code dir=&quot;auto&quot;&gt;output.set_min_power&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;output.set_max_power&lt;/code&gt; action is registered anywhere in the config.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;zero_means_zero_&lt;/code&gt; now has a default initializer (it was undefined before, hidden behind an unconditional setter call driven by the schema default).&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;You are affected only if all three of these are true:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You do &lt;strong&gt;not&lt;/strong&gt; set &lt;code dir=&quot;auto&quot;&gt;min_power&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;max_power&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;zero_means_zero&lt;/code&gt; on any of your outputs in YAML, &lt;strong&gt;and&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;You do &lt;strong&gt;not&lt;/strong&gt; use the &lt;code dir=&quot;auto&quot;&gt;output.set_min_power&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;output.set_max_power&lt;/code&gt; automation actions anywhere, &lt;strong&gt;and&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;You &lt;strong&gt;do&lt;/strong&gt; call &lt;code dir=&quot;auto&quot;&gt;id(...).set_min_power(...)&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;id(...).set_max_power(...)&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;id(...).set_zero_means_zero(...)&lt;/code&gt; directly from a &lt;code dir=&quot;auto&quot;&gt;lambda:&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If any of those is false, your config is unaffected and behaves exactly as before.&lt;/p&gt;
&lt;p&gt;External hardware-driver components that ship their own C++ and call these setters from &lt;code dir=&quot;auto&quot;&gt;setup()&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;dump_config()&lt;/code&gt; fall in this bucket — typically inverter / charger / DC-DC bridges that auto-configure scaling per detected hardware (e.g. several of the &lt;code dir=&quot;auto&quot;&gt;syssi/esphome-*&lt;/code&gt; repos).&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;yaml-lambda-users--add-one-line-of-yaml&quot;&gt;YAML lambda users — add one line of YAML&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The build will fail with a clear error at your lambda:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;error: static assertion failed: set_min_power() requires USE_OUTPUT_FLOAT_POWER_SCALING.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;To enable it, add &apos;min_power: 0%&apos; (or any value) to one output entry in your YAML — the&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;codegen will then keep the scaling fields.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The fix is one line of YAML — add this to &lt;strong&gt;any one&lt;/strong&gt; output entry:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;output&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;platform&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;esp8266_pwm&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;pin&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;min_power&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0%&lt;/span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# ← add this (or max_power: 100%, or zero_means_zero: true)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;min_power: 0%&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;max_power: 100%&lt;/code&gt; are the existing defaults, so this changes no runtime behavior — it just tells the codegen “compile in runtime power scaling so my lambda can use it.”&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;external-component-authors--declare-the-define&quot;&gt;External component authors — declare the define&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;If your component ships its own &lt;code dir=&quot;auto&quot;&gt;output.*&lt;/code&gt; C++ and depends on the scaling setters, you have two options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;(Recommended)&lt;/strong&gt; Add the YAML key on any output. The codegen will turn the define on for you. This is the same flow regular users follow.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If you cannot rely on user YAML, declare the define in your component’s Python codegen:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; esphome.codegen &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; cg&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;to_code&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;cg.&lt;/span&gt;&lt;span&gt;add_define&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;USE_OUTPUT_FLOAT_POWER_SCALING&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;output&lt;/code&gt; component will then include the fields and setters regardless of YAML.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# C++ — find calls to the gated setters from lambdas or component code&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rEn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;set_min_power|set_max_power|set_zero_means_zero&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# YAML — find lambdas that call the setters&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rEn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;set_min_power|set_max_power|set_zero_means_zero&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_configs/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# YAML — check if any output entry already enables the feature (no action needed if so).&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Drop ^ to also catch top-level entries; expect false positives from unrelated&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# components that happen to use a same-named key — visually confirm matches are&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# under an output: entry before concluding you&apos;ve already opted in.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rEn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;(min_power|max_power|zero_means_zero):&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_configs/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component or configuration, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15998&quot;&gt;PR #15998&lt;/a&gt; — Gate FloatOutput power scaling fields behind USE_OUTPUT_FLOAT_POWER_SCALING&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://esphome.io/components/output/&quot;&gt;Output component documentation&lt;/a&gt; — &lt;code dir=&quot;auto&quot;&gt;min_power&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;max_power&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;zero_means_zero&lt;/code&gt; reference&lt;/li&gt;
&lt;li&gt;Pattern reference: &lt;code dir=&quot;auto&quot;&gt;USE_POWER_SUPPLY&lt;/code&gt; (gates &lt;code dir=&quot;auto&quot;&gt;BinaryOutput::power_&lt;/code&gt;), &lt;code dir=&quot;auto&quot;&gt;USE_ENTITY_ICON&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;USE_ENTITY_DEVICE_CLASS&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;USE_ENTITY_UNIT_OF_MEASUREMENT&lt;/code&gt; (gate &lt;code dir=&quot;auto&quot;&gt;EntityBase&lt;/code&gt; strings)&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Main Loop Cadence Decoupled from Scheduler Wake Timing</title><link>https://developers.esphome.io/blog/2026/05/14/main-loop-cadence-decoupled-from-scheduler-wake-timing/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/05/14/main-loop-cadence-decoupled-from-scheduler-wake-timing/</guid><pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every component’s &lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt; now runs at the configured &lt;code dir=&quot;auto&quot;&gt;loop_interval_&lt;/code&gt; (default ~62 Hz). Previously, &lt;strong&gt;when other scheduler activity was running&lt;/strong&gt; — &lt;code dir=&quot;auto&quot;&gt;set_interval&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;set_timeout&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;PollingComponent&lt;/code&gt; updates with sub-&lt;code dir=&quot;auto&quot;&gt;loop_interval_&lt;/code&gt; cadences — the component phase got pulled forward up to ~128 Hz; quiet configs with no such scheduler entries were unaffected and always ran at the documented rate. Background events (MQTT RX, USB RX, BLE events, ESPNOW, camera, &lt;code dir=&quot;auto&quot;&gt;micro_wake_word&lt;/code&gt;, speaker, USB host/CDC, lwIP socket) still wake their component within one tick via the existing &lt;code dir=&quot;auto&quot;&gt;wake_loop_*&lt;/code&gt; paths. &lt;code dir=&quot;auto&quot;&gt;App.set_loop_interval()&lt;/code&gt; — the documented knob for power savings — finally works.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;behavior change&lt;/strong&gt; in &lt;strong&gt;ESPHome 2026.5.0 and later&lt;/strong&gt; with no API break, but it affects every component whose &lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt; implicitly depended on running at ~2× the configured rate when scheduler activity was driving the pull-forward.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15792&quot;&gt;PR #15792&lt;/a&gt;: decouple main loop cadence from scheduler wake timing&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;what-was-happening&quot;&gt;What was happening&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Sampling runtime stats over 60 s on a typical ESP32 IDF config (api + esp32_ble + esp32_ble_tracker + bluetooth_proxy + debug + a couple of sensors) at the default &lt;code dir=&quot;auto&quot;&gt;loop_interval_ = 16 ms&lt;/code&gt;:&lt;/p&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Component&lt;/th&gt;&lt;th&gt;Before&lt;/th&gt;&lt;th&gt;After&lt;/th&gt;&lt;th&gt;What’s counted&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;api&lt;/code&gt;&lt;/td&gt;&lt;td&gt;7650 / 60 s ≈ &lt;strong&gt;128 Hz&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;3720 / 60 s ≈ &lt;strong&gt;62 Hz&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt; calls&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;esp32_ble&lt;/code&gt;&lt;/td&gt;&lt;td&gt;7650 ≈ &lt;strong&gt;128 Hz&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;3720 ≈ &lt;strong&gt;62 Hz&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt; calls&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;esp32_ble_tracker&lt;/code&gt;&lt;/td&gt;&lt;td&gt;7650 ≈ &lt;strong&gt;128 Hz&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;3720 ≈ &lt;strong&gt;62 Hz&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt; calls&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;debug&lt;/code&gt;&lt;/td&gt;&lt;td&gt;7651 ≈ &lt;strong&gt;128 Hz&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;3720 ≈ &lt;strong&gt;62 Hz&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt; calls&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;bluetooth_proxy&lt;/code&gt;&lt;/td&gt;&lt;td&gt;589 ≈ 10 Hz&lt;/td&gt;&lt;td&gt;599 ≈ 10 Hz&lt;/td&gt;&lt;td&gt;100 ms &lt;code dir=&quot;auto&quot;&gt;set_interval&lt;/code&gt; (unaffected)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Every component with a real &lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt; was running at &lt;strong&gt;~2× the configured &lt;code dir=&quot;auto&quot;&gt;loop_interval_&lt;/code&gt;&lt;/strong&gt;. 62 Hz is what the documentation has always promised; the 128 Hz was emergent behavior.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;why-it-happened&quot;&gt;Why it happened&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;Application::loop()&lt;/code&gt; bounded its sleep by &lt;code dir=&quot;auto&quot;&gt;min(loop_interval_ - elapsed, next_schedule_in())&lt;/code&gt; with a &lt;code dir=&quot;auto&quot;&gt;delay_time / 2&lt;/code&gt; floor. If the scheduler had any entry due sooner than &lt;code dir=&quot;auto&quot;&gt;loop_interval_ / 2&lt;/code&gt;, the loop slept for &lt;code dir=&quot;auto&quot;&gt;loop_interval_ / 2&lt;/code&gt; and ran the &lt;strong&gt;entire component phase&lt;/strong&gt; again — every component’s &lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt; got pulled forward by whatever else happened to be scheduling work. The effect was config-dependent and non-local: adding one scheduled item anywhere silently shifted every other component’s cadence.&lt;/p&gt;
&lt;p&gt;Two recent trends made this bite harder: (1) more components are &lt;code dir=&quot;auto&quot;&gt;PollingComponent&lt;/code&gt;s, each of which is a &lt;code dir=&quot;auto&quot;&gt;set_interval&lt;/code&gt; under the hood; (2) more components use &lt;code dir=&quot;auto&quot;&gt;set_interval&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;set_timeout&lt;/code&gt; directly for retries, debouncing, animations, and protocol timing. &lt;code dir=&quot;auto&quot;&gt;App.set_loop_interval()&lt;/code&gt; — the documented knob for power savings — was silently defeated by the same coupling.&lt;/p&gt;
&lt;p&gt;Removing the floor is safe now because &lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;wake_loop_threadsafe()&lt;/code&gt; is accessible everywhere as of 2026.4.0&lt;/strong&gt; (see the &lt;a href=&quot;https://developers.esphome.io/blog/2026/04/09/wake_loop-moved-from-socket-component-into-core/&quot;&gt;2026.4.0 wake_loop blog post&lt;/a&gt;). Any component that needs to wake the loop sooner than &lt;code dir=&quot;auto&quot;&gt;loop_interval_&lt;/code&gt; has a proper way to do it without the floor papering over missing wake-ups.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;Application::loop()&lt;/code&gt; is restructured into two independent phases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Phase A (every tick):&lt;/strong&gt; drain wake notifications, run &lt;code dir=&quot;auto&quot;&gt;scheduler.call()&lt;/code&gt;, feed the watchdog.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase B (gated):&lt;/strong&gt; iterate registered components. Runs when &lt;strong&gt;any&lt;/strong&gt; of these is true:
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;loop_interval_&lt;/code&gt; has elapsed since the last component phase, &lt;strong&gt;or&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;HighFrequencyLoopRequester&lt;/code&gt; is active, &lt;strong&gt;or&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;a background producer set the wake-request flag via &lt;code dir=&quot;auto&quot;&gt;wake_loop_*&lt;/code&gt; (MQTT RX, USB RX, BLE events, ESPNOW, camera, &lt;code dir=&quot;auto&quot;&gt;micro_wake_word&lt;/code&gt;, speaker, USB host/CDC, lwIP socket, or a component-level &lt;code dir=&quot;auto&quot;&gt;enable_loop_soon_any_context()&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sleep between ticks is &lt;code dir=&quot;auto&quot;&gt;min(time-until-next-component-phase, next_schedule_in())&lt;/code&gt;. A scheduler-timer wake runs only Phase A; a &lt;code dir=&quot;auto&quot;&gt;wake_loop_threadsafe()&lt;/code&gt; wake runs Phase B too so the producer’s component can drain its queued work.&lt;/p&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;delay_time / 2&lt;/code&gt; floor is removed entirely. Zero-delay &lt;code dir=&quot;auto&quot;&gt;set_interval(0, ...)&lt;/code&gt; registrations no longer busy-loop the component phase, because Phase B is gated separately from scheduler timer expiry. &lt;code dir=&quot;auto&quot;&gt;HighFrequencyLoopRequester&lt;/code&gt; remains the correct mechanism for “I need fast wakes sometimes.” Zero-delay &lt;code dir=&quot;auto&quot;&gt;defer()&lt;/code&gt; is unaffected.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ordering preserved:&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;defer()&lt;/code&gt; → FIFO before components; scheduled items before components when both are due; scheduled callbacks still main-thread-only; the WDT is fed ≥ once per tick.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Every component that overrides &lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt;&lt;/strong&gt; — in-tree and external alike — but most won’t notice. Specifically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Components whose &lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt; depended on running at ~128 Hz&lt;/strong&gt; — animations stepping per loop tick, debounce / state-machine timing keyed to loop count, anything that implicitly assumed the doubled rate. Those will now step at ~62 Hz when scheduler activity was previously providing the pull-forward.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Background-event producers&lt;/strong&gt; — already wired up. Every existing &lt;code dir=&quot;auto&quot;&gt;wake_loop_*&lt;/code&gt; caller (MQTT RX, USB RX, BLE events, ESPNOW, camera, &lt;code dir=&quot;auto&quot;&gt;micro_wake_word&lt;/code&gt;, speaker, USB host/CDC, lwIP socket, and per-component &lt;code dir=&quot;auto&quot;&gt;enable_loop_soon_any_context()&lt;/code&gt; calls) now sets a wake-request flag before signalling the platform so that Phase B runs in the next tick. No action needed in callers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Components that need sub-&lt;code dir=&quot;auto&quot;&gt;loop_interval_&lt;/code&gt; cadence&lt;/strong&gt; — see the migration guide below for the preferred event-driven pattern. &lt;code dir=&quot;auto&quot;&gt;HighFrequencyLoopRequester&lt;/code&gt; is available but should be a last resort.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Power-managed configs&lt;/strong&gt; using &lt;code dir=&quot;auto&quot;&gt;App.set_loop_interval()&lt;/code&gt; — this knob now actually saves power. If you previously bumped &lt;code dir=&quot;auto&quot;&gt;loop_interval_&lt;/code&gt; to 100 ms and saw no power reduction, expect a real change this release.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;For most components, &lt;strong&gt;do nothing&lt;/strong&gt;. Your &lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt; will simply run at the documented rate.&lt;/p&gt;
&lt;p&gt;If your component genuinely needs to run more often than &lt;code dir=&quot;auto&quot;&gt;loop_interval_&lt;/code&gt;, the &lt;strong&gt;preferred pattern is event-driven, not high-frequency&lt;/strong&gt;: keep the loop disabled while there’s nothing to do, and have your event source re-enable it. The &lt;code dir=&quot;auto&quot;&gt;Component&lt;/code&gt; base class has the primitives you need:&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;API&lt;/th&gt;&lt;th&gt;Where you call it&lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;this-&gt;disable_loop()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Main thread (typically the tail of &lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt; once your work is done)&lt;/td&gt;&lt;td&gt;Take the component out of the iteration list — its &lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt; won’t be called again until something re-enables it.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;this-&gt;enable_loop()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Main thread&lt;/td&gt;&lt;td&gt;Put the component back in the iteration list.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;this-&gt;enable_loop_soon_any_context()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;ISR, FreeRTOS task, or any thread&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;ISR-safe enable. Sets a pending flag that the main loop drains on the next tick. There is intentionally no &lt;code dir=&quot;auto&quot;&gt;disable_loop_soon_any_context()&lt;/code&gt; — use &lt;code dir=&quot;auto&quot;&gt;disable_loop()&lt;/code&gt; from &lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt; itself.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;App.wake_loop_threadsafe()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;FreeRTOS task / non-ISR thread (BLE callbacks, network events, platform task callbacks)&lt;/td&gt;&lt;td&gt;Forces Phase B in the next tick globally; lets every enabled component drain queued work without changing per-component enable state.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;App.wake_loop_any_context()&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;App.wake_loop_isrsafe()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;ISR context&lt;/td&gt;&lt;td&gt;ISR-safe global wake. See &lt;a href=&quot;https://developers.esphome.io/architecture/components/advanced#waking-from-isr&quot;&gt;Waking from ISR&lt;/a&gt; for per-platform IRAM and &lt;code dir=&quot;auto&quot;&gt;xHigherPriorityTaskWoken&lt;/code&gt; notes.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The typical event-driven pattern looks like this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#include&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;esphome/core/application.h&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#include&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;esphome/core/component.h&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; MyEventDrivenComponent : &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; Component {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;public:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ... register hardware / callbacks ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;disable_loop&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;span&gt;  // nothing to do until an event arrives&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;loop&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;drain_queued_events_&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;queue_empty_&lt;/span&gt;&lt;span&gt;()) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;disable_loop&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;span&gt;  // re-enable from on_event()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// Called from a FreeRTOS task / network callback / etc.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;on_event&lt;/span&gt;&lt;span&gt;&lt;span&gt;(Event &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;queue_&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(e);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;enable_loop_soon_any_context&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;span&gt;  // ISR-safe; also wakes the main loop&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;enable_loop_soon_any_context()&lt;/code&gt; both flips the component’s enable bit &lt;em&gt;and&lt;/em&gt; wakes the main loop, so &lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt; runs within a tick of the event landing — regardless of where &lt;code dir=&quot;auto&quot;&gt;loop_interval_&lt;/code&gt; is set. This composes cleanly with &lt;code dir=&quot;auto&quot;&gt;App.set_loop_interval(...)&lt;/code&gt; for power-managed configs: idle current drops because the loop sleeps; latency stays low because events wake it.&lt;/p&gt;
&lt;div dir=&quot;auto&quot;&gt;
&lt;p dir=&quot;auto&quot;&gt;&lt;svg viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;WARNING&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;HighFrequencyLoopRequester&lt;/code&gt; is a last resort&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;HighFrequencyLoopRequester::start()&lt;/code&gt; keeps Phase B running every tick unconditionally. It exists for genuinely time-critical workloads (e.g. tight bit-banged protocol timing where you can’t wait for an event), and even there, scoping the request as narrowly as possible — &lt;code dir=&quot;auto&quot;&gt;start()&lt;/code&gt; only while the critical work is in flight, &lt;code dir=&quot;auto&quot;&gt;stop()&lt;/code&gt; immediately after — is the supported usage. If you’re reaching for it because “I just need faster wakes,” prefer &lt;code dir=&quot;auto&quot;&gt;disable_loop()&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;enable_loop_soon_any_context()&lt;/code&gt; first — it gets you the same latency at a fraction of the wakeup count.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;The full architectural rationale, including how to decide between &lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;set_interval()&lt;/code&gt;, and the scheduler under the new cadence rules, is documented in &lt;a href=&quot;https://developers.esphome.io/architecture/components/advanced#choosing-between-loop-and-the-scheduler&quot;&gt;Choosing Between loop() and the Scheduler&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Components with a loop() override (the work you might want to review).&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# The narrower `override`-style pattern catches modern code; the wider one&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# also picks up older external components that declared `void loop();` without&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# `override` and is precisely where you&apos;re most likely to find code that&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# implicitly depended on the old ~128 Hz cadence.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;void loop() override&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rEn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;void[[:space:]]+loop[[:space:]]*\([[:space:]]*\)&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Zero-delay set_interval calls — previously busy-looped the component phase&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rEn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;set_interval\(\s*0\s*,&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Background producers that should be using a wake primitive&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;wake_loop_threadsafe\|enable_loop_soon_any_context\|wake_loop_any_context\|wake_loop_isrsafe&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Components already opted in to high-frequency wakes (no action needed)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;HighFrequencyLoopRequester&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If your &lt;code dir=&quot;auto&quot;&gt;loop()&lt;/code&gt; was implicitly relying on the ~128 Hz emergent rate and ~62 Hz is now insufficient, switch to &lt;code dir=&quot;auto&quot;&gt;HighFrequencyLoopRequester&lt;/code&gt; (for sustained fast wakes) or a wake primitive driven from your event source (for event-driven wakes).&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15792&quot;&gt;PR #15792&lt;/a&gt; — decouple main loop cadence from scheduler wake timing&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15846&quot;&gt;PR #15846&lt;/a&gt; — prerequisite WDT-feed rate-limit adjustment&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.esphome.io/architecture/components/advanced&quot;&gt;Architecture doc: loop control and main-loop cadence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.esphome.io/blog/2026/04/09/wake_loop-moved-from-socket-component-into-core/&quot;&gt;2026.4.0 blog: wake_loop moved to core&lt;/a&gt; — provided the unconditional wake primitive that made this fix safe&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Modbus Server Split Out of modbus_controller</title><link>https://developers.esphome.io/blog/2026/05/14/modbus-server-split-out-of-modbus_controller/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/05/14/modbus-server-split-out-of-modbus_controller/</guid><pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Modbus server mode has been split out of &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt; into a new dedicated &lt;code dir=&quot;auto&quot;&gt;modbus_server&lt;/code&gt; component. Configurations that used &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt; as a Modbus server (the &lt;code dir=&quot;auto&quot;&gt;role: server&lt;/code&gt; setting lives on the &lt;code dir=&quot;auto&quot;&gt;modbus:&lt;/code&gt; bus, not on &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt;) must move their &lt;code dir=&quot;auto&quot;&gt;server_registers:&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;server_courtesy_response:&lt;/code&gt; blocks under a new top-level &lt;code dir=&quot;auto&quot;&gt;modbus_server:&lt;/code&gt; entry.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;breaking change&lt;/strong&gt; for YAML configurations using server mode in &lt;strong&gt;ESPHome 2026.5.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15509&quot;&gt;PR #15509&lt;/a&gt;: Split modbus_server from modbus_controller&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Server-mode register handling was wedged into &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt; alongside its client-mode polling. The two roles share a transport (the &lt;code dir=&quot;auto&quot;&gt;modbus:&lt;/code&gt; bus) but almost nothing else: server mode dispatches inbound requests to user lambdas, while client mode polls outbound register reads on a schedule. Keeping both in the same component made the controller heavier than it needed to be, complicated review of server-side fixes, and prevented the server logic from sharing helpers with future modbus-based components.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_server&lt;/code&gt; is a thin &lt;code dir=&quot;auto&quot;&gt;Component&lt;/code&gt; that implements &lt;code dir=&quot;auto&quot;&gt;modbus::ModbusDevice&lt;/code&gt; and owns the register table plus the optional courtesy response, with no client-mode machinery. The migration in this PR is intentionally close to a copy/paste — functional improvements to server mode (out-of-range handling, multi-register transactions, parser fixes, etc.) come in follow-up PRs.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;flash-savings-approximate&quot;&gt;Flash savings (approximate)&lt;/h3&gt;&lt;/div&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Configuration&lt;/th&gt;&lt;th&gt;Before&lt;/th&gt;&lt;th&gt;After&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Pure server&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt; ~4.5 KB&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_server&lt;/code&gt; ~1.8 KB&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Pure client&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt; ~6.4 KB&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt; ~3.9 KB&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Devices that previously combined client and server in one &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt; will now have two separate components, but each is much smaller.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;yaml-keys-moved&quot;&gt;YAML keys moved&lt;/h3&gt;&lt;/div&gt;

















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Old (under &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt;)&lt;/th&gt;&lt;th&gt;New (under &lt;code dir=&quot;auto&quot;&gt;modbus_server&lt;/code&gt;)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;server_registers:&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;registers:&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;server_courtesy_response:&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;courtesy_response:&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;modbus_controller-is-now-client-only&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt; is now client-only&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;role: server&lt;/code&gt; on the bus is still supported — that selects the transport-layer role. What changed is that the &lt;strong&gt;register table&lt;/strong&gt; is no longer accepted on &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt;. Put it on &lt;code dir=&quot;auto&quot;&gt;modbus_server&lt;/code&gt; instead.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;YAML configurations that used &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt; as a Modbus server&lt;/strong&gt; — typically battery management bridges (BMS protocol bridges, yamBMS forks), EVSE bridges, energy/inverter proxies, and any “act as a register slave” project. Pure-client &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt; users (the more common case — reading from third-party Modbus devices) are not affected.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;modbus&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;role&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;modbus_controller&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;address&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;server_registers&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;address&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0x0001&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;value_type&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;U_WORD&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;read_lambda&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;return id(my_sensor).state;&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;server_courtesy_response&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;enabled&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;register_value&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;modbus&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;role&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;modbus_server&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;address&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;registers&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;address&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0x0001&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;value_type&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;U_WORD&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;read_lambda&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;return id(my_sensor).state;&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;courtesy_response&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;enabled&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;register_value&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A &lt;code dir=&quot;auto&quot;&gt;modbus:&lt;/code&gt; bus has a single role and your device occupies one side of it: &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt; requires &lt;code dir=&quot;auto&quot;&gt;role: client&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;modbus_server&lt;/code&gt; requires &lt;code dir=&quot;auto&quot;&gt;role: server&lt;/code&gt;, and the bus schema enforces this — you cannot mix the two on the same &lt;code dir=&quot;auto&quot;&gt;modbus:&lt;/code&gt; instance. Modbus only allows one client per bus, so the “both on one device” pattern was never valid; if a previous config had &lt;code dir=&quot;auto&quot;&gt;server_registers:&lt;/code&gt; under a client-mode &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt;, those registers were silently inactive. Run a second &lt;code dir=&quot;auto&quot;&gt;modbus:&lt;/code&gt; bus on a different UART if you need a device that’s both a server on one wire and a client on another.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2026.5.0:&lt;/strong&gt; Server-mode keys removed from &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt;; new &lt;code dir=&quot;auto&quot;&gt;modbus_server&lt;/code&gt; component active.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a clean break — there is no deprecation period or migration shim. &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt; configs that still contain &lt;code dir=&quot;auto&quot;&gt;server_registers:&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;server_courtesy_response:&lt;/code&gt; will fail validation on 2026.5.0. (For contrast, the earlier &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt; → &lt;code dir=&quot;auto&quot;&gt;modbus::helpers&lt;/code&gt; namespace refactor in 2026.4.0 kept deprecation shims through 2026.10.0 — that pattern doesn’t apply here because there’s no in-place rename to forward, and the affected configs are caught at config-validation time with a precise error message rather than at runtime.)&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# YAML — find server-mode keys still under modbus_controller.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# -E (extended regex) keeps the alternation portable across GNU and BSD/macOS grep.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rEn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;server_registers:|server_courtesy_response:&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_configs/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your configuration, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15509&quot;&gt;PR #15509&lt;/a&gt; — Split modbus_server from modbus_controller&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15291&quot;&gt;PR #15291&lt;/a&gt; and &lt;a href=&quot;https://github.com/esphome/esphome/pull/14172&quot;&gt;PR #14172&lt;/a&gt; — Earlier helper-function refactor that enabled this split (&lt;a href=&quot;https://developers.esphome.io/blog/2026/04/09/modbus-helper-functions-moved-to-modbushelpers/&quot;&gt;blog post&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://esphome.io/components/modbus_server/&quot;&gt;ESPHome Modbus Server documentation&lt;/a&gt; (new page)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://esphome.io/components/modbus_controller/&quot;&gt;ESPHome Modbus Controller documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>RingBuffer Moved Out of Core Into ring_buffer Helper Component</title><link>https://developers.esphome.io/blog/2026/05/14/ringbuffer-moved-out-of-core-into-ring_buffer-helper-component/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/05/14/ringbuffer-moved-out-of-core-into-ring_buffer-helper-component/</guid><pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;RingBuffer&lt;/code&gt; has moved from &lt;code dir=&quot;auto&quot;&gt;esphome/core/ring_buffer.h&lt;/code&gt; into a new &lt;code dir=&quot;auto&quot;&gt;ring_buffer&lt;/code&gt; helper component at &lt;code dir=&quot;auto&quot;&gt;esphome/components/ring_buffer/ring_buffer.h&lt;/code&gt; under the &lt;code dir=&quot;auto&quot;&gt;esphome::ring_buffer&lt;/code&gt; namespace. The old location is deprecated and will be removed in &lt;strong&gt;ESPHome 2026.11.0&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.5.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/16298&quot;&gt;PR #16298&lt;/a&gt;: Move core ring buffer to helper component&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The core &lt;code dir=&quot;auto&quot;&gt;RingBuffer&lt;/code&gt; was an ESP32-only implementation used exclusively by audio components — &lt;code dir=&quot;auto&quot;&gt;audio&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;i2s_audio&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;micro_wake_word&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;mixer&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;resampler&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;sound_level&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;speaker&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;voice_assistant&lt;/code&gt;. Nothing outside that audio cluster touches it. Living in &lt;code dir=&quot;auto&quot;&gt;esphome/core/&lt;/code&gt; meant:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code dir=&quot;auto&quot;&gt;ring_buffer.h&lt;/code&gt; header was parsed for every build, regardless of platform or whether audio was in use. Linker stripping kept unused code out of the final binary, but build-time parsing and tooling overhead applied universally.&lt;/li&gt;
&lt;li&gt;Changes to &lt;code dir=&quot;auto&quot;&gt;RingBuffer&lt;/code&gt; couldn’t be staged in an &lt;code dir=&quot;auto&quot;&gt;external_components&lt;/code&gt; PR — they always required a core change.&lt;/li&gt;
&lt;li&gt;The ESP32-only implementation lived alongside genuinely cross-platform core code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Moving it into a real component (&lt;code dir=&quot;auto&quot;&gt;esphome::ring_buffer::RingBuffer&lt;/code&gt;) lets audio consumers &lt;code dir=&quot;auto&quot;&gt;AUTO_LOAD&lt;/code&gt; it the same way every other shared utility is pulled in, and opens the door to iterating on the implementation without touching core.&lt;/p&gt;
&lt;p&gt;The old &lt;code dir=&quot;auto&quot;&gt;esphome/core/ring_buffer.h&lt;/code&gt; header now forwards to the new location and will be removed in &lt;strong&gt;2026.11.0&lt;/strong&gt; per the standard 6-month policy. The behavior of the shim depends on whether your component declares &lt;code dir=&quot;auto&quot;&gt;AUTO_LOAD = [&quot;ring_buffer&quot;]&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;With &lt;code dir=&quot;auto&quot;&gt;AUTO_LOAD&lt;/code&gt;:&lt;/strong&gt; the shim forwards &lt;code dir=&quot;auto&quot;&gt;esphome::RingBuffer&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;esphome::ring_buffer::RingBuffer&lt;/code&gt; via a &lt;code dir=&quot;auto&quot;&gt;using&lt;/code&gt; alias marked &lt;code dir=&quot;auto&quot;&gt;ESPDEPRECATED(...)&lt;/code&gt;. Code keeps compiling, but each use emits a deprecation warning at the call site.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Without &lt;code dir=&quot;auto&quot;&gt;AUTO_LOAD&lt;/code&gt;:&lt;/strong&gt; the new header isn’t on the include path, so the shim fires a hard &lt;code dir=&quot;auto&quot;&gt;#error&lt;/code&gt; telling you exactly what to do: &lt;em&gt;“&lt;code dir=&quot;auto&quot;&gt;esphome/components/ring_buffer/ring_buffer.h&lt;/code&gt; not found. Add ‘ring_buffer’ to your component’s AUTO_LOAD list to use esphome::ring_buffer::RingBuffer.”&lt;/em&gt; The build fails immediately rather than producing a confusing missing-header diagnostic from somewhere else.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;c-include-and-namespace&quot;&gt;C++ include and namespace&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#include&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;esphome/core/ring_buffer.h&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;std::unique_ptr&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;esphome::RingBuffer&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; buf &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; esphome::RingBuffer::&lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1024&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#include&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;esphome/components/ring_buffer/ring_buffer.h&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;std::unique_ptr&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;esphome::ring_buffer::RingBuffer&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; buf &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;esphome::ring_buffer::RingBuffer::&lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1024&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;python-codegen--declare-auto_load&quot;&gt;Python codegen — declare &lt;code dir=&quot;auto&quot;&gt;AUTO_LOAD&lt;/code&gt;&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Any component whose C++ uses &lt;code dir=&quot;auto&quot;&gt;RingBuffer&lt;/code&gt; must auto-load the helper component so the build system pulls in its sources. In your component’s &lt;code dir=&quot;auto&quot;&gt;__init__.py&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;AUTO_LOAD&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;ring_buffer&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If your component already has an &lt;code dir=&quot;auto&quot;&gt;AUTO_LOAD&lt;/code&gt;, append &lt;code dir=&quot;auto&quot;&gt;&quot;ring_buffer&quot;&lt;/code&gt; to it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;External audio / streaming components&lt;/strong&gt; that use &lt;code dir=&quot;auto&quot;&gt;RingBuffer&lt;/code&gt; directly — typically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Custom microphone / I²S audio pipelines&lt;/li&gt;
&lt;li&gt;Speaker / playback components&lt;/li&gt;
&lt;li&gt;Streaming protocol bridges (BLE audio, Nordic UART, etc.)&lt;/li&gt;
&lt;li&gt;Forks or extensions of &lt;code dir=&quot;auto&quot;&gt;micro_wake_word&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;voice_assistant&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;speaker&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All in-tree consumers (&lt;code dir=&quot;auto&quot;&gt;audio&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;i2s_audio&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;micro_wake_word&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;mixer&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;resampler&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;sound_level&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;speaker&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;voice_assistant&lt;/code&gt;) were migrated in &lt;a href=&quot;https://github.com/esphome/esphome/pull/16298&quot;&gt;esphome/esphome#16298&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Non-audio components are unaffected.&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Update the include path:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#include &quot;esphome/core/ring_buffer.h&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#include &quot;esphome/components/ring_buffer/ring_buffer.h&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Update the namespace at every callsite:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;esphome::RingBuffer::create(size);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;esphome::ring_buffer::RingBuffer::create(size);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If you have many callsites, you can avoid editing each one by adding a &lt;code dir=&quot;auto&quot;&gt;using esphome::ring_buffer::RingBuffer;&lt;/code&gt; declaration &lt;strong&gt;inside your component’s own namespace block&lt;/strong&gt; so &lt;code dir=&quot;auto&quot;&gt;RingBuffer&lt;/code&gt; resolves correctly from unqualified references:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;namespace&lt;/span&gt;&lt;span&gt; esphome::my_component {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;using&lt;/span&gt;&lt;span&gt; esphome::ring_buffer::RingBuffer;&lt;/span&gt;&lt;span&gt;   // ← here, not at file scope&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; MyComponent : &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; Component {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;std::unique_ptr&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;RingBuffer&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; buf_;&lt;/span&gt;&lt;span&gt;        // unqualified still works&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;  // namespace esphome::my_component&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Avoid putting the &lt;code dir=&quot;auto&quot;&gt;using&lt;/code&gt; at file scope (outside any namespace) — that pulls the name into the global namespace, which works for &lt;code dir=&quot;auto&quot;&gt;::RingBuffer&lt;/code&gt; lookups but doesn’t help code inside &lt;code dir=&quot;auto&quot;&gt;esphome::my_component&lt;/code&gt; that previously relied on &lt;code dir=&quot;auto&quot;&gt;esphome::RingBuffer&lt;/code&gt; being a sibling.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add &lt;code dir=&quot;auto&quot;&gt;AUTO_LOAD&lt;/code&gt; in your component’s &lt;code dir=&quot;auto&quot;&gt;__init__.py&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;AUTO_LOAD&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;ring_buffer&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The API of &lt;code dir=&quot;auto&quot;&gt;RingBuffer&lt;/code&gt; itself (&lt;code dir=&quot;auto&quot;&gt;create()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;read()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;write()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;write_without_replacement()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;available()&lt;/code&gt;, etc.) is unchanged — only its location and namespace move.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2026.5.0:&lt;/strong&gt; New location active; old &lt;code dir=&quot;auto&quot;&gt;esphome/core/ring_buffer.h&lt;/code&gt; deprecated (compile warning).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026.11.0:&lt;/strong&gt; Old header removed.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# C++ — find the old include&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;esphome/core/ring_buffer.h&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# C++ — find old namespace usage&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;esphome::RingBuffer&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Python — find AUTO_LOAD declarations that need ring_buffer added&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;AUTO_LOAD&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/16298&quot;&gt;PR #16298&lt;/a&gt; — Move core ring buffer to helper component&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>BLE Event Handler Dispatch Devirtualized</title><link>https://developers.esphome.io/blog/2026/04/09/ble-event-handler-dispatch-devirtualized/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/04/09/ble-event-handler-dispatch-devirtualized/</guid><pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Virtual handler interfaces (&lt;code dir=&quot;auto&quot;&gt;GAPEventHandler&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;GAPScanEventHandler&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;GATTcEventHandler&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;GATTsEventHandler&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;BLEStatusEventHandler&lt;/code&gt;) have been replaced with callback-based dispatch. External components that inherit from these interfaces must update their registration approach.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;developer breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.4.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15310&quot;&gt;PR #15310&lt;/a&gt;: Devirtualize BLE event handler dispatch&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The previous virtual handler pattern required vtable lookups and multiple-inheritance this-pointer adjustment thunks on every BLE event dispatch. The new approach uses &lt;code dir=&quot;auto&quot;&gt;StaticCallbackManager&lt;/code&gt; with lambda callbacks, eliminating vtable overhead. Each lambda captures a single pointer (fits in &lt;code dir=&quot;auto&quot;&gt;Callback&lt;/code&gt; inline storage, no heap allocation).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before (virtual dispatch):&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ESP32BLE::loop()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;→ vtable load → indirect call → thunk this-adjust → gap_scan_event_handler()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;After (callback dispatch):&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ESP32BLE::loop()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;→ StaticCallbackManager::call() → Callback::fn_(ctx_, args) → direct call&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The following virtual handler base classes are removed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;GAPEventHandler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;GAPScanEventHandler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;GATTcEventHandler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;GATTsEventHandler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;BLEStatusEventHandler&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Registration now uses &lt;code dir=&quot;auto&quot;&gt;add_*_callback()&lt;/code&gt; methods with lambdas instead of &lt;code dir=&quot;auto&quot;&gt;register_*_handler()&lt;/code&gt; with object pointers. The Python &lt;code dir=&quot;auto&quot;&gt;register_*&lt;/code&gt; helper functions are unchanged — they generate lambda-based callbacks internally.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;External ESP32 BLE components that inherit from the virtual handler classes.&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;c-changes&quot;&gt;C++ changes&lt;/h3&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Remove the handler base class from your inheritance list&lt;/li&gt;
&lt;li&gt;Remove &lt;code dir=&quot;auto&quot;&gt;override&lt;/code&gt; from handler methods&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; MyComponent : &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; Component, &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; GAPScanEventHandler {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gap_scan_event_handler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; BLEScanResult &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;scan_result&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; MyComponent : &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; Component {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gap_scan_event_handler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; BLEScanResult &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;scan_result&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;python-codegen&quot;&gt;Python codegen&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The Python &lt;code dir=&quot;auto&quot;&gt;register_*&lt;/code&gt; helper functions are unchanged — no Python changes needed:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# This still works — generates lambda-based callbacks internally&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;esp32_ble.&lt;/span&gt;&lt;span&gt;register_gap_scan_event_handler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;parent&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; var&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;supporting-multiple-esphome-versions&quot;&gt;Supporting Multiple ESPHome Versions&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before 2026.4.0, inherit from the handler class&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After 2026.4.0, just declare the method&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ESPHOME_VERSION_CODE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;VERSION_CODE&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2026&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; MyComponent : &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; Component {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#else&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; MyComponent : &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; Component, &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; GAPScanEventHandler {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#endif&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gap_scan_event_handler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; BLEScanResult &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;scan_result&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ESPHOME_VERSION_CODE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;VERSION_CODE&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2026&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#endif&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.4.0 (April 2026):&lt;/strong&gt; Virtual handler classes removed&lt;/li&gt;
&lt;li&gt;No deprecation period — this is a class removal&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Find handler class inheritance&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;GAPEventHandler\|GAPScanEventHandler\|GATTcEventHandler\|GATTsEventHandler\|BLEStatusEventHandler&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15310&quot;&gt;PR #15310: Devirtualize BLE event handler dispatch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>CallbackManager: std::function Replaced with Lightweight Callback</title><link>https://developers.esphome.io/blog/2026/04/09/callbackmanager-stdfunction-replaced-with-lightweight-callback/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/04/09/callbackmanager-stdfunction-replaced-with-lightweight-callback/</guid><pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;CallbackManager&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;LazyCallbackManager&lt;/code&gt; now use a lightweight 8-byte &lt;code dir=&quot;auto&quot;&gt;Callback&lt;/code&gt; struct instead of &lt;code dir=&quot;auto&quot;&gt;std::function&lt;/code&gt; (16 bytes). External components that define their own callback registration methods using &lt;code dir=&quot;auto&quot;&gt;std::function&lt;/code&gt; should update to templates for optimal performance.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.4.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/14853&quot;&gt;PR #14853&lt;/a&gt;: Replace std::function with lightweight Callback in CallbackManager&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;std::function&lt;/code&gt; has significant overhead on embedded targets: 16 bytes per instance on 32-bit platforms, plus heap allocation for captured lambdas. The new &lt;code dir=&quot;auto&quot;&gt;Callback&lt;/code&gt; struct uses C++20 &lt;code dir=&quot;auto&quot;&gt;if constexpr&lt;/code&gt; to store small trivially-copyable callables (like &lt;code dir=&quot;auto&quot;&gt;[this]&lt;/code&gt; lambdas) inline within the &lt;code dir=&quot;auto&quot;&gt;Callback&lt;/code&gt; object, alongside a function pointer, avoiding heap allocation while keeping the total size at 8 bytes.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;measured-savings&quot;&gt;Measured savings&lt;/h3&gt;&lt;/div&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Platform&lt;/th&gt;&lt;th&gt;Config&lt;/th&gt;&lt;th&gt;Flash Delta&lt;/th&gt;&lt;th&gt;Heap Delta&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;ESP8266&lt;/td&gt;&lt;td&gt;minimal&lt;/td&gt;&lt;td&gt;&lt;strong&gt;-160 B&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESP8266&lt;/td&gt;&lt;td&gt;ratgdo (cover + sensors)&lt;/td&gt;&lt;td&gt;&lt;strong&gt;-592 B&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;+142 B free&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESP8266&lt;/td&gt;&lt;td&gt;MQTT + web_server + many entities&lt;/td&gt;&lt;td&gt;&lt;strong&gt;-832 B&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;+488 B free&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESP32 IDF&lt;/td&gt;&lt;td&gt;large (climate, BLE, web_server)&lt;/td&gt;&lt;td&gt;&lt;strong&gt;-456 B&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESP32 IDF&lt;/td&gt;&lt;td&gt;minimal&lt;/td&gt;&lt;td&gt;&lt;strong&gt;-212 B&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;+72 B free&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;All callback registration methods (&lt;code dir=&quot;auto&quot;&gt;add_on_*_callback&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;add_*_callback&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;register_listener&lt;/code&gt;, etc.) on entity base classes and components are now templates instead of taking &lt;code dir=&quot;auto&quot;&gt;std::function&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add_on_state_callback&lt;/span&gt;&lt;span&gt;(std::function&amp;#x3C;&lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;float&lt;/span&gt;&lt;span&gt;)&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;callback&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;state_callback_&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(std::&lt;/span&gt;&lt;span&gt;move&lt;/span&gt;&lt;span&gt;(callback));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;template&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;typename&lt;/span&gt;&lt;span&gt; F&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add_on_state_callback&lt;/span&gt;&lt;span&gt;(F &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;callback&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;state_callback_&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(std::&lt;/span&gt;&lt;span&gt;forward&lt;/span&gt;&lt;span&gt;&amp;#x3C;F&gt;(callback));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;External components that define their own callback registration methods taking &lt;code dir=&quot;auto&quot;&gt;std::function&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Existing code &lt;strong&gt;continues to compile and work correctly&lt;/strong&gt;. However, unconverted methods use a less efficient heap-allocation path: the lambda gets wrapped in &lt;code dir=&quot;auto&quot;&gt;std::function&lt;/code&gt; before reaching &lt;code dir=&quot;auto&quot;&gt;CallbackManager::add()&lt;/code&gt;, which then heap-allocates it. Previously the &lt;code dir=&quot;auto&quot;&gt;std::function&lt;/code&gt; was stored directly in the vector element with no extra allocation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Callers of entity callbacks (registering callbacks on ESPHome entities) need no changes.&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Convert callback registration methods from &lt;code dir=&quot;auto&quot;&gt;std::function&lt;/code&gt; to templates:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;your_component.h&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before (still compiles, but now less efficient than before)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; MyComponent : &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; Component {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;public:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add_on_data_callback&lt;/span&gt;&lt;span&gt;(std::function&amp;#x3C;&lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;float&lt;/span&gt;&lt;span&gt;)&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;callback&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;data_callback_&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(std::&lt;/span&gt;&lt;span&gt;move&lt;/span&gt;&lt;span&gt;(callback));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;protected:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CallbackManager&lt;/span&gt;&lt;span&gt;&amp;#x3C;void&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;float&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; data_callback_;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;your_component.h&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After (optimal — inline storage, zero heap allocation for [this] lambdas)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; MyComponent : &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; Component {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;public:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;template&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;typename&lt;/span&gt;&lt;span&gt; F&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add_on_data_callback&lt;/span&gt;&lt;span&gt;(F &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;callback&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;data_callback_&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(std::&lt;/span&gt;&lt;span&gt;forward&lt;/span&gt;&lt;span&gt;&amp;#x3C;F&gt;(callback));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;protected:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CallbackManager&lt;/span&gt;&lt;span&gt;&amp;#x3C;void&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;float&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; data_callback_;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If you had a separate &lt;code dir=&quot;auto&quot;&gt;.cpp&lt;/code&gt; definition, move the body inline into the header and remove the &lt;code dir=&quot;auto&quot;&gt;.cpp&lt;/code&gt; definition (templates must be defined in headers).&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;supporting-multiple-esphome-versions&quot;&gt;Supporting Multiple ESPHome Versions&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The template approach is backward compatible — it works on older ESPHome versions too, since the template accepts &lt;code dir=&quot;auto&quot;&gt;std::function&lt;/code&gt; as well as raw lambdas. No version guard needed.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.4.0 (April 2026):&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;CallbackManager&lt;/code&gt; uses &lt;code dir=&quot;auto&quot;&gt;Callback&lt;/code&gt; instead of &lt;code dir=&quot;auto&quot;&gt;std::function&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;No deprecation period — existing code still compiles but uses a less efficient path&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Find callback registration methods that still use std::function&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;std::function.*&amp;#x26;&amp;#x26;.*callback&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/14853&quot;&gt;PR #14853: Replace std::function with lightweight Callback in CallbackManager&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Climate and Fan Custom Mode Vectors Moved to Entity</title><link>https://developers.esphome.io/blog/2026/04/09/climate-and-fan-custom-mode-vectors-moved-to-entity/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/04/09/climate-and-fan-custom-mode-vectors-moved-to-entity/</guid><pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Custom mode vectors (&lt;code dir=&quot;auto&quot;&gt;custom_fan_modes&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;custom_presets&lt;/code&gt; for climate; &lt;code dir=&quot;auto&quot;&gt;preset_modes&lt;/code&gt; for fan) are now stored on the entity base class instead of being rebuilt inside traits on every call. The old &lt;code dir=&quot;auto&quot;&gt;ClimateTraits&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;FanTraits&lt;/code&gt; setter methods are deprecated.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.4.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15206&quot;&gt;PR #15206&lt;/a&gt;:&lt;/strong&gt; Climate — Store custom mode vectors on Climate entity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15209&quot;&gt;PR #15209&lt;/a&gt;:&lt;/strong&gt; Fan — Store preset mode vector on Fan entity&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;ClimateTraits&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;FanTraits&lt;/code&gt; contained &lt;code dir=&quot;auto&quot;&gt;std::vector&lt;/code&gt; members that were reconstructed on every traits call. Climate uses &lt;code dir=&quot;auto&quot;&gt;traits()&lt;/code&gt; and Fan uses &lt;code dir=&quot;auto&quot;&gt;get_traits()&lt;/code&gt; — both are called during &lt;code dir=&quot;auto&quot;&gt;publish_state()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;control()&lt;/code&gt;, the hottest paths in these components. This caused &lt;strong&gt;heap allocations on every state update&lt;/strong&gt;. On long-running ESP devices, repeated allocations of different sizes fragment the heap and can lead to crashes.&lt;/p&gt;
&lt;p&gt;Moving the vectors to the entity base class eliminates all heap allocation from traits copies.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;climate&quot;&gt;Climate&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Custom fan modes and custom presets are now set directly on the &lt;code dir=&quot;auto&quot;&gt;Climate&lt;/code&gt; entity:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before — set on traits in traits() override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;climate::ClimateTraits &lt;/span&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; traits &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; climate::&lt;/span&gt;&lt;span&gt;ClimateTraits&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set_supported_custom_fan_modes&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Low&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Medium&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;High&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set_supported_custom_presets&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Eco&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Sleep&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; traits;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After — set once during setup or codegen&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;set_supported_custom_fan_modes&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Low&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Medium&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;High&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;set_supported_custom_presets&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Eco&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Sleep&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The old &lt;code dir=&quot;auto&quot;&gt;ClimateTraits::set_supported_custom_fan_modes()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;ClimateTraits::set_supported_custom_presets()&lt;/code&gt; methods still work but are deprecated and will be removed in &lt;strong&gt;2026.11.0&lt;/strong&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;fan&quot;&gt;Fan&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Preset modes are now set directly on the &lt;code dir=&quot;auto&quot;&gt;Fan&lt;/code&gt; entity. Unlike Climate, &lt;code dir=&quot;auto&quot;&gt;Fan::get_traits()&lt;/code&gt; is the virtual method itself, so the &lt;code dir=&quot;auto&quot;&gt;get_traits()&lt;/code&gt; override must wire the stored preset modes into the returned traits via &lt;code dir=&quot;auto&quot;&gt;wire_preset_modes_()&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before — set on traits in get_traits() override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;fan::FanTraits &lt;/span&gt;&lt;span&gt;get_traits&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; fan::&lt;/span&gt;&lt;span&gt;FanTraits&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;preset_modes_&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After — set once during setup or codegen, wire into traits in get_traits()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;set_supported_preset_modes&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Auto&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Sleep&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Nature&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;fan::FanTraits &lt;/span&gt;&lt;span&gt;get_traits&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;fan::FanTraits &lt;/span&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;wire_preset_modes_&lt;/span&gt;&lt;span&gt;(traits);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; traits;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The old &lt;code dir=&quot;auto&quot;&gt;FanTraits::set_supported_preset_modes()&lt;/code&gt; methods still work but are deprecated and will be removed in &lt;strong&gt;2026.11.0&lt;/strong&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;External climate components&lt;/strong&gt; (~35 affected) that call &lt;code dir=&quot;auto&quot;&gt;ClimateTraits::set_supported_custom_fan_modes()&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;ClimateTraits::set_supported_custom_presets()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;External fan components&lt;/strong&gt; (~15 affected) that pass preset modes through &lt;code dir=&quot;auto&quot;&gt;FanTraits&lt;/code&gt; constructors or call &lt;code dir=&quot;auto&quot;&gt;FanTraits::set_supported_preset_modes()&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;climate-1&quot;&gt;Climate&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before — in traits() override, called on every publish_state()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;climate::ClimateTraits &lt;/span&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; traits &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; climate::&lt;/span&gt;&lt;span&gt;ClimateTraits&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set_supported_custom_fan_modes&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Low&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Medium&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;High&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set_supported_custom_presets&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Eco&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Sleep&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ... other traits&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; traits;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After — set once, traits() gets them automatically&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;set_supported_custom_fan_modes&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Low&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Medium&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;High&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;set_supported_custom_presets&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Eco&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Sleep&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;climate::ClimateTraits &lt;/span&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; traits &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; climate::&lt;/span&gt;&lt;span&gt;ClimateTraits&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// custom_fan_modes and custom_presets are wired automatically&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ... other traits&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; traits;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;fan-1&quot;&gt;Fan&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;fan::FanTraits &lt;/span&gt;&lt;span&gt;get_traits&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; fan::&lt;/span&gt;&lt;span&gt;FanTraits&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;preset_modes_&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After — get_traits() override must call wire_preset_modes_() to expose them&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;set_supported_preset_modes&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Auto&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Sleep&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Nature&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;fan::FanTraits &lt;/span&gt;&lt;span&gt;get_traits&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;fan::FanTraits &lt;/span&gt;&lt;span&gt;traits&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;wire_preset_modes_&lt;/span&gt;&lt;span&gt;(traits);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; traits;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Unlike Climate — where the base class’s non-virtual &lt;code dir=&quot;auto&quot;&gt;get_traits()&lt;/code&gt; wraps a virtual &lt;code dir=&quot;auto&quot;&gt;traits()&lt;/code&gt; and wires custom modes automatically — &lt;code dir=&quot;auto&quot;&gt;Fan::get_traits()&lt;/code&gt; is itself the virtual method overridden by components. The base class has no opportunity to inject preset modes, so each override must call &lt;code dir=&quot;auto&quot;&gt;wire_preset_modes_()&lt;/code&gt; explicitly. The internal &lt;code dir=&quot;auto&quot;&gt;speed&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;template&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;hbridge&lt;/code&gt; fan components follow this pattern.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h3 id=&quot;python-codegen&quot;&gt;Python codegen&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Before — setting custom modes via traits in C++ generated code&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cg.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;traits.&lt;/span&gt;&lt;span&gt;set_supported_custom_fan_modes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;modes&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# After — setting custom modes directly on the entity&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cg.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;var.&lt;/span&gt;&lt;span&gt;set_supported_custom_fan_modes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;modes&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.4.0 (April 2026):&lt;/strong&gt; Old traits setters deprecated&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.11.0 (November 2026):&lt;/strong&gt; Deprecated setters removed, traits become trivially copyable&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Climate — find traits-based custom mode setters&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;set_supported_custom_fan_modes\|set_supported_custom_presets&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Fan — find traits-based preset mode setters&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;set_supported_preset_modes&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15206&quot;&gt;PR #15206: Climate — Store custom mode vectors on Climate entity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15209&quot;&gt;PR #15209: Fan — Store preset mode vector on Fan entity&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>FlushResult Renamed to UARTFlushResult</title><link>https://developers.esphome.io/blog/2026/04/09/flushresult-renamed-to-uartflushresult/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/04/09/flushresult-renamed-to-uartflushresult/</guid><pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;FlushResult&lt;/code&gt; enum introduced in ESPHome 2026.3.0 has been renamed to &lt;code dir=&quot;auto&quot;&gt;UARTFlushResult&lt;/code&gt; with prefixed enum values to follow ESPHome’s naming conventions and avoid macro collisions.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.4.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15101&quot;&gt;PR #15101&lt;/a&gt;: Rename FlushResult to UARTFlushResult with UART_FLUSH_RESULT_ prefix&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The original &lt;code dir=&quot;auto&quot;&gt;FlushResult::SUCCESS&lt;/code&gt; value collided with the Realtek RTL SDK’s &lt;code dir=&quot;auto&quot;&gt;#define SUCCESS 0&lt;/code&gt; macro, requiring ugly &lt;code dir=&quot;auto&quot;&gt;#pragma push_macro&lt;/code&gt; workarounds. Renaming to &lt;code dir=&quot;auto&quot;&gt;UARTFlushResult&lt;/code&gt; with &lt;code dir=&quot;auto&quot;&gt;UART_FLUSH_RESULT_&lt;/code&gt; prefixed values follows ESPHome’s conventional component-prefix naming style (matching &lt;code dir=&quot;auto&quot;&gt;UARTParityOptions&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;UART_CONFIG_PARITY_NONE&lt;/code&gt;) and eliminates the collision entirely.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Before&lt;/th&gt;&lt;th&gt;After&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;FlushResult&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;UARTFlushResult&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;FlushResult::SUCCESS&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;UARTFlushResult::UART_FLUSH_RESULT_SUCCESS&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;FlushResult::TIMEOUT&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;FlushResult::FAILED&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;UARTFlushResult::UART_FLUSH_RESULT_FAILED&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;FlushResult::ASSUMED_SUCCESS&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;External components that implement &lt;code dir=&quot;auto&quot;&gt;UARTComponent::flush()&lt;/code&gt; and reference the &lt;code dir=&quot;auto&quot;&gt;FlushResult&lt;/code&gt; type or enum values.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This only affects components updated for the 2026.3.0 &lt;code dir=&quot;auto&quot;&gt;flush()&lt;/code&gt; return type change. If you haven’t updated your &lt;code dir=&quot;auto&quot;&gt;flush()&lt;/code&gt; override yet, see the &lt;a href=&quot;https://developers.esphome.io/blog/2026/03/12/uart-flush-now-returns-flushresult/&quot;&gt;2026.3.0 UART flush blog post&lt;/a&gt; first.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before (2026.3.0)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;FlushResult &lt;/span&gt;&lt;span&gt;flush&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; FlushResult::ASSUMED_SUCCESS;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After (2026.4.0)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;UARTFlushResult &lt;/span&gt;&lt;span&gt;flush&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;supporting-multiple-esphome-versions&quot;&gt;Supporting Multiple ESPHome Versions&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ESPHOME_VERSION_CODE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;VERSION_CODE&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2026&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;UARTFlushResult &lt;/span&gt;&lt;span&gt;flush&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#elif&lt;/span&gt;&lt;span&gt; ESPHOME_VERSION_CODE &lt;/span&gt;&lt;span&gt;&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;VERSION_CODE&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2026&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;FlushResult &lt;/span&gt;&lt;span&gt;flush&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; FlushResult::ASSUMED_SUCCESS;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#else&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;flush&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#endif&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.3.0 (March 2026):&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;FlushResult&lt;/code&gt; introduced&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.4.0 (April 2026):&lt;/strong&gt; Renamed to &lt;code dir=&quot;auto&quot;&gt;UARTFlushResult&lt;/code&gt; with prefixed values&lt;/li&gt;
&lt;li&gt;No deprecation period — this is a rename of a type introduced one release ago&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Find FlushResult references in your component&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;FlushResult&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15101&quot;&gt;PR #15101: Rename FlushResult to UARTFlushResult&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/14608&quot;&gt;PR #14608: UART flush() return type change (2026.3.0)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Modbus Helper Functions Moved to modbus::helpers</title><link>https://developers.esphome.io/blog/2026/04/09/modbus-helper-functions-moved-to-modbushelpers/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/04/09/modbus-helper-functions-moved-to-modbushelpers/</guid><pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Shared helper functions and types have been moved from &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;modbus::helpers&lt;/code&gt; to enable reuse by other modbus-based components. Deprecated shims keep the old names working until &lt;strong&gt;ESPHome 2026.10.0&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;developer breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.4.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15291&quot;&gt;PR #15291&lt;/a&gt;: Share helper functions across modbus components — part A&lt;/strong&gt;
&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/14172&quot;&gt;PR #14172&lt;/a&gt;: Share helper functions across modbus components — part B&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;These PRs are part of a series preparing for a new &lt;code dir=&quot;auto&quot;&gt;modbus_server&lt;/code&gt; component. Helper functions that were previously internal to &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt; are now in &lt;code dir=&quot;auto&quot;&gt;modbus::helpers&lt;/code&gt; so they can be shared across &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;modbus_server&lt;/code&gt;, and other modbus-based components.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;c-symbols-moved-part-a&quot;&gt;C++ symbols moved (Part A)&lt;/h3&gt;&lt;/div&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Old location&lt;/th&gt;&lt;th&gt;New location&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller::SensorValueType&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus::helpers::SensorValueType&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller::value_type_is_float()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus::helpers::value_type_is_float()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller::modbus_register_read_function()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus::helpers::modbus_register_read_function()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller::modbus_register_write_function()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus::helpers::modbus_register_write_function()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller::c_to_hex()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus::helpers::c_to_hex()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller::byte_from_hex_str()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus::helpers::byte_from_hex_str()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller::word_from_hex_str()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus::helpers::word_from_hex_str()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller::dword_from_hex_str()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus::helpers::dword_from_hex_str()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller::qword_from_hex_str()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus::helpers::qword_from_hex_str()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;c-symbols-moved-part-b&quot;&gt;C++ symbols moved (Part B)&lt;/h3&gt;&lt;/div&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Old location&lt;/th&gt;&lt;th&gt;New location&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller::number_to_payload()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus::helpers::number_to_payload()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller::payload_to_number()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus::helpers::payload_to_number()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller::get_data()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus::helpers::get_data()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller::float_to_payload()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus::helpers::float_to_payload()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller::coil_from_vector()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus::helpers::coil_from_vector()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus_controller::mask_and_shift_by_rightbit()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;modbus::helpers::mask_and_shift_by_rightbit()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;python-symbols-moved&quot;&gt;Python symbols moved&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The following Python symbols have moved from &lt;code dir=&quot;auto&quot;&gt;esphome.components.modbus_controller&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;esphome.components.modbus.helpers&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;MODBUS_FUNCTION_CODE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;MODBUS_REGISTER_TYPE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;MODBUS_WRITE_REGISTER_TYPE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;SENSOR_VALUE_TYPE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;TYPE_REGISTER_MAP&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;CPP_TYPE_REGISTER_MAP&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;ModbusRegisterType&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;External components that import helper functions from &lt;code dir=&quot;auto&quot;&gt;modbus_controller&lt;/code&gt;&lt;/strong&gt;, either in C++ (&lt;code dir=&quot;auto&quot;&gt;modbus_controller::number_to_payload()&lt;/code&gt;, etc.) or Python (&lt;code dir=&quot;auto&quot;&gt;from esphome.components.modbus_controller import SENSOR_VALUE_TYPE&lt;/code&gt;, etc.).&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;c&quot;&gt;C++&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Deprecated shims in &lt;code dir=&quot;auto&quot;&gt;modbus_controller.h&lt;/code&gt; forward to the new location — existing code continues to compile with deprecation warnings. A &lt;code dir=&quot;auto&quot;&gt;using&lt;/code&gt; declaration keeps &lt;code dir=&quot;auto&quot;&gt;modbus_controller::SensorValueType&lt;/code&gt; working without warnings.&lt;/p&gt;
&lt;p&gt;To silence warnings, update the namespace:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#include&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;esphome/components/modbus_controller/modbus_controller.h&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; result &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; modbus_controller::&lt;/span&gt;&lt;span&gt;payload_to_number&lt;/span&gt;&lt;span&gt;(data, offset, type);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#include&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;esphome/components/modbus/helpers.h&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; result &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; modbus::helpers::&lt;/span&gt;&lt;span&gt;payload_to_number&lt;/span&gt;&lt;span&gt;(data, offset, type);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;python&quot;&gt;Python&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; esphome.components.modbus_controller &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SENSOR_VALUE_TYPE&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; esphome.components.modbus.helpers &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SENSOR_VALUE_TYPE&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.4.0 (April 2026):&lt;/strong&gt; Functions moved, deprecated shims added&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.10.0 (October 2026):&lt;/strong&gt; Deprecated shims removed&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# C++ — find references to old namespace&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;modbus_controller::number_to_payload\|modbus_controller::payload_to_number&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;modbus_controller::get_data\|modbus_controller::float_to_payload&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;modbus_controller::c_to_hex\|modbus_controller::byte_from_hex_str&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;modbus_controller::SensorValueType&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Python — find old imports&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;from esphome.components.modbus_controller import&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15291&quot;&gt;PR #15291: Share helper functions across modbus components — part A&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/14172&quot;&gt;PR #14172: Share helper functions across modbus components — part B&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Sensor .raw_state Deprecated in Favor of get_raw_state()</title><link>https://developers.esphome.io/blog/2026/04/09/sensor-raw_state-deprecated-in-favor-of-get_raw_state/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/04/09/sensor-raw_state-deprecated-in-favor-of-get_raw_state/</guid><pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Direct access to the sensor &lt;code dir=&quot;auto&quot;&gt;.raw_state&lt;/code&gt; member is now deprecated. Use &lt;code dir=&quot;auto&quot;&gt;get_raw_state()&lt;/code&gt; instead. The &lt;code dir=&quot;auto&quot;&gt;.raw_state&lt;/code&gt; member will be removed in &lt;strong&gt;ESPHome 2026.10.0&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.4.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15094&quot;&gt;PR #15094&lt;/a&gt;: Deprecate .raw_state, guard raw_callback_ behind USE_SENSOR_FILTER&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This follows the same pattern as the &lt;a href=&quot;https://github.com/esphome/esphome/pull/12246&quot;&gt;text_sensor &lt;code dir=&quot;auto&quot;&gt;.raw_state&lt;/code&gt; deprecation&lt;/a&gt;. Accessor methods allow future optimizations to the internal storage without breaking callers.&lt;/p&gt;
&lt;p&gt;Additionally, &lt;code dir=&quot;auto&quot;&gt;raw_callback_&lt;/code&gt; is now guarded behind &lt;code dir=&quot;auto&quot;&gt;USE_SENSOR_FILTER&lt;/code&gt;, saving 4 bytes per sensor instance when no filters are configured.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;float&lt;/span&gt;&lt;span&gt; raw &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;(my_sensor).&lt;/span&gt;&lt;span&gt;raw_state&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;float&lt;/span&gt;&lt;span&gt; raw &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;(my_sensor).&lt;/span&gt;&lt;span&gt;get_raw_state&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;.state&lt;/code&gt; member is &lt;strong&gt;not&lt;/strong&gt; deprecated — it remains a valid public member. However, examples in the docs now show &lt;code dir=&quot;auto&quot;&gt;get_state()&lt;/code&gt; to encourage the accessor pattern.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;External components and lambda users that directly access &lt;code dir=&quot;auto&quot;&gt;.raw_state&lt;/code&gt; on sensor instances.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The deprecated member still works but emits a compiler warning. It will be removed in 2026.10.0.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Replace all direct &lt;code dir=&quot;auto&quot;&gt;.raw_state&lt;/code&gt; access with &lt;code dir=&quot;auto&quot;&gt;get_raw_state()&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;float&lt;/span&gt;&lt;span&gt; raw &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;(my_sensor).&lt;/span&gt;&lt;span&gt;raw_state&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;float&lt;/span&gt;&lt;span&gt; current &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;raw_state&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;float&lt;/span&gt;&lt;span&gt; raw &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;(my_sensor).&lt;/span&gt;&lt;span&gt;get_raw_state&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;float&lt;/span&gt;&lt;span&gt; current &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;get_raw_state&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Nothing outside of &lt;code dir=&quot;auto&quot;&gt;Sensor::publish_state()&lt;/code&gt; should ever write to &lt;code dir=&quot;auto&quot;&gt;raw_state&lt;/code&gt;. If your component does this, it is a bug — use &lt;code dir=&quot;auto&quot;&gt;publish_state()&lt;/code&gt; instead.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;supporting-multiple-esphome-versions&quot;&gt;Supporting Multiple ESPHome Versions&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;get_raw_state()&lt;/code&gt; has been available since before this deprecation (it was previously defined in &lt;code dir=&quot;auto&quot;&gt;.cpp&lt;/code&gt;), so you can simply switch to it without a version guard:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;float&lt;/span&gt;&lt;span&gt; raw &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;(my_sensor).&lt;/span&gt;&lt;span&gt;get_raw_state&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.4.0 (April 2026):&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;.raw_state&lt;/code&gt; deprecated with compiler warning&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.10.0 (October 2026):&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;.raw_state&lt;/code&gt; member removed&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Find direct .raw_state access in your component&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;\.raw_state&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;-&gt;raw_state&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15094&quot;&gt;PR #15094: Deprecate .raw_state, guard raw_callback_ behind USE_SENSOR_FILTER&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>TemplatableFn: 4-Byte Templatable Storage for Trivially Copyable Types</title><link>https://developers.esphome.io/blog/2026/04/09/templatablefn-4-byte-templatable-storage-for-trivially-copyable-types/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/04/09/templatablefn-4-byte-templatable-storage-for-trivially-copyable-types/</guid><pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;TEMPLATABLE_VALUE&lt;/code&gt; macro now uses &lt;code dir=&quot;auto&quot;&gt;TemplatableFn&lt;/code&gt; (4 bytes) instead of &lt;code dir=&quot;auto&quot;&gt;TemplatableValue&lt;/code&gt; (8 bytes) for trivially copyable types like &lt;code dir=&quot;auto&quot;&gt;float&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;uint32_t&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;bool&lt;/code&gt;, and enums. External components that call macro-generated setters with raw C++ constants instead of going through &lt;code dir=&quot;auto&quot;&gt;cg.templatable()&lt;/code&gt; will fail to compile.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.4.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15545&quot;&gt;PR #15545&lt;/a&gt;: Add TemplatableFn for 4-byte function-pointer templatable storage&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every action in ESPHome uses &lt;code dir=&quot;auto&quot;&gt;TEMPLATABLE_VALUE&lt;/code&gt; to store values that can be either constants or lambdas. Previously, each field used &lt;code dir=&quot;auto&quot;&gt;TemplatableValue&lt;/code&gt; (8 bytes: a value slot + function pointer). For trivially copyable types, the value slot is unnecessary — a stateless lambda returning a constant compiles to a plain function pointer and costs only 4 bytes.&lt;/p&gt;
&lt;p&gt;With ~340 &lt;code dir=&quot;auto&quot;&gt;TEMPLATABLE_VALUE&lt;/code&gt; fields across the codebase for trivially copyable types, this saves ~1,360 bytes of RAM.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;three-tier-design&quot;&gt;Three-Tier Design&lt;/h2&gt;&lt;/div&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Type&lt;/th&gt;&lt;th&gt;Size (32-bit)&lt;/th&gt;&lt;th&gt;Use Case&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;TemplatableFn&amp;#x3C;T, Ts...&gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;4 bytes&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Function pointer only. Used by &lt;code dir=&quot;auto&quot;&gt;TEMPLATABLE_VALUE&lt;/code&gt; macro for trivially copyable types.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;TemplatableValue&amp;#x3C;T, Ts...&gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;8 bytes&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Value OR function pointer. Full backward compat — accepts raw constants.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;TemplatableValue&amp;#x3C;std::string, Ts...&gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;8 bytes&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Full implementation with string paths, stateful lambdas. Unchanged.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;TEMPLATABLE_VALUE&lt;/code&gt; macro automatically selects the type via &lt;code dir=&quot;auto&quot;&gt;TemplatableStorage&amp;#x3C;T, Ts...&gt;&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Trivially copyable types&lt;/strong&gt; (&lt;code dir=&quot;auto&quot;&gt;float&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;uint32_t&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;bool&lt;/code&gt;, enums, pointers) → &lt;code dir=&quot;auto&quot;&gt;TemplatableFn&lt;/code&gt; (4 bytes)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-trivially copyable types&lt;/strong&gt; (&lt;code dir=&quot;auto&quot;&gt;std::string&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;std::vector&amp;#x3C;uint8_t&gt;&lt;/code&gt;) → &lt;code dir=&quot;auto&quot;&gt;TemplatableValue&lt;/code&gt; (8 bytes)&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;1-external-components-calling-templatable_value-setters-with-raw-values&quot;&gt;1. External components calling TEMPLATABLE_VALUE setters with raw values&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;If your Python codegen calls a macro-generated setter directly with a raw C++ constant instead of going through &lt;code dir=&quot;auto&quot;&gt;cg.templatable()&lt;/code&gt;, it will fail to compile:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# This will fail — raw value passed to TemplatableFn setter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cg.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;action.&lt;/span&gt;&lt;span&gt;set_brightness&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0.5&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# This works — cg.templatable wraps constants in stateless lambdas&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cg.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;action.&lt;/span&gt;&lt;span&gt;set_brightness&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; cg.&lt;/span&gt;&lt;span&gt;templatable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;CONF_BRIGHTNESS&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[],&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;float&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;2-external-components-using-templatablevalue-for-non-string-types-with-stateful-lambdas&quot;&gt;2. External components using TemplatableValue for non-string types with stateful lambdas&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;TemplatableValue&amp;#x3C;T, Ts...&gt;&lt;/code&gt; for non-string types no longer accepts stateful lambdas (lambdas with captures). A GitHub code search found no external components using this pattern. If needed, use &lt;code dir=&quot;auto&quot;&gt;std::function&amp;#x3C;T(Ts...)&gt;&lt;/code&gt; directly.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;fix-codegen-route-values-through-cgtemplatable&quot;&gt;Fix codegen: route values through cg.templatable()&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Before — raw value passed to setter (broken)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cg.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;action.&lt;/span&gt;&lt;span&gt;set_value&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;CONF_VALUE&lt;/span&gt;&lt;span&gt;]))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# After — value wrapped via cg.templatable (correct)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# args is the list of template arguments, e.g. [(float, &quot;x&quot;)] or []&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;template_ &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; cg.&lt;/span&gt;&lt;span&gt;templatable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;CONF_VALUE&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; args&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cg.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;action.&lt;/span&gt;&lt;span&gt;set_value&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;template_&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This was technically incorrect before — the setter is part of the codegen API and should only receive values produced by &lt;code dir=&quot;auto&quot;&gt;cg.templatable()&lt;/code&gt;. The fix is straightforward.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;optional-migrate-direct-templatablevalue-usage-to-templatablefn&quot;&gt;Optional: migrate direct TemplatableValue usage to TemplatableFn&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;If your component uses &lt;code dir=&quot;auto&quot;&gt;TemplatableValue&lt;/code&gt; directly (not via the macro), it continues to work without changes. To opt into the 4-byte savings:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before (8 bytes, still works)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;TemplatableValue&lt;/span&gt;&lt;span&gt;&amp;#x3C;float&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;float&gt;&lt;/span&gt;&lt;span&gt; min_{NAN};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After (4 bytes)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;TemplatableFn&lt;/span&gt;&lt;span&gt;&amp;#x3C;float&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;float&gt;&lt;/span&gt;&lt;span&gt; min_{[]&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;float&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; -&gt; &lt;/span&gt;&lt;span&gt;float&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; NAN; }};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before (8 bytes, still works)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;TemplatableValue&lt;/span&gt;&lt;span&gt;&amp;#x3C;uint16_t&gt;&lt;/span&gt;&lt;span&gt; port_{&lt;/span&gt;&lt;span&gt;80&lt;/span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After (4 bytes)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;TemplatableFn&lt;/span&gt;&lt;span&gt;&amp;#x3C;uint16_t&gt;&lt;/span&gt;&lt;span&gt; port_{[]&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; -&gt; &lt;/span&gt;&lt;span&gt;uint16_t&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;80&lt;/span&gt;&lt;span&gt;; }};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;TemplatableFn&lt;/code&gt; has the same API as &lt;code dir=&quot;auto&quot;&gt;TemplatableValue&lt;/code&gt; (&lt;code dir=&quot;auto&quot;&gt;has_value()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;value()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;optional_value()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;value_or()&lt;/code&gt;), so no other code changes are needed.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;stateful-lambdas-on-non-string-types&quot;&gt;Stateful lambdas on non-string types&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before (hidden heap allocation via TemplatableValue)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; captured &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;42&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;TemplatableValue&lt;/span&gt;&lt;span&gt;&amp;#x3C;int&gt;&lt;/span&gt;&lt;span&gt; val &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;captured&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; captured; };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After (explicit about the allocation)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;std::function&lt;/span&gt;&lt;span&gt;&amp;#x3C;int&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; val &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;captured&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; captured; };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;TemplatableValue&amp;#x3C;std::string, Ts...&gt;&lt;/code&gt; still supports stateful lambdas — no changes needed for string types.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.4.0 (April 2026):&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;TEMPLATABLE_VALUE&lt;/code&gt; macro uses &lt;code dir=&quot;auto&quot;&gt;TemplatableFn&lt;/code&gt; for trivially copyable types&lt;/li&gt;
&lt;li&gt;No deprecation period for the macro change — &lt;code dir=&quot;auto&quot;&gt;TemplatableValue&lt;/code&gt; used directly is fully backward compatible&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Find TEMPLATABLE_VALUE setters called without cg.templatable()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;cg.add.*set_.*config\[&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/__init__.py&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Find TemplatableValue usage that could migrate to TemplatableFn&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;TemplatableValue&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.esphome.io/architecture/components/automations&quot;&gt;Implementing Automations&lt;/a&gt; covers the full &lt;code dir=&quot;auto&quot;&gt;TEMPLATABLE_VALUE&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;cg.templatable()&lt;/code&gt; pairing for actions&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15545&quot;&gt;PR #15545: Add TemplatableFn for 4-byte function-pointer templatable storage&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Trigger Trampolines Eliminated with build_callback_automation</title><link>https://developers.esphome.io/blog/2026/04/09/trigger-trampolines-eliminated-with-build_callback_automation/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/04/09/trigger-trampolines-eliminated-with-build_callback_automation/</guid><pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Common entity trigger classes have been replaced with lightweight forwarder structs that fit inline in the callback system. The new &lt;code dir=&quot;auto&quot;&gt;build_callback_automation()&lt;/code&gt; API eliminates per-trigger object allocations. Several entity callback signatures have also changed to pass state as an argument.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.4.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15174&quot;&gt;PR #15174&lt;/a&gt;:&lt;/strong&gt; Eliminate trigger trampolines with deduplicated forwarder structs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15198&quot;&gt;PR #15198&lt;/a&gt;:&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;alarm_control_panel&lt;/code&gt; — Migrate triggers to callback automation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15199&quot;&gt;PR #15199&lt;/a&gt;:&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;lock&lt;/code&gt; — Migrate LockStateTrigger to callback automation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15200&quot;&gt;PR #15200&lt;/a&gt;:&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;media_player&lt;/code&gt; — Migrate triggers to callback automation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Previously, each automation trigger created a separate C++ object that existed solely to forward a callback to an &lt;code dir=&quot;auto&quot;&gt;Automation&lt;/code&gt;. For example:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;button press → callback → ButtonPressTrigger::trigger() → Automation::trigger()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Now a lightweight forwarder struct collapses this into the callback itself:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;button press → callback → TriggerForwarder::operator()() → Automation::trigger()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The forwarder fits in the &lt;code dir=&quot;auto&quot;&gt;Callback::ctx_&lt;/code&gt; field — no additional storage needed.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;memory-savings&quot;&gt;Memory savings&lt;/h3&gt;&lt;/div&gt;























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Config&lt;/th&gt;&lt;th&gt;Platform&lt;/th&gt;&lt;th&gt;RAM Saved&lt;/th&gt;&lt;th&gt;Flash Saved&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;ratgdo (garage door)&lt;/td&gt;&lt;td&gt;ESP8266&lt;/td&gt;&lt;td&gt;&lt;strong&gt;88 bytes&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;224 bytes&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;multi-sensor device&lt;/td&gt;&lt;td&gt;ESP32-IDF&lt;/td&gt;&lt;td&gt;&lt;strong&gt;208 bytes&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;280 bytes&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;1-callback-signatures-changed&quot;&gt;1. Callback signatures changed&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Several entity callback signatures changed to pass state as an argument, enabling single-pointer forwarders:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// alarm_control_panel — Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add_on_state_callback&lt;/span&gt;&lt;span&gt;(std::function&amp;#x3C;&lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt;()&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;callback&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;template&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;typename&lt;/span&gt;&lt;span&gt; F&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add_on_state_callback&lt;/span&gt;&lt;span&gt;(F &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;callback&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Callback signature: void(AlarmControlPanelState)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// lock — Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add_on_state_callback&lt;/span&gt;&lt;span&gt;(std::function&amp;#x3C;&lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt;()&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;callback&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;template&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;typename&lt;/span&gt;&lt;span&gt; F&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add_on_state_callback&lt;/span&gt;&lt;span&gt;(F &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;callback&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Callback signature: void(LockState)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// media_player — Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add_on_state_callback&lt;/span&gt;&lt;span&gt;(std::function&amp;#x3C;&lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt;()&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;callback&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;template&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;typename&lt;/span&gt;&lt;span&gt; F&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add_on_state_callback&lt;/span&gt;&lt;span&gt;(F &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt;&lt;span&gt;callback&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Callback signature: void(MediaPlayerState)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;2-automationtrigger_-field-removed&quot;&gt;2. Automation::trigger_ field removed&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;trigger_&lt;/code&gt; protected field on &lt;code dir=&quot;auto&quot;&gt;Automation&lt;/code&gt; (set in constructor, never read) has been removed.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;External components registering callbacks on &lt;code dir=&quot;auto&quot;&gt;alarm_control_panel&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;lock&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;media_player&lt;/code&gt;&lt;/strong&gt; — must update callback signature to accept the state parameter&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External components accessing &lt;code dir=&quot;auto&quot;&gt;Automation::trigger_&lt;/code&gt;&lt;/strong&gt; — this field no longer exists&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;callback-signature-changes&quot;&gt;Callback signature changes&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// alarm_control_panel — Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;parent_&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;add_on_state_callback&lt;/span&gt;&lt;span&gt;([&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; state &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;parent_&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;get_state&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;parent_&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;add_on_state_callback&lt;/span&gt;&lt;span&gt;([&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;AlarmControlPanelState &lt;/span&gt;&lt;span&gt;state&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// state is passed directly, no need to call get_state()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// lock — Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;parent_&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;add_on_state_callback&lt;/span&gt;&lt;span&gt;([&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; state &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;parent_&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;state&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;parent_&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;add_on_state_callback&lt;/span&gt;&lt;span&gt;([&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;LockState &lt;/span&gt;&lt;span&gt;state&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// state is passed directly&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// media_player — Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;parent_&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;add_on_state_callback&lt;/span&gt;&lt;span&gt;([&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; state &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;parent_&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;state&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;parent_&lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt;add_on_state_callback&lt;/span&gt;&lt;span&gt;([&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;MediaPlayerState &lt;/span&gt;&lt;span&gt;state&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// state is passed directly&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;python-codegen-migration&quot;&gt;Python codegen migration&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;If your external component uses &lt;code dir=&quot;auto&quot;&gt;build_automation()&lt;/code&gt; with trigger classes, migrate to &lt;code dir=&quot;auto&quot;&gt;build_callback_automations()&lt;/code&gt; (&lt;a href=&quot;https://github.com/esphome/esphome/pull/15246&quot;&gt;PR #15246&lt;/a&gt;):&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; esphome &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; automation&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;MyStateTrigger &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; my_ns.&lt;/span&gt;&lt;span&gt;class_&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;MyStateTrigger&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; automation.Trigger.&lt;/span&gt;&lt;span&gt;template&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;cg.bool_&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;MyPressTrigger &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; my_ns.&lt;/span&gt;&lt;span&gt;class_&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;MyPressTrigger&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; automation.Trigger.&lt;/span&gt;&lt;span&gt;template&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CONFIG_SCHEMA&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cv.&lt;/span&gt;&lt;span&gt;Schema&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;cv.&lt;/span&gt;&lt;span&gt;Optional&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CONF_ON_STATE&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;: automation.&lt;/span&gt;&lt;span&gt;validate_automation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{cv.&lt;/span&gt;&lt;span&gt;GenerateID&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CONF_TRIGGER_ID&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;: cv.&lt;/span&gt;&lt;span&gt;declare_id&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;MyStateTrigger&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;cv.&lt;/span&gt;&lt;span&gt;Optional&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CONF_ON_PRESS&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;: automation.&lt;/span&gt;&lt;span&gt;validate_automation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{cv.&lt;/span&gt;&lt;span&gt;GenerateID&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CONF_TRIGGER_ID&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;: cv.&lt;/span&gt;&lt;span&gt;declare_id&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;MyPressTrigger&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;to_code&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; conf &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; config.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CONF_ON_STATE&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[]&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;trigger &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cg.&lt;/span&gt;&lt;span&gt;new_Pvariable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;conf&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;CONF_TRIGGER_ID&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; var&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; automation.&lt;/span&gt;&lt;span&gt;build_automation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;trigger&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;span&gt; conf&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; conf &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; config.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CONF_ON_PRESS&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[]&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;trigger &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cg.&lt;/span&gt;&lt;span&gt;new_Pvariable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;conf&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;CONF_TRIGGER_ID&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; var&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; automation.&lt;/span&gt;&lt;span&gt;build_automation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;trigger&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[],&lt;/span&gt;&lt;span&gt; conf&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# After — no trigger classes needed, no CONF_TRIGGER_ID in schema&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; esphome &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; automation&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;_CALLBACK_AUTOMATIONS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;automation.&lt;/span&gt;&lt;span&gt;CallbackAutomation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CONF_ON_STATE&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;add_on_state_callback&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;automation.&lt;/span&gt;&lt;span&gt;CallbackAutomation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CONF_ON_PRESS&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;add_on_press_callback&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CONFIG_SCHEMA&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cv.&lt;/span&gt;&lt;span&gt;Schema&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;cv.&lt;/span&gt;&lt;span&gt;Optional&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CONF_ON_STATE&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;: automation.&lt;/span&gt;&lt;span&gt;validate_automation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;cv.&lt;/span&gt;&lt;span&gt;Optional&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CONF_ON_PRESS&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;: automation.&lt;/span&gt;&lt;span&gt;validate_automation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;to_code&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; automation.&lt;/span&gt;&lt;span&gt;build_callback_automations&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; config&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; _CALLBACK_AUTOMATIONS&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;build_automation()&lt;/code&gt; and all &lt;code dir=&quot;auto&quot;&gt;Trigger&lt;/code&gt; subclasses remain available for triggers that need mutable state beyond a single &lt;code dir=&quot;auto&quot;&gt;Automation*&lt;/code&gt; pointer.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.4.0 (April 2026):&lt;/strong&gt; Trigger classes removed, callback signatures changed&lt;/li&gt;
&lt;li&gt;No deprecation period — these are signature changes and class removals&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Find alarm_control_panel/lock/media_player callback registrations&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;add_on_state_callback&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15174&quot;&gt;PR #15174: Eliminate trigger trampolines with deduplicated forwarder structs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15198&quot;&gt;PR #15198: Migrate alarm_control_panel triggers to callback automation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15199&quot;&gt;PR #15199: Migrate lock triggers to callback automation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15200&quot;&gt;PR #15200: Migrate media_player triggers to callback automation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>wake_loop Moved from Socket Component into Core</title><link>https://developers.esphome.io/blog/2026/04/09/wake_loop-moved-from-socket-component-into-core/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/04/09/wake_loop-moved-from-socket-component-into-core/</guid><pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;wake_loop_threadsafe()&lt;/code&gt; and related wake primitives have moved from the socket component into core. The &lt;code dir=&quot;auto&quot;&gt;require_wake_loop_threadsafe()&lt;/code&gt; opt-in is deprecated (now a no-op) — wake works unconditionally on all platforms.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.4.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15446&quot;&gt;PR #15446&lt;/a&gt;: Move wake_loop out of socket component into core&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The wake mechanism originally lived in the socket component because the first implementation used a UDP loopback socket. Since then, every platform has moved to nearly-free primitives (FreeRTOS task notifications, &lt;code dir=&quot;auto&quot;&gt;esp_schedule()&lt;/code&gt;, ARM &lt;code dir=&quot;auto&quot;&gt;__sev()&lt;/code&gt;), and the socket dependency was unnecessary complexity: 12 components needed to call &lt;code dir=&quot;auto&quot;&gt;require_wake_loop_threadsafe()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;#ifdef USE_WAKE_LOOP_THREADSAFE&lt;/code&gt; guards were needed at every call site, and platforms without networking had silent degradation (up to ~16ms latency).&lt;/p&gt;
&lt;p&gt;Now it just works — no opt-in, no defines, no guards.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;platform-wake-mechanisms&quot;&gt;Platform wake mechanisms&lt;/h3&gt;&lt;/div&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Platform&lt;/th&gt;&lt;th&gt;Mechanism&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;ESP32&lt;/td&gt;&lt;td&gt;FreeRTOS task notifications (ISR-safe)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;LibreTiny&lt;/td&gt;&lt;td&gt;FreeRTOS task notifications&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESP8266&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;esp_schedule()&lt;/code&gt; (IRAM-safe)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;RP2040&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;__sev()&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;__wfe()&lt;/code&gt; (ARM CPU instructions)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Zephyr (nRF52)&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;k_sem_give()&lt;/code&gt; (ISR-safe) / &lt;code dir=&quot;auto&quot;&gt;k_sem_take()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Host&lt;/td&gt;&lt;td&gt;UDP loopback socket + &lt;code dir=&quot;auto&quot;&gt;select()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;removed-defines&quot;&gt;Removed defines&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;USE_WAKE_LOOP_THREADSAFE&lt;/code&gt; — no longer needed, wake is always available&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;USE_SOCKET_SELECT_SUPPORT&lt;/code&gt; — replaced by &lt;code dir=&quot;auto&quot;&gt;USE_HOST&lt;/code&gt; where needed&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;deprecated-python-function&quot;&gt;Deprecated Python function&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;socket.require_wake_loop_threadsafe()&lt;/code&gt; (Python codegen helper) is deprecated (warns, no-op). Remove calls to it.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;no-more-ifdef-guards&quot;&gt;No more &lt;code dir=&quot;auto&quot;&gt;#ifdef&lt;/code&gt; guards&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;defined&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;USE_SOCKET_SELECT_SUPPORT&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;defined&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;USE_WAKE_LOOP_THREADSAFE&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;App&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;wake_loop_threadsafe&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#endif&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;App&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;wake_loop_threadsafe&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;socket-dependency-no-longer-needed-for-wake&quot;&gt;Socket dependency no longer needed for wake&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Components no longer need to declare socket as a dependency just for wake functionality:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;AUTO_LOAD&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;socket&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;# just for wake_loop&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# After — remove if socket was only needed for wake&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# AUTO_LOAD = []&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;External components calling &lt;code dir=&quot;auto&quot;&gt;socket.require_wake_loop_threadsafe()&lt;/code&gt;&lt;/strong&gt; in Python codegen — remove the call&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External components using &lt;code dir=&quot;auto&quot;&gt;#ifdef USE_WAKE_LOOP_THREADSAFE&lt;/code&gt;&lt;/strong&gt; — remove the guard&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External components with socket in &lt;code dir=&quot;auto&quot;&gt;AUTO_LOAD&lt;/code&gt; only for wake&lt;/strong&gt; — remove it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External components checking &lt;code dir=&quot;auto&quot;&gt;USE_SOCKET_SELECT_SUPPORT&lt;/code&gt;&lt;/strong&gt; — use &lt;code dir=&quot;auto&quot;&gt;USE_HOST&lt;/code&gt; instead&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;c--remove-ifdef-guards&quot;&gt;C++ — remove &lt;code dir=&quot;auto&quot;&gt;#ifdef&lt;/code&gt; guards&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;some_background_thread&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;defined&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;USE_SOCKET_SELECT_SUPPORT&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;defined&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;USE_WAKE_LOOP_THREADSAFE&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;App&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;wake_loop_threadsafe&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#endif&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;some_background_thread&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;App&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;wake_loop_threadsafe&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;python-codegen--remove-opt-in-call-and-socket-auto_load&quot;&gt;Python codegen — remove opt-in call and socket AUTO_LOAD&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; esphome.components &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; socket&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;AUTO_LOAD&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;socket&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;to_code&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;socket.&lt;/span&gt;&lt;span&gt;require_wake_loop_threadsafe&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# ... rest of codegen&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# After — remove socket AUTO_LOAD and require call&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;to_code&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# ... rest of codegen (no wake opt-in needed)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.4.0 (April 2026):&lt;/strong&gt; Wake moved to core, &lt;code dir=&quot;auto&quot;&gt;require_wake_loop_threadsafe()&lt;/code&gt; deprecated&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;USE_WAKE_LOOP_THREADSAFE&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;USE_SOCKET_SELECT_SUPPORT&lt;/code&gt; defines removed immediately&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Find require_wake_loop_threadsafe calls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;require_wake_loop_threadsafe&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Find #ifdef guards&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;USE_WAKE_LOOP_THREADSAFE&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Find USE_SOCKET_SELECT_SUPPORT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;USE_SOCKET_SELECT_SUPPORT&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/15446&quot;&gt;PR #15446: Move wake_loop out of socket component into core&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Application Name and Friendly Name Now Return StringRef</title><link>https://developers.esphome.io/blog/2026/03/12/application-name-and-friendly-name-now-return-stringref/</link><guid isPermaLink="true">https://developers.esphome.io/blog/2026/03/12/application-name-and-friendly-name-now-return-stringref/</guid><pubDate>Thu, 12 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;App.get_name()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;App.get_friendly_name()&lt;/code&gt; now return &lt;code dir=&quot;auto&quot;&gt;const StringRef &amp;#x26;&lt;/code&gt; instead of &lt;code dir=&quot;auto&quot;&gt;const std::string &amp;#x26;&lt;/code&gt;. Most code compiles unchanged because &lt;code dir=&quot;auto&quot;&gt;StringRef&lt;/code&gt; provides &lt;code dir=&quot;auto&quot;&gt;.c_str()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;.size()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;.empty()&lt;/code&gt;, and implicit conversion to &lt;code dir=&quot;auto&quot;&gt;std::string&lt;/code&gt;. However, binding the result to &lt;code dir=&quot;auto&quot;&gt;const std::string &amp;#x26;&lt;/code&gt; silently creates a heap-allocated temporary — update these references to &lt;code dir=&quot;auto&quot;&gt;const auto &amp;#x26;&lt;/code&gt; to avoid the allocation.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;breaking change&lt;/strong&gt; for external components in &lt;strong&gt;ESPHome 2026.3.0 and later&lt;/strong&gt;.&lt;/p&gt;

&lt;div&gt;&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/14532&quot;&gt;PR #14532&lt;/a&gt;: Application name and friendly_name return StringRef&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The application name and friendly name are set once at boot and never change. Storing them as &lt;code dir=&quot;auto&quot;&gt;std::string&lt;/code&gt; wastes heap memory for the string object overhead and its dynamic allocation. &lt;code dir=&quot;auto&quot;&gt;StringRef&lt;/code&gt; is a lightweight reference to a string with static lifetime — it avoids the heap allocation entirely.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;memory-savings&quot;&gt;Memory savings&lt;/h3&gt;&lt;/div&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Platform&lt;/th&gt;&lt;th&gt;Flash savings&lt;/th&gt;&lt;th&gt;RAM savings&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;ESP8266&lt;/td&gt;&lt;td&gt;752 bytes&lt;/td&gt;&lt;td&gt;176 bytes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;RP2040&lt;/td&gt;&lt;td&gt;2,508 bytes&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-changing&quot;&gt;What’s Changing&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; std::string &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;get_name&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; std::string &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;get_friendly_name&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; StringRef &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;get_name&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; StringRef &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;get_friendly_name&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;StringRef&lt;/code&gt; provides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;.c_str()&lt;/code&gt; — null-terminated C string pointer&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;.size()&lt;/code&gt; — string length&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;.empty()&lt;/code&gt; — true if empty&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;operator std::string()&lt;/code&gt; — implicit conversion to &lt;code dir=&quot;auto&quot;&gt;std::string&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;operator==&lt;/code&gt; — comparison with string literals and other strings&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Most code compiles unchanged.&lt;/strong&gt; The key issue is when the return value is bound to &lt;code dir=&quot;auto&quot;&gt;const std::string &amp;#x26;&lt;/code&gt; — this triggers an implicit conversion that heap-allocates a temporary &lt;code dir=&quot;auto&quot;&gt;std::string&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;who-this-affects&quot;&gt;Who This Affects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;External components that:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Store &lt;code dir=&quot;auto&quot;&gt;App.get_name()&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;App.get_friendly_name()&lt;/code&gt; in a &lt;code dir=&quot;auto&quot;&gt;const std::string &amp;#x26;&lt;/code&gt; variable&lt;/li&gt;
&lt;li&gt;Pass the result to functions expecting &lt;code dir=&quot;auto&quot;&gt;const std::string &amp;#x26;&lt;/code&gt; (triggers implicit conversion)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Standard YAML configurations are not affected.&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;fix-silent-heap-allocation&quot;&gt;Fix silent heap allocation&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before — compiles but silently heap-allocates a temporary:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; std::string &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;name &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;App&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get_name&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After — zero allocation:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;name &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;App&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get_name&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;passing-to-functions-expecting-stdstring&quot;&gt;Passing to functions expecting std::string&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;If you need to pass the name to a function that takes &lt;code dir=&quot;auto&quot;&gt;const std::string &amp;#x26;&lt;/code&gt;, the implicit conversion works but allocates. If the function also accepts &lt;code dir=&quot;auto&quot;&gt;const char *&lt;/code&gt;, prefer &lt;code dir=&quot;auto&quot;&gt;.c_str()&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Heap-allocates (implicit conversion to std::string)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;some_function_taking_string&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;App&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get_name&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Zero allocation&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;some_function_taking_cstr&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;App&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get_name&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;c_str&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;logging&quot;&gt;Logging&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Works — c_str() returns null-terminated string&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ESP_LOGD&lt;/span&gt;&lt;span&gt;(TAG, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Name: &lt;/span&gt;&lt;span&gt;%s&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;App&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get_name&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;c_str&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;supporting-multiple-esphome-versions&quot;&gt;Supporting Multiple ESPHome Versions&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Using &lt;code dir=&quot;auto&quot;&gt;const auto &amp;#x26;&lt;/code&gt; works on all versions — it binds to &lt;code dir=&quot;auto&quot;&gt;const std::string &amp;#x26;&lt;/code&gt; on older versions and &lt;code dir=&quot;auto&quot;&gt;const StringRef &amp;#x26;&lt;/code&gt; on 2026.3.0+:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Safe on all versions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;name &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;App&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get_name&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ESP_LOGD&lt;/span&gt;&lt;span&gt;(TAG, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Name: &lt;/span&gt;&lt;span&gt;%s&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;c_str&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If you need to explicitly handle both:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ESPHOME_VERSION_CODE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;VERSION_CODE&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2026&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; StringRef &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;name &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;App&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get_name&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#else&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; std::string &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;name &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;App&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get_name&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#endif&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESPHome 2026.3.0 (March 2026):&lt;/strong&gt; Return type changed&lt;/li&gt;
&lt;li&gt;No deprecation period — this is a signature change&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;finding-code-that-needs-updates&quot;&gt;Finding Code That Needs Updates&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Find std::string references to App.get_name()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;std::string.*get_name\|std::string.*get_friendly_name&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Find any usage of App.get_name() or App.get_friendly_name()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;App\.get_name\|App\.get_friendly_name\|get_name()\.c_str\|get_friendly_name()\.c_str&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your_component/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;questions&quot;&gt;Questions?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have questions about migrating your external component, please ask in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discord.gg/KhAMKrd&quot;&gt;ESPHome Discord&lt;/a&gt; - #devs channel&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/discussions&quot;&gt;ESPHome GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;related-documentation&quot;&gt;Related Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/pull/14532&quot;&gt;PR #14532: Application name and friendly_name return StringRef&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/esphome/esphome/blob/dev/esphome/core/string_ref.h&quot;&gt;StringRef source code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>