diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index fa64649df5..ad99de4b4a 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -260,6 +260,7 @@ message DeviceInfoResponse { // Indicates if Z-Wave proxy support is available and features supported uint32 zwave_proxy_feature_flags = 23 [(field_ifdef) = "USE_ZWAVE_PROXY"]; + uint32 zwave_home_id = 24 [(field_ifdef) = "USE_ZWAVE_PROXY"]; } message ListEntitiesRequest { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 697b02b915..a27adfe241 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1474,6 +1474,7 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { #endif #ifdef USE_ZWAVE_PROXY resp.zwave_proxy_feature_flags = zwave_proxy::global_zwave_proxy->get_feature_flags(); + resp.zwave_home_id = zwave_proxy::global_zwave_proxy->get_home_id(); #endif #ifdef USE_API_NOISE resp.api_encryption_supported = true; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 58a083ad06..245933724b 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -132,6 +132,9 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { #ifdef USE_ZWAVE_PROXY buffer.encode_uint32(23, this->zwave_proxy_feature_flags); #endif +#ifdef USE_ZWAVE_PROXY + buffer.encode_uint32(24, this->zwave_home_id); +#endif } void DeviceInfoResponse::calculate_size(ProtoSize &size) const { #ifdef USE_API_PASSWORD @@ -187,6 +190,9 @@ void DeviceInfoResponse::calculate_size(ProtoSize &size) const { #ifdef USE_ZWAVE_PROXY size.add_uint32(2, this->zwave_proxy_feature_flags); #endif +#ifdef USE_ZWAVE_PROXY + size.add_uint32(2, this->zwave_home_id); +#endif } #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index d52cb9eab3..248a4b1f82 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -498,7 +498,7 @@ class DeviceInfo final : public ProtoMessage { class DeviceInfoResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 10; - static constexpr uint8_t ESTIMATED_SIZE = 252; + static constexpr uint16_t ESTIMATED_SIZE = 257; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "device_info_response"; } #endif @@ -561,6 +561,9 @@ class DeviceInfoResponse final : public ProtoMessage { #endif #ifdef USE_ZWAVE_PROXY uint32_t zwave_proxy_feature_flags{0}; +#endif +#ifdef USE_ZWAVE_PROXY + uint32_t zwave_home_id{0}; #endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index d24f9b3fdc..ac43af6d54 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -769,6 +769,9 @@ void DeviceInfoResponse::dump_to(std::string &out) const { #ifdef USE_ZWAVE_PROXY dump_field(out, "zwave_proxy_feature_flags", this->zwave_proxy_feature_flags); #endif +#ifdef USE_ZWAVE_PROXY + dump_field(out, "zwave_home_id", this->zwave_home_id); +#endif } void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); } void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); } diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index e39f857743..12c4ee0c0d 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -8,8 +8,26 @@ namespace zwave_proxy { static const char *const TAG = "zwave_proxy"; +static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20; +// GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...] +static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value +static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 9; // TYPE + CMD + HOME_ID(4) + NODE_ID + checksum + +static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) { + // Calculate Z-Wave frame checksum + // XOR all bytes between SOF and checksum position (exclusive) + // Initial value is 0xFF per Z-Wave protocol specification + uint8_t checksum = 0xFF; + for (uint8_t i = 1; i < length - 1; i++) { + checksum ^= data[i]; + } + return checksum; +} + ZWaveProxy::ZWaveProxy() { global_zwave_proxy = this; } +void ZWaveProxy::setup() { this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS); } + void ZWaveProxy::loop() { if (this->response_handler_()) { ESP_LOGV(TAG, "Handled late response"); @@ -26,6 +44,21 @@ void ZWaveProxy::loop() { return; } if (this->parse_byte_(byte)) { + // Check if this is a GET_NETWORK_IDS response frame + // Frame format: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...] + // We verify: + // - buffer_[0]: Start of frame marker (0x01) + // - buffer_[1]: Length field must be >= 9 to contain all required data + // - buffer_[2]: Command type (0x01 for response) + // - buffer_[3]: Command ID (0x20 for GET_NETWORK_IDS) + if (this->buffer_[3] == ZWAVE_COMMAND_GET_NETWORK_IDS && this->buffer_[2] == ZWAVE_COMMAND_TYPE_RESPONSE && + this->buffer_[1] >= ZWAVE_MIN_GET_NETWORK_IDS_LENGTH && this->buffer_[0] == ZWAVE_FRAME_TYPE_START) { + // Extract the 4-byte Home ID starting at offset 4 + // The frame parser has already validated the checksum and ensured all bytes are present + std::memcpy(this->home_id_.data(), this->buffer_.data() + 4, this->home_id_.size()); + ESP_LOGI(TAG, "Home ID: %s", + format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); + } ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr)); if (this->api_connection_ != nullptr) { // minimize copying to reduce CPU overhead @@ -35,7 +68,7 @@ void ZWaveProxy::loop() { // If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1; } - std::memcpy(this->outgoing_proto_msg_.data, this->buffer_, this->outgoing_proto_msg_.data_len); + std::memcpy(this->outgoing_proto_msg_.data, this->buffer_.data(), this->outgoing_proto_msg_.data_len); this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE); } } @@ -77,6 +110,15 @@ void ZWaveProxy::send_frame(const uint8_t *data, size_t length) { this->write_array(data, length); } +void ZWaveProxy::send_simple_command_(const uint8_t command_id) { + // Send a simple Z-Wave command with no parameters + // Frame format: [SOF][LENGTH][TYPE][CMD][CHECKSUM] + // Where LENGTH=0x03 (3 bytes: TYPE + CMD + CHECKSUM) + uint8_t cmd[] = {0x01, 0x03, 0x00, command_id, 0x00}; + cmd[4] = calculate_frame_checksum(cmd, sizeof(cmd)); + this->send_frame(cmd, sizeof(cmd)); +} + bool ZWaveProxy::parse_byte_(uint8_t byte) { bool frame_completed = false; // Basic parsing logic for received frames @@ -94,43 +136,40 @@ bool ZWaveProxy::parse_byte_(uint8_t byte) { this->end_frame_after_ = this->buffer_index_ + byte; ESP_LOGVV(TAG, "Calculated EOF: %u", this->end_frame_after_); this->buffer_[this->buffer_index_++] = byte; - this->checksum_ ^= byte; this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_TYPE; break; case ZWAVE_PARSING_STATE_WAIT_TYPE: this->buffer_[this->buffer_index_++] = byte; ESP_LOGVV(TAG, "Received TYPE: 0x%02X", byte); - this->checksum_ ^= byte; this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_COMMAND_ID; break; case ZWAVE_PARSING_STATE_WAIT_COMMAND_ID: this->buffer_[this->buffer_index_++] = byte; ESP_LOGVV(TAG, "Received COMMAND ID: 0x%02X", byte); - this->checksum_ ^= byte; this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_PAYLOAD; break; case ZWAVE_PARSING_STATE_WAIT_PAYLOAD: this->buffer_[this->buffer_index_++] = byte; - this->checksum_ ^= byte; ESP_LOGVV(TAG, "Received PAYLOAD: 0x%02X", byte); if (this->buffer_index_ >= this->end_frame_after_) { this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_CHECKSUM; } break; - case ZWAVE_PARSING_STATE_WAIT_CHECKSUM: + case ZWAVE_PARSING_STATE_WAIT_CHECKSUM: { this->buffer_[this->buffer_index_++] = byte; - ESP_LOGVV(TAG, "Received CHECKSUM: 0x%02X", byte); - ESP_LOGV(TAG, "Calculated CHECKSUM: 0x%02X", this->checksum_); - if (this->checksum_ != byte) { - ESP_LOGW(TAG, "Bad checksum: expected 0x%02X, got 0x%02X", this->checksum_, byte); + auto checksum = calculate_frame_checksum(this->buffer_.data(), this->buffer_index_); + ESP_LOGVV(TAG, "CHECKSUM Received: 0x%02X - Calculated: 0x%02X", byte, checksum); + if (checksum != byte) { + ESP_LOGW(TAG, "Bad checksum: expected 0x%02X, got 0x%02X", checksum, byte); this->parsing_state_ = ZWAVE_PARSING_STATE_SEND_NAK; } else { this->parsing_state_ = ZWAVE_PARSING_STATE_SEND_ACK; - ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(this->buffer_, this->buffer_index_).c_str()); + ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(this->buffer_.data(), this->buffer_index_).c_str()); frame_completed = true; } this->response_handler_(); break; + } case ZWAVE_PARSING_STATE_READ_BL_MENU: this->buffer_[this->buffer_index_++] = byte; if (!byte) { @@ -151,7 +190,6 @@ bool ZWaveProxy::parse_byte_(uint8_t byte) { void ZWaveProxy::parse_start_(uint8_t byte) { this->buffer_index_ = 0; - this->checksum_ = 0xFF; this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; switch (byte) { case ZWAVE_FRAME_TYPE_START: diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h index a0f25849e4..5d908b328c 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.h +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -3,8 +3,11 @@ #include "esphome/components/api/api_connection.h" #include "esphome/components/api/api_pb2.h" #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/components/uart/uart.h" +#include + namespace esphome { namespace zwave_proxy { @@ -38,6 +41,7 @@ class ZWaveProxy : public uart::UARTDevice, public Component { public: ZWaveProxy(); + void setup() override; void loop() override; void dump_config() override; @@ -45,21 +49,25 @@ class ZWaveProxy : public uart::UARTDevice, public Component { api::APIConnection *get_api_connection() { return this->api_connection_; } uint32_t get_feature_flags() const { return ZWaveProxyFeature::FEATURE_ZWAVE_PROXY_ENABLED; } + uint32_t get_home_id() { + return encode_uint32(this->home_id_[0], this->home_id_[1], this->home_id_[2], this->home_id_[3]); + } void send_frame(const uint8_t *data, size_t length); protected: + void send_simple_command_(uint8_t command_id); bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer) void parse_start_(uint8_t byte); bool response_handler_(); api::APIConnection *api_connection_{nullptr}; // Current subscribed client - uint8_t buffer_[sizeof(api::ZWaveProxyFrame::data)]; // Fixed buffer for incoming data - uint8_t buffer_index_{0}; // Index for populating the data buffer - uint8_t checksum_{0}; // Checksum of the frame being parsed - uint8_t end_frame_after_{0}; // Payload reception ends after this index - uint8_t last_response_{0}; // Last response type sent + std::array home_id_{0, 0, 0, 0}; // Fixed buffer for home ID + std::array buffer_; // Fixed buffer for incoming data + uint8_t buffer_index_{0}; // Index for populating the data buffer + uint8_t end_frame_after_{0}; // Payload reception ends after this index + uint8_t last_response_{0}; // Last response type sent ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START}; bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode