🚰 Wasseralarm, Wasserzaehler, Wasseruhr optisch, Panasonic EX26A, Leckagesensor

Begonnen von Torxgewinde, 07 Oktober 2023, 10:09:23

Vorheriges Thema - Nächstes Thema

Torxgewinde

Motivation: Einen Alarm auslösen falls eine Wasserleitung platzt, ein Wasserhahn dauernd läuft, ein Gartenschlauch platzt, ...

Um ein Leck in der Wasserversorgung zu detektieren, kann man den Wasserzähler auswerten. Populär ist 2023 eine Lösung mit einem ESP32-Kamera-Modul: https://github.com/jomjol/AI-on-the-edge-device, darum geht es hier nicht.

Ich wollte allerdings das kleine Flügelrädchen auslesen, um die kleinsten Mengen die solch ein mechanischer Zähler überhaupt auflöst zu detektieren. Sehr praktisch ist der Sensor "Panasonic EX26A", der hat einen NPN Schaltausgang und kann mit 12-24V versorgt werden. Gegenüber Bastellösungen mit Laserpointern oder anderen Lichtschranken zeichnet sich dieser Sensor durch seine bessere Haltbarkeit und seinen sinnvollen Fokuspunkt aus. Den Sensorausgang kann man direkt an einen GPIO von einem µC wie einem ESP32 anschließen, einen Pegelwandler braucht man so nicht. Intern muss der µC einen Pull-Up nach Vcc aktivieren, damit der Schaltausgang gut funktionieren kann - alternativ kann man einen realen Widerstand nach Vcc verbauen. Auch müssen der Mikrokontroller und der Sensor eine gemeinsame Masse nutzen.

Praxiserfahrung: Ein ESP32-Modul mit AMS1117 (ein billiger LDO mit "geht so" Spezifikationen) sollte nicht direkt mit 12V versorgt werden, da der LDO sehr warm wird und bei Spitzenströmen dann den Strom begrenzt und der ESP32 dann hängen bleibt.

Grenzen:
#1 Bei ganz geringen Durchlußmengen/Tropfmengen wird nichts detektiert. Da bringt weder die AI-on-the-Edge kamerabasierte Lösung noch ein Flügelradauswertung etwas, da sich der Zähler einfach nicht mitbewegt wenn zu wenig fließt - zumindest bei meinem geeichtem Wasserzaehler hier.

#2 Der Absolutwert weicht ein wenig auf Dauer von dem Zählwerk ab, außer man hat den Umrechnungsfaktor extrem genau getroffen. Das kann die kamerabasierte Lösung besser, da sie ja die Zeiger und Werte abliest.

So sieht der Sensor dann aus:
Du darfst diesen Dateianhang nicht ansehen.
Du darfst diesen Dateianhang nicht ansehen.
Du darfst diesen Dateianhang nicht ansehen. 


ESP32 Code:
Ich gehe auf zwei gute Firmware Möglichkeiten ein den Sensorausgang an FHEM zu übermitteln:

Man kann einfach Tasmota auf dem ESP32 flashen. Der GPIO des Sensoreingangs wird dann als "Counter 1" definiert und ist den Teleperiod Daten als COUNTER_C1 gelistet. Pragmatisch kann man dann "Teleperiod 10" setzen und erhält alle zehn Sekunden den Zählwert des Sensors. Alternativ kann man auch mit der folgenden Rule die Werte sofort bei Änderung übermitteln:
RULE1 ON system#boot DO BACKLOG var1 0; RuleTimer1 60 ENDON
ON counter#c1>%var1% DO BACKLOG RuleTimer1 60; publish %topic%/counter {"timestamp": "%timestamp%", "raw": %value%}; var1 %value%; ENDON
ON Rules#Timer=1 DO BACKLOG publish %topic%/counter {"timestamp": "%timestamp%", "raw": %var1%}; RuleTimer1 60 ENDON

Aufgeschlüsselt und kommentiert macht die obige Rule folgendes:
RULE1 ON system#boot DO BACKLOG var1 0; RuleTimer1 60 ENDON
- Setze die Variable var1 auf 0
- Starte den Timer1 mit 60 Sekunden

ON counter#c1>%var1% DO BACKLOG RuleTimer1 60; publish %topic%/counter {"timestamp": "%timestamp%", "raw": %value%}; var1 %value%; ENDON
- Wenn der Zähler c1 größer als variable var1 ist, dann:
- Stelle Timer1 wieder auf volle 60 Sekunden
- sende eine MQTT Nachricht
- setze var1 auf den Wert des auslösenden Counters c1

