[climate] Store custom mode vectors on Climate entity, not ClimateTraits

ClimateTraits contained two std::vector<const char *> members for custom
fan modes and custom presets. Every get_traits() / traits() call
reconstructed these vectors, causing heap allocations on every
publish_state() and control()/perform() call — the hottest paths in the
climate component.

Move the vector storage to the Climate base class and have ClimateTraits
hold const pointers instead. get_traits() wires the pointers
automatically. This eliminates all heap allocation from ClimateTraits
copies, making the struct trivially copyable (floats + bitmasks +
2 pointers).

Additionally, set_custom_fan_mode_() and set_custom_preset_() no longer
need to call get_traits() just to search for a mode — they search the
Climate-owned vectors directly, removing another traits rebuild from the
control/perform path.
This commit is contained in:
J. Nick Koston
2026-03-26 12:38:25 -10:00
parent 6aafb521c1
commit 7ae1f2f6d7
9 changed files with 89 additions and 73 deletions
@@ -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",
+10 -7
View File
@@ -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<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode,
traits.find_custom_fan_mode_(mode, len), this->has_custom_fan_mode());
return set_custom_mode<ClimateFanMode>(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<ClimatePreset>(this->custom_preset_, this->preset, traits.find_custom_preset_(preset, len),
return set_custom_mode<ClimatePreset>(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) {
+32 -3
View File
@@ -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<const char *> modes) {
this->supported_custom_fan_modes_ = modes;
}
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) {
this->supported_custom_fan_modes_ = modes;
}
template<size_t N> 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<const char *> presets) {
this->supported_custom_presets_ = presets;
}
void set_supported_custom_presets(const std::vector<const char *> &presets) {
this->supported_custom_presets_ = presets;
}
template<size_t N> 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<void(Climate &)> state_callback_{};
LazyCallbackManager<void(ClimateCall &)> 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<const char *> supported_custom_fan_modes_;
std::vector<const char *> 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};
+23 -37
View File
@@ -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<const char *> modes) {
void set_supported_custom_fan_modes(const std::vector<const char *> *modes) {
this->supported_custom_fan_modes_ = modes;
}
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) {
this->supported_custom_fan_modes_ = modes;
}
template<size_t N> 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<std::string> &modes) = delete;
void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) = delete;
const std::vector<const char *> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
const std::vector<const char *> &get_supported_custom_fan_modes() const {
static const std::vector<const char *> 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<const char *> presets) {
void set_supported_custom_presets(const std::vector<const char *> *presets) {
this->supported_custom_presets_ = presets;
}
void set_supported_custom_presets(const std::vector<const char *> &presets) {
this->supported_custom_presets_ = presets;
}
template<size_t N> 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<std::string> &presets) = delete;
void set_supported_custom_presets(std::initializer_list<std::string> presets) = delete;
const std::vector<const char *> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
const std::vector<const char *> &get_supported_custom_presets() const {
static const std::vector<const char *> 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<const char *> supported_custom_fan_modes_;
std::vector<const char *> supported_custom_presets_;
const std::vector<const char *> *supported_custom_fan_modes_{nullptr};
const std::vector<const char *> *supported_custom_presets_{nullptr};
};
} // namespace esphome::climate
+3 -3
View File
@@ -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,
+2 -2
View File
@@ -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
+5 -5
View File
@@ -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())
+2 -4
View File
@@ -46,8 +46,8 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
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<const char *> presets) { this->supported_custom_presets_ = presets; }
void set_custom_fan_modes(std::initializer_list<const char *> modes) { this->supported_custom_fan_modes_ = modes; }
void set_custom_presets(std::initializer_list<const char *> presets) { this->set_supported_custom_presets(presets); }
void set_custom_fan_modes(std::initializer_list<const char *> modes) { this->set_supported_custom_fan_modes(modes); }
protected:
void control(const ClimateCall &call) override;
@@ -55,8 +55,6 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
ClimateModeMask supported_modes_{};
ClimateSwingModeMask supported_swing_modes_{};
ClimatePresetMask supported_presets_{};
std::vector<const char *> supported_custom_presets_{};
std::vector<const char *> supported_custom_fan_modes_{};
Sensor *outdoor_sensor_{nullptr};
Sensor *humidity_sensor_{nullptr};
Sensor *power_sensor_{nullptr};
@@ -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<const char *> 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<PresetEntry> pre
void ThermostatClimate::set_custom_preset_config(std::initializer_list<CustomPresetEntry> presets) {
this->custom_preset_config_ = presets;
// Populate Climate base class custom presets vector
std::vector<const char *> 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;