diff --git a/esphome/components/daikin_madoka/daikin_madoka.cpp b/esphome/components/daikin_madoka/daikin_madoka.cpp index 283fe0a96ac7..e9585b2f68a0 100644 --- a/esphome/components/daikin_madoka/daikin_madoka.cpp +++ b/esphome/components/daikin_madoka/daikin_madoka.cpp @@ -20,6 +20,22 @@ 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. The +// same parameters also gate the symmetric mode-retry below. +static const uint32_t STATUS_RETRY_WINDOW_MS = 30000; +static const uint8_t MAX_STATUS_RETRIES = 2; + +// FAN_ONLY is reported as either byte 0 or byte 5 depending on whether the +// pulse treats it as a transient fan flag or as the main operation mode. +// Treat both as the same outcome when comparing readback to requested mode. +static inline bool madoka_mode_byte_equiv(uint8_t a, uint8_t b) { + if (a == b) return true; + if ((a == 0 && b == 5) || (a == 5 && b == 0)) return true; + return false; +} + void DaikinMadoka::dump_config() { LOG_CLIMATE(TAG, "Daikin Madoka Climate Controller", this); } void DaikinMadoka::setup() { this->receive_semaphore_ = xSemaphoreCreateMutex(); } @@ -81,6 +97,11 @@ 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_mode_byte_ = mode_out; // 255 keeps mode-retry disabled for OFF (no SET_OPERATION_MODE was sent) + this->last_set_ms_ = millis(); + this->status_retry_count_ = 0; + this->mode_retry_count_ = 0; } std::vector temp_setpoint_args; if (call.get_target_temperature_high().has_value()) { @@ -321,6 +342,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) { @@ -332,6 +372,30 @@ void DaikinMadoka::parse_cb_(std::vector msg) { } i += len; } + // Mode retry guard — symmetric to the SETTING_STATUS retry above. If the + // last HA-issued SET_OPERATION_MODE was within the retry window and the + // pulse-reported byte still doesn't match (BLE drop or pulse hadn't + // applied yet), re-issue SET_OPERATION_MODE and pretend the readback + // already matched, so the switch below doesn't snap climate::mode back + // to the stale value. last_set_mode_byte_ == 255 means we haven't issued + // a mode-setting command (OFF doesn't go via OPERATION_MODE) — guard off. + if (this->last_set_ms_ > 0 && + (millis() - this->last_set_ms_) < STATUS_RETRY_WINDOW_MS && + this->last_set_mode_byte_ != 255 && + !madoka_mode_byte_equiv(this->cur_status_.mode, this->last_set_mode_byte_) && + this->mode_retry_count_ < MAX_STATUS_RETRIES) { + this->mode_retry_count_++; + ESP_LOGW(TAG, + "[%s] OPERATION_MODE readback mismatch (got=%d, expected=%d), retry %d/%d", + this->get_name().c_str(), + (int) this->cur_status_.mode, + (int) this->last_set_mode_byte_, + (int) this->mode_retry_count_, + (int) MAX_STATUS_RETRIES); + this->query_(CMD_SET_OPERATION_MODE, + std::vector{0x20, 0x01, this->last_set_mode_byte_}, 600); + this->cur_status_.mode = this->last_set_mode_byte_; + } break; default: break; diff --git a/esphome/components/daikin_madoka/daikin_madoka.h b/esphome/components/daikin_madoka/daikin_madoka.h index c9725276f434..663c0a30d5c5 100644 --- a/esphome/components/daikin_madoka/daikin_madoka.h +++ b/esphome/components/daikin_madoka/daikin_madoka.h @@ -58,6 +58,24 @@ 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}; + // Mode retry guard, symmetric to the SETTING_STATUS retry above. Same drop + // scenario, this time for SET_OPERATION_MODE: chunk silently lost on BLE → + // next poll reports the old pulse-side mode → parse_cb_ flips climate::mode + // in HA back to that stale value. Remembering the raw byte we requested + // (0..5; 255 = unset, e.g. OFF which doesn't go via OPERATION_MODE) lets us + // re-issue SET_OPERATION_MODE up to MAX_STATUS_RETRIES times before + // accepting the readback as truth. + uint8_t last_set_mode_byte_{255}; + uint8_t mode_retry_count_{0}; std::vector> split_payload_(std::vector msg); std::vector prepare_message_(uint16_t cmd, std::vector args);