Safety notice
This project interfaces with a combustion heater that produces open flame, high heat, and carbon monoxide. Improper installation or software faults can result in fire, CO poisoning, serious injury, or death. By using any part of this project you accept full responsibility for your implementation and installation. This project is not affiliated with or endorsed by Eberspächer Group or Espar Products Inc.
At minimum: install a working CO detector, retain the ability to cut heater power independently of this controller, and test all shutdown paths before relying on this system.
What is this?
A custom ESPHome external component that speaks directly to an Espar Airtronic S3 B2L Gasoline heater over its CAN bus, giving you a full climate entity in Home Assistant — no OEM EasyStart Pro controller required.
The CAN protocol was fully reverse-engineered from live bus captures. Every frame ID, byte position, temperature encoding, heartbeat sequence, and init burst has been mapped and is documented in the repo, along with all the raw captures used to derive it.
This was built for a van/truck camper build to replace the proprietary Espar controller with a $15 ESP32 board that integrates into a broader Home Assistant setup. The project was developed in collaboration with Claude (Anthropic's AI assistant) — the RE methodology, ESPHome component architecture, and documentation were built through an iterative human-AI collaboration.
What's working / what's still open
Full HEAT / FAN ONLY / OFF control from Home Assistant climate entity
Real-time heater state (STARTUP / HEATING / FAN / IDLE)
Flame confirmation sensor
Temperature setpoint encoding confirmed at 66°F, 75°F, 78°F, and 80°F
Behavioral fault detection (failed starts, heartbeat loss, lockout)
WS2812 RGB LED status indicator
ESPHome external component — drop-in, no custom firmware needed
Fault frame CAN IDs not yet decoded (P-codes known from service manual; capture methodology documented in docs/fault-codes.md)
Only tested on Airtronic S3 B2L Gasoline 12V — diesel and other variants untested
Boot sync requires heater power and ESP32 to start together (see Known Issues)
Hardware
| Component | Notes |
|---|---|
| WeAct CAN485 V1.0 (ESP32) | ESP32 + onboard CAN transceiver. No external level shifter or transceiver module needed. |
| Molex MicroFit 3.0 dual-row connector | For tapping the heater harness at the XB10 connector without cutting wires |
Important: The onboard 120Ω termination switch (K3) must be OFF. The WeAct taps the bus mid-point at the EasyStart Pro Molex — it is not a bus endpoint. Turn K3 ON only if the WeAct is the sole device on the bus.
Full wiring and pinout details: docs/hardware-setup.md
Heater connector — XB10
| XB10 Pin | Wire color (observed) | Signal |
|---|---|---|
| 7 | GY/BU (grey/blue) | CAN H |
| 8 | GN/YE (green/yellow) | CAN L |
| 2 | BR/BK (brown/black) | Ground reference |
Wire colors can vary by harness revision — always verify with a CAN analyzer before assuming color mapping.
GPIO assignments (WeAct CAN485)
| GPIO | Function |
|---|---|
| 26 | CAN RX |
| 27 | CAN TX |
| 4 | WS2812 RGB LED data |
Installation
Requirements
- ESPHome 2026.3 or later
framework: type: arduino— required for the NeoPixelBus LED component
1. Copy the component
Copy esphome/components/ from the repo into your ESPHome config directory:
your-esphome-config/
├── espar-heater.yaml
├── secrets.yaml
└── components/
└── espar_can/
├── __init__.py
├── espar_can.h
└── espar_can.cpp
2. Create secrets.yaml
wifi_ssid: "YourNetwork"
wifi_password: "YourPassword"
api_encryption_key: "generate with: esphome generate-encryption-key"
ota_password: "choose-any-string"
fallback_hotspot_password: "choose-any-string"
3. Create a cabin temperature sensor in HA (optional but recommended)
The component works without a temperature sensor — it heats continuously when mode=HEAT. For proper thermostat behavior, create a template sensor averaging your cabin sensors:
template:
- sensor:
- name: "Cabin Average Temperature"
unit_of_measurement: "°C"
device_class: temperature
state: >
{{ ( states('sensor.sensor_a') | float(0)
+ states('sensor.sensor_b') | float(0) ) / 2 }}
4. Edit espar-heater.yaml
Update the cabin temperature sensor entity ID to match yours:
sensor:
- platform: homeassistant
entity_id: sensor.cabin_average_temperature # ← your HA sensor
°F sensor? The component works internally in Celsius. If your HA sensor reports Fahrenheit, uncomment the conversion lambda filter in the YAML — otherwise the thermostat will see implausibly high temperatures and stay in IDLE. Check your ESPHome logs for
Current Temperature: 44.95°Cas a sign of this mismatch.
5. Flash
First flash must be via USB:
esphome run espar-heater.yaml
Subsequent updates can be OTA.
Climate entity behaviour
| HA Mode | Behaviour |
|---|---|
| HEAT | Sends HEAT at 85°F (configurable) when cabin < setpoint; sends IDLE when at/above setpoint. The heater's internal thermocouple acts as a safety ceiling only. |
| FAN ONLY | Runs the blower without combustion. Expect ~75s cooldown fan run after switching to OFF. |
| OFF | Sends IDLE command. |
Entity reference
| Entity | Type | Description |
|---|---|---|
climate.espar_heater |
Climate | Main control — HEAT / FAN ONLY / OFF |
sensor.espar_heater_state |
Text sensor | STARTUP / IDLE / HEATING / FAN / UNKNOWN |
binary_sensor.espar_flame_active |
Binary sensor | ON when combustion confirmed |
binary_sensor.espar_connected |
Binary sensor | ON while 0x625 heartbeat is present |
sensor.espar_fault |
Text sensor | OK or fault description |
light.espar_status_led |
Light | WS2812 RGB indicator |
binary_sensor.espar_node_status |
Binary sensor | ESPHome node online/offline |
sensor.espar_node_wifi_signal |
Sensor | WiFi RSSI (dBm) |
button.restart_espar_controller |
Button | Remote OTA-safe restart |
LED colour codes
| Colour | Meaning |
|---|---|
| Dim green | Connected and idle |
| Amber | Heating active |
| Blue | Fan only |
| Red pulsing | Fault / error |
| Off | Not connected to heater |
CAN protocol summary
Full signal map: docs/protocol-reference.md
Bus speed: 500 kbps, standard 11-bit frames
| Direction | ID | Purpose |
|---|---|---|
| Heater → ESP32 | 0x2C4 |
Primary status (state, flame flag, counter) |
| Heater → ESP32 | 0x2C5 |
Secondary status |
| Heater → ESP32 | 0x2C6 |
Sensor / config data |
| Heater → ESP32 | 0x625 |
Heater heartbeat (~100ms) |
| ESP32 → Heater | 0x054 |
Primary command (HEAT / FAN / IDLE + setpoint) |
| ESP32 → Heater | 0x055–0x057 |
Static config frames (sent with every 0x054) |
| ESP32 → Heater | 0x60D |
Controller heartbeat (100ms) |
| ESP32 → Heater | 0x065 |
Periodic status burst |
| ESP32 → Heater | 0x05C–0x10A |
Init burst (sent once at startup) |
Temperature encoding: little-endian uint16 in 0x054 bytes D3/D4, units = 0.1°C per LSB.
Known issues
Boot sync (~20s delay)
The ESP32 must restart when heater power is applied to complete the CAN handshake. The simplest fix is a relay wired to heater power that triggers an ESP32 restart via a Home Assistant automation:
trigger:
- platform: state
entity_id: binary_sensor.heater_power # whatever detects your heater power-on
to: "on"
action:
- delay: 2s
- service: button.press
target:
entity_id: button.restart_espar_controller
Expect ~20s after power-on before the climate entity becomes active.
Heartbeat jitter
The heater occasionally gaps its 0x625 heartbeat by >5s. The component tolerates up to 10s before declaring a disconnect. Occasional "heartbeat lost" log messages at longer intervals are expected and self-recover automatically.
Fault codes not decoded
The 0x2C5 fault register was 0x00 in all captures (no fault induced during RE). Fault reporting is derived from behavioral detection — failed starts, heartbeat loss, lockout — not direct CAN fault code decoding.
Duration timer
Heater run duration is stored in the OEM controller, not on the CAN bus. There is no run-timer in this component; implement one in a Home Assistant automation if needed.
Contributing
Issues, fault frame captures, and PRs are all welcome. The highest-value contributions right now are fault frame captures (trigger a specific fault with SavvyCAN running and share the CSV) and testing on heater variants other than the S3 B2L Gasoline.
See CONTRIBUTING.md for details.
Built for the Trooper camper project — @TrooperDuper. If this saves you from buying an Espar dealer cable, consider dropping a
on the repo.