ESPHome 2026.10.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
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(),
97 state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
98 this->recompute_position_();
100 this->endstop_reached_(true);
101 }
103 });
104}
105
107 this->close_feedback_ = close_feedback;
108
109 close_feedback->add_on_state_callback([this](bool state) {
110 ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(),
111 state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
112 this->recompute_position_();
113 if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) {
114 this->endstop_reached_(false);
115 }
116
118 });
119}
120
122 this->open_endstop_ = open_endstop;
123 open_endstop->add_on_state_callback([this](bool state) {
124 if (state) {
125 this->endstop_reached_(true);
126 }
127 });
128}
129
131 this->close_endstop_ = close_endstop;
132 close_endstop->add_on_state_callback([this](bool state) {
133 if (state) {
134 this->endstop_reached_(false);
135 }
136 });
137}
138#endif
139
140void FeedbackCover::endstop_reached_(bool open_endstop) {
142
143 this->position = open_endstop ? COVER_OPEN : COVER_CLOSED;
144
145 // only act if endstop activated while moving in the right direction, in case we are coming back
146 // from a position slightly past the endpoint
148 float dur = (now - this->start_dir_time_) / 1e3f;
149 ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(),
150 open_endstop ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"), dur);
151
152 // if there is no external mechanism, stop the cover
153 if (!this->has_built_in_endstop_) {
155 } else {
157 }
158 }
159
160 // always sync position and publish
161 this->publish_state();
162 this->last_publish_time_ = now;
163}
164
166 if (is_triggered) {
167 this->current_trigger_operation_ = operation;
168 }
169
170 // if it is setting the actual operation (not triggered one) or
171 // if we don't have moving sensor, we operate in optimistic mode, assuming actions take place immediately
172 // thus, triggered operation always sets current operation.
173 // otherwise, current operation comes from sensor, and may differ from requested operation
174 // this might be from delays or complex actions, or because the movement was not trigger by the component
175 // but initiated externally
176
177#ifdef USE_BINARY_SENSOR
178 if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr))
179#endif
180 {
182 this->current_operation = operation;
183 this->start_dir_time_ = this->last_recompute_time_ = now;
184 this->publish_state();
185 this->last_publish_time_ = now;
186 }
187}
188
189#ifdef USE_BINARY_SENSOR
191 this->close_obstacle_ = close_obstacle;
192
193 close_obstacle->add_on_state_callback([this](bool state) {
196 ESP_LOGD(TAG, "'%s' - Close obstacle detected.", this->name_.c_str());
198
199 if (this->obstacle_rollback_) {
200 this->target_position_ = clamp(this->position + this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN);
202 }
203 }
204 });
205}
206
208 this->open_obstacle_ = open_obstacle;
209
210 open_obstacle->add_on_state_callback([this](bool state) {
213 ESP_LOGD(TAG, "'%s' - Open obstacle detected.", this->name_.c_str());
215
216 if (this->obstacle_rollback_) {
217 this->target_position_ = clamp(this->position - this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN);
219 }
220 }
221 });
222}
223#endif
224
227 return;
229
230 // Recompute position every loop cycle
231 this->recompute_position_();
232
233 // if we initiated the move, check if we reached position or max time
234 // (stoping from endstop sensor is handled in callback)
236 if (this->is_at_target_()) {
237 if (this->has_built_in_endstop_ &&
238 (this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED)) {
239 // Don't trigger stop, let the cover stop by itself.
241 } else {
243 }
244 } else if (now - this->start_dir_time_ > this->max_duration_) {
245 ESP_LOGD(TAG, "'%s' - Max duration reached. Stopping cover.", this->name_.c_str());
247 }
248 }
249
250 // update current position at requested interval, regardless of who started the movement
251 // so that we also update UI if there was an external movement
252 // don't save intermediate positions
253 if (now - this->last_publish_time_ > this->update_interval_) {
254 this->publish_state(false);
255 this->last_publish_time_ = now;
256 }
257}
258
260 // stop action logic
261 if (call.get_stop()) {
263 } else if (call.get_toggle().has_value()) {
264 // toggle action logic: OPEN - STOP - CLOSE
267 } else {
268 if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) {
269 this->target_position_ = COVER_OPEN;
271 } else {
272 this->target_position_ = COVER_CLOSED;
274 }
275 }
276 } else {
277 auto pos_opt = call.get_position();
278 if (!pos_opt.has_value())
279 return;
280 // go to position action
281 auto pos = *pos_opt;
282 if (pos == this->position) {
283 // already at target,
284
285 // for covers with built in end stop, if we don't have sensors we should send the command again
286 // to make sure the assumed state is not wrong
287 if (this->has_built_in_endstop_ && ((pos == COVER_OPEN
288#ifdef USE_BINARY_SENSOR
289 && this->open_endstop_ == nullptr
290#endif
291 && !this->infer_endstop_) ||
292 (pos == COVER_CLOSED
293#ifdef USE_BINARY_SENSOR
294 && this->close_endstop_ == nullptr
295#endif
296 && !this->infer_endstop_))) {
297 this->target_position_ = pos;
299 } else if (this->current_operation != COVER_OPERATION_IDLE ||
301 // if we are moving, stop
303 }
304 } else {
305 this->target_position_ = pos;
307 }
308 }
309}
310
312 if (this->direction_change_waittime_.has_value()) {
313 this->cancel_timeout(DIRECTION_CHANGE_TIMEOUT_ID);
314 }
315 if (this->prev_command_trigger_ != nullptr) {
317 this->prev_command_trigger_ = nullptr;
318 }
319}
320
322 // if initiated externally, current operation might be different from
323 // operation that was triggered, thus evaluate position against what was asked
324
325 switch (this->current_trigger_operation_) {
327 return this->position >= this->target_position_;
329 return this->position <= this->target_position_;
332 default:
333 return true;
334 }
335}
337 Trigger<> *trig;
338
339#ifdef USE_BINARY_SENSOR
340 binary_sensor::BinarySensor *obstacle{nullptr};
341#endif
342
343 switch (dir) {
345 trig = &this->stop_trigger_;
346 break;
348 this->last_operation_ = dir;
349 trig = &this->open_trigger_;
350#ifdef USE_BINARY_SENSOR
351 obstacle = this->open_obstacle_;
352#endif
353 break;
355 this->last_operation_ = dir;
356 trig = &this->close_trigger_;
357#ifdef USE_BINARY_SENSOR
358 obstacle = this->close_obstacle_;
359#endif
360 break;
361 default:
362 return;
363 }
364
365 this->stop_prev_trigger_();
366
367#ifdef USE_BINARY_SENSOR
368 // check if there is an obstacle to start the new operation -> abort without any change
369 // the case when an obstacle appears while moving is handled in the callback
370 if (obstacle != nullptr && obstacle->state) {
371 ESP_LOGD(TAG, "'%s' - %s obstacle detected. Action not started.", this->name_.c_str(),
372 dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"));
373 return;
374 }
375#endif
376
377 // if we are moving and need to move in the opposite direction
378 // check if we have a wait time
379 if (this->direction_change_waittime_.has_value() && dir != COVER_OPERATION_IDLE &&
380 this->current_operation != COVER_OPERATION_IDLE && dir != this->current_operation) {
381 const uint32_t waittime = *this->direction_change_waittime_;
382 ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str());
384 this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, waittime, [this, dir]() { this->start_direction_(dir); });
385 } else {
386 this->set_current_operation_(dir, true);
387 this->prev_command_trigger_ = trig;
388 ESP_LOGD(TAG, "'%s' - Firing '%s' trigger.", this->name_.c_str(),
389 dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("OPEN")
390 : dir == COVER_OPERATION_CLOSING ? LOG_STR_LITERAL("CLOSE")
391 : LOG_STR_LITERAL("STOP"));
392 trig->trigger();
393 }
394}
395
398 return;
399
401 float dir;
402 float action_dur;
403 float min_pos;
404 float max_pos;
405
406 // endstop sensors update position from their callbacks, and sets the fully open/close value
407 // If we have endstop, estimation never reaches the fully open/closed state.
408 // but if movement continues past corresponding endstop (inertia), keep the fully open/close state
409
410 switch (this->current_operation) {
412 dir = 1.0f;
413 action_dur = this->open_duration_;
414 min_pos = COVER_CLOSED;
415 max_pos = (
416#ifdef USE_BINARY_SENSOR
417 this->open_endstop_ != nullptr ||
418#endif
419 this->infer_endstop_) &&
420 this->position < COVER_OPEN
421 ? 0.99f
422 : COVER_OPEN;
423 break;
425 dir = -1.0f;
426 action_dur = this->close_duration_;
427 min_pos = (
428#ifdef USE_BINARY_SENSOR
429 this->close_endstop_ != nullptr ||
430#endif
431 this->infer_endstop_) &&
432 this->position > COVER_CLOSED
433 ? 0.01f
434 : COVER_CLOSED;
435 max_pos = COVER_OPEN;
436 break;
437 default:
438 return;
439 }
440
441 // check if we have an acceleration_wait_time, and remove from position computation
442 if (now - this->start_dir_time_ > this->acceleration_wait_time_) {
443 uint32_t accel_end_time = this->start_dir_time_ + this->acceleration_wait_time_;
444 uint32_t effective_start;
445 if (static_cast<int32_t>(accel_end_time - this->last_recompute_time_) >= 0) {
446 effective_start = accel_end_time;
447 } else {
448 effective_start = this->last_recompute_time_;
449 }
450 this->position += dir * (now - effective_start) / (action_dur - this->acceleration_wait_time_);
451 this->position = clamp(this->position, min_pos, max_pos);
452 }
453 this->last_recompute_time_ = now;
454}
455
456} // 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
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:469
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Inform the parent automation that the event has triggered.
Definition automation.h:461
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:175
void publish_state(bool save=true)
Publish the current state of the cover.
Definition cover.cpp:138
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:1092
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t