Merge branch 'dev' of https://github.com/esphome/esphome into batch_ping_fallback

This commit is contained in:
J. Nick Koston 2025-06-26 10:02:32 +02:00
commit ebf14f50fb
No known key found for this signature in database
22 changed files with 575 additions and 432 deletions

View File

@ -1,28 +1,11 @@
--- ---
name: Lock name: Lock closed issues and PRs
on: on:
schedule: schedule:
- cron: "30 0 * * *" - cron: "30 0 * * *" # Run daily at 00:30 UTC
workflow_dispatch: workflow_dispatch:
permissions:
issues: write
pull-requests: write
concurrency:
group: lock
jobs: jobs:
lock: lock:
runs-on: ubuntu-latest uses: esphome/workflows/.github/workflows/lock.yml@main
steps:
- uses: dessant/lock-threads@v5.0.1
with:
pr-inactive-days: "1"
pr-lock-reason: ""
exclude-any-pr-labels: keep-open
issue-inactive-days: "7"
issue-lock-reason: ""
exclude-any-issue-labels: keep-open

View File

@ -33,9 +33,14 @@ namespace api {
// Since each message could contain multiple protobuf messages when using packet batching, // Since each message could contain multiple protobuf messages when using packet batching,
// this limits the number of messages processed, not the number of TCP packets. // this limits the number of messages processed, not the number of TCP packets.
static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5;
static constexpr uint8_t MAX_PING_RETRIES = 60;
static constexpr uint16_t PING_RETRY_INTERVAL = 1000;
static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2;
static const char *const TAG = "api.connection"; static const char *const TAG = "api.connection";
#ifdef USE_ESP32_CAMERA
static const int ESP32_CAMERA_STOP_STREAM = 5000; static const int ESP32_CAMERA_STOP_STREAM = 5000;
#endif
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
@ -86,16 +91,6 @@ APIConnection::~APIConnection() {
} }
void APIConnection::loop() { void APIConnection::loop() {
if (this->remove_)
return;
if (!network::is_connected()) {
// when network is disconnected force disconnect immediately
// don't wait for timeout
this->on_fatal_error();
ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", this->get_client_combined_info().c_str());
return;
}
if (this->next_close_) { if (this->next_close_) {
// requested a disconnect // requested a disconnect
this->helper_->close(); this->helper_->close();
@ -148,18 +143,19 @@ void APIConnection::loop() {
// Process deferred batch if scheduled // Process deferred batch if scheduled
if (this->deferred_batch_.batch_scheduled && if (this->deferred_batch_.batch_scheduled &&
App.get_loop_component_start_time() - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) {
this->process_batch_(); this->process_batch_();
} }
if (!this->list_entities_iterator_.completed()) if (!this->list_entities_iterator_.completed()) {
this->list_entities_iterator_.advance(); this->list_entities_iterator_.advance();
if (!this->initial_state_iterator_.completed() && this->list_entities_iterator_.completed()) } else if (!this->initial_state_iterator_.completed()) {
this->initial_state_iterator_.advance(); this->initial_state_iterator_.advance();
}
if (this->sent_ping_) { if (this->sent_ping_) {
// Disconnect if not responded within 2.5*keepalive // Disconnect if not responded within 2.5*keepalive
if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) { if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str());
} }
@ -172,6 +168,18 @@ void APIConnection::loop() {
ESP_LOGVV(TAG, "Failed to send ping directly, scheduling at front of batch"); ESP_LOGVV(TAG, "Failed to send ping directly, scheduling at front of batch");
this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE); this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE);
this->sent_ping_ = true; // Mark as sent to avoid scheduling multiple pings this->sent_ping_ = true; // Mark as sent to avoid scheduling multiple pings
// Also set up retry tracking from upstream
this->next_ping_retry_ = now + PING_RETRY_INTERVAL;
this->ping_retries_++;
if (this->ping_retries_ >= MAX_PING_RETRIES) {
on_fatal_error();
ESP_LOGE(TAG, "%s: Ping failed %u times", this->get_client_combined_info().c_str(), this->ping_retries_);
} else if (this->ping_retries_ >= 10) {
ESP_LOGW(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_);
} else {
ESP_LOGD(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_);
}
} }
} }
@ -194,22 +202,20 @@ void APIConnection::loop() {
// bool done = 3; // bool done = 3;
buffer.encode_bool(3, done); buffer.encode_bool(3, done);
bool success = this->send_buffer(buffer, 44); bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE);
if (success) { if (success) {
this->image_reader_.consume_data(to_send); this->image_reader_.consume_data(to_send);
} if (done) {
if (success && done) { this->image_reader_.return_image();
this->image_reader_.return_image(); }
} }
} }
#endif #endif
if (state_subs_at_ != -1) { if (state_subs_at_ >= 0) {
const auto &subs = this->parent_->get_state_subs(); const auto &subs = this->parent_->get_state_subs();
if (state_subs_at_ >= (int) subs.size()) { if (state_subs_at_ < static_cast<int>(subs.size())) {
state_subs_at_ = -1;
} else {
auto &it = subs[state_subs_at_]; auto &it = subs[state_subs_at_];
SubscribeHomeAssistantStateResponse resp; SubscribeHomeAssistantStateResponse resp;
resp.entity_id = it.entity_id; resp.entity_id = it.entity_id;
@ -218,6 +224,8 @@ void APIConnection::loop() {
if (this->send_message(resp)) { if (this->send_message(resp)) {
state_subs_at_++; state_subs_at_++;
} }
} else {
state_subs_at_ = -1;
} }
} }
} }
@ -271,6 +279,11 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes
// Encode directly into buffer // Encode directly into buffer
msg.encode(buffer); msg.encode(buffer);
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log the message for VV debugging
conn->log_send_message_(msg.message_name(), msg.dump());
#endif
// Calculate actual encoded size (not including header that was already added) // Calculate actual encoded size (not including header that was already added)
size_t actual_payload_size = shared_buf.size() - size_before_encode; size_t actual_payload_size = shared_buf.size() - size_before_encode;
@ -1427,7 +1440,7 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
#ifdef USE_EVENT #ifdef USE_EVENT
void APIConnection::send_event(event::Event *event, const std::string &event_type) { void APIConnection::send_event(event::Event *event, const std::string &event_type) {
this->schedule_message_(event, MessageCreator(event_type, EventResponse::MESSAGE_TYPE), EventResponse::MESSAGE_TYPE); this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE);
} }
void APIConnection::send_event_info(event::Event *event) { void APIConnection::send_event_info(event::Event *event) {
this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE); this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE);
@ -1787,7 +1800,8 @@ void APIConnection::process_batch_() {
const auto &item = this->deferred_batch_.items[0]; const auto &item = this->deferred_batch_.items[0];
// Let the creator calculate size and encode if it fits // Let the creator calculate size and encode if it fits
uint16_t payload_size = item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true); uint16_t payload_size =
item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true, item.message_type);
if (payload_size > 0 && if (payload_size > 0 &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) { this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) {
@ -1837,7 +1851,7 @@ void APIConnection::process_batch_() {
for (const auto &item : this->deferred_batch_.items) { for (const auto &item : this->deferred_batch_.items) {
// Try to encode message // Try to encode message
// The creator will calculate overhead to determine if the message fits // The creator will calculate overhead to determine if the message fits
uint16_t payload_size = item.creator(item.entity, this, remaining_size, false); uint16_t payload_size = item.creator(item.entity, this, remaining_size, false, item.message_type);
if (payload_size == 0) { if (payload_size == 0) {
// Message won't fit, stop processing // Message won't fit, stop processing
@ -1900,21 +1914,23 @@ void APIConnection::process_batch_() {
} }
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) const { bool is_single, uint16_t message_type) const {
switch (message_type_) { if (has_tagged_string_ptr_()) {
case 0: // Function pointer // Handle string-based messages
return data_.ptr(entity, conn, remaining_size, is_single); switch (message_type) {
#ifdef USE_EVENT #ifdef USE_EVENT
case EventResponse::MESSAGE_TYPE: { case EventResponse::MESSAGE_TYPE: {
auto *e = static_cast<event::Event *>(entity); auto *e = static_cast<event::Event *>(entity);
return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single); return APIConnection::try_send_event_response(e, *get_string_ptr_(), conn, remaining_size, is_single);
} }
#endif #endif
default:
default: // Should not happen, return 0 to indicate no message
// Should not happen, return 0 to indicate no message return 0;
return 0; }
} else {
// Function pointer case
return data_.ptr(entity, conn, remaining_size, is_single);
} }
} }

View File

@ -484,55 +484,57 @@ class APIConnection : public APIServerConnection {
// Function pointer type for message encoding // Function pointer type for message encoding
using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single);
// Optimized MessageCreator class using union dispatch // Optimized MessageCreator class using tagged pointer
class MessageCreator { class MessageCreator {
// Ensure pointer alignment allows LSB tagging
static_assert(alignof(std::string *) > 1, "String pointer alignment must be > 1 for LSB tagging");
public: public:
// Constructor for function pointer (message_type = 0) // Constructor for function pointer
MessageCreator(MessageCreatorPtr ptr) : message_type_(0) { data_.ptr = ptr; } MessageCreator(MessageCreatorPtr ptr) {
// Function pointers are always aligned, so LSB is 0
data_.ptr = ptr;
}
// Constructor for string state capture // Constructor for string state capture
MessageCreator(const std::string &value, uint16_t msg_type) : message_type_(msg_type) { explicit MessageCreator(const std::string &str_value) {
data_.string_ptr = new std::string(value); // Allocate string and tag the pointer
auto *str = new std::string(str_value);
// Set LSB to 1 to indicate string pointer
data_.tagged = reinterpret_cast<uintptr_t>(str) | 1;
} }
// Destructor // Destructor
~MessageCreator() { ~MessageCreator() {
// Clean up string data for string-based message types if (has_tagged_string_ptr_()) {
if (uses_string_data_()) { delete get_string_ptr_();
delete data_.string_ptr;
} }
} }
// Copy constructor // Copy constructor
MessageCreator(const MessageCreator &other) : message_type_(other.message_type_) { MessageCreator(const MessageCreator &other) {
if (message_type_ == 0) { if (other.has_tagged_string_ptr_()) {
data_.ptr = other.data_.ptr; auto *str = new std::string(*other.get_string_ptr_());
} else if (uses_string_data_()) { data_.tagged = reinterpret_cast<uintptr_t>(str) | 1;
data_.string_ptr = new std::string(*other.data_.string_ptr);
} else { } else {
data_ = other.data_; // For POD types data_ = other.data_;
} }
} }
// Move constructor // Move constructor
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_), message_type_(other.message_type_) { MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.ptr = nullptr; }
other.message_type_ = 0; // Reset other to function pointer type
other.data_.ptr = nullptr;
}
// Assignment operators (needed for batch deduplication) // Assignment operators (needed for batch deduplication)
MessageCreator &operator=(const MessageCreator &other) { MessageCreator &operator=(const MessageCreator &other) {
if (this != &other) { if (this != &other) {
// Clean up current string data if needed // Clean up current string data if needed
if (uses_string_data_()) { if (has_tagged_string_ptr_()) {
delete data_.string_ptr; delete get_string_ptr_();
} }
// Copy new data // Copy new data
message_type_ = other.message_type_; if (other.has_tagged_string_ptr_()) {
if (other.message_type_ == 0) { auto *str = new std::string(*other.get_string_ptr_());
data_.ptr = other.data_.ptr; data_.tagged = reinterpret_cast<uintptr_t>(str) | 1;
} else if (other.uses_string_data_()) {
data_.string_ptr = new std::string(*other.data_.string_ptr);
} else { } else {
data_ = other.data_; data_ = other.data_;
} }
@ -543,30 +545,35 @@ class APIConnection : public APIServerConnection {
MessageCreator &operator=(MessageCreator &&other) noexcept { MessageCreator &operator=(MessageCreator &&other) noexcept {
if (this != &other) { if (this != &other) {
// Clean up current string data if needed // Clean up current string data if needed
if (uses_string_data_()) { if (has_tagged_string_ptr_()) {
delete data_.string_ptr; delete get_string_ptr_();
} }
// Move data // Move data
message_type_ = other.message_type_;
data_ = other.data_; data_ = other.data_;
// Reset other to safe state // Reset other to safe state
other.message_type_ = 0;
other.data_.ptr = nullptr; other.data_.ptr = nullptr;
} }
return *this; return *this;
} }
// Call operator // Call operator - now accepts message_type as parameter
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) const; uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
uint16_t message_type) const;
private: private:
// Helper to check if this message type uses heap-allocated strings // Check if this contains a string pointer
bool uses_string_data_() const { return message_type_ == EventResponse::MESSAGE_TYPE; } bool has_tagged_string_ptr_() const { return (data_.tagged & 1) != 0; }
union CreatorData {
MessageCreatorPtr ptr; // 8 bytes // Get the actual string pointer (clears the tag bit)
std::string *string_ptr; // 8 bytes std::string *get_string_ptr_() const {
} data_; // 8 bytes // NOLINTNEXTLINE(performance-no-int-to-ptr)
uint16_t message_type_; // 2 bytes (0 = function ptr, >0 = state capture) return reinterpret_cast<std::string *>(data_.tagged & ~uintptr_t(1));
}
union {
MessageCreatorPtr ptr;
uintptr_t tagged;
} data_; // 4 bytes on 32-bit
}; };
// Generic batching mechanism for both state updates and entity info // Generic batching mechanism for both state updates and entity info

View File

@ -66,6 +66,17 @@ const char *api_error_to_str(APIError err) {
return "UNKNOWN"; return "UNKNOWN";
} }
// Default implementation for loop - handles sending buffered data
APIError APIFrameHelper::loop() {
if (!this->tx_buf_.empty()) {
APIError err = try_send_tx_buf_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
}
}
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
}
// Helper method to buffer data from IOVs // Helper method to buffer data from IOVs
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
SendBuffer buffer; SendBuffer buffer;
@ -287,13 +298,8 @@ APIError APINoiseFrameHelper::loop() {
} }
} }
if (!this->tx_buf_.empty()) { // Use base class implementation for buffer sending
APIError err = try_send_tx_buf_(); return APIFrameHelper::loop();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
}
}
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
} }
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
@ -339,17 +345,15 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
if (rx_header_buf_[0] != 0x01) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
return APIError::BAD_INDICATOR;
}
// header reading done // header reading done
} }
// read body // read body
uint8_t indicator = rx_header_buf_[0];
if (indicator != 0x01) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", indicator);
return APIError::BAD_INDICATOR;
}
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
if (state_ != State::DATA && msg_size > 128) { if (state_ != State::DATA && msg_size > 128) {
@ -595,10 +599,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::BAD_DATA_PACKET; return APIError::BAD_DATA_PACKET;
} }
// uint16_t type;
// uint16_t data_len;
// uint8_t *data;
// uint8_t *padding; zero or more bytes to fill up the rest of the packet
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
if (data_len > msg_size - 4) { if (data_len > msg_size - 4) {
@ -831,18 +831,12 @@ APIError APIPlaintextFrameHelper::init() {
state_ = State::DATA; state_ = State::DATA;
return APIError::OK; return APIError::OK;
} }
/// Not used for plaintext
APIError APIPlaintextFrameHelper::loop() { APIError APIPlaintextFrameHelper::loop() {
if (state_ != State::DATA) { if (state_ != State::DATA) {
return APIError::BAD_STATE; return APIError::BAD_STATE;
} }
if (!this->tx_buf_.empty()) { // Use base class implementation for buffer sending
APIError err = try_send_tx_buf_(); return APIFrameHelper::loop();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
}
}
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
} }
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter

View File

@ -38,7 +38,7 @@ struct PacketInfo {
: message_type(type), offset(off), payload_size(size), padding(0) {} : message_type(type), offset(off), payload_size(size), padding(0) {}
}; };
enum class APIError : int { enum class APIError : uint16_t {
OK = 0, OK = 0,
WOULD_BLOCK = 1001, WOULD_BLOCK = 1001,
BAD_HANDSHAKE_PACKET_LEN = 1002, BAD_HANDSHAKE_PACKET_LEN = 1002,
@ -74,7 +74,7 @@ class APIFrameHelper {
} }
virtual ~APIFrameHelper() = default; virtual ~APIFrameHelper() = default;
virtual APIError init() = 0; virtual APIError init() = 0;
virtual APIError loop() = 0; virtual APIError loop();
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
std::string getpeername() { return socket_->getpeername(); } std::string getpeername() { return socket_->getpeername(); }

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@ class APIServerConnectionBase : public ProtoService {
template<typename T> bool send_message(const T &msg) { template<typename T> bool send_message(const T &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_send_message_(T::message_name(), msg.dump()); this->log_send_message_(msg.message_name(), msg.dump());
#endif #endif
return this->send_message_(msg, T::MESSAGE_TYPE); return this->send_message_(msg, T::MESSAGE_TYPE);
} }

View File

@ -47,6 +47,11 @@ void APIServer::setup() {
} }
#endif #endif
// Schedule reboot if no clients connect within timeout
if (this->reboot_timeout_ != 0) {
this->schedule_reboot_timeout_();
}
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (this->socket_ == nullptr) { if (this->socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket"); ESP_LOGW(TAG, "Could not create socket");
@ -106,8 +111,6 @@ void APIServer::setup() {
} }
#endif #endif
this->last_connected_ = App.get_loop_component_start_time();
#ifdef USE_ESP32_CAMERA #ifdef USE_ESP32_CAMERA
if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) { if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) {
esp32_camera::global_esp32_camera->add_image_callback( esp32_camera::global_esp32_camera->add_image_callback(
@ -121,6 +124,16 @@ void APIServer::setup() {
#endif #endif
} }
void APIServer::schedule_reboot_timeout_() {
this->status_set_warning();
this->set_timeout("api_reboot", this->reboot_timeout_, []() {
if (!global_api_server->is_connected()) {
ESP_LOGE(TAG, "No clients; rebooting");
App.reboot();
}
});
}
void APIServer::loop() { void APIServer::loop() {
// Accept new clients only if the socket exists and has incoming connections // Accept new clients only if the socket exists and has incoming connections
if (this->socket_ && this->socket_->ready()) { if (this->socket_ && this->socket_->ready()) {
@ -130,51 +143,61 @@ void APIServer::loop() {
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
if (!sock) if (!sock)
break; break;
ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str()); ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str());
auto *conn = new APIConnection(std::move(sock), this); auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn); this->clients_.emplace_back(conn);
conn->start(); conn->start();
// Clear warning status and cancel reboot when first client connects
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
this->cancel_timeout("api_reboot");
}
} }
} }
if (this->clients_.empty()) {
return;
}
// Process clients and remove disconnected ones in a single pass // Process clients and remove disconnected ones in a single pass
if (!this->clients_.empty()) { // Check network connectivity once for all clients
size_t client_index = 0; if (!network::is_connected()) {
while (client_index < this->clients_.size()) { // Network is down - disconnect all clients
auto &client = this->clients_[client_index]; for (auto &client : this->clients_) {
client->on_fatal_error();
if (client->remove_) { ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str());
// Handle disconnection
this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_);
ESP_LOGV(TAG, "Removing connection to %s", client->client_info_.c_str());
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Don't increment client_index since we need to process the swapped element
} else {
// Process active client
client->loop();
client_index++; // Move to next client
}
} }
// Continue to process and clean up the clients below
} }
if (this->reboot_timeout_ != 0) { size_t client_index = 0;
const uint32_t now = App.get_loop_component_start_time(); while (client_index < this->clients_.size()) {
if (!this->is_connected()) { auto &client = this->clients_[client_index];
if (now - this->last_connected_ > this->reboot_timeout_) {
ESP_LOGE(TAG, "No client connected; rebooting"); if (!client->remove_) {
App.reboot(); // Common case: process active client
} client->loop();
this->status_set_warning(); client_index++;
} else { continue;
this->last_connected_ = now;
this->status_clear_warning();
} }
// Rare case: handle disconnection
this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_);
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str());
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Schedule reboot when last client disconnects
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->schedule_reboot_timeout_();
}
// Don't increment client_index since we need to process the swapped element
} }
} }

