Skip to content
Don't build this. It's a bad idea.

Smart Candle

An oil candle that lights itself when Home Assistant says so and blows itself out when it says otherwise. Two copper tubes arch over the wick and do both jobs: an arc jumps the gap between their tips to light it, and a diaphragm pump pushes air up one tube and pulls it down the other to snuff it. An ESP32 with a 1.14 in screen runs the whole thing off two relays and a pair of buttons.

This is mains-free, but it is not safe. It makes an open flame on a timer, in a room, from an API call. Every failure mode ends in fire. Build it on a hard surface, keep an extinguisher within reach, and never leave it armed in an empty house.

AI render of a living room on fire, curtains and sideboard alight, one small candle burning on the coffee table

That is a render, not a photograph, and not anybody’s living room. It is on the page because it is the honest picture of what this project’s failure mode looks like.

Parts

Some links below are Amazon affiliate links. As an Amazon Associate I earn from qualifying purchases — at no extra cost to you. It helps keep these guides free. Outside the US, they should send you to your own Amazon store; if one doesn't, searching the part number there will find it.

If you would rather run it off an 18650 than a pouch cell, the Samsung INR18650-35E 4-pack and a holder with flying leads go straight onto the TP4056 board above. That is what the first version of this ran on.

Choosing a candle

A wax candle is the wrong shape for automation and it is not close. The wick retreats down the wax as it burns, so the point you have to put an arc at moves several centimetres over the life of the candle. You would be automating a target that walks away from you.

An oil candle fixes that in one part. The wick is held at the top of a glass reservoir and stays exactly where it is until the oil runs out — the ignition point sits at a constant height above the base, which is what lets you build a rigid arch and forget about it. Refill it and the geometry is unchanged.

Use lamp oil sold as clean-burning and low-odour. This is a thing that lives on a table in a room somebody is sitting in.

Ignition

Three attempts, in order, and only the third one is a build.

Resistance wire. 32 AWG Kanthal A1 laid against the wick. It heats, and sometimes it lights, and then it oxidises and breaks. Inconsistent from cold, and it burns itself out fast enough that you would be rebuilding the igniter more often than you refill the oil.

A high-voltage generator module. The 400 kV and 700 kV step-up boards, and the 15 kV arc igniter coil. All three are loud, and all three destroy the wick on contact rather than lighting it. A stun-gun module and an arc lighter are not the same device wearing different cases: the lighter runs a much higher frequency at much lower power, which puts heat into the wick instead of blowing it apart.

An arc lighter. This is the one. It fires a stable, quiet arc, it strikes across a few millimetres of air, and it has an internal cell and a USB charger, so it needs nothing from the ESP32 except a relay across its own button.

Arc jumping between two copper tube tips above the wick of a glass oil candle, lighting it

Keep the high-voltage side electrically isolated from the control side. Opto- isolated relays are not optional here — with a direct connection, every arc strike resets the ESP32, and you get a board that reboots every single time it does its job.

Extinguishing

The pump has an inlet and an outlet, and the build uses both. One copper tube blows across the flame, the other one draws at it from the opposite side. Doing both at once puts a real crosswind through a very small volume, which is what gets a wick out in under a second rather than making it flare.

Those are the same two tubes the arc jumps between. That is the whole trick of this build, and it is why there is no separate igniter hardware anywhere on the base: the two copper arches are electrodes to the lighter and ducts to the pump, and each job is fine with the other being there.

The soft airline tubing does a second job as well. It is the only thing between the copper and the pump’s motor, so it is the insulation that keeps the arc from tracking down into the pump. Give it a decent length — 150 mm or so per side — rather than trimming it neat.

Printing the base

candle_legs_v2.stl is one part, 150 × 90 × 25 mm. It is a U — 5 mm-thick arms down each long side, joined by a 4 mm rail at the front with a generous sweep into each corner, open at the back so the candle goes in from behind and the wiring comes out the same way. The candle stands inside it on the table; the frame does not carry it.

