Reverse Engineering Daikin IR Codes for Home Assistant
Tags: homeassistant,daikin,ir,broadlink,smartir,python,reverse-engineering
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
- IRremoteESP8266 — the definitive ESP IR library, with DAIKIN200 decode/send support
- IRremoteESP8266 Issue #1802 — where BRC4M150W support was added
- blafois/Daikin-IR-Reverse — detailed protocol analysis for the standard Daikin ARC protocol
- ESPHome daikin_brc component — ESPHome’s built-in Daikin BRC climate driver
- ESPHome Issue #5902 — BRC4M150W compatibility discussion
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)
Step 6: The Broadlink Problem
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:
-
ESP32 + ESPHome: An ESP32’s dedicated RMT (Remote Control Transceiver) hardware peripheral generates IR signals with microsecond precision. The ESPHome
daikin_brccomponent has been confirmed working for transmitting to BRC4M150W-compatible ACs. Cost: ~$5 for an ESP32 dev board + an IR LED + a transistor. -
Sensibo / SwitchBot Hub: Purpose-built AC controllers with proper Daikin support and Home Assistant integrations. More expensive but zero soldering.
-
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 knowledgecapture_ha.py— Capture, decode, and diff IR signals via Home Assistant’s REST API (no pip dependencies)capture_broadlink.py— Standalone capture tool using thebroadlinkPython 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.