1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-16 14:55:50 +00:00

[climate] Replace std::vector<std::string> with const char* for custom fan modes and presets (#11621)

This commit is contained in:
J. Nick Koston
2025-11-02 23:16:39 -06:00
committed by GitHub
parent a41c7b2b3c
commit 42833c85f5
21 changed files with 475 additions and 230 deletions

View File

@@ -1000,9 +1000,9 @@ message ListEntitiesClimateResponse {
bool supports_action = 12; // Deprecated: use feature_flags
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"];
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"];
repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::vector"];
repeated string supported_custom_fan_modes = 15 [(container_pointer_no_template) = "std::vector<const char *>"];
repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"];
repeated string supported_custom_presets = 17 [(container_pointer) = "std::vector"];
repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector<const char *>"];
bool disabled_by_default = 18;
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 20;

View File

@@ -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<enums::ClimateFanMode>(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->has_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<enums::ClimatePreset>(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->has_custom_preset()) {
resp.set_custom_preset(StringRef(climate->get_custom_preset()));
}
if (traits.get_supports_swing_modes())
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);

View File

@@ -1179,14 +1179,14 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
for (const auto &it : *this->supported_swing_modes) {
buffer.encode_uint32(14, static_cast<uint32_t>(it), true);
}
for (const auto &it : *this->supported_custom_fan_modes) {
buffer.encode_string(15, it, true);
for (const char *it : *this->supported_custom_fan_modes) {
buffer.encode_string(15, it, strlen(it), true);
}
for (const auto &it : *this->supported_presets) {
buffer.encode_uint32(16, static_cast<uint32_t>(it), true);
}
for (const auto &it : *this->supported_custom_presets) {
buffer.encode_string(17, it, true);
for (const char *it : *this->supported_custom_presets) {
buffer.encode_string(17, it, strlen(it), true);
}
buffer.encode_bool(18, this->disabled_by_default);
#ifdef USE_ENTITY_ICON
@@ -1229,8 +1229,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
}
}
if (!this->supported_custom_fan_modes->empty()) {
for (const auto &it : *this->supported_custom_fan_modes) {
size.add_length_force(1, it.size());
for (const char *it : *this->supported_custom_fan_modes) {
size.add_length_force(1, strlen(it));
}
}
if (!this->supported_presets->empty()) {
@@ -1239,8 +1239,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
}
}
if (!this->supported_custom_presets->empty()) {
for (const auto &it : *this->supported_custom_presets) {
size.add_length_force(2, it.size());
for (const char *it : *this->supported_custom_presets) {
size.add_length_force(2, strlen(it));
}
}
size.add_bool(2, this->disabled_by_default);

View File

@@ -1384,9 +1384,9 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
bool supports_action{false};
const climate::ClimateFanModeMask *supported_fan_modes{};
const climate::ClimateSwingModeMask *supported_swing_modes{};
const std::vector<std::string> *supported_custom_fan_modes{};
const std::vector<const char *> *supported_custom_fan_modes{};
const climate::ClimatePresetMask *supported_presets{};
const std::vector<std::string> *supported_custom_presets{};
const std::vector<const char *> *supported_custom_presets{};
float visual_current_temperature_step{0.0f};
bool supports_current_humidity{false};
bool supports_target_humidity{false};

View File

@@ -100,7 +100,6 @@ enum BedjetCommand : uint8_t {
static const uint8_t BEDJET_FAN_SPEED_COUNT = 20;
static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
} // namespace bedjet
} // namespace esphome

View File

@@ -8,15 +8,15 @@ namespace bedjet {
using namespace esphome::climate;
static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
static const char *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
if (fan_step < BEDJET_FAN_SPEED_COUNT)
return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step];
return BEDJET_FAN_STEP_NAMES[fan_step];
return nullptr;
}
static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) {
static uint8_t bedjet_fan_speed_to_step(const char *fan_step_percent) {
for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) {
if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) {
if (strcmp(BEDJET_FAN_STEP_NAMES[i], fan_step_percent) == 0) {
return i;
}
}
@@ -48,7 +48,7 @@ void BedJetClimate::dump_config() {
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
}
for (const auto &mode : traits.get_supported_custom_fan_modes()) {
ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str());
ESP_LOGCONFIG(TAG, " - %s (c)", mode);
}
ESP_LOGCONFIG(TAG, " Supported presets:");
@@ -56,7 +56,7 @@ void BedJetClimate::dump_config() {
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
}
for (const auto &preset : traits.get_supported_custom_presets()) {
ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str());
ESP_LOGCONFIG(TAG, " - %s (c)", preset);
}
}
@@ -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();
}
@@ -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();
}
}
@@ -144,8 +144,7 @@ void BedJetClimate::control(const ClimateCall &call) {
if (result) {
this->mode = CLIMATE_MODE_HEAT;
this->preset = CLIMATE_PRESET_BOOST;
this->custom_preset.reset();
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) {
@@ -153,7 +152,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'",
@@ -164,28 +163,27 @@ void BedJetClimate::control(const ClimateCall &call) {
ESP_LOGW(TAG, "Unsupported preset: %d", preset);
return;
}
} else if (call.get_custom_preset().has_value()) {
std::string preset = *call.get_custom_preset();
} else if (call.has_custom_preset()) {
const char *preset = call.get_custom_preset();
bool result;
if (preset == "M1") {
if (strcmp(preset, "M1") == 0) {
result = this->parent_->button_memory1();
} else if (preset == "M2") {
} else if (strcmp(preset, "M2") == 0) {
result = this->parent_->button_memory2();
} else if (preset == "M3") {
} else if (strcmp(preset, "M3") == 0) {
result = this->parent_->button_memory3();
} else if (preset == "LTD HT") {
} else if (strcmp(preset, "LTD HT") == 0) {
result = this->parent_->button_heat();
} else if (preset == "EXT HT") {
} else if (strcmp(preset, "EXT HT") == 0) {
result = this->parent_->button_ext_heat();
} else {
ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str());
ESP_LOGW(TAG, "Unsupported preset: %s", preset);
return;
}
if (result) {
this->custom_preset = preset;
this->preset.reset();
this->set_custom_preset_(preset);
}
}
@@ -207,19 +205,16 @@ 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();
} else if (call.has_custom_fan_mode()) {
const char *fan_mode = call.get_custom_fan_mode();
auto fan_index = bedjet_fan_speed_to_step(fan_mode);
if (fan_index <= 19) {
ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(),
fan_index);
ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode, 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);
}
}
}
@@ -245,7 +240,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 +250,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 +261,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 +270,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 +279,19 @@ 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->set_preset_(CLIMATE_PRESET_BOOST);
this->mode = CLIMATE_MODE_HEAT;
this->action = CLIMATE_ACTION_HEATING;
break;