ON Rules#Timer=1 DO BACKLOG publish %topic%/counter {"timestamp": "%timestamp%", "raw": %var1%}; RuleTimer1 60 ENDON
- Wenn Timer1 auf 1 heruntergezählt, dann
- sende eine MQTT Nachricht
- Stelle den Timer1 wieder auf 60 Sekunden

Alternativ kann man ein Arduino-Sketch auf dem ESP32 laufen lassen:

Wasserzaehler.ino
/*******************************************************************************
#                                                                              #
#     A minimal firmware for ESP32, MQTT and TLS                               #
#                                                                              #
#                                                                              #
# This program is free software; you can redistribute it and/or modify         #
# it under the terms of the GNU General Public License as published by         #
# the Free Software Foundation; version 2 of the License.                      #
#                                                                              #
# This program is distributed in the hope that it will be useful,              #
# but WITHOUT ANY WARRANTY; without even the implied warranty of               #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                #
# GNU General Public License for more details.                                 #
#                                                                              #
# You should have received a copy of the GNU General Public License            #
# along with this program; if not, write to the Free Software                  #
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA    #
#                                                                              #
********************************************************************************/

#include <map>
#include <string>
#include <iterator>

#include <WiFiMulti.h>
#include <WiFiClientSecure.h>
#include <MQTT.h>
#include <WebServer.h>
#include <Update.h>
#include <Ticker.h>

//ESP-IDF WiFi functions
#include "esp_wifi.h"
//ESP-IDF WDT
#include <esp_task_wdt.h>

WiFiClientSecure net;
MQTTClient MQTTClient;

uint64_t sensorCounter = 0;
uint64_t sensorCounterOld = 0;

WiFiMulti wifiMulti;

WebServer server(80);

#define STATUS_PUSH_INTERVAL_IN_MS 1000*60
#define SENSOR_GPIO 13
#define LED_GPIO 2

#define WDT_TIMEOUT 70

/*******************************************************************************/
std::map<String, String> WiFi_map = {
  {"Meine SSID 1", "12345678"},
  {"Meine SSID 2", "abcdefgh"},
  {"Meine SSID 3", "45678901"}
};

String UpdateUsername = "Nutzername";
String UpdatePassword = "passwort123";

//https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
//configure DST, Timezone and NTP server
#define TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3"
#define NTP_SERVER "192.168.1.1"

String MQTTServerName = "mqtt_server";
uint16_t MQTTPort = 8883;
String MQTTUsername = "mqtt_username";
String MQTTPassword = "mqtt_passwort";
String MQTTDeviceName = "Wasserzaehler";
String MQTTRootTopic = "wasserzaehler";
const char MQTTRootCA[] PROGMEM = R"CERT(
-----BEGIN CERTIFICATE-----




.... put your CA certificate here, not the key! ....




-----END CERTIFICATE-----
)CERT";
/*******************************************************************************/

String version = "Wasserzaehler: " __DATE__ ", " __TIME__;

/******************************************************************************
Description.: write a log message
Input Value.: String with the log message
Return Value: -
******************************************************************************/
void Log(String text) {
  Serial.println(text);
}

/******************************************************************************
Description.: publish whole status via MQTT
Input Value.: -
Return Value: -
******************************************************************************/
void PushStatusViaMQTT() { 
  MQTTClient.publish(MQTTRootTopic+"/status",
  "{"
    "\"FreeHeap\":"+String(ESP.getFreeHeap())+", "+
    "\"uptime\":"+String(millis())+
  "}", true, 0);
 
  MQTTClient.publish(MQTTRootTopic+"/status",
  "{"
    "\"version\":\""+ version +"\", "+
    "\"RSSI\":"+String(WiFi.RSSI())+
  "}", true, 0);

  int8_t max_tx = -1;
  esp_wifi_get_max_tx_power(&max_tx);

  MQTTClient.publish(MQTTRootTopic+"/status",
  "{"
    "\"TX power\":"+String(max_tx)+", "+
    "\"BSSID\":\""+WiFi.BSSIDstr()+"\""+
  "}", true, 0);

  MQTTClient.publish(MQTTRootTopic+"/sensor",
  "{"
    "\"counter\":"+String(sensorCounter)+
  "}", true, 2);
}