The front 25 mm of the U is floored with a 2 mm panel, and that panel is the control surface. Three holes sit on one centreline, 12.5 mm back from the front edge: 16.5 mm in the middle, and 7 mm 30 mm either side of it. The 7 mm pair takes the mini momentary buttons the firmware reads; the middle hole is the arming switch.

There are no holes anywhere for the copper tubes, and that is deliberate — where the arch has to sit depends on the diameter of your candle glass, which is not a number that can be modelled once for everybody. Bend the tubes first, hold them where the arc gap wants to be, then drill the arms to match.

SettingValue
Layer height0.2 mm
Perimeters4
Infill25%
SupportsNone
MaterialPETG
OrientationFlat, as exported

Printed as exported the 2 mm floor is on the plate and nothing overhangs, so it needs no supports. Print it in PETG. PLA goes soft around 60 °C and this part exists to sit under an open flame for an hour at a time; a base that creeps is a base that moves the arc gap off the wick.

Ream the three holes after printing. Vertical holes in FDM always come out undersize, and a panel switch that needs persuading into a 2 mm shelf is a panel switch that cracks it.

Wiring

Every pin below comes out of bruh-candle.yaml, which is the authority.

FunctionGPIONotes
Relay 1 — pump15relay_1. “Blow Out” pulses it for 800 ms
Relay 2 — arc lighter12relay_2. “Make Fire” pulses it for 1666 ms
Blow button21switch_1, inverted. Runs the pump while held
Ignite button22switch_2, inverted. Fires the lighter while held
Display MOSI19SPI, shared bus
Display SCLK18SPI
Display CS5
Display DC16
Display RST23
Display backlight4Also exposed as a switch — see below
Onboard button 00Next display page
Onboard button 135Previous display page
Status LED2status_led; there is no LED there on every board

The two manual buttons are interlocked against the Home Assistant switches, not against each other. Pressing blow does nothing while a “Make Fire” pulse is running, and pressing ignite does nothing while a “Blow Out” pulse is running. Holding both buttons at once is not caught by anything, so don’t.

pin_adc_power (GPIO14) and pin_adc_in (GPIO34) are declared in the substitutions block and then never referenced. There is no battery sensor in this config — those two lines are a hook somebody left for one.

GPIO12 is a strapping pin. The ESP32 samples it at reset to choose the flash voltage, and if something holds it high at boot the chip comes up expecting 1.8 V flash and does not come up at all. That is the pin the arc lighter’s relay is on. Relay modules vary in what they do to their input pin when unpowered — if the board goes into a boot loop the moment you plug the relay in, that is what happened. Move the lighter to a free pin (GPIO13, GPIO26 and GPIO27 are all clear in this config) and change relay_2 in the substitutions to match.

The relay’s trigger polarity has to match the config. Both relays are plain switch: platform: gpio entries with no inverted:, so ESPHome turns a relay on by driving its pin high. That is a high-level-trigger board. A low-level board — like the common 2-channel 5 V opto module — is on when the pin is low, so with one of those the candle lights itself the instant the ESP32 boots and stops when you tell it to fire. If you use one, add inverted: true to both GPIO switch pins before you put any oil in the glass.

Assembly

  1. Print the base and ream the three holes. Fit the two momentary buttons and the arming switch dry, before anything is soldered.

  2. Cut and bend the copper. Two mirrored arches that rise from the base, come over the top of the glass and end with their tips facing each other a few millimetres apart, directly above the wick. Cut with the tube cutter, bend with the bender — kink the arch and you have blocked the air path you need later.

  3. Tap the lighter’s button. Open it, find the two contacts its own button bridges, and solder a pair of leads across them to the relay’s normally-open terminals. Sleeve them in adhesive-lined heat shrink. Bring the lighter’s two electrodes out to the bottom ends of the two copper arches.

  4. Set the gap over the wick. This is the only adjustment that matters. Too wide and the arc will not strike; too tight and it strikes but misses the wick. Get it lighting reliably by hand before you drill the base for the tubes.

  5. Push the airline tubing onto the copper tails, one side to the pump’s inlet and the other to its outlet, and leave the runs long.

  6. Wire the relays, the boost module and the buttons, flash the board, and confirm the entities appear in Home Assistant.

  7. Bench-test dry. Candle empty, no oil, nothing to light. Press each button and each Home Assistant switch and listen for the relay click and the pump. A wrong-polarity relay shows itself here and nowhere else.

  8. Fill it, arm it, and only then test ignition — hard surface, extinguisher at hand, nothing flammable within a metre.

