mirror of
https://github.com/Threnklyn/esphome-dev.git
synced 2026-05-25 23:28:28 +02:00
Create feedback cover component (#3253)
This commit is contained in:
@@ -0,0 +1,445 @@
|
||||
#include "feedback_cover.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace feedback {
|
||||
|
||||
static const char *const TAG = "feedback.cover";
|
||||
|
||||
using namespace esphome::cover;
|
||||
|
||||
void FeedbackCover::setup() {
|
||||
auto restore = this->restore_state_();
|
||||
|
||||
if (restore.has_value()) {
|
||||
restore->apply(this);
|
||||
} else {
|
||||
// if no other information, assume half open
|
||||
this->position = 0.5f;
|
||||
}
|
||||
this->current_operation = COVER_OPERATION_IDLE;
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
// if available, get position from endstop sensors
|
||||
if (this->open_endstop_ != nullptr && this->open_endstop_->state) {
|
||||
this->position = COVER_OPEN;
|
||||
} else if (this->close_endstop_ != nullptr && this->close_endstop_->state) {
|
||||
this->position = COVER_CLOSED;
|
||||
}
|
||||
|
||||
// if available, get moving state from sensors
|
||||
if (this->open_feedback_ != nullptr && this->open_feedback_->state) {
|
||||
this->current_operation = COVER_OPERATION_OPENING;
|
||||
} else if (this->close_feedback_ != nullptr && this->close_feedback_->state) {
|
||||
this->current_operation = COVER_OPERATION_CLOSING;
|
||||
}
|
||||
#endif
|
||||
|
||||
this->last_recompute_time_ = this->start_dir_time_ = millis();
|
||||
}
|
||||
|
||||
CoverTraits FeedbackCover::get_traits() {
|
||||
auto traits = CoverTraits();
|
||||
traits.set_supports_position(true);
|
||||
traits.set_supports_toggle(true);
|
||||
traits.set_is_assumed_state(this->assumed_state_);
|
||||
return traits;
|
||||
}
|
||||
|
||||
void FeedbackCover::dump_config() {
|
||||
LOG_COVER("", "Endstop Cover", this);
|
||||
ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Open Endstop", this->open_endstop_);
|
||||
LOG_BINARY_SENSOR(" ", "Open Feedback", this->open_feedback_);
|
||||
LOG_BINARY_SENSOR(" ", "Open Obstacle", this->open_obstacle_);
|
||||
#endif
|
||||
ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Close Endstop", this->close_endstop_);
|
||||
LOG_BINARY_SENSOR(" ", "Close Feedback", this->close_feedback_);
|
||||
LOG_BINARY_SENSOR(" ", "Close Obstacle", this->close_obstacle_);
|
||||
#endif
|
||||
if (this->has_built_in_endstop_) {
|
||||
ESP_LOGCONFIG(TAG, " Has builtin endstop: YES");
|
||||
}
|
||||
if (this->infer_endstop_) {
|
||||
ESP_LOGCONFIG(TAG, " Infer endstop from movement: YES");
|
||||
}
|
||||
if (this->max_duration_ < UINT32_MAX) {
|
||||
ESP_LOGCONFIG(TAG, " Max Duration: %.1fs", this->max_duration_ / 1e3f);
|
||||
}
|
||||
if (this->direction_change_waittime_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Direction change wait time: %.1fs", *this->direction_change_waittime_ / 1e3f);
|
||||
}
|
||||
if (this->acceleration_wait_time_) {
|
||||
ESP_LOGCONFIG(TAG, " Acceleration wait time: %.1fs", this->acceleration_wait_time_ / 1e3f);
|
||||
}
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->obstacle_rollback_ && (this->open_obstacle_ != nullptr || this->close_obstacle_ != nullptr)) {
|
||||
ESP_LOGCONFIG(TAG, " Obstacle rollback: %.1f%%", this->obstacle_rollback_ * 100);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
|
||||
void FeedbackCover::set_open_sensor(binary_sensor::BinarySensor *open_feedback) {
|
||||
this->open_feedback_ = open_feedback;
|
||||
|
||||
// setup callbacks to react to sensor changes
|
||||
open_feedback->add_on_state_callback([this](bool state) {
|
||||
ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
|
||||
this->recompute_position_();
|
||||
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_OPENING) {
|
||||
this->endstop_reached_(true);
|
||||
}
|
||||
this->set_current_operation_(state ? COVER_OPERATION_OPENING : COVER_OPERATION_IDLE, false);
|
||||
});
|
||||
}
|
||||
|
||||
void FeedbackCover::set_close_sensor(binary_sensor::BinarySensor *close_feedback) {
|
||||
this->close_feedback_ = close_feedback;
|
||||
|
||||
close_feedback->add_on_state_callback([this](bool state) {
|
||||
ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
|
||||
this->recompute_position_();
|
||||
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) {
|
||||
this->endstop_reached_(false);
|
||||
}
|
||||
|
||||
this->set_current_operation_(state ? COVER_OPERATION_CLOSING : COVER_OPERATION_IDLE, false);
|
||||
});
|
||||
}
|
||||
|
||||
void FeedbackCover::set_open_endstop(binary_sensor::BinarySensor *open_endstop) {
|
||||
this->open_endstop_ = open_endstop;
|
||||
open_endstop->add_on_state_callback([this](bool state) {
|
||||
if (state) {
|
||||
this->endstop_reached_(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void FeedbackCover::set_close_endstop(binary_sensor::BinarySensor *close_endstop) {
|
||||
this->close_endstop_ = close_endstop;
|
||||
close_endstop->add_on_state_callback([this](bool state) {
|
||||
if (state) {
|
||||
this->endstop_reached_(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
void FeedbackCover::endstop_reached_(bool open_endstop) {
|
||||
const uint32_t now = millis();
|
||||
|
||||
this->position = open_endstop ? COVER_OPEN : COVER_CLOSED;
|
||||
|
||||
// only act if endstop activated while moving in the right direction, in case we are coming back
|
||||
// from a position slightly past the endpoint
|
||||
if (this->current_trigger_operation_ == (open_endstop ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING)) {
|
||||
float dur = (now - this->start_dir_time_) / 1e3f;
|
||||
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), open_endstop ? "Open" : "Close", dur);
|
||||
|
||||
// if there is no external mechanism, stop the cover
|
||||
if (!this->has_built_in_endstop_) {
|
||||
this->start_direction_(COVER_OPERATION_IDLE);
|
||||
} else {
|
||||
this->set_current_operation_(COVER_OPERATION_IDLE, true);
|
||||
}
|
||||
}
|
||||
|
||||
// always sync position and publish
|
||||
this->publish_state();
|
||||
this->last_publish_time_ = now;
|
||||
}
|
||||
|
||||
void FeedbackCover::set_current_operation_(cover::CoverOperation operation, bool is_triggered) {
|
||||
if (is_triggered) {
|
||||
this->current_trigger_operation_ = operation;
|
||||
}
|
||||
|
||||
// if it is setting the actual operation (not triggered one) or
|
||||
// if we don't have moving sensor, we operate in optimistic mode, assuming actions take place immediately
|
||||
// thus, triggered operation always sets current operation.
|
||||
// otherwise, current operation comes from sensor, and may differ from requested operation
|
||||
// this might be from delays or complex actions, or because the movement was not trigger by the component
|
||||
// but initiated externally
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr))
|
||||
#endif
|
||||
{
|
||||
auto now = millis();
|
||||
this->current_operation = operation;
|
||||
this->start_dir_time_ = this->last_recompute_time_ = now;
|
||||
this->publish_state();
|
||||
this->last_publish_time_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void FeedbackCover::set_close_obstacle_sensor(binary_sensor::BinarySensor *close_obstacle) {
|
||||
this->close_obstacle_ = close_obstacle;
|
||||
|
||||
close_obstacle->add_on_state_callback([this](bool state) {
|
||||
if (state && (this->current_operation == COVER_OPERATION_CLOSING ||
|
||||
this->current_trigger_operation_ == COVER_OPERATION_CLOSING)) {
|
||||
ESP_LOGD(TAG, "'%s' - Close obstacle detected.", this->name_.c_str());
|
||||
this->start_direction_(COVER_OPERATION_IDLE);
|
||||
|
||||
if (this->obstacle_rollback_) {
|
||||
this->target_position_ = clamp(this->position + this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN);
|
||||
this->start_direction_(COVER_OPERATION_OPENING);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void FeedbackCover::set_open_obstacle_sensor(binary_sensor::BinarySensor *open_obstacle) {
|
||||
this->open_obstacle_ = open_obstacle;
|
||||
|
||||
open_obstacle->add_on_state_callback([this](bool state) {
|
||||
if (state && (this->current_operation == COVER_OPERATION_OPENING ||
|
||||
this->current_trigger_operation_ == COVER_OPERATION_OPENING)) {
|
||||
ESP_LOGD(TAG, "'%s' - Open obstacle detected.", this->name_.c_str());
|
||||
this->start_direction_(COVER_OPERATION_IDLE);
|
||||
|
||||
if (this->obstacle_rollback_) {
|
||||
this->target_position_ = clamp(this->position - this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN);
|
||||
this->start_direction_(COVER_OPERATION_CLOSING);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
void FeedbackCover::loop() {
|
||||
if (this->current_operation == COVER_OPERATION_IDLE)
|
||||
return;
|
||||
const uint32_t now = millis();
|
||||
|
||||
// Recompute position every loop cycle
|
||||
this->recompute_position_();
|
||||
|
||||
// if we initiated the move, check if we reached position or max time
|
||||
// (stoping from endstop sensor is handled in callback)
|
||||
if (this->current_trigger_operation_ != COVER_OPERATION_IDLE) {
|
||||
if (this->is_at_target_()) {
|
||||
if (this->has_built_in_endstop_ &&
|
||||
(this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED)) {
|
||||
// Don't trigger stop, let the cover stop by itself.
|
||||
this->set_current_operation_(COVER_OPERATION_IDLE, true);
|
||||
} else {
|
||||
this->start_direction_(COVER_OPERATION_IDLE);
|
||||
}
|
||||
} else if (now - this->start_dir_time_ > this->max_duration_) {
|
||||
ESP_LOGD(TAG, "'%s' - Max duration reached. Stopping cover.", this->name_.c_str());
|
||||
this->start_direction_(COVER_OPERATION_IDLE);
|
||||
}
|
||||
}
|
||||
|
||||
// update current position at requested interval, regardless of who started the movement
|
||||
// so that we also update UI if there was an external movement
|
||||
// don´t save intermediate positions
|
||||
if (now - this->last_publish_time_ > this->update_interval_) {
|
||||
this->publish_state(false);
|
||||
this->last_publish_time_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
void FeedbackCover::control(const CoverCall &call) {
|
||||
// stop action logic
|
||||
if (call.get_stop()) {
|
||||
this->start_direction_(COVER_OPERATION_IDLE);
|
||||
} else if (call.get_toggle().has_value()) {
|
||||
// toggle action logic: OPEN - STOP - CLOSE
|
||||
if (this->current_trigger_operation_ != COVER_OPERATION_IDLE) {
|
||||
this->start_direction_(COVER_OPERATION_IDLE);
|
||||
} else {
|
||||
if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) {
|
||||
this->target_position_ = COVER_OPEN;
|
||||
this->start_direction_(COVER_OPERATION_OPENING);
|
||||
} else {
|
||||
this->target_position_ = COVER_CLOSED;
|
||||
this->start_direction_(COVER_OPERATION_CLOSING);
|
||||
}
|
||||
}
|
||||
} else if (call.get_position().has_value()) {
|
||||
// go to position action
|
||||
auto pos = *call.get_position();
|
||||
if (pos == this->position) {
|
||||
// already at target,
|
||||
|
||||
// for covers with built in end stop, if we don´t have sensors we should send the command again
|
||||
// to make sure the assumed state is not wrong
|
||||
if (this->has_built_in_endstop_ && ((pos == COVER_OPEN
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
&& this->open_endstop_ == nullptr
|
||||
#endif
|
||||
&& !this->infer_endstop_) ||
|
||||
(pos == COVER_CLOSED
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
&& this->close_endstop_ == nullptr
|
||||
#endif
|
||||
&& !this->infer_endstop_))) {
|
||||
this->target_position_ = pos;
|
||||
this->start_direction_(pos == COVER_CLOSED ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING);
|
||||
} else if (this->current_operation != COVER_OPERATION_IDLE ||
|
||||
this->current_trigger_operation_ != COVER_OPERATION_IDLE) {
|
||||
// if we are moving, stop
|
||||
this->start_direction_(COVER_OPERATION_IDLE);
|
||||
}
|
||||
} else {
|
||||
this->target_position_ = pos;
|
||||
this->start_direction_(pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FeedbackCover::stop_prev_trigger_() {
|
||||
if (this->direction_change_waittime_.has_value()) {
|
||||
this->cancel_timeout("direction_change");
|
||||
}
|
||||
if (this->prev_command_trigger_ != nullptr) {
|
||||
this->prev_command_trigger_->stop_action();
|
||||
this->prev_command_trigger_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool FeedbackCover::is_at_target_() const {
|
||||
// if initiated externally, current operation might be different from
|
||||
// operation that was triggered, thus evaluate position against what was asked
|
||||
|
||||
switch (this->current_trigger_operation_) {
|
||||
case COVER_OPERATION_OPENING:
|
||||
return this->position >= this->target_position_;
|
||||
case COVER_OPERATION_CLOSING:
|
||||
return this->position <= this->target_position_;
|
||||
case COVER_OPERATION_IDLE:
|
||||
return this->current_operation == COVER_OPERATION_IDLE;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
void FeedbackCover::start_direction_(CoverOperation dir) {
|
||||
Trigger<> *trig;
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
binary_sensor::BinarySensor *obstacle{nullptr};
|
||||
#endif
|
||||
|
||||
switch (dir) {
|
||||
case COVER_OPERATION_IDLE:
|
||||
trig = this->stop_trigger_;
|
||||
break;
|
||||
case COVER_OPERATION_OPENING:
|
||||
this->last_operation_ = dir;
|
||||
trig = this->open_trigger_;
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
obstacle = this->open_obstacle_;
|
||||
#endif
|
||||
break;
|
||||
case COVER_OPERATION_CLOSING:
|
||||
this->last_operation_ = dir;
|
||||
trig = this->close_trigger_;
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
obstacle = this->close_obstacle_;
|
||||
#endif
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
this->stop_prev_trigger_();
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
// check if there is an obstacle to start the new operation -> abort without any change
|
||||
// the case when an obstacle appears while moving is handled in the callback
|
||||
if (obstacle != nullptr && obstacle->state) {
|
||||
ESP_LOGD(TAG, "'%s' - %s obstacle detected. Action not started.", this->name_.c_str(),
|
||||
dir == COVER_OPERATION_OPENING ? "Open" : "Close");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// if we are moving and need to move in the opposite direction
|
||||
// check if we have a wait time
|
||||
if (this->direction_change_waittime_.has_value() && dir != COVER_OPERATION_IDLE &&
|
||||
this->current_operation != COVER_OPERATION_IDLE && dir != this->current_operation) {
|
||||
ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str());
|
||||
this->start_direction_(COVER_OPERATION_IDLE);
|
||||
|
||||
this->set_timeout("direction_change", *this->direction_change_waittime_,
|
||||
[this, dir]() { this->start_direction_(dir); });
|
||||
|
||||
} else {
|
||||
this->set_current_operation_(dir, true);
|
||||
this->prev_command_trigger_ = trig;
|
||||
ESP_LOGD(TAG, "'%s' - Firing '%s' trigger.", this->name_.c_str(),
|
||||
dir == COVER_OPERATION_OPENING ? "OPEN"
|
||||
: dir == COVER_OPERATION_CLOSING ? "CLOSE"
|
||||
: "STOP");
|
||||
trig->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
void FeedbackCover::recompute_position_() {
|
||||
if (this->current_operation == COVER_OPERATION_IDLE)
|
||||
return;
|
||||
|
||||
const uint32_t now = millis();
|
||||
float dir;
|
||||
float action_dur;
|
||||
float min_pos;
|
||||
float max_pos;
|
||||
|
||||
// endstop sensors update position from their callbacks, and sets the fully open/close value
|
||||
// If we have endstop, estimation never reaches the fully open/closed state.
|
||||
// but if movement continues past corresponding endstop (inertia), keep the fully open/close state
|
||||
|
||||
switch (this->current_operation) {
|
||||
case COVER_OPERATION_OPENING:
|
||||
dir = 1.0f;
|
||||
action_dur = this->open_duration_;
|
||||
min_pos = COVER_CLOSED;
|
||||
max_pos = (
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
this->open_endstop_ != nullptr ||
|
||||
#endif
|
||||
this->infer_endstop_) &&
|
||||
this->position < COVER_OPEN
|
||||
? 0.99f
|
||||
: COVER_OPEN;
|
||||
break;
|
||||
case COVER_OPERATION_CLOSING:
|
||||
dir = -1.0f;
|
||||
action_dur = this->close_duration_;
|
||||
min_pos = (
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
this->close_endstop_ != nullptr ||
|
||||
#endif
|
||||
this->infer_endstop_) &&
|
||||
this->position > COVER_CLOSED
|
||||
? 0.01f
|
||||
: COVER_CLOSED;
|
||||
max_pos = COVER_OPEN;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// check if we have an acceleration_wait_time, and remove from position computation
|
||||
if (now > (this->start_dir_time_ + this->acceleration_wait_time_)) {
|
||||
this->position +=
|
||||
dir * (now - std::max(this->start_dir_time_ + this->acceleration_wait_time_, this->last_recompute_time_)) /
|
||||
(action_dur - this->acceleration_wait_time_);
|
||||
this->position = clamp(this->position, min_pos, max_pos);
|
||||
}
|
||||
this->last_recompute_time_ = now;
|
||||
}
|
||||
|
||||
} // namespace feedback
|
||||
} // namespace esphome
|
||||
Reference in New Issue
Block a user