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

Introduction

Hey everyone!

Sharing my custom integration and ESPHome component for controlling Daikin Madoka BRC1H thermostats from Home Assistant via Bluetooth.

The Madoka BRC1H is a sleek wall-mounted thermostat by Daikin, controllable only via the Daikin mobile app (Bluetooth). No Wi-Fi, no usable cloud API. This project brings full control directly into HA.

Two approaches available:

  1. HA Custom Integration — direct Bluetooth connection from HA server
  2. ESPHome Component — ESP32 acts as a Bluetooth proxy (place it anywhere)

GitHub: GitHub - dasimon135/daikin_madoka: Home Assistant custom component integration for the BRC1H thermostat (madoka) · GitHub (branch madoka)


Features

  • Full HVAC control: mode (heat, cool, auto, fan, dry), target temperature, fan speed
  • Indoor temperature sensor reading
  • Fan modes: Auto, Low, Mid, High
  • Temperature range: 16°C - 32°C (61°F - 90°F)
  • Automatic BLE reconnection
  • Compatible with ESP32 and ESP32-S3 (M5Stack Atom Lite / Atom S3 Lite)

Option 1: HA Custom Integration (Direct Bluetooth)

Requirements

  • Home Assistant with Bluetooth access (USB BLE adapter if needed)
  • If HA runs in Docker: DBUS access required (see below)
  • Thermostat must be within Bluetooth range (~10m / ~30ft)

Installation

  1. Download from GitHub
  2. Copy to custom_components/daikin_madoka/ in your HA config
  3. Restart Home Assistant

Bluetooth Pairing (mandatory)

This is the critical step. The BRC1H requires secure pairing:

# 1. Disconnect the thermostat from any other device (Madoka BT menu → Forget)

# 2. On the HA server (or the machine with the BT adapter):
bluetoothctl
agent KeyboardDisplay
remove <BRC1H_MAC>          # Remove any previous pairing
scan on                      # Wait for BRC1H to appear
scan off
pair <BRC1H_MAC>            # Accept the prompt + confirm on thermostat

:warning: Confirm quickly on the thermostat after pair, otherwise the pairing times out.

HA Configuration

Go to Settings → Integrations → Add → Daikin Madoka and provide:

  • Bluetooth MAC address of the BRC1H
  • Bluetooth adapter name (usually hci0)

Two entities are created:

  • Climate: full control (mode, temperature, fan)
  • Sensor: measured indoor temperature

Docker / VM: DBUS Configuration

If HA runs in Docker, DBUS must be accessible:

# docker-compose.yml
volumes:
  - /var/run/dbus/system_bus_socket:/var/run/dbus/system_bus_socket
privileged: true

To verify it works:

docker exec -ti <container_id> /bin/bash
bleak-lescan -i hci0

Option 2: ESP32 Proxy via ESPHome (recommended)

This is what I use daily. An M5Stack Atom Lite (or any ESP32) placed near the thermostat acts as a Bluetooth proxy.

Why this approach?

  • No need for the HA server to be within Bluetooth range
  • Works flawlessly in Docker/VM without DBUS hassle
  • Single ESP32 can handle multiple thermostats
  • Excellent reliability with automatic reconnection

Tested Hardware

Platform Chip Framework Status
M5Stack Atom Lite ESP32 ESP-IDF :white_check_mark:
M5Stack Atom S3 Lite ESP32-S3 ESP-IDF 5.x :white_check_mark:
Generic ESP32 DevKit ESP32 ESP-IDF / Arduino :white_check_mark:

Component Installation

external_components:
  # From GitHub directly:
  - source: github://dasimon135/daikin_madoka@madoka
    components: [ madoka, ble_client ]

  # OR locally (copy esphome_components/ to your ESPHome config):
  # - source:
  #     type: local
  #     path: esphome_components
  #   components: [ madoka, ble_client ]

The included ble_client is a patched version compatible with ESPHome 2025.10.0+ (fixes consume_connection_slots removal).

Full Config (M5Stack Atom Lite)

substitutions:
  name: madoka-proxy
  friendly_name: "Madoka BLE Proxy"

esphome:
  name: ${name}
  friendly_name: ${friendly_name}