Firmware

bruh-candle.yaml is in the download section below. It is one ESPHome file and it needs three things from you.

  1. secrets.yaml with wifi_ssid, wifi_password and ota_password.

  2. Roboto-Medium.ttf sitting next to the YAML. The font: block loads it by filename from the config directory at three sizes. Without that file ESPHome fails at the compile step, not at the flash step, which is a confusing place to find out.

  3. The substitutions block at the top, which is where every pin in the wiring table lives. Change a pin there and the whole config follows.

Flash it over USB the first time; every flash after that is over WiFi. Holding the board’s GPIO0 button while you reset it is the way into download mode if the USB port comes up mute.

GPIO4 is claimed twice. It appears as the backlight GPIO switch and again as the display’s backlight_pin. Current ESPHome refuses to validate a pin used by two components unless both carry allow_other_uses: true. Add that to both, or delete the switch and let the display own the pin — you lose a backlight entity and nothing else.

What you get in Home Assistant

Two template switches, Make Fire and Blow Out, that pulse their relay and then turn themselves off, so they behave like buttons even though they show up as switches. The two raw relays are exposed too, which is useful for testing and is also a way to leave an arc lighter running indefinitely, so treat them as diagnostics. Beyond that: the two physical buttons as binary sensors, a backlight switch, a restart switch, WiFi signal every 5 minutes and a connection status sensor.

The display never changes itself. There are three pages in the config — “Ready” on black, “FIRE!” on red, “WOOSH!” on blue — and the only thing that moves between them is the two onboard buttons. Nothing in the ignite or blow actions touches the display. If you want the screen to actually report what the candle is doing, add a display.page.show: line to each template switch’s turn_on_action and another back to page1 after the delay. It is four lines and it is the first edit worth making.

Safety

There is a physical switch between the battery and the ESP32, and it is the most important part on the page. The candle has to be armed by hand before Home Assistant can do anything to it, which means a mistyped automation, a stray service call or a compromised API cannot light a flame in an empty room. The firmware cannot enforce that, and no amount of YAML replaces a broken circuit.

Do not put this on a schedule. Do not wire it to presence detection. Do not give it a voice assistant. Light it when you are in the room and snuff it when you leave, and the whole thing is a party trick rather than a hazard.

Files & downloads

Printable parts

Loading 3D model…
Drag to rotate · Scroll to zoom · Right-click to pan

Parts marked from CAD are tessellated from the STEP files below, for previewing only — approximate where the STEP is exact. Download the STEP for the real model.

ESPHome configuration

Copy this into your ESPHome directory and adjust the substitutions at the top. Secrets are referenced by name — see secrets.yaml.example .

