Support fan speed levels (#1541)

* Add fan speed percentage support to the API

* Add float fan speed percentage

* Add percentage support to automation and configuration

* Update Tuya fan

* Fix pylint warning

* Update API to use speed levels instead of percentage

* Use speed levels

* Fix type warnings

* MQTT component now converts between speed levels and enums

* Webserver now supports speed_level

* Update prometheus

* Remove low/medium/high settings from speed fan

* Remove unused enum

* Configurable speed levels for speed fan

* Remove unused import

* Rename speed_level->speed and speed_levels->speed_count

* Rename supported_speed_levels -> supported_speed_count in API and FanTraits

Field id stays the same in the protocol, so the change is not breaking for aioesphome.
This commit is contained in:
Jim Ekman 2021-03-17 14:40:02 +01:00 committed by GitHub
parent 08998caabc
commit 7708b81ef5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 182 additions and 99 deletions

View file

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

View file

@ -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<enums::FanSpeed>(fan->speed);
if (traits.supports_speed()) {
resp.speed_level = fan->speed;
resp.speed = static_cast<enums::FanSpeed>(fan::speed_level_to_enum(fan->speed, traits.supported_speed_count()));
}
if (traits.supports_direction())
resp.direction = static_cast<enums::FanDirection>(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<fan::FanSpeed>(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<fan::FanSpeed>(msg.speed), traits.supported_speed_count()));
}
if (msg.has_direction)
call.set_direction(static_cast<fan::FanDirection>(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;

View file

@ -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<enums::FanDirection>();
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<enums::FanSpeed>(4, this->speed);
buffer.encode_enum<enums::FanDirection>(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<enums::FanDirection>(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<enums::FanDirection>();
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<enums::FanDirection>(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<enums::FanDirection>(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) {

View file

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

View file

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

View file

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

View file

@ -12,7 +12,7 @@ template<typename... Ts> class TurnOnAction : public Action<Ts...> {
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();

View file

@ -0,0 +1,20 @@
#include <cassert>
#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<float>(speed_level) / (supported_speed_levels + 1);
const auto legacy_level = static_cast<int>(clamp(ceilf(speed_ratio * 3), 1, 3));
return static_cast<FanSpeed>(legacy_level - 1);
}
int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) {
const auto enum_level = static_cast<int>(speed) + 1;
const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels);
return static_cast<int>(speed_level);
}
} // namespace fan
} // namespace esphome

View file

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

View file

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

View file

@ -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<FanSpeed> 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<bool> binary_state_;
optional<bool> oscillating_{};
optional<FanSpeed> speed_{};
optional<bool> oscillating_;
optional<int> speed_;
optional<FanDirection> 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};

View file

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

View file

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

View file

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

View file

@ -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<float>(this->fan_->speed) / static_cast<float>(this->speed_count_);
}
ESP_LOGD(TAG, "Setting speed: %.2f", speed);
this->output_->set_level(speed);

View file

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

View file

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

View file

@ -12,6 +12,10 @@
#include <esphome/components/logger/logger.h>
#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());

View file

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

View file

@ -248,6 +248,13 @@ optional<float> parse_float(const std::string &str) {
return {};
return value;
}
optional<int> 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) {

View file

@ -42,6 +42,7 @@ std::string to_string(float val);
std::string to_string(double val);
std::string to_string(long double val);
optional<float> parse_float(const std::string &str);
optional<int> 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);

View file

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