ESPHome  2025.3.3
All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Modules Pages
tormatic_cover.cpp
Go to the documentation of this file.
1 #include <vector>
2 
3 #include "tormatic_cover.h"
4 
5 using namespace std;
6 
7 namespace esphome {
8 namespace tormatic {
9 
10 static const char *const TAG = "tormatic.cover";
11 
12 using namespace esphome::cover;
13 
15  auto restore = this->restore_state_();
16  if (restore.has_value()) {
17  restore->apply(this);
18  return;
19  }
20 
21  // Assume gate is closed without preexisting state.
22  this->position = 0.0f;
23 }
24 
25 cover::CoverTraits Tormatic::get_traits() {
26  auto traits = CoverTraits();
27  traits.set_supports_stop(true);
28  traits.set_supports_position(true);
29  traits.set_is_assumed_state(false);
30  return traits;
31 }
32 
33 void Tormatic::dump_config() {
34  LOG_COVER("", "Tormatic Cover", this);
35  this->check_uart_settings(9600, 1, uart::UART_CONFIG_PARITY_NONE, 8);
36 
37  ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f);
38  ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f);
39 
40  auto restore = this->restore_state_();
41  if (restore.has_value()) {
42  ESP_LOGCONFIG(TAG, " Saved position %d%%", (int) (restore->position * 100.f));
43  }
44 }
45 
46 void Tormatic::update() { this->request_gate_status_(); }
47 
49  auto o_status = this->read_gate_status_();
50  if (o_status) {
51  auto status = o_status.value();
52 
53  this->recalibrate_duration_(status);
54  this->handle_gate_status_(status);
55  }
56 
57  this->recompute_position_();
58  this->stop_at_target_();
59 }
60 
61 void Tormatic::control(const cover::CoverCall &call) {
62  if (call.get_stop()) {
63  this->send_gate_command_(PAUSED);
64  return;
65  }
66 
67  if (call.get_position().has_value()) {
68  auto pos = call.get_position().value();
69  this->control_position_(pos);
70  return;
71  }
72 }
73 
74 // Wrap the Cover's publish_state with a rate limiter. Publishes if the last
75 // publish was longer than ratelimit milliseconds ago. 0 to disable.
76 void Tormatic::publish_state(bool save, uint32_t ratelimit) {
77  auto now = millis();
78  if ((now - this->last_publish_time_) < ratelimit) {
79  return;
80  }
81  this->last_publish_time_ = now;
82 
83  Cover::publish_state(save);
84 };
85 
86 // Recalibrate the gate's estimated open or close duration based on the
87 // actual time the operation took.
88 void Tormatic::recalibrate_duration_(GateStatus s) {
89  if (this->current_status_ == s) {
90  return;
91  }
92 
93  auto now = millis();
94  auto old = this->current_status_;
95 
96  // Gate paused halfway through opening or closing, invalidate the start time
97  // of the current operation. Close/open durations can only be accurately
98  // calibrated on full open or close cycle due to motor acceleration.
99  if (s == PAUSED) {
100  ESP_LOGD(TAG, "Gate paused, clearing direction start time");
101  this->direction_start_time_ = 0;
102  return;
103  }
104 
105  // Record the start time of a state transition if the gate was in the fully
106  // open or closed position before the command.
107  if ((old == CLOSED && s == OPENING) || (old == OPENED && s == CLOSING)) {
108  ESP_LOGD(TAG, "Gate started moving from fully open or closed state");
109  this->direction_start_time_ = now;
110  return;
111  }
112 
113  // The gate was resumed from a paused state, don't attempt recalibration.
114  if (this->direction_start_time_ == 0) {
115  return;
116  }
117 
118  if (s == OPENED) {
119  this->open_duration_ = now - this->direction_start_time_;
120  ESP_LOGI(TAG, "Recalibrated the gate's open duration to %dms", this->open_duration_);
121  }
122  if (s == CLOSED) {
123  this->close_duration_ = now - this->direction_start_time_;
124  ESP_LOGI(TAG, "Recalibrated the gate's close duration to %dms", this->close_duration_);
125  }
126 
127  this->direction_start_time_ = 0;
128 }
129 
130 // Set the Cover's internal state based on a status message
131 // received from the unit.
132 void Tormatic::handle_gate_status_(GateStatus s) {
133  if (this->current_status_ == s) {
134  return;
135  }
136 
137  ESP_LOGI(TAG, "Status changed from %s to %s", gate_status_to_str(this->current_status_), gate_status_to_str(s));
138 
139  switch (s) {
140  case OPENED:
141  // The Novoferm 423 doesn't respond to the first 'Close' command after
142  // being opened completely. Sending a pause command after opening fixes
143  // that.
144  this->send_gate_command_(PAUSED);
145 
146  this->position = COVER_OPEN;
147  break;
148  case CLOSED:
149  this->position = COVER_CLOSED;
150  break;
151  default:
152  break;
153  }
154 
155  this->current_status_ = s;
156  this->current_operation = gate_status_to_cover_operation(s);
157 
158  this->publish_state(true);
159 
160  // This timestamp is used to generate position deltas on every loop() while
161  // the gate is moving. Bump it on each state transition so the first tick
162  // doesn't generate a huge delta.
163  this->last_recompute_time_ = millis();
164 }
165 
166 // Recompute the gate's position and publish the results while
167 // the gate is moving. No-op when the gate is idle.
168 void Tormatic::recompute_position_() {
169  if (this->current_operation == COVER_OPERATION_IDLE) {
170  return;
171  }
172 
173  const uint32_t now = millis();
174  uint32_t diff = now - this->last_recompute_time_;
175 
176  auto direction = +1.0f;
177  uint32_t duration = this->open_duration_;
178  if (this->current_operation == COVER_OPERATION_CLOSING) {
179  direction = -1.0f;
180  duration = this->close_duration_;
181  }
182 
183  auto delta = direction * diff / duration;
184 
185  this->position = clamp(this->position + delta, COVER_CLOSED, COVER_OPEN);
186 
187  this->last_recompute_time_ = now;
188 
189  this->publish_state(true, 250);
190 }
191 
192 // Start moving the gate in the direction of the target position.
193 void Tormatic::control_position_(float target) {
194  if (target == this->position) {
195  return;
196  }
197 
198  if (target == COVER_OPEN) {
199  ESP_LOGI(TAG, "Fully opening gate");
200  this->send_gate_command_(OPENED);
201  return;
202  }
203  if (target == COVER_CLOSED) {
204  ESP_LOGI(TAG, "Fully closing gate");
205  this->send_gate_command_(CLOSED);
206  return;
207  }
208 
209  // Don't set target position when fully opening or closing the gate, the gate
210  // stops automatically when it reaches the configured open/closed positions.
211  this->target_position_ = target;
212 
213  if (target > this->position) {
214  ESP_LOGI(TAG, "Opening gate towards %.1f", target);
215  this->send_gate_command_(OPENED);
216  return;
217  }
218 
219  if (target < this->position) {
220  ESP_LOGI(TAG, "Closing gate towards %.1f", target);
221  this->send_gate_command_(CLOSED);
222  return;
223  }
224 }
225 
226 // Stop the gate if it is moving at or beyond its target position. Target
227 // position is only set when the gate is requested to move to a halfway
228 // position.
229 void Tormatic::stop_at_target_() {
230  if (this->current_operation == COVER_OPERATION_IDLE) {
231  return;
232  }
233  if (!this->target_position_) {
234  return;
235  }
236  auto target = this->target_position_.value();
237 
238  if (this->current_operation == COVER_OPERATION_OPENING && this->position < target) {
239  return;
240  }
241  if (this->current_operation == COVER_OPERATION_CLOSING && this->position > target) {
242  return;
243  }
244 
245  this->send_gate_command_(PAUSED);
246  this->target_position_.reset();
247 }
248 
249 // Read a GateStatus from the unit. The unit only sends messages in response to
250 // status requests or commands, so a message needs to be sent first.
251 optional<GateStatus> Tormatic::read_gate_status_() {
252  if (this->available() < sizeof(MessageHeader)) {
253  return {};
254  }
255 
256  auto o_hdr = this->read_data_<MessageHeader>();
257  if (!o_hdr) {
258  ESP_LOGE(TAG, "Timeout reading message header");
259  return {};
260  }
261  auto hdr = o_hdr.value();
262 
263  switch (hdr.type) {
264  case STATUS: {
265  if (hdr.payload_size() != sizeof(StatusReply)) {
266  ESP_LOGE(TAG, "Header specifies payload size %d but size of StatusReply is %d", hdr.payload_size(),
267  sizeof(StatusReply));
268  }
269 
270  // Read a StatusReply requested by update().
271  auto o_status = this->read_data_<StatusReply>();
272  if (!o_status) {
273  return {};
274  }
275  auto status = o_status.value();
276 
277  return status.state;
278  }
279 
280  case COMMAND:
281  // Commands initiated by control() are simply echoed back by the unit, but
282  // don't guarantee that the unit's internal state has been transitioned,
283  // nor that the motor started moving. A subsequent status request may
284  // still return the previous state. Discard these messages, don't use them
285  // to drive the Cover state machine.
286  break;
287 
288  default:
289  // Unknown message type, drain the remaining amount of bytes specified in
290  // the header.
291  ESP_LOGE(TAG, "Reading remaining %d payload bytes of unknown type 0x%x", hdr.payload_size(), hdr.type);
292  break;
293  }
294 
295  // Drain any unhandled payload bytes described by the message header, if any.
296  this->drain_rx_(hdr.payload_size());
297 
298  return {};
299 }
300 
301 // Send a message to the unit requesting the gate's status.
302 void Tormatic::request_gate_status_() {
303  ESP_LOGV(TAG, "Requesting gate status");
304  StatusRequest req(GATE);
305  this->send_message_(STATUS, req);
306 }
307 
308 // Send a message to the unit issuing a command.
309 void Tormatic::send_gate_command_(GateStatus s) {
310  ESP_LOGI(TAG, "Sending gate command %s", gate_status_to_str(s));
311  CommandRequestReply req(s);
312  this->send_message_(COMMAND, req);
313 }
314 
315 template<typename T> void Tormatic::send_message_(MessageType t, T req) {
316  MessageHeader hdr(t, ++this->seq_tx_, sizeof(req));
317 
318  auto out = serialize(hdr);
319  auto reqv = serialize(req);
320  out.insert(out.end(), reqv.begin(), reqv.end());
321 
322  this->write_array(out);
323 }
324 
325 template<typename T> optional<T> Tormatic::read_data_() {
326  T obj;
327  uint32_t start = millis();
328 
329  auto ok = this->read_array((uint8_t *) &obj, sizeof(obj));
330  if (!ok) {
331  // Couldn't read object successfully, timeout?
332  return {};
333  }
334  obj.byteswap();
335 
336  ESP_LOGV(TAG, "Read %s in %d ms", obj.print().c_str(), millis() - start);
337  return obj;
338 }
339 
340 // Drain up to n amount of bytes from the uart rx buffer.
341 void Tormatic::drain_rx_(uint16_t n) {
342  uint8_t data;
343  uint16_t count = 0;
344  while (this->available()) {
345  this->read_byte(&data);
346  count++;
347 
348  if (n > 0 && count >= n) {
349  return;
350  }
351  }
352 }
353 
354 } // namespace tormatic
355 } // namespace esphome
void setup()
value_type const & value() const
Definition: optional.h:89
void loop()
The cover is currently closing.
Definition: cover.h:86
const char * gate_status_to_str(GateStatus s)
STL namespace.
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:101
uint32_t IRAM_ATTR HOT millis()
Definition: core.cpp:25
FanDirection direction
Definition: fan.h:37
const float COVER_OPEN
Definition: cover.cpp:9
std::vector< uint8_t > serialize(T obj)
send_message_t * send_message_
uint8_t status
Definition: bl0942.h:74
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
float position
Definition: cover.h:14
CoverOperation gate_status_to_cover_operation(GateStatus s)
The cover is currently opening.
Definition: cover.h:84
const optional< float > & get_position() const
Definition: cover.cpp:97
bool get_stop() const
Definition: cover.cpp:147
uint8_t duration
Definition: msa3xx.h:430