| src | ||
| LICENSE | ||
| README.md | ||
ESPHome integration for Hantech A016-09KR2/A IR Protocol
This write-up documents the process of reverse-engineering the proprietary IR protocol of the Hantech A016-09KR2/A portable air conditioner and integrating it with Home Assistant via ESPHome and an ESP32 IR bridge.
The same unit is sold under several brand names in Europe, including Lifetime Air, Oceanic, and Saneo.
Hardware
- ESP32 (generic dev board)
- IR transmitter — 940nm IR LED driven by a NPN transistor
Wiring
| Component | ESP32 pin |
|---|---|
| IR LED (via resistor) | GPIO14 |
I used a NPN transistor to drive the IR LED.
Protocol
| Parameter | Value |
|---|---|
| Carrier frequency | 38 kHz |
| Mark duration | ~485 µs |
| Short gap (bit = 0) | ~612 µs |
| Long gap (bit = 1) | ~1724 µs |
| Final mark | ~539 µs |
| Bit order | MSB first |
| Header burst | None |
Frame Layout
Byte Value Description
────────────────────────────────────────────────────
B1 0x18 Fixed header (also the checksum target)
B2 varies Power + mode
B3 varies Fan speed + sleep flag
B4 varies Target temperature
B5 varies Timer hours (normal commands: 0x00)
B6 varies Timer cancel flag (normal commands: 0x00)
B7 varies Checksum
Checksum
B7 = (0x18 - (B1 + B2 + B3 + B4 + B5 + B6)) & 0xFF
Equivalently: all 7 bytes always sum to 0x18.
B2 — Power and Mode
| Value | Meaning |
|---|---|
0x01 |
Power off |
0x81 |
Cool |
0x82 |
Dry (dehumidify) |
0x84 |
Fan only |
0xC1 |
Timer command |
B3 — Fan Speed and Sleep
| Value | Meaning |
|---|---|
0x01 |
Fan low |
0x04 |
Fan high |
0x11 |
Fan low + sleep mode |
0x14 |
Fan high + sleep mode |
Sleep mode is encoded as bit 4 of B3: fan_byte |= 0x10.
B4 — Temperature
Temperature is encoded as the decimal value in hex:
| °C | Hex |
|---|---|
| 16 | 0x10 |
| 17 | 0x11 |
| 18 | 0x12 |
| … | … |
| 30 | 0x1E |
| 31 | 0x1F |
Range: 16–31°C.
Timer Command
The sleep timer sends a separate frame with B2 = 0xC1:
B1 = 0x18
B2 = 0xC1 (timer mode)
B3 = 0x84 (fixed)
B4 = 0x10 (fixed)
B5 = hours (0x01–0x18 for 1–24 hours)
B6 = 0x00
B7 = checksum
Cancelling the timer: there is no dedicated cancel frame. Sending any normal AC state command within ~4 seconds of a timer command cancels the timer. This is the same behaviour as the physical remote.
ESPHome Configuration
Since ESPHome's climate.template platform was removed in recent versions, the integration is split into:
- ESPHome: exposes
select,number, andswitchentities - Home Assistant: wraps them in a
climateentity via HA's template climate platform
ESPHome (hantech-ac.yaml)
The core of the config is a lambda that builds and transmits the 7-byte frame:
remote_transmitter:
id: ir_tx
pin: GPIO14
carrier_duty_percent: 50%
script:
- id: send_ir
then:
- remote_transmitter.transmit_raw:
transmitter_id: ir_tx
carrier_frequency: 38000Hz
code: !lambda |-
int temp = (int)id(ac_temp).state;
if (temp < 16) temp = 16;
if (temp > 31) temp = 31;
uint8_t fan_byte = (id(ac_fan).state == "high") ? 0x04 : 0x01;
if (id(ac_sleep).state) fan_byte |= 0x10;
uint8_t mode_byte;
std::string mode = id(ac_mode).state;
if (mode == "cool") mode_byte = 0x81;
else if (mode == "dry") mode_byte = 0x82;
else if (mode == "fan_only") mode_byte = 0x84;
else mode_byte = 0x01;
uint8_t b1=0x18, b2=mode_byte, b3=fan_byte,
b4=(uint8_t)temp, b5=0x00, b6=0x00;
uint8_t b7 = (0x18-(b1+b2+b3+b4+b5+b6)) & 0xFF;
uint8_t frame[7] = {b1,b2,b3,b4,b5,b6,b7};
std::vector<int32_t> p;
p.reserve(7*8*2+1);
for (int i=0;i<7;i++)
for (int bit=7;bit>=0;bit--) {
p.push_back(485);
p.push_back(-((frame[i]>>bit)&1 ? 1724 : 612));
}
p.push_back(539);
return p;
The full ESPHome config (timer, sleep mode, and HA climate wrapper) is available in this repository.
Files
hantech-ac.yaml— ESPHome configurationhantech-template.yaml— Home Assistant template climate configuration, pase this in to your configuration.yaml