From ebadaa966016dd9cfbc7fbd3046883ade9be0214 Mon Sep 17 00:00:00 2001 From: Lumpusz 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(climate->fan_mode); + if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) + resp.fan_mode = static_cast(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(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(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(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(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(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(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(msg.preset)); + if (msg.has_custom_preset) + call.set_preset(msg.custom_preset); if (msg.has_swing_mode) call.set_swing_mode(static_cast(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::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::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::Climate return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(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()); return true; } + case 16: { + this->supported_presets.push_back(value.as_enum()); + 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(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(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(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(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(); return true; } + case 12: { + this->preset = value.as_enum(); + 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(8, this->action); buffer.encode_enum(9, this->fan_mode); buffer.encode_enum(10, this->swing_mode); + buffer.encode_string(11, this->custom_fan_mode); + buffer.encode_enum(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(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(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(); 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(); + 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(13, this->fan_mode); buffer.encode_bool(14, this->has_swing_mode); buffer.encode_enum(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(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(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(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 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 supported_fan_modes{}; - std::vector 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 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 supported_fan_modes{}; // NOLINT + std::vector supported_swing_modes{}; // NOLINT + std::vector supported_custom_fan_modes{}; // NOLINT + std::vector supported_presets{}; // NOLINT + std::vector 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 class ControlAction : public Action { 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 class ControlAction : public Action { 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 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 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 &ClimateCall::get_target_temperature_low() const { return const optional &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } const optional &ClimateCall::get_away() const { return this->away_; } const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; } +const optional &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } +const optional &ClimateCall::get_preset() const { return this->preset_; } +const optional &ClimateCall::get_custom_preset() const { return this->custom_preset_; } const optional &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 mode) { } ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { this->fan_mode_ = fan_mode; + this->custom_fan_mode_.reset(); + return *this; +} +ClimateCall &ClimateCall::set_preset(optional preset) { + this->preset_ = preset; + this->custom_preset_.reset(); return *this; } ClimateCall &ClimateCall::set_swing_mode(optional 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 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 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 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 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 preset); void perform(); @@ -86,6 +97,9 @@ class ClimateCall { const optional &get_away() const; const optional &get_fan_mode() const; const optional &get_swing_mode() const; + const optional &get_custom_fan_mode() const; + const optional &get_preset() const; + const optional &get_custom_preset() const; protected: void validate_(); @@ -98,13 +112,25 @@ class ClimateCall { optional away_; optional fan_mode_; optional swing_mode_; + optional custom_fan_mode_; + optional preset_; + optional 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 fan_mode; /// The active swing mode of the climate device. ClimateSwingMode swing_mode; + /// The active custom fan mode of the climate device. + optional custom_fan_mode; + + /// The active preset of the climate device. + optional preset; + + /// The active custom preset mode of the climate device. + optional 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 &supported_custom_fan_modes) { + this->supported_custom_fan_modes_ = supported_custom_fan_modes; +} +const std::vector 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 &supported_custom_presets) { + this->supported_custom_presets_ = supported_custom_presets; +} +const std::vector 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 &supported_custom_fan_modes); + const std::vector 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 &supported_custom_presets); + const std::vector 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 supported_custom_fan_modes_; + std::vector 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(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 mode = p.get_custom_fan_mode(); + set_property(this->custom_fan_mode, mode, need_publish); + } else { + this->custom_fan_mode.reset(); + optional 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 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 custom_fan_modes) { + this->traits_custom_fan_modes_ = custom_fan_modes; + } + void set_custom_presets(std::vector 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 traits_custom_fan_modes_{{}}; + std::vector 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 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 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