-
-
Notifications
You must be signed in to change notification settings - Fork 10
[blog] Add breaking change blog post for Climate entity class optimizations #68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+358
−0
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
358 changes: 358 additions & 0 deletions
358
docs/blog/posts/2025-11-07-climate-entity-optimizations.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,358 @@ | ||
| --- | ||
| date: 2025-11-07 | ||
| authors: | ||
| - bdraco | ||
| comments: true | ||
| --- | ||
|
|
||
| # Climate Entity Class: FiniteSetMask and Flash Storage Optimizations | ||
|
|
||
| ESPHome 2025.11.0 introduces significant memory optimizations to the `Climate` entity class that affect external components implementing custom climate devices. | ||
|
|
||
| <!-- more --> | ||
|
|
||
| ## Background | ||
|
|
||
| Two related PRs optimize the Climate entity class: | ||
|
|
||
| **[PR #11466](https://github.com/esphome/esphome/pull/11466): Replace std::set with FiniteSetMask** | ||
| 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). | ||
|
|
||
| **[PR #11621](https://github.com/esphome/esphome/pull/11621): Store Custom Modes in Flash** | ||
| 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). | ||
|
|
||
| ## What's Changing | ||
|
|
||
| ### For ESPHome 2025.11.0 and Later | ||
|
|
||
| **Trait Storage Changes (Breaking - [PR #11466](https://github.com/esphome/esphome/pull/11466)):** | ||
| ```cpp | ||
| // OLD - std::set for enums | ||
| void set_supported_modes(const std::set<climate::ClimateMode> &modes); | ||
| std::set<climate::ClimateMode> modes_; | ||
|
|
||
| // NEW - FiniteSetMask for enums | ||
| void set_supported_modes(climate::ClimateModeMask modes); | ||
| climate::ClimateModeMask modes_; | ||
| ``` | ||
| **Custom Mode Storage Changes (Breaking - [PR #11621](https://github.com/esphome/esphome/pull/11621)):** | ||
| ```cpp | ||
| // OLD - std::string heap storage | ||
| void add_supported_custom_fan_mode(const std::string &mode); | ||
| std::set<std::string> custom_fan_modes_; | ||
| // NEW - const char* flash storage | ||
| void set_supported_custom_fan_modes(std::initializer_list<const char *> modes); | ||
| std::vector<const char *> custom_fan_modes_; | ||
| ``` | ||
|
|
||
| ## Who This Affects | ||
|
|
||
| This affects **external components** that: | ||
|
|
||
| - Implement custom Climate entities in C++ | ||
| - Store or manipulate climate trait sets (modes, fan modes, swing modes, presets) | ||
| - Use custom fan modes or custom presets | ||
| - Access custom mode members directly (now private, must use accessor methods) | ||
|
|
||
| **Standard YAML configurations are not affected** and require no changes. | ||
|
|
||
| ## Migration Guide | ||
|
|
||
| ### 1. Update Trait Function Signatures (Required Now) | ||
|
|
||
| **For all four enum mask types:** | ||
| ```cpp | ||
| // OLD | ||
| void set_supported_modes(const std::set<climate::ClimateMode> &modes); | ||
| void set_supported_fan_modes(const std::set<climate::ClimateFanMode> &modes); | ||
| void set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes); | ||
| void set_supported_presets(const std::set<climate::ClimatePreset> &presets); | ||
|
|
||
| // NEW | ||
| void set_supported_modes(climate::ClimateModeMask modes); | ||
| void set_supported_fan_modes(climate::ClimateFanModeMask modes); | ||
| void set_supported_swing_modes(climate::ClimateSwingModeMask modes); | ||
| void set_supported_presets(climate::ClimatePresetMask presets); | ||
| ``` | ||
| ### 2. Update Member Variables (Required Now) | ||
| **Enum masks:** | ||
| ```cpp | ||
| // OLD | ||
| std::set<climate::ClimateMode> modes_; | ||
| std::set<climate::ClimateFanMode> fan_modes_; | ||
| std::set<climate::ClimateSwingMode> swing_modes_; | ||
| std::set<climate::ClimatePreset> presets_; | ||
| // NEW | ||
| climate::ClimateModeMask modes_; | ||
| climate::ClimateFanModeMask fan_modes_; | ||
| climate::ClimateSwingModeMask swing_modes_; | ||
| climate::ClimatePresetMask presets_; | ||
| ``` | ||
|
|
||
| **Custom modes (heap → flash storage):** | ||
| ```cpp | ||
| // OLD - std::set in heap | ||
| std::set<std::string> custom_fan_modes_; | ||
| std::set<std::string> custom_presets_; | ||
|
|
||
| // NEW - const char* in flash | ||
| std::vector<const char *> custom_fan_modes_; | ||
| std::vector<const char *> custom_presets_; | ||
| ``` | ||
|
|
||
| ### 3. Update Trait API Calls (Required Now) | ||
|
|
||
| **Checking for enum modes (FiniteSetMask API):** | ||
| ```cpp | ||
| // OLD - std::set API | ||
| if (modes_.find(CLIMATE_MODE_HEAT) != modes_.end()) { ... } | ||
|
|
||
| // NEW - count() method (std::set-compatible) | ||
| if (modes_.count(CLIMATE_MODE_HEAT)) { ... } | ||
| ``` | ||
| **Setting custom modes:** | ||
| ```cpp | ||
| // OLD - incremental add | ||
| traits.add_supported_custom_fan_mode("Turbo"); | ||
| traits.add_supported_custom_fan_mode("Silent"); | ||
| // NEW - set all at once with string literals | ||
| traits.set_supported_custom_fan_modes({"Turbo", "Silent"}); | ||
| traits.set_supported_custom_presets({"Eco", "Comfort", "Boost"}); | ||
| ``` | ||
|
|
||
| ### 4. Update ClimateCall Usage in control() Methods | ||
|
|
||
| **ClimateCall getters now return const char* directly:** | ||
| ```cpp | ||
| // OLD - optional<std::string> | ||
| if (call.get_custom_preset().has_value()) { | ||
| std::string preset = *call.get_custom_preset(); | ||
| if (preset == "Turbo") { | ||
| // ... | ||
| } | ||
| } | ||
|
|
||
| // NEW - const char* with has_*() helper | ||
| if (call.has_custom_preset()) { | ||
| const char *preset = call.get_custom_preset(); | ||
| if (strcmp(preset, "Turbo") == 0) { | ||
| // ... | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### 5. Use Protected Setters for Custom Modes (Required) | ||
|
|
||
| **Custom mode members are now private** - use protected setter methods in derived classes: | ||
|
|
||
| ```cpp | ||
| // OLD - direct member assignment (NO LONGER COMPILES) | ||
| this->custom_fan_mode = "Turbo"; // ERROR: member is private | ||
| this->custom_preset = nullptr; // ERROR: member is private | ||
|
|
||
| // NEW - use protected setter methods | ||
| this->set_custom_fan_mode_("Turbo"); // Validates against traits | ||
| this->clear_custom_preset_(); // Clear custom preset | ||
|
|
||
| // Setting primary modes (automatically clears custom modes) | ||
| this->set_fan_mode_(CLIMATE_FAN_HIGH); // Clears custom_fan_mode_ | ||
| this->set_preset_(CLIMATE_PRESET_AWAY); // Clears custom_preset_ | ||
| ``` | ||
| **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. | ||
| **Protected setter methods available:** | ||
| - `bool set_fan_mode_(ClimateFanMode mode)` - Set fan mode, clear custom fan mode | ||
| - `bool set_custom_fan_mode_(const char *mode)` - Set custom fan mode, clear fan_mode | ||
| - `void clear_custom_fan_mode_()` - Clear custom fan mode | ||
| - `bool set_preset_(ClimatePreset preset)` - Set preset, clear custom preset | ||
| - `bool set_custom_preset_(const char *preset)` - Set custom preset, clear preset | ||
| - `void clear_custom_preset_()` - Clear custom preset | ||
| ### 6. Use Accessor Methods for Reading State | ||
| **Reading custom modes from Climate object:** | ||
| ```cpp | ||
| // OLD - direct member access (NO LONGER COMPILES) | ||
| if (climate->custom_fan_mode.has_value()) { | ||
| resp.set_custom_fan_mode(climate->custom_fan_mode.value()); | ||
| } | ||
| // NEW - use accessor methods | ||
| if (climate->has_custom_fan_mode()) { | ||
| resp.set_custom_fan_mode(climate->get_custom_fan_mode()); | ||
| } | ||
| ``` | ||
|
|
||
| **Public accessor methods on Climate class:** | ||
| - `bool has_custom_fan_mode() const` - Check if custom fan mode is active | ||
| - `const char *get_custom_fan_mode() const` - Get custom fan mode (read-only) | ||
| - `bool has_custom_preset() const` - Check if custom preset is active | ||
| - `const char *get_custom_preset() const` - Get custom preset (read-only) | ||
|
|
||
| ### 7. Remove Unnecessary Includes | ||
|
|
||
| ```cpp | ||
| // Remove: | ||
| #include <set> | ||
| ``` | ||
|
|
||
| ## FiniteSetMask API Compatibility | ||
|
|
||
| The `FiniteSetMask` API is mostly compatible with `std::set`: | ||
|
|
||
| **Compatible methods:** | ||
| - `.insert(value)` - Add mode | ||
| - `.count(value)` - Check if mode exists (returns 0 or 1) | ||
| - `.erase(value)` - Remove mode | ||
| - `.size()`, `.empty()`, `.clear()` - Same as std::set | ||
| - Range-based for loops work identically | ||
|
|
||
| **Differences:** | ||
| - `.find()` is not available - use `.count()` instead | ||
| - Iterators are available but behave slightly differently | ||
|
|
||
| ## Complete Migration Example | ||
|
|
||
| **Before:** | ||
| ```cpp | ||
| #include <set> | ||
|
|
||
| class MyClimate : public Climate { | ||
| public: | ||
| void set_modes(const std::set<climate::ClimateMode> &modes) { | ||
| this->modes_ = modes; | ||
| } | ||
|
|
||
| climate::ClimateTraits traits() override { | ||
| auto traits = climate::ClimateTraits(); | ||
| traits.set_supported_modes(this->modes_); | ||
| traits.add_supported_custom_fan_mode("Turbo"); | ||
| traits.add_supported_custom_fan_mode("Silent"); | ||
| return traits; | ||
| } | ||
|
|
||
| void control(const climate::ClimateCall &call) override { | ||
| if (call.get_custom_fan_mode().has_value()) { | ||
| std::string mode = *call.get_custom_fan_mode(); | ||
| if (mode == "Turbo") { | ||
| this->custom_fan_mode = "Turbo"; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| protected: | ||
| std::set<climate::ClimateMode> modes_; | ||
| }; | ||
| ``` | ||
| **After:** | ||
| ```cpp | ||
| // No <set> include needed | ||
| class MyClimate : public Climate { | ||
| public: | ||
| void set_modes(climate::ClimateModeMask modes) { | ||
| this->modes_ = modes; | ||
| } | ||
| climate::ClimateTraits traits() override { | ||
| auto traits = climate::ClimateTraits(); | ||
| traits.set_supported_modes(this->modes_); | ||
| traits.set_supported_custom_fan_modes({"Turbo", "Silent"}); | ||
| return traits; | ||
| } | ||
| void control(const climate::ClimateCall &call) override { | ||
| if (call.has_custom_fan_mode()) { | ||
| const char *mode = call.get_custom_fan_mode(); | ||
| if (strcmp(mode, "Turbo") == 0) { | ||
| this->set_custom_fan_mode_("Turbo"); // Use protected setter | ||
| } | ||
| } | ||
| } | ||
| protected: | ||
| climate::ClimateModeMask modes_; | ||
| }; | ||
| ``` | ||
|
|
||
| ## Lifetime Safety for Custom Modes | ||
|
|
||
| All `const char*` pointers must point to memory that lives for the component's lifetime: | ||
|
|
||
| **Safe patterns:** | ||
| ```cpp | ||
| // 1. String literals (preferred) | ||
| traits.set_supported_custom_fan_modes({"Turbo", "Silent", "Eco"}); | ||
|
|
||
| // 2. Static constants | ||
| static const char *const MODE_TURBO = "Turbo"; | ||
| traits.set_supported_custom_fan_modes({MODE_TURBO}); | ||
|
|
||
| // 3. C arrays | ||
| static constexpr const char *const FAN_MODES[] = {"Low", "Medium", "High"}; | ||
| traits.set_supported_custom_fan_modes(FAN_MODES); | ||
|
|
||
| // 4. Extracting from existing persistent storage (e.g., std::map keys) | ||
| std::vector<const char *> preset_ptrs; | ||
| for (const auto &entry : this->custom_preset_config_) { | ||
| preset_ptrs.push_back(entry.first.c_str()); // Map key lives with component | ||
| } | ||
| traits.set_supported_custom_presets(preset_ptrs); | ||
| ``` | ||
|
|
||
| **Unsafe patterns (DO NOT USE):** | ||
| ```cpp | ||
| // WRONG - temporary string | ||
| std::string temp = "Mode"; | ||
| traits.set_supported_custom_fan_modes({temp.c_str()}); // Dangling pointer! | ||
|
|
||
| // WRONG - local array | ||
| const char *modes[] = {"Mode1", "Mode2"}; | ||
| traits.set_supported_custom_fan_modes(modes); // Array destroyed after function! | ||
| ``` | ||
| The protected setters (`set_custom_fan_mode_()`, `set_custom_preset_()`) validate that pointers exist in the traits, ensuring they point to persistent memory. | ||
| ## Timeline | ||
| - **ESPHome 2025.11.0 (November 2025):** | ||
bdraco marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - Both changes are active (breaking changes) | ||
| - External components must update to new APIs | ||
| ## Finding Code That Needs Updates | ||
| Search your external component code for these patterns: | ||
| ```bash | ||
| # Find std::set usage for climate enums | ||
| grep -r 'std::set<.*Climate' --include='*.cpp' --include='*.h' | ||
| # Find add_supported_custom usage (removed API) | ||
| grep -r 'add_supported_custom' --include='*.cpp' --include='*.h' | ||
| # Find direct custom mode member access (now private) | ||
| grep -r '->custom_fan_mode' --include='*.cpp' --include='*.h' | ||
| grep -r '->custom_preset' --include='*.cpp' --include='*.h' | ||
| # Find optional<std::string> custom mode usage | ||
| grep -r 'optional<std::string>.*custom' --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 | ||
|
|
||
| - [Climate Component Documentation](https://esphome.io/components/climate/index.html) | ||
| - [PR #11466: FiniteSetMask for Trait Storage](https://github.com/esphome/esphome/pull/11466) | ||
| - [PR #11621: Store Custom Modes in Flash](https://github.com/esphome/esphome/pull/11621) | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.