ESPHome 2026.1.5
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
8#include "esphome/core/log.h"
9#include "esphome/core/util.h"
10
12
13static const char *const TAG = "zwave_proxy";
14
15// Maximum bytes to log in very verbose hex output (168 * 3 = 504, under TX buffer size of 512)
16static constexpr size_t ZWAVE_MAX_LOG_BYTES = 168;
17
18static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20;
19// GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...]
20static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value
21static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 9; // TYPE + CMD + HOME_ID(4) + NODE_ID + checksum
22static constexpr uint32_t HOME_ID_TIMEOUT_MS = 100; // Timeout for waiting for home ID during setup
23
24static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) {
25 // Calculate Z-Wave frame checksum
26 // XOR all bytes between SOF and checksum position (exclusive)
27 // Initial value is 0xFF per Z-Wave protocol specification
28 uint8_t checksum = 0xFF;
29 for (uint8_t i = 1; i < length - 1; i++) {
30 checksum ^= data[i];
31 }
32 return checksum;
33}
34
36
39 this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS);
40}
41
43 // Set up before API so home ID is ready when API starts
45}
46
48 // If we already have the home ID, we can proceed
49 if (this->home_id_ready_) {
50 return true;
51 }
52
53 // Handle any pending responses
54 if (this->response_handler_()) {
55 ESP_LOGV(TAG, "Handled response during setup");
56 }
57
58 // Process UART data to check for home ID
59 this->process_uart_();
60
61 // Check if we got the home ID after processing
62 if (this->home_id_ready_) {
63 return true;
64 }
65
66 // Wait up to HOME_ID_TIMEOUT_MS for home ID response
67 const uint32_t now = App.get_loop_component_start_time();
68 if (now - this->setup_time_ > HOME_ID_TIMEOUT_MS) {
69 ESP_LOGW(TAG, "Timeout reading Home ID during setup");
70 return true; // Proceed anyway after timeout
71 }
72
73 return false; // Keep waiting
74}
75
77 if (this->response_handler_()) {
78 ESP_LOGV(TAG, "Handled late response");
79 }
80 if (this->api_connection_ != nullptr && (!this->api_connection_->is_connection_setup() || !api_is_connected())) {
81 ESP_LOGW(TAG, "Subscriber disconnected");
82 this->api_connection_ = nullptr; // Unsubscribe if disconnected
83 }
84
85 this->process_uart_();
87}
88
90 while (this->available()) {
91 uint8_t byte;
92 if (!this->read_byte(&byte)) {
93 this->status_set_warning("UART read failed");
94 return;
95 }
96 if (this->parse_byte_(byte)) {
97 // Check if this is a GET_NETWORK_IDS response frame
98 // Frame format: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...]
99 // We verify:
100 // - buffer_[0]: Start of frame marker (0x01)
101 // - buffer_[1]: Length field must be >= 9 to contain all required data
102 // - buffer_[2]: Command type (0x01 for response)
103 // - buffer_[3]: Command ID (0x20 for GET_NETWORK_IDS)
104 if (this->buffer_[3] == ZWAVE_COMMAND_GET_NETWORK_IDS && this->buffer_[2] == ZWAVE_COMMAND_TYPE_RESPONSE &&
105 this->buffer_[1] >= ZWAVE_MIN_GET_NETWORK_IDS_LENGTH && this->buffer_[0] == ZWAVE_FRAME_TYPE_START) {
106 // Store the 4-byte Home ID, which starts at offset 4, and notify connected clients if it changed
107 // The frame parser has already validated the checksum and ensured all bytes are present
108 if (this->set_home_id_(&this->buffer_[4])) {
110 }
111 }
112 ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr));
113 if (this->api_connection_ != nullptr) {
114 // Zero-copy: point directly to our buffer
115 this->outgoing_proto_msg_.data = this->buffer_.data();
116 if (this->in_bootloader_) {
118 } else {
119 // If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN
120 this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1;
121 }
123 }
124 }
125 }
126}
127
129 char hex_buf[format_hex_pretty_size(ZWAVE_HOME_ID_SIZE)];
130 ESP_LOGCONFIG(TAG,
131 "Z-Wave Proxy:\n"
132 " Home ID: %s",
133 format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size()));
134}
135
137 if (this->home_id_ready_) {
138 // If a client just authenticated & HomeID is ready, send the current HomeID
139 this->send_homeid_changed_msg_(conn);
140 }
141}
142
144 switch (type) {
146 if (this->api_connection_ != nullptr) {
147 ESP_LOGE(TAG, "Only one API subscription is allowed at a time");
148 return;
149 }
150 this->api_connection_ = api_connection;
151 ESP_LOGV(TAG, "API connection is now subscribed");
152 break;
153
155 if (this->api_connection_ != api_connection) {
156 ESP_LOGV(TAG, "API connection is not subscribed");
157 return;
158 }
159 this->api_connection_ = nullptr;
160 break;
161
162 default:
163 ESP_LOGW(TAG, "Unknown request type: %d", type);
164 break;
165 }
166}
167
168bool ZWaveProxy::set_home_id_(const uint8_t *new_home_id) {
169 if (std::memcmp(this->home_id_.data(), new_home_id, this->home_id_.size()) == 0) {
170 ESP_LOGV(TAG, "Home ID unchanged");
171 return false; // No change
172 }
173 std::memcpy(this->home_id_.data(), new_home_id, this->home_id_.size());
174 char hex_buf[format_hex_pretty_size(ZWAVE_HOME_ID_SIZE)];
175 ESP_LOGI(TAG, "Home ID: %s", format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size()));
176 this->home_id_ready_ = true;
177 return true; // Home ID was changed
178}
179
180void ZWaveProxy::send_frame(const uint8_t *data, size_t length) {
181 // Safety: validate pointer before any access
182 if (data == nullptr) {
183 ESP_LOGE(TAG, "Null data pointer");
184 return;
185 }
186 if (length == 0) {
187 ESP_LOGE(TAG, "Length 0");
188 return;
189 }
190
191 // Skip duplicate single-byte responses (ACK/NAK/CAN)
192 if (length == 1 && data[0] == this->last_response_) {
193 ESP_LOGV(TAG, "Response already sent: 0x%02X", data[0]);
194 return;
195 }
196
197#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
198 char hex_buf[format_hex_pretty_size(ZWAVE_MAX_LOG_BYTES)];
199#endif
200 ESP_LOGVV(TAG, "Sending: %s", format_hex_pretty_to(hex_buf, data, length));
201
202 this->write_array(data, length);
203}
204
208 msg.data = this->home_id_.data();
209 msg.data_len = this->home_id_.size();
210 if (conn != nullptr) {
211 // Send to specific connection
213 } else if (api::global_api_server != nullptr) {
214 // We could add code to manage a second subscription type, but, since this message is
215 // very infrequent and small, we simply send it to all clients
217 }
218}
219
220void ZWaveProxy::send_simple_command_(const uint8_t command_id) {
221 // Send a simple Z-Wave command with no parameters
222 // Frame format: [SOF][LENGTH][TYPE][CMD][CHECKSUM]
223 // Where LENGTH=0x03 (3 bytes: TYPE + CMD + CHECKSUM)
224 uint8_t cmd[] = {0x01, 0x03, 0x00, command_id, 0x00};
225 cmd[4] = calculate_frame_checksum(cmd, sizeof(cmd));
226 this->send_frame(cmd, sizeof(cmd));
227}
228
229bool ZWaveProxy::parse_byte_(uint8_t byte) {
230 bool frame_completed = false;
231 // Basic parsing logic for received frames
232 switch (this->parsing_state_) {
234 this->parse_start_(byte);
235 break;
237 if (!byte) {
238 ESP_LOGW(TAG, "Invalid LENGTH: %u", byte);
240 return false;
241 }
242 ESP_LOGVV(TAG, "Received LENGTH: %u", byte);
243 this->end_frame_after_ = this->buffer_index_ + byte;
244 ESP_LOGVV(TAG, "Calculated EOF: %u", this->end_frame_after_);
245 this->buffer_[this->buffer_index_++] = byte;
247 break;
249 this->buffer_[this->buffer_index_++] = byte;
250 ESP_LOGVV(TAG, "Received TYPE: 0x%02X", byte);
252 break;
254 this->buffer_[this->buffer_index_++] = byte;
255 ESP_LOGVV(TAG, "Received COMMAND ID: 0x%02X", byte);
257 break;
259 this->buffer_[this->buffer_index_++] = byte;
260 ESP_LOGVV(TAG, "Received PAYLOAD: 0x%02X", byte);
261 if (this->buffer_index_ >= this->end_frame_after_) {
263 }
264 break;
266 this->buffer_[this->buffer_index_++] = byte;
267 auto checksum = calculate_frame_checksum(this->buffer_.data(), this->buffer_index_);
268 ESP_LOGVV(TAG, "CHECKSUM Received: 0x%02X - Calculated: 0x%02X", byte, checksum);
269 if (checksum != byte) {
270 ESP_LOGW(TAG, "Bad checksum: expected 0x%02X, got 0x%02X", checksum, byte);
272 } else {
274#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
275 char hex_buf[format_hex_pretty_size(ZWAVE_MAX_LOG_BYTES)];
276#endif
277 ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_index_));
278 frame_completed = true;
279 }
280 this->response_handler_();
281 break;
282 }
284 this->buffer_[this->buffer_index_++] = byte;
285 if (!byte) {
287 frame_completed = true;
288 }
289 break;
292 break; // Should not happen, handled in loop()
293 default:
294 ESP_LOGW(TAG, "Bad parsing state; resetting");
296 break;
297 }
298 return frame_completed;
299}
300
301void ZWaveProxy::parse_start_(uint8_t byte) {
302 this->buffer_index_ = 0;
304 switch (byte) {
306 ESP_LOGVV(TAG, "Received START");
307 if (this->in_bootloader_) {
308 ESP_LOGD(TAG, "Exited bootloader mode");
309 this->in_bootloader_ = false;
310 }
311 this->buffer_[this->buffer_index_++] = byte;
313 return;
315 ESP_LOGVV(TAG, "Received BL_MENU");
316 if (!this->in_bootloader_) {
317 ESP_LOGD(TAG, "Entered bootloader mode");
318 this->in_bootloader_ = true;
319 }
320 this->buffer_[this->buffer_index_++] = byte;
322 return;
324 ESP_LOGVV(TAG, "Received BL_BEGIN_UPLOAD");
325 break;
327 ESP_LOGVV(TAG, "Received ACK");
328 break;
330 ESP_LOGW(TAG, "Received NAK");
331 break;
333 ESP_LOGW(TAG, "Received CAN");
334 break;
335 default:
336 ESP_LOGW(TAG, "Unrecognized START: 0x%02X", byte);
337 return;
338 }
339 // Forward response (ACK/NAK/CAN) back to client for processing
340 if (this->api_connection_ != nullptr) {
341 // Store single byte in buffer and point to it
342 this->buffer_[0] = byte;
343 this->outgoing_proto_msg_.data = this->buffer_.data();
346 }
347}
348
350 switch (this->parsing_state_) {
353 break;
356 break;
359 break;
360 default:
361 return false; // No response handled
362 }
363
364 ESP_LOGVV(TAG, "Sending %s (0x%02X)", this->last_response_ == ZWAVE_FRAME_TYPE_ACK ? "ACK" : "NAK/CAN",
365 this->last_response_);
366 this->write_byte(this->last_response_);
368 return true;
369}
370
371ZWaveProxy *global_zwave_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
372
373} // namespace esphome::zwave_proxy
374
375#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_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:3015
static constexpr uint8_t MESSAGE_TYPE
Definition api_pb2.h:3033
enums::ZWaveProxyRequestType type
Definition api_pb2.h:3038
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_
Definition zwave_proxy.h:76
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)
void api_connection_authenticated(api::APIConnection *conn)
ZWaveParsingState parsing_state_
Definition zwave_proxy.h:88
std::array< uint8_t, MAX_ZWAVE_FRAME_SIZE > buffer_
Definition zwave_proxy.h:77
api::APIConnection * api_connection_
Definition zwave_proxy.h:81
float get_setup_priority() const override
std::array< uint8_t, ZWAVE_HOME_ID_SIZE > home_id_
Definition zwave_proxy.h:78
uint16_t type
@ ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE
Definition api_pb2.h:307
@ ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE
Definition api_pb2.h:308
@ ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE
Definition api_pb2.h:309
APIServer * global_api_server
const float BEFORE_CONNECTION
For components that should be initialized after WiFi and before API is connected.
Definition component.cpp:87
ZWaveProxy * global_zwave_proxy
bool api_is_connected()
Return whether the node has at least one client connected to the native API.
Definition util.cpp:17
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:334
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:830
Application App
Global storage of Application pointer - only one Application can exist.
uint16_t length
Definition tt21100.cpp:0