From cc9f42cc9a8821c5ddf6e78f3b41b1c2eb16e9ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Dec 2025 07:22:36 -1000 Subject: [PATCH 1/3] [api] Add zero-copy support for Home Assistant state response messages --- 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 +++++++--- 5 files changed, 55 insertions(+), 23 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e8c900df26..cd49e3e175 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 126d3cb220..69e90ff1a0 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 8bba13a4de..5c0bff6d2d 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -963,15 +963,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 d3b91ac56b..1ee95c6c78 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1202,13 +1202,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 d733e66a6d..ad917eb342 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 {}"); } From e812d8683a22e3d315e8d23cc6a08892c1c9d584 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Dec 2025 07:23:55 -1000 Subject: [PATCH 2/3] tests --- tests/integration/test_api_homeassistant.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/integration/test_api_homeassistant.py b/tests/integration/test_api_homeassistant.py index 1343691f5f..c297f5be48 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 + client.send_home_assistant_state("sensor.external_temperature", "", "") + # List entities and services _, services = await client.list_entities_services() From 6cb66559bc841890c60a0a7a3dd78f9d45b5b52a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Dec 2025 08:42:52 -1000 Subject: [PATCH 3/3] fix test --- tests/integration/test_api_homeassistant.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_api_homeassistant.py b/tests/integration/test_api_homeassistant.py index c297f5be48..3fe0dfe045 100644 --- a/tests/integration/test_api_homeassistant.py +++ b/tests/integration/test_api_homeassistant.py @@ -182,8 +182,8 @@ async def test_api_homeassistant( # 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 - client.send_home_assistant_state("sensor.external_temperature", "", "") + # 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()