diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index aaa9477985..ede2cc6205 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -303,6 +303,7 @@ message ListEntitiesFanResponse { bool supports_oscillation = 5; bool supports_speed = 6; bool supports_direction = 7; + int32 supported_speed_count = 8; } enum FanSpeed { FAN_SPEED_LOW = 0; @@ -322,8 +323,9 @@ message FanStateResponse { fixed32 key = 1; bool state = 2; bool oscillating = 3; - FanSpeed speed = 4; + FanSpeed speed = 4 [deprecated = true]; FanDirection direction = 5; + int32 speed_level = 6; } message FanCommandRequest { option (id) = 31; @@ -334,12 +336,14 @@ message FanCommandRequest { fixed32 key = 1; bool has_state = 2; bool state = 3; - bool has_speed = 4; - FanSpeed speed = 5; + bool has_speed = 4 [deprecated = true]; + FanSpeed speed = 5 [deprecated = true]; bool has_oscillating = 6; bool oscillating = 7; bool has_direction = 8; FanDirection direction = 9; + bool has_speed_level = 10; + int32 speed_level = 11; } // ==================== LIGHT ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ecbe5b79c6..8098c93781 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -9,6 +9,9 @@ #ifdef USE_HOMEASSISTANT_TIME #include "esphome/components/homeassistant/time/homeassistant_time.h" #endif +#ifdef USE_FAN +#include "esphome/components/fan/fan_helpers.h" +#endif namespace esphome { namespace api { @@ -246,8 +249,10 @@ bool APIConnection::send_fan_state(fan::FanState *fan) { resp.state = fan->state; if (traits.supports_oscillation()) resp.oscillating = fan->oscillating; - if (traits.supports_speed()) - resp.speed = static_cast(fan->speed); + if (traits.supports_speed()) { + resp.speed_level = fan->speed; + resp.speed = static_cast(fan::speed_level_to_enum(fan->speed, traits.supported_speed_count())); + } if (traits.supports_direction()) resp.direction = static_cast(fan->direction); return this->send_fan_state_response(resp); @@ -262,6 +267,7 @@ bool APIConnection::send_fan_info(fan::FanState *fan) { msg.supports_oscillation = traits.supports_oscillation(); msg.supports_speed = traits.supports_speed(); msg.supports_direction = traits.supports_direction(); + msg.supported_speed_count = traits.supported_speed_count(); return this->send_list_entities_fan_response(msg); } void APIConnection::fan_command(const FanCommandRequest &msg) { @@ -269,13 +275,19 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { if (fan == nullptr) return; + auto traits = fan->get_traits(); + auto call = fan->make_call(); if (msg.has_state) call.set_state(msg.state); if (msg.has_oscillating) call.set_oscillating(msg.oscillating); - if (msg.has_speed) - call.set_speed(static_cast(msg.speed)); + if (msg.has_speed_level) { + // Prefer level + call.set_speed(msg.speed_level); + } else if (msg.has_speed) { + call.set_speed(fan::speed_enum_to_level(static_cast(msg.speed), traits.supported_speed_count())); + } if (msg.has_direction) call.set_direction(static_cast(msg.direction)); call.perform(); @@ -590,7 +602,7 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 3; + resp.api_version_minor = 4; resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; this->connection_state_ = ConnectionState::CONNECTED; return resp; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 7a6b55bf91..23538b77bf 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -774,6 +774,10 @@ bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value this->supports_direction = value.as_bool(); return true; } + case 8: { + this->supported_speed_count = value.as_int32(); + return true; + } default: return false; } @@ -814,6 +818,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); buffer.encode_bool(7, this->supports_direction); + buffer.encode_int32(8, this->supported_speed_count); } void ListEntitiesFanResponse::dump_to(std::string &out) const { char buffer[64]; @@ -846,6 +851,11 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append(" supports_direction: "); out.append(YESNO(this->supports_direction)); out.append("\n"); + + out.append(" supported_speed_count: "); + sprintf(buffer, "%d", this->supported_speed_count); + out.append(buffer); + out.append("\n"); out.append("}"); } bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -866,6 +876,10 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->direction = value.as_enum(); return true; } + case 6: { + this->speed_level = value.as_int32(); + return true; + } default: return false; } @@ -886,6 +900,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(3, this->oscillating); buffer.encode_enum(4, this->speed); buffer.encode_enum(5, this->direction); + buffer.encode_int32(6, this->speed_level); } void FanStateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -910,6 +925,11 @@ void FanStateResponse::dump_to(std::string &out) const { out.append(" direction: "); out.append(proto_enum_to_string(this->direction)); out.append("\n"); + + out.append(" speed_level: "); + sprintf(buffer, "%d", this->speed_level); + out.append(buffer); + out.append("\n"); out.append("}"); } bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -946,6 +966,14 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->direction = value.as_enum(); return true; } + case 10: { + this->has_speed_level = value.as_bool(); + return true; + } + case 11: { + this->speed_level = value.as_int32(); + return true; + } default: return false; } @@ -970,6 +998,8 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(7, this->oscillating); buffer.encode_bool(8, this->has_direction); buffer.encode_enum(9, this->direction); + buffer.encode_bool(10, this->has_speed_level); + buffer.encode_int32(11, this->speed_level); } void FanCommandRequest::dump_to(std::string &out) const { char buffer[64]; @@ -1010,6 +1040,15 @@ void FanCommandRequest::dump_to(std::string &out) const { out.append(" direction: "); out.append(proto_enum_to_string(this->direction)); out.append("\n"); + + out.append(" has_speed_level: "); + out.append(YESNO(this->has_speed_level)); + out.append("\n"); + + out.append(" speed_level: "); + sprintf(buffer, "%d", this->speed_level); + out.append(buffer); + 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 abee4a11d4..f70ac74a79 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -284,6 +284,7 @@ class ListEntitiesFanResponse : public ProtoMessage { bool supports_oscillation{false}; // NOLINT bool supports_speed{false}; // NOLINT bool supports_direction{false}; // NOLINT + int32_t supported_speed_count{0}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -299,6 +300,7 @@ class FanStateResponse : public ProtoMessage { bool oscillating{false}; // NOLINT enums::FanSpeed speed{}; // NOLINT enums::FanDirection direction{}; // NOLINT + int32_t speed_level{0}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -317,6 +319,8 @@ class FanCommandRequest : public ProtoMessage { bool oscillating{false}; // NOLINT bool has_direction{false}; // NOLINT enums::FanDirection direction{}; // NOLINT + bool has_speed_level{false}; // NOLINT + int32_t speed_level{0}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -401,9 +405,9 @@ class ListEntitiesSensorResponse : public ProtoMessage { std::string unique_id{}; // NOLINT std::string icon{}; // NOLINT std::string unit_of_measurement{}; // NOLINT - std::string device_class{}; // NOLINT int32_t accuracy_decimals{0}; // NOLINT bool force_update{false}; // NOLINT + std::string device_class{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; diff --git a/esphome/components/binary/fan/binary_fan.cpp b/esphome/components/binary/fan/binary_fan.cpp index 5fd1867e7f..fa6ddc249f 100644 --- a/esphome/components/binary/fan/binary_fan.cpp +++ b/esphome/components/binary/fan/binary_fan.cpp @@ -16,7 +16,7 @@ void binary::BinaryFan::dump_config() { } } void BinaryFan::setup() { - auto traits = fan::FanTraits(this->oscillating_ != nullptr, false, this->direction_ != nullptr); + auto traits = fan::FanTraits(this->oscillating_ != nullptr, false, this->direction_ != nullptr, 0); this->fan_->set_traits(traits); this->fan_->add_on_state_callback([this]() { this->next_update_ = true; }); } diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 79ed72f496..10e38682e7 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -28,14 +28,6 @@ TurnOnAction = fan_ns.class_("TurnOnAction", automation.Action) TurnOffAction = fan_ns.class_("TurnOffAction", automation.Action) ToggleAction = fan_ns.class_("ToggleAction", automation.Action) -FanSpeed = fan_ns.enum("FanSpeed") -FAN_SPEEDS = { - "OFF": FanSpeed.FAN_SPEED_OFF, - "LOW": FanSpeed.FAN_SPEED_LOW, - "MEDIUM": FanSpeed.FAN_SPEED_MEDIUM, - "HIGH": FanSpeed.FAN_SPEED_HIGH, -} - FAN_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(FanState), @@ -128,7 +120,7 @@ def fan_turn_off_to_code(config, action_id, template_arg, args): { cv.Required(CONF_ID): cv.use_id(FanState), cv.Optional(CONF_OSCILLATING): cv.templatable(cv.boolean), - cv.Optional(CONF_SPEED): cv.templatable(cv.enum(FAN_SPEEDS, upper=True)), + cv.Optional(CONF_SPEED): cv.templatable(cv.int_range(1)), } ), ) @@ -139,7 +131,7 @@ def fan_turn_on_to_code(config, action_id, template_arg, args): template_ = yield cg.templatable(config[CONF_OSCILLATING], args, bool) cg.add(var.set_oscillating(template_)) if CONF_SPEED in config: - template_ = yield cg.templatable(config[CONF_SPEED], args, FanSpeed) + template_ = yield cg.templatable(config[CONF_SPEED], args, int) cg.add(var.set_speed(template_)) yield var diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index d96ed994e8..25c92075ba 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -12,7 +12,7 @@ template class TurnOnAction : public Action { explicit TurnOnAction(FanState *state) : state_(state) {} TEMPLATABLE_VALUE(bool, oscillating) - TEMPLATABLE_VALUE(FanSpeed, speed) + TEMPLATABLE_VALUE(int, speed) void play(Ts... x) override { auto call = this->state_->turn_on(); diff --git a/esphome/components/fan/fan_helpers.cpp b/esphome/components/fan/fan_helpers.cpp new file mode 100644 index 0000000000..be16e6bb64 --- /dev/null +++ b/esphome/components/fan/fan_helpers.cpp @@ -0,0 +1,20 @@ +#include +#include "fan_helpers.h" + +namespace esphome { +namespace fan { + +FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) { + const auto speed_ratio = static_cast(speed_level) / (supported_speed_levels + 1); + const auto legacy_level = static_cast(clamp(ceilf(speed_ratio * 3), 1, 3)); + return static_cast(legacy_level - 1); +} + +int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) { + const auto enum_level = static_cast(speed) + 1; + const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels); + return static_cast(speed_level); +} + +} // namespace fan +} // namespace esphome diff --git a/esphome/components/fan/fan_helpers.h b/esphome/components/fan/fan_helpers.h new file mode 100644 index 0000000000..138aa5bca3 --- /dev/null +++ b/esphome/components/fan/fan_helpers.h @@ -0,0 +1,11 @@ +#pragma once +#include "fan_state.h" + +namespace esphome { +namespace fan { + +FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels); +int speed_enum_to_level(FanSpeed speed, int supported_speed_levels); + +} // namespace fan +} // namespace esphome diff --git a/esphome/components/fan/fan_state.cpp b/esphome/components/fan/fan_state.cpp index ae58b04150..5a3a7ecebc 100644 --- a/esphome/components/fan/fan_state.cpp +++ b/esphome/components/fan/fan_state.cpp @@ -1,4 +1,5 @@ #include "fan_state.h" +#include "fan_helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -20,7 +21,7 @@ FanStateCall FanState::make_call() { return FanStateCall(this); } struct FanStateRTCState { bool state; - FanSpeed speed; + int speed; bool oscillating; FanDirection direction; }; @@ -52,16 +53,8 @@ void FanStateCall::perform() const { this->state_->direction = *this->direction_; } if (this->speed_.has_value()) { - switch (*this->speed_) { - case FAN_SPEED_LOW: - case FAN_SPEED_MEDIUM: - case FAN_SPEED_HIGH: - this->state_->speed = *this->speed_; - break; - default: - // protect from invalid input - break; - } + const int speed_count = this->state_->get_traits().supported_speed_count(); + this->state_->speed = static_cast(clamp(*this->speed_, 1, speed_count)); } FanStateRTCState saved{}; @@ -73,13 +66,15 @@ void FanStateCall::perform() const { this->state_->state_callback_.call(); } -FanStateCall &FanStateCall::set_speed(const char *speed) { - if (strcasecmp(speed, "low") == 0) { - this->set_speed(FAN_SPEED_LOW); - } else if (strcasecmp(speed, "medium") == 0) { - this->set_speed(FAN_SPEED_MEDIUM); - } else if (strcasecmp(speed, "high") == 0) { - this->set_speed(FAN_SPEED_HIGH); + +FanStateCall &FanStateCall::set_speed(const char *legacy_speed) { + const auto supported_speed_count = this->state_->get_traits().supported_speed_count(); + if (strcasecmp(legacy_speed, "low") == 0) { + this->set_speed(fan::speed_enum_to_level(FAN_SPEED_LOW, supported_speed_count)); + } else if (strcasecmp(legacy_speed, "medium") == 0) { + this->set_speed(fan::speed_enum_to_level(FAN_SPEED_MEDIUM, supported_speed_count)); + } else if (strcasecmp(legacy_speed, "high") == 0) { + this->set_speed(fan::speed_enum_to_level(FAN_SPEED_HIGH, supported_speed_count)); } return *this; } diff --git a/esphome/components/fan/fan_state.h b/esphome/components/fan/fan_state.h index 7ab8337e94..a0dda4083a 100644 --- a/esphome/components/fan/fan_state.h +++ b/esphome/components/fan/fan_state.h @@ -3,12 +3,13 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#include "esphome/core/log.h" #include "fan_traits.h" namespace esphome { namespace fan { -/// Simple enum to represent the speed of a fan. +/// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon enum FanSpeed { FAN_SPEED_LOW = 0, ///< The fan is running on low speed. FAN_SPEED_MEDIUM = 1, ///< The fan is running on medium speed. @@ -40,15 +41,11 @@ class FanStateCall { this->oscillating_ = oscillating; return *this; } - FanStateCall &set_speed(FanSpeed speed) { + FanStateCall &set_speed(int speed) { this->speed_ = speed; return *this; } - FanStateCall &set_speed(optional speed) { - this->speed_ = speed; - return *this; - } - FanStateCall &set_speed(const char *speed); + FanStateCall &set_speed(const char *legacy_speed); FanStateCall &set_direction(FanDirection direction) { this->direction_ = direction; return *this; @@ -63,8 +60,8 @@ class FanStateCall { protected: FanState *const state_; optional binary_state_; - optional oscillating_{}; - optional speed_{}; + optional oscillating_; + optional speed_; optional direction_{}; }; @@ -86,8 +83,8 @@ class FanState : public Nameable, public Component { bool state{false}; /// The current oscillation state of the fan. bool oscillating{false}; - /// The current fan speed. - FanSpeed speed{FAN_SPEED_HIGH}; + /// The current fan speed level + int speed{}; /// The current direction of the fan FanDirection direction{FAN_DIRECTION_FORWARD}; diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 75663484c5..e69d8e2e53 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -6,8 +6,8 @@ namespace fan { class FanTraits { public: FanTraits() = default; - FanTraits(bool oscillation, bool speed, bool direction) - : oscillation_(oscillation), speed_(speed), direction_(direction) {} + FanTraits(bool oscillation, bool speed, bool direction, int speed_count) + : oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {} /// Return if this fan supports oscillation. bool supports_oscillation() const { return this->oscillation_; } @@ -15,8 +15,12 @@ class FanTraits { void set_oscillation(bool oscillation) { this->oscillation_ = oscillation; } /// Return if this fan supports speed modes. bool supports_speed() const { return this->speed_; } - /// Set whether this fan supports speed modes. + /// Set whether this fan supports speed levels. void set_speed(bool speed) { this->speed_ = speed; } + /// Return how many speed levels the fan has + int supported_speed_count() const { return this->speed_count_; } + /// Set how many speed levels this fan has. + void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; } /// Return if this fan supports changing direction bool supports_direction() const { return this->direction_; } /// Set whether this fan supports changing direction @@ -26,6 +30,7 @@ class FanTraits { bool oscillation_{false}; bool speed_{false}; bool direction_{false}; + int speed_count_{}; }; } // namespace fan diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index f115fe1bac..c020d73105 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #ifdef USE_FAN +#include "esphome/components/fan/fan_helpers.h" namespace esphome { namespace mqtt { @@ -94,9 +95,10 @@ bool MQTTFanComponent::publish_state() { this->state_->oscillating ? "oscillate_on" : "oscillate_off"); failed = failed || !success; } - if (this->state_->get_traits().supports_speed()) { + auto traits = this->state_->get_traits(); + if (traits.supports_speed()) { const char *payload; - switch (this->state_->speed) { + switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) { case FAN_SPEED_LOW: { payload = "low"; break; diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index a8f306d713..fdb0a9af09 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -7,9 +7,7 @@ from esphome.const import ( CONF_DIRECTION_OUTPUT, CONF_OUTPUT_ID, CONF_SPEED, - CONF_LOW, - CONF_MEDIUM, - CONF_HIGH, + CONF_SPEED_COUNT, ) from .. import speed_ns @@ -21,13 +19,10 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( 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, - cv.Optional(CONF_HIGH, default=1.0): cv.percentage, - } + cv.Optional(CONF_SPEED): cv.invalid( + "Configuring individual speeds is deprecated." ), + cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), } ).extend(cv.COMPONENT_SCHEMA) @@ -35,10 +30,10 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( def to_code(config): output_ = yield cg.get_variable(config[CONF_OUTPUT]) state = yield fan.create_fan_state(config) - var = cg.new_Pvariable(config[CONF_OUTPUT_ID], state, output_) + var = cg.new_Pvariable( + config[CONF_OUTPUT_ID], state, output_, config[CONF_SPEED_COUNT] + ) yield cg.register_component(var, config) - speeds = config[CONF_SPEED] - cg.add(var.set_speeds(speeds[CONF_LOW], speeds[CONF_MEDIUM], speeds[CONF_HIGH])) if CONF_OSCILLATION_OUTPUT in config: oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 45117d64c3..0455fa200d 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -1,4 +1,5 @@ #include "speed_fan.h" +#include "esphome/components/fan/fan_helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -16,7 +17,7 @@ void SpeedFan::dump_config() { } } void SpeedFan::setup() { - auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr); + auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); this->fan_->set_traits(traits); this->fan_->add_on_state_callback([this]() { this->next_update_ = true; }); } @@ -29,12 +30,7 @@ void SpeedFan::loop() { { float speed = 0.0f; if (this->fan_->state) { - if (this->fan_->speed == fan::FAN_SPEED_LOW) - speed = this->low_speed_; - else if (this->fan_->speed == fan::FAN_SPEED_MEDIUM) - speed = this->medium_speed_; - else if (this->fan_->speed == fan::FAN_SPEED_HIGH) - speed = this->high_speed_; + speed = static_cast(this->fan_->speed) / static_cast(this->speed_count_); } ESP_LOGD(TAG, "Setting speed: %.2f", speed); this->output_->set_level(speed); diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index cce9d07544..6b7fa0b0f2 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -10,28 +10,22 @@ namespace speed { class SpeedFan : public Component { public: - SpeedFan(fan::FanState *fan, output::FloatOutput *output) : fan_(fan), output_(output) {} + SpeedFan(fan::FanState *fan, output::FloatOutput *output, int speed_count) + : fan_(fan), output_(output), speed_count_(speed_count) {} void setup() override; void loop() override; 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; - this->high_speed_ = high; - } protected: fan::FanState *fan_; output::FloatOutput *output_; output::BinaryOutput *oscillating_{nullptr}; output::BinaryOutput *direction_{nullptr}; - float low_speed_{}; - float medium_speed_{}; - float high_speed_{}; bool next_update_{true}; + int speed_count_{}; }; } // namespace speed diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 9850fa65ed..718f292f5b 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -1,4 +1,5 @@ #include "esphome/core/log.h" +#include "esphome/components/fan/fan_helpers.h" #include "tuya_fan.h" namespace esphome { @@ -7,18 +8,18 @@ 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(), false); + auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value(), false, 3); this->fan_->set_traits(traits); if (this->speed_id_.has_value()) { this->parent_->register_listener(*this->speed_id_, [this](TuyaDatapoint datapoint) { auto call = this->fan_->make_call(); if (datapoint.value_enum == 0x0) - call.set_speed(fan::FAN_SPEED_LOW); + call.set_speed(1); else if (datapoint.value_enum == 0x1) - call.set_speed(fan::FAN_SPEED_MEDIUM); + call.set_speed(2); else if (datapoint.value_enum == 0x2) - call.set_speed(fan::FAN_SPEED_HIGH); + call.set_speed(3); else ESP_LOGCONFIG(TAG, "Speed has invalid value %d", datapoint.value_enum); ESP_LOGD(TAG, "MCU reported speed of: %d", datapoint.value_enum); @@ -75,12 +76,7 @@ void TuyaFan::write_state() { TuyaDatapoint datapoint{}; datapoint.id = *this->speed_id_; datapoint.type = TuyaDatapointType::ENUM; - if (this->fan_->speed == fan::FAN_SPEED_LOW) - datapoint.value_enum = 0; - if (this->fan_->speed == fan::FAN_SPEED_MEDIUM) - datapoint.value_enum = 1; - if (this->fan_->speed == fan::FAN_SPEED_HIGH) - datapoint.value_enum = 2; + datapoint.value_enum = this->fan_->speed - 1; ESP_LOGD(TAG, "Setting speed: %d", datapoint.value_enum); this->parent_->set_datapoint_value(datapoint); } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 9e0e881b1b..fbb215ee17 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -12,6 +12,10 @@ #include #endif +#ifdef USE_FAN +#include "esphome/components/fan/fan_helpers.h" +#endif + namespace esphome { namespace web_server { @@ -364,8 +368,10 @@ std::string WebServer::fan_json(fan::FanState *obj) { root["id"] = "fan-" + obj->get_object_id(); root["state"] = obj->state ? "ON" : "OFF"; root["value"] = obj->state; - if (obj->get_traits().supports_speed()) { - switch (obj->speed) { + const auto traits = obj->get_traits(); + if (traits.supports_speed()) { + root["speed_level"] = obj->speed; + switch (fan::speed_level_to_enum(obj->speed, traits.supported_speed_count())) { case fan::FAN_SPEED_LOW: root["speed"] = "low"; break; @@ -400,6 +406,15 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, UrlMatch matc String speed = request->getParam("speed")->value(); call.set_speed(speed.c_str()); } + if (request->hasParam("speed_level")) { + String speed_level = request->getParam("speed_level")->value(); + auto val = parse_int(speed_level.c_str()); + if (!val.has_value()) { + ESP_LOGW(TAG, "Can't convert '%s' to number!", speed_level.c_str()); + return; + } + call.set_speed(*val); + } if (request->hasParam("oscillation")) { String speed = request->getParam("oscillation")->value(); auto val = parse_on_off(speed.c_str()); diff --git a/esphome/const.py b/esphome/const.py index 5d735698bc..232449e647 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -483,6 +483,7 @@ CONF_SLEEP_WHEN_DONE = "sleep_when_done" CONF_SONY = "sony" CONF_SPEED = "speed" CONF_SPEED_COMMAND_TOPIC = "speed_command_topic" +CONF_SPEED_COUNT = "speed_count" CONF_SPEED_STATE_TOPIC = "speed_state_topic" CONF_SPI_ID = "spi_id" CONF_SPIKE_REJECTION = "spike_rejection" diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 3e7472d2fe..db26e62781 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -248,6 +248,13 @@ optional parse_float(const std::string &str) { return {}; return value; } +optional parse_int(const std::string &str) { + char *end; + int value = ::strtol(str.c_str(), &end, 10); + if (end == nullptr || end != str.end().base()) + return {}; + return value; +} uint32_t fnv1_hash(const std::string &str) { uint32_t hash = 2166136261UL; for (char c : str) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 40e53e601e..63706e8a19 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -42,6 +42,7 @@ std::string to_string(float val); std::string to_string(double val); std::string to_string(long double val); optional parse_float(const std::string &str); +optional parse_int(const std::string &str); /// Sanitize the hostname by removing characters that are not in the allowlist and truncating it to 63 chars. std::string sanitize_hostname(const std::string &hostname); diff --git a/tests/test1.yaml b/tests/test1.yaml index f5c3ab9b57..316f31e68a 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1708,13 +1708,10 @@ fan: direction_output: gpio_26 - platform: speed output: pca_6 + speed_count: 10 name: 'Living Room Fan 2' oscillation_output: gpio_19 direction_output: gpio_26 - speed: - low: 0.45 - medium: 0.75 - high: 1.0 oscillation_state_topic: oscillation/state/topic oscillation_command_topic: oscillation/command/topic speed_state_topic: speed/state/topic