/******************************************************************************
Description.: connect to MQTT server
Input Value.: -
Return Value: true on success, false on error
******************************************************************************/
bool MQTT_connect() {
  if(wifiMulti.run() != WL_CONNECTED) {
    Log("WiFi not connected, not trying to establish MQTT connection");
    return false;
  }
 
  //set last-will-testament, must be set before connecting
  MQTTClient.setWill(String(MQTTRootTopic+"/LWT").c_str(), "offline", true, 0);
 
  while (!MQTTClient.connect(MQTTDeviceName.c_str(), MQTTUsername.c_str(), MQTTPassword.c_str())) {
    Log("could not connect to MQTT server");
    return false;
  }
  Log("connected to MQTT server");
 
  //announce that this device is connected
  return MQTTClient.publish(MQTTRootTopic+"/LWT", "online", true, 0);
}

/******************************************************************************
Description.: overrule the default startup delay for NTP
              (defined as weak function).
Input Value.: -
Return Value: -
******************************************************************************/
uint32_t sntp_startup_delay_MS_rfc_not_less_than_60000 () {
  return 0;
}

/******************************************************************************
Description.: ISR for the sensor input pin
Input Value.: -
Return Value: -
******************************************************************************/
void IRAM_ATTR ISR() {
  sensorCounter++;
}

/******************************************************************************
Description.: setup is called after powering the device on
Input Value.: -
Return Value: -
******************************************************************************/
void setup() {
  Serial.begin(115200);
 
  Log("");
  Log(version);

  esp_task_wdt_init(WDT_TIMEOUT, true);
  esp_task_wdt_add(NULL);

  //allow for many WiFi APs
  for(auto i = WiFi_map.begin(); i != WiFi_map.end(); i++) {
    wifiMulti.addAP((i->first).c_str(), (i->second).c_str());
  }

  //try to connect
  if(wifiMulti.run() == WL_CONNECTED) {
    Log("WiFi.: connected");
    Log("IP...: "+ WiFi.localIP().toString());
    Log("BSSID: "+ WiFi.BSSIDstr());
  }
 
  // disable power save
  // it improved stability in my case, device is mains powered anyway
  esp_wifi_set_ps(WIFI_PS_NONE);

  //allow an authenticated HTTP upload with new firmware images
  //others who already know the WiFi keys can sniff the password
  server.on("/update", HTTP_POST, []() {
      server.sendHeader("Connection", "close");
      server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");

      Log((Update.hasError()) ? "FAIL" : "OK");
      Log("restarting now");
      ESP.restart();
    }, []() {
      if (!server.authenticate(UpdateUsername.c_str(), UpdatePassword.c_str())) {
        Log("Wrong Authentication");
        return server.requestAuthentication(DIGEST_AUTH);
      }
   
      HTTPUpload& upload = server.upload();
     
      if (upload.status == UPLOAD_FILE_START) {
        Log("Update begins");
        Update.begin();
      }
     
      if (upload.status == UPLOAD_FILE_WRITE) {
        Log("Writing "+ String(upload.currentSize) + " bytes");
        Update.write(upload.buf, upload.currentSize);
      }
     
      if (upload.status == UPLOAD_FILE_END) {
        Log("Update ends");
        Update.end(true);
      }     
      });

  //the webservers default page
  server.on("/", []() {
    struct tm tm;
    static char buf[26];
    time_t now = time(&now);
    localtime_r(&now, &tm);
    strftime(buf, sizeof(buf), R"(["%T","%d.%m.%Y"])", &tm);
    server.send(200, "application/json", buf);
  });

  server.begin();
 
  // Configure NTP time synchronization and set the local timezone
  configTime(0, 0, NTP_SERVER);
  setenv("TZ", TIMEZONE, 1);
  tzset();
 
  // Wait until the NTP sync succeeds or 120 seconds have elapsed
  time_t now = time(nullptr);
  while (now < 120) {
    delay(10);
    now = time(nullptr);
  }
 
  // Print the time obtained from NTP (UTC time)
  struct tm timeinfo;
  gmtime_r(&now, &timeinfo);
  Log("Time in UTC: " + String(asctime(&timeinfo)));
 
  // Print the time obtained from NTP (local time)
  localtime_r(&now, &timeinfo);
  Log("Local Time: " + String(asctime(&timeinfo)));

  //establish connection to MQTT server, use the ROOT-CA to authenticate
  //"net" is instanciated from a class that uses ciphers
  net.setCACert(MQTTRootCA);
  //DO NOT USE THIS COMMAND: net.setInsecure();
 
  MQTTClient.begin(MQTTServerName.c_str(), MQTTPort, net);

  //configure GPIO of sensor
  pinMode(SENSOR_GPIO, INPUT_PULLUP);
  pinMode(LED_GPIO, OUTPUT);
  digitalWrite(LED_GPIO, LOW);

  attachInterrupt(SENSOR_GPIO, ISR, CHANGE);
 
  Log("Setup done!");
}

