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:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,25 +184,28 @@ 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()) {
|
||||
@@ -215,25 +216,28 @@ 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()) {
|
||||
@@ -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:");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,7 +84,9 @@ ClimateTraits AirConditioner::traits() {
|
||||
traits.set_supported_modes(this->supported_modes_);
|
||||
traits.set_supported_swing_modes(this->supported_swing_modes_);
|
||||
traits.set_supported_presets(this->supported_presets_);
|
||||
if (!this->supported_custom_presets_.empty())
|
||||
traits.set_supported_custom_presets(this->supported_custom_presets_);
|
||||
if (!this->supported_custom_fan_modes_.empty())
|
||||
traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_);
|
||||
/* + MINIMAL SET OF CAPABILITIES */
|
||||
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
42
tests/integration/test_climate_custom_modes.py
Normal file
42
tests/integration/test_climate_custom_modes.py
Normal 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"
|
||||
)
|
||||
Reference in New Issue
Block a user