View File

@@ -50,21 +50,13 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
// 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({
// We could fetch biodata from bedjet and set these names that way.
// But then we have to invert the lookup in order to send the right preset.
// For now, we can leave them as M1-3 to match the remote buttons.
// EXT HT added to match remote button.
"EXT HT",
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
"M1",
"M2",
"M3",
});
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
traits.add_supported_custom_preset("LTD HT");
} else {
traits.add_supported_custom_preset("EXT HT");
}
traits.set_visual_min_temperature(19.0);
traits.set_visual_max_temperature(43.0);
traits.set_visual_temperature_step(1.0);

View File

@@ -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,29 @@ 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)) {
this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
return *this;
if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) {
return this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
}
}
if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
this->custom_fan_mode_ = 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();
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
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<std::string> fan_mode) {
if (fan_mode.has_value()) {
this->set_fan_mode(fan_mode.value());
@@ -215,26 +216,29 @@ ClimateCall &ClimateCall::set_fan_mode(optional<std::string> 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)) {
this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
return *this;
if (str_equals_case_insensitive(custom_preset, preset_entry.str)) {
return this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
}
}
if (this->parent_->get_traits().supports_custom_preset(preset)) {
this->custom_preset_ = 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();
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
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<std::string> preset) {
if (preset.has_value()) {
this->set_preset(preset.value());
@@ -287,8 +291,6 @@ const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_;
const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
this->target_temperature_high_ = target_temperature_high;
@@ -317,13 +319,13 @@ ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
this->fan_mode_ = fan_mode;
this->custom_fan_mode_.reset();
this->custom_fan_mode_ = nullptr;
return *this;
}
ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
this->preset_ = preset;
this->custom_preset_.reset();
this->custom_preset_ = nullptr;
return *this;
}
@@ -382,13 +384,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() && 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
size_t i = 0;
for (const auto &mode : supported) {
if (mode == custom_fan_mode) {
for (const char *mode : supported) {
if (strcmp(mode, this->custom_fan_mode_) == 0) {
state.custom_fan_mode = i;
break;
}
@@ -399,13 +401,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() && this->has_custom_preset()) {
state.uses_custom_preset = true;
const auto &supported = traits.get_supported_custom_presets();
// std::vector maintains insertion order
size_t i = 0;
for (const auto &preset : supported) {
if (preset == custom_preset) {
for (const char *preset : supported) {
if (strcmp(preset, this->custom_preset_) == 0) {
state.custom_preset = i;
break;
}
@@ -430,14 +432,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->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.has_value()) {
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset.value().c_str());
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()) {
ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode)));
@@ -527,7 +529,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::next(traits.get_supported_custom_fan_modes().cbegin(), 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 +537,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::next(traits.get_supported_custom_presets().cbegin(), 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 +564,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::next(traits.get_supported_custom_fan_modes().cbegin(), 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->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 = *std::next(traits.get_supported_custom_presets().cbegin(), 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->clear_custom_preset_();
}
if (traits.supports_swing_mode(this->swing_mode)) {
climate->swing_mode = this->swing_mode;
@@ -583,28 +585,107 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
climate->publish_state();
}
template<typename T1, typename T2> bool set_alternative(optional<T1> &dst, optional<T2> &alt, const T1 &src) {
bool is_changed = alt.has_value();
alt.reset();
if (is_changed || dst != src) {
dst = src;
is_changed = true;
/** 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<typename T> bool set_primary_mode(optional<T> &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 is_changed;
return false;
}
/** 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<typename T>
bool set_custom_mode(const char *&custom_ptr, optional<T> &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 (pointer is validated by caller from traits)
if (changed || custom_ptr != found_ptr) {
custom_ptr = found_ptr;
return true;
}
return false;
}
// 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_alternative(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 std::string &mode) {
return set_alternative(this->custom_fan_mode, this->fan_mode, mode);
bool Climate::set_custom_fan_mode_(const char *mode) {
auto traits = this->get_traits();
return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode),
this->has_custom_fan_mode());
}
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_custom_preset_(const std::string &preset) {
return set_alternative(this->custom_preset, this->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<ClimatePreset>(this->custom_preset_, this->preset, traits.find_custom_preset_(preset),
this->has_custom_preset());
}
void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; }
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) {
return this->get_traits().find_custom_fan_mode_(custom_fan_mode);
}
const char *Climate::find_custom_preset_(const char *custom_preset) {
return this->get_traits().find_custom_preset_(custom_preset);
}
void Climate::dump_traits_(const char *tag) {
@@ -656,8 +737,8 @@ void Climate::dump_traits_(const char *tag) {
}
if (!traits.get_supported_custom_fan_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported custom fan modes:");
for (const std::string &s : traits.get_supported_custom_fan_modes())
ESP_LOGCONFIG(tag, " - %s", s.c_str());
for (const char *s : traits.get_supported_custom_fan_modes())
ESP_LOGCONFIG(tag, " - %s", s);
}
if (!traits.get_supported_presets().empty()) {
ESP_LOGCONFIG(tag, " Supported presets:");
@@ -666,8 +747,8 @@ void Climate::dump_traits_(const char *tag) {
}
if (!traits.get_supported_custom_presets().empty()) {
ESP_LOGCONFIG(tag, " Supported custom presets:");
for (const std::string &s : traits.get_supported_custom_presets())
ESP_LOGCONFIG(tag, " - %s", s.c_str());
for (const char *s : traits.get_supported_custom_presets())
ESP_LOGCONFIG(tag, " - %s", s);
}
if (!traits.get_supported_swing_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported swing modes:");

View File

@@ -77,6 +77,8 @@ class ClimateCall {
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<std::string> 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.
@@ -91,6 +93,8 @@ class ClimateCall {
ClimateCall &set_preset(const std::string &preset);
/// Set the preset of the climate device based on a string.
ClimateCall &set_preset(optional<std::string> preset);
/// Set the custom preset of the climate device.
ClimateCall &set_preset(const char *custom_preset);
void perform();
@@ -103,8 +107,10 @@ class ClimateCall {
const optional<ClimateFanMode> &get_fan_mode() const;
const optional<ClimateSwingMode> &get_swing_mode() const;
const optional<ClimatePreset> &get_preset() const;
const optional<std::string> &get_custom_fan_mode() const;
const optional<std::string> &get_custom_preset() const;
const char *get_custom_fan_mode() const { return this->custom_fan_mode_; }
const char *get_custom_preset() const { return this->custom_preset_; }
bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
bool has_custom_preset() const { return this->custom_preset_ != nullptr; }
protected:
void validate_();
@@ -118,8 +124,10 @@ class ClimateCall {
optional<ClimateFanMode> fan_mode_;
optional<ClimateSwingMode> swing_mode_;
optional<ClimatePreset> preset_;
optional<std::string> custom_fan_mode_;
optional<std::string> custom_preset_;
private:
const char *custom_fan_mode_{nullptr};
const char *custom_preset_{nullptr};
};
/// Struct used to save the state of the climate device in restore memory.
@@ -212,6 +220,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};
@@ -238,12 +252,6 @@ class Climate : public EntityBase {
/// The active preset of the climate device.
optional<ClimatePreset> preset;
/// The active custom fan mode of the climate device.
optional<std::string> custom_fan_mode;
/// The active custom preset mode of the climate device.
optional<std::string> custom_preset;
/// The active mode of the climate device.
ClimateMode mode{CLIMATE_MODE_OFF};
@@ -253,20 +261,37 @@ 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;
friend struct ClimateDeviceRestoreState;
/// 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 std::string &mode);
bool set_custom_fan_mode_(const char *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);
/// Set custom preset. Reset primary preset. Return true if preset has been changed.
bool set_custom_preset_(const std::string &preset);
bool set_custom_preset_(const char *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);
/// 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.
*
@@ -303,6 +328,21 @@ class Climate : public EntityBase {
optional<float> visual_current_temperature_step_override_{};
optional<float> visual_min_humidity_override_{};
optional<float> visual_max_humidity_override_{};
private:
/** The active custom fan mode (private - enforces use of safe setters).
*
* 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 (private - enforces use of safe setters).
*
* 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};
};
} // namespace climate

View File

@@ -1,5 +1,6 @@
#pragma once
#include <cstring>
#include <vector>
#include "climate_mode.h"
#include "esphome/core/finite_set_mask.h"
@@ -18,16 +19,25 @@ using ClimateSwingModeMask =
FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>;
using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 1>>;
// Lightweight linear search for small vectors (1-20 items)
// Lightweight linear search for small vectors (1-20 items) of const char* pointers
// Avoids std::find template overhead
template<typename T> inline bool vector_contains(const std::vector<T> &vec, const T &value) {
for (const auto &item : vec) {
if (item == value)
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
return true;
}
return false;
}
// Find and return matching pointer from vector, or nullptr if not found
inline const char *vector_find(const std::vector<const char *> &vec, const char *value) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
return item;
}
return nullptr;
}
/** This class contains all static data for climate devices.
*
* All climate devices must support these features:
@@ -55,7 +65,11 @@ template<typename T> inline bool vector_contains(const std::vector<T> &vec, cons
* - temperature step - the step with which to increase/decrease target temperature.
* This also affects with how many decimal places the temperature is shown
*/
class Climate; // Forward declaration
class ClimateTraits {
friend class Climate; // Allow Climate to access protected find methods
public:
/// Get/set feature flags (see ClimateFeatures enum in climate_mode.h)
uint32_t get_feature_flags() const { return this->feature_flags_; }
@@ -128,47 +142,61 @@ class ClimateTraits {
void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; }
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(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();
}
const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; }
void set_supported_custom_fan_modes(std::vector<std::string> supported_custom_fan_modes) {
this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes);
void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) {
this->supported_custom_fan_modes_ = modes;
}
void set_supported_custom_fan_modes(std::initializer_list<std::string> 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);
}
const std::vector<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
// 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_; }
bool supports_custom_fan_mode(const char *custom_fan_mode) const {
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());
}
void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; }
void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); }
void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); }
bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); }
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::vector<std::string> supported_custom_presets) {
this->supported_custom_presets_ = std::move(supported_custom_presets);
void set_supported_custom_presets(std::initializer_list<const char *> presets) {
this->supported_custom_presets_ = presets;
}
void set_supported_custom_presets(std::initializer_list<std::string> 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);
}
const std::vector<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
bool supports_custom_preset(const std::string &custom_preset) const {
// 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_; }
bool supports_custom_preset(const char *custom_preset) const {
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());
}
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); }
@@ -227,6 +255,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::find_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::find_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};
@@ -239,8 +279,17 @@ class ClimateTraits {
climate::ClimateFanModeMask supported_fan_modes_;
climate::ClimateSwingModeMask supported_swing_modes_;
climate::ClimatePresetMask supported_presets_;
std::vector<std::string> supported_custom_fan_modes_;
std::vector<std::string> supported_custom_presets_;
/** Custom mode storage using const char* pointers to eliminate std::string overhead.
*
* 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.
*/
std::vector<const char *> supported_custom_fan_modes_;
std::vector<const char *> supported_custom_presets_;
};
} // namespace climate

