{"id":1523,"date":"2017-12-16T22:19:32","date_gmt":"2017-12-17T06:19:32","guid":{"rendered":"https:\/\/partofthething.com\/thoughts\/?p=1523"},"modified":"2020-08-08T16:31:34","modified_gmt":"2020-08-08T23:31:34","slug":"a-fancy-home-assistant-automation-that-checks-the-weather-and-figures-out-when-to-turn-on-your-heater","status":"publish","type":"post","link":"https:\/\/partofthething.com\/thoughts\/a-fancy-home-assistant-automation-that-checks-the-weather-and-figures-out-when-to-turn-on-your-heater\/","title":{"rendered":"A fancy Home Assistant automation that checks the weather and figures out when to turn on your heater"},"content":{"rendered":"<p>So in the continuing saga with my <a href=\"https:\/\/partofthething.com\/thoughts\/enlighten-your-old-furnace-with-a-raspberry-pi-home-assistant-an-esp8266-and-some-relays\/\">mom&#8217;s home-automated furnace<\/a>, it got extra cold recently and I noticed it wasn&#8217;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&#8217;d be just right.<\/p>\n<figure id=\"attachment_1524\" aria-describedby=\"caption-attachment-1524\" style=\"width: 640px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/partofthething.com\/thoughts\/wp-content\/uploads\/temperatures.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-1524\" src=\"https:\/\/partofthething.com\/thoughts\/wp-content\/uploads\/temperatures.png\" alt=\"Graph of temperatures inside and outside\" width=\"640\" height=\"480\" srcset=\"https:\/\/partofthething.com\/thoughts\/wp-content\/uploads\/temperatures.png 640w, https:\/\/partofthething.com\/thoughts\/wp-content\/uploads\/temperatures-300x225.png 300w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/><\/a><figcaption id=\"caption-attachment-1524\" class=\"wp-caption-text\">Temperatures at my mom&#8217;s house over a few days<\/figcaption><\/figure>\n<p><!--more--><\/p>\n<p>The <a href=\"https:\/\/home-assistant.io\">Home Assistant<\/a> 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:<\/p>\n<pre class=\"lang:default decode:true remarkup-code \">sqlite3 -csv home-assistant_v2.db  'select last_updated, state from states where entity_id=\"sensor.multisensor_temperature\";' &gt;inside.csv\nsqlite3 -csv home-assistant_v2.db  'select last_updated, state from states where entity_id=\"sensor.dark_sky_temperature\";' &gt;outside.csv\nsqlite3 -csv home-assistant_v2.db  'select last_updated, state from states where entity_id=\"switch.living_room_heat\";' &gt;heater.csv<\/pre>\n<p>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&#8217;t know what the outside temperature will do during the heat-up period, but it should be good enough for most purposes.<\/p>\n<p>The Python package, <a href=\"https:\/\/pandas.pydata.org\/\">pandas<\/a> 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:<\/p>\n<pre class=\"height-set:true lang:python decode:true\" title=\"Measuring heatups\">import csv\nimport datetime\n\nimport pandas\n\nimport numpy as np\nimport matplotlib.pyplot as plt\n\ndef read_data():\n    inside = pandas.read_csv('inside.csv', parse_dates=[0],names=['Time','Temperature'],index_col=0)\n    inside = inside[inside&gt;-1000] # filter crap readings\n    outside = pandas.read_csv('outside.csv', parse_dates=[0],names=['Time','Temperature'],index_col=0)\n    heater = pandas.read_csv('heater.csv', parse_dates=[0],names=['Time','Status'],index_col=0)\n    return inside, outside, heater\n\ndef find_heatup_durations(heater, status='off'):\n    \"\"\"Figure out how long the heater was on for each morning.\"\"\"\n    last_time = next(heater.iterrows())[0]\n    heatup_durations = []\n    for time, df in heater.iterrows():\n        diff = time - last_time\n        if df['Status']==status and diff &gt; datetime.timedelta(hours=1):\n            heatup_durations.append((last_time, diff)) # remember when it turned on too\n        last_time = time\n    return heatup_durations\n\ndef find_temperatures_at_times(temperatures, times):\n    \"\"\"Find what the temperature was during the morning heatup.\"\"\"\n    blocks = []\n    for start_time, duration in times:\n        blocks.append((start_time, temperatures[start_time: start_time + duration]))\n    return blocks\n\ndef analyze_heatups(durations, outside_temps, inside_temps):\n    \"\"\"Look at the heatup dynamics and try to build a model.\"\"\"\n    initial_outside = np.array([o[1].values[0] for o in outside_temps])\n    initial_inside = np.array([o[1].values[0] for o in inside_temps])\n    durations_in_minutes = np.array([d[1].total_seconds() for d in durations])\/60.0\n    delta = (initial_inside-initial_outside)[:,0]\n    print(delta)\n    \n    slope, intercept = np.polyfit(delta, durations_in_minutes,1)\n\n    model_temp = np.linspace(min(delta), max(delta), 20)\n    model_duration = slope * model_temp + intercept\n\n    plt.figure()\n    ax = plt.gca()\n    #plt.plot(initial_outside, durations_in_seconds,'o')\n    plt.plot(delta, durations_in_minutes,'o', label='Data')\n    plt.plot(model_temp, model_duration,'-', label='Model')\n    plt.text(0.2,0.1,'D = {:.1f} T + {:.1f}'.format(slope, intercept), transform = ax.transAxes)\n    plt.xlabel('Initial delta Temperature (F)')\n    plt.ylabel('Duration of heatup (min)')\n    plt.title('Duration of heatups')\n    plt.savefig('heatups.png')\n\nif __name__ == '__main__':\n    inside,outside,heater = read_data()\n    durations = find_heatup_durations(heater)\n    outside_temps = find_temperatures_at_times(outside, durations)\n    inside_temps = find_temperatures_at_times(inside, durations)\n    analyze_heatups(durations, outside_temps, inside_temps)<\/pre>\n<p>The results are in! It&#8217;s fairly noisy, but should get the job done.<\/p>\n<figure id=\"attachment_1525\" aria-describedby=\"caption-attachment-1525\" style=\"width: 640px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/partofthething.com\/thoughts\/wp-content\/uploads\/heatups.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-1525\" src=\"https:\/\/partofthething.com\/thoughts\/wp-content\/uploads\/heatups.png\" alt=\"Heatup duration plotted\" width=\"640\" height=\"480\" srcset=\"https:\/\/partofthething.com\/thoughts\/wp-content\/uploads\/heatups.png 640w, https:\/\/partofthething.com\/thoughts\/wp-content\/uploads\/heatups-300x225.png 300w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/><\/a><figcaption id=\"caption-attachment-1525\" class=\"wp-caption-text\">Heatup duration vs. temperature delta<\/figcaption><\/figure>\n<p>So now we have to get Home Assistant to turn on the heat that many minutes before the target &#8220;wake up time&#8221;. <a href=\"https:\/\/home-assistant.io\/docs\/automation\/trigger\/#template-trigger\">Templates triggers<\/a> to the rescue. The template trigger for a 7:45am wakeup looks like this:<\/p>\n<pre class=\"lang:yaml decode:true\" title=\"The template trigger\">    platform: template\n    value_template: &gt;\n        {% if states(\"sensor.multisensor_temperature\") |float &lt; -50 or states(\"sensor.multisensor_temperature\") |float &gt; 120 %}\n            false\n        {% elif (7*60+45)- (now().hour * 60 + now().minute) &lt; (states('sensor.multisensor_temperature') |float  - states('sensor.dark_sky_temperature') |float )  * 7.5 - 196.0  %}\n            true\n        {% else %}\n            false \n        {% endif %}\n<\/pre>\n<p>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&#8217;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).<\/p>\n<p>If you want to try to get extra fancy you can do some thermodynamics calculations and use <a href=\"https:\/\/en.wikipedia.org\/wiki\/Newton's_law_of_cooling#Heat_transfer_version_of_the_law\">Newton&#8217;s law of cooling<\/a> to try to figure out a more robust model. I fiddled with it a bit but didn&#8217;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&#8217;re the relation between temperature gradient and heat flux dQ\/dt. Most of them are approximately constant but it&#8217;s quite noisy. The linear model above suffices.<\/p>\n<figure id=\"attachment_1526\" aria-describedby=\"caption-attachment-1526\" style=\"width: 2000px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/partofthething.com\/thoughts\/wp-content\/uploads\/cooling.png\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-1526 size-full\" src=\"https:\/\/partofthething.com\/thoughts\/wp-content\/uploads\/cooling.png\" alt=\"Noisy graphs\" width=\"2000\" height=\"1200\" srcset=\"https:\/\/partofthething.com\/thoughts\/wp-content\/uploads\/cooling.png 2000w, https:\/\/partofthething.com\/thoughts\/wp-content\/uploads\/cooling-300x180.png 300w, https:\/\/partofthething.com\/thoughts\/wp-content\/uploads\/cooling-768x461.png 768w, https:\/\/partofthething.com\/thoughts\/wp-content\/uploads\/cooling-1024x614.png 1024w\" sizes=\"auto, (max-width: 2000px) 100vw, 2000px\" \/><\/a><figcaption id=\"caption-attachment-1526\" class=\"wp-caption-text\">Analysis of some cooling periods with numerical differentiation.<\/figcaption><\/figure>\n","protected":false},"excerpt":{"rendered":"<p>So in the continuing saga with my mom&#8217;s home-automated furnace, it got extra cold recently and I noticed it wasn&#8217;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 &hellip; <a href=\"https:\/\/partofthething.com\/thoughts\/a-fancy-home-assistant-automation-that-checks-the-weather-and-figures-out-when-to-turn-on-your-heater\/\" class=\"more-link\">Continue reading <span class=\"screen-reader-text\">A fancy Home Assistant automation that checks the weather and figures out when to turn on your heater<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":1524,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"activitypub_content_warning":"","activitypub_content_visibility":"","activitypub_max_image_attachments":4,"activitypub_interaction_policy_quote":"anyone","activitypub_status":"","footnotes":""},"categories":[75],"tags":[],"class_list":["post-1523","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-home-automation"],"_links":{"self":[{"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/posts\/1523","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/comments?post=1523"}],"version-history":[{"count":5,"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/posts\/1523\/revisions"}],"predecessor-version":[{"id":2042,"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/posts\/1523\/revisions\/2042"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/media\/1524"}],"wp:attachment":[{"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/media?parent=1523"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/categories?post=1523"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/tags?post=1523"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}