From f106a24bdece24efe4005ef78f8843deb9c9ccd6 Mon Sep 17 00:00:00 2001 From: pridmen <15156664+pridmen@users.noreply.github.com> Date: Sun, 17 May 2026 02:20:19 +0300 Subject: [PATCH] daikin_madoka: retry SET_SETTING_STATUS when BRC1H drops the chunk silently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When one ESP32 proxies two BRC1H pairs over BLE, the back-to-back SET_OPERATION_MODE (600 ms) and SET_SETTING_STATUS (200 ms) we emit from control() occasionally have the second chunk silently dropped by one of the pulses. SET_OPERATION_MODE goes through (the LCD switches to the requested mode icon), but SET_SETTING_STATUS does not, so the unit stays physically off. Polling 5–15 s later reports status=0 and flips climate::mode to OFF in HA, surprising the user who just turned it on. The other paired BRC1H on the same ESP works fine in the same window. The existing BLE_SEND_MAX_RETRIES loop in query_() only retries the raw chunk write; if all 5 writes succeed at the link layer but the pulse-side parser silently rejects, we never know. Add a higher-level status retry: stash the last requested status byte in last_set_status_byte_ on every control() that emits SET_SETTING_STATUS. In parse_cb_, on CMD_GET_SETTING_STATUS readback within STATUS_RETRY_WINDOW_MS (30 s after control()) — if cur_status_.status disagrees with our intent, re-send SET_SETTING_STATUS once more, up to MAX_STATUS_RETRIES (2). Patch the local cur_status_.status so the downstream switch doesn't flip climate to OFF based on the stale readback. Caveats: - If the user toggled the unit on the BRC1H buttons within the same 30 s window, we'd fight back briefly. Acceptable: the user usually notices and either the second retry settles or a fresh control() resets the guard. - 30 s window is conservative; in practice a single retry resolves it within the same poll cycle. --- .../daikin_madoka/daikin_madoka.cpp | 28 +++++++++++++++++++ .../components/daikin_madoka/daikin_madoka.h | 9 ++++++ 2 files changed, 37 insertions(+) diff --git a/esphome/components/daikin_madoka/daikin_madoka.cpp b/esphome/components/daikin_madoka/daikin_madoka.cpp index 283fe0a96ac7..5b198162b2a9 100644 --- a/esphome/components/daikin_madoka/daikin_madoka.cpp +++ b/esphome/components/daikin_madoka/daikin_madoka.cpp @@ -20,6 +20,12 @@ static const uint16_t CMD_GET_FAN_SPEED = 0x0050; static const uint16_t CMD_SET_FAN_SPEED = 0x4050; static const uint16_t CMD_GET_SENSOR_INFORMATION = 0x0110; +// Status retry parameters: if the BRC1H reports a status that disagrees with +// the last HA-issued status within this window, re-send SET_SETTING_STATUS up +// to MAX_STATUS_RETRIES times before accepting the readback as truth. +static const uint32_t STATUS_RETRY_WINDOW_MS = 30000; +static const uint8_t MAX_STATUS_RETRIES = 2; + void DaikinMadoka::dump_config() { LOG_CLIMATE(TAG, "Daikin Madoka Climate Controller", this); } void DaikinMadoka::setup() { this->receive_semaphore_ = xSemaphoreCreateMutex(); } @@ -81,6 +87,9 @@ void DaikinMadoka::control(const ClimateCall &call) { this->query_(CMD_SET_OPERATION_MODE, std::vector{0x20, 0x01, (uint8_t) mode_out}, 600); } this->query_(CMD_SET_SETTING_STATUS, std::vector{0x20, 0x01, (uint8_t) status_out}, 200); + this->last_set_status_byte_ = static_cast(status_out); + this->last_set_ms_ = millis(); + this->status_retry_count_ = 0; } std::vector temp_setpoint_args; if (call.get_target_temperature_high().has_value()) { @@ -321,6 +330,25 @@ void DaikinMadoka::parse_cb_(std::vector msg) { } i += len; } + // Status retry guard — see header comment on last_set_status_byte_. + if (this->last_set_ms_ > 0 && + (millis() - this->last_set_ms_) < STATUS_RETRY_WINDOW_MS && + ((uint8_t) (this->cur_status_.status ? 1 : 0)) != this->last_set_status_byte_ && + this->status_retry_count_ < MAX_STATUS_RETRIES) { + this->status_retry_count_++; + ESP_LOGW(TAG, + "[%s] SETTING_STATUS readback mismatch (got=%d, expected=%d), retry %d/%d", + this->get_name().c_str(), + (int) (this->cur_status_.status ? 1 : 0), + (int) this->last_set_status_byte_, + (int) this->status_retry_count_, + (int) MAX_STATUS_RETRIES); + this->query_(CMD_SET_SETTING_STATUS, + std::vector{0x20, 0x01, this->last_set_status_byte_}, 200); + // Don't let the rest of parse_cb_ act on the stale readback — pretend the + // pulse acknowledged what we asked for; the next poll round will confirm. + this->cur_status_.status = (this->last_set_status_byte_ != 0); + } break; case CMD_GET_OPERATION_MODE: while (i < message_size) { diff --git a/esphome/components/daikin_madoka/daikin_madoka.h b/esphome/components/daikin_madoka/daikin_madoka.h index c9725276f434..d6cb1ed894ba 100644 --- a/esphome/components/daikin_madoka/daikin_madoka.h +++ b/esphome/components/daikin_madoka/daikin_madoka.h @@ -58,6 +58,15 @@ class DaikinMadoka : public climate::Climate, public esphome::ble_client::BLECli uint16_t wwr_handle_; SemaphoreHandle_t receive_semaphore_ = nullptr; Status cur_status_; + // Status retry guard. The BRC1H sometimes drops the SET_SETTING_STATUS chunk + // silently (typically when one ESP juggles two BRC1H pairs at once over BLE). + // The unit then stays physically off even though we just told it to turn on, + // and the next poll reports status=0 and flips climate::mode to OFF in HA. + // Remember the requested status for a short window and re-issue + // SET_SETTING_STATUS if the readback disagrees, up to a small cap. + uint8_t last_set_status_byte_{0}; + uint32_t last_set_ms_{0}; + uint8_t status_retry_count_{0}; std::vector> split_payload_(std::vector msg); std::vector prepare_message_(uint16_t cmd, std::vector args);