Control Daikin Madoka BRC1H Thermostat via Bluetooth — HA Custom Integration + ESPHome Component

Hi Julian,

Happy to — here’s the full Option 1 route, step by step. The idea: your ESP32 stops being a Madoka client and becomes a standard Bluetooth proxy; Home Assistant does the talking, and the integration handles pairing, reconnection and recovery for you.

Step 1 — un-jam the thermostat first (don’t skip this)

The failed attempts left a stale bond in the BRC1H. On the unit: Bluetooth menu → forget / un-pair, then toggle its Bluetooth off and on again. Otherwise even a perfect config gets refused.

Step 2 — replace your ESPHome config

Delete the external_components:, climate: (platform madoka) and on_disconnect: blocks — the ESP must no longer connect to the thermostat itself, otherwise it occupies its connection slot. Keep your existing esphome: / wifi: / api: / ota: header.

One thing to check before you paste this: which framework your ESP32 is currently on. Look at the top of your existing YAML for an esp32: block — if it says framework: type: arduino (or if there is no framework: line at all on an older ESPHome, which defaulted to arduino), change it to esp-idf as shown below. A proxy that carries active connections needs esp-idf; on arduino you’d get scanning only, and the Madoka would never connect. If your config already has type: esp-idf, you’re set — just leave that block as it is and add the rest.

Two practical notes on that switch:

  • The first upload after changing framework rebuilds everything from scratch — it’s slow (several minutes), that’s normal.
  • If the OTA upload fails right after the switch, plug the ESP32 into USB and flash it once over serial; OTA works again afterwards.
esp32:
  framework:
    type: esp-idf

esp32_ble_tracker:
  scan_parameters:
    interval: 1100ms
    window: 1100ms
    active: true

bluetooth_proxy:
  active: true

# Enables the MITM-capable pairing the BRC1H requires.
# The stock proxy firmware runs io_capability: none — that is exactly why
# the plain bluetooth-proxy build can never pair with a Madoka.
esp32_ble:
  io_capability: display_yes_no

# Pairing responder: it never connects on its own (auto_connect: false).
# It only wakes up when HA starts a pairing through this proxy, shows you
# the 6-digit code as an HA notification, and confirms it on the ESP side.
ble_client:
  - mac_address: "AA:BB:CC:DD:EE:FF"   # your BRC1H
    id: madoka_pairing
    auto_connect: false
    on_numeric_comparison_request:
      then:
        - homeassistant.action:
            action: persistent_notification.create
            data:
              notification_id: madoka_pairing
              title: "Madoka pairing"
              message: !lambda 'return "Code on the thermostat screen: " + str_sprintf("%06u", passkey);'
        - ble_client.numeric_comparison_reply:
            id: madoka_pairing
            accept: true

Flash it. For the notification to work, go to Settings → Devices & Services → ESPHome → your proxy → enable “Allow the device to perform Home Assistant actions”. Without it pairing still works, you just won’t see the code in HA.

And if you’d rather not hand-write any of this: tell me the exact board you bought (M5Stack ATOM Lite, ESP32-DevKitC, Wemos D1 Mini32, …) and I’ll give you the config based on the official ESPHome bluetooth-proxy package for that board instead — same result, one line to maintain, with the pairing bits added on top.

Step 3 — install the integration

HACS → three-dot menu → Custom repositorieshttps://github.com/dasimon135/daikin_madoka, category Integration → install Daikin Madoka → restart HA.

Step 4 — add the thermostat

It should appear on its own as a discovered device (via your proxy). Otherwise: Settings → Devices & Services → Add Integration → Daikin Madoka. On the first connection the BRC1H shows a pairing prompt on its screen — accept it within a few seconds; the ESP side is already auto-confirmed by the responder above.

If something goes wrong, the device page has a sensor.*_connection_status that tells the failure modes apart: not_advertising (out of range / off), needs_pairing (a proxy was refused — press the Reconnect button next to the thermostat and accept the prompt), pairing_slow (busy proxy, it recovers on its own).

