From 1814e4a46bf7ec7eb42294c44e8535b24ee06348 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sat, 16 Nov 2019 12:34:11 -0300 Subject: [PATCH] Add climate dry fan (#845) * add climate dry fan * clang-format * updates, add swing mode, add back compat with old ha * revert client-config add swing * sort const.py * fix missing retur --- esphome/components/api/api.proto | 27 ++++ esphome/components/api/api_connection.cpp | 21 ++- esphome/components/api/api_pb2.cpp | 124 ++++++++++++++++ esphome/components/api/api_pb2.h | 69 ++++++--- esphome/components/api/api_pb2_service.cpp | 2 + esphome/components/api/api_pb2_service.h | 2 + esphome/components/climate/__init__.py | 38 ++++- esphome/components/climate/automation.h | 4 + esphome/components/climate/climate.cpp | 108 ++++++++++++++ esphome/components/climate/climate.h | 24 ++++ esphome/components/climate/climate_mode.cpp | 44 ++++++ esphome/components/climate/climate_mode.h | 46 ++++++ esphome/components/climate/climate_traits.cpp | 94 +++++++++++++ esphome/components/climate/climate_traits.h | 40 ++++++ esphome/components/climate_ir/climate_ir.cpp | 56 +++++++- esphome/components/climate_ir/climate_ir.h | 13 +- esphome/components/coolix/coolix.cpp | 133 +++++++++++------- esphome/components/coolix/coolix.h | 17 ++- esphome/components/mqtt/mqtt_climate.cpp | 10 ++ esphome/const.py | 2 + script/api_protobuf/api_protobuf.py | 35 ++++- 21 files changed, 824 insertions(+), 85 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4e55744384..2e01856a3b 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -653,6 +653,25 @@ enum ClimateMode { CLIMATE_MODE_AUTO = 1; CLIMATE_MODE_COOL = 2; CLIMATE_MODE_HEAT = 3; + CLIMATE_MODE_FAN_ONLY = 4; + CLIMATE_MODE_DRY = 5; +} +enum ClimateFanMode { + CLIMATE_FAN_ON = 0; + CLIMATE_FAN_OFF = 1; + CLIMATE_FAN_AUTO = 2; + CLIMATE_FAN_LOW = 3; + CLIMATE_FAN_MEDIUM = 4; + CLIMATE_FAN_HIGH = 5; + CLIMATE_FAN_MIDDLE = 6; + CLIMATE_FAN_FOCUS = 7; + CLIMATE_FAN_DIFFUSE = 8; +} +enum ClimateSwingMode { + CLIMATE_SWING_OFF = 0; + CLIMATE_SWING_BOTH = 1; + CLIMATE_SWING_VERTICAL = 2; + CLIMATE_SWINT_HORIZONTAL = 3; } enum ClimateAction { CLIMATE_ACTION_OFF = 0; @@ -678,6 +697,8 @@ message ListEntitiesClimateResponse { float visual_temperature_step = 10; bool supports_away = 11; bool supports_action = 12; + repeated ClimateFanMode supported_fan_modes = 13; + repeated ClimateSwingMode supported_swing_modes = 14; } message ClimateStateResponse { option (id) = 47; @@ -693,6 +714,8 @@ message ClimateStateResponse { float target_temperature_high = 6; bool away = 7; ClimateAction action = 8; + ClimateFanMode fan_mode = 9; + ClimateSwingMode swing_mode = 10; } message ClimateCommandRequest { option (id) = 48; @@ -711,4 +734,8 @@ message ClimateCommandRequest { float target_temperature_high = 9; bool has_away = 10; bool away = 11; + bool has_fan_mode = 12; + ClimateFanMode fan_mode = 13; + bool has_swing_mode = 14; + ClimateSwingMode swing_mode = 15; } diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index a329e81cee..8844aa1e1a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -458,6 +458,10 @@ 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_swing_modes()) + resp.swing_mode = static_cast(climate->swing_mode); return this->send_climate_state_response(resp); } bool APIConnection::send_climate_info(climate::Climate *climate) { @@ -470,7 +474,7 @@ bool APIConnection::send_climate_info(climate::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_HEAT, climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_FAN_ONLY}) { if (traits.supports_mode(mode)) msg.supported_modes.push_back(static_cast(mode)); } @@ -479,6 +483,17 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.visual_temperature_step = traits.get_visual_temperature_step(); msg.supports_away = traits.get_supports_away(); msg.supports_action = traits.get_supports_action(); + for (auto fan_mode : {climate::CLIMATE_FAN_ON, climate::CLIMATE_FAN_OFF, climate::CLIMATE_FAN_AUTO, + climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, + climate::CLIMATE_FAN_MIDDLE, climate::CLIMATE_FAN_FOCUS, climate::CLIMATE_FAN_DIFFUSE}) { + if (traits.supports_fan_mode(fan_mode)) + msg.supported_fan_modes.push_back(static_cast(fan_mode)); + } + 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)) + msg.supported_swing_modes.push_back(static_cast(swing_mode)); + } return this->send_list_entities_climate_response(msg); } void APIConnection::climate_command(const ClimateCommandRequest &msg) { @@ -497,6 +512,10 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { call.set_target_temperature_high(msg.target_temperature_high); if (msg.has_away) call.set_away(msg.away); + if (msg.has_fan_mode) + call.set_fan_mode(static_cast(msg.fan_mode)); + if (msg.has_swing_mode) + call.set_swing_mode(static_cast(msg.swing_mode)); call.perform(); } #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 0b6021c224..cca488decf 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1,3 +1,5 @@ +// This file was automatically generated with a tool. +// See scripts/api_protobuf/api_protobuf.py #include "api_pb2.h" #include "esphome/core/log.h" @@ -102,6 +104,48 @@ template<> const char *proto_enum_to_string(enums::ClimateMo return "CLIMATE_MODE_COOL"; case enums::CLIMATE_MODE_HEAT: return "CLIMATE_MODE_HEAT"; + case enums::CLIMATE_MODE_FAN_ONLY: + return "CLIMATE_MODE_FAN_ONLY"; + case enums::CLIMATE_MODE_DRY: + return "CLIMATE_MODE_DRY"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::ClimateFanMode value) { + switch (value) { + case enums::CLIMATE_FAN_ON: + return "CLIMATE_FAN_ON"; + case enums::CLIMATE_FAN_OFF: + return "CLIMATE_FAN_OFF"; + case enums::CLIMATE_FAN_AUTO: + return "CLIMATE_FAN_AUTO"; + case enums::CLIMATE_FAN_LOW: + return "CLIMATE_FAN_LOW"; + case enums::CLIMATE_FAN_MEDIUM: + return "CLIMATE_FAN_MEDIUM"; + case enums::CLIMATE_FAN_HIGH: + return "CLIMATE_FAN_HIGH"; + case enums::CLIMATE_FAN_MIDDLE: + return "CLIMATE_FAN_MIDDLE"; + case enums::CLIMATE_FAN_FOCUS: + return "CLIMATE_FAN_FOCUS"; + case enums::CLIMATE_FAN_DIFFUSE: + return "CLIMATE_FAN_DIFFUSE"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::ClimateSwingMode value) { + switch (value) { + case enums::CLIMATE_SWING_OFF: + return "CLIMATE_SWING_OFF"; + case enums::CLIMATE_SWING_BOTH: + return "CLIMATE_SWING_BOTH"; + case enums::CLIMATE_SWING_VERTICAL: + return "CLIMATE_SWING_VERTICAL"; + case enums::CLIMATE_SWINT_HORIZONTAL: + return "CLIMATE_SWINT_HORIZONTAL"; default: return "UNKNOWN"; } @@ -2458,6 +2502,14 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->supports_action = value.as_bool(); return true; } + case 13: { + this->supported_fan_modes.push_back(value.as_enum()); + return true; + } + case 14: { + this->supported_swing_modes.push_back(value.as_enum()); + return true; + } default: return false; } @@ -2517,6 +2569,12 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(10, this->visual_temperature_step); buffer.encode_bool(11, this->supports_away); buffer.encode_bool(12, this->supports_action); + for (auto &it : this->supported_fan_modes) { + buffer.encode_enum(13, it, true); + } + for (auto &it : this->supported_swing_modes) { + buffer.encode_enum(14, it, true); + } } void ListEntitiesClimateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -2574,6 +2632,18 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(" supports_action: "); out.append(YESNO(this->supports_action)); out.append("\n"); + + for (const auto &it : this->supported_fan_modes) { + out.append(" supported_fan_modes: "); + out.append(proto_enum_to_string(it)); + out.append("\n"); + } + + for (const auto &it : this->supported_swing_modes) { + out.append(" supported_swing_modes: "); + out.append(proto_enum_to_string(it)); + out.append("\n"); + } out.append("}"); } bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -2590,6 +2660,14 @@ bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->action = value.as_enum(); return true; } + case 9: { + this->fan_mode = value.as_enum(); + return true; + } + case 10: { + this->swing_mode = value.as_enum(); + return true; + } default: return false; } @@ -2629,6 +2707,8 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(6, this->target_temperature_high); buffer.encode_bool(7, this->away); buffer.encode_enum(8, this->action); + buffer.encode_enum(9, this->fan_mode); + buffer.encode_enum(10, this->swing_mode); } void ClimateStateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -2669,6 +2749,14 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(" action: "); out.append(proto_enum_to_string(this->action)); out.append("\n"); + + out.append(" fan_mode: "); + out.append(proto_enum_to_string(this->fan_mode)); + out.append("\n"); + + out.append(" swing_mode: "); + out.append(proto_enum_to_string(this->swing_mode)); + out.append("\n"); out.append("}"); } bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -2701,6 +2789,22 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) this->away = value.as_bool(); return true; } + case 12: { + this->has_fan_mode = value.as_bool(); + return true; + } + case 13: { + this->fan_mode = value.as_enum(); + return true; + } + case 14: { + this->has_swing_mode = value.as_bool(); + return true; + } + case 15: { + this->swing_mode = value.as_enum(); + return true; + } default: return false; } @@ -2739,6 +2843,10 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(9, this->target_temperature_high); buffer.encode_bool(10, this->has_away); buffer.encode_bool(11, this->away); + buffer.encode_bool(12, this->has_fan_mode); + buffer.encode_enum(13, this->fan_mode); + buffer.encode_bool(14, this->has_swing_mode); + buffer.encode_enum(15, this->swing_mode); } void ClimateCommandRequest::dump_to(std::string &out) const { char buffer[64]; @@ -2790,6 +2898,22 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append(" away: "); out.append(YESNO(this->away)); out.append("\n"); + + out.append(" has_fan_mode: "); + out.append(YESNO(this->has_fan_mode)); + out.append("\n"); + + out.append(" fan_mode: "); + out.append(proto_enum_to_string(this->fan_mode)); + out.append("\n"); + + out.append(" has_swing_mode: "); + out.append(YESNO(this->has_swing_mode)); + out.append("\n"); + + out.append(" swing_mode: "); + out.append(proto_enum_to_string(this->swing_mode)); + out.append("\n"); out.append("}"); } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 3fe64fcb61..fc855a889a 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1,3 +1,5 @@ +// This file was automatically generated with a tool. +// See scripts/api_protobuf/api_protobuf.py #pragma once #include "proto.h" @@ -50,6 +52,25 @@ enum ClimateMode : uint32_t { CLIMATE_MODE_AUTO = 1, CLIMATE_MODE_COOL = 2, CLIMATE_MODE_HEAT = 3, + CLIMATE_MODE_FAN_ONLY = 4, + CLIMATE_MODE_DRY = 5, +}; +enum ClimateFanMode : uint32_t { + CLIMATE_FAN_ON = 0, + CLIMATE_FAN_OFF = 1, + CLIMATE_FAN_AUTO = 2, + CLIMATE_FAN_LOW = 3, + CLIMATE_FAN_MEDIUM = 4, + CLIMATE_FAN_HIGH = 5, + CLIMATE_FAN_MIDDLE = 6, + CLIMATE_FAN_FOCUS = 7, + CLIMATE_FAN_DIFFUSE = 8, +}; +enum ClimateSwingMode : uint32_t { + CLIMATE_SWING_OFF = 0, + CLIMATE_SWING_BOTH = 1, + CLIMATE_SWING_VERTICAL = 2, + CLIMATE_SWINT_HORIZONTAL = 3, }; enum ClimateAction : uint32_t { CLIMATE_ACTION_OFF = 0, @@ -643,18 +664,20 @@ class CameraImageRequest : public ProtoMessage { }; class ListEntitiesClimateResponse : public ProtoMessage { public: - 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::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 void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -665,14 +688,16 @@ class ListEntitiesClimateResponse : public ProtoMessage { }; class ClimateStateResponse : public ProtoMessage { public: - 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 + 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 void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -693,6 +718,10 @@ class ClimateCommandRequest : public ProtoMessage { 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 void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 13e123c10f..ea6b647c72 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -1,3 +1,5 @@ +// This file was automatically generated with a tool. +// See scripts/api_protobuf/api_protobuf.py #include "api_pb2_service.h" #include "esphome/core/log.h" diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 16662811fe..afbe39e314 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -1,3 +1,5 @@ +// This file was automatically generated with a tool. +// See scripts/api_protobuf/api_protobuf.py #pragma once #include "api_pb2.h" diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 8c9db58694..843b888218 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -5,7 +5,7 @@ from esphome.components import mqtt from esphome.const import CONF_AWAY, CONF_ID, CONF_INTERNAL, CONF_MAX_TEMPERATURE, \ CONF_MIN_TEMPERATURE, CONF_MODE, CONF_TARGET_TEMPERATURE, \ CONF_TARGET_TEMPERATURE_HIGH, CONF_TARGET_TEMPERATURE_LOW, CONF_TEMPERATURE_STEP, CONF_VISUAL, \ - CONF_MQTT_ID, CONF_NAME + CONF_MQTT_ID, CONF_NAME, CONF_FAN_MODE, CONF_SWING_MODE from esphome.core import CORE, coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -22,9 +22,35 @@ CLIMATE_MODES = { 'AUTO': ClimateMode.CLIMATE_MODE_AUTO, 'COOL': ClimateMode.CLIMATE_MODE_COOL, 'HEAT': ClimateMode.CLIMATE_MODE_HEAT, + 'DRY': ClimateMode.CLIMATE_MODE_DRY, + 'FAN_ONLY': ClimateMode.CLIMATE_MODE_FAN_ONLY, +} +validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True) + +ClimateFanMode = climate_ns.enum('ClimateFanMode') +CLIMATE_FAN_MODES = { + 'ON': ClimateFanMode.CLIMATE_FAN_ON, + 'OFF': ClimateFanMode.CLIMATE_FAN_OFF, + 'AUTO': ClimateFanMode.CLIMATE_FAN_AUTO, + 'LOW': ClimateFanMode.CLIMATE_FAN_LOW, + 'MEDIUM': ClimateFanMode.CLIMATE_FAN_MEDIUM, + 'HIGH': ClimateFanMode.CLIMATE_FAN_HIGH, + 'MIDDLE': ClimateFanMode.CLIMATE_FAN_MIDDLE, + 'FOCUS': ClimateFanMode.CLIMATE_FAN_FOCUS, + 'DIFFUSE': ClimateFanMode.CLIMATE_FAN_DIFFUSE, } -validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True) +validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True) + +ClimateSwingMode = climate_ns.enum('ClimateSwingMode') +CLIMATE_SWING_MODES = { + 'OFF': ClimateSwingMode.CLIMATE_SWING_OFF, + 'BOTH': ClimateSwingMode.CLIMATE_SWING_BOTH, + 'VERTICAL': ClimateSwingMode.CLIMATE_SWING_VERTICAL, + 'HORIZONTAL': ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, +} + +validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) # Actions ControlAction = climate_ns.class_('ControlAction', automation.Action) @@ -74,6 +100,8 @@ 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.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode), }) @@ -96,6 +124,12 @@ def climate_control_to_code(config, action_id, template_arg, args): if CONF_AWAY in config: template_ = yield cg.templatable(config[CONF_AWAY], args, bool) cg.add(var.set_away(template_)) + if CONF_FAN_MODE in config: + template_ = yield cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) + cg.add(var.set_fan_mode(template_)) + if CONF_SWING_MODE in config: + template_ = yield cg.templatable(config[CONF_SWING_MODE], args, ClimateSwingMode) + cg.add(var.set_swing_mode(template_)) yield var diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 845773a0ab..0cd52b1036 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -15,6 +15,8 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(float, target_temperature_low) TEMPLATABLE_VALUE(float, target_temperature_high) TEMPLATABLE_VALUE(bool, away) + TEMPLATABLE_VALUE(ClimateFanMode, fan_mode) + TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode) void play(Ts... x) override { auto call = this->climate_->make_call(); @@ -23,6 +25,8 @@ template class ControlAction : public Action { call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...)); 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_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 7c7da6bb0c..443290ed6d 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -13,6 +13,14 @@ void ClimateCall::perform() { const char *mode_s = climate_mode_to_string(*this->mode_); ESP_LOGD(TAG, " Mode: %s", mode_s); } + if (this->fan_mode_.has_value()) { + const char *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); + ESP_LOGD(TAG, " Fan: %s", fan_mode_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); + } if (this->target_temperature_.has_value()) { ESP_LOGD(TAG, " Target Temperature: %.2f", *this->target_temperature_); } @@ -36,6 +44,20 @@ void ClimateCall::validate_() { this->mode_.reset(); } } + 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->swing_mode_.has_value()) { + auto swing_mode = *this->swing_mode_; + if (!traits.supports_swing_mode(swing_mode)) { + ESP_LOGW(TAG, " Swing Mode %s is not supported by this device!", climate_swing_mode_to_string(swing_mode)); + this->swing_mode_.reset(); + } + } if (this->target_temperature_.has_value()) { auto target = *this->target_temperature_; if (traits.get_supports_two_point_target_temperature()) { @@ -91,11 +113,63 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) { this->set_mode(CLIMATE_MODE_COOL); } else if (str_equals_case_insensitive(mode, "HEAT")) { this->set_mode(CLIMATE_MODE_HEAT); + } else if (str_equals_case_insensitive(mode, "FAN_ONLY")) { + this->set_mode(CLIMATE_MODE_FAN_ONLY); + } else if (str_equals_case_insensitive(mode, "DRY")) { + this->set_mode(CLIMATE_MODE_DRY); } else { ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str()); } return *this; } +ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { + this->fan_mode_ = fan_mode; + return *this; +} +ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { + if (str_equals_case_insensitive(fan_mode, "ON")) { + this->set_fan_mode(CLIMATE_FAN_ON); + } else if (str_equals_case_insensitive(fan_mode, "OFF")) { + this->set_fan_mode(CLIMATE_FAN_OFF); + } else if (str_equals_case_insensitive(fan_mode, "AUTO")) { + this->set_fan_mode(CLIMATE_FAN_AUTO); + } else if (str_equals_case_insensitive(fan_mode, "LOW")) { + this->set_fan_mode(CLIMATE_FAN_LOW); + } else if (str_equals_case_insensitive(fan_mode, "MEDIUM")) { + this->set_fan_mode(CLIMATE_FAN_MEDIUM); + } else if (str_equals_case_insensitive(fan_mode, "HIGH")) { + this->set_fan_mode(CLIMATE_FAN_HIGH); + } else if (str_equals_case_insensitive(fan_mode, "MIDDLE")) { + this->set_fan_mode(CLIMATE_FAN_MIDDLE); + } else if (str_equals_case_insensitive(fan_mode, "FOCUS")) { + this->set_fan_mode(CLIMATE_FAN_FOCUS); + } 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()); + } + return *this; +} + +ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) { + this->swing_mode_ = swing_mode; + return *this; +} +ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) { + if (str_equals_case_insensitive(swing_mode, "OFF")) { + this->set_swing_mode(CLIMATE_SWING_OFF); + } else if (str_equals_case_insensitive(swing_mode, "BOTH")) { + this->set_swing_mode(CLIMATE_SWING_BOTH); + } else if (str_equals_case_insensitive(swing_mode, "VERTICAL")) { + this->set_swing_mode(CLIMATE_SWING_VERTICAL); + } else if (str_equals_case_insensitive(swing_mode, "HORIZONTAL")) { + this->set_swing_mode(CLIMATE_SWING_HORIZONTAL); + } else { + ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str()); + } + return *this; +} + ClimateCall &ClimateCall::set_target_temperature(float target_temperature) { this->target_temperature_ = target_temperature; return *this; @@ -113,6 +187,8 @@ const optional &ClimateCall::get_target_temperature() const { return this const optional &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } 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_swing_mode() const { return this->swing_mode_; } ClimateCall &ClimateCall::set_away(bool away) { this->away_ = away; return *this; @@ -137,6 +213,14 @@ ClimateCall &ClimateCall::set_mode(optional mode) { this->mode_ = mode; return *this; } +ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { + this->fan_mode_ = fan_mode; + return *this; +} +ClimateCall &ClimateCall::set_swing_mode(optional swing_mode) { + this->swing_mode_ = swing_mode; + return *this; +} void Climate::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); @@ -165,6 +249,12 @@ 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_swing_modes()) { + state.swing_mode = this->swing_mode; + } this->rtc_.save(&state); } @@ -176,6 +266,12 @@ 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_swing_modes()) { + ESP_LOGD(TAG, " Swing Mode: %s", climate_swing_mode_to_string(this->swing_mode)); + } if (traits.get_supports_current_temperature()) { ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature); } @@ -236,6 +332,12 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { if (traits.get_supports_away()) { call.set_away(this->away); } + if (traits.get_supports_fan_modes()) { + call.set_fan_mode(this->fan_mode); + } + if (traits.get_supports_swing_modes()) { + call.set_swing_mode(this->swing_mode); + } return call; } void ClimateDeviceRestoreState::apply(Climate *climate) { @@ -250,6 +352,12 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { if (traits.get_supports_away()) { climate->away = this->away; } + if (traits.get_supports_fan_modes()) { + climate->fan_mode = this->fan_mode; + } + if (traits.get_supports_swing_modes()) { + climate->swing_mode = this->swing_mode; + } climate->publish_state(); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 4dd872bbed..786afe097a 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -64,6 +64,18 @@ class ClimateCall { ClimateCall &set_target_temperature_high(optional target_temperature_high); ClimateCall &set_away(bool away); ClimateCall &set_away(optional away); + /// Set the fan mode of the climate device. + ClimateCall &set_fan_mode(ClimateFanMode fan_mode); + /// Set the fan mode of the climate device. + 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 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); void perform(); @@ -72,6 +84,8 @@ class ClimateCall { const optional &get_target_temperature_low() const; const optional &get_target_temperature_high() const; const optional &get_away() const; + const optional &get_fan_mode() const; + const optional &get_swing_mode() const; protected: void validate_(); @@ -82,12 +96,16 @@ class ClimateCall { optional target_temperature_low_; optional target_temperature_high_; optional away_; + optional fan_mode_; + optional swing_mode_; }; /// Struct used to save the state of the climate device in restore memory. struct ClimateDeviceRestoreState { ClimateMode mode; bool away; + ClimateFanMode fan_mode; + ClimateSwingMode swing_mode; union { float target_temperature; struct { @@ -149,6 +167,12 @@ class Climate : public Nameable { */ bool away{false}; + /// The active fan mode of the climate device. + ClimateFanMode fan_mode; + + /// The active swing mode of the climate device. + ClimateSwingMode swing_mode; + /** 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 34aa564fb0..aa06ce87f0 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -13,6 +13,10 @@ const char *climate_mode_to_string(ClimateMode mode) { return "COOL"; case CLIMATE_MODE_HEAT: return "HEAT"; + case CLIMATE_MODE_FAN_ONLY: + return "FAN_ONLY"; + case CLIMATE_MODE_DRY: + return "DRY"; default: return "UNKNOWN"; } @@ -30,5 +34,45 @@ const char *climate_action_to_string(ClimateAction action) { } } +const char *climate_fan_mode_to_string(ClimateFanMode fan_mode) { + switch (fan_mode) { + case climate::CLIMATE_FAN_ON: + return "ON"; + case climate::CLIMATE_FAN_OFF: + return "OFF"; + case climate::CLIMATE_FAN_AUTO: + return "AUTO"; + case climate::CLIMATE_FAN_LOW: + return "LOW"; + case climate::CLIMATE_FAN_MEDIUM: + return "MEDIUM"; + case climate::CLIMATE_FAN_HIGH: + return "HIGH"; + case climate::CLIMATE_FAN_MIDDLE: + return "MIDDLE"; + case climate::CLIMATE_FAN_FOCUS: + return "FOCUS"; + case climate::CLIMATE_FAN_DIFFUSE: + return "DIFFUSE"; + default: + return "UNKNOWN"; + } +} + +const char *climate_swing_mode_to_string(ClimateSwingMode swing_mode) { + switch (swing_mode) { + case climate::CLIMATE_SWING_OFF: + return "OFF"; + case climate::CLIMATE_SWING_BOTH: + return "BOTH"; + case climate::CLIMATE_SWING_VERTICAL: + return "VERTICAL"; + case climate::CLIMATE_SWING_HORIZONTAL: + return "HORIZONTAL"; + default: + return "UNKNOWN"; + } +} + } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index e5786286d8..83ef715402 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -15,6 +15,10 @@ enum ClimateMode : uint8_t { CLIMATE_MODE_COOL = 2, /// The climate device is manually set to heat mode (not in auto mode!) CLIMATE_MODE_HEAT = 3, + /// The climate device is manually set to fan only mode + CLIMATE_MODE_FAN_ONLY = 4, + /// The climate device is manually set to dry mode + CLIMATE_MODE_DRY = 5, }; /// Enum for the current action of the climate device. Values match those of ClimateMode. @@ -27,9 +31,51 @@ enum ClimateAction : uint8_t { CLIMATE_ACTION_HEATING = 3, }; +/// Enum for all modes a climate fan can be in +enum ClimateFanMode : uint8_t { + /// The fan mode is set to On + CLIMATE_FAN_ON = 0, + /// The fan mode is set to Off + CLIMATE_FAN_OFF = 1, + /// The fan mode is set to Auto + CLIMATE_FAN_AUTO = 2, + /// The fan mode is set to Low + CLIMATE_FAN_LOW = 3, + /// The fan mode is set to Medium + CLIMATE_FAN_MEDIUM = 4, + /// The fan mode is set to High + CLIMATE_FAN_HIGH = 5, + /// The fan mode is set to Middle + CLIMATE_FAN_MIDDLE = 6, + /// The fan mode is set to Focus + CLIMATE_FAN_FOCUS = 7, + /// The fan mode is set to Diffuse + CLIMATE_FAN_DIFFUSE = 8, +}; + +/// Enum for all modes a climate swing can be in +enum ClimateSwingMode : uint8_t { + /// The sing mode is set to Off + CLIMATE_SWING_OFF = 0, + /// The fan mode is set to Both + CLIMATE_SWING_BOTH = 1, + /// The fan mode is set to Vertical + CLIMATE_SWING_VERTICAL = 2, + /// The fan mode is set to Horizontal + CLIMATE_SWING_HORIZONTAL = 3, +}; + /// Convert the given ClimateMode to a human-readable string. const char *climate_mode_to_string(ClimateMode mode); + +/// Convert the given ClimateAction to a human-readable string. const char *climate_action_to_string(ClimateAction action); +/// Convert the given ClimateFanMode to a human-readable string. +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); + } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index a1db2bc696..6e941bddf0 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -14,6 +14,10 @@ bool ClimateTraits::supports_mode(ClimateMode mode) const { return this->supports_cool_mode_; case CLIMATE_MODE_HEAT: return this->supports_heat_mode_; + case CLIMATE_MODE_FAN_ONLY: + return this->supports_fan_only_mode_; + case CLIMATE_MODE_DRY: + return this->supports_dry_mode_; default: return false; } @@ -29,6 +33,10 @@ void ClimateTraits::set_supports_two_point_target_temperature(bool supports_two_ void ClimateTraits::set_supports_auto_mode(bool supports_auto_mode) { supports_auto_mode_ = supports_auto_mode; } void ClimateTraits::set_supports_cool_mode(bool supports_cool_mode) { supports_cool_mode_ = supports_cool_mode; } void ClimateTraits::set_supports_heat_mode(bool supports_heat_mode) { supports_heat_mode_ = supports_heat_mode; } +void ClimateTraits::set_supports_fan_only_mode(bool supports_fan_only_mode) { + supports_fan_only_mode_ = supports_fan_only_mode; +} +void ClimateTraits::set_supports_dry_mode(bool supports_dry_mode) { supports_dry_mode_ = supports_dry_mode; } void ClimateTraits::set_supports_away(bool supports_away) { supports_away_ = supports_away; } void ClimateTraits::set_supports_action(bool supports_action) { supports_action_ = supports_action; } float ClimateTraits::get_visual_min_temperature() const { return visual_min_temperature_; } @@ -55,5 +63,91 @@ void ClimateTraits::set_visual_temperature_step(float temperature_step) { visual bool ClimateTraits::get_supports_away() const { return supports_away_; } bool ClimateTraits::get_supports_action() const { return supports_action_; } +void ClimateTraits::set_supports_fan_mode_on(bool supports_fan_mode_on) { + this->supports_fan_mode_on_ = supports_fan_mode_on; +} +void ClimateTraits::set_supports_fan_mode_off(bool supports_fan_mode_off) { + this->supports_fan_mode_off_ = supports_fan_mode_off; +} +void ClimateTraits::set_supports_fan_mode_auto(bool supports_fan_mode_auto) { + this->supports_fan_mode_auto_ = supports_fan_mode_auto; +} +void ClimateTraits::set_supports_fan_mode_low(bool supports_fan_mode_low) { + this->supports_fan_mode_low_ = supports_fan_mode_low; +} +void ClimateTraits::set_supports_fan_mode_medium(bool supports_fan_mode_medium) { + this->supports_fan_mode_medium_ = supports_fan_mode_medium; +} +void ClimateTraits::set_supports_fan_mode_high(bool supports_fan_mode_high) { + this->supports_fan_mode_high_ = supports_fan_mode_high; +} +void ClimateTraits::set_supports_fan_mode_middle(bool supports_fan_mode_middle) { + this->supports_fan_mode_middle_ = supports_fan_mode_middle; +} +void ClimateTraits::set_supports_fan_mode_focus(bool supports_fan_mode_focus) { + this->supports_fan_mode_focus_ = supports_fan_mode_focus; +} +void ClimateTraits::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { + this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; +} +bool ClimateTraits::supports_fan_mode(ClimateFanMode fan_mode) const { + switch (fan_mode) { + case climate::CLIMATE_FAN_ON: + return this->supports_fan_mode_on_; + case climate::CLIMATE_FAN_OFF: + return this->supports_fan_mode_off_; + case climate::CLIMATE_FAN_AUTO: + return this->supports_fan_mode_auto_; + case climate::CLIMATE_FAN_LOW: + return this->supports_fan_mode_low_; + case climate::CLIMATE_FAN_MEDIUM: + return this->supports_fan_mode_medium_; + case climate::CLIMATE_FAN_HIGH: + return this->supports_fan_mode_high_; + case climate::CLIMATE_FAN_MIDDLE: + return this->supports_fan_mode_middle_; + case climate::CLIMATE_FAN_FOCUS: + return this->supports_fan_mode_focus_; + case climate::CLIMATE_FAN_DIFFUSE: + return this->supports_fan_mode_diffuse_; + default: + return false; + } +} +bool ClimateTraits::get_supports_fan_modes() const { + return this->supports_fan_mode_on_ || this->supports_fan_mode_off_ || this->supports_fan_mode_auto_ || + 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_supports_swing_mode_off(bool supports_swing_mode_off) { + this->supports_swing_mode_off_ = supports_swing_mode_off; +} +void ClimateTraits::set_supports_swing_mode_both(bool supports_swing_mode_both) { + this->supports_swing_mode_both_ = supports_swing_mode_both; +} +void ClimateTraits::set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) { + this->supports_swing_mode_vertical_ = supports_swing_mode_vertical; +} +void ClimateTraits::set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) { + this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal; +} +bool ClimateTraits::supports_swing_mode(ClimateSwingMode swing_mode) const { + switch (swing_mode) { + case climate::CLIMATE_SWING_OFF: + return this->supports_swing_mode_off_; + case climate::CLIMATE_SWING_BOTH: + return this->supports_swing_mode_both_; + case climate::CLIMATE_SWING_VERTICAL: + return this->supports_swing_mode_vertical_; + case climate::CLIMATE_SWING_HORIZONTAL: + return this->supports_swing_mode_horizontal_; + default: + return false; + } +} +bool ClimateTraits::get_supports_swing_modes() const { + return this->supports_swing_mode_off_ || this->supports_swing_mode_both_ || supports_swing_mode_vertical_ || + supports_swing_mode_horizontal_; +} } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 2d6f44eea6..347a7bc1f2 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -21,10 +21,16 @@ namespace climate { * - auto mode (automatic control) * - cool mode (lowers current temperature) * - heat mode (increases current temperature) + * - dry mode (removes humidity from air) + * - fan mode (only turns on fan) * - supports away - away mode means that the climate device supports two different * target temperature settings: one target temp setting for "away" mode and one for non-away mode. * - supports action - if the climate device supports reporting the active * current action of the device with the action property. + * - supports fan modes - optionally, if it has a fan which can be configured in different ways: + * - on, off, auto, high, medium, low, middle, focus, diffuse + * - supports swing modes - optionally, if it has a swing which can be configured in different ways: + * - off, both, vertical, horizontal * * This class also contains static data for the climate device display: * - visual min/max temperature - tells the frontend what range of temperatures the climate device @@ -41,11 +47,30 @@ class ClimateTraits { void set_supports_auto_mode(bool supports_auto_mode); void set_supports_cool_mode(bool supports_cool_mode); void set_supports_heat_mode(bool supports_heat_mode); + void set_supports_fan_only_mode(bool supports_fan_only_mode); + void set_supports_dry_mode(bool supports_dry_mode); void set_supports_away(bool supports_away); bool get_supports_away() const; void set_supports_action(bool supports_action); bool get_supports_action() const; bool supports_mode(ClimateMode mode) const; + void set_supports_fan_mode_on(bool supports_fan_mode_on); + void set_supports_fan_mode_off(bool supports_fan_mode_off); + void set_supports_fan_mode_auto(bool supports_fan_mode_auto); + void set_supports_fan_mode_low(bool supports_fan_mode_low); + void set_supports_fan_mode_medium(bool supports_fan_mode_medium); + void set_supports_fan_mode_high(bool supports_fan_mode_high); + void set_supports_fan_mode_middle(bool supports_fan_mode_middle); + void set_supports_fan_mode_focus(bool supports_fan_mode_focus); + 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_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); + void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); + bool supports_swing_mode(ClimateSwingMode swing_mode) const; + bool get_supports_swing_modes() const; float get_visual_min_temperature() const; void set_visual_min_temperature(float visual_min_temperature); @@ -61,8 +86,23 @@ class ClimateTraits { bool supports_auto_mode_{false}; bool supports_cool_mode_{false}; bool supports_heat_mode_{false}; + bool supports_fan_only_mode_{false}; + bool supports_dry_mode_{false}; bool supports_away_{false}; bool supports_action_{false}; + bool supports_fan_mode_on_{false}; + bool supports_fan_mode_off_{false}; + bool supports_fan_mode_auto_{false}; + bool supports_fan_mode_low_{false}; + bool supports_fan_mode_medium_{false}; + bool supports_fan_mode_high_{false}; + bool supports_fan_mode_middle_{false}; + bool supports_fan_mode_focus_{false}; + bool supports_fan_mode_diffuse_{false}; + bool supports_swing_mode_off_{false}; + bool supports_swing_mode_both_{false}; + bool supports_swing_mode_vertical_{false}; + bool supports_swing_mode_horizontal_{false}; float visual_min_temperature_{10}; float visual_max_temperature_{30}; diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 4b9a1c0baa..8f06ff2214 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -12,11 +12,60 @@ climate::ClimateTraits ClimateIR::traits() { traits.set_supports_auto_mode(true); traits.set_supports_cool_mode(this->supports_cool_); traits.set_supports_heat_mode(this->supports_heat_); + traits.set_supports_dry_mode(this->supports_dry_); + traits.set_supports_fan_only_mode(this->supports_fan_only_); traits.set_supports_two_point_target_temperature(false); traits.set_supports_away(false); traits.set_visual_min_temperature(this->minimum_temperature_); traits.set_visual_max_temperature(this->maximum_temperature_); traits.set_visual_temperature_step(this->temperature_step_); + for (auto fan_mode : this->fan_modes_) { + switch (fan_mode) { + case climate::CLIMATE_FAN_AUTO: + traits.set_supports_fan_mode_auto(true); + break; + case climate::CLIMATE_FAN_DIFFUSE: + traits.set_supports_fan_mode_diffuse(true); + break; + case climate::CLIMATE_FAN_FOCUS: + traits.set_supports_fan_mode_focus(true); + break; + case climate::CLIMATE_FAN_HIGH: + traits.set_supports_fan_mode_high(true); + break; + case climate::CLIMATE_FAN_LOW: + traits.set_supports_fan_mode_low(true); + break; + case climate::CLIMATE_FAN_MEDIUM: + traits.set_supports_fan_mode_medium(true); + break; + case climate::CLIMATE_FAN_MIDDLE: + traits.set_supports_fan_mode_middle(true); + break; + case climate::CLIMATE_FAN_OFF: + traits.set_supports_fan_mode_off(true); + break; + case climate::CLIMATE_FAN_ON: + traits.set_supports_fan_mode_on(true); + break; + } + } + for (auto swing_mode : this->swing_modes_) { + switch (swing_mode) { + case climate::CLIMATE_SWING_OFF: + traits.set_supports_swing_mode_off(true); + break; + case climate::CLIMATE_SWING_BOTH: + traits.set_supports_swing_mode_both(true); + break; + case climate::CLIMATE_SWING_VERTICAL: + traits.set_supports_swing_mode_vertical(true); + break; + case climate::CLIMATE_SWING_HORIZONTAL: + traits.set_supports_swing_mode_horizontal(true); + break; + } + } return traits; } @@ -40,6 +89,8 @@ void ClimateIR::setup() { // initialize target temperature to some value so that it's not NAN this->target_temperature = roundf(clamp(this->current_temperature, this->minimum_temperature_, this->maximum_temperature_)); + this->fan_mode = climate::CLIMATE_FAN_AUTO; + this->swing_mode = climate::CLIMATE_SWING_OFF; } // Never send nan to HA if (isnan(this->target_temperature)) @@ -51,7 +102,10 @@ void ClimateIR::control(const climate::ClimateCall &call) { this->mode = *call.get_mode(); if (call.get_target_temperature().has_value()) this->target_temperature = *call.get_target_temperature(); - + if (call.get_fan_mode().has_value()) + this->fan_mode = *call.get_fan_mode(); + if (call.get_swing_mode().has_value()) + this->swing_mode = *call.get_swing_mode(); this->transmit_state(); this->publish_state(); } diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index b4c036f3d6..6dc5b43279 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -18,10 +18,17 @@ namespace climate_ir { */ class ClimateIR : public climate::Climate, public Component, public remote_base::RemoteReceiverListener { public: - ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f) { + ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, + bool supports_dry = false, bool supports_fan_only = false, + std::vector fan_modes = {}, + std::vector swing_modes = {}) { this->minimum_temperature_ = minimum_temperature; this->maximum_temperature_ = maximum_temperature; this->temperature_step_ = temperature_step; + this->supports_dry_ = supports_dry; + this->supports_fan_only_ = supports_fan_only; + this->fan_modes_ = fan_modes; + this->swing_modes_ = swing_modes; } void setup() override; @@ -46,6 +53,10 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: bool supports_cool_{true}; bool supports_heat_{true}; + bool supports_dry_{false}; + bool supports_fan_only_{false}; + std::vector fan_modes_ = {}; + std::vector swing_modes_ = {}; remote_transmitter::RemoteTransmitterComponent *transmitter_; sensor::Sensor *sensor_{nullptr}; diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index c08571c2e9..441f43b424 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -12,15 +12,13 @@ const uint32_t COOLIX_LED = 0xB5F5A5; const uint32_t COOLIX_SILENCE_FP = 0xB5F5B6; // On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore. -const uint32_t COOLIX_DEFAULT_STATE = 0xB2BFC8; -const uint32_t COOLIX_DEFAULT_STATE_AUTO_24_FAN = 0xB21F48; const uint8_t COOLIX_COOL = 0b0000; const uint8_t COOLIX_DRY_FAN = 0b0100; const uint8_t COOLIX_AUTO = 0b1000; const uint8_t COOLIX_HEAT = 0b1100; const uint32_t COOLIX_MODE_MASK = 0b1100; const uint32_t COOLIX_FAN_MASK = 0xF000; -const uint32_t COOLIX_FAN_DRY = 0x1000; +const uint32_t COOLIX_FAN_MODE_AUTO_DRY = 0x1000; const uint32_t COOLIX_FAN_AUTO = 0xB000; const uint32_t COOLIX_FAN_MIN = 0x9000; const uint32_t COOLIX_FAN_MED = 0x5000; @@ -28,23 +26,23 @@ const uint32_t COOLIX_FAN_MAX = 0x3000; // Temperature const uint8_t COOLIX_TEMP_RANGE = COOLIX_TEMP_MAX - COOLIX_TEMP_MIN + 1; -const uint8_t COOLIX_FAN_TEMP_CODE = 0b1110; // Part of Fan Mode. +const uint8_t COOLIX_FAN_TEMP_CODE = 0b11100000; // Part of Fan Mode. const uint32_t COOLIX_TEMP_MASK = 0b11110000; const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = { - 0b0000, // 17C - 0b0001, // 18c - 0b0011, // 19C - 0b0010, // 20C - 0b0110, // 21C - 0b0111, // 22C - 0b0101, // 23C - 0b0100, // 24C - 0b1100, // 25C - 0b1101, // 26C - 0b1001, // 27C - 0b1000, // 28C - 0b1010, // 29C - 0b1011 // 30C + 0b00000000, // 17C + 0b00010000, // 18c + 0b00110000, // 19C + 0b00100000, // 20C + 0b01100000, // 21C + 0b01110000, // 22C + 0b01010000, // 23C + 0b01000000, // 24C + 0b11000000, // 25C + 0b11010000, // 26C + 0b10010000, // 27C + 0b10000000, // 28C + 0b10100000, // 29C + 0b10110000 // 30C }; // Constants @@ -59,29 +57,60 @@ static const uint32_t FOOTER_SPACE_US = HEADER_SPACE_US; const uint16_t COOLIX_BITS = 24; void CoolixClimate::transmit_state() { - uint32_t remote_state; + uint32_t remote_state = 0xB20F00; - switch (this->mode) { - case climate::CLIMATE_MODE_COOL: - remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | COOLIX_COOL; - break; - case climate::CLIMATE_MODE_HEAT: - remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | COOLIX_HEAT; - break; - case climate::CLIMATE_MODE_AUTO: - remote_state = COOLIX_DEFAULT_STATE_AUTO_24_FAN; - break; - case climate::CLIMATE_MODE_OFF: - default: - remote_state = COOLIX_OFF; - break; + if (send_swing_cmd_) { + send_swing_cmd_ = false; + remote_state = COOLIX_SWING; + } else { + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + remote_state |= COOLIX_COOL; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state |= COOLIX_HEAT; + break; + case climate::CLIMATE_MODE_AUTO: + remote_state |= COOLIX_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + case climate::CLIMATE_MODE_DRY: + remote_state |= COOLIX_DRY_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + remote_state = COOLIX_OFF; + break; + } + if (this->mode != climate::CLIMATE_MODE_OFF) { + if (this->mode != climate::CLIMATE_MODE_FAN_ONLY) { + auto temp = (uint8_t) roundf(clamp(this->target_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX)); + remote_state |= COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN]; + } else { + remote_state |= COOLIX_FAN_TEMP_CODE; + } + if (this->mode == climate::CLIMATE_MODE_AUTO || this->mode == climate::CLIMATE_MODE_DRY) { + this->fan_mode = climate::CLIMATE_FAN_AUTO; + remote_state |= COOLIX_FAN_MODE_AUTO_DRY; + } else { + switch (this->fan_mode) { + case climate::CLIMATE_FAN_HIGH: + remote_state |= COOLIX_FAN_MAX; + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state |= COOLIX_FAN_MED; + break; + case climate::CLIMATE_FAN_LOW: + remote_state |= COOLIX_FAN_MIN; + break; + case climate::CLIMATE_FAN_AUTO: + default: + remote_state |= COOLIX_FAN_AUTO; + break; + } + } + } } - if (this->mode != climate::CLIMATE_MODE_OFF) { - auto temp = (uint8_t) roundf(clamp(this->target_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX)); - remote_state &= ~COOLIX_TEMP_MASK; // Clear the old temp. - remote_state |= COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN] << 4; - } - ESP_LOGV(TAG, "Sending coolix code: 0x%02X", remote_state); auto transmit = this->transmitter_->transmit(); @@ -161,35 +190,35 @@ bool CoolixClimate::on_receive(remote_base::RemoteReceiveData data) { if (remote_state == COOLIX_OFF) { this->mode = climate::CLIMATE_MODE_OFF; + } else if (remote_state == COOLIX_SWING) { + this->swing_mode = + this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; } else { if ((remote_state & COOLIX_MODE_MASK) == COOLIX_HEAT) this->mode = climate::CLIMATE_MODE_HEAT; else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_AUTO) this->mode = climate::CLIMATE_MODE_AUTO; else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_DRY_FAN) { - // climate::CLIMATE_MODE_DRY; - if ((remote_state & COOLIX_FAN_MASK) == COOLIX_FAN_DRY) - ESP_LOGV(TAG, "Not supported DRY mode. Reporting AUTO"); + if ((remote_state & COOLIX_FAN_MASK) == COOLIX_FAN_MODE_AUTO_DRY) + this->mode = climate::CLIMATE_MODE_DRY; else - ESP_LOGV(TAG, "Not supported FAN Auto mode. Reporting AUTO"); - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_FAN_ONLY; } else this->mode = climate::CLIMATE_MODE_COOL; // Fan Speed - // When climate::CLIMATE_MODE_DRY is implemented replace following line with this: - // if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_DRY) - if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO) - ESP_LOGV(TAG, "Not supported FAN speed AUTO"); + if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_AUTO || + this->mode == climate::CLIMATE_MODE_DRY) + this->fan_mode = climate::CLIMATE_FAN_AUTO; else if ((remote_state & COOLIX_FAN_MIN) == COOLIX_FAN_MIN) - ESP_LOGV(TAG, "Not supported FAN speed MIN"); + this->fan_mode = climate::CLIMATE_FAN_LOW; else if ((remote_state & COOLIX_FAN_MED) == COOLIX_FAN_MED) - ESP_LOGV(TAG, "Not supported FAN speed MED"); + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; else if ((remote_state & COOLIX_FAN_MAX) == COOLIX_FAN_MAX) - ESP_LOGV(TAG, "Not supported FAN speed MAX"); + this->fan_mode = climate::CLIMATE_FAN_HIGH; // Temperature - uint8_t temperature_code = (remote_state & COOLIX_TEMP_MASK) >> 4; + uint8_t temperature_code = remote_state & COOLIX_TEMP_MASK; for (uint8_t i = 0; i < COOLIX_TEMP_RANGE; i++) if (COOLIX_TEMP_MAP[i] == temperature_code) this->target_temperature = i + COOLIX_TEMP_MIN; diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h index ed03a2fd1e..caf93f7621 100644 --- a/esphome/components/coolix/coolix.h +++ b/esphome/components/coolix/coolix.h @@ -11,13 +11,28 @@ const uint8_t COOLIX_TEMP_MAX = 30; // Celsius class CoolixClimate : public climate_ir::ClimateIR { public: - CoolixClimate() : climate_ir::ClimateIR(COOLIX_TEMP_MIN, COOLIX_TEMP_MAX) {} + CoolixClimate() + : climate_ir::ClimateIR(COOLIX_TEMP_MIN, COOLIX_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} + + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override { + send_swing_cmd_ = call.get_swing_mode().has_value(); + // swing resets after unit powered off + if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) + this->swing_mode = climate::CLIMATE_SWING_OFF; + climate_ir::ClimateIR::control(call); + } protected: /// Transmit via IR the state of this climate controller. void transmit_state() override; /// Handle received IR Buffer bool on_receive(remote_base::RemoteReceiveData data) override; + + bool send_swing_cmd_{false}; }; } // namespace coolix diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 48b470cfb2..3f097d9c07 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -30,6 +30,10 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC modes.add("cool"); if (traits.supports_mode(CLIMATE_MODE_HEAT)) modes.add("heat"); + if (traits.supports_mode(CLIMATE_MODE_FAN_ONLY)) + modes.add("fan_only"); + if (traits.supports_mode(CLIMATE_MODE_DRY)) + modes.add("dry"); if (traits.get_supports_two_point_target_temperature()) { // temperature_low_command_topic @@ -155,6 +159,12 @@ bool MQTTClimateComponent::publish_state_() { case CLIMATE_MODE_HEAT: mode_s = "heat"; break; + case CLIMATE_MODE_FAN_ONLY: + mode_s = "fan_only"; + break; + case CLIMATE_MODE_DRY: + mode_s = "dry"; + break; } bool success = true; if (!this->publish(this->get_mode_state_topic(), mode_s)) diff --git a/esphome/const.py b/esphome/const.py index 829b4738ce..0655da8f5e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -153,6 +153,7 @@ CONF_EXPIRE_AFTER = 'expire_after' CONF_EXTERNAL_VCC = 'external_vcc' CONF_FALLING_EDGE = 'falling_edge' CONF_FAMILY = 'family' +CONF_FAN_MODE = 'fan_mode' CONF_FAST_CONNECT = 'fast_connect' CONF_FILE = 'file' CONF_FILTER = 'filter' @@ -422,6 +423,7 @@ CONF_STOP_ACTION = 'stop_action' CONF_SUBNET = 'subnet' CONF_SUPPORTS_COOL = 'supports_cool' CONF_SUPPORTS_HEAT = 'supports_heat' +CONF_SWING_MODE = 'swing_mode' CONF_SWITCHES = 'switches' CONF_SYNC = 'sync' CONF_TAG = 'tag' diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index a8f81c9daf..8373e0bd66 100644 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1,6 +1,19 @@ """Python 3 script to automatically generate C++ classes for ESPHome's native API. It's pretty crappy spaghetti code, but it works. + +you need to install protobuf-compiler: +running protc --version should return +libprotoc 3.6.1 + +then run this script with python3 and the files + + esphome/components/api/api_pb2_service.h + esphome/components/api/api_pb2_service.cpp + esphome/components/api/api_pb2.h + esphome/components/api/api_pb2.cpp + +will be generated, they still need to be formatted """ import re @@ -10,13 +23,17 @@ from subprocess import call # Generate with # protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto + import api_options_pb2 as pb import google.protobuf.descriptor_pb2 as descriptor -cwd = Path(__file__).parent +file_header = '// This file was automatically generated with a tool.\n' +file_header += '// See scripts/api_protobuf/api_protobuf.py\n' + +cwd = Path(__file__).resolve().parent root = cwd.parent.parent / 'esphome' / 'components' / 'api' -prot = cwd / 'api.protoc' -call(['protoc', '-o', prot, '-I', root, 'api.proto']) +prot = root / 'api.protoc' +call(['protoc', '-o', str(prot), '-I', str(root), 'api.proto']) content = prot.read_bytes() d = descriptor.FileDescriptorSet.FromString(content) @@ -617,7 +634,8 @@ def build_message_type(desc): file = d.file[0] -content = '''\ +content = file_header +content += '''\ #pragma once #include "proto.h" @@ -627,7 +645,8 @@ namespace api { ''' -cpp = '''\ +cpp = file_header +cpp += '''\ #include "api_pb2.h" #include "esphome/core/log.h" @@ -739,7 +758,8 @@ def build_service_message_type(mt): return hout, cout -hpp = '''\ +hpp = file_header +hpp += '''\ #pragma once #include "api_pb2.h" @@ -750,7 +770,8 @@ namespace api { ''' -cpp = '''\ +cpp = file_header +cpp += '''\ #include "api_pb2_service.h" #include "esphome/core/log.h"