Skip to content

Fan Entity Class: Preset Mode Flash Storage and Order Preservation

ESPHome 2025.11.0 introduces a memory optimization to the Fan entity class that also changes how preset modes are ordered. This affects external components implementing custom fan devices and may change the display order of preset modes in Home Assistant.

Background

PR #11483: Store Preset Modes in Flash Changes preset mode storage from std::set<std::string> (heap, alphabetically sorted) to std::vector<const char*> (flash, preserves YAML order). Saves ~80 bytes for the std::set structure plus at least 24 bytes overhead per preset (more for longer strings). Strings move from heap to flash memory. This is particularly important for ESP8266 devices with limited heap.

What's Changing

For ESPHome 2025.11.0 and Later

Storage Changes (Breaking - PR #11483):

// OLD - std::set in heap, alphabetically sorted
std::set<std::string> preset_modes_;
traits.set_supported_preset_modes(modes);  // std::set parameter

// NEW - std::vector of const char* in flash, preserves order
std::vector<const char *> preset_modes_;
traits.set_supported_preset_modes({"Low", "Medium", "High"});  // initializer_list

User-Facing Change: Preset modes now appear in Home Assistant in the order you define them in YAML, not alphabetically. This makes Fan consistent with all other components (select options, climate presets, etc.).

Who This Affects

External components that: - Explicitly create std::set<std::string> and pass it to set_supported_preset_modes() in C++ - Store or manipulate fan preset mode lists in member variables

Note: Most components already use the correct syntax since Python code generation produces initializer lists like traits.set_supported_preset_modes({"Low", "Medium", "High"}). This only affects external components that manually create sets in C++ code.

YAML users may notice: - Preset mode order in Home Assistant changes to match YAML order instead of alphabetical - This is a behavioral change - you now control the display order

Standard YAML configurations work without code changes, but the display order may change.

User-Facing Behavior Change

Preset Mode Display Order

Previously, fan preset modes were always sorted alphabetically regardless of YAML order (only fan presets had this limitation). Now they preserve YAML order like all other components.

Example YAML:

fan:
  - platform: template
    name: "Bedroom Fan"
    preset_modes:
      - "Turbo"
      - "Normal"
      - "Sleep"

Before (alphabetical sort): Normal → Sleep → Turbo After (YAML order): Turbo → Normal → Sleep

Action: If you want a specific order in Home Assistant, arrange preset modes in your YAML in that order.

Migration Guide for External Components

1. Update Container Type (Required Now)

// OLD
#include <set>
std::set<std::string> preset_modes_;

// NEW
#include <vector>
std::vector<const char *> preset_modes_;

2. Update Setter Signatures (Required Now)

// OLD
void set_preset_modes(const std::set<std::string> &presets) {
  this->preset_modes_ = presets;
}

// NEW - use initializer list for string literals
void set_preset_modes(std::initializer_list<const char *> presets) {
  this->preset_modes_ = presets;
}

3. Update Trait Calls (If You Explicitly Created Sets)

Note: Most components already pass initializer lists directly and don't need changes. This only affects code that explicitly creates std::set variables.

// OLD - explicitly creating std::set (uncommon)
std::set<std::string> modes = {"Low", "Medium", "High"};
traits.set_supported_preset_modes(modes);

// NEW - initializer list with string literals (most components already did this)
traits.set_supported_preset_modes({"Low", "Medium", "High"});

4. Update Lookups (Required Now)

// OLD - std::set::find
if (this->preset_modes_.find(mode) != this->preset_modes_.end()) {
  // mode is supported
}

// NEW - linear search with strcmp
bool found = false;
for (const char *m : this->preset_modes_) {
  if (strcmp(m, mode.c_str()) == 0) {
    found = true;
    break;
  }
}
if (found) {
  // mode is supported
}

// Or use std::find_if (cleaner but adds STL overhead)
auto it = std::find_if(this->preset_modes_.begin(), this->preset_modes_.end(),
                       [&mode](const char *m) { return strcmp(m, mode.c_str()) == 0; });
if (it != this->preset_modes_.end()) {
  // mode is supported
}

Note: std::find_if is cleaner but adds STL template overhead. For ESP8266 devices with tight flash constraints, prefer the manual loop approach. For typical fan preset counts (3-6 items), linear search performance is negligible and the simpler approach is fine.

5. Remove Unnecessary Includes

// Remove:
#include <set>

Complete Migration Example

Before:

#include <set>

class MyFan : public fan::Fan {
 public:
  void set_preset_modes(const std::set<std::string> &modes) {
    this->preset_modes_ = modes;
  }

  fan::FanTraits get_traits() override {
    auto traits = fan::FanTraits();
    traits.set_supported_preset_modes(this->preset_modes_);
    return traits;
  }

  void control(const fan::FanCall &call) override {
    if (!call.get_preset_mode().empty()) {
      std::string mode = call.get_preset_mode();
      if (this->preset_modes_.find(mode) != this->preset_modes_.end()) {
        // Set mode
      }
    }
  }

 protected:
  std::set<std::string> preset_modes_;
};

After:

#include <vector>

class MyFan : public fan::Fan {
 public:
  void set_preset_modes(std::initializer_list<const char *> modes) {
    this->preset_modes_ = modes;
  }

  fan::FanTraits get_traits() override {
    auto traits = fan::FanTraits();
    traits.set_supported_preset_modes(this->preset_modes_);
    return traits;
  }

  void control(const fan::FanCall &call) override {
    if (!call.get_preset_mode().empty()) {
      const std::string &mode = call.get_preset_mode();
      auto it = std::find_if(this->preset_modes_.begin(), this->preset_modes_.end(),
                             [&mode](const char *m) { return strcmp(m, mode.c_str()) == 0; });
      if (it != this->preset_modes_.end()) {
        // Set mode
      }
    }
  }

 protected:
  std::vector<const char *> preset_modes_;
};

Lifetime Safety for Preset Modes

All const char* pointers must point to memory that lives for the component's lifetime:

Safe patterns:

// 1. String literals (preferred) - stored in flash
traits.set_supported_preset_modes({"Low", "Medium", "High"});

// 2. Static constants
static const char *const PRESET_LOW = "Low";
traits.set_supported_preset_modes({PRESET_LOW});

// 3. C arrays
static constexpr const char *const PRESETS[] = {"Low", "Medium", "High"};
traits.set_supported_preset_modes({PRESETS[0], PRESETS[1], PRESETS[2]});

Unsafe patterns (DO NOT USE):

// WRONG - temporary string
std::string temp = "Low";
traits.set_supported_preset_modes({temp.c_str()});  // Dangling pointer!

// WRONG - local array
const char *modes[] = {"Low", "High"};
traits.set_supported_preset_modes({modes[0], modes[1]});  // Array destroyed!

For dynamic modes (rare):

#include "esphome/core/helpers.h"

class MyFan : public fan::Fan {
 protected:
  // Storage for strings (must persist)
  FixedVector<std::string> preset_strings_;
  // Pointers into preset_strings_
  std::vector<const char *> preset_modes_;

  void setup() override {
    // Read dynamic presets
    this->preset_strings_.init(mode_count);
    for (size_t i = 0; i < mode_count; i++) {
      this->preset_strings_.push_back(this->read_mode_from_device(i));
    }

    // Build pointer array
    this->preset_modes_.clear();
    for (const auto &s : this->preset_strings_) {
      this->preset_modes_.push_back(s.c_str());
    }

    // Set traits
    this->traits_.set_supported_preset_modes(this->preset_modes_);
  }
};

Important: The preset_strings_ member must outlive preset_modes_ and never be resized or modified after the pointers are assigned, as this would invalidate the pointers in preset_modes_. FixedVector guarantees this by allocating all storage upfront with init() and never reallocating.

Timeline

  • ESPHome 2025.11.0 (November 2025):
  • Storage change is active (breaking change for external components)
  • Preset mode order changes to YAML order (user-facing behavior change)

Finding Code That Needs Updates

Search your external component code for these patterns:

# Find std::set usage for fan preset modes
grep -r 'std::set<.*string>.*preset' --include='*.cpp' --include='*.h'

# Find set_supported_preset_modes calls
grep -r 'set_supported_preset_modes' --include='*.cpp' --include='*.h'

# Find preset_modes_ member variables
grep -r 'preset_modes_' --include='*.cpp' --include='*.h'

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.

Comments

Feel free to leave a comment here to discuss this post wth 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.