ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
apc_proteous_cover.cpp
Go to the documentation of this file.
2#include "esphome/core/log.h"
4
6
7ESPHOME_LOG_TAG(TAG, "apc_proteous.cover");
8static constexpr const char *START_CMD = "*1\r"; // start/stop toggle
9static constexpr const char *OPEN_CMD = "*6\r";
10static constexpr const char *CLOSE_CMD = "*7\r";
11static constexpr const char *QUERY_S = "?s\r";
12static constexpr const char *QUERY_X = "?x\r";
13
14// Depending on calibration the reported position may stop a little short of the physical limits
15// (e.g. 99% at the fully open reed switch), so readings this close to an end count as that end.
16static constexpr uint8_t ENDPOINT_MARGIN = 2;
17
18using namespace esphome::cover;
19
21 auto restore = this->restore_state_();
22 if (restore.has_value()) {
23 restore->apply(this);
24 } else {
25 this->position = 0.5f;
26 }
27}
28
30 uint8_t data;
31 while (this->available() > 0) {
32 if (!this->read_byte(&data)) {
33 continue;
34 }
35 if (data == '\r') {
36 if (this->rx_len_ != 0) {
37 this->rx_buffer_[this->rx_len_] = '\0';
38 ESP_LOGVV(TAG, "rx: '%s'", this->rx_buffer_);
39 this->parse_response_();
40 this->rx_len_ = 0;
41 }
42 } else if (data != '\n') {
43 if (this->rx_len_ >= MAX_RESPONSE_LEN) {
44 ESP_LOGVV(TAG, "rx overflow, discarding");
45 this->rx_len_ = 0;
46 }
47 this->rx_buffer_[this->rx_len_++] = static_cast<char>(data);
48 }
49 }
50}
51
53 // Expected format: "?s=XX" or "?x=XX" where XX is a hex value
54 if (this->rx_len_ < 4 || this->rx_buffer_[0] != '?' || this->rx_buffer_[2] != '=') {
55 ESP_LOGV(TAG, "Invalid response: %s", this->rx_buffer_);
56 return;
57 }
58
59 const char *hex_str = this->rx_buffer_ + 3;
60 char *end_ptr;
61 uint8_t value = static_cast<uint8_t>(strtol(hex_str, &end_ptr, 16));
62 if (end_ptr == hex_str) {
63 ESP_LOGW(TAG, "Failed to parse hex value: %s", this->rx_buffer_);
64 return;
65 }
66
67 bool state_changed = false;
68 char type = this->rx_buffer_[1];
69 if (type == 's') {
70 // bit 0 = operating, bit 1 = direction (0 = opening, 1 = closing)
72 if (value & 0x01) {
73 new_operation = (value & 0x02) ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING;
74 }
75 if (this->current_operation != new_operation) {
76 this->current_operation = new_operation;
77 state_changed = true;
78 }
79 // Motion in the commanded direction acknowledges the pending command
80 if ((this->pending_command_ == OPEN_CMD && new_operation == COVER_OPERATION_OPENING) ||
81 (this->pending_command_ == CLOSE_CMD && new_operation == COVER_OPERATION_CLOSING)) {
82 this->clear_pending_();
83 }
84 // A partial move is over once the acknowledged motion has stopped, wherever it stopped, so an
85 // old target cannot halt a movement started from the wall button or the remote later on
86 if (new_operation == COVER_OPERATION_IDLE && this->pending_command_ == nullptr) {
87 this->target_position_.reset();
88 }
89 ESP_LOGV(TAG, "s-status: 0x%02X (operation=%d)", value, new_operation);
90 } else if (type == 'x') {
91 // Position percentage, snapped to the ends so is_open/is_closed can latch despite calibration offsets
92 float new_position;
93 if (value + ENDPOINT_MARGIN >= 100) {
94 new_position = COVER_OPEN;
95 } else if (value <= ENDPOINT_MARGIN) {
96 new_position = COVER_CLOSED;
97 } else {
98 new_position = value / 100.0f;
99 }
100 if (this->position != new_position) {
101 this->position = new_position;
102 state_changed = true;
103 // Until the controller has confirmed motion in the commanded direction, current_operation may
104 // still show the previous one, so a reversing partial move must not be judged against it
105 if (this->target_position_.has_value() && this->pending_command_ == nullptr &&
106 ((this->current_operation == COVER_OPERATION_OPENING && this->position >= *this->target_position_) ||
107 (this->current_operation == COVER_OPERATION_CLOSING && this->position <= *this->target_position_))) {
108 ESP_LOGD(TAG, "Target position %.2f reached", *this->target_position_);
110 }
111 }
112 ESP_LOGV(TAG, "x-status: %d%% (position=%.2f)", value, this->position);
113 }
114
115 if (state_changed) {
116 this->publish_state();
117 }
118}
119
121 this->pending_command_ = nullptr;
122 this->command_retries_ = 0;
123}
124
126 if (this->pending_command_ == nullptr || (now - this->pending_command_time_) < COMMAND_ACK_MS) {
127 return;
128 }
129 bool is_open_cmd = this->pending_command_ == OPEN_CMD;
130 // Nothing to resend once the gate is at the commanded end
131 if ((is_open_cmd && this->position >= COVER_OPEN) || (!is_open_cmd && this->position <= COVER_CLOSED)) {
132 this->clear_pending_();
133 return;
134 }
135 const LogString *label = is_open_cmd ? LOG_STR("open") : LOG_STR("close");
137 ESP_LOGW(TAG, "Gate did not acknowledge %s command after %u retries", LOG_STR_ARG(label), this->command_retries_);
138 this->clear_pending_();
139 this->target_position_.reset();
140 return;
141 }
142 this->command_retries_++;
143 ESP_LOGD(TAG, "Gate did not respond, resending %s command (retry %u)", LOG_STR_ARG(label), this->command_retries_);
144 this->pending_command_time_ = now;
145 this->write_command_(this->pending_command_, now);
146}
147
150 this->retry_pending_command_(now);
151
152 // Let the command and its echo clear the line before the next query
153 if ((now - this->last_command_tx_) < COMMAND_QUIET_MS) {
154 return;
155 }
156 // The line is idle, so anything left in the buffer is an unterminated echo
157 if (this->rx_len_ != 0) {
158 this->rx_buffer_[this->rx_len_] = '\0';
159 ESP_LOGVV(TAG, "discarding stale rx '%s' before query", this->rx_buffer_);
160 this->rx_len_ = 0;
161 }
162
163 const char *query = this->query_s_next_ ? QUERY_S : QUERY_X;
164 ESP_LOGVV(TAG, "tx: %s", query);
165 this->write_str(query);
166 this->query_s_next_ = !this->query_s_next_;
167}
168
170 LOG_COVER("", "APC Proteous Cover", this);
171 LOG_UPDATE_INTERVAL(this);
172}
173
175 auto traits = CoverTraits();
176 traits.set_supports_position(true);
177 traits.set_supports_stop(true);
178 traits.set_supports_toggle(true);
179 return traits;
180}
181
182void APCProteousCover::write_command_(const char *cmd, uint32_t now) {
183 ESP_LOGVV(TAG, "tx: %s", cmd);
184 this->last_command_tx_ = now;
185 this->write_str(cmd);
186}
187
188void APCProteousCover::send_command_(const char *cmd) {
189 // The published state comes only from status reads; this just records the command so a stop
190 // can cancel it before the controller reports motion, and so it can be resent if dropped.
191 // The commands are idempotent, so only skip one when the gate is already moving that way.
192 CoverOperation operation = cmd == OPEN_CMD ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING;
193 if (this->current_operation == operation) {
194 return;
195 }
196 ESP_LOGD(TAG, "Sending %s command",
197 operation == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("open") : LOG_STR_LITERAL("close"));
199 this->pending_command_ = cmd;
200 this->pending_command_time_ = now;
201 this->command_retries_ = 0;
202 this->write_command_(cmd, now);
203}
204
206 // START_CMD is a start/stop toggle, so it is only sent while the gate is moving, or while a
207 // movement command is in flight that the controller has not reported yet; otherwise it would start the gate.
208 if (this->current_operation == COVER_OPERATION_IDLE && this->pending_command_ == nullptr) {
209 ESP_LOGD(TAG, "Cover already idle, ignoring stop command");
210 return;
211 }
212 ESP_LOGD(TAG, "Sending stop command");
213 this->write_command_(START_CMD, now);
214 this->clear_pending_();
215 this->target_position_.reset();
216}
217
219 if (call.get_stop()) {
221 } else if (call.get_position().has_value()) {
222 float target = *call.get_position();
223 ESP_LOGD(TAG, "Target position: %.2f, current: %.2f", target, this->position);
224 // There is no position command: a partial target is reached by moving towards it and stopping
225 // once the reported position gets there
226 this->target_position_.reset();
227 if (target == COVER_OPEN) {
228 this->send_command_(OPEN_CMD);
229 } else if (target == COVER_CLOSED) {
230 this->send_command_(CLOSE_CMD);
231 } else if (target != this->position) {
232 this->target_position_ = target;
233 this->send_command_(target > this->position ? OPEN_CMD : CLOSE_CMD);
234 }
235 } else if (call.get_toggle()) {
236 this->clear_pending_();
237 this->target_position_.reset();
239 }
240}
241
242} // namespace esphome::apc_proteous
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void control(const cover::CoverCall &call) override
void write_command_(const char *cmd, uint32_t now)
const optional< float > & get_position() const
Definition cover.cpp:92
CoverOperation current_operation
The current operation of the cover (idle, opening, closing).
Definition cover.h:115
optional< CoverRestoreState > restore_state_()
Definition cover.cpp:175
void publish_state(bool save=true)
Publish the current state of the cover.
Definition cover.cpp:138
float position
The position of the cover from 0.0 (fully closed) to 1.0 (fully open).
Definition cover.h:121
void write_str(const char *str)
Definition uart.h:34
bool read_byte(uint8_t *data)
Definition uart.h:36
uint8_t type
ESPHOME_LOG_TAG(TAG, "apc_proteous.cover")
CoverOperation
Enum encoding the current operation of a cover.
Definition cover.h:79
@ 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
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t