A thermostat for a heat lamp sauna

My coach at the jiu-jitsu gym built a near-infrared sauna: a small room and six heat lamp bulbs. It worked, with two problems. There was no way to set a temperature, and nothing stopped it from getting hotter than anyone wanted. Heat lamp bulbs are either on or off, so holding a temperature means switching banks of them in and out, and that is what this controller does. It shows the temperature on a small screen, takes a setpoint from a knob or a phone, and shuts the heat off above it.

Hardware

Schematic: ESP32 with DS18B20, OLED, rotary encoder, and a three-channel relay module switching three pairs of heat lamp bulbs on 120 V

  • ESP32 dev board.
  • DS18B20 temperature probe on its adapter board, which carries the 1-Wire pull-up.
  • 128 by 64 OLED over I2C.
  • Rotary encoder with a push button. Turn to set the temperature, press to confirm.
  • Three single-channel relay modules, each breaking the hot line to a pair of bulbs, for six bulbs in three banks. Neutral runs straight to the bulbs.

The relays are arranged in a line, and the firmware knows which banks are neighbors. That matters below.

Control

There is no PID. Bulbs cannot be dimmed, so the control is a staged thermostat with a deadband:

Temperature relative to setpoint Banks on
more than 2°F above none
within 2°F either side hold whatever is on
2 to 10°F below one
10 to 15°F below two
15°F or more below three

The sensor is read every 30 seconds and the control step runs every 30 seconds, which is fast enough for a room that takes half an hour to heat and slow enough that the relays are not chattering.

Rules that protect the bulbs

Heat lamp bulbs die from being switched, not from being on. So the interesting code is in deciding which bank to switch, not how many:

  • Minimum on and off time of 15 seconds. A bank that just changed state is not eligible to change again, whichever direction the thermostat wants.
  • Turn on the bank that has rested longest, with a strong penalty for a bank whose neighbor is already on. Spreading active banks across the room evens out the heat and stops one corner from cooking.
  • Wear leveling. Each bank’s cycle count and total runtime count against it when choosing which to turn on next, so the bulbs age together instead of one bank doing all the work.
  • Last on, first off. When the room reaches temperature, the bank that was lit most recently is the one switched off, so a bank that just started never gets a short pulse.

Cycle counts and runtime are saved to flash every ten cycles and shown on a stats screen, so I know when a bank is due for new bulbs before it fails.

Interfaces

The OLED shows current temperature, setpoint, and how many banks are heating. Pressing the encoder enters set mode, where the setpoint flashes while you turn the knob, and the screen returns to normal after ten seconds without input. A small web server on the ESP exposes status, setpoint, thermostat on and off, a manual mode for testing each relay, and the thresholds, so everything can also be done from a phone on the same network. Firmware updates go over the air.

There is also an ESPHome rewrite of the same controller, which exposes it to Home Assistant as a climate entity and reuses the same pins. Both versions, the wiring diagram, and the build notes are in the repository.

What I would change

The controller stops the sauna from overshooting the setpoint, which was the original problem. It does not yet protect against its own failure: if the temperature probe disconnects, the firmware keeps heating on the last good reading. A sauna with a dead sensor should fail off, not on. That is the first thing the next version fixes, along with a hard ceiling that cuts every relay regardless of setpoint, and a watchdog on the sensor read so a silent probe counts as a fault.

Tagged esp32, arduino, esphome, sensors, sauna.