From ebadaa966016dd9cfbc7fbd3046883ade9be0214 Mon Sep 17 00:00:00 2001
From: Lumpusz <marton.keri@gmail.com>
Date: Fri, 4 Jun 2021 12:04:54 +0200
Subject: [PATCH] Add preset, custom_preset and custom_fan_mode support to
 climate (#1471)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/api/api.proto              |  24 ++-
 esphome/components/api/api_connection.cpp     |  33 +++-
 esphome/components/api/api_pb2.cpp            | 158 ++++++++++++++++-
 esphome/components/api/api_pb2.h              | 104 +++++++-----
 esphome/components/climate/__init__.py        |  35 +++-
 esphome/components/climate/automation.h       |   6 +
 esphome/components/climate/climate.cpp        | 159 ++++++++++++++++--
 esphome/components/climate/climate.h          |  39 ++++-
 esphome/components/climate/climate_mode.cpp   |  23 +++
 esphome/components/climate/climate_mode.h     |  27 ++-
 esphome/components/climate/climate_traits.cpp |  65 +++++++
 esphome/components/climate/climate_traits.h   |  25 +++
 .../climate_ir_lg/climate_ir_lg.cpp           |   2 +-
 esphome/components/coolix/coolix.cpp          |   2 +-
 esphome/components/daikin/daikin.cpp          |   2 +-
 .../fujitsu_general/fujitsu_general.cpp       |   2 +-
 .../hitachi_ac344/hitachi_ac344.cpp           |   4 +-
 esphome/components/midea_ac/climate.py        |  35 +++-
 esphome/components/midea_ac/midea_climate.cpp |  88 +++++++++-
 esphome/components/midea_ac/midea_climate.h   |  14 ++
 esphome/components/midea_ac/midea_frame.cpp   |  72 ++++++++
 esphome/components/midea_ac/midea_frame.h     |  32 +++-
 esphome/components/mqtt/mqtt_climate.cpp      |   7 +-
 esphome/components/tcl112/tcl112.cpp          |   2 +-
 .../thermostat/thermostat_climate.cpp         |   2 +-
 esphome/components/whirlpool/whirlpool.cpp    |   2 +-
 esphome/const.py                              |   8 +
 tests/test1.yaml                              |  17 --
 tests/test3.yaml                              |  37 ++++
 tests/test4.yaml                              |   1 +
 30 files changed, 931 insertions(+), 96 deletions(-)

diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto
index 43e23f640b..f543c2356f 100644
--- a/esphome/components/api/api.proto
+++ b/esphome/components/api/api.proto
@@ -672,11 +672,12 @@ message CameraImageRequest {
 // ==================== CLIMATE ====================
 enum ClimateMode {
   CLIMATE_MODE_OFF = 0;
-  CLIMATE_MODE_AUTO = 1;
+  CLIMATE_MODE_HEAT_COOL = 1;
   CLIMATE_MODE_COOL = 2;
   CLIMATE_MODE_HEAT = 3;
   CLIMATE_MODE_FAN_ONLY = 4;
   CLIMATE_MODE_DRY = 5;
+  CLIMATE_MODE_AUTO = 6;
 }
 enum ClimateFanMode {
   CLIMATE_FAN_ON = 0;
@@ -704,6 +705,15 @@ enum ClimateAction {
   CLIMATE_ACTION_DRYING = 5;
   CLIMATE_ACTION_FAN = 6;
 }
+enum ClimatePreset {
+  CLIMATE_PRESET_ECO = 0;
+  CLIMATE_PRESET_AWAY = 1;
+  CLIMATE_PRESET_BOOST = 2;
+  CLIMATE_PRESET_COMFORT = 3;
+  CLIMATE_PRESET_HOME = 4;
+  CLIMATE_PRESET_SLEEP = 5;
+  CLIMATE_PRESET_ACTIVITY = 6;
+}
 message ListEntitiesClimateResponse {
   option (id) = 46;
   option (source) = SOURCE_SERVER;
@@ -724,6 +734,9 @@ message ListEntitiesClimateResponse {
   bool supports_action = 12;
   repeated ClimateFanMode supported_fan_modes = 13;
   repeated ClimateSwingMode supported_swing_modes = 14;
+  repeated string supported_custom_fan_modes = 15;
+  repeated ClimatePreset supported_presets = 16;
+  repeated string supported_custom_presets = 17;
 }
 message ClimateStateResponse {
   option (id) = 47;
@@ -741,6 +754,9 @@ message ClimateStateResponse {
   ClimateAction action = 8;
   ClimateFanMode fan_mode = 9;
   ClimateSwingMode swing_mode = 10;
+  string custom_fan_mode = 11;
+  ClimatePreset preset = 12;
+  string custom_preset = 13;
 }
 message ClimateCommandRequest {
   option (id) = 48;
@@ -763,4 +779,10 @@ message ClimateCommandRequest {
   ClimateFanMode fan_mode = 13;
   bool has_swing_mode = 14;
   ClimateSwingMode swing_mode = 15;
+  bool has_custom_fan_mode = 16;
+  string custom_fan_mode = 17;
+  bool has_preset = 18;
+  ClimatePreset preset = 19;
+  bool has_custom_preset = 20;
+  string custom_preset = 21;
 }
diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp
index b5fc9b245c..eca95de3c2 100644
--- a/esphome/components/api/api_connection.cpp
+++ b/esphome/components/api/api_connection.cpp
@@ -477,8 +477,14 @@ bool APIConnection::send_climate_state(climate::Climate *climate) {
   }
   if (traits.get_supports_away())
     resp.away = climate->away;
-  if (traits.get_supports_fan_modes())
-    resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode);
+  if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
+    resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
+  if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value())
+    resp.custom_fan_mode = climate->custom_fan_mode.value();
+  if (traits.get_supports_presets() && climate->preset.has_value())
+    resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
+  if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value())
+    resp.custom_preset = climate->custom_preset.value();
   if (traits.get_supports_swing_modes())
     resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
   return this->send_climate_state_response(resp);
@@ -492,8 +498,9 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
   msg.unique_id = get_default_unique_id("climate", climate);
   msg.supports_current_temperature = traits.get_supports_current_temperature();
   msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
-  for (auto mode : {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL,
-                    climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_FAN_ONLY}) {
+  for (auto mode :
+       {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT,
+        climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_HEAT_COOL}) {
     if (traits.supports_mode(mode))
       msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode));
   }
@@ -508,6 +515,18 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
     if (traits.supports_fan_mode(fan_mode))
       msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode));
   }
+  for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) {
+    msg.supported_custom_fan_modes.push_back(custom_fan_mode);
+  }
+  for (auto preset : {climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_AWAY, climate::CLIMATE_PRESET_BOOST,
+                      climate::CLIMATE_PRESET_COMFORT, climate::CLIMATE_PRESET_HOME, climate::CLIMATE_PRESET_SLEEP,
+                      climate::CLIMATE_PRESET_ACTIVITY}) {
+    if (traits.supports_preset(preset))
+      msg.supported_presets.push_back(static_cast<enums::ClimatePreset>(preset));
+  }
+  for (auto const &custom_preset : traits.get_supported_custom_presets()) {
+    msg.supported_custom_presets.push_back(custom_preset);
+  }
   for (auto swing_mode : {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL,
                           climate::CLIMATE_SWING_HORIZONTAL}) {
     if (traits.supports_swing_mode(swing_mode))
@@ -533,6 +552,12 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
     call.set_away(msg.away);
   if (msg.has_fan_mode)
     call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
+  if (msg.has_custom_fan_mode)
+    call.set_fan_mode(msg.custom_fan_mode);
+  if (msg.has_preset)
+    call.set_preset(static_cast<climate::ClimatePreset>(msg.preset));
+  if (msg.has_custom_preset)
+    call.set_preset(msg.custom_preset);
   if (msg.has_swing_mode)
     call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode));
   call.perform();
diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp
index 38efbc2ec4..76cdc44e1e 100644
--- a/esphome/components/api/api_pb2.cpp
+++ b/esphome/components/api/api_pb2.cpp
@@ -118,8 +118,8 @@ template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMo
   switch (value) {
     case enums::CLIMATE_MODE_OFF:
       return "CLIMATE_MODE_OFF";
-    case enums::CLIMATE_MODE_AUTO:
-      return "CLIMATE_MODE_AUTO";
+    case enums::CLIMATE_MODE_HEAT_COOL:
+      return "CLIMATE_MODE_HEAT_COOL";
     case enums::CLIMATE_MODE_COOL:
       return "CLIMATE_MODE_COOL";
     case enums::CLIMATE_MODE_HEAT:
@@ -128,6 +128,8 @@ template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMo
       return "CLIMATE_MODE_FAN_ONLY";
     case enums::CLIMATE_MODE_DRY:
       return "CLIMATE_MODE_DRY";
+    case enums::CLIMATE_MODE_AUTO:
+      return "CLIMATE_MODE_AUTO";
     default:
       return "UNKNOWN";
   }
@@ -188,6 +190,26 @@ template<> const char *proto_enum_to_string<enums::ClimateAction>(enums::Climate
       return "UNKNOWN";
   }
 }
+template<> const char *proto_enum_to_string<enums::ClimatePreset>(enums::ClimatePreset value) {
+  switch (value) {
+    case enums::CLIMATE_PRESET_ECO:
+      return "CLIMATE_PRESET_ECO";
+    case enums::CLIMATE_PRESET_AWAY:
+      return "CLIMATE_PRESET_AWAY";
+    case enums::CLIMATE_PRESET_BOOST:
+      return "CLIMATE_PRESET_BOOST";
+    case enums::CLIMATE_PRESET_COMFORT:
+      return "CLIMATE_PRESET_COMFORT";
+    case enums::CLIMATE_PRESET_HOME:
+      return "CLIMATE_PRESET_HOME";
+    case enums::CLIMATE_PRESET_SLEEP:
+      return "CLIMATE_PRESET_SLEEP";
+    case enums::CLIMATE_PRESET_ACTIVITY:
+      return "CLIMATE_PRESET_ACTIVITY";
+    default:
+      return "UNKNOWN";
+  }
+}
 bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
     case 1: {
@@ -2647,6 +2669,10 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v
       this->supported_swing_modes.push_back(value.as_enum<enums::ClimateSwingMode>());
       return true;
     }
+    case 16: {
+      this->supported_presets.push_back(value.as_enum<enums::ClimatePreset>());
+      return true;
+    }
     default:
       return false;
   }
