mirror of
https://github.com/esphome/esphome.git
synced 2024-11-28 09:44:12 +01:00
Create feedback cover component (#3253)
This commit is contained in:
parent
d56c53c848
commit
4d56a975e6
6 changed files with 744 additions and 8 deletions
|
@ -73,6 +73,7 @@ esphome/components/esp8266/* @esphome/core
|
||||||
esphome/components/exposure_notifications/* @OttoWinter
|
esphome/components/exposure_notifications/* @OttoWinter
|
||||||
esphome/components/ezo/* @ssieb
|
esphome/components/ezo/* @ssieb
|
||||||
esphome/components/fastled_base/* @OttoWinter
|
esphome/components/fastled_base/* @OttoWinter
|
||||||
|
esphome/components/feedback/* @ianchi
|
||||||
esphome/components/fingerprint_grow/* @OnFreund @loongyh
|
esphome/components/fingerprint_grow/* @OnFreund @loongyh
|
||||||
esphome/components/globals/* @esphome/core
|
esphome/components/globals/* @esphome/core
|
||||||
esphome/components/gpio/* @esphome/core
|
esphome/components/gpio/* @esphome/core
|
||||||
|
|
1
esphome/components/feedback/__init__.py
Normal file
1
esphome/components/feedback/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
CODEOWNERS = ["@ianchi"]
|
157
esphome/components/feedback/cover.py
Normal file
157
esphome/components/feedback/cover.py
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome import automation
|
||||||
|
from esphome.components import binary_sensor, cover
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_ASSUMED_STATE,
|
||||||
|
CONF_CLOSE_ACTION,
|
||||||
|
CONF_CLOSE_DURATION,
|
||||||
|
CONF_CLOSE_ENDSTOP,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_OPEN_ACTION,
|
||||||
|
CONF_OPEN_DURATION,
|
||||||
|
CONF_OPEN_ENDSTOP,
|
||||||
|
CONF_STOP_ACTION,
|
||||||
|
CONF_MAX_DURATION,
|
||||||
|
CONF_UPDATE_INTERVAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
CONF_OPEN_SENSOR = "open_sensor"
|
||||||
|
CONF_CLOSE_SENSOR = "close_sensor"
|
||||||
|
CONF_OPEN_OBSTACLE_SENSOR = "open_obstacle_sensor"
|
||||||
|
CONF_CLOSE_OBSTACLE_SENSOR = "close_obstacle_sensor"
|
||||||
|
CONF_HAS_BUILT_IN_ENDSTOP = "has_built_in_endstop"
|
||||||
|
CONF_INFER_ENDSTOP_FROM_MOVEMENT = "infer_endstop_from_movement"
|
||||||
|
CONF_DIRECTION_CHANGE_WAIT_TIME = "direction_change_wait_time"
|
||||||
|
CONF_ACCELERATION_WAIT_TIME = "acceleration_wait_time"
|
||||||
|
CONF_OBSTACLE_ROLLBACK = "obstacle_rollback"
|
||||||
|
|
||||||
|
endstop_ns = cg.esphome_ns.namespace("feedback")
|
||||||
|
FeedbackCover = endstop_ns.class_("FeedbackCover", cover.Cover, cg.Component)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_infer_endstop(config):
|
||||||
|
if config[CONF_INFER_ENDSTOP_FROM_MOVEMENT] is True:
|
||||||
|
if config[CONF_HAS_BUILT_IN_ENDSTOP] is False:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"{CONF_INFER_ENDSTOP_FROM_MOVEMENT} can only be set if {CONF_HAS_BUILT_IN_ENDSTOP} is also set"
|
||||||
|
)
|
||||||
|
|
||||||
|
if CONF_OPEN_SENSOR not in config:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"{CONF_INFER_ENDSTOP_FROM_MOVEMENT} cannot be set if movement sensors are not supplied"
|
||||||
|
)
|
||||||
|
|
||||||
|
if CONF_OPEN_ENDSTOP in config or CONF_CLOSE_ENDSTOP in config:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"{CONF_INFER_ENDSTOP_FROM_MOVEMENT} cannot be set if endstop sensors are supplied"
|
||||||
|
)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_FEEDBACK_COVER_BASE_SCHEMA = cover.COVER_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(FeedbackCover),
|
||||||
|
cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
|
||||||
|
cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True),
|
||||||
|
cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds,
|
||||||
|
cv.Optional(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor),
|
||||||
|
cv.Optional(CONF_OPEN_SENSOR): cv.use_id(binary_sensor.BinarySensor),
|
||||||
|
cv.Optional(CONF_OPEN_OBSTACLE_SENSOR): cv.use_id(binary_sensor.BinarySensor),
|
||||||
|
cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
|
||||||
|
cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
|
||||||
|
cv.Optional(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor),
|
||||||
|
cv.Optional(CONF_CLOSE_SENSOR): cv.use_id(binary_sensor.BinarySensor),
|
||||||
|
cv.Optional(CONF_CLOSE_OBSTACLE_SENSOR): cv.use_id(binary_sensor.BinarySensor),
|
||||||
|
cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds,
|
||||||
|
cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean,
|
||||||
|
cv.Optional(CONF_ASSUMED_STATE): cv.boolean,
|
||||||
|
cv.Optional(
|
||||||
|
CONF_UPDATE_INTERVAL, "1000ms"
|
||||||
|
): cv.positive_time_period_milliseconds,
|
||||||
|
cv.Optional(CONF_INFER_ENDSTOP_FROM_MOVEMENT, False): cv.boolean,
|
||||||
|
cv.Optional(
|
||||||
|
CONF_DIRECTION_CHANGE_WAIT_TIME
|
||||||
|
): cv.positive_time_period_milliseconds,
|
||||||
|
cv.Optional(
|
||||||
|
CONF_ACCELERATION_WAIT_TIME, "0s"
|
||||||
|
): cv.positive_time_period_milliseconds,
|
||||||
|
cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage,
|
||||||
|
},
|
||||||
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
CONFIG_FEEDBACK_COVER_BASE_SCHEMA,
|
||||||
|
cv.has_none_or_all_keys(CONF_OPEN_SENSOR, CONF_CLOSE_SENSOR),
|
||||||
|
validate_infer_endstop,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
await cover.register_cover(var, config)
|
||||||
|
|
||||||
|
# STOP
|
||||||
|
await automation.build_automation(
|
||||||
|
var.get_stop_trigger(), [], config[CONF_STOP_ACTION]
|
||||||
|
)
|
||||||
|
|
||||||
|
# OPEN
|
||||||
|
await automation.build_automation(
|
||||||
|
var.get_open_trigger(), [], config[CONF_OPEN_ACTION]
|
||||||
|
)
|
||||||
|
cg.add(var.set_open_duration(config[CONF_OPEN_DURATION]))
|
||||||
|
if CONF_OPEN_ENDSTOP in config:
|
||||||
|
bin = await cg.get_variable(config[CONF_OPEN_ENDSTOP])
|
||||||
|
cg.add(var.set_open_endstop(bin))
|
||||||
|
if CONF_OPEN_SENSOR in config:
|
||||||
|
bin = await cg.get_variable(config[CONF_OPEN_SENSOR])
|
||||||
|
cg.add(var.set_open_sensor(bin))
|
||||||
|
if CONF_OPEN_OBSTACLE_SENSOR in config:
|
||||||
|
bin = await cg.get_variable(config[CONF_OPEN_OBSTACLE_SENSOR])
|
||||||
|
cg.add(var.set_open_obstacle_sensor(bin))
|
||||||
|
|
||||||
|
# CLOSE
|
||||||
|
await automation.build_automation(
|
||||||
|
var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]
|
||||||
|
)
|
||||||
|
cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION]))
|
||||||
|
if CONF_CLOSE_ENDSTOP in config:
|
||||||
|
bin = await cg.get_variable(config[CONF_CLOSE_ENDSTOP])
|
||||||
|
cg.add(var.set_close_endstop(bin))
|
||||||
|
if CONF_CLOSE_SENSOR in config:
|
||||||
|
bin = await cg.get_variable(config[CONF_CLOSE_SENSOR])
|
||||||
|
cg.add(var.set_close_sensor(bin))
|
||||||
|
if CONF_CLOSE_OBSTACLE_SENSOR in config:
|
||||||
|
bin = await cg.get_variable(config[CONF_CLOSE_OBSTACLE_SENSOR])
|
||||||
|
cg.add(var.set_close_obstacle_sensor(bin))
|
||||||
|
|
||||||
|
# OTHER
|
||||||
|
if CONF_MAX_DURATION in config:
|
||||||
|
cg.add(var.set_max_duration(config[CONF_MAX_DURATION]))
|
||||||
|
|
||||||
|
cg.add(var.set_has_built_in_endstop(config[CONF_HAS_BUILT_IN_ENDSTOP]))
|
||||||
|
|
||||||
|
if CONF_ASSUMED_STATE in config:
|
||||||
|
cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE]))
|
||||||
|
else:
|
||||||
|
cg.add(
|
||||||
|
var.set_assumed_state(
|
||||||
|
not (
|
||||||
|
(CONF_CLOSE_ENDSTOP in config and CONF_OPEN_ENDSTOP in config)
|
||||||
|
or config[CONF_INFER_ENDSTOP_FROM_MOVEMENT]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))
|
||||||
|
cg.add(var.set_infer_endstop(config[CONF_INFER_ENDSTOP_FROM_MOVEMENT]))
|
||||||
|
if CONF_DIRECTION_CHANGE_WAIT_TIME in config:
|
||||||
|
cg.add(
|
||||||
|
var.set_direction_change_waittime(config[CONF_DIRECTION_CHANGE_WAIT_TIME])
|
||||||
|
)
|
||||||
|
cg.add(var.set_acceleration_wait_time(config[CONF_ACCELERATION_WAIT_TIME]))
|
||||||
|
cg.add(var.set_obstacle_rollback(config[CONF_OBSTACLE_ROLLBACK]))
|
445
esphome/components/feedback/feedback_cover.cpp
Normal file
445
esphome/components/feedback/feedback_cover.cpp
Normal file
|
@ -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
|
90
esphome/components/feedback/feedback_cover.h
Normal file
90
esphome/components/feedback/feedback_cover.h
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
|
||||||
|
#ifdef USE_BINARY_SENSOR
|
||||||
|
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||||
|
#endif
|
||||||
|
#include "esphome/components/cover/cover.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace feedback {
|
||||||
|
|
||||||
|
class FeedbackCover : public cover::Cover, public Component {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void loop() override;
|
||||||
|
void dump_config() override;
|
||||||
|
float get_setup_priority() const override { return setup_priority::DATA; };
|
||||||
|
|
||||||
|
Trigger<> *get_open_trigger() const { return this->open_trigger_; }
|
||||||
|
Trigger<> *get_close_trigger() const { return this->close_trigger_; }
|
||||||
|
Trigger<> *get_stop_trigger() const { return this->stop_trigger_; }
|
||||||
|
|
||||||
|
#ifdef USE_BINARY_SENSOR
|
||||||
|
void set_open_endstop(binary_sensor::BinarySensor *open_endstop);
|
||||||
|
void set_open_sensor(binary_sensor::BinarySensor *open_feedback);
|
||||||
|
void set_open_obstacle_sensor(binary_sensor::BinarySensor *open_obstacle);
|
||||||
|
void set_close_endstop(binary_sensor::BinarySensor *close_endstop);
|
||||||
|
void set_close_sensor(binary_sensor::BinarySensor *close_feedback);
|
||||||
|
void set_close_obstacle_sensor(binary_sensor::BinarySensor *close_obstacle);
|
||||||
|
#endif
|
||||||
|
void set_open_duration(uint32_t duration) { this->open_duration_ = duration; }
|
||||||
|
void set_close_duration(uint32_t duration) { this->close_duration_ = duration; }
|
||||||
|
void set_has_built_in_endstop(bool value) { this->has_built_in_endstop_ = value; }
|
||||||
|
void set_assumed_state(bool value) { this->assumed_state_ = value; }
|
||||||
|
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_update_interval(uint32_t interval) { this->update_interval_ = interval; }
|
||||||
|
void set_infer_endstop(bool infer_endstop) { this->infer_endstop_ = infer_endstop; }
|
||||||
|
void set_direction_change_waittime(uint32_t waittime) { this->direction_change_waittime_ = waittime; }
|
||||||
|
void set_acceleration_wait_time(uint32_t waittime) { this->acceleration_wait_time_ = waittime; }
|
||||||
|
|
||||||
|
cover::CoverTraits get_traits() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void control(const cover::CoverCall &call) override;
|
||||||
|
void stop_prev_trigger_();
|
||||||
|
bool is_at_target_() const;
|
||||||
|
void start_direction_(cover::CoverOperation dir);
|
||||||
|
void update_operation_(cover::CoverOperation dir);
|
||||||
|
void endstop_reached_(bool open_endstop);
|
||||||
|
void recompute_position_();
|
||||||
|
void set_current_operation_(cover::CoverOperation operation, bool is_triggered);
|
||||||
|
|
||||||
|
#ifdef USE_BINARY_SENSOR
|
||||||
|
binary_sensor::BinarySensor *open_endstop_{nullptr};
|
||||||
|
binary_sensor::BinarySensor *close_endstop_{nullptr};
|
||||||
|
binary_sensor::BinarySensor *open_feedback_{nullptr};
|
||||||
|
binary_sensor::BinarySensor *close_feedback_{nullptr};
|
||||||
|
binary_sensor::BinarySensor *open_obstacle_{nullptr};
|
||||||
|
binary_sensor::BinarySensor *close_obstacle_{nullptr};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
Trigger<> *open_trigger_{new Trigger<>()};
|
||||||
|
Trigger<> *close_trigger_{new Trigger<>()};
|
||||||
|
Trigger<> *stop_trigger_{new Trigger<>()};
|
||||||
|
|
||||||
|
uint32_t open_duration_{0};
|
||||||
|
uint32_t close_duration_{0};
|
||||||
|
uint32_t max_duration_{UINT32_MAX};
|
||||||
|
optional<uint32_t> direction_change_waittime_{};
|
||||||
|
uint32_t acceleration_wait_time_{0};
|
||||||
|
bool has_built_in_endstop_{false};
|
||||||
|
bool assumed_state_{false};
|
||||||
|
bool infer_endstop_{false};
|
||||||
|
float obstacle_rollback_{0};
|
||||||
|
|
||||||
|
cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING};
|
||||||
|
cover::CoverOperation current_trigger_operation_{cover::COVER_OPERATION_IDLE};
|
||||||
|
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};
|
||||||
|
uint32_t update_interval_{1000};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace feedback
|
||||||
|
} // namespace esphome
|
|
@ -1141,7 +1141,7 @@ sensor:
|
||||||
- platform: max9611
|
- platform: max9611
|
||||||
i2c_id: i2c_bus
|
i2c_id: i2c_bus
|
||||||
shunt_resistance: 0.2 ohm
|
shunt_resistance: 0.2 ohm
|
||||||
gain: '1X'
|
gain: "1X"
|
||||||
voltage:
|
voltage:
|
||||||
name: Max9611 Voltage
|
name: Max9611 Voltage
|
||||||
current:
|
current:
|
||||||
|
@ -1152,7 +1152,6 @@ sensor:
|
||||||
name: Max9611 Temp
|
name: Max9611 Temp
|
||||||
update_interval: 1s
|
update_interval: 1s
|
||||||
|
|
||||||
|
|
||||||
esp32_touch:
|
esp32_touch:
|
||||||
setup_mode: False
|
setup_mode: False
|
||||||
iir_filter: 10ms
|
iir_filter: 10ms
|
||||||
|
@ -1383,6 +1382,19 @@ binary_sensor:
|
||||||
threshold: 100
|
threshold: 100
|
||||||
filters:
|
filters:
|
||||||
- invert:
|
- invert:
|
||||||
|
- platform: template
|
||||||
|
id: open_endstop_sensor
|
||||||
|
- platform: template
|
||||||
|
id: open_sensor
|
||||||
|
- platform: template
|
||||||
|
id: open_obstacle_sensor
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
id: close_endstop_sensor
|
||||||
|
- platform: template
|
||||||
|
id: close_sensor
|
||||||
|
- platform: template
|
||||||
|
id: close_obstacle_sensor
|
||||||
|
|
||||||
pca9685:
|
pca9685:
|
||||||
frequency: 500
|
frequency: 500
|
||||||
|
@ -2546,6 +2558,36 @@ cover:
|
||||||
id: am43_test
|
id: am43_test
|
||||||
ble_client_id: ble_foo
|
ble_client_id: ble_foo
|
||||||
icon: mdi:blinds
|
icon: mdi:blinds
|
||||||
|
- platform: feedback
|
||||||
|
name: "Feedback Cover"
|
||||||
|
id: gate
|
||||||
|
device_class: gate
|
||||||
|
|
||||||
|
infer_endstop_from_movement: false
|
||||||
|
has_built_in_endstop: false
|
||||||
|
max_duration: 30s
|
||||||
|
direction_change_wait_time: 300ms
|
||||||
|
acceleration_wait_time: 150ms
|
||||||
|
obstacle_rollback: 10%
|
||||||
|
|
||||||
|
open_duration: 22.1s
|
||||||
|
open_endstop: open_endstop_sensor
|
||||||
|
open_sensor: open_sensor
|
||||||
|
open_obstacle_sensor: open_obstacle_sensor
|
||||||
|
|
||||||
|
close_duration: 22.4s
|
||||||
|
close_endstop: close_endstop_sensor
|
||||||
|
close_sensor: close_sensor
|
||||||
|
close_obstacle_sensor: close_obstacle_sensor
|
||||||
|
|
||||||
|
open_action:
|
||||||
|
- logger.log: Open Action
|
||||||
|
|
||||||
|
close_action:
|
||||||
|
- logger.log: Close Action
|
||||||
|
|
||||||
|
stop_action:
|
||||||
|
- logger.log: Stop Action
|
||||||
|
|
||||||
debug:
|
debug:
|
||||||
|
|
||||||
|
@ -2617,10 +2659,10 @@ globals:
|
||||||
text_sensor:
|
text_sensor:
|
||||||
- platform: ble_client
|
- platform: ble_client
|
||||||
ble_client_id: ble_foo
|
ble_client_id: ble_foo
|
||||||
name: 'Sensor Location'
|
name: "Sensor Location"
|
||||||
service_uuid: '180d'
|
service_uuid: "180d"
|
||||||
characteristic_uuid: '2a38'
|
characteristic_uuid: "2a38"
|
||||||
descriptor_uuid: '2902'
|
descriptor_uuid: "2902"
|
||||||
notify: true
|
notify: true
|
||||||
update_interval: never
|
update_interval: never
|
||||||
on_notify:
|
on_notify:
|
||||||
|
@ -2712,7 +2754,7 @@ canbus:
|
||||||
lambda: "return x[0] == 0x11;"
|
lambda: "return x[0] == 0x11;"
|
||||||
then:
|
then:
|
||||||
light.toggle: ${roomname}_lights
|
light.toggle: ${roomname}_lights
|
||||||
- can_id: 0b00000000000000000000001000000
|
- can_id: 0b00000000000000000000001000000
|
||||||
can_id_mask: 0b11111000000000011111111000000
|
can_id_mask: 0b11111000000000011111111000000
|
||||||
use_extended_id: true
|
use_extended_id: true
|
||||||
then:
|
then:
|
||||||
|
@ -2750,7 +2792,7 @@ canbus:
|
||||||
lambda: "return x[0] == 0x11;"
|
lambda: "return x[0] == 0x11;"
|
||||||
then:
|
then:
|
||||||
light.toggle: ${roomname}_lights
|
light.toggle: ${roomname}_lights
|
||||||
- can_id: 0b00000000000000000000001000000
|
- can_id: 0b00000000000000000000001000000
|
||||||
can_id_mask: 0b11111000000000011111111000000
|
can_id_mask: 0b11111000000000011111111000000
|
||||||
use_extended_id: true
|
use_extended_id: true
|
||||||
then:
|
then:
|
||||||
|
|
Loading…
Reference in a new issue