bruh-candle.yaml 223 lines
bruh-candle.yaml
# BRUH Smart Candle
# ────────────────────────────────────────────────────────────
# Auto-igniting and auto-extinguishing smart candle controller.
# Features arc lighter ignition, diaphragm pump for extinguishing,
# ST7789V TFT display, and multiple relay-controlled mechanisms.
#
# Hardware:
# - Board: ESP32 Feather
# - Display: ST7789V TFT 320x240
# - Output: 2 relays (igniter and pump)
# - Controls: Manual buttons and capacitive touch sensor
# ────────────────────────────────────────────────────────────
substitutions:
device_name: "BRUH Candle"
device_id: "bruh-candle"
switch_1: GPIO21
switch_2: GPIO22
relay_1: GPIO15
relay_2: GPIO12
pin_tft_mosi: GPIO19
pin_tft_sclk: GPIO18
pin_tft_cs: GPIO5
pin_tft_dc: GPIO16
pin_tft_rst: GPIO23
pin_tfl_bl: GPIO4
pin_button_1: GPIO35
pin_button_0: GPIO0
pin_adc_power: GPIO14
pin_adc_in: GPIO34
esphome:
name: ${device_id}
esp32:
board: featheresp32
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ota:
password: !secret ota_password
platform: esphome
api:
status_led:
pin:
number: GPIO2
logger:
switch:
- platform: gpio
pin: ${relay_1}
id: relay_1
name: "${device_name} Relay 1"
- platform: gpio
pin: ${relay_2}
id: relay_2
name: "${device_name} Relay 2"
- platform: template
name: "${device_name} Make Fire"
id: make_fire
optimistic: true
turn_on_action:
- switch.turn_on: relay_2
- delay: 1666ms
- switch.turn_off: relay_2
- switch.turn_off: make_fire
- platform: template
name: "${device_name} Blow Out"
id: blow_out
optimistic: true
turn_on_action:
- switch.turn_on: relay_1
- delay: 800ms
- switch.turn_off: relay_1
- switch.turn_off: blow_out
- platform: gpio
pin: ${pin_tfl_bl}
name: "${device_name} Backlight"
id: backlight
- platform: restart
name: "${device_name} Restart"
sensor:
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 300s
binary_sensor:
- platform: gpio
pin:
number: ${switch_1}
inverted: yes
id: switch_1
name: "${device_name} Switch 1"
on_press:
if:
condition:
- switch.is_off: make_fire
then:
- switch.turn_on: relay_1
on_release:
if:
condition:
- switch.is_off: make_fire
then:
- switch.turn_off: relay_1
- platform: gpio
pin:
number: ${switch_2}
inverted: yes
id: switch_2
name: "${device_name} Switch 2"
on_press:
if:
condition:
- switch.is_off: blow_out
then:
- switch.turn_on: relay_2
on_release:
if:
condition:
- switch.is_off: blow_out
then:
- switch.turn_off: relay_2
- platform: gpio
pin:
number: ${pin_button_0}
inverted: true
mode: INPUT_PULLUP
name: "${device_name} T-Display Button Input 0"
id: tdisplay_button_input_0
on_press:
then:
- display.page.show_next: my_display
- component.update: my_display
- platform: gpio
pin:
number: ${pin_button_1}
inverted: true
name: "${device_name} T-Display Button Input 1"
id: tdisplay_button_input_1
on_press:
then:
- display.page.show_previous: my_display
- component.update: my_display
- platform: status
name: "${device_name} Status"
spi:
clk_pin: ${pin_tft_sclk}
mosi_pin: ${pin_tft_mosi}
color:
- id: my_red
red: 100%
green: 0%
blue: 0%
- id: my_yellow
red: 100%
green: 100%
blue: 0%
- id: my_green
red: 0%
green: 100%
blue: 0%
- id: my_blue
red: 0%
green: 0%
blue: 100%
- id: my_gray
red: 50%
green: 50%
blue: 50%
- id: my_black
red: 0%
green: 0%
blue: 0%
font:
- file: "Roboto-Medium.ttf"
id: roboto_48
size: 48
- file: "Roboto-Medium.ttf"
id: roboto_24
size: 24
- file: "Roboto-Medium.ttf"
id: roboto_12
size: 12
display:
- platform: st7789v
id: my_display
backlight_pin: ${pin_tfl_bl}
cs_pin: ${pin_tft_cs}
dc_pin: ${pin_tft_dc}
reset_pin: ${pin_tft_rst}
rotation: 270
pages:
- id: page1
lambda: |-
it.fill(my_black);
it.print(45, 40, id(roboto_48), "Ready");
- id: page2
lambda: |-
it.fill(my_red);
it.print(55, 40, id(roboto_48), "FIRE!");
- id: page3
lambda: |-
it.fill(my_blue);
it.print(35, 40, id(roboto_48), "WOOSH!");

View on GitHub · Download raw

CAD source

The design itself, if you want to change it rather than print it. A STEP file is exact geometry and opens in almost anything; a Fusion 360 archive keeps the modelling history, so you can go back and edit the sketch that made the part.

Every file for this project on GitHub →