Some hardware links on this page are affiliate links. As an Amazon Associate I earn from qualifying purchases. We may earn a commission if you buy through them, at no extra cost to you. See the affiliate disclosure.
A thermostat with one heating number and one cooling number wastes energy in mild weather and feels wrong at the edges of the season. This pack computes a comfort band from recent outdoor temperature and nudges the climate entity only when the room has left that band. Mild days are allowed to drift. Cold or hot days still get a setpoint.
Entity ids are blueprint inputs. Do not publish a live thermostat name.
The adaptive equations below are the published ASHRAE 55 limits for occupant-controlled, naturally conditioned spaces. A house with mechanical heating or cooling is outside that scope, so this pack is an energy heuristic that borrows the curve, not a claim that a room meets the standard. When the heat or cool equipment is actually running, treat the result as a setpoint suggestion with your own floor and ceiling.
Pairs with window-open climate pause so an open sash still shuts the equipment off, and with HVAC health monitor if you want to know the equipment failed to move the room. A room sensor can be the ESP32-C6 climate sensor or the external temperature already written to a valve by Sonoff TRVZB external temperature.
What problem it solves
You want the heating and cooling targets to follow the weather, and you want the system left alone while the room is already inside a comfortable band.
How it works
- Prevailing outdoor mean — a template or statistics sensor smooths outdoor temperature over about a week (exponential weight on recent days). The standard uses that prevailing mean, not the reading from the last five minutes. A weather entity’s temperature attribute is the no-extra-hardware source; a shaded outdoor sensor is better.
- Center and band — with the mean in °C, the comfort center is
0.31 × mean + 17.8. The 80% acceptability limits from ASHRAE 55 are0.31 × mean + 21.3(upper) and0.31 × mean + 14.3(lower). The pack only evaluates the curve when the mean is between 10 °C and 33.5 °C. Outside that, it holds the last valid setpoints. - Act only outside the band — if indoor temperature is inside the limits, the blueprint does not write a new setpoint. If the room is colder than the lower limit and the climate entity is heating, the heat setpoint becomes the lower limit, clamped to a floor you set. If the room is warmer than the upper limit and the entity is cooling, the cool setpoint becomes the upper limit, clamped to a ceiling you set.
- Mode —
heatwrites the heat target,coolwrites the cool target,heat_coolcan write both.offis left alone. An optional occupancy boolean can skip writes when nobody is home, or apply a setback you configure. - Rate limit — writes are spaced (pack default 30 minutes) and ignored when the new target is within 0.3 °C of the current one, so a noisy outdoor sensor does not flap the thermostat.
Sleep offsets and per-vendor thermostat quirks are left as optional inputs. The pack does not learn from your manual changes.
Hardware notes
- Indoor temperature. Any
sensorwithdevice_class: temperature. An ESP32-C6 plus AHT20 is the usual node. Put it in the occupied air, not on the thermostat body and not in sun. - Outdoor. A
weatherentity that exposestemperature, or a second sensor. City forecasts lag a wall in the sun. The pack converts °F to °C before the formula. - Climate. Whatever entity already heats or cools. The blueprint calls
climate.set_temperature. It does not install a thermostat.
Snippet
Center and 80% limits from a prevailing-mean sensor. The blueprint substitutes the entity id.
template:
- sensor:
- name: Comfort center
unit_of_measurement: "°C"
state: >
{% set t = states(outdoor_mean) | float(none) %}
{% if t is none or t < 10 or t > 33.5 %}
unknown
{% else %}
{{ (0.31 * t + 17.8) | round(1) }}
{% endif %}
- name: Comfort upper 80
unit_of_measurement: "°C"
state: >
{% set t = states(outdoor_mean) | float(none) %}
{% if t is none or t < 10 or t > 33.5 %}
unknown
{% else %}
{{ (0.31 * t + 21.3) | round(1) }}
{% endif %}
- name: Comfort lower 80
unit_of_measurement: "°C"
state: >
{% set t = states(outdoor_mean) | float(none) %}
{% if t is none or t < 10 or t > 33.5 %}
unknown
{% else %}
{{ (0.31 * t + 14.3) | round(1) }}
{% endif %}
Write a heat setpoint only when the room is below the band:
trigger:
- platform: state
entity_id: !input indoor_temp
- platform: state
entity_id: !input comfort_lower
condition:
- condition: numeric_state
entity_id: !input indoor_temp
below: !input comfort_lower
- condition: state
entity_id: !input climate_entity
attribute: hvac_action
state: "heating"
action:
- action: climate.set_temperature
target:
entity_id: !input climate_entity
data:
temperature: "{{ states(comfort_lower) | float }}"
Clamps, the cool path, and the 30-minute spacing are in the full blueprint.
Setup outline
- Confirm an indoor temperature sensor and an outdoor temperature (weather attribute or sensor) in Developer Tools. Note the unit.
- Add the prevailing-mean sensor and the three comfort templates. Check that a 15 °C mean gives a center near 22.5 °C, an upper near 26.0 °C, and a lower near 18.9 °C.
- Import the blueprint. Select the climate entity, the indoor sensor, the mean, and the min/max clamps (pack defaults 18 °C and 28 °C).
- Set the climate mode to heat or cool. Push the indoor reading across a limit (or temporarily tighten the band) and confirm a single
set_temperaturein the trace. - Put the indoor reading back inside the band and confirm the next cycles do not write.
Troubleshooting
- Setpoint never moves. The room is inside the band, which is the point, or the mean is
unknownbecause the weather entity has notemperatureattribute. Read the three template sensors first. - Setpoint jumps to a nonsense number. The outdoor value was in °F and was not converted, or it was a momentary spike. The pack converts units and ignores means outside 10–33.5 °C.
- Writes every few minutes. The rate limit is off, or the indoor sensor is noisy and straddles the limit. Add the spacing and a small deadband.
- Fights an open window. This pack does not watch contacts. Run window-open climate pause ahead of it, or add the contact as a blueprint condition so an open window blocks writes.
- Heat pump ignores the number. Some climate entities want
target_temp_high/target_temp_lowinheat_cool, nottemperature. The pack has a mode input for that. Check Developer Tools for which fields the entity accepts.
Credits
The comfort center 0.31 × prevailing mean outdoor temperature + 17.8 and the 80% limits (+ 21.3 and + 14.3) are the adaptive equations in ASHRAE Standard 55 for occupant-controlled naturally conditioned spaces. The idea of driving a Home Assistant climate entity from that curve is in community blueprints such as Adaptive Comfort Control Pro. This write-up uses the standard center and 80% limits, and it is an original setpoint helper, not a copy of that blueprint.