mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'integration' of https://github.com/esphome/esphome into integration
This commit is contained in:
		| @@ -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) { | ||||||
| @@ -90,16 +95,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(); | ||||||
| @@ -152,20 +147,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(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   static uint8_t max_ping_retries = 60; |  | ||||||
|   static uint16_t ping_retry_interval = 1000; |  | ||||||
|   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()); | ||||||
|     } |     } | ||||||
| @@ -173,17 +167,15 @@ void APIConnection::loop() { | |||||||
|     ESP_LOGVV(TAG, "Sending keepalive PING"); |     ESP_LOGVV(TAG, "Sending keepalive PING"); | ||||||
|     this->sent_ping_ = this->send_message(PingRequest()); |     this->sent_ping_ = this->send_message(PingRequest()); | ||||||
|     if (!this->sent_ping_) { |     if (!this->sent_ping_) { | ||||||
|       this->next_ping_retry_ = now + ping_retry_interval; |       this->next_ping_retry_ = now + PING_RETRY_INTERVAL; | ||||||
|       this->ping_retries_++; |       this->ping_retries_++; | ||||||
|       std::string warn_str = str_sprintf("%s: Sending keepalive failed %u time(s);", |       if (this->ping_retries_ >= MAX_PING_RETRIES) { | ||||||
|                                          this->get_client_combined_info().c_str(), this->ping_retries_); |  | ||||||
|       if (this->ping_retries_ >= max_ping_retries) { |  | ||||||
|         on_fatal_error(); |         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) { |       } 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 { |       } 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; |     // 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; | ||||||
| @@ -231,6 +221,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; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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(); } | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |   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 | ||||||
|  |   } | ||||||
|  |  | ||||||
|   size_t client_index = 0; |   size_t client_index = 0; | ||||||
|   while (client_index < this->clients_.size()) { |   while (client_index < this->clients_.size()) { | ||||||
|     auto &client = this->clients_[client_index]; |     auto &client = this->clients_[client_index]; | ||||||
|  |  | ||||||
|       if (client->remove_) { |     if (!client->remove_) { | ||||||
|         // Handle disconnection |       // 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_); |     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) |     // Swap with the last element and pop (avoids expensive vector shifts) | ||||||
|     if (client_index < this->clients_.size() - 1) { |     if (client_index < this->clients_.size() - 1) { | ||||||
|       std::swap(this->clients_[client_index], this->clients_.back()); |       std::swap(this->clients_[client_index], this->clients_.back()); | ||||||
|     } |     } | ||||||
|     this->clients_.pop_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 |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (this->reboot_timeout_ != 0) { |     // Schedule reboot when last client disconnects | ||||||
|     const uint32_t now = App.get_loop_component_start_time(); |     if (this->clients_.empty() && this->reboot_timeout_ != 0) { | ||||||
|     if (!this->is_connected()) { |       this->schedule_reboot_timeout_(); | ||||||
|       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(); |  | ||||||
|     } |     } | ||||||
|  |     // Don't increment client_index since we need to process the swapped element | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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_; | ||||||
|   | |||||||
| @@ -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 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -23,16 +23,22 @@ std::string state_class_to_string(StateClass state_class) { | |||||||
| Sensor::Sensor() : state(NAN), raw_state(NAN) {} | Sensor::Sensor() : state(NAN), raw_state(NAN) {} | ||||||
|  |  | ||||||
| int8_t Sensor::get_accuracy_decimals() { | int8_t Sensor::get_accuracy_decimals() { | ||||||
|   if (this->accuracy_decimals_.has_value()) |   if (this->sensor_flags_.has_accuracy_override) | ||||||
|     return *this->accuracy_decimals_; |     return this->accuracy_decimals_; | ||||||
|   return 0; |   return 0; | ||||||
| } | } | ||||||
| void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { this->accuracy_decimals_ = accuracy_decimals; } | void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { | ||||||
|  |   this->accuracy_decimals_ = accuracy_decimals; | ||||||
|  |   this->sensor_flags_.has_accuracy_override = true; | ||||||
|  | } | ||||||
|  |  | ||||||
| void Sensor::set_state_class(StateClass state_class) { this->state_class_ = state_class; } | void Sensor::set_state_class(StateClass state_class) { | ||||||
|  |   this->state_class_ = state_class; | ||||||
|  |   this->sensor_flags_.has_state_class_override = true; | ||||||
|  | } | ||||||
| StateClass Sensor::get_state_class() { | StateClass Sensor::get_state_class() { | ||||||
|   if (this->state_class_.has_value()) |   if (this->sensor_flags_.has_state_class_override) | ||||||
|     return *this->state_class_; |     return this->state_class_; | ||||||
|   return StateClass::STATE_CLASS_NONE; |   return StateClass::STATE_CLASS_NONE; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -80,9 +80,9 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa | |||||||
|    * state changes to the database when they are published, even if the state is the |    * state changes to the database when they are published, even if the state is the | ||||||
|    * same as before. |    * same as before. | ||||||
|    */ |    */ | ||||||
|   bool get_force_update() const { return force_update_; } |   bool get_force_update() const { return sensor_flags_.force_update; } | ||||||
|   /// Set force update mode. |   /// Set force update mode. | ||||||
|   void set_force_update(bool force_update) { force_update_ = force_update; } |   void set_force_update(bool force_update) { sensor_flags_.force_update = force_update; } | ||||||
|  |  | ||||||
|   /// Add a filter to the filter chain. Will be appended to the back. |   /// Add a filter to the filter chain. Will be appended to the back. | ||||||
|   void add_filter(Filter *filter); |   void add_filter(Filter *filter); | ||||||
| @@ -155,9 +155,17 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa | |||||||
|  |  | ||||||
|   Filter *filter_list_{nullptr};  ///< Store all active filters. |   Filter *filter_list_{nullptr};  ///< Store all active filters. | ||||||
|  |  | ||||||
|   optional<int8_t> accuracy_decimals_;                  ///< Accuracy in decimals override |   // Group small members together to avoid padding | ||||||
|   optional<StateClass> state_class_{STATE_CLASS_NONE};  ///< State class override |   int8_t accuracy_decimals_{-1};              ///< Accuracy in decimals (-1 = not set) | ||||||
|   bool force_update_{false};                            ///< Force update mode |   StateClass state_class_{STATE_CLASS_NONE};  ///< State class (STATE_CLASS_NONE = not set) | ||||||
|  |  | ||||||
|  |   // Bit-packed flags for sensor-specific settings | ||||||
|  |   struct SensorFlags { | ||||||
|  |     uint8_t has_accuracy_override : 1; | ||||||
|  |     uint8_t has_state_class_override : 1; | ||||||
|  |     uint8_t force_update : 1; | ||||||
|  |     uint8_t reserved : 5;  // Reserved for future use | ||||||
|  |   } sensor_flags_{}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace sensor | }  // namespace sensor | ||||||
|   | |||||||
| @@ -136,6 +136,10 @@ void Application::loop() { | |||||||
|   this->in_loop_ = false; |   this->in_loop_ = false; | ||||||
|   this->app_state_ = new_app_state; |   this->app_state_ = new_app_state; | ||||||
|  |  | ||||||
|  |   // Process any pending runtime stats printing after all components have run | ||||||
|  |   // This ensures stats printing doesn't affect component timing measurements | ||||||
|  |   runtime_stats.process_pending_stats(last_op_end_time); | ||||||
|  |  | ||||||
|   // Use the last component's end time instead of calling millis() again |   // Use the last component's end time instead of calling millis() again | ||||||
|   auto elapsed = last_op_end_time - this->last_loop_; |   auto elapsed = last_op_end_time - this->last_loop_; | ||||||
|   if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { |   if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -28,11 +28,7 @@ void RuntimeStatsCollector::record_component_time(Component *component, uint32_t | |||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (current_time >= this->next_log_time_) { |   // Don't print stats here anymore - let process_pending_stats handle it | ||||||
|     this->log_stats_(); |  | ||||||
|     this->reset_stats_(); |  | ||||||
|     this->next_log_time_ = current_time + this->log_interval_; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| void RuntimeStatsCollector::log_stats_() { | void RuntimeStatsCollector::log_stats_() { | ||||||
| @@ -82,4 +78,15 @@ void RuntimeStatsCollector::log_stats_() { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) { | ||||||
|  |   if (!this->enabled_ || this->next_log_time_ == 0) | ||||||
|  |     return; | ||||||
|  |  | ||||||
|  |   if (current_time >= this->next_log_time_) { | ||||||
|  |     this->log_stats_(); | ||||||
|  |     this->reset_stats_(); | ||||||
|  |     this->next_log_time_ = current_time + this->log_interval_; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
| @@ -95,6 +95,9 @@ class RuntimeStatsCollector { | |||||||
|  |  | ||||||
|   void record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time); |   void record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time); | ||||||
|  |  | ||||||
|  |   // Process any pending stats printing (should be called after component loop) | ||||||
|  |   void process_pending_stats(uint32_t current_time); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   void log_stats_(); |   void log_stats_(); | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										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 | ||||||
| @@ -8,5 +8,8 @@ sensor: | |||||||
|     name: Test Sensor |     name: Test Sensor | ||||||
|     id: test_sensor |     id: test_sensor | ||||||
|     unit_of_measurement: °C |     unit_of_measurement: °C | ||||||
|  |     accuracy_decimals: 2 | ||||||
|  |     state_class: measurement | ||||||
|  |     force_update: true | ||||||
|     lambda: return 42.0; |     lambda: return 42.0; | ||||||
|     update_interval: 0.1s |     update_interval: 0.1s | ||||||
|   | |||||||
							
								
								
									
										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 | ||||||
| @@ -4,6 +4,7 @@ from __future__ import annotations | |||||||
|  |  | ||||||
| import asyncio | import asyncio | ||||||
|  |  | ||||||
|  | import aioesphomeapi | ||||||
| from aioesphomeapi import EntityState | from aioesphomeapi import EntityState | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| @@ -47,3 +48,23 @@ async def test_host_mode_with_sensor( | |||||||
|         # Verify the sensor state |         # Verify the sensor state | ||||||
|         assert test_sensor_state.state == 42.0 |         assert test_sensor_state.state == 42.0 | ||||||
|         assert len(states) > 0, "No states received" |         assert len(states) > 0, "No states received" | ||||||
|  |  | ||||||
|  |         # Verify the optimized fields are working correctly | ||||||
|  |         # Get entity info to check accuracy_decimals, state_class, etc. | ||||||
|  |         entities, _ = await client.list_entities_services() | ||||||
|  |         sensor_info: aioesphomeapi.SensorInfo | None = None | ||||||
|  |         for entity in entities: | ||||||
|  |             if isinstance(entity, aioesphomeapi.SensorInfo): | ||||||
|  |                 sensor_info = entity | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         assert sensor_info is not None, "Sensor entity info not found" | ||||||
|  |         assert sensor_info.accuracy_decimals == 2, ( | ||||||
|  |             f"Expected accuracy_decimals=2, got {sensor_info.accuracy_decimals}" | ||||||
|  |         ) | ||||||
|  |         assert sensor_info.state_class == 1, ( | ||||||
|  |             f"Expected state_class=1 (measurement), got {sensor_info.state_class}" | ||||||
|  |         ) | ||||||
|  |         assert sensor_info.force_update is True, ( | ||||||
|  |             f"Expected force_update=True, got {sensor_info.force_update}" | ||||||
|  |         ) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user