ESPHome 2025.5.0
Loading...
Searching...
No Matches
tormatic_cover.cpp
Go to the documentation of this file.
1#include <vector>
2
3#include "tormatic_cover.h"
4
5using namespace std;
6
7namespace esphome {
8namespace tormatic {
9
10static const char *const TAG = "tormatic.cover";
11
12using 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
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
34 LOG_COVER("", "Tormatic Cover", this);
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
47
49 auto o_status = this->read_gate_status_();
50 if (o_status) {
51 auto status = o_status.value();
52
55 }
56
57 this->recompute_position_();
58 this->stop_at_target_();
59}
60
62 if (call.get_stop()) {
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.
76void 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
84};
85
86// Recalibrate the gate's estimated open or close duration based on the
87// actual time the operation took.
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.
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.
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;
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.
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.
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_;
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.
193void 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");
201 return;
202 }
203 if (target == COVER_CLOSED) {
204 ESP_LOGI(TAG, "Fully closing gate");
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);
216 return;
217 }
218
219 if (target < this->position) {
220 ESP_LOGI(TAG, "Closing gate towards %.1f", target);
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.
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
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.
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.
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.
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
315template<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
325template<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.
341void 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
send_message_t send_message_
uint8_t status
Definition bl0942.h:8
const optional< float > & get_position() const
Definition cover.cpp:97
CoverOperation current_operation
The current operation of the cover (idle, opening, closing).
Definition cover.h:116
optional< CoverRestoreState > restore_state_()
Definition cover.cpp:200
void publish_state(bool save=true)
Publish the current state of the cover.
Definition cover.cpp:166
float position
The position of the cover from 0.0 (fully closed) to 1.0 (fully open).
Definition cover.h:122
bool has_value() const
Definition optional.h:87
value_type const & value() const
Definition optional.h:89
optional< GateStatus > read_gate_status_()
void send_message_(MessageType t, T r)
void recalibrate_duration_(GateStatus s)
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:33
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:13
bool read_byte(uint8_t *data)
Definition uart.h:29
void write_array(const uint8_t *data, size_t len)
Definition uart.h:21
FanDirection direction
Definition fan.h:3
uint8_t duration
Definition msa3xx.h:0
const float COVER_CLOSED
Definition cover.cpp:10
const float COVER_OPEN
Definition cover.cpp:9
@ COVER_OPERATION_OPENING
The cover is currently opening.
Definition cover.h:84
@ COVER_OPERATION_CLOSING
The cover is currently closing.
Definition cover.h:86
@ COVER_OPERATION_IDLE
The cover is currently idle (not moving)
Definition cover.h:82
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
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:27
constexpr const T & clamp(const T &v, const T &lo, const T &hi, Compare comp)
Definition helpers.h:101