An evening spent trying to control Daikin ceiling cassette air conditioners from Home Assistant using a Broadlink RM4 Mini IR blaster. What started as “surely someone has done this” turned into a full reverse engineering session of the DAIKIN200 IR protocol, programmatic code generation, and ultimately hitting a hardware limitation of the Broadlink itself.

The Setup

  • 2 Daikin ceiling cassette ACs, each with a BRC4M150W wireless IR remote
  • A Broadlink RM4 Mini IR blaster already in the room, controlling an IQAir air purifier via SmartIR
  • Home Assistant running in Docker

The goal: add the Daikin ACs as climate entities in Home Assistant, controllable via automations and eventually voice.

Step 1: Is It Supported Out of the Box?

SmartIR maintains a list of supported climate devices with pre-built IR code JSON files. Searching for Daikin, the closest match was code 1109 for the BRC4C158 remote — a similar BRC4-series model.

Added it to configuration.yaml:

climate:
  - platform: smartir
    name: Living Room AC
    unique_id: living_room_ac
    device_code: 1109
    controller_data: remote.Livingroom_Broadlink_RM4Mini

Restarted HA, sent a command… nothing. The AC didn’t respond. Confirmed the Broadlink was working by toggling the IQAir fan — that worked fine. Code 1109 simply wasn’t compatible with the BRC4M150W.

Step 2: Understanding the Daikin IR Landscape

Research revealed that Daikin has a fragmented IR protocol ecosystem. The IRremoteESP8266 library documents at least 12 distinct Daikin protocol variants:

Protocol Bits Remote Models
DAIKIN 280 ARC433** (standard splits)
DAIKIN128 128 BRC52B63
DAIKIN160 160 ARC423A5
DAIKIN176 176 BRC4C151, BRC4C153
DAIKIN200 200 BRC4M150W
DAIKIN216 216 ARC433B69
…and more    

The BRC4M150W uses DAIKIN200 — a 200-bit (25-byte) protocol, distinct from the DAIKIN176 protocol that SmartIR code 1109 was built for. That’s why it didn’t work.

Daikin IR is also fundamentally different from simpler remotes. Unlike a TV remote where each button sends a single command, Daikin remotes send the entire AC state in every transmission — mode, temperature, fan speed, swing, power, all encoded together. This means you can’t just learn “temperature up” — you need a unique IR code for every possible combination of settings.

For a typical setup (4 modes x 17 temperatures x 4 fan speeds + off), that’s 273 codes per AC unit. Learning them all manually would take hours.

Key Sources

Step 3: The Protocol

By studying the IRremoteESP8266 source code (ir_Daikin.h and ir_Daikin.cpp), I extracted the complete DAIKIN200 byte layout.

IR Signal Structure

The signal uses pulse distance encoding at 38 kHz, split into two sections with a gap between them:

Parameter Value
Header Mark 4,920 us
Header Space 2,230 us
Bit Mark 290 us
One Space 1,850 us
Zero Space 780 us
Gap 29,400 us

Frame Layout (25 bytes)

Section 1 — Preamble (7 bytes)

Byte Value Description
0 0x11 Fixed header
1 0xDA Fixed header
2 0x17 Fixed header
3 0x48+ Unit address
4 0x04 Fixed
5 0x00 Padding
6 Checksum (sum of bytes 0-5 & 0xFF)

Section 2 — Command (18 bytes)

Byte Field Description
7-9 Header 0x11, 0xDA, 0x17
10 Address Mirrors byte 3
11 Padding 0x00
12 AltMode Mode variant bits (upper nibble) + 0x03
13 ModeButton 0x00 for normal operation
14 Mode+Power Mode (bits 6:4) + Power (bit 0)
15-16 Timers Timer on/off
17 Temperature ((temp_C - 9) << 1) & 0x7E
18 Fan+Swing Fan speed (upper nibble) + Swing (lower nibble)
19-23 Unknown Byte 20 = 0x20, rest 0x00
24 Checksum Sum of bytes 7-23 & 0xFF

Mode Encoding

Each mode requires setting both byte 14 (mode) and byte 12 (altmode):

Mode Byte 14 (ON) Byte 12
Cool 0x21 0x73
Heat 0x11 0x73
Dry 0x71 0x23
Fan Only 0x01 0x63

