mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/dev' into batch_ping_fallback
This commit is contained in:
		
							
								
								
									
										23
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,28 +1,11 @@ | ||||
| --- | ||||
| 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 | ||||
|   | ||||
| @@ -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) { | ||||
| @@ -86,16 +91,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(); | ||||
| @@ -148,18 +143,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(); | ||||
|   } | ||||
|  | ||||
|   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()); | ||||
|     } | ||||
| @@ -194,22 +190,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) { | ||||
|       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; | ||||
| @@ -218,6 +212,8 @@ void APIConnection::loop() { | ||||
|       if (this->send_message(resp)) { | ||||
|         state_subs_at_++; | ||||
|       } | ||||
|     } else { | ||||
|       state_subs_at_ = -1; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -271,6 +267,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; | ||||
|  | ||||
| @@ -1427,7 +1428,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); | ||||
| @@ -1787,7 +1788,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)) { | ||||
| @@ -1837,7 +1839,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 | ||||
| @@ -1900,22 +1902,24 @@ 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); | ||||
|         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; | ||||
|     } | ||||
|   } else { | ||||
|     // Function pointer case | ||||
|     return data_.ptr(entity, conn, remaining_size, is_single); | ||||
|   } | ||||
| } | ||||
|  | ||||
| uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, | ||||
|   | ||||
| @@ -484,55 +484,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_; | ||||
|         } | ||||
| @@ -543,30 +545,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()) { | ||||
|   // 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; | ||||
|   while (client_index < this->clients_.size()) { | ||||
|     auto &client = this->clients_[client_index]; | ||||
|  | ||||
|       if (client->remove_) { | ||||
|         // Handle disconnection | ||||
|     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, "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) { | ||||
|       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 | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   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(); | ||||
|     // 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: | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import logging | ||||
| import os | ||||
| from pathlib import Path | ||||
|  | ||||
| from esphome import git | ||||
| from esphome import yaml_util | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
| @@ -23,7 +23,6 @@ from esphome.const import ( | ||||
|     CONF_REFRESH, | ||||
|     CONF_SOURCE, | ||||
|     CONF_TYPE, | ||||
|     CONF_URL, | ||||
|     CONF_VARIANT, | ||||
|     CONF_VERSION, | ||||
|     KEY_CORE, | ||||
| @@ -32,14 +31,13 @@ from esphome.const import ( | ||||
|     KEY_TARGET_FRAMEWORK, | ||||
|     KEY_TARGET_PLATFORM, | ||||
|     PLATFORM_ESP32, | ||||
|     TYPE_GIT, | ||||
|     TYPE_LOCAL, | ||||
|     __version__, | ||||
| ) | ||||
| from esphome.core import CORE, HexInt, TimePeriod | ||||
| from esphome.cpp_generator import RawExpression | ||||
| import esphome.final_validate as fv | ||||
| from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
| from .boards import BOARDS | ||||
| from .const import (  # noqa | ||||
| @@ -49,10 +47,8 @@ from .const import (  # noqa | ||||
|     KEY_EXTRA_BUILD_FILES, | ||||
|     KEY_PATH, | ||||
|     KEY_REF, | ||||
|     KEY_REFRESH, | ||||
|     KEY_REPO, | ||||
|     KEY_SDKCONFIG_OPTIONS, | ||||
|     KEY_SUBMODULES, | ||||
|     KEY_VARIANT, | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32C2, | ||||
| @@ -235,7 +231,7 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): | ||||
| def add_idf_component( | ||||
|     *, | ||||
|     name: str, | ||||
|     repo: str, | ||||
|     repo: str = None, | ||||
|     ref: str = None, | ||||
|     path: str = None, | ||||
|     refresh: TimePeriod = None, | ||||
| @@ -245,30 +241,27 @@ def add_idf_component( | ||||
|     """Add an esp-idf component to the project.""" | ||||
|     if not CORE.using_esp_idf: | ||||
|         raise ValueError("Not an esp-idf project") | ||||
|     if components is None: | ||||
|         components = [] | ||||
|     if name not in CORE.data[KEY_ESP32][KEY_COMPONENTS]: | ||||
|     if not repo and not ref and not path: | ||||
|         raise ValueError("Requires at least one of repo, ref or path") | ||||
|     if refresh or submodules or components: | ||||
|         _LOGGER.warning( | ||||
|             "The refresh, components and submodules parameters in add_idf_component() are " | ||||
|             "deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report " | ||||
|             "an issue to the external_component author and ask them to update it." | ||||
|         ) | ||||
|     if components: | ||||
|         for comp in components: | ||||
|             CORE.data[KEY_ESP32][KEY_COMPONENTS][comp] = { | ||||
|                 KEY_REPO: repo, | ||||
|                 KEY_REF: ref, | ||||
|                 KEY_PATH: f"{path}/{comp}" if path else comp, | ||||
|             } | ||||
|     else: | ||||
|         CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { | ||||
|             KEY_REPO: repo, | ||||
|             KEY_REF: ref, | ||||
|             KEY_PATH: path, | ||||
|             KEY_REFRESH: refresh, | ||||
|             KEY_COMPONENTS: components, | ||||
|             KEY_SUBMODULES: submodules, | ||||
|         } | ||||
|     else: | ||||
|         component_config = CORE.data[KEY_ESP32][KEY_COMPONENTS][name] | ||||
|         if components is not None: | ||||
|             component_config[KEY_COMPONENTS] = list( | ||||
|                 set(component_config[KEY_COMPONENTS] + components) | ||||
|             ) | ||||
|         if submodules is not None: | ||||
|             if component_config[KEY_SUBMODULES] is None: | ||||
|                 component_config[KEY_SUBMODULES] = submodules | ||||
|             else: | ||||
|                 component_config[KEY_SUBMODULES] = list( | ||||
|                     set(component_config[KEY_SUBMODULES] + submodules) | ||||
|                 ) | ||||
|  | ||||
|  | ||||
| def add_extra_script(stage: str, filename: str, path: str): | ||||
| @@ -575,6 +568,17 @@ CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server" | ||||
| CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries" | ||||
| CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" | ||||
|  | ||||
|  | ||||
| def _validate_idf_component(config: ConfigType) -> ConfigType: | ||||
|     """Validate IDF component config and warn about deprecated options.""" | ||||
|     if CONF_REFRESH in config: | ||||
|         _LOGGER.warning( | ||||
|             "The 'refresh' option for IDF components is deprecated and has no effect. " | ||||
|             "It will be removed in ESPHome 2026.1. Please remove it from your configuration." | ||||
|         ) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| ESP_IDF_FRAMEWORK_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
| @@ -614,15 +618,19 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( | ||||
|                 cv.All( | ||||
|                     cv.Schema( | ||||
|                         { | ||||
|                             cv.Required(CONF_NAME): cv.string_strict, | ||||
|                         cv.Required(CONF_SOURCE): cv.SOURCE_SCHEMA, | ||||
|                             cv.Optional(CONF_SOURCE): cv.git_ref, | ||||
|                             cv.Optional(CONF_REF): cv.string, | ||||
|                             cv.Optional(CONF_PATH): cv.string, | ||||
|                         cv.Optional(CONF_REFRESH, default="1d"): cv.All( | ||||
|                             cv.Optional(CONF_REFRESH): cv.All( | ||||
|                                 cv.string, cv.source_refresh | ||||
|                             ), | ||||
|                         } | ||||
|                     ), | ||||
|                     _validate_idf_component, | ||||
|                 ) | ||||
|             ), | ||||
|         } | ||||
| @@ -814,18 +822,12 @@ async def to_code(config): | ||||
|             add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) | ||||
|  | ||||
|         for component in conf[CONF_COMPONENTS]: | ||||
|             source = component[CONF_SOURCE] | ||||
|             if source[CONF_TYPE] == TYPE_GIT: | ||||
|             add_idf_component( | ||||
|                 name=component[CONF_NAME], | ||||
|                     repo=source[CONF_URL], | ||||
|                     ref=source.get(CONF_REF), | ||||
|                 repo=component.get(CONF_SOURCE), | ||||
|                 ref=component.get(CONF_REF), | ||||
|                 path=component.get(CONF_PATH), | ||||
|                     refresh=component[CONF_REFRESH], | ||||
|             ) | ||||
|             elif source[CONF_TYPE] == TYPE_LOCAL: | ||||
|                 _LOGGER.warning("Local components are not implemented yet.") | ||||
|  | ||||
|     elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: | ||||
|         cg.add_platformio_option("framework", "arduino") | ||||
|         cg.add_build_flag("-DUSE_ARDUINO") | ||||
| @@ -924,6 +926,26 @@ def _write_sdkconfig(): | ||||
|         write_file_if_changed(sdk_path, contents) | ||||
|  | ||||
|  | ||||
| def _write_idf_component_yml(): | ||||
|     yml_path = Path(CORE.relative_build_path("src/idf_component.yml")) | ||||
|     if CORE.data[KEY_ESP32][KEY_COMPONENTS]: | ||||
|         components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] | ||||
|         dependencies = {} | ||||
|         for name, component in components.items(): | ||||
|             dependency = {} | ||||
|             if component[KEY_REF]: | ||||
|                 dependency["version"] = component[KEY_REF] | ||||
|             if component[KEY_REPO]: | ||||
|                 dependency["git"] = component[KEY_REPO] | ||||
|             if component[KEY_PATH]: | ||||
|                 dependency["path"] = component[KEY_PATH] | ||||
|             dependencies[name] = dependency | ||||
|         contents = yaml_util.dump({"dependencies": dependencies}) | ||||
|     else: | ||||
|         contents = "" | ||||
|     write_file_if_changed(yml_path, contents) | ||||
|  | ||||
|  | ||||
| # Called by writer.py | ||||
| def copy_files(): | ||||
|     if CORE.using_arduino: | ||||
| @@ -936,6 +958,7 @@ def copy_files(): | ||||
|             ) | ||||
|     if CORE.using_esp_idf: | ||||
|         _write_sdkconfig() | ||||
|         _write_idf_component_yml() | ||||
|         if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: | ||||
|             write_file_if_changed( | ||||
|                 CORE.relative_build_path("partitions.csv"), | ||||
| @@ -952,55 +975,6 @@ def copy_files(): | ||||
|             __version__, | ||||
|         ) | ||||
|  | ||||
|         import shutil | ||||
|  | ||||
|         shutil.rmtree(CORE.relative_build_path("components"), ignore_errors=True) | ||||
|  | ||||
|         if CORE.data[KEY_ESP32][KEY_COMPONENTS]: | ||||
|             components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] | ||||
|  | ||||
|             for name, component in components.items(): | ||||
|                 repo_dir, _ = git.clone_or_update( | ||||
|                     url=component[KEY_REPO], | ||||
|                     ref=component[KEY_REF], | ||||
|                     refresh=component[KEY_REFRESH], | ||||
|                     domain="idf_components", | ||||
|                     submodules=component[KEY_SUBMODULES], | ||||
|                 ) | ||||
|                 mkdir_p(CORE.relative_build_path("components")) | ||||
|                 component_dir = repo_dir | ||||
|                 if component[KEY_PATH] is not None: | ||||
|                     component_dir = component_dir / component[KEY_PATH] | ||||
|  | ||||
|                 if component[KEY_COMPONENTS] == ["*"]: | ||||
|                     shutil.copytree( | ||||
|                         component_dir, | ||||
|                         CORE.relative_build_path("components"), | ||||
|                         dirs_exist_ok=True, | ||||
|                         ignore=shutil.ignore_patterns(".git*"), | ||||
|                         symlinks=True, | ||||
|                         ignore_dangling_symlinks=True, | ||||
|                     ) | ||||
|                 elif len(component[KEY_COMPONENTS]) > 0: | ||||
|                     for comp in component[KEY_COMPONENTS]: | ||||
|                         shutil.copytree( | ||||
|                             component_dir / comp, | ||||
|                             CORE.relative_build_path(f"components/{comp}"), | ||||
|                             dirs_exist_ok=True, | ||||
|                             ignore=shutil.ignore_patterns(".git*"), | ||||
|                             symlinks=True, | ||||
|                             ignore_dangling_symlinks=True, | ||||
|                         ) | ||||
|                 else: | ||||
|                     shutil.copytree( | ||||
|                         component_dir, | ||||
|                         CORE.relative_build_path(f"components/{name}"), | ||||
|                         dirs_exist_ok=True, | ||||
|                         ignore=shutil.ignore_patterns(".git*"), | ||||
|                         symlinks=True, | ||||
|                         ignore_dangling_symlinks=True, | ||||
|                     ) | ||||
|  | ||||
|     for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items(): | ||||
|         if file[KEY_PATH].startswith("http"): | ||||
|             import requests | ||||
|   | ||||
| @@ -17,8 +17,9 @@ namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| static const char *const TAG = "ld2450"; | ||||
| static const char *const NO_MAC("08:05:04:03:02:01"); | ||||
| static const char *const UNKNOWN_MAC("unknown"); | ||||
| static const char *const NO_MAC = "08:05:04:03:02:01"; | ||||
| static const char *const UNKNOWN_MAC = "unknown"; | ||||
| static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; | ||||
|  | ||||
| // LD2450 UART Serial Commands | ||||
| static const uint8_t CMD_ENABLE_CONF = 0x00FF; | ||||
| @@ -98,13 +99,6 @@ static inline std::string get_direction(int16_t speed) { | ||||
|   return STATIONARY; | ||||
| } | ||||
|  | ||||
| static inline std::string format_version(uint8_t *buffer) { | ||||
|   return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], | ||||
|                      buffer[14]); | ||||
| } | ||||
|  | ||||
| LD2450Component::LD2450Component() {} | ||||
|  | ||||
| void LD2450Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
| #ifdef USE_NUMBER | ||||
| @@ -189,7 +183,7 @@ void LD2450Component::dump_config() { | ||||
|                 "  Throttle: %ums\n" | ||||
|                 "  MAC Address: %s\n" | ||||
|                 "  Firmware version: %s", | ||||
|                 this->throttle_, const_cast<char *>(this->mac_.c_str()), const_cast<char *>(this->version_.c_str())); | ||||
|                 this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str()); | ||||
| } | ||||
|  | ||||
| void LD2450Component::loop() { | ||||
| @@ -596,7 +590,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | ||||
| #endif | ||||
|       break; | ||||
|     case lowbyte(CMD_VERSION): | ||||
|       this->version_ = ld2450::format_version(buffer); | ||||
|       this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]); | ||||
|       ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|       if (this->version_text_sensor_ != nullptr) { | ||||
| @@ -617,7 +611,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | ||||
| #endif | ||||
| #ifdef USE_SWITCH | ||||
|       if (this->bluetooth_switch_ != nullptr) { | ||||
|         this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); | ||||
|         this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); | ||||
|       } | ||||
| #endif | ||||
|       break; | ||||
|   | ||||
| @@ -141,7 +141,6 @@ class LD2450Component : public Component, public uart::UARTDevice { | ||||
| #endif | ||||
|  | ||||
|  public: | ||||
|   LD2450Component(); | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   void loop() override; | ||||
| @@ -197,17 +196,17 @@ class LD2450Component : public Component, public uart::UARTDevice { | ||||
|   bool get_timeout_status_(uint32_t check_millis); | ||||
|   uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); | ||||
|  | ||||
|   Target target_info_[MAX_TARGETS]; | ||||
|   Zone zone_config_[MAX_ZONES]; | ||||
|   uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer | ||||
|   uint8_t buffer_data_[MAX_LINE_LENGTH]; | ||||
|   uint32_t last_periodic_millis_ = 0; | ||||
|   uint32_t presence_millis_ = 0; | ||||
|   uint32_t still_presence_millis_ = 0; | ||||
|   uint32_t moving_presence_millis_ = 0; | ||||
|   uint16_t throttle_ = 0; | ||||
|   uint16_t timeout_ = 5; | ||||
|   uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer | ||||
|   uint8_t buffer_data_[MAX_LINE_LENGTH]; | ||||
|   uint8_t zone_type_ = 0; | ||||
|   Target target_info_[MAX_TARGETS]; | ||||
|   Zone zone_config_[MAX_ZONES]; | ||||
|   std::string version_{}; | ||||
|   std::string mac_{}; | ||||
| #ifdef USE_NUMBER | ||||
|   | ||||
| @@ -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,24 +726,37 @@ 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 | ||||
|   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() | ||||
| #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 | ||||
|  | ||||
|   // 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" | ||||
|   | ||||
							
								
								
									
										0
									
								
								script/run-in-env.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								script/run-in-env.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										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