Skip to content

Commit 1f1893e

Browse files
bdracojesserockz
andauthored
[blog] Add breaking change blog post for Climate entity class optimizations (#68)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
1 parent 46e20d8 commit 1f1893e

File tree

1 file changed

+358
-0
lines changed

1 file changed

+358
-0
lines changed
Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
---
2+
date: 2025-11-07
3+
authors:
4+
- bdraco
5+
comments: true
6+
---
7+
8+
# Climate Entity Class: FiniteSetMask and Flash Storage Optimizations
9+
10+
ESPHome 2025.11.0 introduces significant memory optimizations to the `Climate` entity class that affect external components implementing custom climate devices.
11+
12+
<!-- more -->
13+
14+
## Background
15+
16+
Two related PRs optimize the Climate entity class:
17+
18+
**[PR #11466](https://github.com/esphome/esphome/pull/11466): Replace std::set with FiniteSetMask**
19+
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).
20+
21+
**[PR #11621](https://github.com/esphome/esphome/pull/11621): Store Custom Modes in Flash**
22+
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).
23+
24+
## What's Changing
25+
26+
### For ESPHome 2025.11.0 and Later
27+
28+
**Trait Storage Changes (Breaking - [PR #11466](https://github.com/esphome/esphome/pull/11466)):**
29+
```cpp
30+
// OLD - std::set for enums
31+
void set_supported_modes(const std::set<climate::ClimateMode> &modes);
32+
std::set<climate::ClimateMode> modes_;
33+
34+
// NEW - FiniteSetMask for enums
35+
void set_supported_modes(climate::ClimateModeMask modes);
36+
climate::ClimateModeMask modes_;
37+
```
38+
39+
**Custom Mode Storage Changes (Breaking - [PR #11621](https://github.com/esphome/esphome/pull/11621)):**
40+
```cpp
41+
// OLD - std::string heap storage
42+
void add_supported_custom_fan_mode(const std::string &mode);
43+
std::set<std::string> custom_fan_modes_;
44+
45+
// NEW - const char* flash storage
46+
void set_supported_custom_fan_modes(std::initializer_list<const char *> modes);
47+
std::vector<const char *> custom_fan_modes_;
48+
```
49+
50+
## Who This Affects
51+
52+
This affects **external components** that:
53+
54+
- Implement custom Climate entities in C++
55+
- Store or manipulate climate trait sets (modes, fan modes, swing modes, presets)
56+
- Use custom fan modes or custom presets
57+
- Access custom mode members directly (now private, must use accessor methods)
58+
59+
**Standard YAML configurations are not affected** and require no changes.
60+
61+
## Migration Guide
62+
63+
### 1. Update Trait Function Signatures (Required Now)
64+
65+
**For all four enum mask types:**
66+
```cpp
67+
// OLD
68+
void set_supported_modes(const std::set<climate::ClimateMode> &modes);
69+
void set_supported_fan_modes(const std::set<climate::ClimateFanMode> &modes);
70+
void set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes);
71+
void set_supported_presets(const std::set<climate::ClimatePreset> &presets);
72+
73+
// NEW
74+
void set_supported_modes(climate::ClimateModeMask modes);
75+
void set_supported_fan_modes(climate::ClimateFanModeMask modes);
76+
void set_supported_swing_modes(climate::ClimateSwingModeMask modes);
77+
void set_supported_presets(climate::ClimatePresetMask presets);
78+
```
79+
80+
### 2. Update Member Variables (Required Now)
81+
82+
**Enum masks:**
83+
```cpp
84+
// OLD
85+
std::set<climate::ClimateMode> modes_;
86+
std::set<climate::ClimateFanMode> fan_modes_;
87+
std::set<climate::ClimateSwingMode> swing_modes_;
88+
std::set<climate::ClimatePreset> presets_;
89+
90+
// NEW
91+
climate::ClimateModeMask modes_;
92+
climate::ClimateFanModeMask fan_modes_;
93+
climate::ClimateSwingModeMask swing_modes_;
94+
climate::ClimatePresetMask presets_;
95+
```
96+
97+
**Custom modes (heap → flash storage):**
98+
```cpp
99+
// OLD - std::set in heap
100+
std::set<std::string> custom_fan_modes_;
101+
std::set<std::string> custom_presets_;
102+
103+
// NEW - const char* in flash
104+
std::vector<const char *> custom_fan_modes_;
105+
std::vector<const char *> custom_presets_;
106+
```
107+
108+
### 3. Update Trait API Calls (Required Now)
109+
110+
**Checking for enum modes (FiniteSetMask API):**
111+
```cpp
112+
// OLD - std::set API
113+
if (modes_.find(CLIMATE_MODE_HEAT) != modes_.end()) { ... }
114+
115+
// NEW - count() method (std::set-compatible)
116+
if (modes_.count(CLIMATE_MODE_HEAT)) { ... }
117+
```
118+
119+
**Setting custom modes:**
120+
```cpp
121+
// OLD - incremental add
122+
traits.add_supported_custom_fan_mode("Turbo");
123+
traits.add_supported_custom_fan_mode("Silent");
124+
125+
// NEW - set all at once with string literals
126+
traits.set_supported_custom_fan_modes({"Turbo", "Silent"});
127+
traits.set_supported_custom_presets({"Eco", "Comfort", "Boost"});
128+
```
129+
130+
### 4. Update ClimateCall Usage in control() Methods
131+
132+
**ClimateCall getters now return const char* directly:**
133+
```cpp
134+
// OLD - optional<std::string>
135+
if (call.get_custom_preset().has_value()) {
136+
std::string preset = *call.get_custom_preset();
137+
if (preset == "Turbo") {
138+
// ...
139+
}
140+
}
141+
142+
// NEW - const char* with has_*() helper
143+
if (call.has_custom_preset()) {
144+
const char *preset = call.get_custom_preset();
145+
if (strcmp(preset, "Turbo") == 0) {
146+
// ...
147+
}
148+
}
149+
```
150+
151+
### 5. Use Protected Setters for Custom Modes (Required)
152+
153+
**Custom mode members are now private** - use protected setter methods in derived classes:
154+
155+
```cpp
156+
// OLD - direct member assignment (NO LONGER COMPILES)
157+
this->custom_fan_mode = "Turbo"; // ERROR: member is private
158+
this->custom_preset = nullptr; // ERROR: member is private
159+
160+
// NEW - use protected setter methods
161+
this->set_custom_fan_mode_("Turbo"); // Validates against traits
162+
this->clear_custom_preset_(); // Clear custom preset
163+
164+
// Setting primary modes (automatically clears custom modes)
165+
this->set_fan_mode_(CLIMATE_FAN_HIGH); // Clears custom_fan_mode_
166+
this->set_preset_(CLIMATE_PRESET_AWAY); // Clears custom_preset_
167+
```
168+
169+
**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.
170+
171+
**Protected setter methods available:**
172+
- `bool set_fan_mode_(ClimateFanMode mode)` - Set fan mode, clear custom fan mode
173+
- `bool set_custom_fan_mode_(const char *mode)` - Set custom fan mode, clear fan_mode
174+
- `void clear_custom_fan_mode_()` - Clear custom fan mode
175+
- `bool set_preset_(ClimatePreset preset)` - Set preset, clear custom preset
176+
- `bool set_custom_preset_(const char *preset)` - Set custom preset, clear preset
177+
- `void clear_custom_preset_()` - Clear custom preset
178+
179+
### 6. Use Accessor Methods for Reading State
180+
181+
**Reading custom modes from Climate object:**
182+
```cpp
183+
// OLD - direct member access (NO LONGER COMPILES)
184+
if (climate->custom_fan_mode.has_value()) {
185+
resp.set_custom_fan_mode(climate->custom_fan_mode.value());
186+
}
187+
188+
// NEW - use accessor methods
189+
if (climate->has_custom_fan_mode()) {
190+
resp.set_custom_fan_mode(climate->get_custom_fan_mode());
191+
}
192+
```
193+
194+
**Public accessor methods on Climate class:**
195+
- `bool has_custom_fan_mode() const` - Check if custom fan mode is active
196+
- `const char *get_custom_fan_mode() const` - Get custom fan mode (read-only)
197+
- `bool has_custom_preset() const` - Check if custom preset is active
198+
- `const char *get_custom_preset() const` - Get custom preset (read-only)
199+
200+
### 7. Remove Unnecessary Includes
201+
202+
```cpp
203+
// Remove:
204+
#include <set>
205+
```
206+
207+
## FiniteSetMask API Compatibility
208+
209+
The `FiniteSetMask` API is mostly compatible with `std::set`:
210+
211+
**Compatible methods:**
212+
- `.insert(value)` - Add mode
213+
- `.count(value)` - Check if mode exists (returns 0 or 1)
214+
- `.erase(value)` - Remove mode
215+
- `.size()`, `.empty()`, `.clear()` - Same as std::set
216+
- Range-based for loops work identically
217+
218+
**Differences:**
219+
- `.find()` is not available - use `.count()` instead
220+
- Iterators are available but behave slightly differently
221+
222+
## Complete Migration Example
223+
224+
**Before:**
225+
```cpp
226+
#include <set>
227+
228+
class MyClimate : public Climate {
229+
public:
230+
void set_modes(const std::set<climate::ClimateMode> &modes) {
231+
this->modes_ = modes;
232+
}
233+
234+
climate::ClimateTraits traits() override {
235+
auto traits = climate::ClimateTraits();
236+
traits.set_supported_modes(this->modes_);
237+
traits.add_supported_custom_fan_mode("Turbo");
238+
traits.add_supported_custom_fan_mode("Silent");
239+
return traits;
240+
}
241+
242+
void control(const climate::ClimateCall &call) override {
243+
if (call.get_custom_fan_mode().has_value()) {
244+
std::string mode = *call.get_custom_fan_mode();
245+
if (mode == "Turbo") {
246+
this->custom_fan_mode = "Turbo";
247+
}
248+
}
249+
}
250+
251+
protected:
252+
std::set<climate::ClimateMode> modes_;
253+
};
254+
```
255+
256+
**After:**
257+
```cpp
258+
// No <set> include needed
259+
260+
class MyClimate : public Climate {
261+
public:
262+
void set_modes(climate::ClimateModeMask modes) {
263+
this->modes_ = modes;
264+
}
265+
266+
climate::ClimateTraits traits() override {
267+
auto traits = climate::ClimateTraits();
268+
traits.set_supported_modes(this->modes_);
269+
traits.set_supported_custom_fan_modes({"Turbo", "Silent"});
270+
return traits;
271+
}
272+
273+
void control(const climate::ClimateCall &call) override {
274+
if (call.has_custom_fan_mode()) {
275+
const char *mode = call.get_custom_fan_mode();
276+
if (strcmp(mode, "Turbo") == 0) {
277+
this->set_custom_fan_mode_("Turbo"); // Use protected setter
278+
}
279+
}
280+
}
281+
282+
protected:
283+
climate::ClimateModeMask modes_;
284+
};
285+
```
286+
287+
## Lifetime Safety for Custom Modes
288+
289+
All `const char*` pointers must point to memory that lives for the component's lifetime:
290+
291+
**Safe patterns:**
292+
```cpp
293+
// 1. String literals (preferred)
294+
traits.set_supported_custom_fan_modes({"Turbo", "Silent", "Eco"});
295+
296+
// 2. Static constants
297+
static const char *const MODE_TURBO = "Turbo";
298+
traits.set_supported_custom_fan_modes({MODE_TURBO});
299+
300+
// 3. C arrays
301+
static constexpr const char *const FAN_MODES[] = {"Low", "Medium", "High"};
302+
traits.set_supported_custom_fan_modes(FAN_MODES);
303+
304+
// 4. Extracting from existing persistent storage (e.g., std::map keys)
305+
std::vector<const char *> preset_ptrs;
306+
for (const auto &entry : this->custom_preset_config_) {
307+
preset_ptrs.push_back(entry.first.c_str()); // Map key lives with component
308+
}
309+
traits.set_supported_custom_presets(preset_ptrs);
310+
```
311+
312+
**Unsafe patterns (DO NOT USE):**
313+
```cpp
314+
// WRONG - temporary string
315+
std::string temp = "Mode";
316+
traits.set_supported_custom_fan_modes({temp.c_str()}); // Dangling pointer!
317+
318+
// WRONG - local array
319+
const char *modes[] = {"Mode1", "Mode2"};
320+
traits.set_supported_custom_fan_modes(modes); // Array destroyed after function!
321+
```
322+
323+
The protected setters (`set_custom_fan_mode_()`, `set_custom_preset_()`) validate that pointers exist in the traits, ensuring they point to persistent memory.
324+
325+
## Timeline
326+
327+
- **ESPHome 2025.11.0 (November 2025):**
328+
- Both changes are active (breaking changes)
329+
- External components must update to new APIs
330+
331+
## Finding Code That Needs Updates
332+
333+
Search your external component code for these patterns:
334+
335+
```bash
336+
# Find std::set usage for climate enums
337+
grep -r 'std::set<.*Climate' --include='*.cpp' --include='*.h'
338+
339+
# Find add_supported_custom usage (removed API)
340+
grep -r 'add_supported_custom' --include='*.cpp' --include='*.h'
341+
342+
# Find direct custom mode member access (now private)
343+
grep -r '->custom_fan_mode' --include='*.cpp' --include='*.h'
344+
grep -r '->custom_preset' --include='*.cpp' --include='*.h'
345+
346+
# Find optional<std::string> custom mode usage
347+
grep -r 'optional<std::string>.*custom' --include='*.cpp' --include='*.h'
348+
```
349+
350+
## Questions?
351+
352+
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).
353+
354+
## Related Documentation
355+
356+
- [Climate Component Documentation](https://esphome.io/components/climate/index.html)
357+
- [PR #11466: FiniteSetMask for Trait Storage](https://github.com/esphome/esphome/pull/11466)
358+
- [PR #11621: Store Custom Modes in Flash](https://github.com/esphome/esphome/pull/11621)

0 commit comments

Comments
 (0)