mirror of
https://github.com/esphome/esphome.git
synced 2026-05-10 05:37:55 +08:00
[climate] Store custom mode vectors on Climate entity to eliminate heap allocation (#15206)
This commit is contained in:
@@ -61,6 +61,15 @@ void BedJetClimate::dump_config() {
|
||||
}
|
||||
|
||||
void BedJetClimate::setup() {
|
||||
// Set custom modes once during setup — stored on Climate base class, wired via get_traits()
|
||||
this->set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
|
||||
this->set_supported_custom_presets({
|
||||
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
|
||||
"M1",
|
||||
"M2",
|
||||
"M3",
|
||||
});
|
||||
|
||||
// restore set points
|
||||
auto restore = this->restore_state_();
|
||||
if (restore.has_value()) {
|
||||
|
||||
@@ -42,21 +42,14 @@ 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,
|
||||
});
|
||||
// String literals are stored in rodata and valid for program lifetime
|
||||
traits.set_supported_custom_presets({
|
||||
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
|
||||
"M1",
|
||||
"M2",
|
||||
"M3",
|
||||
});
|
||||
// Custom fan modes and presets are set once in setup(), stored on Climate base class,
|
||||
// and wired automatically via get_traits()
|
||||
traits.set_visual_min_temperature(19.0);
|
||||
traits.set_visual_max_temperature(43.0);
|
||||
traits.set_visual_temperature_step(1.0);
|
||||
|
||||
@@ -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_)
|
||||
traits.set_supported_custom_fan_modes_(this->supported_custom_fan_modes_);
|
||||
if (this->supported_custom_presets_)
|
||||
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,6 +706,10 @@ 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) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -711,6 +718,10 @@ const char *Climate::find_custom_preset_(const char *custom_preset) {
|
||||
}
|
||||
|
||||
const char *Climate::find_custom_preset_(const char *custom_preset, size_t len) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -234,6 +235,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->ensure_custom_fan_modes_().assign(modes.begin(), modes.end());
|
||||
}
|
||||
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) {
|
||||
this->ensure_custom_fan_modes_() = modes;
|
||||
}
|
||||
template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
|
||||
this->ensure_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->ensure_custom_presets_().assign(presets.begin(), presets.end());
|
||||
}
|
||||
void set_supported_custom_presets(const std::vector<const char *> &presets) {
|
||||
this->ensure_custom_presets_() = presets;
|
||||
}
|
||||
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
|
||||
this->ensure_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 +359,14 @@ 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_;
|
||||
|
||||
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
|
||||
float visual_min_temperature_override_{NAN};
|
||||
float visual_max_temperature_override_{NAN};
|
||||
@@ -353,16 +377,33 @@ class Climate : public EntityBase {
|
||||
#endif
|
||||
|
||||
private:
|
||||
/// Lazy-allocate custom mode vectors (never freed — entity lives forever).
|
||||
std::vector<const char *> &ensure_custom_fan_modes_() {
|
||||
if (!this->supported_custom_fan_modes_) {
|
||||
this->supported_custom_fan_modes_ = new std::vector<const char *>(); // NOLINT
|
||||
}
|
||||
return *this->supported_custom_fan_modes_;
|
||||
}
|
||||
std::vector<const char *> &ensure_custom_presets_() {
|
||||
if (!this->supported_custom_presets_) {
|
||||
this->supported_custom_presets_ = new std::vector<const char *>(); // NOLINT
|
||||
}
|
||||
return *this->supported_custom_presets_;
|
||||
}
|
||||
|
||||
std::vector<const char *> *supported_custom_fan_modes_{nullptr};
|
||||
std::vector<const char *> *supported_custom_presets_{nullptr};
|
||||
|
||||
/** 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};
|
||||
|
||||
@@ -2,6 +2,33 @@
|
||||
|
||||
namespace esphome::climate {
|
||||
|
||||
// Compat: shared empty vector for getters when no custom modes are set.
|
||||
// Remove in 2026.11.0 when deprecated ClimateTraits setters are removed
|
||||
// and getters can return const vector * instead of const vector &.
|
||||
static const std::vector<const char *> EMPTY_CUSTOM_MODES; // NOLINT
|
||||
|
||||
const std::vector<const char *> &ClimateTraits::get_supported_custom_fan_modes() const {
|
||||
if (this->supported_custom_fan_modes_) {
|
||||
return *this->supported_custom_fan_modes_;
|
||||
}
|
||||
// Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0.
|
||||
if (!this->compat_custom_fan_modes_.empty()) {
|
||||
return this->compat_custom_fan_modes_;
|
||||
}
|
||||
return EMPTY_CUSTOM_MODES;
|
||||
}
|
||||
|
||||
const std::vector<const char *> &ClimateTraits::get_supported_custom_presets() const {
|
||||
if (this->supported_custom_presets_) {
|
||||
return *this->supported_custom_presets_;
|
||||
}
|
||||
// Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0.
|
||||
if (!this->compat_custom_presets_.empty()) {
|
||||
return this->compat_custom_presets_;
|
||||
}
|
||||
return EMPTY_CUSTOM_MODES;
|
||||
}
|
||||
|
||||
int8_t ClimateTraits::get_target_temperature_accuracy_decimals() const {
|
||||
return step_to_accuracy_decimals(this->visual_target_temperature_step_);
|
||||
}
|
||||
|
||||
@@ -147,27 +147,45 @@ 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();
|
||||
if (!this->supported_fan_modes_.empty()) {
|
||||
return true;
|
||||
}
|
||||
// Same precedence as get_supported_custom_fan_modes() getter
|
||||
if (this->supported_custom_fan_modes_) {
|
||||
return !this->supported_custom_fan_modes_->empty();
|
||||
}
|
||||
return !this->compat_custom_fan_modes_.empty(); // Compat: remove in 2026.11.0
|
||||
}
|
||||
const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; }
|
||||
|
||||
// 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<const char *> modes) {
|
||||
this->supported_custom_fan_modes_ = modes;
|
||||
// Compat: store in owned vector. Copies copy the vector (deprecated path still copies this vector).
|
||||
this->compat_custom_fan_modes_ = modes;
|
||||
}
|
||||
// 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<const char *> &modes) {
|
||||
this->supported_custom_fan_modes_ = modes;
|
||||
this->compat_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);
|
||||
// Remove before 2026.11.0
|
||||
template<size_t N>
|
||||
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 char *const (&modes)[N]) {
|
||||
this->compat_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_; }
|
||||
// Compat: returns const ref with empty fallback. In 2026.11.0 change to return const vector *.
|
||||
const std::vector<const char *> &get_supported_custom_fan_modes() const;
|
||||
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)) ||
|
||||
vector_contains(this->compat_custom_fan_modes_, custom_fan_mode); // Compat: remove in 2026.11.0
|
||||
}
|
||||
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 +197,32 @@ class ClimateTraits {
|
||||
bool get_supports_presets() const { return !this->supported_presets_.empty(); }
|
||||
const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; }
|
||||
|
||||
// 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<const char *> presets) {
|
||||
this->supported_custom_presets_ = presets;
|
||||
this->compat_custom_presets_ = presets;
|
||||
}
|
||||
// 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<const char *> &presets) {
|
||||
this->supported_custom_presets_ = presets;
|
||||
this->compat_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);
|
||||
// Remove before 2026.11.0
|
||||
template<size_t N>
|
||||
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 char *const (&presets)[N]) {
|
||||
this->compat_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_; }
|
||||
// Compat: returns const ref with empty fallback. In 2026.11.0 change to return const vector *.
|
||||
const std::vector<const char *> &get_supported_custom_presets() const;
|
||||
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)) ||
|
||||
vector_contains(this->compat_custom_presets_, custom_preset); // Compat: remove in 2026.11.0
|
||||
}
|
||||
bool supports_custom_preset(const std::string &custom_preset) const {
|
||||
return this->supports_custom_preset(custom_preset.c_str());
|
||||
@@ -258,13 +285,25 @@ class ClimateTraits {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set custom mode pointers (only Climate::get_traits() should call these).
|
||||
void set_supported_custom_fan_modes_(const std::vector<const char *> *modes) {
|
||||
this->supported_custom_fan_modes_ = modes;
|
||||
}
|
||||
void set_supported_custom_presets_(const std::vector<const char *> *presets) {
|
||||
this->supported_custom_presets_ = presets;
|
||||
}
|
||||
|
||||
/// 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::find_custom_fan_mode_() instead
|
||||
const char *find_custom_fan_mode_(const char *custom_fan_mode) const {
|
||||
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);
|
||||
if (this->supported_custom_fan_modes_) {
|
||||
return vector_find(*this->supported_custom_fan_modes_, custom_fan_mode, len);
|
||||
}
|
||||
// Compat: check owned vector from deprecated setters. Remove in 2026.11.0.
|
||||
return vector_find(this->compat_custom_fan_modes_, custom_fan_mode, len);
|
||||
}
|
||||
|
||||
/// Find and return the matching custom preset pointer from supported presets, or nullptr if not found
|
||||
@@ -273,7 +312,11 @@ 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);
|
||||
if (this->supported_custom_presets_) {
|
||||
return vector_find(*this->supported_custom_presets_, custom_preset, len);
|
||||
}
|
||||
// Compat: check owned vector from deprecated setters. Remove in 2026.11.0.
|
||||
return vector_find(this->compat_custom_presets_, custom_preset, len);
|
||||
}
|
||||
|
||||
uint32_t feature_flags_{0};
|
||||
@@ -289,16 +332,17 @@ 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};
|
||||
// Compat: owned storage for deprecated setters. Copies copy the vector (copies include this vector).
|
||||
// Remove in 2026.11.0.
|
||||
std::vector<const char *> compat_custom_fan_modes_;
|
||||
std::vector<const char *> compat_custom_presets_;
|
||||
};
|
||||
|
||||
} // namespace esphome::climate
|
||||
|
||||
@@ -16,6 +16,19 @@ class DemoClimate : public climate::Climate, public Component {
|
||||
public:
|
||||
void set_type(DemoClimateType type) { type_ = type; }
|
||||
void setup() override {
|
||||
// Set custom modes once during setup — stored on Climate base class, wired via get_traits()
|
||||
switch (type_) {
|
||||
case DemoClimateType::TYPE_1:
|
||||
break;
|
||||
case DemoClimateType::TYPE_2:
|
||||
this->set_supported_custom_fan_modes({"Auto Low", "Auto High"});
|
||||
this->set_supported_custom_presets({"My Preset"});
|
||||
break;
|
||||
case DemoClimateType::TYPE_3:
|
||||
this->set_supported_custom_fan_modes({"Auto Low", "Auto High"});
|
||||
break;
|
||||
}
|
||||
// Set initial state
|
||||
switch (type_) {
|
||||
case DemoClimateType::TYPE_1:
|
||||
this->current_temperature = 20.0;
|
||||
@@ -105,14 +118,13 @@ 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"});
|
||||
// Custom fan modes and presets are set once in setup()
|
||||
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"});
|
||||
break;
|
||||
case DemoClimateType::TYPE_3:
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
|
||||
@@ -123,7 +135,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"});
|
||||
// Custom fan modes are set once in setup()
|
||||
traits.set_supported_swing_modes({
|
||||
climate::CLIMATE_SWING_OFF,
|
||||
climate::CLIMATE_SWING_HORIZONTAL,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,6 +24,25 @@ template<typename T> void update_property(T &property, const T &value, bool &fla
|
||||
}
|
||||
|
||||
void AirConditioner::on_status_change() {
|
||||
// Add frost protection custom preset once when autoconf completes
|
||||
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK &&
|
||||
this->base_.getCapabilities().supportFrostProtectionPreset() && !this->frost_protection_set_) {
|
||||
// Read existing presets (set by codegen), append frost protection, write back
|
||||
const auto &existing = this->get_traits().get_supported_custom_presets();
|
||||
bool found = false;
|
||||
for (const char *p : existing) {
|
||||
if (strcmp(p, Constants::FREEZE_PROTECTION) == 0) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
std::vector<const char *> merged(existing.begin(), existing.end());
|
||||
merged.push_back(Constants::FREEZE_PROTECTION);
|
||||
this->set_supported_custom_presets(merged);
|
||||
}
|
||||
this->frost_protection_set_ = true;
|
||||
}
|
||||
bool need_publish = false;
|
||||
update_property(this->target_temperature, this->base_.getTargetTemp(), need_publish);
|
||||
update_property(this->current_temperature, this->base_.getIndoorTemp(), need_publish);
|
||||
@@ -91,17 +110,15 @@ 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 (!traits.get_supported_modes().empty())
|
||||
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF);
|
||||
if (!traits.get_supported_swing_modes().empty())
|
||||
|
||||
@@ -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,7 @@ 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_{};
|
||||
bool frost_protection_set_{false};
|
||||
Sensor *outdoor_sensor_{nullptr};
|
||||
Sensor *humidity_sensor_{nullptr};
|
||||
Sensor *power_sensor_{nullptr};
|
||||
|
||||
@@ -333,15 +333,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;
|
||||
}
|
||||
@@ -1306,6 +1298,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;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""Legacy climate component — tests deprecated ClimateTraits setters backward compat.
|
||||
|
||||
Remove this entire directory in 2026.11.0 when the deprecated setters are removed.
|
||||
"""
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
"""Legacy climate platform that uses deprecated ClimateTraits setters."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
legacy_climate_ns = cg.esphome_ns.namespace("legacy_climate_test")
|
||||
LegacyClimate = legacy_climate_ns.class_("LegacyClimate", climate.Climate, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = climate.climate_schema(LegacyClimate).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await climate.new_climate(config)
|
||||
await cg.register_component(var, config)
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::legacy_climate_test {
|
||||
|
||||
/// Test climate that uses the DEPRECATED ClimateTraits setters for custom modes.
|
||||
/// This validates backward compatibility for external components that haven't migrated.
|
||||
class LegacyClimate : public climate::Climate, public Component {
|
||||
public:
|
||||
void setup() override {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
this->target_temperature = 22.0f;
|
||||
this->current_temperature = 20.0f;
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
protected:
|
||||
climate::ClimateTraits traits() override {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_COOL});
|
||||
traits.set_visual_min_temperature(16.0f);
|
||||
traits.set_visual_max_temperature(30.0f);
|
||||
traits.set_visual_temperature_step(0.5f);
|
||||
|
||||
// DEPRECATED API: setting custom modes directly on ClimateTraits.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
traits.set_supported_custom_fan_modes({"Turbo", "Silent", "Auto"});
|
||||
traits.set_supported_custom_presets({"Eco Mode", "Night Mode"});
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
return traits;
|
||||
}
|
||||
|
||||
void control(const climate::ClimateCall &call) override {
|
||||
if (call.get_mode().has_value()) {
|
||||
this->mode = *call.get_mode();
|
||||
}
|
||||
if (call.get_target_temperature().has_value()) {
|
||||
this->target_temperature = *call.get_target_temperature();
|
||||
}
|
||||
if (call.has_custom_fan_mode()) {
|
||||
this->set_custom_fan_mode_(call.get_custom_fan_mode());
|
||||
}
|
||||
if (call.has_custom_preset()) {
|
||||
this->set_custom_preset_(call.get_custom_preset());
|
||||
}
|
||||
this->publish_state();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::legacy_climate_test
|
||||
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
name: legacy-climate-compat
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
components: [legacy_climate_component]
|
||||
|
||||
climate:
|
||||
- platform: legacy_climate_component
|
||||
name: "Legacy Climate"
|
||||
id: legacy_climate
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Integration test for backward compatibility of deprecated ClimateTraits setters.
|
||||
|
||||
Verifies that external components using the old traits.set_supported_custom_fan_modes()
|
||||
and traits.set_supported_custom_presets() API still work correctly during the
|
||||
deprecation period.
|
||||
|
||||
Remove this entire test file and the legacy_climate_component external component
|
||||
in 2026.11.0 when the deprecated ClimateTraits setters are removed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import aioesphomeapi
|
||||
from aioesphomeapi import ClimateInfo
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_legacy_climate_compat(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that deprecated ClimateTraits custom mode setters still work end-to-end."""
|
||||
external_components_path = str(
|
||||
Path(__file__).parent / "fixtures" / "external_components"
|
||||
)
|
||||
yaml_config = yaml_config.replace(
|
||||
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||
)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
entities, _ = await client.list_entities_services()
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
|
||||
climate_infos = [e for e in entities if isinstance(e, ClimateInfo)]
|
||||
assert len(climate_infos) == 1, (
|
||||
f"Expected 1 climate entity, got {len(climate_infos)}"
|
||||
)
|
||||
|
||||
test_climate = climate_infos[0]
|
||||
|
||||
# Verify custom fan modes set via deprecated ClimateTraits setter are exposed
|
||||
assert set(test_climate.supported_custom_fan_modes) == {
|
||||
"Turbo",
|
||||
"Silent",
|
||||
"Auto",
|
||||
}, (
|
||||
f"Expected custom fan modes {{Turbo, Silent, Auto}}, "
|
||||
f"got {test_climate.supported_custom_fan_modes}"
|
||||
)
|
||||
|
||||
# Verify custom presets set via deprecated ClimateTraits setter are exposed
|
||||
assert set(test_climate.supported_custom_presets) == {
|
||||
"Eco Mode",
|
||||
"Night Mode",
|
||||
}, (
|
||||
f"Expected custom presets {{Eco Mode, Night Mode}}, "
|
||||
f"got {test_climate.supported_custom_presets}"
|
||||
)
|
||||
|
||||
# Set up state tracking with InitialStateHelper
|
||||
turbo_future: asyncio.Future[aioesphomeapi.ClimateState] = loop.create_future()
|
||||
|
||||
def on_state(state: aioesphomeapi.EntityState) -> None:
|
||||
if (
|
||||
isinstance(state, aioesphomeapi.ClimateState)
|
||||
and state.custom_fan_mode == "Turbo"
|
||||
and not turbo_future.done()
|
||||
):
|
||||
turbo_future.set_result(state)
|
||||
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
# Verify we can set a custom fan mode via API (tests find_custom_fan_mode_ compat path)
|
||||
client.climate_command(test_climate.key, custom_fan_mode="Turbo")
|
||||
|
||||
try:
|
||||
turbo_state = await asyncio.wait_for(turbo_future, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Custom fan mode 'Turbo' not received within 5 seconds")
|
||||
|
||||
assert turbo_state.custom_fan_mode == "Turbo"
|
||||
Reference in New Issue
Block a user