@@ -2665,6 +2691,14 @@ bool ListEntitiesClimateResponse::decode_length(uint32_t field_id, ProtoLengthDe
       this->unique_id = value.as_string();
       return true;
     }
+    case 15: {
+      this->supported_custom_fan_modes.push_back(value.as_string());
+      return true;
+    }
+    case 17: {
+      this->supported_custom_presets.push_back(value.as_string());
+      return true;
+    }
     default:
       return false;
   }
@@ -2712,6 +2746,15 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
   for (auto &it : this->supported_swing_modes) {
     buffer.encode_enum<enums::ClimateSwingMode>(14, it, true);
   }
+  for (auto &it : this->supported_custom_fan_modes) {
+    buffer.encode_string(15, it, true);
+  }
+  for (auto &it : this->supported_presets) {
+    buffer.encode_enum<enums::ClimatePreset>(16, it, true);
+  }
+  for (auto &it : this->supported_custom_presets) {
+    buffer.encode_string(17, it, true);
+  }
 }
 void ListEntitiesClimateResponse::dump_to(std::string &out) const {
   char buffer[64];
@@ -2781,6 +2824,24 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
     out.append(proto_enum_to_string<enums::ClimateSwingMode>(it));
     out.append("\n");
   }
+
+  for (const auto &it : this->supported_custom_fan_modes) {
+    out.append("  supported_custom_fan_modes: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+
+  for (const auto &it : this->supported_presets) {
+    out.append("  supported_presets: ");
+    out.append(proto_enum_to_string<enums::ClimatePreset>(it));
+    out.append("\n");
+  }
+
+  for (const auto &it : this->supported_custom_presets) {
+    out.append("  supported_custom_presets: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
   out.append("}");
 }
 bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
@@ -2805,6 +2866,24 @@ bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->swing_mode = value.as_enum<enums::ClimateSwingMode>();
       return true;
     }
+    case 12: {
+      this->preset = value.as_enum<enums::ClimatePreset>();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ClimateStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 11: {
+      this->custom_fan_mode = value.as_string();
+      return true;
+    }
+    case 13: {
+      this->custom_preset = value.as_string();
+      return true;
+    }
     default:
       return false;
   }
@@ -2846,6 +2925,9 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_enum<enums::ClimateAction>(8, this->action);
   buffer.encode_enum<enums::ClimateFanMode>(9, this->fan_mode);
   buffer.encode_enum<enums::ClimateSwingMode>(10, this->swing_mode);
+  buffer.encode_string(11, this->custom_fan_mode);
+  buffer.encode_enum<enums::ClimatePreset>(12, this->preset);
+  buffer.encode_string(13, this->custom_preset);
 }
 void ClimateStateResponse::dump_to(std::string &out) const {
   char buffer[64];
@@ -2894,6 +2976,18 @@ void ClimateStateResponse::dump_to(std::string &out) const {
   out.append("  swing_mode: ");
   out.append(proto_enum_to_string<enums::ClimateSwingMode>(this->swing_mode));
   out.append("\n");
+
+  out.append("  custom_fan_mode: ");
+  out.append("'").append(this->custom_fan_mode).append("'");
+  out.append("\n");
+
+  out.append("  preset: ");
+  out.append(proto_enum_to_string<enums::ClimatePreset>(this->preset));
+  out.append("\n");
+
+  out.append("  custom_preset: ");
+  out.append("'").append(this->custom_preset).append("'");
+  out.append("\n");
   out.append("}");
 }
 bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
@@ -2942,6 +3036,36 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
       this->swing_mode = value.as_enum<enums::ClimateSwingMode>();
       return true;
     }
+    case 16: {
+      this->has_custom_fan_mode = value.as_bool();
+      return true;
+    }
+    case 18: {
+      this->has_preset = value.as_bool();
+      return true;
+    }
+    case 19: {
+      this->preset = value.as_enum<enums::ClimatePreset>();
+      return true;
+    }
+    case 20: {
+      this->has_custom_preset = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 17: {
+      this->custom_fan_mode = value.as_string();
+      return true;
+    }
+    case 21: {
+      this->custom_preset = value.as_string();
+      return true;
+    }
     default:
       return false;
   }
@@ -2984,6 +3108,12 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_enum<enums::ClimateFanMode>(13, this->fan_mode);
   buffer.encode_bool(14, this->has_swing_mode);
   buffer.encode_enum<enums::ClimateSwingMode>(15, this->swing_mode);
+  buffer.encode_bool(16, this->has_custom_fan_mode);
+  buffer.encode_string(17, this->custom_fan_mode);
+  buffer.encode_bool(18, this->has_preset);
+  buffer.encode_enum<enums::ClimatePreset>(19, this->preset);
+  buffer.encode_bool(20, this->has_custom_preset);
+  buffer.encode_string(21, this->custom_preset);
 }
 void ClimateCommandRequest::dump_to(std::string &out) const {
   char buffer[64];
@@ -3051,6 +3181,30 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
   out.append("  swing_mode: ");
   out.append(proto_enum_to_string<enums::ClimateSwingMode>(this->swing_mode));
   out.append("\n");
+
+  out.append("  has_custom_fan_mode: ");
+  out.append(YESNO(this->has_custom_fan_mode));
+  out.append("\n");
+
+  out.append("  custom_fan_mode: ");
+  out.append("'").append(this->custom_fan_mode).append("'");
+  out.append("\n");
+
+  out.append("  has_preset: ");
+  out.append(YESNO(this->has_preset));
+  out.append("\n");
+
+  out.append("  preset: ");
+  out.append(proto_enum_to_string<enums::ClimatePreset>(this->preset));
+  out.append("\n");
+
+  out.append("  has_custom_preset: ");
+  out.append(YESNO(this->has_custom_preset));
+  out.append("\n");
+
+  out.append("  custom_preset: ");
+  out.append("'").append(this->custom_preset).append("'");
+  out.append("\n");
   out.append("}");
 }
 
diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h
index e49136b442..365ea0025d 100644
--- a/esphome/components/api/api_pb2.h
+++ b/esphome/components/api/api_pb2.h
@@ -57,11 +57,12 @@ enum ServiceArgType : uint32_t {
 };
 enum ClimateMode : uint32_t {
   CLIMATE_MODE_OFF = 0,
-  CLIMATE_MODE_AUTO = 1,
+  CLIMATE_MODE_HEAT_COOL = 1,
   CLIMATE_MODE_COOL = 2,
   CLIMATE_MODE_HEAT = 3,
   CLIMATE_MODE_FAN_ONLY = 4,
   CLIMATE_MODE_DRY = 5,
+  CLIMATE_MODE_AUTO = 6,
 };
 enum ClimateFanMode : uint32_t {
   CLIMATE_FAN_ON = 0,
@@ -88,6 +89,15 @@ enum ClimateAction : uint32_t {
   CLIMATE_ACTION_DRYING = 5,
   CLIMATE_ACTION_FAN = 6,
 };
+enum ClimatePreset : uint32_t {
+  CLIMATE_PRESET_ECO = 0,
+  CLIMATE_PRESET_AWAY = 1,
+  CLIMATE_PRESET_BOOST = 2,
+  CLIMATE_PRESET_COMFORT = 3,
+  CLIMATE_PRESET_HOME = 4,
+  CLIMATE_PRESET_SLEEP = 5,
+  CLIMATE_PRESET_ACTIVITY = 6,
+};
 
 }  // namespace enums
 
