1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-14 22:05:54 +00:00

Compare commits

...

11 Commits

Author SHA1 Message Date
J. Nick Koston
d43d23a4c7 preen 2025-10-30 10:27:27 -05:00
J. Nick Koston
7fef54ba79 preen 2025-10-30 10:26:11 -05:00
J. Nick Koston
68caee3226 preen 2025-10-30 10:24:20 -05:00
J. Nick Koston
9687ba3c36 Update esphome/components/thermostat/climate.py 2025-10-30 10:22:53 -05:00
J. Nick Koston
e2b3dec9d9 Merge branch 'climate_store_flash' into climate_store_flash_thermostat 2025-10-30 10:21:08 -05:00
J. Nick Koston
d94c7b9c12 [climate] Replace std::vector<std::string> with const char* for custom fan modes and presets 2025-10-30 10:20:21 -05:00
J. Nick Koston
ca6e8e0cc5 preen 2025-10-30 10:15:10 -05:00
J. Nick Koston
c87b53a666 wip 2025-10-30 10:00:11 -05:00
J. Nick Koston
e1b6f84348 wip 2025-10-30 09:59:23 -05:00
J. Nick Koston
0433a84202 fix 2025-10-30 09:58:21 -05:00
J. Nick Koston
0d8ce7fdc9 wip 2025-10-30 09:54:16 -05:00
17 changed files with 210 additions and 84 deletions

View File

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

View File

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

View File

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

View File

@@ -50,21 +50,13 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
// Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead.
climate::CLIMATE_PRESET_BOOST,
});
// String literals are stored in rodata and valid for program lifetime
traits.set_supported_custom_presets({
// We could fetch biodata from bedjet and set these names that way.
// But then we have to invert the lookup in order to send the right preset.
// For now, we can leave them as M1-3 to match the remote buttons.
// EXT HT added to match remote button.
"EXT HT",
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
"M1",
"M2",
"M3",
});
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
traits.add_supported_custom_preset("LTD HT");
} else {
traits.add_supported_custom_preset("EXT HT");
}
traits.set_visual_min_temperature(19.0);
traits.set_visual_max_temperature(43.0);
traits.set_visual_temperature_step(1.0);

View File

