diff --git a/esphome/codegen.py b/esphome/codegen.py index 6d55c6023d..4a2a5975c6 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -69,6 +69,7 @@ from esphome.cpp_types import ( # noqa: F401 JsonObjectConst, Parented, PollingComponent, + StringRef, arduino_json_ns, bool_, const_char_ptr, diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 88618acef4..0a309aac7d 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -435,7 +435,7 @@ async def to_code(config: ConfigType) -> None: cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER") await automation.build_automation( var.get_client_connected_trigger(), - [(cg.std_string, "client_info"), (cg.std_string, "client_address")], + [(cg.StringRef, "client_info"), (cg.StringRef, "client_address")], config[CONF_ON_CLIENT_CONNECTED], ) @@ -443,7 +443,7 @@ async def to_code(config: ConfigType) -> None: cg.add_define("USE_API_CLIENT_DISCONNECTED_TRIGGER") await automation.build_automation( var.get_client_disconnected_trigger(), - [(cg.std_string, "client_info"), (cg.std_string, "client_address")], + [(cg.StringRef, "client_info"), (cg.StringRef, "client_address")], config[CONF_ON_CLIENT_DISCONNECTED], ) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ca2155a277..c8762824fe 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -101,16 +101,14 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) auto &noise_ctx = parent->get_noise_ctx(); if (noise_ctx.has_psk()) { - this->helper_ = - std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)}; + this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx)}; } else { - this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; } #elif defined(USE_API_PLAINTEXT) - this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; #elif defined(USE_API_NOISE) - this->helper_ = std::unique_ptr{ - new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx(), &this->client_info_)}; + this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; #else #error "No frame helper defined" #endif @@ -133,8 +131,8 @@ void APIConnection::start() { } // Initialize client name with peername (IP address) until Hello message provides actual name char peername[socket::PEERNAME_MAX_LEN]; - this->helper_->getpeername_to(peername); - strncpy(this->client_info_.name, peername, sizeof(this->client_info_.name) - 1); + size_t len = this->helper_->getpeername_to(peername); + this->helper_->set_client_name(peername, len); } APIConnection::~APIConnection() { @@ -1508,8 +1506,9 @@ void APIConnection::complete_authentication_() { this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected")); #ifdef USE_API_CLIENT_CONNECTED_TRIGGER - // Trigger expects std::string, get fresh peername from socket - this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->helper_->getpeername()); + char peername_buf[socket::PEERNAME_MAX_LEN]; + this->helper_->getpeername_to(peername_buf); + this->parent_->get_client_connected_trigger()->trigger(this->helper_->get_client_name(), StringRef(peername_buf)); #endif #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { @@ -1524,16 +1523,14 @@ void APIConnection::complete_authentication_() { } bool APIConnection::send_hello_response(const HelloRequest &msg) { - // Copy client name with truncation if needed - size_t copy_len = std::min(static_cast(msg.client_info_len), sizeof(this->client_info_.name) - 1); - memcpy(this->client_info_.name, msg.client_info, copy_len); - this->client_info_.name[copy_len] = '\0'; + // Copy client name with truncation if needed (set_client_name handles truncation) + this->helper_->set_client_name(reinterpret_cast(msg.client_info), msg.client_info_len); this->client_api_version_major_ = msg.api_version_major; this->client_api_version_minor_ = msg.api_version_minor; char peername[socket::PEERNAME_MAX_LEN]; this->helper_->getpeername_to(peername); - ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.name, peername, - this->client_api_version_major_, this->client_api_version_minor_); + ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(), + peername, this->client_api_version_major_, this->client_api_version_minor_); HelloResponse resp; resp.api_version_major = 1; @@ -2110,14 +2107,14 @@ void APIConnection::process_state_subscriptions_() { void APIConnection::log_client_(int level, const LogString *message) { char peername[socket::PEERNAME_MAX_LEN]; this->helper_->getpeername_to(peername); - esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->client_info_.name, peername, + esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(), peername, LOG_STR_ARG(message)); } void APIConnection::log_warning_(const LogString *message, APIError err) { char peername[socket::PEERNAME_MAX_LEN]; this->helper_->getpeername_to(peername); - ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->client_info_.name, peername, LOG_STR_ARG(message), + ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), peername, LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno); } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 0b4f4590a5..8b3eb88f87 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -16,9 +16,6 @@ namespace esphome::api { -// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars) -static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32; - // Keepalive timeout in milliseconds static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; // Maximum number of entities to process in a single batch during initial state/info sending @@ -287,7 +284,7 @@ class APIConnection final : public APIServerConnection { bool try_to_clear_buffer(bool log_out_of_space); bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; - StringRef get_name() const { return StringRef(this->client_name_); } + const char *get_name() const { return this->helper_->get_client_name(); } /// Get peer name (IP address) into a stack buffer - avoids heap allocation size_t get_peername_to(std::span buf) const { return this->helper_->getpeername_to(buf); @@ -525,11 +522,7 @@ class APIConnection final : public APIServerConnection { std::unique_ptr image_reader_; #endif - // Group 3: Client name (32 bytes fixed buffer, avoids heap allocation) - // Note: peername (IP address) is formatted on-demand via helper_->getpeername_to() - char client_name_[CLIENT_INFO_NAME_MAX_LEN]{}; - - // Group 4: 4-byte types + // Group 3: 4-byte types uint32_t last_traffic_; #ifdef USE_API_HOMEASSISTANT_STATES int state_subs_at_ = -1; diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 1263e23ffb..97114e30a7 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -1,6 +1,5 @@ #include "api_frame_helper.h" #ifdef USE_API -#include "api_connection.h" // For ClientInfo struct #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -18,7 +17,7 @@ static const char *const TAG = "api.frame_helper"; do { \ char peername__[socket::PEERNAME_MAX_LEN]; \ this->socket_->getpeername_to(peername__); \ - ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name, peername__, ##__VA_ARGS__); \ + ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername__, ##__VA_ARGS__); \ } while (0) #else #define HELPER_LOG(msg, ...) ((void) 0) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index edc755f0c7..2b0b1f40dc 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -31,6 +31,9 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and oth class ProtoWriteBuffer; +// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars) +static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32; + struct ReadPacketBuffer { const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call) uint16_t data_len; @@ -79,8 +82,16 @@ const LogString *api_error_to_logstr(APIError err); class APIFrameHelper { public: APIFrameHelper() = default; - explicit APIFrameHelper(std::unique_ptr socket, const char *client_name) - : socket_(std::move(socket)), client_name_(client_name) {} + explicit APIFrameHelper(std::unique_ptr socket) : socket_(std::move(socket)) {} + + // Get client name (null-terminated) + const char *get_client_name() const { return this->client_name_; } + // Set client name from buffer with length (truncates if needed) + void set_client_name(const char *name, size_t len) { + size_t copy_len = std::min(len, sizeof(this->client_name_) - 1); + memcpy(this->client_name_, name, copy_len); + this->client_name_[copy_len] = '\0'; + } virtual ~APIFrameHelper() = default; virtual APIError init() = 0; virtual APIError loop(); @@ -187,9 +198,8 @@ class APIFrameHelper { std::vector reusable_iovs_; std::vector rx_buf_; - // Pointer to client name buffer (4 bytes on 32-bit) - // Note: The pointed-to buffer must outlive this APIFrameHelper instance. - const char *client_name_{nullptr}; + // Client name buffer - stores name from Hello message or initial peername + char client_name_[CLIENT_INFO_NAME_MAX_LEN]{}; // Group smaller types together uint16_t rx_buf_len_ = 0; diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 7eb01058db..f6ad50e777 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -9,8 +9,8 @@ namespace esphome::api { class APINoiseFrameHelper final : public APIFrameHelper { public: - APINoiseFrameHelper(std::unique_ptr socket, APINoiseContext &ctx, const ClientInfo *client_info) - : APIFrameHelper(std::move(socket), client_info), ctx_(ctx) { + APINoiseFrameHelper(std::unique_ptr socket, APINoiseContext &ctx) + : APIFrameHelper(std::move(socket)), ctx_(ctx) { // Noise header structure: // Pos 0: indicator (0x01) // Pos 1-2: encrypted payload size (16-bit big-endian) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index ddcc661d40..ec54a415f6 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -1,7 +1,6 @@ #include "api_frame_helper_plaintext.h" #ifdef USE_API #ifdef USE_API_PLAINTEXT -#include "api_connection.h" // For ClientInfo struct #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -23,7 +22,7 @@ static const char *const TAG = "api.plaintext"; do { \ char peername__[socket::PEERNAME_MAX_LEN]; \ this->socket_->getpeername_to(peername__); \ - ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name, peername__, ##__VA_ARGS__); \ + ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername__, ##__VA_ARGS__); \ } while (0) #else #define HELPER_LOG(msg, ...) ((void) 0) diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h index e6bb7262f0..11ae3b8814 100644 --- a/esphome/components/api/api_frame_helper_plaintext.h +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -7,8 +7,7 @@ namespace esphome::api { class APIPlaintextFrameHelper final : public APIFrameHelper { public: - APIPlaintextFrameHelper(std::unique_ptr socket, const char *client_name) - : APIFrameHelper(std::move(socket), client_name) { + explicit APIPlaintextFrameHelper(std::unique_ptr socket) : APIFrameHelper(std::move(socket)) { // Plaintext header structure (worst case): // Pos 0: indicator (0x00) // Pos 1-3: payload size varint (up to 3 bytes) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 5ae6b6e9da..6f4e3c17c6 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -187,13 +187,14 @@ void APIServer::loop() { // Rare case: handle disconnection #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - // Trigger expects std::string, get fresh peername from socket - this->client_disconnected_trigger_->trigger(client->client_info_.name, client->get_peername()); + char peername_buf[socket::PEERNAME_MAX_LEN]; + client->get_peername_to(peername_buf); + this->client_disconnected_trigger_->trigger(client->get_name(), StringRef(peername_buf)); #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES this->unregister_active_action_calls_for_connection(client.get()); #endif - ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name); + ESP_LOGV(TAG, "Remove connection %s", client->get_name()); // Swap with the last element and pop (avoids expensive vector shifts) if (client_index < this->clients_.size() - 1) { diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 96c56fd08a..f6ca869a0f 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -10,6 +10,7 @@ #include "esphome/core/component.h" #include "esphome/core/controller.h" #include "esphome/core/log.h" +#include "esphome/core/string_ref.h" #include "list_entities.h" #include "subscribe_state.h" #ifdef USE_LOGGER @@ -221,12 +222,10 @@ class APIServer : public Component, #endif #ifdef USE_API_CLIENT_CONNECTED_TRIGGER - Trigger *get_client_connected_trigger() const { return this->client_connected_trigger_; } + Trigger *get_client_connected_trigger() const { return this->client_connected_trigger_; } #endif #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - Trigger *get_client_disconnected_trigger() const { - return this->client_disconnected_trigger_; - } + Trigger *get_client_disconnected_trigger() const { return this->client_disconnected_trigger_; } #endif protected: @@ -244,10 +243,10 @@ class APIServer : public Component, // Pointers and pointer-like types first (4 bytes each) std::unique_ptr socket_ = nullptr; #ifdef USE_API_CLIENT_CONNECTED_TRIGGER - Trigger *client_connected_trigger_ = new Trigger(); + Trigger *client_connected_trigger_ = new Trigger(); #endif #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - Trigger *client_disconnected_trigger_ = new Trigger(); + Trigger *client_disconnected_trigger_ = new Trigger(); #endif // 4-byte aligned types diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 0d1813f63b..f4c690e40a 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -23,6 +23,7 @@ size_t = global_ns.namespace("size_t") const_char_ptr = global_ns.namespace("const char *") NAN = global_ns.namespace("NAN") esphome_ns = global_ns # using namespace esphome; +StringRef = esphome_ns.class_("StringRef") FixedVector = esphome_ns.class_("FixedVector") App = esphome_ns.App EntityBase = esphome_ns.class_("EntityBase")