ESPHome 2026.6.0-dev
Loading...
Searching...
No Matches
zwave_proxy.cpp
Go to the documentation of this file.
1#include "zwave_proxy.h"
2
3#ifdef USE_API
4
6
7#include <cinttypes>
10#include "esphome/core/log.h"
11#include "esphome/core/util.h"
12
14
15static const char *const TAG = "zwave_proxy";
16
17// Maximum bytes to log in very verbose hex output (168 * 3 = 504, under TX buffer size of 512)
18static constexpr size_t ZWAVE_MAX_LOG_BYTES = 168;
19
20static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20;
21// GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...]
22static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value
23static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 9; // TYPE + CMD + HOME_ID(4) + NODE_ID + checksum
24static constexpr uint32_t HOME_ID_TIMEOUT_MS = 100; // Timeout for waiting for home ID during setup
25static constexpr uint32_t RECONNECT_DELAY_MS = 500; // Delay between home ID query attempts after reconnect
26static constexpr uint8_t MAX_QUERY_RETRIES = 5; // Max attempts to query home ID after reconnect
27
28static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) {
29 // Calculate Z-Wave frame checksum
30 // XOR all bytes between SOF and checksum position (exclusive)
31 // Initial value is 0xFF per Z-Wave protocol specification
32 uint8_t checksum = 0xFF;
33 for (uint8_t i = 1; i < length - 1; i++) {
34 checksum ^= data[i];
35 }
36 return checksum;
37}
38
40
43 this->was_connected_ = this->parent_->is_connected();
44 if (this->was_connected_) {
45 this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS);
46 }
47}
48
50 // Set up before API so home ID is ready when API starts
52}
53
55 // If we already have the home ID, we can proceed
56 if (this->home_id_ready_) {
57 return true;
58 }
59
60 // Handle any pending responses
61 if (this->response_handler_()) {
62 ESP_LOGV(TAG, "Handled response during setup");
63 }
64
65 // Process UART data to check for home ID
66 this->process_uart_();
67
68 // Check if we got the home ID after processing
69 if (this->home_id_ready_) {
70 return true;
71 }
72
73 // Wait up to HOME_ID_TIMEOUT_MS for home ID response
75 if (now - this->setup_time_ > HOME_ID_TIMEOUT_MS) {
76 ESP_LOGW(TAG, "Timeout reading Home ID during setup");
77 return true; // Proceed anyway after timeout
78 }
79
80 return false; // Keep waiting
81}
82
84 if (this->response_handler_()) {
85 ESP_LOGV(TAG, "Handled late response");
86 }
87 if (this->api_connection_ != nullptr && (!this->api_connection_->is_connection_setup() || !api_is_connected())) {
88 ESP_LOGW(TAG, "Subscriber disconnected");
89 this->api_connection_ = nullptr; // Unsubscribe if disconnected
90 }
91
92 const bool connected = this->parent_->is_connected();
93 if (this->was_connected_ != connected) {
94 this->on_connection_changed_(connected);
95 }
96 if (this->reconnect_time_ != 0) {
98 }
99
100 this->process_uart_();
101 this->status_clear_warning();
102}
103
105 // Caller (inline process_uart_) has already confirmed available() > 0, so use do/while to
106 // drain bytes — available() is still checked at the tail, but not redundantly on entry.
107 do {
108 uint8_t byte;
109 if (!this->read_byte(&byte)) {
110 this->status_set_warning(LOG_STR("UART read failed"));
111 return;
112 }
113 if (this->parse_byte_(byte)) {
114 // Check if this is a GET_NETWORK_IDS response frame
115 // Frame format: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...]
116 // We verify:
117 // - buffer_[0]: Start of frame marker (0x01)
118 // - buffer_[1]: Length field must be >= 9 to contain all required data
119 // - buffer_[2]: Command type (0x01 for response)
120 // - buffer_[3]: Command ID (0x20 for GET_NETWORK_IDS)
121 if (this->buffer_[3] == ZWAVE_COMMAND_GET_NETWORK_IDS && this->buffer_[2] == ZWAVE_COMMAND_TYPE_RESPONSE &&
122 this->buffer_[1] >= ZWAVE_MIN_GET_NETWORK_IDS_LENGTH && this->buffer_[0] == ZWAVE_FRAME_TYPE_START) {
123 // Store the 4-byte Home ID, which starts at offset 4, and notify connected clients if it changed
124 // The frame parser has already validated the checksum and ensured all bytes are present
125 if (this->set_home_id_(&this->buffer_[4])) {
127 }
128 }
129 ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr));
130 if (this->api_connection_ != nullptr) {
131 // Zero-copy: point directly to our buffer
132 this->outgoing_proto_msg_.data = this->buffer_.data();
133 if (this->in_bootloader_) {
135 } else {
136 // If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN
137 this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1;
138 }
140 }
141 }
142 } while (this->available());
143}
144
146 char hex_buf[format_hex_pretty_size(ZWAVE_HOME_ID_SIZE)];
147 ESP_LOGCONFIG(TAG,
148 "Z-Wave Proxy:\n"
149 " Home ID: %s",
150 format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size()));
151}
152
154 if (this->home_id_ready_) {
155 // If a client just authenticated & HomeID is ready, send the current HomeID
156 this->send_homeid_changed_msg_(conn);
157 }
158}
159
161 switch (type) {
163 if (this->api_connection_ != nullptr) {
164 ESP_LOGE(TAG, "Only one API subscription is allowed at a time");
165 return;
166 }
167 this->api_connection_ = api_connection;
168 ESP_LOGV(TAG, "API connection is now subscribed");
169 break;
170
172 if (this->api_connection_ != api_connection) {
173 ESP_LOGV(TAG, "API connection is not subscribed");
174 return;
175 }
176 this->api_connection_ = nullptr;
177 break;
178
179 default:
180 ESP_LOGW(TAG, "Unknown request type: %" PRIu32, static_cast<uint32_t>(type));
181 break;
182 }
183}
184
186 this->was_connected_ = connected;
187 if (connected) {
188 ESP_LOGD(TAG, "Modem reconnected");
190 this->buffer_index_ = 0;
191 this->last_response_ = 0;
192 this->in_bootloader_ = false;
193 // Defer the query — the modem needs time to initialize after power is applied
195 this->query_retries_ = 0;
196 } else {
197 ESP_LOGW(TAG, "Modem disconnected");
198 this->clear_home_id_();
199 }
200}
201
203 if (this->home_id_ready_) {
204 // Got the home ID, cancel remaining retries
205 this->reconnect_time_ = 0;
206 return;
207 }
208 if (App.get_loop_component_start_time() - this->reconnect_time_ <= RECONNECT_DELAY_MS) {
209 return; // Not yet time for next attempt
210 }
211 this->reconnect_time_ = App.get_loop_component_start_time(); // Reset timer for next retry
212 this->query_retries_++;
213 if (this->query_retries_ <= MAX_QUERY_RETRIES) {
214 ESP_LOGD(TAG, "Querying Home ID (attempt %u)", this->query_retries_);
215 this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS);
216 } else {
217 ESP_LOGW(TAG, "Failed to read Home ID after %u attempts", MAX_QUERY_RETRIES);
218 this->reconnect_time_ = 0;
219 }
220}
221
223 static constexpr uint8_t ZERO_HOME_ID[ZWAVE_HOME_ID_SIZE] = {};
224 if (this->set_home_id_(ZERO_HOME_ID)) {
226 }
227 this->home_id_ready_ = false;
229 this->buffer_index_ = 0;
230 this->last_response_ = 0;
231 this->in_bootloader_ = false;
232}
233
234bool ZWaveProxy::set_home_id_(const uint8_t *new_home_id) {
235 if (std::memcmp(this->home_id_.data(), new_home_id, this->home_id_.size()) == 0) {
236 ESP_LOGV(TAG, "Home ID unchanged");
237 return false; // No change
238 }
239 std::memcpy(this->home_id_.data(), new_home_id, this->home_id_.size());
240 char hex_buf[format_hex_pretty_size(ZWAVE_HOME_ID_SIZE)];
241 ESP_LOGI(TAG, "Home ID: %s", format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size()));
242 this->home_id_ready_ = true;
243 return true; // Home ID was changed
244}
245
246void ZWaveProxy::send_frame(const uint8_t *data, size_t length) {
247 // Safety: validate pointer before any access
248 if (data == nullptr) {
249 ESP_LOGE(TAG, "Null data pointer");
250 return;
251 }
252 if (length == 0) {
253 ESP_LOGE(TAG, "Length 0");
254 return;
255 }
256
257 // Skip duplicate single-byte responses (ACK/NAK/CAN)
258 if (length == 1 && data[0] == this->last_response_) {
259 ESP_LOGV(TAG, "Response already sent: 0x%02X", data[0]);
260 return;
261 }
262
263#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
264 char hex_buf[format_hex_pretty_size(ZWAVE_MAX_LOG_BYTES)];
265#endif
266 ESP_LOGVV(TAG, "Sending: %s", format_hex_pretty_to(hex_buf, data, length));
267
268 this->write_array(data, length);
269}
270
274 msg.data = this->home_id_.data();
275 msg.data_len = this->home_id_.size();
276 if (conn != nullptr) {
277 // Send to specific connection
278 conn->send_message(msg);
279 } else if (api::global_api_server != nullptr) {
280 // We could add code to manage a second subscription type, but, since this message is
281 // very infrequent and small, we simply send it to all clients
283 }
284}
285
286void ZWaveProxy::send_simple_command_(const uint8_t command_id) {
287 // Send a simple Z-Wave command with no parameters
288 // Frame format: [SOF][LENGTH][TYPE][CMD][CHECKSUM]
289 // Where LENGTH=0x03 (3 bytes: TYPE + CMD + CHECKSUM)
290 uint8_t cmd[] = {0x01, 0x03, 0x00, command_id, 0x00};
291 cmd[4] = calculate_frame_checksum(cmd, sizeof(cmd));
292 this->send_frame(cmd, sizeof(cmd));
293}
294
295bool ZWaveProxy::parse_byte_(uint8_t byte) {
296 bool frame_completed = false;
297 // Basic parsing logic for received frames
298 switch (this->parsing_state_) {
300 this->parse_start_(byte);
301 break;
303 if (!byte) {
304 ESP_LOGW(TAG, "Invalid LENGTH: %u", byte);
306 return false;
307 }
308 ESP_LOGVV(TAG, "Received LENGTH: %u", byte);
309 this->end_frame_after_ = this->buffer_index_ + byte;
310 ESP_LOGVV(TAG, "Calculated EOF: %u", this->end_frame_after_);
311 this->buffer_[this->buffer_index_++] = byte;
313 break;
315 this->buffer_[this->buffer_index_++] = byte;
316 ESP_LOGVV(TAG, "Received TYPE: 0x%02X", byte);
318 break;
320 this->buffer_[this->buffer_index_++] = byte;
321 ESP_LOGVV(TAG, "Received COMMAND ID: 0x%02X", byte);
323 break;
325 this->buffer_[this->buffer_index_++] = byte;
326 ESP_LOGVV(TAG, "Received PAYLOAD: 0x%02X", byte);
327 if (this->buffer_index_ >= this->end_frame_after_) {
329 }
330 break;
332 this->buffer_[this->buffer_index_++] = byte;
333 auto checksum = calculate_frame_checksum(this->buffer_.data(), this->buffer_index_);
334 ESP_LOGVV(TAG, "CHECKSUM Received: 0x%02X - Calculated: 0x%02X", byte, checksum);
335 if (checksum != byte) {
336 ESP_LOGW(TAG, "Bad checksum: expected 0x%02X, got 0x%02X", checksum, byte);
338 } else {
340#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
341 char hex_buf[format_hex_pretty_size(ZWAVE_MAX_LOG_BYTES)];
342#endif
343 ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_index_));
344 frame_completed = true;
345 }
346 this->response_handler_();
347 break;
348 }
350 if (this->buffer_index_ >= this->buffer_.size()) {
352 break;
353 }
354 this->buffer_[this->buffer_index_++] = byte;
355 if (!byte) {
357 frame_completed = true;
358 }
359 break;
362 break; // Should not happen, handled in loop()
363 default:
364 ESP_LOGW(TAG, "Bad parsing state; resetting");
366 break;
367 }
368 return frame_completed;
369}
370
371void ZWaveProxy::parse_start_(uint8_t byte) {
372 this->buffer_index_ = 0;
374 switch (byte) {
376 ESP_LOGV(TAG, "Received START");
377 if (this->in_bootloader_) {
378 ESP_LOGD(TAG, "Exited bootloader mode");
379 this->in_bootloader_ = false;
380 }
381 this->buffer_[this->buffer_index_++] = byte;
383 return;
385 ESP_LOGV(TAG, "Received BL_MENU");
386 if (!this->in_bootloader_) {
387 ESP_LOGD(TAG, "Entered bootloader mode");
388 this->in_bootloader_ = true;
389 }
390 this->buffer_[this->buffer_index_++] = byte;
392 return;
394 ESP_LOGV(TAG, "Received BL_BEGIN_UPLOAD");
395 break;
397 ESP_LOGV(TAG, "Received ACK");
398 break;
400 ESP_LOGV(TAG, "Received NAK");
401 break;
403 ESP_LOGV(TAG, "Received CAN");
404 break;
405 default:
406 ESP_LOGW(TAG, "Unrecognized START: 0x%02X", byte);
407 return;
408 }
409 // Forward response (ACK/NAK/CAN) back to client for processing
410 if (this->api_connection_ != nullptr) {
411 // Store single byte in buffer and point to it
412 this->buffer_[0] = byte;
413 this->outgoing_proto_msg_.data = this->buffer_.data();
416 }
417}
418
420 switch (this->parsing_state_) {
423 break;
426 break;
429 break;
430 default:
431 return false; // No response handled
432 }
433
434 ESP_LOGVV(TAG, "Sending %s (0x%02X)", this->last_response_ == ZWAVE_FRAME_TYPE_ACK ? "ACK" : "NAK/CAN",
435 this->last_response_);
436 this->write_byte(this->last_response_);
438 return true;
439}
440
441ZWaveProxy *global_zwave_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
442
443} // namespace esphome::zwave_proxy
444
445#endif // USE_API
uint8_t checksum
Definition bl0906.h:3
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 status_clear_warning()
Definition component.h:289
bool send_message(const T &msg)
void on_zwave_proxy_request(const ZWaveProxyRequest &msg)
enums::ZWaveProxyRequestType type
Definition api_pb2.h:3035
UARTComponent * parent_
Definition uart.h:73
bool read_byte(uint8_t *data)
Definition uart.h:34
void write_byte(uint8_t data)
Definition uart.h:18
void write_array(const uint8_t *data, size_t len)
Definition uart.h:26
void zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type)
api::ZWaveProxyFrame outgoing_proto_msg_
void send_frame(const uint8_t *data, size_t length)
void send_homeid_changed_msg_(api::APIConnection *conn=nullptr)
bool set_home_id_(const uint8_t *new_home_id)
void send_simple_command_(uint8_t command_id)
ESPHOME_ALWAYS_INLINE void process_uart_()
Definition zwave_proxy.h:97
void api_connection_authenticated(api::APIConnection *conn)
std::array< uint8_t, MAX_ZWAVE_FRAME_SIZE > buffer_
ESPHOME_ALWAYS_INLINE bool response_handler_()
Definition zwave_proxy.h:86
api::APIConnection * api_connection_
float get_setup_priority() const override
void on_connection_changed_(bool connected)
std::array< uint8_t, ZWAVE_HOME_ID_SIZE > home_id_
uint16_t type
@ ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE
Definition api_pb2.h:327
@ ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE
Definition api_pb2.h:328
@ ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE
Definition api_pb2.h:329
APIServer * global_api_server
constexpr float BEFORE_CONNECTION
For components that should be initialized after WiFi and before API is connected.
Definition component.h:51
ZWaveProxy * global_zwave_proxy
ESPHOME_ALWAYS_INLINE bool api_is_connected()
Return whether the node has at least one client connected to the native API.
Definition util.h:20
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:340
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:1386
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
uint16_t length
Definition tt21100.cpp:0