1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-16 06:45:48 +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 bool supports_action = 12; // Deprecated: use feature_flags
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"]; 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 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 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; bool disabled_by_default = 18;
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 20; 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()) if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.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()) { if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) {
resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value())); resp.set_custom_fan_mode(StringRef(climate->get_custom_fan_mode()));
} }
if (traits.get_supports_presets() && climate->preset.has_value()) { if (traits.get_supports_presets() && climate->preset.has_value()) {
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value()); resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
} }
if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) { if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) {
resp.set_custom_preset(StringRef(climate->custom_preset.value())); resp.set_custom_preset(StringRef(climate->get_custom_preset()));
} }
if (traits.get_supports_swing_modes()) if (traits.get_supports_swing_modes())
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode); 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) { for (const auto &it : *this->supported_swing_modes) {
buffer.encode_uint32(14, static_cast<uint32_t>(it), true); buffer.encode_uint32(14, static_cast<uint32_t>(it), true);
} }
for (const auto &it : *this->supported_custom_fan_modes) { for (const char *it : *this->supported_custom_fan_modes) {
buffer.encode_string(15, it, true); buffer.encode_string(15, it, strlen(it), true);
} }
for (const auto &it : *this->supported_presets) { for (const auto &it : *this->supported_presets) {
buffer.encode_uint32(16, static_cast<uint32_t>(it), true); buffer.encode_uint32(16, static_cast<uint32_t>(it), true);
} }
for (const auto &it : *this->supported_custom_presets) { for (const char *it : *this->supported_custom_presets) {
buffer.encode_string(17, it, true); buffer.encode_string(17, it, strlen(it), true);
} }
buffer.encode_bool(18, this->disabled_by_default); buffer.encode_bool(18, this->disabled_by_default);
#ifdef USE_ENTITY_ICON #ifdef USE_ENTITY_ICON
@@ -1229,8 +1229,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
} }
} }
if (!this->supported_custom_fan_modes->empty()) { if (!this->supported_custom_fan_modes->empty()) {
for (const auto &it : *this->supported_custom_fan_modes) { for (const char *it : *this->supported_custom_fan_modes) {
size.add_length_force(1, it.size()); size.add_length_force(1, strlen(it));
} }
} }
if (!this->supported_presets->empty()) { if (!this->supported_presets->empty()) {
@@ -1239,8 +1239,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
} }
} }
if (!this->supported_custom_presets->empty()) { if (!this->supported_custom_presets->empty()) {
for (const auto &it : *this->supported_custom_presets) { for (const char *it : *this->supported_custom_presets) {
size.add_length_force(2, it.size()); size.add_length_force(2, strlen(it));
} }
} }
size.add_bool(2, this->disabled_by_default); size.add_bool(2, this->disabled_by_default);

View File

@@ -1384,9 +1384,9 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
bool supports_action{false}; bool supports_action{false};
const climate::ClimateFanModeMask *supported_fan_modes{}; const climate::ClimateFanModeMask *supported_fan_modes{};
const climate::ClimateSwingModeMask *supported_swing_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 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}; float visual_current_temperature_step{0.0f};
bool supports_current_humidity{false}; bool supports_current_humidity{false};
bool supports_target_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 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 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 bedjet
} // namespace esphome } // namespace esphome

View File

