|
| 1 | +--- |
| 2 | +date: 2025-11-07 |
| 3 | +authors: |
| 4 | + - bdraco |
| 5 | +comments: true |
| 6 | +--- |
| 7 | + |
| 8 | +# Fan Entity Class: Preset Mode Flash Storage and Order Preservation |
| 9 | + |
| 10 | +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. |
| 11 | + |
| 12 | +<!-- more --> |
| 13 | + |
| 14 | +## Background |
| 15 | + |
| 16 | +**[PR #11483](https://github.com/esphome/esphome/pull/11483): Store Preset Modes in Flash** |
| 17 | +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. |
| 18 | + |
| 19 | +## What's Changing |
| 20 | + |
| 21 | +### For ESPHome 2025.11.0 and Later |
| 22 | + |
| 23 | +**Storage Changes (Breaking - [PR #11483](https://github.com/esphome/esphome/pull/11483)):** |
| 24 | +```cpp |
| 25 | +// OLD - std::set in heap, alphabetically sorted |
| 26 | +std::set<std::string> preset_modes_; |
| 27 | +traits.set_supported_preset_modes(modes); // std::set parameter |
| 28 | + |
| 29 | +// NEW - std::vector of const char* in flash, preserves order |
| 30 | +std::vector<const char *> preset_modes_; |
| 31 | +traits.set_supported_preset_modes({"Low", "Medium", "High"}); // initializer_list |
| 32 | +``` |
| 33 | +
|
| 34 | +**User-Facing Change:** |
| 35 | +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.). |
| 36 | +
|
| 37 | +## Who This Affects |
| 38 | +
|
| 39 | +**External components** that: |
| 40 | +- Explicitly create `std::set<std::string>` and pass it to `set_supported_preset_modes()` in C++ |
| 41 | +- Store or manipulate fan preset mode lists in member variables |
| 42 | +
|
| 43 | +**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. |
| 44 | +
|
| 45 | +**YAML users** may notice: |
| 46 | +- Preset mode order in Home Assistant changes to match YAML order instead of alphabetical |
| 47 | +- This is a **behavioral change** - you now control the display order |
| 48 | +
|
| 49 | +**Standard YAML configurations** work without code changes, but the display order may change. |
| 50 | +
|
| 51 | +## User-Facing Behavior Change |
| 52 | +
|
| 53 | +### Preset Mode Display Order |
| 54 | +
|
| 55 | +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. |
| 56 | +
|
| 57 | +**Example YAML:** |
| 58 | +```yaml |
| 59 | +fan: |
| 60 | + - platform: template |
| 61 | + name: "Bedroom Fan" |
| 62 | + preset_modes: |
| 63 | + - "Turbo" |
| 64 | + - "Normal" |
| 65 | + - "Sleep" |
| 66 | +``` |
| 67 | + |
| 68 | +**Before (alphabetical sort):** Normal → Sleep → Turbo |
| 69 | +**After (YAML order):** Turbo → Normal → Sleep |
| 70 | + |
| 71 | +**Action:** If you want a specific order in Home Assistant, arrange preset modes in your YAML in that order. |
| 72 | + |
| 73 | +## Migration Guide for External Components |
| 74 | + |
| 75 | +### 1. Update Container Type (Required Now) |
| 76 | + |
| 77 | +```cpp |
| 78 | +// OLD |
| 79 | +#include <set> |
| 80 | +std::set<std::string> preset_modes_; |
| 81 | + |
| 82 | +// NEW |
| 83 | +#include <vector> |
| 84 | +std::vector<const char *> preset_modes_; |
| 85 | +``` |
| 86 | + |
| 87 | +### 2. Update Setter Signatures (Required Now) |
| 88 | + |
| 89 | +```cpp |
| 90 | +// OLD |
| 91 | +void set_preset_modes(const std::set<std::string> &presets) { |
| 92 | + this->preset_modes_ = presets; |
| 93 | +} |
| 94 | + |
| 95 | +// NEW - use initializer list for string literals |
| 96 | +void set_preset_modes(std::initializer_list<const char *> presets) { |
| 97 | + this->preset_modes_ = presets; |
| 98 | +} |
| 99 | +``` |
| 100 | +
|
| 101 | +### 3. Update Trait Calls (If You Explicitly Created Sets) |
| 102 | +
|
| 103 | +**Note:** Most components already pass initializer lists directly and don't need changes. This only affects code that explicitly creates `std::set` variables. |
| 104 | +
|
| 105 | +```cpp |
| 106 | +// OLD - explicitly creating std::set (uncommon) |
| 107 | +std::set<std::string> modes = {"Low", "Medium", "High"}; |
| 108 | +traits.set_supported_preset_modes(modes); |
| 109 | +
|
| 110 | +// NEW - initializer list with string literals (most components already did this) |
| 111 | +traits.set_supported_preset_modes({"Low", "Medium", "High"}); |
| 112 | +``` |
| 113 | + |
| 114 | +### 4. Update Lookups (Required Now) |
| 115 | + |
| 116 | +```cpp |
| 117 | +// OLD - std::set::find |
| 118 | +if (this->preset_modes_.find(mode) != this->preset_modes_.end()) { |
| 119 | + // mode is supported |
| 120 | +} |
| 121 | + |
| 122 | +// NEW - linear search with strcmp |
| 123 | +bool found = false; |
| 124 | +for (const char *m : this->preset_modes_) { |
| 125 | + if (strcmp(m, mode.c_str()) == 0) { |
| 126 | + found = true; |
| 127 | + break; |
| 128 | + } |
| 129 | +} |
| 130 | +if (found) { |
| 131 | + // mode is supported |
| 132 | +} |
| 133 | + |
| 134 | +// Or use std::find_if (cleaner but adds STL overhead) |
| 135 | +auto it = std::find_if(this->preset_modes_.begin(), this->preset_modes_.end(), |
| 136 | + [&mode](const char *m) { return strcmp(m, mode.c_str()) == 0; }); |
| 137 | +if (it != this->preset_modes_.end()) { |
| 138 | + // mode is supported |
| 139 | +} |
| 140 | +``` |
| 141 | + |
| 142 | +**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. |
| 143 | + |
| 144 | +### 5. Remove Unnecessary Includes |
| 145 | + |
| 146 | +```cpp |
| 147 | +// Remove: |
| 148 | +#include <set> |
| 149 | +``` |
| 150 | + |
| 151 | +## Complete Migration Example |
| 152 | + |
| 153 | +**Before:** |
| 154 | +```cpp |
| 155 | +#include <set> |
| 156 | + |
| 157 | +class MyFan : public fan::Fan { |
| 158 | + public: |
| 159 | + void set_preset_modes(const std::set<std::string> &modes) { |
| 160 | + this->preset_modes_ = modes; |
| 161 | + } |
| 162 | + |
| 163 | + fan::FanTraits get_traits() override { |
| 164 | + auto traits = fan::FanTraits(); |
| 165 | + traits.set_supported_preset_modes(this->preset_modes_); |
| 166 | + return traits; |
| 167 | + } |
| 168 | + |
| 169 | + void control(const fan::FanCall &call) override { |
| 170 | + if (!call.get_preset_mode().empty()) { |
| 171 | + std::string mode = call.get_preset_mode(); |
| 172 | + if (this->preset_modes_.find(mode) != this->preset_modes_.end()) { |
| 173 | + // Set mode |
| 174 | + } |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + protected: |
| 179 | + std::set<std::string> preset_modes_; |
| 180 | +}; |
| 181 | +``` |
| 182 | +
|
| 183 | +**After:** |
| 184 | +```cpp |
| 185 | +#include <vector> |
| 186 | +
|
| 187 | +class MyFan : public fan::Fan { |
| 188 | + public: |
| 189 | + void set_preset_modes(std::initializer_list<const char *> modes) { |
| 190 | + this->preset_modes_ = modes; |
| 191 | + } |
| 192 | +
|
| 193 | + fan::FanTraits get_traits() override { |
| 194 | + auto traits = fan::FanTraits(); |
| 195 | + traits.set_supported_preset_modes(this->preset_modes_); |
| 196 | + return traits; |
| 197 | + } |
| 198 | +
|
| 199 | + void control(const fan::FanCall &call) override { |
| 200 | + if (!call.get_preset_mode().empty()) { |
| 201 | + const std::string &mode = call.get_preset_mode(); |
| 202 | + auto it = std::find_if(this->preset_modes_.begin(), this->preset_modes_.end(), |
| 203 | + [&mode](const char *m) { return strcmp(m, mode.c_str()) == 0; }); |
| 204 | + if (it != this->preset_modes_.end()) { |
| 205 | + // Set mode |
| 206 | + } |
| 207 | + } |
| 208 | + } |
| 209 | +
|
| 210 | + protected: |
| 211 | + std::vector<const char *> preset_modes_; |
| 212 | +}; |
| 213 | +``` |
| 214 | + |
| 215 | +## Lifetime Safety for Preset Modes |
| 216 | + |
| 217 | +All `const char*` pointers must point to memory that lives for the component's lifetime: |
| 218 | + |
| 219 | +**Safe patterns:** |
| 220 | +```cpp |
| 221 | +// 1. String literals (preferred) - stored in flash |
| 222 | +traits.set_supported_preset_modes({"Low", "Medium", "High"}); |
| 223 | + |
| 224 | +// 2. Static constants |
| 225 | +static const char *const PRESET_LOW = "Low"; |
| 226 | +traits.set_supported_preset_modes({PRESET_LOW}); |
| 227 | + |
| 228 | +// 3. C arrays |
| 229 | +static constexpr const char *const PRESETS[] = {"Low", "Medium", "High"}; |
| 230 | +traits.set_supported_preset_modes({PRESETS[0], PRESETS[1], PRESETS[2]}); |
| 231 | +``` |
| 232 | +
|
| 233 | +**Unsafe patterns (DO NOT USE):** |
| 234 | +```cpp |
| 235 | +// WRONG - temporary string |
| 236 | +std::string temp = "Low"; |
| 237 | +traits.set_supported_preset_modes({temp.c_str()}); // Dangling pointer! |
| 238 | +
|
| 239 | +// WRONG - local array |
| 240 | +const char *modes[] = {"Low", "High"}; |
| 241 | +traits.set_supported_preset_modes({modes[0], modes[1]}); // Array destroyed! |
| 242 | +``` |
| 243 | + |
| 244 | +**For dynamic modes (rare):** |
| 245 | +```cpp |
| 246 | +#include "esphome/core/helpers.h" |
| 247 | + |
| 248 | +class MyFan : public fan::Fan { |
| 249 | + protected: |
| 250 | + // Storage for strings (must persist) |
| 251 | + FixedVector<std::string> preset_strings_; |
| 252 | + // Pointers into preset_strings_ |
| 253 | + std::vector<const char *> preset_modes_; |
| 254 | + |
| 255 | + void setup() override { |
| 256 | + // Read dynamic presets |
| 257 | + this->preset_strings_.init(mode_count); |
| 258 | + for (size_t i = 0; i < mode_count; i++) { |
| 259 | + this->preset_strings_.push_back(this->read_mode_from_device(i)); |
| 260 | + } |
| 261 | + |
| 262 | + // Build pointer array |
| 263 | + this->preset_modes_.clear(); |
| 264 | + for (const auto &s : this->preset_strings_) { |
| 265 | + this->preset_modes_.push_back(s.c_str()); |
| 266 | + } |
| 267 | + |
| 268 | + // Set traits |
| 269 | + this->traits_.set_supported_preset_modes(this->preset_modes_); |
| 270 | + } |
| 271 | +}; |
| 272 | +``` |
| 273 | + |
| 274 | +**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. |
| 275 | + |
| 276 | +## Timeline |
| 277 | + |
| 278 | +- **ESPHome 2025.11.0 (November 2025):** |
| 279 | + - Storage change is active (breaking change for external components) |
| 280 | + - Preset mode order changes to YAML order (user-facing behavior change) |
| 281 | + |
| 282 | +## Finding Code That Needs Updates |
| 283 | + |
| 284 | +Search your external component code for these patterns: |
| 285 | + |
| 286 | +```bash |
| 287 | +# Find std::set usage for fan preset modes |
| 288 | +grep -r 'std::set<.*string>.*preset' --include='*.cpp' --include='*.h' |
| 289 | + |
| 290 | +# Find set_supported_preset_modes calls |
| 291 | +grep -r 'set_supported_preset_modes' --include='*.cpp' --include='*.h' |
| 292 | + |
| 293 | +# Find preset_modes_ member variables |
| 294 | +grep -r 'preset_modes_' --include='*.cpp' --include='*.h' |
| 295 | +``` |
| 296 | + |
| 297 | +## Questions? |
| 298 | + |
| 299 | +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). |
| 300 | + |
| 301 | +## Related Documentation |
| 302 | + |
| 303 | +- [Fan Component Documentation](https://esphome.io/components/fan/index.html) |
| 304 | +- [PR #11483: Store Preset Modes in Flash](https://github.com/esphome/esphome/pull/11483) |
0 commit comments