View File

@@ -28,16 +28,16 @@ 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;
this->set_preset_(climate::CLIMATE_PRESET_AWAY);
break;
}
this->publish_state();
@@ -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();
if (call.has_custom_fan_mode()) {
this->set_custom_fan_mode_(call.get_custom_fan_mode());
}
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();
if (call.has_custom_preset()) {
this->set_custom_preset_(call.get_custom_preset());
}
this->publish_state();
}

View File

@@ -8,9 +8,9 @@ namespace midea {
namespace ac {
const char *const Constants::TAG = "midea";
const std::string Constants::FREEZE_PROTECTION = "freeze protection";
const std::string Constants::SILENT = "silent";
const std::string Constants::TURBO = "turbo";
const char *const Constants::FREEZE_PROTECTION = "freeze protection";
const char *const Constants::SILENT = "silent";
const char *const Constants::TURBO = "turbo";
ClimateMode Converters::to_climate_mode(MideaMode mode) {
switch (mode) {
@@ -108,7 +108,7 @@ bool Converters::is_custom_midea_fan_mode(MideaFanMode mode) {
}
}
const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) {
const char *Converters::to_custom_climate_fan_mode(MideaFanMode mode) {
switch (mode) {
case MideaFanMode::FAN_SILENT:
return Constants::SILENT;
@@ -117,8 +117,8 @@ const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) {
}
}
MideaFanMode Converters::to_midea_fan_mode(const std::string &mode) {
if (mode == Constants::SILENT)
MideaFanMode Converters::to_midea_fan_mode(const char *mode) {
if (strcmp(mode, Constants::SILENT) == 0)
return MideaFanMode::FAN_SILENT;
return MideaFanMode::FAN_TURBO;
}
@@ -151,9 +151,9 @@ ClimatePreset Converters::to_climate_preset(MideaPreset preset) {
bool Converters::is_custom_midea_preset(MideaPreset preset) { return preset == MideaPreset::PRESET_FREEZE_PROTECTION; }
const std::string &Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; }
const char *Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; }
MideaPreset Converters::to_midea_preset(const std::string &preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; }
MideaPreset Converters::to_midea_preset(const char *preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; }
void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities) {
if (capabilities.supportAutoMode())
@@ -169,7 +169,7 @@ void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::
if (capabilities.supportEcoPreset())
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO);
if (capabilities.supportFrostProtectionPreset())
traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION);
traits.set_supported_custom_presets({Constants::FREEZE_PROTECTION});
}
} // namespace ac