View File

@ -142,6 +142,7 @@ class APIServer : public Component, public Controller {
} }
protected: protected:
void schedule_reboot_timeout_();
// Pointers and pointer-like types first (4 bytes each) // Pointers and pointer-like types first (4 bytes each)
std::unique_ptr<socket::Socket> socket_ = nullptr; std::unique_ptr<socket::Socket> socket_ = nullptr;
Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>(); Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>();
@ -150,7 +151,6 @@ class APIServer : public Component, public Controller {
// 4-byte aligned types // 4-byte aligned types
uint32_t reboot_timeout_{300000}; uint32_t reboot_timeout_{300000};
uint32_t batch_delay_{100}; uint32_t batch_delay_{100};
uint32_t last_connected_{0};
// Vectors and strings (12 bytes each on 32-bit) // Vectors and strings (12 bytes each on 32-bit)
std::vector<std::unique_ptr<APIConnection>> clients_; std::vector<std::unique_ptr<APIConnection>> clients_;

View File

@ -335,6 +335,7 @@ class ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
std::string dump() const; std::string dump() const;
virtual void dump_to(std::string &out) const = 0; virtual void dump_to(std::string &out) const = 0;
virtual const char *message_name() const { return "unknown"; }
#endif #endif
protected: protected:

View File

@ -4,7 +4,7 @@ import logging
import os import os
from pathlib import Path from pathlib import Path
from esphome import git from esphome import yaml_util
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@ -23,7 +23,6 @@ from esphome.const import (
CONF_REFRESH, CONF_REFRESH,
CONF_SOURCE, CONF_SOURCE,
CONF_TYPE, CONF_TYPE,
CONF_URL,
CONF_VARIANT, CONF_VARIANT,
CONF_VERSION, CONF_VERSION,
KEY_CORE, KEY_CORE,
@ -32,14 +31,13 @@ from esphome.const import (
KEY_TARGET_FRAMEWORK, KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM, KEY_TARGET_PLATFORM,
PLATFORM_ESP32, PLATFORM_ESP32,
TYPE_GIT,
TYPE_LOCAL,
__version__, __version__,
) )
from esphome.core import CORE, HexInt, TimePeriod from esphome.core import CORE, HexInt, TimePeriod
from esphome.cpp_generator import RawExpression from esphome.cpp_generator import RawExpression
import esphome.final_validate as fv import esphome.final_validate as fv
from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed
from esphome.types import ConfigType
from .boards import BOARDS from .boards import BOARDS
from .const import ( # noqa from .const import ( # noqa
@ -49,10 +47,8 @@ from .const import ( # noqa
KEY_EXTRA_BUILD_FILES, KEY_EXTRA_BUILD_FILES,
KEY_PATH, KEY_PATH,
KEY_REF, KEY_REF,
KEY_REFRESH,
KEY_REPO, KEY_REPO,
KEY_SDKCONFIG_OPTIONS, KEY_SDKCONFIG_OPTIONS,
KEY_SUBMODULES,
KEY_VARIANT, KEY_VARIANT,
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C2, VARIANT_ESP32C2,
@ -235,7 +231,7 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType):
def add_idf_component( def add_idf_component(
*, *,
name: str, name: str,
repo: str, repo: str = None,
ref: str = None, ref: str = None,
path: str = None, path: str = None,
refresh: TimePeriod = None, refresh: TimePeriod = None,
@ -245,30 +241,27 @@ def add_idf_component(
"""Add an esp-idf component to the project.""" """Add an esp-idf component to the project."""
if not CORE.using_esp_idf: if not CORE.using_esp_idf:
raise ValueError("Not an esp-idf project") raise ValueError("Not an esp-idf project")
if components is None: if not repo and not ref and not path:
components = [] raise ValueError("Requires at least one of repo, ref or path")
if name not in CORE.data[KEY_ESP32][KEY_COMPONENTS]: if refresh or submodules or components:
_LOGGER.warning(
"The refresh, components and submodules parameters in add_idf_component() are "
"deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report "
"an issue to the external_component author and ask them to update it."
)
if components:
for comp in components:
CORE.data[KEY_ESP32][KEY_COMPONENTS][comp] = {
KEY_REPO: repo,
KEY_REF: ref,
KEY_PATH: f"{path}/{comp}" if path else comp,
}
else:
CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = {
KEY_REPO: repo, KEY_REPO: repo,
KEY_REF: ref, KEY_REF: ref,
KEY_PATH: path, KEY_PATH: path,
KEY_REFRESH: refresh,
KEY_COMPONENTS: components,
KEY_SUBMODULES: submodules,
} }
else:
component_config = CORE.data[KEY_ESP32][KEY_COMPONENTS][name]
if components is not None:
component_config[KEY_COMPONENTS] = list(
set(component_config[KEY_COMPONENTS] + components)
)
if submodules is not None:
if component_config[KEY_SUBMODULES] is None:
component_config[KEY_SUBMODULES] = submodules
else:
component_config[KEY_SUBMODULES] = list(
set(component_config[KEY_SUBMODULES] + submodules)
)
def add_extra_script(stage: str, filename: str, path: str): def add_extra_script(stage: str, filename: str, path: str):
@ -575,6 +568,17 @@ CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server"
CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries" CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries"
CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface"
def _validate_idf_component(config: ConfigType) -> ConfigType:
"""Validate IDF component config and warn about deprecated options."""
if CONF_REFRESH in config:
_LOGGER.warning(
"The 'refresh' option for IDF components is deprecated and has no effect. "
"It will be removed in ESPHome 2026.1. Please remove it from your configuration."
)
return config
ESP_IDF_FRAMEWORK_SCHEMA = cv.All( ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
@ -614,15 +618,19 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
} }
), ),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
cv.Schema( cv.All(
{ cv.Schema(
cv.Required(CONF_NAME): cv.string_strict, {
cv.Required(CONF_SOURCE): cv.SOURCE_SCHEMA, cv.Required(CONF_NAME): cv.string_strict,
cv.Optional(CONF_PATH): cv.string, cv.Optional(CONF_SOURCE): cv.git_ref,
cv.Optional(CONF_REFRESH, default="1d"): cv.All( cv.Optional(CONF_REF): cv.string,
cv.string, cv.source_refresh cv.Optional(CONF_PATH): cv.string,
), cv.Optional(CONF_REFRESH): cv.All(
} cv.string, cv.source_refresh
),
}
),
_validate_idf_component,
) )
), ),
} }
@ -814,18 +822,12 @@ async def to_code(config):
add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
for component in conf[CONF_COMPONENTS]: for component in conf[CONF_COMPONENTS]:
source = component[CONF_SOURCE] add_idf_component(
if source[CONF_TYPE] == TYPE_GIT: name=component[CONF_NAME],
add_idf_component( repo=component.get(CONF_SOURCE),
name=component[CONF_NAME], ref=component.get(CONF_REF),
repo=source[CONF_URL], path=component.get(CONF_PATH),
ref=source.get(CONF_REF), )
path=component.get(CONF_PATH),
refresh=component[CONF_REFRESH],
)
elif source[CONF_TYPE] == TYPE_LOCAL:
_LOGGER.warning("Local components are not implemented yet.")
elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
cg.add_platformio_option("framework", "arduino") cg.add_platformio_option("framework", "arduino")
cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ARDUINO")
@ -924,6 +926,26 @@ def _write_sdkconfig():
write_file_if_changed(sdk_path, contents) write_file_if_changed(sdk_path, contents)
def _write_idf_component_yml():
yml_path = Path(CORE.relative_build_path("src/idf_component.yml"))
if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
dependencies = {}
for name, component in components.items():
dependency = {}
if component[KEY_REF]:
dependency["version"] = component[KEY_REF]
if component[KEY_REPO]:
dependency["git"] = component[KEY_REPO]
if component[KEY_PATH]:
dependency["path"] = component[KEY_PATH]
dependencies[name] = dependency
contents = yaml_util.dump({"dependencies": dependencies})
else:
contents = ""
write_file_if_changed(yml_path, contents)
# Called by writer.py # Called by writer.py
def copy_files(): def copy_files():
if CORE.using_arduino: if CORE.using_arduino:
@ -936,6 +958,7 @@ def copy_files():
) )
if CORE.using_esp_idf: if CORE.using_esp_idf:
_write_sdkconfig() _write_sdkconfig()
_write_idf_component_yml()
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
write_file_if_changed( write_file_if_changed(
CORE.relative_build_path("partitions.csv"), CORE.relative_build_path("partitions.csv"),
@ -952,55 +975,6 @@ def copy_files():
__version__, __version__,
) )
import shutil
shutil.rmtree(CORE.relative_build_path("components"), ignore_errors=True)
if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
for name, component in components.items():
repo_dir, _ = git.clone_or_update(
url=component[KEY_REPO],
ref=component[KEY_REF],
refresh=component[KEY_REFRESH],
domain="idf_components",
submodules=component[KEY_SUBMODULES],
)
mkdir_p(CORE.relative_build_path("components"))
component_dir = repo_dir
if component[KEY_PATH] is not None:
component_dir = component_dir / component[KEY_PATH]
if component[KEY_COMPONENTS] == ["*"]:
shutil.copytree(
component_dir,
CORE.relative_build_path("components"),
dirs_exist_ok=True,
ignore=shutil.ignore_patterns(".git*"),
symlinks=True,
ignore_dangling_symlinks=True,
)
elif len(component[KEY_COMPONENTS]) > 0:
for comp in component[KEY_COMPONENTS]:
shutil.copytree(
component_dir / comp,
CORE.relative_build_path(f"components/{comp}"),
dirs_exist_ok=True,
ignore=shutil.ignore_patterns(".git*"),
symlinks=True,
ignore_dangling_symlinks=True,
)
else:
shutil.copytree(
component_dir,
CORE.relative_build_path(f"components/{name}"),
dirs_exist_ok=True,
ignore=shutil.ignore_patterns(".git*"),
symlinks=True,
ignore_dangling_symlinks=True,
)
for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items(): for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items():
if file[KEY_PATH].startswith("http"): if file[KEY_PATH].startswith("http"):
import requests import requests

