Compare commits
9 Commits
applicatio
...
jesserockz
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6e7e48b73 | ||
|
|
f80610d958 | ||
|
|
1aacf13888 | ||
|
|
23b1e428de | ||
|
|
f029f4f20e | ||
|
|
79e3d2b2d7 | ||
|
|
c74e5e0f04 | ||
|
|
15ef93ccc9 | ||
|
|
e017250445 |
25
.github/workflows/lock.yml
vendored
25
.github/workflows/lock.yml
vendored
@@ -1,28 +1,13 @@
|
||||
---
|
||||
name: Lock
|
||||
name: Lock closed issues and PRs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 0 * * *"
|
||||
- cron: "30 0 * * *" # Run daily at 00:30 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: lock
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@main
|
||||
with:
|
||||
since-days: 3650
|
||||
|
||||
@@ -33,9 +33,14 @@ namespace api {
|
||||
// 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.
|
||||
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";
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
static const int ESP32_CAMERA_STOP_STREAM = 5000;
|
||||
#endif
|
||||
|
||||
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
|
||||
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
|
||||
@@ -90,16 +95,6 @@ APIConnection::~APIConnection() {
|
||||
}
|
||||
|
||||
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_) {
|
||||
// requested a disconnect
|
||||
this->helper_->close();
|
||||
@@ -152,20 +147,19 @@ void APIConnection::loop() {
|
||||
|
||||
// Process deferred batch if 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_();
|
||||
}
|
||||
|
||||
if (!this->list_entities_iterator_.completed())
|
||||
if (!this->list_entities_iterator_.completed()) {
|
||||
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();
|
||||
}
|
||||
|
||||
static uint8_t max_ping_retries = 60;
|
||||
static uint16_t ping_retry_interval = 1000;
|
||||
if (this->sent_ping_) {
|
||||
// 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();
|
||||
ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str());
|
||||
}
|
||||
@@ -173,17 +167,15 @@ void APIConnection::loop() {
|
||||
ESP_LOGVV(TAG, "Sending keepalive PING");
|
||||
this->sent_ping_ = this->send_message(PingRequest());
|
||||
if (!this->sent_ping_) {
|
||||
this->next_ping_retry_ = now + ping_retry_interval;
|
||||
this->next_ping_retry_ = now + PING_RETRY_INTERVAL;
|
||||
this->ping_retries_++;
|
||||
std::string warn_str = str_sprintf("%s: Sending keepalive failed %u time(s);",
|
||||
this->get_client_combined_info().c_str(), this->ping_retries_);
|
||||
if (this->ping_retries_ >= max_ping_retries) {
|
||||
if (this->ping_retries_ >= MAX_PING_RETRIES) {
|
||||
on_fatal_error();
|
||||
ESP_LOGE(TAG, "%s disconnecting", warn_str.c_str());
|
||||
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 retrying in %u ms", warn_str.c_str(), ping_retry_interval);
|
||||
ESP_LOGW(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "%s retrying in %u ms", warn_str.c_str(), ping_retry_interval);
|
||||
ESP_LOGD(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,22 +199,20 @@ void APIConnection::loop() {
|
||||
// bool done = 3;
|
||||
buffer.encode_bool(3, done);
|
||||
|
||||
bool success = this->send_buffer(buffer, 44);
|
||||
bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE);
|
||||
|
||||
if (success) {
|
||||
this->image_reader_.consume_data(to_send);
|
||||
}
|
||||
if (success && done) {
|
||||
this->image_reader_.return_image();
|
||||
if (done) {
|
||||
this->image_reader_.return_image();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (state_subs_at_ != -1) {
|
||||
if (state_subs_at_ >= 0) {
|
||||
const auto &subs = this->parent_->get_state_subs();
|
||||
if (state_subs_at_ >= (int) subs.size()) {
|
||||
state_subs_at_ = -1;
|
||||
} else {
|
||||
if (state_subs_at_ < static_cast<int>(subs.size())) {
|
||||
auto &it = subs[state_subs_at_];
|
||||
SubscribeHomeAssistantStateResponse resp;
|
||||
resp.entity_id = it.entity_id;
|
||||
@@ -231,6 +221,8 @@ void APIConnection::loop() {
|
||||
if (this->send_message(resp)) {
|
||||
state_subs_at_++;
|
||||
}
|
||||
} else {
|
||||
state_subs_at_ = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,6 +276,11 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes
|
||||
// Encode directly into 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)
|
||||
size_t actual_payload_size = shared_buf.size() - size_before_encode;
|
||||
|
||||
@@ -1440,7 +1437,7 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
|
||||
|
||||
#ifdef USE_EVENT
|
||||
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) {
|
||||
this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE);
|
||||
@@ -1795,7 +1792,8 @@ void APIConnection::process_batch_() {
|
||||
const auto &item = this->deferred_batch_.items[0];
|
||||
|
||||
// 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 &&
|
||||
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) {
|
||||
@@ -1845,7 +1843,7 @@ void APIConnection::process_batch_() {
|
||||
for (const auto &item : this->deferred_batch_.items) {
|
||||
// Try to encode message
|
||||
// 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) {
|
||||
// Message won't fit, stop processing
|
||||
@@ -1908,21 +1906,23 @@ void APIConnection::process_batch_() {
|
||||
}
|
||||
|
||||
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single) const {
|
||||
switch (message_type_) {
|
||||
case 0: // Function pointer
|
||||
return data_.ptr(entity, conn, remaining_size, is_single);
|
||||
|
||||
bool is_single, uint16_t message_type) const {
|
||||
if (has_tagged_string_ptr_()) {
|
||||
// Handle string-based messages
|
||||
switch (message_type) {
|
||||
#ifdef USE_EVENT
|
||||
case EventResponse::MESSAGE_TYPE: {
|
||||
auto *e = static_cast<event::Event *>(entity);
|
||||
return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single);
|
||||
}
|
||||
case EventResponse::MESSAGE_TYPE: {
|
||||
auto *e = static_cast<event::Event *>(entity);
|
||||
return APIConnection::try_send_event_response(e, *get_string_ptr_(), conn, remaining_size, is_single);
|
||||
}
|
||||
#endif
|
||||
|
||||
default:
|
||||
// Should not happen, return 0 to indicate no message
|
||||
return 0;
|
||||
default:
|
||||
// Should not happen, return 0 to indicate no message
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
// Function pointer case
|
||||
return data_.ptr(entity, conn, remaining_size, is_single);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -483,55 +483,57 @@ class APIConnection : public APIServerConnection {
|
||||
// Function pointer type for message encoding
|
||||
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 {
|
||||
// Ensure pointer alignment allows LSB tagging
|
||||
static_assert(alignof(std::string *) > 1, "String pointer alignment must be > 1 for LSB tagging");
|
||||
|
||||
public:
|
||||
// Constructor for function pointer (message_type = 0)
|
||||
MessageCreator(MessageCreatorPtr ptr) : message_type_(0) { data_.ptr = ptr; }
|
||||
// Constructor for function pointer
|
||||
MessageCreator(MessageCreatorPtr ptr) {
|
||||
// Function pointers are always aligned, so LSB is 0
|
||||
data_.ptr = ptr;
|
||||
}
|
||||
|
||||
// Constructor for string state capture
|
||||
MessageCreator(const std::string &value, uint16_t msg_type) : message_type_(msg_type) {
|
||||
data_.string_ptr = new std::string(value);
|
||||
explicit MessageCreator(const std::string &str_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
|
||||
~MessageCreator() {
|
||||
// Clean up string data for string-based message types
|
||||
if (uses_string_data_()) {
|
||||
delete data_.string_ptr;
|
||||
if (has_tagged_string_ptr_()) {
|
||||
delete get_string_ptr_();
|
||||
}
|
||||
}
|
||||
|
||||
// Copy constructor
|
||||
MessageCreator(const MessageCreator &other) : message_type_(other.message_type_) {
|
||||
if (message_type_ == 0) {
|
||||
data_.ptr = other.data_.ptr;
|
||||
} else if (uses_string_data_()) {
|
||||
data_.string_ptr = new std::string(*other.data_.string_ptr);
|
||||
MessageCreator(const MessageCreator &other) {
|
||||
if (other.has_tagged_string_ptr_()) {
|
||||
auto *str = new std::string(*other.get_string_ptr_());
|
||||
data_.tagged = reinterpret_cast<uintptr_t>(str) | 1;
|
||||
} else {
|
||||
data_ = other.data_; // For POD types
|
||||
data_ = other.data_;
|
||||
}
|
||||
}
|
||||
|
||||
// Move constructor
|
||||
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_), message_type_(other.message_type_) {
|
||||
other.message_type_ = 0; // Reset other to function pointer type
|
||||
other.data_.ptr = nullptr;
|
||||
}
|
||||
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.ptr = nullptr; }
|
||||
|
||||
// Assignment operators (needed for batch deduplication)
|
||||
MessageCreator &operator=(const MessageCreator &other) {
|
||||
if (this != &other) {
|
||||
// Clean up current string data if needed
|
||||
if (uses_string_data_()) {
|
||||
delete data_.string_ptr;
|
||||
if (has_tagged_string_ptr_()) {
|
||||
delete get_string_ptr_();
|
||||
}
|
||||
// Copy new data
|
||||
message_type_ = other.message_type_;
|
||||
if (other.message_type_ == 0) {
|
||||
data_.ptr = other.data_.ptr;
|
||||
} else if (other.uses_string_data_()) {
|
||||
data_.string_ptr = new std::string(*other.data_.string_ptr);
|
||||
if (other.has_tagged_string_ptr_()) {
|
||||
auto *str = new std::string(*other.get_string_ptr_());
|
||||
data_.tagged = reinterpret_cast<uintptr_t>(str) | 1;
|
||||
} else {
|
||||
data_ = other.data_;
|
||||
}
|
||||
@@ -542,30 +544,35 @@ class APIConnection : public APIServerConnection {
|
||||
MessageCreator &operator=(MessageCreator &&other) noexcept {
|
||||
if (this != &other) {
|
||||
// Clean up current string data if needed
|
||||
if (uses_string_data_()) {
|
||||
delete data_.string_ptr;
|
||||
if (has_tagged_string_ptr_()) {
|
||||
delete get_string_ptr_();
|
||||
}
|
||||
// Move data
|
||||
message_type_ = other.message_type_;
|
||||
data_ = other.data_;
|
||||
// Reset other to safe state
|
||||
other.message_type_ = 0;
|
||||
other.data_.ptr = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Call operator
|
||||
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) const;
|
||||
// Call operator - now accepts message_type as parameter
|
||||
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
|
||||
uint16_t message_type) const;
|
||||
|
||||
private:
|
||||
// Helper to check if this message type uses heap-allocated strings
|
||||
bool uses_string_data_() const { return message_type_ == EventResponse::MESSAGE_TYPE; }
|
||||
union CreatorData {
|
||||
MessageCreatorPtr ptr; // 8 bytes
|
||||
std::string *string_ptr; // 8 bytes
|
||||
} data_; // 8 bytes
|
||||
uint16_t message_type_; // 2 bytes (0 = function ptr, >0 = state capture)
|
||||
// Check if this contains a string pointer
|
||||
bool has_tagged_string_ptr_() const { return (data_.tagged & 1) != 0; }
|
||||
|
||||
// Get the actual string pointer (clears the tag bit)
|
||||
std::string *get_string_ptr_() const {
|
||||
// NOLINTNEXTLINE(performance-no-int-to-ptr)
|
||||
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
|
||||
|
||||
@@ -66,6 +66,17 @@ const char *api_error_to_str(APIError err) {
|
||||
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
|
||||
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
|
||||
SendBuffer buffer;
|
||||
@@ -287,13 +298,8 @@ APIError APINoiseFrameHelper::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
|
||||
// Use base class implementation for buffer sending
|
||||
return APIFrameHelper::loop();
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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];
|
||||
|
||||
if (state_ != State::DATA && msg_size > 128) {
|
||||
@@ -595,10 +599,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
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 data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
|
||||
if (data_len > msg_size - 4) {
|
||||
@@ -831,18 +831,12 @@ APIError APIPlaintextFrameHelper::init() {
|
||||
state_ = State::DATA;
|
||||
return APIError::OK;
|
||||
}
|
||||
/// Not used for plaintext
|
||||
APIError APIPlaintextFrameHelper::loop() {
|
||||
if (state_ != State::DATA) {
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
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
|
||||
// Use base class implementation for buffer sending
|
||||
return APIFrameHelper::loop();
|
||||
}
|
||||
|
||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
|
||||
|
||||
@@ -38,7 +38,7 @@ struct PacketInfo {
|
||||
: message_type(type), offset(off), payload_size(size), padding(0) {}
|
||||
};
|
||||
|
||||
enum class APIError : int {
|
||||
enum class APIError : uint16_t {
|
||||
OK = 0,
|
||||
WOULD_BLOCK = 1001,
|
||||
BAD_HANDSHAKE_PACKET_LEN = 1002,
|
||||
@@ -74,7 +74,7 @@ class APIFrameHelper {
|
||||
}
|
||||
virtual ~APIFrameHelper() = default;
|
||||
virtual APIError init() = 0;
|
||||
virtual APIError loop() = 0;
|
||||
virtual APIError loop();
|
||||
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
|
||||
bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
|
||||
std::string getpeername() { return socket_->getpeername(); }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@ class APIServerConnectionBase : public ProtoService {
|
||||
|
||||
template<typename T> bool send_message(const T &msg) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_send_message_(T::message_name(), msg.dump());
|
||||
this->log_send_message_(msg.message_name(), msg.dump());
|
||||
#endif
|
||||
return this->send_message_(msg, T::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,11 @@ void APIServer::setup() {
|
||||
}
|
||||
#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
|
||||
if (this->socket_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not create socket");
|
||||
@@ -106,8 +111,6 @@ void APIServer::setup() {
|
||||
}
|
||||
#endif
|
||||
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) {
|
||||
esp32_camera::global_esp32_camera->add_image_callback(
|
||||
@@ -121,6 +124,16 @@ void APIServer::setup() {
|
||||
#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() {
|
||||
// Accept new clients only if the socket exists and has incoming connections
|
||||
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);
|
||||
if (!sock)
|
||||
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);
|
||||
this->clients_.emplace_back(conn);
|
||||
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
|
||||
if (!this->clients_.empty()) {
|
||||
size_t client_index = 0;
|
||||
while (client_index < this->clients_.size()) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
if (client->remove_) {
|
||||
// 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
|
||||
}
|
||||
// Check network connectivity once for all clients
|
||||
if (!network::is_connected()) {
|
||||
// Network is down - disconnect all clients
|
||||
for (auto &client : this->clients_) {
|
||||
client->on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str());
|
||||
}
|
||||
// Continue to process and clean up the clients below
|
||||
}
|
||||
|
||||
if (this->reboot_timeout_ != 0) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (!this->is_connected()) {
|
||||
if (now - this->last_connected_ > this->reboot_timeout_) {
|
||||
ESP_LOGE(TAG, "No client connected; rebooting");
|
||||
App.reboot();
|
||||
}
|
||||
this->status_set_warning();
|
||||
} else {
|
||||
this->last_connected_ = now;
|
||||
this->status_clear_warning();
|
||||
size_t client_index = 0;
|
||||
while (client_index < this->clients_.size()) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
if (!client->remove_) {
|
||||
// Common case: process active client
|
||||
client->loop();
|
||||
client_index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@ class APIServer : public Component, public Controller {
|
||||
}
|
||||
|
||||
protected:
|
||||
void schedule_reboot_timeout_();
|
||||
// Pointers and pointer-like types first (4 bytes each)
|
||||
std::unique_ptr<socket::Socket> socket_ = nullptr;
|
||||
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
|
||||
uint32_t reboot_timeout_{300000};
|
||||
uint32_t batch_delay_{100};
|
||||
uint32_t last_connected_{0};
|
||||
|
||||
// Vectors and strings (12 bytes each on 32-bit)
|
||||
std::vector<std::unique_ptr<APIConnection>> clients_;
|
||||
|
||||
@@ -335,6 +335,7 @@ class ProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
std::string dump() const;
|
||||
virtual void dump_to(std::string &out) const = 0;
|
||||
virtual const char *message_name() const { return "unknown"; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
|
||||
@@ -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
|
||||
message_sent =
|
||||
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
|
||||
|
||||
// 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
|
||||
void Logger::init_log_buffer(size_t 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
|
||||
|
||||
@@ -189,6 +198,10 @@ void Logger::loop() {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -358,6 +358,26 @@ class Logger : public Component {
|
||||
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);
|
||||
}
|
||||
|
||||
#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)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "esphome/core/component.h"
|
||||
@@ -335,11 +337,16 @@ class Application {
|
||||
* Each component can request a high frequency loop execution by using the HighFrequencyLoopRequester
|
||||
* 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.
|
||||
*/
|
||||
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; }
|
||||
|
||||
@@ -618,6 +625,17 @@ class Application {
|
||||
/// Perform a delay while also monitoring socket file descriptors for readiness
|
||||
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_{};
|
||||
|
||||
// Partitioned vector design for looping components
|
||||
@@ -637,11 +655,6 @@ class Application {
|
||||
// and active_end_ is incremented
|
||||
// - This eliminates branch mispredictions from flag checking in the hot loop
|
||||
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
|
||||
std::vector<Device *> devices_{};
|
||||
@@ -713,26 +726,39 @@ class Application {
|
||||
std::vector<update::UpdateEntity *> updates_{};
|
||||
#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 friendly_name_;
|
||||
const char *comment_{nullptr};
|
||||
const char *compilation_time_{nullptr};
|
||||
bool name_add_mac_suffix_;
|
||||
|
||||
// 4-byte members
|
||||
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};
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
// Socket select management
|
||||
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
|
||||
int max_fd_{-1}; // Highest file descriptor number for select()
|
||||
#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
|
||||
int max_fd_{-1}; // Highest file descriptor number for select()
|
||||
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_
|
||||
|
||||
// Variable-sized members at end
|
||||
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
|
||||
};
|
||||
|
||||
|
||||
@@ -27,20 +27,67 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
public:
|
||||
TemplatableValue() : type_(NONE) {}
|
||||
|
||||
template<typename F, enable_if_t<!is_invocable<F, X...>::value, int> = 0>
|
||||
TemplatableValue(F value) : type_(VALUE), value_(std::move(value)) {}
|
||||
template<typename F, enable_if_t<!is_invocable<F, X...>::value, int> = 0> TemplatableValue(F value) : type_(VALUE) {
|
||||
new (&this->value_) T(std::move(value));
|
||||
}
|
||||
|
||||
template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0>
|
||||
TemplatableValue(F f) : type_(LAMBDA), f_(f) {}
|
||||
template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0> TemplatableValue(F f) : type_(LAMBDA) {
|
||||
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; }
|
||||
|
||||
T value(X... x) {
|
||||
if (this->type_ == LAMBDA) {
|
||||
return this->f_(x...);
|
||||
return (*this->f_)(x...);
|
||||
}
|
||||
// return value also when none
|
||||
return this->value_;
|
||||
return this->type_ == VALUE ? this->value_ : T{};
|
||||
}
|
||||
|
||||
optional<T> optional_value(X... x) {
|
||||
@@ -58,14 +105,16 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
}
|
||||
|
||||
protected:
|
||||
enum {
|
||||
enum : uint8_t {
|
||||
NONE,
|
||||
VALUE,
|
||||
LAMBDA,
|
||||
} type_;
|
||||
|
||||
T value_{};
|
||||
std::function<T(X...)> f_{};
|
||||
union {
|
||||
T value_;
|
||||
std::function<T(X...)> *f_;
|
||||
};
|
||||
};
|
||||
|
||||
/** Base class for all automation conditions.
|
||||
|
||||
@@ -132,6 +132,8 @@
|
||||
|
||||
// ESP32-specific feature flags
|
||||
#ifdef USE_ESP32
|
||||
#define USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
#define USE_BLUETOOTH_PROXY
|
||||
#define USE_CAPTIVE_PORTAL
|
||||
#define USE_ESP32_BLE
|
||||
|
||||
@@ -886,7 +886,7 @@ def build_message_type(
|
||||
public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP")
|
||||
snake_name = camel_to_snake(desc.name)
|
||||
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")
|
||||
|
||||
@@ -1356,7 +1356,7 @@ def main() -> None:
|
||||
hpp += " template<typename T>\n"
|
||||
hpp += " bool send_message(const T &msg) {\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 += " return this->send_message_(msg, T::MESSAGE_TYPE);\n"
|
||||
hpp += " }\n\n"
|
||||
|
||||
7
tests/integration/fixtures/api_reboot_timeout.yaml
Normal file
7
tests/integration/fixtures/api_reboot_timeout.yaml
Normal 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
|
||||
35
tests/integration/test_api_reboot_timeout.py
Normal file
35
tests/integration/test_api_reboot_timeout.py
Normal 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
|
||||
Reference in New Issue
Block a user