esp32:
  board: m5stack-atom
  framework:
    type: esp-idf

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

logger:
  level: DEBUG

api:
  encryption:
    key: !secret api_key

ota:

external_components:
  - source: github://dasimon135/daikin_madoka@madoka
    components: [ madoka, ble_client ]

esp32_ble_tracker:
  max_connections: 2

# IMPORTANT: disable classic BT proxy
bluetooth_proxy:
  active: false

ble_client:
  - mac_address: "F0:B3:1E:87:AF:FE"  # ← Your Madoka MAC
    id: madoka_living
    on_disconnect:
      then:
        - ble_client.connect: madoka_living

climate:
  - platform: madoka
    name: "Madoka Living Room"
    ble_client_id: madoka_living
    update_interval: 15s

ESP32-S3 Config (M5Stack Atom S3 Lite)

For ESP32-S3, add the BLE security configuration:

esp32:
  board: m5stack-atoms3
  variant: esp32s3
  framework:
    type: esp-idf
    version: recommended
    sdkconfig_options:
      CONFIG_BT_BLE_50_FEATURES_SUPPORTED: y
      CONFIG_BT_BLE_42_FEATURES_SUPPORTED: y

# REQUIRED for Madoka pairing on ESP32-S3
esp32_ble:
  io_capability: display_yes_no

Pairing Process (ESP32)

  1. Flash the config and open ESPHome logs
  2. The ESP32 connects to the Madoka and initiates pairing
  3. A 6-digit code appears in the logs:
╔══════════════════════════════════════════════════════════╗
║  PAIRING CODE: 790440                                   ║
║  Check that this code matches the one on the Madoka     ║
║  and CONFIRM on the thermostat!                         ║
╚══════════════════════════════════════════════════════════╝
  1. The same code appears on the thermostat screen
  2. Confirm on the thermostat (press OK) — the ESP32 auto-confirms on its side
  3. Subsequent connections are automatic (bonding is saved)

Multiple Thermostats

A single ESP32 can handle 2 thermostats simultaneously (max_connections: 2). Add a second ble_client + climate:

ble_client:
  - mac_address: "F0:B3:1E:87:AF:FE"
    id: madoka_living
    on_disconnect:
      then:
        - ble_client.connect: madoka_living
  - mac_address: "1C:54:9E:90:E3:0E"
    id: madoka_bedroom
    on_disconnect:
      then:
        - ble_client.connect: madoka_bedroom

climate:
  - platform: madoka
    name: "Madoka Living Room"
    ble_client_id: madoka_living
    update_interval: 15s
  - platform: madoka
    name: "Madoka Bedroom"
    ble_client_id: madoka_bedroom
    update_interval: 15s

Troubleshooting

Issue Solution
“device not found” in HA integration Thermostat is connected to another device (mobile app). Forget it first.
“cannot connect” DBUS not available (Docker) or BT adapter missing
Pairing fails (error 0x52) On ESP32-S3: ensure esp32_ble: io_capability: display_yes_no is set
2nd thermostat won’t connect Normal — they connect one at a time. Wait for the 1st to pair.
AttributeError: consume_connection_slots Use the patched ble_client included in this repo

Credits

  • Original HA integration: @mduran80 / pymadoka
  • ESPHome madoka component: Petapton/esphome
  • ESPHome 2025.10+ compatibility fixes & ESP32-S3 support: this repo

GitHub: GitHub - dasimon135/daikin_madoka: Home Assistant custom component integration for the BRC1H thermostat (madoka) · GitHub (branch madoka)

If you have Madoka thermostats at home, give it a try and let me know how it goes! :slightly_smiling_face:

2 Likes

I actually used some shelly gen 3’s to achieve the same (and still use them as light switch too).

All I had to do was re-flash them with esphome :innocent:

Thanks for the integration; this is exactly what I’m looking for. My HA machine is within 2 meters from the Daikin Madoka, so it should be possible to use the BT adapter on my HA machine to connect with the Madoka. Some ‘noob’ questions:

  • How can I upload the integration to the custom_components/daikin_madoka/? I have downloaded the ZIP file from Github, but how to install it on my HA machine? I have installed integrations via HACS, but never manually.
  • Which add-on is used for the BT pairing process?