/******************************************************************************
Description.: this is the main loop
Input Value.: -
Return Value: -
******************************************************************************/
void loop() {
  static unsigned long then = STATUS_PUSH_INTERVAL_IN_MS;

  //handle WiFi connection and loss of connection
  if(wifiMulti.run() != WL_CONNECTED) {
    Log("WiFi NOT ok, retrying");
    for(int i=0; i<10; i++) {
      Log("Try #: "+String(i));
      delay(5000);
      if(wifiMulti.run() == WL_CONNECTED) {
        //now the connection seems to be OK again, just leave this loop()
        return;
      }
    }
    Log("WiFi still NOT ok, tried several times, resetting the whole board");
    ESP.restart();
  }
 
  //handle webserver task
  server.handleClient();

  //handle MQTT task
  if( MQTTClient.loop() ) {
    esp_task_wdt_reset();
  }
 
  //if MQTT lost connection re-establish it
  if (!MQTTClient.connected()) {
    Log("MQTT not connected, establish MQTT connection");
    MQTT_connect();
    PushStatusViaMQTT();
    then = millis();
  }
 
  //publish changes if there are any
  if( sensorCounterOld != sensorCounter ) {
    Log("sensorCounter: "+String(sensorCounter));
    //digitalWrite(LED_GPIO, !digitalRead(LED_GPIO));
    digitalWrite(LED_GPIO, HIGH);
   
    MQTTClient.publish(MQTTRootTopic+"/sensor",
      "{"
        "\"counter\":"+String(sensorCounter)+
      "}", true, 2);

    sensorCounterOld = sensorCounter;
    digitalWrite(LED_GPIO, LOW);
  }

  if( millis()-then >= STATUS_PUSH_INTERVAL_IN_MS ) {
    then = millis();
    PushStatusViaMQTT();
  }

  delay(2000);
}

Sofern man den obigen Arduino-Sketch nutzen möchte, kann man ein OTA Update so anstoßen:
#!/bin/bash

IP="192.168.123.123"
USER="Nutzername"
PASS="passwort123"

curl --verbose -F "image=@Wasserzaehler.ino.bin" "http://$USER:$PASS@"$IP/update

In FHEM kann man dann die MQTT Daten so auslesen (gibt natürlich zig weitere Möglichkeiten):
defmod Wasserzaehler.device MQTT2_DEVICE
attr Wasserzaehler.device IODev Mosquitto
attr Wasserzaehler.device alias Wasserzähler
attr Wasserzaehler.device devicetopic wasserzaehler
attr Wasserzaehler.device group Wasserzaehler
attr Wasserzaehler.device icon wasserzaehler_icon
attr Wasserzaehler.device readingList $DEVICETOPIC/LWT:.*offline { availability=>"offline" }\
$DEVICETOPIC/LWT:.*online { availability=>"online" }\
$DEVICETOPIC/sensor:.* { json2nameValue($EVENT) }\
$DEVICETOPIC/status:.* { json2nameValue($EVENT) }
attr Wasserzaehler.device stateFormat { if (ReadingsVal($name, "availability", "") eq "offline") {\
    return 'offline';;\
  } else {\
    return round(ReadingsVal($name, "liter_total", "???"),3)." liter";; }\
}
attr Wasserzaehler.device userReadings raw_monotonic:counter:.* {\
    my $value = ReadingsNum($name, "counter", 0);;\
    my $oldValue = ReadingsNum($name, "old_raw", 0);;\
    my $flowrate = ReadingsNum($name, "flowrate", 0);;\
    my $this = ReadingsNum($name, $reading, 0);;\
\
    my $diff = $value - $oldValue;;\
    \
    return $this + $diff if ($diff >= 0 and $flowrate > 0);;\
    return $this + $diff if ($diff > 0);;\
\
    return;;\
},\
old_raw:counter:.* {\
    my $this = ReadingsNum($name, $reading, 0);;\
    my $value = ReadingsNum($name, "counter", 0);;\
    \
    return $value if ($this != $value);;\
    return;;\
},\
flowrate:raw_monotonic.* differential { ReadingsNum($name, "raw_monotonic", 0) },\
liter_total:raw_monotonic.* {\
    my $factor = 123;; ##counts per liter\
    \
    ##Liter\
    return ReadingsNum($name, "raw_monotonic", 0) / $factor;;\
},\
liter_flowrate:flowrate.* {\
    my $factor = 123;; ##counts per liter\
    \
    ##Liter per minute\
    return 60 * ReadingsNum($name, "flowrate", 0) / $factor;;\
}
Die monotonic Funktion ist übrigens "zu Fuß" programmiert, da old-readings und monotonic warum auch immer nicht funktioniert hatten. Anzupassen ist der Umrechnungsfaktor 123 für den eigenen Zähler. Das kann man experimentell ermitteln, da die Sternrädchen auf zumindest meinem Zähler da keinen Faktor spezifizieren.

