Add Current based cover (#1439)

* Adding first version of current_base cover. No Interlock yet.

* simplifying code

* Implementing malfunction protection

* Adding test

* Fixing too long lines

* Fixing test sensor names

* Adding missing id's in ade7953 tests

* Adding code owners as requested

* Fixing issue setting position when stop reached

* Fixing issue setting position when stop reached

* Black formatting

* Fixing format issues

* Fix for concurrent changes

Co-authored-by: Oxan van Leeuwen <oxan@oxanvanleeuwen.nl>
This commit is contained in:
Marcos Pérez Ferro 2021-09-27 22:22:45 +02:00 committed by GitHub
parent 1ba560dc9e
commit e30f17f64f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 500 additions and 0 deletions

View file

@ -39,6 +39,7 @@ esphome/components/coolix/* @glmnet
esphome/components/cover/* @esphome/core
esphome/components/cs5460a/* @balrog-kun
esphome/components/ct_clamp/* @jesserockz
esphome/components/current_based/* @djwmarcx
esphome/components/daly_bms/* @s1lvi0
esphome/components/dashboard_import/* @esphome/core
esphome/components/debug/* @OttoWinter

View file

@ -0,0 +1 @@
CODEOWNERS = ["@djwmarcx"]

View file

@ -0,0 +1,124 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.components import sensor, cover
from esphome.const import (
CONF_CLOSE_ACTION,
CONF_CLOSE_DURATION,
CONF_ID,
CONF_OPEN_ACTION,
CONF_OPEN_DURATION,
CONF_STOP_ACTION,
CONF_MAX_DURATION,
)
CONF_OPEN_SENSOR = "open_sensor"
CONF_OPEN_MOVING_CURRENT_THRESHOLD = "open_moving_current_threshold"
CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD = "open_obstacle_current_threshold"
CONF_CLOSE_SENSOR = "close_sensor"
CONF_CLOSE_MOVING_CURRENT_THRESHOLD = "close_moving_current_threshold"
CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD = "close_obstacle_current_threshold"
CONF_OBSTACLE_ROLLBACK = "obstacle_rollback"
CONF_MALFUNCTION_DETECTION = "malfunction_detection"
CONF_MALFUNCTION_ACTION = "malfunction_action"
CONF_START_SENSING_DELAY = "start_sensing_delay"
current_based_ns = cg.esphome_ns.namespace("current_based")
CurrentBasedCover = current_based_ns.class_(
"CurrentBasedCover", cover.Cover, cg.Component
)
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(CurrentBasedCover),
cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
cv.Required(CONF_OPEN_SENSOR): cv.use_id(sensor.Sensor),
cv.Required(CONF_OPEN_MOVING_CURRENT_THRESHOLD): cv.float_range(
min=0, min_included=False
),
cv.Optional(CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD): cv.float_range(
min=0, min_included=False
),
cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True),
cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds,
cv.Required(CONF_CLOSE_SENSOR): cv.use_id(sensor.Sensor),
cv.Required(CONF_CLOSE_MOVING_CURRENT_THRESHOLD): cv.float_range(
min=0, min_included=False
),
cv.Optional(CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD): cv.float_range(
min=0, min_included=False
),
cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage,
cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds,
cv.Optional(CONF_MALFUNCTION_DETECTION, default=True): cv.boolean,
cv.Optional(CONF_MALFUNCTION_ACTION): automation.validate_automation(
single=True
),
cv.Optional(
CONF_START_SENSING_DELAY, default="500ms"
): cv.positive_time_period_milliseconds,
}
).extend(cv.COMPONENT_SCHEMA)
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
yield cover.register_cover(var, config)
yield automation.build_automation(
var.get_stop_trigger(), [], config[CONF_STOP_ACTION]
)
# OPEN
bin = yield cg.get_variable(config[CONF_OPEN_SENSOR])
cg.add(var.set_open_sensor(bin))
cg.add(
var.set_open_moving_current_threshold(
config[CONF_OPEN_MOVING_CURRENT_THRESHOLD]
)
)
if CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD in config:
cg.add(
var.set_open_obstacle_current_threshold(
config[CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD]
)
)
cg.add(var.set_open_duration(config[CONF_OPEN_DURATION]))
yield automation.build_automation(
var.get_open_trigger(), [], config[CONF_OPEN_ACTION]
)
# CLOSE
bin = yield cg.get_variable(config[CONF_CLOSE_SENSOR])
cg.add(var.set_close_sensor(bin))
cg.add(
var.set_close_moving_current_threshold(
config[CONF_CLOSE_MOVING_CURRENT_THRESHOLD]
)
)
if CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD in config:
cg.add(
var.set_close_obstacle_current_threshold(
config[CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD]
)
)
cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION]))
yield automation.build_automation(
var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]
)
cg.add(var.set_obstacle_rollback(config[CONF_OBSTACLE_ROLLBACK]))
if CONF_MAX_DURATION in config:
cg.add(var.set_max_duration(config[CONF_MAX_DURATION]))
cg.add(var.set_malfunction_detection(config[CONF_MALFUNCTION_DETECTION]))
if CONF_MALFUNCTION_ACTION in config:
yield automation.build_automation(
var.get_malfunction_trigger(), [], config[CONF_MALFUNCTION_ACTION]
)
cg.add(var.set_start_sensing_delay(config[CONF_START_SENSING_DELAY]))

View file

@ -0,0 +1,251 @@
#include "current_based_cover.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <cfloat>
namespace esphome {
namespace current_based {
static const char *const TAG = "current_based.cover";
using namespace esphome::cover;
CoverTraits CurrentBasedCover::get_traits() {
auto traits = CoverTraits();
traits.set_supports_position(true);
traits.set_is_assumed_state(false);
return traits;
}
void CurrentBasedCover::control(const CoverCall &call) {
if (call.get_stop()) {
this->direction_idle_();
}
if (call.get_position().has_value()) {
auto pos = *call.get_position();
if (pos == this->position) {
// already at target
} else {
auto op = pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING;
this->target_position_ = pos;
this->start_direction_(op);
}
}
}
void CurrentBasedCover::setup() {
auto restore = this->restore_state_();
if (restore.has_value()) {
restore->apply(this);
} else {
this->position = 0.5f;
}
}
void CurrentBasedCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE)
return;
const uint32_t now = millis();
if (this->current_operation == COVER_OPERATION_OPENING) {
if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction
this->direction_idle_();
this->malfunction_trigger_->trigger();
ESP_LOGI(TAG, "'%s' - Malfunction detected during opening. Current flow detected in close circuit",
this->name_.c_str());
} else if (this->is_opening_blocked_()) { // Blocked
ESP_LOGD(TAG, "'%s' - Obstacle detected during opening.", this->name_.c_str());
this->direction_idle_();
if (this->obstacle_rollback_ != 0) {
this->set_timeout("rollback", 300, [this]() {
ESP_LOGD(TAG, "'%s' - Rollback.", this->name_.c_str());
this->target_position_ = clamp(this->position - this->obstacle_rollback_, 0.0F, 1.0F);
this->start_direction_(COVER_OPERATION_CLOSING);
});
}
} else if (this->is_initial_delay_finished_() && !this->is_opening_()) { // End reached
auto dur = (now - this->start_dir_time_) / 1e3f;
ESP_LOGD(TAG, "'%s' - Open position reached. Took %.1fs.", this->name_.c_str(), dur);
this->direction_idle_(COVER_OPEN);
}
} else if (this->current_operation == COVER_OPERATION_CLOSING) {
if (this->malfunction_detection_ && this->is_opening_()) { // Malfunction
this->direction_idle_();
this->malfunction_trigger_->trigger();
ESP_LOGI(TAG, "'%s' - Malfunction detected during closing. Current flow detected in open circuit",
this->name_.c_str());
} else if (this->is_closing_blocked_()) { // Blocked
ESP_LOGD(TAG, "'%s' - Obstacle detected during closing.", this->name_.c_str());
this->direction_idle_();
if (this->obstacle_rollback_ != 0) {
this->set_timeout("rollback", 300, [this]() {
ESP_LOGD(TAG, "'%s' - Rollback.", this->name_.c_str());
this->target_position_ = clamp(this->position + this->obstacle_rollback_, 0.0F, 1.0F);
this->start_direction_(COVER_OPERATION_OPENING);
});
}
} else if (this->is_initial_delay_finished_() && !this->is_closing_()) { // End reached
auto dur = (now - this->start_dir_time_) / 1e3f;
ESP_LOGD(TAG, "'%s' - Close position reached. Took %.1fs.", this->name_.c_str(), dur);
this->direction_idle_(COVER_CLOSED);
}
} else if (now - this->start_dir_time_ > this->max_duration_) {
ESP_LOGD(TAG, "'%s' - Max duration reached. Stopping cover.", this->name_.c_str());
this->direction_idle_();
}
// Recompute position every loop cycle
this->recompute_position_();
if (this->current_operation != COVER_OPERATION_IDLE && this->is_at_target_()) {
this->direction_idle_();
}
// Send current position every second
if (this->current_operation != COVER_OPERATION_IDLE && now - this->last_publish_time_ > 1000) {
this->publish_state(false);
this->last_publish_time_ = now;
}
}
void CurrentBasedCover::direction_idle_(float new_position) {
this->start_direction_(COVER_OPERATION_IDLE);
if (new_position != FLT_MAX) {
this->position = new_position;
}
this->publish_state();
}
void CurrentBasedCover::dump_config() {
LOG_COVER("", "Endstop Cover", this);
LOG_SENSOR(" ", "Open Sensor", this->open_sensor_);
ESP_LOGCONFIG(TAG, " Open moving current threshold: %.11fA", this->open_moving_current_threshold_);
if (this->open_obstacle_current_threshold_ != FLT_MAX) {
ESP_LOGCONFIG(TAG, " Open obstacle current threshold: %.11fA", this->open_obstacle_current_threshold_);
}
ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f);
LOG_SENSOR(" ", "Close Sensor", this->close_sensor_);
ESP_LOGCONFIG(TAG, " Close moving current threshold: %.11fA", this->close_moving_current_threshold_);
if (this->close_obstacle_current_threshold_ != FLT_MAX) {
ESP_LOGCONFIG(TAG, " Close obstacle current threshold: %.11fA", this->close_obstacle_current_threshold_);
}
ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f);
ESP_LOGCONFIG(TAG, "Obstacle Rollback: %.1f%%", this->obstacle_rollback_ * 100);
if (this->max_duration_ != UINT32_MAX) {
ESP_LOGCONFIG(TAG, "Maximun duration: %.1fs", this->max_duration_ / 1e3f);
}
ESP_LOGCONFIG(TAG, "Start sensing delay: %.1fs", this->start_sensing_delay_ / 1e3f);
ESP_LOGCONFIG(TAG, "Malfunction detection: %s", YESNO(this->malfunction_detection_));
}
float CurrentBasedCover::get_setup_priority() const { return setup_priority::DATA; }
void CurrentBasedCover::stop_prev_trigger_() {
if (this->prev_command_trigger_ != nullptr) {
this->prev_command_trigger_->stop_action();
this->prev_command_trigger_ = nullptr;
}
}
bool CurrentBasedCover::is_opening_() const {
return this->open_sensor_->get_state() > this->open_moving_current_threshold_;
}
bool CurrentBasedCover::is_opening_blocked_() const {
if (this->open_obstacle_current_threshold_ == FLT_MAX) {
return false;
}
return this->open_sensor_->get_state() > this->open_obstacle_current_threshold_;
}
bool CurrentBasedCover::is_closing_() const {
return this->close_sensor_->get_state() > this->close_moving_current_threshold_;
}
bool CurrentBasedCover::is_closing_blocked_() const {
if (this->close_obstacle_current_threshold_ == FLT_MAX) {
return false;
}
return this->open_sensor_->get_state() > this->open_obstacle_current_threshold_;
}
bool CurrentBasedCover::is_initial_delay_finished_() const {
return millis() - this->start_dir_time_ > this->start_sensing_delay_;
}
bool CurrentBasedCover::is_at_target_() const {
switch (this->current_operation) {
case COVER_OPERATION_OPENING:
if (this->target_position_ == COVER_OPEN) {
if (!this->is_initial_delay_finished_()) // During initial delay, state is assumed
return false;
return !this->is_opening_();
}
return this->position >= this->target_position_;
case COVER_OPERATION_CLOSING:
if (this->target_position_ == COVER_CLOSED) {
if (!this->is_initial_delay_finished_()) // During initial delay, state is assumed
return false;
return !this->is_closing_();
}
return this->position <= this->target_position_;
case COVER_OPERATION_IDLE:
default:
return true;
}
}
void CurrentBasedCover::start_direction_(CoverOperation dir) {
if (dir == this->current_operation)
return;
this->recompute_position_();
Trigger<> *trig;
switch (dir) {
case COVER_OPERATION_IDLE:
trig = this->stop_trigger_;
break;
case COVER_OPERATION_OPENING:
trig = this->open_trigger_;
break;
case COVER_OPERATION_CLOSING:
trig = this->close_trigger_;
break;
default:
return;
}
this->current_operation = dir;
this->stop_prev_trigger_();
trig->trigger();
this->prev_command_trigger_ = trig;
const auto now = millis();
this->start_dir_time_ = now;
this->last_recompute_time_ = now;
}
void CurrentBasedCover::recompute_position_() {
if (this->current_operation == COVER_OPERATION_IDLE)
return;
float dir;
float action_dur;
switch (this->current_operation) {
case COVER_OPERATION_OPENING:
dir = 1.0F;
action_dur = this->open_duration_;
break;
case COVER_OPERATION_CLOSING:
dir = -1.0F;
action_dur = this->close_duration_;
break;
default:
return;
}
const auto now = millis();
this->position += dir * (now - this->last_recompute_time_) / action_dur;
this->position = clamp(this->position, 0.0F, 1.0F);
this->last_recompute_time_ = now;
}
} // namespace current_based
} // namespace esphome

View file

@ -0,0 +1,95 @@
#pragma once
#include "esphome/components/cover/cover.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include <cfloat>
namespace esphome {
namespace current_based {
class CurrentBasedCover : public cover::Cover, public Component {
public:
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override;
Trigger<> *get_stop_trigger() const { return this->stop_trigger_; }
Trigger<> *get_open_trigger() const { return this->open_trigger_; }
void set_open_sensor(sensor::Sensor *open_sensor) { this->open_sensor_ = open_sensor; }
void set_open_moving_current_threshold(float open_moving_current_threshold) {
this->open_moving_current_threshold_ = open_moving_current_threshold;
}
void set_open_obstacle_current_threshold(float open_obstacle_current_threshold) {
this->open_obstacle_current_threshold_ = open_obstacle_current_threshold;
}
void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; }
Trigger<> *get_close_trigger() const { return this->close_trigger_; }
void set_close_sensor(sensor::Sensor *close_sensor) { this->close_sensor_ = close_sensor; }
void set_close_moving_current_threshold(float close_moving_current_threshold) {
this->close_moving_current_threshold_ = close_moving_current_threshold;
}
void set_close_obstacle_current_threshold(float close_obstacle_current_threshold) {
this->close_obstacle_current_threshold_ = close_obstacle_current_threshold;
}
void set_close_duration(uint32_t close_duration) { this->close_duration_ = close_duration; }
void set_max_duration(uint32_t max_duration) { this->max_duration_ = max_duration; }
void set_obstacle_rollback(float obstacle_rollback) { this->obstacle_rollback_ = obstacle_rollback; }
void set_malfunction_detection(bool malfunction_detection) { this->malfunction_detection_ = malfunction_detection; }
void set_start_sensing_delay(uint32_t start_sensing_delay) { this->start_sensing_delay_ = start_sensing_delay; }
Trigger<> *get_malfunction_trigger() const { return this->malfunction_trigger_; }
cover::CoverTraits get_traits() override;
protected:
void control(const cover::CoverCall &call) override;
void stop_prev_trigger_();
bool is_at_target_() const;
bool is_opening_() const;
bool is_opening_blocked_() const;
bool is_closing_() const;
bool is_closing_blocked_() const;
bool is_initial_delay_finished_() const;
void direction_idle_(float new_position = FLT_MAX);
void start_direction_(cover::CoverOperation dir);
void recompute_position_();
Trigger<> *stop_trigger_{new Trigger<>()};
sensor::Sensor *open_sensor_{nullptr};
Trigger<> *open_trigger_{new Trigger<>()};
float open_moving_current_threshold_;
float open_obstacle_current_threshold_{FLT_MAX};
uint32_t open_duration_;
sensor::Sensor *close_sensor_{nullptr};
Trigger<> *close_trigger_{new Trigger<>()};
float close_moving_current_threshold_;
float close_obstacle_current_threshold_{FLT_MAX};
uint32_t close_duration_;
uint32_t max_duration_{UINT32_MAX};
bool malfunction_detection_{true};
Trigger<> *malfunction_trigger_{new Trigger<>()};
uint32_t start_sensing_delay_;
float obstacle_rollback_;
Trigger<> *prev_command_trigger_{nullptr};
uint32_t last_recompute_time_{0};
uint32_t start_dir_time_{0};
uint32_t last_publish_time_{0};
float target_position_{0};
};
} // namespace current_based
} // namespace esphome

View file

@ -426,14 +426,19 @@ sensor:
irq_pin: GPIO16
voltage:
name: ADE7953 Voltage
id: ade7953_voltage
current_a:
name: ADE7953 Current A
id: ade7953_current_a
current_b:
name: ADE7953 Current B
id: ade7953_current_b
active_power_a:
name: ADE7953 Active Power A
id: ade7953_active_power_a
active_power_b:
name: ADE7953 Active Power B
id: ade7953_active_power_b
- platform: pzem004t
uart_id: uart3
voltage:
@ -1021,6 +1026,29 @@ cover:
close_action:
- switch.turn_on: gpio_switch2
close_duration: 4.5min
- platform: current_based
name: "Current Based Cover"
open_sensor: ade7953_current_a
open_moving_current_threshold: 0.5
open_obstacle_current_threshold: 0.8
open_duration: 12s
open_action:
- switch.turn_on: gpio_switch1
close_sensor: ade7953_current_b
close_moving_current_threshold: 0.5
close_obstacle_current_threshold: 0.8
close_duration: 10s
close_action:
- switch.turn_on: gpio_switch2
stop_action:
- switch.turn_off: gpio_switch1
- switch.turn_off: gpio_switch2
obstacle_rollback: 30%
start_sensing_delay: 0.8s
malfunction_detection: true
malfunction_action:
then:
- logger.log: "Malfunction Detected"
- platform: template
name: Template Cover with Tilt
tilt_lambda: 'return 0.5;'