Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/blog/.authors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ authors:
description: ESPHome Maintainer
avatar: https://github.com/kbx81.png
url: https://github.com/kbx81
bdraco:
name: J. Nick Koston
description: ESPHome Maintainer
avatar: https://github.com/bdraco.png
url: https://github.com/bdraco
304 changes: 304 additions & 0 deletions docs/blog/posts/2025-11-07-fan-entity-preset-modes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
---
date: 2025-11-07
authors:
- bdraco
comments: true
---

# 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.

<!-- more -->

## Background

**[PR #11483](https://github.com/esphome/esphome/pull/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](https://github.com/esphome/esphome/pull/11483)):**
```cpp
// 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:**
```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)

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

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

### 2. Update Setter Signatures (Required Now)

```cpp
// 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.

```cpp
// 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)

```cpp
// 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

```cpp
// Remove:
#include <set>
```

## Complete Migration Example

**Before:**
```cpp
#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:**
```cpp
#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:**
```cpp
// 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):**
```cpp
// 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):**
```cpp
#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:

```bash
# 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](https://discord.gg/KhAMKrd) or open a [discussion on GitHub](https://github.com/esphome/esphome/discussions).

## Related Documentation

- [Fan Component Documentation](https://esphome.io/components/fan/index.html)
- [PR #11483: Store Preset Modes in Flash](https://github.com/esphome/esphome/pull/11483)