I’ve been working on a personal whole-home climate setup in HA that replaces most of the “smart” logic in my Nest thermostats. My house runs on Nests, which are great for scheduling and pre-heating/cooling, but I could never get the actual “feels like” comfort right. Setting the house to 70º when it’s in the 70s outside is totally different from when it’s in the 40s. I found myself adjusting it more than I expected. Radiant is slow, AC is fast, and I don’t want them fighting each other. I also wanted a true set-and-forget system plus some per-floor nuance. Home Assistant seemed perfect for that.
House Setup
Three floors, radiant heat on all three, AC only on the upper two, and each floor behaves differently (size, layout, usage, schedule, drafts, sun exposure, etc.). There’s also an office with no radiant heat and weak AC, so it’s controlled separately with a convection heater and a flakey temperature sensor—but I’m skipping that here to keep this post simpler.
Design Goals
- One consistent comfort baseline for the whole house
- Automatically adjust for outdoor temperature
- Each floor layers small usage-based offsets on top
- Avoid big swings, stick to gentle, time-based nudges
- HA computes the target temperature, Nest continues to executes it
- Make the whole system readable and debuggable at a glance
Solution
(Screenshot at the bottom shows an example with actual temp values and targets.)
I broke this into 3 layers:
Layer 1: Outdoor-Driven Comfort Offset
Instead of relying on static schedules, HA reacts to weather. I pull outdoor temperatures from a nearby station and compute a 3-day average to decide whether we’re in a “cold” or “warm” period. A simple curve maps outdoor temps to an offset (0 to +3 in heating season, 0 to −3 in cooling season). Example:
Cold outside = 3-day mean < 65º
Offset = +1 for 45–65º, +2 for <45º, etc. (cooling has its own curve)
Automation #1 – Cold Outside
alias: Climate — Toggle Cold Outside
description: ""
triggers:
- trigger: state
entity_id:
- sensor.outside_temp_3d_mean
conditions: []
actions:
- variables:
avg: "{{ states('sensor.outside_temp_3d_mean')|float(60) }}"
- choose:
- conditions:
- condition: template
value_template: "{{ avg <= 57 }}"
sequence:
- target:
entity_id: input_boolean.cold_outside
action: input_boolean.turn_on
- conditions:
- condition: template
value_template: "{{ avg >= 62 }}"
sequence:
- target:
entity_id: input_boolean.cold_outside
action: input_boolean.turn_off
mode: restart
Layer 2: House Target Temperature
I defined an input helper to set the base comfort temperature. That represents my desired “feels like” temperature. The global house target temperature is calculated based on that base temperature plus that outdoor-temperature offset. This keeps the whole house generally a little warmer on cold days and a little cooler on hot ones. Example:
Base 70º, Outside 48º → Offset +1 → Target = 71º
I’m also accounting for when I’m on vacation and clamping minor fluctuations to avoid big swings.
Automation #2 – House Target Temperature
alias: Climate — House Target Temperature
description: ""
triggers:
- trigger: time_pattern
minutes: /30
- trigger: state
entity_id:
- input_boolean.cold_outside
- input_number.house_base_temperature
- input_select.home_presence
conditions: []
actions:
- variables:
outside: >-
{{ states('sensor.weather_station_feels_like')|float(60)
}}
is_cold: "{{ is_state('input_boolean.cold_outside', 'on') }}"
offset: |
{% if is_cold %}
{% if outside < 35 %} 3
{% elif outside < 45 %} 2
{% elif outside < 65 %} 1
{% else %} 0
{% endif %}
{% else %}
{% if outside > 88 %} -3
{% elif outside > 82 %} -2
{% elif outside > 76 %} -1
{% else %} 0
{% endif %}
{% endif %}
target: |
{% if is_state('input_select.home_presence', 'Vacation') %}
{% if is_cold %}
66
{% else %}
78
{% endif %}
{% else %}
{{ states('input_number.house_base_temperature')|float + offset|float }}
{% endif %}
current: "{{ states('input_number.house_target_temperature')|float(0) }}"
diff: "{{ (target|float) - current }}"
- condition: template
value_template: "{{ diff | abs >= 0.9 }}"
- action: input_number.set_value
target:
entity_id: input_number.house_target_temperature
data:
value: "{{ target }}"
mode: restart
Layer 3: Per-Floor Behavior
I created automations for each floor. Think tiny nudges (like +1 in the morning, or 0 at night) based on how that space is actually used and time of day. Example:
Upstairs:
- 05:00–06:00 – Morning pre-warm (+1º)
- 06:00–20:30 – Day baseline (0º)
- 20:00–21:00 – Evening warm-up (+1º)
- 21:00–05:00 – Night setback (-1º)
Floors with heating and cooling get a derived heat/cool set points (range). The one floor with just radiant heat gets a single set point. Each zone ends up feeling “right” at the right time without ever pushing radiant into big temperature swings.
Automation #3 – Upstairs
alias: Climate — Upstairs
description: ""
triggers:
- trigger: state
entity_id:
- input_number.house_target_temperature
- input_select.home_presence
for:
seconds: 3
- trigger: time
at: "05:30:00"
- trigger: time
at: "07:30:00"
- trigger: time
at: "20:00:00"
- trigger: time
at: "21:30:00"
- trigger: time_pattern
minutes: /30
conditions: []
actions:
- variables:
offset: 0
- choose:
- conditions:
- condition: state
entity_id: input_select.home_presence
state: Vacation
sequence:
- variables:
offset: 0
- conditions:
- condition: time
after: "05:30:00"
before: "07:30:00"
sequence:
- variables:
offset: 1
- conditions:
- condition: time
after: "07:30:00"
before: "20:00:00"
sequence:
- variables:
offset: 0
- conditions:
- condition: time
after: "20:00:00"
before: "21:00:00"
sequence:
- variables:
offset: 0
- conditions:
- condition: or
conditions:
- condition: time
after: "21:00:00"
- condition: time
before: "05:30:00"
sequence:
- variables:
offset: -1
default:
- variables:
offset: 0
- variables:
house: "{{ states('input_number.house_target_temperature')|float }}"
is_cold: "{{ is_state('input_boolean.cold_outside','on') }}"
current: "{{ state_attr('climate.upstairs','current_temperature')|float }}"
desired: "{{ house + offset }}"
target: |
{% if is_cold %}
{{ desired }}
{% else %}
{% if current < desired %}
{{ house }}
{% else %}
{{ desired }}
{% endif %}
{% endif %}
new_low: |
{% if is_cold %}
{{ target }}
{% else %}
{{ (target - 3)|round(1) }}
{% endif %}
new_high: |
{% if is_cold %}
{{ (target + 3)|round(1) }}
{% else %}
{{ target }}
{% endif %}
current_low: "{{ state_attr('climate.upstairs','target_temp_low')|float(0) }}"
current_high: "{{ state_attr('climate.upstairs','target_temp_high')|float(0) }}"
- condition: template
value_template: |
{{ (new_low - current_low)|abs >= 0.4
or (new_high - current_high)|abs >= 0.4 }}
- action: climate.set_temperature
target:
entity_id: climate.upstairs
data:
target_temp_low: "{{ new_low }}"
target_temp_high: "{{ new_high }}"
mode: restart
I’m being explicit with redundant choose cases to make reading automation traces easier. Some floors have an additional condition to suppress heating if the current temperature exceeds a certain value. The Office has more conditions based on location and whether I’m working from home or from the office.
Debugging panel
I have a markdown card that explains, in plain English, what each room is targeting and why. It’s been incredibly useful for debugging and tuning. I still update it manually when I tweak automations, but I can figure out something more clever later. I’ve also added quick toggles to enable/disable each floor’s automations in case I want to apply a quick override to the set points.
Results
So far it’s been really stable; predictable warming, and way more control than Nest’s built-in logic alone. I still get the occasional overshoot because I’m still learning how much time it takes for radiant heat to heat each space. So I’m still fine-tuning timings and offsets for each floor, but so far it’s been a huge upgrade. Obviously, I haven’t tested summer behavior yet, and I expect to do some further tweaking when that happens.
Happy to answer questions. Would love to hear feedback or smarter ideas to evolve this!