@@ -8,15 +8,15 @@ namespace bedjet {
using namespace esphome::climate; 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) if (fan_step < BEDJET_FAN_SPEED_COUNT)
return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step]; return BEDJET_FAN_STEP_NAMES[fan_step];
return nullptr; 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++) { 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; return i;
} }
} }
@@ -48,7 +48,7 @@ void BedJetClimate::dump_config() {
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode))); ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
} }
for (const auto &mode : traits.get_supported_custom_fan_modes()) { 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:"); 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))); ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
} }
for (const auto &preset : traits.get_supported_custom_presets()) { 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->target_temperature = NAN;
this->current_temperature = NAN; this->current_temperature = NAN;
this->preset.reset(); this->preset.reset();
this->custom_preset.reset(); this->clear_custom_preset_();
this->publish_state(); this->publish_state();
} }
@@ -120,7 +120,7 @@ void BedJetClimate::control(const ClimateCall &call) {
if (button_result) { if (button_result) {
this->mode = mode; this->mode = mode;
// We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those // 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(); this->preset.reset();
} }
} }
@@ -144,8 +144,7 @@ void BedJetClimate::control(const ClimateCall &call) {
if (result) { if (result) {
this->mode = CLIMATE_MODE_HEAT; this->mode = CLIMATE_MODE_HEAT;
this->preset = CLIMATE_PRESET_BOOST; this->set_preset_(CLIMATE_PRESET_BOOST);
this->custom_preset.reset();
} }
} else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) { } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) {
if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) { 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_)); result = this->parent_->send_button(heat_button(this->heating_mode_));
if (result) { if (result) {
this->preset.reset(); this->preset.reset();
this->custom_preset.reset(); this->clear_custom_preset_();
} }
} else { } else {
ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'", 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); ESP_LOGW(TAG, "Unsupported preset: %d", preset);
return; return;
} }
} else if (call.get_custom_preset().has_value()) { } else if (call.has_custom_preset()) {
std::string preset = *call.get_custom_preset(); const char *preset = call.get_custom_preset();
bool result; bool result;
if (preset == "M1") { if (strcmp(preset, "M1") == 0) {
result = this->parent_->button_memory1(); result = this->parent_->button_memory1();
} else if (preset == "M2") { } else if (strcmp(preset, "M2") == 0) {
result = this->parent_->button_memory2(); result = this->parent_->button_memory2();
} else if (preset == "M3") { } else if (strcmp(preset, "M3") == 0) {
result = this->parent_->button_memory3(); result = this->parent_->button_memory3();
} else if (preset == "LTD HT") { } else if (strcmp(preset, "LTD HT") == 0) {
result = this->parent_->button_heat(); result = this->parent_->button_heat();
} else if (preset == "EXT HT") { } else if (strcmp(preset, "EXT HT") == 0) {
result = this->parent_->button_ext_heat(); result = this->parent_->button_ext_heat();
} else { } else {
ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str()); ESP_LOGW(TAG, "Unsupported preset: %s", preset);
return; return;
} }
if (result) { if (result) {
this->custom_preset = preset; this->set_custom_preset_(preset);
this->preset.reset();
} }
} }
@@ -207,19 +205,16 @@ void BedJetClimate::control(const ClimateCall &call) {
} }
if (result) { if (result) {
this->fan_mode = fan_mode; this->set_fan_mode_(fan_mode);
this->custom_fan_mode.reset();
} }
} else if (call.get_custom_fan_mode().has_value()) { } else if (call.has_custom_fan_mode()) {
auto fan_mode = *call.get_custom_fan_mode(); const char *fan_mode = call.get_custom_fan_mode();
auto fan_index = bedjet_fan_speed_to_step(fan_mode); auto fan_index = bedjet_fan_speed_to_step(fan_mode);
if (fan_index <= 19) { 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(), ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode, fan_index);
fan_index);
bool result = this->parent_->set_fan_index(fan_index); bool result = this->parent_->set_fan_index(fan_index);
if (result) { if (result) {
this->custom_fan_mode = fan_mode; this->set_custom_fan_mode_(fan_mode);
this->fan_mode.reset();
} }
} }
} }
@@ -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); const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step);
if (fan_mode_name != nullptr) { 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. // 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->mode = CLIMATE_MODE_OFF;
this->action = CLIMATE_ACTION_IDLE; this->action = CLIMATE_ACTION_IDLE;
this->fan_mode = CLIMATE_FAN_OFF; this->fan_mode = CLIMATE_FAN_OFF;
this->custom_preset.reset(); this->clear_custom_preset_();
this->preset.reset(); this->preset.reset();
break; break;
@@ -266,7 +261,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
if (this->heating_mode_ == HEAT_MODE_EXTENDED) { if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
this->set_custom_preset_("LTD HT"); this->set_custom_preset_("LTD HT");
} else { } else {
this->custom_preset.reset(); this->clear_custom_preset_();
} }
break; break;
@@ -275,7 +270,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
this->action = CLIMATE_ACTION_HEATING; this->action = CLIMATE_ACTION_HEATING;
this->preset.reset(); this->preset.reset();
if (this->heating_mode_ == HEAT_MODE_EXTENDED) { if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
this->custom_preset.reset(); this->clear_custom_preset_();
} else { } else {
this->set_custom_preset_("EXT HT"); this->set_custom_preset_("EXT HT");
} }
@@ -284,20 +279,19 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
case MODE_COOL: case MODE_COOL:
this->mode = CLIMATE_MODE_FAN_ONLY; this->mode = CLIMATE_MODE_FAN_ONLY;
this->action = CLIMATE_ACTION_COOLING; this->action = CLIMATE_ACTION_COOLING;
this->custom_preset.reset(); this->clear_custom_preset_();
this->preset.reset(); this->preset.reset();
break; break;
case MODE_DRY: case MODE_DRY:
this->mode = CLIMATE_MODE_DRY; this->mode = CLIMATE_MODE_DRY;
this->action = CLIMATE_ACTION_DRYING; this->action = CLIMATE_ACTION_DRYING;
this->custom_preset.reset(); this->clear_custom_preset_();
this->preset.reset(); this->preset.reset();
break; break;
case MODE_TURBO: case MODE_TURBO:
this->preset = CLIMATE_PRESET_BOOST; this->set_preset_(CLIMATE_PRESET_BOOST);
this->custom_preset.reset();
this->mode = CLIMATE_MODE_HEAT; this->mode = CLIMATE_MODE_HEAT;
this->action = CLIMATE_ACTION_HEATING; this->action = CLIMATE_ACTION_HEATING;
break; 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 doesn't have a "TURBO" mode, but we can use the BOOST preset instead.
climate::CLIMATE_PRESET_BOOST, climate::CLIMATE_PRESET_BOOST,
}); });
// String literals are stored in rodata and valid for program lifetime
traits.set_supported_custom_presets({ traits.set_supported_custom_presets({
// We could fetch biodata from bedjet and set these names that way. this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
// 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",
"M1", "M1",
"M2", "M2",
"M3", "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_min_temperature(19.0);
traits.set_visual_max_temperature(43.0); traits.set_visual_max_temperature(43.0);
traits.set_visual_temperature_step(1.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_); const LogString *mode_s = climate_mode_to_string(*this->mode_);
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); 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(); 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()) { 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_); const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_);
ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); 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(); 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()) { if (this->preset_.has_value()) {
this->custom_preset_.reset(); this->custom_preset_ = nullptr;
const LogString *preset_s = climate_preset_to_string(*this->preset_); const LogString *preset_s = climate_preset_to_string(*this->preset_);
ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s));
} }
@@ -96,11 +96,10 @@ void ClimateCall::validate_() {
this->mode_.reset(); this->mode_.reset();
} }
} }
if (this->custom_fan_mode_.has_value()) { if (this->custom_fan_mode_ != nullptr) {
auto custom_fan_mode = *this->custom_fan_mode_; if (!traits.supports_custom_fan_mode(this->custom_fan_mode_)) {
if (!traits.supports_custom_fan_mode(custom_fan_mode)) { ESP_LOGW(TAG, " Fan Mode %s not supported", this->custom_fan_mode_);
ESP_LOGW(TAG, " Fan Mode %s not supported", custom_fan_mode.c_str()); this->custom_fan_mode_ = nullptr;
this->custom_fan_mode_.reset();
} }
} else if (this->fan_mode_.has_value()) { } else if (this->fan_mode_.has_value()) {
auto fan_mode = *this->fan_mode_; auto fan_mode = *this->fan_mode_;
@@ -109,11 +108,10 @@ void ClimateCall::validate_() {
this->fan_mode_.reset(); this->fan_mode_.reset();
} }
} }
if (this->custom_preset_.has_value()) { if (this->custom_preset_ != nullptr) {
auto custom_preset = *this->custom_preset_; if (!traits.supports_custom_preset(this->custom_preset_)) {
if (!traits.supports_custom_preset(custom_preset)) { ESP_LOGW(TAG, " Preset %s not supported", this->custom_preset_);
ESP_LOGW(TAG, " Preset %s not supported", custom_preset.c_str()); this->custom_preset_ = nullptr;
this->custom_preset_.reset();
} }
} else if (this->preset_.has_value()) { } else if (this->preset_.has_value()) {
auto preset = *this->preset_; auto preset = *this->preset_;
@@ -186,26 +184,29 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) {
ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
this->fan_mode_ = fan_mode; this->fan_mode_ = fan_mode;
this->custom_fan_mode_.reset(); this->custom_fan_mode_ = nullptr;
return *this; 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) { for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
if (str_equals_case_insensitive(fan_mode, mode_entry.str)) { if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) {
this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value)); return this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
return *this;
} }
} }
if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) { // Find the matching pointer from parent climate device
this->custom_fan_mode_ = fan_mode; if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) {
this->custom_fan_mode_ = mode_ptr;
this->fan_mode_.reset(); this->fan_mode_.reset();
} else { return *this;
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
} }
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode);
return *this; 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) { ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
if (fan_mode.has_value()) { if (fan_mode.has_value()) {
this->set_fan_mode(fan_mode.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) { ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
this->preset_ = preset; this->preset_ = preset;
this->custom_preset_.reset(); this->custom_preset_ = nullptr;
return *this; 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) { for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
if (str_equals_case_insensitive(preset, preset_entry.str)) { if (str_equals_case_insensitive(custom_preset, preset_entry.str)) {
this->set_preset(static_cast<ClimatePreset>(preset_entry.value)); return this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
return *this;
} }
} }
if (this->parent_->get_traits().supports_custom_preset(preset)) { // Find the matching pointer from parent climate device
this->custom_preset_ = preset; if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) {
this->custom_preset_ = preset_ptr;
this->preset_.reset(); this->preset_.reset();
} else { return *this;
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
} }
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset);
return *this; 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) { ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
if (preset.has_value()) { if (preset.has_value()) {
this->set_preset(preset.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<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_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<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) { ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
this->target_temperature_high_ = 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) { ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
this->fan_mode_ = fan_mode; this->fan_mode_ = fan_mode;
this->custom_fan_mode_.reset(); this->custom_fan_mode_ = nullptr;
return *this; return *this;
} }
ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) { ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
this->preset_ = preset; this->preset_ = preset;
this->custom_preset_.reset(); this->custom_preset_ = nullptr;
return *this; return *this;
} }
@@ -382,13 +384,13 @@ void Climate::save_state_() {
state.uses_custom_fan_mode = false; state.uses_custom_fan_mode = false;
state.fan_mode = this->fan_mode.value(); 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; state.uses_custom_fan_mode = true;
const auto &supported = traits.get_supported_custom_fan_modes(); const auto &supported = traits.get_supported_custom_fan_modes();
// std::vector maintains insertion order // std::vector maintains insertion order
size_t i = 0; size_t i = 0;
for (const auto &mode : supported) { for (const char *mode : supported) {
if (mode == custom_fan_mode) { if (strcmp(mode, this->custom_fan_mode_) == 0) {
state.custom_fan_mode = i; state.custom_fan_mode = i;
break; break;
} }
@@ -399,13 +401,13 @@ void Climate::save_state_() {
state.uses_custom_preset = false; state.uses_custom_preset = false;
state.preset = this->preset.value(); 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; state.uses_custom_preset = true;
const auto &supported = traits.get_supported_custom_presets(); const auto &supported = traits.get_supported_custom_presets();
// std::vector maintains insertion order // std::vector maintains insertion order
size_t i = 0; size_t i = 0;
for (const auto &preset : supported) { for (const char *preset : supported) {
if (preset == custom_preset) { if (strcmp(preset, this->custom_preset_) == 0) {
state.custom_preset = i; state.custom_preset = i;
break; break;
} }
@@ -430,14 +432,14 @@ void Climate::publish_state() {
if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { 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()))); 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()) { if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) {
ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode.value().c_str()); ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_);
} }
if (traits.get_supports_presets() && this->preset.has_value()) { if (traits.get_supports_presets() && this->preset.has_value()) {
ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.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()) { if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) {
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset.value().c_str()); ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_);
} }
if (traits.get_supports_swing_modes()) { if (traits.get_supports_swing_modes()) {
ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); 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->uses_custom_fan_mode) {
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
call.fan_mode_.reset(); 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)) { } else if (traits.supports_fan_mode(this->fan_mode)) {
call.set_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->uses_custom_preset) {
if (this->custom_preset < traits.get_supported_custom_presets().size()) { if (this->custom_preset < traits.get_supported_custom_presets().size()) {
call.preset_.reset(); 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)) { } else if (traits.supports_preset(this->preset)) {
call.set_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->uses_custom_fan_mode) {
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
climate->fan_mode.reset(); 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)) { } else if (traits.supports_fan_mode(this->fan_mode)) {
climate->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->uses_custom_preset) {
if (this->custom_preset < traits.get_supported_custom_presets().size()) { if (this->custom_preset < traits.get_supported_custom_presets().size()) {
climate->preset.reset(); 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)) { } else if (traits.supports_preset(this->preset)) {
climate->preset = this->preset; climate->preset = this->preset;
climate->custom_preset.reset(); climate->clear_custom_preset_();
} }
if (traits.supports_swing_mode(this->swing_mode)) { if (traits.supports_swing_mode(this->swing_mode)) {
climate->swing_mode = this->swing_mode; climate->swing_mode = this->swing_mode;
@@ -583,28 +585,107 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
climate->publish_state(); climate->publish_state();
} }
template<typename T1, typename T2> bool set_alternative(optional<T1> &dst, optional<T2> &alt, const T1 &src) { /** Template helper for setting primary modes (fan_mode, preset) with mutual exclusion.
bool is_changed = alt.has_value(); *
alt.reset(); * Climate devices have mutually exclusive mode pairs:
if (is_changed || dst != src) { * - fan_mode (enum) vs custom_fan_mode_ (const char*)
dst = src; * - preset (enum) vs custom_preset_ (const char*)
is_changed = true; *
* 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) { 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) { bool Climate::set_custom_fan_mode_(const char *mode) {
return set_alternative(this->custom_fan_mode, this->fan_mode, 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) { bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); }
return set_alternative(this->custom_preset, this->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) { 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()) { if (!traits.get_supported_custom_fan_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported custom fan modes:"); ESP_LOGCONFIG(tag, " Supported custom fan modes:");
for (const std::string &s : traits.get_supported_custom_fan_modes()) for (const char *s : traits.get_supported_custom_fan_modes())
ESP_LOGCONFIG(tag, " - %s", s.c_str()); ESP_LOGCONFIG(tag, " - %s", s);
} }
if (!traits.get_supported_presets().empty()) { if (!traits.get_supported_presets().empty()) {
ESP_LOGCONFIG(tag, " Supported presets:"); ESP_LOGCONFIG(tag, " Supported presets:");
@@ -666,8 +747,8 @@ void Climate::dump_traits_(const char *tag) {
} }
if (!traits.get_supported_custom_presets().empty()) { if (!traits.get_supported_custom_presets().empty()) {
ESP_LOGCONFIG(tag, " Supported custom presets:"); ESP_LOGCONFIG(tag, " Supported custom presets:");
for (const std::string &s : traits.get_supported_custom_presets()) for (const char *s : traits.get_supported_custom_presets())
ESP_LOGCONFIG(tag, " - %s", s.c_str()); ESP_LOGCONFIG(tag, " - %s", s);
} }
if (!traits.get_supported_swing_modes().empty()) { if (!traits.get_supported_swing_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported swing modes:"); ESP_LOGCONFIG(tag, " Supported swing modes:");

View File

@@ -77,6 +77,8 @@ class ClimateCall {
ClimateCall &set_fan_mode(const std::string &fan_mode); ClimateCall &set_fan_mode(const std::string &fan_mode);
/// Set the fan mode of the climate device based on a string. /// Set the fan mode of the climate device based on a string.
ClimateCall &set_fan_mode(optional<std::string> fan_mode); 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. /// Set the swing mode of the climate device.
ClimateCall &set_swing_mode(ClimateSwingMode swing_mode); ClimateCall &set_swing_mode(ClimateSwingMode swing_mode);
/// Set the swing mode of the climate device. /// Set the swing mode of the climate device.
@@ -91,6 +93,8 @@ class ClimateCall {
ClimateCall &set_preset(const std::string &preset); ClimateCall &set_preset(const std::string &preset);
/// Set the preset of the climate device based on a string. /// Set the preset of the climate device based on a string.
ClimateCall &set_preset(optional<std::string> preset); ClimateCall &set_preset(optional<std::string> preset);
/// Set the custom preset of the climate device.
ClimateCall &set_preset(const char *custom_preset);
void perform(); void perform();
@@ -103,8 +107,10 @@ class ClimateCall {
const optional<ClimateFanMode> &get_fan_mode() const; const optional<ClimateFanMode> &get_fan_mode() const;
const optional<ClimateSwingMode> &get_swing_mode() const; const optional<ClimateSwingMode> &get_swing_mode() const;
const optional<ClimatePreset> &get_preset() const; const optional<ClimatePreset> &get_preset() const;
const optional<std::string> &get_custom_fan_mode() const; const char *get_custom_fan_mode() const { return this->custom_fan_mode_; }
const optional<std::string> &get_custom_preset() const; 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: protected:
void validate_(); void validate_();
@@ -118,8 +124,10 @@ class ClimateCall {
optional<ClimateFanMode> fan_mode_; optional<ClimateFanMode> fan_mode_;
optional<ClimateSwingMode> swing_mode_; optional<ClimateSwingMode> swing_mode_;
optional<ClimatePreset> preset_; 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. /// 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_min_humidity_override(float visual_min_humidity_override);
void set_visual_max_humidity_override(float visual_max_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. /// The current temperature of the climate device, as reported from the integration.
float current_temperature{NAN}; float current_temperature{NAN};
@@ -238,12 +252,6 @@ class Climate : public EntityBase {
/// The active preset of the climate device. /// The active preset of the climate device.
optional<ClimatePreset> preset; 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. /// The active mode of the climate device.
ClimateMode mode{CLIMATE_MODE_OFF}; ClimateMode mode{CLIMATE_MODE_OFF};
@@ -253,20 +261,37 @@ class Climate : public EntityBase {
/// The active swing mode of the climate device. /// The active swing mode of the climate device.
ClimateSwingMode swing_mode{CLIMATE_SWING_OFF}; 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: protected:
friend ClimateCall; friend ClimateCall;
friend struct ClimateDeviceRestoreState;
/// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed. /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed.
bool set_fan_mode_(ClimateFanMode mode); bool set_fan_mode_(ClimateFanMode mode);
/// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. /// 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. /// Set preset. Reset custom preset. Return true if preset has been changed.
bool set_preset_(ClimatePreset preset); bool set_preset_(ClimatePreset preset);
/// Set custom preset. Reset primary preset. Return true if preset has been changed. /// 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. /** 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_current_temperature_step_override_{};
optional<float> visual_min_humidity_override_{}; optional<float> visual_min_humidity_override_{};
optional<float> visual_max_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 } // namespace climate

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cstring>
#include <vector> #include <vector>
#include "climate_mode.h" #include "climate_mode.h"
#include "esphome/core/finite_set_mask.h" #include "esphome/core/finite_set_mask.h"
@@ -18,16 +19,25 @@ using ClimateSwingModeMask =
FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>; FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>;
using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 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 // Avoids std::find template overhead
template<typename T> inline bool vector_contains(const std::vector<T> &vec, const T &value) { inline bool vector_contains(const std::vector<const char *> &vec, const char *value) {
for (const auto &item : vec) { for (const char *item : vec) {
if (item == value) if (strcmp(item, value) == 0)
return true; return true;
} }
return false; 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. /** This class contains all static data for climate devices.
* *
* All climate devices must support these features: * 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. * - temperature step - the step with which to increase/decrease target temperature.
* This also affects with how many decimal places the temperature is shown * This also affects with how many decimal places the temperature is shown
*/ */
class Climate; // Forward declaration
class ClimateTraits { class ClimateTraits {
friend class Climate; // Allow Climate to access protected find methods
public: public:
/// Get/set feature flags (see ClimateFeatures enum in climate_mode.h) /// Get/set feature flags (see ClimateFeatures enum in climate_mode.h)
uint32_t get_feature_flags() const { return this->feature_flags_; } 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 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_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 supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
bool get_supports_fan_modes() const { bool get_supports_fan_modes() const {
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
} }
const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; } 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) { void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) {
this->supported_custom_fan_modes_ = std::move(supported_custom_fan_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; this->supported_custom_fan_modes_ = modes;
} }
template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) { template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
this->supported_custom_fan_modes_.assign(modes, 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); 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 set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; }
void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } 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 supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); }
bool get_supports_presets() const { return !this->supported_presets_.empty(); } bool get_supports_presets() const { return !this->supported_presets_.empty(); }
const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; }
void set_supported_custom_presets(std::vector<std::string> supported_custom_presets) { void set_supported_custom_presets(std::initializer_list<const char *> presets) {
this->supported_custom_presets_ = std::move(supported_custom_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; this->supported_custom_presets_ = presets;
} }
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) { template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
this->supported_custom_presets_.assign(presets, 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); 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 set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; }
void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } 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}; uint32_t feature_flags_{0};
float visual_min_temperature_{10}; float visual_min_temperature_{10};
float visual_max_temperature_{30}; float visual_max_temperature_{30};
@@ -239,8 +279,17 @@ class ClimateTraits {
climate::ClimateFanModeMask supported_fan_modes_; climate::ClimateFanModeMask supported_fan_modes_;
climate::ClimateSwingModeMask supported_swing_modes_; climate::ClimateSwingModeMask supported_swing_modes_;
climate::ClimatePresetMask supported_presets_; 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 } // namespace climate