Und nun zu einem Alarm, wenn das Wasser ganz lange läuft:
defmod Wasserzaehler.Entnahmedauer.doif DOIF ([Wasserzaehler.device:flowrate] > 0)\
    {Log(1, "Wasser läuft zu lange")}\
    (set Wassermelder.Pushover msg title=Durchflußmesser Es fließt sehr lange das Wasser!)\
DOELSE\
    ()
attr Wasserzaehler.Entnahmedauer.doif alias Wasser läuft zu lange
attr Wasserzaehler.Entnahmedauer.doif cmdState Lange Wasserentnahme|Normal
attr Wasserzaehler.Entnahmedauer.doif comment ##Explizite Regel setzt den Timer zurück, wenn die Bedingung oben nicht mehr wahr ist
attr Wasserzaehler.Entnahmedauer.doif group Wasserzaehler
attr Wasserzaehler.Entnahmedauer.doif icon time_timer
attr Wasserzaehler.Entnahmedauer.doif wait {60*45}:0

Torxgewinde

Ich habe noch an dem Arduino Sketch ein wenig gefeilt:

  • Der Sensor wird nun ziemlich direkt nach dem Einschalten ausgelesen
  • Der Counter bleibt bei ESP.restart() und bei einem Watchdog-Panic erhalten und zählt dann einfach weiter
  • Mehrere NTP Server möglich
  • Sketchname in der Versionskennung übertragen
  • Sketch-Quelltext wird mit der Library incbin-arduino in die Firmware einkompiliert und kann unter der URI http://ip/sourcecode wieder ausgelesen werden, praktisch wenn man den Quelltext mal nicht wiederfindet, gefährlich wenn das Passwort schwach ist, oder jemand die Firmware dumpt.
  • Klartextmeldung warum der ESP neugestartet hat. Flag ob aufgrund eines Wifi-Problems neugestartet wurde.
  • Mit SNTP die Uhrzeit und lokale Zeit inklusive DST geholt.

/*******************************************************************************
#                                                                              #
#     A minimal firmware for ESP32, MQTT and TLS                               #
#                                                                              #
#                                                                              #
# This program is free software; you can redistribute it and/or modify         #
# it under the terms of the GNU General Public License as published by         #
# the Free Software Foundation; version 2 of the License.                      #
#                                                                              #
# This program is distributed in the hope that it will be useful,              #
# but WITHOUT ANY WARRANTY; without even the implied warranty of               #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                #
# GNU General Public License for more details.                                 #
#                                                                              #
# You should have received a copy of the GNU General Public License            #
# along with this program; if not, write to the Free Software                  #
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA    #
#                                                                              #
********************************************************************************/

#include <map>
#include <string>
#include <iterator>

#include <WiFiMulti.h>
#include <WiFiClientSecure.h>
#include <MQTT.h>
#include <WebServer.h>
#include <Update.h>

//include this sourcecode into the firmware, use incbin library
#include "incbin.h"
INCTXT(SketchText, __FILE__);

//ESP-IDF WiFi functions
#include <esp_wifi.h>
#include <esp_task_wdt.h>
#include <esp_sntp.h>

//global variables and objects
String version;
WiFiClientSecure net;
MQTTClient MQTTClient(1024);
uint64_t sensorCounterOld = 0;
WiFiMulti wifiMulti;
WebServer server(80);

//global counter for impulses (raising and falling edge)
//stored to slowmem to keep value across panics and soft-restarts
RTC_NOINIT_ATTR static uint64_t sensorCounter;
RTC_NOINIT_ATTR static bool resetDueToWifiLoss;

//update interval of status
#define STATUS_PUSH_INTERVAL_IN_MS 1000*60
//sensor pin
#define SENSOR_GPIO 13
//onboard LED pin
#define LED_GPIO 2
//seconds for watchdog timeout, will be fed with successfull MQTT loops
#define WDT_TIMEOUT 600
//the name of this *.ino file without path
#define __SKETCH_FILENAME__ (__builtin_strrchr("/" __FILE__, '/') + 1)

/*******************************************************************************/

