ESP32-based smart heat pump controller split across two boards communicating wirelessly via ESP-NOW (~500µs latency, no router needed).
Full replacement for a heat pump control board with Home Assistant integration, LCD menu, and complete control logic.
Caution
This project involves direct connection to 230V AC mains electricity. Improper handling can cause electric shock, fire, or death.
- Only qualified and licensed electricians should install or service this system.
- Disconnect and lock out mains power before opening the heat pump enclosure or touching any wiring.
- The relay outputs switch live 230V loads — compressor (30A), fan, valve, and pump.
- Do not operate with the enclosure open or with exposed conductors.
- Verify local electrical codes and regulations before installation.
If you are not trained and certified to work with high-voltage systems — do not attempt this project.
┌─────────────────────────────┐ ESP-NOW (2.4GHz) ┌─────────────────────────────┐
│ CONTROLLER │◄────────────────────────────────►│ REMOTE │
│ (Outdoor unit) │ │ (Indoor unit) │
│ │ StatusPacket every 500ms ──► │ │
│ • 4× NTC temperature │ ◄── CommandPacket (on action) │ • ST7567 128×64 LCD │
│ sensors │ ◄── SettingsPacket (on change) │ • 5-button navigation │
│ • 4× relay outputs │ │ • WiFi + MQTT │
│ • Control state machine │ │ • Home Assistant │
│ • No WiFi / no display │ │ • Full menu system │
└─────────────────────────────┘ └─────────────────────────────┘
| Controller | Remote | |
|---|---|---|
| Role | Real hardware control | UI + cloud connectivity |
| Works without the other? | ✅ Yes — keeps running autonomously | ✅ Yes — shows last known state |
| WiFi | ✗ Not needed | ✅ Connects to router via WiFiManager |
| Settings stored | Receives from Remote, saves to NVS | Master copy in NVS, synced on every save |
| MQTT / HA | ✗ | ✅ Full Discovery |
Early bench testing of the LCD menu system before any hardware was assembled.
First time both boards were connected together and communicating over ESP-NOW.
Building the final perfboard PCBs for both units.
Installing the Controller into the outdoor unit and connecting all sensors and loads.
The Remote connects to Home Assistant via MQTT with full auto-discovery — no manual entity configuration needed.
The Remote unit installed indoors, fully operational.
- Full heat pump control logic (startup/shutdown sequences, auto defrost, anti-freeze, exhaust protection)
- Reads 4× NTC 10K/3470 temperature sensors
- Drives 4× relays (compressor, fan, 4-way valve, water pump)
- Sends full status to Remote every 500 ms via ESP-NOW
- Receives and applies settings/commands from Remote instantly
- Works fully standalone — no Remote connection required
- 128×64 ST7567 LCD with full menu system
- 5-button navigation (up, down, enter, back, power)
- WiFi via WiFiManager (captive portal on first boot)
- Home Assistant integration via MQTT Discovery
- Backlight PWM with auto-dim after inactivity
- All 12 parameters configurable from on-device menu (U01–U81)
- SAVE mode with up to 3 scheduled heating periods
- Persistent settings in NVS — survive power loss, synced to Controller on boot
- NTP time sync when WiFi connected
- MQTT broker configurable from browser (WiFiManager portal includes MQTT fields)
| Packet | Direction | Interval | Description |
|---|---|---|---|
StatusPacket |
Controller → Remote | Every 500ms | Full state: relays, sensors, state machine |
CommandPacket |
Remote → Controller | On user action | SET_ONLINE or FORCE_DEFROST |
SettingsPacket |
Remote → Controller | On save + every 30s | Full settings sync |
#define CTRL_MAC_BYTES { 0x00, 0x70, 0x07, 0x25, 0x08, 0x74 }
#define REMOTE_MAC_BYTES { 0x00, 0x70, 0x07, 0x25, 0x70, 0xF0 }
#define ESPNOW_CHANNEL 1 // must match router WiFi channelImportant:
ESPNOW_CHANNELmust match the channel your router uses. The Remote prints its WiFi channel on startup via Serial. Updateshared.hif needed.
| Component | Details |
|---|---|
| MCU | ESP32 DOIT DevKit V1 |
| Sensors | 4× NTC 10K/3470 |
| Relays | 4× relay module (Active LOW) |
| Sensor | GPIO |
|---|---|
| Water tank | GPIO 36 |
| Pipe coil | GPIO 39 |
| Compressor exhaust | GPIO 34 |
| Outdoor ambient | GPIO 35 |
GPIO 34, 35, 36, 39 are input-only — external 10KΩ pull-up to 3.3V required.
| Relay | GPIO | Load |
|---|---|---|
| Compressor | GPIO 26 | 30A |
| Outdoor Fan | GPIO 27 | 3A |
| 4-way Valve | GPIO 14 | 3A |
| Water Pump | GPIO 33 | 3A |
| Component | Details |
|---|---|
| MCU | ESP32 DOIT DevKit V1 |
| Display | ST7567 128×64 LCD |
| Buttons | 5× momentary push button |
| LCD Pin | GPIO | Note |
|---|---|---|
| CS | GPIO 0 | Chip Select |
| RST | GPIO 16 | Reset |
| DC | GPIO 2 | Data/Command |
| SCK | GPIO 18 | SPI Clock (VSPI) |
| SDI | GPIO 23 | SPI Data (VSPI) |
| LED | GPIO 32 | Backlight PWM |
| VCC | 3.3V | |
| GND | GND |
| Button | GPIO |
|---|---|
| POWER (long press = on/off) | GPIO 25 |
| UP | GPIO 19 |
| DOWN | GPIO 22 |
| ENTER | GPIO 21 |
| BACK | GPIO 13 |
src/
├── shared.h ESP-NOW packet structs + MAC addresses (shared)
│
├── main_ctrl.cpp Controller entry point
├── espnow_ctrl.h/cpp Controller ESP-NOW (send status, receive cmd/settings)
├── control.h/cpp Heat pump state machine
├── sensors.h/cpp NTC temperature reading
├── relays.h/cpp Relay output control
├── settings.h/cpp NVS persistent storage + dirty flag
│
├── main_remote.cpp Remote entry point
├── espnow_remote.h/cpp Remote ESP-NOW (receive status, send cmd/settings)
├── control_remote.cpp Proxy control.h — returns cached ESP-NOW data
├── sensors_remote.cpp Proxy sensors.h — returns cached ESP-NOW data
├── display.h/cpp ST7567 LCD (U8g2)
├── buttons.h/cpp Debounced button input
├── menu.h/cpp Full LCD menu system
├── wifi_mqtt.h/cpp WiFi + MQTT + HA Discovery
└── config.h Pin definitions & constants
# Flash Controller (outdoor ESP32)
pio run -e controller --target upload
# Flash Remote (indoor ESP32)
pio run -e remote --target uploadlib_deps =
olikraus/U8g2@^2.35.19
knolleary/PubSubClient@^2.8
bblanchon/ArduinoJson@^6.21.5
tzapu/WiFiManager@^2.0.17- Power on → ESP32 creates WiFi AP:
HeatPump-Setup - Connect with phone/laptop → browser opens automatically
- Enter WiFi credentials + MQTT broker details (host, port, user, pass)
- ESP32 connects, gets NTP time, starts sending settings to Controller via ESP-NOW
- Power on → connects to Remote via ESP-NOW automatically
- Uses last saved settings from NVS until Remote syncs
- Starts control loop immediately — no WiFi needed
Auto-discovers via MQTT. All entities appear automatically after connecting to the broker.
| Entity | Type | Description |
|---|---|---|
| Heat Pump | climate |
On/off + setpoint + current temp |
| HP Water Temp | sensor |
Water tank temperature |
| HP Pipe Temp | sensor |
Pipe coil temperature |
| HP Exhaust Temp | sensor |
Compressor exhaust temperature |
| HP Outdoor Temp | sensor |
Outdoor temperature |
| HP Compressor | binary_sensor |
Compressor running state |
| HP Defrost | binary_sensor |
Defrost cycle active |
| Topic | Direction | Description |
|---|---|---|
heatpump/state |
Remote → HA | Full JSON state |
heatpump/cmd |
HA → Remote | Commands |
heatpump/available |
Remote → HA | online / offline (LWT) |
Command examples:
{"mode": "heat"}
{"mode": "off"}
{"setpoint": 45.0}
{"work_mode": "save"}
{"force_defrost": true}Runs on the Controller board.
PUMP ON → 30s → FAN ON + VALVE → 30s → COMPRESSOR ON
COMPRESSOR OFF → 30s → FAN OFF + VALVE → 30s → PUMP OFF
- Triggers after compressor runs ≥ U30 min AND pipe temp ≤ U31°C
- Sequence: COMP+FAN off → 55s → valve reverses → 5s → COMP on
- Ends when pipe temp ≥ U32°C or max U33 min reached
- Compressor delay: 3-minute minimum off time before restart
- Exhaust protection: stops heating if exhaust > U51+10°C (U50)
- Anti-freeze: pump runs 3 min every 30 min when outdoor < 2°C (U52)
| Code | Function | Range | Default |
|---|---|---|---|
| U01 | Hysteresis | 2–10°C | 5°C |
| U02 | Water sensor calibration | ±9.9°C | 0.0°C |
| U30 | Defrost start time | 10–90 min | 45 min |
| U31 | Defrost start temperature | -20–0°C | -5°C |
| U32 | Defrost stop temperature | 2–30°C | 10°C |
| U33 | Max defrost time | 2–15 min | 8 min |
| U34 | Valve mode (0=heat-off, 1=heat-on) | 0/1 | 0 |
| U50 | Exhaust protection mode | 0–2 | 1 |
| U51 | Exhaust protection temp | 50–125°C | 110°C |
| U52 | Anti-freeze | 0/1 | 1 |
| U80 | Auto restart after power loss | 0/1 | 0 |
| U81 | Keypad lock | 0/1 | 0 |

