View File

@@ -28,16 +28,16 @@ class DemoClimate : public climate::Climate, public Component {
this->mode = climate::CLIMATE_MODE_AUTO; this->mode = climate::CLIMATE_MODE_AUTO;
this->action = climate::CLIMATE_ACTION_COOLING; this->action = climate::CLIMATE_ACTION_COOLING;
this->fan_mode = climate::CLIMATE_FAN_HIGH; this->fan_mode = climate::CLIMATE_FAN_HIGH;
this->custom_preset = {"My Preset"}; this->set_custom_preset_("My Preset");
break; break;
case DemoClimateType::TYPE_3: case DemoClimateType::TYPE_3:
this->current_temperature = 21.5; this->current_temperature = 21.5;
this->target_temperature_low = 21.0; this->target_temperature_low = 21.0;
this->target_temperature_high = 22.5; this->target_temperature_high = 22.5;
this->mode = climate::CLIMATE_MODE_HEAT_COOL; 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->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
this->preset = climate::CLIMATE_PRESET_AWAY; this->set_preset_(climate::CLIMATE_PRESET_AWAY);
break; break;
} }
this->publish_state(); this->publish_state();
@@ -58,23 +58,19 @@ class DemoClimate : public climate::Climate, public Component {
this->target_temperature_high = *call.get_target_temperature_high(); this->target_temperature_high = *call.get_target_temperature_high();
} }
if (call.get_fan_mode().has_value()) { if (call.get_fan_mode().has_value()) {
this->fan_mode = *call.get_fan_mode(); this->set_fan_mode_(*call.get_fan_mode());
this->custom_fan_mode.reset();
} }
if (call.get_swing_mode().has_value()) { if (call.get_swing_mode().has_value()) {
this->swing_mode = *call.get_swing_mode(); this->swing_mode = *call.get_swing_mode();
} }
if (call.get_custom_fan_mode().has_value()) { if (call.has_custom_fan_mode()) {
this->custom_fan_mode = *call.get_custom_fan_mode(); this->set_custom_fan_mode_(call.get_custom_fan_mode());
this->fan_mode.reset();
} }
if (call.get_preset().has_value()) { if (call.get_preset().has_value()) {
this->preset = *call.get_preset(); this->set_preset_(*call.get_preset());
this->custom_preset.reset();
} }
if (call.get_custom_preset().has_value()) { if (call.has_custom_preset()) {
this->custom_preset = *call.get_custom_preset(); this->set_custom_preset_(call.get_custom_preset());
this->preset.reset();
} }
this->publish_state(); this->publish_state();
} }

