mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +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: | on: | ||||||
|   schedule: |   schedule: | ||||||
|     - cron: "30 0 * * *" |     - cron: "30 0 * * *"  # Run daily at 00:30 UTC | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|  |  | ||||||
| permissions: |  | ||||||
|   issues: write |  | ||||||
|   pull-requests: write |  | ||||||
|  |  | ||||||
| concurrency: |  | ||||||
|   group: lock |  | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   lock: |   lock: | ||||||
|     runs-on: ubuntu-latest |     uses: esphome/workflows/.github/workflows/lock.yml@main | ||||||
|     steps: |  | ||||||
|       - uses: dessant/lock-threads@v5.0.1 |  | ||||||
|         with: |  | ||||||
|           pr-inactive-days: "1" |  | ||||||
|           pr-lock-reason: "" |  | ||||||
|           exclude-any-pr-labels: keep-open |  | ||||||
|  |  | ||||||
|           issue-inactive-days: "7" |  | ||||||
|           issue-lock-reason: "" |  | ||||||
|           exclude-any-issue-labels: keep-open |  | ||||||
|   | |||||||
| @@ -33,9 +33,14 @@ namespace api { | |||||||
| // Since each message could contain multiple protobuf messages when using packet batching, | // Since each message could contain multiple protobuf messages when using packet batching, | ||||||
| // this limits the number of messages processed, not the number of TCP packets. | // this limits the number of messages processed, not the number of TCP packets. | ||||||
| static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; | static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; | ||||||
|  | static constexpr uint8_t MAX_PING_RETRIES = 60; | ||||||
|  | static constexpr uint16_t PING_RETRY_INTERVAL = 1000; | ||||||
|  | static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; | ||||||
|  |  | ||||||
| static const char *const TAG = "api.connection"; | static const char *const TAG = "api.connection"; | ||||||
|  | #ifdef USE_ESP32_CAMERA | ||||||
| static const int ESP32_CAMERA_STOP_STREAM = 5000; | static const int ESP32_CAMERA_STOP_STREAM = 5000; | ||||||
|  | #endif | ||||||
|  |  | ||||||
| APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) | APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) | ||||||
|     : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { |     : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { | ||||||
| @@ -86,16 +91,6 @@ APIConnection::~APIConnection() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void APIConnection::loop() { | void APIConnection::loop() { | ||||||
|   if (this->remove_) |  | ||||||
|     return; |  | ||||||
|  |  | ||||||
|   if (!network::is_connected()) { |  | ||||||
|     // when network is disconnected force disconnect immediately |  | ||||||
|     // don't wait for timeout |  | ||||||
|     this->on_fatal_error(); |  | ||||||
|     ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", this->get_client_combined_info().c_str()); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   if (this->next_close_) { |   if (this->next_close_) { | ||||||
|     // requested a disconnect |     // requested a disconnect | ||||||
|     this->helper_->close(); |     this->helper_->close(); | ||||||
| @@ -148,18 +143,19 @@ void APIConnection::loop() { | |||||||
|  |  | ||||||
|   // Process deferred batch if scheduled |   // Process deferred batch if scheduled | ||||||
|   if (this->deferred_batch_.batch_scheduled && |   if (this->deferred_batch_.batch_scheduled && | ||||||
|       App.get_loop_component_start_time() - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { |       now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { | ||||||
|     this->process_batch_(); |     this->process_batch_(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (!this->list_entities_iterator_.completed()) |   if (!this->list_entities_iterator_.completed()) { | ||||||
|     this->list_entities_iterator_.advance(); |     this->list_entities_iterator_.advance(); | ||||||
|   if (!this->initial_state_iterator_.completed() && this->list_entities_iterator_.completed()) |   } else if (!this->initial_state_iterator_.completed()) { | ||||||
|     this->initial_state_iterator_.advance(); |     this->initial_state_iterator_.advance(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   if (this->sent_ping_) { |   if (this->sent_ping_) { | ||||||
|     // Disconnect if not responded within 2.5*keepalive |     // Disconnect if not responded within 2.5*keepalive | ||||||
|     if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) { |     if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { | ||||||
|       on_fatal_error(); |       on_fatal_error(); | ||||||
|       ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); |       ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); | ||||||
|     } |     } | ||||||
| @@ -194,22 +190,20 @@ void APIConnection::loop() { | |||||||
|     // bool done = 3; |     // bool done = 3; | ||||||
|     buffer.encode_bool(3, done); |     buffer.encode_bool(3, done); | ||||||
|  |  | ||||||
|     bool success = this->send_buffer(buffer, 44); |     bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE); | ||||||
|  |  | ||||||
|     if (success) { |     if (success) { | ||||||
|       this->image_reader_.consume_data(to_send); |       this->image_reader_.consume_data(to_send); | ||||||
|     } |       if (done) { | ||||||
|     if (success && done) { |  | ||||||
|         this->image_reader_.return_image(); |         this->image_reader_.return_image(); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |   } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   if (state_subs_at_ != -1) { |   if (state_subs_at_ >= 0) { | ||||||
|     const auto &subs = this->parent_->get_state_subs(); |     const auto &subs = this->parent_->get_state_subs(); | ||||||
|     if (state_subs_at_ >= (int) subs.size()) { |     if (state_subs_at_ < static_cast<int>(subs.size())) { | ||||||
|       state_subs_at_ = -1; |  | ||||||
|     } else { |  | ||||||
|       auto &it = subs[state_subs_at_]; |       auto &it = subs[state_subs_at_]; | ||||||
|       SubscribeHomeAssistantStateResponse resp; |       SubscribeHomeAssistantStateResponse resp; | ||||||
|       resp.entity_id = it.entity_id; |       resp.entity_id = it.entity_id; | ||||||
| @@ -218,6 +212,8 @@ void APIConnection::loop() { | |||||||
|       if (this->send_message(resp)) { |       if (this->send_message(resp)) { | ||||||
|         state_subs_at_++; |         state_subs_at_++; | ||||||
|       } |       } | ||||||
|  |     } else { | ||||||
|  |       state_subs_at_ = -1; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -271,6 +267,11 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes | |||||||
|   // Encode directly into buffer |   // Encode directly into buffer | ||||||
|   msg.encode(buffer); |   msg.encode(buffer); | ||||||
|  |  | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  |   // Log the message for VV debugging | ||||||
|  |   conn->log_send_message_(msg.message_name(), msg.dump()); | ||||||
|  | #endif | ||||||
|  |  | ||||||
|   // Calculate actual encoded size (not including header that was already added) |   // Calculate actual encoded size (not including header that was already added) | ||||||
|   size_t actual_payload_size = shared_buf.size() - size_before_encode; |   size_t actual_payload_size = shared_buf.size() - size_before_encode; | ||||||
|  |  | ||||||
| @@ -1427,7 +1428,7 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe | |||||||
|  |  | ||||||
| #ifdef USE_EVENT | #ifdef USE_EVENT | ||||||
| void APIConnection::send_event(event::Event *event, const std::string &event_type) { | void APIConnection::send_event(event::Event *event, const std::string &event_type) { | ||||||
|   this->schedule_message_(event, MessageCreator(event_type, EventResponse::MESSAGE_TYPE), EventResponse::MESSAGE_TYPE); |   this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE); | ||||||
| } | } | ||||||
| void APIConnection::send_event_info(event::Event *event) { | void APIConnection::send_event_info(event::Event *event) { | ||||||
|   this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE); |   this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE); | ||||||
| @@ -1787,7 +1788,8 @@ void APIConnection::process_batch_() { | |||||||
|     const auto &item = this->deferred_batch_.items[0]; |     const auto &item = this->deferred_batch_.items[0]; | ||||||
|  |  | ||||||
|     // Let the creator calculate size and encode if it fits |     // Let the creator calculate size and encode if it fits | ||||||
|     uint16_t payload_size = item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true); |     uint16_t payload_size = | ||||||
|  |         item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true, item.message_type); | ||||||
|  |  | ||||||
|     if (payload_size > 0 && |     if (payload_size > 0 && | ||||||
|         this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) { |         this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) { | ||||||
| @@ -1837,7 +1839,7 @@ void APIConnection::process_batch_() { | |||||||
|   for (const auto &item : this->deferred_batch_.items) { |   for (const auto &item : this->deferred_batch_.items) { | ||||||
|     // Try to encode message |     // Try to encode message | ||||||
|     // The creator will calculate overhead to determine if the message fits |     // The creator will calculate overhead to determine if the message fits | ||||||
|     uint16_t payload_size = item.creator(item.entity, this, remaining_size, false); |     uint16_t payload_size = item.creator(item.entity, this, remaining_size, false, item.message_type); | ||||||
|  |  | ||||||
|     if (payload_size == 0) { |     if (payload_size == 0) { | ||||||
|       // Message won't fit, stop processing |       // Message won't fit, stop processing | ||||||
| @@ -1900,22 +1902,24 @@ void APIConnection::process_batch_() { | |||||||
| } | } | ||||||
|  |  | ||||||
| uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, | uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, | ||||||
|                                                    bool is_single) const { |                                                    bool is_single, uint16_t message_type) const { | ||||||
|   switch (message_type_) { |   if (has_tagged_string_ptr_()) { | ||||||
|     case 0:  // Function pointer |     // Handle string-based messages | ||||||
|       return data_.ptr(entity, conn, remaining_size, is_single); |     switch (message_type) { | ||||||
|  |  | ||||||
| #ifdef USE_EVENT | #ifdef USE_EVENT | ||||||
|       case EventResponse::MESSAGE_TYPE: { |       case EventResponse::MESSAGE_TYPE: { | ||||||
|         auto *e = static_cast<event::Event *>(entity); |         auto *e = static_cast<event::Event *>(entity); | ||||||
|       return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single); |         return APIConnection::try_send_event_response(e, *get_string_ptr_(), conn, remaining_size, is_single); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|       default: |       default: | ||||||
|         // Should not happen, return 0 to indicate no message |         // Should not happen, return 0 to indicate no message | ||||||
|         return 0; |         return 0; | ||||||
|     } |     } | ||||||
|  |   } else { | ||||||
|  |     // Function pointer case | ||||||
|  |     return data_.ptr(entity, conn, remaining_size, is_single); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, | 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 |   // Function pointer type for message encoding | ||||||
|   using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); |   using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); | ||||||
|  |  | ||||||
|   // Optimized MessageCreator class using union dispatch |   // Optimized MessageCreator class using tagged pointer | ||||||
|   class MessageCreator { |   class MessageCreator { | ||||||
|  |     // Ensure pointer alignment allows LSB tagging | ||||||
|  |     static_assert(alignof(std::string *) > 1, "String pointer alignment must be > 1 for LSB tagging"); | ||||||
|  |  | ||||||
|    public: |    public: | ||||||
|     // Constructor for function pointer (message_type = 0) |     // Constructor for function pointer | ||||||
|     MessageCreator(MessageCreatorPtr ptr) : message_type_(0) { data_.ptr = ptr; } |     MessageCreator(MessageCreatorPtr ptr) { | ||||||
|  |       // Function pointers are always aligned, so LSB is 0 | ||||||
|  |       data_.ptr = ptr; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // Constructor for string state capture |     // Constructor for string state capture | ||||||
|     MessageCreator(const std::string &value, uint16_t msg_type) : message_type_(msg_type) { |     explicit MessageCreator(const std::string &str_value) { | ||||||
|       data_.string_ptr = new std::string(value); |       // Allocate string and tag the pointer | ||||||
|  |       auto *str = new std::string(str_value); | ||||||
|  |       // Set LSB to 1 to indicate string pointer | ||||||
|  |       data_.tagged = reinterpret_cast<uintptr_t>(str) | 1; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Destructor |     // Destructor | ||||||
|     ~MessageCreator() { |     ~MessageCreator() { | ||||||
|       // Clean up string data for string-based message types |       if (has_tagged_string_ptr_()) { | ||||||
|       if (uses_string_data_()) { |         delete get_string_ptr_(); | ||||||
|         delete data_.string_ptr; |  | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Copy constructor |     // Copy constructor | ||||||
|     MessageCreator(const MessageCreator &other) : message_type_(other.message_type_) { |     MessageCreator(const MessageCreator &other) { | ||||||
|       if (message_type_ == 0) { |       if (other.has_tagged_string_ptr_()) { | ||||||
|         data_.ptr = other.data_.ptr; |         auto *str = new std::string(*other.get_string_ptr_()); | ||||||
|       } else if (uses_string_data_()) { |         data_.tagged = reinterpret_cast<uintptr_t>(str) | 1; | ||||||
|         data_.string_ptr = new std::string(*other.data_.string_ptr); |  | ||||||
|       } else { |       } else { | ||||||
|         data_ = other.data_;  // For POD types |         data_ = other.data_; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Move constructor |     // Move constructor | ||||||
|     MessageCreator(MessageCreator &&other) noexcept : data_(other.data_), message_type_(other.message_type_) { |     MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.ptr = nullptr; } | ||||||
|       other.message_type_ = 0;  // Reset other to function pointer type |  | ||||||
|       other.data_.ptr = nullptr; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Assignment operators (needed for batch deduplication) |     // Assignment operators (needed for batch deduplication) | ||||||
|     MessageCreator &operator=(const MessageCreator &other) { |     MessageCreator &operator=(const MessageCreator &other) { | ||||||
|       if (this != &other) { |       if (this != &other) { | ||||||
|         // Clean up current string data if needed |         // Clean up current string data if needed | ||||||
|         if (uses_string_data_()) { |         if (has_tagged_string_ptr_()) { | ||||||
|           delete data_.string_ptr; |           delete get_string_ptr_(); | ||||||
|         } |         } | ||||||
|         // Copy new data |         // Copy new data | ||||||
|         message_type_ = other.message_type_; |         if (other.has_tagged_string_ptr_()) { | ||||||
|         if (other.message_type_ == 0) { |           auto *str = new std::string(*other.get_string_ptr_()); | ||||||
|           data_.ptr = other.data_.ptr; |           data_.tagged = reinterpret_cast<uintptr_t>(str) | 1; | ||||||
|         } else if (other.uses_string_data_()) { |  | ||||||
|           data_.string_ptr = new std::string(*other.data_.string_ptr); |  | ||||||
|         } else { |         } else { | ||||||
|           data_ = other.data_; |           data_ = other.data_; | ||||||
|         } |         } | ||||||
| @@ -543,30 +545,35 @@ class APIConnection : public APIServerConnection { | |||||||
|     MessageCreator &operator=(MessageCreator &&other) noexcept { |     MessageCreator &operator=(MessageCreator &&other) noexcept { | ||||||
|       if (this != &other) { |       if (this != &other) { | ||||||
|         // Clean up current string data if needed |         // Clean up current string data if needed | ||||||
|         if (uses_string_data_()) { |         if (has_tagged_string_ptr_()) { | ||||||
|           delete data_.string_ptr; |           delete get_string_ptr_(); | ||||||
|         } |         } | ||||||
|         // Move data |         // Move data | ||||||
|         message_type_ = other.message_type_; |  | ||||||
|         data_ = other.data_; |         data_ = other.data_; | ||||||
|         // Reset other to safe state |         // Reset other to safe state | ||||||
|         other.message_type_ = 0; |  | ||||||
|         other.data_.ptr = nullptr; |         other.data_.ptr = nullptr; | ||||||
|       } |       } | ||||||
|       return *this; |       return *this; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Call operator |     // Call operator - now accepts message_type as parameter | ||||||
|     uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) const; |     uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, | ||||||
|  |                         uint16_t message_type) const; | ||||||
|  |  | ||||||
|    private: |    private: | ||||||
|     // Helper to check if this message type uses heap-allocated strings |     // Check if this contains a string pointer | ||||||
|     bool uses_string_data_() const { return message_type_ == EventResponse::MESSAGE_TYPE; } |     bool has_tagged_string_ptr_() const { return (data_.tagged & 1) != 0; } | ||||||
|     union CreatorData { |  | ||||||
|       MessageCreatorPtr ptr;    // 8 bytes |     // Get the actual string pointer (clears the tag bit) | ||||||
|       std::string *string_ptr;  // 8 bytes |     std::string *get_string_ptr_() const { | ||||||
|     } data_;                    // 8 bytes |       // NOLINTNEXTLINE(performance-no-int-to-ptr) | ||||||
|     uint16_t message_type_;     // 2 bytes (0 = function ptr, >0 = state capture) |       return reinterpret_cast<std::string *>(data_.tagged & ~uintptr_t(1)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     union { | ||||||
|  |       MessageCreatorPtr ptr; | ||||||
|  |       uintptr_t tagged; | ||||||
|  |     } data_;  // 4 bytes on 32-bit | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   // Generic batching mechanism for both state updates and entity info |   // Generic batching mechanism for both state updates and entity info | ||||||
|   | |||||||
| @@ -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(); } | ||||||
|   | |||||||
										
											
												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) { |   template<typename T> bool send_message(const T &msg) { | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|     this->log_send_message_(T::message_name(), msg.dump()); |     this->log_send_message_(msg.message_name(), msg.dump()); | ||||||
| #endif | #endif | ||||||
|     return this->send_message_(msg, T::MESSAGE_TYPE); |     return this->send_message_(msg, T::MESSAGE_TYPE); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -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_; | ||||||
|   | |||||||
| @@ -335,6 +335,7 @@ class ProtoMessage { | |||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   std::string dump() const; |   std::string dump() const; | ||||||
|   virtual void dump_to(std::string &out) const = 0; |   virtual void dump_to(std::string &out) const = 0; | ||||||
|  |   virtual const char *message_name() const { return "unknown"; } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import logging | |||||||
| import os | import os | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  |  | ||||||
| from esphome import git | from esphome import yaml_util | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
| @@ -23,7 +23,6 @@ from esphome.const import ( | |||||||
|     CONF_REFRESH, |     CONF_REFRESH, | ||||||
|     CONF_SOURCE, |     CONF_SOURCE, | ||||||
|     CONF_TYPE, |     CONF_TYPE, | ||||||
|     CONF_URL, |  | ||||||
|     CONF_VARIANT, |     CONF_VARIANT, | ||||||
|     CONF_VERSION, |     CONF_VERSION, | ||||||
|     KEY_CORE, |     KEY_CORE, | ||||||
| @@ -32,14 +31,13 @@ from esphome.const import ( | |||||||
|     KEY_TARGET_FRAMEWORK, |     KEY_TARGET_FRAMEWORK, | ||||||
|     KEY_TARGET_PLATFORM, |     KEY_TARGET_PLATFORM, | ||||||
|     PLATFORM_ESP32, |     PLATFORM_ESP32, | ||||||
|     TYPE_GIT, |  | ||||||
|     TYPE_LOCAL, |  | ||||||
|     __version__, |     __version__, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, HexInt, TimePeriod | from esphome.core import CORE, HexInt, TimePeriod | ||||||
| from esphome.cpp_generator import RawExpression | from esphome.cpp_generator import RawExpression | ||||||
| import esphome.final_validate as fv | import esphome.final_validate as fv | ||||||
| from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed | from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed | ||||||
|  | from esphome.types import ConfigType | ||||||
|  |  | ||||||
| from .boards import BOARDS | from .boards import BOARDS | ||||||
| from .const import (  # noqa | from .const import (  # noqa | ||||||
| @@ -49,10 +47,8 @@ from .const import (  # noqa | |||||||
|     KEY_EXTRA_BUILD_FILES, |     KEY_EXTRA_BUILD_FILES, | ||||||
|     KEY_PATH, |     KEY_PATH, | ||||||
|     KEY_REF, |     KEY_REF, | ||||||
|     KEY_REFRESH, |  | ||||||
|     KEY_REPO, |     KEY_REPO, | ||||||
|     KEY_SDKCONFIG_OPTIONS, |     KEY_SDKCONFIG_OPTIONS, | ||||||
|     KEY_SUBMODULES, |  | ||||||
|     KEY_VARIANT, |     KEY_VARIANT, | ||||||
|     VARIANT_ESP32, |     VARIANT_ESP32, | ||||||
|     VARIANT_ESP32C2, |     VARIANT_ESP32C2, | ||||||
| @@ -235,7 +231,7 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): | |||||||
| def add_idf_component( | def add_idf_component( | ||||||
|     *, |     *, | ||||||
|     name: str, |     name: str, | ||||||
|     repo: str, |     repo: str = None, | ||||||
|     ref: str = None, |     ref: str = None, | ||||||
|     path: str = None, |     path: str = None, | ||||||
|     refresh: TimePeriod = None, |     refresh: TimePeriod = None, | ||||||
| @@ -245,30 +241,27 @@ def add_idf_component( | |||||||
|     """Add an esp-idf component to the project.""" |     """Add an esp-idf component to the project.""" | ||||||
|     if not CORE.using_esp_idf: |     if not CORE.using_esp_idf: | ||||||
|         raise ValueError("Not an esp-idf project") |         raise ValueError("Not an esp-idf project") | ||||||
|     if components is None: |     if not repo and not ref and not path: | ||||||
|         components = [] |         raise ValueError("Requires at least one of repo, ref or path") | ||||||
|     if name not in CORE.data[KEY_ESP32][KEY_COMPONENTS]: |     if refresh or submodules or components: | ||||||
|  |         _LOGGER.warning( | ||||||
|  |             "The refresh, components and submodules parameters in add_idf_component() are " | ||||||
|  |             "deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report " | ||||||
|  |             "an issue to the external_component author and ask them to update it." | ||||||
|  |         ) | ||||||
|  |     if components: | ||||||
|  |         for comp in components: | ||||||
|  |             CORE.data[KEY_ESP32][KEY_COMPONENTS][comp] = { | ||||||
|  |                 KEY_REPO: repo, | ||||||
|  |                 KEY_REF: ref, | ||||||
|  |                 KEY_PATH: f"{path}/{comp}" if path else comp, | ||||||
|  |             } | ||||||
|  |     else: | ||||||
|         CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { |         CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { | ||||||
|             KEY_REPO: repo, |             KEY_REPO: repo, | ||||||
|             KEY_REF: ref, |             KEY_REF: ref, | ||||||
|             KEY_PATH: path, |             KEY_PATH: path, | ||||||
|             KEY_REFRESH: refresh, |  | ||||||
|             KEY_COMPONENTS: components, |  | ||||||
|             KEY_SUBMODULES: submodules, |  | ||||||
|         } |         } | ||||||
|     else: |  | ||||||
|         component_config = CORE.data[KEY_ESP32][KEY_COMPONENTS][name] |  | ||||||
|         if components is not None: |  | ||||||
|             component_config[KEY_COMPONENTS] = list( |  | ||||||
|                 set(component_config[KEY_COMPONENTS] + components) |  | ||||||
|             ) |  | ||||||
|         if submodules is not None: |  | ||||||
|             if component_config[KEY_SUBMODULES] is None: |  | ||||||
|                 component_config[KEY_SUBMODULES] = submodules |  | ||||||
|             else: |  | ||||||
|                 component_config[KEY_SUBMODULES] = list( |  | ||||||
|                     set(component_config[KEY_SUBMODULES] + submodules) |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def add_extra_script(stage: str, filename: str, path: str): | def add_extra_script(stage: str, filename: str, path: str): | ||||||
| @@ -575,6 +568,17 @@ CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server" | |||||||
| CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries" | CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries" | ||||||
| CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" | CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _validate_idf_component(config: ConfigType) -> ConfigType: | ||||||
|  |     """Validate IDF component config and warn about deprecated options.""" | ||||||
|  |     if CONF_REFRESH in config: | ||||||
|  |         _LOGGER.warning( | ||||||
|  |             "The 'refresh' option for IDF components is deprecated and has no effect. " | ||||||
|  |             "It will be removed in ESPHome 2026.1. Please remove it from your configuration." | ||||||
|  |         ) | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
| ESP_IDF_FRAMEWORK_SCHEMA = cv.All( | ESP_IDF_FRAMEWORK_SCHEMA = cv.All( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
|         { |         { | ||||||
| @@ -614,15 +618,19 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( | |||||||
|                 } |                 } | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( |             cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( | ||||||
|  |                 cv.All( | ||||||
|                     cv.Schema( |                     cv.Schema( | ||||||
|                         { |                         { | ||||||
|                             cv.Required(CONF_NAME): cv.string_strict, |                             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_PATH): cv.string, | ||||||
|                         cv.Optional(CONF_REFRESH, default="1d"): cv.All( |                             cv.Optional(CONF_REFRESH): cv.All( | ||||||
|                                 cv.string, cv.source_refresh |                                 cv.string, cv.source_refresh | ||||||
|                             ), |                             ), | ||||||
|                         } |                         } | ||||||
|  |                     ), | ||||||
|  |                     _validate_idf_component, | ||||||
|                 ) |                 ) | ||||||
|             ), |             ), | ||||||
|         } |         } | ||||||
| @@ -814,18 +822,12 @@ async def to_code(config): | |||||||
|             add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) |             add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) | ||||||
|  |  | ||||||
|         for component in conf[CONF_COMPONENTS]: |         for component in conf[CONF_COMPONENTS]: | ||||||
|             source = component[CONF_SOURCE] |  | ||||||
|             if source[CONF_TYPE] == TYPE_GIT: |  | ||||||
|             add_idf_component( |             add_idf_component( | ||||||
|                 name=component[CONF_NAME], |                 name=component[CONF_NAME], | ||||||
|                     repo=source[CONF_URL], |                 repo=component.get(CONF_SOURCE), | ||||||
|                     ref=source.get(CONF_REF), |                 ref=component.get(CONF_REF), | ||||||
|                 path=component.get(CONF_PATH), |                 path=component.get(CONF_PATH), | ||||||
|                     refresh=component[CONF_REFRESH], |  | ||||||
|             ) |             ) | ||||||
|             elif source[CONF_TYPE] == TYPE_LOCAL: |  | ||||||
|                 _LOGGER.warning("Local components are not implemented yet.") |  | ||||||
|  |  | ||||||
|     elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: |     elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: | ||||||
|         cg.add_platformio_option("framework", "arduino") |         cg.add_platformio_option("framework", "arduino") | ||||||
|         cg.add_build_flag("-DUSE_ARDUINO") |         cg.add_build_flag("-DUSE_ARDUINO") | ||||||
| @@ -924,6 +926,26 @@ def _write_sdkconfig(): | |||||||
|         write_file_if_changed(sdk_path, contents) |         write_file_if_changed(sdk_path, contents) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _write_idf_component_yml(): | ||||||
|  |     yml_path = Path(CORE.relative_build_path("src/idf_component.yml")) | ||||||
|  |     if CORE.data[KEY_ESP32][KEY_COMPONENTS]: | ||||||
|  |         components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] | ||||||
|  |         dependencies = {} | ||||||
|  |         for name, component in components.items(): | ||||||
|  |             dependency = {} | ||||||
|  |             if component[KEY_REF]: | ||||||
|  |                 dependency["version"] = component[KEY_REF] | ||||||
|  |             if component[KEY_REPO]: | ||||||
|  |                 dependency["git"] = component[KEY_REPO] | ||||||
|  |             if component[KEY_PATH]: | ||||||
|  |                 dependency["path"] = component[KEY_PATH] | ||||||
|  |             dependencies[name] = dependency | ||||||
|  |         contents = yaml_util.dump({"dependencies": dependencies}) | ||||||
|  |     else: | ||||||
|  |         contents = "" | ||||||
|  |     write_file_if_changed(yml_path, contents) | ||||||
|  |  | ||||||
|  |  | ||||||
| # Called by writer.py | # Called by writer.py | ||||||
| def copy_files(): | def copy_files(): | ||||||
|     if CORE.using_arduino: |     if CORE.using_arduino: | ||||||
| @@ -936,6 +958,7 @@ def copy_files(): | |||||||
|             ) |             ) | ||||||
|     if CORE.using_esp_idf: |     if CORE.using_esp_idf: | ||||||
|         _write_sdkconfig() |         _write_sdkconfig() | ||||||
|  |         _write_idf_component_yml() | ||||||
|         if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: |         if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: | ||||||
|             write_file_if_changed( |             write_file_if_changed( | ||||||
|                 CORE.relative_build_path("partitions.csv"), |                 CORE.relative_build_path("partitions.csv"), | ||||||
| @@ -952,55 +975,6 @@ def copy_files(): | |||||||
|             __version__, |             __version__, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         import shutil |  | ||||||
|  |  | ||||||
|         shutil.rmtree(CORE.relative_build_path("components"), ignore_errors=True) |  | ||||||
|  |  | ||||||
|         if CORE.data[KEY_ESP32][KEY_COMPONENTS]: |  | ||||||
|             components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] |  | ||||||
|  |  | ||||||
|             for name, component in components.items(): |  | ||||||
|                 repo_dir, _ = git.clone_or_update( |  | ||||||
|                     url=component[KEY_REPO], |  | ||||||
|                     ref=component[KEY_REF], |  | ||||||
|                     refresh=component[KEY_REFRESH], |  | ||||||
|                     domain="idf_components", |  | ||||||
|                     submodules=component[KEY_SUBMODULES], |  | ||||||
|                 ) |  | ||||||
|                 mkdir_p(CORE.relative_build_path("components")) |  | ||||||
|                 component_dir = repo_dir |  | ||||||
|                 if component[KEY_PATH] is not None: |  | ||||||
|                     component_dir = component_dir / component[KEY_PATH] |  | ||||||
|  |  | ||||||
|                 if component[KEY_COMPONENTS] == ["*"]: |  | ||||||
|                     shutil.copytree( |  | ||||||
|                         component_dir, |  | ||||||
|                         CORE.relative_build_path("components"), |  | ||||||
|                         dirs_exist_ok=True, |  | ||||||
|                         ignore=shutil.ignore_patterns(".git*"), |  | ||||||
|                         symlinks=True, |  | ||||||
|                         ignore_dangling_symlinks=True, |  | ||||||
|                     ) |  | ||||||
|                 elif len(component[KEY_COMPONENTS]) > 0: |  | ||||||
|                     for comp in component[KEY_COMPONENTS]: |  | ||||||
|                         shutil.copytree( |  | ||||||
|                             component_dir / comp, |  | ||||||
|                             CORE.relative_build_path(f"components/{comp}"), |  | ||||||
|                             dirs_exist_ok=True, |  | ||||||
|                             ignore=shutil.ignore_patterns(".git*"), |  | ||||||
|                             symlinks=True, |  | ||||||
|                             ignore_dangling_symlinks=True, |  | ||||||
|                         ) |  | ||||||
|                 else: |  | ||||||
|                     shutil.copytree( |  | ||||||
|                         component_dir, |  | ||||||
|                         CORE.relative_build_path(f"components/{name}"), |  | ||||||
|                         dirs_exist_ok=True, |  | ||||||
|                         ignore=shutil.ignore_patterns(".git*"), |  | ||||||
|                         symlinks=True, |  | ||||||
|                         ignore_dangling_symlinks=True, |  | ||||||
|                     ) |  | ||||||
|  |  | ||||||
|     for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items(): |     for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items(): | ||||||
|         if file[KEY_PATH].startswith("http"): |         if file[KEY_PATH].startswith("http"): | ||||||
|             import requests |             import requests | ||||||
|   | |||||||
| @@ -17,8 +17,9 @@ namespace esphome { | |||||||
| namespace ld2450 { | namespace ld2450 { | ||||||
|  |  | ||||||
| static const char *const TAG = "ld2450"; | static const char *const TAG = "ld2450"; | ||||||
| static const char *const NO_MAC("08:05:04:03:02:01"); | static const char *const NO_MAC = "08:05:04:03:02:01"; | ||||||
| static const char *const UNKNOWN_MAC("unknown"); | static const char *const UNKNOWN_MAC = "unknown"; | ||||||
|  | static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; | ||||||
|  |  | ||||||
| // LD2450 UART Serial Commands | // LD2450 UART Serial Commands | ||||||
| static const uint8_t CMD_ENABLE_CONF = 0x00FF; | static const uint8_t CMD_ENABLE_CONF = 0x00FF; | ||||||
| @@ -98,13 +99,6 @@ static inline std::string get_direction(int16_t speed) { | |||||||
|   return STATIONARY; |   return STATIONARY; | ||||||
| } | } | ||||||
|  |  | ||||||
| static inline std::string format_version(uint8_t *buffer) { |  | ||||||
|   return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], |  | ||||||
|                      buffer[14]); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| LD2450Component::LD2450Component() {} |  | ||||||
|  |  | ||||||
| void LD2450Component::setup() { | void LD2450Component::setup() { | ||||||
|   ESP_LOGCONFIG(TAG, "Running setup"); |   ESP_LOGCONFIG(TAG, "Running setup"); | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
| @@ -189,7 +183,7 @@ void LD2450Component::dump_config() { | |||||||
|                 "  Throttle: %ums\n" |                 "  Throttle: %ums\n" | ||||||
|                 "  MAC Address: %s\n" |                 "  MAC Address: %s\n" | ||||||
|                 "  Firmware version: %s", |                 "  Firmware version: %s", | ||||||
|                 this->throttle_, const_cast<char *>(this->mac_.c_str()), const_cast<char *>(this->version_.c_str())); |                 this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str()); | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2450Component::loop() { | void LD2450Component::loop() { | ||||||
| @@ -596,7 +590,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | |||||||
| #endif | #endif | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_VERSION): |     case lowbyte(CMD_VERSION): | ||||||
|       this->version_ = ld2450::format_version(buffer); |       this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]); | ||||||
|       ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); |       ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|       if (this->version_text_sensor_ != nullptr) { |       if (this->version_text_sensor_ != nullptr) { | ||||||
| @@ -617,7 +611,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | |||||||
| #endif | #endif | ||||||
| #ifdef USE_SWITCH | #ifdef USE_SWITCH | ||||||
|       if (this->bluetooth_switch_ != nullptr) { |       if (this->bluetooth_switch_ != nullptr) { | ||||||
|         this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); |         this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|       break; |       break; | ||||||
|   | |||||||
| @@ -141,7 +141,6 @@ class LD2450Component : public Component, public uart::UARTDevice { | |||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  public: |  public: | ||||||
|   LD2450Component(); |  | ||||||
|   void setup() override; |   void setup() override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void loop() override; |   void loop() override; | ||||||
| @@ -197,17 +196,17 @@ class LD2450Component : public Component, public uart::UARTDevice { | |||||||
|   bool get_timeout_status_(uint32_t check_millis); |   bool get_timeout_status_(uint32_t check_millis); | ||||||
|   uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); |   uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); | ||||||
|  |  | ||||||
|   Target target_info_[MAX_TARGETS]; |  | ||||||
|   Zone zone_config_[MAX_ZONES]; |  | ||||||
|   uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer |  | ||||||
|   uint8_t buffer_data_[MAX_LINE_LENGTH]; |  | ||||||
|   uint32_t last_periodic_millis_ = 0; |   uint32_t last_periodic_millis_ = 0; | ||||||
|   uint32_t presence_millis_ = 0; |   uint32_t presence_millis_ = 0; | ||||||
|   uint32_t still_presence_millis_ = 0; |   uint32_t still_presence_millis_ = 0; | ||||||
|   uint32_t moving_presence_millis_ = 0; |   uint32_t moving_presence_millis_ = 0; | ||||||
|   uint16_t throttle_ = 0; |   uint16_t throttle_ = 0; | ||||||
|   uint16_t timeout_ = 5; |   uint16_t timeout_ = 5; | ||||||
|  |   uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer | ||||||
|  |   uint8_t buffer_data_[MAX_LINE_LENGTH]; | ||||||
|   uint8_t zone_type_ = 0; |   uint8_t zone_type_ = 0; | ||||||
|  |   Target target_info_[MAX_TARGETS]; | ||||||
|  |   Zone zone_config_[MAX_ZONES]; | ||||||
|   std::string version_{}; |   std::string version_{}; | ||||||
|   std::string mac_{}; |   std::string mac_{}; | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
|   | |||||||
| @@ -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) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
|  | #include <algorithm> | ||||||
|  | #include <limits> | ||||||
| #include <string> | #include <string> | ||||||
| #include <vector> | #include <vector> | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| @@ -335,11 +337,16 @@ class Application { | |||||||
|    * Each component can request a high frequency loop execution by using the HighFrequencyLoopRequester |    * Each component can request a high frequency loop execution by using the HighFrequencyLoopRequester | ||||||
|    * helper in helpers.h |    * helper in helpers.h | ||||||
|    * |    * | ||||||
|  |    * Note: This method is not called by ESPHome core code. It is only used by lambda functions | ||||||
|  |    * in YAML configurations or by external components. | ||||||
|  |    * | ||||||
|    * @param loop_interval The interval in milliseconds to run the core loop at. Defaults to 16 milliseconds. |    * @param loop_interval The interval in milliseconds to run the core loop at. Defaults to 16 milliseconds. | ||||||
|    */ |    */ | ||||||
|   void set_loop_interval(uint32_t loop_interval) { this->loop_interval_ = loop_interval; } |   void set_loop_interval(uint32_t loop_interval) { | ||||||
|  |     this->loop_interval_ = std::min(loop_interval, static_cast<uint32_t>(std::numeric_limits<uint16_t>::max())); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   uint32_t get_loop_interval() const { return this->loop_interval_; } |   uint32_t get_loop_interval() const { return static_cast<uint32_t>(this->loop_interval_); } | ||||||
|  |  | ||||||
|   void schedule_dump_config() { this->dump_config_at_ = 0; } |   void schedule_dump_config() { this->dump_config_at_ = 0; } | ||||||
|  |  | ||||||
| @@ -618,6 +625,17 @@ class Application { | |||||||
|   /// Perform a delay while also monitoring socket file descriptors for readiness |   /// Perform a delay while also monitoring socket file descriptors for readiness | ||||||
|   void yield_with_select_(uint32_t delay_ms); |   void yield_with_select_(uint32_t delay_ms); | ||||||
|  |  | ||||||
|  |   // === Member variables ordered by size to minimize padding === | ||||||
|  |  | ||||||
|  |   // Pointer-sized members first | ||||||
|  |   Component *current_component_{nullptr}; | ||||||
|  |   const char *comment_{nullptr}; | ||||||
|  |   const char *compilation_time_{nullptr}; | ||||||
|  |  | ||||||
|  |   // size_t members | ||||||
|  |   size_t dump_config_at_{SIZE_MAX}; | ||||||
|  |  | ||||||
|  |   // Vectors (largest members) | ||||||
|   std::vector<Component *> components_{}; |   std::vector<Component *> components_{}; | ||||||
|  |  | ||||||
|   // Partitioned vector design for looping components |   // Partitioned vector design for looping components | ||||||
| @@ -637,11 +655,6 @@ class Application { | |||||||
|   //   and active_end_ is incremented |   //   and active_end_ is incremented | ||||||
|   // - This eliminates branch mispredictions from flag checking in the hot loop |   // - This eliminates branch mispredictions from flag checking in the hot loop | ||||||
|   std::vector<Component *> looping_components_{}; |   std::vector<Component *> looping_components_{}; | ||||||
|   uint16_t looping_components_active_end_{0}; |  | ||||||
|  |  | ||||||
|   // For safe reentrant modifications during iteration |  | ||||||
|   uint16_t current_loop_index_{0}; |  | ||||||
|   bool in_loop_{false}; |  | ||||||
|  |  | ||||||
| #ifdef USE_DEVICES | #ifdef USE_DEVICES | ||||||
|   std::vector<Device *> devices_{}; |   std::vector<Device *> devices_{}; | ||||||
| @@ -713,24 +726,37 @@ class Application { | |||||||
|   std::vector<update::UpdateEntity *> updates_{}; |   std::vector<update::UpdateEntity *> updates_{}; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_SOCKET_SELECT_SUPPORT | ||||||
|  |   std::vector<int> socket_fds_;  // Vector of all monitored socket file descriptors | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |   // String members | ||||||
|   std::string name_; |   std::string name_; | ||||||
|   std::string friendly_name_; |   std::string friendly_name_; | ||||||
|   const char *comment_{nullptr}; |  | ||||||
|   const char *compilation_time_{nullptr}; |   // 4-byte members | ||||||
|   bool name_add_mac_suffix_; |  | ||||||
|   uint32_t last_loop_{0}; |   uint32_t last_loop_{0}; | ||||||
|   uint32_t loop_interval_{16}; |  | ||||||
|   size_t dump_config_at_{SIZE_MAX}; |  | ||||||
|   uint8_t app_state_{0}; |  | ||||||
|   volatile bool has_pending_enable_loop_requests_{false}; |  | ||||||
|   Component *current_component_{nullptr}; |  | ||||||
|   uint32_t loop_component_start_time_{0}; |   uint32_t loop_component_start_time_{0}; | ||||||
|  |  | ||||||
| #ifdef USE_SOCKET_SELECT_SUPPORT | #ifdef USE_SOCKET_SELECT_SUPPORT | ||||||
|   // Socket select management |  | ||||||
|   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() |   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 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_ |   fd_set read_fds_{};       // Working fd_set for select(), copied from base_read_fds_ | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -27,20 +27,67 @@ template<typename T, typename... X> class TemplatableValue { | |||||||
|  public: |  public: | ||||||
|   TemplatableValue() : type_(NONE) {} |   TemplatableValue() : type_(NONE) {} | ||||||
|  |  | ||||||
|   template<typename F, enable_if_t<!is_invocable<F, X...>::value, int> = 0> |   template<typename F, enable_if_t<!is_invocable<F, X...>::value, int> = 0> TemplatableValue(F value) : type_(VALUE) { | ||||||
|   TemplatableValue(F value) : type_(VALUE), value_(std::move(value)) {} |     new (&this->value_) T(std::move(value)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0> |   template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0> TemplatableValue(F f) : type_(LAMBDA) { | ||||||
|   TemplatableValue(F f) : type_(LAMBDA), f_(f) {} |     this->f_ = new std::function<T(X...)>(std::move(f)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Copy constructor | ||||||
|  |   TemplatableValue(const TemplatableValue &other) : type_(other.type_) { | ||||||
|  |     if (type_ == VALUE) { | ||||||
|  |       new (&this->value_) T(other.value_); | ||||||
|  |     } else if (type_ == LAMBDA) { | ||||||
|  |       this->f_ = new std::function<T(X...)>(*other.f_); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Move constructor | ||||||
|  |   TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) { | ||||||
|  |     if (type_ == VALUE) { | ||||||
|  |       new (&this->value_) T(std::move(other.value_)); | ||||||
|  |     } else if (type_ == LAMBDA) { | ||||||
|  |       this->f_ = other.f_; | ||||||
|  |       other.f_ = nullptr; | ||||||
|  |     } | ||||||
|  |     other.type_ = NONE; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Assignment operators | ||||||
|  |   TemplatableValue &operator=(const TemplatableValue &other) { | ||||||
|  |     if (this != &other) { | ||||||
|  |       this->~TemplatableValue(); | ||||||
|  |       new (this) TemplatableValue(other); | ||||||
|  |     } | ||||||
|  |     return *this; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   TemplatableValue &operator=(TemplatableValue &&other) noexcept { | ||||||
|  |     if (this != &other) { | ||||||
|  |       this->~TemplatableValue(); | ||||||
|  |       new (this) TemplatableValue(std::move(other)); | ||||||
|  |     } | ||||||
|  |     return *this; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ~TemplatableValue() { | ||||||
|  |     if (type_ == VALUE) { | ||||||
|  |       this->value_.~T(); | ||||||
|  |     } else if (type_ == LAMBDA) { | ||||||
|  |       delete this->f_; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   bool has_value() { return this->type_ != NONE; } |   bool has_value() { return this->type_ != NONE; } | ||||||
|  |  | ||||||
|   T value(X... x) { |   T value(X... x) { | ||||||
|     if (this->type_ == LAMBDA) { |     if (this->type_ == LAMBDA) { | ||||||
|       return this->f_(x...); |       return (*this->f_)(x...); | ||||||
|     } |     } | ||||||
|     // return value also when none |     // return value also when none | ||||||
|     return this->value_; |     return this->type_ == VALUE ? this->value_ : T{}; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   optional<T> optional_value(X... x) { |   optional<T> optional_value(X... x) { | ||||||
| @@ -58,14 +105,16 @@ template<typename T, typename... X> class TemplatableValue { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   enum { |   enum : uint8_t { | ||||||
|     NONE, |     NONE, | ||||||
|     VALUE, |     VALUE, | ||||||
|     LAMBDA, |     LAMBDA, | ||||||
|   } type_; |   } type_; | ||||||
|  |  | ||||||
|   T value_{}; |   union { | ||||||
|   std::function<T(X...)> f_{}; |     T value_; | ||||||
|  |     std::function<T(X...)> *f_; | ||||||
|  |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** Base class for all automation conditions. | /** Base class for all automation conditions. | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -886,7 +886,7 @@ def build_message_type( | |||||||
|         public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP") |         public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP") | ||||||
|         snake_name = camel_to_snake(desc.name) |         snake_name = camel_to_snake(desc.name) | ||||||
|         public_content.append( |         public_content.append( | ||||||
|             f'static constexpr const char *message_name() {{ return "{snake_name}"; }}' |             f'const char *message_name() const override {{ return "{snake_name}"; }}' | ||||||
|         ) |         ) | ||||||
|         public_content.append("#endif") |         public_content.append("#endif") | ||||||
|  |  | ||||||
| @@ -1356,7 +1356,7 @@ def main() -> None: | |||||||
|     hpp += "  template<typename T>\n" |     hpp += "  template<typename T>\n" | ||||||
|     hpp += "  bool send_message(const T &msg) {\n" |     hpp += "  bool send_message(const T &msg) {\n" | ||||||
|     hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" |     hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" | ||||||
|     hpp += "    this->log_send_message_(T::message_name(), msg.dump());\n" |     hpp += "    this->log_send_message_(msg.message_name(), msg.dump());\n" | ||||||
|     hpp += "#endif\n" |     hpp += "#endif\n" | ||||||
|     hpp += "    return this->send_message_(msg, T::MESSAGE_TYPE);\n" |     hpp += "    return this->send_message_(msg, T::MESSAGE_TYPE);\n" | ||||||
|     hpp += "  }\n\n" |     hpp += "  }\n\n" | ||||||
|   | |||||||
							
								
								
									
										0
									
								
								script/run-in-env.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										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