From 5697d549a82b83adba19efdb241604dcf509584e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Jun 2025 23:44:08 +0200 Subject: [PATCH 01/34] Use scheduler for api reboot --- esphome/components/api/api_server.cpp | 43 +++++++++++++++++---------- esphome/components/api/api_server.h | 2 +- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 740e4259b1..ae732fc234 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -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 client connected; rebooting"); + App.reboot(); + } + }); +} + void APIServer::loop() { // Accept new clients only if the socket exists and has incoming connections if (this->socket_ && this->socket_->ready()) { @@ -135,6 +148,12 @@ void APIServer::loop() { 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"); + } } } @@ -154,6 +173,12 @@ void APIServer::loop() { 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 } else { // Process active client @@ -163,19 +188,7 @@ void APIServer::loop() { } } - 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(); - } - } + // Reboot timeout is now handled by connection/disconnection events } void APIServer::dump_config() { diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 33412d8a68..27341dc596 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -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_ = nullptr; Trigger *client_connected_trigger_ = new Trigger(); @@ -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> clients_; From 8ec998ff30c57fc27d4e2fb0bfde630d05b6b1d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 10:52:34 +0200 Subject: [PATCH 02/34] more api loop reductions --- esphome/components/api/api_connection.cpp | 2 +- esphome/components/api/api_server.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ef791d462c..8f814f9f42 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -158,7 +158,7 @@ void APIConnection::loop() { 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; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index ae732fc234..8f7add646c 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -136,7 +136,7 @@ void APIServer::schedule_reboot_timeout_() { void APIServer::loop() { // Accept new clients only if the socket exists and has incoming connections - if (this->socket_ && this->socket_->ready()) { + if (this->socket_->ready()) { while (true) { struct sockaddr_storage source_addr; socklen_t addr_len = sizeof(source_addr); From d6725fc1caf873681c4d213abb5e2ad0b09d7dc7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 10:54:50 +0200 Subject: [PATCH 03/34] more api loop reductions --- esphome/components/api/api_connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8f814f9f42..fc6c4d4cf7 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -152,7 +152,7 @@ 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_(); } From e8c250a03c5ed2834f16861bbd63a084cff819fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 10:59:00 +0200 Subject: [PATCH 04/34] more api loop reductions --- esphome/components/api/api_connection.cpp | 7 ------- esphome/components/api/api_server.cpp | 13 +++++++++++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index fc6c4d4cf7..ac729e7652 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -93,13 +93,6 @@ 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(); diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 8f7add646c..23c8ef30cd 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -159,6 +159,9 @@ void APIServer::loop() { // Process clients and remove disconnected ones in a single pass if (!this->clients_.empty()) { + // Check network connectivity once for all clients + bool network_connected = network::is_connected(); + size_t client_index = 0; while (client_index < this->clients_.size()) { auto &client = this->clients_[client_index]; @@ -181,8 +184,14 @@ void APIServer::loop() { // Don't increment client_index since we need to process the swapped element } else { - // Process active client - client->loop(); + // Process active client only if network is connected + if (network_connected) { + client->loop(); + } else { + // Force disconnect when network is unavailable + client->on_fatal_error(); + ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", client->get_client_combined_info().c_str()); + } client_index++; // Move to next client } } From e767f30886f9d2b1f72e10cf5b6fc3cc89090700 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 10:59:49 +0200 Subject: [PATCH 05/34] more api loop reductions --- esphome/components/api/api_frame_helper.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 7e90153091..a20c0c10c5 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -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, From a3a3bdc7ebb75c6406174ecefefe89332bffc323 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:02:27 +0200 Subject: [PATCH 06/34] more api loop reductions --- esphome/components/api/api_frame_helper.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index ff660f439e..e0cbe5513a 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -831,7 +831,6 @@ APIError APIPlaintextFrameHelper::init() { state_ = State::DATA; return APIError::OK; } -/// Not used for plaintext APIError APIPlaintextFrameHelper::loop() { if (state_ != State::DATA) { return APIError::BAD_STATE; From 0bc59b97de8a481b17f541099e655cb93f9830f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:06:51 +0200 Subject: [PATCH 07/34] more api loop reductions --- esphome/components/api/api_frame_helper.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index e0cbe5513a..d859aafd70 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -339,17 +339,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) { From 20405c84ac680e31d1d199b023dec5388dee8199 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:10:07 +0200 Subject: [PATCH 08/34] preen --- esphome/components/api/api_server.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 23c8ef30cd..97c8ffcc75 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -196,8 +196,6 @@ void APIServer::loop() { } } } - - // Reboot timeout is now handled by connection/disconnection events } void APIServer::dump_config() { From 2c315595f0cb3d05a0518e821d187d7e397d73c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:12:04 +0200 Subject: [PATCH 09/34] preen --- esphome/components/api/api_connection.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ac729e7652..c0ba925e5f 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -154,8 +154,8 @@ void APIConnection::loop() { 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; + static constexpr uint8_t max_ping_retries = 60; + static constexpr 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) { From 147f6012b2990838dc1e0c2c5dde37f76126a6d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:16:34 +0200 Subject: [PATCH 10/34] preen --- esphome/components/api/api_connection.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index c0ba925e5f..2a8bd7e16d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -158,7 +158,8 @@ void APIConnection::loop() { static constexpr 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) { + static constexpr uint32_t keepalive_disconnect_timeout = (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()); } @@ -168,15 +169,13 @@ void APIConnection::loop() { if (!this->sent_ping_) { 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) { 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_); } } } From 13b23f840b94b6beeee7fa0b797f895913b349a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:17:17 +0200 Subject: [PATCH 11/34] preen --- esphome/components/api/api_connection.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2a8bd7e16d..88bf91ea94 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -90,9 +90,6 @@ APIConnection::~APIConnection() { } void APIConnection::loop() { - if (this->remove_) - return; - if (this->next_close_) { // requested a disconnect this->helper_->close(); From 047a3e0e8c585e7926f56b9625b1b45e162d52e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:18:47 +0200 Subject: [PATCH 12/34] preen --- esphome/components/api/api_connection.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 88bf91ea94..e69c2f7cd3 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -33,6 +33,8 @@ 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 const char *const TAG = "api.connection"; static const int ESP32_CAMERA_STOP_STREAM = 5000; From c5ef7ebd27f8e945993e6f8c5b1a030e8c8a1e19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:19:07 +0200 Subject: [PATCH 13/34] preen --- esphome/components/api/api_connection.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index e69c2f7cd3..814fcafb53 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -153,8 +153,6 @@ void APIConnection::loop() { else if (!this->initial_state_iterator_.completed()) this->initial_state_iterator_.advance(); - static constexpr uint8_t max_ping_retries = 60; - static constexpr uint16_t ping_retry_interval = 1000; if (this->sent_ping_) { // Disconnect if not responded within 2.5*keepalive static constexpr uint32_t keepalive_disconnect_timeout = (KEEPALIVE_TIMEOUT_MS * 5) / 2; @@ -166,9 +164,9 @@ 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_++; - if (this->ping_retries_ >= max_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) { From 8d5d18064df4589569321ee91388e7158f59a16a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:19:56 +0200 Subject: [PATCH 14/34] preen --- esphome/components/api/api_connection.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 814fcafb53..057376579e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -35,6 +35,7 @@ namespace api { 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 int ESP32_CAMERA_STOP_STREAM = 5000; From 02e61ef5d3b748e5cc5fc9b2923807a3edf46037 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:20:06 +0200 Subject: [PATCH 15/34] preen --- esphome/components/api/api_connection.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 057376579e..35e78e0ef5 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -156,8 +156,7 @@ void APIConnection::loop() { if (this->sent_ping_) { // Disconnect if not responded within 2.5*keepalive - static constexpr uint32_t keepalive_disconnect_timeout = (KEEPALIVE_TIMEOUT_MS * 5) / 2; - if (now - this->last_traffic_ > keepalive_disconnect_timeout) { + 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()); } From 19cbc8c33bfeac45923c7d92f6e09bb3e068cfc5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:21:37 +0200 Subject: [PATCH 16/34] preen --- esphome/components/api/api_connection.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 35e78e0ef5..f4eca0cad8 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -200,9 +200,9 @@ void APIConnection::loop() { 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 From b0c02341ff646bbd06d0f9810030f3c40e5787b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:22:08 +0200 Subject: [PATCH 17/34] preen --- esphome/components/api/api_connection.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index f4eca0cad8..459f450ea2 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -207,11 +207,9 @@ void APIConnection::loop() { } #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(subs.size())) { auto &it = subs[state_subs_at_]; SubscribeHomeAssistantStateResponse resp; resp.entity_id = it.entity_id; @@ -220,6 +218,8 @@ void APIConnection::loop() { if (this->send_message(resp)) { state_subs_at_++; } + } else { + state_subs_at_ = -1; } } } From 5898d34b0a8368a2215a79d575ceb88d886a9df5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:22:45 +0200 Subject: [PATCH 18/34] preen --- esphome/components/api/api_connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 459f450ea2..585f6fa200 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -196,7 +196,7 @@ 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); From ddbda5032bd5a5b40f90b35090bf6d4ca3f2820b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:25:24 +0200 Subject: [PATCH 19/34] preen --- esphome/components/api/api_frame_helper.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index d859aafd70..772b7e802b 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -593,11 +593,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) { state_ = State::FAILED; @@ -608,7 +603,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { buffer->container = std::move(frame.msg); buffer->data_offset = 4; buffer->data_len = data_len; - buffer->type = type; + buffer->type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; return APIError::OK; } APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) { From b76e34fb7bda577aa5f9e5c9f22ba0bfb90f5e8e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:25:52 +0200 Subject: [PATCH 20/34] preen --- esphome/components/api/api_frame_helper.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 772b7e802b..53985a5c0e 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -593,6 +593,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::BAD_DATA_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) { state_ = State::FAILED; @@ -603,7 +604,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { buffer->container = std::move(frame.msg); buffer->data_offset = 4; buffer->data_len = data_len; - buffer->type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; + buffer->type = type; return APIError::OK; } APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) { From f67490b69b1a2d1a0b51a816261c03e43c0557e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:29:04 +0200 Subject: [PATCH 21/34] preen --- esphome/components/api/api_frame_helper.cpp | 29 +++++++++++---------- esphome/components/api/api_frame_helper.h | 2 +- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 53985a5c0e..af6dd0220d 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -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 @@ -829,13 +835,8 @@ 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 diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index a20c0c10c5..1e157278a1 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -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(); } From edeafd5a537f944ae8d4b8e62dc562c731ecc87c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:31:38 +0200 Subject: [PATCH 22/34] preen --- esphome/components/api/api_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 97c8ffcc75..046053872a 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -136,7 +136,7 @@ void APIServer::schedule_reboot_timeout_() { void APIServer::loop() { // Accept new clients only if the socket exists and has incoming connections - if (this->socket_->ready()) { + if (this->socket_ && this->socket_->ready()) { while (true) { struct sockaddr_storage source_addr; socklen_t addr_len = sizeof(source_addr); From 56a02409c8f12ef122c64a36603be2cdae82a641 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:34:11 +0200 Subject: [PATCH 23/34] preen --- esphome/components/api/api_server.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 046053872a..2bdcb3c45c 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -160,7 +160,14 @@ void APIServer::loop() { // Process clients and remove disconnected ones in a single pass if (!this->clients_.empty()) { // Check network connectivity once for all clients - bool network_connected = network::is_connected(); + if (!network::is_connected()) { + // Network is down - disconnect all clients + for (auto &client : this->clients_) { + client->on_fatal_error(); + ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", client->get_client_combined_info().c_str()); + } + return; // All clients will be marked for removal, cleanup will happen next loop + } size_t client_index = 0; while (client_index < this->clients_.size()) { @@ -184,14 +191,8 @@ void APIServer::loop() { // Don't increment client_index since we need to process the swapped element } else { - // Process active client only if network is connected - if (network_connected) { - client->loop(); - } else { - // Force disconnect when network is unavailable - client->on_fatal_error(); - ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", client->get_client_combined_info().c_str()); - } + // Network is connected, process the client + client->loop(); client_index++; // Move to next client } } From 6a22ea1c7d4f94e0683967f197d4da30fbdff33d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:35:41 +0200 Subject: [PATCH 24/34] preen --- esphome/components/api/api_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 2bdcb3c45c..ab1568c80b 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -166,7 +166,7 @@ void APIServer::loop() { client->on_fatal_error(); ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", client->get_client_combined_info().c_str()); } - return; // All clients will be marked for removal, cleanup will happen next loop + // Continue to process and clean up the clients below } size_t client_index = 0; From 93245a24b57a9c0e399487cc59bf097a38c8a72a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:36:54 +0200 Subject: [PATCH 25/34] preen --- esphome/components/api/api_server.cpp | 66 ++++++++++++++------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index ab1568c80b..156cf7cc6e 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -158,43 +158,45 @@ void APIServer::loop() { } // Process clients and remove disconnected ones in a single pass - if (!this->clients_.empty()) { - // 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 unavailable; disconnecting", client->get_client_combined_info().c_str()); - } - // Continue to process and clean up the clients below + if (this->clients_.empty()) { + return; + } + + // 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 unavailable; disconnecting", client->get_client_combined_info().c_str()); } + // Continue to process and clean up the clients below + } - size_t client_index = 0; - while (client_index < this->clients_.size()) { - auto &client = this->clients_[client_index]; + 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()); + 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(); - - // 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 - } else { - // Network is connected, process the client - client->loop(); - client_index++; // Move to next client + // 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 + } else { + // Network is connected, process the client + client->loop(); + client_index++; // Move to next client } } } From 76a59759b21ecca5eb9f173ed0ccecd8cc558535 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:37:27 +0200 Subject: [PATCH 26/34] preen --- esphome/components/api/api_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 156cf7cc6e..13c3ba0ec4 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -157,11 +157,11 @@ void APIServer::loop() { } } - // Process clients and remove disconnected ones in a single pass if (this->clients_.empty()) { return; } + // Process clients and remove disconnected ones in a single pass // Check network connectivity once for all clients if (!network::is_connected()) { // Network is down - disconnect all clients From 686cc58d6c3f945080365ad98f3c3fe16cbff2c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:37:59 +0200 Subject: [PATCH 27/34] preen --- esphome/components/api/api_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 13c3ba0ec4..d2b9a0cfb9 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -143,7 +143,7 @@ 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); From 97b26fbefed9431ab852211be0486eb249ace0b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:38:10 +0200 Subject: [PATCH 28/34] preen --- esphome/components/api/api_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index d2b9a0cfb9..a79fc99a72 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -167,7 +167,7 @@ void APIServer::loop() { // Network is down - disconnect all clients for (auto &client : this->clients_) { client->on_fatal_error(); - ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", client->get_client_combined_info().c_str()); + ESP_LOGW(TAG, "%s: Network down; disconnecting", client->get_client_combined_info().c_str()); } // Continue to process and clean up the clients below } From 5dc54782e5a5bfe80f307387bc9ac683e451e9f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:38:30 +0200 Subject: [PATCH 29/34] preen --- esphome/components/api/api_server.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index a79fc99a72..ae278a424e 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -167,7 +167,7 @@ void APIServer::loop() { // Network is down - disconnect all clients for (auto &client : this->clients_) { client->on_fatal_error(); - ESP_LOGW(TAG, "%s: Network down; disconnecting", client->get_client_combined_info().c_str()); + ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str()); } // Continue to process and clean up the clients below } @@ -179,7 +179,7 @@ void APIServer::loop() { 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()); + 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) { From 170869b7dbcb166435a2048598c571515346284a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:39:25 +0200 Subject: [PATCH 30/34] preen --- esphome/components/api/api_server.cpp | 40 +++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index ae278a424e..ad1eeda8ea 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -176,28 +176,28 @@ void APIServer::loop() { 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, "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 - } else { - // Network is connected, process the client + if (!client->remove_) { + // Common case: process active client client->loop(); - client_index++; // Move to next client + 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 } } From 0773819778b6186f8b1e37591024154460320b9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:45:58 +0200 Subject: [PATCH 31/34] cleanup --- .../fixtures/api_reboot_timeout.yaml | 7 ++++ tests/integration/test_api_reboot_timeout.py | 38 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 tests/integration/fixtures/api_reboot_timeout.yaml create mode 100644 tests/integration/test_api_reboot_timeout.py diff --git a/tests/integration/fixtures/api_reboot_timeout.yaml b/tests/integration/fixtures/api_reboot_timeout.yaml new file mode 100644 index 0000000000..114dd2fece --- /dev/null +++ b/tests/integration/fixtures/api_reboot_timeout.yaml @@ -0,0 +1,7 @@ +esphome: + name: api-reboot-test +host: +api: + reboot_timeout: 1s # Very short timeout for fast testing +logger: + level: DEBUG diff --git a/tests/integration/test_api_reboot_timeout.py b/tests/integration/test_api_reboot_timeout.py new file mode 100644 index 0000000000..9836b42025 --- /dev/null +++ b/tests/integration/test_api_reboot_timeout.py @@ -0,0 +1,38 @@ +"""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.""" + reboot_detected = False + reboot_pattern = re.compile(r"No client connected; rebooting") + + def check_output(line: str) -> None: + """Check output for reboot message.""" + nonlocal reboot_detected + if reboot_pattern.search(line): + reboot_detected = True + + # Run the device without connecting any API client + async with run_compiled(yaml_config, line_callback=check_output): + # Wait for up to 3 seconds for the reboot to occur + # (1s timeout + some margin for processing) + start_time = asyncio.get_event_loop().time() + while not reboot_detected: + await asyncio.sleep(0.1) + elapsed = asyncio.get_event_loop().time() - start_time + if elapsed > 3.0: + pytest.fail("Device did not reboot within expected timeout") + + # Verify that reboot was detected + assert reboot_detected, "Reboot message was not detected in output" From 0eea1c0e400a41cff69ee735ef6347cc769f0632 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:56:09 +0200 Subject: [PATCH 32/34] preen --- tests/integration/test_api_reboot_timeout.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_api_reboot_timeout.py b/tests/integration/test_api_reboot_timeout.py index 9836b42025..51f4ab160b 100644 --- a/tests/integration/test_api_reboot_timeout.py +++ b/tests/integration/test_api_reboot_timeout.py @@ -27,10 +27,11 @@ async def test_api_reboot_timeout( async with run_compiled(yaml_config, line_callback=check_output): # Wait for up to 3 seconds for the reboot to occur # (1s timeout + some margin for processing) - start_time = asyncio.get_event_loop().time() + loop = asyncio.get_running_loop() + start_time = loop.time() while not reboot_detected: await asyncio.sleep(0.1) - elapsed = asyncio.get_event_loop().time() - start_time + elapsed = loop.time() - start_time if elapsed > 3.0: pytest.fail("Device did not reboot within expected timeout") From e3aaf3219dad32e565a5b9c165c7bef5473670bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:58:16 +0200 Subject: [PATCH 33/34] speed up test --- .../fixtures/api_reboot_timeout.yaml | 2 +- tests/integration/test_api_reboot_timeout.py | 26 ++++++++----------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/tests/integration/fixtures/api_reboot_timeout.yaml b/tests/integration/fixtures/api_reboot_timeout.yaml index 114dd2fece..881bb5b2fc 100644 --- a/tests/integration/fixtures/api_reboot_timeout.yaml +++ b/tests/integration/fixtures/api_reboot_timeout.yaml @@ -2,6 +2,6 @@ esphome: name: api-reboot-test host: api: - reboot_timeout: 1s # Very short timeout for fast testing + reboot_timeout: 0.5s # Very short timeout for fast testing logger: level: DEBUG diff --git a/tests/integration/test_api_reboot_timeout.py b/tests/integration/test_api_reboot_timeout.py index 51f4ab160b..7cace506b2 100644 --- a/tests/integration/test_api_reboot_timeout.py +++ b/tests/integration/test_api_reboot_timeout.py @@ -14,26 +14,22 @@ async def test_api_reboot_timeout( run_compiled: RunCompiledFunction, ) -> None: """Test that the device reboots when no API clients connect within the timeout.""" - reboot_detected = False + loop = asyncio.get_running_loop() + reboot_future = loop.create_future() reboot_pattern = re.compile(r"No client connected; rebooting") def check_output(line: str) -> None: """Check output for reboot message.""" - nonlocal reboot_detected - if reboot_pattern.search(line): - reboot_detected = True + 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 up to 3 seconds for the reboot to occur - # (1s timeout + some margin for processing) - loop = asyncio.get_running_loop() - start_time = loop.time() - while not reboot_detected: - await asyncio.sleep(0.1) - elapsed = loop.time() - start_time - if elapsed > 3.0: - pytest.fail("Device did not reboot within expected timeout") + # 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") - # Verify that reboot was detected - assert reboot_detected, "Reboot message was not detected in output" + # Test passes if we get here - reboot was detected From 971e954a545dfb2c54e18e91bb9be4e0eb5f9a01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 11:59:07 +0200 Subject: [PATCH 34/34] follow logging guidelines --- esphome/components/api/api_server.cpp | 2 +- tests/integration/test_api_reboot_timeout.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index ad1eeda8ea..583837af82 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -128,7 +128,7 @@ 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 client connected; rebooting"); + ESP_LOGE(TAG, "No clients; rebooting"); App.reboot(); } }); diff --git a/tests/integration/test_api_reboot_timeout.py b/tests/integration/test_api_reboot_timeout.py index 7cace506b2..dd9f5fbd1e 100644 --- a/tests/integration/test_api_reboot_timeout.py +++ b/tests/integration/test_api_reboot_timeout.py @@ -16,7 +16,7 @@ async def test_api_reboot_timeout( """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 client connected; rebooting") + reboot_pattern = re.compile(r"No clients; rebooting") def check_output(line: str) -> None: """Check output for reboot message."""