mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	[climate] Replace std::set with FiniteSetMask for trait storage (#11466)
This commit is contained in:
		| @@ -989,7 +989,7 @@ message ListEntitiesClimateResponse { | ||||
|  | ||||
|   bool supports_current_temperature = 5; // Deprecated: use feature_flags | ||||
|   bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags | ||||
|   repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"]; | ||||
|   repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"]; | ||||
|   float visual_min_temperature = 8; | ||||
|   float visual_max_temperature = 9; | ||||
|   float visual_target_temperature_step = 10; | ||||
| @@ -998,11 +998,11 @@ message ListEntitiesClimateResponse { | ||||
|   // Deprecated in API version 1.5 | ||||
|   bool legacy_supports_away = 11 [deprecated=true]; | ||||
|   bool supports_action = 12; // Deprecated: use feature_flags | ||||
|   repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"]; | ||||
|   repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"]; | ||||
|   repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"]; | ||||
|   repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set<climate::ClimatePreset>"]; | ||||
|   repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"]; | ||||
|   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 ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; | ||||
|   repeated string supported_custom_presets = 17 [(container_pointer) = "std::vector"]; | ||||
|   bool disabled_by_default = 18; | ||||
|   string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; | ||||
|   EntityCategory entity_category = 20; | ||||
|   | ||||
| @@ -669,18 +669,18 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection | ||||
|   msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); | ||||
|   // Current feature flags and other supported parameters | ||||
|   msg.feature_flags = traits.get_feature_flags(); | ||||
|   msg.supported_modes = &traits.get_supported_modes_for_api_(); | ||||
|   msg.supported_modes = &traits.get_supported_modes(); | ||||
|   msg.visual_min_temperature = traits.get_visual_min_temperature(); | ||||
|   msg.visual_max_temperature = traits.get_visual_max_temperature(); | ||||
|   msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); | ||||
|   msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); | ||||
|   msg.visual_min_humidity = traits.get_visual_min_humidity(); | ||||
|   msg.visual_max_humidity = traits.get_visual_max_humidity(); | ||||
|   msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_(); | ||||
|   msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_(); | ||||
|   msg.supported_presets = &traits.get_supported_presets_for_api_(); | ||||
|   msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_(); | ||||
|   msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_(); | ||||
|   msg.supported_fan_modes = &traits.get_supported_fan_modes(); | ||||
|   msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes(); | ||||
|   msg.supported_presets = &traits.get_supported_presets(); | ||||
|   msg.supported_custom_presets = &traits.get_supported_custom_presets(); | ||||
|   msg.supported_swing_modes = &traits.get_supported_swing_modes(); | ||||
|   return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, | ||||
|                                      is_single); | ||||
| } | ||||
|   | ||||
| @@ -1377,16 +1377,16 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { | ||||
| #endif | ||||
|   bool supports_current_temperature{false}; | ||||
|   bool supports_two_point_target_temperature{false}; | ||||
|   const std::set<climate::ClimateMode> *supported_modes{}; | ||||
|   const climate::ClimateModeMask *supported_modes{}; | ||||
|   float visual_min_temperature{0.0f}; | ||||
|   float visual_max_temperature{0.0f}; | ||||
|   float visual_target_temperature_step{0.0f}; | ||||
|   bool supports_action{false}; | ||||
|   const std::set<climate::ClimateFanMode> *supported_fan_modes{}; | ||||
|   const std::set<climate::ClimateSwingMode> *supported_swing_modes{}; | ||||
|   const std::set<std::string> *supported_custom_fan_modes{}; | ||||
|   const std::set<climate::ClimatePreset> *supported_presets{}; | ||||
|   const std::set<std::string> *supported_custom_presets{}; | ||||
|   const climate::ClimateFanModeMask *supported_fan_modes{}; | ||||
|   const climate::ClimateSwingModeMask *supported_swing_modes{}; | ||||
|   const std::vector<std::string> *supported_custom_fan_modes{}; | ||||
|   const climate::ClimatePresetMask *supported_presets{}; | ||||
|   const std::vector<std::string> *supported_custom_presets{}; | ||||
|   float visual_current_temperature_step{0.0f}; | ||||
|   bool supports_current_humidity{false}; | ||||
|   bool supports_target_humidity{false}; | ||||
|   | ||||
| @@ -99,9 +99,8 @@ enum BedjetCommand : uint8_t { | ||||
|  | ||||
| static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; | ||||
|  | ||||
| static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; | ||||
| static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; | ||||
| static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; | ||||
| static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_; | ||||
|  | ||||
| }  // namespace bedjet | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -43,7 +43,7 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli | ||||
|     }); | ||||
|  | ||||
|     // It would be better if we had a slider for the fan modes. | ||||
|     traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET); | ||||
|     traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES); | ||||
|     traits.set_supported_presets({ | ||||
|         // If we support NONE, then have to decide what happens if the user switches to it (turn off?) | ||||
|         // climate::CLIMATE_PRESET_NONE, | ||||
|   | ||||
| @@ -385,7 +385,7 @@ void Climate::save_state_() { | ||||
|   if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { | ||||
|     state.uses_custom_fan_mode = true; | ||||
|     const auto &supported = traits.get_supported_custom_fan_modes(); | ||||
|     // std::set has consistent order (lexicographic for strings) | ||||
|     // std::vector maintains insertion order | ||||
|     size_t i = 0; | ||||
|     for (const auto &mode : supported) { | ||||
|       if (mode == custom_fan_mode) { | ||||
| @@ -402,7 +402,7 @@ void Climate::save_state_() { | ||||
|   if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { | ||||
|     state.uses_custom_preset = true; | ||||
|     const auto &supported = traits.get_supported_custom_presets(); | ||||
|     // std::set has consistent order (lexicographic for strings) | ||||
|     // std::vector maintains insertion order | ||||
|     size_t i = 0; | ||||
|     for (const auto &preset : supported) { | ||||
|       if (preset == custom_preset) { | ||||
|   | ||||
| @@ -7,6 +7,7 @@ namespace esphome { | ||||
| namespace climate { | ||||
|  | ||||
| /// Enum for all modes a climate device can be in. | ||||
| /// NOTE: If adding values, update ClimateModeMask in climate_traits.h to use the new last value | ||||
| enum ClimateMode : uint8_t { | ||||
|   /// The climate device is off | ||||
|   CLIMATE_MODE_OFF = 0, | ||||
| @@ -24,7 +25,7 @@ enum ClimateMode : uint8_t { | ||||
|    * For example, the target temperature can be adjusted based on a schedule, or learned behavior. | ||||
|    * The target temperature can't be adjusted when in this mode. | ||||
|    */ | ||||
|   CLIMATE_MODE_AUTO = 6 | ||||
|   CLIMATE_MODE_AUTO = 6  // Update ClimateModeMask in climate_traits.h if adding values after this | ||||
| }; | ||||
|  | ||||
| /// Enum for the current action of the climate device. Values match those of ClimateMode. | ||||
| @@ -43,6 +44,7 @@ enum ClimateAction : uint8_t { | ||||
|   CLIMATE_ACTION_FAN = 6, | ||||
| }; | ||||
|  | ||||
| /// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value | ||||
| enum ClimateFanMode : uint8_t { | ||||
|   /// The fan mode is set to On | ||||
|   CLIMATE_FAN_ON = 0, | ||||
| @@ -63,10 +65,11 @@ enum ClimateFanMode : uint8_t { | ||||
|   /// The fan mode is set to Diffuse | ||||
|   CLIMATE_FAN_DIFFUSE = 8, | ||||
|   /// The fan mode is set to Quiet | ||||
|   CLIMATE_FAN_QUIET = 9, | ||||
|   CLIMATE_FAN_QUIET = 9,  // Update ClimateFanModeMask in climate_traits.h if adding values after this | ||||
| }; | ||||
|  | ||||
| /// Enum for all modes a climate swing can be in | ||||
| /// NOTE: If adding values, update ClimateSwingModeMask in climate_traits.h to use the new last value | ||||
| enum ClimateSwingMode : uint8_t { | ||||
|   /// The swing mode is set to Off | ||||
|   CLIMATE_SWING_OFF = 0, | ||||
| @@ -75,10 +78,11 @@ enum ClimateSwingMode : uint8_t { | ||||
|   /// The fan mode is set to Vertical | ||||
|   CLIMATE_SWING_VERTICAL = 2, | ||||
|   /// The fan mode is set to Horizontal | ||||
|   CLIMATE_SWING_HORIZONTAL = 3, | ||||
|   CLIMATE_SWING_HORIZONTAL = 3,  // Update ClimateSwingModeMask in climate_traits.h if adding values after this | ||||
| }; | ||||
|  | ||||
| /// Enum for all preset modes | ||||
| /// NOTE: If adding values, update ClimatePresetMask in climate_traits.h to use the new last value | ||||
| enum ClimatePreset : uint8_t { | ||||
|   /// No preset is active | ||||
|   CLIMATE_PRESET_NONE = 0, | ||||
| @@ -95,7 +99,7 @@ enum ClimatePreset : uint8_t { | ||||
|   /// Device is prepared for sleep | ||||
|   CLIMATE_PRESET_SLEEP = 6, | ||||
|   /// Device is reacting to activity (e.g., movement sensors) | ||||
|   CLIMATE_PRESET_ACTIVITY = 7, | ||||
|   CLIMATE_PRESET_ACTIVITY = 7,  // Update ClimatePresetMask in climate_traits.h if adding values after this | ||||
| }; | ||||
|  | ||||
| enum ClimateFeature : uint32_t { | ||||
|   | ||||
| @@ -1,19 +1,33 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <set> | ||||
| #include <vector> | ||||
| #include "climate_mode.h" | ||||
| #include "esphome/core/finite_set_mask.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| #ifdef USE_API | ||||
| namespace api { | ||||
| class APIConnection; | ||||
| }  // namespace api | ||||
| #endif | ||||
|  | ||||
| namespace climate { | ||||
|  | ||||
| // Type aliases for climate enum bitmasks | ||||
| // These replace std::set<EnumType> to eliminate red-black tree overhead | ||||
| // For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position) | ||||
| // Bitmask size is automatically calculated from the last enum value | ||||
| using ClimateModeMask = FiniteSetMask<ClimateMode, DefaultBitPolicy<ClimateMode, CLIMATE_MODE_AUTO + 1>>; | ||||
| using ClimateFanModeMask = FiniteSetMask<ClimateFanMode, DefaultBitPolicy<ClimateFanMode, CLIMATE_FAN_QUIET + 1>>; | ||||
| 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: | ||||
| @@ -107,48 +121,60 @@ class ClimateTraits { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); } | ||||
|   void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } | ||||
|   void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } | ||||
|   bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); } | ||||
|   const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; } | ||||
|   const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; } | ||||
|  | ||||
|   void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); } | ||||
|   void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; } | ||||
|   void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } | ||||
|   void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_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 std::set<ClimateFanMode> &get_supported_fan_modes() const { return this->supported_fan_modes_; } | ||||
|   const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; } | ||||
|  | ||||
|   void set_supported_custom_fan_modes(std::set<std::string> supported_custom_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); | ||||
|   } | ||||
|   const std::set<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } | ||||
|   void set_supported_custom_fan_modes(std::initializer_list<std::string> 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 { | ||||
|     return this->supported_custom_fan_modes_.count(custom_fan_mode); | ||||
|     return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); | ||||
|   } | ||||
|  | ||||
|   void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); } | ||||
|   void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } | ||||
|   void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } | ||||
|   void add_supported_custom_preset(const std::string &preset) { this->supported_custom_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 std::set<climate::ClimatePreset> &get_supported_presets() const { return this->supported_presets_; } | ||||
|   const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } | ||||
|  | ||||
|   void set_supported_custom_presets(std::set<std::string> supported_custom_presets) { | ||||
|   void set_supported_custom_presets(std::vector<std::string> supported_custom_presets) { | ||||
|     this->supported_custom_presets_ = std::move(supported_custom_presets); | ||||
|   } | ||||
|   const std::set<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; } | ||||
|   void set_supported_custom_presets(std::initializer_list<std::string> 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 { | ||||
|     return this->supported_custom_presets_.count(custom_preset); | ||||
|     return vector_contains(this->supported_custom_presets_, custom_preset); | ||||
|   } | ||||
|  | ||||
|   void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); } | ||||
|   void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } | ||||
|   void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } | ||||
|   bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); } | ||||
|   bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); } | ||||
|   const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; } | ||||
|   const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; } | ||||
|  | ||||
|   float get_visual_min_temperature() const { return this->visual_min_temperature_; } | ||||
|   void set_visual_min_temperature(float visual_min_temperature) { | ||||
| @@ -179,23 +205,6 @@ class ClimateTraits { | ||||
|   void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; } | ||||
|  | ||||
|  protected: | ||||
| #ifdef USE_API | ||||
|   // The API connection is a friend class to access internal methods | ||||
|   friend class api::APIConnection; | ||||
|   // These methods return references to internal data structures. | ||||
|   // They are used by the API to avoid copying data when encoding messages. | ||||
|   // Warning: Do not use these methods outside of the API connection code. | ||||
|   // They return references to internal data that can be invalidated. | ||||
|   const std::set<ClimateMode> &get_supported_modes_for_api_() const { return this->supported_modes_; } | ||||
|   const std::set<ClimateFanMode> &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; } | ||||
|   const std::set<std::string> &get_supported_custom_fan_modes_for_api_() const { | ||||
|     return this->supported_custom_fan_modes_; | ||||
|   } | ||||
|   const std::set<climate::ClimatePreset> &get_supported_presets_for_api_() const { return this->supported_presets_; } | ||||
|   const std::set<std::string> &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; } | ||||
|   const std::set<ClimateSwingMode> &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; } | ||||
| #endif | ||||
|  | ||||
|   void set_mode_support_(climate::ClimateMode mode, bool supported) { | ||||
|     if (supported) { | ||||
|       this->supported_modes_.insert(mode); | ||||
| @@ -226,12 +235,12 @@ class ClimateTraits { | ||||
|   float visual_min_humidity_{30}; | ||||
|   float visual_max_humidity_{99}; | ||||
|  | ||||
|   std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF}; | ||||
|   std::set<climate::ClimateFanMode> supported_fan_modes_; | ||||
|   std::set<climate::ClimateSwingMode> supported_swing_modes_; | ||||
|   std::set<climate::ClimatePreset> supported_presets_; | ||||
|   std::set<std::string> supported_custom_fan_modes_; | ||||
|   std::set<std::string> supported_custom_presets_; | ||||
|   climate::ClimateModeMask supported_modes_{climate::CLIMATE_MODE_OFF}; | ||||
|   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_; | ||||
| }; | ||||
|  | ||||
| }  // namespace climate | ||||
|   | ||||
| @@ -24,16 +24,18 @@ class ClimateIR : public Component, | ||||
|                   public remote_base::RemoteTransmittable { | ||||
|  public: | ||||
|   ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, | ||||
|             bool supports_dry = false, bool supports_fan_only = false, std::set<climate::ClimateFanMode> fan_modes = {}, | ||||
|             std::set<climate::ClimateSwingMode> swing_modes = {}, std::set<climate::ClimatePreset> presets = {}) { | ||||
|             bool supports_dry = false, bool supports_fan_only = false, | ||||
|             climate::ClimateFanModeMask fan_modes = climate::ClimateFanModeMask(), | ||||
|             climate::ClimateSwingModeMask swing_modes = climate::ClimateSwingModeMask(), | ||||
|             climate::ClimatePresetMask presets = climate::ClimatePresetMask()) { | ||||
|     this->minimum_temperature_ = minimum_temperature; | ||||
|     this->maximum_temperature_ = maximum_temperature; | ||||
|     this->temperature_step_ = temperature_step; | ||||
|     this->supports_dry_ = supports_dry; | ||||
|     this->supports_fan_only_ = supports_fan_only; | ||||
|     this->fan_modes_ = std::move(fan_modes); | ||||
|     this->swing_modes_ = std::move(swing_modes); | ||||
|     this->presets_ = std::move(presets); | ||||
|     this->fan_modes_ = fan_modes; | ||||
|     this->swing_modes_ = swing_modes; | ||||
|     this->presets_ = presets; | ||||
|   } | ||||
|  | ||||
|   void setup() override; | ||||
| @@ -60,9 +62,9 @@ class ClimateIR : public Component, | ||||
|   bool supports_heat_{true}; | ||||
|   bool supports_dry_{false}; | ||||
|   bool supports_fan_only_{false}; | ||||
|   std::set<climate::ClimateFanMode> fan_modes_ = {}; | ||||
|   std::set<climate::ClimateSwingMode> swing_modes_ = {}; | ||||
|   std::set<climate::ClimatePreset> presets_ = {}; | ||||
|   climate::ClimateFanModeMask fan_modes_{}; | ||||
|   climate::ClimateSwingModeMask swing_modes_{}; | ||||
|   climate::ClimatePresetMask presets_{}; | ||||
|  | ||||
|   sensor::Sensor *sensor_{nullptr}; | ||||
| }; | ||||
|   | ||||
| @@ -171,7 +171,7 @@ void HaierClimateBase::toggle_power() { | ||||
|       PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional<haier_protocol::HaierMessage>()}); | ||||
| } | ||||
|  | ||||
| void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) { | ||||
| void HaierClimateBase::set_supported_swing_modes(climate::ClimateSwingModeMask modes) { | ||||
|   this->traits_.set_supported_swing_modes(modes); | ||||
|   if (!modes.empty()) | ||||
|     this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); | ||||
| @@ -179,13 +179,13 @@ void HaierClimateBase::set_supported_swing_modes(const std::set<climate::Climate | ||||
|  | ||||
| void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); } | ||||
|  | ||||
| void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) { | ||||
| void HaierClimateBase::set_supported_modes(climate::ClimateModeMask modes) { | ||||
|   this->traits_.set_supported_modes(modes); | ||||
|   this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF);        // Always available | ||||
|   this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);  // Always available | ||||
| } | ||||
|  | ||||
| void HaierClimateBase::set_supported_presets(const std::set<climate::ClimatePreset> &presets) { | ||||
| void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets) { | ||||
|   this->traits_.set_supported_presets(presets); | ||||
|   if (!presets.empty()) | ||||
|     this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE); | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <chrono> | ||||
| #include <set> | ||||
| #include "esphome/components/climate/climate.h" | ||||
| #include "esphome/components/uart/uart.h" | ||||
| #include "esphome/core/automation.h" | ||||
| @@ -60,9 +59,9 @@ class HaierClimateBase : public esphome::Component, | ||||
|   void send_power_off_command(); | ||||
|   void toggle_power(); | ||||
|   void reset_protocol() { this->reset_protocol_request_ = true; }; | ||||
|   void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes); | ||||
|   void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes); | ||||
|   void set_supported_presets(const std::set<esphome::climate::ClimatePreset> &presets); | ||||
|   void set_supported_modes(esphome::climate::ClimateModeMask modes); | ||||
|   void set_supported_swing_modes(esphome::climate::ClimateSwingModeMask modes); | ||||
|   void set_supported_presets(esphome::climate::ClimatePresetMask presets); | ||||
|   bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; }; | ||||
|   size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; | ||||
|   size_t read_array(uint8_t *data, size_t len) noexcept override { | ||||
|   | ||||
| @@ -1033,9 +1033,9 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * | ||||
|   { | ||||
|     // Swing mode | ||||
|     ClimateSwingMode old_swing_mode = this->swing_mode; | ||||
|     const std::set<ClimateSwingMode> &swing_modes = traits_.get_supported_swing_modes(); | ||||
|     bool vertical_swing_supported = swing_modes.find(CLIMATE_SWING_VERTICAL) != swing_modes.end(); | ||||
|     bool horizontal_swing_supported = swing_modes.find(CLIMATE_SWING_HORIZONTAL) != swing_modes.end(); | ||||
|     const auto &swing_modes = traits_.get_supported_swing_modes(); | ||||
|     bool vertical_swing_supported = swing_modes.count(CLIMATE_SWING_VERTICAL); | ||||
|     bool horizontal_swing_supported = swing_modes.count(CLIMATE_SWING_HORIZONTAL); | ||||
|     if (horizontal_swing_supported && | ||||
|         (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) { | ||||
|       if (vertical_swing_supported && | ||||
| @@ -1218,13 +1218,13 @@ void HonClimate::fill_control_messages_queue_() { | ||||
|                                                 (uint8_t) hon_protocol::DataParameters::QUIET_MODE, | ||||
|                                             quiet_mode_buf, 2); | ||||
|     } | ||||
|     if ((fast_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_BOOST) != presets.end()))) { | ||||
|     if ((fast_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_BOOST)) { | ||||
|       this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, | ||||
|                                             (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + | ||||
|                                                 (uint8_t) hon_protocol::DataParameters::FAST_MODE, | ||||
|                                             fast_mode_buf, 2); | ||||
|     } | ||||
|     if ((away_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_AWAY) != presets.end()))) { | ||||
|     if ((away_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_AWAY)) { | ||||
|       this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, | ||||
|                                             (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + | ||||
|                                                 (uint8_t) hon_protocol::DataParameters::TEN_DEGREE, | ||||
|   | ||||
| @@ -97,11 +97,10 @@ const float TEMP_MAX = 100;  // Celsius | ||||
| class HeatpumpIRClimate : public climate_ir::ClimateIR { | ||||
|  public: | ||||
|   HeatpumpIRClimate() | ||||
|       : climate_ir::ClimateIR( | ||||
|             TEMP_MIN, TEMP_MAX, 1.0f, true, true, | ||||
|             std::set<climate::ClimateFanMode>{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, | ||||
|                                               climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_AUTO}, | ||||
|             std::set<climate::ClimateSwingMode>{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, | ||||
|       : climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true, | ||||
|                               {climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, | ||||
|                                climate::CLIMATE_FAN_AUTO}, | ||||
|                               {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, | ||||
|                                climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {} | ||||
|   void setup() override; | ||||
|   void set_protocol(Protocol protocol) { this->protocol_ = protocol; } | ||||
|   | ||||
| @@ -19,6 +19,9 @@ using climate::ClimateTraits; | ||||
| using climate::ClimateMode; | ||||
| using climate::ClimateSwingMode; | ||||
| using climate::ClimateFanMode; | ||||
| using climate::ClimateModeMask; | ||||
| using climate::ClimateSwingModeMask; | ||||
| using climate::ClimatePresetMask; | ||||
|  | ||||
| class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>, public climate::Climate { | ||||
|  public: | ||||
| @@ -40,20 +43,20 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>, | ||||
|   void do_power_on() { this->base_.setPowerState(true); } | ||||
|   void do_power_off() { this->base_.setPowerState(false); } | ||||
|   void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); } | ||||
|   void set_supported_modes(const std::set<ClimateMode> &modes) { this->supported_modes_ = modes; } | ||||
|   void set_supported_swing_modes(const std::set<ClimateSwingMode> &modes) { this->supported_swing_modes_ = modes; } | ||||
|   void set_supported_presets(const std::set<ClimatePreset> &presets) { this->supported_presets_ = presets; } | ||||
|   void set_custom_presets(const std::set<std::string> &presets) { this->supported_custom_presets_ = presets; } | ||||
|   void set_custom_fan_modes(const std::set<std::string> &modes) { this->supported_custom_fan_modes_ = modes; } | ||||
|   void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } | ||||
|   void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } | ||||
|   void set_supported_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; } | ||||
|  | ||||
|  protected: | ||||
|   void control(const ClimateCall &call) override; | ||||
|   ClimateTraits traits() override; | ||||
|   std::set<ClimateMode> supported_modes_{}; | ||||
|   std::set<ClimateSwingMode> supported_swing_modes_{}; | ||||
|   std::set<ClimatePreset> supported_presets_{}; | ||||
|   std::set<std::string> supported_custom_presets_{}; | ||||
|   std::set<std::string> supported_custom_fan_modes_{}; | ||||
|   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_{}; | ||||
|   Sensor *outdoor_sensor_{nullptr}; | ||||
|   Sensor *humidity_sensor_{nullptr}; | ||||
|   Sensor *power_sensor_{nullptr}; | ||||
|   | ||||
| @@ -40,6 +40,10 @@ enum OnBootRestoreFrom : uint8_t { | ||||
| }; | ||||
|  | ||||
| struct ThermostatClimateTimer { | ||||
|   ThermostatClimateTimer() = default; | ||||
|   ThermostatClimateTimer(bool active, uint32_t time, uint32_t started, std::function<void()> func) | ||||
|       : active(active), time(time), started(started), func(std::move(func)) {} | ||||
|  | ||||
|   bool active; | ||||
|   uint32_t time; | ||||
|   uint32_t started; | ||||
|   | ||||
| @@ -405,7 +405,7 @@ void ToshibaClimate::setup() { | ||||
|   this->swing_modes_ = this->toshiba_swing_modes_(); | ||||
|  | ||||
|   // Ensure swing mode is always initialized to a valid value | ||||
|   if (this->swing_modes_.empty() || this->swing_modes_.find(this->swing_mode) == this->swing_modes_.end()) { | ||||
|   if (this->swing_modes_.empty() || !this->swing_modes_.count(this->swing_mode)) { | ||||
|     // No swing support for this model or current swing mode not supported, reset to OFF | ||||
|     this->swing_mode = climate::CLIMATE_SWING_OFF; | ||||
|   } | ||||
|   | ||||
| @@ -71,10 +71,10 @@ class ToshibaClimate : public climate_ir::ClimateIR { | ||||
|       return TOSHIBA_RAS_2819T_TEMP_C_MAX; | ||||
|     return TOSHIBA_GENERIC_TEMP_C_MAX;  // Default to GENERIC for unknown models | ||||
|   } | ||||
|   std::set<climate::ClimateSwingMode> toshiba_swing_modes_() { | ||||
|   climate::ClimateSwingModeMask toshiba_swing_modes_() { | ||||
|     return (this->model_ == MODEL_GENERIC) | ||||
|                ? std::set<climate::ClimateSwingMode>{} | ||||
|                : std::set<climate::ClimateSwingMode>{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}; | ||||
|                ? climate::ClimateSwingModeMask() | ||||
|                : climate::ClimateSwingModeMask{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}; | ||||
|   } | ||||
|   void encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, uint8_t nbytes, uint8_t repeat); | ||||
|   bool decode_(remote_base::RemoteReceiveData *data, uint8_t *message, uint8_t nbytes); | ||||
|   | ||||
| @@ -312,18 +312,12 @@ climate::ClimateTraits TuyaClimate::traits() { | ||||
|     traits.add_supported_preset(climate::CLIMATE_PRESET_NONE); | ||||
|   } | ||||
|   if (this->swing_vertical_id_.has_value() && this->swing_horizontal_id_.has_value()) { | ||||
|     std::set<climate::ClimateSwingMode> supported_swing_modes = { | ||||
|         climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, | ||||
|         climate::CLIMATE_SWING_HORIZONTAL}; | ||||
|     traits.set_supported_swing_modes(std::move(supported_swing_modes)); | ||||
|     traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, | ||||
|                                       climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL}); | ||||
|   } else if (this->swing_vertical_id_.has_value()) { | ||||
|     std::set<climate::ClimateSwingMode> supported_swing_modes = {climate::CLIMATE_SWING_OFF, | ||||
|                                                                  climate::CLIMATE_SWING_VERTICAL}; | ||||
|     traits.set_supported_swing_modes(std::move(supported_swing_modes)); | ||||
|     traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}); | ||||
|   } else if (this->swing_horizontal_id_.has_value()) { | ||||
|     std::set<climate::ClimateSwingMode> supported_swing_modes = {climate::CLIMATE_SWING_OFF, | ||||
|                                                                  climate::CLIMATE_SWING_HORIZONTAL}; | ||||
|     traits.set_supported_swing_modes(std::move(supported_swing_modes)); | ||||
|     traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL}); | ||||
|   } | ||||
|  | ||||
|   if (fan_speed_id_) { | ||||
|   | ||||
| @@ -44,6 +44,7 @@ class InitialStateHelper: | ||||
|         helper = InitialStateHelper(entities) | ||||
|         client.subscribe_states(helper.on_state_wrapper(user_callback)) | ||||
|         await helper.wait_for_initial_states() | ||||
|         # Access initial states via helper.initial_states[key] | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, entities: list[EntityInfo]) -> None: | ||||
| @@ -63,6 +64,8 @@ class InitialStateHelper: | ||||
|         self._entities_by_id = { | ||||
|             (entity.device_id, entity.key): entity for entity in entities | ||||
|         } | ||||
|         # Store initial states by key for test access | ||||
|         self.initial_states: dict[int, EntityState] = {} | ||||
|  | ||||
|         # Log all entities | ||||
|         _LOGGER.debug( | ||||
| @@ -127,6 +130,9 @@ class InitialStateHelper: | ||||
|  | ||||
|             # If this entity is waiting for initial state | ||||
|             if entity_id in self._wait_initial_states: | ||||
|                 # Store the initial state for test access | ||||
|                 self.initial_states[state.key] = state | ||||
|  | ||||
|                 # Remove from waiting set | ||||
|                 self._wait_initial_states.discard(entity_id) | ||||
|  | ||||
|   | ||||
| @@ -2,12 +2,11 @@ | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import asyncio | ||||
|  | ||||
| import aioesphomeapi | ||||
| from aioesphomeapi import ClimateAction, ClimateMode, ClimatePreset, EntityState | ||||
| from aioesphomeapi import ClimateAction, ClimateInfo, ClimateMode, ClimatePreset | ||||
| import pytest | ||||
|  | ||||
| from .state_utils import InitialStateHelper | ||||
| from .types import APIClientConnectedFactory, RunCompiledFunction | ||||
|  | ||||
|  | ||||
| @@ -18,26 +17,27 @@ async def test_host_mode_climate_basic_state( | ||||
|     api_client_connected: APIClientConnectedFactory, | ||||
| ) -> None: | ||||
|     """Test basic climate state reporting.""" | ||||
|     loop = asyncio.get_running_loop() | ||||
|     async with run_compiled(yaml_config), api_client_connected() as client: | ||||
|         states: dict[int, EntityState] = {} | ||||
|         climate_future: asyncio.Future[EntityState] = loop.create_future() | ||||
|         # Get entities and set up state synchronization | ||||
|         entities, services = await client.list_entities_services() | ||||
|         initial_state_helper = InitialStateHelper(entities) | ||||
|         climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] | ||||
|         assert len(climate_infos) >= 1, "Expected at least 1 climate entity" | ||||
|  | ||||
|         def on_state(state: EntityState) -> None: | ||||
|             states[state.key] = state | ||||
|             if ( | ||||
|                 isinstance(state, aioesphomeapi.ClimateState) | ||||
|                 and not climate_future.done() | ||||
|             ): | ||||
|                 climate_future.set_result(state) | ||||
|  | ||||
|         client.subscribe_states(on_state) | ||||
|         # Subscribe with the wrapper (no-op callback since we just want initial states) | ||||
|         client.subscribe_states(initial_state_helper.on_state_wrapper(lambda _: None)) | ||||
|  | ||||
|         # Wait for all initial states to be broadcast | ||||
|         try: | ||||
|             climate_state = await asyncio.wait_for(climate_future, timeout=5.0) | ||||
|             await initial_state_helper.wait_for_initial_states() | ||||
|         except TimeoutError: | ||||
|             pytest.fail("Climate state not received within 5 seconds") | ||||
|             pytest.fail("Timeout waiting for initial states") | ||||
|  | ||||
|         # Get the climate entity and its initial state | ||||
|         test_climate = climate_infos[0] | ||||
|         climate_state = initial_state_helper.initial_states.get(test_climate.key) | ||||
|  | ||||
|         assert climate_state is not None, "Climate initial state not found" | ||||
|         assert isinstance(climate_state, aioesphomeapi.ClimateState) | ||||
|         assert climate_state.mode == ClimateMode.OFF | ||||
|         assert climate_state.action == ClimateAction.OFF | ||||
|   | ||||
		Reference in New Issue
	
	Block a user