diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 9bafe7cb3e..fac7fa8c98 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() { @@ -1507,8 +1505,10 @@ 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(std::string(this->helper_->get_client_name()), + std::string(peername_buf)); #endif #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { @@ -1523,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(msg.client_info.size(), sizeof(this->client_info_.name) - 1); - memcpy(this->client_info_.name, msg.client_info.c_str(), 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 +2108,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 b5700b61aa..5bfe37319b 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -16,16 +16,6 @@ namespace esphome::api { -// Client information structure - uses fixed buffer to avoid std::string heap allocation -// 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 ClientInfo { - char name[CLIENT_INFO_NAME_MAX_LEN]{}; // Client name from Hello message - // Note: peername (IP address) is not stored here to save memory. - // Use helper_->getpeername_to() when needed. -}; - // 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 @@ -294,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_info_.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); @@ -532,10 +522,7 @@ class APIConnection final : public APIServerConnection { std::unique_ptr image_reader_; #endif - // Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each) - ClientInfo client_info_; - - // 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 85a74e32cf..2b0b1f40dc 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -29,11 +29,11 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266 static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms #endif -// Forward declaration -struct ClientInfo; - 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; @@ -82,8 +82,16 @@ const LogString *api_error_to_logstr(APIError err); class APIFrameHelper { public: APIFrameHelper() = default; - explicit APIFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) - : socket_(std::move(socket)), client_info_(client_info) {} + 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(); @@ -190,9 +198,8 @@ class APIFrameHelper { std::vector reusable_iovs_; std::vector rx_buf_; - // Pointer to client info (4 bytes on 32-bit) - // Note: The pointed-to ClientInfo object must outlive this APIFrameHelper instance. - const ClientInfo *client_info_{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.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 8a3c14b432..eb56c1824c 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -29,7 +29,7 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") 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_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 bba981d26b..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 ClientInfo *client_info) - : APIFrameHelper(std::move(socket), client_info) { + 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 b2915442fb..b18b13ba99 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(std::string(client->get_name()), std::string(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) {