ESPHome 2026.5.1
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
8static const char *const 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_BINARY_SENSOR
83 if (this->obstacle_rollback_ && (this->open_obstacle_ != nullptr || this->close_obstacle_ != nullptr)) {
84 ESP_LOGCONFIG(TAG, " Obstacle rollback: %.1f%%", this->obstacle_rollback_ * 100);
85 }
86#endif
87}
88
89#ifdef USE_BINARY_SENSOR
90
92 this->open_feedback_ = open_feedback;
93
94 // setup callbacks to react to sensor changes
95 open_feedback->add_on_state_callback([this](bool state) {
96 ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
97 this->recompute_position_();
99 this->endstop_reached_(true);
100 }
102 });
103}
104
106 this->close_feedback_ = close_feedback;
107
108 close_feedback->add_on_state_callback([this](bool state) {
109 ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
110 this->recompute_position_();
111 if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) {
112 this->endstop_reached_(false);
113 }
114
116 });
117}
118
120 this->open_endstop_ = open_endstop;
121 open_endstop->add_on_state_callback([this](bool state) {
122 if (state) {
123 this->endstop_reached_(true);
124 }
125 });
126}
127
129 this->close_endstop_ = close_endstop;
130 close_endstop->add_on_state_callback([this](bool state) {
131 if (state) {
132 this->endstop_reached_(false);
133 }
134 });
135}
136#endif
137
138void FeedbackCover::endstop_reached_(bool open_endstop) {
140
141 this->position = open_endstop ? COVER_OPEN : COVER_CLOSED;
142
143 // only act if endstop activated while moving in the right direction, in case we are coming back
144 // from a position slightly past the endpoint
146 float dur = (now - this->start_dir_time_) / 1e3f;
147 ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), open_endstop ? "Open" : "Close", dur);
148
149 // if there is no external mechanism, stop the cover
150 if (!this->has_built_in_endstop_) {
152 } else {
154 }
155 }
156
157 // always sync position and publish
158 this->publish_state();
159 this->last_publish_time_ = now;
160}
161
163 if (is_triggered) {
164 this->current_trigger_operation_ = operation;
165 }
166
167 // if it is setting the actual operation (not triggered one) or
168 // if we don't have moving sensor, we operate in optimistic mode, assuming actions take place immediately
169 // thus, triggered operation always sets current operation.
170 // otherwise, current operation comes from sensor, and may differ from requested operation
171 // this might be from delays or complex actions, or because the movement was not trigger by the component
172 // but initiated externally
173
174#ifdef USE_BINARY_SENSOR
175 if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr))
176#endif
177 {
179 this->current_operation = operation;
180 this->start_dir_time_ = this->last_recompute_time_ = now;
181 this->publish_state();
182 this->last_publish_time_ = now;
183 }
184}
185
186#ifdef USE_BINARY_SENSOR
188 this->close_obstacle_ = close_obstacle;
189
190 close_obstacle->add_on_state_callback([this](bool state) {
193 ESP_LOGD(TAG, "'%s' - Close obstacle detected.", this->name_.c_str());
195
196 if (this->obstacle_rollback_) {
197 this->target_position_ = clamp(this->position + this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN);
199 }
200 }
201 });
202}
203
205 this->open_obstacle_ = open_obstacle;
206
207 open_obstacle->add_on_state_callback([this](bool state) {
210 ESP_LOGD(TAG, "'%s' - Open obstacle detected.", this->name_.c_str());
212
213 if (this->obstacle_rollback_) {
214 this->target_position_ = clamp(this->position - this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN);
216 }
217 }
218 });
219}
220#endif
221
224 return;
226
227 // Recompute position every loop cycle
228 this->recompute_position_();
229
230 // if we initiated the move, check if we reached position or max time
231 // (stoping from endstop sensor is handled in callback)
233 if (this->is_at_target_()) {
234 if (this->has_built_in_endstop_ &&
235 (this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED)) {
236 // Don't trigger stop, let the cover stop by itself.
238 } else {
240 }
241 } else if (now - this->start_dir_time_ > this->max_duration_) {
242 ESP_LOGD(TAG, "'%s' - Max duration reached. Stopping cover.", this->name_.c_str());
244 }
245 }
246
247 // update current position at requested interval, regardless of who started the movement
248 // so that we also update UI if there was an external movement
249 // don't save intermediate positions
250 if (now - this->last_publish_time_ > this->update_interval_) {
251 this->publish_state(false);
252 this->last_publish_time_ = now;
253 }
254}
255
257 // stop action logic
258 if (call.get_stop()) {
260 } else if (call.get_toggle().has_value()) {
261 // toggle action logic: OPEN - STOP - CLOSE
264 } else {
265 if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) {
266 this->target_position_ = COVER_OPEN;
268 } else {
269 this->target_position_ = COVER_CLOSED;
271 }
272 }
273 } else {
274 auto pos_opt = call.get_position();
275 if (!pos_opt.has_value())
276 return;
277 // go to position action
278 auto pos = *pos_opt;
279 if (pos == this->position) {
280 // already at target,
281
282 // for covers with built in end stop, if we don't have sensors we should send the command again
283 // to make sure the assumed state is not wrong
284 if (this->has_built_in_endstop_ && ((pos == COVER_OPEN
285#ifdef USE_BINARY_SENSOR
286 && this->open_endstop_ == nullptr
287#endif
288 && !this->infer_endstop_) ||
289 (pos == COVER_CLOSED
290#ifdef USE_BINARY_SENSOR
291 && this->close_endstop_ == nullptr
292#endif
293 && !this->infer_endstop_))) {
294 this->target_position_ = pos;
296 } else if (this->current_operation != COVER_OPERATION_IDLE ||
298 // if we are moving, stop
300 }
301 } else {
302 this->target_position_ = pos;
304 }
305 }
306}
307
309 if (this->direction_change_waittime_.has_value()) {
310 this->cancel_timeout(DIRECTION_CHANGE_TIMEOUT_ID);
311 }
312 if (this->prev_command_trigger_ != nullptr) {
314 this->prev_command_trigger_ = nullptr;
315 }
316}
317
319 // if initiated externally, current operation might be different from
320 // operation that was triggered, thus evaluate position against what was asked
321
322 switch (this->current_trigger_operation_) {
324 return this->position >= this->target_position_;
326 return this->position <= this->target_position_;
329 default:
330 return true;
331 }
332}
334 Trigger<> *trig;
335
336#ifdef USE_BINARY_SENSOR
337 binary_sensor::BinarySensor *obstacle{nullptr};
338#endif
339
340 switch (dir) {
342 trig = &this->stop_trigger_;
343 break;
345 this->last_operation_ = dir;
346 trig = &this->open_trigger_;
347#ifdef USE_BINARY_SENSOR
348 obstacle = this->open_obstacle_;
349#endif
350 break;
352 this->last_operation_ = dir;
353 trig = &this->close_trigger_;
354#ifdef USE_BINARY_SENSOR
355 obstacle = this->close_obstacle_;
356#endif
357 break;
358 default:
359 return;
360 }
361
362 this->stop_prev_trigger_();
363
364#ifdef USE_BINARY_SENSOR
365 // check if there is an obstacle to start the new operation -> abort without any change
366 // the case when an obstacle appears while moving is handled in the callback
367 if (obstacle != nullptr && obstacle->state) {
368 ESP_LOGD(TAG, "'%s' - %s obstacle detected. Action not started.", this->name_.c_str(),
369 dir == COVER_OPERATION_OPENING ? "Open" : "Close");
370 return;
371 }
372#endif
373
374 // if we are moving and need to move in the opposite direction
375 // check if we have a wait time
376 if (this->direction_change_waittime_.has_value() && dir != COVER_OPERATION_IDLE &&
377 this->current_operation != COVER_OPERATION_IDLE && dir != this->current_operation) {
378 const uint32_t waittime = *this->direction_change_waittime_;
379 ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str());
381 this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, waittime, [this, dir]() { this->start_direction_(dir); });
382 } else {
383 this->set_current_operation_(dir, true);
384 this->prev_command_trigger_ = trig;
385 ESP_LOGD(TAG, "'%s' - Firing '%s' trigger.", this->name_.c_str(),
386 dir == COVER_OPERATION_OPENING ? "OPEN"
387 : dir == COVER_OPERATION_CLOSING ? "CLOSE"
388 : "STOP");
389 trig->trigger();
390 }
391}
392
395 return;
396
398 float dir;
399 float action_dur;
400 float min_pos;
401 float max_pos;
402
403 // endstop sensors update position from their callbacks, and sets the fully open/close value
404 // If we have endstop, estimation never reaches the fully open/closed state.
405 // but if movement continues past corresponding endstop (inertia), keep the fully open/close state
406
407 switch (this->current_operation) {
409 dir = 1.0f;
410 action_dur = this->open_duration_;
411 min_pos = COVER_CLOSED;
412 max_pos = (
413#ifdef USE_BINARY_SENSOR
414 this->open_endstop_ != nullptr ||
415#endif
416 this->infer_endstop_) &&
417 this->position < COVER_OPEN
418 ? 0.99f
419 : COVER_OPEN;
420 break;
422 dir = -1.0f;
423 action_dur = this->close_duration_;
424 min_pos = (
425#ifdef USE_BINARY_SENSOR
426 this->close_endstop_ != nullptr ||
427#endif
428 this->infer_endstop_) &&
429 this->position > COVER_CLOSED
430 ? 0.01f
431 : COVER_CLOSED;
432 max_pos = COVER_OPEN;
433 break;
434 default:
435 return;
436 }
437
438 // check if we have an acceleration_wait_time, and remove from position computation
439 if (now - this->start_dir_time_ > this->acceleration_wait_time_) {
440 uint32_t accel_end_time = this->start_dir_time_ + this->acceleration_wait_time_;
441 uint32_t effective_start;
442 if (static_cast<int32_t>(accel_end_time - this->last_recompute_time_) >= 0) {
443 effective_start = accel_end_time;
444 } else {
445 effective_start = this->last_recompute_time_;
446 }
447 this->position += dir * (now - effective_start) / (action_dur - this->acceleration_wait_time_);
448 this->position = clamp(this->position, min_pos, max_pos);
449 }
450 this->last_recompute_time_ = now;
451}
452
453} // 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.
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_timeout(const std voi set_timeout)(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
Definition component.h:510
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_timeout(const std boo cancel_timeout)(const char *name)
Cancel a timeout function.
Definition component.h:532
void add_on_state_callback(F &&callback)
constexpr const char * c_str() const
Definition string_ref.h:73
void stop_action()
Stop any action connected to this trigger.
Definition automation.h:490
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Inform the parent automation that the event has triggered.
Definition automation.h:482
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:94
CoverOperation current_operation
The current operation of the cover (idle, opening, closing).
Definition cover.h:115
optional< CoverRestoreState > restore_state_()
Definition cover.cpp:179
void publish_state(bool save=true)
Publish the current state of the cover.
Definition cover.cpp:142
float position
The position of the cover from 0.0 (fully closed) to 1.0 (fully open).
Definition cover.h:121
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
CoverOperation
Enum encoding the current operation of a cover.
Definition cover.h:79
@ COVER_OPERATION_OPENING
The cover is currently opening.
Definition cover.h:83
@ COVER_OPERATION_CLOSING
The cover is currently closing.
Definition cover.h:85
@ COVER_OPERATION_IDLE
The cover is currently idle (not moving)
Definition cover.h:81
size_t size_t pos
Definition helpers.h:1038
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t