From 9dd6be4061c8b6a47362d42debb66050bb35769c Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 27 Sep 2025 17:50:18 -0500 Subject: [PATCH] [zwave_proxy, api] Add notification message when Z-Wave HomeID changes (#10860) --- esphome/components/api/api.proto | 4 ++- esphome/components/api/api_pb2.cpp | 21 +++++++++++ esphome/components/api/api_pb2.h | 8 ++++- esphome/components/api/api_pb2_dump.cpp | 5 +++ esphome/components/api/api_server.cpp | 9 +++++ esphome/components/api/api_server.h | 3 ++ .../components/zwave_proxy/zwave_proxy.cpp | 36 +++++++++++++++---- esphome/components/zwave_proxy/zwave_proxy.h | 1 + 8 files changed, 79 insertions(+), 8 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 9273cca2d3..0e385c4a17 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2310,11 +2310,13 @@ message ZWaveProxyFrame { enum ZWaveProxyRequestType { ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; + ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2; } message ZWaveProxyRequest { option (id) = 129; - option (source) = SOURCE_CLIENT; + option (source) = SOURCE_BOTH; option (ifdef) = "USE_ZWAVE_PROXY"; ZWaveProxyRequestType type = 1; + bytes data = 2 [(pointer_to_buffer) = true]; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 410ba2334e..0140c60e5b 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3112,6 +3112,27 @@ bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } return true; } +bool ZWaveProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + // Use raw data directly to avoid allocation + this->data = value.data(); + this->data_len = value.size(); + break; + } + default: + return false; + } + return true; +} +void ZWaveProxyRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint32(1, static_cast(this->type)); + buffer.encode_bytes(2, this->data, this->data_len); +} +void ZWaveProxyRequest::calculate_size(ProtoSize &size) const { + size.add_uint32(1, static_cast(this->type)); + size.add_length(2, this->data_len); +} #endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index ee8472b21c..d71ee9777d 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -280,6 +280,7 @@ enum UpdateCommand : uint32_t { enum ZWaveProxyRequestType : uint32_t { ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0, ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, + ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2, }; #endif @@ -2971,16 +2972,21 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage { class ZWaveProxyRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 129; - static constexpr uint8_t ESTIMATED_SIZE = 2; + static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "z_wave_proxy_request"; } #endif enums::ZWaveProxyRequestType type{}; + const uint8_t *data{nullptr}; + uint16_t data_len{0}; + void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; #endif diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index a5494168f9..c5f1d99dd4 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -662,6 +662,8 @@ template<> const char *proto_enum_to_string(enums: return "ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE"; case enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE: return "ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE"; + case enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE: + return "ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE"; default: return "UNKNOWN"; } @@ -2161,6 +2163,9 @@ void ZWaveProxyFrame::dump_to(std::string &out) const { void ZWaveProxyRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ZWaveProxyRequest"); dump_field(out, "type", static_cast(this->type)); + out.append(" data: "); + out.append(format_hex_pretty(this->data, this->data_len)); + out.append("\n"); } #endif diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index ff94b92b5d..1d5c06092f 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -356,6 +356,15 @@ void APIServer::on_update(update::UpdateEntity *obj) { } #endif +#ifdef USE_ZWAVE_PROXY +void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) { + // We could add code to manage a second subscription type, but, since this message type is + // very infrequent and small, we simply send it to all clients + for (auto &c : this->clients_) + c->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE); +} +#endif + #ifdef USE_ALARM_CONTROL_PANEL API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel) #endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 354c764825..627870af1d 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -126,6 +126,9 @@ class APIServer : public Component, public Controller { #ifdef USE_UPDATE void on_update(update::UpdateEntity *obj) override; #endif +#ifdef USE_ZWAVE_PROXY + void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); +#endif bool is_connected() const; diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index feaf6e2d42..70932da87c 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -1,4 +1,5 @@ #include "zwave_proxy.h" +#include "esphome/components/api/api_server.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -97,12 +98,19 @@ void ZWaveProxy::process_uart_() { // - 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 + // Store the 4-byte Home ID, which starts at offset 4, and notify connected clients if it changed // 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()); - this->home_id_ready_ = true; - ESP_LOGI(TAG, "Home ID: %s", - format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); + if (this->set_home_id(&this->buffer_[4])) { + api::ZWaveProxyRequest msg; + msg.type = api::enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE; + msg.data = this->home_id_.data(); + msg.data_len = this->home_id_.size(); + if (api::global_api_server != nullptr) { + // We could add code to manage a second subscription type, but, since this message is + // very infrequent and small, we simply send it to all clients + api::global_api_server->on_zwave_proxy_request(msg); + } + } } ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr)); if (this->api_connection_ != nullptr) { @@ -120,7 +128,12 @@ void ZWaveProxy::process_uart_() { } } -void ZWaveProxy::dump_config() { ESP_LOGCONFIG(TAG, "Z-Wave Proxy"); } +void ZWaveProxy::dump_config() { + ESP_LOGCONFIG(TAG, + "Z-Wave Proxy:\n" + " Home ID: %s", + format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); +} void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type) { switch (type) { @@ -145,6 +158,17 @@ void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::en } } +bool ZWaveProxy::set_home_id(const uint8_t *new_home_id) { + if (std::memcmp(this->home_id_.data(), new_home_id, this->home_id_.size()) == 0) { + ESP_LOGV(TAG, "Home ID unchanged"); + return false; // No change + } + std::memcpy(this->home_id_.data(), new_home_id, this->home_id_.size()); + ESP_LOGI(TAG, "Home ID: %s", format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); + this->home_id_ready_ = true; + return true; // Home ID was changed +} + void ZWaveProxy::send_frame(const uint8_t *data, size_t length) { if (length == 1 && data[0] == this->last_response_) { ESP_LOGV(TAG, "Skipping sending duplicate response: 0x%02X", data[0]); diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h index ea6837888b..a9123a81ca 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.h +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -56,6 +56,7 @@ class ZWaveProxy : public uart::UARTDevice, public Component { uint32_t get_home_id() { return encode_uint32(this->home_id_[0], this->home_id_[1], this->home_id_[2], this->home_id_[3]); } + bool set_home_id(const uint8_t *new_home_id); // Store a new home ID. Returns true if it changed. void send_frame(const uint8_t *data, size_t length);