diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4bb7d1b555..403242d236 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -301,12 +301,17 @@ message ListEntitiesFanResponse { bool supports_oscillation = 5; bool supports_speed = 6; + bool supports_direction = 7; } enum FanSpeed { FAN_SPEED_LOW = 0; FAN_SPEED_MEDIUM = 1; FAN_SPEED_HIGH = 2; } +enum FanDirection { + FAN_DIRECTION_FORWARD = 0; + FAN_DIRECTION_REVERSE = 1; +} message FanStateResponse { option (id) = 23; option (source) = SOURCE_SERVER; @@ -317,6 +322,7 @@ message FanStateResponse { bool state = 2; bool oscillating = 3; FanSpeed speed = 4; + FanDirection direction = 5; } message FanCommandRequest { option (id) = 31; @@ -331,6 +337,8 @@ message FanCommandRequest { FanSpeed speed = 5; bool has_oscillating = 6; bool oscillating = 7; + bool has_direction = 8; + FanDirection direction = 9; } // ==================== LIGHT ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index beccf91d27..1956f3119d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -248,6 +248,8 @@ bool APIConnection::send_fan_state(fan::FanState *fan) { resp.oscillating = fan->oscillating; if (traits.supports_speed()) resp.speed = static_cast(fan->speed); + if (traits.supports_direction()) + resp.direction = static_cast(fan->direction); return this->send_fan_state_response(resp); } bool APIConnection::send_fan_info(fan::FanState *fan) { @@ -259,6 +261,7 @@ bool APIConnection::send_fan_info(fan::FanState *fan) { msg.unique_id = get_default_unique_id("fan", fan); msg.supports_oscillation = traits.supports_oscillation(); msg.supports_speed = traits.supports_speed(); + msg.supports_direction = traits.supports_direction(); return this->send_list_entities_fan_response(msg); } void APIConnection::fan_command(const FanCommandRequest &msg) { @@ -273,6 +276,8 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { call.set_oscillating(msg.oscillating); if (msg.has_speed) call.set_speed(static_cast(msg.speed)); + if (msg.has_direction) + call.set_direction(static_cast(msg.direction)); call.perform(); } #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 6b98f95f53..c659561aa8 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -52,6 +52,16 @@ template<> const char *proto_enum_to_string(enums::FanSpeed val return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::FanDirection value) { + switch (value) { + case enums::FAN_DIRECTION_FORWARD: + return "FAN_DIRECTION_FORWARD"; + case enums::FAN_DIRECTION_REVERSE: + return "FAN_DIRECTION_REVERSE"; + default: + return "UNKNOWN"; + } +} template<> const char *proto_enum_to_string(enums::LogLevel value) { switch (value) { case enums::LOG_LEVEL_NONE: @@ -760,6 +770,10 @@ bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value this->supports_speed = value.as_bool(); return true; } + case 7: { + this->supports_direction = value.as_bool(); + return true; + } default: return false; } @@ -799,6 +813,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(4, this->unique_id); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); + buffer.encode_bool(7, this->supports_direction); } void ListEntitiesFanResponse::dump_to(std::string &out) const { char buffer[64]; @@ -827,6 +842,10 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append(" supports_speed: "); out.append(YESNO(this->supports_speed)); out.append("\n"); + + out.append(" supports_direction: "); + out.append(YESNO(this->supports_direction)); + out.append("\n"); out.append("}"); } bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -843,6 +862,10 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->speed = value.as_enum(); return true; } + case 5: { + this->direction = value.as_enum(); + return true; + } default: return false; } @@ -862,6 +885,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->oscillating); buffer.encode_enum(4, this->speed); + buffer.encode_enum(5, this->direction); } void FanStateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -882,6 +906,10 @@ void FanStateResponse::dump_to(std::string &out) const { out.append(" speed: "); out.append(proto_enum_to_string(this->speed)); out.append("\n"); + + out.append(" direction: "); + out.append(proto_enum_to_string(this->direction)); + out.append("\n"); out.append("}"); } bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -910,6 +938,14 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->oscillating = value.as_bool(); return true; } + case 8: { + this->has_direction = value.as_bool(); + return true; + } + case 9: { + this->direction = value.as_enum(); + return true; + } default: return false; } @@ -932,6 +968,8 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(5, this->speed); buffer.encode_bool(6, this->has_oscillating); buffer.encode_bool(7, this->oscillating); + buffer.encode_bool(8, this->has_direction); + buffer.encode_enum(9, this->direction); } void FanCommandRequest::dump_to(std::string &out) const { char buffer[64]; @@ -964,6 +1002,14 @@ void FanCommandRequest::dump_to(std::string &out) const { out.append(" oscillating: "); out.append(YESNO(this->oscillating)); out.append("\n"); + + out.append(" has_direction: "); + out.append(YESNO(this->has_direction)); + out.append("\n"); + + out.append(" direction: "); + out.append(proto_enum_to_string(this->direction)); + out.append("\n"); out.append("}"); } bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 8be89f0365..306bdbf5a9 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -28,6 +28,10 @@ enum FanSpeed : uint32_t { FAN_SPEED_MEDIUM = 1, FAN_SPEED_HIGH = 2, }; +enum FanDirection : uint32_t { + FAN_DIRECTION_FORWARD = 0, + FAN_DIRECTION_REVERSE = 1, +}; enum LogLevel : uint32_t { LOG_LEVEL_NONE = 0, LOG_LEVEL_ERROR = 1, @@ -279,6 +283,7 @@ class ListEntitiesFanResponse : public ProtoMessage { std::string unique_id{}; // NOLINT bool supports_oscillation{false}; // NOLINT bool supports_speed{false}; // NOLINT + bool supports_direction{false}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -289,10 +294,11 @@ class ListEntitiesFanResponse : public ProtoMessage { }; class FanStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool state{false}; // NOLINT - bool oscillating{false}; // NOLINT - enums::FanSpeed speed{}; // NOLINT + uint32_t key{0}; // NOLINT + bool state{false}; // NOLINT + bool oscillating{false}; // NOLINT + enums::FanSpeed speed{}; // NOLINT + enums::FanDirection direction{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -302,13 +308,15 @@ class FanStateResponse : public ProtoMessage { }; class FanCommandRequest : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool has_state{false}; // NOLINT - bool state{false}; // NOLINT - bool has_speed{false}; // NOLINT - enums::FanSpeed speed{}; // NOLINT - bool has_oscillating{false}; // NOLINT - bool oscillating{false}; // NOLINT + uint32_t key{0}; // NOLINT + bool has_state{false}; // NOLINT + bool state{false}; // NOLINT + bool has_speed{false}; // NOLINT + enums::FanSpeed speed{}; // NOLINT + bool has_oscillating{false}; // NOLINT + bool oscillating{false}; // NOLINT + bool has_direction{false}; // NOLINT + enums::FanDirection direction{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; diff --git a/esphome/components/binary/fan/__init__.py b/esphome/components/binary/fan/__init__.py index dbfe1a8286..6969c1dbbf 100644 --- a/esphome/components/binary/fan/__init__.py +++ b/esphome/components/binary/fan/__init__.py @@ -1,7 +1,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import fan, output -from esphome.const import CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, CONF_OUTPUT_ID +from esphome.const import CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, \ + CONF_OUTPUT, CONF_OUTPUT_ID from .. import binary_ns BinaryFan = binary_ns.class_('BinaryFan', cg.Component) @@ -9,6 +10,7 @@ BinaryFan = binary_ns.class_('BinaryFan', cg.Component) CONFIG_SCHEMA = fan.FAN_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BinaryFan), cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), }).extend(cv.COMPONENT_SCHEMA) @@ -25,3 +27,7 @@ def to_code(config): if CONF_OSCILLATION_OUTPUT in config: oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) cg.add(var.set_oscillating(oscillation_output)) + + if CONF_DIRECTION_OUTPUT in config: + direction_output = yield cg.get_variable(config[CONF_DIRECTION_OUTPUT]) + cg.add(var.set_direction(direction_output)) diff --git a/esphome/components/binary/fan/binary_fan.cpp b/esphome/components/binary/fan/binary_fan.cpp index 986902efe5..5fd1867e7f 100644 --- a/esphome/components/binary/fan/binary_fan.cpp +++ b/esphome/components/binary/fan/binary_fan.cpp @@ -11,9 +11,12 @@ void binary::BinaryFan::dump_config() { if (this->fan_->get_traits().supports_oscillation()) { ESP_LOGCONFIG(TAG, " Oscillation: YES"); } + if (this->fan_->get_traits().supports_direction()) { + ESP_LOGCONFIG(TAG, " Direction: YES"); + } } void BinaryFan::setup() { - auto traits = fan::FanTraits(this->oscillating_ != nullptr, false); + auto traits = fan::FanTraits(this->oscillating_ != nullptr, false, this->direction_ != nullptr); this->fan_->set_traits(traits); this->fan_->add_on_state_callback([this]() { this->next_update_ = true; }); } @@ -41,6 +44,16 @@ void BinaryFan::loop() { } ESP_LOGD(TAG, "Setting oscillation: %s", ONOFF(enable)); } + + if (this->direction_ != nullptr) { + bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE; + if (enable) { + this->direction_->turn_on(); + } else { + this->direction_->turn_off(); + } + ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); + } } float BinaryFan::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/binary/fan/binary_fan.h b/esphome/components/binary/fan/binary_fan.h index 980d2629f6..93294b8dee 100644 --- a/esphome/components/binary/fan/binary_fan.h +++ b/esphome/components/binary/fan/binary_fan.h @@ -16,11 +16,13 @@ class BinaryFan : public Component { void dump_config() override; float get_setup_priority() const override; void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } + void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } protected: fan::FanState *fan_; output::BinaryOutput *output_; output::BinaryOutput *oscillating_{nullptr}; + output::BinaryOutput *direction_{nullptr}; bool next_update_{true}; }; diff --git a/esphome/components/fan/fan_state.cpp b/esphome/components/fan/fan_state.cpp index af170a755c..ae58b04150 100644 --- a/esphome/components/fan/fan_state.cpp +++ b/esphome/components/fan/fan_state.cpp @@ -22,6 +22,7 @@ struct FanStateRTCState { bool state; FanSpeed speed; bool oscillating; + FanDirection direction; }; void FanState::setup() { @@ -34,6 +35,7 @@ void FanState::setup() { call.set_state(recovered.state); call.set_speed(recovered.speed); call.set_oscillating(recovered.oscillating); + call.set_direction(recovered.direction); call.perform(); } float FanState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } @@ -46,6 +48,9 @@ void FanStateCall::perform() const { if (this->oscillating_.has_value()) { this->state_->oscillating = *this->oscillating_; } + if (this->direction_.has_value()) { + this->state_->direction = *this->direction_; + } if (this->speed_.has_value()) { switch (*this->speed_) { case FAN_SPEED_LOW: @@ -63,6 +68,7 @@ void FanStateCall::perform() const { saved.state = this->state_->state; saved.speed = this->state_->speed; saved.oscillating = this->state_->oscillating; + saved.direction = this->state_->direction; this->state_->rtc_.save(&saved); this->state_->state_callback_.call(); diff --git a/esphome/components/fan/fan_state.h b/esphome/components/fan/fan_state.h index 4e937c68bd..7ab8337e94 100644 --- a/esphome/components/fan/fan_state.h +++ b/esphome/components/fan/fan_state.h @@ -15,6 +15,9 @@ enum FanSpeed { FAN_SPEED_HIGH = 2 ///< The fan is running on high/full speed. }; +/// Simple enum to represent the direction of a fan +enum FanDirection { FAN_DIRECTION_FORWARD = 0, FAN_DIRECTION_REVERSE = 1 }; + class FanState; class FanStateCall { @@ -46,6 +49,14 @@ class FanStateCall { return *this; } FanStateCall &set_speed(const char *speed); + FanStateCall &set_direction(FanDirection direction) { + this->direction_ = direction; + return *this; + } + FanStateCall &set_direction(optional direction) { + this->direction_ = direction; + return *this; + } void perform() const; @@ -54,6 +65,7 @@ class FanStateCall { optional binary_state_; optional oscillating_{}; optional speed_{}; + optional direction_{}; }; class FanState : public Nameable, public Component { @@ -76,6 +88,8 @@ class FanState : public Nameable, public Component { bool oscillating{false}; /// The current fan speed. FanSpeed speed{FAN_SPEED_HIGH}; + /// The current direction of the fan + FanDirection direction{FAN_DIRECTION_FORWARD}; FanStateCall turn_on(); FanStateCall turn_off(); diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index c46adbf013..75663484c5 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -6,7 +6,8 @@ namespace fan { class FanTraits { public: FanTraits() = default; - FanTraits(bool oscillation, bool speed) : oscillation_(oscillation), speed_(speed) {} + FanTraits(bool oscillation, bool speed, bool direction) + : oscillation_(oscillation), speed_(speed), direction_(direction) {} /// Return if this fan supports oscillation. bool supports_oscillation() const { return this->oscillation_; } @@ -16,10 +17,15 @@ class FanTraits { bool supports_speed() const { return this->speed_; } /// Set whether this fan supports speed modes. void set_speed(bool speed) { this->speed_ = speed; } + /// Return if this fan supports changing direction + bool supports_direction() const { return this->direction_; } + /// Set whether this fan supports changing direction + void set_direction(bool direction) { this->direction_ = direction; } protected: bool oscillation_{false}; bool speed_{false}; + bool direction_{false}; }; } // namespace fan diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index 65ee5960f0..420c957d87 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import fan, output -from esphome.const import CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, \ +from esphome.const import CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, CONF_DIRECTION_OUTPUT, \ CONF_OUTPUT_ID, CONF_SPEED, CONF_LOW, CONF_MEDIUM, CONF_HIGH from .. import speed_ns @@ -11,6 +11,7 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SpeedFan), cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput), cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), cv.Optional(CONF_SPEED, default={}): cv.Schema({ cv.Optional(CONF_LOW, default=0.33): cv.percentage, cv.Optional(CONF_MEDIUM, default=0.66): cv.percentage, @@ -30,3 +31,7 @@ def to_code(config): if CONF_OSCILLATION_OUTPUT in config: oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) cg.add(var.set_oscillating(oscillation_output)) + + if CONF_DIRECTION_OUTPUT in config: + direction_output = yield cg.get_variable(config[CONF_DIRECTION_OUTPUT]) + cg.add(var.set_direction(direction_output)) diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 3bfbc1fc3c..45117d64c3 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -11,9 +11,12 @@ void SpeedFan::dump_config() { if (this->fan_->get_traits().supports_oscillation()) { ESP_LOGCONFIG(TAG, " Oscillation: YES"); } + if (this->fan_->get_traits().supports_direction()) { + ESP_LOGCONFIG(TAG, " Direction: YES"); + } } void SpeedFan::setup() { - auto traits = fan::FanTraits(this->oscillating_ != nullptr, true); + auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr); this->fan_->set_traits(traits); this->fan_->add_on_state_callback([this]() { this->next_update_ = true; }); } @@ -46,6 +49,16 @@ void SpeedFan::loop() { } ESP_LOGD(TAG, "Setting oscillation: %s", ONOFF(enable)); } + + if (this->direction_ != nullptr) { + bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE; + if (enable) { + this->direction_->turn_on(); + } else { + this->direction_->turn_off(); + } + ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); + } } float SpeedFan::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 74910bda94..cce9d07544 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -16,6 +16,7 @@ class SpeedFan : public Component { void dump_config() override; float get_setup_priority() const override; void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } + void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } void set_speeds(float low, float medium, float high) { this->low_speed_ = low; this->medium_speed_ = medium; @@ -26,6 +27,7 @@ class SpeedFan : public Component { fan::FanState *fan_; output::FloatOutput *output_; output::BinaryOutput *oscillating_{nullptr}; + output::BinaryOutput *direction_{nullptr}; float low_speed_{}; float medium_speed_{}; float high_speed_{}; diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index b9fe2c0829..9850fa65ed 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -7,7 +7,7 @@ namespace tuya { static const char *TAG = "tuya.fan"; void TuyaFan::setup() { - auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value()); + auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value(), false); this->fan_->set_traits(traits); if (this->speed_id_.has_value()) { diff --git a/esphome/const.py b/esphome/const.py index 5ac2b3c9ca..4226e25afa 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -130,6 +130,7 @@ CONF_DIMENSIONS = 'dimensions' CONF_DIO_PIN = 'dio_pin' CONF_DIR_PIN = 'dir_pin' CONF_DIRECTION = 'direction' +CONF_DIRECTION_OUTPUT = 'direction_output' CONF_DISCOVERY = 'discovery' CONF_DISCOVERY_PREFIX = 'discovery_prefix' CONF_DISCOVERY_RETAIN = 'discovery_retain' diff --git a/tests/test1.yaml b/tests/test1.yaml index a6c0152928..bd44ff6e63 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1467,9 +1467,13 @@ fan: - platform: binary output: gpio_26 name: "Living Room Fan 1" + oscillation_output: gpio_19 + direction_output: gpio_26 - platform: speed output: pca_6 name: "Living Room Fan 2" + oscillation_output: gpio_19 + direction_output: gpio_26 speed: low: 0.45 medium: 0.75