From 210320b8ccb9b4f628e4ba325caa451cff2c4923 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 18:43:17 -0500 Subject: [PATCH 01/40] simplify --- esphome/components/climate/climate.cpp | 126 ++++++++++++-------- esphome/components/climate/climate.h | 31 +++-- esphome/components/climate/climate_traits.h | 14 ++- 3 files changed, 107 insertions(+), 64 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 64f43ffd80..275db1d423 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -50,21 +50,21 @@ void ClimateCall::perform() { const LogString *mode_s = climate_mode_to_string(*this->mode_); ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); } - if (this->custom_fan_mode_.has_value()) { + if (this->custom_fan_mode_ != nullptr) { this->fan_mode_.reset(); - ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_.value().c_str()); + ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_); } if (this->fan_mode_.has_value()) { - this->custom_fan_mode_.reset(); + this->custom_fan_mode_ = nullptr; const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); } - if (this->custom_preset_.has_value()) { + if (this->custom_preset_ != nullptr) { this->preset_.reset(); - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_.value().c_str()); + ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); } if (this->preset_.has_value()) { - this->custom_preset_.reset(); + this->custom_preset_ = nullptr; const LogString *preset_s = climate_preset_to_string(*this->preset_); ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); } @@ -96,11 +96,10 @@ void ClimateCall::validate_() { this->mode_.reset(); } } - if (this->custom_fan_mode_.has_value()) { - auto custom_fan_mode = *this->custom_fan_mode_; - if (!traits.supports_custom_fan_mode(custom_fan_mode)) { - ESP_LOGW(TAG, " Fan Mode %s not supported", custom_fan_mode.c_str()); - this->custom_fan_mode_.reset(); + if (this->custom_fan_mode_ != nullptr) { + if (!traits.supports_custom_fan_mode(this->custom_fan_mode_)) { + ESP_LOGW(TAG, " Fan Mode %s not supported", this->custom_fan_mode_); + this->custom_fan_mode_ = nullptr; } } else if (this->fan_mode_.has_value()) { auto fan_mode = *this->fan_mode_; @@ -109,11 +108,10 @@ void ClimateCall::validate_() { this->fan_mode_.reset(); } } - if (this->custom_preset_.has_value()) { - auto custom_preset = *this->custom_preset_; - if (!traits.supports_custom_preset(custom_preset)) { - ESP_LOGW(TAG, " Preset %s not supported", custom_preset.c_str()); - this->custom_preset_.reset(); + if (this->custom_preset_ != nullptr) { + if (!traits.supports_custom_preset(this->custom_preset_)) { + ESP_LOGW(TAG, " Preset %s not supported", this->custom_preset_); + this->custom_preset_ = nullptr; } } else if (this->preset_.has_value()) { auto preset = *this->preset_; @@ -186,26 +184,33 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) { ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { this->fan_mode_ = fan_mode; - this->custom_fan_mode_.reset(); + this->custom_fan_mode_ = nullptr; return *this; } -ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { +ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) { + // Check if it's a standard enum mode first for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) { - if (str_equals_case_insensitive(fan_mode, mode_entry.str)) { + if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) { this->set_fan_mode(static_cast(mode_entry.value)); return *this; } } - if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) { - this->custom_fan_mode_ = fan_mode; - this->fan_mode_.reset(); - } else { - ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str()); + // Find the matching pointer from traits + const auto &supported = this->parent_->get_traits().get_supported_custom_fan_modes(); + for (const char *mode : supported) { + if (strcmp(mode, custom_fan_mode) == 0) { + this->custom_fan_mode_ = mode; + this->fan_mode_.reset(); + return *this; + } } + ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode); return *this; } +ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { return this->set_fan_mode(fan_mode.c_str()); } + ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { if (fan_mode.has_value()) { this->set_fan_mode(fan_mode.value()); @@ -215,26 +220,33 @@ ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { ClimateCall &ClimateCall::set_preset(ClimatePreset preset) { this->preset_ = preset; - this->custom_preset_.reset(); + this->custom_preset_ = nullptr; return *this; } -ClimateCall &ClimateCall::set_preset(const std::string &preset) { +ClimateCall &ClimateCall::set_preset(const char *custom_preset) { + // Check if it's a standard enum preset first for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) { - if (str_equals_case_insensitive(preset, preset_entry.str)) { + if (str_equals_case_insensitive(custom_preset, preset_entry.str)) { this->set_preset(static_cast(preset_entry.value)); return *this; } } - if (this->parent_->get_traits().supports_custom_preset(preset)) { - this->custom_preset_ = preset; - this->preset_.reset(); - } else { - ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str()); + // Find the matching pointer from traits + const auto &supported = this->parent_->get_traits().get_supported_custom_presets(); + for (const char *preset : supported) { + if (strcmp(preset, custom_preset) == 0) { + this->custom_preset_ = preset; + this->preset_.reset(); + return *this; + } } + ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset); return *this; } +ClimateCall &ClimateCall::set_preset(const std::string &preset) { return this->set_preset(preset.c_str()); } + ClimateCall &ClimateCall::set_preset(optional preset) { if (preset.has_value()) { this->set_preset(preset.value()); @@ -287,8 +299,22 @@ const optional &ClimateCall::get_mode() const { return this->mode_; const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional &ClimateCall::get_swing_mode() const { return this->swing_mode_; } const optional &ClimateCall::get_preset() const { return this->preset_; } -const optional &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } -const optional &ClimateCall::get_custom_preset() const { return this->custom_preset_; } +const char *ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } +const char *ClimateCall::get_custom_preset() const { return this->custom_preset_; } + +optional ClimateCall::get_custom_fan_mode_optional() const { + if (this->custom_fan_mode_ != nullptr) { + return std::string(this->custom_fan_mode_); + } + return {}; +} + +optional ClimateCall::get_custom_preset_optional() const { + if (this->custom_preset_ != nullptr) { + return std::string(this->custom_preset_); + } + return {}; +} ClimateCall &ClimateCall::set_target_temperature_high(optional target_temperature_high) { this->target_temperature_high_ = target_temperature_high; @@ -317,13 +343,13 @@ ClimateCall &ClimateCall::set_mode(optional mode) { ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { this->fan_mode_ = fan_mode; - this->custom_fan_mode_.reset(); + this->custom_fan_mode_ = nullptr; return *this; } ClimateCall &ClimateCall::set_preset(optional preset) { this->preset_ = preset; - this->custom_preset_.reset(); + this->custom_preset_ = nullptr; return *this; } @@ -382,13 +408,13 @@ void Climate::save_state_() { state.uses_custom_fan_mode = false; state.fan_mode = this->fan_mode.value(); } - if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { + if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode != nullptr) { state.uses_custom_fan_mode = true; const auto &supported = traits.get_supported_custom_fan_modes(); // std::vector maintains insertion order size_t i = 0; for (const char *mode : supported) { - if (strcmp(mode, custom_fan_mode.value().c_str()) == 0) { + if (strcmp(mode, custom_fan_mode) == 0) { state.custom_fan_mode = i; break; } @@ -399,13 +425,13 @@ void Climate::save_state_() { state.uses_custom_preset = false; state.preset = this->preset.value(); } - if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { + if (!traits.get_supported_custom_presets().empty() && custom_preset != nullptr) { state.uses_custom_preset = true; const auto &supported = traits.get_supported_custom_presets(); // std::vector maintains insertion order size_t i = 0; for (const char *preset : supported) { - if (strcmp(preset, custom_preset.value().c_str()) == 0) { + if (strcmp(preset, custom_preset) == 0) { state.custom_preset = i; break; } @@ -430,14 +456,14 @@ void Climate::publish_state() { if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); } - if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode.has_value()) { - ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode.value().c_str()); + if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode != nullptr) { + ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode); } if (traits.get_supports_presets() && this->preset.has_value()) { ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); } - if (!traits.get_supported_custom_presets().empty() && this->custom_preset.has_value()) { - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset.value().c_str()); + if (!traits.get_supported_custom_presets().empty() && this->custom_preset != nullptr) { + ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset); } if (traits.get_supports_swing_modes()) { ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); @@ -527,7 +553,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { if (this->uses_custom_fan_mode) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { call.fan_mode_.reset(); - call.custom_fan_mode_ = std::string(traits.get_supported_custom_fan_modes()[this->custom_fan_mode]); + call.custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; } } else if (traits.supports_fan_mode(this->fan_mode)) { call.set_fan_mode(this->fan_mode); @@ -535,7 +561,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { if (this->uses_custom_preset) { if (this->custom_preset < traits.get_supported_custom_presets().size()) { call.preset_.reset(); - call.custom_preset_ = std::string(traits.get_supported_custom_presets()[this->custom_preset]); + call.custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset]; } } else if (traits.supports_preset(this->preset)) { call.set_preset(this->preset); @@ -562,20 +588,20 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { if (this->uses_custom_fan_mode) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { climate->fan_mode.reset(); - climate->custom_fan_mode = std::string(traits.get_supported_custom_fan_modes()[this->custom_fan_mode]); + climate->custom_fan_mode = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; } } else if (traits.supports_fan_mode(this->fan_mode)) { climate->fan_mode = this->fan_mode; - climate->custom_fan_mode.reset(); + climate->custom_fan_mode = nullptr; } if (this->uses_custom_preset) { if (this->custom_preset < traits.get_supported_custom_presets().size()) { climate->preset.reset(); - climate->custom_preset = std::string(traits.get_supported_custom_presets()[this->custom_preset]); + climate->custom_preset = traits.get_supported_custom_presets()[this->custom_preset]; } } else if (traits.supports_preset(this->preset)) { climate->preset = this->preset; - climate->custom_preset.reset(); + climate->custom_preset = nullptr; } if (traits.supports_swing_mode(this->swing_mode)) { climate->swing_mode = this->swing_mode; diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 0c3e3ebe16..49ea2a47a8 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -74,9 +74,13 @@ class ClimateCall { /// Set the fan mode of the climate device. ClimateCall &set_fan_mode(optional fan_mode); /// Set the fan mode of the climate device based on a string. - ClimateCall &set_fan_mode(const std::string &fan_mode); + __attribute__((deprecated("Use set_fan_mode(const char*) instead"))) ClimateCall &set_fan_mode( + const std::string &fan_mode); /// Set the fan mode of the climate device based on a string. - ClimateCall &set_fan_mode(optional fan_mode); + __attribute__((deprecated("Use set_fan_mode(const char*) instead"))) ClimateCall &set_fan_mode( + optional fan_mode); + /// Set the custom fan mode of the climate device. + ClimateCall &set_fan_mode(const char *custom_fan_mode); /// Set the swing mode of the climate device. ClimateCall &set_swing_mode(ClimateSwingMode swing_mode); /// Set the swing mode of the climate device. @@ -88,9 +92,12 @@ class ClimateCall { /// Set the preset of the climate device. ClimateCall &set_preset(optional preset); /// Set the preset of the climate device based on a string. - ClimateCall &set_preset(const std::string &preset); + __attribute__((deprecated("Use set_preset(const char*) instead"))) ClimateCall &set_preset(const std::string &preset); /// Set the preset of the climate device based on a string. - ClimateCall &set_preset(optional preset); + __attribute__((deprecated("Use set_preset(const char*) instead"))) ClimateCall &set_preset( + optional preset); + /// Set the custom preset of the climate device. + ClimateCall &set_preset(const char *custom_preset); void perform(); @@ -103,8 +110,12 @@ class ClimateCall { const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_preset() const; - const optional &get_custom_fan_mode() const; - const optional &get_custom_preset() const; + const char *get_custom_fan_mode() const; + const char *get_custom_preset() const; + /// @deprecated Use get_custom_fan_mode() (returns const char*) instead (since 2025.11.0) + optional get_custom_fan_mode_optional() const; + /// @deprecated Use get_custom_preset() (returns const char*) instead (since 2025.11.0) + optional get_custom_preset_optional() const; protected: void validate_(); @@ -118,8 +129,8 @@ class ClimateCall { optional fan_mode_; optional swing_mode_; optional preset_; - optional custom_fan_mode_; - optional custom_preset_; + const char *custom_fan_mode_{nullptr}; + const char *custom_preset_{nullptr}; }; /// Struct used to save the state of the climate device in restore memory. @@ -239,10 +250,10 @@ class Climate : public EntityBase { optional preset; /// The active custom fan mode of the climate device. - optional custom_fan_mode; + const char *custom_fan_mode{nullptr}; /// The active custom preset mode of the climate device. - optional custom_preset; + const char *custom_preset{nullptr}; /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index f0e0dbe02b..7405918fea 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -135,13 +135,16 @@ class ClimateTraits { this->supported_custom_fan_modes_.assign(modes, modes + N); } const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } - bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { + bool supports_custom_fan_mode(const char *custom_fan_mode) const { for (const char *mode : this->supported_custom_fan_modes_) { - if (strcmp(mode, custom_fan_mode.c_str()) == 0) + if (strcmp(mode, custom_fan_mode) == 0) return true; } return false; } + bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { + return this->supports_custom_fan_mode(custom_fan_mode.c_str()); + } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } @@ -159,13 +162,16 @@ class ClimateTraits { this->supported_custom_presets_.assign(presets, presets + N); } const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } - bool supports_custom_preset(const std::string &custom_preset) const { + bool supports_custom_preset(const char *custom_preset) const { for (const char *preset : this->supported_custom_presets_) { - if (strcmp(preset, custom_preset.c_str()) == 0) + if (strcmp(preset, custom_preset) == 0) return true; } return false; } + bool supports_custom_preset(const std::string &custom_preset) const { + return this->supports_custom_preset(custom_preset.c_str()); + } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } From c3c1ae8e7f75d721fdcd387090f18b2d655918d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 18:44:28 -0500 Subject: [PATCH 02/40] simplify --- esphome/components/climate/climate.cpp | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 275db1d423..f0c466203f 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -619,18 +619,40 @@ template bool set_alternative(optional &dst, optio return is_changed; } +// Overload for optional + const char* pointer +template bool set_alternative(optional &dst, const char *&alt, const T &src) { + bool is_changed = (alt != nullptr); + alt = nullptr; + if (is_changed || dst != src) { + dst = src; + is_changed = true; + } + return is_changed; +} + +// Overload for const char* pointer + optional +template bool set_alternative(const char *&dst, optional &alt, const char *src) { + bool is_changed = alt.has_value(); + alt.reset(); + if (is_changed || dst != src) { + dst = src; + is_changed = true; + } + return is_changed; +} + bool Climate::set_fan_mode_(ClimateFanMode mode) { return set_alternative(this->fan_mode, this->custom_fan_mode, mode); } bool Climate::set_custom_fan_mode_(const std::string &mode) { - return set_alternative(this->custom_fan_mode, this->fan_mode, mode); + return set_alternative(this->custom_fan_mode, this->fan_mode, mode.c_str()); } bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } bool Climate::set_custom_preset_(const std::string &preset) { - return set_alternative(this->custom_preset, this->preset, preset); + return set_alternative(this->custom_preset, this->preset, preset.c_str()); } void Climate::dump_traits_(const char *tag) { From 9161d3a758c5e673f9dedb3b1a016c932e2ac1f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 18:48:05 -0500 Subject: [PATCH 03/40] simplify --- esphome/components/climate/climate.cpp | 35 ++++++++++---------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index f0c466203f..756051d6ce 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -609,35 +609,26 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->publish_state(); } -template bool set_alternative(optional &dst, optional &alt, const T1 &src) { - bool is_changed = alt.has_value(); - alt.reset(); - if (is_changed || dst != src) { - dst = src; - is_changed = true; - } - return is_changed; -} +// Generic template to set one value while clearing its alternative (mutual exclusion) +// Handles both optional and const char* types automatically using compile-time type detection +template bool set_alternative(T1 &dst, T2 &alt, T3 src) { + bool is_changed = false; -// Overload for optional + const char* pointer -template bool set_alternative(optional &dst, const char *&alt, const T &src) { - bool is_changed = (alt != nullptr); - alt = nullptr; - if (is_changed || dst != src) { - dst = src; - is_changed = true; + // Clear the alternative based on its type (pointer or optional) + if constexpr (std::is_pointer_v>) { + is_changed = (alt != nullptr); + alt = nullptr; + } else { + is_changed = alt.has_value(); + alt.reset(); } - return is_changed; -} -// Overload for const char* pointer + optional -template bool set_alternative(const char *&dst, optional &alt, const char *src) { - bool is_changed = alt.has_value(); - alt.reset(); + // Set the destination value if (is_changed || dst != src) { dst = src; is_changed = true; } + return is_changed; } From 42e6b4326fc2052ff5f8a2c52f549a16c97dfc4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 18:51:19 -0500 Subject: [PATCH 04/40] simplify --- esphome/components/climate/climate_traits.h | 22 +++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 7405918fea..1d4d8b6097 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -9,6 +9,16 @@ namespace esphome { namespace climate { +// Lightweight linear search for small vectors (1-20 items) of const char* pointers +// Avoids std::find template overhead +inline bool vector_contains(const std::vector &vec, const char *value) { + for (const char *item : vec) { + if (strcmp(item, value) == 0) + return true; + } + return false; +} + // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead // For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position) @@ -136,11 +146,7 @@ class ClimateTraits { } const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } bool supports_custom_fan_mode(const char *custom_fan_mode) const { - for (const char *mode : this->supported_custom_fan_modes_) { - if (strcmp(mode, custom_fan_mode) == 0) - return true; - } - return false; + return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); } bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { return this->supports_custom_fan_mode(custom_fan_mode.c_str()); @@ -163,11 +169,7 @@ class ClimateTraits { } const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } bool supports_custom_preset(const char *custom_preset) const { - for (const char *preset : this->supported_custom_presets_) { - if (strcmp(preset, custom_preset) == 0) - return true; - } - return false; + return vector_contains(this->supported_custom_presets_, custom_preset); } bool supports_custom_preset(const std::string &custom_preset) const { return this->supports_custom_preset(custom_preset.c_str()); From 4d39e15920d9a85a6a7d8f23e80ab1922336e36b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 18:53:13 -0500 Subject: [PATCH 05/40] simplify --- esphome/components/climate/climate.cpp | 13 ++++++------- esphome/components/climate/climate_traits.h | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 756051d6ce..dc83189692 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -197,13 +197,12 @@ ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) { } } // Find the matching pointer from traits - const auto &supported = this->parent_->get_traits().get_supported_custom_fan_modes(); - for (const char *mode : supported) { - if (strcmp(mode, custom_fan_mode) == 0) { - this->custom_fan_mode_ = mode; - this->fan_mode_.reset(); - return *this; - } + auto traits = this->parent_->get_traits(); + const char *mode_ptr = traits.find_custom_fan_mode(custom_fan_mode); + if (mode_ptr != nullptr) { + this->custom_fan_mode_ = mode_ptr; + this->fan_mode_.reset(); + return *this; } ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode); return *this; diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 1d4d8b6097..e5171867d5 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -151,6 +151,14 @@ class ClimateTraits { bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { return this->supports_custom_fan_mode(custom_fan_mode.c_str()); } + /// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found + const char *find_custom_fan_mode(const char *custom_fan_mode) const { + for (const char *mode : this->supported_custom_fan_modes_) { + if (strcmp(mode, custom_fan_mode) == 0) + return mode; + } + return nullptr; + } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } @@ -174,6 +182,14 @@ class ClimateTraits { bool supports_custom_preset(const std::string &custom_preset) const { return this->supports_custom_preset(custom_preset.c_str()); } + /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found + const char *find_custom_preset(const char *custom_preset) const { + for (const char *preset : this->supported_custom_presets_) { + if (strcmp(preset, custom_preset) == 0) + return preset; + } + return nullptr; + } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } From 6b2a85541d7e22a56e0f74953c212b4e5c354db9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 18:55:06 -0500 Subject: [PATCH 06/40] simplify --- esphome/components/api/api_connection.cpp | 4 ++-- esphome/components/climate/climate.cpp | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 382c4acc16..8730828994 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -699,11 +699,11 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { if (msg.has_fan_mode) call.set_fan_mode(static_cast(msg.fan_mode)); if (msg.has_custom_fan_mode) - call.set_fan_mode(msg.custom_fan_mode); + call.set_fan_mode(msg.custom_fan_mode.c_str()); if (msg.has_preset) call.set_preset(static_cast(msg.preset)); if (msg.has_custom_preset) - call.set_preset(msg.custom_preset); + call.set_preset(msg.custom_preset.c_str()); if (msg.has_swing_mode) call.set_swing_mode(static_cast(msg.swing_mode)); call.perform(); diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index dc83189692..ff97265d9e 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -232,13 +232,12 @@ ClimateCall &ClimateCall::set_preset(const char *custom_preset) { } } // Find the matching pointer from traits - const auto &supported = this->parent_->get_traits().get_supported_custom_presets(); - for (const char *preset : supported) { - if (strcmp(preset, custom_preset) == 0) { - this->custom_preset_ = preset; - this->preset_.reset(); - return *this; - } + auto traits = this->parent_->get_traits(); + const char *preset_ptr = traits.find_custom_preset(custom_preset); + if (preset_ptr != nullptr) { + this->custom_preset_ = preset_ptr; + this->preset_.reset(); + return *this; } ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset); return *this; From 39beaae20f411f03a6fa006c00c05cbdf5de35f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 18:56:42 -0500 Subject: [PATCH 07/40] simplify --- esphome/components/climate/climate.cpp | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index ff97265d9e..20df78ea1f 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -635,13 +635,33 @@ bool Climate::set_fan_mode_(ClimateFanMode mode) { } bool Climate::set_custom_fan_mode_(const std::string &mode) { - return set_alternative(this->custom_fan_mode, this->fan_mode, mode.c_str()); + auto traits = this->get_traits(); + const char *mode_ptr = traits.find_custom_fan_mode(mode.c_str()); + if (mode_ptr != nullptr) { + return set_alternative(this->custom_fan_mode, this->fan_mode, mode_ptr); + } + // Mode not found in supported custom modes, clear it + if (this->custom_fan_mode != nullptr) { + this->custom_fan_mode = nullptr; + return true; + } + return false; } bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } bool Climate::set_custom_preset_(const std::string &preset) { - return set_alternative(this->custom_preset, this->preset, preset.c_str()); + auto traits = this->get_traits(); + const char *preset_ptr = traits.find_custom_preset(preset.c_str()); + if (preset_ptr != nullptr) { + return set_alternative(this->custom_preset, this->preset, preset_ptr); + } + // Preset not found in supported custom presets, clear it + if (this->custom_preset != nullptr) { + this->custom_preset = nullptr; + return true; + } + return false; } void Climate::dump_traits_(const char *tag) { From b9d0e4061b0e2ce405a739a17805c213e0f9ac1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 18:58:52 -0500 Subject: [PATCH 08/40] simplify --- esphome/components/climate/climate.h | 8 ++++---- esphome/components/climate/climate_traits.h | 21 +++++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 49ea2a47a8..0600cf234c 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -110,12 +110,12 @@ class ClimateCall { const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_preset() const; + /// @deprecated Use get_custom_fan_mode() (returns const char*) instead (since 2025.11.0) + optional get_custom_fan_mode() const; + /// @deprecated Use get_custom_preset() (returns const char*) instead (since 2025.11.0) + optional get_custom_preset() const; const char *get_custom_fan_mode() const; const char *get_custom_preset() const; - /// @deprecated Use get_custom_fan_mode() (returns const char*) instead (since 2025.11.0) - optional get_custom_fan_mode_optional() const; - /// @deprecated Use get_custom_preset() (returns const char*) instead (since 2025.11.0) - optional get_custom_preset_optional() const; protected: void validate_(); diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index e5171867d5..1fba56888f 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -19,6 +19,15 @@ inline bool vector_contains(const std::vector &vec, const char *va return false; } +// Find and return matching pointer from vector, or nullptr if not found +inline const char *vector_find(const std::vector &vec, const char *value) { + for (const char *item : vec) { + if (strcmp(item, value) == 0) + return item; + } + return nullptr; +} + // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead // For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position) @@ -153,11 +162,7 @@ class ClimateTraits { } /// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found const char *find_custom_fan_mode(const char *custom_fan_mode) const { - for (const char *mode : this->supported_custom_fan_modes_) { - if (strcmp(mode, custom_fan_mode) == 0) - return mode; - } - return nullptr; + return vector_find(this->supported_custom_fan_modes_, custom_fan_mode); } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } @@ -184,11 +189,7 @@ class ClimateTraits { } /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found const char *find_custom_preset(const char *custom_preset) const { - for (const char *preset : this->supported_custom_presets_) { - if (strcmp(preset, custom_preset) == 0) - return preset; - } - return nullptr; + return vector_find(this->supported_custom_presets_, custom_preset); } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } From f66f9c4eafe4360e17bc8d847d56b03446bd7b99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:00:02 -0500 Subject: [PATCH 09/40] simplify --- esphome/components/climate/climate.cpp | 4 ++-- esphome/components/climate/climate.h | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 20df78ea1f..a9d42523d8 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -300,14 +300,14 @@ const optional &ClimateCall::get_preset() const { return this->pr const char *ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } const char *ClimateCall::get_custom_preset() const { return this->custom_preset_; } -optional ClimateCall::get_custom_fan_mode_optional() const { +optional ClimateCall::get_custom_fan_mode() const { if (this->custom_fan_mode_ != nullptr) { return std::string(this->custom_fan_mode_); } return {}; } -optional ClimateCall::get_custom_preset_optional() const { +optional ClimateCall::get_custom_preset() const { if (this->custom_preset_ != nullptr) { return std::string(this->custom_preset_); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 0600cf234c..49ea2a47a8 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -110,12 +110,12 @@ class ClimateCall { const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_preset() const; - /// @deprecated Use get_custom_fan_mode() (returns const char*) instead (since 2025.11.0) - optional get_custom_fan_mode() const; - /// @deprecated Use get_custom_preset() (returns const char*) instead (since 2025.11.0) - optional get_custom_preset() const; const char *get_custom_fan_mode() const; const char *get_custom_preset() const; + /// @deprecated Use get_custom_fan_mode() (returns const char*) instead (since 2025.11.0) + optional get_custom_fan_mode_optional() const; + /// @deprecated Use get_custom_preset() (returns const char*) instead (since 2025.11.0) + optional get_custom_preset_optional() const; protected: void validate_(); From 952f6f5029a79780b7679e75d94c8f560e9ac496 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:01:48 -0500 Subject: [PATCH 10/40] simplify --- esphome/components/climate/climate.cpp | 2 -- esphome/components/climate/climate.h | 8 ++------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index a9d42523d8..dc5b411eaf 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -297,8 +297,6 @@ const optional &ClimateCall::get_mode() const { return this->mode_; const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional &ClimateCall::get_swing_mode() const { return this->swing_mode_; } const optional &ClimateCall::get_preset() const { return this->preset_; } -const char *ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } -const char *ClimateCall::get_custom_preset() const { return this->custom_preset_; } optional ClimateCall::get_custom_fan_mode() const { if (this->custom_fan_mode_ != nullptr) { diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 49ea2a47a8..7f1ac0a4aa 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -110,12 +110,8 @@ class ClimateCall { const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_preset() const; - const char *get_custom_fan_mode() const; - const char *get_custom_preset() const; - /// @deprecated Use get_custom_fan_mode() (returns const char*) instead (since 2025.11.0) - optional get_custom_fan_mode_optional() const; - /// @deprecated Use get_custom_preset() (returns const char*) instead (since 2025.11.0) - optional get_custom_preset_optional() const; + optional get_custom_fan_mode() const; + optional get_custom_preset() const; protected: void validate_(); From 41bd8951dc8c05d3f2e9e3bc3f8ac009ed5bdd08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:02:45 -0500 Subject: [PATCH 11/40] simplify --- esphome/components/api/api_connection.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8730828994..5a33a82842 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -637,14 +637,14 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection } if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) resp.fan_mode = static_cast(climate->fan_mode.value()); - if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) { - resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value())); + if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode != nullptr) { + resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode)); } if (traits.get_supports_presets() && climate->preset.has_value()) { resp.preset = static_cast(climate->preset.value()); } - if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) { - resp.set_custom_preset(StringRef(climate->custom_preset.value())); + if (!traits.get_supported_custom_presets().empty() && climate->custom_preset != nullptr) { + resp.set_custom_preset(StringRef(climate->custom_preset)); } if (traits.get_supports_swing_modes()) resp.swing_mode = static_cast(climate->swing_mode); From 4565dcc4d9bc962c71c900aff3949059c0872422 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:03:01 -0500 Subject: [PATCH 12/40] simplify --- esphome/components/climate/climate.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 7f1ac0a4aa..5928df822e 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -74,11 +74,9 @@ class ClimateCall { /// Set the fan mode of the climate device. ClimateCall &set_fan_mode(optional fan_mode); /// Set the fan mode of the climate device based on a string. - __attribute__((deprecated("Use set_fan_mode(const char*) instead"))) ClimateCall &set_fan_mode( - const std::string &fan_mode); + ClimateCall &set_fan_mode(const std::string &fan_mode); /// Set the fan mode of the climate device based on a string. - __attribute__((deprecated("Use set_fan_mode(const char*) instead"))) ClimateCall &set_fan_mode( - optional fan_mode); + ClimateCall &set_fan_mode(optional fan_mode); /// Set the custom fan mode of the climate device. ClimateCall &set_fan_mode(const char *custom_fan_mode); /// Set the swing mode of the climate device. From 46e4fe28969eb674c60b853cbd27dd637c6b749b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:03:12 -0500 Subject: [PATCH 13/40] simplify --- esphome/components/climate/climate.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 5928df822e..e5d098291c 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -90,10 +90,9 @@ class ClimateCall { /// Set the preset of the climate device. ClimateCall &set_preset(optional preset); /// Set the preset of the climate device based on a string. - __attribute__((deprecated("Use set_preset(const char*) instead"))) ClimateCall &set_preset(const std::string &preset); + ClimateCall &set_preset(const std::string &preset); /// Set the preset of the climate device based on a string. - __attribute__((deprecated("Use set_preset(const char*) instead"))) ClimateCall &set_preset( - optional preset); + ClimateCall &set_preset(optional preset); /// Set the custom preset of the climate device. ClimateCall &set_preset(const char *custom_preset); From 8c90ea860cd0316cf51f69b6c50a7810fc8143b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:04:52 -0500 Subject: [PATCH 14/40] simplify --- esphome/components/climate/climate.cpp | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index dc5b411eaf..80027ee377 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -299,17 +299,11 @@ const optional &ClimateCall::get_swing_mode() const { return t const optional &ClimateCall::get_preset() const { return this->preset_; } optional ClimateCall::get_custom_fan_mode() const { - if (this->custom_fan_mode_ != nullptr) { - return std::string(this->custom_fan_mode_); - } - return {}; + return this->custom_fan_mode_ != nullptr ? std::string(this->custom_fan_mode_) : optional{}; } optional ClimateCall::get_custom_preset() const { - if (this->custom_preset_ != nullptr) { - return std::string(this->custom_preset_); - } - return {}; + return this->custom_preset_ != nullptr ? std::string(this->custom_preset_) : optional{}; } ClimateCall &ClimateCall::set_target_temperature_high(optional target_temperature_high) { From 1864cf6ad830d55377c3cbf652a0b1cb579b6319 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:08:40 -0500 Subject: [PATCH 15/40] simplify --- esphome/components/climate/climate.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 80027ee377..196269a736 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -192,14 +192,12 @@ ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) { // Check if it's a standard enum mode first for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) { if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) { - this->set_fan_mode(static_cast(mode_entry.value)); - return *this; + return this->set_fan_mode(static_cast(mode_entry.value)); } } // Find the matching pointer from traits auto traits = this->parent_->get_traits(); - const char *mode_ptr = traits.find_custom_fan_mode(custom_fan_mode); - if (mode_ptr != nullptr) { + if (const char *mode_ptr = traits.find_custom_fan_mode(custom_fan_mode)) { this->custom_fan_mode_ = mode_ptr; this->fan_mode_.reset(); return *this; @@ -227,14 +225,12 @@ ClimateCall &ClimateCall::set_preset(const char *custom_preset) { // Check if it's a standard enum preset first for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) { if (str_equals_case_insensitive(custom_preset, preset_entry.str)) { - this->set_preset(static_cast(preset_entry.value)); - return *this; + return this->set_preset(static_cast(preset_entry.value)); } } // Find the matching pointer from traits auto traits = this->parent_->get_traits(); - const char *preset_ptr = traits.find_custom_preset(custom_preset); - if (preset_ptr != nullptr) { + if (const char *preset_ptr = traits.find_custom_preset(custom_preset)) { this->custom_preset_ = preset_ptr; this->preset_.reset(); return *this; From af165539e667af0b5b0d988f5b2097bec848895f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:37:02 -0500 Subject: [PATCH 16/40] simplify --- esphome/components/climate/climate_traits.h | 26 +++++++++++++------ .../thermostat/thermostat_climate.cpp | 16 +++++++----- esphome/components/web_server/web_server.cpp | 10 +++---- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 1fba56888f..869224b117 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -65,7 +65,13 @@ using ClimatePresetMask = FiniteSetMaskfeature_flags_; } @@ -160,10 +166,6 @@ class ClimateTraits { bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { return this->supports_custom_fan_mode(custom_fan_mode.c_str()); } - /// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found - const char *find_custom_fan_mode(const char *custom_fan_mode) const { - return vector_find(this->supported_custom_fan_modes_, custom_fan_mode); - } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } @@ -187,10 +189,6 @@ class ClimateTraits { bool supports_custom_preset(const std::string &custom_preset) const { return this->supports_custom_preset(custom_preset.c_str()); } - /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found - const char *find_custom_preset(const char *custom_preset) const { - return vector_find(this->supported_custom_presets_, custom_preset); - } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } @@ -249,6 +247,18 @@ class ClimateTraits { } } + /// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found + /// This is protected as it's an implementation detail - use Climate::set_custom_fan_mode_() instead + const char *find_custom_fan_mode(const char *custom_fan_mode) const { + return vector_find(this->supported_custom_fan_modes_, custom_fan_mode); + } + + /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found + /// This is protected as it's an implementation detail - use Climate::set_custom_preset_() instead + const char *find_custom_preset(const char *custom_preset) const { + return vector_find(this->supported_custom_presets_, custom_preset); + } + uint32_t feature_flags_{0}; float visual_min_temperature_{10}; float visual_max_temperature_{30}; diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 6842bd4be8..b5fce2f6fd 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -223,7 +223,8 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { if (this->setup_complete_) { this->change_custom_preset_(call.get_custom_preset().value()); } else { - this->custom_preset = call.get_custom_preset().value(); + // Use the base class method which handles pointer lookup internally + this->set_custom_preset_(call.get_custom_preset().value()); } } @@ -1171,7 +1172,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { } else { ESP_LOGI(TAG, "No changes required to apply preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); } - this->custom_preset.reset(); + this->custom_preset = nullptr; this->preset = preset; } else { ESP_LOGW(TAG, "Preset %s not configured; ignoring", LOG_STR_ARG(climate::climate_preset_to_string(preset))); @@ -1183,11 +1184,12 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) if (config != this->custom_preset_config_.end()) { ESP_LOGV(TAG, "Custom preset %s requested", custom_preset.c_str()); - if (this->change_preset_internal_(config->second) || (!this->custom_preset.has_value()) || - this->custom_preset.value() != custom_preset) { + if (this->change_preset_internal_(config->second) || (this->custom_preset == nullptr) || + strcmp(this->custom_preset, custom_preset.c_str()) != 0) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; - this->custom_preset = custom_preset; + // Use the base class method which handles pointer lookup and preset reset internally + this->set_custom_preset_(custom_preset); if (trig != nullptr) { trig->trigger(); } @@ -1196,9 +1198,9 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) ESP_LOGI(TAG, "Custom preset %s applied", custom_preset.c_str()); } else { ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset.c_str()); + // Still need to ensure preset is reset and custom_preset is set + this->set_custom_preset_(custom_preset); } - this->preset.reset(); - this->custom_preset = custom_preset; } else { ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset.c_str()); } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1d08ef5a35..ee626b8b9b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1312,7 +1312,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf for (climate::ClimatePreset m : traits.get_supported_presets()) opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); } - if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { + if (!traits.get_supported_custom_presets().empty() && obj->custom_preset != nullptr) { JsonArray opt = root["custom_presets"].to(); for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); @@ -1333,14 +1333,14 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf if (traits.get_supports_fan_modes() && obj->fan_mode.has_value()) { root["fan_mode"] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); } - if (!traits.get_supported_custom_fan_modes().empty() && obj->custom_fan_mode.has_value()) { - root["custom_fan_mode"] = obj->custom_fan_mode.value().c_str(); + if (!traits.get_supported_custom_fan_modes().empty() && obj->custom_fan_mode != nullptr) { + root["custom_fan_mode"] = obj->custom_fan_mode; } if (traits.get_supports_presets() && obj->preset.has_value()) { root["preset"] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); } - if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { - root["custom_preset"] = obj->custom_preset.value().c_str(); + if (!traits.get_supported_custom_presets().empty() && obj->custom_preset != nullptr) { + root["custom_preset"] = obj->custom_preset; } if (traits.get_supports_swing_modes()) { root["swing_mode"] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); From 56c6cc8c9f8cc7b4631764b4b8c60d1ca96b1c4e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:43:07 -0500 Subject: [PATCH 17/40] simplify --- esphome/components/climate/climate.cpp | 54 ++++++++++----------- esphome/components/climate/climate.h | 10 ++++ esphome/components/climate/climate_traits.h | 14 +++--- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 196269a736..9b896a3a4b 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -195,9 +195,8 @@ ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) { return this->set_fan_mode(static_cast(mode_entry.value)); } } - // Find the matching pointer from traits - auto traits = this->parent_->get_traits(); - if (const char *mode_ptr = traits.find_custom_fan_mode(custom_fan_mode)) { + // Find the matching pointer from parent climate device + if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) { this->custom_fan_mode_ = mode_ptr; this->fan_mode_.reset(); return *this; @@ -228,9 +227,8 @@ ClimateCall &ClimateCall::set_preset(const char *custom_preset) { return this->set_preset(static_cast(preset_entry.value)); } } - // Find the matching pointer from traits - auto traits = this->parent_->get_traits(); - if (const char *preset_ptr = traits.find_custom_preset(custom_preset)) { + // Find the matching pointer from parent climate device + if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) { this->custom_preset_ = preset_ptr; this->preset_.reset(); return *this; @@ -622,34 +620,34 @@ bool Climate::set_fan_mode_(ClimateFanMode mode) { return set_alternative(this->fan_mode, this->custom_fan_mode, mode); } -bool Climate::set_custom_fan_mode_(const std::string &mode) { +bool Climate::set_custom_fan_mode_(const char *mode) { auto traits = this->get_traits(); - const char *mode_ptr = traits.find_custom_fan_mode(mode.c_str()); - if (mode_ptr != nullptr) { - return set_alternative(this->custom_fan_mode, this->fan_mode, mode_ptr); - } - // Mode not found in supported custom modes, clear it - if (this->custom_fan_mode != nullptr) { - this->custom_fan_mode = nullptr; - return true; - } - return false; + const char *mode_ptr = traits.find_custom_fan_mode_(mode); + return mode_ptr != nullptr ? set_alternative(this->custom_fan_mode, this->fan_mode, mode_ptr) + : (this->custom_fan_mode != nullptr ? (this->custom_fan_mode = nullptr, true) : false); } +bool Climate::set_custom_fan_mode_(const std::string &mode) { return this->set_custom_fan_mode_(mode.c_str()); } + bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } -bool Climate::set_custom_preset_(const std::string &preset) { +bool Climate::set_custom_preset_(const char *preset) { auto traits = this->get_traits(); - const char *preset_ptr = traits.find_custom_preset(preset.c_str()); - if (preset_ptr != nullptr) { - return set_alternative(this->custom_preset, this->preset, preset_ptr); - } - // Preset not found in supported custom presets, clear it - if (this->custom_preset != nullptr) { - this->custom_preset = nullptr; - return true; - } - return false; + const char *preset_ptr = traits.find_custom_preset_(preset); + return preset_ptr != nullptr ? set_alternative(this->custom_preset, this->preset, preset_ptr) + : (this->custom_preset != nullptr ? (this->custom_preset = nullptr, true) : false); +} + +bool Climate::set_custom_preset_(const std::string &preset) { return this->set_custom_preset_(preset.c_str()); } + +const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) { + auto traits = this->get_traits(); + return traits.find_custom_fan_mode_(custom_fan_mode); +} + +const char *Climate::find_custom_preset_(const char *custom_preset) { + auto traits = this->get_traits(); + return traits.find_custom_preset_(custom_preset); } void Climate::dump_traits_(const char *tag) { diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index e5d098291c..ea94128f5c 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -263,15 +263,25 @@ class Climate : public EntityBase { /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed. bool set_fan_mode_(ClimateFanMode mode); + /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. + bool set_custom_fan_mode_(const char *mode); /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. bool set_custom_fan_mode_(const std::string &mode); /// Set preset. Reset custom preset. Return true if preset has been changed. bool set_preset_(ClimatePreset preset); + /// Set custom preset. Reset primary preset. Return true if preset has been changed. + bool set_custom_preset_(const char *preset); /// Set custom preset. Reset primary preset. Return true if preset has been changed. bool set_custom_preset_(const std::string &preset); + /// Find and return the matching custom fan mode pointer from traits, or nullptr if not found. + const char *find_custom_fan_mode_(const char *custom_fan_mode); + + /// Find and return the matching custom preset pointer from traits, or nullptr if not found. + const char *find_custom_preset_(const char *custom_preset); + /** Get the default traits of this climate device. * * Traits are static data that encode the capabilities and static data for a climate device such as supported diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 869224b117..65103cdaad 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -65,12 +65,10 @@ using ClimatePresetMask = FiniteSetMasksupported_custom_fan_modes_, custom_fan_mode); } /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found - /// This is protected as it's an implementation detail - use Climate::set_custom_preset_() instead - const char *find_custom_preset(const char *custom_preset) const { + /// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead + const char *find_custom_preset_(const char *custom_preset) const { return vector_find(this->supported_custom_presets_, custom_preset); } From dda7b52f944a9cbcca865715fbe300dbdca8f37d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:44:30 -0500 Subject: [PATCH 18/40] simplify --- esphome/components/climate/climate_traits.h | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 65103cdaad..cbd9d1dbf4 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -9,6 +9,16 @@ namespace esphome { namespace climate { +// Type aliases for climate enum bitmasks +// These replace std::set to eliminate red-black tree overhead +// For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position) +// Bitmask size is automatically calculated from the last enum value +using ClimateModeMask = FiniteSetMask>; +using ClimateFanModeMask = FiniteSetMask>; +using ClimateSwingModeMask = + FiniteSetMask>; +using ClimatePresetMask = FiniteSetMask>; + // Lightweight linear search for small vectors (1-20 items) of const char* pointers // Avoids std::find template overhead inline bool vector_contains(const std::vector &vec, const char *value) { @@ -28,16 +38,6 @@ inline const char *vector_find(const std::vector &vec, const char return nullptr; } -// Type aliases for climate enum bitmasks -// These replace std::set to eliminate red-black tree overhead -// For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position) -// Bitmask size is automatically calculated from the last enum value -using ClimateModeMask = FiniteSetMask>; -using ClimateFanModeMask = FiniteSetMask>; -using ClimateSwingModeMask = - FiniteSetMask>; -using ClimatePresetMask = FiniteSetMask>; - /** This class contains all static data for climate devices. * * All climate devices must support these features: From 13148f2c893acf2dca62d2e5bd35b4d93e9fd064 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:47:45 -0500 Subject: [PATCH 19/40] simplify --- esphome/components/api/api_connection.cpp | 4 ++-- esphome/components/climate/climate.h | 6 ++++++ esphome/components/web_server/web_server.cpp | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5a33a82842..2914f15b4d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -637,13 +637,13 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection } if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) resp.fan_mode = static_cast(climate->fan_mode.value()); - if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode != nullptr) { + if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) { resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode)); } if (traits.get_supports_presets() && climate->preset.has_value()) { resp.preset = static_cast(climate->preset.value()); } - if (!traits.get_supported_custom_presets().empty() && climate->custom_preset != nullptr) { + if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) { resp.set_custom_preset(StringRef(climate->custom_preset)); } if (traits.get_supports_swing_modes()) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index ea94128f5c..5166e19319 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -216,6 +216,12 @@ class Climate : public EntityBase { void set_visual_min_humidity_override(float visual_min_humidity_override); void set_visual_max_humidity_override(float visual_max_humidity_override); + /// Check if a custom fan mode is currently active. + bool has_custom_fan_mode() const { return this->custom_fan_mode != nullptr; } + + /// Check if a custom preset is currently active. + bool has_custom_preset() const { return this->custom_preset != nullptr; } + /// The current temperature of the climate device, as reported from the integration. float current_temperature{NAN}; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index ee626b8b9b..7901869b2f 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1312,7 +1312,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf for (climate::ClimatePreset m : traits.get_supported_presets()) opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); } - if (!traits.get_supported_custom_presets().empty() && obj->custom_preset != nullptr) { + if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { JsonArray opt = root["custom_presets"].to(); for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); @@ -1333,13 +1333,13 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf if (traits.get_supports_fan_modes() && obj->fan_mode.has_value()) { root["fan_mode"] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); } - if (!traits.get_supported_custom_fan_modes().empty() && obj->custom_fan_mode != nullptr) { + if (!traits.get_supported_custom_fan_modes().empty() && obj->has_custom_fan_mode()) { root["custom_fan_mode"] = obj->custom_fan_mode; } if (traits.get_supports_presets() && obj->preset.has_value()) { root["preset"] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); } - if (!traits.get_supported_custom_presets().empty() && obj->custom_preset != nullptr) { + if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { root["custom_preset"] = obj->custom_preset; } if (traits.get_supports_swing_modes()) { From 219a318ee35773e81233396b05148ec9d67a37d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:50:11 -0500 Subject: [PATCH 20/40] simplify --- esphome/components/climate/climate.cpp | 8 ++++---- esphome/components/thermostat/thermostat_climate.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 9b896a3a4b..c48a94bb73 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -392,7 +392,7 @@ void Climate::save_state_() { state.uses_custom_fan_mode = false; state.fan_mode = this->fan_mode.value(); } - if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode != nullptr) { + if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { state.uses_custom_fan_mode = true; const auto &supported = traits.get_supported_custom_fan_modes(); // std::vector maintains insertion order @@ -409,7 +409,7 @@ void Climate::save_state_() { state.uses_custom_preset = false; state.preset = this->preset.value(); } - if (!traits.get_supported_custom_presets().empty() && custom_preset != nullptr) { + if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { state.uses_custom_preset = true; const auto &supported = traits.get_supported_custom_presets(); // std::vector maintains insertion order @@ -440,13 +440,13 @@ void Climate::publish_state() { if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); } - if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode != nullptr) { + if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode); } if (traits.get_supports_presets() && this->preset.has_value()) { ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); } - if (!traits.get_supported_custom_presets().empty() && this->custom_preset != nullptr) { + if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset); } if (traits.get_supports_swing_modes()) { diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index b5fce2f6fd..1a53a66f77 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -1184,7 +1184,7 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) if (config != this->custom_preset_config_.end()) { ESP_LOGV(TAG, "Custom preset %s requested", custom_preset.c_str()); - if (this->change_preset_internal_(config->second) || (this->custom_preset == nullptr) || + if (this->change_preset_internal_(config->second) || !this->has_custom_preset() || strcmp(this->custom_preset, custom_preset.c_str()) != 0) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; From 34d2056413a2aab5edefa8f58d255e4989d89f99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:51:54 -0500 Subject: [PATCH 21/40] simplify --- esphome/components/climate/climate.cpp | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index c48a94bb73..9f9a0ca5d6 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -623,8 +623,15 @@ bool Climate::set_fan_mode_(ClimateFanMode mode) { bool Climate::set_custom_fan_mode_(const char *mode) { auto traits = this->get_traits(); const char *mode_ptr = traits.find_custom_fan_mode_(mode); - return mode_ptr != nullptr ? set_alternative(this->custom_fan_mode, this->fan_mode, mode_ptr) - : (this->custom_fan_mode != nullptr ? (this->custom_fan_mode = nullptr, true) : false); + if (mode_ptr != nullptr) { + return set_alternative(this->custom_fan_mode, this->fan_mode, mode_ptr); + } + // Mode not found in supported custom modes, clear it if currently set + if (this->has_custom_fan_mode()) { + this->custom_fan_mode = nullptr; + return true; + } + return false; } bool Climate::set_custom_fan_mode_(const std::string &mode) { return this->set_custom_fan_mode_(mode.c_str()); } @@ -634,8 +641,15 @@ bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->p bool Climate::set_custom_preset_(const char *preset) { auto traits = this->get_traits(); const char *preset_ptr = traits.find_custom_preset_(preset); - return preset_ptr != nullptr ? set_alternative(this->custom_preset, this->preset, preset_ptr) - : (this->custom_preset != nullptr ? (this->custom_preset = nullptr, true) : false); + if (preset_ptr != nullptr) { + return set_alternative(this->custom_preset, this->preset, preset_ptr); + } + // Preset not found in supported custom presets, clear it if currently set + if (this->has_custom_preset()) { + this->custom_preset = nullptr; + return true; + } + return false; } bool Climate::set_custom_preset_(const std::string &preset) { return this->set_custom_preset_(preset.c_str()); } From 5013b7be87d26b16acf72b6a8dedaf9190484fe8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:55:46 -0500 Subject: [PATCH 22/40] simplify --- esphome/components/climate/climate.h | 22 ++++++++++++------- .../thermostat/thermostat_climate.cpp | 2 -- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 5166e19319..495d9f700f 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -217,10 +217,10 @@ class Climate : public EntityBase { void set_visual_max_humidity_override(float visual_max_humidity_override); /// Check if a custom fan mode is currently active. - bool has_custom_fan_mode() const { return this->custom_fan_mode != nullptr; } + bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; } /// Check if a custom preset is currently active. - bool has_custom_preset() const { return this->custom_preset != nullptr; } + bool has_custom_preset() const { return this->custom_preset_ != nullptr; } /// The current temperature of the climate device, as reported from the integration. float current_temperature{NAN}; @@ -248,12 +248,6 @@ class Climate : public EntityBase { /// The active preset of the climate device. optional preset; - /// The active custom fan mode of the climate device. - const char *custom_fan_mode{nullptr}; - - /// The active custom preset mode of the climate device. - const char *custom_preset{nullptr}; - /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; @@ -263,6 +257,12 @@ class Climate : public EntityBase { /// The active swing mode of the climate device. ClimateSwingMode swing_mode{CLIMATE_SWING_OFF}; + /// Get the active custom fan mode (read-only access). + const char *get_custom_fan_mode() const { return this->custom_fan_mode_; } + + /// Get the active custom preset (read-only access). + const char *get_custom_preset() const { return this->custom_preset_; } + protected: friend ClimateCall; @@ -323,6 +323,12 @@ class Climate : public EntityBase { optional visual_current_temperature_step_override_{}; optional visual_min_humidity_override_{}; optional visual_max_humidity_override_{}; + + /// The active custom fan mode of the climate device (protected - use get_custom_fan_mode() or setters). + const char *custom_fan_mode_{nullptr}; + + /// The active custom preset mode of the climate device (protected - use get_custom_preset() or setters). + const char *custom_preset_{nullptr}; }; } // namespace climate diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 1a53a66f77..4e9c7e4d71 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -1198,8 +1198,6 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) ESP_LOGI(TAG, "Custom preset %s applied", custom_preset.c_str()); } else { ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset.c_str()); - // Still need to ensure preset is reset and custom_preset is set - this->set_custom_preset_(custom_preset); } } else { ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset.c_str()); From cd513b0672879b890b3790a82ee295393f7f128e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:02:28 -0500 Subject: [PATCH 23/40] simplify --- esphome/components/api/api_connection.cpp | 4 +-- .../bedjet/climate/bedjet_climate.cpp | 25 +++++++-------- esphome/components/climate/climate.cpp | 32 +++++++++++-------- esphome/components/climate/climate.h | 4 +++ .../thermostat/thermostat_climate.cpp | 4 +-- esphome/components/web_server/web_server.cpp | 4 +-- 6 files changed, 39 insertions(+), 34 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2914f15b4d..7413b0c419 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -638,13 +638,13 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) resp.fan_mode = static_cast(climate->fan_mode.value()); if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) { - resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode)); + resp.set_custom_fan_mode(StringRef(climate->get_custom_fan_mode())); } if (traits.get_supports_presets() && climate->preset.has_value()) { resp.preset = static_cast(climate->preset.value()); } if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) { - resp.set_custom_preset(StringRef(climate->custom_preset)); + resp.set_custom_preset(StringRef(climate->get_custom_preset())); } if (traits.get_supports_swing_modes()) resp.swing_mode = static_cast(climate->swing_mode); diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 65fa092e8e..302229f254 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -79,7 +79,7 @@ void BedJetClimate::reset_state_() { this->target_temperature = NAN; this->current_temperature = NAN; this->preset.reset(); - this->custom_preset.reset(); + this->clear_custom_preset_(); this->publish_state(); } @@ -184,8 +184,7 @@ void BedJetClimate::control(const ClimateCall &call) { } if (result) { - this->custom_preset = preset; - this->preset.reset(); + this->set_custom_preset_(preset.c_str()); } } @@ -207,8 +206,7 @@ void BedJetClimate::control(const ClimateCall &call) { } if (result) { - this->fan_mode = fan_mode; - this->custom_fan_mode.reset(); + this->set_fan_mode_(fan_mode); } } else if (call.get_custom_fan_mode().has_value()) { auto fan_mode = *call.get_custom_fan_mode(); @@ -218,8 +216,7 @@ void BedJetClimate::control(const ClimateCall &call) { fan_index); bool result = this->parent_->set_fan_index(fan_index); if (result) { - this->custom_fan_mode = fan_mode; - this->fan_mode.reset(); + this->set_custom_fan_mode_(fan_mode.c_str()); } } } @@ -245,7 +242,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step); if (fan_mode_name != nullptr) { - this->custom_fan_mode = *fan_mode_name; + this->set_custom_fan_mode_(fan_mode_name); } // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. @@ -255,7 +252,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { this->mode = CLIMATE_MODE_OFF; this->action = CLIMATE_ACTION_IDLE; this->fan_mode = CLIMATE_FAN_OFF; - this->custom_preset.reset(); + this->clear_custom_preset_(); this->preset.reset(); break; @@ -266,7 +263,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { if (this->heating_mode_ == HEAT_MODE_EXTENDED) { this->set_custom_preset_("LTD HT"); } else { - this->custom_preset.reset(); + this->clear_custom_preset_(); } break; @@ -275,7 +272,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { this->action = CLIMATE_ACTION_HEATING; this->preset.reset(); if (this->heating_mode_ == HEAT_MODE_EXTENDED) { - this->custom_preset.reset(); + this->clear_custom_preset_(); } else { this->set_custom_preset_("EXT HT"); } @@ -284,20 +281,20 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { case MODE_COOL: this->mode = CLIMATE_MODE_FAN_ONLY; this->action = CLIMATE_ACTION_COOLING; - this->custom_preset.reset(); + this->clear_custom_preset_(); this->preset.reset(); break; case MODE_DRY: this->mode = CLIMATE_MODE_DRY; this->action = CLIMATE_ACTION_DRYING; - this->custom_preset.reset(); + this->clear_custom_preset_(); this->preset.reset(); break; case MODE_TURBO: this->preset = CLIMATE_PRESET_BOOST; - this->custom_preset.reset(); + this->clear_custom_preset_(); this->mode = CLIMATE_MODE_HEAT; this->action = CLIMATE_ACTION_HEATING; break; diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 9f9a0ca5d6..5bf32e4c28 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -398,7 +398,7 @@ void Climate::save_state_() { // std::vector maintains insertion order size_t i = 0; for (const char *mode : supported) { - if (strcmp(mode, custom_fan_mode) == 0) { + if (strcmp(mode, this->custom_fan_mode_) == 0) { state.custom_fan_mode = i; break; } @@ -415,7 +415,7 @@ void Climate::save_state_() { // std::vector maintains insertion order size_t i = 0; for (const char *preset : supported) { - if (strcmp(preset, custom_preset) == 0) { + if (strcmp(preset, this->custom_preset_) == 0) { state.custom_preset = i; break; } @@ -441,13 +441,13 @@ void Climate::publish_state() { ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); } if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { - ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode); + ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_); } if (traits.get_supports_presets() && this->preset.has_value()) { ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); } if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset); + ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); } if (traits.get_supports_swing_modes()) { ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); @@ -572,20 +572,20 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { if (this->uses_custom_fan_mode) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { climate->fan_mode.reset(); - climate->custom_fan_mode = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; + climate->custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; } } else if (traits.supports_fan_mode(this->fan_mode)) { climate->fan_mode = this->fan_mode; - climate->custom_fan_mode = nullptr; + climate->clear_custom_fan_mode_(); } if (this->uses_custom_preset) { if (this->custom_preset < traits.get_supported_custom_presets().size()) { climate->preset.reset(); - climate->custom_preset = traits.get_supported_custom_presets()[this->custom_preset]; + climate->custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset]; } } else if (traits.supports_preset(this->preset)) { climate->preset = this->preset; - climate->custom_preset = nullptr; + climate->clear_custom_preset_(); } if (traits.supports_swing_mode(this->swing_mode)) { climate->swing_mode = this->swing_mode; @@ -617,18 +617,18 @@ template bool set_alternative(T1 &dst, T2 } bool Climate::set_fan_mode_(ClimateFanMode mode) { - return set_alternative(this->fan_mode, this->custom_fan_mode, mode); + return set_alternative(this->fan_mode, this->custom_fan_mode_, mode); } bool Climate::set_custom_fan_mode_(const char *mode) { auto traits = this->get_traits(); const char *mode_ptr = traits.find_custom_fan_mode_(mode); if (mode_ptr != nullptr) { - return set_alternative(this->custom_fan_mode, this->fan_mode, mode_ptr); + return set_alternative(this->custom_fan_mode_, this->fan_mode, mode_ptr); } // Mode not found in supported custom modes, clear it if currently set if (this->has_custom_fan_mode()) { - this->custom_fan_mode = nullptr; + this->custom_fan_mode_ = nullptr; return true; } return false; @@ -636,17 +636,19 @@ bool Climate::set_custom_fan_mode_(const char *mode) { bool Climate::set_custom_fan_mode_(const std::string &mode) { return this->set_custom_fan_mode_(mode.c_str()); } -bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } +void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } + +bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset_, preset); } bool Climate::set_custom_preset_(const char *preset) { auto traits = this->get_traits(); const char *preset_ptr = traits.find_custom_preset_(preset); if (preset_ptr != nullptr) { - return set_alternative(this->custom_preset, this->preset, preset_ptr); + return set_alternative(this->custom_preset_, this->preset, preset_ptr); } // Preset not found in supported custom presets, clear it if currently set if (this->has_custom_preset()) { - this->custom_preset = nullptr; + this->custom_preset_ = nullptr; return true; } return false; @@ -654,6 +656,8 @@ bool Climate::set_custom_preset_(const char *preset) { bool Climate::set_custom_preset_(const std::string &preset) { return this->set_custom_preset_(preset.c_str()); } +void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; } + const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) { auto traits = this->get_traits(); return traits.find_custom_fan_mode_(custom_fan_mode); diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 495d9f700f..0c3393028a 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -273,6 +273,8 @@ class Climate : public EntityBase { bool set_custom_fan_mode_(const char *mode); /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. bool set_custom_fan_mode_(const std::string &mode); + /// Clear custom fan mode. + void clear_custom_fan_mode_(); /// Set preset. Reset custom preset. Return true if preset has been changed. bool set_preset_(ClimatePreset preset); @@ -281,6 +283,8 @@ class Climate : public EntityBase { bool set_custom_preset_(const char *preset); /// Set custom preset. Reset primary preset. Return true if preset has been changed. bool set_custom_preset_(const std::string &preset); + /// Clear custom preset. + void clear_custom_preset_(); /// Find and return the matching custom fan mode pointer from traits, or nullptr if not found. const char *find_custom_fan_mode_(const char *custom_fan_mode); diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 4e9c7e4d71..2c8e3e4d9a 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -1172,7 +1172,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { } else { ESP_LOGI(TAG, "No changes required to apply preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); } - this->custom_preset = nullptr; + this->clear_custom_preset_(); this->preset = preset; } else { ESP_LOGW(TAG, "Preset %s not configured; ignoring", LOG_STR_ARG(climate::climate_preset_to_string(preset))); @@ -1185,7 +1185,7 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) if (config != this->custom_preset_config_.end()) { ESP_LOGV(TAG, "Custom preset %s requested", custom_preset.c_str()); if (this->change_preset_internal_(config->second) || !this->has_custom_preset() || - strcmp(this->custom_preset, custom_preset.c_str()) != 0) { + strcmp(this->get_custom_preset(), custom_preset.c_str()) != 0) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; // Use the base class method which handles pointer lookup and preset reset internally diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 7901869b2f..a1bba22cdb 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1334,13 +1334,13 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf root["fan_mode"] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); } if (!traits.get_supported_custom_fan_modes().empty() && obj->has_custom_fan_mode()) { - root["custom_fan_mode"] = obj->custom_fan_mode; + root["custom_fan_mode"] = obj->get_custom_fan_mode(); } if (traits.get_supports_presets() && obj->preset.has_value()) { root["preset"] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); } if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { - root["custom_preset"] = obj->custom_preset; + root["custom_preset"] = obj->get_custom_preset(); } if (traits.get_supports_swing_modes()) { root["swing_mode"] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); From b4045b09632b4fe5f9d124f879597437d5f5f840 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:04:55 -0500 Subject: [PATCH 24/40] simplify --- esphome/components/climate/climate.cpp | 4 ++-- esphome/components/demo/demo_climate.h | 16 ++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 5bf32e4c28..c95fcd90b7 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -628,7 +628,7 @@ bool Climate::set_custom_fan_mode_(const char *mode) { } // Mode not found in supported custom modes, clear it if currently set if (this->has_custom_fan_mode()) { - this->custom_fan_mode_ = nullptr; + this->clear_custom_fan_mode_(); return true; } return false; @@ -648,7 +648,7 @@ bool Climate::set_custom_preset_(const char *preset) { } // Preset not found in supported custom presets, clear it if currently set if (this->has_custom_preset()) { - this->custom_preset_ = nullptr; + this->clear_custom_preset_(); return true; } return false; diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h index 84b16e7ec5..0a71ec6dab 100644 --- a/esphome/components/demo/demo_climate.h +++ b/esphome/components/demo/demo_climate.h @@ -28,14 +28,14 @@ class DemoClimate : public climate::Climate, public Component { this->mode = climate::CLIMATE_MODE_AUTO; this->action = climate::CLIMATE_ACTION_COOLING; this->fan_mode = climate::CLIMATE_FAN_HIGH; - this->custom_preset = {"My Preset"}; + this->set_custom_preset_("My Preset"); break; case DemoClimateType::TYPE_3: this->current_temperature = 21.5; this->target_temperature_low = 21.0; this->target_temperature_high = 22.5; this->mode = climate::CLIMATE_MODE_HEAT_COOL; - this->custom_fan_mode = {"Auto Low"}; + this->set_custom_fan_mode_("Auto Low"); this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; this->preset = climate::CLIMATE_PRESET_AWAY; break; @@ -58,23 +58,19 @@ class DemoClimate : public climate::Climate, public Component { this->target_temperature_high = *call.get_target_temperature_high(); } if (call.get_fan_mode().has_value()) { - this->fan_mode = *call.get_fan_mode(); - this->custom_fan_mode.reset(); + this->set_fan_mode_(*call.get_fan_mode()); } if (call.get_swing_mode().has_value()) { this->swing_mode = *call.get_swing_mode(); } if (call.get_custom_fan_mode().has_value()) { - this->custom_fan_mode = *call.get_custom_fan_mode(); - this->fan_mode.reset(); + this->set_custom_fan_mode_(call.get_custom_fan_mode()->c_str()); } if (call.get_preset().has_value()) { - this->preset = *call.get_preset(); - this->custom_preset.reset(); + this->set_preset_(*call.get_preset()); } if (call.get_custom_preset().has_value()) { - this->custom_preset = *call.get_custom_preset(); - this->preset.reset(); + this->set_custom_preset_(call.get_custom_preset()->c_str()); } this->publish_state(); } From 70ec33f41840e2d84cf4ef4b35961acc9b8e55d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:07:33 -0500 Subject: [PATCH 25/40] simplify --- esphome/components/bedjet/climate/bedjet_climate.cpp | 8 ++++---- esphome/components/climate/climate.h | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 302229f254..737000f9ae 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -120,7 +120,7 @@ void BedJetClimate::control(const ClimateCall &call) { if (button_result) { this->mode = mode; // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those - this->custom_preset.reset(); + this->clear_custom_preset_(); this->preset.reset(); } } @@ -145,7 +145,7 @@ void BedJetClimate::control(const ClimateCall &call) { if (result) { this->mode = CLIMATE_MODE_HEAT; this->preset = CLIMATE_PRESET_BOOST; - this->custom_preset.reset(); + this->clear_custom_preset_(); } } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) { if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) { @@ -153,7 +153,7 @@ void BedJetClimate::control(const ClimateCall &call) { result = this->parent_->send_button(heat_button(this->heating_mode_)); if (result) { this->preset.reset(); - this->custom_preset.reset(); + this->clear_custom_preset_(); } } else { ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'", @@ -242,7 +242,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step); if (fan_mode_name != nullptr) { - this->set_custom_fan_mode_(fan_mode_name); + this->set_custom_fan_mode_(fan_mode_name->c_str()); } // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 0c3393028a..8e2bd67995 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -265,6 +265,7 @@ class Climate : public EntityBase { protected: friend ClimateCall; + friend struct ClimateDeviceRestoreState; /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed. bool set_fan_mode_(ClimateFanMode mode); From 03ec52752bc42e4acee70a3a0589ec60440e52b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:09:45 -0500 Subject: [PATCH 26/40] simplify --- esphome/components/climate/climate.cpp | 42 +++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index c95fcd90b7..f0f50973ab 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -617,14 +617,30 @@ template bool set_alternative(T1 &dst, T2 } bool Climate::set_fan_mode_(ClimateFanMode mode) { - return set_alternative(this->fan_mode, this->custom_fan_mode_, mode); + // Clear the custom fan mode (mutual exclusion) + bool changed = this->custom_fan_mode_ != nullptr; + this->custom_fan_mode_ = nullptr; + // Set the primary fan mode + if (changed || !this->fan_mode.has_value() || this->fan_mode.value() != mode) { + this->fan_mode = mode; + return true; + } + return false; } bool Climate::set_custom_fan_mode_(const char *mode) { auto traits = this->get_traits(); const char *mode_ptr = traits.find_custom_fan_mode_(mode); if (mode_ptr != nullptr) { - return set_alternative(this->custom_fan_mode_, this->fan_mode, mode_ptr); + // Clear the primary fan mode (mutual exclusion) + bool changed = this->fan_mode.has_value(); + this->fan_mode.reset(); + // Set the custom fan mode + if (changed || this->custom_fan_mode_ != mode_ptr) { + this->custom_fan_mode_ = mode_ptr; + return true; + } + return false; } // Mode not found in supported custom modes, clear it if currently set if (this->has_custom_fan_mode()) { @@ -638,13 +654,31 @@ bool Climate::set_custom_fan_mode_(const std::string &mode) { return this->set_c void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } -bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset_, preset); } +bool Climate::set_preset_(ClimatePreset preset) { + // Clear the custom preset (mutual exclusion) + bool changed = this->custom_preset_ != nullptr; + this->custom_preset_ = nullptr; + // Set the primary preset + if (changed || !this->preset.has_value() || this->preset.value() != preset) { + this->preset = preset; + return true; + } + return false; +} bool Climate::set_custom_preset_(const char *preset) { auto traits = this->get_traits(); const char *preset_ptr = traits.find_custom_preset_(preset); if (preset_ptr != nullptr) { - return set_alternative(this->custom_preset_, this->preset, preset_ptr); + // Clear the primary preset (mutual exclusion) + bool changed = this->preset.has_value(); + this->preset.reset(); + // Set the custom preset + if (changed || this->custom_preset_ != preset_ptr) { + this->custom_preset_ = preset_ptr; + return true; + } + return false; } // Preset not found in supported custom presets, clear it if currently set if (this->has_custom_preset()) { From 60a303adb83eef202141a883ed5545fb7ddc88dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:10:36 -0500 Subject: [PATCH 27/40] simplify --- esphome/components/climate/climate.cpp | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index f0f50973ab..36c407242a 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -593,29 +593,6 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->publish_state(); } -// Generic template to set one value while clearing its alternative (mutual exclusion) -// Handles both optional and const char* types automatically using compile-time type detection -template bool set_alternative(T1 &dst, T2 &alt, T3 src) { - bool is_changed = false; - - // Clear the alternative based on its type (pointer or optional) - if constexpr (std::is_pointer_v>) { - is_changed = (alt != nullptr); - alt = nullptr; - } else { - is_changed = alt.has_value(); - alt.reset(); - } - - // Set the destination value - if (is_changed || dst != src) { - dst = src; - is_changed = true; - } - - return is_changed; -} - bool Climate::set_fan_mode_(ClimateFanMode mode) { // Clear the custom fan mode (mutual exclusion) bool changed = this->custom_fan_mode_ != nullptr; From d1bb5c4d790aeab887de7425f7b3c52968a94695 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:16:36 -0500 Subject: [PATCH 28/40] simplify --- esphome/components/climate/climate.cpp | 83 +++++++++++--------------- 1 file changed, 36 insertions(+), 47 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 36c407242a..fc26ff524a 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -593,76 +593,65 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->publish_state(); } -bool Climate::set_fan_mode_(ClimateFanMode mode) { - // Clear the custom fan mode (mutual exclusion) - bool changed = this->custom_fan_mode_ != nullptr; - this->custom_fan_mode_ = nullptr; - // Set the primary fan mode - if (changed || !this->fan_mode.has_value() || this->fan_mode.value() != mode) { - this->fan_mode = mode; +// Template helper for setting primary modes with mutual exclusion +// Clears custom pointer and sets primary optional value +template bool set_primary_mode_(optional &primary, const char *&custom_ptr, T value) { + // Clear the custom mode (mutual exclusion) + bool changed = custom_ptr != nullptr; + custom_ptr = nullptr; + // Set the primary mode + if (changed || !primary.has_value() || primary.value() != value) { + primary = value; return true; } return false; } -bool Climate::set_custom_fan_mode_(const char *mode) { - auto traits = this->get_traits(); - const char *mode_ptr = traits.find_custom_fan_mode_(mode); - if (mode_ptr != nullptr) { - // Clear the primary fan mode (mutual exclusion) - bool changed = this->fan_mode.has_value(); - this->fan_mode.reset(); - // Set the custom fan mode - if (changed || this->custom_fan_mode_ != mode_ptr) { - this->custom_fan_mode_ = mode_ptr; +// Template helper for setting custom modes with mutual exclusion +// Takes pre-computed values: the found pointer from traits and whether custom mode is currently set +template +bool set_custom_mode_(const char *&custom_ptr, optional &primary, const char *found_ptr, bool has_custom) { + if (found_ptr != nullptr) { + // Clear the primary mode (mutual exclusion) + bool changed = primary.has_value(); + primary.reset(); + // Set the custom mode + if (changed || custom_ptr != found_ptr) { + custom_ptr = found_ptr; return true; } return false; } - // Mode not found in supported custom modes, clear it if currently set - if (this->has_custom_fan_mode()) { - this->clear_custom_fan_mode_(); + // Mode not found in supported modes, clear it if currently set + if (has_custom) { + custom_ptr = nullptr; return true; } return false; } +bool Climate::set_fan_mode_(ClimateFanMode mode) { + return set_primary_mode_(this->fan_mode, this->custom_fan_mode_, mode); +} + +bool Climate::set_custom_fan_mode_(const char *mode) { + auto traits = this->get_traits(); + return set_custom_mode_(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode), + this->has_custom_fan_mode()); +} + bool Climate::set_custom_fan_mode_(const std::string &mode) { return this->set_custom_fan_mode_(mode.c_str()); } void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } bool Climate::set_preset_(ClimatePreset preset) { - // Clear the custom preset (mutual exclusion) - bool changed = this->custom_preset_ != nullptr; - this->custom_preset_ = nullptr; - // Set the primary preset - if (changed || !this->preset.has_value() || this->preset.value() != preset) { - this->preset = preset; - return true; - } - return false; + return set_primary_mode_(this->preset, this->custom_preset_, preset); } bool Climate::set_custom_preset_(const char *preset) { auto traits = this->get_traits(); - const char *preset_ptr = traits.find_custom_preset_(preset); - if (preset_ptr != nullptr) { - // Clear the primary preset (mutual exclusion) - bool changed = this->preset.has_value(); - this->preset.reset(); - // Set the custom preset - if (changed || this->custom_preset_ != preset_ptr) { - this->custom_preset_ = preset_ptr; - return true; - } - return false; - } - // Preset not found in supported custom presets, clear it if currently set - if (this->has_custom_preset()) { - this->clear_custom_preset_(); - return true; - } - return false; + return set_custom_mode_(this->custom_preset_, this->preset, traits.find_custom_preset_(preset), + this->has_custom_preset()); } bool Climate::set_custom_preset_(const std::string &preset) { return this->set_custom_preset_(preset.c_str()); } From a073ec4e11c5db0d27b096a0addb68cc2cff92ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:19:07 -0500 Subject: [PATCH 29/40] simplify --- esphome/components/climate/climate.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index fc26ff524a..3656f57cc2 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -659,13 +659,11 @@ bool Climate::set_custom_preset_(const std::string &preset) { return this->set_c void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; } const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) { - auto traits = this->get_traits(); - return traits.find_custom_fan_mode_(custom_fan_mode); + return this->get_traits().find_custom_fan_mode_(custom_fan_mode); } const char *Climate::find_custom_preset_(const char *custom_preset) { - auto traits = this->get_traits(); - return traits.find_custom_preset_(custom_preset); + return this->get_traits().find_custom_preset_(custom_preset); } void Climate::dump_traits_(const char *tag) { From 6dd29f1917847736cd4607653d3c558d56a9fe6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:25:26 -0500 Subject: [PATCH 30/40] simplify --- esphome/components/climate/climate.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 3656f57cc2..07c75fada7 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -595,7 +595,7 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { // Template helper for setting primary modes with mutual exclusion // Clears custom pointer and sets primary optional value -template bool set_primary_mode_(optional &primary, const char *&custom_ptr, T value) { +template bool set_primary_mode(optional &primary, const char *&custom_ptr, T value) { // Clear the custom mode (mutual exclusion) bool changed = custom_ptr != nullptr; custom_ptr = nullptr; @@ -610,7 +610,7 @@ template bool set_primary_mode_(optional &primary, const char *&c // Template helper for setting custom modes with mutual exclusion // Takes pre-computed values: the found pointer from traits and whether custom mode is currently set template -bool set_custom_mode_(const char *&custom_ptr, optional &primary, const char *found_ptr, bool has_custom) { +bool set_custom_mode(const char *&custom_ptr, optional &primary, const char *found_ptr, bool has_custom) { if (found_ptr != nullptr) { // Clear the primary mode (mutual exclusion) bool changed = primary.has_value(); @@ -631,27 +631,25 @@ bool set_custom_mode_(const char *&custom_ptr, optional &primary, const char } bool Climate::set_fan_mode_(ClimateFanMode mode) { - return set_primary_mode_(this->fan_mode, this->custom_fan_mode_, mode); + return set_primary_mode(this->fan_mode, this->custom_fan_mode_, mode); } bool Climate::set_custom_fan_mode_(const char *mode) { auto traits = this->get_traits(); - return set_custom_mode_(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode), - this->has_custom_fan_mode()); + return set_custom_mode(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode), + this->has_custom_fan_mode()); } bool Climate::set_custom_fan_mode_(const std::string &mode) { return this->set_custom_fan_mode_(mode.c_str()); } void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } -bool Climate::set_preset_(ClimatePreset preset) { - return set_primary_mode_(this->preset, this->custom_preset_, preset); -} +bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); } bool Climate::set_custom_preset_(const char *preset) { auto traits = this->get_traits(); - return set_custom_mode_(this->custom_preset_, this->preset, traits.find_custom_preset_(preset), - this->has_custom_preset()); + return set_custom_mode(this->custom_preset_, this->preset, traits.find_custom_preset_(preset), + this->has_custom_preset()); } bool Climate::set_custom_preset_(const std::string &preset) { return this->set_custom_preset_(preset.c_str()); } From 0a86254b8444ccd4804d699476bfa610fda4b150 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:32:28 -0500 Subject: [PATCH 31/40] simplify --- esphome/components/climate/climate.cpp | 4 ---- esphome/components/climate/climate.h | 4 ---- esphome/components/thermostat/thermostat_climate.cpp | 4 ++-- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 07c75fada7..ebc9e466e0 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -640,8 +640,6 @@ bool Climate::set_custom_fan_mode_(const char *mode) { this->has_custom_fan_mode()); } -bool Climate::set_custom_fan_mode_(const std::string &mode) { return this->set_custom_fan_mode_(mode.c_str()); } - void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); } @@ -652,8 +650,6 @@ bool Climate::set_custom_preset_(const char *preset) { this->has_custom_preset()); } -bool Climate::set_custom_preset_(const std::string &preset) { return this->set_custom_preset_(preset.c_str()); } - void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; } const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) { diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 8e2bd67995..c36625b2ae 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -272,8 +272,6 @@ class Climate : public EntityBase { /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. bool set_custom_fan_mode_(const char *mode); - /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. - bool set_custom_fan_mode_(const std::string &mode); /// Clear custom fan mode. void clear_custom_fan_mode_(); @@ -282,8 +280,6 @@ class Climate : public EntityBase { /// Set custom preset. Reset primary preset. Return true if preset has been changed. bool set_custom_preset_(const char *preset); - /// Set custom preset. Reset primary preset. Return true if preset has been changed. - bool set_custom_preset_(const std::string &preset); /// Clear custom preset. void clear_custom_preset_(); diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 2c8e3e4d9a..5e52c4721e 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -224,7 +224,7 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { this->change_custom_preset_(call.get_custom_preset().value()); } else { // Use the base class method which handles pointer lookup internally - this->set_custom_preset_(call.get_custom_preset().value()); + this->set_custom_preset_(call.get_custom_preset().value().c_str()); } } @@ -1189,7 +1189,7 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; // Use the base class method which handles pointer lookup and preset reset internally - this->set_custom_preset_(custom_preset); + this->set_custom_preset_(custom_preset.c_str()); if (trig != nullptr) { trig->trigger(); } From 1fd6f7bcd32ad5ceee96ccb90868e2a6b6e568f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:41:44 -0500 Subject: [PATCH 32/40] simplify --- esphome/components/bedjet/climate/bedjet_climate.cpp | 8 ++------ esphome/components/demo/demo_climate.h | 2 +- esphome/components/thermostat/thermostat_climate.cpp | 4 +--- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 737000f9ae..52cc76f147 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -144,8 +144,7 @@ void BedJetClimate::control(const ClimateCall &call) { if (result) { this->mode = CLIMATE_MODE_HEAT; - this->preset = CLIMATE_PRESET_BOOST; - this->clear_custom_preset_(); + this->set_preset_(CLIMATE_PRESET_BOOST); } } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) { if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) { @@ -259,7 +258,6 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { case MODE_HEAT: this->mode = CLIMATE_MODE_HEAT; this->action = CLIMATE_ACTION_HEATING; - this->preset.reset(); if (this->heating_mode_ == HEAT_MODE_EXTENDED) { this->set_custom_preset_("LTD HT"); } else { @@ -270,7 +268,6 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { case MODE_EXTHT: this->mode = CLIMATE_MODE_HEAT; this->action = CLIMATE_ACTION_HEATING; - this->preset.reset(); if (this->heating_mode_ == HEAT_MODE_EXTENDED) { this->clear_custom_preset_(); } else { @@ -293,8 +290,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { break; case MODE_TURBO: - this->preset = CLIMATE_PRESET_BOOST; - this->clear_custom_preset_(); + this->set_preset_(CLIMATE_PRESET_BOOST); this->mode = CLIMATE_MODE_HEAT; this->action = CLIMATE_ACTION_HEATING; break; diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h index 0a71ec6dab..f8944b0735 100644 --- a/esphome/components/demo/demo_climate.h +++ b/esphome/components/demo/demo_climate.h @@ -37,7 +37,7 @@ class DemoClimate : public climate::Climate, public Component { this->mode = climate::CLIMATE_MODE_HEAT_COOL; this->set_custom_fan_mode_("Auto Low"); this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; - this->preset = climate::CLIMATE_PRESET_AWAY; + this->set_preset_(climate::CLIMATE_PRESET_AWAY); break; } this->publish_state(); diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 5e52c4721e..d2f5db3b32 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -1162,7 +1162,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { this->preset.value() != preset) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; - this->preset = preset; + this->set_preset_(preset); if (trig != nullptr) { trig->trigger(); } @@ -1172,8 +1172,6 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { } else { ESP_LOGI(TAG, "No changes required to apply preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); } - this->clear_custom_preset_(); - this->preset = preset; } else { ESP_LOGW(TAG, "Preset %s not configured; ignoring", LOG_STR_ARG(climate::climate_preset_to_string(preset))); } From f6e8fdcd9149d22d76b00dcb05c363f43dbd8d2e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:50:00 -0500 Subject: [PATCH 33/40] simplify --- esphome/components/api/api_connection.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7413b0c419..a0e0638860 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -699,11 +699,11 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { if (msg.has_fan_mode) call.set_fan_mode(static_cast(msg.fan_mode)); if (msg.has_custom_fan_mode) - call.set_fan_mode(msg.custom_fan_mode.c_str()); + call.set_fan_mode(msg.custom_fan_mode); if (msg.has_preset) call.set_preset(static_cast(msg.preset)); if (msg.has_custom_preset) - call.set_preset(msg.custom_preset.c_str()); + call.set_preset(msg.custom_preset); if (msg.has_swing_mode) call.set_swing_mode(static_cast(msg.swing_mode)); call.perform(); From d7f55e9977c7240e5436967caf0c27c7ae3445f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:53:30 -0500 Subject: [PATCH 34/40] fixes --- esphome/components/bedjet/climate/bedjet_climate.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 52cc76f147..877fd6f771 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -258,6 +258,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { case MODE_HEAT: this->mode = CLIMATE_MODE_HEAT; this->action = CLIMATE_ACTION_HEATING; + this->preset.reset(); if (this->heating_mode_ == HEAT_MODE_EXTENDED) { this->set_custom_preset_("LTD HT"); } else { @@ -268,6 +269,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { case MODE_EXTHT: this->mode = CLIMATE_MODE_HEAT; this->action = CLIMATE_ACTION_HEATING; + this->preset.reset(); if (this->heating_mode_ == HEAT_MODE_EXTENDED) { this->clear_custom_preset_(); } else { From 1b5a942f6160c711efc8b4f1846f0f045506e899 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:58:02 -0500 Subject: [PATCH 35/40] fixes --- esphome/components/thermostat/thermostat_climate.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index d2f5db3b32..8258fa9d65 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -1197,6 +1197,9 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) } else { ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset.c_str()); } + // Note: set_custom_preset_() above handles preset.reset() and custom_preset_ assignment internally. + // The old code had these lines here unconditionally, which was a bug (double assignment, state modification + // even when no changes were needed). Now properly handled by the protected setter with mutual exclusion. } else { ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset.c_str()); } From c36b7781589d139a420b9deda02cbebfc2ee629b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 21:07:23 -0500 Subject: [PATCH 36/40] safety --- esphome/components/climate/climate.cpp | 46 ++++++++++++++++++--- esphome/components/climate/climate.h | 30 +++++++++++++- esphome/components/climate/climate_traits.h | 46 +++++++++++++++++++-- 3 files changed, 112 insertions(+), 10 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index ebc9e466e0..e596582de8 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -593,8 +593,25 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->publish_state(); } -// Template helper for setting primary modes with mutual exclusion -// Clears custom pointer and sets primary optional value +/** Template helper for setting primary modes (fan_mode, preset) with mutual exclusion. + * + * Climate devices have mutually exclusive mode pairs: + * - fan_mode (enum) vs custom_fan_mode_ (const char*) + * - preset (enum) vs custom_preset_ (const char*) + * + * Only one mode in each pair can be active at a time. This helper ensures setting a primary + * mode automatically clears its corresponding custom mode. + * + * Example state transitions: + * Before: custom_fan_mode_="Turbo", fan_mode=nullopt + * Call: set_fan_mode_(CLIMATE_FAN_HIGH) + * After: custom_fan_mode_=nullptr, fan_mode=CLIMATE_FAN_HIGH + * + * @param primary The primary mode optional (fan_mode or preset) + * @param custom_ptr Reference to the custom mode pointer (custom_fan_mode_ or custom_preset_) + * @param value The new primary mode value to set + * @return true if state changed, false if already set to this value + */ template bool set_primary_mode(optional &primary, const char *&custom_ptr, T value) { // Clear the custom mode (mutual exclusion) bool changed = custom_ptr != nullptr; @@ -607,15 +624,34 @@ template bool set_primary_mode(optional &primary, const char *&cu return false; } -// Template helper for setting custom modes with mutual exclusion -// Takes pre-computed values: the found pointer from traits and whether custom mode is currently set +/** Template helper for setting custom modes (custom_fan_mode_, custom_preset_) with mutual exclusion. + * + * This helper ensures setting a custom mode automatically clears its corresponding primary mode. + * It also validates that the custom mode exists in the device's supported modes (lifetime safety). + * + * Example state transitions: + * Before: fan_mode=CLIMATE_FAN_HIGH, custom_fan_mode_=nullptr + * Call: set_custom_fan_mode_("Turbo") + * After: fan_mode=nullopt, custom_fan_mode_="Turbo" (pointer from traits) + * + * Lifetime Safety: + * - found_ptr must come from traits.find_custom_*_mode_() + * - Only pointers found in traits are stored, ensuring they remain valid + * - Prevents dangling pointers from temporary strings + * + * @param custom_ptr Reference to the custom mode pointer to set + * @param primary The primary mode optional to clear + * @param found_ptr The validated pointer from traits (nullptr if not found) + * @param has_custom Whether a custom mode is currently active + * @return true if state changed, false otherwise + */ template bool set_custom_mode(const char *&custom_ptr, optional &primary, const char *found_ptr, bool has_custom) { if (found_ptr != nullptr) { // Clear the primary mode (mutual exclusion) bool changed = primary.has_value(); primary.reset(); - // Set the custom mode + // Set the custom mode (pointer is validated by caller from traits) if (changed || custom_ptr != found_ptr) { custom_ptr = found_ptr; return true; diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index c36625b2ae..c6cd7005c5 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -325,10 +325,36 @@ class Climate : public EntityBase { optional visual_min_humidity_override_{}; optional visual_max_humidity_override_{}; - /// The active custom fan mode of the climate device (protected - use get_custom_fan_mode() or setters). + /** The active custom fan mode of the climate device. + * + * PROTECTED ACCESS: External components must use get_custom_fan_mode() for read access. + * Derived climate classes must use set_custom_fan_mode_() / clear_custom_fan_mode_() to modify. + * + * POINTER LIFETIME SAFETY: + * This pointer MUST always point to an entry in the traits.supported_custom_fan_modes_ vector, + * or be nullptr. The protected setter set_custom_fan_mode_() enforces this by calling + * traits.find_custom_fan_mode_() to validate and obtain the correct pointer. + * + * Never assign directly - always use setters: + * this->set_custom_fan_mode_("Turbo"); // ✓ Safe - validates against traits + * this->custom_fan_mode_ = "Turbo"; // ✗ UNSAFE - may create dangling pointer + */ const char *custom_fan_mode_{nullptr}; - /// The active custom preset mode of the climate device (protected - use get_custom_preset() or setters). + /** The active custom preset mode of the climate device. + * + * PROTECTED ACCESS: External components must use get_custom_preset() for read access. + * Derived climate classes must use set_custom_preset_() / clear_custom_preset_() to modify. + * + * POINTER LIFETIME SAFETY: + * This pointer MUST always point to an entry in the traits.supported_custom_presets_ vector, + * or be nullptr. The protected setter set_custom_preset_() enforces this by calling + * traits.find_custom_preset_() to validate and obtain the correct pointer. + * + * Never assign directly - always use setters: + * this->set_custom_preset_("Eco"); // ✓ Safe - validates against traits + * this->custom_preset_ = "Eco"; // ✗ UNSAFE - may create dangling pointer + */ const char *custom_preset_{nullptr}; }; diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index cbd9d1dbf4..14dcbcff6c 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -157,6 +157,11 @@ class ClimateTraits { template void set_supported_custom_fan_modes(const char *const (&modes)[N]) { this->supported_custom_fan_modes_.assign(modes, modes + N); } + + // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages + void set_supported_custom_fan_modes(const std::vector &modes) = delete; + void set_supported_custom_fan_modes(std::initializer_list modes) = delete; + const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } bool supports_custom_fan_mode(const char *custom_fan_mode) const { return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); @@ -180,6 +185,11 @@ class ClimateTraits { template void set_supported_custom_presets(const char *const (&presets)[N]) { this->supported_custom_presets_.assign(presets, presets + N); } + + // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages + void set_supported_custom_presets(const std::vector &presets) = delete; + void set_supported_custom_presets(std::initializer_list presets) = delete; + const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } bool supports_custom_preset(const char *custom_preset) const { return vector_contains(this->supported_custom_presets_, custom_preset); @@ -269,9 +279,39 @@ class ClimateTraits { climate::ClimateFanModeMask supported_fan_modes_; climate::ClimateSwingModeMask supported_swing_modes_; climate::ClimatePresetMask supported_presets_; - // Store const char* pointers to avoid std::string overhead - // Pointers must remain valid for traits lifetime (typically string literals in rodata, - // or pointers to strings with sufficient lifetime like member variables) + + /** Custom mode storage using const char* pointers to eliminate std::string overhead. + * + * POINTER LIFETIME SAFETY REQUIREMENTS: + * Pointers stored here MUST remain valid for the entire lifetime of the ClimateTraits object. + * This is guaranteed when pointers point to: + * + * 1. String literals (rodata section, valid for program lifetime): + * traits.set_supported_custom_fan_modes({"Turbo", "Silent"}); + * + * 2. Static const data (valid for program lifetime): + * static const char* PRESET_ECO = "Eco"; + * traits.set_supported_custom_presets({PRESET_ECO}); + * + * 3. Member variables with sufficient lifetime: + * class MyClimate { + * std::vector custom_presets_; // Lives as long as component + * ClimateTraits traits() { + * // Extract from map keys that live as long as the component + * for (const auto& [name, config] : preset_map_) { + * custom_presets_.push_back(name.c_str()); + * } + * traits.set_supported_custom_presets(custom_presets_); + * } + * }; + * + * UNSAFE PATTERNS TO AVOID: + * std::string temp = "Mode"; + * traits.set_supported_custom_fan_modes({temp.c_str()}); // DANGLING POINTER! + * + * Protected setters in Climate class automatically validate pointers against these + * vectors, ensuring only safe pointers are stored in device state. + */ std::vector supported_custom_fan_modes_; std::vector supported_custom_presets_; }; From 868d01ae039657eb5ca4fe89997cfd635ccb620e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 21:10:01 -0500 Subject: [PATCH 37/40] safety --- esphome/components/climate/climate.h | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index c6cd7005c5..050fc5e475 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -325,35 +325,40 @@ class Climate : public EntityBase { optional visual_min_humidity_override_{}; optional visual_max_humidity_override_{}; + private: /** The active custom fan mode of the climate device. * - * PROTECTED ACCESS: External components must use get_custom_fan_mode() for read access. - * Derived climate classes must use set_custom_fan_mode_() / clear_custom_fan_mode_() to modify. + * PRIVATE ACCESS (compile-time enforced safety): + * - External components: Use get_custom_fan_mode() for read-only access + * - Derived classes: Use set_custom_fan_mode_() / clear_custom_fan_mode_() to modify + * - Direct assignment is prevented at compile time * * POINTER LIFETIME SAFETY: * This pointer MUST always point to an entry in the traits.supported_custom_fan_modes_ vector, * or be nullptr. The protected setter set_custom_fan_mode_() enforces this by calling * traits.find_custom_fan_mode_() to validate and obtain the correct pointer. * - * Never assign directly - always use setters: + * The private access level provides compile-time enforcement: * this->set_custom_fan_mode_("Turbo"); // ✓ Safe - validates against traits - * this->custom_fan_mode_ = "Turbo"; // ✗ UNSAFE - may create dangling pointer + * this->custom_fan_mode_ = "Turbo"; // ✗ Compile error - private member */ const char *custom_fan_mode_{nullptr}; /** The active custom preset mode of the climate device. * - * PROTECTED ACCESS: External components must use get_custom_preset() for read access. - * Derived climate classes must use set_custom_preset_() / clear_custom_preset_() to modify. + * PRIVATE ACCESS (compile-time enforced safety): + * - External components: Use get_custom_preset() for read-only access + * - Derived classes: Use set_custom_preset_() / clear_custom_preset_() to modify + * - Direct assignment is prevented at compile time * * POINTER LIFETIME SAFETY: * This pointer MUST always point to an entry in the traits.supported_custom_presets_ vector, * or be nullptr. The protected setter set_custom_preset_() enforces this by calling * traits.find_custom_preset_() to validate and obtain the correct pointer. * - * Never assign directly - always use setters: + * The private access level provides compile-time enforcement: * this->set_custom_preset_("Eco"); // ✓ Safe - validates against traits - * this->custom_preset_ = "Eco"; // ✗ UNSAFE - may create dangling pointer + * this->custom_preset_ = "Eco"; // ✗ Compile error - private member */ const char *custom_preset_{nullptr}; }; From 1378e52838f1d1c1ae02a9473694d91cfb8815ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 21:10:19 -0500 Subject: [PATCH 38/40] safety --- esphome/components/climate/climate.h | 34 +++++----------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 050fc5e475..091483a033 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -326,39 +326,17 @@ class Climate : public EntityBase { optional visual_max_humidity_override_{}; private: - /** The active custom fan mode of the climate device. + /** The active custom fan mode (private - enforces use of safe setters). * - * PRIVATE ACCESS (compile-time enforced safety): - * - External components: Use get_custom_fan_mode() for read-only access - * - Derived classes: Use set_custom_fan_mode_() / clear_custom_fan_mode_() to modify - * - Direct assignment is prevented at compile time - * - * POINTER LIFETIME SAFETY: - * This pointer MUST always point to an entry in the traits.supported_custom_fan_modes_ vector, - * or be nullptr. The protected setter set_custom_fan_mode_() enforces this by calling - * traits.find_custom_fan_mode_() to validate and obtain the correct pointer. - * - * The private access level provides compile-time enforcement: - * this->set_custom_fan_mode_("Turbo"); // ✓ Safe - validates against traits - * this->custom_fan_mode_ = "Turbo"; // ✗ Compile error - private member + * Points to an entry in traits.supported_custom_fan_modes_ or nullptr. + * Use get_custom_fan_mode() to read, set_custom_fan_mode_() to modify. */ const char *custom_fan_mode_{nullptr}; - /** The active custom preset mode of the climate device. + /** The active custom preset (private - enforces use of safe setters). * - * PRIVATE ACCESS (compile-time enforced safety): - * - External components: Use get_custom_preset() for read-only access - * - Derived classes: Use set_custom_preset_() / clear_custom_preset_() to modify - * - Direct assignment is prevented at compile time - * - * POINTER LIFETIME SAFETY: - * This pointer MUST always point to an entry in the traits.supported_custom_presets_ vector, - * or be nullptr. The protected setter set_custom_preset_() enforces this by calling - * traits.find_custom_preset_() to validate and obtain the correct pointer. - * - * The private access level provides compile-time enforcement: - * this->set_custom_preset_("Eco"); // ✓ Safe - validates against traits - * this->custom_preset_ = "Eco"; // ✗ Compile error - private member + * Points to an entry in traits.supported_custom_presets_ or nullptr. + * Use get_custom_preset() to read, set_custom_preset_() to modify. */ const char *custom_preset_{nullptr}; }; From 5c99eabd1a8db566b812fbd5e89bb36fb740ce4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 21:11:33 -0500 Subject: [PATCH 39/40] safety --- esphome/components/climate/climate_traits.h | 33 ++++----------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 14dcbcff6c..fff1144620 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -282,35 +282,12 @@ class ClimateTraits { /** Custom mode storage using const char* pointers to eliminate std::string overhead. * - * POINTER LIFETIME SAFETY REQUIREMENTS: - * Pointers stored here MUST remain valid for the entire lifetime of the ClimateTraits object. - * This is guaranteed when pointers point to: + * Pointers must remain valid for the ClimateTraits lifetime. Safe patterns: + * - String literals: set_supported_custom_fan_modes({"Turbo", "Silent"}) + * - Static data: static const char* MODE = "Eco"; + * - Component members: Extract from long-lived std::map keys or member vectors * - * 1. String literals (rodata section, valid for program lifetime): - * traits.set_supported_custom_fan_modes({"Turbo", "Silent"}); - * - * 2. Static const data (valid for program lifetime): - * static const char* PRESET_ECO = "Eco"; - * traits.set_supported_custom_presets({PRESET_ECO}); - * - * 3. Member variables with sufficient lifetime: - * class MyClimate { - * std::vector custom_presets_; // Lives as long as component - * ClimateTraits traits() { - * // Extract from map keys that live as long as the component - * for (const auto& [name, config] : preset_map_) { - * custom_presets_.push_back(name.c_str()); - * } - * traits.set_supported_custom_presets(custom_presets_); - * } - * }; - * - * UNSAFE PATTERNS TO AVOID: - * std::string temp = "Mode"; - * traits.set_supported_custom_fan_modes({temp.c_str()}); // DANGLING POINTER! - * - * Protected setters in Climate class automatically validate pointers against these - * vectors, ensuring only safe pointers are stored in device state. + * Climate class setters validate pointers are from these vectors before storing. */ std::vector supported_custom_fan_modes_; std::vector supported_custom_presets_; From fae90194e7c0461579adadf1392dce6b8b2a3b41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 21:12:27 -0500 Subject: [PATCH 40/40] safety --- esphome/components/climate/climate_traits.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index fff1144620..0eecf9789f 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -284,8 +284,7 @@ class ClimateTraits { * * Pointers must remain valid for the ClimateTraits lifetime. Safe patterns: * - String literals: set_supported_custom_fan_modes({"Turbo", "Silent"}) - * - Static data: static const char* MODE = "Eco"; - * - Component members: Extract from long-lived std::map keys or member vectors + * - Static const data: static const char* MODE = "Eco"; * * Climate class setters validate pointers are from these vectors before storing. */