View File

@ -17,8 +17,9 @@ namespace esphome {
namespace ld2450 { namespace ld2450 {
static const char *const TAG = "ld2450"; static const char *const TAG = "ld2450";
static const char *const NO_MAC("08:05:04:03:02:01"); static const char *const NO_MAC = "08:05:04:03:02:01";
static const char *const UNKNOWN_MAC("unknown"); static const char *const UNKNOWN_MAC = "unknown";
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
// LD2450 UART Serial Commands // LD2450 UART Serial Commands
static const uint8_t CMD_ENABLE_CONF = 0x00FF; static const uint8_t CMD_ENABLE_CONF = 0x00FF;
@ -98,13 +99,6 @@ static inline std::string get_direction(int16_t speed) {
return STATIONARY; return STATIONARY;
} }
static inline std::string format_version(uint8_t *buffer) {
return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15],
buffer[14]);
}
LD2450Component::LD2450Component() {}
void LD2450Component::setup() { void LD2450Component::setup() {
ESP_LOGCONFIG(TAG, "Running setup"); ESP_LOGCONFIG(TAG, "Running setup");
#ifdef USE_NUMBER #ifdef USE_NUMBER
@ -189,7 +183,7 @@ void LD2450Component::dump_config() {
" Throttle: %ums\n" " Throttle: %ums\n"
" MAC Address: %s\n" " MAC Address: %s\n"
" Firmware version: %s", " Firmware version: %s",
this->throttle_, const_cast<char *>(this->mac_.c_str()), const_cast<char *>(this->version_.c_str())); this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str());
} }
void LD2450Component::loop() { void LD2450Component::loop() {
@ -596,7 +590,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
#endif #endif
break; break;
case lowbyte(CMD_VERSION): case lowbyte(CMD_VERSION):
this->version_ = ld2450::format_version(buffer); this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]);
ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str());
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr) { if (this->version_text_sensor_ != nullptr) {
@ -617,7 +611,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
if (this->bluetooth_switch_ != nullptr) { if (this->bluetooth_switch_ != nullptr) {
this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC);
} }
#endif #endif
break; break;

View File

@ -141,7 +141,6 @@ class LD2450Component : public Component, public uart::UARTDevice {
#endif #endif
public: public:
LD2450Component();
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
void loop() override; void loop() override;
@ -197,17 +196,17 @@ class LD2450Component : public Component, public uart::UARTDevice {
bool get_timeout_status_(uint32_t check_millis); bool get_timeout_status_(uint32_t check_millis);
uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving);
Target target_info_[MAX_TARGETS];
Zone zone_config_[MAX_ZONES];
uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer
uint8_t buffer_data_[MAX_LINE_LENGTH];
uint32_t last_periodic_millis_ = 0; uint32_t last_periodic_millis_ = 0;
uint32_t presence_millis_ = 0; uint32_t presence_millis_ = 0;
uint32_t still_presence_millis_ = 0; uint32_t still_presence_millis_ = 0;
uint32_t moving_presence_millis_ = 0; uint32_t moving_presence_millis_ = 0;
uint16_t throttle_ = 0; uint16_t throttle_ = 0;
uint16_t timeout_ = 5; uint16_t timeout_ = 5;
uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer
uint8_t buffer_data_[MAX_LINE_LENGTH];
uint8_t zone_type_ = 0; uint8_t zone_type_ = 0;
Target target_info_[MAX_TARGETS];
Zone zone_config_[MAX_ZONES];
std::string version_{}; std::string version_{};
std::string mac_{}; std::string mac_{};
#ifdef USE_NUMBER #ifdef USE_NUMBER

View File

@ -48,6 +48,11 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
// For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered // For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered
message_sent = message_sent =
this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), current_task, format, args); this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), current_task, format, args);
if (message_sent) {
// Enable logger loop to process the buffered message
// This is safe to call from any context including ISRs
this->enable_loop_soon_any_context();
}
#endif // USE_ESPHOME_TASK_LOG_BUFFER #endif // USE_ESPHOME_TASK_LOG_BUFFER
// Emergency console logging for non-main tasks when ring buffer is full or disabled // Emergency console logging for non-main tasks when ring buffer is full or disabled
@ -139,6 +144,10 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #ifdef USE_ESPHOME_TASK_LOG_BUFFER
void Logger::init_log_buffer(size_t total_buffer_size) { void Logger::init_log_buffer(size_t total_buffer_size) {
this->log_buffer_ = esphome::make_unique<logger::TaskLogBuffer>(total_buffer_size); this->log_buffer_ = esphome::make_unique<logger::TaskLogBuffer>(total_buffer_size);
// Start with loop disabled when using task buffer (unless using USB CDC)
// The loop will be enabled automatically when messages arrive
this->disable_loop_when_buffer_empty_();
} }
#endif #endif
@ -189,6 +198,10 @@ void Logger::loop() {
this->write_msg_(this->tx_buffer_); this->write_msg_(this->tx_buffer_);
} }
} }
} else {
// No messages to process, disable loop if appropriate
// This reduces overhead when there's no async logging activity
this->disable_loop_when_buffer_empty_();
} }
#endif #endif
} }