Thanks in advance.

Just uploaded to ZIP file to the HA server, unpacked it via Terminal & SSH in custom_components. Also succesfully paired my Madoka via bluetoothctl. But when I want to add the integration via Settings → Integrations → Add → Daikin Madoka, I got this failure:

What I am doing wrong…? Information from log:

Logger: homeassistant.config_entries
Source: config_entries.py:3969
First occurred: 2:43:35 PM (4 occurrences)
Last logged: 3:51:19 PM

Error occurred loading flow for integration daikin_madoka: cannot import name ‘discover’ from ‘bleak’ (/usr/local/lib/python3.13/site-packages/bleak/init.py)

Bei mir kommt der gleiche Fehler: Der Konfigurationsfluss konnte nicht geladen werden: {“message”:“Invalid handler specified”}

Logger: homeassistant.config_entries
Quelle: config_entries.py:3969
Erstmals aufgetreten: 06:01:51 (1 Vorkommnis)
Zuletzt protokolliert: 06:01:51

Error occurred loading flow for integration daikin_madoka: cannot import name ‘discover’ from ‘bleak’ (/usr/local/lib/python3.13/site-packages/bleak/init.py)

Logger: homeassistant.util.loop
Quelle: util/loop.py:137
Erstmals aufgetreten: 06:01:51 (1 Vorkommnis)
Zuletzt protokolliert: 06:01:51
Detected blocking call to import_module with args (‘custom_components.daikin_madoka.config_flow’,) inside the event loop by integration ‘config’ at homeassistant/components/config/config_entries.py, line 195: return await super()._post_impl(request, data) (offender: /usr/src/homeassistant/homeassistant/loader.py, line 1307: return importlib.import_module(f"{self.pkg_path}.{platform_name}")), please create a bug report at GitHub · Where software is built For developers, please see Blocking operations with asyncio | Home Assistant Developer Docs Traceback (most recent call last): File “”, line 198, in _run_module_as_main File “”, line 88, in _run_code File “/usr/src/homeassistant/homeassistant/main.py”, line 229, in sys.exit(main()) File “/usr/src/homeassistant/homeassistant/main.py”, line 215, in main exit_code = runner.run(runtime_conf) File “/usr/src/homeassistant/homeassistant/runner.py”, line 289, in run return loop.run_until_complete(setup_and_run_hass(runtime_config)) File “/usr/local/lib/python3.13/asyncio/base_events.py”, line 712, in run_until_complete self.run_forever() File “/usr/local/lib/python3.13/asyncio/base_events.py”, line 683, in run_forever self._run_once() File “/usr/local/lib/python3.13/asyncio/base_events.py”, line 2050, in _run_once handle._run() File “/usr/local/lib/python3.13/asyncio/events.py”, line 89, in _run self._context.run(self._callback, *self._args) File “/usr/local/lib/python3.13/site-packages/aiohttp/web_protocol.py”, line 510, in _handle_request resp = await request_handler(request) File “/usr/local/lib/python3.13/site-packages/aiohttp/web_app.py”, line 569, in _handle return await handler(request) File “/usr/local/lib/python3.13/site-packages/aiohttp/web_middlewares.py”, line 117, in impl return await handler(request) File “/usr/src/homeassistant/homeassistant/components/http/security_filter.py”, line 92, in security_filter_middleware return await handler(request) File “/usr/src/homeassistant/homeassistant/components/http/forwarded.py”, line 87, in forwarded_middleware return await handler(request) File “/usr/src/homeassistant/homeassistant/components/http/request_context.py”, line 26, in request_context_middleware return await handler(request) File “/usr/src/homeassistant/homeassistant/components/http/ban.py”, line 86, in ban_middleware return await handler(request) File “/usr/src/homeassistant/homeassistant/components/http/auth.py”, line 242, in auth_middleware return await handler(request) File “/usr/src/homeassistant/homeassistant/components/http/headers.py”, line 41, in headers_middleware response = await handler(request) File “/usr/src/homeassistant/homeassistant/helpers/http.py”, line 73, in handle result = await handler(request, **request.match_info) File “/usr/src/homeassistant/homeassistant/components/http/decorators.py”, line 83, in with_admin return await func(self, request, *args, **kwargs) File “/usr/src/homeassistant/homeassistant/components/http/data_validator.py”, line 74, in wrapper return await method(view, request, data, *args, **kwargs) File “/usr/src/homeassistant/homeassistant/components/config/config_entries.py”, line 188, in post return await self._post_impl(request, data) File “/usr/src/homeassistant/homeassistant/components/config/config_entries.py”, line 195, in _post_impl return await super()._post_impl(request, data)

