Skip to content

daikin_madoka: retry SET_OPERATION_MODE when BRC1H drops the chunk silently - #15

Closed
pridmen wants to merge 2 commits into
Petapton:madokafrom
pridmen:madoka-mode-retry-guard
Closed

pridmen wants to merge 2 commits into
Petapton:madokafrom
pridmen:madoka-mode-retry-guard

Conversation

@pridmen

@pridmen pridmen commented May 17, 2026

Copy link
Copy Markdown

Summary

Symmetric counterpart to the SETTING_STATUS retry from #14: re-issues SET_OPERATION_MODE when the BRC1H reports a stale mode byte after a recent HA-side mode change, instead of letting parse_cb_ flip climate::mode back to the stale value.

Stacks on #14 — please merge #14 first (or treat this as #14 + this commit). The diff itself is +46 / -1 and only touches daikin_madoka.{cpp,h}.

Why

When one ESP32 proxies two BRC1H pairs over BLE, the back-to-back SET_OPERATION_MODE (600 ms) emitted from control() occasionally has its chunk silently dropped by one of the pulses. The unit stays in its previous mode, and 5–15 s later the next CMD_GET_OPERATION_MODE poll reports the stale byte. parse_cb_ then flips climate::mode in HA back to that value — surprising the user who just picked COOL/HEAT/AUTO.

Concrete repro seen today:

  • Earlier scene-restore left both BRC1H pairs in FAN_ONLY (previous mode on pulse).
  • 40 min later, a HomeKit COOL command went through; SET_SETTING_STATUS landed (unit physically on), but SET_OPERATION_MODE chunk was silently dropped at the pulse.
  • HA showed cool for ~24 s, then the next poll returned mode=0 (FAN_ONLY) and parse_cb_ snapped HA back to fan_only.

The existing sticky-FAN_ONLY guard only protects against the inverse (pulse reverting FAN_ONLY back to a main mode), so it didn't help here. BLE_SEND_MAX_RETRIES only retries the link-layer chunk write — if the link accepts but the pulse parser silently rejects, we never know.

