Generic Thermostat, a Relay, and On/Off Control Without the Chaos
Generic Thermostat, a Relay, and On/Off Control Without the Chaos
In Lesson 5 you controlled a TRV head through a climate entity. Now you're building your own virtual thermostat: a temperature sensor plus a heating switch. This is the on/off route from Lesson 3: but first on a low-risk load, not on the main gas boiler.
The generic_thermostat integration in HA pairs a temperature sensor with a switch (switch) and creates a climate entity. It decides on its own when to turn heating on and off: with hysteresis and a minimum cycle duration. It doesn't replace the boiler's documentation or the OpenTherm integration from Lesson 7.
Plan on about 110 to 120 minutes. From here on, I'll shorten Home Assistant to HA.
A reminder from Lesson 5
A TRV head regulates a room: not always the boiler.
Generic Thermostat controls the switch you wired up. If that's the main gas boiler with no documented control input: this lesson isn't for you at the start. The comfort_automation_enabled helper can still control the target temperature on a climate entity, but first configure and test the virtual thermostat itself.
A real-life problem: the relay clicks every two minutes
You wired a Wi-Fi relay to the electric heater in the office and wrote an automation: if sensor.office_temperature drops below 68°F, turn on switch.office_heater. Five minutes later, the relay is switching on and off every few seconds. You hear it clicking, the temperature jumps around, and you don't know if it's a sensor glitch or logic that's too tight with no hysteresis.
Or worse: you wired a relay to the boiler input "because someone did it on YouTube." The boiler started cycling on and off at the wrong rhythm. Modulation disappeared, and the installer says that was never part of the design.
Generic Thermostat solves the first problem: it gives you ready-made thermostat logic with hysteresis and a minimum cycle time. It doesn't solve the second problem: you still don't wire up the main boiler without the map from Lesson 3 and the documentation.
What's actually the risk
Generic Thermostat on the main boiler: a plain on/off on a modulating device.
No min_cycle_duration: the switch turns on and off too often.
Duplicate logic: generic_thermostat and a separate automation both control the same switch.
A sensor failure with no alarm: the thermostat runs on the last known value or stops reacting: and you don't know.
The wrong sensor: a sensor above the radiator instead of where you actually measure comfort.
What generic_thermostat is
Generic Thermostat is a ready-made software thermostat in HA, available both as a helper and as YAML configuration. It isn't a separate device on the wall. HA takes the temperature from a chosen sensor, controls a specified switch, and creates a climate entity. It needs two pieces:
target_sensor: the entity with the room temperature, e.g. sensor.office_temperature.
heater: the heating switch, e.g. switch.office_heater (it has to be a toggle-type entity).
In heating mode: when the sensor's temperature drops below the threshold (target temperature minus tolerance), HA turns on the switch. When it reaches the upper threshold: it turns it off. This creates a climate entity, e.g. climate.office_thermostat. One generic_thermostat instance controls one switch.
You can add the helper through Settings → Devices & Services → Helpers → Create Helper → Generic Thermostat: that's a fast start. For the fuller set of parameters (hysteresis, min_cycle_duration), set them in YAML in configuration.yaml or in a package file: an example for a test heater follows below.
What to test on: before you touch the boiler
From Lessons 2 and 3: safe test loads include a heating mat in the office, a small electric heater on wheels, a heat lamp in a shed: devices where an HA failure won't threaten the gas installation. Don't start with the boiler's control input without a diagram from your installer.
| Load | Sensible at the start | Note |
|---|---|---|
| A test electric heater | Yes | Fast response: good for learning hysteresis |
| A desk heating mat | Yes | Low power, low risk |
| A bathroom fan | Not in this lesson | That's Lesson 8: different logic |
| The main gas boiler | Not at the start | Modulation, OpenTherm, Lesson 7 |
An electrical note
An electric heater, a heating mat, and other resistive loads draw real power. Before testing, check the device's wattage, the maximum current of the relay or smart outlet, the quality of the contacts, the enclosure temperature, and the mounting location. Don't use a cheap, unenclosed module for a 120V/230V heater. If you're not sure, test at low power or consult an electrician.
Plan B with a relay
A relay should have a clear state after a power loss and after losing its connection to HA: check the module's documentation (normally open vs. normally closed). A manual load switch or a physical relay switch is your emergency stop. Don't rely solely on the app.
YAML: a test thermostat for the office
Adapt the entity names to your own house. After changing configuration.yaml, check the configuration for errors in HA, then restart Home Assistant. The name and location of this feature in the UI can vary between versions, so follow your own installation's current menu.
climate:
- platform: generic_thermostat
name: Office thermostat
unique_id: office_thermostat_test
heater: switch.office_heater
target_sensor: sensor.office_temperature
min_temp: 15
max_temp: 26
target_temp: 20
ac_mode: false
cold_tolerance: 0.5
hot_tolerance: 0.5
min_cycle_duration:
minutes: 5
initial_hvac_mode: "off"
precision: 0.5
initial_hvac_mode: "off" is a deliberately safe start: the thermostat won't turn heating on by itself after an HA restart, until you switch the mode to heat from the UI. ac_mode: false means we're treating the switch as heating, not air conditioning.
If you want to control the temperature-setting step in the UI separately, you can later use target_temp_step. For a start, precision: 0.5 is sufficient and easy to read.
Hysteresis: cold_tolerance and hot_tolerance
These are tolerances around the target temperature: instead of turning heating on at exactly 68.0°F and off at exactly 68.0°F.
cold_tolerance: how far below the target the sensor can drop before heating turns on. With a target of 68°F and a tolerance of roughly 1°F, heating starts below about 67°F.
hot_tolerance: when heating turns off above the target. With a target of 68°F and a tolerance of roughly 1°F, heating shuts off around 69°F.
Tolerances that are too tight with a slow heater mean frequent switching. Too loose means uncomfortable temperature swings. Start around 0.5 to 1°F and watch the chart for a few hours.
min_cycle_duration: protecting the relay
The HA documentation describes min_cycle_duration as the minimum time before the switch can be turned off after being turned on. It protects against overly short cycles in HA's own logic, but it's not a universal hardware safeguard: a heater, a boiler, a compressor, a mat, and a pump all have different requirements. For a test heater, 3 to 10 minutes is common; for a boiler, the decision belongs to the documentation. A test load needs its own thermostat or a temperature limiter: Generic Thermostat only controls based on one sensor.
This isn't an electrical safeguard. min_cycle_duration limits HA's switching logic, but it doesn't check whether the wiring, the relay, and the load are properly sized for the device's power.
Optionally, in YAML you can add max_cycle_duration (a maximum on-time) or cycle_cooldown (a pause before turning back on). To start, min_cycle_duration is enough: you'll fine-tune the rest after observation.
Don't duplicate the logic with an automation
Once you have a working generic_thermostat, don't write a separate automation that turns on the same switch.office_heater based on the sensor's temperature. Two controllers on one switch is the same chaos as HA plus the manufacturer's app on a smart thermostat.
The automations from Lesson 5 still make sense for TRV heads. Here, you're mainly changing the target temperature on the climate.office_thermostat entity based on comfort_mode: just like with a TRV head, gated by comfort_automation_enabled. You leave the actual switching to generic_thermostat.
Automation: change the thermostat's target temperature
This automation doesn't control the switch directly. It only changes the target temperature on the climate.office_thermostat entity. Turning switch.office_heater on and off is still Generic Thermostat's job.
alias: "Comfort: office thermostat by mode"
mode: single
triggers:
- trigger: state
entity_id:
- input_select.comfort_mode
- input_number.comfort_temperature
- input_number.eco_temperature
- input_number.night_temperature
- input_number.away_temperature
conditions:
- condition: state
entity_id: input_boolean.comfort_automation_enabled
state: "on"
- condition: template
value_template: "{{ states(''climate.office_thermostat'') not in [''unavailable'', ''unknown''] }}"
actions:
- choose:
- conditions:
- condition: state
entity_id: input_select.comfort_mode
state: "Comfort"
sequence:
- action: climate.set_temperature
target:
entity_id: climate.office_thermostat
data:
temperature: "{{ states(''input_number.comfort_temperature'') | float(21) }}"
- conditions:
- condition: state
entity_id: input_select.comfort_mode
state: "Eco"
sequence:
- action: climate.set_temperature
target:
entity_id: climate.office_thermostat
data:
temperature: "{{ states(''input_number.eco_temperature'') | float(19) }}"
- conditions:
- condition: state
entity_id: input_select.comfort_mode
state: "Night"
sequence:
- action: climate.set_temperature
target:
entity_id: climate.office_thermostat
data:
temperature: "{{ states(''input_number.night_temperature'') | float(18) }}"
- conditions:
- condition: state
entity_id: input_select.comfort_mode
state: "Away"
sequence:
- action: climate.set_temperature
target:
entity_id: climate.office_thermostat
data:
temperature: "{{ states(''input_number.away_temperature'') | float(16) }}"
The Summer mode: set climate.turn_off (as in Lesson 5). Guests: a higher temperature helper, same pattern. After turning on comfort_automation_enabled, add that helper to the triggers, so the temperature is set right away. After the sensor goes unavailable, the thermostat stays off: the measurement coming back doesn't turn it back on automatically (fail-safe; turn it on by hand). Test: physically unplugging the sensor, an HA restart during heating, power coming back to the relay, the device's restore behavior, and a manual shutoff with HA off.
A temperature sensor failure
If sensor.office_temperature goes into unavailable or unknown, the virtual thermostat loses reliable data. Don't assume "it'll probably be fine." The rule from the security module: detect the missing measurement and respond.
A simple alert: adjust the entities. This doesn't automatically turn off heating; it just informs you first. Turn off the thermostat deliberately, after testing:
alias: "Comfort: alert, office sensor unavailable"
mode: single
triggers:
- trigger: state
entity_id: sensor.office_temperature
to:
- unavailable
- unknown
for: "00:10:00"
conditions: []
actions:
- action: persistent_notification.create
data:
title: "Heating: no reading from the office"
message: "The office temperature sensor hasn't reported in 10 minutes. Check the test thermostat."
After an alert like this, manually check the state of climate.office_thermostat and switch.office_heater. Don't assume the alert solves the problem by itself.
Optional: sensor down → turn off the thermostat
With a test load, you can decide that a missing sensor turns off the thermostat. That's safer than heating with no measurement, but it has to be a deliberate decision, since in some installations turning off heating can also be a problem.
alias: "Comfort: turn off the office thermostat with no sensor"
mode: single
triggers:
- trigger: state
entity_id: sensor.office_temperature
to:
- unavailable
- unknown
for: "00:10:00"
actions:
- action: climate.turn_off
target:
entity_id: climate.office_thermostat
- action: persistent_notification.create
data:
title: "Heating: office thermostat turned off"
message: "The office temperature sensor hasn't reported in 10 minutes. The test thermostat has been turned off."
An on/off thermostat note
# generic_thermostat: diagnostics
Test date: ____
Load (not the main boiler): ____
Sensor: ____
Switch: ____
Climate entity: ____
cold_tolerance: ____
hot_tolerance: ____
min_cycle_duration: ____
Time to turn on after a temperature drop: ____
Time to turn off after reaching the target: ____
Plan B manual switch: ____
Alert on sensor unavailable: yes / no
Dashboard: the test thermostat
type: entities
title: Test thermostat: office
entities:
- entity: climate.office_thermostat
name: Office thermostat
- entity: sensor.office_temperature
name: Office temperature
- entity: switch.office_heater
name: Heater switch
- entity: input_select.comfort_mode
name: Comfort mode
- entity: input_boolean.comfort_automation_enabled
name: Comfort automation
Add a history card for the sensor and the switch's state: you'll see the heating cycles and whether min_cycle_duration is actually preventing overly short on-times.
How to test this lesson
1. Configure generic_thermostat on a test load. Leave initial_hvac_mode: "off", then turn on heat mode by hand.
2. Set the target temperature and watch: when the switch turns on, when it turns off, how long the cycle lasts.
3. Change cold_tolerance / hot_tolerance and compare the switching frequency on the chart.
4. Simulate a missing sensor, if you can do it safely. Keep in mind some battery-powered devices don't immediately go unavailable: they show the last value for a long time instead. Check how your model behaves.
5. Check your Plan B: a physical switch with HA turned off.
Common mistakes
Zero hysteresis: tolerances of 0 and the switch clicks every few minutes.
min_cycle_duration: 0: no protection against short cycles.
Two controllers on one switch: generic_thermostat plus an automation like "if temp < X."
The gas boiler as a starting point: modulation and warranty coverage: a topic for your installer and Lesson 7.
What not to do
Don't wire up the main gas boiler "just to try it" if you don't have documentation for the control input and your installer's sign-off.
Don't confuse generic_thermostat with OpenTherm Gateway: they're different integrations with different capabilities (Lesson 7).
Don't ignore unavailable on the sensor: set up an alert or a manual fallback procedure.
Practical assignment
☐ Choose a test load (not the main boiler) and record it in the on/off thermostat note.
☐ Configure generic_thermostat in YAML with cold_tolerance, hot_tolerance, and min_cycle_duration.
☐ Test the heating cycles on the sensor and switch chart.
☐ Add an alert for the sensor going unavailable / unknown (or note why you're skipping it for now).
☐ Create the test thermostat dashboard.
☐ Note whether this route fits your house, or whether you need OpenTherm / Modbus (Lesson 7).
Key takeaways
generic_thermostat = a sensor plus a switch plus thermostat logic in HA.
Hysteresis and min_cycle_duration tidy up the on/off logic, but don't replace electrical safeguards.
Test on low risk: not on the main boiler without documentation.
One controller per switch: a virtual thermostat, not a second automation loop.
What's next
In Lesson 7, we'll go through OpenTherm Gateway, Modbus, and boiler and heat pump protocols: when to only read data, and when to carefully control it. Generic Thermostat stays a simple on/off route; OpenTherm and Modbus are a different league of requirements and documentation.
If you understand that a virtual thermostat means hysteresis and cycle time: not a magic "turn on at 68°F": you're ready for deliberate decisions at the boiler, not just at the test heater.