English · 简体中文
Local Home Assistant and Apple HomeKit control for a Panasonic fresh-air heat exchanger — an energy / heat recovery ventilator (ERV / HRV / 新风 / 全热交换器). No proprietary module, no cloud, no subscription: a single ~US$5 ESP32 takes the place of the OEM module and talks to the unit directly.
Panasonic sells two optional add-on modules for these units, a Wi-Fi module (app + cloud) and an RS485 module (Modbus for building automation), and both plug into the same CN12 port. This project needs neither. One ESP32-S3 running ESPHome takes that slot and speaks the unit's private CN12 protocol straight to the main board, so everything stays on your LAN.
Compared with the alternatives — wiring dumb relays (on/off and boost only), or keeping the RS485 module and adding a Modbus/TCP gateway — this speaks the protocol natively, so you get full power / mode / fan control with nothing but the ESP.
Highlights
- 💸 Low cost — the whole build is one ESP32 (about ¥30 / US$5) and a few jumper wires. The OEM Wi-Fi and RS485 modules cost several times more and lock you into the cloud or a building system.
- 🔌 No OEM module needed — the ESP replaces the Wi-Fi or RS485 module and talks to the unit directly.
- ☁️ No cloud, no app — control never leaves your network.
- 🔄 Two-way state — a change at the wall panel shows up in Home Assistant within about a second.
- 🍎 HomeKit-ready — one Home Assistant package turns it into an Apple Home fan with speeds and modes.
Toggle the unit in Apple Home — the OEM wall controller lights up in response. All local, no cloud.
Same unit in Apple Home (left) and Home Assistant (right).
┌────────────┐ CN12 ┌──────────┐ Wi-Fi ┌────────────────┐ ┌──────────┐
│ ERV main │ 9600 8N1 │ ESP32-S3 │ native │ Home Assistant │ │ Apple │
│ board │◄═══════════►│ ESPHome │◄══════════►│ │◄═══►│ HomeKit │
└────────────┘ └──────────┘ API └────────────────┘ └──────────┘
▲ the ESP sits where the OEM Wi-Fi / RS485 module used to, and talks to the board directly
The CN12 protocol is undocumented; it was reverse-engineered with a logic analyzer and verified on a real FY-25ZDP1C. If you just want it working, follow the guide below. If you want to know how it works, jump to How it works and How it was reverse-engineered at the end.
- Repository layout
- What you need
- Compatibility
- Install and wire it up
- Home Assistant and HomeKit
- Entities
- Configuration
- Troubleshooting
- How it works
- How it was reverse-engineered
- State sync and efficiency
- Contributing and feedback
- Credits
- License
| Path | What it is | When you use it |
|---|---|---|
panasonic-erv.yaml |
ESPHome device config (entities, UART, pins) | flash this to your ESP32 |
components/panasonic_erv/ |
the driver — C++ protocol + Python codegen | pulled in by the config; edit to extend the protocol |
homeassistant/packages/panasonic_erv.yaml |
Home Assistant template-fan package | copy into HA config/packages/ for HomeKit |
tools/decode.py |
CN12 frame decoder + command builder (pure Python) | decode captures or build test commands when adapting a variant |
secrets.yaml.example |
secrets template | copy to secrets.yaml, fill Wi-Fi + keys |
docs/PROTOCOL.md · 中文 |
reverse-engineered protocol + official register map | to understand or extend the protocol |
docs/WIRING.md · 中文 |
wiring + power guide | when connecting the ESP to CN12 |
The complete bill of materials:
- 1 × ESP32-S3 dev board — the example targets the common
esp32-s3-devkitc-1. About ¥30 / US$5. - A few jumper wires — female-to-female is the easiest onto the CN12 header.
- A USB-C cable — for the first flash only.
- A computer running the ESPHome command line, or Home Assistant's ESPHome add-on.
That is the entire cost. No Panasonic Wi-Fi or RS485 module, no hub, no account, no subscription. A soldering iron is only needed later if you want to power the ESP from the unit's own 5 V instead of leaving a USB cable on it.
Verified on the Panasonic FY-25ZDP1C. Both the Wi-Fi module (FY-WF15ZDP1C) and the RS485 module
(FY-RS15ZDP2C) plug into the same CN12 port, and Panasonic's manual lists that module across the
whole ERV range, so this should work on any unit below regardless of which module yours shipped
with. Only the FY-25ZDP1C is physically tested; treat the rest as "should work, untested". Available
modes differ by model class (a thin-domestic unit's mode 0x05 is exhaust; on a cold-region unit
the same value is severe-cold, see docs/PROTOCOL.md). This component implements
the thin-domestic power/mode/fan control; larger units expose extra functions it does not touch yet.
| Class | Models |
|---|---|
| Thin domestic | FY-15ZDP1C, FY-25ZDP1C ✅, FY-35ZDP1C, FY-50ZDP1C, FY-15GZD1, FY-25GZD1, FY-35GZD1, FY-50GZD1, FY-RZ18DP1, FY-RZ28DP1, FY-RZ38DP1 |
| Thin domestic, cold-region | FY-15ZDP1CX, FY-25ZDP1CX, FY-35ZDP1CX |
| DC-motor | FY-15ZJD2C, FY-25ZJD2C, FY-35ZJD2C, FY-15GZJP2, FY-25GZJP2, FY-35GZJP2, FY-15ZJP2C, FY-25ZJP2C, FY-35ZJP2C |
| Commercial | FY-15ZY1C, FY-25ZY1C, FY-35ZY1C, FY-50ZY1C, FY-65ZY1C, FY-80ZY1C, FY-1KZY1C, FY-1HZY1C, FY-2KZY1C |
| Cabinet | FY-50ZR1C, FY-70ZR1C |
If you try one that isn't the FY-25ZDP1C, please open an issue with the result — it helps everyone.
Warning. This replaces an OEM part on a mains appliance using an undocumented protocol. You may void your warranty, and you do this at your own risk. Always work with the unit unplugged. An OEM module and the ESP must never be on the bus at the same time. See
docs/WIRING.md.
You can do the entire software side on the bench first, then wire it into the unit. Step by step:
1. Install ESPHome.
pip install esphome # or use the ESPHome add-on inside Home Assistant2. Get the config and set your secrets.
git clone https://github.com/shadowzzp/esphome-panasonic-erv
cd esphome-panasonic-erv
cp secrets.yaml.example secrets.yaml # then edit your Wi-Fi SSID and password
openssl rand -base64 32 # paste the output as panasonic_erv_api_key
openssl rand -base64 32 # paste the output as panasonic_erv_ota_passwordThe included panasonic-erv.yaml already points at the local components/
folder, so it builds as-is.
3. Flash it over USB. Plug the ESP32 into your computer and run:
esphome run panasonic-erv.yaml --device /dev/ttyACM0Every later update goes over Wi-Fi instead — no cable, no opening the case:
esphome run panasonic-erv.yaml --device <device-ip>4. Wire it to the unit. Power off the ERV at the breaker and unplug the OEM module from CN12.
Connect three wires (full guide and a wiring diagram in docs/WIRING.md):
| ESP32-S3 | CN12 | |
|---|---|---|
GPIO18 |
← | line A (board TX) |
GPIO17 |
→ | line B (board RX) |
GND |
— | GND |
Both CN12 lines are 3.3 V on the board's side, so the ESP connects directly — no level shifter. Never leave the OEM module plugged in alongside the ESP.
The ESP32-S3 (lower left) wired into the ERV's CN12 in place of the OEM module.
5. Power up. The example ships with tx_enable_at_boot: true, so the ESP registers itself as the
module on boot and the panel's F02 error clears. You are now in control.
Within a few minutes the device appears in Home Assistant over the ESPHome native API (Settings →
Devices & services). Confirm it and paste the panasonic_erv_api_key when asked. You get a Power
switch and Mode / Fan selects, plus diagnostic entities.
For Apple Home, add one file. Copy
homeassistant/packages/panasonic_erv.yaml into your
HA config/packages/ folder, make sure configuration.yaml has:
homeassistant:
packages: !include_dir_named packagesthen reload (Developer Tools → YAML → Template Entities, or restart). It folds the switch and two
selects into one fan.panasonic_erv, which the HomeKit Bridge exposes as a fan with on/off, three
speeds, and the modes as presets — synced both ways. Keep the underscore in the filename: HA
silently ignores package files whose names contain a hyphen.
| Entity | Type | Notes |
|---|---|---|
switch.panasonic_erv_power |
switch | on / off |
select.panasonic_erv_mode |
select | 热交换 / 内循环 / 外循环 (heat-exchange / recirculate / exhaust) |
select.panasonic_erv_fan |
select | 1 / 2 / 3 |
fan.panasonic_erv |
fan | the unified entity from the HA package, for HomeKit |
switch.panasonic_erv_tx_enable |
switch (config) | master bus kill-switch; turn off before reinserting an OEM module |
switch.panasonic_erv_auto_keepalive |
switch (config) | registration and keepalive loop |
button.panasonic_erv_send_registration |
button (diag) | replay the boot handshake |
button.panasonic_erv_restart / …_safe_mode_ota_recovery / …_send_poll |
button (diag) | remote recovery |
text.panasonic_erv_raw_tx_hex |
text (config) | inject an arbitrary frame for protocol work |
number.panasonic_erv_cksum_base |
number (config) | live-tune the checksum base for variants |
sensor.panasonic_erv_* |
sensor (diag) | Wi-Fi signal, uptime, RX/TX frame counters |
panasonic_erv:
id: erv
uart_id: erv_uart
tx_enable_at_boot: true # default false. true = register + keepalive on boot (OEM module removed)
cksum_base: 0xC0BA # checksum base (Σ payload + base); default verified on FY-25ZDP1Ctx_enable_at_boot defaults to false so the component is listen-only until you opt in, which is
the safe state while an OEM module is still plugged in and you are sniffing the bus.
Panel shows F02. The board is not seeing a registered module. Check that tx_enable_at_boot is
true (or toggle TX Enable on), the OEM module is removed, and TX/RX are not swapped. Press
Send Registration.
Fan or mode change is ignored while the unit is off. Expected. The board will not change speed or mode while powered off. Turn power on first; the HomeKit fan does this automatically.
No status / nothing on the bus. Confirm a common ground and that rx_pin is on line A (board TX).
Blank panel after many rapid commands. Transient; power-cycle the unit or press a panel button. The command coalescing in the firmware is there to prevent this.
The rest is for the curious. Skip it if your fan already works.
An OEM module and the main board exchange A5 A5 5A 5A-framed packets over CN12 at 9600 8N1. The
ESPHome component takes the module's place and plays its role:
- Registration. On boot it replays the module's power-up handshake, then keeps the link alive
with a poll every ~2 s and a heartbeat every ~6 s. Without this the panel shows
F02. - Reading state. It parses the board's status frames (
03 0B … RUN MODE FAN) into power, mode, and fan, so a change made at the wall panel shows up in Home Assistant too. - Sending commands. It builds 66-byte command frames (
02 0B) with the correct checksum, using masked writes so it can change one field without disturbing the others.
The full frame-by-frame spec, including the official Modbus register map, is in
docs/PROTOCOL.md.
The CN12 protocol isn't published, so it was worked out from scratch on the bench with a logic analyzer. The short version:
- Find the bus. Unplugging the OEM Wi-Fi module makes the panel show
F02(device-connection error). That confirms the module↔board link is what carries control, and CN12 is that link. - Listen. A logic analyzer on line A — the board's 3.3 V transmit line, read-only and safe —
showed 9600 8N1 traffic framed by
A5 A5 5A 5A. - Map the fields. Pressing power / mode / fan on the wall panel and watching which bytes changed
pinned down RUN, MODE and FAN inside the
36 00 00 00 …payload. - Crack the checksum. Each command carries a 16-bit
b5 b6field. Part of it looked random per boot — a red herring, that turned out to be the board's own status sequence. The command checksum was deterministic:(Σ payload + 0xC0BA) & 0xFFFF, consistent across three captured commands. Synthesizing a power-on command with that checksum and sending it made the unit physically start — the moment it all came together. - Pass as the module. Replaying the module's boot handshake and keepalive cadence makes the board
accept the ESP as a genuine module, so the panel never falls back to
F02.
Bench setup: tapping the CN12 bus to capture and decode the protocol.
The full spec is in docs/PROTOCOL.md.
Two design choices keep control snappy without flooding the network or the slow 9600-baud bus.
Slider floods. Dragging the HomeKit speed slider fires a burst of set-percentage calls. The firmware coalesces them and only sends the final, settled value, so the bus and the unit's panel never see the intermediate steps.
Idle traffic. ESPHome's select does not de-duplicate state, so polling one republishes to HA
every interval whether or not anything changed. Instead the selects here are optimistic (instant
feedback when you tap) and a small interval lambda pushes the board's real state back only when it
actually changes, and only after an in-flight command has settled. The result is instant UI, about a
one-second sync when something changes at the wall panel, and almost no traffic when idle. The logger
runs at INFO and the per-frame hex formatting is compiled out unless you raise it to DEBUG, so a
sealed unit can still be deep-debugged over the air without paying for it the rest of the time.
This runs reliably on my own FY-25ZDP1C, and I'm open-sourcing it so nobody else has to reverse-engineer the bus from scratch. Use it directly, or as a reference for your own build.
The protocol is verified on one model. The other units in the compatibility list
should work too, but I can't test them all — if yours behaves a little differently, that's expected
and usually a small tweak away (the tools/decode.py helper and the
protocol spec exist for exactly that).
Issues and pull requests are welcome, especially:
- "works on model X" — or "needed change Y" — reports, so the compatibility list reflects reality
- extra registers or features from the larger units
- fixes and improvements
If it saved you some time, a star helps others find it.
The "emulate the module" approach was proven by DomiStyle/esphome-panasonic-ac, which does the same for Panasonic air conditioners (the CN-CNT / CZ-TACG1 protocol). The ERV's CN12 framing is different, so this protocol was reversed from scratch, but that project showed it was worth doing. Built on ESPHome.