Climate and Fan Custom Mode Vectors Moved to Entity
Custom mode vectors (custom_fan_modes, custom_presets for climate; preset_modes for fan) are now stored on the entity base class instead of being rebuilt inside traits on every call. The old ClimateTraits and FanTraits setter methods are deprecated.
This is a breaking change for external components in ESPHome 2026.4.0 and later.
Background
Section titled “Background”- PR #15206: Climate — Store custom mode vectors on Climate entity
- PR #15209: Fan — Store preset mode vector on Fan entity
ClimateTraits and FanTraits contained std::vector members that were reconstructed on every traits call. Climate uses traits() and Fan uses get_traits() — both are called during publish_state() and control(), the hottest paths in these components. This caused heap allocations on every state update. On long-running ESP devices, repeated allocations of different sizes fragment the heap and can lead to crashes.
Moving the vectors to the entity base class eliminates all heap allocation from traits copies.
What’s Changing
Section titled “What’s Changing”Climate
Section titled “Climate”Custom fan modes and custom presets are now set directly on the Climate entity:
// Before — set on traits in traits() overrideclimate::ClimateTraits traits() override { auto traits = climate::ClimateTraits(); traits.set_supported_custom_fan_modes({"Low", "Medium", "High"}); traits.set_supported_custom_presets({"Eco", "Sleep"}); return traits;}
// After — set once during setup or codegenvoid setup() override { this->set_supported_custom_fan_modes({"Low", "Medium", "High"}); this->set_supported_custom_presets({"Eco", "Sleep"});}The old ClimateTraits::set_supported_custom_fan_modes() and ClimateTraits::set_supported_custom_presets() methods still work but are deprecated and will be removed in 2026.11.0.
Preset modes are now set directly on the Fan entity. Unlike Climate, Fan::get_traits() is the virtual method itself, so the get_traits() override must wire the stored preset modes into the returned traits via wire_preset_modes_():
// Before — set on traits in get_traits() overridefan::FanTraits get_traits() override { return fan::FanTraits(true, true, true, this->preset_modes_);}
// After — set once during setup or codegen, wire into traits in get_traits()void setup() override { this->set_supported_preset_modes({"Auto", "Sleep", "Nature"});}
fan::FanTraits get_traits() override { fan::FanTraits traits(true, true, true); this->wire_preset_modes_(traits); return traits;}The old FanTraits::set_supported_preset_modes() methods still work but are deprecated and will be removed in 2026.11.0.
Who This Affects
Section titled “Who This Affects”External climate components (~35 affected) that call ClimateTraits::set_supported_custom_fan_modes() or ClimateTraits::set_supported_custom_presets().
External fan components (~15 affected) that pass preset modes through FanTraits constructors or call FanTraits::set_supported_preset_modes().
Migration Guide
Section titled “Migration Guide”Climate
Section titled “Climate”// Before — in traits() override, called on every publish_state()climate::ClimateTraits traits() override { auto traits = climate::ClimateTraits(); traits.set_supported_custom_fan_modes({"Low", "Medium", "High"}); traits.set_supported_custom_presets({"Eco", "Sleep"}); // ... other traits return traits;}
// After — set once, traits() gets them automaticallyvoid setup() override { this->set_supported_custom_fan_modes({"Low", "Medium", "High"}); this->set_supported_custom_presets({"Eco", "Sleep"});}
climate::ClimateTraits traits() override { auto traits = climate::ClimateTraits(); // custom_fan_modes and custom_presets are wired automatically // ... other traits return traits;}// Beforefan::FanTraits get_traits() override { return fan::FanTraits(true, true, true, this->preset_modes_);}
// After — get_traits() override must call wire_preset_modes_() to expose themvoid setup() override { this->set_supported_preset_modes({"Auto", "Sleep", "Nature"});}
fan::FanTraits get_traits() override { fan::FanTraits traits(true, true, true); this->wire_preset_modes_(traits); return traits;}Note: Unlike Climate — where the base class’s non-virtual
get_traits()wraps a virtualtraits()and wires custom modes automatically —Fan::get_traits()is itself the virtual method overridden by components. The base class has no opportunity to inject preset modes, so each override must callwire_preset_modes_()explicitly. The internalspeed,template, andhbridgefan components follow this pattern.
Python codegen
Section titled “Python codegen”# Before — setting custom modes via traits in C++ generated codecg.add(traits.set_supported_custom_fan_modes(modes))
# After — setting custom modes directly on the entitycg.add(var.set_supported_custom_fan_modes(modes))Timeline
Section titled “Timeline”- ESPHome 2026.4.0 (April 2026): Old traits setters deprecated
- ESPHome 2026.11.0 (November 2026): Deprecated setters removed, traits become trivially copyable
Finding Code That Needs Updates
Section titled “Finding Code That Needs Updates”# Climate — find traits-based custom mode settersgrep -rn 'set_supported_custom_fan_modes\|set_supported_custom_presets' your_component/
# Fan — find traits-based preset mode settersgrep -rn 'set_supported_preset_modes' your_component/Questions?
Section titled “Questions?”If you have questions about migrating your external component, please ask in:
- ESPHome Discord - #devs channel
- ESPHome GitHub Discussions
Related Documentation
Section titled “Related Documentation”Copyright © 2026 ESPHome - A project from the Open Home Foundation
Comments
Feel free to leave a comment here to discuss this post with others. You can ask questions, share your experience, or suggest improvements. If you have a question about a specific feature or issue, please consider using the ESPHome Discord. Stick to English and follow ESPHome's code of conduct. These comments exist on a discussion on GitHub, so you can also comment there directly if you prefer.