//wifi networks SSIDs and PSKs
std::map<String, String> WiFimap = {
  {"meine SSID 1", "passwort123"},
  {"meine SSID 2", "passwort456"},
  {"meine SSID 3", "passwort789"}
};

//username and password for OTA updates and retrieving this sourcecode
String UpdateUsername = "nutzername";
String UpdatePassword = "supergeheim";

//SNTP settings
//https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
//configure DST, Timezone and NTP server
#define TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3"
std::vector<std::string> NTPservers = {
  "pool.ntp.org",
  "0.europe.pool.ntp.org"
};

//MQTT+TLS settings
String MQTTServerName = "mein_MQTT_SERVER";
uint16_t MQTTPort = 8883;
String MQTTUsername = "username";
String MQTTPassword = "password";
String MQTTDeviceName = "Wasserzaehler";
String MQTTRootTopic = "wasserzaehler";
const char MQTTRootCA[] PROGMEM = R"CERT(
-----BEGIN CERTIFICATE-----

...Das Zertifikat der ROOT-CA des MQTT Servers, nicht der private Key!...

-----END CERTIFICATE-----
)CERT";
/*******************************************************************************/

/******************************************************************************
Description.: write a log message
Input Value.: String with the log message
Return Value: -
******************************************************************************/
void Log(String text) {
  Serial.println(text);
}

/******************************************************************************
Description.: convert reset reason number into a String object
Input Value.: reset reason from esp_reset_reason()
Return Value: String with the human readable reset reason
******************************************************************************/
String get_reset_reason(int reason)
{
  static const char* reset_reasons[] PROGMEM = {
    "NO_MEAN, no meaning", // 0
    "POWERON_RESET, Vbat power on reset", // 1
    "NO_MEAN, no meaning", // 2
    "SW_RESET, Software reset digital core", // 3
    "OWDT_RESET, Legacy watch dog reset digital core", // 4
    "DEEPSLEEP_RESET, Deep Sleep reset digital core", // 5
    "SDIO_RESET, Reset by SLC module, reset digital core", // 6
    "TG0WDT_SYS_RESET, Timer Group0 Watch dog reset digital core", // 7
    "TG1WDT_SYS_RESET, Timer Group1 Watch dog reset digital core", // 8
    "RTCWDT_SYS_RESET, RTC Watch dog Reset digital core", // 9
    "INTRUSION_RESET, Instrusion tested to reset CPU", // 10
    "TGWDT_CPU_RESET, Time Group reset CPU", // 11
    "SW_CPU_RESET, Software reset CPU", // 12
    "RTCWDT_CPU_RESET, RTC Watch dog Reset CPU", // 13
    "EXT_CPU_RESET, for APP CPU, reseted by PRO CPU", // 14
    "RTCWDT_BROWN_OUT_RESET, Reset when the vdd voltage is not stable", // 15
    "RTCWDT_RTC_RESET, RTC Watch dog reset digital core and rtc module" // 16
  };

  if (reason >= 0 && reason < sizeof(reset_reasons) / sizeof(reset_reasons[0])) {
    return FPSTR(reset_reasons[reason]);
  } else {
    return "ERROR, Unknown reset reason as input";
  }
}

/******************************************************************************
Description.: publish whole status via MQTT
Input Value.: -
Return Value: true on success and false on error
******************************************************************************/
bool PushStatusViaMQTT() { 
  int8_t max_tx = -1;
  esp_wifi_get_max_tx_power(&max_tx);

  time_t now = time(nullptr);
  struct tm timeinfo;
  localtime_r(&now, &timeinfo);
  String timeStr = String(asctime(&timeinfo));
  timeStr.replace("\n", "");

  return MQTTClient.publish(MQTTRootTopic+"/status",
  "{"
    "\"FreeHeap\":"+String(ESP.getFreeHeap())+", "+
    "\"TXpower\":"+String(max_tx)+", "+
    "\"RSSI\":"+String(WiFi.RSSI())+", "+
    "\"uptime\":"+String(millis())+", "+
    "\"localtime\":\""+timeStr+"\", "+
    "\"SSID\":\""+String(WiFi.SSID())+"\", "+
    "\"BSSID\":\""+WiFi.BSSIDstr()+"\", "+
    "\"IP\":\""+WiFi.localIP().toString()+"\", "+
    "\"counter\":"+String(sensorCounter)+
  "}", true, 1);
}