@@ -687,20 +697,23 @@ class CameraImageRequest : public ProtoMessage {
 };
 class ListEntitiesClimateResponse : public ProtoMessage {
  public:
-  std::string object_id{};
-  uint32_t key{0};
-  std::string name{};
-  std::string unique_id{};
-  bool supports_current_temperature{false};
-  bool supports_two_point_target_temperature{false};
-  std::vector<enums::ClimateMode> supported_modes{};
-  float visual_min_temperature{0.0f};
-  float visual_max_temperature{0.0f};
-  float visual_temperature_step{0.0f};
-  bool supports_away{false};
-  bool supports_action{false};
-  std::vector<enums::ClimateFanMode> supported_fan_modes{};
-  std::vector<enums::ClimateSwingMode> supported_swing_modes{};
+  std::string object_id{};                                       // NOLINT
+  uint32_t key{0};                                               // NOLINT
+  std::string name{};                                            // NOLINT
+  std::string unique_id{};                                       // NOLINT
+  bool supports_current_temperature{false};                      // NOLINT
+  bool supports_two_point_target_temperature{false};             // NOLINT
+  std::vector<enums::ClimateMode> supported_modes{};             // NOLINT
+  float visual_min_temperature{0.0f};                            // NOLINT
+  float visual_max_temperature{0.0f};                            // NOLINT
+  float visual_temperature_step{0.0f};                           // NOLINT
+  bool supports_away{false};                                     // NOLINT
+  bool supports_action{false};                                   // NOLINT
+  std::vector<enums::ClimateFanMode> supported_fan_modes{};      // NOLINT
+  std::vector<enums::ClimateSwingMode> supported_swing_modes{};  // NOLINT
+  std::vector<std::string> supported_custom_fan_modes{};         // NOLINT
+  std::vector<enums::ClimatePreset> supported_presets{};         // NOLINT
+  std::vector<std::string> supported_custom_presets{};           // NOLINT
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -711,45 +724,56 @@ class ListEntitiesClimateResponse : public ProtoMessage {
 };
 class ClimateStateResponse : public ProtoMessage {
  public:
-  uint32_t key{0};
-  enums::ClimateMode mode{};
-  float current_temperature{0.0f};
-  float target_temperature{0.0f};
-  float target_temperature_low{0.0f};
-  float target_temperature_high{0.0f};
-  bool away{false};
-  enums::ClimateAction action{};
-  enums::ClimateFanMode fan_mode{};
-  enums::ClimateSwingMode swing_mode{};
+  uint32_t key{0};                       // NOLINT
+  enums::ClimateMode mode{};             // NOLINT
+  float current_temperature{0.0f};       // NOLINT
+  float target_temperature{0.0f};        // NOLINT
+  float target_temperature_low{0.0f};    // NOLINT
+  float target_temperature_high{0.0f};   // NOLINT
+  bool away{false};                      // NOLINT
+  enums::ClimateAction action{};         // NOLINT
+  enums::ClimateFanMode fan_mode{};      // NOLINT
+  enums::ClimateSwingMode swing_mode{};  // NOLINT
+  std::string custom_fan_mode{};         // NOLINT
+  enums::ClimatePreset preset{};         // NOLINT
+  std::string custom_preset{};           // NOLINT
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
  protected:
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
 class ClimateCommandRequest : public ProtoMessage {
  public:
-  uint32_t key{0};
-  bool has_mode{false};
-  enums::ClimateMode mode{};
-  bool has_target_temperature{false};
-  float target_temperature{0.0f};
-  bool has_target_temperature_low{false};
-  float target_temperature_low{0.0f};
-  bool has_target_temperature_high{false};
-  float target_temperature_high{0.0f};
-  bool has_away{false};
-  bool away{false};
-  bool has_fan_mode{false};
-  enums::ClimateFanMode fan_mode{};
-  bool has_swing_mode{false};
-  enums::ClimateSwingMode swing_mode{};
+  uint32_t key{0};                          // NOLINT
+  bool has_mode{false};                     // NOLINT
+  enums::ClimateMode mode{};                // NOLINT
+  bool has_target_temperature{false};       // NOLINT
+  float target_temperature{0.0f};           // NOLINT
+  bool has_target_temperature_low{false};   // NOLINT
+  float target_temperature_low{0.0f};       // NOLINT
+  bool has_target_temperature_high{false};  // NOLINT
+  float target_temperature_high{0.0f};      // NOLINT
+  bool has_away{false};                     // NOLINT
+  bool away{false};                         // NOLINT
+  bool has_fan_mode{false};                 // NOLINT
+  enums::ClimateFanMode fan_mode{};         // NOLINT
+  bool has_swing_mode{false};               // NOLINT
+  enums::ClimateSwingMode swing_mode{};     // NOLINT
+  bool has_custom_fan_mode{false};          // NOLINT
+  std::string custom_fan_mode{};            // NOLINT
+  bool has_preset{false};                   // NOLINT
+  enums::ClimatePreset preset{};            // NOLINT
+  bool has_custom_preset{false};            // NOLINT
+  std::string custom_preset{};              // NOLINT
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
  protected:
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
 
diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py
index fb163c96ae..5a4492216e 100644
--- a/esphome/components/climate/__init__.py
+++ b/esphome/components/climate/__init__.py
@@ -4,11 +4,14 @@ from esphome import automation
 from esphome.components import mqtt
 from esphome.const import (
     CONF_AWAY,
+    CONF_CUSTOM_FAN_MODE,
+    CONF_CUSTOM_PRESET,
     CONF_ID,
     CONF_INTERNAL,
     CONF_MAX_TEMPERATURE,
     CONF_MIN_TEMPERATURE,
     CONF_MODE,
+    CONF_PRESET,
     CONF_TARGET_TEMPERATURE,
     CONF_TARGET_TEMPERATURE_HIGH,
     CONF_TARGET_TEMPERATURE_LOW,
@@ -33,11 +36,12 @@ ClimateTraits = climate_ns.class_("ClimateTraits")
 ClimateMode = climate_ns.enum("ClimateMode")
 CLIMATE_MODES = {
     "OFF": ClimateMode.CLIMATE_MODE_OFF,
-    "AUTO": ClimateMode.CLIMATE_MODE_AUTO,
+    "HEAT_COOL": ClimateMode.CLIMATE_HEAT_COOL,
     "COOL": ClimateMode.CLIMATE_MODE_COOL,
     "HEAT": ClimateMode.CLIMATE_MODE_HEAT,
     "DRY": ClimateMode.CLIMATE_MODE_DRY,
     "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY,
+    "AUTO": ClimateMode.CLIMATE_MODE_AUTO,
 }
 validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True)
 
@@ -56,6 +60,19 @@ CLIMATE_FAN_MODES = {
 
 validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True)
 
+ClimatePreset = climate_ns.enum("ClimatePreset")
+CLIMATE_PRESETS = {
+    "ECO": ClimatePreset.CLIMATE_PRESET_ECO,
+    "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
+    "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
+    "COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT,
+    "HOME": ClimatePreset.CLIMATE_PRESET_HOME,
+    "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
+    "ACTIVITY": ClimatePreset.CLIMATE_PRESET_ACTIVITY,
+}
+
+validate_climate_preset = cv.enum(CLIMATE_PRESETS, upper=True)
+
 ClimateSwingMode = climate_ns.enum("ClimateSwingMode")
 CLIMATE_SWING_MODES = {
     "OFF": ClimateSwingMode.CLIMATE_SWING_OFF,
@@ -117,7 +134,12 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
         cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature),
         cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature),
         cv.Optional(CONF_AWAY): cv.templatable(cv.boolean),
-        cv.Optional(CONF_FAN_MODE): cv.templatable(validate_climate_fan_mode),
+        cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
+            validate_climate_fan_mode
+        ),
+        cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.string_strict,
+        cv.Exclusive(CONF_PRESET, "preset"): cv.templatable(validate_climate_preset),
+        cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.string_strict,
         cv.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode),
     }
 )
@@ -151,6 +173,15 @@ async def climate_control_to_code(config, action_id, template_arg, args):
     if CONF_FAN_MODE in config:
         template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode)
         cg.add(var.set_fan_mode(template_))
+    if CONF_CUSTOM_FAN_MODE in config:
+        template_ = await cg.templatable(config[CONF_CUSTOM_FAN_MODE], args, str)
+        cg.add(var.set_custom_fan_mode(template_))
+    if CONF_PRESET in config:
+        template_ = await cg.templatable(config[CONF_PRESET], args, ClimatePreset)
+        cg.add(var.set_preset(template_))
+    if CONF_CUSTOM_PRESET in config:
+        template_ = await cg.templatable(config[CONF_CUSTOM_PRESET], args, str)
+        cg.add(var.set_custom_preset(template_))
     if CONF_SWING_MODE in config:
         template_ = await cg.templatable(
             config[CONF_SWING_MODE], args, ClimateSwingMode
diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h
index 0cd52b1036..b0b71cb7d7 100644
--- a/esphome/components/climate/automation.h
+++ b/esphome/components/climate/automation.h
@@ -16,6 +16,9 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
   TEMPLATABLE_VALUE(float, target_temperature_high)
   TEMPLATABLE_VALUE(bool, away)
   TEMPLATABLE_VALUE(ClimateFanMode, fan_mode)
+  TEMPLATABLE_VALUE(std::string, custom_fan_mode)
+  TEMPLATABLE_VALUE(ClimatePreset, preset)
+  TEMPLATABLE_VALUE(std::string, custom_preset)
   TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode)
 
   void play(Ts... x) override {
@@ -26,6 +29,9 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
     call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...));
     call.set_away(this->away_.optional_value(x...));
     call.set_fan_mode(this->fan_mode_.optional_value(x...));
+    call.set_fan_mode(this->custom_fan_mode_.optional_value(x...));
+    call.set_preset(this->preset_.optional_value(x...));
+    call.set_preset(this->custom_preset_.optional_value(x...));
     call.set_swing_mode(this->swing_mode_.optional_value(x...));
     call.perform();
   }
diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp
index 443290ed6d..c047d96cdb 100644
--- a/esphome/components/climate/climate.cpp
+++ b/esphome/components/climate/climate.cpp
@@ -1,5 +1,4 @@
 #include "climate.h"
-#include "esphome/core/log.h"
 
 namespace esphome {
 namespace climate {
@@ -13,10 +12,24 @@ void ClimateCall::perform() {
     const char *mode_s = climate_mode_to_string(*this->mode_);
     ESP_LOGD(TAG, "  Mode: %s", mode_s);
   }
+  if (this->custom_fan_mode_.has_value()) {
+    this->fan_mode_.reset();
+    ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_.value().c_str());
+  }
   if (this->fan_mode_.has_value()) {
+    this->custom_fan_mode_.reset();
     const char *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_);
     ESP_LOGD(TAG, "  Fan: %s", fan_mode_s);
   }
+  if (this->custom_preset_.has_value()) {
+    this->preset_.reset();
+    ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_.value().c_str());
+  }
+  if (this->preset_.has_value()) {
+    this->custom_preset_.reset();
+    const char *preset_s = climate_preset_to_string(*this->preset_);
+    ESP_LOGD(TAG, "  Preset: %s", preset_s);
+  }
   if (this->swing_mode_.has_value()) {
     const char *swing_mode_s = climate_swing_mode_to_string(*this->swing_mode_);
     ESP_LOGD(TAG, "  Swing: %s", swing_mode_s);
@@ -44,13 +57,32 @@ void ClimateCall::validate_() {
       this->mode_.reset();
     }
   }
-  if (this->fan_mode_.has_value()) {
+  if (this->custom_fan_mode_.has_value()) {
+    auto custom_fan_mode = *this->custom_fan_mode_;
+    if (!traits.supports_custom_fan_mode(custom_fan_mode)) {
+      ESP_LOGW(TAG, "  Fan Mode %s is not supported by this device!", custom_fan_mode.c_str());
+      this->custom_fan_mode_.reset();
+    }
+  } else if (this->fan_mode_.has_value()) {
     auto fan_mode = *this->fan_mode_;
     if (!traits.supports_fan_mode(fan_mode)) {
       ESP_LOGW(TAG, "  Fan Mode %s is not supported by this device!", climate_fan_mode_to_string(fan_mode));
       this->fan_mode_.reset();
     }
   }
+  if (this->custom_preset_.has_value()) {
+    auto custom_preset = *this->custom_preset_;
+    if (!traits.supports_custom_preset(custom_preset)) {
+      ESP_LOGW(TAG, "  Preset %s is not supported by this device!", custom_preset.c_str());
+      this->custom_preset_.reset();
+    }
+  } else if (this->preset_.has_value()) {
+    auto preset = *this->preset_;
+    if (!traits.supports_preset(preset)) {
+      ESP_LOGW(TAG, "  Preset %s is not supported by this device!", climate_preset_to_string(preset));
+      this->preset_.reset();
+    }
+  }
   if (this->swing_mode_.has_value()) {
     auto swing_mode = *this->swing_mode_;
     if (!traits.supports_swing_mode(swing_mode)) {
@@ -117,6 +149,8 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) {
     this->set_mode(CLIMATE_MODE_FAN_ONLY);
   } else if (str_equals_case_insensitive(mode, "DRY")) {
     this->set_mode(CLIMATE_MODE_DRY);
+  } else if (str_equals_case_insensitive(mode, "HEAT_COOL")) {
+    this->set_mode(CLIMATE_MODE_HEAT_COOL);
   } else {
     ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
   }
@@ -124,6 +158,7 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) {
 }
 ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
   this->fan_mode_ = fan_mode;
+  this->custom_fan_mode_.reset();
   return *this;
 }
 ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
