A compact Wi-Fi-enabled controller for French fil pilote electric radiators, built around an ESP32-C3 module.
The system uses optotriacs and simple transistor drivers to generate the four standard fil pilote modes, and communicates via MQTT for seamless integration with Home Assistant.
- Controls 4 radiators, each supporting the 4 official fil
pilote modes:
- Confort (no signal)
- Eco (negative half-wave)
- Hors-gel / Frost protection (positive half-wave)
- Stop (full wave)
- Uses 2 MOC3021 optotriacs per radiator (8 total)
- Uses optoisolation to ensure safe AC control
- Uses 2N2222 transistor to limit the current steered from GPIOs
- On-board status LEDs for each half-wave
- MQTT-based command interface
- Compact hardware using an ESP32-C3 module
French electric radiators accept control commands via a dedicated wire called the fil pilote. By injecting specific AC waveforms, the radiator switches modes:
Confort: No signal
Eco: Negative half-wave
Hors-gel: Positive half-wave
Stop: Full-wave (both half-waves)
- 1 ESP32-C3 microcontroller board
- 8 MOC3021 optotriacs
- 8 Diodes for AC steering
- 8 Status LEDs
- 8 2N2222 NPN transistors
- Resistors:
- 120 Ω for the MOC3021 LED drive
- 10k Ω resistors for the 2N2222 base
- 1k - 4.7k (depends on LED color) LED resistors for the status indicators
- Each radiator needs two MOC3021s (one for each half-wave).
- A correctly oriented diode allows the MOC to pass either the positive or negative AC half-cycle.
- The ESP32-C3 GPIO activates a transistor, which drives the MOC LED (and indicator LED).
- The radiator receives the correct waveform depending on which MOCs are activated.
Safety note: You are working with 230 V AC. Ensure correct isolation, board spacing, and a safe enclosure.
Note: The topics and messages can be modified through esp config (see below).
The firmware listens to four MQTT commands:
- `confort`
- `eco`
- `hors-gel`
- `stop`
Each radiator listens on:
<base_topic>/<id>/set
ex:
heater-control/1/set
heater-control/2/set
heater-control/3/set
heater-control/4/set
You must have the ESP-IDF toolchain installed. (Installation instructions will be added later.)
Before building the project, activate ESP-IDF. (Steps will be documented later.)
From the project directory:
idf.py set-target esp32-c3This generates a sdkconfig file.
idf.py menuconfigNavigate to:
Component config →
Heater Control →
Configure:
- Wi-Fi SSID
- Wi-Fi password
- GPIO assignments
- MQTT server settings
Save and exit.
idf.py build flash monitorCreate an input select helper called input_select.heater_control, with 4 options (CONFORT, ECO, HORS_GEL, STOP).
Create an automation that triggers the MQTT messages:
alias: MQTT Heater Control
description: Publish state changes of any heater
triggers:
- entity_id:
- input_select.living_room_heater
- input_select.bedroom_heater
- input_select.office_heater
- input_select.bathroom_heater
trigger: state
actions:
- data:
topic: |
heater_control/{{ trigger.entity_id.split('.')[-1] }}
payload: |
{{ trigger.to_state.state | lower }}
action: mqtt.publishnotes:
Create a card that will display 4 buttons, which will trigger the input select change, which will trigger the automation.
Notes:
- This example uses
custom:button-cardwhich must be downloaded first. If you want a simpler option, you can create a simple dropdown.
First, we need to create the template in Dashboard UI. This prevent a lot of duplication, as we will need it for each radiator. Go to your dashboard, the click Edit > Raw Configuration Editor. Paste the following snippet at the top. Modify the colors and style according to your needs.
button_card_templates:
heater_mode_button_base:
show_name: false
show_state: false
styles:
card:
- padding: 8px
- height: 48px
- border-radius: 12px
icon:
- width: 26px
- height: 26px
tap_action:
action: call-service
service: input_select.select_option
heater_btn_confort:
template: heater_mode_button_base
icon: mdi:radiator
variables:
mode_name: CONFORT
styles:
icon:
- color: var(--orange-color)
state:
- value: CONFORT
styles:
card:
- background-color: var(--orange-color)
icon:
- color: white
heater_btn_eco:
template: heater_mode_button_base
icon: mdi:power-sleep
variables:
mode_name: ECO
styles:
icon:
- color: var(--blue-color)
state:
- value: ECO
styles:
card:
- background-color: var(--blue-color)
icon:
- color: white
heater_btn_hgel:
template: heater_mode_button_base
icon: mdi:weather-hail
variables:
mode_name: HORS_GEL
styles:
icon:
- color: var(--light-blue-color)
state:
- value: HORS_GEL
styles:
card:
- background-color: var(--light-blue-color)
icon:
- color: white
heater_btn_stop:
template: heater_mode_button_base
icon: mdi:radiator-off
variables:
mode_name: STOP
styles:
icon:
- color: var(--grey-color)
state:
- value: STOP
styles:
card:
- background-color: var(--grey-color)
icon:
- color: whiteThen we will create the cards.
type: vertical-stack
cards:
- type: tile
entity: input_select.bedroom_heater
features_position: bottom
vertical: false
color: deep-orange
name: Heater
hide_state: false
state_content:
- state
- last_changed
grid_options:
columns: full
tap_action:
action: none
icon_tap_action:
action: more-info
- type: horizontal-stack
cards:
- type: custom:button-card
template: heater_btn_confort
entity: input_select.bedroom_heater
tap_action:
action: call-service
service: input_select.select_option
target:
entity_id: input_select.bedroom_heater
data:
option: CONFORT
- type: custom:button-card
template: heater_btn_eco
entity: input_select.bedroom_heater
tap_action:
action: call-service
service: input_select.select_option
target:
entity_id: input_select.bedroom_heater
data:
option: ECO
- type: custom:button-card
template: heater_btn_hgel
entity: input_select.bedroom_heater
tap_action:
action: call-service
service: input_select.select_option
target:
entity_id: input_select.bedroom_heater
data:
option: HORS_GEL
- type: custom:button-card
template: heater_btn_stop
entity: input_select.bedroom_heater
tap_action:
action: call-service
service: input_select.select_option
target:
entity_id: input_select.bedroom_heater
data:
option: STOP