View File

@@ -20,9 +20,9 @@ using MideaPreset = dudanov::midea::ac::Preset;
class Constants {
public:
static const char *const TAG;
static const std::string FREEZE_PROTECTION;
static const std::string SILENT;
static const std::string TURBO;
static const char *const FREEZE_PROTECTION;
static const char *const SILENT;
static const char *const TURBO;
};
class Converters {
@@ -32,15 +32,15 @@ class Converters {
static MideaSwingMode to_midea_swing_mode(ClimateSwingMode mode);
static ClimateSwingMode to_climate_swing_mode(MideaSwingMode mode);
static MideaPreset to_midea_preset(ClimatePreset preset);
static MideaPreset to_midea_preset(const std::string &preset);
static MideaPreset to_midea_preset(const char *preset);
static bool is_custom_midea_preset(MideaPreset preset);
static ClimatePreset to_climate_preset(MideaPreset preset);
static const std::string &to_custom_climate_preset(MideaPreset preset);
static const char *to_custom_climate_preset(MideaPreset preset);
static MideaFanMode to_midea_fan_mode(ClimateFanMode fan_mode);
static MideaFanMode to_midea_fan_mode(const std::string &fan_mode);
static MideaFanMode to_midea_fan_mode(const char *fan_mode);
static bool is_custom_midea_fan_mode(MideaFanMode fan_mode);
static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode);
static const std::string &to_custom_climate_fan_mode(MideaFanMode fan_mode);
static const char *to_custom_climate_fan_mode(MideaFanMode fan_mode);
static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities);
};

