ESPHome 2026.3.3
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
6namespace esphome {
7namespace feedback {
8
9static const char *const TAG = "feedback.cover";
10
11using namespace esphome::cover;
12
14 auto restore = this->restore_state_();
15
16 if (restore.has_value()) {
17 restore->apply(this);
18 } else {
19 // if no other information, assume half open
20 this->position = 0.5f;
21 }
23
24#ifdef USE_BINARY_SENSOR
25 // if available, get position from endstop sensors
26 if (this->open_endstop_ != nullptr && this->open_endstop_->state) {
27 this->position = COVER_OPEN;
28 } else if (this->close_endstop_ != nullptr && this->close_endstop_->state) {
29 this->position = COVER_CLOSED;
30 }
31
32 // if available, get moving state from sensors
33 if (this->open_feedback_ != nullptr && this->open_feedback_->state) {
35 } else if (this->close_feedback_ != nullptr && this->close_feedback_->state) {
37 }
38#endif
39
41}
42
44 auto traits = CoverTraits();
45 traits.set_supports_stop(true);
46 traits.set_supports_position(true);
47 traits.set_supports_toggle(true);
48 traits.set_is_assumed_state(this->assumed_state_);
49 return traits;
50}
51
53 LOG_COVER("", "Endstop Cover", this);
54 ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f);
55#ifdef USE_BINARY_SENSOR
56 LOG_BINARY_SENSOR(" ", "Open Endstop", this->open_endstop_);
57 LOG_BINARY_SENSOR(" ", "Open Feedback", this->open_feedback_);
58 LOG_BINARY_SENSOR(" ", "Open Obstacle", this->open_obstacle_);
59#endif
60 ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f);
61#ifdef USE_BINARY_SENSOR
62 LOG_BINARY_SENSOR(" ", "Close Endstop", this->close_endstop_);
63 LOG_BINARY_SENSOR(" ", "Close Feedback", this->close_feedback_);
64 LOG_BINARY_SENSOR(" ", "Close Obstacle", this->close_obstacle_);
65#endif
66 if (this->has_built_in_endstop_) {
67 ESP_LOGCONFIG(TAG, " Has builtin endstop: YES");
68 }
69 if (this->infer_endstop_) {
70 ESP_LOGCONFIG(TAG, " Infer endstop from movement: YES");
71 }
72 if (this->max_duration_ < UINT32_MAX) {
73 ESP_LOGCONFIG(TAG, " Max Duration: %.1fs", this->max_duration_ / 1e3f);
74 }
75 if (this->direction_change_waittime_.has_value()) {
76 ESP_LOGCONFIG(TAG, " Direction change wait time: %.1fs", *this->direction_change_waittime_ / 1e3f);
77 }
78 if (this->acceleration_wait_time_) {
79 ESP_LOGCONFIG(TAG, " Acceleration wait time: %.1fs", this->acceleration_wait_time_ / 1e3f);
80 }
81#ifdef USE_BINARY_SENSOR
82 if (this->obstacle_rollback_ && (this->open_obstacle_ != nullptr || this->close_obstacle_ != nullptr)) {
83 ESP_LOGCONFIG(TAG, " Obstacle rollback: %.1f%%", this->obstacle_rollback_ * 100);
84 }
85#endif
86}
87
88#ifdef USE_BINARY_SENSOR
89
91 this->open_feedback_ = open_feedback;
92
93 // setup callbacks to react to sensor changes
94 open_feedback->add_on_state_callback([this](bool state) {
95 ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
96 this->recompute_position_();
98 this->endstop_reached_(true);
99 }
101 });
102}
103
105 this->close_feedback_ = close_feedback;
106
107 close_feedback->add_on_state_callback([this](bool state) {
108 ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
109 this->recompute_position_();
110 if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) {
111 this->endstop_reached_(false);
112 }
113
115 });
116}
117
119 this->open_endstop_ = open_endstop;
120 open_endstop->add_on_state_callback([this](bool state) {
121 if (state) {
122 this->endstop_reached_(true);
123 }
124 });
125}
126
128 this->close_endstop_ = close_endstop;
129 close_endstop->add_on_state_callback([this](bool state) {
130 if (state) {
131 this->endstop_reached_(false);
132 }
133 });
134}
135#endif
136
137void FeedbackCover::endstop_reached_(bool open_endstop) {
138 const uint32_t now = millis();
139
140 this->position = open_endstop ? COVER_OPEN : COVER_CLOSED;
141
142 // only act if endstop activated while moving in the right direction, in case we are coming back
143 // from a position slightly past the endpoint
145 float dur = (now - this->start_dir_time_) / 1e3f;
146 ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), open_endstop ? "Open" : "Close", dur);
147
148 // if there is no external mechanism, stop the cover
149 if (!this->has_built_in_endstop_) {
151 } else {
153 }
154 }
155
156 // always sync position and publish
157 this->publish_state();
158 this->last_publish_time_ = now;
159}
160
162 if (is_triggered) {
163 this->current_trigger_operation_ = operation;
164 }
165
166 // if it is setting the actual operation (not triggered one) or
167 // if we don't have moving sensor, we operate in optimistic mode, assuming actions take place immediately
168 // thus, triggered operation always sets current operation.
169 // otherwise, current operation comes from sensor, and may differ from requested operation
170 // this might be from delays or complex actions, or because the movement was not trigger by the component
171 // but initiated externally
172
173#ifdef USE_BINARY_SENSOR
174 if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr))
175#endif
176 {
177 auto now = millis();
178 this->current_operation = operation;
179 this->start_dir_time_ = this->last_recompute_time_ = now;
180 this->publish_state();
181 this->last_publish_time_ = now;
182 }
183}
184
185#ifdef USE_BINARY_SENSOR
187 this->close_obstacle_ = close_obstacle;
188
189 close_obstacle->add_on_state_callback([this](bool state) {
192 ESP_LOGD(TAG, "'%s' - Close obstacle detected.", this->name_.c_str());
194
195 if (this->obstacle_rollback_) {
196 this->target_position_ = clamp(this->position + this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN);
198 }
199 }
200 });
201}
202
204 this->open_obstacle_ = open_obstacle;
205
206 open_obstacle->add_on_state_callback([this](bool state) {
209 ESP_LOGD(TAG, "'%s' - Open obstacle detected.", this->name_.c_str());
211
212 if (this->obstacle_rollback_) {
213 this->target_position_ = clamp(this->position - this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN);
215 }
216 }
217 });
218}
219#endif
220
223 return;
225
226 // Recompute position every loop cycle
227 this->recompute_position_();
228
229 // if we initiated the move, check if we reached position or max time
230 // (stoping from endstop sensor is handled in callback)
232 if (this->is_at_target_()) {
233 if (this->has_built_in_endstop_ &&
234 (this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED)) {
235 // Don't trigger stop, let the cover stop by itself.
237 } else {
239 }
240 } else if (now - this->start_dir_time_ > this->max_duration_) {
241 ESP_LOGD(TAG, "'%s' - Max duration reached. Stopping cover.", this->name_.c_str());
243 }
244 }
245
246 // update current position at requested interval, regardless of who started the movement
247 // so that we also update UI if there was an external movement
248 // don't save intermediate positions
249 if (now - this->last_publish_time_ > this->update_interval_) {
250 this->publish_state(false);
251 this->last_publish_time_ = now;
252 }
253}
254
256 // stop action logic
257 if (call.get_stop()) {
259 } else if (call.get_toggle().has_value()) {
260 // toggle action logic: OPEN - STOP - CLOSE
263 } else {
264 if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) {
265 this->target_position_ = COVER_OPEN;
267 } else {
268 this->target_position_ = COVER_CLOSED;
270 }
271 }
272 } else {
273 auto pos_opt = call.get_position();
274 if (!pos_opt.has_value())
275 return;
276 // go to position action
277 auto pos = *pos_opt;
278 if (pos == this->position) {
279 // already at target,
280
281 // for covers with built in end stop, if we don't have sensors we should send the command again
282 // to make sure the assumed state is not wrong
283 if (this->has_built_in_endstop_ && ((pos == COVER_OPEN
284#ifdef USE_BINARY_SENSOR
285 && this->open_endstop_ == nullptr
286#endif
287 && !this->infer_endstop_) ||
288 (pos == COVER_CLOSED
289#ifdef USE_BINARY_SENSOR
290 && this->close_endstop_ == nullptr
291#endif
292 && !this->infer_endstop_))) {
293 this->target_position_ = pos;
295 } else if (this->current_operation != COVER_OPERATION_IDLE ||
297 // if we are moving, stop
299 }
300 } else {
301 this->target_position_ = pos;
303 }
304 }
305}
306
308 if (this->direction_change_waittime_.has_value()) {
309 this->cancel_timeout("direction_change");
310 }
311 if (this->prev_command_trigger_ != nullptr) {
313 this->prev_command_trigger_ = nullptr;
314 }
315}
316
318 // if initiated externally, current operation might be different from
319 // operation that was triggered, thus evaluate position against what was asked
320
321 switch (this->current_trigger_operation_) {
323 return this->position >= this->target_position_;
325 return this->position <= this->target_position_;
328 default:
329 return true;
330 }
331}
333 Trigger<> *trig;
334
335#ifdef USE_BINARY_SENSOR
336 binary_sensor::BinarySensor *obstacle{nullptr};
337#endif
338
339 switch (dir) {
341 trig = &this->stop_trigger_;
342 break;
344 this->last_operation_ = dir;
345 trig = &this->open_trigger_;
346#ifdef USE_BINARY_SENSOR
347 obstacle = this->open_obstacle_;
348#endif
349 break;
351 this->last_operation_ = dir;
352 trig = &this->close_trigger_;
353#ifdef USE_BINARY_SENSOR
354 obstacle = this->close_obstacle_;
355#endif
356 break;
357 default:
358 return;
359 }
360
361 this->stop_prev_trigger_();
362
363#ifdef USE_BINARY_SENSOR
364 // check if there is an obstacle to start the new operation -> abort without any change
365 // the case when an obstacle appears while moving is handled in the callback
366 if (obstacle != nullptr && obstacle->state) {
367 ESP_LOGD(TAG, "'%s' - %s obstacle detected. Action not started.", this->name_.c_str(),
368 dir == COVER_OPERATION_OPENING ? "Open" : "Close");
369 return;
370 }
371#endif
372
373 // if we are moving and need to move in the opposite direction
374 // check if we have a wait time
375 if (this->direction_change_waittime_.has_value() && dir != COVER_OPERATION_IDLE &&
376 this->current_operation != COVER_OPERATION_IDLE && dir != this->current_operation) {
377 ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str());
379
380 this->set_timeout("direction_change", *this->direction_change_waittime_,
381 [this, dir]() { this->start_direction_(dir); });
382
383 } else {
384 this->set_current_operation_(dir, true);
385 this->prev_command_trigger_ = trig;
386 ESP_LOGD(TAG, "'%s' - Firing '%s' trigger.", this->name_.c_str(),
387 dir == COVER_OPERATION_OPENING ? "OPEN"
388 : dir == COVER_OPERATION_CLOSING ? "CLOSE"
389 : "STOP");
390 trig->trigger();
391 }
392}
393
396 return;
397
398 const uint32_t now = millis();
399 float dir;
400 float action_dur;
401 float min_pos;
402 float max_pos;
403
404 // endstop sensors update position from their callbacks, and sets the fully open/close value
405 // If we have endstop, estimation never reaches the fully open/closed state.
406 // but if movement continues past corresponding endstop (inertia), keep the fully open/close state
407
408 switch (this->current_operation) {
410 dir = 1.0f;
411 action_dur = this->open_duration_;
412 min_pos = COVER_CLOSED;
413 max_pos = (
414#ifdef USE_BINARY_SENSOR
415 this->open_endstop_ != nullptr ||
416#endif
417 this->infer_endstop_) &&
418 this->position < COVER_OPEN
419 ? 0.99f
420 : COVER_OPEN;
421 break;
423 dir = -1.0f;
424 action_dur = this->close_duration_;
425 min_pos = (
426#ifdef USE_BINARY_SENSOR
427 this->close_endstop_ != nullptr ||
428#endif
429 this->infer_endstop_) &&
430 this->position > COVER_CLOSED
431 ? 0.01f
432 : COVER_CLOSED;
433 max_pos = COVER_OPEN;
434 break;
435 default:
436 return;
437 }
438
439 // check if we have an acceleration_wait_time, and remove from position computation
440 if (now - this->start_dir_time_ > this->acceleration_wait_time_) {
441 uint32_t accel_end_time = this->start_dir_time_ + this->acceleration_wait_time_;
442 uint32_t effective_start;
443 if (static_cast<int32_t>(accel_end_time - this->last_recompute_time_) >= 0) {
444 effective_start = accel_end_time;
445 } else {
446 effective_start = this->last_recompute_time_;
447 }
448 this->position += dir * (now - effective_start) / (action_dur - this->acceleration_wait_time_);
449 this->position = clamp(this->position, min_pos, max_pos);
450 }
451 this->last_recompute_time_ = now;
452}
453
454} // namespace feedback
455} // namespace esphome
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:451
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:473
void add_on_state_callback(std::function< void(T)> &&callback)
constexpr const char * c_str() const
Definition string_ref.h:73
void trigger(const Ts &...x)
Inform the parent automation that the event has triggered.
Definition automation.h:325
void stop_action()
Stop any action connected to this trigger.
Definition automation.h:333
Base class for all binary_sensor-type classes.
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:180
void publish_state(bool save=true)
Publish the current state of the cover.
Definition cover.cpp:143
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
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
size_t size_t pos
Definition helpers.h:929
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:26
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t