@@ -146,11 +181,59 @@ ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
   } else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) {
     this->set_fan_mode(CLIMATE_FAN_DIFFUSE);
   } else {
-    ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
+    auto custom_fan_modes = this->parent_->get_traits().get_supported_custom_fan_modes();
+    if (std::find(custom_fan_modes.begin(), custom_fan_modes.end(), fan_mode) != custom_fan_modes.end()) {
+      this->custom_fan_mode_ = fan_mode;
+      this->fan_mode_.reset();
+    } else {
+      ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
+    }
+  }
+  return *this;
+}
+ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
+  if (fan_mode.has_value()) {
+    this->set_fan_mode(fan_mode.value());
+  }
+  return *this;
+}
+ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
+  this->preset_ = preset;
+  this->custom_preset_.reset();
+  return *this;
+}
+ClimateCall &ClimateCall::set_preset(const std::string &preset) {
+  if (str_equals_case_insensitive(preset, "ECO")) {
+    this->set_preset(CLIMATE_PRESET_ECO);
+  } else if (str_equals_case_insensitive(preset, "AWAY")) {
+    this->set_preset(CLIMATE_PRESET_AWAY);
+  } else if (str_equals_case_insensitive(preset, "BOOST")) {
+    this->set_preset(CLIMATE_PRESET_BOOST);
+  } else if (str_equals_case_insensitive(preset, "COMFORT")) {
+    this->set_preset(CLIMATE_PRESET_COMFORT);
+  } else if (str_equals_case_insensitive(preset, "HOME")) {
+    this->set_preset(CLIMATE_PRESET_HOME);
+  } else if (str_equals_case_insensitive(preset, "SLEEP")) {
+    this->set_preset(CLIMATE_PRESET_SLEEP);
+  } else if (str_equals_case_insensitive(preset, "ACTIVITY")) {
+    this->set_preset(CLIMATE_PRESET_ACTIVITY);
+  } else {
+    auto custom_presets = this->parent_->get_traits().get_supported_custom_presets();
+    if (std::find(custom_presets.begin(), custom_presets.end(), preset) != custom_presets.end()) {
+      this->custom_preset_ = preset;
+      this->preset_.reset();
+    } else {
+      ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
+    }
+  }
+  return *this;
+}
+ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
+  if (preset.has_value()) {
+    this->set_preset(preset.value());
   }
   return *this;
 }
-
 ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) {
   this->swing_mode_ = swing_mode;
   return *this;
@@ -188,6 +271,9 @@ const optional<float> &ClimateCall::get_target_temperature_low() const { return
 const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; }
 const optional<bool> &ClimateCall::get_away() const { return this->away_; }
 const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
+const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
+const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
+const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
 const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
 ClimateCall &ClimateCall::set_away(bool away) {
   this->away_ = away;
@@ -215,6 +301,12 @@ ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
 }
 ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
   this->fan_mode_ = fan_mode;
+  this->custom_fan_mode_.reset();
+  return *this;
+}
+ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
+  this->preset_ = preset;
+  this->custom_preset_.reset();
   return *this;
 }
 ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode) {
@@ -249,8 +341,31 @@ void Climate::save_state_() {
   if (traits.get_supports_away()) {
     state.away = this->away;
   }
-  if (traits.get_supports_fan_modes()) {
-    state.fan_mode = this->fan_mode;
+  if (traits.get_supports_fan_modes() && fan_mode.has_value()) {
+    state.uses_custom_fan_mode = false;
+    state.fan_mode = this->fan_mode.value();
+  }
+  if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) {
+    state.uses_custom_fan_mode = true;
+    auto &custom_fan_modes = traits.get_supported_custom_fan_modes();
+    auto it = std::find(custom_fan_modes.begin(), custom_fan_modes.end(), this->custom_fan_mode.value());
+    // only set custom fan mode if value exists, otherwise leave it as is
+    if (it != custom_fan_modes.cend()) {
+      state.custom_fan_mode = std::distance(custom_fan_modes.begin(), it);
+    }
+  }
+  if (traits.get_supports_presets() && preset.has_value()) {
+    state.uses_custom_preset = false;
+    state.preset = this->preset.value();
+  }
+  if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) {
+    state.uses_custom_preset = true;
+    auto custom_presets = traits.get_supported_custom_presets();
+    auto it = std::find(custom_presets.begin(), custom_presets.end(), this->custom_preset.value());
+    // only set custom preset if value exists, otherwise leave it as is
+    if (it != custom_presets.cend()) {
+      state.custom_preset = std::distance(custom_presets.begin(), it);
+    }
   }
   if (traits.get_supports_swing_modes()) {
     state.swing_mode = this->swing_mode;
@@ -266,8 +381,17 @@ void Climate::publish_state() {
   if (traits.get_supports_action()) {
     ESP_LOGD(TAG, "  Action: %s", climate_action_to_string(this->action));
   }
-  if (traits.get_supports_fan_modes()) {
-    ESP_LOGD(TAG, "  Fan Mode: %s", climate_fan_mode_to_string(this->fan_mode));
+  if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) {
+    ESP_LOGD(TAG, "  Fan Mode: %s", climate_fan_mode_to_string(this->fan_mode.value()));
+  }
+  if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode.has_value()) {
+    ESP_LOGD(TAG, "  Custom Fan Mode: %s", this->custom_fan_mode.value().c_str());
+  }
+  if (traits.get_supports_presets() && this->preset.has_value()) {
+    ESP_LOGD(TAG, "  Preset: %s", climate_preset_to_string(this->preset.value()));
+  }
+  if (!traits.get_supported_custom_presets().empty() && this->custom_preset.has_value()) {
+    ESP_LOGD(TAG, "  Custom Preset: %s", this->custom_preset.value().c_str());
   }
   if (traits.get_supports_swing_modes()) {
     ESP_LOGD(TAG, "  Swing Mode: %s", climate_swing_mode_to_string(this->swing_mode));
@@ -332,9 +456,12 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
   if (traits.get_supports_away()) {
     call.set_away(this->away);
   }
-  if (traits.get_supports_fan_modes()) {
+  if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
     call.set_fan_mode(this->fan_mode);
   }
+  if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
+    call.set_preset(this->preset);
+  }
   if (traits.get_supports_swing_modes()) {
     call.set_swing_mode(this->swing_mode);
   }
@@ -352,9 +479,21 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
   if (traits.get_supports_away()) {
     climate->away = this->away;
   }
-  if (traits.get_supports_fan_modes()) {
+  if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) {
     climate->fan_mode = this->fan_mode;
   }
+  if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) {
+    climate->custom_fan_mode = traits.get_supported_custom_fan_modes()[this->custom_fan_mode];
+  }
+  if (traits.get_supports_presets() && !this->uses_custom_preset) {
+    climate->preset = this->preset;
+  }
+  if (!traits.get_supported_custom_presets().empty() && this->uses_custom_preset) {
+    climate->custom_preset = traits.get_supported_custom_presets()[this->custom_preset];
+  }
+  if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) {
+    climate->custom_preset = traits.get_supported_custom_presets()[this->preset];
+  }
   if (traits.get_supports_swing_modes()) {
     climate->swing_mode = this->swing_mode;
   }
diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h
index 786afe097a..cd69469692 100644
--- a/esphome/components/climate/climate.h
+++ b/esphome/components/climate/climate.h
@@ -3,6 +3,7 @@
 #include "esphome/core/component.h"
 #include "esphome/core/helpers.h"
 #include "esphome/core/preferences.h"
+#include "esphome/core/log.h"
 #include "climate_mode.h"
 #include "climate_traits.h"
 