View File

@@ -64,13 +64,13 @@ void AirConditioner::control(const ClimateCall &call) {
ctrl.mode = Converters::to_midea_mode(call.get_mode().value());
if (call.get_preset().has_value()) {
ctrl.preset = Converters::to_midea_preset(call.get_preset().value());
} else if (call.get_custom_preset().has_value()) {
ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().value());
} else if (call.has_custom_preset()) {
ctrl.preset = Converters::to_midea_preset(call.get_custom_preset());
}
if (call.get_fan_mode().has_value()) {
ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value());
} else if (call.get_custom_fan_mode().has_value()) {
ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().value());
} else if (call.has_custom_fan_mode()) {
ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode());
}
this->base_.control(ctrl);
}
@@ -84,8 +84,10 @@ 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_);
traits.set_supported_custom_presets(this->supported_custom_presets_);
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_);
if (!this->supported_custom_fan_modes_.empty())
traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_);
/* + MINIMAL SET OF CAPABILITIES */
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW);

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(const std::vector<std::string> &presets) { this->supported_custom_presets_ = presets; }
void set_custom_fan_modes(const std::vector<std::string> &modes) { this->supported_custom_fan_modes_ = modes; }
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; }
protected:
void control(const ClimateCall &call) override;
@@ -55,8 +55,8 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
ClimateModeMask supported_modes_{};
ClimateSwingModeMask supported_swing_modes_{};
ClimatePresetMask supported_presets_{};
std::vector<std::string> supported_custom_presets_{};
std::vector<std::string> supported_custom_fan_modes_{};
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};

