Add support for controlling fan direction (#1051)

* Fix fan oscillation trait not being used

* Add fan direction support to SpeedFan

* Add fan direction to API

* Add fan direction support to BinaryFan

* Fix CI errors

* Fix python format

* Change some ordering to trigger CI

* Add test for the configuration
This commit is contained in:
Jim Persson 2020-06-14 21:54:31 +02:00 committed by GitHub
parent 35a2258f12
commit 0bb81e5b2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 156 additions and 17 deletions

View File

@ -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 ====================

View File

@ -248,6 +248,8 @@ bool APIConnection::send_fan_state(fan::FanState *fan) {
resp.oscillating = fan->oscillating;
if (traits.supports_speed())
resp.speed = static_cast<enums::FanSpeed>(fan->speed);
if (traits.supports_direction())
resp.direction = static_cast<enums::FanDirection>(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<fan::FanSpeed>(msg.speed));
if (msg.has_direction)
call.set_direction(static_cast<fan::FanDirection>(msg.direction));
call.perform();
}
#endif

View File

@ -52,6 +52,16 @@ template<> const char *proto_enum_to_string<enums::FanSpeed>(enums::FanSpeed val
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::FanDirection>(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>(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<enums::FanSpeed>();
return true;
}
case 5: {
this->direction = value.as_enum<enums::FanDirection>();
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<enums::FanSpeed>(4, this->speed);
buffer.encode_enum<enums::FanDirection>(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<enums::FanSpeed>(this->speed));
out.append("\n");
out.append(" direction: ");
out.append(proto_enum_to_string<enums::FanDirection>(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<enums::FanDirection>();
return true;
}
default:
return false;
}
@ -932,6 +968,8 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_enum<enums::FanSpeed>(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<enums::FanDirection>(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<enums::FanDirection>(this->direction));
out.append("\n");
out.append("}");
}
bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {

View File

@ -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;

View File

@ -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))

View File

@ -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; }

View File

@ -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};
};

View File

@ -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();

View File

@ -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<FanDirection> direction) {
this->direction_ = direction;
return *this;
}
void perform() const;
@ -54,6 +65,7 @@ class FanStateCall {
optional<bool> binary_state_;
optional<bool> oscillating_{};
optional<FanSpeed> speed_{};
optional<FanDirection> 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();

View File

@ -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

View File

@ -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))

View File

@ -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; }

View File

@ -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_{};

View File

@ -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()) {

View File

@ -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'

View File

@ -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