@@ -70,12 +71,22 @@ class ClimateCall {
   ClimateCall &set_fan_mode(optional<ClimateFanMode> fan_mode);
   /// Set the fan mode of the climate device based on a string.
   ClimateCall &set_fan_mode(const std::string &fan_mode);
+  /// Set the fan mode of the climate device based on a string.
+  ClimateCall &set_fan_mode(optional<std::string> fan_mode);
   /// Set the swing mode of the climate device.
   ClimateCall &set_swing_mode(ClimateSwingMode swing_mode);
   /// Set the swing mode of the climate device.
   ClimateCall &set_swing_mode(optional<ClimateSwingMode> swing_mode);
   /// Set the swing mode of the climate device based on a string.
   ClimateCall &set_swing_mode(const std::string &swing_mode);
+  /// Set the preset of the climate device.
+  ClimateCall &set_preset(ClimatePreset preset);
+  /// Set the preset of the climate device.
+  ClimateCall &set_preset(optional<ClimatePreset> preset);
+  /// Set the preset of the climate device based on a string.
+  ClimateCall &set_preset(const std::string &preset);
+  /// Set the preset of the climate device based on a string.
+  ClimateCall &set_preset(optional<std::string> preset);
 
   void perform();
 
@@ -86,6 +97,9 @@ class ClimateCall {
   const optional<bool> &get_away() const;
   const optional<ClimateFanMode> &get_fan_mode() const;
   const optional<ClimateSwingMode> &get_swing_mode() const;
+  const optional<std::string> &get_custom_fan_mode() const;
+  const optional<ClimatePreset> &get_preset() const;
+  const optional<std::string> &get_custom_preset() const;
 
  protected:
   void validate_();
@@ -98,13 +112,25 @@ class ClimateCall {
   optional<bool> away_;
   optional<ClimateFanMode> fan_mode_;
   optional<ClimateSwingMode> swing_mode_;
+  optional<std::string> custom_fan_mode_;
+  optional<ClimatePreset> preset_;
+  optional<std::string> custom_preset_;
 };
 
 /// Struct used to save the state of the climate device in restore memory.
 struct ClimateDeviceRestoreState {
   ClimateMode mode;
   bool away;
-  ClimateFanMode fan_mode;
+  bool uses_custom_fan_mode{false};
+  union {
+    ClimateFanMode fan_mode;
+    uint8_t custom_fan_mode;
+  };
+  bool uses_custom_preset{false};
+  union {
+    ClimatePreset preset;
+    uint8_t custom_preset;
+  };
   ClimateSwingMode swing_mode;
   union {
     float target_temperature;
@@ -168,11 +194,20 @@ class Climate : public Nameable {
   bool away{false};
 
   /// The active fan mode of the climate device.
-  ClimateFanMode fan_mode;
+  optional<ClimateFanMode> fan_mode;
 
   /// The active swing mode of the climate device.
   ClimateSwingMode swing_mode;
 
+  /// The active custom fan mode of the climate device.
+  optional<std::string> custom_fan_mode;
+
+  /// The active preset of the climate device.
+  optional<ClimatePreset> preset;
+
+  /// The active custom preset mode of the climate device.
+  optional<std::string> custom_preset;
+
   /** Add a callback for the climate device state, each time the state of the climate device is updated
    * (using publish_state), this callback will be called.
    *
diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp
index ddcc4af4d9..4540208a3f 100644
--- a/esphome/components/climate/climate_mode.cpp
+++ b/esphome/components/climate/climate_mode.cpp
@@ -17,6 +17,8 @@ const char *climate_mode_to_string(ClimateMode mode) {
       return "FAN_ONLY";
     case CLIMATE_MODE_DRY:
       return "DRY";
+    case CLIMATE_MODE_HEAT_COOL:
+      return "HEAT_COOL";
     default:
       return "UNKNOWN";
   }
@@ -80,5 +82,26 @@ const char *climate_swing_mode_to_string(ClimateSwingMode swing_mode) {
   }
 }
 
+const char *climate_preset_to_string(ClimatePreset preset) {
+  switch (preset) {
+    case climate::CLIMATE_PRESET_ECO:
+      return "ECO";
+    case climate::CLIMATE_PRESET_AWAY:
+      return "AWAY";
+    case climate::CLIMATE_PRESET_BOOST:
+      return "BOOST";
+    case climate::CLIMATE_PRESET_COMFORT:
+      return "COMFORT";
+    case climate::CLIMATE_PRESET_HOME:
+      return "HOME";
+    case climate::CLIMATE_PRESET_SLEEP:
+      return "SLEEP";
+    case climate::CLIMATE_PRESET_ACTIVITY:
+      return "ACTIVITY";
+    default:
+      return "UNKNOWN";
+  }
+}
+
 }  // namespace climate
 }  // namespace esphome
diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h
index 8037ea2196..e129fca91d 100644
--- a/esphome/components/climate/climate_mode.h
+++ b/esphome/components/climate/climate_mode.h
@@ -10,7 +10,7 @@ enum ClimateMode : uint8_t {
   /// The climate device is off (not in auto, heat or cool mode)
   CLIMATE_MODE_OFF = 0,
   /// The climate device is set to automatically change the heating/cooling cycle
-  CLIMATE_MODE_AUTO = 1,
+  CLIMATE_MODE_HEAT_COOL = 1,
   /// The climate device is manually set to cool mode (not in auto mode!)
   CLIMATE_MODE_COOL = 2,
   /// The climate device is manually set to heat mode (not in auto mode!)
@@ -19,6 +19,8 @@ enum ClimateMode : uint8_t {
   CLIMATE_MODE_FAN_ONLY = 4,
   /// The climate device is manually set to dry mode
   CLIMATE_MODE_DRY = 5,
+  /// The climate device is manually set to heat-cool mode
+  CLIMATE_MODE_AUTO = 6
 };
 
 /// Enum for the current action of the climate device. Values match those of ClimateMode.
@@ -61,7 +63,7 @@ enum ClimateFanMode : uint8_t {
 
 /// Enum for all modes a climate swing can be in
 enum ClimateSwingMode : uint8_t {
-  /// The sing mode is set to Off
+  /// The swing mode is set to Off
   CLIMATE_SWING_OFF = 0,
   /// The fan mode is set to Both
   CLIMATE_SWING_BOTH = 1,
@@ -71,6 +73,24 @@ enum ClimateSwingMode : uint8_t {
   CLIMATE_SWING_HORIZONTAL = 3,
 };
 
+/// Enum for all modes a climate swing can be in
+enum ClimatePreset : uint8_t {
+  /// Preset is set to ECO
+  CLIMATE_PRESET_ECO = 0,
+  /// Preset is set to AWAY
+  CLIMATE_PRESET_AWAY = 1,
+  /// Preset is set to BOOST
+  CLIMATE_PRESET_BOOST = 2,
+  /// Preset is set to COMFORT
+  CLIMATE_PRESET_COMFORT = 3,
+  /// Preset is set to HOME
+  CLIMATE_PRESET_HOME = 4,
+  /// Preset is set to SLEEP
+  CLIMATE_PRESET_SLEEP = 5,
+  /// Preset is set to ACTIVITY
+  CLIMATE_PRESET_ACTIVITY = 6,
+};
+
 /// Convert the given ClimateMode to a human-readable string.
 const char *climate_mode_to_string(ClimateMode mode);
 
@@ -83,5 +103,8 @@ const char *climate_fan_mode_to_string(ClimateFanMode mode);
 /// Convert the given ClimateSwingMode to a human-readable string.
 const char *climate_swing_mode_to_string(ClimateSwingMode mode);
 
+/// Convert the given ClimateSwingMode to a human-readable string.
+const char *climate_preset_to_string(ClimatePreset preset);
+
 }  // namespace climate
 }  // namespace esphome
diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp
index 6e941bddf0..eda4722fcb 100644
--- a/esphome/components/climate/climate_traits.cpp
+++ b/esphome/components/climate/climate_traits.cpp
@@ -119,6 +119,71 @@ bool ClimateTraits::get_supports_fan_modes() const {
          this->supports_fan_mode_low_ || this->supports_fan_mode_medium_ || this->supports_fan_mode_high_ ||
          this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_;
 }
+void ClimateTraits::set_supported_custom_fan_modes(std::vector<std::string> &supported_custom_fan_modes) {
+  this->supported_custom_fan_modes_ = supported_custom_fan_modes;
+}
+const std::vector<std::string> ClimateTraits::get_supported_custom_fan_modes() const {
+  return this->supported_custom_fan_modes_;
+}
+bool ClimateTraits::supports_custom_fan_mode(std::string &custom_fan_mode) const {
+  return std::count(this->supported_custom_fan_modes_.begin(), this->supported_custom_fan_modes_.end(),
+                    custom_fan_mode);
+}
+bool ClimateTraits::supports_preset(ClimatePreset preset) const {
+  switch (preset) {
+    case climate::CLIMATE_PRESET_ECO:
+      return this->supports_preset_eco_;
+    case climate::CLIMATE_PRESET_AWAY:
+      return this->supports_preset_away_;
+    case climate::CLIMATE_PRESET_BOOST:
+      return this->supports_preset_boost_;
+    case climate::CLIMATE_PRESET_COMFORT:
+      return this->supports_preset_comfort_;
+    case climate::CLIMATE_PRESET_HOME:
+      return this->supports_preset_home_;
+    case climate::CLIMATE_PRESET_SLEEP:
+      return this->supports_preset_sleep_;
+    case climate::CLIMATE_PRESET_ACTIVITY:
+      return this->supports_preset_activity_;
+    default:
+      return false;
+  }
+}
+void ClimateTraits::set_supports_preset_eco(bool supports_preset_eco) {
+  this->supports_preset_eco_ = supports_preset_eco;
+}
+void ClimateTraits::set_supports_preset_away(bool supports_preset_away) {
+  this->supports_preset_away_ = supports_preset_away;
+}
+void ClimateTraits::set_supports_preset_boost(bool supports_preset_boost) {
+  this->supports_preset_boost_ = supports_preset_boost;
+}
+void ClimateTraits::set_supports_preset_comfort(bool supports_preset_comfort) {
+  this->supports_preset_comfort_ = supports_preset_comfort;
+}
+void ClimateTraits::set_supports_preset_home(bool supports_preset_home) {
+  this->supports_preset_home_ = supports_preset_home;
+}
+void ClimateTraits::set_supports_preset_sleep(bool supports_preset_sleep) {
+  this->supports_preset_sleep_ = supports_preset_sleep;
+}
+void ClimateTraits::set_supports_preset_activity(bool supports_preset_activity) {
+  this->supports_preset_activity_ = supports_preset_activity;
+}
+bool ClimateTraits::get_supports_presets() const {
+  return this->supports_preset_eco_ || this->supports_preset_away_ || this->supports_preset_boost_ ||
+         this->supports_preset_comfort_ || this->supports_preset_home_ || this->supports_preset_sleep_ ||
+         this->supports_preset_activity_;
+}
+void ClimateTraits::set_supported_custom_presets(std::vector<std::string> &supported_custom_presets) {
+  this->supported_custom_presets_ = supported_custom_presets;
+}
+const std::vector<std::string> ClimateTraits::get_supported_custom_presets() const {
+  return this->supported_custom_presets_;
+}
+bool ClimateTraits::supports_custom_preset(std::string &custom_preset) const {
+  return std::count(this->supported_custom_presets_.begin(), this->supported_custom_presets_.end(), custom_preset);
+}
 void ClimateTraits::set_supports_swing_mode_off(bool supports_swing_mode_off) {
   this->supports_swing_mode_off_ = supports_swing_mode_off;
 }
diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h
index 347a7bc1f2..f0a48ca308 100644
--- a/esphome/components/climate/climate_traits.h
+++ b/esphome/components/climate/climate_traits.h
@@ -1,5 +1,6 @@
 #pragma once
 
+#include "esphome/core/helpers.h"
 #include "climate_mode.h"
 
 namespace esphome {
@@ -65,6 +66,21 @@ class ClimateTraits {
   void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse);
   bool supports_fan_mode(ClimateFanMode fan_mode) const;
   bool get_supports_fan_modes() const;
+  void set_supported_custom_fan_modes(std::vector<std::string> &supported_custom_fan_modes);
+  const std::vector<std::string> get_supported_custom_fan_modes() const;
+  bool supports_custom_fan_mode(std::string &custom_fan_mode) const;
+  bool supports_preset(ClimatePreset preset) const;
+  void set_supports_preset_eco(bool supports_preset_eco);
+  void set_supports_preset_away(bool supports_preset_away);
+  void set_supports_preset_boost(bool supports_preset_boost);
+  void set_supports_preset_comfort(bool supports_preset_comfort);
+  void set_supports_preset_home(bool supports_preset_home);
+  void set_supports_preset_sleep(bool supports_preset_sleep);
+  void set_supports_preset_activity(bool supports_preset_activity);
+  bool get_supports_presets() const;
+  void set_supported_custom_presets(std::vector<std::string> &supported_custom_presets);
+  const std::vector<std::string> get_supported_custom_presets() const;
+  bool supports_custom_preset(std::string &custom_preset) const;
   void set_supports_swing_mode_off(bool supports_swing_mode_off);
   void set_supports_swing_mode_both(bool supports_swing_mode_both);
   void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical);
@@ -103,6 +119,15 @@ class ClimateTraits {
   bool supports_swing_mode_both_{false};
   bool supports_swing_mode_vertical_{false};
   bool supports_swing_mode_horizontal_{false};
+  bool supports_preset_eco_{false};
+  bool supports_preset_away_{false};
+  bool supports_preset_boost_{false};
+  bool supports_preset_comfort_{false};
+  bool supports_preset_home_{false};
+  bool supports_preset_sleep_{false};
+  bool supports_preset_activity_{false};
+  std::vector<std::string> supported_custom_fan_modes_;
+  std::vector<std::string> supported_custom_presets_;
 
   float visual_min_temperature_{10};
   float visual_max_temperature_{30};
diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.cpp b/esphome/components/climate_ir_lg/climate_ir_lg.cpp
index ee73d30796..983d33c0b1 100644
--- a/esphome/components/climate_ir_lg/climate_ir_lg.cpp
+++ b/esphome/components/climate_ir_lg/climate_ir_lg.cpp
@@ -72,7 +72,7 @@ void LgIrClimate::transmit_state() {
       remote_state |= FAN_AUTO;
     } else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY ||
                this->mode == climate::CLIMATE_MODE_HEAT) {
-      switch (this->fan_mode) {
+      switch (this->fan_mode.value()) {
         case climate::CLIMATE_FAN_HIGH:
           remote_state |= FAN_MAX;
           break;
diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp
index 441f43b424..e50521a348 100644
--- a/esphome/components/coolix/coolix.cpp
+++ b/esphome/components/coolix/coolix.cpp
@@ -93,7 +93,7 @@ void CoolixClimate::transmit_state() {
         this->fan_mode = climate::CLIMATE_FAN_AUTO;
         remote_state |= COOLIX_FAN_MODE_AUTO_DRY;
       } else {
-        switch (this->fan_mode) {
+        switch (this->fan_mode.value()) {
           case climate::CLIMATE_FAN_HIGH:
             remote_state |= COOLIX_FAN_MAX;
             break;
diff --git a/esphome/components/daikin/daikin.cpp b/esphome/components/daikin/daikin.cpp
index 0701344a8b..e0ffd46387 100644
--- a/esphome/components/daikin/daikin.cpp
+++ b/esphome/components/daikin/daikin.cpp
@@ -94,7 +94,7 @@ uint8_t DaikinClimate::operation_mode_() {
 
 uint16_t DaikinClimate::fan_speed_() {
   uint16_t fan_speed;
-  switch (this->fan_mode) {
+  switch (this->fan_mode.value()) {
     case climate::CLIMATE_FAN_LOW:
       fan_speed = DAIKIN_FAN_1 << 8;
       break;
diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp
index 2676609d9b..8671f38e8e 100644
--- a/esphome/components/fujitsu_general/fujitsu_general.cpp
+++ b/esphome/components/fujitsu_general/fujitsu_general.cpp
@@ -140,7 +140,7 @@ void FujitsuGeneralClimate::transmit_state() {
   }
 
   // Set fan
-  switch (this->fan_mode) {
+  switch (this->fan_mode.value()) {
     case climate::CLIMATE_FAN_HIGH:
       SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_HIGH);
       break;
diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.cpp b/esphome/components/hitachi_ac344/hitachi_ac344.cpp
index 8d56c7f51c..b2798b608a 100644
--- a/esphome/components/hitachi_ac344/hitachi_ac344.cpp
+++ b/esphome/components/hitachi_ac344/hitachi_ac344.cpp
@@ -164,11 +164,13 @@ void HitachiClimate::transmit_state() {
     case climate::CLIMATE_MODE_OFF:
       set_power_(false);
       break;
+    default:
+      ESP_LOGW(TAG, "Unsupported mode: %s", climate_mode_to_string(this->mode));
   }
 
   set_temp_(static_cast<uint8_t>(this->target_temperature));
 
-  switch (this->fan_mode) {
+  switch (this->fan_mode.value()) {
     case climate::CLIMATE_FAN_LOW:
       set_fan_(HITACHI_AC344_FAN_LOW);
       break;
diff --git a/esphome/components/midea_ac/climate.py b/esphome/components/midea_ac/climate.py
index 7bf77d7c6b..00aa979515 100644
--- a/esphome/components/midea_ac/climate.py
+++ b/esphome/components/midea_ac/climate.py
@@ -2,7 +2,12 @@ from esphome.components import climate, sensor
 import esphome.config_validation as cv
 import esphome.codegen as cg
 from esphome.const import (
+    CONF_CUSTOM_FAN_MODES,
+    CONF_CUSTOM_PRESETS,
     CONF_ID,
+    CONF_PRESET_BOOST,
+    CONF_PRESET_ECO,
+    CONF_PRESET_SLEEP,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -18,7 +23,6 @@ from esphome.components.midea_dongle import CONF_MIDEA_DONGLE_ID, MideaDongle
 
 AUTO_LOAD = ["climate", "sensor", "midea_dongle"]
 CODEOWNERS = ["@dudanov"]
-
 CONF_BEEPER = "beeper"
 CONF_SWING_HORIZONTAL = "swing_horizontal"
 CONF_SWING_BOTH = "swing_both"
@@ -28,14 +32,36 @@ CONF_HUMIDITY_SETPOINT = "humidity_setpoint"
 midea_ac_ns = cg.esphome_ns.namespace("midea_ac")
 MideaAC = midea_ac_ns.class_("MideaAC", climate.Climate, cg.Component)
 
+CLIMATE_CUSTOM_FAN_MODES = {
+    "SILENT": "silent",
+    "TURBO": "turbo",
+}
+
+validate_climate_custom_fan_mode = cv.enum(CLIMATE_CUSTOM_FAN_MODES, upper=True)
+
+CLIMATE_CUSTOM_PRESETS = {
+    "FREEZE_PROTECTION": "freeze protection",
+}
+
+validate_climate_custom_preset = cv.enum(CLIMATE_CUSTOM_PRESETS, upper=True)
+
 CONFIG_SCHEMA = cv.All(
     climate.CLIMATE_SCHEMA.extend(
         {
             cv.GenerateID(): cv.declare_id(MideaAC),
             cv.GenerateID(CONF_MIDEA_DONGLE_ID): cv.use_id(MideaDongle),
             cv.Optional(CONF_BEEPER, default=False): cv.boolean,
+            cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list(
+                validate_climate_custom_fan_mode
+            ),
+            cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(
+                validate_climate_custom_preset
+            ),
             cv.Optional(CONF_SWING_HORIZONTAL, default=False): cv.boolean,
             cv.Optional(CONF_SWING_BOTH, default=False): cv.boolean,
+            cv.Optional(CONF_PRESET_ECO, default=False): cv.boolean,
+            cv.Optional(CONF_PRESET_SLEEP, default=False): cv.boolean,
+            cv.Optional(CONF_PRESET_BOOST, default=False): cv.boolean,
             cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
                 UNIT_CELSIUS,
                 ICON_THERMOMETER,
@@ -65,8 +91,15 @@ async def to_code(config):
     paren = await cg.get_variable(config[CONF_MIDEA_DONGLE_ID])
     cg.add(var.set_midea_dongle_parent(paren))
     cg.add(var.set_beeper_feedback(config[CONF_BEEPER]))
+    if CONF_CUSTOM_FAN_MODES in config:
+        cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES]))
+    if CONF_CUSTOM_PRESETS in config:
+        cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS]))
     cg.add(var.set_swing_horizontal(config[CONF_SWING_HORIZONTAL]))
     cg.add(var.set_swing_both(config[CONF_SWING_BOTH]))
+    cg.add(var.set_preset_eco(config[CONF_PRESET_ECO]))
+    cg.add(var.set_preset_sleep(config[CONF_PRESET_SLEEP]))
+    cg.add(var.set_preset_boost(config[CONF_PRESET_BOOST]))
     if CONF_OUTDOOR_TEMPERATURE in config:
         sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
         cg.add(var.set_outdoor_temperature_sensor(sens))
diff --git a/esphome/components/midea_ac/midea_climate.cpp b/esphome/components/midea_ac/midea_climate.cpp
index 8a74251696..f98cf74ac1 100644
--- a/esphome/components/midea_ac/midea_climate.cpp
+++ b/esphome/components/midea_ac/midea_climate.cpp
@@ -40,8 +40,24 @@ void MideaAC::on_frame(const midea_dongle::Frame &frame) {
   set_property(this->mode, p.get_mode(), need_publish);
   set_property(this->target_temperature, p.get_target_temp(), need_publish);
   set_property(this->current_temperature, p.get_indoor_temp(), need_publish);
-  set_property(this->fan_mode, p.get_fan_mode(), need_publish);
+  if (p.is_custom_fan_mode()) {
+    this->fan_mode.reset();
+    optional<std::string> mode = p.get_custom_fan_mode();
+    set_property(this->custom_fan_mode, mode, need_publish);
+  } else {
+    this->custom_fan_mode.reset();
+    optional<climate::ClimateFanMode> mode = p.get_fan_mode();
+    set_property(this->fan_mode, mode, need_publish);
+  }
   set_property(this->swing_mode, p.get_swing_mode(), need_publish);
+  if (p.is_custom_preset()) {
+    this->preset.reset();
+    optional<std::string> preset = p.get_custom_preset();
+    set_property(this->custom_preset, preset, need_publish);
+  } else {
+    this->custom_preset.reset();
+    set_property(this->preset, p.get_preset(), need_publish);
+  }
   if (need_publish)
     this->publish_state();
   set_sensor(this->outdoor_sensor_, p.get_outdoor_temp());
@@ -61,6 +77,48 @@ void MideaAC::on_update() {
   }
 }
 
+bool MideaAC::allow_preset(climate::ClimatePreset preset) const {
+  switch (preset) {
+    case climate::CLIMATE_PRESET_ECO:
+      if (this->mode == climate::CLIMATE_MODE_COOL) {
+        return true;
+      } else {
+        ESP_LOGD(TAG, "ECO preset is only available in COOL mode");
+      }
+      break;
+    case climate::CLIMATE_PRESET_SLEEP:
+      if (this->mode == climate::CLIMATE_MODE_FAN_ONLY || this->mode == climate::CLIMATE_MODE_DRY) {
+        ESP_LOGD(TAG, "SLEEP preset is not available in FAN_ONLY or DRY mode");
+      } else {
+        return true;
+      }
+      break;
+    case climate::CLIMATE_PRESET_BOOST:
+      if (this->mode == climate::CLIMATE_MODE_HEAT || this->mode == climate::CLIMATE_MODE_COOL) {
+        return true;
+      } else {
+        ESP_LOGD(TAG, "BOOST preset is only available in HEAT or COOL mode");
+      }
+      break;
+    case climate::CLIMATE_PRESET_HOME:
+      return true;
+    default:
+      break;
+  }
+  return false;
+}
+
+bool MideaAC::allow_custom_preset(const std::string &custom_preset) const {
+  if (custom_preset == MIDEA_FREEZE_PROTECTION_PRESET) {
+    if (this->mode == climate::CLIMATE_MODE_HEAT) {
+      return true;
+    } else {
+      ESP_LOGD(TAG, "%s is only available in HEAT mode", MIDEA_FREEZE_PROTECTION_PRESET.c_str());
+    }
+  }
+  return false;
+}
+
 void MideaAC::control(const climate::ClimateCall &call) {
   if (call.get_mode().has_value() && call.get_mode().value() != this->mode) {
     this->cmd_frame_.set_mode(call.get_mode().value());
@@ -70,14 +128,34 @@ void MideaAC::control(const climate::ClimateCall &call) {
     this->cmd_frame_.set_target_temp(call.get_target_temperature().value());
     this->ctrl_request_ = true;
   }
-  if (call.get_fan_mode().has_value() && call.get_fan_mode().value() != this->fan_mode) {
+  if (call.get_fan_mode().has_value() &&
+      (!this->fan_mode.has_value() || this->fan_mode.value() != call.get_fan_mode().value())) {
+    this->custom_fan_mode.reset();
     this->cmd_frame_.set_fan_mode(call.get_fan_mode().value());
     this->ctrl_request_ = true;
   }
+  if (call.get_custom_fan_mode().has_value() &&
+      (!this->custom_fan_mode.has_value() || this->custom_fan_mode.value() != call.get_custom_fan_mode().value())) {
+    this->fan_mode.reset();
+    this->cmd_frame_.set_custom_fan_mode(call.get_custom_fan_mode().value());
+    this->ctrl_request_ = true;
+  }
   if (call.get_swing_mode().has_value() && call.get_swing_mode().value() != this->swing_mode) {
     this->cmd_frame_.set_swing_mode(call.get_swing_mode().value());
     this->ctrl_request_ = true;
   }
+  if (call.get_preset().has_value() && this->allow_preset(call.get_preset().value()) &&
+      (!this->preset.has_value() || this->preset.value() != call.get_preset().value())) {
+    this->custom_preset.reset();
+    this->cmd_frame_.set_preset(call.get_preset().value());
+    this->ctrl_request_ = true;
+  }
+  if (call.get_custom_preset().has_value() && this->allow_custom_preset(call.get_custom_preset().value()) &&
+      (!this->custom_preset.has_value() || this->custom_preset.value() != call.get_custom_preset().value())) {
+    this->preset.reset();
+    this->cmd_frame_.set_custom_preset(call.get_custom_preset().value());
+    this->ctrl_request_ = true;
+  }
   if (this->ctrl_request_) {
     this->cmd_frame_.set_beeper_feedback(this->beeper_feedback_);
     this->cmd_frame_.finalize();
@@ -98,10 +176,16 @@ climate::ClimateTraits MideaAC::traits() {
   traits.set_supports_fan_mode_low(true);
   traits.set_supports_fan_mode_medium(true);
   traits.set_supports_fan_mode_high(true);
+  traits.set_supported_custom_fan_modes(this->traits_custom_fan_modes_);
   traits.set_supports_swing_mode_off(true);
   traits.set_supports_swing_mode_vertical(true);
   traits.set_supports_swing_mode_horizontal(this->traits_swing_horizontal_);
   traits.set_supports_swing_mode_both(this->traits_swing_both_);
+  traits.set_supports_preset_home(true);
+  traits.set_supports_preset_eco(this->traits_preset_eco_);
+  traits.set_supports_preset_sleep(this->traits_preset_sleep_);
+  traits.set_supports_preset_boost(this->traits_preset_boost_);
+  traits.set_supported_custom_presets(this->traits_custom_presets_);
   traits.set_supports_current_temperature(true);
   return traits;
 }
diff --git a/esphome/components/midea_ac/midea_climate.h b/esphome/components/midea_ac/midea_climate.h
index f08350b252..0a63312961 100644
--- a/esphome/components/midea_ac/midea_climate.h
+++ b/esphome/components/midea_ac/midea_climate.h
@@ -22,6 +22,15 @@ class MideaAC : public midea_dongle::MideaAppliance, public climate::Climate, pu
   void set_beeper_feedback(bool state) { this->beeper_feedback_ = state; }
   void set_swing_horizontal(bool state) { this->traits_swing_horizontal_ = state; }
   void set_swing_both(bool state) { this->traits_swing_both_ = state; }
+  void set_preset_eco(bool state) { this->traits_preset_eco_ = state; }
+  void set_preset_sleep(bool state) { this->traits_preset_sleep_ = state; }
+  void set_preset_boost(bool state) { this->traits_preset_boost_ = state; }
+  bool allow_preset(climate::ClimatePreset preset) const;
+  void set_custom_fan_modes(std::vector<std::string> custom_fan_modes) {
+    this->traits_custom_fan_modes_ = custom_fan_modes;
+  }
+  void set_custom_presets(std::vector<std::string> custom_presets) { this->traits_custom_presets_ = custom_presets; }
+  bool allow_custom_preset(const std::string &custom_preset) const;
 
  protected:
   /// Override control to change settings of the climate device.
@@ -41,6 +50,11 @@ class MideaAC : public midea_dongle::MideaAppliance, public climate::Climate, pu
   bool beeper_feedback_{false};
   bool traits_swing_horizontal_{false};
   bool traits_swing_both_{false};
+  bool traits_preset_eco_{false};
+  bool traits_preset_sleep_{false};
+  bool traits_preset_boost_{false};
+  std::vector<std::string> traits_custom_fan_modes_{{}};
+  std::vector<std::string> traits_custom_presets_{{}};
 };
 
 }  // namespace midea_ac
diff --git a/esphome/components/midea_ac/midea_frame.cpp b/esphome/components/midea_ac/midea_frame.cpp
index 2d9be1bdc5..e90155bad3 100644
--- a/esphome/components/midea_ac/midea_frame.cpp
+++ b/esphome/components/midea_ac/midea_frame.cpp
@@ -3,6 +3,11 @@
 namespace esphome {
 namespace midea_ac {
 
+static const char *TAG = "midea_ac";
+const std::string MIDEA_SILENT_FAN_MODE = "silent";
+const std::string MIDEA_TURBO_FAN_MODE = "turbo";
+const std::string MIDEA_FREEZE_PROTECTION_PRESET = "freeze protection";
+
 const uint8_t QueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x00,
                                     0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                                     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x68};
@@ -80,6 +85,54 @@ void PropertiesFrame::set_mode(climate::ClimateMode mode) {
   this->pbuf_[12] |= m << 5;
 }
 
+optional<climate::ClimatePreset> PropertiesFrame::get_preset() const {
+  if (this->get_eco_mode()) {
+    return climate::CLIMATE_PRESET_ECO;
+  } else if (this->get_sleep_mode()) {
+    return climate::CLIMATE_PRESET_SLEEP;
+  } else if (this->get_turbo_mode()) {
+    return climate::CLIMATE_PRESET_BOOST;
+  } else {
+    return climate::CLIMATE_PRESET_HOME;
+  }
+}
+
+void PropertiesFrame::set_preset(climate::ClimatePreset preset) {
+  switch (preset) {
+    case climate::CLIMATE_PRESET_ECO:
+      this->set_eco_mode(true);
+      break;
+    case climate::CLIMATE_PRESET_SLEEP:
+      this->set_sleep_mode(true);
+      break;
+    case climate::CLIMATE_PRESET_BOOST:
+      this->set_turbo_mode(true);
+      break;
+    default:
+      break;
+  }
+}
+
+bool PropertiesFrame::is_custom_preset() const { return this->get_freeze_protection_mode(); }
+
+const std::string &PropertiesFrame::get_custom_preset() const { return midea_ac::MIDEA_FREEZE_PROTECTION_PRESET; };
+
+void PropertiesFrame::set_custom_preset(const std::string &preset) {
+  if (preset == MIDEA_FREEZE_PROTECTION_PRESET) {
+    this->set_freeze_protection_mode(true);
+  }
+}
+
+bool PropertiesFrame::is_custom_fan_mode() const {
+  switch (this->pbuf_[13]) {
+    case MIDEA_FAN_SILENT:
+    case MIDEA_FAN_TURBO:
+      return true;
+    default:
+      return false;
+  }
+}
+
 climate::ClimateFanMode PropertiesFrame::get_fan_mode() const {
   switch (this->pbuf_[13]) {
     case MIDEA_FAN_LOW:
@@ -112,6 +165,25 @@ void PropertiesFrame::set_fan_mode(climate::ClimateFanMode mode) {
   this->pbuf_[13] = m;
 }
 
+const std::string &PropertiesFrame::get_custom_fan_mode() const {
+  switch (this->pbuf_[13]) {
+    case MIDEA_FAN_SILENT:
+      return MIDEA_SILENT_FAN_MODE;
+    default:
+      return MIDEA_TURBO_FAN_MODE;
+  }
+}
+
+void PropertiesFrame::set_custom_fan_mode(const std::string &mode) {
+  uint8_t m;
+  if (mode == MIDEA_SILENT_FAN_MODE) {
+    m = MIDEA_FAN_SILENT;
+  } else {
+    m = MIDEA_FAN_TURBO;
+  }
+  this->pbuf_[13] = m;
+}
+
 climate::ClimateSwingMode PropertiesFrame::get_swing_mode() const {
   switch (this->pbuf_[17] & 0x0F) {
     case MIDEA_SWING_VERTICAL:
diff --git a/esphome/components/midea_ac/midea_frame.h b/esphome/components/midea_ac/midea_frame.h
index e07a5bf946..a84161b4af 100644
--- a/esphome/components/midea_ac/midea_frame.h
+++ b/esphome/components/midea_ac/midea_frame.h
@@ -5,6 +5,10 @@
 namespace esphome {
 namespace midea_ac {
 
+extern const std::string MIDEA_SILENT_FAN_MODE;
+extern const std::string MIDEA_TURBO_FAN_MODE;
+extern const std::string MIDEA_FREEZE_PROTECTION_PRESET;
+
 /// Enum for all modes a Midea device can be in.
 enum MideaMode : uint8_t {
   /// The Midea device is set to automatically change the heating/cooling cycle
@@ -23,12 +27,16 @@ enum MideaMode : uint8_t {
 enum MideaFanMode : uint8_t {
   /// The fan mode is set to Auto
   MIDEA_FAN_AUTO = 102,
+  /// The fan mode is set to Silent
+  MIDEA_FAN_SILENT = 20,
   /// The fan mode is set to Low
   MIDEA_FAN_LOW = 40,
   /// The fan mode is set to Medium
   MIDEA_FAN_MEDIUM = 60,
   /// The fan mode is set to High
   MIDEA_FAN_HIGH = 80,
+  /// The fan mode is set to Turbo
+  MIDEA_FAN_TURBO = 100,
 };
 
 /// Enum for all modes a Midea swing can be in
@@ -65,9 +73,13 @@ class PropertiesFrame : public midea_dongle::BaseFrame {
   void set_mode(climate::ClimateMode mode);
 
   /* FAN SPEED */
+  bool is_custom_fan_mode() const;
   climate::ClimateFanMode get_fan_mode() const;
   void set_fan_mode(climate::ClimateFanMode mode);
 
+  const std::string &get_custom_fan_mode() const;
+  void set_custom_fan_mode(const std::string &mode);
+
   /* SWING MODE */
   climate::ClimateSwingMode get_swing_mode() const;
   void set_swing_mode(climate::ClimateSwingMode mode);
@@ -82,16 +94,28 @@ class PropertiesFrame : public midea_dongle::BaseFrame {
   float get_humidity_setpoint() const;
 
   /* ECO MODE */
-  bool get_eco_mode() const { return this->pbuf_[19]; }
-  void set_eco_mode(bool state) { this->set_bytemask_(19, 0xFF, state); }
+  bool get_eco_mode() const { return this->pbuf_[19] & 0x10; }
+  void set_eco_mode(bool state) { this->set_bytemask_(19, 0x80, state); }
 
   /* SLEEP MODE */
   bool get_sleep_mode() const { return this->pbuf_[20] & 0x01; }
   void set_sleep_mode(bool state) { this->set_bytemask_(20, 0x01, state); }
 
   /* TURBO MODE */
-  bool get_turbo_mode() const { return this->pbuf_[20] & 0x02; }
-  void set_turbo_mode(bool state) { this->set_bytemask_(20, 0x02, state); }
+  bool get_turbo_mode() const { return this->pbuf_[18] & 0x20; }
+  void set_turbo_mode(bool state) { this->set_bytemask_(18, 0x20, state); }
+
+  /* FREEZE PROTECTION */
+  bool get_freeze_protection_mode() const { return this->pbuf_[31] & 0x80; }
+  void set_freeze_protection_mode(bool state) { this->set_bytemask_(31, 0x80, state); }
+
+  /* PRESET */
+  optional<climate::ClimatePreset> get_preset() const;
+  void set_preset(climate::ClimatePreset preset);
+
+  bool is_custom_preset() const;
+  const std::string &get_custom_preset() const;
+  void set_custom_preset(const std::string &preset);
 
   /* POWER USAGE */
   float get_power_usage() const;
diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp
index 2a95ed2f64..47923dc924 100644
--- a/esphome/components/mqtt/mqtt_climate.cpp
+++ b/esphome/components/mqtt/mqtt_climate.cpp
@@ -35,6 +35,8 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC
     modes.add("fan_only");
   if (traits.supports_mode(CLIMATE_MODE_DRY))
     modes.add("dry");
+  if (traits.supports_mode(CLIMATE_MODE_HEAT_COOL))
+    modes.add("heat_cool");
 
   if (traits.get_supports_two_point_target_temperature()) {
     // temperature_low_command_topic
@@ -231,6 +233,9 @@ bool MQTTClimateComponent::publish_state_() {
     case CLIMATE_MODE_DRY:
       mode_s = "dry";
       break;
+    case CLIMATE_MODE_HEAT_COOL:
+      mode_s = "heat_cool";
+      break;
   }
   bool success = true;
   if (!this->publish(this->get_mode_state_topic(), mode_s))
@@ -287,7 +292,7 @@ bool MQTTClimateComponent::publish_state_() {
 
   if (traits.get_supports_fan_modes()) {
     const char *payload = "";
-    switch (this->device_->fan_mode) {
+    switch (this->device_->fan_mode.value()) {
       case CLIMATE_FAN_ON:
         payload = "on";
         break;
diff --git a/esphome/components/tcl112/tcl112.cpp b/esphome/components/tcl112/tcl112.cpp
index 3e7eb7ec9a..91cec27094 100644
--- a/esphome/components/tcl112/tcl112.cpp
+++ b/esphome/components/tcl112/tcl112.cpp
@@ -88,7 +88,7 @@ void Tcl112Climate::transmit_state() {
 
   // Set fan
   uint8_t selected_fan;
-  switch (this->fan_mode) {
+  switch (this->fan_mode.value()) {
     case climate::CLIMATE_FAN_HIGH:
       selected_fan = TCL112_FAN_HIGH;
       break;
diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp
index 64a7c1b05d..3bab0e85fd 100644
--- a/esphome/components/thermostat/thermostat_climate.cpp
+++ b/esphome/components/thermostat/thermostat_climate.cpp
@@ -33,7 +33,7 @@ float ThermostatClimate::hysteresis() { return this->hysteresis_; }
 void ThermostatClimate::refresh() {
   this->switch_to_mode_(this->mode);
   this->switch_to_action_(compute_action_());
-  this->switch_to_fan_mode_(this->fan_mode);
+  this->switch_to_fan_mode_(this->fan_mode.value());
   this->switch_to_swing_mode_(this->swing_mode);
   this->publish_state();
 }
diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp
index eba08d5bbe..e7c93246f2 100644
--- a/esphome/components/whirlpool/whirlpool.cpp
+++ b/esphome/components/whirlpool/whirlpool.cpp
@@ -81,7 +81,7 @@ void WhirlpoolClimate::transmit_state() {
   remote_state[3] |= (uint8_t)(temp - this->temperature_min_()) << 4;
 
   // Fan speed
-  switch (this->fan_mode) {
+  switch (this->fan_mode.value()) {
     case climate::CLIMATE_FAN_HIGH:
       remote_state[2] |= WHIRLPOOL_FAN_HIGH;
       break;
diff --git a/esphome/const.py b/esphome/const.py
index 18ef10bb01..907a49e2ad 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -145,6 +145,10 @@ CONF_CSS_URL = "css_url"
 CONF_CURRENT = "current"
 CONF_CURRENT_OPERATION = "current_operation"
 CONF_CURRENT_RESISTOR = "current_resistor"
+CONF_CUSTOM_FAN_MODE = "custom_fan_mode"
+CONF_CUSTOM_FAN_MODES = "custom_fan_modes"
+CONF_CUSTOM_PRESET = "custom_preset"
+CONF_CUSTOM_PRESETS = "custom_presets"
 CONF_DALLAS_ID = "dallas_id"
 CONF_DATA = "data"
 CONF_DATA_PIN = "data_pin"
@@ -449,6 +453,10 @@ CONF_POWER_FACTOR = "power_factor"
 CONF_POWER_ON_VALUE = "power_on_value"
 CONF_POWER_SAVE_MODE = "power_save_mode"
 CONF_POWER_SUPPLY = "power_supply"
+CONF_PRESET = "preset"
+CONF_PRESET_BOOST = "preset_boost"
+CONF_PRESET_ECO = "preset_eco"
+CONF_PRESET_SLEEP = "preset_sleep"
 CONF_PRESSURE = "pressure"
 CONF_PRIORITY = "priority"
 CONF_PROTOCOL = "protocol"
diff --git a/tests/test1.yaml b/tests/test1.yaml
index 378d4192d3..29df5857d3 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -1525,23 +1525,6 @@ climate:
     name: Toshiba Climate
   - platform: hitachi_ac344
     name: Hitachi Climate
-  - platform: midea_ac
-    visual:
-      min_temperature: 18 °C
-      max_temperature: 25 °C
-      temperature_step: 0.1 °C
-    name: 'Electrolux EACS'
-    beeper: true
-    outdoor_temperature:
-      name: 'Temp'
-    power_usage:
-      name: 'Power'
-    humidity_setpoint:
-      name: 'Hum'
-
-midea_dongle:
-  uart_id: uart0
-  strength_icon: true
 
 switch:
   - platform: gpio
diff --git a/tests/test3.yaml b/tests/test3.yaml
index 8f6b33bd5d..af5398b604 100644
--- a/tests/test3.yaml
+++ b/tests/test3.yaml
@@ -658,6 +658,17 @@ script:
   - id: my_script
     then:
       - lambda: 'ESP_LOGD("main", "Hello World!");'
+  - id: climate_custom
+    then:
+      - climate.control:
+          id: midea_ac_unit
+          custom_preset: FREEZE_PROTECTION
+          custom_fan_mode: SILENT
+  - id: climate_preset
+    then:
+      - climate.control:
+          id: midea_ac_unit
+          preset: SLEEP
 
 sm2135:
   data_pin: GPIO12
@@ -819,6 +830,32 @@ climate:
       kp: 0.0
       ki: 0.0
       kd: 0.0
+  - platform: midea_ac
+    id: midea_ac_unit
+    visual:
+      min_temperature: 18 °C
+      max_temperature: 25 °C
+      temperature_step: 0.1 °C
+    name: "Electrolux EACS"
+    beeper: true
+    custom_fan_modes:
+      - SILENT
+      - TURBO
+    preset_eco: true
+    preset_sleep: true
+    preset_boost: true
+    custom_presets:
+      - FREEZE_PROTECTION
+    outdoor_temperature:
+      name: "Temp"
+    power_usage:
+      name: "Power"
+    humidity_setpoint:
+      name: "Hum"
+
+midea_dongle:
+  uart_id: uart1
+  strength_icon: true
 
 cover:
   - platform: endstop
diff --git a/tests/test4.yaml b/tests/test4.yaml
index 9e09f20449..7868fd4968 100644
--- a/tests/test4.yaml
+++ b/tests/test4.yaml
@@ -166,6 +166,7 @@ display:
       it.rectangle(3, 3, it.get_width()-6, it.get_height()-6, red);
     rotation: 0°
     update_interval: 16ms
+    
   - platform: waveshare_epaper
     cs_pin: GPIO23
     dc_pin: GPIO23