ESPHome 2025.12.0-dev
Loading...
Searching...
No Matches
zwave_proxy.cpp
Go to the documentation of this file.
1#include "zwave_proxy.h"
5#include "esphome/core/log.h"
6#include "esphome/core/util.h"
7
8namespace esphome {
9namespace zwave_proxy {
10
11static const char *const TAG = "zwave_proxy";
12
13static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20;
14// GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...]
15static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value
16static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 9; // TYPE + CMD + HOME_ID(4) + NODE_ID + checksum
17static constexpr uint32_t HOME_ID_TIMEOUT_MS = 100; // Timeout for waiting for home ID during setup
18
19static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) {
20 // Calculate Z-Wave frame checksum
21 // XOR all bytes between SOF and checksum position (exclusive)
22 // Initial value is 0xFF per Z-Wave protocol specification
23 uint8_t checksum = 0xFF;
24 for (uint8_t i = 1; i < length - 1; i++) {
25 checksum ^= data[i];
26 }
27 return checksum;
28}
29
31
34 this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS);
35}
36
38 // Set up before API so home ID is ready when API starts
40}
41
43 // If we already have the home ID, we can proceed
44 if (this->home_id_ready_) {
45 return true;
46 }
47
48 // Handle any pending responses
49 if (this->response_handler_()) {
50 ESP_LOGV(TAG, "Handled response during setup");
51 }
52
53 // Process UART data to check for home ID
54 this->process_uart_();
55
56 // Check if we got the home ID after processing
57 if (this->home_id_ready_) {
58 return true;
59 }
60
61 // Wait up to HOME_ID_TIMEOUT_MS for home ID response
62 const uint32_t now = App.get_loop_component_start_time();
63 if (now - this->setup_time_ > HOME_ID_TIMEOUT_MS) {
64 ESP_LOGW(TAG, "Timeout reading Home ID during setup");
65 return true; // Proceed anyway after timeout
66 }
67
68 return false; // Keep waiting
69}
70
72 if (this->response_handler_()) {
73 ESP_LOGV(TAG, "Handled late response");
74 }
75 if (this->api_connection_ != nullptr && (!this->api_connection_->is_connection_setup() || !api_is_connected())) {
76 ESP_LOGW(TAG, "Subscriber disconnected");
77 this->api_connection_ = nullptr; // Unsubscribe if disconnected
78 }
79
80 this->process_uart_();
82}
83
85 while (this->available()) {
86 uint8_t byte;
87 if (!this->read_byte(&byte)) {
88 this->status_set_warning("UART read failed");
89 return;
90 }
91 if (this->parse_byte_(byte)) {
92 // Check if this is a GET_NETWORK_IDS response frame
93 // Frame format: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...]
94 // We verify:
95 // - buffer_[0]: Start of frame marker (0x01)
96 // - buffer_[1]: Length field must be >= 9 to contain all required data
97 // - buffer_[2]: Command type (0x01 for response)
98 // - buffer_[3]: Command ID (0x20 for GET_NETWORK_IDS)
99 if (this->buffer_[3] == ZWAVE_COMMAND_GET_NETWORK_IDS && this->buffer_[2] == ZWAVE_COMMAND_TYPE_RESPONSE &&
100 this->buffer_[1] >= ZWAVE_MIN_GET_NETWORK_IDS_LENGTH && this->buffer_[0] == ZWAVE_FRAME_TYPE_START) {
101 // Store the 4-byte Home ID, which starts at offset 4, and notify connected clients if it changed
102 // The frame parser has already validated the checksum and ensured all bytes are present
103 if (this->set_home_id(&this->buffer_[4])) {
105 }
106 }
107 ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr));
108 if (this->api_connection_ != nullptr) {
109 // Zero-copy: point directly to our buffer
110 this->outgoing_proto_msg_.data = this->buffer_.data();
111 if (this->in_bootloader_) {
113 } else {
114 // If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN
115 this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1;
116 }
118 }
119 }
120 }
121}
122
124 ESP_LOGCONFIG(TAG,
125 "Z-Wave Proxy:\n"
126 " Home ID: %s",
127 format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str());
128}
129
131 if (this->home_id_ready_) {
132 // If a client just authenticated & HomeID is ready, send the current HomeID
133 this->send_homeid_changed_msg_(conn);
134 }
135}
136
138 switch (type) {
140 if (this->api_connection_ != nullptr) {
141 ESP_LOGE(TAG, "Only one API subscription is allowed at a time");
142 return;
143 }
144 this->api_connection_ = api_connection;
145 ESP_LOGV(TAG, "API connection is now subscribed");
146 break;
148 if (this->api_connection_ != api_connection) {
149 ESP_LOGV(TAG, "API connection is not subscribed");
150 return;
151 }
152 this->api_connection_ = nullptr;
153 break;
154 default:
155 ESP_LOGW(TAG, "Unknown request type: %d", type);
156 break;
157 }
158}
159
160bool ZWaveProxy::set_home_id(const uint8_t *new_home_id) {
161 if (std::memcmp(this->home_id_.data(), new_home_id, this->home_id_.size()) == 0) {
162 ESP_LOGV(TAG, "Home ID unchanged");
163 return false; // No change
164 }
165 std::memcpy(this->home_id_.data(), new_home_id, this->home_id_.size());
166 ESP_LOGI(TAG, "Home ID: %s", format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str());
167 this->home_id_ready_ = true;
168 return true; // Home ID was changed
169}
170
171void ZWaveProxy::send_frame(const uint8_t *data, size_t length) {
172 if (length == 1 && data[0] == this->last_response_) {
173 ESP_LOGV(TAG, "Skipping sending duplicate response: 0x%02X", data[0]);
174 return;
175 }
176 ESP_LOGVV(TAG, "Sending: %s", format_hex_pretty(data, length).c_str());
177 this->write_array(data, length);
178}
179
183 msg.data = this->home_id_.data();
184 msg.data_len = this->home_id_.size();
185 if (conn != nullptr) {
186 // Send to specific connection
188 } else if (api::global_api_server != nullptr) {
189 // We could add code to manage a second subscription type, but, since this message is
190 // very infrequent and small, we simply send it to all clients
192 }
193}
194
195void ZWaveProxy::send_simple_command_(const uint8_t command_id) {
196 // Send a simple Z-Wave command with no parameters
197 // Frame format: [SOF][LENGTH][TYPE][CMD][CHECKSUM]
198 // Where LENGTH=0x03 (3 bytes: TYPE + CMD + CHECKSUM)
199 uint8_t cmd[] = {0x01, 0x03, 0x00, command_id, 0x00};
200 cmd[4] = calculate_frame_checksum(cmd, sizeof(cmd));
201 this->send_frame(cmd, sizeof(cmd));
202}
203
204bool ZWaveProxy::parse_byte_(uint8_t byte) {
205 bool frame_completed = false;
206 // Basic parsing logic for received frames
207 switch (this->parsing_state_) {
209 this->parse_start_(byte);
210 break;
212 if (!byte) {
213 ESP_LOGW(TAG, "Invalid LENGTH: %u", byte);
215 return false;
216 }
217 ESP_LOGVV(TAG, "Received LENGTH: %u", byte);
218 this->end_frame_after_ = this->buffer_index_ + byte;
219 ESP_LOGVV(TAG, "Calculated EOF: %u", this->end_frame_after_);
220 this->buffer_[this->buffer_index_++] = byte;
222 break;
224 this->buffer_[this->buffer_index_++] = byte;
225 ESP_LOGVV(TAG, "Received TYPE: 0x%02X", byte);
227 break;
229 this->buffer_[this->buffer_index_++] = byte;
230 ESP_LOGVV(TAG, "Received COMMAND ID: 0x%02X", byte);
232 break;
234 this->buffer_[this->buffer_index_++] = byte;
235 ESP_LOGVV(TAG, "Received PAYLOAD: 0x%02X", byte);
236 if (this->buffer_index_ >= this->end_frame_after_) {
238 }
239 break;
241 this->buffer_[this->buffer_index_++] = byte;
242 auto checksum = calculate_frame_checksum(this->buffer_.data(), this->buffer_index_);
243 ESP_LOGVV(TAG, "CHECKSUM Received: 0x%02X - Calculated: 0x%02X", byte, checksum);
244 if (checksum != byte) {
245 ESP_LOGW(TAG, "Bad checksum: expected 0x%02X, got 0x%02X", checksum, byte);
247 } else {
249 ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(this->buffer_.data(), this->buffer_index_).c_str());
250 frame_completed = true;
251 }
252 this->response_handler_();
253 break;
254 }
256 this->buffer_[this->buffer_index_++] = byte;
257 if (!byte) {
259 frame_completed = true;
260 }
261 break;
264 break; // Should not happen, handled in loop()
265 default:
266 ESP_LOGW(TAG, "Bad parsing state; resetting");
268 break;
269 }
270 return frame_completed;
271}
272
273void ZWaveProxy::parse_start_(uint8_t byte) {
274 this->buffer_index_ = 0;
276 switch (byte) {
278 ESP_LOGVV(TAG, "Received START");
279 if (this->in_bootloader_) {
280 ESP_LOGD(TAG, "Exited bootloader mode");
281 this->in_bootloader_ = false;
282 }
283 this->buffer_[this->buffer_index_++] = byte;
285 return;
287 ESP_LOGVV(TAG, "Received BL_MENU");
288 if (!this->in_bootloader_) {
289 ESP_LOGD(TAG, "Entered bootloader mode");
290 this->in_bootloader_ = true;
291 }
292 this->buffer_[this->buffer_index_++] = byte;
294 return;
296 ESP_LOGVV(TAG, "Received BL_BEGIN_UPLOAD");
297 break;
299 ESP_LOGVV(TAG, "Received ACK");
300 break;
302 ESP_LOGW(TAG, "Received NAK");
303 break;
305 ESP_LOGW(TAG, "Received CAN");
306 break;
307 default:
308 ESP_LOGW(TAG, "Unrecognized START: 0x%02X", byte);
309 return;
310 }
311 // Forward response (ACK/NAK/CAN) back to client for processing
312 if (this->api_connection_ != nullptr) {
313 // Store single byte in buffer and point to it
314 this->buffer_[0] = byte;
315 this->outgoing_proto_msg_.data = this->buffer_.data();
318 }
319}
320
322 switch (this->parsing_state_) {
325 break;
328 break;
331 break;
332 default:
333 return false; // No response handled
334 }
335
336 ESP_LOGVV(TAG, "Sending %s (0x%02X)", this->last_response_ == ZWAVE_FRAME_TYPE_ACK ? "ACK" : "NAK/CAN",
337 this->last_response_);
338 this->write_byte(this->last_response_);
340 return true;
341}
342
343ZWaveProxy *global_zwave_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
344
345} // namespace zwave_proxy
346} // namespace esphome
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_set_warning(const char *message=nullptr)
void status_clear_warning()
bool is_connection_setup() override
bool send_message(const ProtoMessage &msg, uint8_t message_type)
void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg)
static constexpr uint8_t MESSAGE_TYPE
Definition api_pb2.h:2992
static constexpr uint8_t MESSAGE_TYPE
Definition api_pb2.h:3010
enums::ZWaveProxyRequestType type
Definition api_pb2.h:3015
bool read_byte(uint8_t *data)
Definition uart.h:35
void write_byte(uint8_t data)
Definition uart.h:19
void write_array(const uint8_t *data, size_t len)
Definition uart.h:27
void zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type)
api::ZWaveProxyFrame outgoing_proto_msg_
Definition zwave_proxy.h:73
void send_frame(const uint8_t *data, size_t length)
void send_homeid_changed_msg_(api::APIConnection *conn=nullptr)
void send_simple_command_(uint8_t command_id)
bool set_home_id(const uint8_t *new_home_id)
void api_connection_authenticated(api::APIConnection *conn)
ZWaveParsingState parsing_state_
Definition zwave_proxy.h:85
std::array< uint8_t, MAX_ZWAVE_FRAME_SIZE > buffer_
Definition zwave_proxy.h:74
std::array< uint8_t, 4 > home_id_
Definition zwave_proxy.h:75
api::APIConnection * api_connection_
Definition zwave_proxy.h:78
float get_setup_priority() const override
uint16_t type
@ ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE
Definition api_pb2.h:281
@ ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE
Definition api_pb2.h:282
@ ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE
Definition api_pb2.h:283
APIServer * global_api_server
const float BEFORE_CONNECTION
For components that should be initialized after WiFi and before API is connected.
Definition component.cpp:65
ZWaveProxy * global_zwave_proxy
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
bool api_is_connected()
Return whether the node has at least one client connected to the native API.
Definition util.cpp:17
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length)
Format a byte array in pretty-printed, human-readable hex format.
Definition helpers.cpp:317
Application App
Global storage of Application pointer - only one Application can exist.
uint16_t length
Definition tt21100.cpp:0