Optimum Start for Heating Systems
A heating system that starts at a time, which changes daily, automatically, in view of occupancy times, external and internal temperatures etc., and consequently saving on energy bills = ‘optimum start’.
For example, my office, a place I only ever get to by half past eight (8:30 for those across the Pond) in the morning, was constantly up to temperature 30 minutes before I ever got there. This extra heat all adds up.
This is my first ever post. I apologies in advance if any protocols are amiss.
Over a year and a half ago I wanted to implement optimum start, a solution based on occupancy times. I found that temperature set points and turning the heating on at set times was not the most efficient solution. Fancy thermostats may provide for this, but I could not afford them and maybe the new thermostat cards can do this. Anyway, I searched high and low for such a system with the hope of integrating it into Home Assistant. It was never found or maybe my googling capacities are sub par. I found pretty much nothing, so here it is.
After much blood, sweat and tears and a year later, everything is operating well. I thought I would share it with you, as it seems that there is still not much out there. Someone may find it helpful.
How it works:
It uses this formula:
t=(V x Ts-Tr)/(Qin*(Uv x A x (To-Tr)), which provides a result in minutes. It then subtracts this value from a set occupancy time (using a date/time helper) to give an optimum start time.
This is the formula in a spreadsheet. Good for testing. D5 is volume etc.
t=(D5*(D6-D7))/(D11-((D8 * D9*(D7-D10))/1000))
This is the formula transcribed into template format. I apologise if this is clunky. I am by no means a programmer.
{% set area = 172.29 %}
{% set volume = 225 %}
{% set Uvalue = 0.46 %}
{% set Qin = (states('input_number.w_input_bh')|float)/1000 %}
{% set preheat_time = 900 %}
{% set Tdiff = (states('input_number.temp_differential_bh')|float) %}
{% set Ts = (states('input_number.bh_room_temp_set_point')|float)-(Tdiff) %}
{% set Tr = (states('sensor.upper_dorm_temp_avg_30mins')|float) %}
{% set To = (states('sensor.outdoor_temp_average_over_last_2_hours')|float) %}
{% set occ_time_secs = (state_attr('input_datetime.occupancy_am_time_optimum_start_bh','timestamp') | int) %}
{% set occ_time = ((state_attr('input_datetime.occupancy_am_time_optimum_start_bh','timestamp') | int)| timestamp_custom("%H:%M:%S", False))%}
{% set opt_start_time = (((((occ_time_secs)-(((volume*((Ts)-(Tr)))/((Qin-(((Uvalue*area*((Tr)-(To)))))/1000)))|round(0) *60)-preheat_time))| timestamp_custom("%H:%M:%S", False)))%}
{{now() > today_at (opt_start_time)}}
This template is used as a trigger in an automation, which when renders ‘true’ fires the heating.
Some more details; it calculates the amount of minutes to heat the space and subtracts this time from the occupancy time, called the ‘opt_start_time’. Once the present time ‘now’ exceeds the ‘opt_start_time’ a ‘true’ result is rendered, which triggers an automation.
The formula explained:
{% set area = 172.29 %}
# your area of all the heat loss surfaces eg. external walls, floor and ceiling. Not your floor area. I generally ignored internal walls, though I would include them if if the temperature difference between them is large eg. a cold garage next to your heating space.
{% set volume = 225 %}
# Floor area times the space height
{% set Uvalue = 0.46 %}
# the sum of all your Uvalues. I calculated the first few spaces based on the CIBSE Guide A, but guestimated the figure for other spaces (I have six heating systems using this). One can easily insert a figure of 0.45/0.50 to save the hassle of calculating Uvalues. Insert a higher value for older buildings and a lower value for newer builds.
{% set Qin = (states('input_number.w_input_bh')|float)/1000 %}
# the total heat output of your heat emitters in Watts in the room where the room sensor is. eg. radiators in the space, not your whole house. If you can’t bothered calculating this, insert 30 or 40 W/2m of floor area and adjust it later to suit. I used a helper for this, which means I can easily tweak it later to get a perfect optimum start time result.
{% set preheat_time = 900 %}
# I inserted this value of seconds to allow the boilers to heat up. Reduce or increase as you wish.
{% set Tdiff = (states('input_number.temp_differential_bh')|float) %}
# This takes the room set point and subtracts a value from it, so the room temperature will be the room set point minus Tdiff at occupancy time. This means the heating will continue to run after occupancy time until it reaches the set point. Lets face it, people like it when the heat is still on for a while. I used a number helper for this set at ‘1’ ie. one degree of the set point.
{% set Ts = (states('input_number.bh_room_temp_set_point')|float)-(Tdiff) %}
# The room temperature set point minus Tdiff.
{% set Tr = (states('sensor.upper_dorm_temp_avg_30mins')|float) %}
# The room temperature sensor. I used an average due to sensitive thermostats.
{% set To = (states('sensor.outdoor_temp_average_over_last_2_hours')|float) %}
# The outdoor temperature. Again I used an average to avoid errors and fluctuations.
{% set occ_time = ((state_attr('input_datetime.occupancy_am_time_optimum_start_bh','timestamp') | int)| timestamp_custom("%H:%M:%S", False))%}
# An input date/time helper. The desired occupancy time set on your dashboard. Not actually used in the formula, but left it in for kicks.
{% set occ_time_secs = (state_attr('input_datetime.occupancy_am_time_optimum_start_bh','timestamp') | int) %}
# Uses the above date/time helper, but extracts the time in seconds, which is necessary for the next step.
{% set opt_start_time = (((((occ_time_secs)-(((volume*((Ts)-(Tr)))/((Qin-(((Uvalue*area*((Tr)-(To)))))/1000)))|round(0) *60)-preheat_time))| timestamp_custom("%H:%M:%S", False)))%}
# the calculated optimum start time.
{{ now() > today_at (opt_start_time)}}
# Once the optimum start time is greater than ‘now’ time, the template renders ‘true’ and fires the heating.
I hope this helps someone out there. Enjoy!