How

  • Stash the requested raw mode byte in last_set_mode_byte_ on every control() call that emitted SET_OPERATION_MODE (255 means "unset", used for the OFF case where we don't send a mode byte).
  • In parse_cb_ for CMD_GET_OPERATION_MODE, after reading cur_status_.mode, compare it to last_set_mode_byte_ within STATUS_RETRY_WINDOW_MS.
  • On mismatch, re-issue SET_OPERATION_MODE (up to MAX_STATUS_RETRIES), then set cur_status_.mode = last_set_mode_byte_ so the downstream switch doesn't snap climate::mode to the stale value while the retry is in flight. The next poll round confirms or triggers another retry.
  • madoka_mode_byte_equiv treats byte 0 and byte 5 as equivalent — both decode to CLIMATE_MODE_FAN_ONLY downstream, and the BRC1H is free to switch between them.

Reuses the same STATUS_RETRY_WINDOW_MS = 30000 / MAX_STATUS_RETRIES = 2 constants as #14.

Test plan

Verified on a dual-BRC1H ESP32 proxy that reproduced the original bug:

  • fan_only → cool, observed for 41 s — cool stable, no rollback (pre-fix: rolled back to fan_only after 24 s).
  • cool → heat, observed for 31 s — heat stable.
  • heat → fan_only, observed for 31 s — fan_only stable.
  • fan_only → cool, observed for 30 s — cool stable.
  • All control() paths still go through to the BRC1H (LCD mode icon flips immediately, fan setpoints land).
  • OFF path unchanged: last_set_mode_byte_ = 255 keeps the guard disabled, no spurious retries.

🤖 Generated with Claude Code

pridmen and others added 2 commits May 17, 2026 02:20
…lently

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.
…lently

Symmetric counterpart to the SET_SETTING_STATUS retry: when one ESP32
proxies two BRC1H pairs over BLE, the back-to-back SET_OPERATION_MODE
(600 ms) emitted from control() occasionally has its chunk silently
dropped by one of the pulses. The unit stays in the previous mode
(typically FAN_ONLY left over from an earlier scene), and 5-15 s later
the next CMD_GET_OPERATION_MODE poll reports the stale byte and flips
climate::mode in HA back to that value — surprising the user who just
picked COOL/HEAT/AUTO in HomeKit or HA.

Concrete trigger seen here: scene-restore at the end of an airing
routine left both BRC1H pairs in FAN_ONLY. Forty minutes later, a
HomeKit COOL command went through to the active climate entity (state
flipped to cool in HA), but the SET_OPERATION_MODE chunk for that
device was dropped at the pulse, while SET_SETTING_STATUS landed.
The next poll round read mode=0 (FAN_ONLY) from the BRC1H and
parse_cb_ snapped HA back to fan_only within 24 s. The existing
sticky-FAN_ONLY guard only protects against the inverse (pulse
reverting FAN_ONLY back to a main mode), so it didn't help here.

We already remember last_set_status_byte_ / last_set_ms_ from the
SETTING_STATUS retry path. Stash the requested raw mode byte too
(255 = unset, used for the OFF case where no SET_OPERATION_MODE is
sent), and on a CMD_GET_OPERATION_MODE readback mismatch within
STATUS_RETRY_WINDOW_MS, re-issue SET_OPERATION_MODE up to
MAX_STATUS_RETRIES times. Pretend the readback matched what we
requested so the switch below does not snap climate::mode to the
stale value while we wait for the retry to land — the next poll
round will confirm or trigger another retry.

Byte 0 and 5 are treated as equivalent by madoka_mode_byte_equiv —
both decode to CLIMATE_MODE_FAN_ONLY downstream, and the BRC1H is
free to switch between them on its own. Reuses the same window and
retry cap as the SETTING_STATUS guard.
@pridmen
pridmen requested a review from Petapton as a code owner May 17, 2026 09:24
@github-actions

Copy link
Copy Markdown

To use the changes from this PR as an external component, add the following to your ESPHome configuration YAML file:

external_components:
  - source: github://Petapton/esphome@pull/15/head
    components: [daikin_madoka]
    refresh: 1h

(Added by the PR bot)

@Petapton

Copy link
Copy Markdown
Owner

Can't reproduce. Please at least provide logs.

@pridmen

pridmen commented Jun 26, 2026

Copy link
Copy Markdown
Author

Same situation as #14 (this is its symmetric counterpart): the rig it reproduced on — two BRC1H on one ESP32 — is no longer assembled. I moved each BRC1H onto its own ESP32 next to the panel to get past chronic 0x3e/133 establish failures + a weak second link, and on the split setup the mode-revert doesn't occur, so I have no live capture to give you right now.

Concrete case I saw at the time: both pulses left in FAN_ONLY by an earlier scene restore; a later HomeKit COOL command landed SET_SETTING_STATUS (unit physically on) but the SET_OPERATION_MODE chunk was silently dropped at the pulse. HA showed cool for ~24 s, then the next CMD_GET_OPERATION_MODE poll returned the stale mode (0/FAN_ONLY) and parse_cb_ snapped HA back to fan_only. The sticky-FAN_ONLY guard in #10 only covers the inverse direction, so it didn't help here.

How to reproduce / catch it on a two-on-one-ESP rig: logger: VERBOSE on daikin_madoka; set a main mode (cool/heat) from HA while both devices are connected + polled; watch whether CMD_GET_OPERATION_MODE returns the previous mode within ~30 s and parse_cb_ reverts climate::mode. On mismatch this PR re-issues SET_OPERATION_MODE (up to MAX_STATUS_RETRIES) and holds cur_status_.mode at the requested value while the retry is in flight; it reuses the STATUS_RETRY_WINDOW_MS / MAX_STATUS_RETRIES constants from #14.

I'll revisit single-ESP-two-devices later and capture it then. Keeping it open — close if you'd prefer.

@Petapton Petapton closed this Jun 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants