So in the continuing saga with my mom’s home-automated furnace, it got extra cold recently and I noticed it wasn’t getting up to temperature in time for her to wake up. I figured I could come up with a formula to compute the time needed to come to temperature and turn on the furnace at a dynamic time in the morning so it’d be just right.

The Home Assistant GUI only shows the past 24 hours, but the Recorder component saves data for as long as you want. I have mine cutting it off after a week. you can query the data to get nice csv files for processing with sqlite commands like this:

sqlite3 -csv home-assistant_v2.db 'select last_updated, state from states where entity_id="sensor.multisensor_temperature";' >inside.csv sqlite3 -csv home-assistant_v2.db 'select last_updated, state from states where entity_id="sensor.dark_sky_temperature";' >outside.csv sqlite3 -csv home-assistant_v2.db 'select last_updated, state from states where entity_id="switch.living_room_heat";' >heater.csv 1 2 3 sqlite3 - csv home - assistant_v2 . db 'select last_updated, state from states where entity_id="sensor.multisensor_temperature";' > inside . csv sqlite3 - csv home - assistant_v2 . db 'select last_updated, state from states where entity_id="sensor.dark_sky_temperature";' > outside . csv sqlite3 - csv home - assistant_v2 . db 'select last_updated, state from states where entity_id="switch.living_room_heat";' > heater . csv

The simplest solution here is to just do a first-order approximation and say that heatup duration is probably proportional to the temperature delta between inside and outside at the moment you need to make a decision. This is rough because you don’t know what the outside temperature will do during the heat-up period, but it should be good enough for most purposes.

The Python package, pandas makes it really easy to process these data. The code below processed the raw data and computed the least-squares fit of the relationship between the temperature delta and the heatup duration:

Measuring heatups import csv import datetime import pandas import numpy as np import matplotlib.pyplot as plt def read_data(): inside = pandas.read_csv('inside.csv', parse_dates=[0],names=['Time','Temperature'],index_col=0) inside = inside[inside>-1000] # filter crap readings outside = pandas.read_csv('outside.csv', parse_dates=[0],names=['Time','Temperature'],index_col=0) heater = pandas.read_csv('heater.csv', parse_dates=[0],names=['Time','Status'],index_col=0) return inside, outside, heater def find_heatup_durations(heater, status='off'): """Figure out how long the heater was on for each morning.""" last_time = next(heater.iterrows())[0] heatup_durations = [] for time, df in heater.iterrows(): diff = time - last_time if df['Status']==status and diff > datetime.timedelta(hours=1): heatup_durations.append((last_time, diff)) # remember when it turned on too last_time = time return heatup_durations def find_temperatures_at_times(temperatures, times): """Find what the temperature was during the morning heatup.""" blocks = [] for start_time, duration in times: blocks.append((start_time, temperatures[start_time: start_time + duration])) return blocks def analyze_heatups(durations, outside_temps, inside_temps): """Look at the heatup dynamics and try to build a model.""" initial_outside = np.array([o[1].values[0] for o in outside_temps]) initial_inside = np.array([o[1].values[0] for o in inside_temps]) durations_in_minutes = np.array([d[1].total_seconds() for d in durations])/60.0 delta = (initial_inside-initial_outside)[:,0] print(delta) slope, intercept = np.polyfit(delta, durations_in_minutes,1) model_temp = np.linspace(min(delta), max(delta), 20) model_duration = slope * model_temp + intercept plt.figure() ax = plt.gca() #plt.plot(initial_outside, durations_in_seconds,'o') plt.plot(delta, durations_in_minutes,'o', label='Data') plt.plot(model_temp, model_duration,'-', label='Model') plt.text(0.2,0.1,'D = {:.1f} T + {:.1f}'.format(slope, intercept), transform = ax.transAxes) plt.xlabel('Initial delta Temperature (F)') plt.ylabel('Duration of heatup (min)') plt.title('Duration of heatups') plt.savefig('heatups.png') if __name__ == '__main__': inside,outside,heater = read_data() durations = find_heatup_durations(heater) outside_temps = find_temperatures_at_times(outside, durations) inside_temps = find_temperatures_at_times(inside, durations) analyze_heatups(durations, outside_temps, inside_temps) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import csv import datetime import pandas import numpy as np import matplotlib . pyplot as plt def read_data ( ) : inside = pandas . read_csv ( 'inside.csv' , parse_dates = [ 0 ] , names = [ 'Time' , 'Temperature' ] , index_col = 0 ) inside = inside [ inside > - 1000 ] # filter crap readings outside = pandas . read_csv ( 'outside.csv' , parse_dates = [ 0 ] , names = [ 'Time' , 'Temperature' ] , index_col = 0 ) heater = pandas . read_csv ( 'heater.csv' , parse_dates = [ 0 ] , names = [ 'Time' , 'Status' ] , index_col = 0 ) return inside , outside , heater def find_heatup_durations ( heater , status = 'off' ) : """Figure out how long the heater was on for each morning.""" last_time = next ( heater . iterrows ( ) ) [ 0 ] heatup_durations = [ ] for time , df in heater . iterrows ( ) : diff = time - last_time if df [ 'Status' ] == status and diff > datetime . timedelta ( hours = 1 ) : heatup_durations . append ( ( last_time , diff ) ) # remember when it turned on too last_time = time return heatup_durations def find_temperatures_at_times ( temperatures , times ) : """Find what the temperature was during the morning heatup.""" blocks = [ ] for start_time , duration in times : blocks . append ( ( start_time , temperatures [ start_time : start_time + duration ] ) ) return blocks def analyze_heatups ( durations , outside_temps , inside_temps ) : """Look at the heatup dynamics and try to build a model.""" initial_outside = np . array ( [ o [ 1 ] . values [ 0 ] for o in outside_temps ] ) initial_inside = np . array ( [ o [ 1 ] . values [ 0 ] for o in inside_temps ] ) durations_in_minutes = np . array ( [ d [ 1 ] . total_seconds ( ) for d in durations ] ) / 60.0 delta = ( initial_inside - initial_outside ) [ : , 0 ] print ( delta ) slope , intercept = np . polyfit ( delta , durations_in_minutes , 1 ) model_temp = np . linspace ( min ( delta ) , max ( delta ) , 20 ) model_duration = slope * model_temp + intercept plt . figure ( ) ax = plt . gca ( ) #plt.plot(initial_outside, durations_in_seconds,'o') plt . plot ( delta , durations_in_minutes , 'o' , label = 'Data' ) plt . plot ( model_temp , model_duration , '-' , label = 'Model' ) plt . text ( 0.2 , 0.1 , 'D = {:.1f} T + {:.1f}' . format ( slope , intercept ) , transform = ax . transAxes ) plt . xlabel ( 'Initial delta Temperature (F)' ) plt . ylabel ( 'Duration of heatup (min)' ) plt . title ( 'Duration of heatups' ) plt . savefig ( 'heatups.png' ) if __name__ == '__main__' : inside , outside , heater = read_data ( ) durations = find_heatup_durations ( heater ) outside_temps = find_temperatures_at_times ( outside , durations ) inside_temps = find_temperatures_at_times ( inside , durations ) analyze_heatups ( durations , outside_temps , inside_temps )