@@ -387,8 +387,8 @@ void Climate::save_state_() {
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, custom_fan_mode.value().c_str()) == 0) {
state.custom_fan_mode = i;
break;
}
@@ -404,8 +404,8 @@ void Climate::save_state_() {
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, custom_preset.value().c_str()) == 0) {
state.custom_preset = i;
break;
}
@@ -527,7 +527,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_ = std::string(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 +535,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_ = std::string(traits.get_supported_custom_presets()[this->custom_preset]);
}
} else if (traits.supports_preset(this->preset)) {
call.set_preset(this->preset);
@@ -562,7 +562,7 @@ 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 = std::string(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;
@@ -571,7 +571,7 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
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 = std::string(traits.get_supported_custom_presets()[this->custom_preset]);
}
} else if (traits.supports_preset(this->preset)) {
climate->preset = this->preset;
@@ -656,8 +656,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 +666,8 @@ void Climate::dump_traits_(const char *tag) {
}
if (!traits.get_supported_custom_presets().empty()) {
ESP_LOGCONFIG(tag, " Supported custom presets:");
for (const std::string &s : traits.get_supported_custom_presets())
ESP_LOGCONFIG(tag, " - %s", s.c_str());
for (const char *s : traits.get_supported_custom_presets())
ESP_LOGCONFIG(tag, " - %s", s);
}
if (!traits.get_supported_swing_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported swing modes:");

View File

@@ -1,5 +1,6 @@
#pragma once
#include <cstring>
#include <vector>
#include "climate_mode.h"
#include "esphome/core/finite_set_mask.h"
@@ -18,16 +19,6 @@ 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)
// 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)
return true;
}
return false;
}
/** This class contains all static data for climate devices.
*
* All climate devices must support these features:
@@ -128,46 +119,46 @@ 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<std::string> modes) {
void set_supported_custom_fan_modes(std::initializer_list<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);
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) {
this->supported_custom_fan_modes_ = modes;
}
const std::vector<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
const std::vector<const char *> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode);
for (const char *mode : this->supported_custom_fan_modes_) {
if (strcmp(mode, custom_fan_mode.c_str()) == 0)
return true;
}
return false;
}
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<std::string> presets) {
void set_supported_custom_presets(std::initializer_list<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);
void set_supported_custom_presets(const std::vector<const char *> &presets) {
this->supported_custom_presets_ = presets;
}
const std::vector<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
const std::vector<const char *> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
bool supports_custom_preset(const std::string &custom_preset) const {
return vector_contains(this->supported_custom_presets_, custom_preset);
for (const char *preset : this->supported_custom_presets_) {
if (strcmp(preset, custom_preset.c_str()) == 0)
return true;
}
return false;
}
void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; }
@@ -239,8 +230,11 @@ 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_;
// Store const char* pointers to avoid std::string overhead
// Pointers must remain valid for traits lifetime (typically string literals in rodata,
// or pointers to strings with sufficient lifetime like member variables)
std::vector<const char *> supported_custom_fan_modes_;
std::vector<const char *> supported_custom_presets_;
};
} // namespace climate

View File

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

View File

@@ -20,9 +20,9 @@ using MideaPreset = dudanov::midea::ac::Preset;
class Constants {
public:
static const char *const TAG;
static const std::string FREEZE_PROTECTION;
static const std::string SILENT;
static const std::string TURBO;
static const char *const FREEZE_PROTECTION;
static const char *const SILENT;
static const char *const TURBO;
};
class Converters {
@@ -35,12 +35,12 @@ class Converters {
static MideaPreset to_midea_preset(const std::string &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 bool is_custom_midea_fan_mode(MideaFanMode fan_mode);
static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode);
static const std::string &to_custom_climate_fan_mode(MideaFanMode fan_mode);
static const char *to_custom_climate_fan_mode(MideaFanMode fan_mode);
static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities);
};

View File

@@ -84,8 +84,10 @@ ClimateTraits AirConditioner::traits() {
traits.set_supported_modes(this->supported_modes_);
traits.set_supported_swing_modes(this->supported_swing_modes_);
traits.set_supported_presets(this->supported_presets_);
traits.set_supported_custom_presets(this->supported_custom_presets_);
traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_);
if (!this->supported_custom_presets_.empty())
traits.set_supported_custom_presets(this->supported_custom_presets_);
if (!this->supported_custom_fan_modes_.empty())
traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_);
/* + MINIMAL SET OF CAPABILITIES */
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW);

View File

@@ -46,8 +46,8 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; }
void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; }
void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; }
void set_custom_presets(const std::vector<std::string> &presets) { this->supported_custom_presets_ = presets; }
void set_custom_fan_modes(const std::vector<std::string> &modes) { this->supported_custom_fan_modes_ = modes; }
void set_custom_presets(std::initializer_list<const char *> presets) { this->supported_custom_presets_ = presets; }
void set_custom_fan_modes(std::initializer_list<const char *> modes) { this->supported_custom_fan_modes_ = modes; }
protected:
void control(const ClimateCall &call) override;
@@ -55,8 +55,8 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
ClimateModeMask supported_modes_{};
ClimateSwingModeMask supported_swing_modes_{};
ClimatePresetMask supported_presets_{};
std::vector<std::string> supported_custom_presets_{};
std::vector<std::string> supported_custom_fan_modes_{};
std::vector<const char *> supported_custom_presets_{};
std::vector<const char *> supported_custom_fan_modes_{};
Sensor *outdoor_sensor_{nullptr};
Sensor *humidity_sensor_{nullptr};
Sensor *power_sensor_{nullptr};

View File

