diff --git a/CODEOWNERS b/CODEOWNERS index eebaf02671..53669b8b99 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -52,6 +52,8 @@ esphome/components/globals/* @esphome/core esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle esphome/components/havells_solar/* @sourabhjaiswal +esphome/components/hbridge/fan/* @WeekendWarrior +esphome/components/hbridge/light/* @DotNetDann esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/homeassistant/* @OttoWinter esphome/components/hrxl_maxsonar_wr/* @netmikey diff --git a/esphome/components/hbridge/__init__.py b/esphome/components/hbridge/__init__.py index e69de29bb2..7eae863ff5 100644 --- a/esphome/components/hbridge/__init__.py +++ b/esphome/components/hbridge/__init__.py @@ -0,0 +1,3 @@ +import esphome.codegen as cg + +hbridge_ns = cg.esphome_ns.namespace("hbridge") diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py new file mode 100644 index 0000000000..b169978acd --- /dev/null +++ b/esphome/components/hbridge/fan/__init__.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.automation import maybe_simple_id +from esphome.components import fan, output +from esphome.const import ( + CONF_ID, + CONF_DECAY_MODE, + CONF_SPEED_COUNT, + CONF_PIN_A, + CONF_PIN_B, + CONF_ENABLE_PIN, +) +from .. import hbridge_ns + + +CODEOWNERS = ["@WeekendWarrior"] + + +HBridgeFan = hbridge_ns.class_("HBridgeFan", fan.FanState) + +DecayMode = hbridge_ns.enum("DecayMode") +DECAY_MODE_OPTIONS = { + "SLOW": DecayMode.DECAY_MODE_SLOW, + "FAST": DecayMode.DECAY_MODE_FAST, +} + +# Actions +BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action) + + +CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( + { + cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan), + cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput), + cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput), + cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum( + DECAY_MODE_OPTIONS, upper=True + ), + cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), + } +).extend(cv.COMPONENT_SCHEMA) + + +@automation.register_action( + "fan.hbridge.brake", + BrakeAction, + maybe_simple_id({cv.Required(CONF_ID): cv.use_id(HBridgeFan)}), +) +async def fan_hbridge_brake_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +async def to_code(config): + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_SPEED_COUNT], + config[CONF_DECAY_MODE], + ) + await fan.register_fan(var, config) + pin_a_ = await cg.get_variable(config[CONF_PIN_A]) + cg.add(var.set_pin_a(pin_a_)) + pin_b_ = await cg.get_variable(config[CONF_PIN_B]) + cg.add(var.set_pin_b(pin_b_)) + + if CONF_ENABLE_PIN in config: + enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN]) + cg.add(var.set_enable_pin(enable_pin)) diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp new file mode 100644 index 0000000000..a4e5429ff4 --- /dev/null +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -0,0 +1,85 @@ +#include "hbridge_fan.h" +#include "esphome/components/fan/fan_helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace hbridge { + +static const char *const TAG = "fan.hbridge"; + +void HBridgeFan::set_hbridge_levels_(float a_level, float b_level) { + this->pin_a_->set_level(a_level); + this->pin_b_->set_level(b_level); + ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f", a_level, b_level); +} + +// constant IN1/IN2, PWM on EN => power control, fast current decay +// constant IN1/EN, PWM on IN2 => power control, slow current decay +void HBridgeFan::set_hbridge_levels_(float a_level, float b_level, float enable) { + this->pin_a_->set_level(a_level); + this->pin_b_->set_level(b_level); + this->enable_->set_level(enable); + ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f, enable: %.2f", a_level, b_level, enable); +} + +fan::FanStateCall HBridgeFan::brake() { + ESP_LOGD(TAG, "Braking"); + (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f) : this->set_hbridge_levels_(1.0f, 1.0f, 1.0f); + return this->make_call().set_state(false); +} + +void HBridgeFan::dump_config() { + ESP_LOGCONFIG(TAG, "Fan '%s':", this->get_name().c_str()); + if (this->get_traits().supports_oscillation()) { + ESP_LOGCONFIG(TAG, " Oscillation: YES"); + } + if (this->get_traits().supports_direction()) { + ESP_LOGCONFIG(TAG, " Direction: YES"); + } + if (this->decay_mode_ == DECAY_MODE_SLOW) { + ESP_LOGCONFIG(TAG, " Decay Mode: Slow"); + } else { + ESP_LOGCONFIG(TAG, " Decay Mode: Fast"); + } +} +void HBridgeFan::setup() { + auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + this->set_traits(traits); + this->add_on_state_callback([this]() { this->next_update_ = true; }); +} +void HBridgeFan::loop() { + if (!this->next_update_) { + return; + } + this->next_update_ = false; + + float speed = 0.0f; + if (this->state) { + speed = static_cast(this->speed) / static_cast(this->speed_count_); + } + if (speed == 0.0f) { // off means idle + (this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, speed) + : this->set_hbridge_levels_(speed, speed, speed); + return; + } + if (this->direction == fan::FAN_DIRECTION_FORWARD) { + if (this->decay_mode_ == DECAY_MODE_SLOW) { + (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f - speed, 1.0f) + : this->set_hbridge_levels_(1.0f - speed, 1.0f, 1.0f); + } else { // DECAY_MODE_FAST + (this->enable_ == nullptr) ? this->set_hbridge_levels_(0.0f, speed) + : this->set_hbridge_levels_(0.0f, 1.0f, speed); + } + } else { // fan::FAN_DIRECTION_REVERSE + if (this->decay_mode_ == DECAY_MODE_SLOW) { + (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f - speed) + : this->set_hbridge_levels_(1.0f, 1.0f - speed, 1.0f); + } else { // DECAY_MODE_FAST + (this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, 0.0f) + : this->set_hbridge_levels_(1.0f, 0.0f, speed); + } + } +} + +} // namespace hbridge +} // namespace esphome diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h new file mode 100644 index 0000000000..984318c8d6 --- /dev/null +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -0,0 +1,58 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/output/binary_output.h" +#include "esphome/components/output/float_output.h" +#include "esphome/components/fan/fan_state.h" + +namespace esphome { +namespace hbridge { + +enum DecayMode { + DECAY_MODE_SLOW = 0, + DECAY_MODE_FAST = 1, +}; + +class HBridgeFan : public fan::FanState { + public: + HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {} + + void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } + void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } + void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } + + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + fan::FanStateCall brake(); + + int get_speed_count() { return this->speed_count_; } + // update Hbridge without a triggered FanState change, eg. for acceleration/deceleration ramping + void internal_update() { this->next_update_ = true; } + + protected: + output::FloatOutput *pin_a_; + output::FloatOutput *pin_b_; + output::FloatOutput *enable_{nullptr}; + output::BinaryOutput *oscillating_{nullptr}; + bool next_update_{true}; + int speed_count_{}; + DecayMode decay_mode_{DECAY_MODE_SLOW}; + + void set_hbridge_levels_(float a_level, float b_level); + void set_hbridge_levels_(float a_level, float b_level, float enable); +}; + +template class BrakeAction : public Action { + public: + explicit BrakeAction(HBridgeFan *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->brake(); } + + HBridgeFan *parent_; +}; + +} // namespace hbridge +} // namespace esphome diff --git a/esphome/components/hbridge/light.py b/esphome/components/hbridge/light/__init__.py similarity index 94% rename from esphome/components/hbridge/light.py rename to esphome/components/hbridge/light/__init__.py index b4ae45977a..fe5c3e9845 100644 --- a/esphome/components/hbridge/light.py +++ b/esphome/components/hbridge/light/__init__.py @@ -2,8 +2,10 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import light, output from esphome.const import CONF_OUTPUT_ID, CONF_PIN_A, CONF_PIN_B +from .. import hbridge_ns + +CODEOWNERS = ["@DotNetDann"] -hbridge_ns = cg.esphome_ns.namespace("hbridge") HBridgeLightOutput = hbridge_ns.class_( "HBridgeLightOutput", cg.PollingComponent, light.LightOutput ) diff --git a/esphome/components/hbridge/hbridge_light_output.h b/esphome/components/hbridge/light/hbridge_light_output.h similarity index 100% rename from esphome/components/hbridge/hbridge_light_output.h rename to esphome/components/hbridge/light/hbridge_light_output.h diff --git a/esphome/const.py b/esphome/const.py index 932e7e8c61..011b5deddb 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -165,6 +165,7 @@ CONF_DAYS_OF_WEEK = "days_of_week" CONF_DC_PIN = "dc_pin" CONF_DEASSERT_RTS_DTR = "deassert_rts_dtr" CONF_DEBOUNCE = "debounce" +CONF_DECAY_MODE = "decay_mode" CONF_DECELERATION = "deceleration" CONF_DEFAULT_MODE = "default_mode" CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = "default_target_temperature_high"