diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 652b456850..d6384456d5 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -763,7 +763,7 @@ message SubscribeHomeassistantServicesRequest { message HomeassistantServiceMap { string key = 1; - string value = 2 [(no_zero_copy) = true]; + string value = 2; } message HomeassistantActionRequest { @@ -779,7 +779,7 @@ message HomeassistantActionRequest { bool is_event = 5; uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"]; bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; - string response_template = 8 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; + string response_template = 8 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; } // Message sent by Home Assistant to ESPHome with service call response data diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index 1916e84625..a863f2c7a8 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -27,7 +27,6 @@ extend google.protobuf.MessageOptions { extend google.protobuf.FieldOptions { optional string field_ifdef = 1042; optional uint32 fixed_array_size = 50007; - optional bool no_zero_copy = 50008 [default=false]; optional bool fixed_array_skip_zero = 50009 [default=false]; optional string fixed_array_size_define = 50010; optional string fixed_array_with_length_define = 50011; diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 01fe44d7c7..e21b8596ca 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1053,7 +1053,7 @@ class SubscribeHomeassistantServicesRequest final : public ProtoMessage { class HomeassistantServiceMap final : public ProtoMessage { public: StringRef key{}; - std::string value{}; + StringRef value{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1081,7 +1081,7 @@ class HomeassistantActionRequest final : public ProtoMessage { bool wants_response{false}; #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - std::string response_template{}; + StringRef response_template{}; #endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index b16164270b..2fd9cb0dd2 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -265,7 +265,7 @@ class CustomAPIDevice { for (auto &it : data) { auto &kv = resp.data.emplace_back(); kv.key = StringRef(it.first); - kv.value = it.second; // value is std::string (no_zero_copy), assign directly + kv.value = StringRef(it.second); // data map lives until send completes } global_api_server->send_homeassistant_action(resp); } @@ -308,7 +308,7 @@ class CustomAPIDevice { for (auto &it : data) { auto &kv = resp.data.emplace_back(); kv.key = StringRef(it.first); - kv.value = it.second; // value is std::string (no_zero_copy), assign directly + kv.value = StringRef(it.second); // data map lives until send completes } global_api_server->send_homeassistant_action(resp); } diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index a17c99b8ba..9bffe18764 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -149,11 +149,21 @@ template class HomeAssistantServiceCallAction : public Actionservice_.value(x...); resp.service = StringRef(service_value); resp.is_event = this->flags_.is_event; - this->populate_service_map(resp.data, this->data_, x...); - this->populate_service_map(resp.data_template, this->data_template_, x...); - this->populate_service_map(resp.variables, this->variables_, x...); + + // Local storage for lambda-evaluated strings - lives until after send + FixedVector data_storage; + FixedVector data_template_storage; + FixedVector variables_storage; + + this->populate_service_map(resp.data, this->data_, data_storage, x...); + this->populate_service_map(resp.data_template, this->data_template_, data_template_storage, x...); + this->populate_service_map(resp.variables, this->variables_, variables_storage, x...); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + // IMPORTANT: Declare at outer scope so it lives until send_homeassistant_action returns. + std::string response_template_value; +#endif if (this->flags_.wants_status) { // Generate a unique call ID for this service call static uint32_t call_id_counter = 1; @@ -164,8 +174,8 @@ template class HomeAssistantServiceCallAction : public Actionflags_.has_response_template) { - std::string response_template_value = this->response_template_.value(x...); - resp.response_template = response_template_value; + response_template_value = this->response_template_.value(x...); + resp.response_template = StringRef(response_template_value); } } #endif @@ -205,12 +215,31 @@ template class HomeAssistantServiceCallAction : public Action - static void populate_service_map(VectorType &dest, SourceType &source, Ts... x) { + static void populate_service_map(VectorType &dest, SourceType &source, FixedVector &value_storage, + Ts... x) { dest.init(source.size()); + + // Count non-static strings to allocate exact storage needed + size_t lambda_count = 0; + for (const auto &it : source) { + if (!it.value.is_static_string()) { + lambda_count++; + } + } + value_storage.init(lambda_count); + for (auto &it : source) { auto &kv = dest.emplace_back(); kv.key = StringRef(it.key); - kv.value = it.value.value(x...); + + if (it.value.is_static_string()) { + // Static string from YAML - zero allocation + kv.value = StringRef(it.value.get_static_string()); + } else { + // Lambda evaluation - store result, reference it + value_storage.push_back(it.value.value(x...)); + kv.value = StringRef(value_storage.back()); + } } } diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp index 82387a81e9..92ecd5ea39 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.cpp +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -91,11 +91,14 @@ void HomeassistantNumber::control(float value) { resp.data.init(2); auto &entity_id = resp.data.emplace_back(); entity_id.key = ENTITY_ID_KEY; - entity_id.value = this->entity_id_; + entity_id.value = StringRef(this->entity_id_); auto &entity_value = resp.data.emplace_back(); entity_value.key = VALUE_KEY; - entity_value.value = to_string(value); + // Stack buffer - no heap allocation; %g produces shortest representation + char value_buf[16]; + snprintf(value_buf, sizeof(value_buf), "%g", value); + entity_value.value = StringRef(value_buf); api::global_api_server->send_homeassistant_action(resp); } diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.cpp b/esphome/components/homeassistant/switch/homeassistant_switch.cpp index 79d17eb290..cc3d582bf3 100644 --- a/esphome/components/homeassistant/switch/homeassistant_switch.cpp +++ b/esphome/components/homeassistant/switch/homeassistant_switch.cpp @@ -55,7 +55,7 @@ void HomeassistantSwitch::write_state(bool state) { resp.data.init(1); auto &entity_id_kv = resp.data.emplace_back(); entity_id_kv.key = ENTITY_ID_KEY; - entity_id_kv.value = this->entity_id_; + entity_id_kv.value = StringRef(this->entity_id_); api::global_api_server->send_homeassistant_action(resp); } diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 61d2944acf..585b434bb2 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -159,6 +159,12 @@ template class TemplatableValue { return this->value(x...); } + /// Check if this holds a static string (const char* stored without allocation) + bool is_static_string() const { return this->type_ == STATIC_STRING; } + + /// Get the static string pointer (only valid if is_static_string() returns true) + const char *get_static_string() const { return this->static_str_; } + protected: enum : uint8_t { NONE, diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index c61555805e..118c87356e 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -376,10 +376,8 @@ def create_field_type_info( return BytesType(field, needs_decode, needs_encode) - # Special handling for string fields - use StringRef for zero-copy unless no_zero_copy is set + # Special handling for string fields - use StringRef for zero-copy if field.type == 9: - if get_field_opt(field, pb.no_zero_copy, False): - return StringType(field, needs_decode, needs_encode) return PointerToStringBufferType(field, None) validate_field_type(field.type, field.name) @@ -585,15 +583,12 @@ class StringType(TypeInfo): def public_content(self) -> list[str]: content: list[str] = [] - # Check if no_zero_copy option is set - no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False) - - # Add std::string storage if message needs decoding OR if no_zero_copy is set - if self._needs_decode or no_zero_copy: + # Add std::string storage if message needs decoding + if self._needs_decode: content.append(f"std::string {self.field_name}{{}};") - # Only add StringRef if encoding is needed AND no_zero_copy is not set - if self._needs_encode and not no_zero_copy: + # Add StringRef if encoding is needed + if self._needs_encode: content.extend( [ # Add StringRef field if message needs encoding @@ -608,27 +603,14 @@ class StringType(TypeInfo): @property def encode_content(self) -> str: - # Check if no_zero_copy option is set - no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False) - - if no_zero_copy: - # Use the std::string directly - return f"buffer.encode_string({self.number}, this->{self.field_name});" # Use the StringRef return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_);" def dump(self, name): - # Check if no_zero_copy option is set - no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False) - # If name is 'it', this is a repeated field element - always use string if name == "it": return "append_quoted_string(out, StringRef(it));" - # If no_zero_copy is set, always use std::string - if no_zero_copy: - return f'out.append("\'").append(this->{self.field_name}).append("\'");' - # For SOURCE_CLIENT only, always use std::string if not self._needs_encode: return f'out.append("\'").append(this->{self.field_name}).append("\'");' @@ -648,13 +630,6 @@ class StringType(TypeInfo): @property def dump_content(self) -> str: - # Check if no_zero_copy option is set - no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False) - - # If no_zero_copy is set, always use std::string - if no_zero_copy: - return f'dump_field(out, "{self.name}", this->{self.field_name});' - # For SOURCE_CLIENT only, use std::string if not self._needs_encode: return f'dump_field(out, "{self.name}", this->{self.field_name});' @@ -670,17 +645,8 @@ class StringType(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - # Check if no_zero_copy option is set - no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False) - - # For SOURCE_CLIENT only messages or no_zero_copy, use the string field directly - if not self._needs_encode or no_zero_copy: - # For no_zero_copy, we need to use .size() on the string - if no_zero_copy and name != "it": - field_id_size = self.calculate_field_id_size() - return ( - f"size.add_length({field_id_size}, this->{self.field_name}.size());" - ) + # For SOURCE_CLIENT only messages, use the string field directly + if not self._needs_encode: return self._get_simple_size_calculation(name, force, "add_length") # Check if this is being called from a repeated field context