ESPHome 2026.5.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
8namespace esphome {
9namespace tormatic {
10
11static const char *const TAG = "tormatic.cover";
12
13// Time to poll the UART when flushing after desync. At 9600 baud, a full
14// 12-byte message takes ~12.5ms, so 15ms guarantees all bytes have arrived.
15static constexpr uint32_t DRAIN_TIMEOUT_MS = 15;
16
17using namespace esphome::cover;
18
20 auto restore = this->restore_state_();
21 if (restore.has_value()) {
22 restore->apply(this);
23 return;
24 }
25
26 // Assume gate is closed without preexisting state.
27 this->position = 0.0f;
28}
29
31 auto traits = CoverTraits();
32 traits.set_supports_stop(true);
33 traits.set_supports_position(true);
34 traits.set_is_assumed_state(false);
35 return traits;
36}
37
39 LOG_COVER("", "Tormatic Cover", this);
41
42 ESP_LOGCONFIG(TAG,
43 " Open Duration: %.1fs\n"
44 " Close Duration: %.1fs",
45 this->open_duration_ / 1e3f, this->close_duration_ / 1e3f);
46
47 auto restore = this->restore_state_();
48 if (restore.has_value()) {
49 ESP_LOGCONFIG(TAG, " Saved position %d%%", (int) (restore->position * 100.f));
50 }
51}
52
54
56 auto o_status = this->read_gate_status_();
57 if (o_status) {
58 auto status = o_status.value();
59
62 }
63
64 this->recompute_position_();
65 this->stop_at_target_();
66}
67
69 if (call.get_stop()) {
71 return;
72 }
73
74 auto pos_val = call.get_position();
75 if (pos_val.has_value()) {
76 auto pos = *pos_val;
78 return;
79 }
80}
81
82// Wrap the Cover's publish_state with a rate limiter. Publishes if the last
83// publish was longer than ratelimit milliseconds ago. 0 to disable.
84void Tormatic::publish_state(bool save, uint32_t ratelimit) {
85 auto now = millis();
86 if ((now - this->last_publish_time_) < ratelimit) {
87 return;
88 }
89 this->last_publish_time_ = now;
90
92};
93
94// Recalibrate the gate's estimated open or close duration based on the
95// actual time the operation took.
97 if (this->current_status_ == s) {
98 return;
99 }
100
101 auto now = millis();
102 auto old = this->current_status_;
103
104 // Gate paused halfway through opening or closing, invalidate the start time
105 // of the current operation. Close/open durations can only be accurately
106 // calibrated on full open or close cycle due to motor acceleration.
107 if (s == PAUSED) {
108 ESP_LOGD(TAG, "Gate paused, clearing direction start time");
109 this->direction_start_time_ = 0;
110 return;
111 }
112
113 // Record the start time of a state transition if the gate was in the fully
114 // open or closed position before the command.
115 if ((old == CLOSED && s == OPENING) || (old == OPENED && s == CLOSING)) {
116 ESP_LOGD(TAG, "Gate started moving from fully open or closed state");
117 this->direction_start_time_ = now;
118 return;
119 }
120
121 // The gate was resumed from a paused state, don't attempt recalibration.
122 if (this->direction_start_time_ == 0) {
123 return;
124 }
125
126 if (s == OPENED) {
127 this->open_duration_ = now - this->direction_start_time_;
128 ESP_LOGI(TAG, "Recalibrated the gate's open duration to %" PRIu32 "ms", this->open_duration_);
129 }
130 if (s == CLOSED) {
131 this->close_duration_ = now - this->direction_start_time_;
132 ESP_LOGI(TAG, "Recalibrated the gate's close duration to %" PRIu32 "ms", this->close_duration_);
133 }
134
135 this->direction_start_time_ = 0;
136}
137
138// Set the Cover's internal state based on a status message
139// received from the unit.
141 if (this->current_status_ == s) {
142 return;
143 }
144
145 ESP_LOGI(TAG, "Status changed from %s to %s", gate_status_to_str(this->current_status_), gate_status_to_str(s));
146
147 switch (s) {
148 case OPENED:
149 // The Novoferm 423 doesn't respond to the first 'Close' command after
150 // being opened completely. Sending a pause command after opening fixes
151 // that.
153
154 this->position = COVER_OPEN;
155 break;
156 case CLOSED:
157 this->position = COVER_CLOSED;
158 break;
159 default:
160 break;
161 }
162
163 this->current_status_ = s;
165
166 this->publish_state(true);
167
168 // This timestamp is used to generate position deltas on every loop() while
169 // the gate is moving. Bump it on each state transition so the first tick
170 // doesn't generate a huge delta.
172}
173
174// Recompute the gate's position and publish the results while
175// the gate is moving. No-op when the gate is idle.
178 return;
179 }
180
181 const uint32_t now = millis();
182 uint32_t diff = now - this->last_recompute_time_;
183
184 auto direction = +1.0f;
187 direction = -1.0f;
188 duration = this->close_duration_;
189 }
190
191 if (duration == 0)
192 return;
193
194 auto delta = direction * diff / duration;
195
196 this->position = clamp(this->position + delta, COVER_CLOSED, COVER_OPEN);
197
198 this->last_recompute_time_ = now;
199
200 this->publish_state(true, 250);
201}
202
203// Start moving the gate in the direction of the target position.
204void Tormatic::control_position_(float target) {
205 if (target == this->position) {
206 return;
207 }
208
209 if (target == COVER_OPEN) {
210 ESP_LOGI(TAG, "Fully opening gate");
212 return;
213 }
214 if (target == COVER_CLOSED) {
215 ESP_LOGI(TAG, "Fully closing gate");
217 return;
218 }
219
220 // Don't set target position when fully opening or closing the gate, the gate
221 // stops automatically when it reaches the configured open/closed positions.
222 this->target_position_ = target;
223
224 if (target > this->position) {
225 ESP_LOGI(TAG, "Opening gate towards %.1f", target);
227 return;
228 }
229
230 if (target < this->position) {
231 ESP_LOGI(TAG, "Closing gate towards %.1f", target);
233 return;
234 }
235}
236
237// Stop the gate if it is moving at or beyond its target position. Target
238// position is only set when the gate is requested to move to a halfway
239// position.
242 return;
243 }
244 if (!this->target_position_) {
245 return;
246 }
247 auto target = this->target_position_.value();
248
249 if (this->current_operation == COVER_OPERATION_OPENING && this->position < target) {
250 return;
251 }
252 if (this->current_operation == COVER_OPERATION_CLOSING && this->position > target) {
253 return;
254 }
255
257 this->target_position_.reset();
258}
259
260// Read a GateStatus from the unit. The unit only sends messages in response to
261// status requests or commands, so a message needs to be sent first.
262optional<GateStatus> Tormatic::read_gate_status_() {
263 if (!this->pending_hdr_) {
264 if (this->available() < sizeof(MessageHeader)) {
265 return {};
266 }
267
269 if (!this->pending_hdr_) {
270 return {};
271 }
272
273 // Sanity check: valid messages have small payloads (3-4 bytes). A large
274 // or impossible payload_size means the stream is out of sync (corrupted
275 // byte, dropped data, etc.). Flush the buffer so we can resync on the
276 // next request/response cycle.
277 if (this->pending_hdr_->payload_size() > sizeof(CommandRequestReply)) {
278 ESP_LOGW(TAG, "Unexpected payload size %" PRIu32 ", flushing rx buffer", this->pending_hdr_->payload_size());
279 this->pending_hdr_.reset();
280 this->drain_rx_();
281 return {};
282 }
283 }
284
285 // Wait for all payload bytes to arrive before processing.
286 if (this->available() < this->pending_hdr_->payload_size()) {
287 return {};
288 }
289
290 auto hdr = *this->pending_hdr_;
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 tormatic
393} // namespace esphome
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
CoverOperation gate_status_to_cover_operation(GateStatus s)
std::vector< uint8_t > serialize(T obj)
const char * gate_status_to_str(GateStatus s)
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
size_t size_t pos
Definition helpers.h:1082
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:26
static void uint32_t