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