// pre compiletime config //#define DEBUG_VERBOSE #define SPIFFS_DBG #define SPIFFS_USE_MAGIC #define FIRMWARE_NAME "WiFiThermostat" #define VERSION "0.3.3" // default values, can later be overridden via configuration (conf) #define DEVICE_NAME "WiFiThermo-1" #define WIFI_APMODE_PASSWORD "LassMiRein" #define DEFAULT_HTTP_USER "" #define DEFAULT_HTTP_PASS "" #define HTTP_SET_TOKEN "grzbrz" #define MQTT_SERVER "mqtt.lan" #define MQTT_PORT 1883 #define MQTT_USER "" #define MQTT_PASS "" #define MQTT_TOPIC_IN "Test/Thermostat" #define MQTT_TOPIC_OUT "Test/Thermostat/stat" #define MQTT_OUT_RETAIN false #define MQTT_WILLTOPIC "Test/Thermostat/availability" #define MQTT_WILLQOS 2 #define MQTT_WILLRETAIN false #define MQTT_WILLMSG "offline" #define MQTT_CONNMSG "online" #define DOMOTICZ_OUT_TOPIC "domoticz/out" // default values, can later be overridden via configuration (conf2) #define DOMOTICZ_IDX_THERMOSTAT 0 #define DOMOTICZ_IDX_THERMOSTATMODE 0 #define DOMOTICZ_IDX_TEMPHUMSENSOR 0 #define DOMOTICZ_IDX_HEATING 0 #define DOMOTICZ_IDX_PIR 0 #define OUTTEMP_TOPIC_IN "" #define OUTHUM_TOPIC_IN "" #define MQTT_TOPIC_PIR "" // extra publish topic for PIR sensor #define AUTOSAVE_SETTEMP true #define AUTOSAVE_SETMODE true #define DEFAULT_HEATING_MIN_OFFTIME 120 // minimal time the heating keeps turned off in s #define DEFAULT_SETTEMP_MIN 16.0 // minimal temperature that can be set #define DEFAULT_SETTEMP_MAX 25.0 // maximal temperature that can be set #define DEFAULT_SETTEMP_LOW 18.0 // set temperature in night/low mode #define DEFAULT_SETTEMP_LOW2 20.0 // set temperature in night/low mode #define DEFAULT_SETTEMP_HEATOFF 3.0 // set temperature in OFF mode (freezing guard > 0) #define DEFAULT_HYSTERESIS 0.1 // hysteresis, normally 0.1 - 0.5 #define SETTEMP_DECREASE_VALUE 0.0 // decreases the set temp to overcome further temperature rise when the heating is already switched off #define TEMPSENSOR_CORRECTION_VALUE 0.0 // correction value for temperature sensor reading #define HUMSENSOR_CORRECTION_VALUE 0 // correction value for humidity sensor reading #define DEFAULT_MEASURE_INTERVAL 15 // interval for temp/hum measurement #define DEFAULT_DISPLAY_INTERVAL 5 // interval for display updates (if out-temp is active, display will toggle in this interval) #define DEFAULT_DISPLAY_TIMEOUT 30 // display timeout after keypress (illumination) #define DEFAULT_PIR_ENABLES_DISPLAY false #define INSIDE_TEMP_LABEL "I" #define OUTSIDE_TEMP_LABEL "O" #define MODE_NAME_0 "off" #define MODE_NAME_1 "heat" #define PRESET_NAME_0 "none" #define PRESET_NAME_1 "reduction1" #define PRESET_NAME_2 "reduction2" // default initial values #define DEFAULT_SETTEMP 21.5 #define DEFAULT_HEATINGMODE 1 #define DEFAULT_PRESET 1 // default values that can only be configured at compile time / hardware configuration #define CLEARCONF_TOKEN "DOIT!" // Token used to reset configuration via http call on http:///delconf?token= (use when password is forgotten) #define BUTTON_DEBOUNCE_TIME 120 #define BUTTON_HOLD_TIME 750 #define DOMOTICZ_IN_TOPIC "domoticz/in" // if Domoticz IDXes are configured, updates will be sent to this topic #define SETTEMP_LOW_MIN 14.0 // minimal configurable temperature for reduction mode #define SETTEMP_LOW_MAX 20.0 // maximal configurable temperature for reduction mode #define DOMOTICZ_DISMISSUPDATE_TIMEOUT 2500 // after a value was changed by data from domoticz/out, domoticz/out parsing for this device will be turned off for this time to prevent infinite loops #define DOMOTICZ_FORCEUPDATE_INTERVAL 15 // interval in min to force update of domoticz devices #define MQTT_HEARTBEAT_MAXAGE 180000 // interval for MQTT heartbeat message. only applicable if MQTT IN-topic is defined. after this timeout MQTT reconnect is forced // pin assignments and I2C addresses #define PIN_DHTSENSOR 13 #define PIN_RELAIS 15 //16 #define PIN_BUTTON_PLUS 2 #define PIN_BUTTON_MINUS 0 #define PIN_BUTTON_MODE 14 #define PIN_PIRSENSOR 12 #define DHTTYPE DHT22 // DHT sensor type #define LCDADDR 0x27 // I2C address LCD #define LCDCOLS 16 #define LCDLINES 2 // default logic levels #define RELAISONSTATE HIGH #define BUTTONONSTATE LOW #include #include #include #include PushButton buttonPlus = PushButton(PIN_BUTTON_PLUS, ENABLE_INTERNAL_PULLUP); PushButton buttonMinus = PushButton(PIN_BUTTON_MINUS, ENABLE_INTERNAL_PULLUP); PushButton buttonMode = PushButton(PIN_BUTTON_MODE, ENABLE_INTERNAL_PULLUP); PushButton pirSensor = PushButton(PIN_PIRSENSOR, PRESSED_WHEN_HIGH); #include #include #include #include #include #include #include #include "PubSubClient.h" #include #include #include #include #include #include #ifndef MESSZ #define MESSZ 405 // Max number of characters in JSON message string (4 x DS18x20 sensors) #endif // Max message size calculated by PubSubClient is (MQTT_MAX_PACKET_SIZE < 5 + 2 + strlen(topic) + plength) #if (MQTT_MAX_PACKET_SIZE -TOPSZ -7) < MESSZ // If the max message size is too small, throw an error at compile time // See pubsubclient.c line 359 #error "MQTT_MAX_PACKET_SIZE is too small in libraries/PubSubClient/src/PubSubClient.h, increase it to at least 512" #endif boolean serialdebug = true; boolean mqttdebug = true; boolean WifiInApMode = false; unsigned long WifiApModeStartedAt; // config variables - do not change here! //conf char deviceName[31]; // device name - for web interface and AP-Mode SSID char hostName[31]; // announced hostname on WiFi connection char wifiAPModePassword[31]; char http_user[31]; char http_pass[31]; char http_token[31]; char mqtt_server[41]; int mqtt_port = MQTT_PORT; char mqtt_user[31]; char mqtt_pass[31]; char mqtt_topic_in[51]; // MQTT in topic for commands char mqtt_topic_out[51]; // MQTT out base topic, will be extended by various value names boolean mqtt_outRetain = MQTT_OUT_RETAIN; // send MQTT out with retain flag char mqtt_willTopic[51]; // MQTT Last Will topic int mqtt_willQos = MQTT_WILLQOS; // MQTT Last Will topic QOS boolean mqtt_willRetain = MQTT_WILLRETAIN; // MQTT Last Will retain char mqtt_willMsg[31]; // MQTT Last Will payload char mqtt_connMsg[31]; char domoticz_out_topic[55]; // domoticz out topic to subscribe to (only applicable if domoticzIdx_Thermostat and/or domoticzIdx_ThermostatMode is set to >0) char mqtt_topic_in_cmd[61]; char mqtt_topic_in_setTemp[61]; char mqtt_topic_in_setMode[61]; char mqtt_topic_in_setPreset[61]; //conf2 int domoticzIdx_Thermostat = DOMOTICZ_IDX_THERMOSTAT; int domoticzIdx_ThermostatMode = DOMOTICZ_IDX_THERMOSTATMODE; int domoticzIdx_TempHumSensor = DOMOTICZ_IDX_TEMPHUMSENSOR; int domoticzIdx_Heating = DOMOTICZ_IDX_HEATING; int domoticzIdx_PIR = DOMOTICZ_IDX_PIR; char outTemp_topic_in[51]; char outHum_topic_in[51]; char mqtt_topic_pir[51]; boolean autoSaveSetTemp = AUTOSAVE_SETTEMP; boolean autoSaveHeatingMode = AUTOSAVE_SETMODE; int heatingMinOffTime = DEFAULT_HEATING_MIN_OFFTIME; // minimal time the heating keeps turned off in s float setTempMin = DEFAULT_SETTEMP_MIN; // minimal temperature that can be set float setTempMax = DEFAULT_SETTEMP_MAX; // maximal temperature that can be set float setTempLow = DEFAULT_SETTEMP_LOW; // set temperature in night/low mode float setTempLow2 = DEFAULT_SETTEMP_LOW2; // set temperature in night/low mode float hysteresis = DEFAULT_HYSTERESIS; // hysteresis, normally 0.1 - 0.5 float setTempDecreaseVal = SETTEMP_DECREASE_VALUE; // decreases the set temp to overcome further temperature rise when the heating is already switched off float tempCorrVal = TEMPSENSOR_CORRECTION_VALUE; // correction value for temperature sensor reading int humCorrVal = HUMSENSOR_CORRECTION_VALUE; // correction value for humidity sensor reading int measureInterval = DEFAULT_MEASURE_INTERVAL; // interval for temp/hum measurement int displayInterval = DEFAULT_DISPLAY_INTERVAL; // interval for display updates (if out-temp is active, display will toggle in this interval) int displayInterval_saved = DEFAULT_DISPLAY_INTERVAL; int displayTimeout = DEFAULT_DISPLAY_TIMEOUT; // display timeout after keypress (illumination) boolean PIR_enablesDisplay = DEFAULT_PIR_ENABLES_DISPLAY; // PIR sensor enables display illumination char modename0[15]; char modename1[15]; char psetname0[15]; char psetname1[15]; char psetname2[15]; char offMessage[15]; char itemplab[2]; char otemplab[2]; int maxMeasurementAge = 300000; //set values float setTemp = DEFAULT_SETTEMP; float currSetTemp = DEFAULT_SETTEMP; byte heatingMode = DEFAULT_HEATINGMODE; // 0 = off, 1 = heat byte preset = DEFAULT_PRESET; // 0 = normal/day, 1 = night/reduction 1, 2 = reduction 2 (long term leave) float setTempSaved; byte heatingModeSaved; // 0 = off, 1 = heat/on byte presetSaved; // 0 = normal/day, 1 = night/reduction 1, 2 = reduction 2 // not changeable via configuration float setTempLowMin = SETTEMP_LOW_MIN; float setTempLowMax = SETTEMP_LOW_MAX; boolean debug = true; int debounceTime = BUTTON_DEBOUNCE_TIME; int buttonHoldTime = BUTTON_HOLD_TIME; // global variables float currTemp; // last reading from DHT sensor float currTemp_raw; // last reading from DHT sensor int currHum; // last reading from DHT sensor int currHum_raw; // last reading from DHT sensor boolean turnHeatingOn = false; // true if heating is active (relais switched on) unsigned long heatingLastOnMillis; // last time heating was switched on unsigned long heatingLastOffMillis; // last time heating was switched off float outTemp; // outside temp (via MQTT if enabled and in-topic configured) int outHum; // outside temp (via MQTT if enabled and in-topic configured) long outTempHumLastUpdate; // last reading from out temp/hum source char outTemp_newValue[6]; boolean outTemp_parseNewValue; char outHum_newValue[4]; boolean outHum_parseNewValue; char currentModeName[15]; char currentPresetName[15]; byte whichTempToDisplay; // 1=temp inside (from DHT sensor), 2= temp outside (via MQTT) - if out temp/hum available this value and the displayed value pair toggles with every displayInterval unsigned long lastMeasure = 0; // millis of last temp/hum measurement unsigned long lastDisplayUpdate = 0; // millis of last display update unsigned long lastDisplayToggle = 0; // millis of last display toggle unsigned long lastTempUpdate = 0; // last update time of DHT reading char msg[50]; // buffer MQTT in payload char topic[50]; // buffer MQTT in topic boolean displayActive = false; // gets true when button is pressed. display light gets switched on until timeout. button actions are only performed while display is active boolean PIRSensorOn = false; unsigned long heatingOnTime, heatingOffTime; boolean useDomoticz = false; // will be set to true in setup() if idx-values other than 0 are configured boolean domoticzOutParseData = false; // indicates that domoticz/out json data is buffered, will then be parsed in next loop() run boolean domoticzOutParserBusy = false; // indicates that domoticz/out json data is currently processed - no futher data will be accepted until finished char domoticzOutPayload[450]; // buffer for domoticz/out data int dismissUpdateFromDomoticzTimeout = DOMOTICZ_DISMISSUPDATE_TIMEOUT; // after a value was changed by data from domoticz/out, domoticz/out parsing for this device will be turned off for this time to prevent infinite loops unsigned long lastUpdate_setTemp = 0; // set to millis() every time setTemp value is changed. next update from domoticz/out will be rejected for dismissUpdateFromDomoticzTimeout in this case unsigned long lastUpdate_heatingMode = 0; // set to millis() every time heatingMode value is changed. next update from domoticz/out will be rejected for dismissUpdateFromDomoticzTimeout in this case unsigned long lastUpdate_preset = 0; // set to millis() every time preset value is changed. next update from domoticz/out will be rejected for dismissUpdateFromDomoticzTimeout in this case boolean lastUpdateFromDomoticz_setTemp = false; boolean lastUpdateFromDomoticz_heatingMode = false; int domoticzUpdateInterval = DOMOTICZ_FORCEUPDATE_INTERVAL; // interval in min to force update of domoticz devices char cmdPayload[101]; // buffer for commands boolean cmdInQueue = false; // command is queued and will be processed next loop() run boolean saveConfigToFlash = false; // conf is saved in next loop() run boolean saveConfig2ToFlash = false; // conf2 is saved in next loop() run unsigned int saveValuesTimeout = 5000; unsigned long lastValueChange; // is set to millis() whenever setTemp value and/or heatingMode value is changed. used for autoSave function with hardcoded 5s timeout boolean setTempAlreadySaved = true; // only save if not yet done boolean heatingModeAlreadySaved = true; // only save if not yet done boolean presetAlreadySaved = true; // only save if not yet done unsigned long lastRun = 0; int count100ms = 0; int countSeconds = 0; int countMeasureInterval = 0; int countDisplayInterval = 0; int displayOverlayMsgTimeout = 2; boolean togglingTempHumAIDisplay = false; byte mqttMode = 0; unsigned long mqttLastReconnectAttempt = 0; int mqttReconnectAttempts = 0; int mqttReconnects = 0; boolean mqttConnected = false; unsigned long mqttLastHeartbeat; boolean mqttInTopicSubscribed = false; boolean pendingRestart = false; boolean doRestart = false; unsigned long pendingRestart_lastMillis = 0; boolean displayShowFullscreenMsg = false; //unsigned long displayShowFullscreenMsg_lastMillis = 0; boolean pendingPresetToggle = false; boolean updateDisplayImmediately = false; int pendingPreset; char pendingPresetName[15]; //unsigned long pendingPreset_millis = 0; //int pendingPreset_timeout = 2000; boolean displayShowLine2OverlayMsg = false; DHT dht(PIN_DHTSENSOR, DHTTYPE); LiquidCrystal_I2C lcd(LCDADDR, LCDCOLS, LCDLINES); // set the LCD address to 0x27 for a 16 chars and 2 line display WiFiClient espClient; void mqttCallback(char* topic, byte* payload, unsigned int length); PubSubClient mqttclient(espClient); ESP8266WebServer httpServer(80); DNSServer dnsServer; PersWiFiManager persWM(httpServer, dnsServer); ESP8266HTTPUpdateServer httpUpdater; void setup() { Serial.begin(115200); delay(500); Serial.println(); Serial.print(FIRMWARE_NAME); Serial.print(" v"); Serial.print(VERSION); Serial.println("starting..."); pinMode(PIN_RELAIS, OUTPUT); digitalWrite(PIN_RELAIS, !RELAISONSTATE); pinMode(PIN_PIRSENSOR, INPUT); buttonPlus.configureButton(configurePushButton); //buttonPlus.onPress(onButtonPressed); // When the button is first pressed, call the function onButtonPressed (further down the page) buttonPlus.onHoldRepeat(1000, 350, onButtonHeld); // Once the button has been held for 1 second (1000ms) call onButtonHeld. Call it again every 350ms until it is let go buttonPlus.onRelease(50, 500, onButtonReleased); // When the button is held >50ms and released after <500ms, call onButtonReleased buttonMinus.configureButton(configurePushButton); //buttonMinus.onPress(onButtonPressed); // When the button is first pressed, call the function onButtonPressed (further down the page) buttonMinus.onHoldRepeat(1000, 350, onButtonHeld); // Once the button has been held for 1 second (1000ms) call onButtonHeld. Call it again every 350ms until it is let go buttonMinus.onRelease(50, 500, onButtonReleased); // When the button is held >50ms and released after <500ms, call onButtonReleased buttonMode.configureButton(configurePushButton); //buttonMode.onPress(onButtonPressed); // When the button is first pressed, call the function onButtonPressed (further down the page) //buttonMode.onHold(1000, onButtonHeldNoRepeat); // Once the button has been held for 1 second (1000ms) call onButtonHeld buttonMode.onHoldRepeat(1000, 1000, onButtonHeld); // Once the button has been held for 1 second (1000ms) call onButtonHeld buttonMode.onRelease(50, 500, onButtonReleased); // When the button is held >50ms and released after <500ms, call onButtonReleased pirSensor.configureButton(configurePushButton); //pirSensor.onPress(onButtonPressed); // When the button is first pressed, call the function onButtonPressed (further down the page) pirSensor.onHold(500, onButtonHeldNoRepeat); // Once the button has been held for 1 second (1000ms) call onButtonHeld pirSensor.onRelease(500, onButtonReleased); // When the button is held >50ms and released after <500ms, call onButtonReleased //set conf default values (bool, int and float variables are set at declaration) strlcpy(deviceName, DEVICE_NAME, 31); strlcpy(wifiAPModePassword, WIFI_APMODE_PASSWORD, 31); strlcpy(http_user, DEFAULT_HTTP_USER, 31); strlcpy(http_pass, DEFAULT_HTTP_PASS, 31); strlcpy(http_token, HTTP_SET_TOKEN, 31); strlcpy(mqtt_server, MQTT_SERVER, 41); strlcpy(mqtt_user, MQTT_USER, 31); strlcpy(mqtt_pass, MQTT_PASS, 31); strlcpy(mqtt_topic_in, MQTT_TOPIC_IN, 51); strlcpy(mqtt_topic_out, MQTT_TOPIC_OUT, 51); strlcpy(mqtt_willTopic, MQTT_WILLTOPIC, 51); strlcpy(mqtt_willMsg, MQTT_WILLMSG, 31); strlcpy(mqtt_connMsg, MQTT_CONNMSG, 31); strlcpy(domoticz_out_topic, DOMOTICZ_OUT_TOPIC, 51); // changeable subscription topic, as domoticz supports different flat/hierarchical out-topics //set conf2 default values (bool, int and float variables are set at declaration) strlcpy(outTemp_topic_in, OUTTEMP_TOPIC_IN, 51); strlcpy(outHum_topic_in, OUTHUM_TOPIC_IN, 51); strlcpy(mqtt_topic_pir, MQTT_TOPIC_PIR, 51); strlcpy(modename0, MODE_NAME_0, 15); strlcpy(modename1, MODE_NAME_1, 15); strlcpy(psetname0, PRESET_NAME_0, 15); strlcpy(psetname1, PRESET_NAME_1, 15); strlcpy(psetname2, PRESET_NAME_2, 15); strlcpy(itemplab, INSIDE_TEMP_LABEL, 2); strlcpy(otemplab, OUTSIDE_TEMP_LABEL, 2); Serial.println("default config values loaded.."); Serial.println("Mounting FS..."); if (!SPIFFS.begin()) { Serial.println("Failed to mount file system"); return; } //uncomment for initial SPIFFS format //SPIFFS.format(); //Serial.print("Format SPIFFS complete."); if (!SPIFFS.exists("/formatComplete.txt")) { Serial.println("Please wait 30 secs for SPIFFS to be formatted"); SPIFFS.format(); Serial.println("Spiffs formatted"); File f = SPIFFS.open("/formatComplete.txt", "w"); if (!f) { Serial.println("file open failed"); } else { f.println("Format Complete"); } f.close(); } else { Serial.println("SPIFFS is formatted. Moving along..."); } // // load config from SPIFFS if files exist if (!loadConfig()) { Serial.println("Failed to load conf.json"); } else { Serial.println("conf.json loaded"); } if (!loadConfig2()) { Serial.println("Failed to load conf2.json"); } else { Serial.println("conf2.json loaded"); } if (!loadSetTemp()) { Serial.println("Failed to load file 'setTemp'"); } else { Serial.println("file 'setTemp' loaded"); } if (!loadHeatingMode()) { Serial.println("Failed to load file 'heatingMode'"); } else { Serial.println("file 'heatingMode' loaded"); } if (!loadPreset()) { Serial.println("Failed to load file 'preset'"); } else { Serial.println("file 'preset' loaded"); } //if configuration returns empty strings - use the default from source if (strlen(deviceName) < 4) strlcpy(deviceName, DEVICE_NAME, 31); if (modename0[0] == '\0') strlcpy(modename0, MODE_NAME_0, 15); if (modename1[0] == '\0') strlcpy(modename1, MODE_NAME_1, 15); if (psetname0[0] == '\0') strlcpy(psetname0, PRESET_NAME_0, 15); if (psetname1[0] == '\0') strlcpy(psetname1, PRESET_NAME_1, 15); if (psetname2[0] == '\0') strlcpy(psetname2, PRESET_NAME_2, 15); if (itemplab[0] == '\0') strlcpy(itemplab, INSIDE_TEMP_LABEL, 2); if (otemplab[0] == '\0') strlcpy(otemplab, OUTSIDE_TEMP_LABEL, 2); setTempSaved = setTemp; heatingModeSaved = heatingMode; presetSaved = preset; updateCurrentHeatingModeName(); updateCurrentPresetName(); // initialize DHT11/22 temp/hum sensor dht.begin(); if (strlen(hostName) >= 4) { // if no hostname is set WiFi manager will create a unique one automatically based on MAC address WiFi.hostname(hostName); } mqttPrepareSubscribeTopics(); checkUseDomoticz(); //optional code handlers to run everytime wifi is connected... persWM.onConnect([]() { Serial.println("wifi connected"); Serial.println(WiFi.SSID()); Serial.println(WiFi.localIP()); WifiInApMode = false; displayShowWifiConnected(); }); //...or AP mode is started persWM.onAp([]() { Serial.println("AP MODE"); Serial.println(persWM.getApSsid()); WifiInApMode = true; WifiApModeStartedAt = millis(); displayShowWifiConnectionError(); }); // sets network name and password for AP mode if (strlen(wifiAPModePassword) < 8) { strlcpy(wifiAPModePassword, WIFI_APMODE_PASSWORD, 31); } persWM.setApCredentials(deviceName, wifiAPModePassword); // persWM.setApCredentials(DEVICE_NAME); //make connecting/disconnecting non-blocking persWM.setConnectNonBlock(true); //in non-blocking mode, program will continue past this point without waiting persWM.begin(); delay(500); httpServerInit(); mqttPrepareConnection(); mqttClientInit(); initDisplay(); Serial.println("setup complete."); //delay(1000); } //void setup void loop() { checkMillis(); persWM.handleWiFi(); //in non-blocking mode, handleWiFi must be called in the main loop yield(); mqttHandleConnection(); yield(); outTempHum_updateOnNewValue(); yield(); dnsServer.processNextRequest(); httpServer.handleClient(); buttonPlus.update(); buttonMinus.update(); buttonMode.update(); pirSensor.update(); yield(); evalCmd(); yield(); if ( domoticzOutParseData ) { parseDomoticzOut(); yield(); } if (Serial.available()) { serialEvent(); yield(); } if (updateDisplayImmediately) { updateDisplayImmediately = false; updateDisplay(); } } //void loop