Two things worth knowing:

  • The bond lives on the proxy, not in HA. One proxy = one pairing. If you later add a second active proxy in range, it needs the same YAML and its own prompt — an unpaired active proxy will happily win the connection and then fail with “Insufficient authentication”.
  • You get the single setpoint you wanted outside AUTO mode, plus indoor/outdoor temperature, operating time, filter, and the diagnostics above.

Full reference config (multi-thermostat, multi-proxy, troubleshooting table) is here if you need it later: daikin_madoka/docs/esphome-proxy.md at 8a5ec4d096be9dc13f0be0e7dcf9971e85bae09d · dasimon135/daikin_madoka · GitHub

Shout if the prompt doesn’t show up — that’s the one step that needs you physically at the thermostat.

1 Like

Update: now in the HACS default store! :tada:
The integration has just been accepted into the HACS default repositories (PR #9238 merged).

You no longer need to add it as a custom repository — just search for “Daikin Madoka” directly in HACS.

It may take a few hours to show up while HACS refreshes its catalog data.

Thanks everyone for the feedback and testing that helped get it here!

Thanks David for the update!
I previously had a patch installed which worked around the issue. Now updated everything but I still have some issues with the code (see below).

v3.8.1: setpoint writes still rejected on single-setpoint units (the two set points must match)

On my three BRC1H units the write is still not applied, and the debug log pins down why.

Environment

  • daikin_madoka v3.8.1, pymadoka-ng 0.3.10
  • Home Assistant Core on HAOS, Raspberry Pi 3, host adapter hci0 (no proxy)
  • BRC1H configured with single-setpoint logic (Madoka app: Setpoint logic → Remote controller + Single setpoint), so range_enabled = 0 and the unit always reports cooling_set_point == heating_set_point.

Evidence

Unit in COOL at 24 °C. climate.set_temperature → 22 °C. Outgoing payload (CMD 16448):

3d004040 20020b00 21020c00 300100 310101 320100
a00110 a10110 a2020800 a3020800 a40115 a50115
b00120 b10120 b2021000 b3021000 b4011b b5011b
  • attr 0x20 (cooling) = 0x0B00 = 22 ← requested
  • attr 0x21 (heating) = 0x0C00 = 24 ← unchanged
  • all range/limit/mode attrs correctly echo the device’s own values (mode=1, limits 16/32, symbols 21/27) — the v2.4.0 fix is working

The device answers with the usual 55-byte UPDATE frame and the immediate re-read still returns 24. Every QUERY in the whole session reports the two set points equal (24/24, then 25/25 on another unit) — the unit never holds mismatched set points.

Cause

In climate.py::async_set_temperature, with a single ATTR_TEMPERATURE the target is applied to only one of the two set points:

if operation_mode != OperationModeEnum.HEAT:
    new_status.cooling_set_point = round(target)
if operation_mode != OperationModeEnum.COOL:
    new_status.heating_set_point = round(target)

In COOL that writes cooling = 22, heating = 24. On a single-setpoint unit that pair is inconsistent with the device’s own configuration, and the BRC1H rejects the frame exactly as it did with the zeroed limits — same silent failure mode, different field.

I confirmed the inverse experimentally before upgrading: a local patch that wrote both set points to the same target applied correctly in COOL, HEAT and AUTO on all three units.

Suggested fix

When the unit is not in range mode, keep the two set points equal:

if target is not None:
    operation_mode = self.controller.operation_mode.status.operation_mode
    if not self._range_active:
        new_status.cooling_set_point = round(target)
        new_status.heating_set_point = round(target)
    else:
        if operation_mode != OperationModeEnum.HEAT:
            new_status.cooling_set_point = round(target)
        if operation_mode != OperationModeEnum.COOL:
            new_status.heating_set_point = round(target)

Running this locally on v3.8.1; happy to test a proper patch or open a PR.

Minor, separate

The same log shows recurring Query failed for SetPoint after 2 attempts, skipping: Could not send command: connection lost on the Pi’s built-in adapter with three units — the grace period added in 3.2.0 does keep the entities alive, so this is only noise for me, but flagging it in case it is useful.