From cd7922faaf62ef1aef8f522203b940185ef738df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 20:58:49 +0000 Subject: [PATCH 1/2] Bump aioesphomeapi from 41.9.0 to 41.9.1 (#10857) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4256804722..24c93fc4bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20250904.0 -aioesphomeapi==41.9.0 +aioesphomeapi==41.9.1 zeroconf==0.147.2 puremagic==1.30 ruamel.yaml==0.18.15 # dashboard_import From d0d7abb54254569a9d5520f8b0cc5de6a41d4e31 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Sep 2025 16:15:28 -0500 Subject: [PATCH 2/2] Implement zero-copy for strings in base API calls (#10851) --- esphome/components/api/api.proto | 6 +++--- esphome/components/api/api_connection.cpp | 14 ++++++++++---- esphome/components/api/api_pb2.cpp | 21 +++++++++++++++------ esphome/components/api/api_pb2.h | 15 +++++++++------ esphome/components/api/api_pb2_dump.cpp | 15 ++++++++++++--- esphome/components/api/api_server.cpp | 7 ++++--- esphome/components/api/api_server.h | 2 +- script/api_protobuf/api_protobuf.py | 8 ++++++++ 8 files changed, 62 insertions(+), 26 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 26f679e2d7..c58aabd3c6 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -102,7 +102,7 @@ message HelloRequest { // For example "Home Assistant" // Not strictly necessary to send but nice for debugging // purposes. - string client_info = 1; + string client_info = 1 [(pointer_to_buffer) = true]; uint32 api_version_major = 2; uint32 api_version_minor = 3; } @@ -139,7 +139,7 @@ message AuthenticationRequest { option (ifdef) = "USE_API_PASSWORD"; // The password to log in with - string password = 1; + string password = 1 [(pointer_to_buffer) = true]; } // Confirmation of successful connection. After this the connection is available for all traffic. @@ -824,7 +824,7 @@ message GetTimeResponse { option (no_delay) = true; fixed32 epoch_seconds = 1; - string timezone = 2; + string timezone = 2 [(pointer_to_buffer) = true]; } // ==================== USER-DEFINES SERVICES ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index a27adfe241..357b6e1a32 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1078,8 +1078,14 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { if (homeassistant::global_homeassistant_time != nullptr) { homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds); #ifdef USE_TIME_TIMEZONE - if (!value.timezone.empty() && value.timezone != homeassistant::global_homeassistant_time->get_timezone()) { - homeassistant::global_homeassistant_time->set_timezone(value.timezone); + if (value.timezone_len > 0) { + const std::string ¤t_tz = homeassistant::global_homeassistant_time->get_timezone(); + // Compare without allocating a string + if (current_tz.length() != value.timezone_len || + memcmp(current_tz.c_str(), value.timezone, value.timezone_len) != 0) { + homeassistant::global_homeassistant_time->set_timezone( + std::string(reinterpret_cast(value.timezone), value.timezone_len)); + } } #endif } @@ -1374,7 +1380,7 @@ void APIConnection::complete_authentication_() { } bool APIConnection::send_hello_response(const HelloRequest &msg) { - this->client_info_.name = msg.client_info; + this->client_info_.name.assign(reinterpret_cast(msg.client_info), msg.client_info_len); this->client_info_.peername = this->helper_->getpeername(); this->client_api_version_major_ = msg.api_version_major; this->client_api_version_minor_ = msg.api_version_minor; @@ -1402,7 +1408,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) { bool APIConnection::send_authenticate_response(const AuthenticationRequest &msg) { AuthenticationResponse resp; // bool invalid_password = 1; - resp.invalid_password = !this->parent_->check_password(msg.password); + resp.invalid_password = !this->parent_->check_password(msg.password, msg.password_len); if (!resp.invalid_password) { this->complete_authentication_(); } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 4388a13ca1..0a6b271fac 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -22,9 +22,12 @@ bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: - this->client_info = value.as_string(); + case 1: { + // Use raw data directly to avoid allocation + this->client_info = value.data(); + this->client_info_len = value.size(); break; + } default: return false; } @@ -45,9 +48,12 @@ void HelloResponse::calculate_size(ProtoSize &size) const { #ifdef USE_API_PASSWORD bool AuthenticationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: - this->password = value.as_string(); + case 1: { + // Use raw data directly to avoid allocation + this->password = value.data(); + this->password_len = value.size(); break; + } default: return false; } @@ -917,9 +923,12 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel #endif bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: - this->timezone = value.as_string(); + case 2: { + // Use raw data directly to avoid allocation + this->timezone = value.data(); + this->timezone_len = value.size(); break; + } default: return false; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index b6531b37a6..b0503b4b7a 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -330,11 +330,12 @@ class CommandProtoMessage : public ProtoDecodableMessage { class HelloRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 1; - static constexpr uint8_t ESTIMATED_SIZE = 17; + static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "hello_request"; } #endif - std::string client_info{}; + const uint8_t *client_info{nullptr}; + uint16_t client_info_len{0}; uint32_t api_version_major{0}; uint32_t api_version_minor{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -370,11 +371,12 @@ class HelloResponse final : public ProtoMessage { class AuthenticationRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 3; - static constexpr uint8_t ESTIMATED_SIZE = 9; + static constexpr uint8_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "authentication_request"; } #endif - std::string password{}; + const uint8_t *password{nullptr}; + uint16_t password_len{0}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1188,12 +1190,13 @@ class GetTimeRequest final : public ProtoMessage { class GetTimeResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 37; - static constexpr uint8_t ESTIMATED_SIZE = 14; + static constexpr uint8_t ESTIMATED_SIZE = 24; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "get_time_response"; } #endif uint32_t epoch_seconds{0}; - std::string timezone{}; + const uint8_t *timezone{nullptr}; + uint16_t timezone_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 e2a4772692..131f6e361a 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -670,7 +670,9 @@ template<> const char *proto_enum_to_string(enums: void HelloRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "HelloRequest"); - dump_field(out, "client_info", this->client_info); + out.append(" client_info: "); + out.append(format_hex_pretty(this->client_info, this->client_info_len)); + out.append("\n"); dump_field(out, "api_version_major", this->api_version_major); dump_field(out, "api_version_minor", this->api_version_minor); } @@ -682,7 +684,12 @@ void HelloResponse::dump_to(std::string &out) const { dump_field(out, "name", this->name_ref_); } #ifdef USE_API_PASSWORD -void AuthenticationRequest::dump_to(std::string &out) const { dump_field(out, "password", this->password); } +void AuthenticationRequest::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "AuthenticationRequest"); + out.append(" password: "); + out.append(format_hex_pretty(this->password, this->password_len)); + out.append("\n"); +} void AuthenticationResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "AuthenticationResponse"); dump_field(out, "invalid_password", this->invalid_password); @@ -1136,7 +1143,9 @@ void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeReques void GetTimeResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "GetTimeResponse"); dump_field(out, "epoch_seconds", this->epoch_seconds); - dump_field(out, "timezone", this->timezone); + out.append(" timezone: "); + out.append(format_hex_pretty(this->timezone, this->timezone_len)); + out.append("\n"); } #ifdef USE_API_SERVICES void ListEntitiesServicesArgument::dump_to(std::string &out) const { diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 1f38f4a31a..775bfe902b 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -217,12 +217,12 @@ void APIServer::dump_config() { } #ifdef USE_API_PASSWORD -bool APIServer::check_password(const std::string &password) const { +bool APIServer::check_password(const uint8_t *password_data, size_t password_len) const { // depend only on input password length const char *a = this->password_.c_str(); uint32_t len_a = this->password_.length(); - const char *b = password.c_str(); - uint32_t len_b = password.length(); + const char *b = reinterpret_cast(password_data); + uint32_t len_b = password_len; // disable optimization with volatile volatile uint32_t length = len_b; @@ -245,6 +245,7 @@ bool APIServer::check_password(const std::string &password) const { return result == 0; } + #endif void APIServer::handle_disconnect(APIConnection *conn) {} diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 8b5e624df2..e5470e852d 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -37,7 +37,7 @@ class APIServer : public Component, public Controller { void on_shutdown() override; bool teardown() override; #ifdef USE_API_PASSWORD - bool check_password(const std::string &password) const; + bool check_password(const uint8_t *password_data, size_t password_len) const; void set_password(const std::string &password); #endif void set_port(uint16_t port); diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 22bebcbd29..7f3f8014f7 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -373,6 +373,14 @@ def create_field_type_info( # Traditional fixed array approach with copy return FixedArrayBytesType(field, fixed_size) + # Check for pointer_to_buffer option on string fields + if field.type == 9: + has_pointer_to_buffer = get_field_opt(field, pb.pointer_to_buffer, False) + + if has_pointer_to_buffer: + # Zero-copy pointer approach for strings + return PointerToBytesBufferType(field, None) + # Special handling for bytes fields if field.type == 12: return BytesType(field, needs_decode, needs_encode)