ESPHome 2026.6.0-dev
Loading...
Searching...
No Matches
tormatic_cover.cpp
Go to the documentation of this file.
1#include <cinttypes>
2#include <vector>
3
4#include "tormatic_cover.h"
5
6using namespace std;
7
9
10static const char *const TAG = "tormatic.cover";
11
12// Time to poll the UART when flushing after desync. At 9600 baud, a full
13// 12-byte message takes ~12.5ms, so 15ms guarantees all bytes have arrived.
14static constexpr uint32_t DRAIN_TIMEOUT_MS = 15;
15
16using namespace esphome::cover;
17
19 auto restore = this->restore_state_();
20 if (restore.has_value()) {
21 restore->apply(this);
22 return;
23 }
24
25 // Assume gate is closed without preexisting state.
26 this->position = 0.0f;
27}
28
30 auto traits = CoverTraits();
31 traits.set_supports_stop(true);
32 traits.set_supports_position(true);
33 traits.set_is_assumed_state(false);
34 return traits;
35}
36
38 LOG_COVER("", "Tormatic Cover", this);
40
41 ESP_LOGCONFIG(TAG,
42 " Open Duration: %.1fs\n"
43 " Close Duration: %.1fs",
44 this->open_duration_ / 1e3f, this->close_duration_ / 1e3f);
45
46 auto restore = this->restore_state_();
47 if (restore.has_value()) {
48 ESP_LOGCONFIG(TAG, " Saved position %d%%", (int) (restore->position * 100.f));
49 }
50}
51
53
55 auto o_status = this->read_gate_status_();
56 if (o_status) {
57 auto status = o_status.value();
58
61 }
62
63 this->recompute_position_();
64 this->stop_at_target_();
65}
66
68 if (call.get_stop()) {
70 return;
71 }
72
73 auto pos_val = call.get_position();
74 if (pos_val.has_value()) {
75 auto pos = *pos_val;
77 return;
78 }
79}
80
81// Wrap the Cover's publish_state with a rate limiter. Publishes if the last
82// publish was longer than ratelimit milliseconds ago. 0 to disable.
83void Tormatic::publish_state(bool save, uint32_t ratelimit) {
84 auto now = millis();
85 if ((now - this->last_publish_time_) < ratelimit) {
86 return;
87 }
88 this->last_publish_time_ = now;
89
91};
92
93// Recalibrate the gate's estimated open or close duration based on the
94// actual time the operation took.
96 if (this->current_status_ == s) {
97 return;
98 }
99
100 auto now = millis();
101 auto old = this->current_status_;
102
103 // Gate paused halfway through opening or closing, invalidate the start time
104 // of the current operation. Close/open durations can only be accurately
105 // calibrated on full open or close cycle due to motor acceleration.
106 if (s == PAUSED) {
107 ESP_LOGD(TAG, "Gate paused, clearing direction start time");
108 this->direction_start_time_ = 0;
109 return;
110 }
111
112 // Record the start time of a state transition if the gate was in the fully
113 // open or closed position before the command.
114 if ((old == CLOSED && s == OPENING) || (old == OPENED && s == CLOSING)) {
115 ESP_LOGD(TAG, "Gate started moving from fully open or closed state");
116 this->direction_start_time_ = now;
117 return;
118 }
119
120 // The gate was resumed from a paused state, don't attempt recalibration.
121 if (this->direction_start_time_ == 0) {
122 return;
123 }
124
125 if (s == OPENED) {
126 this->open_duration_ = now - this->direction_start_time_;
127 ESP_LOGI(TAG, "Recalibrated the gate's open duration to %" PRIu32 "ms", this->open_duration_);
128 }
129 if (s == CLOSED) {
130 this->close_duration_ = now - this->direction_start_time_;
131 ESP_LOGI(TAG, "Recalibrated the gate's close duration to %" PRIu32 "ms", this->close_duration_);
132 }
133
134 this->direction_start_time_ = 0;
135}
136
137// Set the Cover's internal state based on a status message
138// received from the unit.
140 if (this->current_status_ == s) {
141 return;
142 }
143
144 ESP_LOGI(TAG, "Status changed from %s to %s", gate_status_to_str(this->current_status_), gate_status_to_str(s));
145
146 switch (s) {
147 case OPENED:
148 // The Novoferm 423 doesn't respond to the first 'Close' command after
149 // being opened completely. Sending a pause command after opening fixes
150 // that.
152
153 this->position = COVER_OPEN;
154 break;
155 case CLOSED:
156 this->position = COVER_CLOSED;
157 break;
158 default:
159 break;
160 }
161
162 this->current_status_ = s;
164
165 this->publish_state(true);
166
167 // This timestamp is used to generate position deltas on every loop() while
168 // the gate is moving. Bump it on each state transition so the first tick
169 // doesn't generate a huge delta.
171}
172
173// Recompute the gate's position and publish the results while
174// the gate is moving. No-op when the gate is idle.
177 return;
178 }
179
180 const uint32_t now = millis();
181 uint32_t diff = now - this->last_recompute_time_;
182
183 auto direction = +1.0f;
186 direction = -1.0f;
187 duration = this->close_duration_;
188 }
189
190 if (duration == 0)
191 return;
192
193 auto delta = direction * diff / duration;
194
195 this->position = clamp(this->position + delta, COVER_CLOSED, COVER_OPEN);
196
197 this->last_recompute_time_ = now;
198
199 this->publish_state(true, 250);
200}
201
202// Start moving the gate in the direction of the target position.
203void Tormatic::control_position_(float target) {
204 if (target == this->position) {
205 return;
206 }
207
208 if (target == COVER_OPEN) {
209 ESP_LOGI(TAG, "Fully opening gate");
211 return;
212 }
213 if (target == COVER_CLOSED) {
214 ESP_LOGI(TAG, "Fully closing gate");
216 return;
217 }
218
219 // Don't set target position when fully opening or closing the gate, the gate
220 // stops automatically when it reaches the configured open/closed positions.
221 this->target_position_ = target;
222
223 if (target > this->position) {
224 ESP_LOGI(TAG, "Opening gate towards %.1f", target);
226 return;
227 }
228
229 if (target < this->position) {
230 ESP_LOGI(TAG, "Closing gate towards %.1f", target);
232 return;
233 }
234}
235
236// Stop the gate if it is moving at or beyond its target position. Target
237// position is only set when the gate is requested to move to a halfway
238// position.
241 return;
242 }
243 if (!this->target_position_) {
244 return;
245 }
246 auto target = this->target_position_.value();
247
248 if (this->current_operation == COVER_OPERATION_OPENING && this->position < target) {
249 return;
250 }
251 if (this->current_operation == COVER_OPERATION_CLOSING && this->position > target) {
252 return;
253 }
254
256 this->target_position_.reset();
257}
258
259// Read a GateStatus from the unit. The unit only sends messages in response to
260// status requests or commands, so a message needs to be sent first.
261optional<GateStatus> Tormatic::read_gate_status_() {
262 if (!this->pending_hdr_) {
263 if (this->available() < sizeof(MessageHeader)) {
264 return {};
265 }
266
268 if (!this->pending_hdr_) {
269 return {};
270 }
271
272 // Sanity check: valid messages have small payloads (3-4 bytes). A large
273 // or impossible payload_size means the stream is out of sync (corrupted
274 // byte, dropped data, etc.). Flush the buffer so we can resync on the
275 // next request/response cycle.
276 if (this->pending_hdr_->payload_size() > sizeof(CommandRequestReply)) {
277 ESP_LOGW(TAG, "Unexpected payload size %" PRIu32 ", flushing rx buffer", this->pending_hdr_->payload_size());
278 this->pending_hdr_.reset();
279 this->drain_rx_();
280 return {};
281 }
282 }
283
284 auto hdr = this->pending_hdr_.value();
285
286 // Wait for all payload bytes to arrive before processing.
287 if (this->available() < hdr.payload_size()) {
288 return {};
289 }
290
291 this->pending_hdr_.reset();
292
293 switch (hdr.type) {
294 case STATUS: {
295 if (hdr.payload_size() != sizeof(StatusReply)) {
296 ESP_LOGE(TAG, "Header specifies payload size %" PRIu32 " but size of StatusReply is %zu", hdr.payload_size(),
297 sizeof(StatusReply));
298 this->drain_rx_(hdr.payload_size());
299 return {};
300 }
301
302 auto o_status = this->read_data_<StatusReply>();
303 if (!o_status) {
304 return {};
305 }
306
307 return o_status->state;
308 }
309
310 case COMMAND:
311 // Commands initiated by control() are simply echoed back by the unit, but
312 // don't guarantee that the unit's internal state has been transitioned,
313 // nor that the motor started moving. A subsequent status request may
314 // still return the previous state. Discard these messages, don't use them
315 // to drive the Cover state machine.
316 break;
317
318 default:
319 // Unknown message type, drain the remaining amount of bytes specified in
320 // the header.
321 ESP_LOGE(TAG, "Reading remaining %" PRIu32 " payload bytes of unknown type 0x%x", hdr.payload_size(), hdr.type);
322 break;
323 }
324
325 // Drain any unhandled payload bytes described by the message header, if any.
326 this->drain_rx_(hdr.payload_size());
327
328 return {};
329}
330
331// Send a message to the unit requesting the gate's status.
333 ESP_LOGV(TAG, "Requesting gate status");
334 StatusRequest req(GATE);
335 this->send_message_(STATUS, req);
336}
337
338// Send a message to the unit issuing a command.
340 ESP_LOGI(TAG, "Sending gate command %s", gate_status_to_str(s));
341 CommandRequestReply req(s);
342 this->send_message_(COMMAND, req);
343}
344
345template<typename T> void Tormatic::send_message_(MessageType t, T req) {
346 MessageHeader hdr(t, ++this->seq_tx_, sizeof(req));
347
348 auto out = serialize(hdr);
349 auto reqv = serialize(req);
350 out.insert(out.end(), reqv.begin(), reqv.end());
351
352 this->write_array(out);
353}
354
355template<typename T> optional<T> Tormatic::read_data_() {
356 T obj;
357 uint32_t start = millis();
358
359 auto ok = this->read_array((uint8_t *) &obj, sizeof(obj));
360 if (!ok) {
361 // Couldn't read object successfully, timeout?
362 return {};
363 }
364 obj.byteswap();
365
366 ESP_LOGV(TAG, "Read %s in %" PRIu32 " ms", obj.print().c_str(), millis() - start);
367 return obj;
368}
369
370// Drain bytes from the uart rx buffer. When n > 0, drain exactly n bytes
371// (caller must ensure they are available). When n == 0, poll for 15ms to
372// guarantee a full packet time at 9600 baud has elapsed, consuming any
373// bytes still in transit.
374void Tormatic::drain_rx_(uint16_t n) {
375 uint8_t data;
376 if (n > 0) {
377 for (uint16_t i = 0; i < n; i++) {
378 if (!this->read_byte(&data)) {
379 return;
380 }
381 }
382 } else {
383 uint32_t start = millis();
384 while (millis() - start < DRAIN_TIMEOUT_MS) {
385 if (this->available()) {
386 this->read_byte(&data);
387 }
388 }
389 }
390}
391
392} // namespace esphome::tormatic
uint8_t status
Definition bl0942.h:8
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
optional< GateStatus > read_gate_status_()
void send_message_(MessageType t, T r)
void recalibrate_duration_(GateStatus s)
optional< MessageHeader > pending_hdr_
optional< float > target_position_
void send_gate_command_(GateStatus s)
void control(const cover::CoverCall &call) override
void control_position_(float target)
void handle_gate_status_(GateStatus s)
cover::CoverTraits get_traits() override
void publish_state(bool save=true, uint32_t ratelimit=0)
optional< std::array< uint8_t, N > > read_array()
Definition uart.h:38
void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits=1, UARTParityOptions parity=UART_CONFIG_PARITY_NONE, uint8_t data_bits=8)
Check that the configuration of the UART bus matches the provided values and otherwise print a warnin...
Definition uart.cpp:16
bool read_byte(uint8_t *data)
Definition uart.h:34
void write_array(const uint8_t *data, size_t len)
Definition uart.h:26
FanDirection direction
Definition fan.h:5
uint8_t duration
Definition msa3xx.h:0
@ 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
This file implements the UART protocol spoken over the on-board Micro-USB (Type B) connector of Torma...
CoverOperation gate_status_to_cover_operation(GateStatus s)
std::vector< uint8_t > serialize(T obj)
const char * gate_status_to_str(GateStatus s)
size_t size_t pos
Definition helpers.h:1038
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
static void uint32_t