@@ -9,6 +9,8 @@ from esphome.const import (
CONF_COOL_DEADBAND,
CONF_COOL_MODE,
CONF_COOL_OVERRUN,
CONF_CUSTOM_FAN_MODES,
CONF_CUSTOM_PRESETS,
CONF_DEFAULT_MODE,
CONF_DEFAULT_TARGET_TEMPERATURE_HIGH,
CONF_DEFAULT_TARGET_TEMPERATURE_LOW,
@@ -658,6 +660,8 @@ CONFIG_SCHEMA = cv.All(
}
),
cv.Optional(CONF_PRESET): cv.ensure_list(PRESET_CONFIG_SCHEMA),
cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list(cv.string_strict),
cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(cv.string_strict),
cv.Optional(CONF_ON_BOOT_RESTORE_FROM): validate_on_boot_restore_from,
cv.Optional(CONF_PRESET_CHANGE): automation.validate_automation(
single=True
@@ -1008,3 +1012,22 @@ async def to_code(config):
await automation.build_automation(
var.get_preset_change_trigger(), [], config[CONF_PRESET_CHANGE]
)
# Collect custom preset names from preset map (non-standard) and custom_presets list
custom_preset_names = [
preset_config[CONF_NAME]
for preset_config in config.get(CONF_PRESET, [])
if preset_config[CONF_NAME].upper() not in climate.CLIMATE_PRESETS
]
custom_preset_names.extend(config.get(CONF_CUSTOM_PRESETS, []))
if custom_preset_names:
cg.add(var.set_custom_presets(custom_preset_names))
# Collect custom fan modes (filter out standard enum fan modes)
custom_fan_modes = [
mode
for mode in config.get(CONF_CUSTOM_FAN_MODES, [])
if mode.upper() not in climate.CLIMATE_FAN_MODES
]
if custom_fan_modes:
cg.add(var.set_custom_fan_modes(custom_fan_modes))

View File

@@ -321,8 +321,12 @@ 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);
// Custom presets and fan modes are set directly from Python (includes both map entries and additional lists)
if (!this->additional_custom_presets_.empty()) {
traits.set_supported_custom_presets(this->additional_custom_presets_);
}
if (!this->additional_custom_fan_modes_.empty()) {
traits.set_supported_custom_fan_modes(this->additional_custom_fan_modes_);
}
return traits;
}
@@ -1248,6 +1252,14 @@ void ThermostatClimate::set_custom_preset_config(const std::string &name,
this->custom_preset_config_[name] = config;
}
void ThermostatClimate::set_custom_fan_modes(std::initializer_list<const char *> custom_fan_modes) {
this->additional_custom_fan_modes_ = custom_fan_modes;
}
void ThermostatClimate::set_custom_presets(std::initializer_list<const char *> custom_presets) {
this->additional_custom_presets_ = custom_presets;
}
ThermostatClimate::ThermostatClimate()
: cool_action_trigger_(new Trigger<>()),
supplemental_cool_action_trigger_(new Trigger<>()),

View File

@@ -133,6 +133,8 @@ class ThermostatClimate : public climate::Climate, public Component {
void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config);
void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config);
void set_custom_fan_modes(std::initializer_list<const char *> custom_fan_modes);
void set_custom_presets(std::initializer_list<const char *> custom_presets);
Trigger<> *get_cool_action_trigger() const;
Trigger<> *get_supplemental_cool_action_trigger() const;
@@ -537,6 +539,10 @@ class ThermostatClimate : public climate::Climate, public Component {
std::map<climate::ClimatePreset, ThermostatClimateTargetTempConfig> preset_config_{};
/// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset")
std::map<std::string, ThermostatClimateTargetTempConfig> custom_preset_config_{};
/// Additional custom fan modes to expose (beyond those with actions)
std::vector<const char *> additional_custom_fan_modes_{};
/// Additional custom presets to expose (beyond those in custom_preset_config_)
std::vector<const char *> additional_custom_presets_{};
};
} // namespace thermostat

View File

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

View File

@@ -15,6 +15,12 @@ climate:
- name: Away
default_target_temperature_low: 16°C
default_target_temperature_high: 20°C
custom_fan_modes:
- "Custom Fan 1"
- "Custom Fan 2"
custom_presets:
- "Custom Preset 1"
- "Custom Preset 2"
idle_action:
- logger.log: idle_action
cool_action:

View File

@@ -0,0 +1,39 @@
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
custom_fan_modes:
- "Turbo"
- "Silent"
- "Sleep Mode"
custom_presets:
- "Eco Plus"
- "Comfort"
- "Vacation Mode"
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,51 @@
"""Integration test for climate custom fan modes and 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 fan modes and 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 custom fan modes are exposed
custom_fan_modes = test_climate.supported_custom_fan_modes
assert len(custom_fan_modes) == 3, (
f"Expected 3 custom fan modes, got {len(custom_fan_modes)}"
)
assert "Turbo" in custom_fan_modes, "Expected 'Turbo' in custom fan modes"
assert "Silent" in custom_fan_modes, "Expected 'Silent' in custom fan modes"
assert "Sleep Mode" in custom_fan_modes, (
"Expected 'Sleep Mode' in custom fan modes"
)
# 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 (from custom_presets: config list)
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 "Comfort" in custom_presets, "Expected 'Comfort' in custom presets"
assert "Vacation Mode" in custom_presets, (
"Expected 'Vacation Mode' in custom presets"
)