Fan Speed (upper nibble of byte 18)

Speed Value
Low 0x1
Medium 0x3
High 0x5
Auto 0xA

Step 4: Discovering Unit Addresses

With two identical ACs controlled by identical BRC4M150W remotes, each remote only controls its paired unit. The address must be encoded somewhere in the IR frame. But where?

Rather than guessing, I captured one IR signal from each remote and diffed them. The capture script uses Home Assistant’s remote.learn_command service via the REST API, then reads the learned code from the Broadlink storage file and decodes it:

# capture_ir_ha.py (simplified)
def learn_command(remote_num):
    """Put Broadlink in learning mode, capture, decode."""
    ha_api("/api/services/remote/learn_command", method="POST", data={
        "entity_id": "remote.livingroom_broadlink_rm4mini",
        "device": "daikin",
        "command": f"daikin_remote_{remote_num}",
    })
    time.sleep(20)  # wait for button press

    # Read learned code from HA storage
    storage_path = Path("~/.homeassistant/.storage").expanduser()
    for code_file in storage_path.glob("broadlink_remote_*_codes"):
        with open(code_file) as f:
            storage = json.load(f)
        # ... extract and decode the base64 IR code

The decoder converts the Broadlink binary format back to raw timings, then parses the DAIKIN200 frame:

def broadlink_to_raw_timings(data):
    """Convert Broadlink packet to microsecond timings."""
    length = struct.unpack("<H", data[2:4])[0]
    pulse_data = data[4:4 + length]
    timings = []
    i = 0
    while i < len(pulse_data):
        if pulse_data[i] == 0x00:
            val = struct.unpack(">H", pulse_data[i+1:i+3])[0]
            i += 3
        else:
            val = pulse_data[i]
            i += 1
        timings.append(val * 8192 / 269)  # ticks to microseconds
    return timings

The Diff

Captured one button press from each remote (same settings: Cool, 27C) and compared:

Byte      Remote 1      Remote 2      Notes
------------------------------------------------------
S1[0]     0x11          0x11          Header
S1[1]     0xDA          0xDA          Header
S1[2]     0x17          0x17          Header
S1[3]     0x48          0x49          <-- DIFFERS (Address)
S1[4]     0x04          0x04          Header
S1[5]     0x00          0x00          Padding
S1[6]     0x4E          0x4F          <-- DIFFERS (Checksum)
S2[0-2]   11 DA 17      11 DA 17      Header
S2[3]     0x48          0x49          <-- DIFFERS (Address mirror)
S2[4-16]  (identical)   (identical)   Mode, temp, fan, etc.
S2[17]    0x28          0x29          <-- DIFFERS (Checksum)

The address is encoded in bytes 3 and 10 (mirrored in both sections):

Unit Address Byte
AC 1 0x48
AC 2 0x49

Base value 0x48 + unit index. The checksums naturally differ since they include the address byte. Everything else is identical — confirming that the address is the only differentiator.

Step 5: Generating All IR Codes Programmatically

With the protocol fully understood and addresses known, I wrote a Python script to generate complete SmartIR-compatible JSON files — no manual learning required:

def build_daikin200_frame(power_on, mode_name, temp_c, fan_name, unit_id=0):
    """Build a 25-byte DAIKIN200 IR frame."""
    state = [0x00] * 25
    addr_byte = 0x48 + (unit_id & 0x03)

    # Section 1: Preamble
    state[0:6] = [0x11, 0xDA, 0x17, addr_byte, 0x04, 0x00]

    # Section 2: Command
    state[7:11] = [0x11, 0xDA, 0x17, addr_byte]
    state[12] = (MODES[mode_name]["altmode"] << 4) | 0x03
    state[14] = (MODES[mode_name]["mode"] << 4) | (0x01 if power_on else 0x00)
    state[17] = ((temp_c - 9) << 1) & 0x7E
    fan_val = FAN_SPEEDS[fan_name] if power_on else 0x0
    state[18] = (fan_val << 4) | 0x07
    state[20] = 0x20

    # Checksums
    state[6] = sum(state[0:6]) & 0xFF
    state[24] = sum(state[7:24]) & 0xFF
    return state

The generated frames were verified against real captures — byte-for-byte match:

Generated: 11 DA 17 48 04 00 4E 11 DA 17 48 00 73 00 20 00 00 24 07 00 20 00 00 00 28
Real:      11 DA 17 48 04 00 4E 11 DA 17 48 00 73 00 20 00 00 24 07 00 20 00 00 00 28
Match: True

The byte frames are then converted to raw IR timings (mark/space pairs at the correct microsecond durations), then encoded into Broadlink’s Base64 format:

def bytes_to_raw_timings(frame_bytes):
    """Convert 25-byte frame to IR mark/space timings."""
    timings = []
    for section in [frame_bytes[0:7], frame_bytes[7:25]]:
        timings.extend([HDR_MARK, HDR_SPACE])
        for byte_val in section:
            for bit_pos in range(8):  # LSB first
                bit = (byte_val >> bit_pos) & 1
                timings.append(BIT_MARK)
                timings.append(ONE_SPACE if bit else ZERO_SPACE)
        timings.extend([BIT_MARK, GAP])
    return timings

def raw_timings_to_broadlink_base64(timings):
    """Convert microsecond timings to Broadlink Base64."""
    pulses = bytearray()
    for t in timings:
        ticks = round(t * 269 / 8192)
        if ticks > 255:
            pulses.append(0x00)
            pulses.extend(struct.pack(">H", ticks))
        else:
            pulses.append(ticks)

    packet = bytearray([0x26, 0x00])
    packet.extend(struct.pack("<H", len(pulses)))
    packet.extend(pulses)
    if len(packet) % 2: packet.append(0x00)
    packet.extend(b"\x0d\x05\x00\x00")
    return base64.b64encode(packet).decode("ascii")

Running the generator produces SmartIR JSON files with 273 codes per unit — covering every combination of mode, temperature, and fan speed:

$ python3 generate_daikin200_smartir.py
Generated 9200.json for AC 1 (address 0x48)
Generated 9201.json for AC 2 (address 0x49)

With perfect protocol knowledge and verified byte-for-byte frame accuracy, I configured SmartIR with the generated codes and sent a command. The Broadlink transmitted… and the AC ignored it.

To isolate the issue, I tried replaying the exact captured signal from the physical remote — the raw Base64 blob that the Broadlink itself had captured from Remote 2 moments earlier. Held the Broadlink right next to the AC’s IR receiver. Still nothing.

Meanwhile, the IQAir air purifier responded instantly to Broadlink commands from across the room.

The conclusion: the Broadlink RM4 Mini cannot accurately reproduce Daikin IR signals. This is a known issue. The DAIKIN200 protocol uses very tight timings (290us bit marks), and the Broadlink quantizes all timings to ~30us ticks. While this is fine for simpler protocols like the IQAir’s, Daikin’s IR receivers are strict enough to reject the Broadlink’s approximated signal.

What’s Next

The protocol reverse engineering and code generation work is complete and verified. The remaining problem is purely hardware — the Broadlink can’t reproduce the signal with sufficient timing precision.

Options for actually getting this working:

  1. ESP32 + ESPHome: An ESP32’s dedicated RMT (Remote Control Transceiver) hardware peripheral generates IR signals with microsecond precision. The ESPHome daikin_brc component has been confirmed working for transmitting to BRC4M150W-compatible ACs. Cost: ~$5 for an ESP32 dev board + an IR LED + a transistor.

  2. Sensibo / SwitchBot Hub: Purpose-built AC controllers with proper Daikin support and Home Assistant integrations. More expensive but zero soldering.

  3. Tuya IR blaster: Cheap alternative that some users report handles Daikin better than Broadlink.

Full Source Code

The complete capture and generation tools are available at github.com/AnilDaoud/daikin200-ir-codegen:

  • generate_smartir.py — Generate SmartIR JSON files from protocol knowledge
  • capture_ha.py — Capture, decode, and diff IR signals via Home Assistant’s REST API (no pip dependencies)
  • capture_broadlink.py — Standalone capture tool using the broadlink Python module directly (no HA required)

The repo also includes a full DAIKIN200 protocol reference in the README.

The tools are generic enough to work with any DAIKIN200 setup — just run the capture/diff workflow to discover your unit addresses, then generate all 273 SmartIR codes per unit in seconds.