ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
feedback_cover.cpp
Go to the documentation of this file.
1#include "feedback_cover.h"
2#include "esphome/core/hal.h"
3#include "esphome/core/log.h"
5
7
8ESPHOME_LOG_TAG(TAG, "feedback.cover");
9
10static constexpr uint32_t DIRECTION_CHANGE_TIMEOUT_ID = 1;
11
12using namespace esphome::cover;
13
15 auto restore = this->restore_state_();
16
17 if (restore.has_value()) {
18 restore->apply(this);
19 } else {
20 // if no other information, assume half open
21 this->position = 0.5f;
22 }
24
25#ifdef USE_BINARY_SENSOR
26 // if available, get position from endstop sensors
27 if (this->open_endstop_ != nullptr && this->open_endstop_->state) {
28 this->position = COVER_OPEN;
29 } else if (this->close_endstop_ != nullptr && this->close_endstop_->state) {
30 this->position = COVER_CLOSED;
31 }
32
33 // if available, get moving state from sensors
34 if (this->open_feedback_ != nullptr && this->open_feedback_->state) {
36 } else if (this->close_feedback_ != nullptr && this->close_feedback_->state) {
38 }
39#endif
40
42}
43
45 auto traits = CoverTraits();
46 traits.set_supports_stop(true);
47 traits.set_supports_position(true);
48 traits.set_supports_toggle(true);
49 traits.set_is_assumed_state(this->assumed_state_);
50 return traits;
51}
52
54 LOG_COVER("", "Endstop Cover", this);
55 ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f);
56#ifdef USE_BINARY_SENSOR
57 LOG_BINARY_SENSOR(" ", "Open Endstop", this->open_endstop_);
58 LOG_BINARY_SENSOR(" ", "Open Feedback", this->open_feedback_);
59 LOG_BINARY_SENSOR(" ", "Open Obstacle", this->open_obstacle_);
60#endif
61 ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f);
62#ifdef USE_BINARY_SENSOR
63 LOG_BINARY_SENSOR(" ", "Close Endstop", this->close_endstop_);
64 LOG_BINARY_SENSOR(" ", "Close Feedback", this->close_feedback_);
65 LOG_BINARY_SENSOR(" ", "Close Obstacle", this->close_obstacle_);
66#endif
67 if (this->has_built_in_endstop_) {
68 ESP_LOGCONFIG(TAG, " Has builtin endstop: YES");
69 }
70 if (this->infer_endstop_) {
71 ESP_LOGCONFIG(TAG, " Infer endstop from movement: YES");
72 }
73 if (this->max_duration_ < UINT32_MAX) {
74 ESP_LOGCONFIG(TAG, " Max Duration: %.1fs", this->max_duration_ / 1e3f);
75 }
76 if (this->direction_change_waittime_.has_value()) {
77 ESP_LOGCONFIG(TAG, " Direction change wait time: %.1fs", *this->direction_change_waittime_ / 1e3f);
78 }
79 if (this->acceleration_wait_time_) {
80 ESP_LOGCONFIG(TAG, " Acceleration wait time: %.1fs", this->acceleration_wait_time_ / 1e3f);
81 }
82#ifdef USE_FEEDBACK_COVER_OVERSHOOT
83 if (this->overshoot_duration_) {
84 ESP_LOGCONFIG(TAG, " Overshoot duration: %.1fs", this->overshoot_duration_ / 1e3f);
85 }
86#endif
87#ifdef USE_BINARY_SENSOR
88 if (this->obstacle_rollback_ && (this->open_obstacle_ != nullptr || this->close_obstacle_ != nullptr)) {
89 ESP_LOGCONFIG(TAG, " Obstacle rollback: %.1f%%", this->obstacle_rollback_ * 100);
90 }
91#endif
92}
93
94#ifdef USE_BINARY_SENSOR
95
97 this->open_feedback_ = open_feedback;
98
99 // setup callbacks to react to sensor changes
100 open_feedback->add_on_state_callback([this](bool state) {
101 ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", LOG_STR_ARG(this->get_log_name()),
102 state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
103 this->recompute_position_();
104 if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_OPENING) {
105 this->endstop_reached_(true);
106 }
108 });
109}
110
112 this->close_feedback_ = close_feedback;
113
114 close_feedback->add_on_state_callback([this](bool state) {
115 ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", LOG_STR_ARG(this->get_log_name()),
116 state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
117 this->recompute_position_();
118 if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) {
119 this->endstop_reached_(false);
120 }
121
123 });
124}
125
127 this->open_endstop_ = open_endstop;
128 open_endstop->add_on_state_callback([this](bool state) {
129 if (state) {
130 this->endstop_reached_(true);
131 }
132 });
133}
134
136 this->close_endstop_ = close_endstop;
137 close_endstop->add_on_state_callback([this](bool state) {
138 if (state) {
139 this->endstop_reached_(false);
140 }
141 });
142}
143#endif
144
145void FeedbackCover::endstop_reached_(bool open_endstop) {
147
148 this->position = open_endstop ? COVER_OPEN : COVER_CLOSED;
149
150 // only act if endstop activated while moving in the right direction, in case we are coming back
151 // from a position slightly past the endpoint
153 float dur = (now - this->start_dir_time_) / 1e3f;
154 ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", LOG_STR_ARG(this->get_log_name()),
155 open_endstop ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"), dur);
156
157 // if there is no external mechanism, stop the cover
158 if (!this->has_built_in_endstop_) {
160 } else {
162 }
163 }
164
165 // always sync position and publish
166 this->publish_state();
167 this->last_publish_time_ = now;
168}
169
171 if (is_triggered) {
172 this->current_trigger_operation_ = operation;
173 }
174
175 // if it is setting the actual operation (not triggered one) or
176 // if we don't have moving sensor, we operate in optimistic mode, assuming actions take place immediately
177 // thus, triggered operation always sets current operation.
178 // otherwise, current operation comes from sensor, and may differ from requested operation
179 // this might be from delays or complex actions, or because the movement was not trigger by the component
180 // but initiated externally
181
182#ifdef USE_BINARY_SENSOR
183 if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr))
184#endif
185 {
187 this->current_operation = operation;
188 this->start_dir_time_ = this->last_recompute_time_ = now;
189 this->publish_state();
190 this->last_publish_time_ = now;
191 }
192}
193
194#ifdef USE_BINARY_SENSOR
196 this->close_obstacle_ = close_obstacle;
197
198 close_obstacle->add_on_state_callback([this](bool state) {
201 ESP_LOGD(TAG, "'%s' - Close obstacle detected.", LOG_STR_ARG(this->get_log_name()));
203
204 if (this->obstacle_rollback_) {
205 this->target_position_ = clamp(this->position + this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN);
207 }
208 }
209 });
210}
211
213 this->open_obstacle_ = open_obstacle;
214
215 open_obstacle->add_on_state_callback([this](bool state) {
218 ESP_LOGD(TAG, "'%s' - Open obstacle detected.", LOG_STR_ARG(this->get_log_name()));
220
221 if (this->obstacle_rollback_) {
222 this->target_position_ = clamp(this->position - this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN);
224 }
225 }
226 });
227}
228#endif
229
232 return;
234
235 // Recompute position every loop cycle
236 this->recompute_position_();
237
238 // if we initiated the move, check if we reached position or max time
239 // (stoping from endstop sensor is handled in callback)
241 if (this->is_at_target_()) {
242 const bool at_end = this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED;
243 if (this->has_built_in_endstop_ && at_end) {
244 // Don't trigger stop, let the cover stop by itself.
246#ifdef USE_FEEDBACK_COVER_OVERSHOOT
247 } else if (at_end && this->overshoot_duration_) {
248 // The define is global, so a cover without the option still lands here with a zero duration
249 // Keep driving past the end position for a while so accumulated timing error is squeezed out
250 if (!this->start_overshoot_time_) {
251 this->start_overshoot_time_ = now;
252 } else if (now - this->start_overshoot_time_ > this->overshoot_duration_ ||
253 now - this->start_dir_time_ > this->max_duration_) {
254 ESP_LOGD(TAG, "'%s' - Overshoot duration reached. Stopping cover.", LOG_STR_ARG(this->get_log_name()));
256 }
257#endif
258 } else {
260 }
261 } else if (now - this->start_dir_time_ > this->max_duration_) {
262 ESP_LOGD(TAG, "'%s' - Max duration reached. Stopping cover.", LOG_STR_ARG(this->get_log_name()));
264 }
265 }
266
267 // update current position at requested interval, regardless of who started the movement
268 // so that we also update UI if there was an external movement
269 // don't save intermediate positions
270 if (now - this->last_publish_time_ > this->update_interval_) {
271 this->publish_state(false);
272 this->last_publish_time_ = now;
273 }
274}
275
277 // stop action logic
278 if (call.get_stop()) {
280 } else if (call.get_toggle().has_value()) {
281 // toggle action logic: OPEN - STOP - CLOSE
284 } else {
285 if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) {
286 this->target_position_ = COVER_OPEN;
288 } else {
289 this->target_position_ = COVER_CLOSED;
291 }
292 }
293 } else {
294 auto pos_opt = call.get_position();
295 if (!pos_opt.has_value())
296 return;
297 // go to position action
298 auto pos = *pos_opt;
299 if (pos == this->position) {
300 // already at target,
301
302 // for covers with built in end stop, if we don't have sensors we should send the command again
303 // to make sure the assumed state is not wrong
304 if (this->has_built_in_endstop_ && ((pos == COVER_OPEN
305#ifdef USE_BINARY_SENSOR
306 && this->open_endstop_ == nullptr
307#endif
308 && !this->infer_endstop_) ||
309 (pos == COVER_CLOSED
310#ifdef USE_BINARY_SENSOR
311 && this->close_endstop_ == nullptr
312#endif
313 && !this->infer_endstop_))) {
314 this->target_position_ = pos;
316 } else if (this->current_operation != COVER_OPERATION_IDLE ||
318 // if we are moving, stop
320 }
321 } else {
322 this->target_position_ = pos;
324 }
325 }
326}
327
329 if (this->direction_change_waittime_.has_value()) {
330 this->cancel_timeout(DIRECTION_CHANGE_TIMEOUT_ID);
331 }
332 if (this->prev_command_trigger_ != nullptr) {
334 this->prev_command_trigger_ = nullptr;
335 }
336}
337
339 // if initiated externally, current operation might be different from
340 // operation that was triggered, thus evaluate position against what was asked
341
342 switch (this->current_trigger_operation_) {
344 return this->position >= this->target_position_;
346 return this->position <= this->target_position_;
349 default:
350 return true;
351 }
352}
354 Trigger<> *trig;
355
356#ifdef USE_BINARY_SENSOR
357 binary_sensor::BinarySensor *obstacle{nullptr};
358#endif
359
360#ifdef USE_FEEDBACK_COVER_OVERSHOOT
361 // Any new command ends a running overshoot
362 this->start_overshoot_time_ = 0;
363#endif
364
365 switch (dir) {
367 trig = &this->stop_trigger_;
368 break;
370 this->last_operation_ = dir;
371 trig = &this->open_trigger_;
372#ifdef USE_BINARY_SENSOR
373 obstacle = this->open_obstacle_;
374#endif
375 break;
377 this->last_operation_ = dir;
378 trig = &this->close_trigger_;
379#ifdef USE_BINARY_SENSOR
380 obstacle = this->close_obstacle_;
381#endif
382 break;
383 default:
384 return;
385 }
386
387 this->stop_prev_trigger_();
388
389#ifdef USE_BINARY_SENSOR
390 // check if there is an obstacle to start the new operation -> abort without any change
391 // the case when an obstacle appears while moving is handled in the callback
392 if (obstacle != nullptr && obstacle->state) {
393 ESP_LOGD(TAG, "'%s' - %s obstacle detected. Action not started.", LOG_STR_ARG(this->get_log_name()),
394 dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"));
395 return;
396 }
397#endif
398
399 // if we are moving and need to move in the opposite direction
400 // check if we have a wait time
401 if (this->direction_change_waittime_.has_value() && dir != COVER_OPERATION_IDLE &&
402 this->current_operation != COVER_OPERATION_IDLE && dir != this->current_operation) {
403 const uint32_t waittime = *this->direction_change_waittime_;
404 ESP_LOGD(TAG, "'%s' - Reversing direction.", LOG_STR_ARG(this->get_log_name()));
406 this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, waittime, [this, dir]() { this->start_direction_(dir); });
407 } else {
408 this->set_current_operation_(dir, true);
409 this->prev_command_trigger_ = trig;
410 ESP_LOGD(TAG, "'%s' - Firing '%s' trigger.", LOG_STR_ARG(this->get_log_name()),
411 dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("OPEN")
412 : dir == COVER_OPERATION_CLOSING ? LOG_STR_LITERAL("CLOSE")
413 : LOG_STR_LITERAL("STOP"));
414 trig->trigger();
415 }
416}
417
420 return;
421
423 float dir;
424 float action_dur;
425 float min_pos;
426 float max_pos;
427
428 // endstop sensors update position from their callbacks, and sets the fully open/close value
429 // If we have endstop, estimation never reaches the fully open/closed state.
430 // but if movement continues past corresponding endstop (inertia), keep the fully open/close state
431
432 switch (this->current_operation) {
434 dir = 1.0f;
435 action_dur = this->open_duration_;
436 min_pos = COVER_CLOSED;
437 max_pos = (
438#ifdef USE_BINARY_SENSOR
439 this->open_endstop_ != nullptr ||
440#endif
441 this->infer_endstop_) &&
442 this->position < COVER_OPEN
443 ? 0.99f
444 : COVER_OPEN;
445 break;
447 dir = -1.0f;
448 action_dur = this->close_duration_;
449 min_pos = (
450#ifdef USE_BINARY_SENSOR
451 this->close_endstop_ != nullptr ||
452#endif
453 this->infer_endstop_) &&
454 this->position > COVER_CLOSED
455 ? 0.01f
456 : COVER_CLOSED;
457 max_pos = COVER_OPEN;
458 break;
459 default:
460 return;
461 }
462
463 // check if we have an acceleration_wait_time, and remove from position computation
464 if (now - this->start_dir_time_ > this->acceleration_wait_time_) {
465 uint32_t accel_end_time = this->start_dir_time_ + this->acceleration_wait_time_;
466 uint32_t effective_start;
467 if (static_cast<int32_t>(accel_end_time - this->last_recompute_time_) >= 0) {
468 effective_start = accel_end_time;
469 } else {
470 effective_start = this->last_recompute_time_;
471 }
472 this->position += dir * (now - effective_start) / (action_dur - this->acceleration_wait_time_);
473 this->position = clamp(this->position, min_pos, max_pos);
474 }
475 this->last_recompute_time_ = now;
476}
477
478} // namespace esphome::feedback
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
bool cancel_timeout(const char *name)
Cancel a timeout function.
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
const LogString * get_log_name() const
Get the name for a "%s" log argument (LOG_STR_ARG).
void add_on_state_callback(F &&callback)
void stop_action()
Stop any action connected to this trigger.
Definition automation.h:564
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Inform the parent automation that the event has triggered.
Definition automation.h:556
Base class for all binary_sensor-type classes.
bool state
The current state of this binary sensor. Also used as the backing storage for StatefulEntityBase.
const optional< bool > & get_toggle() const
Definition cover.cpp:101
CoverOperation current_operation
The current operation of the cover (idle, opening, closing).
Definition cover.h:120
optional< CoverRestoreState > restore_state_()
Definition cover.cpp:195
void publish_state(bool save=true)
Publish the current state of the cover.
Definition cover.cpp:158
float position
The position of the cover from 0.0 (fully closed) to 1.0 (fully open).
Definition cover.h:126
cover::CoverTraits get_traits() override
cover::CoverOperation current_trigger_operation_
binary_sensor::BinarySensor * open_endstop_
void start_direction_(cover::CoverOperation dir)
void set_open_obstacle_sensor(binary_sensor::BinarySensor *open_obstacle)
void set_open_sensor(binary_sensor::BinarySensor *open_feedback)
void set_current_operation_(cover::CoverOperation operation, bool is_triggered)
binary_sensor::BinarySensor * close_endstop_
void set_close_endstop(binary_sensor::BinarySensor *close_endstop)
optional< uint32_t > direction_change_waittime_
binary_sensor::BinarySensor * close_obstacle_
void set_close_obstacle_sensor(binary_sensor::BinarySensor *close_obstacle)
void control(const cover::CoverCall &call) override
binary_sensor::BinarySensor * open_feedback_
void endstop_reached_(bool open_endstop)
void set_open_endstop(binary_sensor::BinarySensor *open_endstop)
cover::CoverOperation last_operation_
binary_sensor::BinarySensor * open_obstacle_
binary_sensor::BinarySensor * close_feedback_
void set_close_sensor(binary_sensor::BinarySensor *close_feedback)
bool state
Definition fan.h:2
ESPHOME_LOG_TAG(TAG, "cover")
CoverOperation
Enum encoding the current operation of a cover.
Definition cover.h:84
@ COVER_OPERATION_OPENING
The cover is currently opening.
Definition cover.h:88
@ COVER_OPERATION_CLOSING
The cover is currently closing.
Definition cover.h:90
@ COVER_OPERATION_IDLE
The cover is currently idle (not moving)
Definition cover.h:86
size_t size_t pos
Definition helpers.h:1255
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t