From 2fde6aab452aa965eabfeae04e0ec5bdfc6dbf76 Mon Sep 17 00:00:00 2001 From: Taylor Jones Date: Tue, 5 May 2026 11:31:54 -0400 Subject: [PATCH] AirConditioner: coalesce rapid control() calls instead of dropping them control() previously short-circuited with `if (m_sendControl) return;` while a previous control was in flight. Any control() call arriving in that ~1-2s window was silently discarded with no log or error. This breaks the common Home Assistant pattern where an automation fires several climate service calls in quick succession. ESPHome's midea wrapper translates each HA set_hvac_mode / set_preset_mode / set_temperature / set_fan_mode into a separate control() call. With four calls within ~100 ms, only the first lands; the others vanish. The gate was conflating two concerns -- preventing stale-diff against mid-update internal state, and preventing reentrancy from the response callback chain -- by also throwing away the user's input. Replace the silent drop with coalescing: when control() arrives while a request is in flight, merge its set fields into m_pendingControl (latest-writer-wins per field). After the in-flight request's onSuccess / onError fires (by which time m_readStatus has refreshed m_mode / m_preset / etc. from the AC's response), m_flushPending() re-invokes control() with the merged struct. The follow-up therefore diffs against fresh state, preserving both original guarantees while no longer dropping input. Coalesces N rapid calls into at most 2 UART frames (initial + atomic merged follow-up). --- .../Appliance/AirConditioner/AirConditioner.h | 7 ++++ .../AirConditioner/AirConditioner.cpp | 32 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/include/Appliance/AirConditioner/AirConditioner.h b/include/Appliance/AirConditioner/AirConditioner.h index cc783c34..baf3edaf 100644 --- a/include/Appliance/AirConditioner/AirConditioner.h +++ b/include/Appliance/AirConditioner/AirConditioner.h @@ -59,6 +59,13 @@ class AirConditioner : public ApplianceBase { Preset m_lastPreset{Preset::PRESET_NONE}; StatusData m_status{}; bool m_sendControl{}; + // Coalesces control() calls that arrive while a previous control is in flight. + // Without this, rapid back-to-back calls (e.g. four single-field HA service calls) + // are silently dropped -- only the first survives. + Control m_pendingControl{}; + bool m_hasPendingControl{}; + void m_mergePending(const Control &control); + void m_flushPending(); }; } // namespace ac diff --git a/src/Appliance/AirConditioner/AirConditioner.cpp b/src/Appliance/AirConditioner/AirConditioner.cpp index acfe85ef..b2e2d9d3 100644 --- a/src/Appliance/AirConditioner/AirConditioner.cpp +++ b/src/Appliance/AirConditioner/AirConditioner.cpp @@ -39,8 +39,10 @@ static bool checkConstraints(const Mode &mode, const Preset &preset) { } void AirConditioner::control(const Control &control) { - if (this->m_sendControl) + if (this->m_sendControl) { + this->m_mergePending(control); return; + } StatusData status = this->m_status; Mode mode = this->m_mode; Preset preset = this->m_preset; @@ -109,15 +111,43 @@ void AirConditioner::m_setStatus(StatusData status) { // onSuccess [this]() { this->m_sendControl = false; + this->m_flushPending(); }, // onError [this]() { LOG_W(TAG, "SET_STATUS(0x40) request failed..."); this->m_sendControl = false; + // Pending fields still flush; the in-flight control's fields are NOT retried + // (avoids retry storms on a wedged AC). + this->m_flushPending(); } ); } +void AirConditioner::m_mergePending(const Control &control) { + LOG_D(TAG, "Coalescing control() -- current request still in flight, merging into pending"); + if (control.mode.hasValue()) + this->m_pendingControl.mode = control.mode; + if (control.preset.hasValue()) + this->m_pendingControl.preset = control.preset; + if (control.fanMode.hasValue()) + this->m_pendingControl.fanMode = control.fanMode; + if (control.swingMode.hasValue()) + this->m_pendingControl.swingMode = control.swingMode; + if (control.targetTemp.hasValue()) + this->m_pendingControl.targetTemp = control.targetTemp; + this->m_hasPendingControl = true; +} + +void AirConditioner::m_flushPending() { + if (!this->m_hasPendingControl) + return; + this->m_hasPendingControl = false; + Control pending = this->m_pendingControl; + this->m_pendingControl = Control{}; + this->control(pending); +} + void AirConditioner::setPowerState(bool state) { if (state != this->getPowerState()) { Control control;