View File

@@ -54,7 +54,7 @@ void ThermostatClimate::setup() {
if (this->default_preset_ != climate::ClimatePreset::CLIMATE_PRESET_NONE) {
this->change_preset_(this->default_preset_);
} else if (!this->default_custom_preset_.empty()) {
this->change_custom_preset_(this->default_custom_preset_);
this->change_custom_preset_(this->default_custom_preset_.c_str());
}
}
@@ -218,12 +218,13 @@ void ThermostatClimate::control(const climate::ClimateCall &call) {
this->preset = call.get_preset().value();
}
}
if (call.get_custom_preset().has_value()) {
if (call.has_custom_preset()) {
// setup_complete_ blocks modifying/resetting the temps immediately after boot
if (this->setup_complete_) {
this->change_custom_preset_(call.get_custom_preset().value());
this->change_custom_preset_(call.get_custom_preset());
} 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());
}
}
@@ -321,9 +322,17 @@ climate::ClimateTraits ThermostatClimate::traits() {
for (auto &it : this->preset_config_) {
traits.add_supported_preset(it.first);
}
for (auto &it : this->custom_preset_config_) {
traits.add_supported_custom_preset(it.first);
// Extract custom preset names from the custom_preset_config_ map
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 &it : this->custom_preset_config_) {
custom_preset_names.push_back(it.first.c_str());
}
traits.set_supported_custom_presets(custom_preset_names);
}
return traits;
}
@@ -1153,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();
}
@@ -1163,36 +1172,36 @@ 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->preset = preset;
} else {
ESP_LOGW(TAG, "Preset %s not configured; ignoring", LOG_STR_ARG(climate::climate_preset_to_string(preset)));
}
}
void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) {
void ThermostatClimate::change_custom_preset_(const char *custom_preset) {
auto config = this->custom_preset_config_.find(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) {
ESP_LOGV(TAG, "Custom preset %s requested", custom_preset);
if (this->change_preset_internal_(config->second) || !this->has_custom_preset() ||
strcmp(this->get_custom_preset(), custom_preset) != 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();
}
this->refresh();
ESP_LOGI(TAG, "Custom preset %s applied", custom_preset.c_str());
ESP_LOGI(TAG, "Custom preset %s applied", custom_preset);
} else {
ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset.c_str());
ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset);
// 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.
}
this->preset.reset();
this->custom_preset = custom_preset;
} else {
ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset.c_str());
ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset);
}
}

