Skip to content

Commit 1841a3e

Browse files
bdracojesserockz
andauthored
[blog] Add breaking change blog post for Fan entity preset mode optimizations (#69)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
1 parent 1f1893e commit 1841a3e

File tree

1 file changed

+304
-0
lines changed

1 file changed

+304
-0
lines changed
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
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

Comments
 (0)