mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'integration' into memory_api
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; | ||||
|   | ||||
| @@ -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}; | ||||
|   | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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:"); | ||||
|   | ||||
| @@ -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,52 @@ 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_; } | ||||
|   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<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_; } | ||||
|   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 +236,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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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); | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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}; | ||||
|   | ||||
| @@ -321,9 +321,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; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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