This commit is contained in:
J. Nick Koston
2025-12-27 18:05:26 -10:00
parent d404e37449
commit d2bab26e67
12 changed files with 50 additions and 51 deletions

View File

@@ -69,6 +69,7 @@ from esphome.cpp_types import ( # noqa: F401
JsonObjectConst,
Parented,
PollingComponent,
StringRef,
arduino_json_ns,
bool_,
const_char_ptr,

View File

@@ -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],
)

View File

@@ -101,16 +101,14 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> 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<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)};
this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx)};
} else {
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)};
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
}
#elif defined(USE_API_PLAINTEXT)
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)};
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
#elif defined(USE_API_NOISE)
this->helper_ = std::unique_ptr<APIFrameHelper>{
new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx(), &this->client_info_)};
this->helper_ = std::unique_ptr<APIFrameHelper>{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<uint8_t>(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<size_t>(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<const char *>(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);
}

View File

@@ -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<char, socket::PEERNAME_MAX_LEN> buf) const {
return this->helper_->getpeername_to(buf);
@@ -525,11 +522,7 @@ class APIConnection final : public APIServerConnection {
std::unique_ptr<camera::CameraImageReader> 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;

View File

@@ -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)

View File

@@ -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::Socket> socket, const char *client_name)
: socket_(std::move(socket)), client_name_(client_name) {}
explicit APIFrameHelper(std::unique_ptr<socket::Socket> 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<struct iovec> reusable_iovs_;
std::vector<uint8_t> 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;

View File

@@ -9,8 +9,8 @@ namespace esphome::api {
class APINoiseFrameHelper final : public APIFrameHelper {
public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx, const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info), ctx_(ctx) {
APINoiseFrameHelper(std::unique_ptr<socket::Socket> 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)

View File

@@ -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)

View File

@@ -7,8 +7,7 @@ namespace esphome::api {
class APIPlaintextFrameHelper final : public APIFrameHelper {
public:
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const char *client_name)
: APIFrameHelper(std::move(socket), client_name) {
explicit APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)

View File

@@ -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) {

View File

@@ -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<std::string, std::string> *get_client_connected_trigger() const { return this->client_connected_trigger_; }
Trigger<StringRef, StringRef> *get_client_connected_trigger() const { return this->client_connected_trigger_; }
#endif
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
Trigger<std::string, std::string> *get_client_disconnected_trigger() const {
return this->client_disconnected_trigger_;
}
Trigger<StringRef, StringRef> *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::Socket> socket_ = nullptr;
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>();
Trigger<StringRef, StringRef> *client_connected_trigger_ = new Trigger<StringRef, StringRef>();
#endif
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
Trigger<std::string, std::string> *client_disconnected_trigger_ = new Trigger<std::string, std::string>();
Trigger<StringRef, StringRef> *client_disconnected_trigger_ = new Trigger<StringRef, StringRef>();
#endif
// 4-byte aligned types

View File

@@ -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")