View File

@ -358,6 +358,26 @@ class Logger : public Component {
static const uint16_t RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR); static const uint16_t RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR);
this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size);
} }
#ifdef USE_ESP32
// Disable loop when task buffer is empty (with USB CDC check)
inline void disable_loop_when_buffer_empty_() {
// Thread safety note: This is safe even if another task calls enable_loop_soon_any_context()
// concurrently. If that happens between our check and disable_loop(), the enable request
// will be processed on the next main loop iteration since:
// - disable_loop() takes effect immediately
// - enable_loop_soon_any_context() sets a pending flag that's checked at loop start
#if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO)
// Only disable if not using USB CDC (which needs loop for connection detection)
if (this->uart_ != UART_SELECTION_USB_CDC) {
this->disable_loop();
}
#else
// No USB CDC support, always safe to disable
this->disable_loop();
#endif
}
#endif
}; };
extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@ -1,5 +1,7 @@
#pragma once #pragma once
#include <algorithm>
#include <limits>
#include <string> #include <string>
#include <vector> #include <vector>
#include "esphome/core/component.h" #include "esphome/core/component.h"
@ -335,11 +337,16 @@ class Application {
* Each component can request a high frequency loop execution by using the HighFrequencyLoopRequester * Each component can request a high frequency loop execution by using the HighFrequencyLoopRequester
* helper in helpers.h * helper in helpers.h
* *
* Note: This method is not called by ESPHome core code. It is only used by lambda functions
* in YAML configurations or by external components.
*
* @param loop_interval The interval in milliseconds to run the core loop at. Defaults to 16 milliseconds. * @param loop_interval The interval in milliseconds to run the core loop at. Defaults to 16 milliseconds.
*/ */
void set_loop_interval(uint32_t loop_interval) { this->loop_interval_ = loop_interval; } void set_loop_interval(uint32_t loop_interval) {
this->loop_interval_ = std::min(loop_interval, static_cast<uint32_t>(std::numeric_limits<uint16_t>::max()));
}
uint32_t get_loop_interval() const { return this->loop_interval_; } uint32_t get_loop_interval() const { return static_cast<uint32_t>(this->loop_interval_); }
void schedule_dump_config() { this->dump_config_at_ = 0; } void schedule_dump_config() { this->dump_config_at_ = 0; }
@ -618,6 +625,17 @@ class Application {
/// Perform a delay while also monitoring socket file descriptors for readiness /// Perform a delay while also monitoring socket file descriptors for readiness
void yield_with_select_(uint32_t delay_ms); void yield_with_select_(uint32_t delay_ms);
// === Member variables ordered by size to minimize padding ===
// Pointer-sized members first
Component *current_component_{nullptr};
const char *comment_{nullptr};
const char *compilation_time_{nullptr};
// size_t members
size_t dump_config_at_{SIZE_MAX};
// Vectors (largest members)
std::vector<Component *> components_{}; std::vector<Component *> components_{};
// Partitioned vector design for looping components // Partitioned vector design for looping components
@ -637,11 +655,6 @@ class Application {
// and active_end_ is incremented // and active_end_ is incremented
// - This eliminates branch mispredictions from flag checking in the hot loop // - This eliminates branch mispredictions from flag checking in the hot loop
std::vector<Component *> looping_components_{}; std::vector<Component *> looping_components_{};
uint16_t looping_components_active_end_{0};
// For safe reentrant modifications during iteration
uint16_t current_loop_index_{0};
bool in_loop_{false};
#ifdef USE_DEVICES #ifdef USE_DEVICES
std::vector<Device *> devices_{}; std::vector<Device *> devices_{};
@ -713,26 +726,39 @@ class Application {
std::vector<update::UpdateEntity *> updates_{}; std::vector<update::UpdateEntity *> updates_{};
#endif #endif
#ifdef USE_SOCKET_SELECT_SUPPORT
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
#endif
// String members
std::string name_; std::string name_;
std::string friendly_name_; std::string friendly_name_;
const char *comment_{nullptr};
const char *compilation_time_{nullptr}; // 4-byte members
bool name_add_mac_suffix_;
uint32_t last_loop_{0}; uint32_t last_loop_{0};
uint32_t loop_interval_{16};
size_t dump_config_at_{SIZE_MAX};
uint8_t app_state_{0};
volatile bool has_pending_enable_loop_requests_{false};
Component *current_component_{nullptr};
uint32_t loop_component_start_time_{0}; uint32_t loop_component_start_time_{0};
#ifdef USE_SOCKET_SELECT_SUPPORT #ifdef USE_SOCKET_SELECT_SUPPORT
// Socket select management int max_fd_{-1}; // Highest file descriptor number for select()
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors #endif
// 2-byte members (grouped together for alignment)
uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds)
uint16_t looping_components_active_end_{0};
uint16_t current_loop_index_{0}; // For safe reentrant modifications during iteration
// 1-byte members (grouped together to minimize padding)
uint8_t app_state_{0};
bool name_add_mac_suffix_;
bool in_loop_{false};
volatile bool has_pending_enable_loop_requests_{false};
#ifdef USE_SOCKET_SELECT_SUPPORT
bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes
int max_fd_{-1}; // Highest file descriptor number for select()
fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes // Variable-sized members at end
fd_set read_fds_{}; // Working fd_set for select(), copied from base_read_fds_ fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes
fd_set read_fds_{}; // Working fd_set for select(), copied from base_read_fds_
#endif #endif
}; };

View File

@ -27,20 +27,67 @@ template<typename T, typename... X> class TemplatableValue {
public: public:
TemplatableValue() : type_(NONE) {} TemplatableValue() : type_(NONE) {}
template<typename F, enable_if_t<!is_invocable<F, X...>::value, int> = 0> template<typename F, enable_if_t<!is_invocable<F, X...>::value, int> = 0> TemplatableValue(F value) : type_(VALUE) {
TemplatableValue(F value) : type_(VALUE), value_(std::move(value)) {} new (&this->value_) T(std::move(value));
}
template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0> template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0> TemplatableValue(F f) : type_(LAMBDA) {
TemplatableValue(F f) : type_(LAMBDA), f_(f) {} this->f_ = new std::function<T(X...)>(std::move(f));
}
// Copy constructor
TemplatableValue(const TemplatableValue &other) : type_(other.type_) {
if (type_ == VALUE) {
new (&this->value_) T(other.value_);
} else if (type_ == LAMBDA) {
this->f_ = new std::function<T(X...)>(*other.f_);
}
}
// Move constructor
TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) {
if (type_ == VALUE) {
new (&this->value_) T(std::move(other.value_));
} else if (type_ == LAMBDA) {
this->f_ = other.f_;
other.f_ = nullptr;
}
other.type_ = NONE;
}
// Assignment operators
TemplatableValue &operator=(const TemplatableValue &other) {
if (this != &other) {
this->~TemplatableValue();
new (this) TemplatableValue(other);
}
return *this;
}
TemplatableValue &operator=(TemplatableValue &&other) noexcept {
if (this != &other) {
this->~TemplatableValue();
new (this) TemplatableValue(std::move(other));
}
return *this;
}
~TemplatableValue() {
if (type_ == VALUE) {
this->value_.~T();
} else if (type_ == LAMBDA) {
delete this->f_;
}
}
bool has_value() { return this->type_ != NONE; } bool has_value() { return this->type_ != NONE; }
T value(X... x) { T value(X... x) {
if (this->type_ == LAMBDA) { if (this->type_ == LAMBDA) {
return this->f_(x...); return (*this->f_)(x...);
} }
// return value also when none // return value also when none
return this->value_; return this->type_ == VALUE ? this->value_ : T{};
} }
optional<T> optional_value(X... x) { optional<T> optional_value(X... x) {
@ -58,14 +105,16 @@ template<typename T, typename... X> class TemplatableValue {
} }
protected: protected:
enum { enum : uint8_t {
NONE, NONE,
VALUE, VALUE,
LAMBDA, LAMBDA,
} type_; } type_;
T value_{}; union {
std::function<T(X...)> f_{}; T value_;
std::function<T(X...)> *f_;
};
}; };
/** Base class for all automation conditions. /** Base class for all automation conditions.

View File

@ -132,6 +132,8 @@
// ESP32-specific feature flags // ESP32-specific feature flags
#ifdef USE_ESP32 #ifdef USE_ESP32
#define USE_ESPHOME_TASK_LOG_BUFFER
#define USE_BLUETOOTH_PROXY #define USE_BLUETOOTH_PROXY
#define USE_CAPTIVE_PORTAL #define USE_CAPTIVE_PORTAL
#define USE_ESP32_BLE #define USE_ESP32_BLE

View File

@ -886,7 +886,7 @@ def build_message_type(
public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP") public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP")
snake_name = camel_to_snake(desc.name) snake_name = camel_to_snake(desc.name)
public_content.append( public_content.append(
f'static constexpr const char *message_name() {{ return "{snake_name}"; }}' f'const char *message_name() const override {{ return "{snake_name}"; }}'
) )
public_content.append("#endif") public_content.append("#endif")
@ -1356,7 +1356,7 @@ def main() -> None:
hpp += " template<typename T>\n" hpp += " template<typename T>\n"
hpp += " bool send_message(const T &msg) {\n" hpp += " bool send_message(const T &msg) {\n"
hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
hpp += " this->log_send_message_(T::message_name(), msg.dump());\n" hpp += " this->log_send_message_(msg.message_name(), msg.dump());\n"
hpp += "#endif\n" hpp += "#endif\n"
hpp += " return this->send_message_(msg, T::MESSAGE_TYPE);\n" hpp += " return this->send_message_(msg, T::MESSAGE_TYPE);\n"
hpp += " }\n\n" hpp += " }\n\n"

0
script/run-in-env.py Normal file → Executable file
View File

View File

@ -0,0 +1,7 @@
esphome:
name: api-reboot-test
host:
api:
reboot_timeout: 0.5s # Very short timeout for fast testing
logger:
level: DEBUG

View File

@ -0,0 +1,35 @@
"""Test API server reboot timeout functionality."""
import asyncio
import re
import pytest
from .types import RunCompiledFunction
@pytest.mark.asyncio
async def test_api_reboot_timeout(
yaml_config: str,
run_compiled: RunCompiledFunction,
) -> None:
"""Test that the device reboots when no API clients connect within the timeout."""
loop = asyncio.get_running_loop()
reboot_future = loop.create_future()
reboot_pattern = re.compile(r"No clients; rebooting")
def check_output(line: str) -> None:
"""Check output for reboot message."""
if not reboot_future.done() and reboot_pattern.search(line):
reboot_future.set_result(True)
# Run the device without connecting any API client
async with run_compiled(yaml_config, line_callback=check_output):
# Wait for reboot with timeout
# (0.5s reboot timeout + some margin for processing)
try:
await asyncio.wait_for(reboot_future, timeout=2.0)
except asyncio.TimeoutError:
pytest.fail("Device did not reboot within expected timeout")
# Test passes if we get here - reboot was detected