The results are in! It’s fairly noisy, but should get the job done.

So now we have to get Home Assistant to turn on the heat that many minutes before the target “wake up time”. Templates triggers to the rescue. The template trigger for a 7:45am wakeup looks like this:

The template trigger platform: template value_template: > {% if states("sensor.multisensor_temperature") |float < -50 or states("sensor.multisensor_temperature") |float > 120 %} false {% elif (7*60+45)- (now().hour * 60 + now().minute) < (states('sensor.multisensor_temperature') |float - states('sensor.dark_sky_temperature') |float ) * 7.5 - 196.0 %} true {% else %} false {% endif %} 1 2 3 4 5 6 7 8 9 platform : template value_template : > { % if states ( "sensor.multisensor_temperature" ) | float < -50 or states ( "sensor.multisensor_temperature" ) | float > 120 % } false { % elif ( 7 *60 +45 ) - ( now ( ) . hour * 60 + now ( ) . minute ) < ( states ( 'sensor.multisensor_temperature' ) | float - states ( 'sensor.dark_sky_temperature' ) | float ) * 7 . 5 - 196 . 0 % } true { % else % } false { % endif % }

Whenever the indoor or outdoor temperature sensors change, this checks to see if the number of minutes between now and 7:45am is less than the required duration for full heatup and triggers if so. You’ll want to turn off the automation as part of its own action and turn it back on when the system shuts down for the night (in another automation).

If you want to try to get extra fancy you can do some thermodynamics calculations and use Newton’s law of cooling to try to figure out a more robust model. I fiddled with it a bit but didn’t get anything super interesting. Here is a plot of some intermediate cooling analysis. The coefficients on the top left are supposed to have a constant slope (they’re the relation between temperature gradient and heat flux dQ/dt. Most of them are approximately constant but it’s quite noisy. The linear model above suffices.