View File

@@ -199,7 +199,7 @@ class ThermostatClimate : public climate::Climate, public Component {
/// Change to a provided preset setting; will reset temperature, mode, fan, and swing modes accordingly
void change_preset_(climate::ClimatePreset preset);
/// Change to a provided custom preset setting; will reset temperature, mode, fan, and swing modes accordingly
void change_custom_preset_(const std::string &custom_preset);
void change_custom_preset_(const char *custom_preset);
/// Applies the temperature, mode, fan, and swing modes of the provided config.
/// This is agnostic of custom vs built in preset

View File

@@ -1315,7 +1315,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->has_custom_preset()) {
JsonArray opt = root["custom_presets"].to<JsonArray>();
for (auto const &custom_preset : traits.get_supported_custom_presets())
opt.add(custom_preset);
@@ -1336,14 +1336,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->has_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->custom_preset.has_value()) {
root["custom_preset"] = obj->custom_preset.value().c_str();
if (!traits.get_supported_custom_presets().empty() && obj->has_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));

View File

@@ -1610,8 +1610,9 @@ class RepeatedTypeInfo(TypeInfo):
# Other types need the actual value
# Special handling for const char* elements
if self._use_pointer and "const char" in self._container_no_template:
field_id_size = self.calculate_field_id_size()
o += f" for (const char *it : {container_ref}) {{\n"
o += " size.add_length_force(1, strlen(it));\n"
o += f" size.add_length_force({field_id_size}, strlen(it));\n"
else:
auto_ref = "" if self._ti_is_bool else "&"
o += f" for (const auto {auto_ref}it : {container_ref}) {{\n"

View File

@@ -0,0 +1,40 @@
esphome:
name: climate-custom-modes-test
host:
api:
logger:
sensor:
- platform: template
id: thermostat_sensor
lambda: "return 22.0;"
climate:
- platform: thermostat
id: test_thermostat
name: Test Thermostat Custom Modes
sensor: thermostat_sensor
preset:
- name: Away
default_target_temperature_low: 16°C
default_target_temperature_high: 20°C
- name: Eco Plus
default_target_temperature_low: 18°C
default_target_temperature_high: 22°C
- name: Super Saver
default_target_temperature_low: 20°C
default_target_temperature_high: 24°C
- name: Vacation Mode
default_target_temperature_low: 15°C
default_target_temperature_high: 18°C
idle_action:
- logger.log: idle_action
cool_action:
- logger.log: cool_action
heat_action:
- logger.log: heat_action
min_cooling_off_time: 10s
min_cooling_run_time: 10s
min_heating_off_time: 10s
min_heating_run_time: 10s
min_idle_time: 10s

View File

@@ -0,0 +1,42 @@
"""Integration test for climate custom presets."""
from __future__ import annotations
from aioesphomeapi import ClimateInfo, ClimatePreset
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_climate_custom_fan_modes_and_presets(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that custom presets are properly exposed via API."""
async with run_compiled(yaml_config), api_client_connected() as client:
# Get entities and services
entities, services = await client.list_entities_services()
climate_infos = [e for e in entities if isinstance(e, ClimateInfo)]
assert len(climate_infos) == 1, "Expected exactly 1 climate entity"
test_climate = climate_infos[0]
# Verify enum presets are exposed (from preset: config map)
assert ClimatePreset.AWAY in test_climate.supported_presets, (
"Expected AWAY in enum presets"
)
# Verify custom string presets are exposed (non-standard preset names from preset map)
custom_presets = test_climate.supported_custom_presets
assert len(custom_presets) == 3, (
f"Expected 3 custom presets, got {len(custom_presets)}: {custom_presets}"
)
assert "Eco Plus" in custom_presets, "Expected 'Eco Plus' in custom presets"
assert "Super Saver" in custom_presets, (
"Expected 'Super Saver' in custom presets"
)
assert "Vacation Mode" in custom_presets, (
"Expected 'Vacation Mode' in custom presets"
)