diff --git a/esphome/components/bedjet/climate/bedjet_climate.h b/esphome/components/bedjet/climate/bedjet_climate.h index 05f4a849e07..1d2f28b7cd9 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.h +++ b/esphome/components/bedjet/climate/bedjet_climate.h @@ -42,16 +42,17 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli climate::CLIMATE_MODE_DRY, }); - // It would be better if we had a slider for the fan modes. - traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES); traits.set_supported_presets({ // If we support NONE, then have to decide what happens if the user switches to it (turn off?) // climate::CLIMATE_PRESET_NONE, // Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead. climate::CLIMATE_PRESET_BOOST, }); + // Custom fan modes and presets are stored on Climate base class and wired via get_traits() + // It would be better if we had a slider for the fan modes. + this->set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES); // String literals are stored in rodata and valid for program lifetime - traits.set_supported_custom_presets({ + this->set_supported_custom_presets({ this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT", "M1", "M2", diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 32cac0961c6..51d91469c17 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -484,6 +484,11 @@ void Climate::publish_state() { ClimateTraits Climate::get_traits() { auto traits = this->traits(); + // Wire custom mode pointers from Climate-owned storage + if (!this->supported_custom_fan_modes_.empty()) + traits.set_supported_custom_fan_modes(&this->supported_custom_fan_modes_); + if (!this->supported_custom_presets_.empty()) + traits.set_supported_custom_presets(&this->supported_custom_presets_); #ifdef USE_CLIMATE_VISUAL_OVERRIDES if (!std::isnan(this->visual_min_temperature_override_)) { traits.set_visual_min_temperature(this->visual_min_temperature_override_); @@ -681,9 +686,8 @@ bool Climate::set_fan_mode_(ClimateFanMode mode) { } bool Climate::set_custom_fan_mode_(const char *mode, size_t len) { - auto traits = this->get_traits(); - return set_custom_mode(this->custom_fan_mode_, this->fan_mode, - traits.find_custom_fan_mode_(mode, len), this->has_custom_fan_mode()); + return set_custom_mode(this->custom_fan_mode_, this->fan_mode, this->find_custom_fan_mode_(mode, len), + this->has_custom_fan_mode()); } void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } @@ -691,8 +695,7 @@ 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_custom_preset_(const char *preset, size_t len) { - auto traits = this->get_traits(); - return set_custom_mode(this->custom_preset_, this->preset, traits.find_custom_preset_(preset, len), + return set_custom_mode(this->custom_preset_, this->preset, this->find_custom_preset_(preset, len), this->has_custom_preset()); } @@ -703,7 +706,7 @@ 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->get_traits().find_custom_fan_mode_(custom_fan_mode, len); + return vector_find(this->supported_custom_fan_modes_, custom_fan_mode, len); } const char *Climate::find_custom_preset_(const char *custom_preset) { @@ -711,7 +714,7 @@ 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->get_traits().find_custom_preset_(custom_preset, len); + return vector_find(this->supported_custom_presets_, custom_preset, len); } void Climate::dump_traits_(const char *tag) { diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 0251365dd8a..6214afcbe32 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -234,6 +234,28 @@ class Climate : public EntityBase { void set_visual_max_humidity_override(float visual_max_humidity_override); #endif + /// Set the supported custom fan modes (stored on Climate, referenced by ClimateTraits). + void set_supported_custom_fan_modes(std::initializer_list modes) { + this->supported_custom_fan_modes_ = modes; + } + void set_supported_custom_fan_modes(const std::vector &modes) { + this->supported_custom_fan_modes_ = modes; + } + template void set_supported_custom_fan_modes(const char *const (&modes)[N]) { + this->supported_custom_fan_modes_.assign(modes, modes + N); + } + + /// Set the supported custom presets (stored on Climate, referenced by ClimateTraits). + void set_supported_custom_presets(std::initializer_list presets) { + this->supported_custom_presets_ = presets; + } + void set_supported_custom_presets(const std::vector &presets) { + this->supported_custom_presets_ = presets; + } + template void set_supported_custom_presets(const char *const (&presets)[N]) { + this->supported_custom_presets_.assign(presets, presets + N); + } + /// Check if a custom fan mode is currently active. bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; } @@ -336,13 +358,20 @@ class Climate : public EntityBase { * called from publish_state() */ void save_state_(const ClimateTraits &traits); - void save_state_() { this->save_state_(this->traits()); } + void save_state_() { this->save_state_(this->get_traits()); } void dump_traits_(const char *tag); LazyCallbackManager state_callback_{}; LazyCallbackManager control_callback_{}; ESPPreferenceObject rtc_; + + /** Custom mode storage - owned by Climate, referenced by ClimateTraits via pointer. + * Pointers in these vectors must point to string literals or static data. + */ + std::vector supported_custom_fan_modes_; + std::vector supported_custom_presets_; + #ifdef USE_CLIMATE_VISUAL_OVERRIDES float visual_min_temperature_override_{NAN}; float visual_max_temperature_override_{NAN}; @@ -355,14 +384,14 @@ class Climate : public EntityBase { private: /** The active custom fan mode (private - enforces use of safe setters). * - * Points to an entry in traits.supported_custom_fan_modes_ or nullptr. + * Points to an entry in 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 (private - enforces use of safe setters). * - * Points to an entry in traits.supported_custom_presets_ or nullptr. + * Points to an entry in supported_custom_presets_ or nullptr. * Use get_custom_preset() to read, set_custom_preset_() to modify. */ const char *custom_preset_{nullptr}; diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 80ef0854d59..c0e844126f6 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -147,27 +147,21 @@ class ClimateTraits { void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } bool get_supports_fan_modes() const { - return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); + return !this->supported_fan_modes_.empty() || + (this->supported_custom_fan_modes_ && !this->supported_custom_fan_modes_->empty()); } const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; } - void set_supported_custom_fan_modes(std::initializer_list modes) { + void set_supported_custom_fan_modes(const std::vector *modes) { this->supported_custom_fan_modes_ = modes; } - void set_supported_custom_fan_modes(const std::vector &modes) { - this->supported_custom_fan_modes_ = modes; - } - 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_; } + const std::vector &get_supported_custom_fan_modes() const { + static const std::vector empty; + return this->supported_custom_fan_modes_ ? *this->supported_custom_fan_modes_ : empty; + } bool supports_custom_fan_mode(const char *custom_fan_mode) const { - return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); + return this->supported_custom_fan_modes_ && 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()); @@ -179,23 +173,16 @@ class ClimateTraits { bool get_supports_presets() const { return !this->supported_presets_.empty(); } const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } - void set_supported_custom_presets(std::initializer_list presets) { + void set_supported_custom_presets(const std::vector *presets) { this->supported_custom_presets_ = presets; } - void set_supported_custom_presets(const std::vector &presets) { - this->supported_custom_presets_ = presets; - } - 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_; } + const std::vector &get_supported_custom_presets() const { + static const std::vector empty; + return this->supported_custom_presets_ ? *this->supported_custom_presets_ : empty; + } bool supports_custom_preset(const char *custom_preset) const { - return vector_contains(this->supported_custom_presets_, custom_preset); + return this->supported_custom_presets_ && 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()); @@ -264,7 +251,8 @@ class ClimateTraits { return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode)); } const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len) const { - return vector_find(this->supported_custom_fan_modes_, custom_fan_mode, len); + return this->supported_custom_fan_modes_ ? vector_find(*this->supported_custom_fan_modes_, custom_fan_mode, len) + : nullptr; } /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found @@ -273,7 +261,8 @@ class ClimateTraits { return this->find_custom_preset_(custom_preset, strlen(custom_preset)); } const char *find_custom_preset_(const char *custom_preset, size_t len) const { - return vector_find(this->supported_custom_presets_, custom_preset, len); + return this->supported_custom_presets_ ? vector_find(*this->supported_custom_presets_, custom_preset, len) + : nullptr; } uint32_t feature_flags_{0}; @@ -289,16 +278,13 @@ class ClimateTraits { climate::ClimateSwingModeMask supported_swing_modes_; climate::ClimatePresetMask supported_presets_; - /** Custom mode storage using const char* pointers to eliminate std::string overhead. + /** Custom mode storage - pointers to vectors owned by the Climate base class. * - * Pointers must remain valid for the ClimateTraits lifetime. Safe patterns: - * - String literals: set_supported_custom_fan_modes({"Turbo", "Silent"}) - * - Static const data: static const char* MODE = "Eco"; - * - * Climate class setters validate pointers are from these vectors before storing. + * ClimateTraits does not own this data; Climate stores the vectors and + * get_traits() wires these pointers automatically. */ - std::vector supported_custom_fan_modes_; - std::vector supported_custom_presets_; + const std::vector *supported_custom_fan_modes_{nullptr}; + const std::vector *supported_custom_presets_{nullptr}; }; } // namespace esphome::climate diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h index c5f07ac1145..1706ba804e5 100644 --- a/esphome/components/demo/demo_climate.h +++ b/esphome/components/demo/demo_climate.h @@ -105,14 +105,14 @@ class DemoClimate : public climate::Climate, public Component { climate::CLIMATE_FAN_DIFFUSE, climate::CLIMATE_FAN_QUIET, }); - traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"}); + this->set_supported_custom_fan_modes({"Auto Low", "Auto High"}); traits.set_supported_swing_modes({ climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL, }); - traits.set_supported_custom_presets({"My Preset"}); + this->set_supported_custom_presets({"My Preset"}); break; case DemoClimateType::TYPE_3: traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | @@ -123,7 +123,7 @@ class DemoClimate : public climate::Climate, public Component { climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_HEAT_COOL, }); - traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"}); + this->set_supported_custom_fan_modes({"Auto Low", "Auto High"}); traits.set_supported_swing_modes({ climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, diff --git a/esphome/components/midea/ac_adapter.cpp b/esphome/components/midea/ac_adapter.cpp index d903db4a1b8..8b20a562c8b 100644 --- a/esphome/components/midea/ac_adapter.cpp +++ b/esphome/components/midea/ac_adapter.cpp @@ -168,8 +168,8 @@ void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea:: traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_BOOST); if (capabilities.supportEcoPreset()) traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO); - if (capabilities.supportFrostProtectionPreset()) - traits.set_supported_custom_presets({Constants::FREEZE_PROTECTION}); + // Frost protection custom preset is handled by AirConditioner directly + // since custom presets are stored on the Climate base class } } // namespace ac diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 4d59a4fbbca..235698128ab 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -91,17 +91,17 @@ ClimateTraits AirConditioner::traits() { traits.set_supported_modes(this->supported_modes_); traits.set_supported_swing_modes(this->supported_swing_modes_); traits.set_supported_presets(this->supported_presets_); - if (!this->supported_custom_presets_.empty()) - traits.set_supported_custom_presets(this->supported_custom_presets_); - if (!this->supported_custom_fan_modes_.empty()) - traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); + // Custom fan modes and presets are stored on Climate base class and wired via get_traits() /* + MINIMAL SET OF CAPABILITIES */ traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH); - if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) + if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) { Converters::to_climate_traits(traits, this->base_.getCapabilities()); + if (this->base_.getCapabilities().supportFrostProtectionPreset()) + this->set_supported_custom_presets({Constants::FREEZE_PROTECTION}); + } if (!traits.get_supported_modes().empty()) traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF); if (!traits.get_supported_swing_modes().empty()) diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index 70833b8bcca..da248346868 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -46,8 +46,8 @@ class AirConditioner : public ApplianceBase, void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } - void set_custom_presets(std::initializer_list presets) { this->supported_custom_presets_ = presets; } - void set_custom_fan_modes(std::initializer_list modes) { this->supported_custom_fan_modes_ = modes; } + void set_custom_presets(std::initializer_list presets) { this->set_supported_custom_presets(presets); } + void set_custom_fan_modes(std::initializer_list modes) { this->set_supported_custom_fan_modes(modes); } protected: void control(const ClimateCall &call) override; @@ -55,8 +55,6 @@ class AirConditioner : public ApplianceBase, ClimateModeMask supported_modes_{}; ClimateSwingModeMask supported_swing_modes_{}; ClimatePresetMask supported_presets_{}; - std::vector supported_custom_presets_{}; - std::vector supported_custom_fan_modes_{}; Sensor *outdoor_sensor_{nullptr}; Sensor *humidity_sensor_{nullptr}; Sensor *power_sensor_{nullptr}; diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index d52a22f880d..453df6859a6 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -332,15 +332,7 @@ climate::ClimateTraits ThermostatClimate::traits() { traits.add_supported_preset(entry.preset); } - // Extract custom preset names from the custom_preset_config_ vector - if (!this->custom_preset_config_.empty()) { - std::vector custom_preset_names; - custom_preset_names.reserve(this->custom_preset_config_.size()); - for (const auto &entry : this->custom_preset_config_) { - custom_preset_names.push_back(entry.name); - } - traits.set_supported_custom_presets(custom_preset_names); - } + // Custom presets are stored on Climate base class and wired via get_traits() return traits; } @@ -1293,6 +1285,13 @@ void ThermostatClimate::set_preset_config(std::initializer_list pre void ThermostatClimate::set_custom_preset_config(std::initializer_list presets) { this->custom_preset_config_ = presets; + // Populate Climate base class custom presets vector + std::vector names; + names.reserve(presets.size()); + for (const auto &entry : this->custom_preset_config_) { + names.push_back(entry.name); + } + this->set_supported_custom_presets(names); } ThermostatClimate::ThermostatClimate() = default;