View File

@@ -8,9 +8,9 @@ namespace midea {
namespace ac { namespace ac {
const char *const Constants::TAG = "midea"; const char *const Constants::TAG = "midea";
const std::string Constants::FREEZE_PROTECTION = "freeze protection"; const char *const Constants::FREEZE_PROTECTION = "freeze protection";
const std::string Constants::SILENT = "silent"; const char *const Constants::SILENT = "silent";
const std::string Constants::TURBO = "turbo"; const char *const Constants::TURBO = "turbo";
ClimateMode Converters::to_climate_mode(MideaMode mode) { ClimateMode Converters::to_climate_mode(MideaMode mode) {
switch (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) { switch (mode) {
case MideaFanMode::FAN_SILENT: case MideaFanMode::FAN_SILENT:
return Constants::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) { MideaFanMode Converters::to_midea_fan_mode(const char *mode) {
if (mode == Constants::SILENT) if (strcmp(mode, Constants::SILENT) == 0)
return MideaFanMode::FAN_SILENT; return MideaFanMode::FAN_SILENT;
return MideaFanMode::FAN_TURBO; 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; } 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) { void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities) {
if (capabilities.supportAutoMode()) if (capabilities.supportAutoMode())
@@ -169,7 +169,7 @@ void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::
if (capabilities.supportEcoPreset()) if (capabilities.supportEcoPreset())
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO); traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO);
if (capabilities.supportFrostProtectionPreset()) if (capabilities.supportFrostProtectionPreset())
traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION); traits.set_supported_custom_presets({Constants::FREEZE_PROTECTION});
} }
} // namespace ac } // namespace ac

View File

@@ -20,9 +20,9 @@ using MideaPreset = dudanov::midea::ac::Preset;
class Constants { class Constants {
public: public:
static const char *const TAG; static const char *const TAG;
static const std::string FREEZE_PROTECTION; static const char *const FREEZE_PROTECTION;
static const std::string SILENT; static const char *const SILENT;
static const std::string TURBO; static const char *const TURBO;
}; };
class Converters { class Converters {
@@ -32,15 +32,15 @@ class Converters {
static MideaSwingMode to_midea_swing_mode(ClimateSwingMode mode); static MideaSwingMode to_midea_swing_mode(ClimateSwingMode mode);
static ClimateSwingMode to_climate_swing_mode(MideaSwingMode mode); static ClimateSwingMode to_climate_swing_mode(MideaSwingMode mode);
static MideaPreset to_midea_preset(ClimatePreset preset); 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 bool is_custom_midea_preset(MideaPreset preset);
static ClimatePreset to_climate_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(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 bool is_custom_midea_fan_mode(MideaFanMode fan_mode);
static ClimateFanMode to_climate_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); 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()); ctrl.mode = Converters::to_midea_mode(call.get_mode().value());
if (call.get_preset().has_value()) { if (call.get_preset().has_value()) {
ctrl.preset = Converters::to_midea_preset(call.get_preset().value()); ctrl.preset = Converters::to_midea_preset(call.get_preset().value());
} else if (call.get_custom_preset().has_value()) { } else if (call.has_custom_preset()) {
ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().value()); ctrl.preset = Converters::to_midea_preset(call.get_custom_preset());
} }
if (call.get_fan_mode().has_value()) { if (call.get_fan_mode().has_value()) {
ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value()); ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value());
} else if (call.get_custom_fan_mode().has_value()) { } else if (call.has_custom_fan_mode()) {
ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().value()); ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode());
} }
this->base_.control(ctrl); this->base_.control(ctrl);
} }
@@ -84,8 +84,10 @@ ClimateTraits AirConditioner::traits() {
traits.set_supported_modes(this->supported_modes_); traits.set_supported_modes(this->supported_modes_);
traits.set_supported_swing_modes(this->supported_swing_modes_); traits.set_supported_swing_modes(this->supported_swing_modes_);
traits.set_supported_presets(this->supported_presets_); traits.set_supported_presets(this->supported_presets_);
traits.set_supported_custom_presets(this->supported_custom_presets_); if (!this->supported_custom_presets_.empty())
traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); 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 */ /* + MINIMAL SET OF CAPABILITIES */
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO); 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_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_modes(ClimateModeMask modes) { this->supported_modes_ = modes; }
void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_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_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_presets(std::initializer_list<const char *> 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_fan_modes(std::initializer_list<const char *> modes) { this->supported_custom_fan_modes_ = modes; }
protected: protected:
void control(const ClimateCall &call) override; void control(const ClimateCall &call) override;
@@ -55,8 +55,8 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
ClimateModeMask supported_modes_{}; ClimateModeMask supported_modes_{};
ClimateSwingModeMask supported_swing_modes_{}; ClimateSwingModeMask supported_swing_modes_{};
ClimatePresetMask supported_presets_{}; ClimatePresetMask supported_presets_{};
std::vector<std::string> supported_custom_presets_{}; std::vector<const char *> supported_custom_presets_{};
std::vector<std::string> supported_custom_fan_modes_{}; std::vector<const char *> supported_custom_fan_modes_{};
Sensor *outdoor_sensor_{nullptr}; Sensor *outdoor_sensor_{nullptr};
Sensor *humidity_sensor_{nullptr}; Sensor *humidity_sensor_{nullptr};
Sensor *power_sensor_{nullptr}; Sensor *power_sensor_{nullptr};

View File

@@ -54,7 +54,7 @@ void ThermostatClimate::setup() {
if (this->default_preset_ != climate::ClimatePreset::CLIMATE_PRESET_NONE) { if (this->default_preset_ != climate::ClimatePreset::CLIMATE_PRESET_NONE) {
this->change_preset_(this->default_preset_); this->change_preset_(this->default_preset_);
} else if (!this->default_custom_preset_.empty()) { } 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(); 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 // setup_complete_ blocks modifying/resetting the temps immediately after boot
if (this->setup_complete_) { if (this->setup_complete_) {
this->change_custom_preset_(call.get_custom_preset().value()); this->change_custom_preset_(call.get_custom_preset());
} else { } 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_) { for (auto &it : this->preset_config_) {
traits.add_supported_preset(it.first); 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; return traits;
} }
@@ -1153,7 +1162,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) {
this->preset.value() != preset) { this->preset.value() != preset) {
// Fire any preset changed trigger if defined // Fire any preset changed trigger if defined
Trigger<> *trig = this->preset_change_trigger_; Trigger<> *trig = this->preset_change_trigger_;
this->preset = preset; this->set_preset_(preset);
if (trig != nullptr) { if (trig != nullptr) {
trig->trigger(); trig->trigger();
} }
@@ -1163,36 +1172,36 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) {
} else { } else {
ESP_LOGI(TAG, "No changes required to apply preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); 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 { } else {
ESP_LOGW(TAG, "Preset %s not configured; ignoring", LOG_STR_ARG(climate::climate_preset_to_string(preset))); 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); auto config = this->custom_preset_config_.find(custom_preset);
if (config != this->custom_preset_config_.end()) { if (config != this->custom_preset_config_.end()) {
ESP_LOGV(TAG, "Custom preset %s requested", custom_preset.c_str()); ESP_LOGV(TAG, "Custom preset %s requested", custom_preset);
if (this->change_preset_internal_(config->second) || (!this->custom_preset.has_value()) || if (this->change_preset_internal_(config->second) || !this->has_custom_preset() ||
this->custom_preset.value() != custom_preset) { strcmp(this->get_custom_preset(), custom_preset) != 0) {
// Fire any preset changed trigger if defined // Fire any preset changed trigger if defined
Trigger<> *trig = this->preset_change_trigger_; 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) { if (trig != nullptr) {
trig->trigger(); trig->trigger();
} }
this->refresh(); this->refresh();
ESP_LOGI(TAG, "Custom preset %s applied", custom_preset.c_str()); ESP_LOGI(TAG, "Custom preset %s applied", custom_preset);
} else { } 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 { } 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 /// Change to a provided preset setting; will reset temperature, mode, fan, and swing modes accordingly
void change_preset_(climate::ClimatePreset preset); void change_preset_(climate::ClimatePreset preset);
/// Change to a provided custom preset setting; will reset temperature, mode, fan, and swing modes accordingly /// 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. /// Applies the temperature, mode, fan, and swing modes of the provided config.
/// This is agnostic of custom vs built in preset /// 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()) for (climate::ClimatePreset m : traits.get_supported_presets())
opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); 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>(); JsonArray opt = root["custom_presets"].to<JsonArray>();
for (auto const &custom_preset : traits.get_supported_custom_presets()) for (auto const &custom_preset : traits.get_supported_custom_presets())
opt.add(custom_preset); 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()) { 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())); 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()) { if (!traits.get_supported_custom_fan_modes().empty() && obj->has_custom_fan_mode()) {
root["custom_fan_mode"] = obj->custom_fan_mode.value().c_str(); root["custom_fan_mode"] = obj->get_custom_fan_mode();
} }
if (traits.get_supports_presets() && obj->preset.has_value()) { if (traits.get_supports_presets() && obj->preset.has_value()) {
root["preset"] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); root["preset"] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value()));
} }
if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) {
root["custom_preset"] = obj->custom_preset.value().c_str(); root["custom_preset"] = obj->get_custom_preset();
} }
if (traits.get_supports_swing_modes()) { if (traits.get_supports_swing_modes()) {
root["swing_mode"] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); 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 # Other types need the actual value
# Special handling for const char* elements # Special handling for const char* elements
if self._use_pointer and "const char" in self._container_no_template: 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 += 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: else:
auto_ref = "" if self._ti_is_bool else "&" auto_ref = "" if self._ti_is_bool else "&"
o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" 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"
)