Espar Airtronic S3 — CAN Bus Climate Integration

GitHub: GitHub - TrooperDuper/espar-airtronic-esphome: CAN bus reverse engineering & ESPHome controller for the Espar Airtronic S3 B2L gasoline heater · GitHub


:warning: 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

:white_check_mark: Full HEAT / FAN ONLY / OFF control from Home Assistant climate entity
:white_check_mark: Real-time heater state (STARTUP / HEATING / FAN / IDLE)
:white_check_mark: Flame confirmation sensor
:white_check_mark: Temperature setpoint encoding confirmed at 66°F, 75°F, 78°F, and 80°F
:white_check_mark: Behavioral fault detection (failed starts, heartbeat loss, lockout)
:white_check_mark: WS2812 RGB LED status indicator
:white_check_mark: ESPHome external component — drop-in, no custom firmware needed

:warning: Fault frame CAN IDs not yet decoded (P-codes known from service manual; capture methodology documented in docs/fault-codes.md)
:warning: Only tested on Airtronic S3 B2L Gasoline 12V — diesel and other variants untested
:warning: 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°C as 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 :star: on the repo.