From 5a9e55707b7f675b3d883b0baab1a220a4d75430 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Mar 2026 13:50:18 -1000 Subject: [PATCH] Fix compat: heap-allocate deprecated vectors, add find fallback Two bugs fixed from Copilot review: 1. Dangling pointer on copy: deprecated setters stored data in an OwnedCustomModes struct on ClimateTraits. If the traits object was copied (when NRVO doesn't apply), the copy's pointer dangled. Fix: deprecated setters now heap-allocate (intentional leak, same pattern as Climate entity). Pointer survives any copy. Remove the OwnedCustomModes wrapper entirely. 2. find_custom_fan_mode_ / find_custom_preset_ only searched the Climate-owned vectors, breaking external components using the deprecated traits setters. Fix: fall back to get_traits() when the entity vector is null. --- esphome/components/climate/climate.cpp | 13 +++++++-- esphome/components/climate/climate_traits.h | 31 ++++++--------------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 19a71959aac..7e3df90889b 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -706,8 +706,11 @@ const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) { } const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode, size_t len) { - return this->supported_custom_fan_modes_ ? vector_find(*this->supported_custom_fan_modes_, custom_fan_mode, len) - : nullptr; + if (this->supported_custom_fan_modes_) { + return vector_find(*this->supported_custom_fan_modes_, custom_fan_mode, len); + } + // Fallback for deprecated path: external components may set modes on ClimateTraits directly + return this->get_traits().find_custom_fan_mode_(custom_fan_mode, len); } const char *Climate::find_custom_preset_(const char *custom_preset) { @@ -715,7 +718,11 @@ const char *Climate::find_custom_preset_(const char *custom_preset) { } const char *Climate::find_custom_preset_(const char *custom_preset, size_t len) { - return this->supported_custom_presets_ ? vector_find(*this->supported_custom_presets_, custom_preset, len) : nullptr; + if (this->supported_custom_presets_) { + return vector_find(*this->supported_custom_presets_, custom_preset, len); + } + // Fallback for deprecated path: external components may set modes on ClimateTraits directly + return this->get_traits().find_custom_preset_(custom_preset, len); } void Climate::dump_traits_(const char *tag) { diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 4b8a67cf6f0..fdca2638c28 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -159,18 +159,18 @@ class ClimateTraits { // Remove before 2026.11.0 ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0") void set_supported_custom_fan_modes(std::initializer_list modes) { - this->owned_custom_modes_.fan_modes = modes; - this->supported_custom_fan_modes_ = &this->owned_custom_modes_.fan_modes; + // NOLINT - intentional leak: pointer must survive copies of ClimateTraits + this->supported_custom_fan_modes_ = new std::vector(modes); // NOLINT } // Remove before 2026.11.0 ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0") void set_supported_custom_fan_modes(const std::vector &modes) { - this->owned_custom_modes_.fan_modes = modes; - this->supported_custom_fan_modes_ = &this->owned_custom_modes_.fan_modes; + this->supported_custom_fan_modes_ = new std::vector(modes); // NOLINT } const std::vector &get_supported_custom_fan_modes() const { - return this->supported_custom_fan_modes_ ? *this->supported_custom_fan_modes_ : this->owned_custom_modes_.fan_modes; + static const std::vector EMPTY_VECTOR; + return this->supported_custom_fan_modes_ ? *this->supported_custom_fan_modes_ : EMPTY_VECTOR; } bool supports_custom_fan_mode(const char *custom_fan_mode) const { return this->supported_custom_fan_modes_ && vector_contains(*this->supported_custom_fan_modes_, custom_fan_mode); @@ -192,18 +192,17 @@ class ClimateTraits { // Remove before 2026.11.0 ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0") void set_supported_custom_presets(std::initializer_list presets) { - this->owned_custom_modes_.presets = presets; - this->supported_custom_presets_ = &this->owned_custom_modes_.presets; + this->supported_custom_presets_ = new std::vector(presets); // NOLINT } // Remove before 2026.11.0 ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0") void set_supported_custom_presets(const std::vector &presets) { - this->owned_custom_modes_.presets = presets; - this->supported_custom_presets_ = &this->owned_custom_modes_.presets; + this->supported_custom_presets_ = new std::vector(presets); // NOLINT } const std::vector &get_supported_custom_presets() const { - return this->supported_custom_presets_ ? *this->supported_custom_presets_ : this->owned_custom_modes_.presets; + static const std::vector EMPTY_VECTOR; + return this->supported_custom_presets_ ? *this->supported_custom_presets_ : EMPTY_VECTOR; } bool supports_custom_preset(const char *custom_preset) const { return this->supported_custom_presets_ && vector_contains(*this->supported_custom_presets_, custom_preset); @@ -309,18 +308,6 @@ class ClimateTraits { */ const std::vector *supported_custom_fan_modes_{nullptr}; const std::vector *supported_custom_presets_{nullptr}; - /** Compat storage for deprecated setters — skipped on copy to avoid overhead. - * Remove in 2026.11.0 along with the deprecated overloads. - */ - struct OwnedCustomModes { - std::vector fan_modes; - std::vector presets; - OwnedCustomModes() = default; - OwnedCustomModes(const OwnedCustomModes &) {} // NOLINT - no-op copy: compat data is not propagated - OwnedCustomModes &operator=(const OwnedCustomModes &) { return *this; } // NOLINT - OwnedCustomModes(OwnedCustomModes &&) = default; - OwnedCustomModes &operator=(OwnedCustomModes &&) = default; - } owned_custom_modes_; }; } // namespace esphome::climate