From c70eab931e003be5704ebdc6261f62c7357537c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Dec 2025 07:31:54 -1000 Subject: [PATCH] [api] Add zero-copy support for Home Assistant state response messages (#12585) --- esphome/components/api/api.proto | 6 ++--- esphome/components/api/api_connection.cpp | 28 +++++++++++++++------ esphome/components/api/api_pb2.cpp | 21 +++++++++++----- esphome/components/api/api_pb2.h | 11 +++++--- esphome/components/api/api_pb2_dump.cpp | 12 ++++++--- tests/integration/test_api_homeassistant.py | 6 +++++ 6 files changed, 61 insertions(+), 23 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 5d44d7e549..bf39f0b14b 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -824,9 +824,9 @@ message HomeAssistantStateResponse { option (no_delay) = true; option (ifdef) = "USE_API_HOMEASSISTANT_STATES"; - string entity_id = 1; - string state = 2; - string attribute = 3; + string entity_id = 1 [(pointer_to_buffer) = true]; + string state = 2 [(pointer_to_buffer) = true]; + string attribute = 3 [(pointer_to_buffer) = true]; } // ==================== IMPORT TIME ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 0f551d1bc3..1bcb90b0b0 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1582,15 +1582,29 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { #ifdef USE_API_HOMEASSISTANT_STATES void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) { - for (auto &it : this->parent_->get_state_subs()) { - // Compare entity_id and attribute with message fields - bool entity_match = (strcmp(it.entity_id, msg.entity_id.c_str()) == 0); - bool attribute_match = (it.attribute != nullptr && strcmp(it.attribute, msg.attribute.c_str()) == 0) || - (it.attribute == nullptr && msg.attribute.empty()); + // Skip if entity_id is empty (invalid message) + if (msg.entity_id_len == 0) { + return; + } - if (entity_match && attribute_match) { - it.callback(msg.state); + for (auto &it : this->parent_->get_state_subs()) { + // Compare entity_id: check length matches and content matches + size_t entity_id_len = strlen(it.entity_id); + if (entity_id_len != msg.entity_id_len || memcmp(it.entity_id, msg.entity_id, msg.entity_id_len) != 0) { + continue; } + + // Compare attribute: either both have matching attribute, or both have none + size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0; + if (sub_attr_len != msg.attribute_len || + (sub_attr_len > 0 && memcmp(it.attribute, msg.attribute, sub_attr_len) != 0)) { + continue; + } + + // Create temporary string for callback (callback takes const std::string &) + // Handle empty state (nullptr with len=0) + std::string state(msg.state_len > 0 ? reinterpret_cast(msg.state) : "", msg.state_len); + it.callback(state); } } #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 8b84f9651f..6a2d902f8f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -966,15 +966,24 @@ void SubscribeHomeAssistantStateResponse::calculate_size(ProtoSize &size) const } bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: - this->entity_id = value.as_string(); + case 1: { + // Use raw data directly to avoid allocation + this->entity_id = value.data(); + this->entity_id_len = value.size(); break; - case 2: - this->state = value.as_string(); + } + case 2: { + // Use raw data directly to avoid allocation + this->state = value.data(); + this->state_len = value.size(); break; - case 3: - this->attribute = value.as_string(); + } + case 3: { + // Use raw data directly to avoid allocation + this->attribute = value.data(); + this->attribute_len = value.size(); break; + } default: return false; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 668c0af461..22deb19be8 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1203,13 +1203,16 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage { class HomeAssistantStateResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 40; - static constexpr uint8_t ESTIMATED_SIZE = 27; + static constexpr uint8_t ESTIMATED_SIZE = 57; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "home_assistant_state_response"; } #endif - std::string entity_id{}; - std::string state{}; - std::string attribute{}; + const uint8_t *entity_id{nullptr}; + uint16_t entity_id_len{0}; + const uint8_t *state{nullptr}; + uint16_t state_len{0}; + const uint8_t *attribute{nullptr}; + uint16_t attribute_len{0}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 38c3b473e6..7815eb73e4 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1184,9 +1184,15 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { } void HomeAssistantStateResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "HomeAssistantStateResponse"); - dump_field(out, "entity_id", this->entity_id); - dump_field(out, "state", this->state); - dump_field(out, "attribute", this->attribute); + out.append(" entity_id: "); + out.append(format_hex_pretty(this->entity_id, this->entity_id_len)); + out.append("\n"); + out.append(" state: "); + out.append(format_hex_pretty(this->state, this->state_len)); + out.append("\n"); + out.append(" attribute: "); + out.append(format_hex_pretty(this->attribute, this->attribute_len)); + out.append("\n"); } #endif void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } diff --git a/tests/integration/test_api_homeassistant.py b/tests/integration/test_api_homeassistant.py index 1343691f5f..3fe0dfe045 100644 --- a/tests/integration/test_api_homeassistant.py +++ b/tests/integration/test_api_homeassistant.py @@ -179,6 +179,12 @@ async def test_api_homeassistant( client.send_home_assistant_state("binary_sensor.external_motion", "", "ON") client.send_home_assistant_state("weather.home", "condition", "sunny") + # Test edge cases for zero-copy implementation safety + # Empty entity_id should be silently ignored (no crash) + client.send_home_assistant_state("", "", "should_be_ignored") + # Empty state with valid entity should work (use different entity to not interfere with test) + client.send_home_assistant_state("sensor.edge_case_empty_state", "", "") + # List entities and services _, services = await client.list_entities_services()