From 859101ddc9fa55db7f31dfbb4a9411f1d4c736b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 17:42:50 -0600 Subject: [PATCH] [api][event] Send events immediately to prevent loss during rapid triggers (#11777) --- esphome/components/api/api_connection.cpp | 4 +- esphome/components/api/api_connection.h | 54 ++++++++++++++++++----- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ca9ddaedf4..b4230576de 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1295,8 +1295,8 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #ifdef USE_EVENT void APIConnection::send_event(event::Event *event, const char *event_type) { - this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, - EventResponse::ESTIMATED_SIZE); + this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, + EventResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index a77c93a2d5..6cfd108927 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -650,21 +650,30 @@ class APIConnection final : public APIServerConnection { } #endif + // Helper to check if a message type should bypass batching + // Returns true if: + // 1. It's an UpdateStateResponse (always send immediately to handle cases where + // the main loop is blocked, e.g., during OTA updates) + // 2. It's an EventResponse (events are edge-triggered - every occurrence matters) + // 3. OR: User has opted into immediate sending (should_try_send_immediately = true + // AND batch_delay = 0) + inline bool should_send_immediately_(uint8_t message_type) const { + return ( +#ifdef USE_UPDATE + message_type == UpdateStateResponse::MESSAGE_TYPE || +#endif +#ifdef USE_EVENT + message_type == EventResponse::MESSAGE_TYPE || +#endif + (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)); + } + // Helper method to send a message either immediately or via batching + // Tries immediate send if should_send_immediately_() returns true and buffer has space + // Falls back to batching if immediate send fails or isn't applicable bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type, uint8_t estimated_size) { - // Try to send immediately if: - // 1. It's an UpdateStateResponse (always send immediately to handle cases where - // the main loop is blocked, e.g., during OTA updates) - // 2. OR: We should try to send immediately (should_try_send_immediately = true) - // AND Batch delay is 0 (user has opted in to immediate sending) - // 3. AND: Buffer has space available - if (( -#ifdef USE_UPDATE - message_type == UpdateStateResponse::MESSAGE_TYPE || -#endif - (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) && - this->helper_->can_write_without_blocking()) { + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { // Now actually encode and send if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) && this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { @@ -682,6 +691,27 @@ class APIConnection final : public APIServerConnection { return this->schedule_message_(entity, creator, message_type, estimated_size); } + // Overload for MessageCreator (used by events which need to capture event_type) + bool send_message_smart_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { + // Try to send immediately if message type should bypass batching and buffer has space + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { + // Now actually encode and send + if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type) && + this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { +#ifdef HAS_PROTO_MESSAGE_DUMP + // Log the message in verbose mode + this->log_proto_message_(entity, creator, message_type); +#endif + return true; + } + + // If immediate send failed, fall through to batching + } + + // Fall back to scheduled batching + return this->schedule_message_(entity, std::move(creator), message_type, estimated_size); + } + // Helper function to schedule a deferred message with known message type bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);