{"id":2272,"date":"2018-08-08T16:52:59","date_gmt":"2018-08-08T13:52:59","guid":{"rendered":"http:\/\/old.razwww.ro\/?p=2272"},"modified":"2019-02-23T14:33:34","modified_gmt":"2019-02-23T12:33:34","slug":"chronothermostat","status":"publish","type":"post","link":"https:\/\/old.razwww.ro\/index.php\/2018\/08\/08\/chronothermostat\/","title":{"rendered":"Chronothermostat"},"content":{"rendered":"<p>This was my first actual project with Arduino, and after about 6 months it was done (this included learning how to work with Arduinos, waiting for various components from Asia and waiting for the box to be 3D printed) \ud83d\ude42<\/p>\n<p>The schedule (time ranges and respective temperatures) is hardcoded, I don&#8217;t see the use of having a menu to program it.<\/p>\n<p>The button functions are:<br \/>\n* Temperature up, it will reset when the next time range starts<br \/>\n* Temperature down, it will reset when the next time range starts<br \/>\n* Reset, it will set the default temperature for the current time range<br \/>\n* Hold current temperature, it will never reset<br \/>\n* Backlight<br \/>\n* Other info (Uptime, IP address and an easter egg \ud83d\ude42 )<\/p>\n\n\t\t<style>\n\t\t\t#gallery-1 {\n\t\t\t\tmargin: auto;\n\t\t\t}\n\t\t\t#gallery-1 .gallery-item {\n\t\t\t\tfloat: left;\n\t\t\t\tmargin-top: 10px;\n\t\t\t\ttext-align: center;\n\t\t\t\twidth: 33%;\n\t\t\t}\n\t\t\t#gallery-1 img {\n\t\t\t\tborder: 2px solid #cfcfcf;\n\t\t\t}\n\t\t\t#gallery-1 .gallery-caption {\n\t\t\t\tmargin-left: 0;\n\t\t\t}\n\t\t\t\/* see gallery_shortcode() in wp-includes\/media.php *\/\n\t\t<\/style>\n\t\t<div id='gallery-1' class='gallery galleryid-2272 gallery-columns-3 gallery-size-thumbnail'><dl class='gallery-item'>\n\t\t\t<dt class='gallery-icon landscape'>\n\t\t\t\t<a href='https:\/\/old.razwww.ro\/img_20180505_200223\/'><img loading=\"lazy\" decoding=\"async\" width=\"150\" height=\"113\" src=\"https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/IMG_20180505_200223-400x300.jpg\" class=\"attachment-thumbnail size-thumbnail\" alt=\"\" srcset=\"https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/IMG_20180505_200223-400x300.jpg 400w, https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/IMG_20180505_200223-768x576.jpg 768w, https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/IMG_20180505_200223-1024x768.jpg 1024w\" sizes=\"auto, (max-width: 150px) 100vw, 150px\" \/><\/a>\n\t\t\t<\/dt><\/dl><dl class='gallery-item'>\n\t\t\t<dt class='gallery-icon landscape'>\n\t\t\t\t<a href='https:\/\/old.razwww.ro\/img_20180726_222252-1\/'><img loading=\"lazy\" decoding=\"async\" width=\"150\" height=\"113\" src=\"https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/IMG_20180726_222252-1-400x300.jpg\" class=\"attachment-thumbnail size-thumbnail\" alt=\"\" srcset=\"https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/IMG_20180726_222252-1-400x300.jpg 400w, https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/IMG_20180726_222252-1-768x576.jpg 768w, https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/IMG_20180726_222252-1-1024x768.jpg 1024w\" sizes=\"auto, (max-width: 150px) 100vw, 150px\" \/><\/a>\n\t\t\t<\/dt><\/dl><dl class='gallery-item'>\n\t\t\t<dt class='gallery-icon landscape'>\n\t\t\t\t<a href='https:\/\/old.razwww.ro\/img_20180804_031550\/'><img loading=\"lazy\" decoding=\"async\" width=\"150\" height=\"113\" src=\"https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/IMG_20180804_031550-400x300.jpg\" class=\"attachment-thumbnail size-thumbnail\" alt=\"\" srcset=\"https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/IMG_20180804_031550-400x300.jpg 400w, https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/IMG_20180804_031550-768x576.jpg 768w, https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/IMG_20180804_031550-1024x768.jpg 1024w\" sizes=\"auto, (max-width: 150px) 100vw, 150px\" \/><\/a>\n\t\t\t<\/dt><\/dl><br style=\"clear: both\" \/><dl class='gallery-item'>\n\t\t\t<dt class='gallery-icon landscape'>\n\t\t\t\t<a href='https:\/\/old.razwww.ro\/img_20180808_185410\/'><img loading=\"lazy\" decoding=\"async\" width=\"150\" height=\"113\" src=\"https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/IMG_20180808_185410-400x300.jpg\" class=\"attachment-thumbnail size-thumbnail\" alt=\"\" srcset=\"https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/IMG_20180808_185410-400x300.jpg 400w, https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/IMG_20180808_185410-768x576.jpg 768w, https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/IMG_20180808_185410-1024x768.jpg 1024w\" sizes=\"auto, (max-width: 150px) 100vw, 150px\" \/><\/a>\n\t\t\t<\/dt><\/dl><dl class='gallery-item'>\n\t\t\t<dt class='gallery-icon portrait'>\n\t\t\t\t<a href='https:\/\/old.razwww.ro\/web\/'><img loading=\"lazy\" decoding=\"async\" width=\"105\" height=\"150\" src=\"https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/web-279x400.png\" class=\"attachment-thumbnail size-thumbnail\" alt=\"\" srcset=\"https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/web-279x400.png 279w, https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/web.png 515w\" sizes=\"auto, (max-width: 105px) 100vw, 105px\" \/><\/a>\n\t\t\t<\/dt><\/dl><dl class='gallery-item'>\n\t\t\t<dt class='gallery-icon landscape'>\n\t\t\t\t<a href='https:\/\/old.razwww.ro\/web-stats\/'><img loading=\"lazy\" decoding=\"async\" width=\"150\" height=\"99\" src=\"https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/web-stats-400x264.png\" class=\"attachment-thumbnail size-thumbnail\" alt=\"\" srcset=\"https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/web-stats-400x264.png 400w, https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/web-stats-768x507.png 768w, https:\/\/old.razwww.ro\/wp-content\/uploads\/2018\/08\/web-stats.png 796w\" sizes=\"auto, (max-width: 150px) 100vw, 150px\" \/><\/a>\n\t\t\t<\/dt><\/dl><br style=\"clear: both\" \/>\n\t\t<\/div>\n\n<p>The code:<\/p>\n<pre>[code lang=\"cpp\"]\n\/*\n\n  Copyright \u00a9 2018 Razvan Murariu &amp;amp;amp;amp;lt;razwww @gmail.com&amp;amp;amp;amp;gt;\n  This program is free software. It comes without any warranty, to\n  the extent permitted by applicable law. You can redistribute it\n  and\/or modify it under the terms of the Do What The Fuck You Want\n  To Public License, Version 2, as published by Sam Hocevar. See\n  the COPYING file for more details.\n\n  The circuit:\n   Arduino Mega 2560\n   LCD SDA pin to analog pin 20, blue wire\n   LCD SCL pin to analog pin 21, green wire\n   LCD VCC pin to 5V\n   LCD GND pin to ground\n   RTC\/SD CS pin to pin 48\n   RTC\/SD MISO pin to digital 50, gray wire\n   RTC\/SD MOSI pin to pin 51, yellow wire\n   RTC\/SD SCK pin to digital pin 52, purple wire\n   RTC\/SD SCL pin to analog pin 21\n   RTC\/SD SDA pin to analog pin 20\n   RTC\/SD VCC pin to 5V\n   RTC\/SD GND pin to ground\n   Network card MISO pin to digital pin 50, gray wire\n   Network card MOSI pin to digital pin 51, yellow wire\n   Network card SCS pin to digital pin 10\n   Network card SCLK pin to pin 52, purple wire\n   Temp sensor (interior) pin to digital pin 8\n   Temp sensor (exterior) pin to digital pin 9\n   Buttons to digital pins 2-7\n   Relay to analog pin A0 &amp;amp;amp;amp;amp; VIn\n*\/\n\n#include &amp;amp;amp;amp;lt;dht.h&amp;amp;amp;amp;gt;\n#include &amp;amp;amp;amp;lt;bounce2.h&amp;amp;amp;amp;gt;\n#include &amp;amp;amp;amp;lt;wire.h&amp;amp;amp;amp;gt;\n#include &amp;amp;amp;amp;lt;liquidcrystal_i2c.h&amp;amp;amp;amp;gt;\n#include &amp;amp;amp;amp;lt;spi.h&amp;amp;amp;amp;gt;\n#include &amp;amp;amp;amp;lt;ethernet2.h&amp;amp;amp;amp;gt;\n#include &amp;amp;amp;amp;lt;sd.h&amp;amp;amp;amp;gt;\n#include &amp;amp;amp;amp;lt;ds1307rtc.h&amp;amp;amp;amp;gt;  \/\/ a basic DS1307 library that returns time as a time_t\n\n\/\/things that you may change:\n#define displayTime 8000  \/\/ display duration of &quot;other info&quot;\n#define lightTime 8000 \/\/ light duration\n#define buttonDownInterval 3000 \/\/ how long to press the button for &quot;about&quot;\n#define lightOnKeypress 1  \/\/light for each button\n#define prod 1 \/\/ in test mode: permanent backlight\nconst String secret=&quot;********&quot;;  \/\/ secret for URL\n\n#define tempMin 5\n#define tempMax 30\n#define tempConfort 20\n#define hysteresis 0.5\n\n\/\/if you change the number of elements in the arrays you must also change the program table\nconst int timeInterval[4] = {480, 510, 1320, 30}; \/\/ 8:00, 8:30, 22:00, 00:30\nconst int tempInterval[4] = {20, 18, 21, 19}; \/\/ corresponding temp for each time interval\nconst byte numberOfIntervals = sizeof(timeInterval) \/ sizeof(int);\n\nconst long writeTempInterval = 300000;  \/\/ 5 min, interval for writing temps to file\n\/\/end of things that you may change :)\n\n#define modeAuto 0 \/\/ full auto mode, based on the intervals\n#define modeManual 1 \/\/ manual mode, it will reset when the next interval starts\n#define modeHold 2 \/\/ hold mode, permanent manual, it will reset when the RESET button is pushed\n#define modeInfoH 3     \/\/ Info Humidity\n#define modeInfoUp 4    \/\/ Uptime and RAM\n#define modeInfoIp 5  \/\/ IP and mask\n#define modeAbout 6   \/\/ About\n\n#define DHTPin 8     \/\/ pin for temperature sensor\n#define DHTType DHT22   \/\/ type of temperature sensor\n#define DHTExtPin 9     \/\/ pin for exterior temperature sensor\n#define DHTExtType DHT22   \/\/ type of exterior temperature sensor\n\n#define DS1307_I2C_ADDRESS 0x68 \/\/ RTC\n#define relayPort A0  \/\/ analog port for the relay\n\nfloat temp; \/\/ real temperature (inside)\nfloat tempExt; \/\/ real temperature (outside)\nfloat tempAuto; \/\/ temperature according to the auto program\nfloat tempManual; \/\/ temperature set with the +\/- buttons\nfloat humidityIn;\nfloat humidityExt;\nfloat hic; \/\/ heat index in Celsius\nfloat hicExt;\nbyte mode = 0;\nbyte modeReturn = mode; \/\/ remember the mode when displaying info screens\nbyte modeOld = mode; \/\/ used for clearing the display when the mode changes\nint dayOld;  \/\/ used for syncing NTP once a day\nboolean relay = false;\nboolean relayOld = relay; \/\/ used for logging the relay state change\nint totalMinutes;\nint previousMinutes;\nbyte otherInfo = 0;\n\/\/ degree centigrade symbol\nuint8_t degree[8] = {0x8, 0xf4, 0x8, 0x43, 0x4, 0x4, 0x43, 0x0};\n\/\/ for non-I2C LCD byte degree[8] = {B01000, B10100, B01000, B00011, B00100, B00100, B00011, B00000};\n\/\/ flame symbol\nuint8_t flame[8] = {0x12, 0x09, 0x12, 0x09, 0x12, 0x09, 0x12, 0x1F};\n\/\/ for non-I2C LCD byte flame[8] = {0b10010, 0b01001, 0b10010, 0b01001, 0b10010, 0b01001, 0b10010, 0b11111};\nunsigned long timerLight = -1; \/\/ temporary store millis for display duration\nunsigned long timerInfo = -1;\nunsigned long button3Down = 0;  \/\/ for &quot;about&quot;. If the button is pressed when time overflows it won't work\nconst String message = &quot;               Made by Razvan &amp;amp;amp;amp;amp; Coralia&quot;;\nconst uint8_t BUTTON_PINS[6] = {7, 6, 5, 4, 3, 2};\nconst int chipSelect = 48; \/\/ CS pin for SD card\nFile systemLog;\nFile temperatureLog;\nString systemLogFilename;\nString temperatureLogFilename;\nString logString;\nunsigned long previousMillis = 0;   \/\/ will store when temps were written to file\n\n\/\/ethernet settings\nconst boolean dhcp = 0;\nconst byte mac[] = { 0x90, 0xA2, 0xDA, 0x0D, 0x85, 0xD9 };   \/\/physical mac address, mandatory for DHCP or fixed address\nconst byte ip[] = { ***, ***, ***, *** };                 \/\/ ip in lan\nconst byte dnserver[] = { 8, 8, 8, 8 };                \/\/not really used, but needed in the initialization\nconst byte subnet[] = { ***, ***, ***, *** };              \/\/subnet mask\nconst byte gateway[] = { ***, ***, ***, *** };              \/\/ default gateway\nEthernetServer server(80);                       \/\/server port\n\n\/\/ for NTP\nconst unsigned int localPort = 8888;       \/\/ local port to listen for UDP packets\nconst char timeServer[] = &quot;94.53.216.184&quot;; \/\/ time.nist.gov NTP server\nconst int NTP_PACKET_SIZE = 48; \/\/ NTP time stamp is in the first 48 bytes of the message\nbyte packetBuffer[ NTP_PACKET_SIZE]; \/\/buffer to hold incoming and outgoing packets\n\/\/ A UDP instance to let us send and receive packets over UDP\nEthernetUDP Udp;\n\n\/\/ Initialize LCD\nLiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);  \/\/ 20x4\n\n\/\/ Initialize DHT sensor\nDHT dht(DHTPin, DHTType);\nDHT dhtExt(DHTExtPin, DHTExtType);\n\nBounce * buttons = new Bounce[6];\nBounce debouncer = Bounce();  \/\/for about\n\n\/\/ returns the hour and minute when the next interval begins\nvoid nextInterval(int &amp;amp;amp;amp;amp;myHour, int &amp;amp;amp;amp;amp;myMinute) {\n  int nextAutoTime;\n  if (totalMinutes &amp;amp;amp;amp;gt; timeInterval[2] || totalMinutes &amp;amp;amp;amp;lt; timeInterval[3]) {\n    nextAutoTime = timeInterval[3];\n  }\n  else if (totalMinutes &amp;amp;amp;amp;lt; timeInterval[0]) {\n    nextAutoTime = timeInterval[0];\n  }\n  else if (totalMinutes &amp;amp;amp;amp;lt; timeInterval[1]) {\n    nextAutoTime = timeInterval[1];\n  }\n  else {\n    nextAutoTime = timeInterval[2];\n  }\n  myHour = nextAutoTime \/ 60;\n  myMinute = nextAutoTime % 60;\n}\n\n\/\/ - minTotalMinutes: the start of the interval to check\n\/\/ - maxTotalMinutes: the end of the interval to check\n\/\/ - tempAutoLocal: the auto temperature of the interval to check\nvoid compareTemperatureAuto (int minTotalMinutes, int maxTotalMinutes, float tempAutoLocal) {\n  if (mode != modeAuto) { \/\/ this is not our case and we exit the method\n    return;\n  }\n  if (minTotalMinutes &amp;amp;amp;amp;lt; maxTotalMinutes) {\n    if (totalMinutes &amp;amp;amp;amp;gt;= minTotalMinutes &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; totalMinutes &amp;amp;amp;amp;lt; maxTotalMinutes) {\n      if (temp &amp;amp;amp;amp;lt; (tempAuto - hysteresis) ) {\n        relay = 1;\n      }\n      else if (temp &amp;amp;amp;amp;gt; (tempAuto + hysteresis) ) {\n        relay = 0;\n      }\n      tempAuto = tempAutoLocal;\n    }\n  }\n  else { \/\/ roll-over to the next day\n    if (totalMinutes &amp;amp;amp;amp;gt;= minTotalMinutes || totalMinutes &amp;amp;amp;amp;lt; maxTotalMinutes) {\n      if ( temp &amp;amp;amp;amp;lt;= (tempAuto - hysteresis) ) {\n        relay = 1;\n      }\n      else if ( temp &amp;amp;amp;amp;gt;= (tempAuto + hysteresis) ) {\n        relay = 0;\n      }\n      tempAuto = tempAutoLocal;\n    }\n  }\n}\n\n\/\/function to display mode, clock, target temperature &amp;amp;amp;amp;amp; others on the LCD\nvoid displayMode () {\n  char hourMinute [6];\n  if (mode == modeAuto || mode == modeManual || mode == modeHold) {\n    \/\/ current temp\n    lcd.setCursor(0, 0);\n    lcd.print(F(&quot;In=&quot;));\n    lcd.print(temp, 1);\n    lcd.print((char)0);\n\n    lcd.setCursor(10, 0);\n    lcd.print(F(&quot;Out=&quot;));\n    lcd.print(tempExt, 1);\n    lcd.print((char)0);\n\n    lcd.setCursor(0, 2);\n    lcd.print(F(&quot;Set&quot;));\n\n    \/\/clock &amp;amp;amp;amp;amp; time\n    sprintf (hourMinute, &quot;%02u:%02u&quot;, hour(), minute());\n    lcd.setCursor(0, 3);\n    lcd.print(hourMinute);\n    lcd.print(F(&quot;  &quot;));\n    char dayMonthYear [11];\n    sprintf (dayMonthYear, &quot;%02u.%02u.%04u&quot;, day(), month(), year());\n    lcd.print(dayMonthYear);\n\n    \/\/ relay indicator\n    lcd.setCursor(19, 3);\n    relay ? lcd.print((char)1) : lcd.print(F(&quot; &quot;));\n  }\n\n  switch (mode) {\n    int myHour, myMinute;\n    case modeAuto:\n      lcd.setCursor(4, 2);\n      lcd.print(tempAuto,1);\n      lcd.print((char)0);\n      lcd.print(F(&quot; auto&amp;amp;amp;amp;gt;&quot;));\n      if (myHour &amp;amp;amp;amp;lt; 10) {\n        lcd.print(F(&quot;0&quot;));\n      }\n      lcd.print(myHour, DEC);\n      lcd.print(F(&quot;:&quot;));\n      if (myMinute &amp;amp;amp;amp;lt; 10) {\n        lcd.print(F(&quot;0&quot;));\n      }\n      lcd.print(myMinute, DEC);\n      break;\n    case modeManual:\n      lcd.setCursor(4, 2);\n      lcd.print(tempManual,1);\n      lcd.print((char)0);\n      lcd.print(F(&quot; man.&amp;amp;amp;amp;gt;&quot;));\n      nextInterval(myHour, myMinute);\n      if (myHour &amp;amp;amp;amp;lt; 10) {\n        lcd.print(F(&quot;0&quot;));\n      }\n      lcd.print(myHour, DEC);\n      lcd.print(F(&quot;:&quot;));\n      if (myMinute &amp;amp;amp;amp;lt; 10) {\n        lcd.print(F(&quot;0&quot;));\n      }\n      lcd.print(myMinute, DEC);\n      break;\n    case modeHold:\n      lcd.setCursor(4, 2);\n      lcd.print(tempManual,1);\n      lcd.print((char)0);\n      lcd.print(F(&quot; HOLD&quot;));\n      break;\n    case modeInfoH:         \/\/ first info screen\n      lcd.setCursor(0, 0);\n      lcd.print(F(&quot;Humidity:&quot;));\n      lcd.setCursor(0, 1);\n      lcd.print(F(&quot;In=&quot;));\n      lcd.print(humidityIn, 1);\n      lcd.print(F(&quot;%&quot;));\n      lcd.setCursor(10, 1);\n      lcd.print(F(&quot;Out=&quot;));\n      lcd.print(humidityExt, 1);\n      lcd.print(F(&quot;%&quot;));\n\n      lcd.setCursor(0, 2);\n      lcd.print(F(&quot;Heat index:&quot;));\n      lcd.setCursor(0, 3);\n      lcd.print(F(&quot;In=&quot;));\n      lcd.print(hic);\n      lcd.print((char)0);\n      lcd.setCursor(10, 3);\n      lcd.print(F(&quot;Out=&quot;));\n      lcd.print(hicExt);\n      lcd.print((char)0);\n      break;\n    case modeInfoUp:      \/\/ second info screen\n      lcd.setCursor(0, 0);\n      lcd.print(F(&quot;Up &quot;));\n      lcd.print(uptime());\n      lcd.setCursor(0, 1);\n      lcd.print(F(&quot;RAM free &quot;));\n      lcd.print(percentFreeRam());\n      lcd.print(F(&quot;%&quot;));\n      break;\n    case modeInfoIp:      \/\/ third info screen\n      lcd.setCursor(0, 0);\n      lcd.print(F(&quot;Network info: &quot;));\n      dhcp ? lcd.print(F(&quot;DCHP&quot;)) : lcd.print(F(&quot;static&quot;));\n      lcd.setCursor(0, 1);\n      lcd.print(F(&quot;IP &quot;));\n      lcd.print(Ethernet.localIP());\n      lcd.setCursor(0, 2);\n      lcd.print(F(&quot;Mask &quot;));\n      lcd.print(Ethernet.subnetMask());\n      lcd.setCursor(0, 3);\n      lcd.print(F(&quot;GW &quot;));\n      lcd.print(Ethernet.gatewayIP());\n      break;\n    case modeAbout:   \/\/ easteregg :)\n      for (int i=0; i&amp;amp;amp;amp;lt;message .length(); i++) {\n        lcd.clear();\n        lcd.setCursor(3,0);\n        lcd.print(F(&quot;Cronotermostat&quot;));\n        lcd.setCursor(0,2);\n        lcd.print(message.substring(i, i+20));\n        delay(300);\n      }\n      mode = modeReturn;\n      break;\n  }\n}\n\nvoid funcDisplayLastMode() {\n  mode = modeReturn;\n}\n\nvoid noBacklight()\n{\n  \/\/ todo Coralia - refactor\n  lcd.noBacklight();\n}\n\nvoid switchBack(long waitTime, unsigned long &amp;amp;amp;amp;amp;timer, void (*function)()) {\n  if ( timer != -1 &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; millis() - timer &amp;amp;amp;amp;gt; waitTime) {\n    \/\/ call parameter method\n    (*function)();\n    timer = -1;\n  }\n}\n\nint freeRam () {\n  extern int __heap_start, *__brkval;\n  int v;\n  return (int) &amp;amp;amp;amp;amp;v - (__brkval == 0 ? (int) &amp;amp;amp;amp;amp;__heap_start : (int) __brkval);\n}\n\nlong percentFreeRam () {\n  long freeR = freeRam();\n  return ( freeR * 100 ) \/ 8192;   \/\/ for Uno\/Nano: \/ 2048\n}\n\nvoid lightOn() {\n  timerLight = millis();\n  lcd.backlight();\n}\n\nvoid pushTempPlus () {\n    if (mode == modeAuto) {\n    mode = modeManual;\n    \/\/ if the current program is cooler than the confort temp, go directy to confort temp\n    if (tempAuto &amp;amp;amp;amp;lt; tempConfort) {\n      tempManual = tempConfort;\n    }\n    else {\n      tempManual = tempAuto + 0.5;\n    }\n  }\n  else {\n    tempManual = tempManual + 0.5;\n  }\n}\n\nvoid pushTempMinus () {\n    if (mode == modeAuto) {\n    mode = modeManual;\n    tempManual = tempAuto - 0.5;\n  }\n  else {\n    tempManual = tempManual - 0.5;\n  }\n}\n\nvoid pushHold () {\n  if (mode == modeAuto) {\n    mode = modeHold;\n    tempManual = tempAuto;\n  }\n  else if (mode == modeManual) {\n    mode = modeHold;\n  }\n  else if (tempAuto == tempManual) { \/\/we already are in modeHold, so setureset to what it was before\n    mode = modeAuto;\n  }\n  else {\n    mode = modeManual;\n  }\n}\n\nvoid getButton () {\n  for (int i = 0; i &amp;amp;amp;amp;lt; 6; i++)  {    \/\/ Update the Bounce instance\n    buttons[i].update();\n    if (buttons[i].rose()) {\n      if (i == 3) {\n        button3Down = 0;\n      }\n    }\n    if ( buttons[i].fell() ) {    \/\/ If it fell, do the appropriate action\n\n      if (i == 0) { \/\/light\n        lightOn();\n        logInfo(&quot;physical button 'light'&quot;);\n      }\n\n      else if (i == 1) { \/\/ temp +\n        if ( lightOnKeypress ) {\n          lightOn();\n        }\n        pushTempPlus();\n        logInfo(&quot;physical button 'temp+'&quot;);\n      }\n\n      else if (i == 2) { \/\/ temp -\n\n        if ( lightOnKeypress ) {\n          lightOn();\n        }\n        pushTempMinus();\n        logInfo(&quot;physical button 'temp-'&quot;);\n      }\n\n      else if (i == 3) { \/\/ reset to auto + about\n        if ( lightOnKeypress ) {\n          lightOn();\n        }\n        mode = modeAuto;\n        tempManual = tempAuto;\n        button3Down = millis();\/\/ start counting for long press\n        logInfo(&quot;physical button 'reset'&quot;);\n      }\n\n      else if (i == 4) { \/\/ mode hold\n        if ( lightOnKeypress ) {\n          lightOn();\n        }\n        pushHold();\n        logInfo(&quot;physical button 'hold'&quot;);\n      }\n\n      else if (i == 5) { \/\/ other info\n\n        if ( lightOnKeypress ) {\n          lightOn();\n        }\n        switch (mode) {\n          case modeAuto:\n          case modeManual:\n          case modeHold:\n            modeReturn = mode;\n            mode = modeInfoH;\n            timerInfo = millis();\n            break;\n          case modeInfoH:\n            mode = modeInfoUp;\n            timerInfo = millis();\n            break;\n          case modeInfoUp:\n            mode = modeInfoIp;\n            timerInfo = millis();\n            break;\n          case modeInfoIp:\n            mode = modeReturn;\n            break;\n        }\n        logInfo(&quot;physical button 'other info'&quot;);\n      }\n    }\n  }\n  if (button3Down != 0 &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; (millis() - button3Down &amp;amp;amp;amp;gt; buttonDownInterval) ) {\n    modeReturn = mode;\n    mode = modeAbout;\n    timerInfo = millis();\n    button3Down = 0;\n    logInfo(&quot;physical button 'reset' long&quot;);\n  }\n}\n\nString uptime() {  \/\/ todo? rollover\n  long up = millis();\n  long days = 0;\n  long hours = 0;\n  long mins = 0;\n  mins = up \/ 1000 \/ 60;\n  hours = mins \/ 60;\n  days = hours \/ 24;\n  mins = mins - (hours * 60);\n  hours = hours - (days * 24);\n  String uptime = &quot;&quot;;\n  if (days &amp;amp;amp;amp;gt; 0) {\n    uptime += days;\n    uptime += &quot;d &quot;;\n  }\n  uptime += hours;\n  uptime += &quot;h &quot;;\n  uptime += mins;\n  uptime += &quot;m &quot;;\n  return uptime;\n}\n\nvoid listStatFiles(EthernetClient client) {\n  File workingDir = SD.open(&quot;\/data\/&quot;);\n  client.println(F(&quot;&amp;amp;amp;amp;lt;ul&amp;amp;amp;amp;gt;&quot;));\n    while(true) {\n      File entry =  workingDir.openNextFile();\n      if (! entry) {\n        break;\n      }\n      client.print(F(&quot;&amp;amp;amp;amp;lt;li&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;a href=\\&quot;\/HC.htm?file=&quot;));\n      client.print(entry.name());\n      client.print(F(&quot;\\&quot;&amp;amp;amp;amp;gt;&quot;));\n      client.print(entry.name());\n      client.println(F(&quot;&amp;amp;amp;amp;lt;\/a&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/li&amp;amp;amp;amp;gt;&quot;));\n      entry.close();\n    }\n  client.println(F(&quot;&amp;amp;amp;amp;lt;\/ul&amp;amp;amp;amp;gt;&quot;));\n  workingDir.close();\n}\n\nvoid listLogFiles(EthernetClient client) {\n  File workingDir = SD.open(&quot;\/logs\/&quot;);\n  client.println(F(&quot;&amp;amp;amp;amp;lt;ul&amp;amp;amp;amp;gt;&quot;));\n    while(true) {\n      File entry =  workingDir.openNextFile();\n      if (! entry) {\n        break;\n      }\n      client.print(F(&quot;&amp;amp;amp;amp;lt;li&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;a href=\\&quot;&quot;));\n      client.print(entry.name());\n      client.print(F(&quot;\\&quot;&amp;amp;amp;amp;gt;&quot;));\n      client.print(entry.name());\n      client.println(F(&quot;&amp;amp;amp;amp;lt;\/a&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/li&amp;amp;amp;amp;gt;&quot;));\n      entry.close();\n    }\n  client.println(F(&quot;&amp;amp;amp;amp;lt;\/ul&amp;amp;amp;amp;gt;&quot;));\n  workingDir.close();\n}\n\n\/\/ standard response header\nvoid httpOK(EthernetClient client, String type) {\n  client.println(F(&quot;HTTP\/1.1 200 OK&quot;));\n  client.println(&quot;Content-Type: text\/&quot;+type);\n  client.println(F(&quot;Connection: close&quot;));\n  client.println();\n  if (type == &quot;html&quot;) {\n    client.println(F(&quot;&amp;amp;amp;amp;lt; !DOCTYPE html&amp;amp;amp;amp;gt;&quot;));\n    client.println(F(&quot;&amp;amp;amp;amp;lt;link rel=\\&quot;icon\\&quot; href=\\&quot;data:;base64,=\\&quot;\/&amp;amp;amp;amp;gt;&quot;));  \/\/ avoid favicon requests\n  }\n}\n\n\/\/ 404 response header\nvoid http404(EthernetClient client) {\n  client.println(F(&quot;HTTP\/1.1 404 Not Found&quot;));\n  client.println(F(&quot;Content-Type: text\/plain&quot;));\n  client.println();\n  client.println(F(&quot;404 Not Found&quot;));\n}\n\nvoid writeSD(String logString, String filename) {  \/\/ writes 'logString' to 'filename' on SD card\n  File dataFile = SD.open(filename, FILE_WRITE);\n  if (!dataFile) {\n    Serial.print(F(&quot;error opening &quot;));\n    Serial.println(filename);\n  }\n  else {\n    dataFile.println(logString);\n    dataFile.close();\n  }\n}\n\nvoid logInfo(String logString) {  \/\/ writes messages to console and to the .log file\n  File dataFile = SD.open(systemLogFilename, FILE_WRITE);\n  if (!dataFile) {\n    Serial.print(F(&quot;error opening &quot;));\n    Serial.println(systemLogFilename);\n  }\n  else {\n    String tempLog = &quot;&quot;;\n    char fullDateAndTime [20];\n    sprintf (fullDateAndTime, &quot;%04u.%02u.%02u %02u:%02u:%02u&quot;, year(), month(), day(), hour(), minute(), second());\n    tempLog += fullDateAndTime;\n    tempLog += &quot; &quot;;\n    tempLog += now();\n    tempLog += &quot; &quot;;\n    tempLog += logString;\n    dataFile.println(tempLog);\n    dataFile.close();\n    Serial.println(tempLog);\n  }\n}\n\n\/\/ sync with NTP server\n\/\/ send an NTP request to the time server at the given address\nvoid sendNTPpacket(char* address) {\n  \/\/ set all bytes in the buffer to 0\n  memset(packetBuffer, 0, NTP_PACKET_SIZE);\n  \/\/ Initialize values needed to form NTP request\n  \/\/ (see URL above for details on the packets)\n  packetBuffer[0] = 0b11100011;   \/\/ LI, Version, Mode\n  packetBuffer[1] = 0;     \/\/ Stratum, or type of clock\n  packetBuffer[2] = 6;     \/\/ Polling Interval\n  packetBuffer[3] = 0xEC;  \/\/ Peer Clock Precision\n  \/\/ 8 bytes of zero for Root Delay &amp;amp;amp;amp;amp; Root Dispersion\n  packetBuffer[12]  = 49;\n  packetBuffer[13]  = 0x4E;\n  packetBuffer[14]  = 49;\n  packetBuffer[15]  = 52;\n\n  \/\/ all NTP fields have been given values, now\n  \/\/ you can send a packet requesting a timestamp:\n  Udp.beginPacket(address, 123); \/\/NTP requests are to port 123\n  Udp.write(packetBuffer, NTP_PACKET_SIZE);\n  Udp.endPacket();\n}\n\nString displayAddress(IPAddress address)\n{\n return String(address[0]) + &quot;.&quot; +\n        String(address[1]) + &quot;.&quot; +\n        String(address[2]) + &quot;.&quot; +\n        String(address[3]);\n}\n\n\/\/for AJAX display SET temperature\nvoid GetTemp(EthernetClient client)\n{\n  client.print(F(&quot;&amp;amp;amp;amp;lt;strong&amp;amp;amp;amp;gt;Set at: &amp;amp;amp;amp;lt;\/strong&amp;amp;amp;amp;gt;&quot;));\n  if ( mode == modeAuto ){\n    client.print(tempAuto, 1);\n  }\n  if ( mode == modeManual || mode == modeHold ){\n    client.print(tempManual, 1);\n  }\n  client.println(F(&quot; &amp;amp;amp;amp;amp;deg;C &amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;&quot;));\n\n  client.print(F(&quot;&amp;amp;amp;amp;lt;strong&amp;amp;amp;amp;gt;Relay: &amp;amp;amp;amp;lt;\/strong&amp;amp;amp;amp;gt;&quot;));\n  if (relay) {\n    client.print(F(&quot;&amp;amp;amp;amp;amp;#9728;  ON  &amp;amp;amp;amp;amp;#9728;&quot;));\n  }\n  else {\n    client.print(F(&quot;OFF&quot;));\n  }\n\n  client.print(F(&quot;&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;strong&amp;amp;amp;amp;gt;Program: &amp;amp;amp;amp;lt;\/strong&amp;amp;amp;amp;gt;&quot;));\n  if ( mode == modeAuto ){\n    client.print(F(&quot;auto until &quot;));\n    int myHour, myMinute;\n    nextInterval(myHour, myMinute);\n    if (myHour &amp;amp;amp;amp;lt; 10) {\n      client.print(F(&quot;0&quot;));\n    }\n    client.print(myHour, DEC);\n    client.print(F(&quot;:&quot;));\n    if (myMinute &amp;amp;amp;amp;lt; 10) {\n      client.print(F(&quot;0&quot;));\n    }\n    client.println(myMinute, DEC);\n}\n\n  if ( mode == modeManual){\n    client.print(F(&quot;manual until &quot;));\n    int myHour, myMinute;\n        nextInterval(myHour, myMinute);\n    if (myHour &amp;amp;amp;amp;lt; 10) {\n      client.print(F(&quot;0&quot;));\n    }\n    client.print(myHour, DEC);\n    client.print(F(&quot;:&quot;));\n    if (myMinute &amp;amp;amp;amp;lt; 10) {\n      client.print(F(&quot;0&quot;));\n    }\n    client.println(myMinute, DEC);\n  }\n  if (mode == modeHold ){\n    client.print(F(&quot;HOLD&quot;));\n  }\n}\n\nvoid setup() {\n  Wire.begin();\n  Serial.begin(9600);\n\n  pinMode(LED_BUILTIN, OUTPUT);\n  digitalWrite(LED_BUILTIN, LOW);\n\n  digitalWrite(relayPort, HIGH); \/\/to avoid bouncing at startup\n  pinMode(relayPort, OUTPUT);\n\n  pinMode(2, OUTPUT);\n  pinMode(53, OUTPUT);  \/\/needed for the network card\n\n  setSyncProvider(RTC.get); \/\/ get the time from the RTC\n\n  \/\/ set the initial time for RTC:\n  \/\/ setTime(hours, minutes, seconds, days, months, years);\n\/\/  setTime(23, 42, 0, 4, 4, 2018);\n\n  lcd.begin(20, 4);\n  if ( prod ) {\n    lcd.noBacklight();\n  }\n  else {\n    lcd.backlight();\n  }\n  lcd.createChar(0, degree);\n  lcd.createChar(1, flame);\n\n  dht.begin();\n  dhtExt.begin();\n\n  for (int i = 0; i &amp;amp;amp;amp;lt; 6; i++) {\n    buttons[i].attach( BUTTON_PINS[i] , INPUT_PULLUP  );       \/\/setup the bounce instance for the current button\n    buttons[i].interval(25);              \/\/ interval in ms\n  }\n\n  Serial.println(F(&quot;Initializing SD card...&quot;));\n  if (!SD.begin(chipSelect)) {\n    Serial.println(F(&quot;SD card initialization failed!&quot;));\n    return;\n  }\n  logInfo(&quot;SD card initialization done&quot;);\n\n  if (dhcp == 0) {\n    Ethernet.begin(mac,ip,dns,gateway,subnet);     \/\/ initialize Ethernet device with fixed address\n  }\n  else {\n    Ethernet.begin(mac);  \/\/ initialize Ethernet device with DHCP\n  }\n  server.begin();                                \/\/ start to listen for clients\n  logInfo(&quot;Server started&quot;);\n  String tempIP = &quot;IP address &quot;;\n  dhcp ? tempIP += &quot;(DHCP): &quot; : tempIP += &quot;(static): &quot;;\n  tempIP += displayAddress(Ethernet.localIP());\n  logInfo(tempIP);\n  tempIP = &quot;Subnet mask: &quot;;\n  tempIP += displayAddress(Ethernet.subnetMask());\n  logInfo(tempIP);\n  tempIP = &quot;Gateway: &quot;;\n  tempIP += displayAddress(Ethernet.gatewayIP());\n  logInfo(tempIP);\n\n  Udp.begin(localPort);  \/\/ for NTP\n\n  lightOn();  \/\/ light up once at startup\n\n  delay(2000); \/\/ read DHT sensors, enable in prod\n}\n\nvoid loop() {\n\n  \/\/ Reading temperature or humidity takes about 250 milliseconds\n  \/\/ Sensor readings may also be up to 2 seconds 'old'\n\n  \/\/ Read temperature as Celsius (the default)\n  temp = dht.readTemperature();\n  tempExt = dhtExt.readTemperature();\n\n  humidityIn = dht.readHumidity();\n  humidityExt = dhtExt.readHumidity();\n\n  char yearMonth [8];\n  sprintf (yearMonth, &quot;%04u-%02u&quot;, year(), month());\n  systemLogFilename = String (&quot;logs\/&quot;) + yearMonth + String(&quot;.log&quot;);\n  temperatureLogFilename = String (&quot;data\/&quot;) + yearMonth + String(&quot;.csv&quot;);\n\n  \/\/ regularly write temps to SD card\n  unsigned long currentMillis = millis();\n  if ( currentMillis - previousMillis &amp;amp;amp;amp;gt;= writeTempInterval ) {\n    previousMillis = currentMillis;\n    String tempLog = &quot;&quot;;\n    tempLog += now();\n    tempLog += &quot;,&quot;;\n    tempLog += (temp);\n    tempLog += &quot;,&quot;;\n    tempLog += (tempExt);\n    writeSD(tempLog, temperatureLogFilename);\n  }\n\n\/\/NTP sync once a day\nif (day() != dayOld) {\n  dayOld = day();\n  logInfo(&quot;Trying to sync the RTC...&quot;);\n  sendNTPpacket(timeServer); \/\/ send an NTP packet to a time server\n\n  \/\/ wait to see if a reply is available\n  delay(1000);\n  if (Udp.parsePacket()) {\n    \/\/ We've received a packet, read the data from it\n    Udp.read(packetBuffer, NTP_PACKET_SIZE); \/\/ read the packet into the buffer\n\n    \/\/ the timestamp starts at byte 40 of the received packet and is four bytes,\n    \/\/ or two words, long. First, extract the two words:\n\n    unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);\n    unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);\n    \/\/ combine the four bytes (two words) into a long integer\n    \/\/ this is NTP time (seconds since Jan 1 1900):\n    unsigned long secsSince1900 = highWord &amp;amp;amp;amp;lt; &amp;amp;amp;amp;lt; 16 | lowWord;\n\n    \/\/ Unix time starts on Jan 1 1970. In seconds, that's 2208988800:\n    const unsigned long seventyYears = 2208988800UL;\n    \/\/ subtract seventy years:\n    unsigned long epoch = secsSince1900 - seventyYears;\n    epoch += (3 * 3600L); \/\/ adjust for GMT+3\n    epoch -= 1; \/\/ adjust for the delay above\n\n    String logNTP = &quot;RTC adjusted by &quot;;\n    int difference = epoch-now(); \/\/was unsigned\n    logNTP += difference;\n    logNTP += (&quot; seconds&quot;);\n    logInfo (logNTP);\n    RTC.set(epoch);\n\n  }\n  Ethernet.maintain();\n}\n\n  \/\/ Check if any reads failed and exit early (to try again).\n  if (isnan(humidityIn) || isnan(temp)) {\n    logInfo(&quot;Failed to read from DHT sensor!&quot;);\n    return;\n  }\n\n  if (isnan(humidityExt) || isnan(tempExt)) {\n    logInfo(&quot;Failed to read from outside DHT sensor!&quot;);\n\/\/    return;\n  }\n\n  \/\/ Compute heat index in Celsius (isFahreheit = false)\n  hic = dht.computeHeatIndex(temp, humidityIn, false);\n  hicExt = dhtExt.computeHeatIndex(tempExt, humidityExt, false);\n\n  previousMinutes = totalMinutes;\n  totalMinutes = hour() * 60 + minute();\n\n  \/\/main code\n  switch (mode) {\n    case modeAuto:\n      compareTemperatureAuto(timeInterval[0], timeInterval[1], tempInterval[0]);\n      compareTemperatureAuto(timeInterval[1], timeInterval[2], tempInterval[1]);\n      compareTemperatureAuto(timeInterval[2], timeInterval[3], tempInterval[2]);\n      compareTemperatureAuto(timeInterval[3], timeInterval[0], tempInterval[3]);\n      break;\n    case modeManual:\n      if (temp &amp;amp;amp;amp;lt;= (tempManual - hysteresis) ) {\n        relay = 1;\n      }\n      else if (temp &amp;amp;amp;amp;gt;= (tempManual + hysteresis) ) {\n        relay = 0;\n      }\n      if ((totalMinutes != previousMinutes) &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; (\n        totalMinutes == timeInterval[0] || totalMinutes == timeInterval[1] ||\n          totalMinutes == timeInterval[2] || totalMinutes == timeInterval[3])) {\n        mode = modeAuto;\n      }\n      break;\n    case modeHold:\n      if (temp &amp;amp;amp;amp;lt; = (tempManual - hysteresis) ) {\n        relay = 1;\n      }\n      else if (temp &amp;amp;amp;amp;gt;= (tempManual + hysteresis) ) {\n        relay = 0;\n      }\n      break;\n  }\n\n  \/\/function to display mode &amp;amp;amp;amp;amp; target temperature\n  displayMode ();\n\n  getButton();\n\n  switchBack(displayTime, timerInfo, funcDisplayLastMode);\n  switchBack(lightTime, timerLight, *noBacklight);\n\n  if (mode == modeManual &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; tempManual == tempAuto) {\n    mode = modeAuto;\n  }\n\n  if (mode != modeOld) {\n    lcd.clear();\n    modeOld = mode;\n    switch (mode) {  \/\/ log the mode change\n      case 0:\n        logInfo(&quot;Mod AUTO&quot;);\n        break;\n      case 1:\n        logInfo(&quot;Mod MANUAL&quot;);\n        break;\n      case 2:\n        logInfo(&quot;Mod HOLD&quot;);\n        break;\n      case 3:\n        logInfo(&quot;Mod INFO humidity&quot;);\n        break;\n      case 4:\n        logInfo(&quot;Mod INFO uptime and RAM&quot;);\n        break;\n      case 5:\n        logInfo(&quot;Mod INFO IP&quot;);\n        break;\n      case 6:\n        logInfo(&quot;Mod ABOUT&quot;);\n        break;\n    }\n  }\n\n  if (relay == 0) {\n    digitalWrite(relayPort, HIGH);\n  }\n  else if (relay == 1) {\n    digitalWrite(relayPort, LOW);\n  }\n\n  if (relay != relayOld) {  \/\/ log the relay change\n    relay ? logInfo(&quot;Relay ON&quot;) : logInfo(&quot;Relay OFF&quot;);\n    relayOld = relay;\n  }\n\n  tempManual = constrain(tempManual, tempMin, tempMax);\n\n  EthernetClient client = server.available();    \/\/ look for the client\n    \/\/ webserver\n    if (client) {  \/\/ got client?\n      String response=&quot;&quot;;\n      while (client.connected()) {\n          if (client.available()) {\n              char c=client.read();\n              Serial.write(c);\n              if (c=='\\n') {\n                  \/\/ end of the first line\n                  break;\n              }\n              response+=c;\n          }\n      }\n      \/\/only log server responses if it's not an ajax query\n      if ( response.indexOf(&quot;ajax_text&quot;) &amp;amp;amp;amp;lt; 0 ) {\n        logInfo(response);\n      }\n\n      if (response.startsWith(&quot;GET \/stat?secret=&quot; + secret)) {\n        httpOK(client, &quot;html&quot;);  \/\/standard response header\n        client.println(F(&quot;&amp;amp;amp;amp;lt;h2&amp;amp;amp;amp;gt;List of files:&quot;));\n        listStatFiles(client);\n      }\n\n      else if (response.startsWith(&quot;GET \/logs?secret=&quot; + secret)) {\n        httpOK(client, &quot;html&quot;);  \/\/standard response header\n        client.println(F(&quot;&amp;amp;amp;amp;lt;h2&amp;amp;amp;amp;gt;List of files:&amp;amp;amp;amp;lt;\/h2&amp;amp;amp;amp;gt;&quot;));\n        listLogFiles(client);\n      }\n\n      else if (response.indexOf(&quot;file=&quot;) &amp;amp;amp;amp;gt; 0 &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; response.indexOf(&quot;.LOG&quot;) &amp;amp;amp;amp;lt; 0) {\n        String filename;\n        int slash = response.indexOf('\/');\n        filename = response.substring(slash+1, slash+7);\n\n        File file = SD.open(filename,FILE_READ);\n        if (!file) {\n          http404(client);\n        }\n\n        httpOK(client, &quot;html&quot;);  \/\/standard response header\n\n        while(file.available()) {\n          int num_bytes_read;\n          uint8_t byte_buffer[32];\n\n          num_bytes_read=file.read(byte_buffer,32);\n          client.write(byte_buffer,num_bytes_read);\n        }\n        file.close();\n      }\n      \/\/ feed CSV files requested by HC.htm\n      else if (response.indexOf(&quot;file=&quot;) &amp;amp;amp;amp;lt; 0 &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; response.indexOf(&quot;.CSV&quot;) &amp;amp;amp;amp;gt; 0) {\n        String filename;\n        int slash = response.indexOf('\/');\n        filename = &quot;data\/&quot; + response.substring(slash+1, slash+12);\n\n        File file = SD.open(filename,FILE_READ);\n        if (!file) {\n          http404(client);\n        }\n\n        httpOK(client, &quot;html&quot;);  \/\/standard response header\n        while(file.available()) {\n          int num_bytes_read;\n          uint8_t byte_buffer[32];\n\n          num_bytes_read=file.read(byte_buffer,32);\n          client.write(byte_buffer,num_bytes_read);\n        }\n        file.close();\n      }\n      \/\/ feed LOG files\n      else if ( response.indexOf(&quot;.LOG&quot;) &amp;amp;amp;amp;gt; 0) {\n        String filename;\n        int slash = response.indexOf('\/');\n        filename = &quot;logs\/&quot; + response.substring(slash+1, slash+12);\n\n        File file = SD.open(filename,FILE_READ);\n        if (!file) {\n          http404(client);\n        }\n\n        httpOK(client, &quot;plain&quot;);  \/\/standard response header\n        while(file.available()) {\n          int num_bytes_read;\n          uint8_t byte_buffer[32];\n\n          num_bytes_read=file.read(byte_buffer,32);\n          client.write(byte_buffer,num_bytes_read);\n        }\n        file.close();\n      }\n\n      else if (response.indexOf(&quot;ajax_text&quot;) &amp;amp;amp;amp;gt; -1) {\n        GetTemp(client);\n      }\n\n      else if (response.startsWith(&quot;GET \/crono?secret=&quot; + secret)) {\n        httpOK(client, &quot;html&quot;);  \/\/standard response header\n        client.println(F(&quot;&amp;amp;amp;amp;lt;html&amp;amp;amp;amp;gt;&quot;));\n        client.println(F(&quot;&amp;amp;amp;amp;lt;head&amp;amp;amp;amp;gt;&quot;));\n        client.println(F(&quot;&amp;amp;amp;amp;lt;title&amp;amp;amp;amp;gt;Cronotermostat&amp;amp;amp;amp;lt;\/title&amp;amp;amp;amp;gt;&quot;));\n        client.println(F(&quot;&amp;amp;amp;amp;lt;script&amp;amp;amp;amp;gt;&quot;));\n\n        client.println(&quot;function GetTemp() {&quot;);\n        client.println(&quot;nocache = \\&quot;&amp;amp;amp;amp;amp;nocache=\\&quot;\\\n                                         + Math.random() * 1000000;&quot;);\n        client.println(&quot;var request = new XMLHttpRequest();&quot;);\n        client.println(&quot;request.onreadystatechange = function() {&quot;);\n        client.println(&quot;if (this.readyState == 4) {&quot;);\n        client.println(&quot;if (this.status == 200) {&quot;);\n        client.println(&quot;if (this.responseText != null) {&quot;);\n        client.println(&quot;document.getElementById(\\&quot;ajax_div\\&quot;)\\\n.innerHTML = this.responseText;&quot;);\n        client.println(&quot;}}}}&quot;);\n        client.println(\n        &quot;request.open(\\&quot;GET\\&quot;, \\&quot;ajax_text\\&quot; + nocache, true);&quot;);\n        client.println(&quot;request.send(null);&quot;);\n        client.println(&quot;setTimeout('GetTemp()', 1000);&quot;);\n        client.println(&quot;}&quot;);\n        client.println(&quot;&amp;amp;amp;amp;lt;\/script&amp;amp;amp;amp;gt;&quot;);\n        client.println(&quot;&amp;amp;amp;amp;lt;\/head&amp;amp;amp;amp;gt;&quot;);\n        client.println(&quot;&amp;amp;amp;amp;lt;body onload=\\&quot;GetTemp()\\&quot;&amp;amp;amp;amp;gt;&quot;);\n\n        \/\/temperature\n        client.print(F(&quot;&amp;amp;amp;amp;lt;table&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;tr&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td colspan=\\&quot;2\\&quot;&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;strong&amp;amp;amp;amp;gt;Temperature:&amp;amp;amp;amp;lt;\/strong&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/tr&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;tr&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;strong&amp;amp;amp;amp;gt;In = &quot;));\n        client.print(temp,1);\n        client.print(F(&quot; &amp;amp;amp;amp;amp;deg;C &amp;amp;amp;amp;lt;\/strong&amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;nbsp;&amp;amp;amp;amp;amp;nbsp;&amp;amp;amp;amp;amp;nbsp;&amp;amp;amp;amp;amp;nbsp;&quot;));\n        client.print(F(&quot;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;Out = &quot;));\n        client.print(tempExt,1);\n        client.print(F(&quot; &amp;amp;amp;amp;amp;deg;C&quot;));\n        client.println(F(&quot;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/tr&amp;amp;amp;amp;gt;&quot;));\n\n        \/\/humidity\n        client.print(F(&quot;&amp;amp;amp;amp;lt;tr&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td colspan=\\&quot;2\\&quot;&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;strong&amp;amp;amp;amp;gt;Humidity:&amp;amp;amp;amp;lt;\/strong&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/tr&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;tr&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;In: &quot;));\n        client.print(humidityIn, 1);\n        client.print(F(&quot; %&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;Out: &quot;));\n        client.print(humidityExt, 0);\n        client.println(F(&quot; %&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/tr&amp;amp;amp;amp;gt;&quot;));\n        client.print(F(&quot;&amp;amp;amp;amp;lt;tr&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td colspan=\\&quot;2\\&quot;&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;strong&amp;amp;amp;amp;gt;Heat index:&amp;amp;amp;amp;lt;\/strong&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/tr&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;tr&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;In: &quot;));\n        client.print(hic);\n        client.print(F(&quot;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;Out: &quot;));\n        client.print(hicExt);\n        client.println(F(&quot;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/tr&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;tr&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td colspan=\\&quot;2\\&quot;&amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;nbsp;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/tr&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/table&amp;amp;amp;amp;gt;&quot;));\n\n        \/\/mode &amp;amp;amp;amp;amp; next interval\n        client.print(F(&quot;&amp;amp;amp;amp;lt;div id=\\&quot;ajax_div\\&quot;&amp;amp;amp;amp;gt;Loading...&amp;amp;amp;amp;lt;\/div&amp;amp;amp;amp;gt;&quot;));\n\n        \/\/ buttons\n        client.print(F(&quot;&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;input type=submit value=Plus style=width:80px;height:25px onClick=location.href='\/crono?secret=&quot;));\n        client.print(secret);\n        client.println(F(&quot;?tempPlus;'\/&amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;nbsp;&quot;));\n\n        client.print(F(&quot;&amp;amp;amp;amp;lt;input type=submit value=Minus style=width:80px;height:25px onClick=location.href='\/crono?secret=&quot;));\n        client.print(secret);\n        client.println(F(&quot;?tempMinus;'\/&amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;nbsp;&quot;));\n\n        client.print(F(&quot;&amp;amp;amp;amp;lt;input type=submit value=Reset style=width:80px;height:25px onClick=location.href='\/crono?secret=&quot;));\n        client.print(secret);\n        client.println(F(&quot;?reset;'\/&amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;nbsp;&quot;));\n\n        client.print(F(&quot;&amp;amp;amp;amp;lt;input type=submit value=Hold style=width:80px;height:25px onClick=location.href='\/crono?secret=&quot;));\n        client.print(secret);\n        client.println(F(&quot;?hold;'\/&amp;amp;amp;amp;gt;&quot;));\n\n        client.print(F(&quot;&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;a href=\\&quot;.\/stat?secret=&quot;));\n        client.print(secret);\n        client.println(F(&quot;\\&quot; target=_blank&amp;amp;amp;amp;gt;Statistics&amp;amp;amp;amp;lt;\/a&amp;amp;amp;amp;gt;&quot;));\n\n        client.print(F(&quot;&amp;amp;amp;amp;amp;nbsp;&amp;amp;amp;amp;amp;nbsp;&amp;amp;amp;amp;amp;nbsp; &amp;amp;amp;amp;lt;a href=\\&quot;.\/logs?secret=&quot;));\n        client.print(secret);\n        client.println(F(&quot;\\&quot; target=_blank&amp;amp;amp;amp;gt;Logs&amp;amp;amp;amp;lt;\/a&amp;amp;amp;amp;gt;&quot;));\n\n        \/\/ program table\n        client.print(F(&quot;&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;Program: &amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;&quot;));\n        client.print(F(&quot;&amp;amp;amp;amp;lt;table&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;tr&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&quot;));\n        client.print(timeInterval[0] \/ 60);\n        client.print(F(&quot;:&quot;));\n        if ( timeInterval[0] % 60 &amp;amp;amp;amp;lt; 10 ){\n          client.print(F(&quot;0&quot;));\n        }\n        client.print(timeInterval[0] % 60);\n        client.print(F(&quot;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&quot;));\n        client.print(timeInterval[1] \/ 60);\n        client.print(F(&quot;:&quot;));\n        if ( timeInterval[1] % 60 &amp;amp;amp;amp;lt; 10 ){\n          client.print(F(&quot;0&quot;));\n        }\n        client.print(timeInterval[1] % 60);\n        client.print(F(&quot;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&quot;));\n        client.print(timeInterval[2] \/ 60);\n        client.print(F(&quot;:&quot;));\n        if ( timeInterval[2] % 60 &amp;amp;amp;amp;lt; 10 ){\n          client.print(F(&quot;0&quot;));\n        }\n        client.print(timeInterval[2] % 60);\n        client.print(F(&quot;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&quot;));\n        client.print(timeInterval[3] \/ 60);\n        client.print(F(&quot;:&quot;));\n        if ( timeInterval[3] % 60 &amp;amp;amp;amp;lt; 10 ){\n          client.print(F(&quot;0&quot;));\n        }\n        client.print(timeInterval[3] % 60);\n        client.print(F(&quot;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/tr&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;tr&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&quot;));\n        client.print(tempInterval[0]);\n        client.print(F(&quot;&amp;amp;amp;amp;amp;deg;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&quot;));\n        client.print(tempInterval[1]);\n        client.print(F(&quot;&amp;amp;amp;amp;amp;deg;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&quot;));\n        client.print(tempInterval[2]);\n        client.print(F(&quot;&amp;amp;amp;amp;amp;deg;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;&quot;));\n        client.print(tempInterval[3]);\n        client.println(F(&quot;&amp;amp;amp;amp;amp;deg;&amp;amp;amp;amp;lt;\/td&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/tr&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;\/table&amp;amp;amp;amp;gt;&quot;));\n\n        \/\/ Clock and date\n        client.print(F(&quot;&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;It's &quot;));\n        char hourMinute [6];\n        sprintf (hourMinute, &quot;%02u:%02u&quot;, hour(), minute());\n        client.print(hourMinute);\n        client.print(F(&quot;&amp;amp;amp;amp;amp;nbsp; &amp;amp;amp;amp;amp;nbsp;&quot;));\n        char dayMonthYear [11];\n        sprintf (dayMonthYear, &quot;%02u.%02u.%04u&quot;, day(), month(), year());\n        client.print(dayMonthYear);\n\n        \/\/ uptime &amp;amp;amp;amp;amp; RAM\n        client.print(F(&quot;&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;Up &quot;));\n        client.println(uptime());\n        client.print(F(&quot;&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;RAM &quot;));\n        client.print(percentFreeRam());\n        client.println(F(&quot;% free&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;br \/&amp;amp;amp;amp;gt;&quot;));\n\n        client.println(F(&quot;&amp;amp;amp;amp;lt;\/body&amp;amp;amp;amp;gt;&quot;));\n        client.println(F(&quot;&amp;amp;amp;amp;lt;\/html&amp;amp;amp;amp;gt;&quot;));\n      }\n      else {\n        http404(client);\n      }\n\n      delay(1);\n      client.stop();\n\n      if(response.indexOf(&quot;tempPlus&quot;) &amp;amp;amp;amp;gt; 0) { \/\/temp +\n        pushTempPlus();\n        logInfo(&quot;web button 'temp+'&quot;);\n      }\n\n      if(response.indexOf(&quot;tempMinus&quot;) &amp;amp;amp;amp;gt; 0) { \/\/temp -\n        pushTempMinus();\n        logInfo(&quot;web button 'temp-'&quot;);\n      }\n\n      if(response.indexOf(&quot;reset&quot;) &amp;amp;amp;amp;gt; 0) { \/\/reset\n        mode = modeAuto;\n        tempManual = tempAuto;\n        logInfo(&quot;web button 'reset'&quot;);\n      }\n\n      if(response.indexOf(&quot;hold&quot;) &amp;amp;amp;amp;gt; 0) { \/\/hold\n        pushHold();\n        logInfo(&quot;web button 'hold'&quot;);\n      }\n\n      response=&quot;&quot;;  \/\/clearing string for next read\n    }\n}\n[\/code]<\/pre>\n<p>The most recent version of the code is found on <a href=\"https:\/\/github.com\/razwww\/Chronothermostat\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This was my first actual project with Arduino, and after about 6 months it was done (this included learning how to work with Arduinos, waiting for various components from Asia and waiting for the box to be 3D printed) \ud83d\ude42 The schedule (time ranges and respective temperatures) is hardcoded, I don&#8217;t see the use of [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[15],"tags":[],"class_list":["post-2272","post","type-post","status-publish","format-standard","hentry","category-arduino"],"_links":{"self":[{"href":"https:\/\/old.razwww.ro\/index.php\/wp-json\/wp\/v2\/posts\/2272","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/old.razwww.ro\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/old.razwww.ro\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/old.razwww.ro\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/old.razwww.ro\/index.php\/wp-json\/wp\/v2\/comments?post=2272"}],"version-history":[{"count":2,"href":"https:\/\/old.razwww.ro\/index.php\/wp-json\/wp\/v2\/posts\/2272\/revisions"}],"predecessor-version":[{"id":2456,"href":"https:\/\/old.razwww.ro\/index.php\/wp-json\/wp\/v2\/posts\/2272\/revisions\/2456"}],"wp:attachment":[{"href":"https:\/\/old.razwww.ro\/index.php\/wp-json\/wp\/v2\/media?parent=2272"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/old.razwww.ro\/index.php\/wp-json\/wp\/v2\/categories?post=2272"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/old.razwww.ro\/index.php\/wp-json\/wp\/v2\/tags?post=2272"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}