A heating automation in Home Assistant, or its Generic Thermostat integration, makes every on/off decision on the server. When Home Assistant restarts for an update, or Wi-Fi between it and the relay drops, nothing is in charge of the heater. ESPHome's thermostat climate platform moves the control loop onto the ESP32 itself. The board reads the temperature, switches the relay and remembers its settings. Home Assistant only shows a thermostat card where you change the target, the mode or the preset.
The example below uses an ESP32, a DS18B20 temperature sensor and one relay for heating only. The same platform handles cooling, fans and two set points. The configuration grows, but the principles stay the same.
Before you wire anything
The relay contacts carry whatever you connect to them. If the heat source has terminals for an on/off room thermostat, check in its manual what kind of contact they expect. Work on mains wiring belongs to someone qualified to do it. Check the relay's rated current against the load, not just the voltage printed on the module.
Read the heater's manual too. The IEC 60335-2-30 safety standard for room heaters requires the instructions for visibly glowing radiant heaters (other than those mounted high on a wall) to warn against using them with a timer, a remote control system or any other device that switches the heater on automatically, because of the fire risk when the heater is covered or badly placed. An ESP32 with a relay is exactly that kind of device.
The sensor: DS18B20 on a 1-Wire bus
The DS18B20 needs a pull-up resistor of about 4.7 kΩ between 3.3 V and the data line, and a GPIO pin that works as both input and output. In ESPHome you first define the bus, then the sensor:
one_wire:
- platform: gpio
pin: GPIO4
sensor:
- platform: dallas_temp
id: room_temperature
name: "Room temperature"
update_interval: 30s
filters:
- timeout: 2min
The pin numbers in this article are examples, so use the ones that match your wiring. By default the sensor reads every 60 seconds at 12-bit resolution. Thirty seconds is plenty for a room.
The timeout filter matters more than it looks. When a reading fails, the dallas_temp component already publishes NaN (not a number) instead of a value. The filter covers the other case, when nothing is published at all: after 2 minutes without a new value it sends NaN. That makes a broken sensor visible to the thermostat, and a later section explains why that is what you want.
You can feed the thermostat a temperature from Home Assistant instead, with a homeassistant sensor. That brings back the dependency you are trying to remove, so if you do it, keep the timeout filter.
The relay: off at boot and hidden from Home Assistant
switch:
- platform: gpio
id: heater_relay
pin: GPIO26
restore_mode: ALWAYS_OFF
internal: true
ALWAYS_OFF is the default for switches, but writing it down makes the intent clear: after a reboot the relay starts open, and the thermostat decides from there. internal: true keeps the switch out of Home Assistant, so nobody can turn the heater on directly and bypass the thermostat. If the relay module switches on when its input is low, add inverted: true under pin:.
One more thing to test on the bench: the ESPHome documentation warns that at reset, before any ESPHome code runs, GPIO pins may have pull-ups active, so a relay can switch on briefly before ESPHome turns it off. Power-cycle the board a few times and listen for the relay clicking.
The thermostat
climate:
- platform: thermostat
id: room_thermostat
name: "Living room thermostat"
sensor: room_temperature
heat_deadband: 0.3
heat_overrun: 0.3
min_heating_run_time: 300s
min_heating_off_time: 300s
min_idle_time: 30s
startup_delay: true
heat_action:
- switch.turn_on: heater_relay
idle_action:
- switch.turn_off: heater_relay
visual:
min_temperature: 5
max_temperature: 25
temperature_step: 0.5
default_preset: Home
on_boot_restore_from: memory
preset:
- name: Home
default_target_temperature_low: 21
mode: HEAT
- name: Eco
default_target_temperature_low: 18
- name: Away
default_target_temperature_low: 12
Only heat_action is defined, so the thermostat runs in single-point mode and Home Assistant shows a card with one set point. Going through the rest:
Hysteresis. heat_deadband says how far below the set point the temperature may fall before heating starts, and heat_overrun how far above it heating continues. Both default to 0.5 °C. With 21 °C and 0.3 for both, the relay closes at 20.7 °C and opens at 21.3 °C. Smaller values hold the temperature tighter at the cost of more switching.
Minimum times. min_heating_run_time and min_heating_off_time are required whenever heat_action is set, and min_idle_time is always required. They stop the relay from chattering when the reading hovers around a threshold. Five minutes each is the value the ESPHome examples use. A boiler or heat pump may come with its own recommendation for minimum cycle length.
Startup delay. With startup_delay: true, the minimum off time has to pass after the ESP32 starts before heating may switch on. A power cut that flickers on and off then won't switch the heat source on and off at the same rhythm.
The temperature range. For the thermostat platform, min_temperature and max_temperature under visual are more than cosmetics: they limit the set points it accepts. A target sent from an automation or the card is clamped to 5-25 °C.
Presets and memory. Home, Eco and Away are standard preset names. Any other name becomes a custom preset, which works the same way. on_boot_restore_from: memory is the default: after a reboot the thermostat comes back with the mode and targets it had. The default_preset is used only when no saved settings can be read, such as on the very first boot.
A common mistake is to put the relay into heat_mode instead of heat_action. The *_mode actions run when the user changes the mode, for example to light an LED. The documentation says explicitly that they are not for switching heating or cooling equipment.
What happens when the sensor or the network fails
ESPHome's documentation doesn't spell out the sensor-failure case, but the source code of the thermostat does. When the current temperature is NaN, the controller switches its action to off and calls idle_action, which opens the relay, as soon as the minimum run time allows. With the timeout filter above, a disconnected or silent sensor ends in the heater being switched off, not left running.
The network is the other half. By default the native API reboots the ESP32 if no client has connected for 15 minutes, because the ESP sometimes reports a connection that isn't really working. For a thermostat, that means a reboot every quarter of an hour while Home Assistant is down. Set:
api:
reboot_timeout: 0s
Leave the Wi-Fi reboot_timeout at its default of 15 minutes. The documentation warns that the IP stack sometimes needs a full reboot to recover. After such a reboot the relay starts open, the thermostat restores its settings, and startup_delay holds the heating off for the minimum off time before it resumes.
An extra limit for overheating
The thermostat only knows the set point. If you want a software backstop on top of the heater's own thermal protection, switch the thermostat off when the room gets unreasonably hot:
sensor:
- platform: dallas_temp
# ... as above
on_value_range:
- above: 28
then:
- climate.control:
id: room_thermostat
mode: "OFF"
on_value_range fires once, when the value crosses into the range. It won't keep switching the thermostat off, and it won't switch it back on. You decide when to turn it on again. It still runs on the same board and the same sensor, so it does not replace a thermal cut-out in the heater.
In Home Assistant
Once the board is added to Home Assistant, the ESPHome integration creates one climate entity. The temperature sensor appears as a separate entity, and the relay doesn't appear at all, because it is internal. Use the thermostat card to set the target and switch presets. A schedule then becomes a pair of automations calling climate.set_preset_mode, for example Eco at 23:00 and Home at 6:00. If Home Assistant is down at 6:00, the thermostat keeps the Eco preset and goes on regulating to 18 °C. Nothing switches off, the change just comes later.
Place the sensor where it measures the room, not the heater: away from the radiator, out of direct sun and not inside the same box as the relay and the ESP32, which both warm up.