/******************************************************************************
Description.: connect to MQTT server
Input Value.: -
Return Value: true on success, false on error
******************************************************************************/
bool MQTT_connect() {
  if(wifiMulti.run() != WL_CONNECTED) {
    Log("WiFi not connected, not trying to establish MQTT connection");
    return false;
  }
 
  //set last-will-testament, must be set before connecting
  MQTTClient.setWill(String(MQTTRootTopic+"/LWT").c_str(), "offline", true, 0);
 
  while (!MQTTClient.connect(MQTTDeviceName.c_str(), MQTTUsername.c_str(), MQTTPassword.c_str())) {
    Log("could not connect to MQTT server");
    return false;
  }
  Log("connected to MQTT server");
 
  //announce that this device is connected
  return MQTTClient.publish(MQTTRootTopic+"/LWT", "online", true, 0);
}

/******************************************************************************
Description.: overrule the default startup delay for NTP
              (defined as weak function).
              Idea of non zero or randomized values seems to prevent many
              requests at once when many devices are started together
Input Value.: -
Return Value: random time between 10 and 2000 ms
******************************************************************************/
uint32_t sntp_startup_delay_MS_rfc_not_less_than_60000 () {
  return random(10, 2000);
}

/******************************************************************************
Description.: overrule the default sync interval for NTP
              (defined as weak function).
Input Value.: -
Return Value: sync interval in ms
******************************************************************************/
uint32_t sntp_update_delay_MS_rfc_not_less_than_15000() {
  return 12*60*60*1000;
}

/******************************************************************************
Description.: ISR for the sensor input pin, increments the counter
Input Value.: -
Return Value: -
******************************************************************************/
void IRAM_ATTR ISR() {
  sensorCounter++;
}

/******************************************************************************
Description.: setup is called after powering the device on
Input Value.: -
Return Value: -
******************************************************************************/
void setup() {
  version = __SKETCH_FILENAME__;
  version += ": " __DATE__ ", " __TIME__;
  Serial.begin(115200);
 
  Log("");
  Log(version);

  //on powerup reset the counter
  if (esp_reset_reason() == ESP_RST_POWERON) {
    Log("ESP was just switched ON\r\n");
    sensorCounter = 0;
    resetDueToWifiLoss = false;
  }

  //set watchdog
  esp_task_wdt_init(WDT_TIMEOUT, true);
  esp_task_wdt_add(NULL);

  //configure GPIO of sensor
  pinMode(SENSOR_GPIO, INPUT_PULLUP);
  pinMode(LED_GPIO, OUTPUT);
  digitalWrite(LED_GPIO, LOW);
  attachInterrupt(SENSOR_GPIO, ISR, CHANGE);

  //allow for many WiFi APs
  for(auto i = WiFimap.begin(); i != WiFimap.end(); i++) {
    wifiMulti.addAP((i->first).c_str(), (i->second).c_str());
  }

  //try to connect
  if(wifiMulti.run() == WL_CONNECTED) {
    Log("WiFi.: connected");
    Log("IP...: "+ WiFi.localIP().toString());
    Log("BSSID: "+ WiFi.BSSIDstr());
    Log("SSID.: "+ String(WiFi.SSID()));
  }
 
  // disable power save
  // it improved stability in my case, device is mains powered anyway
  esp_wifi_set_ps(WIFI_PS_NONE);

  //allow an authenticated HTTP upload with new firmware images
  //others who already know the WiFi keys can sniff the password
  server.on("/update", HTTP_POST, []() {
      server.sendHeader("Connection", "close");
      server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");

      Log((Update.hasError()) ? "FAIL" : "OK");
      Log("restarting now");
      ESP.restart();
    }, []() {
      if (!server.authenticate(UpdateUsername.c_str(), UpdatePassword.c_str())) {
        Log("Wrong or missing authentication");
        return server.requestAuthentication(DIGEST_AUTH);
      }
   
      HTTPUpload& upload = server.upload();
     
      if (upload.status == UPLOAD_FILE_START) {
        Log("Update begins");
        Update.begin();
      }
     
      if (upload.status == UPLOAD_FILE_WRITE) {
        Log("Writing "+ String(upload.currentSize) + " bytes");
        Update.write(upload.buf, upload.currentSize);
      }
     
      if (upload.status == UPLOAD_FILE_END) {
        Log("Update ends");
        Update.end(true);
      }    
      });

  //the webservers default page
  server.on("/", []() {
    struct tm tm;
    static char buf[26];
    time_t now = time(&now);
    localtime_r(&now, &tm);
    strftime(buf, sizeof(buf), R"(["%T","%d.%m.%Y"])", &tm);
    server.send(200, "application/json", buf);
  });

  //the sketch sourcecode can be obtained just in case you lost it
  server.on("/sourcecode", []() {
    if (!server.authenticate(UpdateUsername.c_str(), UpdatePassword.c_str())) {
      Log("Wrong or missing authentication");
      return server.requestAuthentication(DIGEST_AUTH);
    }
   
    server.send(200, "text/plain", gSketchTextData);
  });

  //run webserver
  server.begin();
 
  // Configure NTP time synchronization and set the local timezone
  esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
  sntp_set_time_sync_notification_cb([](struct timeval *tv) {
      time_t now = tv->tv_sec;
      struct tm timeinfo;
      localtime_r(&now, &timeinfo);
      String timeStr = String(asctime(&timeinfo));
      timeStr.replace("\n", "");
      Log("Callback Sync Notification: Local Time: " + timeStr);
    });

  for (int i=0; i<NTPservers.size(); ++i) {
    esp_sntp_setservername(i, NTPservers[i].c_str());
  }

  esp_sntp_init();
  esp_sntp_set_sync_mode(SNTP_SYNC_MODE_SMOOTH);
  setenv("TZ", TIMEZONE, 1);
  tzset();
 
  // Wait until the NTP sync succeeds or 120 seconds have elapsed
  time_t now = time(nullptr);
  while (now < 120) {
    delay(10);
    now = time(nullptr);
  }
 
  // Print the time obtained from NTP (UTC time)
  struct tm timeinfo;
  gmtime_r(&now, &timeinfo);
  String timeStr = String(asctime(&timeinfo));
  timeStr.replace("\n", "");
  Log("Time in UTC: " + timeStr);
 
  // Print the time obtained from NTP (local time)
  localtime_r(&now, &timeinfo);
  timeStr = String(asctime(&timeinfo));
  timeStr.replace("\n", "");
  Log("Local Time: " + timeStr);

  //establish connection to MQTT server, use the ROOT-CA to authenticate
  //"net" is instanciated from a class that uses ciphers
  net.setCACert(MQTTRootCA);
  //DO NOT USE THIS COMMAND: net.setInsecure();
 
  MQTTClient.begin(MQTTServerName.c_str(), MQTTPort, net);

  if (MQTT_connect()) {
    bool pub = MQTTClient.publish(MQTTRootTopic+"/status",
    "{"
      "\"version\":\""+ version +"\", "+
      "\"esp_get_idf_version\":\""+String(esp_get_idf_version())+"\", "+
      "\"resetReason\":\""+get_reset_reason(esp_reset_reason())+"\", "+
      "\"resetDueToWifiLoss\":\""+String(resetDueToWifiLoss)+"\""+
    "}", true, 1);

    //only clear the error variable resetDueToWifiLoss if publishing was OK
    if (pub) {
      resetDueToWifiLoss = false;
    }
  }

  Log("Setup done!");
}

