Climate Entity Class: FiniteSetMask and Flash Storage Optimizations
ESPHome 2025.11.0 introduces significant memory optimizations to the Climate entity class that affect external components implementing custom climate devices.
Background
Section titled “Background”Two related PRs optimize the Climate entity class:
PR #11466: Replace std::set with FiniteSetMask
Replaces std::set<EnumType> with FiniteSetMask for storing climate trait enums (modes, fan modes, swing modes, presets). Real device measurements show ~440 bytes heap savings per climate entity, plus 2,587 bytes flash savings and elimination of red-black tree code (~4KB).
PR #11621: Store Custom Modes in Flash
Changes custom fan mode and preset storage from std::vector<std::string> to std::vector<const char*>, storing strings in flash. Saves ~48 bytes per ClimateCall and ~24 bytes per custom mode/preset string (24 bytes std::string overhead + string length).
What’s Changing
Section titled “What’s Changing”For ESPHome 2025.11.0 and Later
Section titled “For ESPHome 2025.11.0 and Later”Trait Storage Changes (Breaking - PR #11466):
// OLD - std::set for enumsvoid set_supported_modes(const std::set<climate::ClimateMode> &modes);std::set<climate::ClimateMode> modes_;
// NEW - FiniteSetMask for enumsvoid set_supported_modes(climate::ClimateModeMask modes);climate::ClimateModeMask modes_;Custom Mode Storage Changes (Breaking - PR #11621):
// OLD - std::string heap storagevoid add_supported_custom_fan_mode(const std::string &mode);std::set<std::string> custom_fan_modes_;
// NEW - const char* flash storagevoid set_supported_custom_fan_modes(std::initializer_list<const char *> modes);std::vector<const char *> custom_fan_modes_;Who This Affects
Section titled “Who This Affects”This affects external components that:
- Implement custom Climate entities in C++
- Store or manipulate climate trait sets (modes, fan modes, swing modes, presets)
- Use custom fan modes or custom presets
- Access custom mode members directly (now private, must use accessor methods)
Standard YAML configurations are not affected and require no changes.
Migration Guide
Section titled “Migration Guide”1. Update Trait Function Signatures (Required Now)
Section titled “1. Update Trait Function Signatures (Required Now)”For all four enum mask types:
// OLDvoid set_supported_modes(const std::set<climate::ClimateMode> &modes);void set_supported_fan_modes(const std::set<climate::ClimateFanMode> &modes);void set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes);void set_supported_presets(const std::set<climate::ClimatePreset> &presets);
// NEWvoid set_supported_modes(climate::ClimateModeMask modes);void set_supported_fan_modes(climate::ClimateFanModeMask modes);void set_supported_swing_modes(climate::ClimateSwingModeMask modes);void set_supported_presets(climate::ClimatePresetMask presets);2. Update Member Variables (Required Now)
Section titled “2. Update Member Variables (Required Now)”Enum masks:
// OLDstd::set<climate::ClimateMode> modes_;std::set<climate::ClimateFanMode> fan_modes_;std::set<climate::ClimateSwingMode> swing_modes_;std::set<climate::ClimatePreset> presets_;
// NEWclimate::ClimateModeMask modes_;climate::ClimateFanModeMask fan_modes_;climate::ClimateSwingModeMask swing_modes_;climate::ClimatePresetMask presets_;Custom modes (heap → flash storage):
// OLD - std::set in heapstd::set<std::string> custom_fan_modes_;std::set<std::string> custom_presets_;
// NEW - const char* in flashstd::vector<const char *> custom_fan_modes_;std::vector<const char *> custom_presets_;3. Update Trait API Calls (Required Now)
Section titled “3. Update Trait API Calls (Required Now)”Checking for enum modes (FiniteSetMask API):
// OLD - std::set APIif (modes_.find(CLIMATE_MODE_HEAT) != modes_.end()) { ... }
// NEW - count() method (std::set-compatible)if (modes_.count(CLIMATE_MODE_HEAT)) { ... }Setting custom modes:
// OLD - incremental addtraits.add_supported_custom_fan_mode("Turbo");traits.add_supported_custom_fan_mode("Silent");
// NEW - set all at once with string literalstraits.set_supported_custom_fan_modes({"Turbo", "Silent"});traits.set_supported_custom_presets({"Eco", "Comfort", "Boost"});4. Update ClimateCall Usage in control() Methods
Section titled “4. Update ClimateCall Usage in control() Methods”ClimateCall getters now return const char directly:*
// OLD - optional<std::string>if (call.get_custom_preset().has_value()) { std::string preset = *call.get_custom_preset(); if (preset == "Turbo") { // ... }}
// NEW - const char* with has_*() helperif (call.has_custom_preset()) { const char *preset = call.get_custom_preset(); if (strcmp(preset, "Turbo") == 0) { // ... }}5. Use Protected Setters for Custom Modes (Required)
Section titled “5. Use Protected Setters for Custom Modes (Required)”Custom mode members are now private - use protected setter methods in derived classes:
// OLD - direct member assignment (NO LONGER COMPILES)this->custom_fan_mode = "Turbo"; // ERROR: member is privatethis->custom_preset = nullptr; // ERROR: member is private
// NEW - use protected setter methodsthis->set_custom_fan_mode_("Turbo"); // Validates against traitsthis->clear_custom_preset_(); // Clear custom preset
// Setting primary modes (automatically clears custom modes)this->set_fan_mode_(CLIMATE_FAN_HIGH); // Clears custom_fan_mode_this->set_preset_(CLIMATE_PRESET_AWAY); // Clears custom_preset_Why private? Climate devices require mutual exclusion between primary modes (e.g., CLIMATE_FAN_HIGH) and custom modes (e.g., "Turbo"). Private members with protected setters enforce this automatically, preventing bugs.
Protected setter methods available:
bool set_fan_mode_(ClimateFanMode mode)- Set fan mode, clear custom fan modebool set_custom_fan_mode_(const char *mode)- Set custom fan mode, clear fan_modevoid clear_custom_fan_mode_()- Clear custom fan modebool set_preset_(ClimatePreset preset)- Set preset, clear custom presetbool set_custom_preset_(const char *preset)- Set custom preset, clear presetvoid clear_custom_preset_()- Clear custom preset
6. Use Accessor Methods for Reading State
Section titled “6. Use Accessor Methods for Reading State”Reading custom modes from Climate object:
// OLD - direct member access (NO LONGER COMPILES)if (climate->custom_fan_mode.has_value()) { resp.set_custom_fan_mode(climate->custom_fan_mode.value());}
// NEW - use accessor methodsif (climate->has_custom_fan_mode()) { resp.set_custom_fan_mode(climate->get_custom_fan_mode());}Public accessor methods on Climate class:
bool has_custom_fan_mode() const- Check if custom fan mode is activeconst char *get_custom_fan_mode() const- Get custom fan mode (read-only)bool has_custom_preset() const- Check if custom preset is activeconst char *get_custom_preset() const- Get custom preset (read-only)
7. Remove Unnecessary Includes
Section titled “7. Remove Unnecessary Includes”// Remove:#include <set>FiniteSetMask API Compatibility
Section titled “FiniteSetMask API Compatibility”The FiniteSetMask API is mostly compatible with std::set:
Compatible methods:
.insert(value)- Add mode.count(value)- Check if mode exists (returns 0 or 1).erase(value)- Remove mode.size(),.empty(),.clear()- Same as std::set- Range-based for loops work identically
Differences:
.find()is not available - use.count()instead- Iterators are available but behave slightly differently
Complete Migration Example
Section titled “Complete Migration Example”Before:
#include <set>
class MyClimate : public Climate { public: void set_modes(const std::set<climate::ClimateMode> &modes) { this->modes_ = modes; }
climate::ClimateTraits traits() override { auto traits = climate::ClimateTraits(); traits.set_supported_modes(this->modes_); traits.add_supported_custom_fan_mode("Turbo"); traits.add_supported_custom_fan_mode("Silent"); return traits; }
void control(const climate::ClimateCall &call) override { if (call.get_custom_fan_mode().has_value()) { std::string mode = *call.get_custom_fan_mode(); if (mode == "Turbo") { this->custom_fan_mode = "Turbo"; } } }
protected: std::set<climate::ClimateMode> modes_;};After:
// No <set> include needed
class MyClimate : public Climate { public: void set_modes(climate::ClimateModeMask modes) { this->modes_ = modes; }
climate::ClimateTraits traits() override { auto traits = climate::ClimateTraits(); traits.set_supported_modes(this->modes_); traits.set_supported_custom_fan_modes({"Turbo", "Silent"}); return traits; }
void control(const climate::ClimateCall &call) override { if (call.has_custom_fan_mode()) { const char *mode = call.get_custom_fan_mode(); if (strcmp(mode, "Turbo") == 0) { this->set_custom_fan_mode_("Turbo"); // Use protected setter } } }
protected: climate::ClimateModeMask modes_;};Lifetime Safety for Custom Modes
Section titled “Lifetime Safety for Custom Modes”All const char* pointers must point to memory that lives for the component’s lifetime:
Safe patterns:
// 1. String literals (preferred)traits.set_supported_custom_fan_modes({"Turbo", "Silent", "Eco"});
// 2. Static constantsstatic const char *const MODE_TURBO = "Turbo";traits.set_supported_custom_fan_modes({MODE_TURBO});
// 3. C arraysstatic constexpr const char *const FAN_MODES[] = {"Low", "Medium", "High"};traits.set_supported_custom_fan_modes(FAN_MODES);
// 4. Extracting from existing persistent storage (e.g., std::map keys)std::vector<const char *> preset_ptrs;for (const auto &entry : this->custom_preset_config_) { preset_ptrs.push_back(entry.first.c_str()); // Map key lives with component}traits.set_supported_custom_presets(preset_ptrs);Unsafe patterns (DO NOT USE):
// WRONG - temporary stringstd::string temp = "Mode";traits.set_supported_custom_fan_modes({temp.c_str()}); // Dangling pointer!
// WRONG - local arrayconst char *modes[] = {"Mode1", "Mode2"};traits.set_supported_custom_fan_modes(modes); // Array destroyed after function!The protected setters (set_custom_fan_mode_(), set_custom_preset_()) validate that pointers exist in the traits, ensuring they point to persistent memory.
Timeline
Section titled “Timeline”- ESPHome 2025.11.0 (November 2025):
- Both changes are active (breaking changes)
- External components must update to new APIs
Finding Code That Needs Updates
Section titled “Finding Code That Needs Updates”Search your external component code for these patterns:
# Find std::set usage for climate enumsgrep -r 'std::set<.*Climate' --include='*.cpp' --include='*.h'
# Find add_supported_custom usage (removed API)grep -r 'add_supported_custom' --include='*.cpp' --include='*.h'
# Find direct custom mode member access (now private)grep -r '->custom_fan_mode' --include='*.cpp' --include='*.h'grep -r '->custom_preset' --include='*.cpp' --include='*.h'
# Find optional<std::string> custom mode usagegrep -r 'optional<std::string>.*custom' --include='*.cpp' --include='*.h'Questions?
Section titled “Questions?”If you have questions about these changes or need help migrating your external component, please ask in the ESPHome Discord or open a discussion on GitHub.
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.