hi. I have an issue with my madoka controllers. I have connected them and can switch them on/off and read their status however i cannot change the temperature. here is the logs commented. can you please help out?

Setpoint writes are not applied by the device (on/off works) — SetPoint UPDATE returns an all-zero 55-byte response

Summary

Using the HA Integration (Option 1, direct Bluetooth), turning the unit on/off works and all reads work, but changing the target temperature has no effect. The write command is sent with the correct value, but the device does not apply it: an immediate re-read on the same connection still shows the previous setpoint, and the requested value never appears in any subsequent read.

Changing the setpoint directly from the physical BRC1H wall controller works fine, so it is not a unit-level lock or a centralized-controller restriction.

Environment

  • daikin_madoka v2.3.0 (manifest pins pymadoka @ git+https://github.com/dasimon135/pymadoka.git@dc8663253c8f6883511de8081406760248c87574)
  • Home Assistant Core 2026.6.4, Home Assistant OS 18.0
  • Raspberry Pi 3 (raspberrypi3-64), built-in Bluetooth adapter hci0
  • Option 1 (direct BLE from the HA host)
  • Device: Daikin BRC1H (Madoka), operation_mode = COOL. Reproduced on multiple units (full-range 16–32 and a range-restricted 22–27 unit).

What works vs. what doesn't

  • :white_check_mark: PowerState write (on/off) — applied and reflected on next read
  • :white_check_mark: All queries (temperatures, set point, operation mode, fan speed, …)
  • :cross_mark: climate.set_temperature (cooling/heating set point write) — never applied

Evidence (debug log)

Single controller, COOL mode, current cooling_set_point = 26. User requests 20 °C. The value is encoded correctly (20 * 128 = 2560 = 0x0A00):

[pymadoka.connection] Sending cmd payload: 3d00404020020a0021020d00...
        (attr 0x20 = 0x0A00 = 20 ; attr 0x21 = 0x0D00 = 26)
[pymadoka.connection] CMD 16448. Chunk #1..4/4 sent
[pymadoka.feature]    SetPoint UPDATE response received (55 bytes)
[pymadoka.feature]    SetPoint status updated, new value:
{"cooling_set_point": 0, "heating_set_point": 0, "range_enabled": 0, "mode": 0,
 ... every field 0 ...}

Immediately afterwards, on the same connection, the integration re-reads the set point:

[pymadoka.feature]    SetPoint QUERY response received (61 bytes)
{"cooling_set_point": 26, ... "cooling_lowerlimit": 16, "cooling_upperlimit": 32}   <-- unchanged

Across the entire session, cooling_set_point is never equal to the requested value (20). A second write attempt behaves identically. By contrast, a PowerState UPDATE in the same session is applied and reflected on the next read.

Observations / likely cause

  1. The UPDATE (write) response is 55 bytes, whereas a normal QUERY response is 61 bytes. SetPointStatus.set_values() parses the UPDATE response using the same field offsets as a query, which yields all-zeros — so the write response is a different structure (possibly a NAK/error frame) and/or is being mis-parsed as a valid status.
  2. In pymadoka/features/setpoint.py, get_values() encodes the two set points correctly (value * 128) but sets every other field to a fixed value (range_enabled = 0, mode = 2, and all *_lowerlimit / *_upperlimit / min_* / symbol fields = 0). It is plausible the device rejects a set-point write whose range/limit fields are zeroed or inconsistent with its current state.
  3. Reads decode correctly (/128) and the physical controller changes the set point fine, so the unit does accept set-point changes in principle.

Request

Could you review (a) the SetPoint UPDATE command payload — specifically whether the zeroed range_enabled / limit fields cause the device to reject the write — and (b) the parsing of the 55-byte UPDATE response? Happy to provide full debug logs and to test patches.