/******************************************************************************
Description.: this is the main loop
Input Value.: -
Return Value: -
******************************************************************************/
void loop() {
  static unsigned long then = STATUS_PUSH_INTERVAL_IN_MS;

  //handle WiFi connection and loss of connection
  if(wifiMulti.run() != WL_CONNECTED) {
    Log("WiFi NOT ok, retrying");
    for(int i=0; i < (WDT_TIMEOUT-10); i++) {
      Log("Try #: "+String(i));
      delay(1000);
      if(wifiMulti.run() == WL_CONNECTED) {
        //now the connection seems to be OK again, just leave this loop()
        return;
      }
    }
    Log("WiFi still NOT ok, tried several times, resetting the whole board");
    resetDueToWifiLoss = true;
    ESP.restart();
  }
 
  //handle webserver task
  server.handleClient();

  //handle MQTT task, feed watchdog on success
  if( MQTTClient.loop() ) {
    esp_task_wdt_reset();
  }
 
  //if MQTT lost connection re-establish it
  if (!MQTTClient.connected()) {
    Log("MQTT not connected, establish MQTT connection");
    MQTT_connect();
    PushStatusViaMQTT();
    then = millis();
  }
 
  //publish changes if there are any
  if( sensorCounterOld != sensorCounter ) {
    Log("sensorCounter: "+String(sensorCounter));
    digitalWrite(LED_GPIO, HIGH);
   
    MQTTClient.publish(MQTTRootTopic+"/sensor",
      "{"
        "\"counter\":"+String(sensorCounter)+
      "}", true, 2);

    sensorCounterOld = sensorCounter;
    digitalWrite(LED_GPIO, LOW);
  }

  if( millis()-then >= STATUS_PUSH_INTERVAL_IN_MS ) {
    then = millis();
    PushStatusViaMQTT();
  }
 
  delay(2000);
}