From 6acf310e6d985341345e98a9b72199f228eb24d7 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Tue, 24 Aug 2021 18:29:31 +0200 Subject: [PATCH 01/75] s0 enhancements --- HeishaMon/HeishaMon.ino | 4 +- HeishaMon/htmlcode.h | 2 +- HeishaMon/s0.cpp | 90 +++++++++++++++++++++++++++++++------- HeishaMon/s0.h | 3 ++ HeishaMon/version.h | 2 +- HeishaMon/webfunctions.cpp | 9 ++++ 6 files changed, 90 insertions(+), 20 deletions(-) diff --git a/HeishaMon/HeishaMon.ino b/HeishaMon/HeishaMon.ino index 531dcf76..a56191a8 100644 --- a/HeishaMon/HeishaMon.ino +++ b/HeishaMon/HeishaMon.ino @@ -709,7 +709,9 @@ void loop() { message += ESP.getFreeHeap(); message += F(" bytes ## Wifi: "); message += getWifiQuality(); - message += F("% ## Mqtt reconnects: "); + message += F("% (RSSI: "); + message += WiFi.RSSI(); + message += F(") ## Mqtt reconnects: "); message += mqttReconnects; message += F(" ## Correct data: "); message += readpercentage; diff --git a/HeishaMon/htmlcode.h b/HeishaMon/htmlcode.h index 23c17a76..ab6cacb8 100644 --- a/HeishaMon/htmlcode.h +++ b/HeishaMon/htmlcode.h @@ -208,7 +208,7 @@ static const char webBodyRootDallasValues[] PROGMEM = static const char webBodyRootS0Values[] PROGMEM = "
" "

Current S0 kWh meters values

" - "
S0 portWattWatthourWatthourTotal
...Loading...
"; + "
S0 portWattWatthourWatthourTotalPulse quality
...Loading...
"; static const char webBodyRootConsole[] PROGMEM = "
" diff --git a/HeishaMon/s0.cpp b/HeishaMon/s0.cpp index 74f6252f..0faecb22 100644 --- a/HeishaMon/s0.cpp +++ b/HeishaMon/s0.cpp @@ -13,16 +13,46 @@ s0DataStruct actS0Data[NUM_S0_COUNTERS]; s0SettingsStruct actS0Settings[NUM_S0_COUNTERS]; //volatile pulse detectors for s0 -volatile unsigned long new_pulse_s0[2] = {0, 0}; - +volatile unsigned long new_pulse_low_s0[2] = {0, 0}; +volatile unsigned long new_pulse_high_s0[2] = {0, 0}; +volatile bool last_state_s0[2] = {HIGH, HIGH}; //These are the interrupt routines. Make them as short as possible so we don't block other interrupts (for example serial data) +/* There are situations where a CHANGE on GPIO input is detected from HIGH to HIGH (or maybe also LOW to LOW) + * So we need an extra check for the pulse changes for FALLING and RISING + */ ICACHE_RAM_ATTR void onS0Pulse1() { - new_pulse_s0[0] = millis(); + unsigned long currentTime = millis(); + if (last_state_s0[0] == LOW ) { + if (digitalRead(actS0Settings[0].gpiopin) == HIGH) { //make sure we are high now + //this was a rising + last_state_s0[0] = HIGH; + new_pulse_high_s0[0] = currentTime; + } + } else { + if (digitalRead(actS0Settings[0].gpiopin) == LOW) { //make sure we are low now + //this was a falling + last_state_s0[0] = LOW; + new_pulse_low_s0[0] = currentTime; + } + } } ICACHE_RAM_ATTR void onS0Pulse2() { - new_pulse_s0[1] = millis(); + unsigned long currentTime = millis(); + if (last_state_s0[1] == LOW ) { + if (digitalRead(actS0Settings[1].gpiopin) == HIGH) { //make sure we are high now + //this was a rising + last_state_s0[1] = HIGH; + new_pulse_high_s0[1] = currentTime; + } + } else { + if (digitalRead(actS0Settings[1].gpiopin) == LOW) { //make sure we are low now + //this was a falling + last_state_s0[1] = LOW; + new_pulse_low_s0[1] = currentTime; + } + } } void initS0Sensors(s0SettingsStruct s0Settings[]) { @@ -32,7 +62,7 @@ void initS0Sensors(s0SettingsStruct s0Settings[]) { actS0Settings[0].lowerPowerInterval = s0Settings[0].lowerPowerInterval; pinMode(actS0Settings[0].gpiopin, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(actS0Settings[0].gpiopin), onS0Pulse1, RISING); + attachInterrupt(digitalPinToInterrupt(actS0Settings[0].gpiopin), onS0Pulse1, CHANGE); actS0Data[0].nextReport = millis() + MINREPORTEDS0TIME; //initial report after interval, not directly at boot //setup s0 port 2 @@ -40,12 +70,12 @@ void initS0Sensors(s0SettingsStruct s0Settings[]) { actS0Settings[1].ppkwh = s0Settings[1].ppkwh; actS0Settings[1].lowerPowerInterval = s0Settings[1].lowerPowerInterval; pinMode(actS0Settings[1].gpiopin, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(actS0Settings[1].gpiopin), onS0Pulse2, RISING); + attachInterrupt(digitalPinToInterrupt(actS0Settings[1].gpiopin), onS0Pulse2, CHANGE); actS0Data[1].nextReport = millis() + MINREPORTEDS0TIME; //initial report after interval, not directly at boot } void restore_s0_Watthour(int s0Port, float watthour) { - if ((s0Port == 1) || (s0Port == 2)) { + if ((s0Port == 1) || (s0Port == 2)) { unsigned int newTotal = int(watthour * (actS0Settings[s0Port - 1].ppkwh / 1000.0)); if (newTotal > actS0Data[s0Port - 1].pulsesTotal) actS0Data[s0Port - 1].pulsesTotal = newTotal; } @@ -70,22 +100,46 @@ void s0Loop(PubSubClient &mqtt_client, void (*log_message)(char*), char* mqtt_to unsigned long millisThisLoop = millis(); for (int i = 0 ; i < NUM_S0_COUNTERS ; i++) { + char tmp_log_msg[256]; + unsigned long pulseInterval = 0; //first handle new detected pulses noInterrupts(); - unsigned long new_pulse = new_pulse_s0[i]; + unsigned long new_pulse_low = new_pulse_low_s0[i]; + unsigned long new_pulse_high = new_pulse_high_s0[i]; interrupts(); - unsigned long pulseInterval = new_pulse - actS0Data[i].lastPulse; - if (pulseInterval > 50L) { //50ms debounce filter, this also prevents division by zero to occur a few lines further down the road if pulseInterval = 0 - if (actS0Data[i].lastPulse > 0) { //Do not calculate watt for the first pulse since reboot because we will always report a too high watt. Better to show 0 watt at first pulse. - actS0Data[i].watt = (3600000000.0 / pulseInterval) / actS0Settings[i].ppkwh; - } - actS0Data[i].lastPulse = new_pulse; - actS0Data[i].pulses++; - if ((unsigned long)(actS0Data[i].nextReport - millisThisLoop) > MINREPORTEDS0TIME) { //loop was in standby interval - actS0Data[i].nextReport = 0; // report now + if (new_pulse_high > new_pulse_low) { + //pulse detected, now check if it is valid + if ( ((new_pulse_high - new_pulse_low) > actS0Settings[i].minimalPulseWidth) && ((new_pulse_high - new_pulse_low) < 10 * actS0Settings[i].minimalPulseWidth) ) { //within pulse width and pulse width * 10 + pulseInterval = new_pulse_low - actS0Data[i].lastPulse; + if (pulseInterval > actS0Settings[i].minimalPulseWidth ) { + sprintf_P(tmp_log_msg, PSTR("S0 port %i pulse counted as valid! (pulse width: %lu, interval: %lu)"), i + 1, (new_pulse_high - new_pulse_low), pulseInterval); + log_message(tmp_log_msg); + actS0Data[i].goodPulses++; + if (actS0Data[i].lastPulse > 0) { //Do not calculate watt for the first pulse since reboot because we will always report a too high watt. Better to show 0 watt at first pulse. + actS0Data[i].watt = (3600000000.0 / pulseInterval) / actS0Settings[i].ppkwh; + } + actS0Data[i].lastPulse = new_pulse_low; + actS0Data[i].pulses++; + + if ((unsigned long)(actS0Data[i].nextReport - millisThisLoop) > MINREPORTEDS0TIME) { //loop was in standby interval + actS0Data[i].nextReport = 0; // report now + } + } else { + sprintf_P(tmp_log_msg, PSTR("S0 port %i pulse counted as invalid! (pulse width: %lu, interval: %lu)"), i + 1, (new_pulse_high - new_pulse_low), pulseInterval); + log_message(tmp_log_msg); + actS0Data[i].badPulses++; + } + } else { + sprintf_P(tmp_log_msg, PSTR("S0 port %i pulse reset. Noise detected! (pulse width: %lu)"), i + 1, (new_pulse_high - new_pulse_low)); + log_message(tmp_log_msg); + actS0Data[i].badPulses++; } + noInterrupts(); + new_pulse_high_s0[i] = new_pulse_low_s0[i]; //only need to reset the time for high pulse because low always needs to be first for a valid pulse + interrupts(); } + //then report after nextReport if (millisThisLoop > actS0Data[i].nextReport) { @@ -142,6 +196,7 @@ String s0TableOutput() { output = output + F("") + actS0Data[i].watt + F(""); output = output + F("") + (actS0Data[i].pulses * ( 1000.0 / actS0Settings[i].ppkwh)) + F(""); output = output + F("") + (actS0Data[i].pulsesTotal * ( 1000.0 / actS0Settings[i].ppkwh)) + F(""); + output = output + F("") + (100 * (actS0Data[i].goodPulses + 1) / (actS0Data[i].goodPulses + actS0Data[i].badPulses + 1)) + F("% "); output = output + F(""); } return output; @@ -155,6 +210,7 @@ String s0JsonOutput() { output = output + F("\"Watt\": \"") + actS0Data[i].watt + F("\","); output = output + F("\"Watthour\": \"") + (actS0Data[i].pulses * ( 1000.0 / actS0Settings[i].ppkwh)) + F("\","); output = output + F("\"WatthourTotal\": \"") + (actS0Data[i].pulsesTotal * ( 1000.0 / actS0Settings[i].ppkwh)) + F("\""); + output = output + F("\"Pulse Quality\": \"") + (100 * (actS0Data[i].goodPulses + 1) / (actS0Data[i].goodPulses + actS0Data[i].badPulses + 1)) + F("\""); output = output + F("}"); if (i < NUM_S0_COUNTERS - 1) output = output + F(","); } diff --git a/HeishaMon/s0.h b/HeishaMon/s0.h index 11f148cb..14090c5e 100644 --- a/HeishaMon/s0.h +++ b/HeishaMon/s0.h @@ -8,6 +8,7 @@ struct s0SettingsStruct { byte gpiopin = 255; unsigned int ppkwh = 1000; //pulses per Wh of the connected meter unsigned int lowerPowerInterval = 60; //configurabel low power interval + unsigned int minimalPulseWidth = 50; //configurabel minimal s0 pulse width }; struct s0DataStruct { @@ -16,6 +17,8 @@ struct s0DataStruct { unsigned int watt = 0; //calculated average power unsigned long lastPulse = 0; //last pulse in millis unsigned long nextReport = 0; //next time we reported the s0 value in millis + unsigned long goodPulses = 0; + unsigned long badPulses = 0; }; void initS0Sensors(s0SettingsStruct s0Settings[]); diff --git a/HeishaMon/version.h b/HeishaMon/version.h index 1acf19e9..40763c6e 100644 --- a/HeishaMon/version.h +++ b/HeishaMon/version.h @@ -1 +1 @@ -static const char* heishamon_version = "2.0"; +static const char* heishamon_version = "2.1-iy-1"; diff --git a/HeishaMon/webfunctions.cpp b/HeishaMon/webfunctions.cpp index 92b3a027..4a12db70 100644 --- a/HeishaMon/webfunctions.cpp +++ b/HeishaMon/webfunctions.cpp @@ -21,6 +21,8 @@ void getWifiScanResults(int networksFound) { } int dBmToQuality(int dBm) { + if (dBm == 31) + return -1; if (dBm <= -100) return 0; if (dBm >= -50) @@ -116,9 +118,11 @@ void loadSettings(settingsStruct *heishamonSettings) { if (jsonDoc["s0_1_gpio"]) heishamonSettings->s0Settings[0].gpiopin = jsonDoc["s0_1_gpio"]; if (jsonDoc["s0_1_ppkwh"]) heishamonSettings->s0Settings[0].ppkwh = jsonDoc["s0_1_ppkwh"]; if (jsonDoc["s0_1_interval"]) heishamonSettings->s0Settings[0].lowerPowerInterval = jsonDoc["s0_1_interval"]; + if (jsonDoc["s0_1_minpulsewidth"]) heishamonSettings->s0Settings[0].minimalPulseWidth = jsonDoc["s0_1_minpulsewidth"]; if (jsonDoc["s0_2_gpio"]) heishamonSettings->s0Settings[1].gpiopin = jsonDoc["s0_2_gpio"]; if (jsonDoc["s0_2_ppkwh"]) heishamonSettings->s0Settings[1].ppkwh = jsonDoc["s0_2_ppkwh"]; if (jsonDoc["s0_2_interval"] ) heishamonSettings->s0Settings[1].lowerPowerInterval = jsonDoc["s0_2_interval"]; + if (jsonDoc["s0_2_minpulsewidth"]) heishamonSettings->s0Settings[1].minimalPulseWidth = jsonDoc["s0_2_minpulsewidth"]; } else { log_message("Failed to load json config, forcing config reset."); WiFi.persistent(true); @@ -500,9 +504,11 @@ bool handleSettings(ESP8266WebServer *httpServer, settingsStruct *heishamonSetti if (httpServer->hasArg("s0_1_gpio")) jsonDoc["s0_1_gpio"] = httpServer->arg("s0_1_gpio"); if (httpServer->hasArg("s0_1_ppkwh")) jsonDoc["s0_1_ppkwh"] = httpServer->arg("s0_1_ppkwh"); if (httpServer->hasArg("s0_1_interval")) jsonDoc["s0_1_interval"] = httpServer->arg("s0_1_interval"); + if (httpServer->hasArg("s0_1_minpulsewidth")) jsonDoc["s0_1_minpulsewidth"] = httpServer->arg("s0_1_minpulsewidth"); if (httpServer->hasArg("s0_2_gpio")) jsonDoc["s0_2_gpio"] = httpServer->arg("s0_2_gpio"); if (httpServer->hasArg("s0_2_ppkwh")) jsonDoc["s0_2_ppkwh"] = httpServer->arg("s0_2_ppkwh"); if (httpServer->hasArg("s0_2_interval")) jsonDoc["s0_2_interval"] = httpServer->arg("s0_2_interval"); + if (httpServer->hasArg("s0_2_minpulsewidth")) jsonDoc["s0_2_minpulsewidth"] = httpServer->arg("s0_2_minpulsewidth"); } else { jsonDoc["use_s0"] = "disabled"; } @@ -721,6 +727,9 @@ bool handleSettings(ESP8266WebServer *httpServer, settingsStruct *heishamonSetti httptext = httptext + F("S0 port ") + (i + 1) + F(" reporting interval during standby/low power usage:"); httptext = httptext + F("s0Settings[i].lowerPowerInterval) + F("\"> seconds"); httptext = httptext + F(""); + httptext = httptext + F("S0 port ") + (i + 1) + F(" minimal pulse width:"); + httptext = httptext + F("s0Settings[i].minimalPulseWidth) + F("\"> milliseconds"); + httptext = httptext + F(""); httptext = httptext + F("S0 port ") + (i + 1) + F(" standby/low power usage threshold: Watt"); httptext = httptext + F(""); } From 063aaf6b05f0757092144d5176b5b73dfdf7c15a Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Wed, 25 Aug 2021 07:54:01 +0200 Subject: [PATCH 02/75] more s0 pulse measuring improvements --- HeishaMon/s0.cpp | 127 ++++++++++++++++++++------------------------ HeishaMon/version.h | 2 +- 2 files changed, 59 insertions(+), 70 deletions(-) diff --git a/HeishaMon/s0.cpp b/HeishaMon/s0.cpp index 0faecb22..71956740 100644 --- a/HeishaMon/s0.cpp +++ b/HeishaMon/s0.cpp @@ -1,6 +1,7 @@ #include #include "commands.h" #include "s0.h" +#include #define MQTT_RETAIN_VALUES 1 // do we retain 1wire values? @@ -12,47 +13,34 @@ s0DataStruct actS0Data[NUM_S0_COUNTERS]; //global array for s0 Settings s0SettingsStruct actS0Settings[NUM_S0_COUNTERS]; -//volatile pulse detectors for s0 -volatile unsigned long new_pulse_low_s0[2] = {0, 0}; -volatile unsigned long new_pulse_high_s0[2] = {0, 0}; -volatile bool last_state_s0[2] = {HIGH, HIGH}; +//volatile pulse timer ringbuffer for s0 +//pushing timers into a 10 long buffer so we hopefully don't miss pulses while processing noise +RingBuf new_pulse_low[2]; +RingBuf new_pulse_high[2]; //These are the interrupt routines. Make them as short as possible so we don't block other interrupts (for example serial data) -/* There are situations where a CHANGE on GPIO input is detected from HIGH to HIGH (or maybe also LOW to LOW) - * So we need an extra check for the pulse changes for FALLING and RISING - */ -ICACHE_RAM_ATTR void onS0Pulse1() { - unsigned long currentTime = millis(); - if (last_state_s0[0] == LOW ) { - if (digitalRead(actS0Settings[0].gpiopin) == HIGH) { //make sure we are high now - //this was a rising - last_state_s0[0] = HIGH; - new_pulse_high_s0[0] = currentTime; - } - } else { - if (digitalRead(actS0Settings[0].gpiopin) == LOW) { //make sure we are low now - //this was a falling - last_state_s0[0] = LOW; - new_pulse_low_s0[0] = currentTime; - } - } +//We are using seperate rising and falling routines as 'changing' doesn't seem to work properly. Need to test more. +ICACHE_RAM_ATTR void onS0Pulse1Rising(); //predefine +ICACHE_RAM_ATTR void onS0Pulse2Rising(); //predefine + +ICACHE_RAM_ATTR void onS0Pulse1Falling() { + new_pulse_low[0].push(millis()); + attachInterrupt(digitalPinToInterrupt(actS0Settings[0].gpiopin), onS0Pulse1Rising, RISING); } -ICACHE_RAM_ATTR void onS0Pulse2() { - unsigned long currentTime = millis(); - if (last_state_s0[1] == LOW ) { - if (digitalRead(actS0Settings[1].gpiopin) == HIGH) { //make sure we are high now - //this was a rising - last_state_s0[1] = HIGH; - new_pulse_high_s0[1] = currentTime; - } - } else { - if (digitalRead(actS0Settings[1].gpiopin) == LOW) { //make sure we are low now - //this was a falling - last_state_s0[1] = LOW; - new_pulse_low_s0[1] = currentTime; - } - } +ICACHE_RAM_ATTR void onS0Pulse1Rising() { + new_pulse_high[0].push(millis()); + attachInterrupt(digitalPinToInterrupt(actS0Settings[0].gpiopin), onS0Pulse1Falling, FALLING); +} + +ICACHE_RAM_ATTR void onS0Pulse2Falling() { + new_pulse_low[1].push(millis()); + attachInterrupt(digitalPinToInterrupt(actS0Settings[1].gpiopin), onS0Pulse2Rising, RISING); +} + +ICACHE_RAM_ATTR void onS0Pulse2Rising() { + new_pulse_high[1].push(millis()); + attachInterrupt(digitalPinToInterrupt(actS0Settings[1].gpiopin), onS0Pulse2Falling, FALLING); } void initS0Sensors(s0SettingsStruct s0Settings[]) { @@ -62,7 +50,7 @@ void initS0Sensors(s0SettingsStruct s0Settings[]) { actS0Settings[0].lowerPowerInterval = s0Settings[0].lowerPowerInterval; pinMode(actS0Settings[0].gpiopin, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(actS0Settings[0].gpiopin), onS0Pulse1, CHANGE); + attachInterrupt(digitalPinToInterrupt(actS0Settings[0].gpiopin), onS0Pulse1Falling, FALLING); actS0Data[0].nextReport = millis() + MINREPORTEDS0TIME; //initial report after interval, not directly at boot //setup s0 port 2 @@ -70,7 +58,7 @@ void initS0Sensors(s0SettingsStruct s0Settings[]) { actS0Settings[1].ppkwh = s0Settings[1].ppkwh; actS0Settings[1].lowerPowerInterval = s0Settings[1].lowerPowerInterval; pinMode(actS0Settings[1].gpiopin, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(actS0Settings[1].gpiopin), onS0Pulse2, CHANGE); + attachInterrupt(digitalPinToInterrupt(actS0Settings[1].gpiopin), onS0Pulse2Falling, FALLING); actS0Data[1].nextReport = millis() + MINREPORTEDS0TIME; //initial report after interval, not directly at boot } @@ -102,41 +90,42 @@ void s0Loop(PubSubClient &mqtt_client, void (*log_message)(char*), char* mqtt_to for (int i = 0 ; i < NUM_S0_COUNTERS ; i++) { char tmp_log_msg[256]; unsigned long pulseInterval = 0; + unsigned long cur_pulse_low = 0; + unsigned long cur_pulse_high = 0; + //first handle new detected pulses - noInterrupts(); - unsigned long new_pulse_low = new_pulse_low_s0[i]; - unsigned long new_pulse_high = new_pulse_high_s0[i]; - interrupts(); - if (new_pulse_high > new_pulse_low) { - //pulse detected, now check if it is valid - if ( ((new_pulse_high - new_pulse_low) > actS0Settings[i].minimalPulseWidth) && ((new_pulse_high - new_pulse_low) < 10 * actS0Settings[i].minimalPulseWidth) ) { //within pulse width and pulse width * 10 - pulseInterval = new_pulse_low - actS0Data[i].lastPulse; - if (pulseInterval > actS0Settings[i].minimalPulseWidth ) { - sprintf_P(tmp_log_msg, PSTR("S0 port %i pulse counted as valid! (pulse width: %lu, interval: %lu)"), i + 1, (new_pulse_high - new_pulse_low), pulseInterval); - log_message(tmp_log_msg); - actS0Data[i].goodPulses++; - if (actS0Data[i].lastPulse > 0) { //Do not calculate watt for the first pulse since reboot because we will always report a too high watt. Better to show 0 watt at first pulse. - actS0Data[i].watt = (3600000000.0 / pulseInterval) / actS0Settings[i].ppkwh; - } - actS0Data[i].lastPulse = new_pulse_low; - actS0Data[i].pulses++; - if ((unsigned long)(actS0Data[i].nextReport - millisThisLoop) > MINREPORTEDS0TIME) { //loop was in standby interval - actS0Data[i].nextReport = 0; // report now + if (new_pulse_high[i].lockedPop(cur_pulse_high)) { // if there is a rising edge there must be a pulse + if (new_pulse_low[i].lockedPop(cur_pulse_low)) { // so get the falling edge time also. We don't need to stop interrupts between popping both values because new values during interrupt will be pushed on the end of the ringbuffer + if (cur_pulse_high > cur_pulse_low) { //and then check if indeed the rising edge is after falling edge + //pulse detected, now check if it is valid + if ( ((cur_pulse_high - cur_pulse_low) > actS0Settings[i].minimalPulseWidth) && ((cur_pulse_high - cur_pulse_low) < 10 * actS0Settings[i].minimalPulseWidth) ) { //within pulse width and pulse width * 10 + pulseInterval = cur_pulse_low - actS0Data[i].lastPulse; + if (pulseInterval > actS0Settings[i].minimalPulseWidth ) { + sprintf_P(tmp_log_msg, PSTR("S0 port %i pulse counted as valid! (pulse width: %lu, interval: %lu)"), i + 1, (cur_pulse_high - cur_pulse_low), pulseInterval); + log_message(tmp_log_msg); + actS0Data[i].goodPulses++; + if (actS0Data[i].lastPulse > 0) { //Do not calculate watt for the first pulse since reboot because we will always report a too high watt. Better to show 0 watt at first pulse. + actS0Data[i].watt = (3600000000.0 / pulseInterval) / actS0Settings[i].ppkwh; + } + actS0Data[i].lastPulse = cur_pulse_low; + actS0Data[i].pulses++; + + if ((unsigned long)(actS0Data[i].nextReport - millisThisLoop) > MINREPORTEDS0TIME) { //loop was in standby interval + actS0Data[i].nextReport = 0; // report now + } + } else { + sprintf_P(tmp_log_msg, PSTR("S0 port %i pulse counted as invalid! (pulse width: %lu, interval: %lu)"), i + 1, (cur_pulse_high - cur_pulse_low), pulseInterval); + log_message(tmp_log_msg); + actS0Data[i].badPulses++; + } + } else { + sprintf_P(tmp_log_msg, PSTR("S0 port %i pulse reset. Noise detected! (pulse width: %lu)"), i + 1, (cur_pulse_high - cur_pulse_low)); + log_message(tmp_log_msg); + actS0Data[i].badPulses++; } - } else { - sprintf_P(tmp_log_msg, PSTR("S0 port %i pulse counted as invalid! (pulse width: %lu, interval: %lu)"), i + 1, (new_pulse_high - new_pulse_low), pulseInterval); - log_message(tmp_log_msg); - actS0Data[i].badPulses++; } - } else { - sprintf_P(tmp_log_msg, PSTR("S0 port %i pulse reset. Noise detected! (pulse width: %lu)"), i + 1, (new_pulse_high - new_pulse_low)); - log_message(tmp_log_msg); - actS0Data[i].badPulses++; } - noInterrupts(); - new_pulse_high_s0[i] = new_pulse_low_s0[i]; //only need to reset the time for high pulse because low always needs to be first for a valid pulse - interrupts(); } diff --git a/HeishaMon/version.h b/HeishaMon/version.h index 40763c6e..cf30c19e 100644 --- a/HeishaMon/version.h +++ b/HeishaMon/version.h @@ -1 +1 @@ -static const char* heishamon_version = "2.1-iy-1"; +static const char* heishamon_version = "2.1-iy-2"; From ad4e32d50a7b99e5ef1d419a55ee3e0073d40c88 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Wed, 25 Aug 2021 08:16:10 +0200 Subject: [PATCH 03/75] code cleanup --- HeishaMon/HeishaMon.ino | 10 ++++----- HeishaMon/commands.cpp | 44 +++++++++++++++++++------------------- HeishaMon/dallas.cpp | 2 +- HeishaMon/htmlcode.h | 10 ++++----- HeishaMon/webfunctions.cpp | 2 +- LIBSUSED.md | 8 +++---- README.md | 6 +++--- 7 files changed, 41 insertions(+), 41 deletions(-) diff --git a/HeishaMon/HeishaMon.ino b/HeishaMon/HeishaMon.ino index a56191a8..72aea015 100644 --- a/HeishaMon/HeishaMon.ino +++ b/HeishaMon/HeishaMon.ino @@ -595,18 +595,18 @@ void setupConditionals() { if (heishamonSettings.use_s0) initS0Sensors(heishamonSettings.s0Settings); } void setup() { - + //first get total memory before we do anything getFreeMemory(); //set boottime getUptime(); - - + + setupSerial(); setupSerial1(); - - + + Serial.println(); Serial.println(F("--- HEISHAMON ---")); Serial.println(F("starting...")); diff --git a/HeishaMon/commands.cpp b/HeishaMon/commands.cpp index 05c64c44..67f94622 100644 --- a/HeishaMon/commands.cpp +++ b/HeishaMon/commands.cpp @@ -64,7 +64,7 @@ unsigned int set_heatpump_state(char *msg, unsigned char *cmd, char *log_msg) { } unsigned int set_pump(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_pump_string(msg); byte pump_state = 16; @@ -87,7 +87,7 @@ unsigned int set_pump(char *msg, unsigned char *cmd, char *log_msg) { } unsigned int set_max_pump_duty(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_pumpduty_string(msg); byte pumpduty = set_pumpduty_string.toInt() + 1; @@ -107,7 +107,7 @@ unsigned int set_max_pump_duty(char *msg, unsigned char *cmd, char *log_msg) { } unsigned int set_quiet_mode(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_quiet_mode_string(msg); byte quiet_mode = (set_quiet_mode_string.toInt() + 1) * 8; @@ -127,7 +127,7 @@ unsigned int set_quiet_mode(char *msg, unsigned char *cmd, char *log_msg) { } unsigned int set_z1_heat_request_temperature(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_temperature_string(msg); byte request_temp = set_temperature_string.toInt() + 128; @@ -147,7 +147,7 @@ unsigned int set_z1_heat_request_temperature(char *msg, unsigned char *cmd, char } unsigned int set_z1_cool_request_temperature(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_temperature_string(msg); byte request_temp = set_temperature_string.toInt() + 128; @@ -167,7 +167,7 @@ unsigned int set_z1_cool_request_temperature(char *msg, unsigned char *cmd, char } unsigned int set_z2_heat_request_temperature(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_temperature_string(msg); byte request_temp = set_temperature_string.toInt() + 128; @@ -187,7 +187,7 @@ unsigned int set_z2_heat_request_temperature(char *msg, unsigned char *cmd, char } unsigned int set_z2_cool_request_temperature(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_temperature_string(msg); byte request_temp = set_temperature_string.toInt() + 128; @@ -207,7 +207,7 @@ unsigned int set_z2_cool_request_temperature(char *msg, unsigned char *cmd, char } unsigned int set_force_DHW(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_force_DHW_string(msg); byte force_DHW_mode = 64; //hex 0x40 @@ -230,7 +230,7 @@ unsigned int set_force_DHW(char *msg, unsigned char *cmd, char *log_msg) { } unsigned int set_force_defrost(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_force_defrost_string(msg); byte force_defrost_mode = 0; @@ -253,7 +253,7 @@ unsigned int set_force_defrost(char *msg, unsigned char *cmd, char *log_msg) { } unsigned int set_force_sterilization(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_force_sterilization_string(msg); byte force_sterilization_mode = 0; @@ -276,7 +276,7 @@ unsigned int set_force_sterilization(char *msg, unsigned char *cmd, char *log_ms } unsigned int set_holiday_mode(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_holiday_string(msg); byte set_holiday = 16; //hex 0x10 @@ -299,7 +299,7 @@ unsigned int set_holiday_mode(char *msg, unsigned char *cmd, char *log_msg) { } unsigned int set_powerful_mode(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_powerful_string(msg); byte set_powerful = (set_powerful_string.toInt() ) + 73; @@ -319,7 +319,7 @@ unsigned int set_powerful_mode(char *msg, unsigned char *cmd, char *log_msg) { } unsigned int set_DHW_temp(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_DHW_temp_string(msg); byte set_DHW_temp = set_DHW_temp_string.toInt() + 128; @@ -339,7 +339,7 @@ unsigned int set_DHW_temp(char *msg, unsigned char *cmd, char *log_msg) { } unsigned int set_operation_mode(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_mode_string(msg); byte set_mode; @@ -405,7 +405,7 @@ unsigned int set_curves(char *msg, unsigned char *cmd, char *log_msg) { } unsigned int set_zones(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_mode_string(msg); byte set_mode; @@ -431,7 +431,7 @@ unsigned int set_zones(char *msg, unsigned char *cmd, char *log_msg) { } unsigned int set_floor_heat_delta(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_temperature_string(msg); byte request_temp = set_temperature_string.toInt() + 128; @@ -451,7 +451,7 @@ unsigned int set_floor_heat_delta(char *msg, unsigned char *cmd, char *log_msg) } unsigned int set_floor_cool_delta(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_temperature_string(msg); byte request_temp = set_temperature_string.toInt() + 128; @@ -471,7 +471,7 @@ unsigned int set_floor_cool_delta(char *msg, unsigned char *cmd, char *log_msg) } unsigned int set_dhw_heat_delta(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String set_temperature_string(msg); byte request_temp = set_temperature_string.toInt() + 128; @@ -515,7 +515,7 @@ unsigned int set_reset(char *msg, unsigned char *cmd, char *log_msg) { } unsigned int set_heater_delay_time(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String stringValue(msg); byte byteValue = stringValue.toInt() + 1; @@ -534,7 +534,7 @@ unsigned int set_heater_delay_time(char *msg, unsigned char *cmd, char *log_msg) return sizeof(panasonicSendQuery); } unsigned int set_heater_start_delta(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String stringValue(msg); byte byteValue = stringValue.toInt() + 128; @@ -553,7 +553,7 @@ unsigned int set_heater_start_delta(char *msg, unsigned char *cmd, char *log_msg return sizeof(panasonicSendQuery); } unsigned int set_heater_stop_delta(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String stringValue(msg); byte byteValue = stringValue.toInt() + 128; @@ -572,7 +572,7 @@ unsigned int set_heater_stop_delta(char *msg, unsigned char *cmd, char *log_msg) return sizeof(panasonicSendQuery); } unsigned int set_main_schedule(char *msg, unsigned char *cmd, char *log_msg) { - unsigned int len = 0; + String stringValue(msg); byte byteValue = 64; //hex 0x40 diff --git a/HeishaMon/dallas.cpp b/HeishaMon/dallas.cpp index caf97ba0..b298ac99 100644 --- a/HeishaMon/dallas.cpp +++ b/HeishaMon/dallas.cpp @@ -90,7 +90,7 @@ void readNewDallasTemp(PubSubClient &mqtt_client, void (*log_message)(char*), ch } void dallasLoop(PubSubClient &mqtt_client, void (*log_message)(char*), char* mqtt_topic_base) { - if ((DALLASASYNC) && ((unsigned long)(millis() - dallasTimer) > ((1000 * dallasTimerWait)-1000)) ) { + if ((DALLASASYNC) && ((unsigned long)(millis() - dallasTimer) > ((1000 * dallasTimerWait) - 1000)) ) { DS18B20.requestTemperatures(); // get temperatures for next run 1 second before getting the temperatures (async) } if ((unsigned long)(millis() - dallasTimer) > (1000 * dallasTimerWait)) { diff --git a/HeishaMon/htmlcode.h b/HeishaMon/htmlcode.h index ab6cacb8..161a3625 100644 --- a/HeishaMon/htmlcode.h +++ b/HeishaMon/htmlcode.h @@ -571,14 +571,14 @@ static const char webCSS[] PROGMEM = "#cli{ background: black; color: white; width: 100%; height: 400px!important; }" ""; - + static const char changewifissidJS[] PROGMEM = ""; + ""; static const char populatescanwifiJS[] PROGMEM = ""; + +static const char settingsForm[] PROGMEM = +"
" +"

Settings

" +"
" +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +"
Please wait, loading saved settings...
" +" Hostname:" +" " +"
" +" Wifi SSID:" +" " +" " +"
" +" Wifi password:" +" " +"
" +" Update username:" +" " +"
" +" Current update password:" +" default password: \"heisha\"" +"
" +" New update password:" +" " +"
" +" Mqtt topic base:" +" " +"
" +" Mqtt server:" +" " +"
" +" Mqtt port:" +" " +"
" +" Mqtt username:" +" " +"
" +" Mqtt password:" +" " +"
" +" How often new values are collected from heatpump:" +" seconds (min 5 sec)" +"
" +" How often all heatpump values are retransmitted to MQTT broker:" +" seconds" +"
" +" Listen only mode:" +" " +"
" +" Debug log to MQTT topic from start:" +" " +"
" +" Debug log hexdump enable from start:" +" " +"
" +" Debug log to serial1 (GPIO2):" +" " +"
" +" Emulate optional PCB:" +" " +"
" +" " +" " +" " +" " +" " +"
" +" Use 1wire DS18b20:" +" " +"
" +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +"
" +" How often new values are collected from 1wire:" +" seconds (min 5 sec)" +"
" +" How often all 1wire values are retransmitted to MQTT broker:" +" seconds" +"
" +" DS18b20 temperature resolution:" +" " +" " +" " +" " +"
" +" " +" " +" " +" " +" " +"
" +" Use s0 kWh metering:" +" " +"
" +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +"
S0 port 1 GPIO:" +" " +"
S0 port 1 imp/kwh:" +" " +"
S0 port 1 reporting interval during standby/low power usage:" +" seconds" +"
S0 port 1 minimal pulse width:" +" milliseconds" +"
S0 port 1 maximal pulse width:" +" milliseconds" +"
S0 port 1 standby/low power usage threshold: Watt" +"
S0 port 2 GPIO:" +" " +"
S0 port 2 imp/kwh:" +" " +"
S0 port 2 reporting interval during standby/low power usage:" +" seconds" +"
S0 port 2 minimal pulse width:" +" milliseconds" +"
S0 port 2 maximal pulse width:" +" milliseconds" +"
S0 port 2 standby/low power usage threshold: Watt" +"
" +"

" +" " +"
" +"
Factory reset" +"
"; + +const char populategetsettingsJS[] PROGMEM = + ""; + +static const char showFirmwarePage[] PROGMEM = + "" + "
" + "
" + " Firmware:
" + "

" + " " + "
" + "
"; + +static const char firmwareSuccessResponse[] PROGMEM = + "Update success! Rebooting..."; + +static const char firmwareFailResponse[] PROGMEM = + "Update failed! Please try again..."; diff --git a/HeishaMon/s0.cpp b/HeishaMon/s0.cpp index f2cc3a64..ff402371 100644 --- a/HeishaMon/s0.cpp +++ b/HeishaMon/s0.cpp @@ -182,37 +182,89 @@ void s0Loop(PubSubClient &mqtt_client, void (*log_message)(char*), char* mqtt_to } unsigned long tablePulses[NUM_S0_COUNTERS]; -String s0TableOutput() { - String output = F(""); + +void s0TableOutput(struct webserver_t *client) { for (int i = 0; i < NUM_S0_COUNTERS; i++) { - output = output + F(""); - output = output + F("") + (i + 1) + F(""); - output = output + F("") + actS0Data[i].watt + F(""); - output = output + F("") + ((actS0Data[i].pulsesTotal - tablePulses[i]) * ( 1000.0 / actS0Settings[i].ppkwh)) + F(""); + webserver_send_content_P(client, PSTR(""), 8); + + char str[12]; + itoa(i+1, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(""), 9); + + itoa(actS0Data[i].watt, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(""), 9); + + itoa(((actS0Data[i].pulsesTotal - tablePulses[i]) * ( 1000.0/actS0Settings[i].ppkwh)), str, 10); + webserver_send_content(client, str, strlen(str)); + tablePulses[i] = actS0Data[i].pulsesTotal; - output = output + F("") + (actS0Data[i].pulsesTotal * ( 1000.0 / actS0Settings[i].ppkwh)) + F(""); - output = output + F("") + (100 * (actS0Data[i].goodPulses + 1) / (actS0Data[i].goodPulses + actS0Data[i].badPulses + 1)) + F("% "); - output = output + F("") + actS0Data[i].avgPulseWidth + F(""); - output = output + F(""); + + webserver_send_content_P(client, PSTR(""), 9); + + itoa((actS0Data[i].pulsesTotal * (1000.0 / actS0Settings[i].ppkwh)), str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(""), 9); + + itoa((100 * (actS0Data[i].goodPulses + 1) / (actS0Data[i].goodPulses + actS0Data[i].badPulses + 1)), str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR("%"), 10); + + itoa(actS0Data[i].avgPulseWidth, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(""), 10); } - return output; } unsigned long jsonPulses[NUM_S0_COUNTERS]; -String s0JsonOutput() { - String output = F("["); + +void s0JsonOutput(struct webserver_t *client) { + webserver_send_content_P(client, PSTR("["), 1); for (int i = 0; i < NUM_S0_COUNTERS; i++) { - output = output + F("{"); - output = output + F("\"S0 port\": \"") + (i + 1) + F("\","); - output = output + F("\"Watt\": \"") + actS0Data[i].watt + F("\","); - output = output + F("\"Watthour\": \"") + ((actS0Data[i].pulsesTotal - jsonPulses[i]) * ( 1000.0 / actS0Settings[i].ppkwh)) + F("\","); + webserver_send_content_P(client, PSTR("{\"S0 port\":\""), 12); + + char str[12]; + itoa(i+1, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR("\",\"Watt\":\""), 10); + + itoa(actS0Data[i].watt, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR("\",\"Watthour\":\""), 14); + + itoa(((actS0Data[i].pulsesTotal - tablePulses[i]) * (1000.0/actS0Settings[i].ppkwh)), str, 10); + webserver_send_content(client, str, strlen(str)); + jsonPulses[i] = actS0Data[i].pulsesTotal; - output = output + F("\"WatthourTotal\": \"") + (actS0Data[i].pulsesTotal * ( 1000.0 / actS0Settings[i].ppkwh)) + F("\","); - output = output + F("\"PulseQuality\": \"") + (100 * (actS0Data[i].goodPulses + 1) / (actS0Data[i].goodPulses + actS0Data[i].badPulses + 1)) + F("\","); - output = output + F("\"AvgPulseWidth\": \"") + actS0Data[i].avgPulseWidth + F("\""); - output = output + F("}"); - if (i < NUM_S0_COUNTERS - 1) output = output + F(","); + + webserver_send_content_P(client, PSTR("\",\"WatthourTotal\":\""), 19); + + itoa((actS0Data[i].pulsesTotal * (1000.0 / actS0Settings[i].ppkwh)), str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR("\",\"PulseQuality\":\""), 18); + + itoa((100 * (actS0Data[i].goodPulses + 1) / (actS0Data[i].goodPulses + actS0Data[i].badPulses + 1)), str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR("\",\"AvgPulseWidth\":\""), 19); + + itoa(actS0Data[i].avgPulseWidth, str, 10); + webserver_send_content(client, str, strlen(str)); + + if(i < NUM_S0_COUNTERS - 1) { + webserver_send_content_P(client, PSTR("\"},"), 3); + } else { + webserver_send_content_P(client, PSTR("\"}"), 2); + } } - output = output + F("]"); - return output; + webserver_send_content_P(client, PSTR("]"), 1); } diff --git a/HeishaMon/s0.h b/HeishaMon/s0.h index 9d948cab..3292e3a8 100644 --- a/HeishaMon/s0.h +++ b/HeishaMon/s0.h @@ -1,4 +1,5 @@ #include +#include "src/common/webserver.h" #define NUM_S0_COUNTERS 2 #define DEFAULT_S0_PIN_1 12 // S0_1 pin, for now a static config - should be in config menu later @@ -26,5 +27,5 @@ struct s0DataStruct { void initS0Sensors(s0SettingsStruct s0Settings[]); void restore_s0_Watthour(int s0Port, float watthour); void s0Loop(PubSubClient &mqtt_client, void (*log_message)(char*), char* mqtt_topic_base, s0SettingsStruct s0Settings[]); -String s0TableOutput(void); -String s0JsonOutput(void); +void s0TableOutput(struct webserver_t *client); +void s0JsonOutput(struct webserver_t *client); diff --git a/HeishaMon/smartcontrol.cpp b/HeishaMon/smartcontrol.cpp deleted file mode 100644 index a71cc7ce..00000000 --- a/HeishaMon/smartcontrol.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "commands.h" -#include "smartcontrol.h" - -unsigned long heatCurveTimer = 0; -bool heatCurveFirst = true; -short avgOutsideTemp = 0; -short avgOutsideTempArray[96] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - -// returns the calculated average outside temperature -String getAvgOutsideTemp() { - char avgOutsideTempStr[55]; - sprintf_P(avgOutsideTempStr, PSTR("Current calculated average outside temperature: %dC."), avgOutsideTemp); - return String(avgOutsideTempStr); -} - -bool send_command(byte* command, int length); - -void smartControlLoop(void (*log_message)(char*), SmartControlSettingsStruct SmartControlSettings, String actData[], unsigned long goodreads) { - if (goodreads > 0) { - char log_msg[256]; - if ((millis() - heatCurveTimer > (1000 * 1800)) || heatCurveFirst) { //every 0.5h - log_message((char*)"Calculate new outside temperature average"); - heatCurveTimer = millis(); - - short currentOutsideTemp = actData[14].toInt(); - if (heatCurveFirst) { - log_message((char*)"Fill average outside temperature array"); - heatCurveFirst = false; - for (unsigned int i = 0 ; i < 96 ; i++) { - avgOutsideTempArray[i] = currentOutsideTemp; - } - } else { - log_message((char*)"Add current outside temperature to average array"); - for (unsigned int i = 95 ; i > 0 ; i--) { - avgOutsideTempArray[i] = avgOutsideTempArray[i - 1]; - } - avgOutsideTempArray[0] = currentOutsideTemp; - } - - long outsideTempSum = 0; - for (unsigned int i = 0 ; i < ((SmartControlSettings.avgHourHeatCurve * 2) + 1); i++) { - outsideTempSum = outsideTempSum + avgOutsideTempArray[i]; - } - - avgOutsideTemp = int(outsideTempSum / (SmartControlSettings.avgHourHeatCurve * 2)); - sprintf_P(log_msg, PSTR("Current calculated average outside temperature: %dC."), avgOutsideTemp); - log_message(log_msg); - - log_message((char*)"Send new heat request temperature setpoint"); - short heatRequest = int(SmartControlSettings.heatCurveLookup[35]); - if (avgOutsideTemp > 15) { - heatRequest = int(SmartControlSettings.heatCurveLookup[35]); - } else if (avgOutsideTemp < -20) { - heatRequest = int(SmartControlSettings.heatCurveLookup[0]); - } else { - heatRequest = int(SmartControlSettings.heatCurveLookup[(avgOutsideTemp + 20)]); - } - sprintf(log_msg, "Current heat request temperature: %dC.", heatRequest); log_message(log_msg); - - log_msg[0] = 0; - unsigned char cmd[256] = { 0 }; - char msg[3]; - unsigned int len = 0; - sprintf(msg, "%d", heatRequest); - len = set_z1_heat_request_temperature(msg, cmd, log_msg); - log_message(log_msg); - send_command(cmd, len); - } - } -} diff --git a/HeishaMon/smartcontrol.h b/HeishaMon/smartcontrol.h deleted file mode 100644 index c1f04388..00000000 --- a/HeishaMon/smartcontrol.h +++ /dev/null @@ -1,15 +0,0 @@ -struct SmartControlSettingsStruct { - bool enableHeatCurve = false; //Enable or dissable heating curve control from Heishamon - - short avgHourHeatCurve = 0; // Outside temperature average of hours for heating curve control - short heatCurveTargetHigh = 60; // Heating curve target high temperature - short heatCurveTargetLow = 20; // Heating curve target low temperature - short heatCurveOutHigh = 15; // Heating curve outside high temperature - short heatCurveOutLow = -20; // Heating curve outside low temperature - - short heatCurveLookup[36] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // Lookup table for heating curve -}; - -String getAvgOutsideTemp(void); - -void smartControlLoop(void (*log_message)(char*), SmartControlSettingsStruct SmartControlSettings, String actData[], unsigned long goodreads); diff --git a/HeishaMon/src/common/mem.cpp b/HeishaMon/src/common/mem.cpp new file mode 100755 index 00000000..305f3c4b --- /dev/null +++ b/HeishaMon/src/common/mem.cpp @@ -0,0 +1,19 @@ +/* + Copyright (C) CurlyMo + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +unsigned int alignedbytes(int v) { + return v; +} + +unsigned int alignedbuffer(int v) { +#ifdef ESP8266 + return (v + 3) & ~0x3; +#else + return v; +#endif +} diff --git a/HeishaMon/src/common/mem.h b/HeishaMon/src/common/mem.h new file mode 100755 index 00000000..1540b431 --- /dev/null +++ b/HeishaMon/src/common/mem.h @@ -0,0 +1,23 @@ +/* + Copyright (C) CurlyMo + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +#ifndef _MEM_H_ +#define _MEM_H_ + +unsigned int alignedbytes(int v); +unsigned int alignedbuffer(int v); + +#define OUT_OF_MEMORY while(0) { } + +#define STRDUP strdup +#define REALLOC realloc +#define CALLOC calloc +#define MALLOC malloc +#define FREE(a) do { free(a); (a) = NULL; } while(0) + +#endif diff --git a/HeishaMon/src/common/strncasestr.cpp b/HeishaMon/src/common/strncasestr.cpp new file mode 100755 index 00000000..7f744120 --- /dev/null +++ b/HeishaMon/src/common/strncasestr.cpp @@ -0,0 +1,40 @@ +/* + Copyright (C) CurlyMo + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +#include +#include +#include + +unsigned char *strncasestr(unsigned char *str1, const char *str2, uint16_t size) { + uint16_t a = 0, b = 0, c = 0; + uint16_t len = strlen(str2); + for(a=0;a= 65 && ch <= 90) { + ch += 32; + } + if(ch == str2[0] && a+len <= size) { + c = a; + a++; + for(b=1;b<=len;a++, b++) { + ch = str1[a]; + if(ch >= 65 && ch <= 90) { + ch += 32; + } + if(str2[b] != ch) { + break; + } + } + if(b == len) { + return &str1[a-len]; + } + a = c; + } + } + return NULL; +} diff --git a/HeishaMon/src/common/strncasestr.h b/HeishaMon/src/common/strncasestr.h new file mode 100755 index 00000000..97fc630e --- /dev/null +++ b/HeishaMon/src/common/strncasestr.h @@ -0,0 +1,21 @@ +/* + Copyright (C) CurlyMo + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +#ifndef STRNCASESTR +#define STRNCASESTR + +#include +#include + +/* + * Find the first occurrence of find in s, where the search is limited to the + * first slen characters of s. + */ +unsigned char *strncasestr(unsigned char *str1, const char *str2, uint16_t len); + +#endif \ No newline at end of file diff --git a/HeishaMon/src/common/strnstr.cpp b/HeishaMon/src/common/strnstr.cpp new file mode 100755 index 00000000..f2a647f9 --- /dev/null +++ b/HeishaMon/src/common/strnstr.cpp @@ -0,0 +1,34 @@ +/* + Copyright (C) CurlyMo + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +#include +#include +#include + +unsigned char *strnstr(unsigned char *str1, const char *str2, uint16_t size) { + uint16_t a = 0, b = 0, c = 0; + uint16_t len = strlen(str2); + for(a=0;a +#include + +/* + * Find the first occurrence of find in s, where the search is limited to the + * first slen characters of s. + */ +unsigned char *strnstr(unsigned char *str1, const char *str2, uint16_t len); + +#endif \ No newline at end of file diff --git a/HeishaMon/src/common/timerqueue.cpp b/HeishaMon/src/common/timerqueue.cpp index 35aea66e..0ba39352 100644 --- a/HeishaMon/src/common/timerqueue.cpp +++ b/HeishaMon/src/common/timerqueue.cpp @@ -7,41 +7,48 @@ */ #ifdef ESP8266 -#include + #include #endif #include #include #include #include -#include #include +#include #include "mem.h" #include "timerqueue.h" static unsigned int lasttime = 0; +static unsigned int *calls = NULL; +static unsigned int nrcalls = 0; + +#ifndef ESP8266 +static unsigned int micros() { + struct timeval tv; + gettimeofday(&tv,NULL); + + return 1000000 * tv.tv_sec + tv.tv_usec;; +} +#endif static void timerqueue_sort() { - int a = 1; // parent; - int b = a*2; // left child; - - while(b <= timerqueue_size) { - if(b < timerqueue_size && - ((timerqueue[b]->sec > timerqueue[b+1]->sec) || - (timerqueue[b]->sec == timerqueue[b+1]->sec && timerqueue[b]->usec > timerqueue[b+1]->usec))) { - b++; - } - if((timerqueue[a]->sec > timerqueue[b]->sec) || - (timerqueue[a]->sec == timerqueue[b]->sec && timerqueue[a]->usec > timerqueue[b]->usec)) { - struct timerqueue_t *tmp = timerqueue[a]; - timerqueue[a] = timerqueue[b]; - timerqueue[b] = tmp; - } else { - break; + int matched = 1; + while(matched) { + int a = 0; + matched = 0; + for(a=0;aremove < timerqueue[a+1]->remove || + (timerqueue[a]->remove == timerqueue[a+1]->remove && timerqueue[a]->sec > timerqueue[a+1]->sec) || + (timerqueue[a]->remove == timerqueue[a+1]->remove && timerqueue[a]->sec == timerqueue[a+1]->sec && timerqueue[a]->usec > timerqueue[a+1]->usec)) { + struct timerqueue_t *node = timerqueue[a+1]; + timerqueue[a+1] = timerqueue[a]; + timerqueue[a] = node; + matched = 1; + break; + } } - a = b; - b = a*2; } } @@ -49,80 +56,137 @@ struct timerqueue_t *timerqueue_pop() { if(timerqueue_size == 0) { return NULL; } - struct timerqueue_t *x = timerqueue[1]; - timerqueue[1] = timerqueue[timerqueue_size]; - timerqueue[timerqueue_size] = NULL; - - int a = 1; // parent; - int b = a*2; // left child; + struct timerqueue_t *x = timerqueue[0]; + timerqueue[0] = timerqueue[timerqueue_size-1]; timerqueue_size--; + + if(timerqueue_size == 0) { + free(timerqueue); + timerqueue = NULL; + } else { + if((timerqueue = (struct timerqueue_t **)realloc(timerqueue, sizeof(struct timerqueue_t *)*timerqueue_size)) == NULL) { + #ifdef ESP8266 + Serial.printf("Out of memory %s:#%d\n", __FUNCTION__, __LINE__); + ESP.restart(); + exit(-1); + #else + fprintf(stderr, "Out of memory %s:#%d\n", __FUNCTION__, __LINE__); + exit(-1); + #endif + } + } + + int a = 0; + for(a=0;asec -= x->sec; + timerqueue[a]->usec -= x->usec; + if(timerqueue[a]->usec < 0) { + timerqueue[a]->sec -= 1; + timerqueue[a]->usec += 1000000; + } + } + timerqueue_sort(); return x; } struct timerqueue_t *timerqueue_peek() { - return timerqueue[1]; + if(timerqueue_size == 0) { + return NULL; + } + return timerqueue[0]; } void timerqueue_insert(int sec, int usec, int nr) { - int a = 0; - - for(a=1;a<=timerqueue_size;a++) { + struct timerqueue_t *node = NULL; + int a = 0, matched = 0, x = 0, y = 0; + + for(a=0;anr == nr) { timerqueue[a]->sec = sec; timerqueue[a]->usec = usec; + if(sec <= 0 && usec <= 0) { + timerqueue[a]->remove = 1; + for(x=0;xnr) { + if(nrcalls > 0) { + for(y=x;yremove == 1) { + struct timerqueue_t *node = timerqueue_pop(); + free(node); + } else { + break; + } } + + return; + } else if(sec == 0 && usec == 0) { + return; } - if((timerqueue = (struct timerqueue_t **)realloc(timerqueue, sizeof(struct timerqueue_t *)*(timerqueue_size+2))) == NULL) { - //OUT_OF_MEMORY + if((timerqueue = (struct timerqueue_t **)realloc(timerqueue, sizeof(struct timerqueue_t *)*(timerqueue_size+1))) == NULL) { +#ifdef ESP8266 + Serial.printf("Out of memory %s:#%d\n", __FUNCTION__, __LINE__); + ESP.restart(); + exit(-1); +#else + fprintf(stderr, "Out of memory %s:#%d\n", __FUNCTION__, __LINE__); + exit(-1); +#endif } - struct timerqueue_t *node = (struct timerqueue_t *)malloc(sizeof(struct timerqueue_t)); + node = (struct timerqueue_t *)malloc(sizeof(struct timerqueue_t)); if(node == NULL) { - //OUT_OF_MEMORY +#ifdef ESP8266 + Serial.printf("Out of memory %s:#%d\n", __FUNCTION__, __LINE__); + ESP.restart(); + exit(-1); +#else + fprintf(stderr, "Out of memory %s:#%d\n", __FUNCTION__, __LINE__); + exit(-1); +#endif } memset(node, 0, sizeof(struct timerqueue_t)); node->sec = sec; node->usec = usec; node->nr = nr; - int i = (timerqueue_size+1)/2; // parent - int x = timerqueue_size+1; // child - - timerqueue[timerqueue_size+1] = node; - while(i > 0) { - struct timerqueue_t *tmp = timerqueue[i]; - if((timerqueue[x]->sec < tmp->sec) || - (timerqueue[x]->sec == tmp->sec && timerqueue[x]->usec < tmp->usec)) { - timerqueue[i] = timerqueue[x]; - timerqueue[x] = tmp; - } - x = i; // parent becomes child - i /= 2; // new parent - } - timerqueue_size++; + timerqueue[timerqueue_size++] = node; + timerqueue_sort(); } void timerqueue_update(void) { struct timeval tv; unsigned int curtime = 0; - unsigned int nrcalls = 0, *calls = { 0 }; curtime = micros(); unsigned int diff = curtime - lasttime; unsigned int sec = diff / 1000000; unsigned int usec = diff - ((diff / 1000000) * 1000000); - int a = 0; + int a = 0, x = 0; lasttime = curtime; - for(a=1;a<=timerqueue_size;a++) { + for(a=0;asec -= sec; timerqueue[a]->usec -= usec; if(timerqueue[a]->usec < 0) { @@ -130,26 +194,40 @@ void timerqueue_update(void) { timerqueue[a]->sec -= 1; } - if(timerqueue[a]->sec < 0 || (timerqueue[a]->sec == 0 && timerqueue[a]->usec == 0)) { + if(timerqueue[a]->sec < 0 || (timerqueue[a]->sec == 0 && timerqueue[a]->usec <= 0)) { int nr = timerqueue[a]->nr; if((calls = (unsigned int *)realloc(calls, (nrcalls+1)*sizeof(int))) == NULL) { - //OUT_OF_MEMORY +#ifdef ESP8266 + Serial.printf("Out of memory %s:#%d\n", __FUNCTION__, __LINE__); + ESP.restart(); + exit(-1); +#else + fprintf(stderr, "Out of memory %s:#%d\n", __FUNCTION__, __LINE__); + exit(-1); +#endif } calls[nrcalls++] = nr; } } - for(a=1;a<=timerqueue_size;a++) { + for(a=0;asec < 0 || (timerqueue[a]->sec == 0 && timerqueue[a]->usec == 0)) { struct timerqueue_t *node = timerqueue_pop(); free(node); a--; } } - for(a=0;a 0) { + timer_cb(calls[0]); + if(nrcalls > 0) { + for(a=0;a 0) { + if(calls != NULL) { free(calls); + calls = NULL; } nrcalls = 0; } diff --git a/HeishaMon/src/common/timerqueue.h b/HeishaMon/src/common/timerqueue.h index aae093d6..3075dcaf 100644 --- a/HeishaMon/src/common/timerqueue.h +++ b/HeishaMon/src/common/timerqueue.h @@ -1,30 +1,31 @@ -/* - Copyright (C) CurlyMo - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -#ifndef _RULES_TIMERQUEUE_H_ -#define _RULES_TIMERQUEUE_H_ - -#include - -typedef struct timerqueue_t { - int sec; - int usec; - int nr; -} timerqueue_t; - -extern struct timerqueue_t **timerqueue; -extern int timerqueue_size; -extern void timer_cb(int nr); - -struct timerqueue_t *timerqueue_pop(); -struct timerqueue_t *timerqueue_peek(); -void timerqueue_insert(int sec, int usec, int nr); -void timerqueue_update(void); - - +/* + Copyright (C) CurlyMo + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +#ifndef _RULES_TIMERQUEUE_H_ +#define _RULES_TIMERQUEUE_H_ + +#include + +typedef struct timerqueue_t { + int sec; + int usec; + int nr; + int remove; +} timerqueue_t; + +extern struct timerqueue_t **timerqueue; +extern int timerqueue_size; +extern void timer_cb(int nr); + +struct timerqueue_t *timerqueue_pop(); +struct timerqueue_t *timerqueue_peek(); +void timerqueue_update(void); +void timerqueue_insert(int sec, int usec, int nr); + + #endif \ No newline at end of file diff --git a/HeishaMon/src/common/webserver.cpp b/HeishaMon/src/common/webserver.cpp new file mode 100755 index 00000000..9b0188e2 --- /dev/null +++ b/HeishaMon/src/common/webserver.cpp @@ -0,0 +1,1891 @@ +/* + Copyright (C) CurlyMo + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +#ifdef __linux__ + #pragma GCC diagnostic ignored "-Wwrite-strings" + + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + + #include "webserver.h" + #include "strncasestr.h" + #include "strnstr.h" + #include "unittest.h" +#else + #define LWIP_INTERNAL + + #include + #include + #include + + #define LWIP_SO_RCVBUF 1 + + #include "strncasestr.h" + #include "strnstr.h" + + #include "lwip/opt.h" + #include "lwip/tcp.h" + #include "lwip/inet.h" + #include "lwip/dns.h" + #include "lwip/init.h" + #include "lwip/errno.h" + + #include "webserver.h" + #include +#endif + +#define MIN(a,b) (((a)<(b))?(a):(b)) +#ifndef ERR_OK + #define ERR_OK 0 +#endif + +void log_message(char *string); + +struct webserver_client_t clients[WEBSERVER_MAX_CLIENTS]; +static tcp_pcb *async_server = NULL; +#ifdef ESP8266 +static WiFiServer sync_server(0); +#endif +static uint8_t *rbuffer = NULL; + +#if defined(ESP8266) +static uint16_t tcp_write_P(tcp_pcb *pcb, PGM_P buf, uint16_t len, uint8_t flags) { + char *str = (char *)malloc(len+1); + if(str == NULL) { + Serial1.printf("Out of memory %s:#%d\n", __FUNCTION__, __LINE__); + ESP.restart(); + exit(-1); + } + memset(str, 0, len+1); + strncpy_P(str, buf, len); + uint16_t ret = tcp_write(pcb, str, len, flags); + free(str); + return ret; +} +#endif + +int16_t urldecode(const unsigned char *src, int src_len, unsigned char *dst, int dst_len, int is_form_url_encoded) { + int i, j, a, b; + +#define HEXTOI(x) (isdigit(x) ? x - '0' : x - 'W') + + for(i = j = 0; i < src_len && j < dst_len - 1; i++, j++) { + if(src[i] == '%' && i < src_len - 2 && + isxdigit(*(const unsigned char *)(src + i + 1)) && + isxdigit(*(const unsigned char *)(src + i + 2))) { + a = tolower(*(const unsigned char *)(src + i + 1)); + b = tolower(*(const unsigned char *)(src + i + 2)); + dst[j] = (char)((HEXTOI(a) << 4) | HEXTOI(b)); + i += 2; + } else if(is_form_url_encoded && src[i] == '+') { + dst[j] = ' '; + } else { + dst[j] = src[i]; + } + } + + dst[j] = '\0'; // Null-terminate the destination + + return i >= src_len ? j : -1; +} + +static int webserver_parse_post(struct webserver_t *client, uint16_t size) { + struct arguments_t args; + unsigned char *ptrA = (unsigned char *)memchr(client->buffer, '=', size); + unsigned char *ptrB = (unsigned char *)memchr(client->buffer, ' ', size); + unsigned char *ptrC = (unsigned char *)memchr(client->buffer, '&', size); + unsigned char *ptrD = ptrA; + unsigned char *ptrE = ptrC; + char c = '='; + int16_t pos = 0; + int16_t posA = WEBSERVER_BUFFER_SIZE+1, posB = WEBSERVER_BUFFER_SIZE+1, posC = WEBSERVER_BUFFER_SIZE+1; + + if(ptrA != NULL) { + posA = ptrA - client->buffer; + } + if(ptrB != NULL) { + posB = ptrB - client->buffer; + } + if(ptrC != NULL) { + posC = ptrC - client->buffer; + } + + pos = MIN(posA, MIN(posB, posC)); + + if(ptrA != NULL || ptrB != NULL || ptrC != NULL) { + if(posB == pos) { + c = ' '; + ptrA = ptrB; + } + if(posC == pos) { + c = '&'; + ptrA = ptrC; + } + + /* + * & delimiter + */ + + unsigned char *ptr1 = (unsigned char *)memchr(&client->buffer[pos+1], '&', size-(pos+1)); + if(ptr1 != NULL) { + uint16_t pos1 = ptr1-client->buffer; + + int16_t pos2 = urldecode(client->buffer, + pos + 1, + client->buffer, + pos + 1, 1); + + if(pos2 > -1) { + client->buffer[pos2 - 1] = 0; + } else { + client->buffer[pos] = 0; + } + + int16_t pos3 = urldecode(&client->buffer[pos+1], + ((pos1-1)-pos) + 1, + &client->buffer[pos+1], + ((pos1-1)-pos) + 1, 1); + + if(pos3 > -1) { + client->buffer[pos + 1 + pos3] = 0; + } else { + client->buffer[pos1] = 0; + } + + args.name = &client->buffer[0]; + args.value = &client->buffer[pos+1]; + args.len = (pos1-1)-pos; + if(pos3 > -1) { + args.len = pos3 - 1; + } + + if(client->callback != NULL) { + if(client->callback(client, &args) == -1) { + return -1; /*LCOV_EXCL_LINE*/ + } + } + + if(pos2 > -1) { + client->buffer[pos2-1] = c; + client->buffer[pos] = ' '; + } else { + client->buffer[pos] = c; + } + if(pos3 > -1) { + client->buffer[pos + 1 + pos3] = '&'; + client->buffer[pos1] = ' '; + } else { + client->buffer[pos1] = '&'; + } + + memmove(&client->buffer[0], &client->buffer[pos1+1], size-(pos1+1)); + client->ptr = size-(pos1+1); + client->buffer[client->ptr] = 0; + + return 1; + } + + if(client->readlen + size == client->totallen) { + int16_t pos2 = urldecode(client->buffer, + pos + 1, + client->buffer, + pos + 1, 1); + + if(pos2 > -1) { + client->buffer[pos2 - 1] = 0; + } else { + client->buffer[pos] = 0; + } + + int16_t pos3 = urldecode(&client->buffer[pos+1], + (size - (pos + 1)) + 1, + &client->buffer[pos+1], + (size - (pos + 1)) + 1, 1); + + if(pos3 > -1) { + client->buffer[pos + 1 + pos3] = 0; + } else { + client->buffer[size] = 0; + } + + args.name = &client->buffer[0]; + args.value = &client->buffer[pos+1]; + args.len = size - (pos + 1); + + if(pos3 > -1) { + args.len = pos3 - 1; + } + + if(client->callback != NULL) { + if(client->callback(client, &args) == -1) { + return -1; /*LCOV_EXCL_LINE*/ + } + } + + memmove(&client->buffer[0], &client->buffer[size], size); + client->ptr = 0; + client->buffer[client->ptr] = 0; + + return 0; + } + + ptr1 = (unsigned char *)memrchr(client->buffer, '%', size); + if(ptr1 != NULL) { + uint16_t pos1 = ptr1 - client->buffer; + /* + * A encoded character always start with a + * percentage mark followed by two numbers. + * To properly decode an url we need to + * keep those together. + */ + if(pos1+2 >= WEBSERVER_BUFFER_SIZE) { + int16_t pos2 = urldecode(client->buffer, + pos + 1, + client->buffer, + pos + 1, 1); + + if(pos2 > -1) { + client->buffer[pos2 - 1] = 0; + } else { + client->buffer[pos] = 0; + } + + int16_t pos3 = urldecode(&client->buffer[pos+1], + (pos1 - (pos + 1)) + 1, + &client->buffer[pos+1], + (pos1 - (pos + 1)) + 1, 1); + + client->buffer[pos1] = 0; + + args.name = &client->buffer[0]; + args.value = &client->buffer[pos+1]; + args.len = (pos1 - (pos + 1)) + 1; + + if(pos3 > -1) { + args.len = pos3 - 1; + } + + if(client->callback != NULL) { + if(client->callback(client, &args) == -1) { + return -1; /*LCOV_EXCL_LINE*/ + } + } + + client->buffer[pos1] = '%'; + + if(pos2 > -1) { + client->buffer[pos2-1] = c; + client->buffer[pos] = ' '; + pos = pos2; + } else { + client->buffer[pos] = c; + } + + memmove(&client->buffer[pos+1], &client->buffer[pos1], (size-pos1)); + client->ptr = (size - (pos1 - pos)) + 1; + client->buffer[client->ptr] = 0; + + return 1; + } + } + + if(client->ptr >= WEBSERVER_BUFFER_SIZE) { + /* + * GET end delimiter before HTTP/1.1 + */ + + ptr1 = (unsigned char *)memchr(&client->buffer[pos+1], ' ', size - (pos + 1)); + unsigned char *ptr2 = (unsigned char *)memchr(&client->buffer[pos], '&', size - (pos)); + char d = ' '; + + if(ptr1 != NULL || ptr2 != NULL) { + if((ptr1 == NULL && ptr2 != NULL) || (ptr1 != NULL && ptr2 != NULL && ptr2 < ptr1)) { + ptr1 = ptr2; + d = '&'; + } + uint16_t pos1 = ptr1 - client->buffer; + int16_t pos2 = urldecode(client->buffer, + pos + 1, + client->buffer, + pos + 1, 1); + + if(pos2 > -1) { + client->buffer[pos2 - 1] = 0; + } else { + client->buffer[pos] = 0; + } + + int16_t pos3 = -1; + if(d == ' ') { + pos3 = urldecode(&client->buffer[pos+1], + (pos1 - (pos + 1)) + 1, + &client->buffer[pos+1], + (pos1 - (pos + 1)) + 1, 1); + + client->buffer[pos1] = 0; + } + + args.name = &client->buffer[0]; + if(d == ' ') { + args.value = &client->buffer[pos+1]; + args.len = size - (pos + 1); + if(pos3 > -1) { + args.len = pos3 - 1; + } + } else { + args.value = NULL; + args.len = 0; + } + + if(client->callback != NULL) { + if(client->callback(client, &args) == -1) { + return -1; /*LCOV_EXCL_LINE*/ + } + } + + if(pos3 > -1) { + client->buffer[pos3-1] = d; + client->buffer[pos] = d; + pos1 = pos3; + } else { + client->buffer[pos1] = d; + } + if(d == '&') { + pos1++; + } + + memmove(&client->buffer[0], &client->buffer[pos1], size-(pos1)); + client->ptr = size-(pos1); + client->buffer[client->ptr] = 0; + + } else { + int16_t pos2 = urldecode(client->buffer, + pos + 1, + client->buffer, + pos + 1, 1); + + if(pos2 > -1) { + client->buffer[pos2 - 1] = 0; + } else { + client->buffer[pos] = 0; + } + + int16_t pos3 = urldecode(&client->buffer[pos+1], + size - (pos + 1) + 1, + &client->buffer[pos+1], + size - (pos + 1) + 1, 1); + + args.name = &client->buffer[0]; + args.value = &client->buffer[pos+1]; + args.len = size - (pos + 1); + + if(pos3 > -1) { + args.len = pos3 - 1; + } + + if(client->callback != NULL) { + if(client->callback(client, &args) == -1) { + return -1; /*LCOV_EXCL_LINE*/ + } + } + + if(pos2 > -1) { + client->buffer[pos2-1] = c; + client->buffer[pos] = d; + pos = pos2; + } else { + client->buffer[pos] = c; + } + + memmove(&client->buffer[pos+1], &client->buffer[size], size-(pos+1)); + client->ptr = (pos+1); + + client->buffer[client->ptr] = 0; + + return 1; + } + } + + if(ptrD == NULL && ptrE == NULL && ptrA != NULL && (posB == WEBSERVER_BUFFER_SIZE+1 || posB > 0)) { + uint16_t pos1 = ptrA-client->buffer; + + int16_t pos2 = urldecode(client->buffer, + pos + 1, + client->buffer, + pos + 1, 1); + + if(pos2 > -1) { + client->buffer[pos2 - 1] = 0; + } else { + client->buffer[pos] = 0; + } + + args.name = &client->buffer[0]; + args.value = NULL; + args.len = 0; + + if(client->callback != NULL) { + if(client->callback(client, &args) == -1) { + return -1; /*LCOV_EXCL_LINE*/ + } + } + + if(pos2 > -1) { + client->buffer[pos2-1] = c; + client->buffer[pos] = ' '; + } else { + client->buffer[pos] = c; + } + + memmove(&client->buffer[0], &client->buffer[pos1], size-(pos1)); + client->ptr = size-(pos1); + client->buffer[client->ptr] = 0; + + return 0; + } + + } + + return 0; +} + +int8_t http_parse_request(struct webserver_t *client, uint8_t **buf, uint16_t *len) { + uint16_t hasread = MIN(WEBSERVER_BUFFER_SIZE-client->ptr, *len); + + while(*len > 0) { + hasread = MIN(WEBSERVER_BUFFER_SIZE-client->ptr, (*len)); + memcpy(&client->buffer[client->ptr], &(*buf)[0], hasread); + + client->ptr += hasread; + memmove(&(*buf)[0], &(*buf)[hasread], (*len)-hasread); + + *len -= hasread; + + /* + * Request method + */ + if(client->substep == 0) { + if(memcmp_P(client->buffer, PSTR("GET "), 4) == 0) { + client->method = 0; + if(client->callback != NULL) { + client->step = WEBSERVER_CLIENT_REQUEST_METHOD; + if(client->callback != NULL) { + if(client->callback(client, (void *)"GET") == -1) { + client->step = WEBSERVER_CLIENT_CLOSE; + return -1; + } + } + client->step = WEBSERVER_CLIENT_READ_HEADER; + } + memmove(&client->buffer[0], &client->buffer[4], client->ptr-4); + client->ptr -= 4; + client->substep = 1; + } + if(memcmp_P(client->buffer, PSTR("POST "), 5) == 0) { + client->method = 1; + client->reqtype = 0; + client->step = WEBSERVER_CLIENT_REQUEST_METHOD; + if(client->callback != NULL) { + if(client->callback(client, (void *)"POST") == -1) { + client->step = WEBSERVER_CLIENT_CLOSE; + return -1; + } + } + client->step = WEBSERVER_CLIENT_READ_HEADER; + memmove(&client->buffer[0], &client->buffer[5], client->ptr-5); + client->ptr -= 5; + client->substep = 1; + } + } + /* + * Request URI + */ + if(client->substep == 1) { + unsigned char *ptr1 = (unsigned char *)memchr(client->buffer, '?', client->ptr); + unsigned char *ptr2 = (unsigned char *)memchr(client->buffer, ' ', client->ptr); + if(ptr2 == NULL || (ptr1 != NULL && ptr2 > ptr1)) { + if(ptr1 == NULL) { + if(client->ptr == WEBSERVER_BUFFER_SIZE) { + // Request URI two long + return -1; + } else { + return 1; + } + } else { + uint16_t pos = ptr1-client->buffer; + client->buffer[pos] = 0; + client->substep = 2; + client->step = WEBSERVER_CLIENT_REQUEST_URI; + if(client->callback != NULL) { + if(client->callback(client, client->buffer) == -1) { + client->step = WEBSERVER_CLIENT_CLOSE; + return -1; + } + } + memmove(&client->buffer[0], &client->buffer[pos+1], client->ptr-(pos+1)); + client->ptr -= (pos+1); + } + } else { + uint16_t pos = ptr2-client->buffer; + client->buffer[pos] = 0; + client->substep = 2; + client->step = WEBSERVER_CLIENT_REQUEST_URI; + if(client->callback != NULL) { + if(client->callback(client, client->buffer) == -1) { + client->step = WEBSERVER_CLIENT_CLOSE; + return -1; + } + } + client->buffer[pos] = ' '; + memmove(&client->buffer[0], &client->buffer[pos], client->ptr-(pos)); + client->ptr -= pos; + } + } + if(client->substep == 2) { + client->step = WEBSERVER_CLIENT_ARGS; + int ret = webserver_parse_post(client, client->ptr); + client->step = WEBSERVER_CLIENT_READ_HEADER; + + if(ret == -1) { + return -1; /*LCOV_EXCL_LINE*/ + } + + if(ret == 1) { + continue; + } + + if(client->ptr >= 4) { + if(memcmp_P(client->buffer, " HTTP/1.1", 9) == 0) { + client->substep = 3; + } else { + continue; + } + } + } + if(client->substep == 3) { + uint16_t i = 0; + while(i < client->ptr-2) { + if(memcmp_P(&client->buffer[i], PSTR("\r\n"), 2) == 0) { + memmove(&client->buffer[0], &client->buffer[i+2], client->ptr-(i+2)); + client->ptr -= (i + 2); + client->substep = 4; + break; + } + i++; + } + } + if(client->substep == 4) { + unsigned char *ptr = (unsigned char *)memchr(client->buffer, ':', client->ptr); + + while(ptr != NULL) { + struct arguments_t args; + uint16_t i = ptr-client->buffer, x = 0; + client->buffer[i] = 0; + args.name = &client->buffer[0]; + args.value = NULL; + x = i; + i++; + /* + * Make sure we can at least compare + * the double \r\n\r\n at the end + * of the header + */ + while(i <= client->ptr-4) { + if(memcmp_P(&client->buffer[i], PSTR("\r\n"), 2) == 0 || + (client->ptr == WEBSERVER_BUFFER_SIZE && i == WEBSERVER_BUFFER_SIZE-4)) { + while(client->buffer[x+1] == ' ') { + x++; + } + args.value = &client->buffer[x+1]; + args.len = (i-x)-1; + if((client->ptr == WEBSERVER_BUFFER_SIZE && i == WEBSERVER_BUFFER_SIZE-4)) { + args.len += 2; + } + + if(memcmp_P(args.name, PSTR("Content-Length"), 14) == 0) { + char tmp[args.len+1]; + memset(&tmp, 0, args.len+1); + memcpy(tmp, &client->buffer[x+1], args.len); + client->totallen = atoi(tmp); + } + if(memcmp_P(args.name, PSTR("Content-Type"), 12) == 0) { + if(strncasestr(&client->buffer[x+1], "multipart/form-data", client->ptr-(x+1)) != NULL) { + client->reqtype = 1; + char tmp[args.len+1]; + memset(&tmp, 0, args.len+1); + memcpy(tmp, &client->buffer[x+1], args.len); + { + char *ptr = strstr(tmp, "boundary="); + uint8_t pos = (ptr-tmp)+strlen("boundary="); + memmove(&tmp[0], &tmp[pos], args.len-pos); + tmp[args.len-pos] = 0; + if((client->boundary = strdup(tmp)) == NULL) { +#ifdef ESP8266 + Serial1.printf("Out of memory %s:#%d\n", __FUNCTION__, __LINE__); + ESP.restart(); + exit(-1); +#endif + } + } + } + } + client->step = WEBSERVER_CLIENT_HEADER; + if(client->callback != NULL) { + if(client->callback(client, &args) == -1) { + client->step = WEBSERVER_CLIENT_CLOSE; + return -1; + } + } + client->step = WEBSERVER_CLIENT_READ_HEADER; + + client->buffer[i] = 0; + if((client->ptr == WEBSERVER_BUFFER_SIZE && i == WEBSERVER_BUFFER_SIZE-4)) { + memmove(&client->buffer[x], &client->buffer[i+2], client->ptr-(i+2)); + client->buffer[x-1] = ':'; + client->ptr -= (i + 2 - x); + } else { + memmove(&client->buffer[0], &client->buffer[i+2], client->ptr-(i+2)); + client->ptr -= (i + 2); + } + break; + } + i++; + } + if(args.value == NULL) { + client->buffer[x] = ':'; + break; + } + ptr = (unsigned char *)memchr(client->buffer, ':', client->ptr); + } + + if(client->ptr >= 2 && memcmp_P(client->buffer, PSTR("\r\n"), 2) == 0) { + memmove(&client->buffer[0], &client->buffer[2], client->ptr-2); + client->ptr -= 2; + client->readlen = 0; + if(client->ptr == 0 && *len > 0) { + client->substep = 5; + continue; + } + return 0; + } + } + if(client->substep == 5) { + return 0; + } + } + + return 1; +} + +int http_parse_body(struct webserver_t *client, char *buf, uint16_t len) { + uint16_t hasread = MIN(WEBSERVER_BUFFER_SIZE-client->ptr, len); + uint16_t pos = 0; + + while(1) { + if(pos < len) { + hasread = MIN(WEBSERVER_BUFFER_SIZE-client->ptr, len-pos);; + memcpy(&client->buffer[client->ptr], &buf[pos], hasread); + client->ptr += hasread; + pos += hasread; + } + + uint16_t toread = client->ptr; + int ret = webserver_parse_post(client, client->ptr); + if(ret == 1 || ret == 0) { + client->readlen += (toread - client->ptr); + } + + if(ret == -1) { + return -1; /*LCOV_EXCL_LINE*/ + } + if(ret == 0) { + break; + } + } + + return 0; +} + +char *strnstr(const char *haystack, const char *needle, size_t len) { + int i; + size_t needle_len; + + if(0 == (needle_len = strnlen(needle, len))) { + return (char *)haystack; + } + + for(i=0; i<=(int)(len-needle_len); i++) { + if((haystack[0] == needle[0]) && (0 == strncmp(haystack, needle, needle_len))) { + return (char *)haystack; + } + + haystack++; + } + return NULL; +} + +int http_parse_multipart_body(struct webserver_t *client, unsigned char *buf, uint16_t len) { + uint16_t hasread = MIN(WEBSERVER_BUFFER_SIZE-client->ptr, len); + uint16_t rpos = 0, loop = 1; + + while(rpos < len) { + hasread = MIN(WEBSERVER_BUFFER_SIZE-client->ptr, len-rpos); + memcpy(&client->buffer[client->ptr], &buf[rpos], hasread); + client->ptr += hasread; + rpos += hasread; + loop = 1; + + while(loop) { + switch(client->substep) { + // Boundary + case 0: { + unsigned char *ptr = strnstr(client->buffer, client->boundary, client->ptr); + unsigned char *ptr1 = (unsigned char *)memchr(client->buffer, '=', client->ptr); + uint16_t pos1 = 0; + if(ptr1 != NULL) { + pos1 = (ptr1-client->buffer)+1; + } + if(ptr != NULL) { + uint16_t pos = (ptr-client->buffer)+strlen(client->boundary); + if(pos1 > pos) { + /* + * Only compensate for the key at the + * beginning of the buffer when at least + * one key has been encountered. This is + * the case when the boundary is placed + * after the key. + */ + pos1 = 0; + } + + if(pos+1 <= client->ptr) { + if(client->buffer[pos] == '\r' && client->buffer[pos+1] == '\n') { + memmove(&client->buffer[0], &client->buffer[pos+1], client->ptr-(pos+1)); + client->ptr = client->ptr-(pos+1); + client->buffer[client->ptr] = 0; + client->readlen += ((pos+1)-pos1); + client->substep = 1; + } + } + if(pos+3 <= client->ptr) { + if(client->buffer[pos] == '-' && client->buffer[pos+1] == '-' && + client->buffer[pos+2] == '\r' && client->buffer[pos+3] == '\n') { + client->readlen += ((pos+4)-(pos1)); + if(client->readlen == client->totallen) { + return 0; + } else { + // Error, content length does not match end boundary + } + } else { + loop = 0; + } + } else { + loop = 0; + } + } else if(client->ptr < WEBSERVER_BUFFER_SIZE) { + loop = 0; + } else { + /* + * We encountered the boundary delimiter, but + * it wasn't the one from this request, but it + * was part of the POST body + */ + client->substep = 8; + } + if(client->substep == 0) { + loop = 0; + } + } break; + // Content-Disposition + case 1: { + unsigned char *ptr = strncasestr(client->buffer, "content-disposition:", client->ptr); + if(ptr != NULL) { + uint16_t pos = (ptr-client->buffer)+strlen("content-disposition:"); + while(client->buffer[pos++] == ' '); + pos--; + memmove(&client->buffer[0], &client->buffer[pos], client->ptr-(pos)); + client->ptr = client->ptr-(pos); + client->buffer[client->ptr] = 0; + client->readlen += pos; + client->substep = 2; + } else { + loop = 0; + } + } break; + // End of content-disposition + case 2: { + unsigned char *ptr = (unsigned char *)memchr(client->buffer, ';', client->ptr); + if(ptr != NULL) { + uint16_t pos = (ptr-client->buffer+1); + while(client->buffer[pos++] == ' '); + pos--; + memmove(&client->buffer[0], &client->buffer[pos], client->ptr-(pos)); + client->ptr -= pos; + client->buffer[client->ptr] = 0; + client->readlen += pos; + client->substep = 3; + } else { + loop = 0; + } + } break; + // Name + case 3: { + unsigned char *ptr = strncasestr(client->buffer, "name=\"", client->ptr); + if(ptr != NULL) { + uint16_t pos = (ptr-client->buffer)+strlen("name=\""); + memmove(&client->buffer[0], &client->buffer[pos], client->ptr-(pos)); + client->ptr = client->ptr-(pos); + client->readlen += pos; + client->substep = 6; + } else { + loop = 0; + } + } break; + // Filename etc. + case 4: { + unsigned char *ptr = strncasestr(client->buffer, "\";", client->ptr); + if(ptr != NULL) { + uint16_t pos = (ptr-client->buffer); + unsigned char *ptr1 = strncasestr(&client->buffer[pos], "\r\n", client->ptr-pos); + if(ptr1 != NULL) { + client->buffer[pos++] = '='; + uint16_t pos1 = (ptr1-client->buffer); + uint16_t newlen = client->ptr-((pos1+2)-pos); + memmove(&client->buffer[pos], &client->buffer[pos1+2], newlen); + client->ptr = newlen; + client->readlen += (pos1+2); + client->substep = 5; + } else { + client->substep = 6; + } + } else { + loop = 0; + } + } break; + // Content-type + case 5: { + unsigned char *ptr = (unsigned char *)memchr(client->buffer, '=', client->ptr); + if(ptr != NULL) { + uint16_t pos = (ptr-client->buffer)+1; + + if((client->ptr - pos) >= 4) { + unsigned char *ptr1 = strnstr(&client->buffer[pos], "\r\n\r\n", client->ptr-pos); + if(ptr1 != NULL) { + uint16_t pos1 = (ptr1-client->buffer)+4; + uint16_t newlen = client->ptr-(pos1-pos); + memmove(&client->buffer[pos], &client->buffer[pos1], newlen); + client->ptr = newlen; + client->readlen += (pos1-pos); + client->substep = 7; + } else { + loop = 0; + } + } else { + loop = 0; + } + } else { + loop = 0; + } + } break; + // Name + case 6: { + if(client->ptr >= 2) { + unsigned char *ptr = strnstr(client->buffer, "\";", client->ptr); + if(ptr != NULL) { + unsigned char *ptr1 = strnstr(client->buffer, "\r\n", client->ptr); + if(ptr1 != NULL) { + client->substep = 4; + } else { + loop = 0; + } + } else { + unsigned char *ptr1 = strnstr(client->buffer, "\"\r\n", client->ptr); + if(ptr1 != NULL) { + uint16_t pos = (ptr1-client->buffer); + /* + * Since we're increasing the pos + * right after, check for 5 positions + */ + if((client->ptr - pos) >= 5) { + client->buffer[pos++] = '=';; + unsigned char *ptr2 = strnstr(&client->buffer[pos], "\r\n\r\n", client->ptr-pos); + if(ptr2 != NULL) { + uint16_t pos1 = (ptr2-client->buffer)+4; + + memmove(&client->buffer[pos], &client->buffer[pos1], client->ptr-pos1); + client->ptr -= (pos1-pos); + client->readlen += pos1; + client->substep = 7; + } else { + loop = 0; + } + } else { + loop = 0; + } + } else { + loop = 0; + } + } + } else { + loop = 0; + } + } break; + // Value + case 8: + case 7: { + unsigned char *ptr = strnstr(client->buffer, "\r\n--", client->ptr); + if(ptr != NULL && client->substep != 8) { + uint16_t pos = (ptr-client->buffer); + + ptr = (unsigned char *)memchr(client->buffer, '=', client->ptr); + uint16_t vlen = 0; + + if(ptr != NULL) { + vlen = (ptr-client->buffer); + } else { + // error + return -1; /*LCOV_EXCL_LINE*/ + } + + struct arguments_t args; + client->buffer[vlen] = 0; + + args.name = &client->buffer[0]; + args.value = &client->buffer[vlen+1]; + args.len = pos-(vlen+1); + + if(client->callback != NULL) { + uint8_t ret = client->callback(client, &args); + if(ret == -1) { + return -1; + } + } + + client->buffer[vlen] = '='; + + memmove(&client->buffer[vlen+1], &client->buffer[pos], client->ptr-(pos-(vlen+1))); + client->readlen += (pos-(vlen+1)); + client->ptr -= (pos-(vlen+1)); + client->substep = 0; + } else if(client->ptr == WEBSERVER_BUFFER_SIZE) { + if(client->substep == 8) { + client->substep = 7; + } + unsigned char *ptr = (unsigned char *)memchr(client->buffer, '=', client->ptr); + if(ptr != NULL) { + uint16_t pos = (ptr-client->buffer); + + struct arguments_t args; + client->buffer[pos] = 0; + + args.name = &client->buffer[0]; + args.value = &client->buffer[pos+1]; + args.len = WEBSERVER_BUFFER_SIZE-(pos+1); + + if(client->callback != NULL) { + uint8_t ret = client->callback(client, &args); + if(ret == -1) { + return -1; + } + } + client->buffer[pos] = '='; + client->readlen += (client->ptr-(pos+1)); + client->ptr = pos+1; + } else { + // error + return -1; + } + } else { + loop = 0; + } + } break; + } + } + } + + return 0; +} + +#ifdef __linux__ +static char *code_to_text(uint16_t code) { +#else +static PGM_P code_to_text(uint16_t code) { +#endif + /* LCOV_EXCL_START*/ + switch(code) { + case 100: + return PSTR("Continue"); + case 101: + return PSTR("Switching Protocols"); + /* LCOV_EXCL_STOP*/ + case 200: + return PSTR("OK"); + /* LCOV_EXCL_START*/ + case 201: + return PSTR("Created"); + case 202: + return PSTR("Accepted"); + case 203: + return PSTR("Non-Authoritative Information"); + case 204: + return PSTR("No Content"); + case 205: + return PSTR("Reset Content"); + case 206: + return PSTR("Partial Content"); + case 300: + return PSTR("Multiple Choices"); + /* LCOV_EXCL_STOP*/ + case 301: + return PSTR("Moved Permanently"); + /* LCOV_EXCL_START*/ + case 302: + return PSTR("Found"); + case 303: + return PSTR("See Other"); + case 304: + return PSTR("Not Modified"); + case 305: + return PSTR("Use Proxy"); + case 307: + return PSTR("Temporary Redirect"); + case 400: + return PSTR("Bad Request"); + case 401: + return PSTR("Unauthorized"); + case 402: + return PSTR("Payment Required"); + case 403: + return PSTR("Forbidden"); + case 404: + return PSTR("Not Found"); + case 405: + return PSTR("Method Not Allowed"); + case 406: + return PSTR("Not Acceptable"); + case 407: + return PSTR("Proxy Authentication Required"); + case 408: + return PSTR("Request Timeout"); + case 409: + return PSTR("Conflict"); + case 410: + return PSTR("Gone"); + case 411: + return PSTR("Length Required"); + case 412: + return PSTR("Precondition Failed"); + case 413: + return PSTR("Request Entity Too Large"); + case 414: + return PSTR("URI Too Long"); + case 415: + return PSTR("Unsupported Media Type"); + case 416: + return PSTR("Range not satisfiable"); + case 417: + return PSTR("Expectation Failed"); + case 500: + return PSTR("Internal Server Error"); + case 501: + return PSTR("Not Implemented"); + case 502: + return PSTR("Bad Gateway"); + case 503: + return PSTR("Service Unavailable"); + case 504: + return PSTR("Gateway Timeout"); + case 505: + return PSTR("HTTP Version not supported"); + default: + return PSTR(""); + } + /* LCOV_EXCL_STOP*/ +} + +static uint16_t webserver_create_header(struct webserver_t *client, uint16_t code, char *mimetype, uint16_t len) { + uint16_t i = 0; + unsigned char buffer[512], *p = buffer; + memset(buffer, '\0', sizeof(buffer)); + + i += snprintf_P((char *)&p[i], sizeof(buffer), PSTR("HTTP/1.1 %d %s\r\n"), code, code_to_text(code)); + if(client->callback != NULL) { + client->step = WEBSERVER_CLIENT_CREATE_HEADER; + struct header_t header; + header.buffer = &p[i]; + header.ptr = i; + + if(client->callback(client, &header) == -1) { + if(strstr_P((char *)&p[i], PSTR("\r\n\r\n")) == NULL) { + if(strstr((char *)&p[i], PSTR("\r\n")) != NULL) { + header.ptr += snprintf_P((char *)&p[header.ptr], sizeof(buffer)-header.ptr, PSTR("\r\n")); + } else { + header.ptr += snprintf_P((char *)&p[header.ptr], sizeof(buffer)-header.ptr, PSTR("\r\n\r\n")); + } + } + client->step = WEBSERVER_CLIENT_WRITE; + i = header.ptr; + return i; + } + + if(header.ptr > i && strstr_P((char *)&p[i], PSTR("\r\n")) == NULL) { + header.ptr += snprintf((char *)&p[header.ptr], sizeof(buffer)-header.ptr, PSTR("\r\n")); + } + i = header.ptr; + client->step = WEBSERVER_CLIENT_WRITE; + } + i += snprintf_P((char *)&p[i], sizeof(buffer) - i, PSTR("Server: ESP8266\r\n")); + i += snprintf_P((char *)&p[i], sizeof(buffer) - i, PSTR("Keep-Alive: timeout=15, max=100\r\n")); + i += snprintf_P((char *)&p[i], sizeof(buffer) - i, PSTR("Content-Type: %s\r\n"), mimetype); + i += snprintf_P((char *)&p[i], sizeof(buffer) - i, PSTR("Content-Length: %d\r\n\r\n"), len); + + + if(client->async == 1) { + tcp_write(client->pcb, &buffer, i, 0); + tcp_output(client->pcb); + } else { + if(client->client.write(buffer, i) > 0) { + client->lastseen = millis(); + } + } + + return i; +} + +static int webserver_process_send(struct webserver_t *client) { + struct sendlist_t *tmp = client->sendlist; + uint16_t cpylen = client->totallen, i = 0, cpyptr = client->ptr; + unsigned char cpy[client->totallen+1]; + + if(client->chunked == 1) { + while(tmp != NULL && cpylen > 0) { + if(cpyptr == 0) { + if(cpylen >= tmp->size) { + cpyptr += tmp->size; + cpylen -= tmp->size; + tmp = tmp->next; + cpyptr = 0; + } else { + cpyptr += cpylen; + cpylen = 0; + } + } else if(cpyptr+cpylen >= tmp->size) { + cpylen -= (tmp->size-cpyptr); + tmp = tmp->next; + cpyptr = 0; + } else { + cpyptr += cpylen; + cpylen = 0; + } + } + + unsigned char chunk_size[12]; + size_t n = snprintf_P((char *)chunk_size, sizeof(chunk_size), PSTR("%X\r\n"), client->totallen - cpylen); + + if(client->async == 1) { + tcp_write(client->pcb, chunk_size, n, 0); + } else { + if(client->client.write(chunk_size, n) > 0) { + client->lastseen = millis(); + } + } + i += n; + } + + if(client->sendlist != NULL) { + while(client->sendlist != NULL && client->totallen > 0) { + if(client->ptr == 0) { + if(client->totallen >= client->sendlist->size) { + if(client->sendlist->type == 1) { + memcpy_P(cpy, &((PGM_P)client->sendlist->ptr)[client->ptr], client->sendlist->size); + if(client->async == 1) { + tcp_write(client->pcb, cpy, client->sendlist->size, TCP_WRITE_FLAG_MORE); + } else { + if(client->client.write(cpy, client->sendlist->size) > 0) { + client->lastseen = millis(); + } + } + } else { + if(client->async == 1) { + tcp_write(client->pcb, &((unsigned char *)client->sendlist->ptr)[client->ptr], client->sendlist->size, TCP_WRITE_FLAG_MORE); + } else { + if(client->client.write(&((unsigned char *)client->sendlist->ptr)[client->ptr], client->sendlist->size) > 0) { + client->lastseen = millis(); + } + } + } + i += client->sendlist->size; + client->ptr += client->sendlist->size; + client->totallen -= client->sendlist->size; + + tmp = client->sendlist; + client->sendlist = client->sendlist->next; + if(tmp->type == 0) { + free(tmp->ptr); + } + free(tmp); + client->ptr = 0; + } else { + if(client->sendlist->type == 1) { + memcpy_P(cpy, &((PGM_P)client->sendlist->ptr)[client->ptr], client->totallen); + if(client->async == 1) { + tcp_write(client->pcb, cpy, client->totallen, TCP_WRITE_FLAG_MORE); + } else { + if(client->client.write(cpy, client->totallen) > 0) { + client->lastseen = millis(); + } + } + } else { + if(client->async == 1) { + tcp_write(client->pcb, &((unsigned char *)client->sendlist->ptr)[client->ptr], client->totallen, TCP_WRITE_FLAG_MORE); + } else { + if(client->client.write(&((unsigned char *)client->sendlist->ptr)[client->ptr], client->totallen) > 0) { + client->lastseen = millis(); + } + } + } + i += client->totallen; + client->ptr += client->totallen; + client->totallen = 0; + } + } else if(client->ptr+client->totallen >= client->sendlist->size) { + if(client->sendlist->type == 1) { + memcpy_P(cpy, &((PGM_P)client->sendlist->ptr)[client->ptr], (client->sendlist->size-client->ptr)); + if(client->async == 1) { + tcp_write(client->pcb, cpy, (client->sendlist->size-client->ptr), TCP_WRITE_FLAG_MORE); + } else { + if(client->client.write(cpy, (client->sendlist->size-client->ptr)) > 0) { + client->lastseen = millis(); + } + } + } else { + if(client->async == 1) { + tcp_write(client->pcb, &((unsigned char *)client->sendlist->ptr)[client->ptr], (client->sendlist->size-client->ptr), TCP_WRITE_FLAG_MORE); + } else { + if(client->client.write(&((unsigned char *)client->sendlist->ptr)[client->ptr], (client->sendlist->size-client->ptr)) > 0) { + client->lastseen = millis(); + } + } + } + i += (client->sendlist->size-client->ptr); + client->totallen -= (client->sendlist->size-client->ptr); + tmp = client->sendlist; + client->sendlist = client->sendlist->next; + if(tmp->type == 0) { + free(tmp->ptr); + } + client->ptr = 0; + } else { + if(client->sendlist->type == 1) { + memcpy_P(cpy, &((PGM_P)client->sendlist->ptr)[client->ptr], client->totallen); + if(client->async == 1) { + tcp_write(client->pcb, cpy, client->totallen, TCP_WRITE_FLAG_MORE); + } else { + if(client->client.write(cpy, client->totallen) > 0) { + client->lastseen = millis(); + } + } + } else { + if(client->async == 1) { + tcp_write(client->pcb, &((unsigned char *)client->sendlist->ptr)[client->ptr], client->totallen, TCP_WRITE_FLAG_MORE); + } else { + if(client->client.write(&((unsigned char *)client->sendlist->ptr)[client->ptr], client->totallen) > 0) { + client->lastseen = millis(); + } + } + } + client->ptr += client->totallen; + client->totallen = 0; + } + } + if(client->chunked == 1) { + if(client->async == 1) { + tcp_write_P(client->pcb, PSTR("\r\n"), 2, TCP_WRITE_FLAG_MORE); + } else { + if(client->client.write_P((char *)PSTR("\r\n"), 2) > 0) { + client->lastseen = millis(); + } + } + } + } + + if(client->sendlist == NULL) { + client->content++; + client->step = WEBSERVER_CLIENT_WRITE; + if(client->callback(client, NULL) == -1) { + client->step = WEBSERVER_CLIENT_CLOSE; + } else { + client->step = WEBSERVER_CLIENT_SENDING; + } + if(client->sendlist == NULL) { + if(client->chunked == 1) { + if(client->async == 1) { + tcp_write_P(client->pcb, PSTR("0\r\n\r\n"), 5, 0); + } else { + if(client->client.write_P((char *)PSTR("0\r\n\r\n"), 5) > 0) { + client->lastseen = millis(); + } + } + i += 5; + } else { + if(client->async == 1) { + tcp_write_P(client->pcb, PSTR("\r\n\r\n"), 4, 0); + } else { + if(client->client.write_P((char *)PSTR("\r\n\r\n"), 4) > 0) { + client->lastseen = millis(); + } + } + i += 4; + } + client->step = WEBSERVER_CLIENT_CLOSE; + client->ptr = 0; + client->content = 0; + } + } + if(client->async == 1) { + tcp_output(client->pcb); + } + + return i; +} + +void webserver_send_content_P(struct webserver_t *client, PGM_P buf, uint16_t size) { + struct sendlist_t *node = (struct sendlist_t *)malloc(sizeof(struct sendlist_t)); + /*LCOV_EXCL_START*/ + if(node == NULL) { +#ifdef ESP8266 + Serial1.printf("Out of memory %s:#%d\n", __FUNCTION__, __LINE__); + ESP.restart(); + exit(-1); +#endif + } + /*LCOV_EXCL_STOP*/ + memset(node, 0, sizeof(struct sendlist_t)); + node->ptr = (void *)buf; + node->size = size; + node->type = 1; + if(client->sendlist == NULL) { + client->sendlist = node; + client->sendlist_head = node; + } else { + client->sendlist_head->next = node; + client->sendlist_head = node; + } +} + +void webserver_send_content(struct webserver_t *client, char *buf, uint16_t size) { + struct sendlist_t *node = (struct sendlist_t *)malloc(sizeof(struct sendlist_t)); + /*LCOV_EXCL_START*/ + if(node == NULL) { +#ifdef ESP8266 + Serial1.printf("Out of memory %s:#%d\n", __FUNCTION__, __LINE__); + ESP.restart(); + exit(-1); +#endif + } + /*LCOV_EXCL_STOP*/ + memset(node, 0, sizeof(struct sendlist_t)); + node->ptr = strdup(buf); + node->size = size; + node->type = 0; + if(client->sendlist == NULL) { + client->sendlist = node; + client->sendlist_head = node; + } else { + client->sendlist_head->next = node; + client->sendlist_head = node; + } +} + +int8_t webserver_send(struct webserver_t *client, uint16_t code, char *mimetype, uint16_t data_len) { + uint16_t i = 0; + if(data_len == 0) { + unsigned char buffer[512], *p = buffer; + memset(buffer, '\0', sizeof(buffer)); + + client->chunked = 1; + i = snprintf_P((char *)p, sizeof(buffer), PSTR("HTTP/1.1 %d %s\r\n"), code, code_to_text(code)); + if(client->callback != NULL) { + client->step = WEBSERVER_CLIENT_CREATE_HEADER; + struct header_t header; + header.buffer = &p[i]; + header.ptr = i; + + if(client->callback(client, &header) == -1) { + if(strstr_P((char *)&p[i], PSTR("\r\n\r\n")) == NULL) { + if(strstr_P((char *)&p[i], PSTR("\r\n")) != NULL) { + header.ptr += snprintf((char *)&p[header.ptr], sizeof(buffer)-header.ptr, PSTR("\r\n")); + } else { + header.ptr += snprintf((char *)&p[header.ptr], sizeof(buffer)-header.ptr, PSTR("\r\n\r\n")); + } + } + client->step = WEBSERVER_CLIENT_WRITE; + i = header.ptr; + goto done; + } + if(header.ptr > i && strstr_P((char *)&p[i], PSTR("\r\n")) == NULL) { + header.ptr += snprintf((char *)&p[header.ptr], sizeof(buffer)-header.ptr, PSTR("\r\n")); + } + i = header.ptr; + client->step = WEBSERVER_CLIENT_WRITE; + } + + i += snprintf((char *)&p[i], sizeof(buffer)-i, PSTR("Keep-Alive: timeout=15, max=100\r\n")); + i += snprintf((char *)&p[i], sizeof(buffer)-i, PSTR("Content-Type: %s\r\n"), mimetype); + i += snprintf((char *)&p[i], sizeof(buffer)-i, PSTR("Transfer-Encoding: chunked\r\n\r\n")); + +done: + if(client->async == 1) { + tcp_write(client->pcb, &buffer, i, 0); + tcp_output(client->pcb); + } else{ + if(client->client.write((unsigned char *)&buffer, i) > 0) { + client->lastseen = millis(); + } + } + } else { + client->chunked = 0; + i = webserver_create_header(client, code, mimetype, data_len); + } + + if(i > 0) { + return 0; + } else { + return -1; + } +} + +/* LCOV_EXCL_START*/ +static void webserver_client_close(struct webserver_t *client) { +#ifdef ESP8266 + char log_msg[256]; + sprintf_P(log_msg, PSTR("Closing webserver client: %s:%d"), IPAddress(client->pcb->remote_ip.addr).toString().c_str(), client->pcb->remote_port); + log_message(log_msg); + + client->step = 0; + + tcp_recv(client->pcb, NULL); + tcp_sent(client->pcb, NULL); + tcp_poll(client->pcb, NULL, 0); + + tcp_close(client->pcb); + client->pcb = NULL; + + webserver_reset_client(client); +#endif +} +/* LCOV_EXCL_STOP*/ + +#ifdef ESP8266 +err_t webserver_sent(void *arg, tcp_pcb *pcb, uint16_t len) { + uint16_t i = 0; + for(i=0;i 0) { + /* + * Leave room for chunk overhead + */ + clients[i].data.totallen -= 16; + webserver_process_send(&clients[i].data); + } + } + if(clients[i].data.step == WEBSERVER_CLIENT_CLOSE) { + webserver_client_close(&clients[i].data); + } + break; + } + } + return ERR_OK; +} +#endif + +uint8_t webserver_sync_receive(struct webserver_t *client, uint8_t *rbuffer, uint16_t size) { + if(client->step == WEBSERVER_CLIENT_READ_HEADER) { + if(http_parse_request(client, &rbuffer, &size) == 0) { + if(client->method == 1) { + client->step = WEBSERVER_CLIENT_ARGS; + if(client->reqtype == 0) { + client->readlen = 0; + if(http_parse_body(client, (char *)rbuffer, size) == -1) { + client->step = WEBSERVER_CLIENT_CLOSE; + } + } else if(client->reqtype == 1) { + client->substep = 0; + if(http_parse_multipart_body(client, (unsigned char *)rbuffer, size) == -1) { + client->step = WEBSERVER_CLIENT_CLOSE; + } + } + + if(client->readlen == client->totallen) { + client->step = WEBSERVER_CLIENT_WRITE; + } + + if(client->step == WEBSERVER_CLIENT_ARGS) { + return 1; + } + } else { + client->step = WEBSERVER_CLIENT_WRITE; + } + } + } + + if(client->step == WEBSERVER_CLIENT_ARGS) { + if(client->reqtype == 0) { + if(http_parse_body(client, (char *)rbuffer, size) == -1) { + client->step = WEBSERVER_CLIENT_CLOSE; + } + } else if(client->reqtype == 1) { + if(http_parse_multipart_body(client, (unsigned char *)rbuffer, size) == -1) { + client->step = WEBSERVER_CLIENT_CLOSE; + } + } + + if(client->readlen == client->totallen) { + client->step = WEBSERVER_CLIENT_WRITE; + } + } + return 0; +} + +err_t webserver_async_receive(void *arg, tcp_pcb *pcb, struct pbuf *data, err_t err) { + uint16_t size = 0; + uint8_t i = 0; + + if(data == NULL) { + for(i=0;ipayload; + size = b->len; + + for(i=0;istep == WEBSERVER_CLIENT_WRITE) { + if((client->totallen = tcp_sndbuf(client->pcb)) > 0) { + client->totallen -= 16; + if(client->callback != NULL) { + if(client->callback(client, NULL) == -1) { + client->step = WEBSERVER_CLIENT_CLOSE; + return -1; + } + client->content++; + client->ptr = 0; + } else { + client->step = WEBSERVER_CLIENT_CLOSE; + return -1; + } + } + } + break; + } + } + tcp_recved(pcb, b->len); + b = b->next; + } + pbuf_free(data); + + return ERR_OK; +} + +#ifdef ESP8266 +err_t webserver_poll(void *arg, struct tcp_pcb *pcb) { + uint8_t i = 0; + for(i=0;ipcb != NULL) { + tcp_close(client->pcb); + client->pcb = NULL; + } + if(client->active == 1) { + client->client.stop(); + } +#endif + + client->readlen = 0; + client->reqtype = 0; + client->method = 0; + client->async = 0; + client->totallen = 0; + client->active = 0; + client->step = 0; + client->substep = 0; + client->chunked = 0; + client->ptr = 0; + client->route = 0; + client->lastseen = 0; + client->content = 0; + + struct sendlist_t *tmp = NULL; + while(client->sendlist) { + tmp = client->sendlist; + client->sendlist = client->sendlist->next; + if(tmp->type == 0) { + free(tmp->ptr); + } + free(tmp); + } + if(client->boundary != NULL) { + free(client->boundary); + } + + client->sendlist = NULL; + client->sendlist_head = NULL; + client->boundary = NULL; + memset(&client->buffer, 0, WEBSERVER_BUFFER_SIZE); +} + +#ifdef ESP8266 +err_t webserver_client(void *arg, tcp_pcb *pcb, err_t err) { + uint8_t i = 0; + for(i=0;iremote_ip.addr).toString().c_str(), clients[i].data.pcb->remote_port); + log_message(log_msg); + + //tcp_nagle_disable(pcb); + tcp_recv(pcb, &webserver_async_receive); + tcp_sent(pcb, &webserver_sent); + // 15 seconds timer + tcp_poll(pcb, &webserver_poll, WEBSERVER_CLIENT_TIMEOUT*2); + break; + } + } + return ERR_OK; +} +#endif + +void webserver_loop(void) { + uint16_t size = 0; + uint8_t i = 0; + + for(i=0;i WEBSERVER_CLIENT_CONNECTING) { + if((unsigned long)(millis() - clients[i].data.lastseen) > WEBSERVER_CLIENT_TIMEOUT) { +#ifdef ESP8266 + char log_msg[256]; + sprintf_P(log_msg, PSTR("Timeout webserver client: %s:%d"), clients[i].data.client.remoteIP().toString().c_str(), clients[i].data.client.remotePort()); + log_message(log_msg); +#endif + clients[i].data.step = WEBSERVER_CLIENT_CLOSE; + } + } + switch(clients[i].data.step) { + case WEBSERVER_CLIENT_CONNECTING: { + if(clients[i].data.client.available()) { + clients[i].data.step = WEBSERVER_CLIENT_READ_HEADER; + } + clients[i].data.ptr = 0; + memset(&clients[i].data.buffer, 0, WEBSERVER_BUFFER_SIZE); + } break; + case WEBSERVER_CLIENT_ARGS: + case WEBSERVER_CLIENT_READ_HEADER: { + if(clients[i].data.client.connected() || clients[i].data.client.available()) { + if(clients[i].data.client.available()) { + uint8_t *p = (uint8_t *)rbuffer; + size = clients[i].data.client.read( + p, + WEBSERVER_READ_SIZE + ); + } + } else if(!clients[i].data.client.connected()) { + clients[i].data.step = WEBSERVER_CLIENT_CLOSE; + } else { + continue; + } + if(size > 0) { + clients[i].data.lastseen = millis(); + if(webserver_sync_receive(&clients[i].data, rbuffer, size) == -1) { + clients[i].data.step = WEBSERVER_CLIENT_CLOSE; + } + } + } break; + case WEBSERVER_CLIENT_WRITE: { + if(clients[i].data.callback != NULL) { + if(clients[i].data.step == WEBSERVER_CLIENT_WRITE) { + if(clients[i].data.callback(&clients[i].data, NULL) == -1) { + clients[i].data.step = WEBSERVER_CLIENT_CLOSE; + } else if(clients[i].data.content > 0) { + clients[i].data.step = WEBSERVER_CLIENT_SENDING; + } else { + clients[i].data.step = WEBSERVER_CLIENT_WRITE; + clients[i].data.content++; + } + } + clients[i].data.ptr = 0; + } else { + clients[i].data.step = WEBSERVER_CLIENT_CLOSE; + continue; + } + } break; + case WEBSERVER_CLIENT_SENDING: { + clients[i].data.totallen = MTU_SIZE; + /* + * Leave room for chunk overhead + */ + clients[i].data.totallen -= 16; + webserver_process_send(&clients[i].data); + } break; +#ifdef ESP8266 + case WEBSERVER_CLIENT_CLOSE: { + char log_msg[256]; + sprintf_P(log_msg, PSTR("Closing webserver client: %s:%d"), clients[i].data.client.remoteIP().toString().c_str(), clients[i].data.client.remotePort()); + log_message(log_msg); + + clients[i].data.client.stop(); + webserver_reset_client(&clients[i].data); + } break; +#endif + } + } + +#if defined(ESP8266) + if(sync_server.hasClient()) { + for(i=0;i + #include "lwip/opt.h" + #include "lwip/tcp.h" + #include "lwip/inet.h" + #include "lwip/dns.h" + #include "lwip/init.h" + #include "lwip/errno.h" + #include + #include + #include +#endif + +#if !defined(err_t) && !defined(ESP8266) + #define err_t uint8_t +#endif + +#ifndef ESP8266 +typedef struct tcp_pcb { +} tcp_pcb; + +typedef struct pbuf { + unsigned int len; + void *payload; + struct pbuf *next; +} pbuf; +#endif + +typedef struct header_t { + unsigned char *buffer; + uint16_t ptr; +} header_t; + +struct webserver_t; +extern struct webserver_client_t clients[WEBSERVER_MAX_CLIENTS]; + +typedef int8_t (webserver_cb_t)(struct webserver_t *client, void *data); + +typedef struct arguments_t { + unsigned char *name; + unsigned char *value; + uint16_t len; +} arguments_t; + +typedef struct sendlist_t { + void *ptr; + uint16_t type:1; + uint16_t size:15; + struct sendlist_t *next; +} sendlist_t; + +#ifndef ESP8266 +struct WiFiClient { + int (*write)(unsigned char *, int i); + int (*write_P)(unsigned char *, int i); + int (*available)(); + int (*connected)(); + int (*read)(uint8_t *buffer, int size); +}; + #define PGM_P unsigned char * +#endif + +typedef struct webserver_t { + tcp_pcb *pcb; + WiFiClient client; + unsigned long lastseen; + uint8_t active:1; + uint8_t reqtype:1; + uint8_t async:1; + uint8_t method:1; + uint8_t chunked:4; + uint8_t step:4; + uint8_t substep:4; + uint16_t ptr; + uint32_t totallen; + uint32_t readlen; + uint16_t content; + uint8_t route; + struct sendlist_t *sendlist; + struct sendlist_t *sendlist_head; + webserver_cb_t *callback; + unsigned char buffer[WEBSERVER_BUFFER_SIZE]; + char *boundary; +} webserver_t; + +typedef struct webserver_client_t { + struct webserver_t data; +} webserver_client_t; + +typedef enum { + WEBSERVER_CLIENT_CONNECTING = 1, + WEBSERVER_CLIENT_REQUEST_METHOD, + WEBSERVER_CLIENT_REQUEST_URI, + WEBSERVER_CLIENT_READ_HEADER, + WEBSERVER_CLIENT_CREATE_HEADER, + WEBSERVER_CLIENT_WRITE, + WEBSERVER_CLIENT_SENDING, + WEBSERVER_CLIENT_HEADER, + WEBSERVER_CLIENT_ARGS, + WEBSERVER_CLIENT_CLOSE, +} webserver_steps; + +int8_t webserver_start(int port, webserver_cb_t *callback, uint8_t async); +void webserver_loop(void); +void webserver_send_content(struct webserver_t *client, char *buf, uint16_t len); +void webserver_send_content_P(struct webserver_t *client, PGM_P buf, uint16_t len); +err_t webserver_async_receive(void *arg, tcp_pcb *pcb, struct pbuf *data, err_t err); +uint8_t webserver_sync_receive(struct webserver_t *client, uint8_t *rbuffer, uint16_t size); +void webserver_loop(void); +int16_t urldecode(const unsigned char *src, int src_len, unsigned char *dst, int dst_len, int is_form_url_encoded); +int8_t webserver_send(struct webserver_t *client, uint16_t code, char *mimetype, uint16_t data_len); +void webserver_client_stop(struct webserver_t *client); +void webserver_reset_client(struct webserver_t *client); + +#endif diff --git a/HeishaMon/version.h b/HeishaMon/version.h index 571c536f..cd8b90a1 100644 --- a/HeishaMon/version.h +++ b/HeishaMon/version.h @@ -1 +1 @@ -static const char* heishamon_version = "2.1-iy-12"; +static const char* heishamon_version = "2.1-iy-PR76"; diff --git a/HeishaMon/webfunctions.cpp b/HeishaMon/webfunctions.cpp old mode 100644 new mode 100755 index 38b6047c..4dc091b6 --- a/HeishaMon/webfunctions.cpp +++ b/HeishaMon/webfunctions.cpp @@ -3,22 +3,25 @@ #include "version.h" #include "htmlcode.h" #include "commands.h" +#include "src/common/webserver.h" +#include "src/common/timerqueue.h" #include -#include - #include //https://github.com/bblanchon/ArduinoJson #define UPTIME_OVERFLOW 4294967295 // Uptime overflow value -static int numSsid = 0; +static String wifiJsonList = ""; -void log_message(char* string); +struct websettings_t { + String name; + String value; + struct websettings_t *next; +}; +static struct websettings_t *websettings = NULL; -void getWifiScanResults(int networksFound) { - numSsid = networksFound; -} +void log_message(char* string); int dBmToQuality(int dBm) { if (dBm == 31) @@ -30,6 +33,50 @@ int dBmToQuality(int dBm) { return 2 * (dBm + 100); } + +void getWifiScanResults(int numSsid) { + if (numSsid > 0) { //found wifi networks + wifiJsonList = "["; + int indexes[numSsid]; + for (int i = 0; i < numSsid; i++) { //fill the sorted list with normal indexes first + indexes[i] = i; + } + for (int i = 0; i < numSsid; i++) { //then sort + for (int j = i + 1; j < numSsid; j++) { + if (WiFi.RSSI(indexes[j]) > WiFi.RSSI(indexes[i])) { + int temp = indexes[j]; + indexes[j] = indexes[i]; + indexes[i] = temp; + } + } + } + String ssid; + for (int i = 0; i < numSsid; i++) { //then remove duplicates + if (indexes[i] == -1) continue; + ssid = WiFi.SSID(indexes[i]); + for (int j = i + 1; j < numSsid; j++) { + if (ssid == WiFi.SSID(indexes[j])) { + indexes[j] = -1; + } + } + } + bool firstSSID = true; + for (int i = 0; i < numSsid; i++) { //then output json + if (indexes[i] == -1) { + continue; + } + if (!firstSSID) { + wifiJsonList = wifiJsonList + ","; + } + wifiJsonList = wifiJsonList + "{\"ssid\":\"" + WiFi.SSID(indexes[i]) + "\", \"rssi\": \"" + dBmToQuality(WiFi.RSSI(indexes[i])) + "%\"}"; + firstSSID = false; + } + wifiJsonList = wifiJsonList + "]"; + } +} + + + int getWifiQuality() { if (WiFi.status() != WL_CONNECTED) return -1; @@ -46,7 +93,7 @@ int getFreeMemory() { } // returns system uptime in seconds -String getUptime() { +char *getUptime(void) { static uint32_t last_uptime = 0; static uint8_t uptime_overflows = 0; @@ -56,14 +103,22 @@ String getUptime() { last_uptime = millis(); uint32_t t = uptime_overflows * (UPTIME_OVERFLOW / 1000) + (last_uptime / 1000); - char uptime[200]; uint8_t d = t / 86400L; uint8_t h = ((t % 86400L) / 3600L) % 60; uint32_t rem = t % 3600L; uint8_t m = rem / 60; uint8_t sec = rem % 60; - sprintf_P(uptime, PSTR("%d day%s %d hour%s %d minute%s %d second%s"), d, (d == 1) ? "" : "s", h, (h == 1) ? "" : "s", m, (m == 1) ? "" : "s", sec, (sec == 1) ? "" : "s"); - return String(uptime); + + unsigned int len = snprintf_P(NULL, 0, PSTR("%d day%s %d hour%s %d minute%s %d second%s"), d, (d == 1) ? "" : "s", h, (h == 1) ? "" : "s", m, (m == 1) ? "" : "s", sec, (sec == 1) ? "" : "s"); + char *str = (char *)malloc(len+2); + if(str == NULL) { + Serial1.printf("Out of memory %s:#%d\n", __FUNCTION__, __LINE__); + ESP.restart(); + exit(-1); + } + memset(str, 0, len+2); + snprintf_P(str, len+1, PSTR("%d day%s %d hour%s %d minute%s %d second%s"), d, (d == 1) ? "" : "s", h, (h == 1) ? "" : "s", m, (m == 1) ? "" : "s", sec, (sec == 1) ? "" : "s"); + return str; } void loadSettings(settingsStruct *heishamonSettings) { @@ -142,35 +197,6 @@ void loadSettings(settingsStruct *heishamonSettings) { WiFi.disconnect(); WiFi.persistent(false); } - - if (LittleFS.exists("/heatcurve.json")) { - //file exists, reading and loading - log_message((char *)"reading heatingcurve file"); - File configFile = LittleFS.open("/heatcurve.json", "r"); - if (configFile) { - log_message((char *)"opened heating curve config file"); - size_t size = configFile.size(); - // Allocate a buffer to store contents of the file. - std::unique_ptr buf(new char[size]); - - configFile.readBytes(buf.get(), size); - DynamicJsonDocument jsonDoc(1024); - DeserializationError error = deserializeJson(jsonDoc, buf.get()); - serializeJson(jsonDoc, Serial); - if (!error) { - heishamonSettings->SmartControlSettings.enableHeatCurve = ( jsonDoc["enableHeatCurve"] == "enabled" ) ? true : false; - if ( jsonDoc["avgHourHeatCurve"]) heishamonSettings->SmartControlSettings.avgHourHeatCurve = jsonDoc["avgHourHeatCurve"]; - if ( jsonDoc["heatCurveTargetHigh"]) heishamonSettings->SmartControlSettings.heatCurveTargetHigh = jsonDoc["heatCurveTargetHigh"]; - if ( jsonDoc["heatCurveTargetLow"]) heishamonSettings->SmartControlSettings.heatCurveTargetLow = jsonDoc["heatCurveTargetLow"]; - if ( jsonDoc["heatCurveOutHigh"]) heishamonSettings->SmartControlSettings.heatCurveOutHigh = jsonDoc["heatCurveOutHigh"]; - if ( jsonDoc["heatCurveOutLow"]) heishamonSettings->SmartControlSettings.heatCurveOutLow = jsonDoc["heatCurveOutLow"]; - for (unsigned int i = 0 ; i < 36 ; i++) { - if ( jsonDoc["heatCurveLookup"][i]) heishamonSettings->SmartControlSettings.heatCurveLookup[i] = jsonDoc["heatCurveLookup"][i]; - } - } - configFile.close(); - } - } } else { log_message((char *)"failed to mount FS"); } @@ -179,7 +205,6 @@ void loadSettings(settingsStruct *heishamonSettings) { } void setupWifi(settingsStruct *heishamonSettings) { - log_message((char *)"Wifi reconnecting with new configuration..."); //no sleep wifi WiFi.setSleepMode(WIFI_NONE_SLEEP); @@ -209,175 +234,50 @@ void setupWifi(settingsStruct *heishamonSettings) { } else { WiFi.hostname(heishamonSettings->wifi_hostname); } - //initiate a wifi scan at boot to fill the wifi scan list - WiFi.scanNetworksAsync(getWifiScanResults); -} - -void handleRoot(ESP8266WebServer *httpServer, float readpercentage, int mqttReconnects, settingsStruct *heishamonSettings) { - httpServer->setContentLength(CONTENT_LENGTH_UNKNOWN); - httpServer->send(200, "text/html", ""); - httpServer->sendContent_P(webHeader); - httpServer->sendContent_P(webCSS); - httpServer->sendContent_P(webBodyStart); - httpServer->sendContent_P(webBodyRoot1); - httpServer->sendContent(heishamon_version); - httpServer->sendContent_P(webBodyRoot2); - - if (heishamonSettings->use_1wire) httpServer->sendContent_P(webBodyRootDallasTab); - if (heishamonSettings->use_s0) httpServer->sendContent_P(webBodyRootS0Tab); - httpServer->sendContent_P(webBodyRootConsoleTab); - httpServer->sendContent_P(webBodyEndDiv); - - httpServer->sendContent_P(webBodyRootStatusWifi); - httpServer->sendContent(String(getWifiQuality())); - httpServer->sendContent_P(webBodyRootStatusMemory); - httpServer->sendContent(String(getFreeMemory())); - httpServer->sendContent_P(webBodyRootStatusReceived); - httpServer->sendContent(String(readpercentage)); - httpServer->sendContent_P(webBodyRootStatusReconnects); - httpServer->sendContent(String(mqttReconnects)); - httpServer->sendContent_P(webBodyRootStatusUptime); - httpServer->sendContent(getUptime()); - httpServer->sendContent_P(webBodyEndDiv); - - httpServer->sendContent_P(webBodyRootHeatpumpValues); - if (heishamonSettings->use_1wire)httpServer->sendContent_P(webBodyRootDallasValues); - if (heishamonSettings->use_s0) httpServer->sendContent_P(webBodyRootS0Values); - httpServer->sendContent_P(webBodyRootConsole); - - httpServer->sendContent_P(menuJS); - httpServer->sendContent_P(refreshJS); - httpServer->sendContent_P(selectJS); - httpServer->sendContent_P(websocketJS); - httpServer->sendContent_P(webFooter); - httpServer->sendContent(""); - httpServer->client().stop(); } -void handleTableRefresh(ESP8266WebServer *httpServer, String actData[]) { - httpServer->setContentLength(CONTENT_LENGTH_UNKNOWN); - httpServer->send(200, "text/html", ""); - if (httpServer->hasArg("1wire")) { - httpServer->sendContent(dallasTableOutput()); - } else if (httpServer->hasArg("s0")) { - httpServer->sendContent(s0TableOutput()); - } else { - for (unsigned int topic = 0 ; topic < NUMBER_OF_TOPICS ; topic++) { - String topicdesc; - const char *valuetext = "value"; - if (strcmp_P(valuetext, topicDescription[topic][0]) == 0) { - topicdesc = topicDescription[topic][1]; - } - else { - int value = actData[topic].toInt(); - int maxvalue = atoi(topicDescription[topic][0]); - if ((value < 0) || (value > maxvalue)) { - topicdesc = "unknown"; - } - else { - topicdesc = topicDescription[topic][value + 1]; //plus one, because 0 is the maxvalue container - } - } - String tabletext = ""; - tabletext = tabletext + "TOP" + topic + ""; - tabletext = tabletext + "" + topics[topic] + ""; - tabletext = tabletext + "" + actData[topic] + ""; - tabletext = tabletext + "" + topicdesc + ""; - tabletext = tabletext + ""; - httpServer->sendContent(tabletext); - } +int handleFactoryReset(struct webserver_t *client) { + switch(client->content) { + case 0: { + webserver_send(client, 200, (char *)"text/html", 0); + webserver_send_content_P(client, webHeader, strlen_P(webHeader)); + webserver_send_content_P(client, webCSS, strlen_P(webCSS)); + webserver_send_content_P(client, refreshMeta, strlen_P(refreshMeta)); + } break; + case 1: { + webserver_send_content_P(client, webBodyStart, strlen_P(webBodyStart)); + webserver_send_content_P(client, webBodyRebootWarning, strlen_P(webBodyRebootWarning)); + webserver_send_content_P(client, menuJS, strlen_P(menuJS)); + webserver_send_content_P(client, webFooter, strlen_P(webFooter)); + } break; + case 2: { + timerqueue_insert(1, 0, -1); // Start reboot sequence + } break; } - httpServer->sendContent(""); - httpServer->client().stop(); -} -void handleJsonOutput(ESP8266WebServer *httpServer, String actData[]) { - httpServer->setContentLength(CONTENT_LENGTH_UNKNOWN); - httpServer->sendHeader("Access-Control-Allow-Origin", "*"); - httpServer->send(200, "application/json", ""); - //begin json - String tabletext = F("{"); - //heatpump values in json - tabletext = tabletext + F("\"heatpump\":["); - httpServer->sendContent(tabletext); - for (unsigned int topic = 0 ; topic < NUMBER_OF_TOPICS ; topic++) { - String topicdesc; - const char *valuetext = "value"; - if (strcmp_P(valuetext, topicDescription[topic][0]) == 0) { - topicdesc = topicDescription[topic][1]; - } - else { - int value = actData[topic].toInt(); - int maxvalue = atoi(topicDescription[topic][0]); - if ((value < 0) || (value > maxvalue)) { - topicdesc = "unknown"; - } - else { - topicdesc = topicDescription[topic][value + 1]; //plus one, because 0 is the maxvalue container - } - } - tabletext = F("{"); - tabletext = tabletext + F("\"Topic\": \"TOP") + topic + F("\","); - tabletext = tabletext + F("\"Name\": \"") + topics[topic] + F("\","); - tabletext = tabletext + F("\"Value\": \"") + actData[topic] + F("\","); - tabletext = tabletext + F("\"Description\": \"") + topicdesc + F("\""); - tabletext = tabletext + F("}"); - if (topic < NUMBER_OF_TOPICS - 1) { - tabletext = tabletext + F(","); - } - httpServer->sendContent(tabletext); - } - tabletext = F("]"); - httpServer->sendContent(tabletext); - //1wire data in json - tabletext = F(",\"1wire\":"); - tabletext = tabletext + dallasJsonOutput(); - httpServer->sendContent(tabletext); - //s0 data in json - tabletext = F(",\"s0\":"); - tabletext = tabletext + s0JsonOutput(); - httpServer->sendContent(tabletext); - //end json string - tabletext = F("}"); - httpServer->sendContent(tabletext); - httpServer->sendContent(""); - httpServer->client().stop(); + return 0; } -void handleFactoryReset(ESP8266WebServer *httpServer) { - httpServer->setContentLength(CONTENT_LENGTH_UNKNOWN); - httpServer->send(200, "text/html", ""); - httpServer->sendContent_P(webHeader); - httpServer->sendContent_P(webCSS); - httpServer->sendContent_P(refreshMeta); - httpServer->sendContent_P(webBodyStart); - httpServer->sendContent_P(webBodyFactoryResetWarning); - httpServer->sendContent_P(menuJS); - httpServer->sendContent_P(webFooter); - httpServer->sendContent(""); - httpServer->client().stop(); - delay(1000); - LittleFS.begin(); - LittleFS.format(); - WiFi.disconnect(true); - delay(1000); - ESP.restart(); -} +int handleReboot(struct webserver_t *client) { + switch(client->content) { + case 0: { + webserver_send(client, 200, (char *)"text/html", 0); + webserver_send_content_P(client, webHeader, strlen_P(webHeader)); + webserver_send_content_P(client, webCSS, strlen_P(webCSS)); + webserver_send_content_P(client, refreshMeta, strlen_P(refreshMeta)); + } break; + case 1: { + webserver_send_content_P(client, webBodyStart, strlen_P(webBodyStart)); + webserver_send_content_P(client, webBodyRebootWarning, strlen_P(webBodyRebootWarning)); + webserver_send_content_P(client, menuJS, strlen_P(menuJS)); + webserver_send_content_P(client, webFooter, strlen_P(webFooter)); + } break; + case 2: { + timerqueue_insert(5, 0, -2); // Start reboot sequence + } break; + } -void handleReboot(ESP8266WebServer *httpServer) { - httpServer->setContentLength(CONTENT_LENGTH_UNKNOWN); - httpServer->send(200, "text/html", ""); - httpServer->sendContent_P(webHeader); - httpServer->sendContent_P(webCSS); - httpServer->sendContent_P(refreshMeta); - httpServer->sendContent_P(webBodyStart); - httpServer->sendContent_P(webBodyRebootWarning); - httpServer->sendContent_P(menuJS); - httpServer->sendContent_P(webFooter); - httpServer->sendContent(""); - httpServer->client().stop(); - delay(1000); - ESP.restart(); + return 0; } void settingsToJson(DynamicJsonDocument &jsonDoc, settingsStruct *heishamonSettings) { @@ -443,739 +343,765 @@ void saveJsonToConfig(DynamicJsonDocument &jsonDoc) { } } -bool handleSettings(ESP8266WebServer *httpServer, settingsStruct *heishamonSettings) { - //check if POST was made with save settings - if (httpServer->args()) { - bool reconnectWiFi = false; - DynamicJsonDocument jsonDoc(1024); +int saveSettings(struct webserver_t *client, settingsStruct *heishamonSettings) { + const char *wifi_ssid = NULL; + const char *wifi_password = NULL; + const char *new_ota_password = NULL; + const char *current_ota_password = NULL; + const char *use_s0 = NULL; + + bool reconnectWiFi = false; + DynamicJsonDocument jsonDoc(1024); + + settingsToJson(jsonDoc, heishamonSettings); //stores current settings in a json document + + jsonDoc["listenonly"] = String(""); + jsonDoc["logMqtt"] = String(""); + jsonDoc["logHexdump"] = String(""); + jsonDoc["logSerial1"] = String(""); + jsonDoc["optionalPCB"] = String(""); + jsonDoc["use_1wire"] = String(""); + jsonDoc["use_s0"] = String(""); + + struct websettings_t *tmp = websettings; + while(tmp) { + if(strcmp(tmp->name.c_str(), "wifi_hostname") == 0) { + jsonDoc["wifi_hostname"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "mqtt_topic_base") == 0) { + jsonDoc["mqtt_topic_base"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "mqtt_server") == 0) { + jsonDoc["mqtt_server"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "mqtt_port") == 0) { + jsonDoc["mqtt_port"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "mqtt_username") == 0) { + jsonDoc["mqtt_username"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "mqtt_password") == 0) { + jsonDoc["mqtt_password"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "use_1wire") == 0) { + jsonDoc["use_1wire"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "use_s0") == 0) { + jsonDoc["use_s0"] = tmp->value; + if(strcmp(tmp->value.c_str(), "enabled") == 0) { + use_s0 = tmp->value.c_str(); + } + } else if(strcmp(tmp->name.c_str(), "listenonly") == 0) { + jsonDoc["listenonly"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "logMqtt") == 0) { + jsonDoc["logMqtt"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "logHexdump") == 0) { + jsonDoc["logHexdump"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "logSerial1") == 0) { + jsonDoc["logSerial1"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "optionalPCB") == 0) { + jsonDoc["optionalPCB"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "waitTime") == 0) { + jsonDoc["waitTime"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "waitDallasTime") == 0) { + jsonDoc["waitDallasTime"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "updateAllTime") == 0) { + jsonDoc["updateAllTime"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "dallasResolution") == 0) { + jsonDoc["dallasResolution"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "updataAllDallasTime") == 0) { + jsonDoc["updataAllDallasTime"] = tmp->value; + } else if(strcmp(tmp->name.c_str(), "wifi_ssid") == 0) { + wifi_ssid = tmp->value.c_str(); + } else if(strcmp(tmp->name.c_str(), "wifi_password") == 0) { + wifi_password = tmp->value.c_str(); + } else if(strcmp(tmp->name.c_str(), "new_ota_password") == 0) { + new_ota_password = tmp->value.c_str(); + } else if(strcmp(tmp->name.c_str(), "current_ota_password") == 0) { + current_ota_password = tmp->value.c_str(); + } + tmp = tmp->next; + } - settingsToJson(jsonDoc, heishamonSettings); //stores current settings in a json document + tmp = websettings; + while(tmp) { + if(use_s0 != NULL && strcmp(tmp->name.c_str(), "s0_1_gpio") == 0) { + jsonDoc["s0_1_gpio"] = tmp->value; + } else if(use_s0 != NULL && strcmp(tmp->name.c_str(), "s0_1_ppkwh") == 0) { + jsonDoc["s0_1_ppkwh"] = tmp->value; + } else if(use_s0 != NULL && strcmp(tmp->name.c_str(), "s0_1_interval") == 0) { + jsonDoc["s0_1_interval"] = tmp->value; + } else if(use_s0 != NULL && strcmp(tmp->name.c_str(), "s0_1_minpulsewidth") == 0) { + jsonDoc["s0_1_minpulsewidth"] = tmp->value; + } else if(use_s0 != NULL && strcmp(tmp->name.c_str(), "s0_1_maxpulsewidth") == 0) { + jsonDoc["s0_1_maxpulsewidth"] = tmp->value; + } else if(use_s0 != NULL && strcmp(tmp->name.c_str(), "s0_2_gpio") == 0) { + jsonDoc["s0_2_gpio"] = tmp->value; + } else if(use_s0 != NULL && strcmp(tmp->name.c_str(), "s0_2_ppkwh") == 0) { + jsonDoc["s0_2_ppkwh"] = tmp->value; + } else if(use_s0 != NULL && strcmp(tmp->name.c_str(), "s0_2_ppkwh") == 0) { + jsonDoc["s0_2_ppkwh"] = tmp->value; + } else if(use_s0 != NULL && strcmp(tmp->name.c_str(), "s0_2_interval") == 0) { + jsonDoc["s0_2_interval"] = tmp->value; + } else if(use_s0 != NULL && strcmp(tmp->name.c_str(), "s0_2_minpulsewidth") == 0) { + jsonDoc["s0_2_minpulsewidth"] = tmp->value; + } else if(use_s0 != NULL && strcmp(tmp->name.c_str(), "s0_2_maxpulsewidth") == 0) { + jsonDoc["s0_2_maxpulsewidth"] = tmp->value; + } + tmp = tmp->next; + } - //then overwrite with new settings - if (httpServer->hasArg("wifi_hostname")) { - jsonDoc["wifi_hostname"] = httpServer->arg("wifi_hostname"); - } - if (httpServer->hasArg("wifi_ssid") && httpServer->hasArg("wifi_password")) { - if (strcmp(jsonDoc["wifi_ssid"], httpServer->arg("wifi_ssid").c_str()) != 0 || strcmp(jsonDoc["wifi_password"], httpServer->arg("wifi_password").c_str()) != 0) { - reconnectWiFi = true; - } - } - if (httpServer->hasArg("wifi_ssid")) { - jsonDoc["wifi_ssid"] = httpServer->arg("wifi_ssid").c_str(); - } - if (httpServer->hasArg("wifi_password")) { - jsonDoc["wifi_password"] = httpServer->arg("wifi_password").c_str(); - } - if (httpServer->hasArg("new_ota_password") && (httpServer->arg("new_ota_password") != NULL) && (httpServer->arg("current_ota_password") != NULL) ) { - if (httpServer->hasArg("current_ota_password") && (strcmp(heishamonSettings->ota_password, httpServer->arg("current_ota_password").c_str()) == 0 )) { - jsonDoc["ota_password"] = httpServer->arg("new_ota_password"); - } - else { - httpServer->setContentLength(CONTENT_LENGTH_UNKNOWN); - httpServer->send(200, "text/html", ""); - httpServer->sendContent_P(webHeader); - httpServer->sendContent_P(webCSS); - httpServer->sendContent_P(webBodyStart); - httpServer->sendContent_P(webBodySettings1); - httpServer->sendContent_P(webBodySettingsResetPasswordWarning); - httpServer->sendContent_P(refreshMeta); - httpServer->sendContent_P(webFooter); - httpServer->sendContent(""); - httpServer->client().stop(); - return true; - } - } - if (httpServer->hasArg("mqtt_topic_base")) { - jsonDoc["mqtt_topic_base"] = httpServer->arg("mqtt_topic_base"); - } - if (httpServer->hasArg("mqtt_server")) { - jsonDoc["mqtt_server"] = httpServer->arg("mqtt_server"); - } - if (httpServer->hasArg("mqtt_port")) { - jsonDoc["mqtt_port"] = httpServer->arg("mqtt_port"); - } - if (httpServer->hasArg("mqtt_username")) { - jsonDoc["mqtt_username"] = httpServer->arg("mqtt_username"); - } - if (httpServer->hasArg("mqtt_password")) { - jsonDoc["mqtt_password"] = httpServer->arg("mqtt_password"); - } - if (httpServer->hasArg("use_1wire")) { - jsonDoc["use_1wire"] = "enabled"; - } else { - jsonDoc["use_1wire"] = "disabled"; - } - if (httpServer->hasArg("use_s0")) { - jsonDoc["use_s0"] = "enabled"; - if (httpServer->hasArg("s0_1_gpio")) jsonDoc["s0_1_gpio"] = httpServer->arg("s0_1_gpio"); - if (httpServer->hasArg("s0_1_ppkwh")) jsonDoc["s0_1_ppkwh"] = httpServer->arg("s0_1_ppkwh"); - if (httpServer->hasArg("s0_1_interval")) jsonDoc["s0_1_interval"] = httpServer->arg("s0_1_interval"); - if (httpServer->hasArg("s0_1_minpulsewidth")) jsonDoc["s0_1_minpulsewidth"] = httpServer->arg("s0_1_minpulsewidth"); - if (httpServer->hasArg("s0_1_maxpulsewidth")) jsonDoc["s0_1_maxpulsewidth"] = httpServer->arg("s0_1_maxpulsewidth"); - if (httpServer->hasArg("s0_2_gpio")) jsonDoc["s0_2_gpio"] = httpServer->arg("s0_2_gpio"); - if (httpServer->hasArg("s0_2_ppkwh")) jsonDoc["s0_2_ppkwh"] = httpServer->arg("s0_2_ppkwh"); - if (httpServer->hasArg("s0_2_interval")) jsonDoc["s0_2_interval"] = httpServer->arg("s0_2_interval"); - if (httpServer->hasArg("s0_2_minpulsewidth")) jsonDoc["s0_2_minpulsewidth"] = httpServer->arg("s0_2_minpulsewidth"); - if (httpServer->hasArg("s0_2_maxpulsewidth")) jsonDoc["s0_2_maxpulsewidth"] = httpServer->arg("s0_2_maxpulsewidth"); - } else { - jsonDoc["use_s0"] = "disabled"; - } - if (httpServer->hasArg("listenonly")) { - jsonDoc["listenonly"] = "enabled"; - } else { - jsonDoc["listenonly"] = "disabled"; - } - if (httpServer->hasArg("logMqtt")) { - jsonDoc["logMqtt"] = "enabled"; - } else { - jsonDoc["logMqtt"] = "disabled"; - } - if (httpServer->hasArg("logHexdump")) { - jsonDoc["logHexdump"] = "enabled"; - } else { - jsonDoc["logHexdump"] = "disabled"; - } - if (httpServer->hasArg("logSerial1")) { - jsonDoc["logSerial1"] = "enabled"; - } else { - jsonDoc["logSerial1"] = "disabled"; - } - if (httpServer->hasArg("optionalPCB")) { - jsonDoc["optionalPCB"] = "enabled"; + while(websettings) { + tmp = websettings; + websettings = websettings->next; + free(tmp); + } + + if(new_ota_password != NULL && strlen(new_ota_password) > 0 && current_ota_password != NULL && strlen(current_ota_password) > 0) { + if(strcmp(heishamonSettings->ota_password, current_ota_password) == 0) { + jsonDoc["ota_password"] = new_ota_password; } else { - jsonDoc["optionalPCB"] = "disabled"; - } - if (httpServer->hasArg("waitTime")) { - jsonDoc["waitTime"] = httpServer->arg("waitTime"); - } - if (httpServer->hasArg("waitDallasTime")) { - jsonDoc["waitDallasTime"] = httpServer->arg("waitDallasTime"); - } - if (httpServer->hasArg("dallasResolution")) { - jsonDoc["dallasResolution"] = httpServer->arg("dallasResolution"); - } - if (httpServer->hasArg("updateAllTime")) { - jsonDoc["updateAllTime"] = httpServer->arg("updateAllTime"); - } - if (httpServer->hasArg("updataAllDallasTime")) { - jsonDoc["updataAllDallasTime"] = httpServer->arg("updataAllDallasTime"); + client->route = 111; + return 0; } + } - saveJsonToConfig(jsonDoc); //save to config file - loadSettings(heishamonSettings); //load config file to current settings - - if (reconnectWiFi) { - httpServer->setContentLength(CONTENT_LENGTH_UNKNOWN); - httpServer->send(200, "text/html", ""); - httpServer->sendContent_P(webHeader); - httpServer->sendContent_P(webCSS); - httpServer->sendContent_P(webBodyStart); - httpServer->sendContent_P(webBodySettings1); - httpServer->sendContent_P(webBodySettingsNewWifiWarning); - httpServer->sendContent_P(refreshMeta); - httpServer->sendContent_P(webFooter); - httpServer->sendContent(""); - httpServer->client().stop(); - setupWifi(heishamonSettings); - return true; + if(wifi_password != NULL && wifi_ssid != NULL && strlen(wifi_ssid) > 0 && strlen(wifi_password) > 0) { + if(strcmp(jsonDoc["wifi_ssid"], wifi_ssid) != 0 || strcmp(jsonDoc["wifi_password"], wifi_password) != 0) { + reconnectWiFi = true; } - - } - - httpServer->setContentLength(CONTENT_LENGTH_UNKNOWN); - httpServer->send(200, "text/html", ""); - httpServer->sendContent_P(webHeader); - httpServer->sendContent_P(webCSS); - httpServer->sendContent_P(webBodyStart); - httpServer->sendContent_P(webBodySettings1); - - String httptext = F("
"); - httptext = httptext + F("

Settings

"); - httptext = httptext + F("
"); - httptext = httptext + F(""); - httptext = httptext + F(""); - httptext = httptext + F(""); - httptext = httptext + F(""); - httptext = httptext + F(""); - httptext = httptext + F("
"); - httptext = httptext + F("Hostname:"); - httptext = httptext + F("wifi_hostname + F("\">"); - httptext = httptext + F("
"); - httptext = httptext + F("Wifi SSID:"); - // wifi scan select box - httptext = httptext + F("wifi_ssid + F("\">"); - httptext = httptext + F(""); - // - httptext = httptext + F("
"); - httptext = httptext + F("Wifi password:"); - httptext = httptext + F("wifi_password + F("\">"); - httptext = httptext + F("
"); - httptext = httptext + F("Update username:"); - httptext = httptext + F(""); - httptext = httptext + F("
"); - httptext = httptext + F("Current update password:"); - httptext = httptext + F(" default password: \"heisha\""); - httptext = httptext + F("
"); - httptext = httptext + F("New update password:"); - httptext = httptext + F(""); - httptext = httptext + F("
"); - httptext = httptext + F("Mqtt topic base:"); - httptext = httptext + F("mqtt_topic_base + F("\">"); - httptext = httptext + F("
"); - httptext = httptext + F("Mqtt server:"); - httptext = httptext + F("mqtt_server + F("\">"); - httptext = httptext + F("
"); - httptext = httptext + F("Mqtt port:"); - httptext = httptext + F("mqtt_port + F("\">"); - httptext = httptext + F("
"); - httptext = httptext + F("Mqtt username:"); - httptext = httptext + F("mqtt_username + F("\">"); - httptext = httptext + F("
"); - httptext = httptext + F("Mqtt password:"); - httptext = httptext + F("mqtt_password + F("\">"); - httptext = httptext + F("
"); - httptext = httptext + F("How often new values are collected from heatpump:"); - httptext = httptext + F("waitTime + F("\"> seconds (min 5 sec)"); - httptext = httptext + F("
"); - httptext = httptext + F("How often all heatpump values are retransmitted to MQTT broker:"); - httptext = httptext + F("updateAllTime + F("\"> seconds"); - httptext = httptext + F("
"); - - httpServer->sendContent(httptext); - httptext = F("Listen only mode:"); - if (heishamonSettings->listenonly) { - httptext = httptext + F(""); - } else { - httptext = httptext + F(""); + if(wifi_ssid != NULL) { + jsonDoc["wifi_ssid"] = String(wifi_ssid); } - httptext = httptext + F("
"); - httptext = httptext + F("Debug log to MQTT topic from start:"); - if (heishamonSettings->logMqtt) { - httptext = httptext + F(""); - } else { - httptext = httptext + F(""); + if(wifi_password != NULL) { + jsonDoc["wifi_password"] = String(wifi_password); } - httptext = httptext + F("
"); - httptext = httptext + F("Debug log hexdump enable from start:"); - if (heishamonSettings->logHexdump) { - httptext = httptext + F(""); - } else { - httptext = httptext + F(""); + + serializeJson(jsonDoc, Serial); + + saveJsonToConfig(jsonDoc); //save to config file + loadSettings(heishamonSettings); //load config file to current settings + + if(reconnectWiFi) { + client->route = 112; + return 0; } - httptext = httptext + F("
"); - httptext = httptext + F("Debug log to serial1 (GPIO2):"); - if (heishamonSettings->logSerial1) { - httptext = httptext + F(""); - } else { - httptext = httptext + F(""); + + client->route = 113; + return 0; +} + +int cacheSettings(struct webserver_t *client, struct arguments_t * args) { + struct websettings_t *tmp = websettings; + while(tmp) { + if(strcmp(tmp->name.c_str(), (char *)args->name) == 0) { + char *cpy = (char *)malloc(args->len+1); + memset(cpy, 0, args->len+1); + memcpy(cpy, args->value, args->len); + tmp->value += cpy; + free(cpy); + break; + } + tmp = tmp->next; } - httptext = httptext + F("
"); - httptext = httptext + F("Emulate optional PCB:"); - if (heishamonSettings->optionalPCB) { - httptext = httptext + F(""); - } else { - httptext = httptext + F(""); + if(tmp == NULL) { + websettings_t *node = new websettings_t; + if(node == NULL) { + Serial1.printf("Out of memory %s:#%d\n", __FUNCTION__, __LINE__); + ESP.restart(); + exit(-1); + } + node->next = NULL; + node->name += (char *)args->name; + + if(args->value != NULL) { + char *cpy = (char *)malloc(args->len+1); + if(node == NULL) { + Serial1.printf("Out of memory %s:#%d\n", __FUNCTION__, __LINE__); + ESP.restart(); + exit(-1); + } + memset(cpy, 0, args->len+1); + strncpy(cpy, (char *)args->value, args->len); + node->value += cpy; + free(cpy); + } + + node->next = websettings; + websettings = node; } - httptext = httptext + F("
"); - httpServer->sendContent(httptext); + return 0; +} - // 1wire - httptext = F(""); - httptext = httptext + F(""); - httptext = httptext + F("
"); - httptext = httptext + F("Use 1wire DS18b20:"); - if (heishamonSettings->use_1wire) { - httptext = httptext + F(""); - httptext = httptext + F("
"); - httptext = httptext + F(""); - } else { - httptext = httptext + F(""); - httptext = httptext + F(""); - httptext = httptext + F("
"); - httptext = httptext + F(""); +int settingsNewPassword(struct webserver_t *client, settingsStruct *heishamonSettings) { + switch(client->content) { + case 0: { + webserver_send(client, 200, (char *)"text/html", 0); + webserver_send_content_P(client, webHeader, strlen_P(webHeader)); + webserver_send_content_P(client, webCSS, strlen_P(webCSS)); + webserver_send_content_P(client, webBodyStart, strlen_P(webBodyStart)); + } break; + case 1: { + webserver_send_content_P(client, webBodySettings1, strlen_P(webBodySettings1)); + webserver_send_content_P(client, webBodySettingsResetPasswordWarning, strlen_P(webBodySettingsResetPasswordWarning)); + } break; + case 2: { + webserver_send_content_P(client, refreshMeta, strlen_P(refreshMeta)); + webserver_send_content_P(client, webFooter, strlen_P(webFooter)); + } break; + case 3: { + setupConditionals(); + } break; } - httptext = httptext + F(""); - httptext = httptext + F("
"); - httptext = httptext + F("How often new values are collected from 1wire:"); - httptext = httptext + F("waitDallasTime + F("\"> seconds (min 5 sec)"); - httptext = httptext + F("
"); - httptext = httptext + F("How often all 1wire values are retransmitted to MQTT broker:"); - httptext = httptext + F("updataAllDallasTime + F("\"> seconds"); - httptext = httptext + F("
"); - httptext = httptext + F("DS18b20 temperature resolution:"); - String checked[4] = {"","","",""}; - if ((heishamonSettings->dallasResolution >= 9) && (heishamonSettings->dallasResolution<=12)) checked[heishamonSettings->dallasResolution-9] = "checked"; - httptext = httptext + F(""); - httptext = httptext + F(""); - httptext = httptext + F(""); - httptext = httptext + F(""); - httptext = httptext + F("
"); - - httpServer->sendContent(httptext); - // s0 - httptext = F(""); - httptext = httptext + F(""); - httptext = httptext + F("
"); - httptext = httptext + F("Use s0 kWh metering:"); - if (heishamonSettings->use_s0) { - httptext = httptext + F(""); - httptext = httptext + F("
"); - httptext = httptext + F(""); - } else { - httptext = httptext + F(""); - httptext = httptext + F(""); - httptext = httptext + F("
"); - httptext = httptext + F(""); + + return 0; +} + +int settingsReconnectWifi(struct webserver_t *client, settingsStruct *heishamonSettings) { + switch(client->content) { + case 0: { + webserver_send(client, 200, (char *)"text/html", 0); + webserver_send_content_P(client, webHeader, strlen_P(webHeader)); + webserver_send_content_P(client, webCSS, strlen_P(webCSS)); + webserver_send_content_P(client, webBodyStart, strlen_P(webBodyStart)); + } break; + case 1: { + webserver_send_content_P(client, webBodySettings1, strlen_P(webBodySettings1)); + webserver_send_content_P(client, settingsForm, strlen_P(settingsForm)); + webserver_send_content_P(client, menuJS, strlen_P(menuJS)); + } break; + case 2: { + webserver_send_content_P(client, webBodySettingsNewWifiWarning, strlen_P(webBodySettingsNewWifiWarning)); + webserver_send_content_P(client, refreshMeta, strlen_P(refreshMeta)); + webserver_send_content_P(client, webFooter, strlen_P(webFooter)); + } break; + case 3: { + setupWifi(heishamonSettings); + } break; } - //begin default S0 pins hack - if (heishamonSettings->s0Settings[0].gpiopin == 255) heishamonSettings->s0Settings[0].gpiopin = DEFAULT_S0_PIN_1; - if (heishamonSettings->s0Settings[1].gpiopin == 255) heishamonSettings->s0Settings[1].gpiopin = DEFAULT_S0_PIN_2; - //end default S0 pins hack - for (int i = 0; i < NUM_S0_COUNTERS; i++) { - httptext = httptext + F(""); + + return 0; +} + +int getSettings(struct webserver_t *client, settingsStruct *heishamonSettings) { + switch(client->content) { + case 0: { + webserver_send(client, 200, (char *)"application/json", 0); + webserver_send_content_P(client, PSTR("{\"wifi_hostname\":\""), 18); + webserver_send_content(client, heishamonSettings->wifi_hostname, strlen(heishamonSettings->wifi_hostname)); + webserver_send_content_P(client, PSTR("\",\"wifi_ssid\":\""), 15); + webserver_send_content(client, heishamonSettings->wifi_ssid, strlen(heishamonSettings->wifi_ssid)); + } break; + case 1: { + webserver_send_content_P(client, PSTR("\",\"wifi_password\":\""), 19); + webserver_send_content(client, heishamonSettings->wifi_password, strlen(heishamonSettings->wifi_password)); + webserver_send_content_P(client, PSTR("\",\"current_ota_password\":\""), 26); + webserver_send_content_P(client, PSTR("\",\"new_ota_password\":\""), 22); + } break; + case 2: { + webserver_send_content_P(client, PSTR("\",\"mqtt_topic_base\":\""), 21); + webserver_send_content(client, heishamonSettings->mqtt_topic_base, strlen(heishamonSettings->mqtt_topic_base)); + webserver_send_content_P(client, PSTR("\",\"mqtt_server\":\""), 17); + webserver_send_content(client, heishamonSettings->mqtt_server, strlen(heishamonSettings->mqtt_server)); + } break; + case 3: { + webserver_send_content_P(client, PSTR("\",\"mqtt_port\":\""), 15); + webserver_send_content(client, heishamonSettings->mqtt_port, strlen(heishamonSettings->mqtt_port)); + webserver_send_content_P(client, PSTR("\",\"mqtt_username\":\""), 19); + webserver_send_content(client, heishamonSettings->mqtt_username, strlen(heishamonSettings->mqtt_username)); + } break; + case 4: { + webserver_send_content_P(client, PSTR("\",\"mqtt_password\":\""), 19); + webserver_send_content(client, heishamonSettings->mqtt_password, strlen(heishamonSettings->mqtt_password)); + webserver_send_content_P(client, PSTR("\",\"waitTime\":"), 13); + + char str[20]; + itoa(heishamonSettings->waitTime, str, 10); + webserver_send_content(client, str, strlen(str)); + } break; + case 5: { + char str[20]; + webserver_send_content_P(client, PSTR(",\"updateAllTime\":"), 17); + + itoa(heishamonSettings->updateAllTime, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"listenonly\":"), 14); + + itoa(heishamonSettings->listenonly, str, 10); + webserver_send_content(client, str, strlen(str)); + } break; + case 6: { + char str[20]; + webserver_send_content_P(client, PSTR(",\"logMqtt\":"), 11); + + itoa(heishamonSettings->logMqtt, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"logHexdump\":"), 14); + + itoa(heishamonSettings->logHexdump, str, 10); + webserver_send_content(client, str, strlen(str)); + } break; + case 7: { + char str[20]; + webserver_send_content_P(client, PSTR(",\"logSerial1\":"), 14); + + itoa(heishamonSettings->logSerial1, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"optionalPCB\":"), 15); + + itoa(heishamonSettings->optionalPCB, str, 10); + webserver_send_content(client, str, strlen(str)); + } break; + case 8: { + char str[20]; + webserver_send_content_P(client, PSTR(",\"use_1wire\":"), 13); + + itoa(heishamonSettings->use_1wire, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"waitDallasTime\":"), 18); + + itoa(heishamonSettings->waitDallasTime, str, 10); + webserver_send_content(client, str, strlen(str)); + } break; + case 9: { + char str[20]; + webserver_send_content_P(client, PSTR(",\"updataAllDallasTime\":"), 23); + + itoa(heishamonSettings->updataAllDallasTime, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"dallasResolution\":"), 20); + + itoa(heishamonSettings->dallasResolution , str, 10); + webserver_send_content(client, str, strlen(str)); + } break; + case 10: { + char str[20]; + webserver_send_content_P(client, PSTR(",\"use_s0\":"), 10); + + itoa(heishamonSettings->use_s0, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"s0_1_gpio\":"), 13); + + int i = 0; + + if (heishamonSettings->s0Settings[i].gpiopin == 255) heishamonSettings->s0Settings[i].gpiopin = DEFAULT_S0_PIN_1; //dirty hack + itoa(heishamonSettings->s0Settings[i].gpiopin, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"s0_1_ppkwh\":"), 14); + + itoa(heishamonSettings->s0Settings[i].ppkwh, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"s0_1_interval\":"), 17); + + itoa(heishamonSettings->s0Settings[i].lowerPowerInterval, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"s0_1_minpulsewidth\":"), 22); + + itoa(heishamonSettings->s0Settings[i].minimalPulseWidth, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"s0_1_maxpulsewidth\":"), 22); + + itoa(heishamonSettings->s0Settings[i].maximalPulseWidth, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"s0_1_minwatt\":"), 16); + + itoa((int) round((3600 * 1000 / heishamonSettings->s0Settings[i].ppkwh) / heishamonSettings->s0Settings[i].lowerPowerInterval), str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"s0_2_gpio\":"), 13); + } break; + case 11: { + char str[20]; + int i = 1; + + if (heishamonSettings->s0Settings[i].gpiopin == 255) heishamonSettings->s0Settings[i].gpiopin = DEFAULT_S0_PIN_2; //dirty hack + itoa(heishamonSettings->s0Settings[i].gpiopin, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"s0_2_ppkwh\":"), 14); + + itoa(heishamonSettings->s0Settings[i].ppkwh, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"s0_2_interval\":"), 17); + + itoa(heishamonSettings->s0Settings[i].lowerPowerInterval, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"s0_2_minpulsewidth\":"), 22); + + itoa(heishamonSettings->s0Settings[i].minimalPulseWidth, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"s0_2_maxpulsewidth\":"), 22); + + itoa(heishamonSettings->s0Settings[i].maximalPulseWidth, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(",\"s0_2_minwatt\":"), 16); + + itoa((int) round((3600 * 1000 / heishamonSettings->s0Settings[i].ppkwh) / heishamonSettings->s0Settings[i].lowerPowerInterval), str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR("}"), 1); + } break; } - httptext = httptext + F("
"); - httptext = httptext + F("S0 port ") + (i + 1) + F(" GPIO:"); - httptext = httptext + F("s0Settings[i].gpiopin + F("\">"); - httptext = httptext + F("
"); - httptext = httptext + F("S0 port ") + (i + 1) + F(" imp/kwh:"); - httptext = httptext + F("s0Settings[i].ppkwh) + F("\">"); - httptext = httptext + F("
"); - httptext = httptext + F("S0 port ") + (i + 1) + F(" reporting interval during standby/low power usage:"); - httptext = httptext + F("s0Settings[i].lowerPowerInterval) + F("\"> seconds"); - httptext = httptext + F("
"); - httptext = httptext + F("S0 port ") + (i + 1) + F(" minimal pulse width:"); - httptext = httptext + F("s0Settings[i].minimalPulseWidth) + F("\"> milliseconds"); - httptext = httptext + F("
"); - httptext = httptext + F("S0 port ") + (i + 1) + F(" maximal pulse width:"); - httptext = httptext + F("s0Settings[i].maximalPulseWidth) + F("\"> milliseconds"); - httptext = httptext + F("
"); - httptext = httptext + F("S0 port ") + (i + 1) + F(" standby/low power usage threshold: Watt"); - httptext = httptext + F("
"); - - httptext = httptext + F("

"); - httptext = httptext + F(""); - httptext = httptext + F("
"); - httptext = httptext + F("
Factory reset"); - httptext = httptext + F("
"); - httpServer->sendContent(httptext); - - httpServer->sendContent_P(menuJS); - httpServer->sendContent_P(settingsJS); - httpServer->sendContent_P(populatescanwifiJS); - httpServer->sendContent_P(changewifissidJS); - httpServer->sendContent_P(webFooter); - httpServer->sendContent(""); - httpServer->client().stop(); - - /* - * need to reload some settings in main loop if save was done - */ - if (httpServer->args()) { - return true; + return 0; +} + +int handleSettings(struct webserver_t *client) { + switch(client->content) { + case 0: { + webserver_send(client, 200, (char *)"text/html", 0); + webserver_send_content_P(client, webHeader, strlen_P(webHeader)); + webserver_send_content_P(client, webCSS, strlen_P(webCSS)); + webserver_send_content_P(client, webBodyStart, strlen_P(webBodyStart)); + webserver_send_content_P(client, webBodySettings1, strlen_P(webBodySettings1)); + } break; + case 1: { + webserver_send_content_P(client, settingsForm, strlen_P(settingsForm)); + webserver_send_content_P(client, menuJS, strlen_P(menuJS)); + webserver_send_content_P(client, settingsJS, strlen_P(settingsJS)); + webserver_send_content_P(client, populatescanwifiJS, strlen_P(populatescanwifiJS)); + } break; + case 2: { + webserver_send_content_P(client, changewifissidJS, strlen_P(changewifissidJS)); + webserver_send_content_P(client, populategetsettingsJS, strlen_P(populategetsettingsJS)); + webserver_send_content_P(client, webFooter, strlen_P(webFooter)); + } break; } - else { - return false; + + return 0; +} + +int handleWifiScan(struct webserver_t *client) { + if(client->content == 0) { + webserver_send(client, 200, (char *)"application/json", 0); + char *str = (char *)wifiJsonList.c_str(); + webserver_send_content(client, str, strlen(str)); } + //initatie a new async scan for next try + WiFi.scanNetworksAsync(getWifiScanResults); + return 0; } -void handleSmartcontrol(ESP8266WebServer *httpServer, settingsStruct *heishamonSettings, String actData[]) { - httpServer->setContentLength(CONTENT_LENGTH_UNKNOWN); - httpServer->send(200, "text/html", ""); - httpServer->sendContent_P(webHeader); - httpServer->sendContent_P(webCSS); - httpServer->sendContent_P(webBodyStart); - httpServer->sendContent_P(webBodySmartcontrol1); - httpServer->sendContent_P(webBodySmartcontrol2); - - String httptext = F("
"); - httpServer->sendContent(httptext); - httpServer->sendContent_P(webBodyEndDiv); - - //Heating curve - httpServer->sendContent_P(webBodySmartcontrolHeatingcurve1); - - //check if POST was made with save settings, if yes then save and reboot - if (httpServer->args()) { - DynamicJsonDocument jsonDoc(1024); - //set jsonDoc with current settings - if ( heishamonSettings->SmartControlSettings.enableHeatCurve) { - jsonDoc["enableHeatCurve"] = "enabled"; - } else { - jsonDoc["enableHeatCurve"] = "disabled"; - } - jsonDoc["avgHourHeatCurve"] = heishamonSettings->SmartControlSettings.avgHourHeatCurve; - jsonDoc["heatCurveTargetHigh"] = heishamonSettings->SmartControlSettings.heatCurveTargetHigh; - jsonDoc["heatCurveTargetLow"] = heishamonSettings->SmartControlSettings.heatCurveTargetLow; - jsonDoc["heatCurveOutHigh"] = heishamonSettings->SmartControlSettings.heatCurveOutHigh; - jsonDoc["heatCurveOutLow"] = heishamonSettings->SmartControlSettings.heatCurveOutLow; - for (unsigned int i = 0 ; i < 36 ; i++) { - jsonDoc["heatCurveLookup"][i] = heishamonSettings->SmartControlSettings.heatCurveLookup[i]; - } +int handleDebug(struct webserver_t *client, char *hex, byte hex_len) { + if(client->content == 0) { + webserver_send(client, 200, (char *)"text/plain", 0); + char log_msg[254]; - //then overwrite with new settings - if (httpServer->hasArg("heatingcurve")) { - jsonDoc["enableHeatCurve"] = "enabled"; - } else { - jsonDoc["enableHeatCurve"] = "disabled"; - } - if (httpServer->hasArg("average-time")) { - jsonDoc["avgHourHeatCurve"] = httpServer->arg("average-time"); - } - if (httpServer->hasArg("hcth")) { - jsonDoc["heatCurveTargetHigh"] = httpServer->arg("hcth"); - } - if (httpServer->hasArg("hctl")) { - jsonDoc["heatCurveTargetLow"] = httpServer->arg("hctl"); - } - if (httpServer->hasArg("hcoh")) { - jsonDoc["heatCurveOutHigh"] = httpServer->arg("hcoh"); - } - if (httpServer->hasArg("hcol")) { - jsonDoc["heatCurveOutLow"] = httpServer->arg("hcol"); - } - if (httpServer->hasArg("lookup0")) { - jsonDoc["heatCurveLookup"][0] = httpServer->arg("lookup0"); - } - if (httpServer->hasArg("lookup1")) { - jsonDoc["heatCurveLookup"][1] = httpServer->arg("lookup1"); - } - if (httpServer->hasArg("lookup2")) { - jsonDoc["heatCurveLookup"][2] = httpServer->arg("lookup2"); - } - if (httpServer->hasArg("lookup3")) { - jsonDoc["heatCurveLookup"][3] = httpServer->arg("lookup3"); - } - if (httpServer->hasArg("lookup4")) { - jsonDoc["heatCurveLookup"][4] = httpServer->arg("lookup4"); - } - if (httpServer->hasArg("lookup5")) { - jsonDoc["heatCurveLookup"][5] = httpServer->arg("lookup5"); - } - if (httpServer->hasArg("lookup6")) { - jsonDoc["heatCurveLookup"][6] = httpServer->arg("lookup6"); - } - if (httpServer->hasArg("lookup7")) { - jsonDoc["heatCurveLookup"][7] = httpServer->arg("lookup7"); - } - if (httpServer->hasArg("lookup8")) { - jsonDoc["heatCurveLookup"][8] = httpServer->arg("lookup8"); - } - if (httpServer->hasArg("lookup9")) { - jsonDoc["heatCurveLookup"][9] = httpServer->arg("lookup9"); - } - if (httpServer->hasArg("lookup10")) { - jsonDoc["heatCurveLookup"][10] = httpServer->arg("lookup10"); - } - if (httpServer->hasArg("lookup11")) { - jsonDoc["heatCurveLookup"][11] = httpServer->arg("lookup11"); - } - if (httpServer->hasArg("lookup12")) { - jsonDoc["heatCurveLookup"][12] = httpServer->arg("lookup12"); - } - if (httpServer->hasArg("lookup13")) { - jsonDoc["heatCurveLookup"][13] = httpServer->arg("lookup13"); - } - if (httpServer->hasArg("lookup14")) { - jsonDoc["heatCurveLookup"][14] = httpServer->arg("lookup14"); - } - if (httpServer->hasArg("lookup15")) { - jsonDoc["heatCurveLookup"][15] = httpServer->arg("lookup15"); - } - if (httpServer->hasArg("lookup16")) { - jsonDoc["heatCurveLookup"][16] = httpServer->arg("lookup16"); - } - if (httpServer->hasArg("lookup17")) { - jsonDoc["heatCurveLookup"][17] = httpServer->arg("lookup17"); - } - if (httpServer->hasArg("lookup18")) { - jsonDoc["heatCurveLookup"][18] = httpServer->arg("lookup18"); - } - if (httpServer->hasArg("lookup19")) { - jsonDoc["heatCurveLookup"][19] = httpServer->arg("lookup19"); - } - if (httpServer->hasArg("lookup20")) { - jsonDoc["heatCurveLookup"][20] = httpServer->arg("lookup20"); - } - if (httpServer->hasArg("lookup21")) { - jsonDoc["heatCurveLookup"][21] = httpServer->arg("lookup21"); - } - if (httpServer->hasArg("lookup22")) { - jsonDoc["heatCurveLookup"][22] = httpServer->arg("lookup22"); - } - if (httpServer->hasArg("lookup23")) { - jsonDoc["heatCurveLookup"][23] = httpServer->arg("lookup23"); - } - if (httpServer->hasArg("lookup24")) { - jsonDoc["heatCurveLookup"][24] = httpServer->arg("lookup24"); - } - if (httpServer->hasArg("lookup25")) { - jsonDoc["heatCurveLookup"][25] = httpServer->arg("lookup25"); - } - if (httpServer->hasArg("lookup26")) { - jsonDoc["heatCurveLookup"][26] = httpServer->arg("lookup26"); - } - if (httpServer->hasArg("lookup27")) { - jsonDoc["heatCurveLookup"][27] = httpServer->arg("lookup27"); - } - if (httpServer->hasArg("lookup28")) { - jsonDoc["heatCurveLookup"][28] = httpServer->arg("lookup28"); - } - if (httpServer->hasArg("lookup29")) { - jsonDoc["heatCurveLookup"][29] = httpServer->arg("lookup29"); - } - if (httpServer->hasArg("lookup30")) { - jsonDoc["heatCurveLookup"][30] = httpServer->arg("lookup30"); - } - if (httpServer->hasArg("lookup31")) { - jsonDoc["heatCurveLookup"][31] = httpServer->arg("lookup31"); - } - if (httpServer->hasArg("lookup32")) { - jsonDoc["heatCurveLookup"][32] = httpServer->arg("lookup32"); - } - if (httpServer->hasArg("lookup33")) { - jsonDoc["heatCurveLookup"][33] = httpServer->arg("lookup33"); - } - if (httpServer->hasArg("lookup34")) { - jsonDoc["heatCurveLookup"][34] = httpServer->arg("lookup34"); - } - if (httpServer->hasArg("lookup35")) { - jsonDoc["heatCurveLookup"][35] = httpServer->arg("lookup35"); - } - if (LittleFS.begin()) { - File configFile = LittleFS.open("/heatcurve.json", "w"); - if (configFile) { - serializeJson(jsonDoc, configFile); - configFile.close(); - delay(1000); - - httpServer->sendContent_P(webBodySettingsSaveMessage); - httpServer->sendContent_P(refreshMeta); - httpServer->sendContent_P(webFooter); - httpServer->sendContent(""); - httpServer->client().stop(); - delay(1000); - ESP.restart(); + #define LOGHEXBYTESPERLINE 32 + for (int i = 0; i < hex_len; i += LOGHEXBYTESPERLINE) { + char buffer [(LOGHEXBYTESPERLINE * 3) + 1]; + buffer[LOGHEXBYTESPERLINE * 3] = '\0'; + for (int j = 0; ((j < LOGHEXBYTESPERLINE) && ((i + j) < hex_len)); j++) { + sprintf(&buffer[3 * j], PSTR("%02X "), hex[i + j]); } + uint8_t len = sprintf_P(log_msg, PSTR("data: %s\n"), buffer); + webserver_send_content(client, log_msg, len); } } + return 0; +} - int heatingMode = actData[76].toInt(); - if (heatingMode == 1) { - httptext = F("
"); - if (heishamonSettings->SmartControlSettings.enableHeatCurve == true) { - httptext = httptext + F(""); - } else { - httptext = httptext + F(""); - } - httptext = httptext + F("

"); - httptext = httptext + F("SmartControlSettings.heatCurveTargetHigh + F("\" min=\"20\" max=\"60\" required>"); - httptext = httptext + F("
"); - httptext = httptext + F("SmartControlSettings.heatCurveTargetLow + F("\" min=\"20\" max=\"60\" required>"); - httptext = httptext + F("
"); - httptext = httptext + F("SmartControlSettings.heatCurveOutHigh + F("\" min=\"-20\" max=\"15\" required>"); - httptext = httptext + F("
"); - httptext = httptext + F("SmartControlSettings.heatCurveOutLow + F("\" min=\"-20\" max=\"15\" required>"); - httptext = httptext + F("


"); - httptext = httptext + F(""); - httptext = httptext + F("
"); - httptext = httptext + F("

"); - httptext = httptext + getAvgOutsideTemp(); - httptext = httptext + F("

"); - httpServer->sendContent(httptext); - httpServer->sendContent_P(webBodyEndDiv); - - httpServer->sendContent_P(webBodySmartcontrolHeatingcurveSVG); - httpServer->sendContent_P(webBodySmartcontrolHeatingcurve2); - httpServer->sendContent_P(webBodyEndDiv); - } else { - httptext = F("Heating mode must be \"direct heating\" to enable this option"); - httpServer->sendContent(httptext); - httpServer->sendContent_P(webBodyEndDiv); +void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) { + switch (type) { + case WStype_DISCONNECTED: + break; + case WStype_CONNECTED: { + } break; + case WStype_TEXT: + break; + case WStype_BIN: + break; + case WStype_PONG: { + } break; + default: + break; } +} - httpServer->sendContent_P(webBodyEndDiv); - - //Other example - // httpServer->sendContent_P(webBodySmartcontrolOtherexample); - // httptext = "...Loading..."; - // httptext = httptext + ""; - // httpServer->sendContent(httptext); - // httpServer->sendContent_P(webBodyEndDiv); - - httptext = ""; - httpServer->sendContent(httptext); - httpServer->sendContent_P(webBodyEndDiv); - - httpServer->sendContent_P(menuJS); - httpServer->sendContent_P(selectJS); - // httpServer->sendContent_P(heatingCurveJS); - httpServer->sendContent_P(webFooter); - httpServer->sendContent(""); - httpServer->client().stop(); +int handleRoot(struct webserver_t *client, float readpercentage, int mqttReconnects, settingsStruct *heishamonSettings) { + switch(client->content) { + case 0: { + webserver_send(client, 200, (char *)"text/html", 0); + webserver_send_content_P(client, webHeader, strlen_P(webHeader)); + webserver_send_content_P(client, webCSS, strlen_P(webCSS)); + webserver_send_content_P(client, webBodyStart, strlen_P(webBodyStart)); + webserver_send_content_P(client, webBodyRoot1, strlen_P(webBodyRoot1)); + } break; + case 1: { + webserver_send_content_P(client, heishamon_version, strlen_P(heishamon_version)); + webserver_send_content_P(client, webBodyRoot2, strlen_P(webBodyRoot2)); + if(heishamonSettings->use_1wire) { + webserver_send_content_P(client, webBodyRootDallasTab, strlen_P(webBodyRootDallasTab)); + } + if(heishamonSettings->use_s0) { + webserver_send_content_P(client, webBodyRootS0Tab, strlen_P(webBodyRootS0Tab)); + } + webserver_send_content_P(client, webBodyRootConsoleTab, strlen_P(webBodyRootConsoleTab)); + } break; + case 2: { + webserver_send_content_P(client, webBodyEndDiv, strlen_P(webBodyEndDiv)); + webserver_send_content_P(client, webBodyRootStatusWifi, strlen_P(webBodyRootStatusWifi)); + char str[200]; + itoa(getWifiQuality(), str, 10); + webserver_send_content(client, (char *)str, strlen(str)); + webserver_send_content_P(client, webBodyRootStatusMemory, strlen_P(webBodyRootStatusMemory)); + } break; + case 3: { + char str[200]; + itoa(getFreeMemory(), str, 10); + webserver_send_content(client, (char *)str, strlen(str)); + webserver_send_content_P(client, webBodyRootStatusReceived, strlen_P(webBodyRootStatusReceived)); + str[200]; + itoa(readpercentage, str, 10); + webserver_send_content(client, (char *)str, strlen(str)); + } break; + case 4: { + webserver_send_content_P(client, webBodyRootStatusReconnects, strlen_P(webBodyRootStatusReconnects)); + char str[200]; + itoa(mqttReconnects, str, 10); + webserver_send_content(client, (char *)str, strlen(str)); + webserver_send_content_P(client, webBodyRootStatusUptime, strlen_P(webBodyRootStatusUptime)); + char *up = getUptime(); + webserver_send_content(client, up, strlen(up)); + free(up); + } break; + case 5: { + webserver_send_content_P(client, webBodyEndDiv, strlen_P(webBodyEndDiv)); + webserver_send_content_P(client, webBodyRootHeatpumpValues, strlen_P(webBodyRootHeatpumpValues)); + if(heishamonSettings->use_1wire) { + webserver_send_content_P(client, webBodyRootDallasValues, strlen_P(webBodyRootDallasValues)); + } + if(heishamonSettings->use_s0) { + webserver_send_content_P(client, webBodyRootS0Values, strlen_P(webBodyRootS0Values)); + } + webserver_send_content_P(client, webBodyRootConsole, strlen_P(webBodyRootConsole)); + webserver_send_content_P(client, menuJS, strlen_P(menuJS)); + } break; + case 6: { + webserver_send_content_P(client, refreshJS, strlen_P(refreshJS)); + webserver_send_content_P(client, selectJS, strlen_P(selectJS)); + webserver_send_content_P(client, websocketJS, strlen_P(websocketJS)); + webserver_send_content_P(client, webFooter, strlen_P(webFooter)); + } break; + } + return 0; } -void handleWifiScan(ESP8266WebServer *httpServer) { - httpServer->setContentLength(CONTENT_LENGTH_UNKNOWN); - httpServer->sendHeader("Access-Control-Allow-Origin", "*"); - httpServer->send(200, "application/json", ""); +int handleTableRefresh(struct webserver_t *client, String actData[]) { + int ret = 0; + + if(client->route == 11) { + if(client->content == 0) { + webserver_send(client, 200, (char *)"text/html", 0); + dallasTableOutput(client); + } + } else if(client->route == 12) { + if(client->content == 0) { + webserver_send(client, 200, (char *)"text/html", 0); + s0TableOutput(client); + } + } else if(client->route == 10) { + if(client->content == 0) { + webserver_send(client, 200, (char *)"text/html", 0); + } + if(client->content < NUMBER_OF_TOPICS) { + for(uint8_t topic = client->content; topic < NUMBER_OF_TOPICS && topic < client->content + 4; topic++) { + String topicdesc; + const char *valuetext = "value"; + if(strcmp_P(valuetext, topicDescription[topic][0]) == 0) { + topicdesc = topicDescription[topic][1]; + } else { + int value = actData[topic].toInt(); + int maxvalue = atoi(topicDescription[topic][0]); + if ((value < 0) || (value > maxvalue)) { + topicdesc = _unknown; + } + else { + topicdesc = topicDescription[topic][value + 1]; //plus one, because 0 is the maxvalue container + } + } - if (numSsid > 0) { //found wifi networks - String httptext = "["; - int indexes[numSsid]; - for (int i = 0; i < numSsid; i++) { //fill the sorted list with normal indexes first - indexes[i] = i; - } - for (int i = 0; i < numSsid; i++) { //then sort - for (int j = i + 1; j < numSsid; j++) { - if (WiFi.RSSI(indexes[j]) > WiFi.RSSI(indexes[i])) { - int temp = indexes[j]; - indexes[j] = indexes[i]; - indexes[i] = temp; + webserver_send_content_P(client, PSTR("TOP"), 11); + + char str[12]; + itoa(topic, str, 10); + webserver_send_content(client, str, strlen(str)); + + webserver_send_content_P(client, PSTR(""), 9); + + String t = topics[topic]; + char *tmp = (char *)t.c_str(); + webserver_send_content(client, tmp, strlen(tmp)); + + webserver_send_content_P(client, PSTR(""), 9); + + { + char *str = (char *)actData[topic].c_str(); + webserver_send_content(client, str, strlen(str)); + } + + webserver_send_content_P(client, PSTR(""), 9); + + { + char *str = (char *)topicdesc.c_str(); + webserver_send_content(client, str, strlen(str)); } + + webserver_send_content_P(client, PSTR(""), 10); } + // The webserver also increases by 1 + client->content += 3; } - String ssid; - for (int i = 0; i < numSsid; i++) { //then remove duplicates - if (indexes[i] == -1) continue; - ssid = WiFi.SSID(indexes[i]); - for (int j = i + 1; j < numSsid; j++) { - if (ssid == WiFi.SSID(indexes[j])) { - indexes[j] = -1; + } + return 0; +} + +int handleJsonOutput(struct webserver_t *client, String actData[]) { + if(client->content == 0) { + webserver_send(client, 200, (char *)"application/json", 0); + webserver_send_content_P(client, PSTR("{\"heatpump\":["), 13); + } else if(client->content < NUMBER_OF_TOPICS) { + for(uint8_t topic = client->content; topic < NUMBER_OF_TOPICS && topic < client->content + 4; topic++) { + PGM_P topicdesc; + const char *valuetext = "value"; + if(strcmp_P(valuetext, topicDescription[topic][0]) == 0) { + topicdesc = topicDescription[topic][1]; + } else { + int value = actData[topic].toInt(); + int maxvalue = atoi(topicDescription[topic][0]); + if ((value < 0) || (value > maxvalue)) { + topicdesc = _unknown; + } else { + topicdesc = topicDescription[topic][value + 1]; //plus one, because 0 is the maxvalue container } } - } - bool firstSSID = true; - for (int i = 0; i < numSsid; i++) { //then output json - if (indexes[i] == -1) continue; - if (!firstSSID) { - httptext = httptext + ","; + + webserver_send_content_P(client, PSTR("{\"Topic\":\"TOP"), 13); + + { + char str[12]; + itoa(topic, str, 10); + webserver_send_content(client, str, strlen(str)); } - httptext = httptext + "{\"ssid\":\"" + WiFi.SSID(indexes[i]) + "\", \"rssi\": \"" + dBmToQuality(WiFi.RSSI(indexes[i])) + "%\"}"; - firstSSID = false; - } - httptext = httptext + "]"; - httpServer->sendContent(httptext); - } - httpServer->sendContent(""); - httpServer->client().stop(); - //initatie a new async scan for next try - WiFi.scanNetworksAsync(getWifiScanResults); -} + webserver_send_content_P(client, PSTR("\",\"Name\":\""), 10); + webserver_send_content_P(client, topics[topic], strlen_P(topics[topic])); -bool send_command(byte* command, int length); + webserver_send_content_P(client, PSTR("\",\"Value\":\""), 11); -void handleREST(ESP8266WebServer *httpServer, bool optionalPCB) { + { + char *str = (char *)actData[topic].c_str(); + webserver_send_content_P(client, str, strlen(str)); + } - httpServer->setContentLength(CONTENT_LENGTH_UNKNOWN); - httpServer->sendHeader("Access-Control-Allow-Origin", "*"); - httpServer->send(200, "text/plain", ""); + webserver_send_content_P(client, PSTR("\",\"Description\":\""), 17); - String httptext = ""; - if (httpServer->method() == HTTP_GET) { - for (uint8_t i = 0; i < httpServer->args(); i++) { - unsigned char cmd[256] = { 0 }; - char log_msg[256] = { 0 }; - unsigned int len = 0; + webserver_send_content_P(client, topicdesc, strlen_P(topicdesc)); - for (uint8_t x = 0; x < sizeof(commands) / sizeof(commands[0]); x++) { - if (strcmp(httpServer->argName(i).c_str(), commands[x].name) == 0) { - len = commands[x].func((char *)httpServer->arg(i).c_str(), cmd, log_msg); - httptext = httptext + log_msg + "\n"; - log_message(log_msg); - send_command(cmd, len); - } + webserver_send_content_P(client, PSTR("\"}"), 2); + + if(topic < NUMBER_OF_TOPICS - 1) { + webserver_send_content_P(client, PSTR(","), 1); } } - if (optionalPCB) { - //optional commands - for (uint8_t i = 0; i < httpServer->args(); i++) { - unsigned char cmd[256] = { 0 }; - char log_msg[256] = { 0 }; - unsigned int len = 0; - - for (uint8_t x = 0; x < sizeof(optionalCommands) / sizeof(optionalCommands[0]); x++) { - if (strcmp(httpServer->argName(i).c_str(), optionalCommands[x].name) == 0) { - len = optionalCommands[x].func((char *)httpServer->arg(i).c_str(), log_msg); - httptext = httptext + log_msg + "\n"; - log_message(log_msg); - } - } - } + // The webserver also increases by 1 + client->content += 3; + if(client->content > NUMBER_OF_TOPICS) { + client->content = NUMBER_OF_TOPICS; } + } else if(client->content == NUMBER_OF_TOPICS+1) { + webserver_send_content_P(client, PSTR("],\"1wire\":"), 10); + + dallasJsonOutput(client); + } else if(client->content == NUMBER_OF_TOPICS+2) { + webserver_send_content_P(client, PSTR(",\"s0\":"), 6); + + s0JsonOutput(client); + + webserver_send_content_P(client, PSTR("}"), 1); } + return 0; +} - httpServer->sendContent(httptext); - httpServer->sendContent(""); - httpServer->client().stop(); +int showFirmware(struct webserver_t *client) { + if(client->content == 0) { + webserver_send(client, 200, (char *)"text/html", 0); + webserver_send_content_P(client, webHeader, strlen_P(webHeader)); + webserver_send_content_P(client, webCSS, strlen_P(webCSS)); + webserver_send_content_P(client, webBodyStart, strlen_P(webBodyStart)); + webserver_send_content_P(client, showFirmwarePage, strlen_P(showFirmwarePage)); + webserver_send_content_P(client, menuJS, strlen_P(menuJS)); + webserver_send_content_P(client, webFooter, strlen_P(webFooter)); + } + return 0; } -void handleDebug(ESP8266WebServer *httpServer, char *hex, byte hex_len) { - httpServer->setContentLength(CONTENT_LENGTH_UNKNOWN); - httpServer->sendHeader("Access-Control-Allow-Origin", "*"); - httpServer->send(200, "text/plain", ""); - char log_msg[256]; - -#define LOGHEXBYTESPERLINE 32 - for (int i = 0; i < hex_len; i += LOGHEXBYTESPERLINE) { - char buffer [(LOGHEXBYTESPERLINE * 3) + 1]; - buffer[LOGHEXBYTESPERLINE * 3] = '\0'; - for (int j = 0; ((j < LOGHEXBYTESPERLINE) && ((i + j) < hex_len)); j++) { - sprintf(&buffer[3 * j], PSTR("%02X "), hex[i + j]); - } - sprintf_P(log_msg, PSTR("data: %s"), buffer ); httpServer->sendContent(log_msg); httpServer->sendContent("\n"); +int showFirmwareSuccess(struct webserver_t *client) { + if(client->content == 0) { + webserver_send(client, 200, (char *)"text/html", strlen_P(firmwareSuccessResponse)); + webserver_send_content_P(client, firmwareSuccessResponse, strlen_P(firmwareSuccessResponse)); } + return 0; +} - httpServer->sendContent(""); - httpServer->client().stop(); +static void printUpdateError(char **out, uint8_t size){ + uint8_t len = 0; + len = snprintf_P(*out, size, PSTR("
ERROR[%u]: "), Update.getError()); + if(Update.getError() == UPDATE_ERROR_OK){ + snprintf_P(&(*out)[len], size - len, PSTR("No Error")); + } else if(Update.getError() == UPDATE_ERROR_WRITE){ + snprintf_P(&(*out)[len], size - len, PSTR("Flash Write Failed")); + } else if(Update.getError() == UPDATE_ERROR_ERASE){ + snprintf_P(&(*out)[len], size - len, PSTR("Flash Erase Failed")); + } else if(Update.getError() == UPDATE_ERROR_READ){ + snprintf_P(&(*out)[len], size - len, PSTR("Flash Read Failed")); + } else if(Update.getError() == UPDATE_ERROR_SPACE){ + snprintf_P(&(*out)[len], size - len, PSTR("Not Enough Space")); + } else if(Update.getError() == UPDATE_ERROR_SIZE){ + snprintf_P(&(*out)[len], size - len, PSTR("Bad Size Given")); + } else if(Update.getError() == UPDATE_ERROR_STREAM){ + snprintf_P(&(*out)[len], size - len, PSTR("Stream Read Timeout")); +#ifdef UPDATE_ERROR_NO_DATA + } else if(Update.getError() == UPDATE_ERROR_NO_DATA){ + snprintf_P(&(*out)[len], size - len, PSTR("No data supplied")); +#endif + } else if(Update.getError() == UPDATE_ERROR_MD5){ + snprintf_P(&(*out)[len], size - len,PSTR("MD5 Failed\n")); + } else if(Update.getError() == UPDATE_ERROR_SIGN){ + snprintf_P(&(*out)[len], size - len, PSTR("Signature verification failed")); + } else if(Update.getError() == UPDATE_ERROR_FLASH_CONFIG){ + snprintf_P(&(*out)[len], size - len, PSTR("Flash config wrong real: %d IDE: %d\n"), ESP.getFlashChipRealSize(), ESP.getFlashChipSize()); + } else if(Update.getError() == UPDATE_ERROR_NEW_FLASH_CONFIG){ + snprintf_P(&(*out)[len], size - len, PSTR("new Flash config wrong real: %d\n"), ESP.getFlashChipRealSize()); + } else if(Update.getError() == UPDATE_ERROR_MAGIC_BYTE){ + snprintf_P(&(*out)[len], size - len, PSTR("Magic byte is wrong, not 0xE9")); + } else if (Update.getError() == UPDATE_ERROR_BOOTSTRAP){ + snprintf_P(&(*out)[len], size - len, PSTR("Invalid bootstrapping state, reset ESP8266 before updating")); + } else { + snprintf_P(&(*out)[len], size - len, PSTR("UNKNOWN")); + } } -void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) { - switch (type) { - case WStype_DISCONNECTED: - break; - case WStype_CONNECTED: { - } break; - case WStype_TEXT: - break; - case WStype_BIN: - break; - case WStype_PONG: { - } break; - default: - break; + +int showFirmwareFail(struct webserver_t *client) { + if(client->content == 0) { + char str[256] = { '\0' }, *p = str; + printUpdateError(&p, sizeof(str)); + + webserver_send(client, 200, (char *)"text/html", strlen_P(firmwareFailResponse)+strlen(str)); + webserver_send_content_P(client, firmwareFailResponse, strlen_P(firmwareFailResponse)); + webserver_send_content(client, str, strlen(str)); } + return 0; } diff --git a/HeishaMon/webfunctions.h b/HeishaMon/webfunctions.h old mode 100644 new mode 100755 index 134c1a83..a5e1325c --- a/HeishaMon/webfunctions.h +++ b/HeishaMon/webfunctions.h @@ -1,14 +1,17 @@ +#define LWIP_INTERNAL + #include -#include #include #include #include #include #include +#include "src/common/webserver.h" #include "dallas.h" #include "s0.h" #include "gpio.h" -#include "smartcontrol.h" + +void log_message(char* string); static IPAddress apIP(192, 168, 4, 1); @@ -41,26 +44,35 @@ struct settingsStruct { s0SettingsStruct s0Settings[NUM_S0_COUNTERS]; gpioSettingsStruct gpioSettings; - SmartControlSettingsStruct SmartControlSettings; }; +void setupConditionals(); int getFreeMemory(void); -String getUptime(void); +char *getUptime(void); void setupWifi(settingsStruct *heishamonSettings); int getWifiQuality(void); int getFreeMemory(void); -void handleRoot(ESP8266WebServer *httpServer, float readpercentage, int mqttReconnects, settingsStruct *heishamonSettings); -void handleTableRefresh(ESP8266WebServer *httpServer, String actData[]); -void handleJsonOutput(ESP8266WebServer *httpServer, String actData[]); -void handleFactoryReset(ESP8266WebServer *httpServer); -void handleReboot(ESP8266WebServer *httpServer); -void handleDebug(ESP8266WebServer *httpServer, char *hex, byte hex_len); +void log_message(char *string); +int8_t webserver_cb(struct webserver_t *client, void *data); +void getWifiScanResults(int numSsid); +int handleRoot(struct webserver_t *client, float readpercentage, int mqttReconnects, settingsStruct *heishamonSettings); +int handleTableRefresh(struct webserver_t *client, String actData[]); +int handleJsonOutput(struct webserver_t *client, String actData[]); +int handleFactoryReset(struct webserver_t *client); +int handleReboot(struct webserver_t *client); +int handleDebug(struct webserver_t *client, char *hex, byte hex_len); void settingsToJson(DynamicJsonDocument &jsonDoc, settingsStruct *heishamonSettings); void saveJsonToConfig(DynamicJsonDocument &jsonDoc); void loadSettings(settingsStruct *heishamonSettings); -bool handleSettings(ESP8266WebServer *httpServer, settingsStruct *heishamonSettings); -void handleWifiScan(ESP8266WebServer *httpServer); -void handleSmartcontrol(ESP8266WebServer *httpServer, settingsStruct *heishamonSettings, String actData[]); -void handleREST(ESP8266WebServer *httpServer, bool optionalPCB); +int getSettings(struct webserver_t *client, settingsStruct *heishamonSettings); +int handleSettings(struct webserver_t *client); +int saveSettings(struct webserver_t *client, settingsStruct *heishamonSettings); +int settingsReconnectWifi(struct webserver_t *client, settingsStruct *heishamonSettings); +int settingsNewPassword(struct webserver_t *client, settingsStruct *heishamonSettings); +int cacheSettings(struct webserver_t *client, struct arguments_t * args); +int handleWifiScan(struct webserver_t *client); void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length); +int showFirmware(struct webserver_t *client); +int showFirmwareSuccess(struct webserver_t *client); +int showFirmwareFail(struct webserver_t *client); diff --git a/binaries/HeishaMon-PR76.bin b/binaries/HeishaMon-PR76.bin new file mode 100644 index 0000000000000000000000000000000000000000..d1a47ecf138689a015b62b1d2e8a2262b6069e02 GIT binary patch literal 486320 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 Sk$T?^8=FCM@b|3F6Vi7#@X7u|yZ0&L`vwH> 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 literal 0 HcmV?d00001 From c9c4c76c9af26b51465190ac50a7728961de7e79 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Sun, 5 Dec 2021 08:23:58 +0100 Subject: [PATCH 33/75] indenting arduino IDE style --- HeishaMon/HeishaMon.ino | 496 ++++++------- HeishaMon/dallas.cpp | 8 +- HeishaMon/htmlcode.h | 520 ++++++------- HeishaMon/s0.cpp | 32 +- HeishaMon/version.h | 2 +- HeishaMon/webfunctions.cpp | 694 +++++++++--------- ...R76.bin => HeishaMon.ino.d1-v2.2-iy-1.bin} | Bin 486320 -> 486416 bytes 7 files changed, 877 insertions(+), 875 deletions(-) rename binaries/{HeishaMon-PR76.bin => HeishaMon.ino.d1-v2.2-iy-1.bin} (57%) diff --git a/HeishaMon/HeishaMon.ino b/HeishaMon/HeishaMon.ino index bf93acf4..b115c146 100755 --- a/HeishaMon/HeishaMon.ino +++ b/HeishaMon/HeishaMon.ino @@ -103,28 +103,28 @@ struct timerqueue_t **timerqueue = NULL; int timerqueue_size = 0; /* - * check_wifi will process wifi reconnecting managing - */ + check_wifi will process wifi reconnecting managing +*/ void check_wifi() { if ((WiFi.status() != WL_CONNECTED) || (!WiFi.localIP())) { /* - * if we are not connected to an AP - * we must be in softAP so respond to DNS - */ + if we are not connected to an AP + we must be in softAP so respond to DNS + */ dnsServer.processNextRequest(); /* we need to stop reconnecting to a configured wifi network if there is a hotspot user connected - * also, do not disconnect if wifi network scan is active - */ + also, do not disconnect if wifi network scan is active + */ if ((heishamonSettings.wifi_ssid[0] != '\0') && (WiFi.status() != WL_DISCONNECTED) && (WiFi.scanComplete() != -1) && (WiFi.softAPgetStationNum() > 0)) { log_message((char *)"WiFi lost, but softAP station connecting, so stop trying to connect to configured ssid..."); WiFi.disconnect(true); } /* only start this routine if timeout on - * reconnecting to AP and SSID is set - */ + reconnecting to AP and SSID is set + */ if ((heishamonSettings.wifi_ssid[0] != '\0') && ((unsigned long)(millis() - lastWifiRetryTimer) > WIFIRETRYTIMER ) ) { lastWifiRetryTimer = millis(); if (WiFi.softAPSSID() == "") { @@ -437,254 +437,254 @@ void setupOTA() { } int8_t webserver_cb(struct webserver_t *client, void *dat) { - switch(client->step) { + switch (client->step) { case WEBSERVER_CLIENT_REQUEST_METHOD: { - if(strcmp((char *)dat, "POST") == 0) { - client->route = 110; - } - return 0; - } break; - case WEBSERVER_CLIENT_REQUEST_URI: { - if(strcmp((char *)dat, "/") == 0) { - client->route = 1; - } else if(strcmp((char *)dat, "/tablerefresh") == 0) { - client->route = 10; - } else if(strcmp((char *)dat, "/json") == 0) { - client->route = 20; - } else if(strcmp((char *)dat, "/reboot") == 0) { - client->route = 30; - } else if(strcmp((char *)dat, "/debug") == 0) { - client->route = 40; - log_message((char*)"Debug URL requested"); - } else if(strcmp((char *)dat, "/wifiscan") == 0) { - client->route = 50; - } else if(strcmp((char *)dat, "/togglelog") == 0) { - client->route = 1; - log_message((char*)"Toggled mqtt log flag"); - heishamonSettings.logMqtt ^= true; - } else if(strcmp((char *)dat, "/togglehexdump") == 0) { - client->route = 1; - log_message((char*)"Toggled hexdump log flag"); - heishamonSettings.logHexdump ^= true; - } else if(strcmp((char *)dat, "/hotspot-detect.html") == 0 || - strcmp((char *)dat, "/fwlink") == 0 || - strcmp((char *)dat, "/generate_204") == 0 || - strcmp((char *)dat, "/gen_204") == 0 || - strcmp((char *)dat, "/popup") == 0) { - client->route = 80; - } else if(strcmp((char *)dat, "/factoryreset") == 0) { - client->route = 90; - } else if(strcmp((char *)dat, "/command") == 0) { - RESTmsg.clear(); - client->route = 100; - } else if(client->route == 110) { - // Only accept settings POST requests - if(strcmp((char *)dat, "/savesettings") == 0) { + if (strcmp((char *)dat, "POST") == 0) { client->route = 110; - } else if(strcmp((char *)dat, "/firmware") == 0) { - client->route = 150; - - Update.runAsync(true); - if(!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)){ - Update.printError(Serial1); + } + return 0; + } break; + case WEBSERVER_CLIENT_REQUEST_URI: { + if (strcmp((char *)dat, "/") == 0) { + client->route = 1; + } else if (strcmp((char *)dat, "/tablerefresh") == 0) { + client->route = 10; + } else if (strcmp((char *)dat, "/json") == 0) { + client->route = 20; + } else if (strcmp((char *)dat, "/reboot") == 0) { + client->route = 30; + } else if (strcmp((char *)dat, "/debug") == 0) { + client->route = 40; + log_message((char*)"Debug URL requested"); + } else if (strcmp((char *)dat, "/wifiscan") == 0) { + client->route = 50; + } else if (strcmp((char *)dat, "/togglelog") == 0) { + client->route = 1; + log_message((char*)"Toggled mqtt log flag"); + heishamonSettings.logMqtt ^= true; + } else if (strcmp((char *)dat, "/togglehexdump") == 0) { + client->route = 1; + log_message((char*)"Toggled hexdump log flag"); + heishamonSettings.logHexdump ^= true; + } else if (strcmp((char *)dat, "/hotspot-detect.html") == 0 || + strcmp((char *)dat, "/fwlink") == 0 || + strcmp((char *)dat, "/generate_204") == 0 || + strcmp((char *)dat, "/gen_204") == 0 || + strcmp((char *)dat, "/popup") == 0) { + client->route = 80; + } else if (strcmp((char *)dat, "/factoryreset") == 0) { + client->route = 90; + } else if (strcmp((char *)dat, "/command") == 0) { + RESTmsg.clear(); + client->route = 100; + } else if (client->route == 110) { + // Only accept settings POST requests + if (strcmp((char *)dat, "/savesettings") == 0) { + client->route = 110; + } else if (strcmp((char *)dat, "/firmware") == 0) { + client->route = 150; + + Update.runAsync(true); + if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { + Update.printError(Serial1); + } + } else { + return -1; } + } else if (strcmp((char *)dat, "/settings") == 0) { + client->route = 120; + } else if (strcmp((char *)dat, "/getsettings") == 0) { + client->route = 130; + } else if (strcmp((char *)dat, "/firmware") == 0) { + client->route = 140; } else { - return -1; + client->route = 0; } - } else if(strcmp((char *)dat, "/settings") == 0) { - client->route = 120; - } else if(strcmp((char *)dat, "/getsettings") == 0) { - client->route = 130; - } else if(strcmp((char *)dat, "/firmware") == 0) { - client->route = 140; - } else { - client->route = 0; - } - return 0; - } break; + return 0; + } break; case WEBSERVER_CLIENT_ARGS: { - struct arguments_t *args = (struct arguments_t *)dat; - switch(client->route) { - case 10: { - if(strcmp((char *)args->name, "1wire") == 0) { - client->route = 11; - } else if(strcmp((char *)args->name, "s0") == 0) { - client->route = 12; - } - } break; - case 100: { - unsigned char cmd[256] = { 0 }; - char cpy[args->len+1]; - char log_msg[256] = { 0 }; - unsigned int len = 0; - - memset(&cpy, 0, args->len+1); - snprintf((char *)&cpy, args->len, "%.*s", args->len, args->value); - - for(uint8_t x = 0; x < sizeof(commands) / sizeof(commands[0]); x++) { - if(strcmp((char *)args->name, commands[x].name) == 0) { - len = commands[x].func(cpy, cmd, log_msg); - RESTmsg = RESTmsg + log_msg + "\n"; - log_message(log_msg); - send_command(cmd, len); - } - } - - memset(&cmd, 256, 0); - memset(&log_msg, 256, 0); + struct arguments_t *args = (struct arguments_t *)dat; + switch (client->route) { + case 10: { + if (strcmp((char *)args->name, "1wire") == 0) { + client->route = 11; + } else if (strcmp((char *)args->name, "s0") == 0) { + client->route = 12; + } + } break; + case 100: { + unsigned char cmd[256] = { 0 }; + char cpy[args->len + 1]; + char log_msg[256] = { 0 }; + unsigned int len = 0; + + memset(&cpy, 0, args->len + 1); + snprintf((char *)&cpy, args->len, "%.*s", args->len, args->value); + + for (uint8_t x = 0; x < sizeof(commands) / sizeof(commands[0]); x++) { + if (strcmp((char *)args->name, commands[x].name) == 0) { + len = commands[x].func(cpy, cmd, log_msg); + RESTmsg = RESTmsg + log_msg + "\n"; + log_message(log_msg); + send_command(cmd, len); + } + } - if(heishamonSettings.optionalPCB) { - //optional commands - for (uint8_t x = 0; x < sizeof(optionalCommands) / sizeof(optionalCommands[0]); x++) { - if (strcmp((char *)args->name, optionalCommands[x].name) == 0) { - len = optionalCommands[x].func(cpy, log_msg); - RESTmsg = RESTmsg + log_msg + "\n"; + memset(&cmd, 256, 0); + memset(&log_msg, 256, 0); + + if (heishamonSettings.optionalPCB) { + //optional commands + for (uint8_t x = 0; x < sizeof(optionalCommands) / sizeof(optionalCommands[0]); x++) { + if (strcmp((char *)args->name, optionalCommands[x].name) == 0) { + len = optionalCommands[x].func(cpy, log_msg); + RESTmsg = RESTmsg + log_msg + "\n"; + log_message(log_msg); + } + } + } + } break; + case 110: { + return cacheSettings(client, args); + } break; + case 150: { + if (uploadpercentage != (unsigned int)(((float)client->readlen / (float)client->totallen) * 100)) { + uploadpercentage = (unsigned int)(((float)client->readlen / (float)client->totallen) * 100); + sprintf_P(log_msg, PSTR("Uploading new firmware: %d%%"), uploadpercentage); log_message(log_msg); } - } - } - } break; - case 110: { - return cacheSettings(client, args); - } break; - case 150: { - if(uploadpercentage != (unsigned int)(((float)client->readlen/(float)client->totallen)*100)) { - uploadpercentage = (unsigned int)(((float)client->readlen/(float)client->totallen)*100); - sprintf_P(log_msg, PSTR("Uploading new firmware: %d%%"), uploadpercentage); - log_message(log_msg); - } - if(!Update.hasError() && strcmp((char *)args->name, "firmware") == 0){ - if(Update.write((uint8_t *)args->value, args->len) != args->len){ - Update.printError(Serial1); - } - } - } break; - } - } break; + if (!Update.hasError() && strcmp((char *)args->name, "firmware") == 0) { + if (Update.write((uint8_t *)args->value, args->len) != args->len) { + Update.printError(Serial1); + } + } + } break; + } + } break; case WEBSERVER_CLIENT_HEADER: { - struct arguments_t *args = (struct arguments_t *)dat; - return 0; - } break; + struct arguments_t *args = (struct arguments_t *)dat; + return 0; + } break; case WEBSERVER_CLIENT_WRITE: { - switch(client->route) { - case 0: { - if(client->content == 0) { - webserver_send(client, 404, (char *)"text/plain", 13); - webserver_send_content_P(client, PSTR("404 Not Found"), 13); - } - return 0; - } break; - case 1: { - return handleRoot(client, readpercentage, mqttReconnects, &heishamonSettings); - } break; - case 10: - case 11: - case 12: { - return handleTableRefresh(client, actData); - } break; - case 20: { - return handleJsonOutput(client, actData); - } break; - case 30: { - return handleReboot(client); - } break; - case 40: { - return handleDebug(client, (char *)data, 203); - } break; - case 50: { - return handleWifiScan(client); - } break; - case 80: { - return handleSettings(client); - } break; - case 90: { - return handleFactoryReset(client); - } break; - case 100: { - if(client->content == 0) { - webserver_send(client, 200, (char *)"text/plain", 0); - char *str = (char *)RESTmsg.c_str(); - webserver_send_content(client, (char *)str, strlen(str)); - RESTmsg.clear(); - } - return 0; - } break; - case 110: { - int ret = saveSettings(client, &heishamonSettings); - switch(client->route) { - case 111: { + switch (client->route) { + case 0: { + if (client->content == 0) { + webserver_send(client, 404, (char *)"text/plain", 13); + webserver_send_content_P(client, PSTR("404 Not Found"), 13); + } + return 0; + } break; + case 1: { + return handleRoot(client, readpercentage, mqttReconnects, &heishamonSettings); + } break; + case 10: + case 11: + case 12: { + return handleTableRefresh(client, actData); + } break; + case 20: { + return handleJsonOutput(client, actData); + } break; + case 30: { + return handleReboot(client); + } break; + case 40: { + return handleDebug(client, (char *)data, 203); + } break; + case 50: { + return handleWifiScan(client); + } break; + case 80: { + return handleSettings(client); + } break; + case 90: { + return handleFactoryReset(client); + } break; + case 100: { + if (client->content == 0) { + webserver_send(client, 200, (char *)"text/plain", 0); + char *str = (char *)RESTmsg.c_str(); + webserver_send_content(client, (char *)str, strlen(str)); + RESTmsg.clear(); + } + return 0; + } break; + case 110: { + int ret = saveSettings(client, &heishamonSettings); + switch (client->route) { + case 111: { + return settingsNewPassword(client, &heishamonSettings); + } break; + case 112: { + return settingsReconnectWifi(client, &heishamonSettings); + } break; + case 113: { + webserver_send(client, 301, (char *)"text/plain", 0); + } break; + } + return 0; + } break; + case 111: { return settingsNewPassword(client, &heishamonSettings); } break; - case 112: { + case 112: { return settingsReconnectWifi(client, &heishamonSettings); } break; - case 113: { + case 120: { + return handleSettings(client); + } break; + case 130: { + return getSettings(client, &heishamonSettings); + } break; + case 140: { + return showFirmware(client); + } break; + case 150: { + if (uploadpercentage != (unsigned int)(((float)client->readlen / (float)client->totallen) * 100)) { + uploadpercentage = (unsigned int)(((float)client->readlen / (float)client->totallen) * 100); + sprintf_P(log_msg, PSTR("Uploading new firmware: %d%%"), uploadpercentage); + log_message(log_msg); + } + if (Update.end(true)) { + log_message((char *)"Update Success"); + timerqueue_insert(15, 0, -2); // Start reboot sequence + return showFirmwareSuccess(client); + } else { + Update.printError(Serial1); + return showFirmwareFail(client); + } + } break; + default: { webserver_send(client, 301, (char *)"text/plain", 0); } break; - } - return 0; - } break; - case 111: { - return settingsNewPassword(client, &heishamonSettings); - } break; - case 112: { - return settingsReconnectWifi(client, &heishamonSettings); - } break; - case 120: { - return handleSettings(client); - } break; - case 130: { - return getSettings(client, &heishamonSettings); - } break; - case 140: { - return showFirmware(client); - } break; - case 150: { - if(uploadpercentage != (unsigned int)(((float)client->readlen/(float)client->totallen)*100)) { - uploadpercentage = (unsigned int)(((float)client->readlen/(float)client->totallen)*100); - sprintf_P(log_msg, PSTR("Uploading new firmware: %d%%"), uploadpercentage); - log_message(log_msg); - } - if(Update.end(true)){ - log_message((char *)"Update Success"); - timerqueue_insert(15, 0, -2); // Start reboot sequence - return showFirmwareSuccess(client); - } else { - Update.printError(Serial1); - return showFirmwareFail(client); - } - } break; - default: { - webserver_send(client, 301, (char *)"text/plain", 0); - } break; - } - return -1; - } break; + } + return -1; + } break; case WEBSERVER_CLIENT_CREATE_HEADER: { - struct header_t *header = (struct header_t *)dat; - switch(client->route) { - case 113: { - header->ptr += sprintf((char *)header->buffer, "Location: /settings"); - return -1; - } break; - case 0: - case 60: - case 70: { - header->ptr += sprintf((char *)header->buffer, "Location: /"); - return -1; - } break; - default: { - if(client->route != 0) { - header->ptr += sprintf((char *)header->buffer, "Access-Control-Allow-Origin: *"); - } - } break; - } - return 0; - } break; + struct header_t *header = (struct header_t *)dat; + switch (client->route) { + case 113: { + header->ptr += sprintf((char *)header->buffer, "Location: /settings"); + return -1; + } break; + case 0: + case 60: + case 70: { + header->ptr += sprintf((char *)header->buffer, "Location: /"); + return -1; + } break; + default: { + if (client->route != 0) { + header->ptr += sprintf((char *)header->buffer, "Access-Control-Allow-Origin: *"); + } + } break; + } + return 0; + } break; default: { - return 0; - } break; + return 0; + } break; } return 0; } @@ -785,19 +785,19 @@ void timer_cb(int nr) { sprintf_P(log_msg, PSTR("%d seconds timer interval"), nr); log_message(log_msg); - if(nr > 0) { + if (nr > 0) { timerqueue_insert(nr, 0, nr); } else { - switch(nr) { + switch (nr) { case -1: { - LittleFS.begin(); - LittleFS.format(); - WiFi.disconnect(true); - timerqueue_insert(1, 0, -2); - } break; + LittleFS.begin(); + LittleFS.format(); + WiFi.disconnect(true); + timerqueue_insert(1, 0, -2); + } break; case -2: { - ESP.restart(); - } break; + ESP.restart(); + } break; } } @@ -949,6 +949,8 @@ void loop() { stats += ESP.getVcc() / 1024.0; stats += F(",\"free memory\":"); stats += getFreeMemory(); + stats += F(",\"free heap\":"); + stats += ESP.getFreeHeap(); stats += F(",\"wifi\":"); stats += getWifiQuality(); stats += F(",\"mqtt reconnects\":"); @@ -982,6 +984,6 @@ void loop() { MDNS.announce(); } } - + timerqueue_update(); } diff --git a/HeishaMon/dallas.cpp b/HeishaMon/dallas.cpp index dd9ef002..cdbf9711 100644 --- a/HeishaMon/dallas.cpp +++ b/HeishaMon/dallas.cpp @@ -108,14 +108,14 @@ void dallasLoop(PubSubClient &mqtt_client, void (*log_message)(char*), char* mqt void dallasJsonOutput(struct webserver_t *client) { webserver_send_content_P(client, PSTR("["), 1); - for(int i = 0; i < dallasDevicecount; i++) { + for (int i = 0; i < dallasDevicecount; i++) { webserver_send_content_P(client, PSTR("{\"Sensor\":\""), 11); webserver_send_content(client, actDallasData[i].address, strlen(actDallasData[i].address)); webserver_send_content_P(client, PSTR("\",\"Temperature\":\""), 17); char str[64]; - dtostrf(actDallasData[i].temperature,0,2,str); + dtostrf(actDallasData[i].temperature, 0, 2, str); webserver_send_content(client, str, strlen(str)); - if(i < dallasDevicecount - 1) { + if (i < dallasDevicecount - 1) { webserver_send_content_P(client, PSTR("\"},"), 3); } else { webserver_send_content_P(client, PSTR("\"}"), 2); @@ -130,7 +130,7 @@ void dallasTableOutput(struct webserver_t *client) { webserver_send_content(client, actDallasData[i].address, strlen(actDallasData[i].address)); webserver_send_content_P(client, PSTR(""), 9); char str[64]; - dtostrf(actDallasData[i].temperature,0,2,str); + dtostrf(actDallasData[i].temperature, 0, 2, str); webserver_send_content(client, str, strlen(str)); webserver_send_content_P(client, PSTR(""), 10); } diff --git a/HeishaMon/htmlcode.h b/HeishaMon/htmlcode.h index b3029b6e..325d82c9 100644 --- a/HeishaMon/htmlcode.h +++ b/HeishaMon/htmlcode.h @@ -602,266 +602,266 @@ static const char populatescanwifiJS[] PROGMEM = ""; static const char settingsForm[] PROGMEM = -""; + "
" + "

Settings

" + "
" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + "
Please wait, loading saved settings...
" + " Hostname:" + " " + "
" + " Wifi SSID:" + " " + " " + "
" + " Wifi password:" + " " + "
" + " Update username:" + " " + "
" + " Current update password:" + " default password: \"heisha\"" + "
" + " New update password:" + " " + "
" + " Mqtt topic base:" + " " + "
" + " Mqtt server:" + " " + "
" + " Mqtt port:" + " " + "
" + " Mqtt username:" + " " + "
" + " Mqtt password:" + " " + "
" + " How often new values are collected from heatpump:" + " seconds (min 5 sec)" + "
" + " How often all heatpump values are retransmitted to MQTT broker:" + " seconds" + "
" + " Listen only mode:" + " " + "
" + " Debug log to MQTT topic from start:" + " " + "
" + " Debug log hexdump enable from start:" + " " + "
" + " Debug log to serial1 (GPIO2):" + " " + "
" + " Emulate optional PCB:" + " " + "
" + " " + " " + " " + " " + " " + "
" + " Use 1wire DS18b20:" + " " + "
" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + "
" + " How often new values are collected from 1wire:" + " seconds (min 5 sec)" + "
" + " How often all 1wire values are retransmitted to MQTT broker:" + " seconds" + "
" + " DS18b20 temperature resolution:" + " " + " " + " " + " " + "
" + " " + " " + " " + " " + " " + "
" + " Use s0 kWh metering:" + " " + "
" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + "
S0 port 1 GPIO:" + " " + "
S0 port 1 imp/kwh:" + " " + "
S0 port 1 reporting interval during standby/low power usage:" + " seconds" + "
S0 port 1 minimal pulse width:" + " milliseconds" + "
S0 port 1 maximal pulse width:" + " milliseconds" + "
S0 port 1 standby/low power usage threshold: Watt" + "
S0 port 2 GPIO:" + " " + "
S0 port 2 imp/kwh:" + " " + "
S0 port 2 reporting interval during standby/low power usage:" + " seconds" + "
S0 port 2 minimal pulse width:" + " milliseconds" + "
S0 port 2 maximal pulse width:" + " milliseconds" + "
S0 port 2 standby/low power usage threshold: Watt" + "
" + "

" + " " + "
" + "
Factory reset" + "
"; const char populategetsettingsJS[] PROGMEM = ""; static const char settingsForm[] PROGMEM = - "
" + "
" + "

Please wait, loading saved settings...

" + "
" + "
" "

Settings

" "
" " " " " - " " - " " - " " " " " " "
Please wait, loading saved settings...
" " Hostname:" @@ -858,7 +858,7 @@ static const char settingsForm[] PROGMEM = "
" "

" - " " + " " "
" "
Factory reset" "
"; @@ -895,7 +895,8 @@ const char populategetsettingsJS[] PROGMEM = " };" " };" " };" - " document.getElementById(\"loading_settings\").parentNode.style.display = \"none\";" + " document.getElementById(\"loading_settings\").style.display = \"none\";" + " document.getElementById(\"settings_form\").style.display = \"block\";" " changeMinWatt(1);" " changeMinWatt(2);" " };" From ef4bdc4d25bf24cdc2adfcb1ecd1482e48354748 Mon Sep 17 00:00:00 2001 From: CurlyMoo Date: Mon, 20 Dec 2021 16:32:31 +0100 Subject: [PATCH 38/75] Differentiate between push SHA and PR SHA (#83) --- .github/workflows/main.yml | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0e218edb..dd18523c 100755 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,6 +10,21 @@ jobs: - name: Checkout uses: actions/checkout@v2 + - name: Update version + if: github.event_name != 'pull_request' + run: cd HeishaMon && echo "static const char *heishamon_version = \"Alpha-${GITHUB_SHA::6}\";" > version.h && cat version.h + shell: bash + + - name: Set PR SHA in env + if: github.event_name == 'pull_request' + run: echo "GITHUB_PR_SHA=${{github.event.pull_request.head.sha}}" >> $GITHUB_ENV + shell: bash + + - name: Update version + if: github.event_name == 'pull_request' + run: cd HeishaMon && echo "static const char *heishamon_version = \"Alpha-${GITHUB_PR_SHA::6}\";" > version.h && cat version.h + shell: bash + - name: Setup Arduino CLI uses: arduino/setup-arduino-cli@v1 @@ -19,10 +34,6 @@ jobs: - name: Install dependencies run: arduino-cli lib install ringbuffer pubsubclient doubleresetdetect arduinojson dallastemperature onewire WebSockets - - name: Update version - run: cd HeishaMon && echo "static const char *heishamon_version = \"Alpha-${GITHUB_SHA::6}\";" > version.h - shell: bash - - name: Compile Sketch run: cd HeishaMon && arduino-cli compile --fqbn=esp8266:esp8266:d1_mini:xtal=160,vt=flash,ssl=all,eesz=4M2M,ip=lm2f,dbg=Disabled,lvl=None____,wipe=none,baud=921600 --vid-pid=1A86_7523 --warnings=none --verbose HeishaMon.ino @@ -31,5 +42,3 @@ jobs: with: name: HeishaMon.ino.bin path: C:\\Users\\runneradmin\\AppData\\Local\\Temp\\arduino-sketch-*\\*.ino.bin - - From 505defd365c8eda10b9e1ec4f54a2d58e70c3ceb Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Tue, 21 Dec 2021 22:32:55 +0100 Subject: [PATCH 39/75] predefined char arrays saves memory during progmem to memory actions a non-predifned array length causes the whole progmem array to be copied to memory instead of only the array element --- HeishaMon/decode.h | 4 ++-- HeishaMon/version.h | 2 +- HeishaMon/webfunctions.cpp | 6 +----- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/HeishaMon/decode.h b/HeishaMon/decode.h index d7a6903a..c2a8c0b1 100644 --- a/HeishaMon/decode.h +++ b/HeishaMon/decode.h @@ -86,7 +86,7 @@ static const byte knownModels[sizeof(Model) / sizeof(Model[0])][10] = { //stores #define NUMBER_OF_TOPICS 106 //last topic number + 1 #define NUMBER_OF_OPT_TOPICS 7 //last topic number + 1 -static const char *optTopics[] PROGMEM = { +static const char optTopics[][20] PROGMEM = { "Z1_Water_Pump", // OPT0 "Z1_Mixing_Valve", // OPT1 "Z2_Water_Pump", // OPT2 @@ -96,7 +96,7 @@ static const char *optTopics[] PROGMEM = { "Alarm_State", // OPT6 }; -static const char *topics[] PROGMEM = { +static const char topics[][40] PROGMEM = { "Heatpump_State", //TOP0 "Pump_Flow", //TOP1 "Force_DHW_State", //TOP2 diff --git a/HeishaMon/version.h b/HeishaMon/version.h index f55e3e33..252c1069 100644 --- a/HeishaMon/version.h +++ b/HeishaMon/version.h @@ -1 +1 @@ -static const char* heishamon_version = "2.2-iy-1"; +static const char* heishamon_version = "2.2-iy-2"; diff --git a/HeishaMon/webfunctions.cpp b/HeishaMon/webfunctions.cpp index 0a597f51..6bacf92b 100755 --- a/HeishaMon/webfunctions.cpp +++ b/HeishaMon/webfunctions.cpp @@ -939,11 +939,7 @@ int handleTableRefresh(struct webserver_t *client, String actData[]) { webserver_send_content_P(client, PSTR(""), 9); - String t = topics[topic]; - char *tmp = (char *)t.c_str(); - webserver_send_content(client, tmp, strlen(tmp)); - - webserver_send_content_P(client, PSTR(""), 9); + webserver_send_content_P(client, topics[topic], strlen_P(topics[topic])); { char *str = (char *)actData[topic].c_str(); From 1afba2b3ab5a698176b0e977766c9b1b1d16f839 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Tue, 21 Dec 2021 23:23:26 +0100 Subject: [PATCH 40/75] fix removing a line too many --- HeishaMon/webfunctions.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HeishaMon/webfunctions.cpp b/HeishaMon/webfunctions.cpp index 6bacf92b..85410027 100755 --- a/HeishaMon/webfunctions.cpp +++ b/HeishaMon/webfunctions.cpp @@ -938,8 +938,8 @@ int handleTableRefresh(struct webserver_t *client, String actData[]) { webserver_send_content(client, str, strlen(str)); webserver_send_content_P(client, PSTR(""), 9); - webserver_send_content_P(client, topics[topic], strlen_P(topics[topic])); + webserver_send_content_P(client, PSTR(""), 9); { char *str = (char *)actData[topic].c_str(); From e4be168ad6259bead0b388c2a683f21b659a46b9 Mon Sep 17 00:00:00 2001 From: CurlyMoo Date: Wed, 22 Dec 2021 17:03:38 +0100 Subject: [PATCH 41/75] Added configurable NTP functionality (#77) Co-authored-by: IgorYbema --- HeishaMon/HeishaMon.ino | 30 ++- HeishaMon/htmlcode.h | 497 ++++++++++++++++++++++++++++++++++++- HeishaMon/webfunctions.cpp | 180 ++++++++++---- HeishaMon/webfunctions.h | 13 +- 4 files changed, 659 insertions(+), 61 deletions(-) diff --git a/HeishaMon/HeishaMon.ino b/HeishaMon/HeishaMon.ino index 5d5bea83..d04e9d2e 100755 --- a/HeishaMon/HeishaMon.ino +++ b/HeishaMon/HeishaMon.ino @@ -10,6 +10,7 @@ #include +#include "lwip/apps/sntp.h" #include "src/common/timerqueue.h" #include "webfunctions.h" #include "decode.h" @@ -167,6 +168,8 @@ void check_wifi() settingsToJson(jsonDoc, &heishamonSettings); //stores current settings in a json document saveJsonToConfig(jsonDoc); //save to config file } + + ntpReload(&heishamonSettings); } /* @@ -215,25 +218,33 @@ void mqtt_reconnect() void log_message(char* string) { + time_t rawtime; + rawtime = time(NULL); + struct tm *timeinfo = localtime(&rawtime); + char timestring[32]; + strftime(timestring,32,"%c",timeinfo); + char log_line[320]; + sprintf(log_line,"%s (%lu): %s",timestring,millis(),string); + if (heishamonSettings.logSerial1) { - Serial1.print(millis()); - Serial1.print(": "); - Serial1.println(string); + Serial1.println(log_line); } if (heishamonSettings.logMqtt && mqtt_client.connected()) { char log_topic[256]; sprintf(log_topic, "%s/%s", heishamonSettings.mqtt_topic_base, mqtt_logtopic); - if (!mqtt_client.publish(log_topic, string)) { - Serial1.print(millis()); - Serial1.print(F(": ")); - Serial1.println(F("MQTT publish log message failed!")); + if (!mqtt_client.publish(log_topic, log_line)) { + if (heishamonSettings.logSerial1) { + Serial1.print(millis()); + Serial1.print(F(": ")); + Serial1.println(F("MQTT publish log message failed!")); + } mqtt_client.disconnect(); } } if (webSocket.connectedClients() > 0) { - webSocket.broadcastTXT(string, strlen(string)); + webSocket.broadcastTXT(log_line, strlen(log_line)); } } @@ -828,6 +839,9 @@ void setup() { setupMqtt(); setupHttp(); + sntp_setoperatingmode(SNTP_OPMODE_POLL); + sntp_init(); + switchSerial(); //switch serial to gpio13/gpio15 WiFi.printDiag(Serial1); diff --git a/HeishaMon/htmlcode.h b/HeishaMon/htmlcode.h index 4d9cb718..f0650d6e 100644 --- a/HeishaMon/htmlcode.h +++ b/HeishaMon/htmlcode.h @@ -601,7 +601,8 @@ static const char populatescanwifiJS[] PROGMEM = "refreshWifiScan();" ""; -static const char settingsForm[] PROGMEM = + +static const char settingsForm1[] PROGMEM = "
" "

Please wait, loading saved settings...

" "
" @@ -691,6 +692,23 @@ static const char settingsForm[] PROGMEM = " " " " " " + " NTP servers (comma separated):" + " " + " " + " " + " " + " " + " " + " Timezone:" + " " + " " + " " + " " + " " + " " " How often new values are collected from heatpump:" " " " seconds (min 5 sec)" @@ -893,6 +911,14 @@ const char populategetsettingsJS[] PROGMEM = " };" " };" " };" + " if(el[0].type.indexOf(\"select\") > -1) {" + " var children = el[0].childNodes;" + " for(var x = 0; x < children.length; x++) {" + " if(children[x].value == jsonOptions[key]) {" + " children[x].selected = true;" + " };" + " };" + " };" " };" " };" " document.getElementById(\"loading_settings\").style.display = \"none\";" @@ -929,3 +955,472 @@ static const char firmwareSuccessResponse[] PROGMEM = static const char firmwareFailResponse[] PROGMEM = "Update failed! Please try again..."; + +// https://github.com/nayarsystems/posix_tz_db +struct tzStruct { + char name[32]; + char value[46]; +}; +const tzStruct tzdata[] PROGMEM = { + { "ETC/GMT", "GMT0" }, + { "Africa/Abidjan", "GMT0" }, + { "Africa/Accra", "GMT0" }, + { "Africa/Addis_Ababa", "EAT-3" }, + { "Africa/Algiers", "CET-1" }, + { "Africa/Asmara", "EAT-3" }, + { "Africa/Bamako", "GMT0" }, + { "Africa/Bangui", "WAT-1" }, + { "Africa/Banjul", "GMT0" }, + { "Africa/Bissau", "GMT0" }, + { "Africa/Blantyre", "CAT-2" }, + { "Africa/Brazzaville", "WAT-1" }, + { "Africa/Bujumbura", "CAT-2" }, + { "Africa/Cairo", "EET-2" }, + { "Africa/Casablanca", "<+01>-1" }, + { "Africa/Ceuta", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Africa/Conakry", "GMT0" }, + { "Africa/Dakar", "GMT0" }, + { "Africa/Dar_es_Salaam", "EAT-3" }, + { "Africa/Djibouti", "EAT-3" }, + { "Africa/Douala", "WAT-1" }, + { "Africa/El_Aaiun", "<+01>-1" }, + { "Africa/Freetown", "GMT0" }, + { "Africa/Gaborone", "CAT-2" }, + { "Africa/Harare", "CAT-2" }, + { "Africa/Johannesburg", "SAST-2" }, + { "Africa/Juba", "CAT-2" }, + { "Africa/Kampala", "EAT-3" }, + { "Africa/Khartoum", "CAT-2" }, + { "Africa/Kigali", "CAT-2" }, + { "Africa/Kinshasa", "WAT-1" }, + { "Africa/Lagos", "WAT-1" }, + { "Africa/Libreville", "WAT-1" }, + { "Africa/Lome", "GMT0" }, + { "Africa/Luanda", "WAT-1" }, + { "Africa/Lubumbashi", "CAT-2" }, + { "Africa/Lusaka", "CAT-2" }, + { "Africa/Malabo", "WAT-1" }, + { "Africa/Maputo", "CAT-2" }, + { "Africa/Maseru", "SAST-2" }, + { "Africa/Mbabane", "SAST-2" }, + { "Africa/Mogadishu", "EAT-3" }, + { "Africa/Monrovia", "GMT0" }, + { "Africa/Nairobi", "EAT-3" }, + { "Africa/Ndjamena", "WAT-1" }, + { "Africa/Niamey", "WAT-1" }, + { "Africa/Nouakchott", "GMT0" }, + { "Africa/Ouagadougou", "GMT0" }, + { "Africa/Porto-Novo", "WAT-1" }, + { "Africa/Sao_Tome", "GMT0" }, + { "Africa/Tripoli", "EET-2" }, + { "Africa/Tunis", "CET-1" }, + { "Africa/Windhoek", "CAT-2" }, + { "America/Adak", "HST10HDT,M3.2.0,M11.1.0" }, + { "America/Anchorage", "AKST9AKDT,M3.2.0,M11.1.0" }, + { "America/Anguilla", "AST4" }, + { "America/Antigua", "AST4" }, + { "America/Araguaina", "<-03>3" }, + { "America/Argentina/Buenos_Aires", "<-03>3" }, + { "America/Argentina/Catamarca", "<-03>3" }, + { "America/Argentina/Cordoba", "<-03>3" }, + { "America/Argentina/Jujuy", "<-03>3" }, + { "America/Argentina/La_Rioja", "<-03>3" }, + { "America/Argentina/Mendoza", "<-03>3" }, + { "America/Argentina/Rio_Gallegos", "<-03>3" }, + { "America/Argentina/Salta", "<-03>3" }, + { "America/Argentina/San_Juan", "<-03>3" }, + { "America/Argentina/San_Luis", "<-03>3" }, + { "America/Argentina/Tucuman", "<-03>3" }, + { "America/Argentina/Ushuaia", "<-03>3" }, + { "America/Aruba", "AST4" }, + { "America/Asuncion", "<-04>4<-03>,M10.1.0/0,M3.4.0/0" }, + { "America/Atikokan", "EST5" }, + { "America/Bahia", "<-03>3" }, + { "America/Bahia_Banderas", "CST6CDT,M4.1.0,M10.5.0" }, + { "America/Barbados", "AST4" }, + { "America/Belem", "<-03>3" }, + { "America/Belize", "CST6" }, + { "America/Blanc-Sablon", "AST4" }, + { "America/Boa_Vista", "<-04>4" }, + { "America/Bogota", "<-05>5" }, + { "America/Boise", "MST7MDT,M3.2.0,M11.1.0" }, + { "America/Cambridge_Bay", "MST7MDT,M3.2.0,M11.1.0" }, + { "America/Campo_Grande", "<-04>4" }, + { "America/Cancun", "EST5" }, + { "America/Caracas", "<-04>4" }, + { "America/Cayenne", "<-03>3" }, + { "America/Cayman", "EST5" }, + { "America/Chicago", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Chihuahua", "MST7MDT,M4.1.0,M10.5.0" }, + { "America/Costa_Rica", "CST6" }, + { "America/Creston", "MST7" }, + { "America/Cuiaba", "<-04>4" }, + { "America/Curacao", "AST4" }, + { "America/Danmarkshavn", "GMT0" }, + { "America/Dawson", "MST7" }, + { "America/Dawson_Creek", "MST7" }, + { "America/Denver", "MST7MDT,M3.2.0,M11.1.0" }, + { "America/Detroit", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Dominica", "AST4" }, + { "America/Edmonton", "MST7MDT,M3.2.0,M11.1.0" }, + { "America/Eirunepe", "<-05>5" }, + { "America/El_Salvador", "CST6" }, + { "America/Fortaleza", "<-03>3" }, + { "America/Fort_Nelson", "MST7" }, + { "America/Glace_Bay", "AST4ADT,M3.2.0,M11.1.0" }, + { "America/Godthab", "<-03>3<-02>,M3.5.0/-2,M10.5.0/-1" }, + { "America/Goose_Bay", "AST4ADT,M3.2.0,M11.1.0" }, + { "America/Grand_Turk", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Grenada", "AST4" }, + { "America/Guadeloupe", "AST4" }, + { "America/Guatemala", "CST6" }, + { "America/Guayaquil", "<-05>5" }, + { "America/Guyana", "<-04>4" }, + { "America/Halifax", "AST4ADT,M3.2.0,M11.1.0" }, + { "America/Havana", "CST5CDT,M3.2.0/0,M11.1.0/1" }, + { "America/Hermosillo", "MST7" }, + { "America/Indiana/Indianapolis", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Indiana/Knox", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Indiana/Marengo", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Indiana/Petersburg", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Indiana/Tell_City", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Indiana/Vevay", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Indiana/Vincennes", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Indiana/Winamac", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Inuvik", "MST7MDT,M3.2.0,M11.1.0" }, + { "America/Iqaluit", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Jamaica", "EST5" }, + { "America/Juneau", "AKST9AKDT,M3.2.0,M11.1.0" }, + { "America/Kentucky/Louisville", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Kentucky/Monticello", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Kralendijk", "AST4" }, + { "America/La_Paz", "<-04>4" }, + { "America/Lima", "<-05>5" }, + { "America/Los_Angeles", "PST8PDT,M3.2.0,M11.1.0" }, + { "America/Lower_Princes", "AST4" }, + { "America/Maceio", "<-03>3" }, + { "America/Managua", "CST6" }, + { "America/Manaus", "<-04>4" }, + { "America/Marigot", "AST4" }, + { "America/Martinique", "AST4" }, + { "America/Matamoros", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Mazatlan", "MST7MDT,M4.1.0,M10.5.0" }, + { "America/Menominee", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Merida", "CST6CDT,M4.1.0,M10.5.0" }, + { "America/Metlakatla", "AKST9AKDT,M3.2.0,M11.1.0" }, + { "America/Mexico_City", "CST6CDT,M4.1.0,M10.5.0" }, + { "America/Miquelon", "<-03>3<-02>,M3.2.0,M11.1.0" }, + { "America/Moncton", "AST4ADT,M3.2.0,M11.1.0" }, + { "America/Monterrey", "CST6CDT,M4.1.0,M10.5.0" }, + { "America/Montevideo", "<-03>3" }, + { "America/Montreal", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Montserrat", "AST4" }, + { "America/Nassau", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/New_York", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Nipigon", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Nome", "AKST9AKDT,M3.2.0,M11.1.0" }, + { "America/Noronha", "<-02>2" }, + { "America/North_Dakota/Beulah", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/North_Dakota/Center", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/North_Dakota/New_Salem", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Nuuk", "<-03>3<-02>,M3.5.0/-2,M10.5.0/-1" }, + { "America/Ojinaga", "MST7MDT,M3.2.0,M11.1.0" }, + { "America/Panama", "EST5" }, + { "America/Pangnirtung", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Paramaribo", "<-03>3" }, + { "America/Phoenix", "MST7" }, + { "America/Port-au-Prince", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Port_of_Spain", "AST4" }, + { "America/Porto_Velho", "<-04>4" }, + { "America/Puerto_Rico", "AST4" }, + { "America/Punta_Arenas", "<-03>3" }, + { "America/Rainy_River", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Rankin_Inlet", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Recife", "<-03>3" }, + { "America/Regina", "CST6" }, + { "America/Resolute", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Rio_Branco", "<-05>5" }, + { "America/Santarem", "<-03>3" }, + { "America/Santiago", "<-04>4<-03>,M9.1.6/24,M4.1.6/24" }, + { "America/Santo_Domingo", "AST4" }, + { "America/Sao_Paulo", "<-03>3" }, + { "America/Scoresbysund", "<-01>1<+00>,M3.5.0/0,M10.5.0/1" }, + { "America/Sitka", "AKST9AKDT,M3.2.0,M11.1.0" }, + { "America/St_Barthelemy", "AST4" }, + { "America/St_Johns", "NST3:30NDT,M3.2.0,M11.1.0" }, + { "America/St_Kitts", "AST4" }, + { "America/St_Lucia", "AST4" }, + { "America/St_Thomas", "AST4" }, + { "America/St_Vincent", "AST4" }, + { "America/Swift_Current", "CST6" }, + { "America/Tegucigalpa", "CST6" }, + { "America/Thule", "AST4ADT,M3.2.0,M11.1.0" }, + { "America/Thunder_Bay", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Tijuana", "PST8PDT,M3.2.0,M11.1.0" }, + { "America/Toronto", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Tortola", "AST4" }, + { "America/Vancouver", "PST8PDT,M3.2.0,M11.1.0" }, + { "America/Whitehorse", "MST7" }, + { "America/Winnipeg", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Yakutat", "AKST9AKDT,M3.2.0,M11.1.0" }, + { "America/Yellowknife", "MST7MDT,M3.2.0,M11.1.0" }, + { "Antarctica/Casey", "<+11>-11" }, + { "Antarctica/Davis", "<+07>-7" }, + { "Antarctica/DumontDUrville", "<+10>-10" }, + { "Antarctica/Macquarie", "AEST-10AEDT,M10.1.0,M4.1.0/3" }, + { "Antarctica/Mawson", "<+05>-5" }, + { "Antarctica/McMurdo", "NZST-12NZDT,M9.5.0,M4.1.0/3" }, + { "Antarctica/Palmer", "<-03>3" }, + { "Antarctica/Rothera", "<-03>3" }, + { "Antarctica/Syowa", "<+03>-3" }, + { "Antarctica/Troll", "<+00>0<+02>-2,M3.5.0/1,M10.5.0/3" }, + { "Antarctica/Vostok", "<+06>-6" }, + { "Arctic/Longyearbyen", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Asia/Aden", "<+03>-3" }, + { "Asia/Almaty", "<+06>-6" }, + { "Asia/Amman", "EET-2EEST,M2.5.4/24,M10.5.5/1" }, + { "Asia/Anadyr", "<+12>-12" }, + { "Asia/Aqtau", "<+05>-5" }, + { "Asia/Aqtobe", "<+05>-5" }, + { "Asia/Ashgabat", "<+05>-5" }, + { "Asia/Atyrau", "<+05>-5" }, + { "Asia/Baghdad", "<+03>-3" }, + { "Asia/Bahrain", "<+03>-3" }, + { "Asia/Baku", "<+04>-4" }, + { "Asia/Bangkok", "<+07>-7" }, + { "Asia/Barnaul", "<+07>-7" }, + { "Asia/Beirut", "EET-2EEST,M3.5.0/0,M10.5.0/0" }, + { "Asia/Bishkek", "<+06>-6" }, + { "Asia/Brunei", "<+08>-8" }, + { "Asia/Chita", "<+09>-9" }, + { "Asia/Choibalsan", "<+08>-8" }, + { "Asia/Colombo", "<+0530>-5:30" }, + { "Asia/Damascus", "EET-2EEST,M3.5.5/0,M10.5.5/0" }, + { "Asia/Dhaka", "<+06>-6" }, + { "Asia/Dili", "<+09>-9" }, + { "Asia/Dubai", "<+04>-4" }, + { "Asia/Dushanbe", "<+05>-5" }, + { "Asia/Famagusta", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Asia/Gaza", "EET-2EEST,M3.4.4/48,M10.5.5/1" }, + { "Asia/Hebron", "EET-2EEST,M3.4.4/48,M10.5.5/1" }, + { "Asia/Ho_Chi_Minh", "<+07>-7" }, + { "Asia/Hong_Kong", "HKT-8" }, + { "Asia/Hovd", "<+07>-7" }, + { "Asia/Irkutsk", "<+08>-8" }, + { "Asia/Jakarta", "WIB-7" }, + { "Asia/Jayapura", "WIT-9" }, + { "Asia/Jerusalem", "IST-2IDT,M3.4.4/26,M10.5.0" }, + { "Asia/Kabul", "<+0430>-4:30" }, + { "Asia/Kamchatka", "<+12>-12" }, + { "Asia/Karachi", "PKT-5" }, + { "Asia/Kathmandu", "<+0545>-5:45" }, + { "Asia/Khandyga", "<+09>-9" }, + { "Asia/Kolkata", "IST-5:30" }, + { "Asia/Krasnoyarsk", "<+07>-7" }, + { "Asia/Kuala_Lumpur", "<+08>-8" }, + { "Asia/Kuching", "<+08>-8" }, + { "Asia/Kuwait", "<+03>-3" }, + { "Asia/Macau", "CST-8" }, + { "Asia/Magadan", "<+11>-11" }, + { "Asia/Makassar", "WITA-8" }, + { "Asia/Manila", "PST-8" }, + { "Asia/Muscat", "<+04>-4" }, + { "Asia/Nicosia", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Asia/Novokuznetsk", "<+07>-7" }, + { "Asia/Novosibirsk", "<+07>-7" }, + { "Asia/Omsk", "<+06>-6" }, + { "Asia/Oral", "<+05>-5" }, + { "Asia/Phnom_Penh", "<+07>-7" }, + { "Asia/Pontianak", "WIB-7" }, + { "Asia/Pyongyang", "KST-9" }, + { "Asia/Qatar", "<+03>-3" }, + { "Asia/Qyzylorda", "<+05>-5" }, + { "Asia/Riyadh", "<+03>-3" }, + { "Asia/Sakhalin", "<+11>-11" }, + { "Asia/Samarkand", "<+05>-5" }, + { "Asia/Seoul", "KST-9" }, + { "Asia/Shanghai", "CST-8" }, + { "Asia/Singapore", "<+08>-8" }, + { "Asia/Srednekolymsk", "<+11>-11" }, + { "Asia/Taipei", "CST-8" }, + { "Asia/Tashkent", "<+05>-5" }, + { "Asia/Tbilisi", "<+04>-4" }, + { "Asia/Tehran", "<+0330>-3:30<+0430>,J79/24,J263/24" }, + { "Asia/Thimphu", "<+06>-6" }, + { "Asia/Tokyo", "JST-9" }, + { "Asia/Tomsk", "<+07>-7" }, + { "Asia/Ulaanbaatar", "<+08>-8" }, + { "Asia/Urumqi", "<+06>-6" }, + { "Asia/Ust-Nera", "<+10>-10" }, + { "Asia/Vientiane", "<+07>-7" }, + { "Asia/Vladivostok", "<+10>-10" }, + { "Asia/Yakutsk", "<+09>-9" }, + { "Asia/Yangon", "<+0630>-6:30" }, + { "Asia/Yekaterinburg", "<+05>-5" }, + { "Asia/Yerevan", "<+04>-4" }, + { "Atlantic/Azores", "<-01>1<+00>,M3.5.0/0,M10.5.0/1" }, + { "Atlantic/Bermuda", "AST4ADT,M3.2.0,M11.1.0" }, + { "Atlantic/Canary", "WET0WEST,M3.5.0/1,M10.5.0" }, + { "Atlantic/Cape_Verde", "<-01>1" }, + { "Atlantic/Faroe", "WET0WEST,M3.5.0/1,M10.5.0" }, + { "Atlantic/Madeira", "WET0WEST,M3.5.0/1,M10.5.0" }, + { "Atlantic/Reykjavik", "GMT0" }, + { "Atlantic/South_Georgia", "<-02>2" }, + { "Atlantic/Stanley", "<-03>3" }, + { "Atlantic/St_Helena", "GMT0" }, + { "Australia/Adelaide", "ACST-9:30ACDT,M10.1.0,M4.1.0/3" }, + { "Australia/Brisbane", "AEST-10" }, + { "Australia/Broken_Hill", "ACST-9:30ACDT,M10.1.0,M4.1.0/3" }, + { "Australia/Currie", "AEST-10AEDT,M10.1.0,M4.1.0/3" }, + { "Australia/Darwin", "ACST-9:30" }, + { "Australia/Eucla", "<+0845>-8:45" }, + { "Australia/Hobart", "AEST-10AEDT,M10.1.0,M4.1.0/3" }, + { "Australia/Lindeman", "AEST-10" }, + { "Australia/Lord_Howe", "<+1030>-10:30<+11>-11,M10.1.0,M4.1.0" }, + { "Australia/Melbourne", "AEST-10AEDT,M10.1.0,M4.1.0/3" }, + { "Australia/Perth", "AWST-8" }, + { "Australia/Sydney", "AEST-10AEDT,M10.1.0,M4.1.0/3" }, + { "Europe/Amsterdam", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Andorra", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Astrakhan", "<+04>-4" }, + { "Europe/Athens", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Belgrade", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Berlin", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Bratislava", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Brussels", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Bucharest", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Budapest", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Busingen", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Chisinau", "EET-2EEST,M3.5.0,M10.5.0/3" }, + { "Europe/Copenhagen", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Dublin", "IST-1GMT0,M10.5.0,M3.5.0/1" }, + { "Europe/Gibraltar", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Guernsey", "GMT0BST,M3.5.0/1,M10.5.0" }, + { "Europe/Helsinki", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Isle_of_Man", "GMT0BST,M3.5.0/1,M10.5.0" }, + { "Europe/Istanbul", "<+03>-3" }, + { "Europe/Jersey", "GMT0BST,M3.5.0/1,M10.5.0" }, + { "Europe/Kaliningrad", "EET-2" }, + { "Europe/Kiev", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Kirov", "<+03>-3" }, + { "Europe/Lisbon", "WET0WEST,M3.5.0/1,M10.5.0" }, + { "Europe/Ljubljana", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/London", "GMT0BST,M3.5.0/1,M10.5.0" }, + { "Europe/Luxembourg", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Madrid", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Malta", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Mariehamn", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Minsk", "<+03>-3" }, + { "Europe/Monaco", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Moscow", "MSK-3" }, + { "Europe/Oslo", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Paris", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Podgorica", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Prague", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Riga", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Rome", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Samara", "<+04>-4" }, + { "Europe/San_Marino", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Sarajevo", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Saratov", "<+04>-4" }, + { "Europe/Simferopol", "MSK-3" }, + { "Europe/Skopje", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Sofia", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Stockholm", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Tallinn", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Tirane", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Ulyanovsk", "<+04>-4" }, + { "Europe/Uzhgorod", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Vaduz", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Vatican", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Vienna", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Vilnius", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Volgograd", "<+03>-3" }, + { "Europe/Warsaw", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Zagreb", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Zaporozhye", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Zurich", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Indian/Antananarivo", "EAT-3" }, + { "Indian/Chagos", "<+06>-6" }, + { "Indian/Christmas", "<+07>-7" }, + { "Indian/Cocos", "<+0630>-6:30" }, + { "Indian/Comoro", "EAT-3" }, + { "Indian/Kerguelen", "<+05>-5" }, + { "Indian/Mahe", "<+04>-4" }, + { "Indian/Maldives", "<+05>-5" }, + { "Indian/Mauritius", "<+04>-4" }, + { "Indian/Mayotte", "EAT-3" }, + { "Indian/Reunion", "<+04>-4" }, + { "Pacific/Apia", "<+13>-13" }, + { "Pacific/Auckland", "NZST-12NZDT,M9.5.0,M4.1.0/3" }, + { "Pacific/Bougainville", "<+11>-11" }, + { "Pacific/Chatham", "<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45" }, + { "Pacific/Chuuk", "<+10>-10" }, + { "Pacific/Easter", "<-06>6<-05>,M9.1.6/22,M4.1.6/22" }, + { "Pacific/Efate", "<+11>-11" }, + { "Pacific/Enderbury", "<+13>-13" }, + { "Pacific/Fakaofo", "<+13>-13" }, + { "Pacific/Fiji", "<+12>-12<+13>,M11.2.0,M1.2.3/99" }, + { "Pacific/Funafuti", "<+12>-12" }, + { "Pacific/Galapagos", "<-06>6" }, + { "Pacific/Gambier", "<-09>9" }, + { "Pacific/Guadalcanal", "<+11>-11" }, + { "Pacific/Guam", "ChST-10" }, + { "Pacific/Honolulu", "HST10" }, + { "Pacific/Kiritimati", "<+14>-14" }, + { "Pacific/Kosrae", "<+11>-11" }, + { "Pacific/Kwajalein", "<+12>-12" }, + { "Pacific/Majuro", "<+12>-12" }, + { "Pacific/Marquesas", "<-0930>9:30" }, + { "Pacific/Midway", "SST11" }, + { "Pacific/Nauru", "<+12>-12" }, + { "Pacific/Niue", "<-11>11" }, + { "Pacific/Norfolk", "<+11>-11<+12>,M10.1.0,M4.1.0/3" }, + { "Pacific/Noumea", "<+11>-11" }, + { "Pacific/Pago_Pago", "SST11" }, + { "Pacific/Palau", "<+09>-9" }, + { "Pacific/Pitcairn", "<-08>8" }, + { "Pacific/Pohnpei", "<+11>-11" }, + { "Pacific/Port_Moresby", "<+10>-10" }, + { "Pacific/Rarotonga", "<-10>10" }, + { "Pacific/Saipan", "ChST-10" }, + { "Pacific/Tahiti", "<-10>10" }, + { "Pacific/Tarawa", "<+12>-12" }, + { "Pacific/Tongatapu", "<+13>-13" }, + { "Pacific/Wake", "<+12>-12" }, + { "Pacific/Wallis", "<+12>-12" }, + { "Etc/GMT-0", "GMT0" }, + { "Etc/GMT-1", "<+01>-1" }, + { "Etc/GMT-2", "<+02>-2" }, + { "Etc/GMT-3", "<+03>-3" }, + { "Etc/GMT-4", "<+04>-4" }, + { "Etc/GMT-5", "<+05>-5" }, + { "Etc/GMT-6", "<+06>-6" }, + { "Etc/GMT-7", "<+07>-7" }, + { "Etc/GMT-8", "<+08>-8" }, + { "Etc/GMT-9", "<+09>-9" }, + { "Etc/GMT-10", "<+10>-10" }, + { "Etc/GMT-11", "<+11>-11" }, + { "Etc/GMT-12", "<+12>-12" }, + { "Etc/GMT-13", "<+13>-13" }, + { "Etc/GMT-14", "<+14>-14" }, + { "Etc/GMT0", "GMT0" }, + { "Etc/GMT+0", "GMT0" }, + { "Etc/GMT+1", "<-01>1" }, + { "Etc/GMT+2", "<-02>2" }, + { "Etc/GMT+3", "<-03>3" }, + { "Etc/GMT+4", "<-04>4" }, + { "Etc/GMT+5", "<-05>5" }, + { "Etc/GMT+6", "<-06>6" }, + { "Etc/GMT+7", "<-07>7" }, + { "Etc/GMT+8", "<-08>8" }, + { "Etc/GMT+9", "<-09>9" }, + { "Etc/GMT+10", "<-10>10" }, + { "Etc/GMT+11", "<-11>11" }, + { "Etc/GMT+12", "<-12>12" }, + { "Etc/UCT", "UTC0" }, + { "Etc/UTC", "UTC0" }, + { "Etc/Greenwich", "GMT0" }, + { "Etc/Universal", "UTC0" }, + { "Etc/Zulu", "UTC0" }, +}; diff --git a/HeishaMon/webfunctions.cpp b/HeishaMon/webfunctions.cpp index 85410027..61f0448a 100755 --- a/HeishaMon/webfunctions.cpp +++ b/HeishaMon/webfunctions.cpp @@ -6,8 +6,12 @@ #include "src/common/webserver.h" #include "src/common/timerqueue.h" +#include "lwip/apps/sntp.h" +#include "lwip/dns.h" + #include #include //https://github.com/bblanchon/ArduinoJson +#include #define UPTIME_OVERFLOW 4294967295 // Uptime overflow value @@ -20,6 +24,7 @@ struct websettings_t { }; static struct websettings_t *websettings = NULL; +static uint8_t ntpservers = 0; void log_message(char* string); @@ -75,8 +80,6 @@ void getWifiScanResults(int numSsid) { } } - - int getWifiQuality() { if (WiFi.status() != WL_CONNECTED) return -1; @@ -110,17 +113,62 @@ char *getUptime(void) { uint8_t sec = rem % 60; unsigned int len = snprintf_P(NULL, 0, PSTR("%d day%s %d hour%s %d minute%s %d second%s"), d, (d == 1) ? "" : "s", h, (h == 1) ? "" : "s", m, (m == 1) ? "" : "s", sec, (sec == 1) ? "" : "s"); + char *str = (char *)malloc(len + 2); if (str == NULL) { Serial1.printf("Out of memory %s:#%d\n", __FUNCTION__, __LINE__); ESP.restart(); exit(-1); } + memset(str, 0, len + 2); snprintf_P(str, len + 1, PSTR("%d day%s %d hour%s %d minute%s %d second%s"), d, (d == 1) ? "" : "s", h, (h == 1) ? "" : "s", m, (m == 1) ? "" : "s", sec, (sec == 1) ? "" : "s"); return str; } +void ntp_dns_found(const char *name, const ip4_addr *addr, void *arg) { + sntp_stop(); + sntp_setserver(ntpservers++, addr); + sntp_init(); +} + +void ntpReload(settingsStruct *heishamonSettings) { + ip_addr_t addr; + uint8_t len = strlen(heishamonSettings->ntp_servers); + uint8_t ptr = 0, i = 0; + ntpservers = 0; + for (i = 0; i <= len; i++) { + if (heishamonSettings->ntp_servers[i] == ',') { + heishamonSettings->ntp_servers[i] = 0; + + uint8_t err = dns_gethostbyname(&heishamonSettings->ntp_servers[ptr], &addr, ntp_dns_found, 0); + if (err == ERR_OK) { + sntp_stop(); + sntp_setserver(ntpservers++, &addr); + sntp_init(); + } + heishamonSettings->ntp_servers[i++] = ','; + while (heishamonSettings->ntp_servers[i] == ' ') { + i++; + } + ptr = i; + } + } + + uint8_t err = dns_gethostbyname(&heishamonSettings->ntp_servers[ptr], &addr, ntp_dns_found, 0); + if (err == ERR_OK) { + sntp_stop(); + sntp_setserver(ntpservers++, &addr); + sntp_init(); + } + + sntp_stop(); + tzStruct tz; + memcpy_P(&tz, &tzdata[heishamonSettings->timezone], sizeof(tz)); + setTZ(tz.value); + sntp_init(); +} + void loadSettings(settingsStruct *heishamonSettings) { //read configuration from FS json log_message((char *)"mounting FS..."); @@ -155,6 +203,8 @@ void loadSettings(settingsStruct *heishamonSettings) { if ( jsonDoc["mqtt_port"] ) strncpy(heishamonSettings->mqtt_port, jsonDoc["mqtt_port"], sizeof(heishamonSettings->mqtt_port)); if ( jsonDoc["mqtt_username"] ) strncpy(heishamonSettings->mqtt_username, jsonDoc["mqtt_username"], sizeof(heishamonSettings->mqtt_username)); if ( jsonDoc["mqtt_password"] ) strncpy(heishamonSettings->mqtt_password, jsonDoc["mqtt_password"], sizeof(heishamonSettings->mqtt_password)); + if ( jsonDoc["ntp_servers"] ) strncpy(heishamonSettings->ntp_servers, jsonDoc["ntp_servers"], sizeof(heishamonSettings->ntp_servers)); + if ( jsonDoc["timezone"]) heishamonSettings->timezone = jsonDoc["timezone"]; heishamonSettings->use_1wire = ( jsonDoc["use_1wire"] == "enabled" ) ? true : false; heishamonSettings->use_s0 = ( jsonDoc["use_s0"] == "enabled" ) ? true : false; heishamonSettings->listenonly = ( jsonDoc["listenonly"] == "enabled" ) ? true : false; @@ -182,6 +232,7 @@ void loadSettings(settingsStruct *heishamonSettings) { if (jsonDoc["s0_2_interval"] ) heishamonSettings->s0Settings[1].lowerPowerInterval = jsonDoc["s0_2_interval"]; if (jsonDoc["s0_2_minpulsewidth"]) heishamonSettings->s0Settings[1].minimalPulseWidth = jsonDoc["s0_2_minpulsewidth"]; if (jsonDoc["s0_2_maxpulsewidth"]) heishamonSettings->s0Settings[1].maximalPulseWidth = jsonDoc["s0_2_maxpulsewidth"]; + ntpReload(heishamonSettings); } else { log_message((char *)"Failed to load json config, forcing config reset."); WiFi.persistent(true); @@ -394,6 +445,10 @@ int saveSettings(struct webserver_t *client, settingsStruct *heishamonSettings) jsonDoc["logSerial1"] = tmp->value; } else if (strcmp(tmp->name.c_str(), "optionalPCB") == 0) { jsonDoc["optionalPCB"] = tmp->value; + } else if (strcmp(tmp->name.c_str(), "ntp_servers") == 0) { + jsonDoc["ntp_servers"] = tmp->value; + } else if (strcmp(tmp->name.c_str(), "timezone") == 0) { + jsonDoc["timezone"] = tmp->value; } else if (strcmp(tmp->name.c_str(), "waitTime") == 0) { jsonDoc["waitTime"] = tmp->value; } else if (strcmp(tmp->name.c_str(), "waitDallasTime") == 0) { @@ -471,8 +526,6 @@ int saveSettings(struct webserver_t *client, settingsStruct *heishamonSettings) jsonDoc["wifi_password"] = String(wifi_password); } - serializeJson(jsonDoc, Serial); - saveJsonToConfig(jsonDoc); //save to config file loadSettings(heishamonSettings); //load config file to current settings @@ -553,26 +606,35 @@ int settingsNewPassword(struct webserver_t *client, settingsStruct *heishamonSet } int settingsReconnectWifi(struct webserver_t *client, settingsStruct *heishamonSettings) { - switch (client->content) { - case 0: { - webserver_send(client, 200, (char *)"text/html", 0); - webserver_send_content_P(client, webHeader, strlen_P(webHeader)); - webserver_send_content_P(client, webCSS, strlen_P(webCSS)); - webserver_send_content_P(client, webBodyStart, strlen_P(webBodyStart)); - } break; - case 1: { - webserver_send_content_P(client, webBodySettings1, strlen_P(webBodySettings1)); - webserver_send_content_P(client, settingsForm, strlen_P(settingsForm)); - webserver_send_content_P(client, menuJS, strlen_P(menuJS)); - } break; - case 2: { - webserver_send_content_P(client, webBodySettingsNewWifiWarning, strlen_P(webBodySettingsNewWifiWarning)); - webserver_send_content_P(client, refreshMeta, strlen_P(refreshMeta)); - webserver_send_content_P(client, webFooter, strlen_P(webFooter)); - } break; - case 3: { - setupWifi(heishamonSettings); - } break; + uint16_t size = sizeof(tzdata) / sizeof(tzdata[0]); + if (client->content == 0) { + webserver_send(client, 200, (char *)"text/html", 0); + webserver_send_content_P(client, webHeader, strlen_P(webHeader)); + webserver_send_content_P(client, webCSS, strlen_P(webCSS)); + webserver_send_content_P(client, webBodyStart, strlen_P(webBodyStart)); + } else if (client->content == 1) { + webserver_send_content_P(client, webBodySettings1, strlen_P(webBodySettings1)); + webserver_send_content_P(client, settingsForm1, strlen_P(settingsForm1)); + } else if (client->content >= 2 && client->content < size + 2) { + webserver_send_content_P(client, PSTR(""), 9); + } else if (client->content == size + 2) { + webserver_send_content_P(client, settingsForm2, strlen_P(settingsForm2)); + webserver_send_content_P(client, menuJS, strlen_P(menuJS)); + } else if (client->content == size + 3) { + webserver_send_content_P(client, webBodySettingsNewWifiWarning, strlen_P(webBodySettingsNewWifiWarning)); + webserver_send_content_P(client, refreshMeta, strlen_P(refreshMeta)); + webserver_send_content_P(client, webFooter, strlen_P(webFooter)); + } else if (client->content == size + 4) { + setupWifi(heishamonSettings); } return 0; @@ -608,11 +670,23 @@ int getSettings(struct webserver_t *client, settingsStruct *heishamonSettings) { case 4: { webserver_send_content_P(client, PSTR("\",\"mqtt_password\":\""), 19); webserver_send_content(client, heishamonSettings->mqtt_password, strlen(heishamonSettings->mqtt_password)); - webserver_send_content_P(client, PSTR("\",\"waitTime\":"), 13); + webserver_send_content_P(client, PSTR("\",\"ntp_servers\":\""), 17); + webserver_send_content(client, heishamonSettings->ntp_servers, strlen(heishamonSettings->ntp_servers)); + webserver_send_content_P(client, PSTR("\",\"timezone\":"), 13); - char str[20]; - itoa(heishamonSettings->waitTime, str, 10); - webserver_send_content(client, str, strlen(str)); + { + char str[20]; + itoa(heishamonSettings->timezone, str, 10); + webserver_send_content(client, str, strlen(str)); + } + + webserver_send_content_P(client, PSTR(",\"waitTime\":"), 12); + + { + char str[20]; + itoa(heishamonSettings->waitTime, str, 10); + webserver_send_content(client, str, strlen(str)); + } } break; case 5: { char str[20]; @@ -756,25 +830,36 @@ int getSettings(struct webserver_t *client, settingsStruct *heishamonSettings) { } int handleSettings(struct webserver_t *client) { - switch (client->content) { - case 0: { - webserver_send(client, 200, (char *)"text/html", 0); - webserver_send_content_P(client, webHeader, strlen_P(webHeader)); - webserver_send_content_P(client, webCSS, strlen_P(webCSS)); - webserver_send_content_P(client, webBodyStart, strlen_P(webBodyStart)); - webserver_send_content_P(client, webBodySettings1, strlen_P(webBodySettings1)); - } break; - case 1: { - webserver_send_content_P(client, settingsForm, strlen_P(settingsForm)); - webserver_send_content_P(client, menuJS, strlen_P(menuJS)); - webserver_send_content_P(client, settingsJS, strlen_P(settingsJS)); - webserver_send_content_P(client, populatescanwifiJS, strlen_P(populatescanwifiJS)); - } break; - case 2: { - webserver_send_content_P(client, changewifissidJS, strlen_P(changewifissidJS)); - webserver_send_content_P(client, populategetsettingsJS, strlen_P(populategetsettingsJS)); - webserver_send_content_P(client, webFooter, strlen_P(webFooter)); - } break; + + uint16_t size = sizeof(tzdata) / sizeof(tzdata[0]); + if (client->content == 0) { + webserver_send(client, 200, (char *)"text/html", 0); + webserver_send_content_P(client, webHeader, strlen_P(webHeader)); + webserver_send_content_P(client, webCSS, strlen_P(webCSS)); + webserver_send_content_P(client, webBodyStart, strlen_P(webBodyStart)); + } else if (client->content == 1) { + webserver_send_content_P(client, webBodySettings1, strlen_P(webBodySettings1)); + webserver_send_content_P(client, settingsForm1, strlen_P(settingsForm1)); + } else if (client->content >= 2 && client->content < size + 2) { + webserver_send_content_P(client, PSTR(""), 9); + } else if (client->content == size + 2) { + webserver_send_content_P(client, settingsForm2, strlen_P(settingsForm2)); + webserver_send_content_P(client, menuJS, strlen_P(menuJS)); + webserver_send_content_P(client, settingsJS, strlen_P(settingsJS)); + webserver_send_content_P(client, populatescanwifiJS, strlen_P(populatescanwifiJS)); + } else if (client->content == size + 3) { + webserver_send_content_P(client, changewifissidJS, strlen_P(changewifissidJS)); + webserver_send_content_P(client, populategetsettingsJS, strlen_P(populategetsettingsJS)); + webserver_send_content_P(client, webFooter, strlen_P(webFooter)); } return 0; @@ -808,6 +893,7 @@ int handleDebug(struct webserver_t *client, char *hex, byte hex_len) { webserver_send_content(client, log_msg, len); } } + return 0; } diff --git a/HeishaMon/webfunctions.h b/HeishaMon/webfunctions.h index a5e1325c..a304d1f7 100755 --- a/HeishaMon/webfunctions.h +++ b/HeishaMon/webfunctions.h @@ -16,11 +16,12 @@ void log_message(char* string); static IPAddress apIP(192, 168, 4, 1); struct settingsStruct { - unsigned int waitTime = 5; // how often data is read from heatpump - unsigned int waitDallasTime = 5; // how often temps are read from 1wire - unsigned int dallasResolution = 12; // dallas temp resolution (9 to 12) - unsigned int updateAllTime = 300; // how often all data is resend to mqtt - unsigned int updataAllDallasTime = 300; //how often all 1wire data is resent to mqtt + uint16_t waitTime = 5; // how often data is read from heatpump + uint16_t waitDallasTime = 5; // how often temps are read from 1wire + uint16_t dallasResolution = 12; // dallas temp resolution (9 to 12) + uint16_t updateAllTime = 300; // how often all data is resend to mqtt + uint16_t updataAllDallasTime = 300; //how often all 1wire data is resent to mqtt + uint16_t timezone = 0; const char* update_path = "/firmware"; const char* update_username = "admin"; @@ -33,6 +34,7 @@ struct settingsStruct { char mqtt_username[64]; char mqtt_password[64]; char mqtt_topic_base[40] = "panasonic_heat_pump"; + char ntp_servers[254] = "pool.ntp.org"; bool listenonly = false; //listen only so heishamon can be installed parallel to cz-taw1, set commands will not work though bool optionalPCB = false; //do we emulate an optional PCB? @@ -52,6 +54,7 @@ char *getUptime(void); void setupWifi(settingsStruct *heishamonSettings); int getWifiQuality(void); int getFreeMemory(void); +void ntpReload(settingsStruct *heishamonSettings); void log_message(char *string); int8_t webserver_cb(struct webserver_t *client, void *data); From b2f08452a6d12c650e99e0511bb8eb237a6ccd3e Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Thu, 30 Dec 2021 13:40:07 +0100 Subject: [PATCH 42/75] Optimize usage of actdata and actoptdata (#87) * move from actdata string array to char array * small change in string usage * actoptdata improvement * some small fixes --- HeishaMon/HeishaMon.ino | 18 +++-- HeishaMon/decode.cpp | 147 ++++++++++++++++++++----------------- HeishaMon/decode.h | 5 +- HeishaMon/webfunctions.cpp | 20 ++--- HeishaMon/webfunctions.h | 4 +- 5 files changed, 105 insertions(+), 89 deletions(-) diff --git a/HeishaMon/HeishaMon.ino b/HeishaMon/HeishaMon.ino index d04e9d2e..9ce70da7 100755 --- a/HeishaMon/HeishaMon.ino +++ b/HeishaMon/HeishaMon.ino @@ -65,9 +65,11 @@ static int uploadpercentage = 0; char data[MAXDATASIZE] = { '\0' }; byte data_length = 0; -// store actual data in an String array -String actData[NUMBER_OF_TOPICS]; -String actOptData[NUMBER_OF_OPT_TOPICS]; +// store actual data +#define DATASIZE 203 +char actData[DATASIZE] = { '\0' }; +#define OPTDATASIZE 20 +char actOptData[OPTDATASIZE] = { '\0' }; String RESTmsg = ""; // log message to sprintf to @@ -316,15 +318,17 @@ bool readSerial() log_message((char*)"Checksum and header received ok!"); goodreads++; - if (data_length == 203) { //for now only return true for this datagram because we can not decode the shorter datagram yet - data_length = 0; + if (data_length == DATASIZE) { //decode the normal data decode_heatpump_data(data, actData, mqtt_client, log_message, heishamonSettings.mqtt_topic_base, heishamonSettings.updateAllTime); + memcpy(actData,data,DATASIZE); + data_length = 0; return true; } - else if (data_length == 20 ) { //optional pcb acknowledge answer + else if (data_length == OPTDATASIZE ) { //optional pcb acknowledge answer log_message((char*)"Received optional PCB ack answer. Decoding this in OPT topics."); - data_length = 0; decode_optional_heatpump_data(data, actOptData, mqtt_client, log_message, heishamonSettings.mqtt_topic_base, heishamonSettings.updateAllTime); + memcpy(actOptData,data,OPTDATASIZE); + data_length = 0; return true; } else { diff --git a/HeishaMon/decode.cpp b/HeishaMon/decode.cpp index 9e7d37bd..a718db6d 100644 --- a/HeishaMon/decode.cpp +++ b/HeishaMon/decode.cpp @@ -133,51 +133,86 @@ void resetlastalldatatime() { lastalldatatime = 0; } +String getDataValue(char* data, unsigned int Topic_Number) { + String Topic_Value; + byte Input_Byte; + switch (Topic_Number) { //switch on topic numbers, some have special needs + case 1: + Topic_Value = getPumpFlow(data); + break; + case 11: + Topic_Value = String(word(data[183], data[182]) - 1); + break; + case 12: + Topic_Value = String(word(data[180], data[179]) - 1); + break; + case 90: + Topic_Value = String(word(data[186], data[185]) - 1); + break; + case 91: + Topic_Value = String(word(data[189], data[188]) - 1); + break; + case 44: + Topic_Value = getErrorInfo(data); + break; + case 92: + Topic_Value = getModel(data); + break; + default: + byte cpy; + memcpy_P(&cpy, &topicBytes[Topic_Number], sizeof(byte)); + Input_Byte = data[cpy]; + Topic_Value = topicFunctions[Topic_Number](Input_Byte); + break; + } + return Topic_Value; +} + +String getOptDataValue(char* data, unsigned int Topic_Number) { + String Topic_Value; + switch (Topic_Number) { //switch on topic numbers, some have special needs + case 0: + Topic_Value = String(data[4] >> 7); + break; + case 1: + Topic_Value = String((data[4] >> 5) & 0b11); + break; + case 2: + Topic_Value = String((data[4] >> 4) & 0b1); + break; + case 3: + Topic_Value = String((data[4] >> 2) & 0b11); + break; + case 4: + Topic_Value = String((data[4] >> 1) & 0b1); + break; + case 5: + Topic_Value = String((data[4] >> 0) & 0b1); + break; + case 6: + Topic_Value = String((data[5] >> 0) & 0b1); + break; + default: + break; + } + return Topic_Value; +} + // Decode //////////////////////////////////////////////////////////////////////////// -void decode_heatpump_data(char* data, String actData[], PubSubClient &mqtt_client, void (*log_message)(char*), char* mqtt_topic_base, unsigned int updateAllTime) { - char log_msg[256]; - char mqtt_topic[256]; +void decode_heatpump_data(char* data, char* actData, PubSubClient &mqtt_client, void (*log_message)(char*), char* mqtt_topic_base, unsigned int updateAllTime) { bool updatenow = false; - for (unsigned int Topic_Number = 0 ; Topic_Number < NUMBER_OF_TOPICS ; Topic_Number++) { - byte Input_Byte; String Topic_Value; - switch (Topic_Number) { //switch on topic numbers, some have special needs - case 1: - Topic_Value = getPumpFlow(data); - break; - case 11: - Topic_Value = String(word(data[183], data[182]) - 1); - break; - case 12: - Topic_Value = String(word(data[180], data[179]) - 1); - break; - case 90: - Topic_Value = String(word(data[186], data[185]) - 1); - break; - case 91: - Topic_Value = String(word(data[189], data[188]) - 1); - break; - case 44: - Topic_Value = getErrorInfo(data); - break; - case 92: - Topic_Value = getModel(data); - break; - default: - byte cpy; - memcpy_P(&cpy, &topicBytes[Topic_Number], sizeof(byte)); - Input_Byte = data[cpy]; - Topic_Value = topicFunctions[Topic_Number](Input_Byte); - break; - } + Topic_Value = getDataValue(data, Topic_Number); + if ((unsigned long)(millis() - lastalldatatime) > (1000 * updateAllTime)) { updatenow = true; - lastalldatatime = millis();; + lastalldatatime = millis(); } - if ((updatenow) || ( actData[Topic_Number] != Topic_Value )) { - actData[Topic_Number] = Topic_Value; + if ((updatenow) || ( getDataValue(actData, Topic_Number) != Topic_Value )) { + char log_msg[256]; + char mqtt_topic[256]; sprintf_P(log_msg, PSTR("received TOP%d %s: %s"), Topic_Number, topics[Topic_Number], Topic_Value.c_str()); log_message(log_msg); sprintf(mqtt_topic, "%s/%s/%s", mqtt_topic_base, mqtt_topic_values, topics[Topic_Number]); @@ -186,46 +221,20 @@ void decode_heatpump_data(char* data, String actData[], PubSubClient &mqtt_clien } } -void decode_optional_heatpump_data(char* data, String actOptData[], PubSubClient & mqtt_client, void (*log_message)(char*), char* mqtt_topic_base, unsigned int updateAllTime) { - char log_msg[256]; - char mqtt_topic[256]; +void decode_optional_heatpump_data(char* data, char* actOptData, PubSubClient & mqtt_client, void (*log_message)(char*), char* mqtt_topic_base, unsigned int updateAllTime) { bool updatenow = false; - for (unsigned int Topic_Number = 0 ; Topic_Number < NUMBER_OF_OPT_TOPICS ; Topic_Number++) { - byte Input_Byte; String Topic_Value; - switch (Topic_Number) { //switch on topic numbers, some have special needs - case 0: - Topic_Value = String(data[4] >> 7); - break; - case 1: - Topic_Value = String((data[4] >> 5) & 0b11); - break; - case 2: - Topic_Value = String((data[4] >> 4) & 0b1); - break; - case 3: - Topic_Value = String((data[4] >> 2) & 0b11); - break; - case 4: - Topic_Value = String((data[4] >> 1) & 0b1); - break; - case 5: - Topic_Value = String((data[4] >> 0) & 0b1); - break; - case 6: - Topic_Value = String((data[5] >> 0) & 0b1); - break; - default: - break; - } + Topic_Value = getOptDataValue(data, Topic_Number); + if ((unsigned long)(millis() - lastalloptdatatime) > (1000 * updateAllTime)) { updatenow = true; lastalloptdatatime = millis(); } - if ((updatenow) || ( actOptData[Topic_Number] != Topic_Value )) { - actOptData[Topic_Number] = Topic_Value; + if ((updatenow) || ( getDataValue(actOptData, Topic_Number) != Topic_Value )) { + char log_msg[256]; + char mqtt_topic[256]; sprintf_P(log_msg, PSTR("received OPT%d %s: %s"), Topic_Number, optTopics[Topic_Number], Topic_Value.c_str()); log_message(log_msg); sprintf(mqtt_topic, "%s/%s/%s", mqtt_topic_base, mqtt_topic_pcbvalues, optTopics[Topic_Number]); diff --git a/HeishaMon/decode.h b/HeishaMon/decode.h index c2a8c0b1..1c040dcd 100644 --- a/HeishaMon/decode.h +++ b/HeishaMon/decode.h @@ -7,8 +7,9 @@ void resetlastalldatatime(); -void decode_heatpump_data(char* data, String actData[], PubSubClient &mqtt_client, void (*log_message)(char*), char* mqtt_topic_base, unsigned int updateAllTime); -void decode_optional_heatpump_data(char* data, String actOptData[], PubSubClient &mqtt_client, void (*log_message)(char*), char* mqtt_topic_base, unsigned int updateAllTime); +String getDataValue(char* data, unsigned int Topic_Number); +void decode_heatpump_data(char* data, char* actData, PubSubClient &mqtt_client, void (*log_message)(char*), char* mqtt_topic_base, unsigned int updateAllTime); +void decode_optional_heatpump_data(char* data, char* actOptDat, PubSubClient &mqtt_client, void (*log_message)(char*), char* mqtt_topic_base, unsigned int updateAllTime); String unknown(byte input); String getBit1and2(byte input); diff --git a/HeishaMon/webfunctions.cpp b/HeishaMon/webfunctions.cpp index 61f0448a..a11c3d98 100755 --- a/HeishaMon/webfunctions.cpp +++ b/HeishaMon/webfunctions.cpp @@ -983,7 +983,7 @@ int handleRoot(struct webserver_t *client, float readpercentage, int mqttReconne return 0; } -int handleTableRefresh(struct webserver_t *client, String actData[]) { +int handleTableRefresh(struct webserver_t *client, char* actData) { int ret = 0; if (client->route == 11) { @@ -1007,7 +1007,7 @@ int handleTableRefresh(struct webserver_t *client, String actData[]) { if (strcmp_P(valuetext, topicDescription[topic][0]) == 0) { topicdesc = topicDescription[topic][1]; } else { - int value = actData[topic].toInt(); + int value = getDataValue(actData, topic).toInt(); int maxvalue = atoi(topicDescription[topic][0]); if ((value < 0) || (value > maxvalue)) { topicdesc = _unknown; @@ -1025,10 +1025,11 @@ int handleTableRefresh(struct webserver_t *client, String actData[]) { webserver_send_content_P(client, PSTR(""), 9); webserver_send_content_P(client, topics[topic], strlen_P(topics[topic])); - webserver_send_content_P(client, PSTR(""), 9); + webserver_send_content_P(client, PSTR(""), 9); { - char *str = (char *)actData[topic].c_str(); + String dataValue = getDataValue(actData, topic); + char* str = (char *)dataValue.c_str(); webserver_send_content(client, str, strlen(str)); } @@ -1048,18 +1049,18 @@ int handleTableRefresh(struct webserver_t *client, String actData[]) { return 0; } -int handleJsonOutput(struct webserver_t *client, String actData[]) { +int handleJsonOutput(struct webserver_t *client, char* actData) { if (client->content == 0) { webserver_send(client, 200, (char *)"application/json", 0); webserver_send_content_P(client, PSTR("{\"heatpump\":["), 13); } else if (client->content < NUMBER_OF_TOPICS) { - for (uint8_t topic = client->content; topic < NUMBER_OF_TOPICS && topic < client->content + 4; topic++) { + for (uint8_t topic = client->content - 1; topic < NUMBER_OF_TOPICS && topic < client->content + 4; topic++) { PGM_P topicdesc; const char *valuetext = "value"; if (strcmp_P(valuetext, topicDescription[topic][0]) == 0) { topicdesc = topicDescription[topic][1]; } else { - int value = actData[topic].toInt(); + int value = getDataValue(actData, topic).toInt(); int maxvalue = atoi(topicDescription[topic][0]); if ((value < 0) || (value > maxvalue)) { topicdesc = _unknown; @@ -1083,8 +1084,9 @@ int handleJsonOutput(struct webserver_t *client, String actData[]) { webserver_send_content_P(client, PSTR("\",\"Value\":\""), 11); { - char *str = (char *)actData[topic].c_str(); - webserver_send_content_P(client, str, strlen(str)); + String dataValue = getDataValue(actData, topic); + char* str = (char *)dataValue.c_str(); + webserver_send_content(client, str, strlen(str)); } webserver_send_content_P(client, PSTR("\",\"Description\":\""), 17); diff --git a/HeishaMon/webfunctions.h b/HeishaMon/webfunctions.h index a304d1f7..dc35af81 100755 --- a/HeishaMon/webfunctions.h +++ b/HeishaMon/webfunctions.h @@ -60,8 +60,8 @@ void log_message(char *string); int8_t webserver_cb(struct webserver_t *client, void *data); void getWifiScanResults(int numSsid); int handleRoot(struct webserver_t *client, float readpercentage, int mqttReconnects, settingsStruct *heishamonSettings); -int handleTableRefresh(struct webserver_t *client, String actData[]); -int handleJsonOutput(struct webserver_t *client, String actData[]); +int handleTableRefresh(struct webserver_t *client, char* actData); +int handleJsonOutput(struct webserver_t *client, char* actData); int handleFactoryReset(struct webserver_t *client); int handleReboot(struct webserver_t *client); int handleDebug(struct webserver_t *client, char *hex, byte hex_len); From 0a1e1c1a1f48f68878ce30d6256d3787c79e1b1e Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Thu, 30 Dec 2021 13:40:52 +0100 Subject: [PATCH 43/75] MD5 checksum for firmware upload (#85) * add md5 upload check * autofill md hash from filename Format: randomname-versionnumber-md5hash.bin * Actions (#86) * Update main.yml * Update main.yml * make sure only one uploader is active --- .github/workflows/main.yml | 8 +++-- HeishaMon/HeishaMon.ino | 60 +++++++++++++++++++++++++------------- HeishaMon/htmlcode.h | 23 ++++++++++++--- 3 files changed, 64 insertions(+), 27 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dd18523c..440a0786 100755 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,10 +35,14 @@ jobs: run: arduino-cli lib install ringbuffer pubsubclient doubleresetdetect arduinojson dallastemperature onewire WebSockets - name: Compile Sketch - run: cd HeishaMon && arduino-cli compile --fqbn=esp8266:esp8266:d1_mini:xtal=160,vt=flash,ssl=all,eesz=4M2M,ip=lm2f,dbg=Disabled,lvl=None____,wipe=none,baud=921600 --vid-pid=1A86_7523 --warnings=none --verbose HeishaMon.ino + run: cd HeishaMon && arduino-cli compile --output-dir . --fqbn=esp8266:esp8266:d1_mini:xtal=160,vt=flash,ssl=all,eesz=4M2M,ip=lm2f,dbg=Disabled,lvl=None____,wipe=none,baud=921600 --vid-pid=1A86_7523 --warnings=none --verbose HeishaMon.ino + + - name: Add MD5 checksum + run: cd HeishaMon && MD5=`md5sum HeishaMon.ino.bin | cut -d\ -f1` && mv HeishaMon.ino.bin HeishaMon-alpha-$MD5.bin + shell: bash - name: Upload artifacts uses: actions/upload-artifact@v2 with: name: HeishaMon.ino.bin - path: C:\\Users\\runneradmin\\AppData\\Local\\Temp\\arduino-sketch-*\\*.ino.bin + path: HeishaMon/HeishaMon-*.bin diff --git a/HeishaMon/HeishaMon.ino b/HeishaMon/HeishaMon.ino index 9ce70da7..dd590829 100755 --- a/HeishaMon/HeishaMon.ino +++ b/HeishaMon/HeishaMon.ino @@ -497,11 +497,18 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { if (strcmp((char *)dat, "/savesettings") == 0) { client->route = 110; } else if (strcmp((char *)dat, "/firmware") == 0) { - client->route = 150; - - Update.runAsync(true); - if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { - Update.printError(Serial1); + if (!Update.isRunning()) { + Update.runAsync(true); + if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { + Update.printError(Serial1); + return -1; + } else { + client->route = 150; + } + } else { + Serial1.println("New firmware update client, while previous isn't finished yet! Assume broken connection, abort!"); + Update.end(); + return -1; } } else { return -1; @@ -535,7 +542,7 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { unsigned int len = 0; memset(&cpy, 0, args->len + 1); - snprintf((char *)&cpy, args->len, "%.*s", args->len, args->value); + snprintf((char *)&cpy, args->len + 1, "%.*s", args->len, args->value); for (uint8_t x = 0; x < sizeof(commands) / sizeof(commands[0]); x++) { if (strcmp((char *)args->name, commands[x].name) == 0) { @@ -564,14 +571,28 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { return cacheSettings(client, args); } break; case 150: { - if (uploadpercentage != (unsigned int)(((float)client->readlen / (float)client->totallen) * 100)) { - uploadpercentage = (unsigned int)(((float)client->readlen / (float)client->totallen) * 100); - sprintf_P(log_msg, PSTR("Uploading new firmware: %d%%"), uploadpercentage); - log_message(log_msg); - } - if (!Update.hasError() && strcmp((char *)args->name, "firmware") == 0) { - if (Update.write((uint8_t *)args->value, args->len) != args->len) { - Update.printError(Serial1); + if (!Update.hasError()) { + if (strcmp((char *)args->name, "md5") == 0) { + + char md5[args->len + 1]; + memset(&md5, 0, args->len + 1); + snprintf((char *)&md5, args->len + 1, "%.*s", args->len, args->value); + sprintf_P(log_msg, PSTR("Firmware MD5 expected: %s"), md5); + log_message(log_msg); + if (!Update.setMD5(md5)) { + log_message((char *)"Failed to set expected update file MD5!"); + Update.end(false); + } + } else if (!Update.hasError() && strcmp((char *)args->name, "firmware") == 0) { + if (Update.write((uint8_t *)args->value, args->len) != args->len) { + Update.printError(Serial1); + } else { + if (uploadpercentage != (unsigned int)(((float)client->readlen / (float)client->totallen) * 20)) { + uploadpercentage = (unsigned int)(((float)client->readlen / (float)client->totallen) * 20); + sprintf_P(log_msg, PSTR("Uploading new firmware: %d%%"), uploadpercentage * 5); + log_message(log_msg); + } + } } } } break; @@ -653,14 +674,11 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { return showFirmware(client); } break; case 150: { - if (uploadpercentage != (unsigned int)(((float)client->readlen / (float)client->totallen) * 100)) { - uploadpercentage = (unsigned int)(((float)client->readlen / (float)client->totallen) * 100); - sprintf_P(log_msg, PSTR("Uploading new firmware: %d%%"), uploadpercentage); - log_message(log_msg); - } if (Update.end(true)) { - log_message((char *)"Update Success"); - timerqueue_insert(15, 0, -2); // Start reboot sequence + String updateHash = Update.md5String(); + sprintf_P(log_msg, PSTR("Uploading success. MD5: %s"), updateHash.c_str()); + log_message(log_msg); + timerqueue_insert(2, 0, -2); // Start reboot sequence return showFirmwareSuccess(client); } else { Update.printError(Serial1); diff --git a/HeishaMon/htmlcode.h b/HeishaMon/htmlcode.h index f0650d6e..d02174ba 100644 --- a/HeishaMon/htmlcode.h +++ b/HeishaMon/htmlcode.h @@ -942,11 +942,26 @@ static const char showFirmwarePage[] PROGMEM = "Toggle mqtt log" "Toggle hexdump log" "
" + "" "
" - "
" - " Firmware:
" - "

" - " " + " " + "

Firmware:

" + "

" + "

" + " " "
" "
"; From 6b2933e6451e216cb3bbb601cd0a54cf2cb0b2bd Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Thu, 30 Dec 2021 13:42:13 +0100 Subject: [PATCH 44/75] hard update version info in main --- HeishaMon/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HeishaMon/version.h b/HeishaMon/version.h index 252c1069..9512682f 100644 --- a/HeishaMon/version.h +++ b/HeishaMon/version.h @@ -1 +1 @@ -static const char* heishamon_version = "2.2-iy-2"; +static const char* heishamon_version = "2.2-iy-3"; From 015bb0646856e93451d2b6f2bd8fb444079a2396 Mon Sep 17 00:00:00 2001 From: CurlyMoo Date: Thu, 30 Dec 2021 13:44:14 +0100 Subject: [PATCH 45/75] Adding Pump_Flowrate_Mode for J series (#80) * Try adding Maximum_Pump_Speed for J series * change topic name and add documentation Co-authored-by: IgorYbema --- HeishaMon/decode.h | 7 ++++++- MQTT-Topics.md | 1 + ProtocolByteDecrypt.md | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/HeishaMon/decode.h b/HeishaMon/decode.h index 1c040dcd..9c82e2e2 100644 --- a/HeishaMon/decode.h +++ b/HeishaMon/decode.h @@ -84,7 +84,7 @@ static const byte knownModels[sizeof(Model) / sizeof(Model[0])][10] = { //stores 0x62, 0xD2, 0x0B, 0x41, 0x54, 0x32, 0xD2, 0x0C, 0x45, 0x55, }; -#define NUMBER_OF_TOPICS 106 //last topic number + 1 +#define NUMBER_OF_TOPICS 107 //last topic number + 1 #define NUMBER_OF_OPT_TOPICS 7 //last topic number + 1 static const char optTopics[][20] PROGMEM = { @@ -204,6 +204,7 @@ static const char topics[][40] PROGMEM = { "Solar_Off_Delta", //TOP103 "Solar_Frost_Protection", //TOP104 "Solar_High_Limit", //TOP105 + "Pump_Flowrate_Mode", //TOP106 }; static const byte topicBytes[] PROGMEM = { //can store the index as byte (8-bit unsigned humber) as there aren't more then 255 bytes (actually only 203 bytes) to decode @@ -313,6 +314,7 @@ static const byte topicBytes[] PROGMEM = { //can store the index as byte (8-bit 62, //TOP103 63, //TOP104 64, //TOP105 + 29, //TOP106 }; typedef String (*topicFP)(byte); @@ -424,12 +426,14 @@ static const topicFP topicFunctions[] PROGMEM = { getIntMinus128, //TOP103 getIntMinus128, //TOP104 getIntMinus128, //TOP105 + getBit3and4, //TOP106 }; static const char *DisabledEnabled[] PROGMEM = {"2", "Disabled", "Enabled"}; static const char *BlockedFree[] PROGMEM = {"2", "Blocked", "Free"}; static const char *OffOn[] PROGMEM = {"2", "Off", "On"}; static const char *InactiveActive[] PROGMEM = {"2", "Inactive", "Active"}; +static const char *PumpFlowRateMode[] PROGMEM = {"2", "DeltaT", "Max flow"}; static const char *HolidayState[] PROGMEM = {"3", "Off", "Scheduled", "Active"}; static const char *OpModeDesc[] PROGMEM = {"9", "Heat", "Cool", "Auto(heat)", "DHW", "Heat+DHW", "Cool+DHW", "Auto(heat)+DHW", "Auto(cool)", "Auto(cool)+DHW"}; static const char *Powerfulmode[] PROGMEM = {"4", "Off", "30min", "60min", "90min"}; @@ -559,4 +563,5 @@ static const char **topicDescription[] PROGMEM = { Kelvin, //TOP103 Celsius, //TOP104 Celsius, //TOP105 + PumpFlowRateMode,//TOP106 }; diff --git a/MQTT-Topics.md b/MQTT-Topics.md index 857b169b..058ba714 100644 --- a/MQTT-Topics.md +++ b/MQTT-Topics.md @@ -122,6 +122,7 @@ TOP102 | main/Solar_On_Delta | Solar heating delta on TOP103 | main/Solar_Off_Delta | solar heating delta off TOP104 | main/Solar_Frost_Protection | Solar frost protection temp TOP105 | main/Solar_High_Limit | Solar max temp limit +TOP106 | main/Pump_Flowrate_mode | Settings for pump flow rate (0=DeltaT, 1=Maximum flow, J-series only) diff --git a/ProtocolByteDecrypt.md b/ProtocolByteDecrypt.md index d74f1fa9..81d2219d 100644 --- a/ProtocolByteDecrypt.md +++ b/ProtocolByteDecrypt.md @@ -31,7 +31,7 @@ | TOP | 26 | 55 | (hex) Biwalent Off=55, Biwalent alternative =56, Biwalent parallel=5A | Biwalent settings | | TOP | 27 | 05 | SG Ready Control on/off (bit5and6) ,Demand Control on/off (bit7and8) | SG Ready Control, Demand Control | | TOP76+TOP81 | 28 | 09 | (hex) 09 - Compensation curve heat and direct cool, 05 - both compensation curves , 0a - direct heat and direct cool, 06 - heat direct, cool compensation curve | Operation Setup -Installer -water temperature heating on status and cooling | -| TOP | 29 | 00 | | 3d and 4th bit looks to be setting for J-series deltaT or max flow switch | +| TOP106 | 29 | 00 | | 3d and 4th bit setting for J-series deltaT or max flow switch | | TOP | 30 | 00 | | 0 byte | | TOP | 31 | 00 | | 0 byte | | TOP | 32 | 00 | | 0 byte | From af3f404a9650e461f31f8ae967b347c1c74129ec Mon Sep 17 00:00:00 2001 From: CurlyMoo Date: Thu, 30 Dec 2021 14:15:58 +0100 Subject: [PATCH 46/75] Various webserver improvements (#84) ```c -----------------------------354617515641648399072235817744\r\n Content-Disposition: form-data; name="rules"\r\n \r\n on ds18b20#28610695f0013c42 then\r\n #foo = ds18b20#28610695f0013c42;\r\n end\r\n \r\n on ?setpoint then\r\n #bar = ?setpoint;\r\n end\r\n -----------------------------354617515641648399072235817744--\r\n \r\n ``` The webserver detects `\r\n--` chunks as possible boundary delimiters. However, when a chunks splits the `\r\n` and `--` tokens the `\r\n` is added to the content and not seen as a potential boundary delimiter. This commit fixes that. --- HeishaMon/src/common/webserver.cpp | 99 ++++++++++++++++++++---------- HeishaMon/src/common/webserver.h | 3 +- 2 files changed, 69 insertions(+), 33 deletions(-) diff --git a/HeishaMon/src/common/webserver.cpp b/HeishaMon/src/common/webserver.cpp index 9b0188e2..df0cba2c 100755 --- a/HeishaMon/src/common/webserver.cpp +++ b/HeishaMon/src/common/webserver.cpp @@ -788,6 +788,10 @@ int http_parse_multipart_body(struct webserver_t *client, unsigned char *buf, ui client->buffer[pos+2] == '\r' && client->buffer[pos+3] == '\n') { client->readlen += ((pos+4)-(pos1)); if(client->readlen == client->totallen) { + if(client->boundary != NULL) { + free(client->boundary); + client->boundary = NULL; + } return 0; } else { // Error, content length does not match end boundary @@ -984,6 +988,14 @@ int http_parse_multipart_body(struct webserver_t *client, unsigned char *buf, ui client->ptr -= (pos-(vlen+1)); client->substep = 0; } else if(client->ptr == WEBSERVER_BUFFER_SIZE) { + uint8_t ending = 0; + /* + * Double check that the CR / LN don't belong + * to the boundary delimiter. + */ + if(strncmp((char *)&client->buffer[client->ptr-2], "\r\n", 2) == 0) { + ending = 2; + } if(client->substep == 8) { client->substep = 7; } @@ -996,7 +1008,7 @@ int http_parse_multipart_body(struct webserver_t *client, unsigned char *buf, ui args.name = &client->buffer[0]; args.value = &client->buffer[pos+1]; - args.len = WEBSERVER_BUFFER_SIZE-(pos+1); + args.len = (WEBSERVER_BUFFER_SIZE-ending)-(pos+1); if(client->callback != NULL) { uint8_t ret = client->callback(client, &args); @@ -1005,8 +1017,12 @@ int http_parse_multipart_body(struct webserver_t *client, unsigned char *buf, ui } } client->buffer[pos] = '='; - client->readlen += (client->ptr-(pos+1)); - client->ptr = pos+1; + if(ending == 2) { + client->buffer[pos+1] = '\r'; + client->buffer[pos+2] = '\n'; + } + client->readlen += ((client->ptr-(pos+1))-ending); + client->ptr = pos+1+ending; } else { // error return -1; @@ -1160,7 +1176,7 @@ static uint16_t webserver_create_header(struct webserver_t *client, uint16_t cod tcp_write(client->pcb, &buffer, i, 0); tcp_output(client->pcb); } else { - if(client->client.write(buffer, i) > 0) { + if(client->client->write(buffer, i) > 0) { client->lastseen = millis(); } } @@ -1172,6 +1188,7 @@ static int webserver_process_send(struct webserver_t *client) { struct sendlist_t *tmp = client->sendlist; uint16_t cpylen = client->totallen, i = 0, cpyptr = client->ptr; unsigned char cpy[client->totallen+1]; + memset(&cpy, 0, client->totallen+1); if(client->chunked == 1) { while(tmp != NULL && cpylen > 0) { @@ -1201,7 +1218,7 @@ static int webserver_process_send(struct webserver_t *client) { if(client->async == 1) { tcp_write(client->pcb, chunk_size, n, 0); } else { - if(client->client.write(chunk_size, n) > 0) { + if(client->client->write(chunk_size, n) > 0) { client->lastseen = millis(); } } @@ -1217,7 +1234,7 @@ static int webserver_process_send(struct webserver_t *client) { if(client->async == 1) { tcp_write(client->pcb, cpy, client->sendlist->size, TCP_WRITE_FLAG_MORE); } else { - if(client->client.write(cpy, client->sendlist->size) > 0) { + if(client->client->write(cpy, client->sendlist->size) > 0) { client->lastseen = millis(); } } @@ -1225,7 +1242,7 @@ static int webserver_process_send(struct webserver_t *client) { if(client->async == 1) { tcp_write(client->pcb, &((unsigned char *)client->sendlist->ptr)[client->ptr], client->sendlist->size, TCP_WRITE_FLAG_MORE); } else { - if(client->client.write(&((unsigned char *)client->sendlist->ptr)[client->ptr], client->sendlist->size) > 0) { + if(client->client->write(&((unsigned char *)client->sendlist->ptr)[client->ptr], client->sendlist->size) > 0) { client->lastseen = millis(); } } @@ -1247,7 +1264,7 @@ static int webserver_process_send(struct webserver_t *client) { if(client->async == 1) { tcp_write(client->pcb, cpy, client->totallen, TCP_WRITE_FLAG_MORE); } else { - if(client->client.write(cpy, client->totallen) > 0) { + if(client->client->write(cpy, client->totallen) > 0) { client->lastseen = millis(); } } @@ -1255,7 +1272,7 @@ static int webserver_process_send(struct webserver_t *client) { if(client->async == 1) { tcp_write(client->pcb, &((unsigned char *)client->sendlist->ptr)[client->ptr], client->totallen, TCP_WRITE_FLAG_MORE); } else { - if(client->client.write(&((unsigned char *)client->sendlist->ptr)[client->ptr], client->totallen) > 0) { + if(client->client->write(&((unsigned char *)client->sendlist->ptr)[client->ptr], client->totallen) > 0) { client->lastseen = millis(); } } @@ -1270,7 +1287,7 @@ static int webserver_process_send(struct webserver_t *client) { if(client->async == 1) { tcp_write(client->pcb, cpy, (client->sendlist->size-client->ptr), TCP_WRITE_FLAG_MORE); } else { - if(client->client.write(cpy, (client->sendlist->size-client->ptr)) > 0) { + if(client->client->write(cpy, (client->sendlist->size-client->ptr)) > 0) { client->lastseen = millis(); } } @@ -1278,7 +1295,7 @@ static int webserver_process_send(struct webserver_t *client) { if(client->async == 1) { tcp_write(client->pcb, &((unsigned char *)client->sendlist->ptr)[client->ptr], (client->sendlist->size-client->ptr), TCP_WRITE_FLAG_MORE); } else { - if(client->client.write(&((unsigned char *)client->sendlist->ptr)[client->ptr], (client->sendlist->size-client->ptr)) > 0) { + if(client->client->write(&((unsigned char *)client->sendlist->ptr)[client->ptr], (client->sendlist->size-client->ptr)) > 0) { client->lastseen = millis(); } } @@ -1290,6 +1307,7 @@ static int webserver_process_send(struct webserver_t *client) { if(tmp->type == 0) { free(tmp->ptr); } + free(tmp); client->ptr = 0; } else { if(client->sendlist->type == 1) { @@ -1297,7 +1315,7 @@ static int webserver_process_send(struct webserver_t *client) { if(client->async == 1) { tcp_write(client->pcb, cpy, client->totallen, TCP_WRITE_FLAG_MORE); } else { - if(client->client.write(cpy, client->totallen) > 0) { + if(client->client->write(cpy, client->totallen) > 0) { client->lastseen = millis(); } } @@ -1305,7 +1323,7 @@ static int webserver_process_send(struct webserver_t *client) { if(client->async == 1) { tcp_write(client->pcb, &((unsigned char *)client->sendlist->ptr)[client->ptr], client->totallen, TCP_WRITE_FLAG_MORE); } else { - if(client->client.write(&((unsigned char *)client->sendlist->ptr)[client->ptr], client->totallen) > 0) { + if(client->client->write(&((unsigned char *)client->sendlist->ptr)[client->ptr], client->totallen) > 0) { client->lastseen = millis(); } } @@ -1318,7 +1336,7 @@ static int webserver_process_send(struct webserver_t *client) { if(client->async == 1) { tcp_write_P(client->pcb, PSTR("\r\n"), 2, TCP_WRITE_FLAG_MORE); } else { - if(client->client.write_P((char *)PSTR("\r\n"), 2) > 0) { + if(client->client->write_P((char *)PSTR("\r\n"), 2) > 0) { client->lastseen = millis(); } } @@ -1338,7 +1356,7 @@ static int webserver_process_send(struct webserver_t *client) { if(client->async == 1) { tcp_write_P(client->pcb, PSTR("0\r\n\r\n"), 5, 0); } else { - if(client->client.write_P((char *)PSTR("0\r\n\r\n"), 5) > 0) { + if(client->client->write_P((char *)PSTR("0\r\n\r\n"), 5) > 0) { client->lastseen = millis(); } } @@ -1347,7 +1365,7 @@ static int webserver_process_send(struct webserver_t *client) { if(client->async == 1) { tcp_write_P(client->pcb, PSTR("\r\n\r\n"), 4, 0); } else { - if(client->client.write_P((char *)PSTR("\r\n\r\n"), 4) > 0) { + if(client->client->write_P((char *)PSTR("\r\n\r\n"), 4) > 0) { client->lastseen = millis(); } } @@ -1356,6 +1374,7 @@ static int webserver_process_send(struct webserver_t *client) { client->step = WEBSERVER_CLIENT_CLOSE; client->ptr = 0; client->content = 0; + client->userdata = NULL; } } if(client->async == 1) { @@ -1455,7 +1474,7 @@ int8_t webserver_send(struct webserver_t *client, uint16_t code, char *mimetype, tcp_write(client->pcb, &buffer, i, 0); tcp_output(client->pcb); } else{ - if(client->client.write((unsigned char *)&buffer, i) > 0) { + if(client->client->write((unsigned char *)&buffer, i) > 0) { client->lastseen = millis(); } } @@ -1473,7 +1492,13 @@ int8_t webserver_send(struct webserver_t *client, uint16_t code, char *mimetype, /* LCOV_EXCL_START*/ static void webserver_client_close(struct webserver_t *client) { + if(client->callback != NULL) { + client->callback(client, NULL); + } #ifdef ESP8266 + if(client->callback != NULL) { + client->callback(client, NULL); + } char log_msg[256]; sprintf_P(log_msg, PSTR("Closing webserver client: %s:%d"), IPAddress(client->pcb->remote_ip.addr).toString().c_str(), client->pcb->remote_port); log_message(log_msg); @@ -1578,7 +1603,6 @@ err_t webserver_async_receive(void *arg, tcp_pcb *pcb, struct pbuf *data, err_t if(data == NULL) { for(i=0;ipcb = NULL; } if(client->active == 1) { - client->client.stop(); + client->client->stop(); + delete client->client; + client->client = NULL; } #endif @@ -1664,6 +1690,7 @@ void webserver_reset_client(struct webserver_t *client) { client->route = 0; client->lastseen = 0; client->content = 0; + client->userdata = NULL; struct sendlist_t *tmp = NULL; while(client->sendlist) { @@ -1676,6 +1703,7 @@ void webserver_reset_client(struct webserver_t *client) { } if(client->boundary != NULL) { free(client->boundary); + client->boundary = NULL; } client->sendlist = NULL; @@ -1722,15 +1750,19 @@ void webserver_loop(void) { if((unsigned long)(millis() - clients[i].data.lastseen) > WEBSERVER_CLIENT_TIMEOUT) { #ifdef ESP8266 char log_msg[256]; - sprintf_P(log_msg, PSTR("Timeout webserver client: %s:%d"), clients[i].data.client.remoteIP().toString().c_str(), clients[i].data.client.remotePort()); + sprintf_P(log_msg, PSTR("Timeout webserver client: %s:%d"), clients[i].data.client->remoteIP().toString().c_str(), clients[i].data.client->remotePort()); log_message(log_msg); #endif clients[i].data.step = WEBSERVER_CLIENT_CLOSE; } } + if(!clients[i].data.client->connected()) { + clients[i].data.step = WEBSERVER_CLIENT_CLOSE; + } + switch(clients[i].data.step) { case WEBSERVER_CLIENT_CONNECTING: { - if(clients[i].data.client.available()) { + if(clients[i].data.client->available()) { clients[i].data.step = WEBSERVER_CLIENT_READ_HEADER; } clients[i].data.ptr = 0; @@ -1738,15 +1770,15 @@ void webserver_loop(void) { } break; case WEBSERVER_CLIENT_ARGS: case WEBSERVER_CLIENT_READ_HEADER: { - if(clients[i].data.client.connected() || clients[i].data.client.available()) { - if(clients[i].data.client.available()) { + if(clients[i].data.client->connected() || clients[i].data.client->available()) { + if(clients[i].data.client->available()) { uint8_t *p = (uint8_t *)rbuffer; - size = clients[i].data.client.read( + size = clients[i].data.client->read( p, WEBSERVER_READ_SIZE ); } - } else if(!clients[i].data.client.connected()) { + } else if(!clients[i].data.client->connected()) { clients[i].data.step = WEBSERVER_CLIENT_CLOSE; } else { continue; @@ -1786,11 +1818,14 @@ void webserver_loop(void) { } break; #ifdef ESP8266 case WEBSERVER_CLIENT_CLOSE: { + if(clients[i].data.callback != NULL) { + clients[i].data.callback(&clients[i].data, NULL); + } char log_msg[256]; - sprintf_P(log_msg, PSTR("Closing webserver client: %s:%d"), clients[i].data.client.remoteIP().toString().c_str(), clients[i].data.client.remotePort()); + sprintf_P(log_msg, PSTR("Closing webserver client: %s:%d"), clients[i].data.client->remoteIP().toString().c_str(), clients[i].data.client->remotePort()); log_message(log_msg); - clients[i].data.client.stop(); + clients[i].data.client->stop(); webserver_reset_client(&clients[i].data); } break; #endif @@ -1798,10 +1833,10 @@ void webserver_loop(void) { } #if defined(ESP8266) - if(sync_server.hasClient()) { + while(sync_server.hasClient()) { for(i=0;isetNoDelay(true); + clients[i].data.client->setTimeout(5000); char log_msg[256]; - sprintf_P(log_msg, PSTR("New webserver client: %s:%d"), clients[i].data.client.remoteIP().toString().c_str(), clients[i].data.client.remotePort()); + sprintf_P(log_msg, PSTR("New webserver client: %s:%d"), clients[i].data.client->remoteIP().toString().c_str(), clients[i].data.client->remotePort()); log_message(log_msg); break; } diff --git a/HeishaMon/src/common/webserver.h b/HeishaMon/src/common/webserver.h index 68f75e16..15a2536f 100755 --- a/HeishaMon/src/common/webserver.h +++ b/HeishaMon/src/common/webserver.h @@ -93,7 +93,7 @@ struct WiFiClient { typedef struct webserver_t { tcp_pcb *pcb; - WiFiClient client; + WiFiClient *client; unsigned long lastseen; uint8_t active:1; uint8_t reqtype:1; @@ -112,6 +112,7 @@ typedef struct webserver_t { webserver_cb_t *callback; unsigned char buffer[WEBSERVER_BUFFER_SIZE]; char *boundary; + void *userdata; } webserver_t; typedef struct webserver_client_t { From 84a3490161f3bf146b510dd95282b8f3ac077da1 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Thu, 30 Dec 2021 18:22:32 +0100 Subject: [PATCH 47/75] fix web table/json view when actdata empty --- HeishaMon/webfunctions.cpp | 57 ++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/HeishaMon/webfunctions.cpp b/HeishaMon/webfunctions.cpp index a11c3d98..65c62a87 100755 --- a/HeishaMon/webfunctions.cpp +++ b/HeishaMon/webfunctions.cpp @@ -1002,20 +1002,6 @@ int handleTableRefresh(struct webserver_t *client, char* actData) { } if (client->content < NUMBER_OF_TOPICS) { for (uint8_t topic = client->content; topic < NUMBER_OF_TOPICS && topic < client->content + 4; topic++) { - String topicdesc; - const char *valuetext = "value"; - if (strcmp_P(valuetext, topicDescription[topic][0]) == 0) { - topicdesc = topicDescription[topic][1]; - } else { - int value = getDataValue(actData, topic).toInt(); - int maxvalue = atoi(topicDescription[topic][0]); - if ((value < 0) || (value > maxvalue)) { - topicdesc = _unknown; - } - else { - topicdesc = topicDescription[topic][value + 1]; //plus one, because 0 is the maxvalue container - } - } webserver_send_content_P(client, PSTR("TOP"), 11); @@ -1028,16 +1014,24 @@ int handleTableRefresh(struct webserver_t *client, char* actData) { webserver_send_content_P(client, PSTR(""), 9); { - String dataValue = getDataValue(actData, topic); + String dataValue = actData[0] == '\0' ? "" : getDataValue(actData, topic); char* str = (char *)dataValue.c_str(); webserver_send_content(client, str, strlen(str)); } webserver_send_content_P(client, PSTR(""), 9); - { - char *str = (char *)topicdesc.c_str(); - webserver_send_content(client, str, strlen(str)); + int maxvalue = atoi(topicDescription[topic][0]); + int value = actData[0] == '\0' ? 0 : getDataValue(actData, topic).toInt(); + if (maxvalue == 0) { //this takes the special case where the description is a real value description instead of a mode, so value should take first index (= 0 + 1) + value = 0; + } + if ((value < 0) || (value > maxvalue)) { + webserver_send_content_P(client, _unknown, strlen_P(_unknown)); + } + else { + webserver_send_content_P(client, topicDescription[topic][value + 1], strlen_P(topicDescription[topic][value + 1])); + } webserver_send_content_P(client, PSTR(""), 10); @@ -1054,20 +1048,7 @@ int handleJsonOutput(struct webserver_t *client, char* actData) { webserver_send(client, 200, (char *)"application/json", 0); webserver_send_content_P(client, PSTR("{\"heatpump\":["), 13); } else if (client->content < NUMBER_OF_TOPICS) { - for (uint8_t topic = client->content - 1; topic < NUMBER_OF_TOPICS && topic < client->content + 4; topic++) { - PGM_P topicdesc; - const char *valuetext = "value"; - if (strcmp_P(valuetext, topicDescription[topic][0]) == 0) { - topicdesc = topicDescription[topic][1]; - } else { - int value = getDataValue(actData, topic).toInt(); - int maxvalue = atoi(topicDescription[topic][0]); - if ((value < 0) || (value > maxvalue)) { - topicdesc = _unknown; - } else { - topicdesc = topicDescription[topic][value + 1]; //plus one, because 0 is the maxvalue container - } - } + for (uint8_t topic = client->content - 1; topic < NUMBER_OF_TOPICS && topic < client->content + 4 ; topic++) { webserver_send_content_P(client, PSTR("{\"Topic\":\"TOP"), 13); @@ -1091,7 +1072,17 @@ int handleJsonOutput(struct webserver_t *client, char* actData) { webserver_send_content_P(client, PSTR("\",\"Description\":\""), 17); - webserver_send_content_P(client, topicdesc, strlen_P(topicdesc)); + int maxvalue = atoi(topicDescription[topic][0]); + int value = actData[0] == '\0' ? 0 : getDataValue(actData, topic).toInt(); + if (maxvalue == 0) { //this takes the special case where the description is a real value description instead of a mode, so value should take first index (= 0 + 1) + value = 0; + } + if ((value < 0) || (value > maxvalue)) { + webserver_send_content_P(client, _unknown, strlen_P(_unknown)); + } + else { + webserver_send_content_P(client, topicDescription[topic][value + 1], strlen_P(topicDescription[topic][value + 1])); + } webserver_send_content_P(client, PSTR("\"}"), 2); From eedf4e870e83f788b1fd3d82509e050bab753842 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Fri, 31 Dec 2021 15:19:16 +0100 Subject: [PATCH 48/75] GET request fix on large input --- HeishaMon/src/common/webserver.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HeishaMon/src/common/webserver.cpp b/HeishaMon/src/common/webserver.cpp index df0cba2c..37d093e6 100755 --- a/HeishaMon/src/common/webserver.cpp +++ b/HeishaMon/src/common/webserver.cpp @@ -305,7 +305,7 @@ static int webserver_parse_post(struct webserver_t *client, uint16_t size) { } } - if(client->ptr >= WEBSERVER_BUFFER_SIZE) { + if(client->ptr >= WEBSERVER_BUFFER_SIZE && strncmp((char *)&client->buffer[pos+1], "HTTP/1.1", 8) != 0) { /* * GET end delimiter before HTTP/1.1 */ From 9c91183fb3db2d6fb8b9f5247da10184b56690e9 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Sun, 2 Jan 2022 19:18:17 +0100 Subject: [PATCH 49/75] forgot PROGMEM in knownmodels --- HeishaMon/decode.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HeishaMon/decode.h b/HeishaMon/decode.h index 9c82e2e2..ac34bf5e 100644 --- a/HeishaMon/decode.h +++ b/HeishaMon/decode.h @@ -58,7 +58,7 @@ static const char *Model[] PROGMEM = { "IDU: WH-SDC0305J3E5 ODU: WH-UD05JE5", }; -static const byte knownModels[sizeof(Model) / sizeof(Model[0])][10] = { //stores the bytes #129 to #138 of known models in the same order as the const above +static const byte knownModels[sizeof(Model) / sizeof(Model[0])][10] PROGMEM = { //stores the bytes #129 to #138 of known models in the same order as the const above 0xE2, 0xCF, 0x0B, 0x13, 0x33, 0x32, 0xD1, 0x0C, 0x16, 0x33, 0xE2, 0xCF, 0x0B, 0x14, 0x33, 0x42, 0xD1, 0x0B, 0x17, 0x33, 0xE2, 0xCF, 0x0D, 0x77, 0x09, 0x12, 0xD0, 0x0B, 0x05, 0x11, From 3d3cabdaee3c175cfa7be94ffcb6abe007f638ce Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Sun, 2 Jan 2022 23:12:08 +0100 Subject: [PATCH 50/75] make ssid and password correct length --- HeishaMon/webfunctions.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HeishaMon/webfunctions.h b/HeishaMon/webfunctions.h index dc35af81..d5e29e3e 100755 --- a/HeishaMon/webfunctions.h +++ b/HeishaMon/webfunctions.h @@ -25,8 +25,8 @@ struct settingsStruct { const char* update_path = "/firmware"; const char* update_username = "admin"; - char wifi_ssid[40] = ""; - char wifi_password[40] = ""; + char wifi_ssid[33] = ""; + char wifi_password[65] = ""; char wifi_hostname[40] = "HeishaMon"; char ota_password[40] = "heisha"; char mqtt_server[40]; From 9fa71d8b683dc00bc8630309fc0107735a7eaa70 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Tue, 4 Jan 2022 13:32:37 +0100 Subject: [PATCH 51/75] Mmu3216 and safe non32bitaccess compiling (#88) * add mmu=3216 * add safe non-32bit access instead of fast --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 440a0786..e1d6890d 100755 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,7 +35,7 @@ jobs: run: arduino-cli lib install ringbuffer pubsubclient doubleresetdetect arduinojson dallastemperature onewire WebSockets - name: Compile Sketch - run: cd HeishaMon && arduino-cli compile --output-dir . --fqbn=esp8266:esp8266:d1_mini:xtal=160,vt=flash,ssl=all,eesz=4M2M,ip=lm2f,dbg=Disabled,lvl=None____,wipe=none,baud=921600 --vid-pid=1A86_7523 --warnings=none --verbose HeishaMon.ino + run: cd HeishaMon && arduino-cli compile --output-dir . --fqbn=esp8266:esp8266:d1_mini:xtal=160,vt=flash,ssl=basic,mmu=3216,non32xfer=safe,eesz=4M2M,ip=lm2f,dbg=Disabled,lvl=None____,wipe=none,baud=921600 --vid-pid=1A86_7523 --warnings=none --verbose HeishaMon.ino - name: Add MD5 checksum run: cd HeishaMon && MD5=`md5sum HeishaMon.ino.bin | cut -d\ -f1` && mv HeishaMon.ino.bin HeishaMon-alpha-$MD5.bin From b5ddb46b70488e5aad6e813c5e93c1e05dca695d Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Tue, 4 Jan 2022 21:01:03 +0100 Subject: [PATCH 52/75] resend data on mqtt reconnect fix --- HeishaMon/HeishaMon.ino | 10 ++++++---- HeishaMon/dallas.cpp | 2 +- HeishaMon/decode.cpp | 19 +++++++++---------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/HeishaMon/HeishaMon.ino b/HeishaMon/HeishaMon.ino index dd590829..1f215dc6 100755 --- a/HeishaMon/HeishaMon.ino +++ b/HeishaMon/HeishaMon.ino @@ -65,7 +65,7 @@ static int uploadpercentage = 0; char data[MAXDATASIZE] = { '\0' }; byte data_length = 0; -// store actual data +// store actual data #define DATASIZE 203 char actData[DATASIZE] = { '\0' }; #define OPTDATASIZE 20 @@ -188,7 +188,7 @@ void check_wifi() void mqtt_reconnect() { unsigned long now = millis(); - if ((unsigned long)(now - lastMqttReconnectAttempt) > MQTTRECONNECTTIMER) { //only try reconnect each MQTTRECONNECTTIMER seconds or on boot when lastMqttReconnectAttempt is still 0 + if ((lastMqttReconnectAttempt == 0) || ((unsigned long)(now - lastMqttReconnectAttempt) > MQTTRECONNECTTIMER)) { //only try reconnect each MQTTRECONNECTTIMER seconds or on boot when lastMqttReconnectAttempt is still 0 lastMqttReconnectAttempt = now; log_message((char*)"Reconnecting to mqtt server ..."); char topic[256]; @@ -212,8 +212,10 @@ void mqtt_reconnect() sprintf_P(mqtt_topic, PSTR("%s/%s/WatthourTotal/2"), heishamonSettings.mqtt_topic_base, mqtt_topic_s0); mqtt_client.subscribe(mqtt_topic); } - if (heishamonSettings.use_1wire) resetlastalldatatime_dallas; //resend all 1wire values to mqtt - resetlastalldatatime; //resend all heatpump values to mqtt + if (mqttReconnects == 1) { //only resend all data on first connect to mqtt so a data bomb like and bad mqtt server will not cause a reconnect bomb everytime + if (heishamonSettings.use_1wire) resetlastalldatatime_dallas(); //resend all 1wire values to mqtt + resetlastalldatatime(); //resend all heatpump values to mqtt + } } } } diff --git a/HeishaMon/dallas.cpp b/HeishaMon/dallas.cpp index cdbf9711..7befa671 100644 --- a/HeishaMon/dallas.cpp +++ b/HeishaMon/dallas.cpp @@ -66,7 +66,7 @@ void readNewDallasTemp(PubSubClient &mqtt_client, void (*log_message)(char*), ch char valueStr[20]; bool updatenow = false; - if ((unsigned long)(millis() - lastalldatatime_dallas) > (1000 * updateAllDallasTime)) { + if ((lastalldatatime_dallas == 0) || ((unsigned long)(millis() - lastalldatatime_dallas) > (1000 * updateAllDallasTime))) { updatenow = true; lastalldatatime_dallas = millis(); } diff --git a/HeishaMon/decode.cpp b/HeishaMon/decode.cpp index a718db6d..5a37fc90 100644 --- a/HeishaMon/decode.cpp +++ b/HeishaMon/decode.cpp @@ -131,6 +131,7 @@ String getErrorInfo(char* data) { // TOP44 // void resetlastalldatatime() { lastalldatatime = 0; + lastalloptdatatime = 0; } String getDataValue(char* data, unsigned int Topic_Number) { @@ -201,15 +202,14 @@ String getOptDataValue(char* data, unsigned int Topic_Number) { // Decode //////////////////////////////////////////////////////////////////////////// void decode_heatpump_data(char* data, char* actData, PubSubClient &mqtt_client, void (*log_message)(char*), char* mqtt_topic_base, unsigned int updateAllTime) { bool updatenow = false; - + if ((lastalldatatime == 0) || ((unsigned long)(millis() - lastalldatatime) > (1000 * updateAllTime))) { + updatenow = true; + lastalldatatime = millis(); + } for (unsigned int Topic_Number = 0 ; Topic_Number < NUMBER_OF_TOPICS ; Topic_Number++) { String Topic_Value; Topic_Value = getDataValue(data, Topic_Number); - if ((unsigned long)(millis() - lastalldatatime) > (1000 * updateAllTime)) { - updatenow = true; - lastalldatatime = millis(); - } if ((updatenow) || ( getDataValue(actData, Topic_Number) != Topic_Value )) { char log_msg[256]; char mqtt_topic[256]; @@ -223,15 +223,14 @@ void decode_heatpump_data(char* data, char* actData, PubSubClient &mqtt_client, void decode_optional_heatpump_data(char* data, char* actOptData, PubSubClient & mqtt_client, void (*log_message)(char*), char* mqtt_topic_base, unsigned int updateAllTime) { bool updatenow = false; - + if ((lastalloptdatatime == 0) || ((unsigned long)(millis() - lastalloptdatatime) > (1000 * updateAllTime))) { + updatenow = true; + lastalloptdatatime = millis(); + } for (unsigned int Topic_Number = 0 ; Topic_Number < NUMBER_OF_OPT_TOPICS ; Topic_Number++) { String Topic_Value; Topic_Value = getOptDataValue(data, Topic_Number); - if ((unsigned long)(millis() - lastalloptdatatime) > (1000 * updateAllTime)) { - updatenow = true; - lastalloptdatatime = millis(); - } if ((updatenow) || ( getDataValue(actOptData, Topic_Number) != Topic_Value )) { char log_msg[256]; char mqtt_topic[256]; From b2a6b0f24c395e3e30d0a49d0460ffa16a56f7f4 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Wed, 5 Jan 2022 17:59:25 +0100 Subject: [PATCH 53/75] refresh visible table only --- HeishaMon/htmlcode.h | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/HeishaMon/htmlcode.h b/HeishaMon/htmlcode.h index d02174ba..cc2d161e 100644 --- a/HeishaMon/htmlcode.h +++ b/HeishaMon/htmlcode.h @@ -62,8 +62,8 @@ static const char websocketJS[] PROGMEM = static const char refreshJS[] PROGMEM = ""; static const char selectJS[] PROGMEM = ""; @@ -999,463 +999,926 @@ struct tzStruct { const tzStruct tzdata[] PROGMEM = { { "ETC/GMT", "GMT0" }, { "Africa/Abidjan", "GMT0" }, - { "Africa/Accra", "GMT0" }, - { "Africa/Addis_Ababa", "EAT-3" }, - { "Africa/Algiers", "CET-1" }, - { "Africa/Asmara", "EAT-3" }, - { "Africa/Bamako", "GMT0" }, - { "Africa/Bangui", "WAT-1" }, - { "Africa/Banjul", "GMT0" }, - { "Africa/Bissau", "GMT0" }, - { "Africa/Blantyre", "CAT-2" }, - { "Africa/Brazzaville", "WAT-1" }, - { "Africa/Bujumbura", "CAT-2" }, - { "Africa/Cairo", "EET-2" }, - { "Africa/Casablanca", "<+01>-1" }, - { "Africa/Ceuta", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Africa/Conakry", "GMT0" }, - { "Africa/Dakar", "GMT0" }, - { "Africa/Dar_es_Salaam", "EAT-3" }, - { "Africa/Djibouti", "EAT-3" }, - { "Africa/Douala", "WAT-1" }, - { "Africa/El_Aaiun", "<+01>-1" }, - { "Africa/Freetown", "GMT0" }, - { "Africa/Gaborone", "CAT-2" }, - { "Africa/Harare", "CAT-2" }, - { "Africa/Johannesburg", "SAST-2" }, - { "Africa/Juba", "CAT-2" }, - { "Africa/Kampala", "EAT-3" }, - { "Africa/Khartoum", "CAT-2" }, - { "Africa/Kigali", "CAT-2" }, - { "Africa/Kinshasa", "WAT-1" }, - { "Africa/Lagos", "WAT-1" }, - { "Africa/Libreville", "WAT-1" }, - { "Africa/Lome", "GMT0" }, - { "Africa/Luanda", "WAT-1" }, - { "Africa/Lubumbashi", "CAT-2" }, - { "Africa/Lusaka", "CAT-2" }, - { "Africa/Malabo", "WAT-1" }, - { "Africa/Maputo", "CAT-2" }, - { "Africa/Maseru", "SAST-2" }, - { "Africa/Mbabane", "SAST-2" }, - { "Africa/Mogadishu", "EAT-3" }, - { "Africa/Monrovia", "GMT0" }, - { "Africa/Nairobi", "EAT-3" }, - { "Africa/Ndjamena", "WAT-1" }, - { "Africa/Niamey", "WAT-1" }, - { "Africa/Nouakchott", "GMT0" }, - { "Africa/Ouagadougou", "GMT0" }, - { "Africa/Porto-Novo", "WAT-1" }, - { "Africa/Sao_Tome", "GMT0" }, - { "Africa/Tripoli", "EET-2" }, - { "Africa/Tunis", "CET-1" }, - { "Africa/Windhoek", "CAT-2" }, - { "America/Adak", "HST10HDT,M3.2.0,M11.1.0" }, - { "America/Anchorage", "AKST9AKDT,M3.2.0,M11.1.0" }, - { "America/Anguilla", "AST4" }, - { "America/Antigua", "AST4" }, - { "America/Araguaina", "<-03>3" }, - { "America/Argentina/Buenos_Aires", "<-03>3" }, - { "America/Argentina/Catamarca", "<-03>3" }, - { "America/Argentina/Cordoba", "<-03>3" }, - { "America/Argentina/Jujuy", "<-03>3" }, - { "America/Argentina/La_Rioja", "<-03>3" }, - { "America/Argentina/Mendoza", "<-03>3" }, - { "America/Argentina/Rio_Gallegos", "<-03>3" }, - { "America/Argentina/Salta", "<-03>3" }, - { "America/Argentina/San_Juan", "<-03>3" }, - { "America/Argentina/San_Luis", "<-03>3" }, - { "America/Argentina/Tucuman", "<-03>3" }, - { "America/Argentina/Ushuaia", "<-03>3" }, - { "America/Aruba", "AST4" }, - { "America/Asuncion", "<-04>4<-03>,M10.1.0/0,M3.4.0/0" }, - { "America/Atikokan", "EST5" }, - { "America/Bahia", "<-03>3" }, - { "America/Bahia_Banderas", "CST6CDT,M4.1.0,M10.5.0" }, - { "America/Barbados", "AST4" }, - { "America/Belem", "<-03>3" }, - { "America/Belize", "CST6" }, - { "America/Blanc-Sablon", "AST4" }, - { "America/Boa_Vista", "<-04>4" }, - { "America/Bogota", "<-05>5" }, - { "America/Boise", "MST7MDT,M3.2.0,M11.1.0" }, - { "America/Cambridge_Bay", "MST7MDT,M3.2.0,M11.1.0" }, - { "America/Campo_Grande", "<-04>4" }, - { "America/Cancun", "EST5" }, - { "America/Caracas", "<-04>4" }, - { "America/Cayenne", "<-03>3" }, - { "America/Cayman", "EST5" }, - { "America/Chicago", "CST6CDT,M3.2.0,M11.1.0" }, - { "America/Chihuahua", "MST7MDT,M4.1.0,M10.5.0" }, - { "America/Costa_Rica", "CST6" }, - { "America/Creston", "MST7" }, - { "America/Cuiaba", "<-04>4" }, - { "America/Curacao", "AST4" }, - { "America/Danmarkshavn", "GMT0" }, - { "America/Dawson", "MST7" }, - { "America/Dawson_Creek", "MST7" }, - { "America/Denver", "MST7MDT,M3.2.0,M11.1.0" }, - { "America/Detroit", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Dominica", "AST4" }, - { "America/Edmonton", "MST7MDT,M3.2.0,M11.1.0" }, - { "America/Eirunepe", "<-05>5" }, - { "America/El_Salvador", "CST6" }, - { "America/Fortaleza", "<-03>3" }, - { "America/Fort_Nelson", "MST7" }, - { "America/Glace_Bay", "AST4ADT,M3.2.0,M11.1.0" }, - { "America/Godthab", "<-03>3<-02>,M3.5.0/-2,M10.5.0/-1" }, - { "America/Goose_Bay", "AST4ADT,M3.2.0,M11.1.0" }, - { "America/Grand_Turk", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Grenada", "AST4" }, - { "America/Guadeloupe", "AST4" }, - { "America/Guatemala", "CST6" }, - { "America/Guayaquil", "<-05>5" }, - { "America/Guyana", "<-04>4" }, - { "America/Halifax", "AST4ADT,M3.2.0,M11.1.0" }, - { "America/Havana", "CST5CDT,M3.2.0/0,M11.1.0/1" }, - { "America/Hermosillo", "MST7" }, - { "America/Indiana/Indianapolis", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Indiana/Knox", "CST6CDT,M3.2.0,M11.1.0" }, - { "America/Indiana/Marengo", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Indiana/Petersburg", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Indiana/Tell_City", "CST6CDT,M3.2.0,M11.1.0" }, - { "America/Indiana/Vevay", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Indiana/Vincennes", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Indiana/Winamac", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Inuvik", "MST7MDT,M3.2.0,M11.1.0" }, - { "America/Iqaluit", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Jamaica", "EST5" }, - { "America/Juneau", "AKST9AKDT,M3.2.0,M11.1.0" }, - { "America/Kentucky/Louisville", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Kentucky/Monticello", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Kralendijk", "AST4" }, - { "America/La_Paz", "<-04>4" }, - { "America/Lima", "<-05>5" }, - { "America/Los_Angeles", "PST8PDT,M3.2.0,M11.1.0" }, - { "America/Lower_Princes", "AST4" }, - { "America/Maceio", "<-03>3" }, - { "America/Managua", "CST6" }, - { "America/Manaus", "<-04>4" }, - { "America/Marigot", "AST4" }, - { "America/Martinique", "AST4" }, - { "America/Matamoros", "CST6CDT,M3.2.0,M11.1.0" }, - { "America/Mazatlan", "MST7MDT,M4.1.0,M10.5.0" }, - { "America/Menominee", "CST6CDT,M3.2.0,M11.1.0" }, - { "America/Merida", "CST6CDT,M4.1.0,M10.5.0" }, - { "America/Metlakatla", "AKST9AKDT,M3.2.0,M11.1.0" }, - { "America/Mexico_City", "CST6CDT,M4.1.0,M10.5.0" }, - { "America/Miquelon", "<-03>3<-02>,M3.2.0,M11.1.0" }, - { "America/Moncton", "AST4ADT,M3.2.0,M11.1.0" }, - { "America/Monterrey", "CST6CDT,M4.1.0,M10.5.0" }, - { "America/Montevideo", "<-03>3" }, - { "America/Montreal", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Montserrat", "AST4" }, - { "America/Nassau", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/New_York", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Nipigon", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Nome", "AKST9AKDT,M3.2.0,M11.1.0" }, - { "America/Noronha", "<-02>2" }, - { "America/North_Dakota/Beulah", "CST6CDT,M3.2.0,M11.1.0" }, - { "America/North_Dakota/Center", "CST6CDT,M3.2.0,M11.1.0" }, - { "America/North_Dakota/New_Salem", "CST6CDT,M3.2.0,M11.1.0" }, - { "America/Nuuk", "<-03>3<-02>,M3.5.0/-2,M10.5.0/-1" }, - { "America/Ojinaga", "MST7MDT,M3.2.0,M11.1.0" }, - { "America/Panama", "EST5" }, - { "America/Pangnirtung", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Paramaribo", "<-03>3" }, - { "America/Phoenix", "MST7" }, - { "America/Port-au-Prince", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Port_of_Spain", "AST4" }, - { "America/Porto_Velho", "<-04>4" }, - { "America/Puerto_Rico", "AST4" }, - { "America/Punta_Arenas", "<-03>3" }, - { "America/Rainy_River", "CST6CDT,M3.2.0,M11.1.0" }, - { "America/Rankin_Inlet", "CST6CDT,M3.2.0,M11.1.0" }, - { "America/Recife", "<-03>3" }, - { "America/Regina", "CST6" }, - { "America/Resolute", "CST6CDT,M3.2.0,M11.1.0" }, - { "America/Rio_Branco", "<-05>5" }, - { "America/Santarem", "<-03>3" }, - { "America/Santiago", "<-04>4<-03>,M9.1.6/24,M4.1.6/24" }, - { "America/Santo_Domingo", "AST4" }, - { "America/Sao_Paulo", "<-03>3" }, - { "America/Scoresbysund", "<-01>1<+00>,M3.5.0/0,M10.5.0/1" }, - { "America/Sitka", "AKST9AKDT,M3.2.0,M11.1.0" }, - { "America/St_Barthelemy", "AST4" }, - { "America/St_Johns", "NST3:30NDT,M3.2.0,M11.1.0" }, - { "America/St_Kitts", "AST4" }, - { "America/St_Lucia", "AST4" }, - { "America/St_Thomas", "AST4" }, - { "America/St_Vincent", "AST4" }, - { "America/Swift_Current", "CST6" }, - { "America/Tegucigalpa", "CST6" }, - { "America/Thule", "AST4ADT,M3.2.0,M11.1.0" }, - { "America/Thunder_Bay", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Tijuana", "PST8PDT,M3.2.0,M11.1.0" }, - { "America/Toronto", "EST5EDT,M3.2.0,M11.1.0" }, - { "America/Tortola", "AST4" }, - { "America/Vancouver", "PST8PDT,M3.2.0,M11.1.0" }, - { "America/Whitehorse", "MST7" }, - { "America/Winnipeg", "CST6CDT,M3.2.0,M11.1.0" }, - { "America/Yakutat", "AKST9AKDT,M3.2.0,M11.1.0" }, - { "America/Yellowknife", "MST7MDT,M3.2.0,M11.1.0" }, - { "Antarctica/Casey", "<+11>-11" }, - { "Antarctica/Davis", "<+07>-7" }, - { "Antarctica/DumontDUrville", "<+10>-10" }, - { "Antarctica/Macquarie", "AEST-10AEDT,M10.1.0,M4.1.0/3" }, - { "Antarctica/Mawson", "<+05>-5" }, - { "Antarctica/McMurdo", "NZST-12NZDT,M9.5.0,M4.1.0/3" }, - { "Antarctica/Palmer", "<-03>3" }, - { "Antarctica/Rothera", "<-03>3" }, - { "Antarctica/Syowa", "<+03>-3" }, - { "Antarctica/Troll", "<+00>0<+02>-2,M3.5.0/1,M10.5.0/3" }, - { "Antarctica/Vostok", "<+06>-6" }, - { "Arctic/Longyearbyen", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Asia/Aden", "<+03>-3" }, - { "Asia/Almaty", "<+06>-6" }, - { "Asia/Amman", "EET-2EEST,M2.5.4/24,M10.5.5/1" }, - { "Asia/Anadyr", "<+12>-12" }, - { "Asia/Aqtau", "<+05>-5" }, - { "Asia/Aqtobe", "<+05>-5" }, - { "Asia/Ashgabat", "<+05>-5" }, - { "Asia/Atyrau", "<+05>-5" }, - { "Asia/Baghdad", "<+03>-3" }, - { "Asia/Bahrain", "<+03>-3" }, - { "Asia/Baku", "<+04>-4" }, - { "Asia/Bangkok", "<+07>-7" }, - { "Asia/Barnaul", "<+07>-7" }, - { "Asia/Beirut", "EET-2EEST,M3.5.0/0,M10.5.0/0" }, - { "Asia/Bishkek", "<+06>-6" }, - { "Asia/Brunei", "<+08>-8" }, - { "Asia/Chita", "<+09>-9" }, - { "Asia/Choibalsan", "<+08>-8" }, - { "Asia/Colombo", "<+0530>-5:30" }, - { "Asia/Damascus", "EET-2EEST,M3.5.5/0,M10.5.5/0" }, - { "Asia/Dhaka", "<+06>-6" }, - { "Asia/Dili", "<+09>-9" }, - { "Asia/Dubai", "<+04>-4" }, - { "Asia/Dushanbe", "<+05>-5" }, - { "Asia/Famagusta", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, - { "Asia/Gaza", "EET-2EEST,M3.4.4/48,M10.5.5/1" }, - { "Asia/Hebron", "EET-2EEST,M3.4.4/48,M10.5.5/1" }, - { "Asia/Ho_Chi_Minh", "<+07>-7" }, - { "Asia/Hong_Kong", "HKT-8" }, - { "Asia/Hovd", "<+07>-7" }, - { "Asia/Irkutsk", "<+08>-8" }, - { "Asia/Jakarta", "WIB-7" }, - { "Asia/Jayapura", "WIT-9" }, - { "Asia/Jerusalem", "IST-2IDT,M3.4.4/26,M10.5.0" }, - { "Asia/Kabul", "<+0430>-4:30" }, - { "Asia/Kamchatka", "<+12>-12" }, - { "Asia/Karachi", "PKT-5" }, - { "Asia/Kathmandu", "<+0545>-5:45" }, - { "Asia/Khandyga", "<+09>-9" }, - { "Asia/Kolkata", "IST-5:30" }, - { "Asia/Krasnoyarsk", "<+07>-7" }, - { "Asia/Kuala_Lumpur", "<+08>-8" }, - { "Asia/Kuching", "<+08>-8" }, - { "Asia/Kuwait", "<+03>-3" }, - { "Asia/Macau", "CST-8" }, - { "Asia/Magadan", "<+11>-11" }, - { "Asia/Makassar", "WITA-8" }, - { "Asia/Manila", "PST-8" }, - { "Asia/Muscat", "<+04>-4" }, - { "Asia/Nicosia", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, - { "Asia/Novokuznetsk", "<+07>-7" }, - { "Asia/Novosibirsk", "<+07>-7" }, - { "Asia/Omsk", "<+06>-6" }, - { "Asia/Oral", "<+05>-5" }, - { "Asia/Phnom_Penh", "<+07>-7" }, - { "Asia/Pontianak", "WIB-7" }, - { "Asia/Pyongyang", "KST-9" }, - { "Asia/Qatar", "<+03>-3" }, - { "Asia/Qyzylorda", "<+05>-5" }, - { "Asia/Riyadh", "<+03>-3" }, - { "Asia/Sakhalin", "<+11>-11" }, - { "Asia/Samarkand", "<+05>-5" }, - { "Asia/Seoul", "KST-9" }, - { "Asia/Shanghai", "CST-8" }, - { "Asia/Singapore", "<+08>-8" }, - { "Asia/Srednekolymsk", "<+11>-11" }, - { "Asia/Taipei", "CST-8" }, - { "Asia/Tashkent", "<+05>-5" }, - { "Asia/Tbilisi", "<+04>-4" }, - { "Asia/Tehran", "<+0330>-3:30<+0430>,J79/24,J263/24" }, - { "Asia/Thimphu", "<+06>-6" }, - { "Asia/Tokyo", "JST-9" }, - { "Asia/Tomsk", "<+07>-7" }, - { "Asia/Ulaanbaatar", "<+08>-8" }, - { "Asia/Urumqi", "<+06>-6" }, - { "Asia/Ust-Nera", "<+10>-10" }, - { "Asia/Vientiane", "<+07>-7" }, - { "Asia/Vladivostok", "<+10>-10" }, - { "Asia/Yakutsk", "<+09>-9" }, - { "Asia/Yangon", "<+0630>-6:30" }, - { "Asia/Yekaterinburg", "<+05>-5" }, - { "Asia/Yerevan", "<+04>-4" }, - { "Atlantic/Azores", "<-01>1<+00>,M3.5.0/0,M10.5.0/1" }, - { "Atlantic/Bermuda", "AST4ADT,M3.2.0,M11.1.0" }, - { "Atlantic/Canary", "WET0WEST,M3.5.0/1,M10.5.0" }, - { "Atlantic/Cape_Verde", "<-01>1" }, - { "Atlantic/Faroe", "WET0WEST,M3.5.0/1,M10.5.0" }, - { "Atlantic/Madeira", "WET0WEST,M3.5.0/1,M10.5.0" }, - { "Atlantic/Reykjavik", "GMT0" }, - { "Atlantic/South_Georgia", "<-02>2" }, - { "Atlantic/Stanley", "<-03>3" }, - { "Atlantic/St_Helena", "GMT0" }, - { "Australia/Adelaide", "ACST-9:30ACDT,M10.1.0,M4.1.0/3" }, - { "Australia/Brisbane", "AEST-10" }, - { "Australia/Broken_Hill", "ACST-9:30ACDT,M10.1.0,M4.1.0/3" }, - { "Australia/Currie", "AEST-10AEDT,M10.1.0,M4.1.0/3" }, - { "Australia/Darwin", "ACST-9:30" }, - { "Australia/Eucla", "<+0845>-8:45" }, - { "Australia/Hobart", "AEST-10AEDT,M10.1.0,M4.1.0/3" }, - { "Australia/Lindeman", "AEST-10" }, - { "Australia/Lord_Howe", "<+1030>-10:30<+11>-11,M10.1.0,M4.1.0" }, - { "Australia/Melbourne", "AEST-10AEDT,M10.1.0,M4.1.0/3" }, - { "Australia/Perth", "AWST-8" }, - { "Australia/Sydney", "AEST-10AEDT,M10.1.0,M4.1.0/3" }, - { "Europe/Amsterdam", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Andorra", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Astrakhan", "<+04>-4" }, - { "Europe/Athens", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, - { "Europe/Belgrade", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Berlin", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Bratislava", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Brussels", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Bucharest", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, - { "Europe/Budapest", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Busingen", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Chisinau", "EET-2EEST,M3.5.0,M10.5.0/3" }, - { "Europe/Copenhagen", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Dublin", "IST-1GMT0,M10.5.0,M3.5.0/1" }, - { "Europe/Gibraltar", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Guernsey", "GMT0BST,M3.5.0/1,M10.5.0" }, - { "Europe/Helsinki", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, - { "Europe/Isle_of_Man", "GMT0BST,M3.5.0/1,M10.5.0" }, - { "Europe/Istanbul", "<+03>-3" }, - { "Europe/Jersey", "GMT0BST,M3.5.0/1,M10.5.0" }, - { "Europe/Kaliningrad", "EET-2" }, - { "Europe/Kiev", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, - { "Europe/Kirov", "<+03>-3" }, - { "Europe/Lisbon", "WET0WEST,M3.5.0/1,M10.5.0" }, - { "Europe/Ljubljana", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/London", "GMT0BST,M3.5.0/1,M10.5.0" }, - { "Europe/Luxembourg", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Madrid", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Malta", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Mariehamn", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, - { "Europe/Minsk", "<+03>-3" }, - { "Europe/Monaco", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Moscow", "MSK-3" }, - { "Europe/Oslo", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Paris", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Podgorica", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Prague", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Riga", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, - { "Europe/Rome", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Samara", "<+04>-4" }, - { "Europe/San_Marino", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Sarajevo", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Saratov", "<+04>-4" }, - { "Europe/Simferopol", "MSK-3" }, - { "Europe/Skopje", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Sofia", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, - { "Europe/Stockholm", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Tallinn", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, - { "Europe/Tirane", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Ulyanovsk", "<+04>-4" }, - { "Europe/Uzhgorod", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, - { "Europe/Vaduz", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Vatican", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Vienna", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Vilnius", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, - { "Europe/Volgograd", "<+03>-3" }, - { "Europe/Warsaw", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Zagreb", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Europe/Zaporozhye", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, - { "Europe/Zurich", "CET-1CEST,M3.5.0,M10.5.0/3" }, - { "Indian/Antananarivo", "EAT-3" }, - { "Indian/Chagos", "<+06>-6" }, - { "Indian/Christmas", "<+07>-7" }, - { "Indian/Cocos", "<+0630>-6:30" }, - { "Indian/Comoro", "EAT-3" }, - { "Indian/Kerguelen", "<+05>-5" }, - { "Indian/Mahe", "<+04>-4" }, - { "Indian/Maldives", "<+05>-5" }, - { "Indian/Mauritius", "<+04>-4" }, - { "Indian/Mayotte", "EAT-3" }, - { "Indian/Reunion", "<+04>-4" }, - { "Pacific/Apia", "<+13>-13" }, - { "Pacific/Auckland", "NZST-12NZDT,M9.5.0,M4.1.0/3" }, - { "Pacific/Bougainville", "<+11>-11" }, - { "Pacific/Chatham", "<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45" }, - { "Pacific/Chuuk", "<+10>-10" }, - { "Pacific/Easter", "<-06>6<-05>,M9.1.6/22,M4.1.6/22" }, - { "Pacific/Efate", "<+11>-11" }, - { "Pacific/Enderbury", "<+13>-13" }, - { "Pacific/Fakaofo", "<+13>-13" }, - { "Pacific/Fiji", "<+12>-12<+13>,M11.2.0,M1.2.3/99" }, - { "Pacific/Funafuti", "<+12>-12" }, - { "Pacific/Galapagos", "<-06>6" }, - { "Pacific/Gambier", "<-09>9" }, - { "Pacific/Guadalcanal", "<+11>-11" }, - { "Pacific/Guam", "ChST-10" }, - { "Pacific/Honolulu", "HST10" }, - { "Pacific/Kiritimati", "<+14>-14" }, - { "Pacific/Kosrae", "<+11>-11" }, - { "Pacific/Kwajalein", "<+12>-12" }, - { "Pacific/Majuro", "<+12>-12" }, - { "Pacific/Marquesas", "<-0930>9:30" }, - { "Pacific/Midway", "SST11" }, - { "Pacific/Nauru", "<+12>-12" }, - { "Pacific/Niue", "<-11>11" }, - { "Pacific/Norfolk", "<+11>-11<+12>,M10.1.0,M4.1.0/3" }, - { "Pacific/Noumea", "<+11>-11" }, - { "Pacific/Pago_Pago", "SST11" }, - { "Pacific/Palau", "<+09>-9" }, - { "Pacific/Pitcairn", "<-08>8" }, - { "Pacific/Pohnpei", "<+11>-11" }, - { "Pacific/Port_Moresby", "<+10>-10" }, - { "Pacific/Rarotonga", "<-10>10" }, - { "Pacific/Saipan", "ChST-10" }, - { "Pacific/Tahiti", "<-10>10" }, - { "Pacific/Tarawa", "<+12>-12" }, - { "Pacific/Tongatapu", "<+13>-13" }, - { "Pacific/Wake", "<+12>-12" }, - { "Pacific/Wallis", "<+12>-12" }, - { "Etc/GMT-0", "GMT0" }, - { "Etc/GMT-1", "<+01>-1" }, - { "Etc/GMT-2", "<+02>-2" }, - { "Etc/GMT-3", "<+03>-3" }, - { "Etc/GMT-4", "<+04>-4" }, - { "Etc/GMT-5", "<+05>-5" }, - { "Etc/GMT-6", "<+06>-6" }, - { "Etc/GMT-7", "<+07>-7" }, - { "Etc/GMT-8", "<+08>-8" }, - { "Etc/GMT-9", "<+09>-9" }, - { "Etc/GMT-10", "<+10>-10" }, - { "Etc/GMT-11", "<+11>-11" }, - { "Etc/GMT-12", "<+12>-12" }, - { "Etc/GMT-13", "<+13>-13" }, - { "Etc/GMT-14", "<+14>-14" }, - { "Etc/GMT0", "GMT0" }, - { "Etc/GMT+0", "GMT0" }, - { "Etc/GMT+1", "<-01>1" }, - { "Etc/GMT+2", "<-02>2" }, - { "Etc/GMT+3", "<-03>3" }, - { "Etc/GMT+4", "<-04>4" }, - { "Etc/GMT+5", "<-05>5" }, - { "Etc/GMT+6", "<-06>6" }, - { "Etc/GMT+7", "<-07>7" }, - { "Etc/GMT+8", "<-08>8" }, - { "Etc/GMT+9", "<-09>9" }, - { "Etc/GMT+10", "<-10>10" }, - { "Etc/GMT+11", "<-11>11" }, - { "Etc/GMT+12", "<-12>12" }, - { "Etc/UCT", "UTC0" }, - { "Etc/UTC", "UTC0" }, - { "Etc/Greenwich", "GMT0" }, - { "Etc/Universal", "UTC0" }, - { "Etc/Zulu", "UTC0" }, + { "Africa/Accra", "GMT0" }, + { "Africa/Addis_Ababa", "EAT-3" }, + { "Africa/Algiers", "CET-1" }, + { "Africa/Asmara", "EAT-3" }, + { "Africa/Bamako", "GMT0" }, + { "Africa/Bangui", "WAT-1" }, + { "Africa/Banjul", "GMT0" }, + { "Africa/Bissau", "GMT0" }, + { "Africa/Blantyre", "CAT-2" }, + { "Africa/Brazzaville", "WAT-1" }, + { "Africa/Bujumbura", "CAT-2" }, + { "Africa/Cairo", "EET-2" }, + { "Africa/Casablanca", "<+01>-1" }, + { "Africa/Ceuta", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Africa/Conakry", "GMT0" }, + { "Africa/Dakar", "GMT0" }, + { "Africa/Dar_es_Salaam", "EAT-3" }, + { "Africa/Djibouti", "EAT-3" }, + { "Africa/Douala", "WAT-1" }, + { "Africa/El_Aaiun", "<+01>-1" }, + { "Africa/Freetown", "GMT0" }, + { "Africa/Gaborone", "CAT-2" }, + { "Africa/Harare", "CAT-2" }, + { "Africa/Johannesburg", "SAST-2" }, + { "Africa/Juba", "CAT-2" }, + { "Africa/Kampala", "EAT-3" }, + { "Africa/Khartoum", "CAT-2" }, + { "Africa/Kigali", "CAT-2" }, + { "Africa/Kinshasa", "WAT-1" }, + { "Africa/Lagos", "WAT-1" }, + { "Africa/Libreville", "WAT-1" }, + { "Africa/Lome", "GMT0" }, + { "Africa/Luanda", "WAT-1" }, + { "Africa/Lubumbashi", "CAT-2" }, + { "Africa/Lusaka", "CAT-2" }, + { "Africa/Malabo", "WAT-1" }, + { "Africa/Maputo", "CAT-2" }, + { "Africa/Maseru", "SAST-2" }, + { "Africa/Mbabane", "SAST-2" }, + { "Africa/Mogadishu", "EAT-3" }, + { "Africa/Monrovia", "GMT0" }, + { "Africa/Nairobi", "EAT-3" }, + { "Africa/Ndjamena", "WAT-1" }, + { "Africa/Niamey", "WAT-1" }, + { "Africa/Nouakchott", "GMT0" }, + { "Africa/Ouagadougou", "GMT0" }, + { "Africa/Porto-Novo", "WAT-1" }, + { "Africa/Sao_Tome", "GMT0" }, + { "Africa/Tripoli", "EET-2" }, + { "Africa/Tunis", "CET-1" }, + { "Africa/Windhoek", "CAT-2" }, + { "America/Adak", "HST10HDT,M3.2.0,M11.1.0" }, + { "America/Anchorage", "AKST9AKDT,M3.2.0,M11.1.0" }, + { "America/Anguilla", "AST4" }, + { "America/Antigua", "AST4" }, + { "America/Araguaina", "<-03>3" }, + { "America/Argentina/Buenos_Aires", "<-03>3" }, + { "America/Argentina/Catamarca", "<-03>3" }, + { "America/Argentina/Cordoba", "<-03>3" }, + { "America/Argentina/Jujuy", "<-03>3" }, + { "America/Argentina/La_Rioja", "<-03>3" }, + { "America/Argentina/Mendoza", "<-03>3" }, + { "America/Argentina/Rio_Gallegos", "<-03>3" }, + { "America/Argentina/Salta", "<-03>3" }, + { "America/Argentina/San_Juan", "<-03>3" }, + { "America/Argentina/San_Luis", "<-03>3" }, + { "America/Argentina/Tucuman", "<-03>3" }, + { "America/Argentina/Ushuaia", "<-03>3" }, + { "America/Aruba", "AST4" }, + { "America/Asuncion", "<-04>4<-03>,M10.1.0/0,M3.4.0/0" }, + { "America/Atikokan", "EST5" }, + { "America/Bahia", "<-03>3" }, + { "America/Bahia_Banderas", "CST6CDT,M4.1.0,M10.5.0" }, + { "America/Barbados", "AST4" }, + { "America/Belem", "<-03>3" }, + { "America/Belize", "CST6" }, + { "America/Blanc-Sablon", "AST4" }, + { "America/Boa_Vista", "<-04>4" }, + { "America/Bogota", "<-05>5" }, + { "America/Boise", "MST7MDT,M3.2.0,M11.1.0" }, + { "America/Cambridge_Bay", "MST7MDT,M3.2.0,M11.1.0" }, + { "America/Campo_Grande", "<-04>4" }, + { "America/Cancun", "EST5" }, + { "America/Caracas", "<-04>4" }, + { "America/Cayenne", "<-03>3" }, + { "America/Cayman", "EST5" }, + { "America/Chicago", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Chihuahua", "MST7MDT,M4.1.0,M10.5.0" }, + { "America/Costa_Rica", "CST6" }, + { "America/Creston", "MST7" }, + { "America/Cuiaba", "<-04>4" }, + { "America/Curacao", "AST4" }, + { "America/Danmarkshavn", "GMT0" }, + { "America/Dawson", "MST7" }, + { "America/Dawson_Creek", "MST7" }, + { "America/Denver", "MST7MDT,M3.2.0,M11.1.0" }, + { "America/Detroit", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Dominica", "AST4" }, + { "America/Edmonton", "MST7MDT,M3.2.0,M11.1.0" }, + { "America/Eirunepe", "<-05>5" }, + { "America/El_Salvador", "CST6" }, + { "America/Fortaleza", "<-03>3" }, + { "America/Fort_Nelson", "MST7" }, + { "America/Glace_Bay", "AST4ADT,M3.2.0,M11.1.0" }, + { "America/Godthab", "<-03>3<-02>,M3.5.0/-2,M10.5.0/-1" }, + { "America/Goose_Bay", "AST4ADT,M3.2.0,M11.1.0" }, + { "America/Grand_Turk", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Grenada", "AST4" }, + { "America/Guadeloupe", "AST4" }, + { "America/Guatemala", "CST6" }, + { "America/Guayaquil", "<-05>5" }, + { "America/Guyana", "<-04>4" }, + { "America/Halifax", "AST4ADT,M3.2.0,M11.1.0" }, + { "America/Havana", "CST5CDT,M3.2.0/0,M11.1.0/1" }, + { "America/Hermosillo", "MST7" }, + { "America/Indiana/Indianapolis", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Indiana/Knox", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Indiana/Marengo", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Indiana/Petersburg", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Indiana/Tell_City", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Indiana/Vevay", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Indiana/Vincennes", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Indiana/Winamac", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Inuvik", "MST7MDT,M3.2.0,M11.1.0" }, + { "America/Iqaluit", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Jamaica", "EST5" }, + { "America/Juneau", "AKST9AKDT,M3.2.0,M11.1.0" }, + { "America/Kentucky/Louisville", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Kentucky/Monticello", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Kralendijk", "AST4" }, + { "America/La_Paz", "<-04>4" }, + { "America/Lima", "<-05>5" }, + { "America/Los_Angeles", "PST8PDT,M3.2.0,M11.1.0" }, + { "America/Lower_Princes", "AST4" }, + { "America/Maceio", "<-03>3" }, + { "America/Managua", "CST6" }, + { "America/Manaus", "<-04>4" }, + { "America/Marigot", "AST4" }, + { "America/Martinique", "AST4" }, + { "America/Matamoros", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Mazatlan", "MST7MDT,M4.1.0,M10.5.0" }, + { "America/Menominee", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Merida", "CST6CDT,M4.1.0,M10.5.0" }, + { "America/Metlakatla", "AKST9AKDT,M3.2.0,M11.1.0" }, + { "America/Mexico_City", "CST6CDT,M4.1.0,M10.5.0" }, + { "America/Miquelon", "<-03>3<-02>,M3.2.0,M11.1.0" }, + { "America/Moncton", "AST4ADT,M3.2.0,M11.1.0" }, + { "America/Monterrey", "CST6CDT,M4.1.0,M10.5.0" }, + { "America/Montevideo", "<-03>3" }, + { "America/Montreal", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Montserrat", "AST4" }, + { "America/Nassau", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/New_York", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Nipigon", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Nome", "AKST9AKDT,M3.2.0,M11.1.0" }, + { "America/Noronha", "<-02>2" }, + { "America/North_Dakota/Beulah", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/North_Dakota/Center", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/North_Dakota/New_Salem", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Nuuk", "<-03>3<-02>,M3.5.0/-2,M10.5.0/-1" }, + { "America/Ojinaga", "MST7MDT,M3.2.0,M11.1.0" }, + { "America/Panama", "EST5" }, + { "America/Pangnirtung", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Paramaribo", "<-03>3" }, + { "America/Phoenix", "MST7" }, + { "America/Port-au-Prince", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Port_of_Spain", "AST4" }, + { "America/Porto_Velho", "<-04>4" }, + { "America/Puerto_Rico", "AST4" }, + { "America/Punta_Arenas", "<-03>3" }, + { "America/Rainy_River", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Rankin_Inlet", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Recife", "<-03>3" }, + { "America/Regina", "CST6" }, + { "America/Resolute", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Rio_Branco", "<-05>5" }, + { "America/Santarem", "<-03>3" }, + { "America/Santiago", "<-04>4<-03>,M9.1.6/24,M4.1.6/24" }, + { "America/Santo_Domingo", "AST4" }, + { "America/Sao_Paulo", "<-03>3" }, + { "America/Scoresbysund", "<-01>1<+00>,M3.5.0/0,M10.5.0/1" }, + { "America/Sitka", "AKST9AKDT,M3.2.0,M11.1.0" }, + { "America/St_Barthelemy", "AST4" }, + { "America/St_Johns", "NST3:30NDT,M3.2.0,M11.1.0" }, + { "America/St_Kitts", "AST4" }, + { "America/St_Lucia", "AST4" }, + { "America/St_Thomas", "AST4" }, + { "America/St_Vincent", "AST4" }, + { "America/Swift_Current", "CST6" }, + { "America/Tegucigalpa", "CST6" }, + { "America/Thule", "AST4ADT,M3.2.0,M11.1.0" }, + { "America/Thunder_Bay", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Tijuana", "PST8PDT,M3.2.0,M11.1.0" }, + { "America/Toronto", "EST5EDT,M3.2.0,M11.1.0" }, + { "America/Tortola", "AST4" }, + { "America/Vancouver", "PST8PDT,M3.2.0,M11.1.0" }, + { "America/Whitehorse", "MST7" }, + { "America/Winnipeg", "CST6CDT,M3.2.0,M11.1.0" }, + { "America/Yakutat", "AKST9AKDT,M3.2.0,M11.1.0" }, + { "America/Yellowknife", "MST7MDT,M3.2.0,M11.1.0" }, + { "Antarctica/Casey", "<+11>-11" }, + { "Antarctica/Davis", "<+07>-7" }, + { "Antarctica/DumontDUrville", "<+10>-10" }, + { "Antarctica/Macquarie", "AEST-10AEDT,M10.1.0,M4.1.0/3" }, + { "Antarctica/Mawson", "<+05>-5" }, + { "Antarctica/McMurdo", "NZST-12NZDT,M9.5.0,M4.1.0/3" }, + { "Antarctica/Palmer", "<-03>3" }, + { "Antarctica/Rothera", "<-03>3" }, + { "Antarctica/Syowa", "<+03>-3" }, + { "Antarctica/Troll", "<+00>0<+02>-2,M3.5.0/1,M10.5.0/3" }, + { "Antarctica/Vostok", "<+06>-6" }, + { "Arctic/Longyearbyen", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Asia/Aden", "<+03>-3" }, + { "Asia/Almaty", "<+06>-6" }, + { "Asia/Amman", "EET-2EEST,M2.5.4/24,M10.5.5/1" }, + { "Asia/Anadyr", "<+12>-12" }, + { "Asia/Aqtau", "<+05>-5" }, + { "Asia/Aqtobe", "<+05>-5" }, + { "Asia/Ashgabat", "<+05>-5" }, + { "Asia/Atyrau", "<+05>-5" }, + { "Asia/Baghdad", "<+03>-3" }, + { "Asia/Bahrain", "<+03>-3" }, + { "Asia/Baku", "<+04>-4" }, + { "Asia/Bangkok", "<+07>-7" }, + { "Asia/Barnaul", "<+07>-7" }, + { "Asia/Beirut", "EET-2EEST,M3.5.0/0,M10.5.0/0" }, + { "Asia/Bishkek", "<+06>-6" }, + { "Asia/Brunei", "<+08>-8" }, + { "Asia/Chita", "<+09>-9" }, + { "Asia/Choibalsan", "<+08>-8" }, + { "Asia/Colombo", "<+0530>-5:30" }, + { "Asia/Damascus", "EET-2EEST,M3.5.5/0,M10.5.5/0" }, + { "Asia/Dhaka", "<+06>-6" }, + { "Asia/Dili", "<+09>-9" }, + { "Asia/Dubai", "<+04>-4" }, + { "Asia/Dushanbe", "<+05>-5" }, + { "Asia/Famagusta", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Asia/Gaza", "EET-2EEST,M3.4.4/48,M10.5.5/1" }, + { "Asia/Hebron", "EET-2EEST,M3.4.4/48,M10.5.5/1" }, + { "Asia/Ho_Chi_Minh", "<+07>-7" }, + { "Asia/Hong_Kong", "HKT-8" }, + { "Asia/Hovd", "<+07>-7" }, + { "Asia/Irkutsk", "<+08>-8" }, + { "Asia/Jakarta", "WIB-7" }, + { "Asia/Jayapura", "WIT-9" }, + { "Asia/Jerusalem", "IST-2IDT,M3.4.4/26,M10.5.0" }, + { "Asia/Kabul", "<+0430>-4:30" }, + { "Asia/Kamchatka", "<+12>-12" }, + { "Asia/Karachi", "PKT-5" }, + { "Asia/Kathmandu", "<+0545>-5:45" }, + { "Asia/Khandyga", "<+09>-9" }, + { "Asia/Kolkata", "IST-5:30" }, + { "Asia/Krasnoyarsk", "<+07>-7" }, + { "Asia/Kuala_Lumpur", "<+08>-8" }, + { "Asia/Kuching", "<+08>-8" }, + { "Asia/Kuwait", "<+03>-3" }, + { "Asia/Macau", "CST-8" }, + { "Asia/Magadan", "<+11>-11" }, + { "Asia/Makassar", "WITA-8" }, + { "Asia/Manila", "PST-8" }, + { "Asia/Muscat", "<+04>-4" }, + { "Asia/Nicosia", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Asia/Novokuznetsk", "<+07>-7" }, + { "Asia/Novosibirsk", "<+07>-7" }, + { "Asia/Omsk", "<+06>-6" }, + { "Asia/Oral", "<+05>-5" }, + { "Asia/Phnom_Penh", "<+07>-7" }, + { "Asia/Pontianak", "WIB-7" }, + { "Asia/Pyongyang", "KST-9" }, + { "Asia/Qatar", "<+03>-3" }, + { "Asia/Qyzylorda", "<+05>-5" }, + { "Asia/Riyadh", "<+03>-3" }, + { "Asia/Sakhalin", "<+11>-11" }, + { "Asia/Samarkand", "<+05>-5" }, + { "Asia/Seoul", "KST-9" }, + { "Asia/Shanghai", "CST-8" }, + { "Asia/Singapore", "<+08>-8" }, + { "Asia/Srednekolymsk", "<+11>-11" }, + { "Asia/Taipei", "CST-8" }, + { "Asia/Tashkent", "<+05>-5" }, + { "Asia/Tbilisi", "<+04>-4" }, + { "Asia/Tehran", "<+0330>-3:30<+0430>,J79/24,J263/24" }, + { "Asia/Thimphu", "<+06>-6" }, + { "Asia/Tokyo", "JST-9" }, + { "Asia/Tomsk", "<+07>-7" }, + { "Asia/Ulaanbaatar", "<+08>-8" }, + { "Asia/Urumqi", "<+06>-6" }, + { "Asia/Ust-Nera", "<+10>-10" }, + { "Asia/Vientiane", "<+07>-7" }, + { "Asia/Vladivostok", "<+10>-10" }, + { "Asia/Yakutsk", "<+09>-9" }, + { "Asia/Yangon", "<+0630>-6:30" }, + { "Asia/Yekaterinburg", "<+05>-5" }, + { "Asia/Yerevan", "<+04>-4" }, + { "Atlantic/Azores", "<-01>1<+00>,M3.5.0/0,M10.5.0/1" }, + { "Atlantic/Bermuda", "AST4ADT,M3.2.0,M11.1.0" }, + { "Atlantic/Canary", "WET0WEST,M3.5.0/1,M10.5.0" }, + { "Atlantic/Cape_Verde", "<-01>1" }, + { "Atlantic/Faroe", "WET0WEST,M3.5.0/1,M10.5.0" }, + { "Atlantic/Madeira", "WET0WEST,M3.5.0/1,M10.5.0" }, + { "Atlantic/Reykjavik", "GMT0" }, + { "Atlantic/South_Georgia", "<-02>2" }, + { "Atlantic/Stanley", "<-03>3" }, + { "Atlantic/St_Helena", "GMT0" }, + { "Australia/Adelaide", "ACST-9:30ACDT,M10.1.0,M4.1.0/3" }, + { "Australia/Brisbane", "AEST-10" }, + { "Australia/Broken_Hill", "ACST-9:30ACDT,M10.1.0,M4.1.0/3" }, + { "Australia/Currie", "AEST-10AEDT,M10.1.0,M4.1.0/3" }, + { "Australia/Darwin", "ACST-9:30" }, + { "Australia/Eucla", "<+0845>-8:45" }, + { "Australia/Hobart", "AEST-10AEDT,M10.1.0,M4.1.0/3" }, + { "Australia/Lindeman", "AEST-10" }, + { "Australia/Lord_Howe", "<+1030>-10:30<+11>-11,M10.1.0,M4.1.0" }, + { "Australia/Melbourne", "AEST-10AEDT,M10.1.0,M4.1.0/3" }, + { "Australia/Perth", "AWST-8" }, + { "Australia/Sydney", "AEST-10AEDT,M10.1.0,M4.1.0/3" }, + { "Europe/Amsterdam", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Andorra", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Astrakhan", "<+04>-4" }, + { "Europe/Athens", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Belgrade", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Berlin", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Bratislava", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Brussels", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Bucharest", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Budapest", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Busingen", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Chisinau", "EET-2EEST,M3.5.0,M10.5.0/3" }, + { "Europe/Copenhagen", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Dublin", "IST-1GMT0,M10.5.0,M3.5.0/1" }, + { "Europe/Gibraltar", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Guernsey", "GMT0BST,M3.5.0/1,M10.5.0" }, + { "Europe/Helsinki", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Isle_of_Man", "GMT0BST,M3.5.0/1,M10.5.0" }, + { "Europe/Istanbul", "<+03>-3" }, + { "Europe/Jersey", "GMT0BST,M3.5.0/1,M10.5.0" }, + { "Europe/Kaliningrad", "EET-2" }, + { "Europe/Kiev", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Kirov", "<+03>-3" }, + { "Europe/Lisbon", "WET0WEST,M3.5.0/1,M10.5.0" }, + { "Europe/Ljubljana", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/London", "GMT0BST,M3.5.0/1,M10.5.0" }, + { "Europe/Luxembourg", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Madrid", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Malta", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Mariehamn", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Minsk", "<+03>-3" }, + { "Europe/Monaco", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Moscow", "MSK-3" }, + { "Europe/Oslo", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Paris", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Podgorica", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Prague", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Riga", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Rome", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Samara", "<+04>-4" }, + { "Europe/San_Marino", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Sarajevo", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Saratov", "<+04>-4" }, + { "Europe/Simferopol", "MSK-3" }, + { "Europe/Skopje", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Sofia", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Stockholm", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Tallinn", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Tirane", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Ulyanovsk", "<+04>-4" }, + { "Europe/Uzhgorod", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Vaduz", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Vatican", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Vienna", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Vilnius", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Volgograd", "<+03>-3" }, + { "Europe/Warsaw", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Zagreb", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Europe/Zaporozhye", "EET-2EEST,M3.5.0/3,M10.5.0/4" }, + { "Europe/Zurich", "CET-1CEST,M3.5.0,M10.5.0/3" }, + { "Indian/Antananarivo", "EAT-3" }, + { "Indian/Chagos", "<+06>-6" }, + { "Indian/Christmas", "<+07>-7" }, + { "Indian/Cocos", "<+0630>-6:30" }, + { "Indian/Comoro", "EAT-3" }, + { "Indian/Kerguelen", "<+05>-5" }, + { "Indian/Mahe", "<+04>-4" }, + { "Indian/Maldives", "<+05>-5" }, + { "Indian/Mauritius", "<+04>-4" }, + { "Indian/Mayotte", "EAT-3" }, + { "Indian/Reunion", "<+04>-4" }, + { "Pacific/Apia", "<+13>-13" }, + { "Pacific/Auckland", "NZST-12NZDT,M9.5.0,M4.1.0/3" }, + { "Pacific/Bougainville", "<+11>-11" }, + { "Pacific/Chatham", "<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45" }, + { "Pacific/Chuuk", "<+10>-10" }, + { "Pacific/Easter", "<-06>6<-05>,M9.1.6/22,M4.1.6/22" }, + { "Pacific/Efate", "<+11>-11" }, + { "Pacific/Enderbury", "<+13>-13" }, + { "Pacific/Fakaofo", "<+13>-13" }, + { "Pacific/Fiji", "<+12>-12<+13>,M11.2.0,M1.2.3/99" }, + { "Pacific/Funafuti", "<+12>-12" }, + { "Pacific/Galapagos", "<-06>6" }, + { "Pacific/Gambier", "<-09>9" }, + { "Pacific/Guadalcanal", "<+11>-11" }, + { "Pacific/Guam", "ChST-10" }, + { "Pacific/Honolulu", "HST10" }, + { "Pacific/Kiritimati", "<+14>-14" }, + { "Pacific/Kosrae", "<+11>-11" }, + { "Pacific/Kwajalein", "<+12>-12" }, + { "Pacific/Majuro", "<+12>-12" }, + { "Pacific/Marquesas", "<-0930>9:30" }, + { "Pacific/Midway", "SST11" }, + { "Pacific/Nauru", "<+12>-12" }, + { "Pacific/Niue", "<-11>11" }, + { "Pacific/Norfolk", "<+11>-11<+12>,M10.1.0,M4.1.0/3" }, + { "Pacific/Noumea", "<+11>-11" }, + { "Pacific/Pago_Pago", "SST11" }, + { "Pacific/Palau", "<+09>-9" }, + { "Pacific/Pitcairn", "<-08>8" }, + { "Pacific/Pohnpei", "<+11>-11" }, + { "Pacific/Port_Moresby", "<+10>-10" }, + { "Pacific/Rarotonga", "<-10>10" }, + { "Pacific/Saipan", "ChST-10" }, + { "Pacific/Tahiti", "<-10>10" }, + { "Pacific/Tarawa", "<+12>-12" }, + { "Pacific/Tongatapu", "<+13>-13" }, + { "Pacific/Wake", "<+12>-12" }, + { "Pacific/Wallis", "<+12>-12" }, + { "Etc/GMT-0", "GMT0" }, + { "Etc/GMT-1", "<+01>-1" }, + { "Etc/GMT-2", "<+02>-2" }, + { "Etc/GMT-3", "<+03>-3" }, + { "Etc/GMT-4", "<+04>-4" }, + { "Etc/GMT-5", "<+05>-5" }, + { "Etc/GMT-6", "<+06>-6" }, + { "Etc/GMT-7", "<+07>-7" }, + { "Etc/GMT-8", "<+08>-8" }, + { "Etc/GMT-9", "<+09>-9" }, + { "Etc/GMT-10", "<+10>-10" }, + { "Etc/GMT-11", "<+11>-11" }, + { "Etc/GMT-12", "<+12>-12" }, + { "Etc/GMT-13", "<+13>-13" }, + { "Etc/GMT-14", "<+14>-14" }, + { "Etc/GMT0", "GMT0" }, + { "Etc/GMT+0", "GMT0" }, + { "Etc/GMT+1", "<-01>1" }, + { "Etc/GMT+2", "<-02>2" }, + { "Etc/GMT+3", "<-03>3" }, + { "Etc/GMT+4", "<-04>4" }, + { "Etc/GMT+5", "<-05>5" }, + { "Etc/GMT+6", "<-06>6" }, + { "Etc/GMT+7", "<-07>7" }, + { "Etc/GMT+8", "<-08>8" }, + { "Etc/GMT+9", "<-09>9" }, + { "Etc/GMT+10", "<-10>10" }, + { "Etc/GMT+11", "<-11>11" }, + { "Etc/GMT+12", "<-12>12" }, + { "Etc/UCT", "UTC0" }, + { "Etc/UTC", "UTC0" }, + { "Etc/Greenwich", "GMT0" }, + { "Etc/Universal", "UTC0" }, + { "Etc/Zulu", "UTC0" }, }; + +static const char tzDataOptions[] PROGMEM = + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; diff --git a/HeishaMon/webfunctions.cpp b/HeishaMon/webfunctions.cpp index b13ab97b..681073b0 100755 --- a/HeishaMon/webfunctions.cpp +++ b/HeishaMon/webfunctions.cpp @@ -819,8 +819,6 @@ int getSettings(struct webserver_t *client, settingsStruct *heishamonSettings) { } int handleSettings(struct webserver_t *client) { - - uint16_t size = sizeof(tzdata) / sizeof(tzdata[0]); if (client->content == 0) { webserver_send(client, 200, (char *)"text/html", 0); webserver_send_content_P(client, webHeader, strlen_P(webHeader)); @@ -829,25 +827,15 @@ int handleSettings(struct webserver_t *client) { } else if (client->content == 1) { webserver_send_content_P(client, webBodySettings1, strlen_P(webBodySettings1)); webserver_send_content_P(client, settingsForm1, strlen_P(settingsForm1)); - } else if (client->content >= 2 && client->content < size + 2) { - webserver_send_content_P(client, PSTR(""), 9); - } else if (client->content == size + 2) { + webserver_send_content_P(client, tzDataOptions, strlen_P(tzDataOptions)); + } else if (client->content == 2) { webserver_send_content_P(client, settingsForm2, strlen_P(settingsForm2)); webserver_send_content_P(client, menuJS, strlen_P(menuJS)); webserver_send_content_P(client, settingsJS, strlen_P(settingsJS)); + webserver_send_content_P(client, populategetsettingsJS, strlen_P(populategetsettingsJS)); + } else if (client->content == 3) { webserver_send_content_P(client, populatescanwifiJS, strlen_P(populatescanwifiJS)); - } else if (client->content == size + 3) { webserver_send_content_P(client, changewifissidJS, strlen_P(changewifissidJS)); - webserver_send_content_P(client, populategetsettingsJS, strlen_P(populategetsettingsJS)); webserver_send_content_P(client, webFooter, strlen_P(webFooter)); } From e5cb1f9c54c280fa5c6a7d63e897c2e5fa53b3b3 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Sat, 8 Jan 2022 20:29:47 +0100 Subject: [PATCH 60/75] also destroy cached websettings at wrong password I could also clear the websetting in the if else but this seems nicer as it is following the same structure as the wifi ssid/pass check. --- HeishaMon/webfunctions.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/HeishaMon/webfunctions.cpp b/HeishaMon/webfunctions.cpp index 681073b0..832394ca 100755 --- a/HeishaMon/webfunctions.cpp +++ b/HeishaMon/webfunctions.cpp @@ -402,6 +402,7 @@ int saveSettings(struct webserver_t *client, settingsStruct *heishamonSettings) const char *use_s0 = NULL; bool reconnectWiFi = false; + bool wrongPassword = false; DynamicJsonDocument jsonDoc(1024); settingsToJson(jsonDoc, heishamonSettings); //stores current settings in a json document @@ -503,8 +504,7 @@ int saveSettings(struct webserver_t *client, settingsStruct *heishamonSettings) if (strcmp(heishamonSettings->ota_password, current_ota_password) == 0) { jsonDoc["ota_password"] = new_ota_password; } else { - client->route = 111; - return 0; + wrongPassword = true; } } @@ -529,6 +529,11 @@ int saveSettings(struct webserver_t *client, settingsStruct *heishamonSettings) delete tmp; } + if (wrongPassword) { + client->route = 111; + return 0; + } + if (reconnectWiFi) { client->route = 112; return 0; From 447c56ca4fa005beaed614c4c9b687c0f5d7f9db Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Wed, 12 Jan 2022 19:02:24 +0100 Subject: [PATCH 61/75] firmware upload check improvements --- HeishaMon/HeishaMon.ino | 27 ++++++++++++++------------- HeishaMon/htmlcode.h | 30 +++++++++++++++++------------- HeishaMon/webfunctions.cpp | 3 +++ 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/HeishaMon/HeishaMon.ino b/HeishaMon/HeishaMon.ino index b99c6129..fceac26c 100755 --- a/HeishaMon/HeishaMon.ino +++ b/HeishaMon/HeishaMon.ino @@ -575,9 +575,8 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { return cacheSettings(client, args); } break; case 150: { - if (!Update.hasError()) { - if (strcmp((char *)args->name, "md5") == 0) { - + if (Update.isRunning() && (!Update.hasError())) { + if ((strcmp((char *)args->name, "md5") == 0) && (args->len > 0)) { char md5[args->len + 1]; memset(&md5, 0, args->len + 1); snprintf((char *)&md5, args->len + 1, "%.*s", args->len, args->value); @@ -587,9 +586,10 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { log_message((char *)"Failed to set expected update file MD5!"); Update.end(false); } - } else if (!Update.hasError() && strcmp((char *)args->name, "firmware") == 0) { + } else if (strcmp((char *)args->name, "firmware") == 0) { if (Update.write((uint8_t *)args->value, args->len) != args->len) { Update.printError(Serial1); + Update.end(false); } else { if (uploadpercentage != (unsigned int)(((float)client->readlen / (float)client->totallen) * 20)) { uploadpercentage = (unsigned int)(((float)client->readlen / (float)client->totallen) * 20); @@ -678,16 +678,17 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { return showFirmware(client); } break; case 150: { - if (Update.end(true)) { - String updateHash = Update.md5String(); - sprintf_P(log_msg, PSTR("Uploading success. MD5: %s"), updateHash.c_str()); - log_message(log_msg); - timerqueue_insert(2, 0, -2); // Start reboot sequence - return showFirmwareSuccess(client); - } else { - Update.printError(Serial1); - return showFirmwareFail(client); + if (Update.isRunning()) { + if (Update.end(true)) { + log_message((char*)"Firmware update success"); + timerqueue_insert(2, 0, -2); // Start reboot sequence + return showFirmwareSuccess(client); + } else { + Update.printError(Serial1); + return showFirmwareFail(client); + } } + return 0; } break; default: { webserver_send(client, 301, (char *)"text/plain", 0); diff --git a/HeishaMon/htmlcode.h b/HeishaMon/htmlcode.h index 85abe772..7abc4990 100644 --- a/HeishaMon/htmlcode.h +++ b/HeishaMon/htmlcode.h @@ -955,14 +955,10 @@ const char populategetsettingsJS[] PROGMEM = ""; static const char showFirmwarePage[] PROGMEM = - "" "" + "" "
" - "
" + " " "

Firmware:

" "

" - "

" - " " + "

Warning
If you leave the MD5 checksum empty there will be no check on the uploaded firmware which could cause a bricked HeishaMon!
In this case but also other unforseen errors during update requires you to be able to restore the firmware using a TTL cable!

" + " " "
" - "
"; + "
" -"

Settings

" -"
" -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -"
Please wait, loading saved settings...
" -" Hostname:" -" " -"
" -" Wifi SSID:" -" " -" " -"
" -" Wifi password:" -" " -"
" -" Update username:" -" " -"
" -" Current update password:" -" default password: \"heisha\"" -"
" -" New update password:" -" " -"
" -" Mqtt topic base:" -" " -"
" -" Mqtt server:" -" " -"
" -" Mqtt port:" -" " -"
" -" Mqtt username:" -" " -"
" -" Mqtt password:" -" " -"
" -" How often new values are collected from heatpump:" -" seconds (min 5 sec)" -"
" -" How often all heatpump values are retransmitted to MQTT broker:" -" seconds" -"
" -" Listen only mode:" -" " -"
" -" Debug log to MQTT topic from start:" -" " -"
" -" Debug log hexdump enable from start:" -" " -"
" -" Debug log to serial1 (GPIO2):" -" " -"
" -" Emulate optional PCB:" -" " -"
" -" " -" " -" " -" " -" " -"
" -" Use 1wire DS18b20:" -" " -"
" -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -"
" -" How often new values are collected from 1wire:" -" seconds (min 5 sec)" -"
" -" How often all 1wire values are retransmitted to MQTT broker:" -" seconds" -"
" -" DS18b20 temperature resolution:" -" " -" " -" " -" " -"
" -" " -" " -" " -" " -" " -"
" -" Use s0 kWh metering:" -" " -"
" -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -" " -"
S0 port 1 GPIO:" -" " -"
S0 port 1 imp/kwh:" -" " -"
S0 port 1 reporting interval during standby/low power usage:" -" seconds" -"
S0 port 1 minimal pulse width:" -" milliseconds" -"
S0 port 1 maximal pulse width:" -" milliseconds" -"
S0 port 1 standby/low power usage threshold: Watt" -"
S0 port 2 GPIO:" -" " -"
S0 port 2 imp/kwh:" -" " -"
S0 port 2 reporting interval during standby/low power usage:" -" seconds" -"
S0 port 2 minimal pulse width:" -" milliseconds" -"
S0 port 2 maximal pulse width:" -" milliseconds" -"
S0 port 2 standby/low power usage threshold: Watt" -"
" -"

" -" " -"
" -"
Factory reset" -"
" + "
" + "

Console output

" + "
Enable autoscroll
"; static const char firmwareSuccessResponse[] PROGMEM = "Update success! Rebooting..."; diff --git a/HeishaMon/webfunctions.cpp b/HeishaMon/webfunctions.cpp index 832394ca..23254c88 100755 --- a/HeishaMon/webfunctions.cpp +++ b/HeishaMon/webfunctions.cpp @@ -1098,9 +1098,12 @@ int showFirmware(struct webserver_t *client) { webserver_send_content_P(client, webCSS, strlen_P(webCSS)); webserver_send_content_P(client, webBodyStart, strlen_P(webBodyStart)); webserver_send_content_P(client, showFirmwarePage, strlen_P(showFirmwarePage)); + } else if (client->content == 1) { + webserver_send_content_P(client, websocketJS, strlen_P(websocketJS)); webserver_send_content_P(client, menuJS, strlen_P(menuJS)); webserver_send_content_P(client, webFooter, strlen_P(webFooter)); } + return 0; } From 116b759f070ffa8562e6e99faa4fcd8c8b8f526b Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Fri, 14 Jan 2022 17:13:33 +0100 Subject: [PATCH 62/75] fix weird wifi -1% but online status --- HeishaMon/HeishaMon.ino | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/HeishaMon/HeishaMon.ino b/HeishaMon/HeishaMon.ino index fceac26c..457e1272 100755 --- a/HeishaMon/HeishaMon.ino +++ b/HeishaMon/HeishaMon.ino @@ -110,8 +110,11 @@ int timerqueue_size = 0; */ void check_wifi() { - - if ((WiFi.status() != WL_CONNECTED) || (!WiFi.localIP())) { + if ((WiFi.status() != WL_CONNECTED) && (WiFi.localIP())) { + // special case where it seems that we are not connect but we do have working IP (causing the -1% wifi signal), do a reset. + log_message((char *)"Weird case, WiFi seems disconnected but is not. Resetting WiFi!"); + setupWifi(&heishamonSettings); + } else if ((WiFi.status() != WL_CONNECTED) || (!WiFi.localIP())) { /* if we are not connected to an AP we must be in softAP so respond to DNS From 17b944475e5e9b644694123eea1def202b7850c2 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Fri, 14 Jan 2022 22:02:19 +0100 Subject: [PATCH 63/75] progress bar in javascript for firmware upload --- HeishaMon/htmlcode.h | 51 ++++++++++++++++++++++++++++++-------- HeishaMon/webfunctions.cpp | 5 ++-- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/HeishaMon/htmlcode.h b/HeishaMon/htmlcode.h index 7abc4990..34b052ec 100644 --- a/HeishaMon/htmlcode.h +++ b/HeishaMon/htmlcode.h @@ -956,9 +956,6 @@ const char populategetsettingsJS[] PROGMEM = static const char showFirmwarePage[] PROGMEM = "" "
" "Home" @@ -978,22 +1010,19 @@ static const char showFirmwarePage[] PROGMEM = "Toggle hexdump log" "
" "
" - "
" + " " "

Firmware:

" "

" "

Warning
If you leave the MD5 checksum empty there will be no check on the uploaded firmware which could cause a bricked HeishaMon!
In this case but also other unforseen errors during update requires you to be able to restore the firmware using a TTL cable!

" - " " "
" - "
" - "
" - "

Console output

" - "
Enable autoscroll
"; + "

" + "
"; static const char firmwareSuccessResponse[] PROGMEM = - "Update success! Rebooting..."; + "Update success! Rebooting. This page will refresh afterwards."; static const char firmwareFailResponse[] PROGMEM = - "Update failed! Please try again..."; + "Update failed! Please try again..."; // https://github.com/nayarsystems/posix_tz_db struct tzStruct { diff --git a/HeishaMon/webfunctions.cpp b/HeishaMon/webfunctions.cpp index 23254c88..d46e688f 100755 --- a/HeishaMon/webfunctions.cpp +++ b/HeishaMon/webfunctions.cpp @@ -1097,9 +1097,8 @@ int showFirmware(struct webserver_t *client) { webserver_send_content_P(client, webHeader, strlen_P(webHeader)); webserver_send_content_P(client, webCSS, strlen_P(webCSS)); webserver_send_content_P(client, webBodyStart, strlen_P(webBodyStart)); - webserver_send_content_P(client, showFirmwarePage, strlen_P(showFirmwarePage)); } else if (client->content == 1) { - webserver_send_content_P(client, websocketJS, strlen_P(websocketJS)); + webserver_send_content_P(client, showFirmwarePage, strlen_P(showFirmwarePage)); webserver_send_content_P(client, menuJS, strlen_P(menuJS)); webserver_send_content_P(client, webFooter, strlen_P(webFooter)); } @@ -1117,7 +1116,7 @@ int showFirmwareSuccess(struct webserver_t *client) { static void printUpdateError(char **out, uint8_t size) { uint8_t len = 0; - len = snprintf_P(*out, size, PSTR("
ERROR[%u]: "), Update.getError()); + len = snprintf_P(*out, size, PSTR("ERROR[%u]: "), Update.getError()); if (Update.getError() == UPDATE_ERROR_OK) { snprintf_P(&(*out)[len], size - len, PSTR("No Error")); } else if (Update.getError() == UPDATE_ERROR_WRITE) { From 0e50f971c227996f02a0b1b2f95350853b50a2af Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Wed, 19 Jan 2022 07:37:51 +0100 Subject: [PATCH 64/75] Update README.md --- binaries/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/binaries/README.md b/binaries/README.md index 0f7ff0db..a081215a 100644 --- a/binaries/README.md +++ b/binaries/README.md @@ -6,4 +6,5 @@ The LittleFS versions will, after updating to this version, reset your HeishaMon From version 1.0 some topics are changed so you need to update your automation for this. The sensors are now in /main/ (before /sdc/) and the commands are expected in /commands/ (before in root topic). Check MQTT-Topics.md for the overview of all topics +The latest production release is v2.0. If you decide to try out a later development version, you should be able to restore the firmware using a USB-TTL cable as sometimes upgrading to a development versions seems to fail and brick the heishamon pcb. From 185b598e74d32dc4d9031c57af3ddfc1b796f270 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Thu, 20 Jan 2022 09:51:30 +0100 Subject: [PATCH 65/75] fix opt data error on compare new value --- HeishaMon/decode.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HeishaMon/decode.cpp b/HeishaMon/decode.cpp index 5a37fc90..dc02304a 100644 --- a/HeishaMon/decode.cpp +++ b/HeishaMon/decode.cpp @@ -231,7 +231,7 @@ void decode_optional_heatpump_data(char* data, char* actOptData, PubSubClient & String Topic_Value; Topic_Value = getOptDataValue(data, Topic_Number); - if ((updatenow) || ( getDataValue(actOptData, Topic_Number) != Topic_Value )) { + if ((updatenow) || ( getOptDataValue(actOptData, Topic_Number) != Topic_Value )) { char log_msg[256]; char mqtt_topic[256]; sprintf_P(log_msg, PSTR("received OPT%d %s: %s"), Topic_Number, optTopics[Topic_Number], Topic_Value.c_str()); From cf34d533b336532270bcd032cc2a32677a5434fa Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Thu, 20 Jan 2022 10:02:53 +0100 Subject: [PATCH 66/75] publish raw binary data to mqtt --- HeishaMon/HeishaMon.ino | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/HeishaMon/HeishaMon.ino b/HeishaMon/HeishaMon.ino index 457e1272..b4c7f15d 100755 --- a/HeishaMon/HeishaMon.ino +++ b/HeishaMon/HeishaMon.ino @@ -328,6 +328,11 @@ bool readSerial() if (data_length == DATASIZE) { //decode the normal data decode_heatpump_data(data, actData, mqtt_client, log_message, heishamonSettings.mqtt_topic_base, heishamonSettings.updateAllTime); memcpy(actData, data, DATASIZE); + { + char mqtt_topic[256]; + sprintf(mqtt_topic, "%s/raw/data", heishamonSettings.mqtt_topic_base); + mqtt_client.publish(mqtt_topic, (const uint8_t *)actData, DATASIZE, false); //do not retain this raw data + } data_length = 0; return true; } From 30a9b9b24c3c154513d58fadd84b9917546ce5e3 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Fri, 21 Jan 2022 22:44:12 +0100 Subject: [PATCH 67/75] fix bad header affecting bad reads --- HeishaMon/HeishaMon.ino | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/HeishaMon/HeishaMon.ino b/HeishaMon/HeishaMon.ino index b4c7f15d..649d72ea 100755 --- a/HeishaMon/HeishaMon.ino +++ b/HeishaMon/HeishaMon.ino @@ -288,20 +288,22 @@ bool isValidReceiveChecksum() { bool readSerial() { - if (data_length == 0 ) totalreads++; //this is the start of a new read - - while ((Serial.available()) && (data_length < MAXDATASIZE)) { - data[data_length] = Serial.read(); //read available data and place it after the last received data - data_length++; + int len = 0; + while ((Serial.available()) && (len < MAXDATASIZE)) { + data[data_length+len] = Serial.read(); //read available data and place it after the last received data + len++; if (data[0] != 113) { //wrong header received! log_message((char*)"Received bad header. Ignoring this data!"); - if (heishamonSettings.logHexdump) logHex(data, data_length); + if (heishamonSettings.logHexdump) logHex(data, len); badheaderread++; data_length = 0; return false; //return so this while loop does not loop forever if there happens to be a continous invalid data stream } } + if ((len > 0) && (data_length == 0 )) totalreads++; //this is the start of a new read + data_length += len; + if (data_length > 1) { //should have received length part of header now if ((data_length > (data[1] + 3)) || (data_length >= MAXDATASIZE) ) { From 7a7ecfd53c5d1d937e20715caa97a47544ede035 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Wed, 18 May 2022 08:34:24 +0200 Subject: [PATCH 68/75] Test pr78 optionalpcb (#92) * Implement rules config page and add userdata webserver field * Better memory management * Initialize userdata * Remove obsolete variable * Also check FS:File boolean * Implementing rules * Allow ds18b20 sensors as rules event * Fix proper length of ds18b20 strcmp * Actually subscribe to opentherm topic * Allow opentherm vars as event triggers * Fix small edge case in webserver ```c -----------------------------354617515641648399072235817744\r\n Content-Disposition: form-data; name="rules"\r\n \r\n on ds18b20#28610695f0013c42 then\r\n #foo = ds18b20#28610695f0013c42;\r\n end\r\n \r\n on ?setpoint then\r\n #bar = ?setpoint;\r\n end\r\n -----------------------------354617515641648399072235817744--\r\n \r\n ``` The webserver detects `\r\n--` chunks as possible boundary delimiters. However, when a chunks splits the `\r\n` and `--` tokens the `\r\n` is added to the content and not seen as a potential boundary delimiter. This commit fixes that. * hide settings while loading * Differentiate between push SHA and PR SHA (#83) * Fix small edge case in webserver ```c -----------------------------354617515641648399072235817744\r\n Content-Disposition: form-data; name="rules"\r\n \r\n on ds18b20#28610695f0013c42 then\r\n #foo = ds18b20#28610695f0013c42;\r\n end\r\n \r\n on ?setpoint then\r\n #bar = ?setpoint;\r\n end\r\n -----------------------------354617515641648399072235817744--\r\n \r\n ``` The webserver detects `\r\n--` chunks as possible boundary delimiters. However, when a chunks splits the `\r\n` and `--` tokens the `\r\n` is added to the content and not seen as a potential boundary delimiter. This commit fixes that. * Fixed memory leak in rules.cpp * Workaround too deep function nesting with ringbuffer * Updated documentation to reflect last commits * Store WiFiClient in heap instead of local stack * Forgot to push updated header * Reclaim memory * Reclaim more memory * Reclaim memory in webserver * Don't log rules over MQTT * Store opentherm data in String * Re-enable send_initial_query * Properly communicate NULL * Various rules lib fixes * Allocate mempool for rules storage * Directly send rule commands * Communicate more memory info * add missing ntpservers global static * Added configurable NTP functionality (#77) Co-authored-by: IgorYbema * Better handle PROGMEM structs * Fix few bugs in previous commit * optimize topicdescription handling * sync xmlhttprequest to not overload the webserver * Fix MQTT opentherm topic bug * Only send command when not in listenonly mode * Fix small inconsistency in webserver * Reclaim some more memory * Make @TD-er happy :) * Reserve space for string stats objects * Use static buffers for webserver * Switch PSTR to F for log_message * Revert "Use static buffers for webserver" This reverts commit ecad22132260d15a5a395047ecfe568be4bc5f14. * make ssid and password correct length * Move rules to 2nd heap * Fix merge with main * Fixes in regard to watchdog * Updated webserver to latest version * fix txtoffset alignment * new version of webserver * Fixed merge * Add websockets support to webserver * Add missing libraries * Allow 5 webserver / -socket clients * Proper websocket ping * Increase webserver timeout * Free websocket key when as early as possible * disable initial query again to prevent a lot of start errors * Properly calculate necessary bytes * Make local varstack static * capitalize min / max defines * Remove unused variable * Rename strincmp to strnicmp * Remove need for valstack and better folder structure for rules * Change rules memory free to used * fix merge issue in javascript firmware page * 1 sec optionalpcb * fix weird wifi -1% but online status * progress bar in javascript for firmware upload * Delete HeishaMon.ino.d1_mini.bin * fix rules callback memory free issue causing reboot * better fix for the reboot issue on rules * fix opt data error on compare new value * publish raw binary data to mqtt * add some firmware upload debugging messages * fix merge mistake Co-authored-by: CurlyMoo Co-authored-by: CurlyMoo --- HeishaMon/HeishaMon.ino | 368 +- HeishaMon/commands.cpp | 12 +- HeishaMon/commands.h | 21 +- HeishaMon/dallas.cpp | 8 +- HeishaMon/dallas.h | 5 + HeishaMon/decode.cpp | 13 +- HeishaMon/decode.h | 29 +- HeishaMon/htmlcode.h | 106 +- HeishaMon/rules.cpp | 1537 +++++++ HeishaMon/rules.h | 28 + HeishaMon/src/common/base64.cpp | 209 + HeishaMon/src/common/base64.h | 13 + HeishaMon/src/common/log.cpp | 96 + HeishaMon/src/common/log.h | 24 + HeishaMon/src/common/sha1.cpp | 201 + HeishaMon/src/common/sha1.h | 18 + HeishaMon/src/common/stricmp.cpp | 24 + HeishaMon/src/common/stricmp.h | 14 + HeishaMon/src/common/strnicmp.cpp | 29 + HeishaMon/src/common/strnicmp.h | 14 + HeishaMon/src/common/webserver.cpp | 786 +++- HeishaMon/src/common/webserver.h | 65 +- HeishaMon/src/rules/function.cpp | 45 + HeishaMon/src/rules/function.h | 22 + HeishaMon/src/rules/functions/coalesce.cpp | 74 + HeishaMon/src/rules/functions/coalesce.h | 17 + HeishaMon/src/rules/functions/isset.cpp | 54 + HeishaMon/src/rules/functions/isset.h | 17 + HeishaMon/src/rules/functions/max.cpp | 56 + HeishaMon/src/rules/functions/max.h | 17 + HeishaMon/src/rules/functions/min.cpp | 55 + HeishaMon/src/rules/functions/min.h | 17 + HeishaMon/src/rules/functions/round.cpp | 56 + HeishaMon/src/rules/functions/round.h | 17 + HeishaMon/src/rules/functions/settimer.cpp | 90 + HeishaMon/src/rules/functions/settimer.h | 18 + HeishaMon/src/rules/operator.cpp | 61 + HeishaMon/src/rules/operator.h | 24 + HeishaMon/src/rules/operators/and.cpp | 110 + HeishaMon/src/rules/operators/and.h | 16 + HeishaMon/src/rules/operators/divide.cpp | 82 + HeishaMon/src/rules/operators/divide.h | 16 + HeishaMon/src/rules/operators/eq.cpp | 93 + HeishaMon/src/rules/operators/eq.h | 16 + HeishaMon/src/rules/operators/ge.cpp | 129 + HeishaMon/src/rules/operators/ge.h | 16 + HeishaMon/src/rules/operators/gt.cpp | 129 + HeishaMon/src/rules/operators/gt.h | 16 + HeishaMon/src/rules/operators/le.cpp | 129 + HeishaMon/src/rules/operators/le.h | 16 + HeishaMon/src/rules/operators/lt.cpp | 129 + HeishaMon/src/rules/operators/lt.h | 16 + HeishaMon/src/rules/operators/minus.cpp | 118 + HeishaMon/src/rules/operators/minus.h | 16 + HeishaMon/src/rules/operators/mod.cpp | 119 + HeishaMon/src/rules/operators/mod.h | 16 + HeishaMon/src/rules/operators/multiply.cpp | 98 + HeishaMon/src/rules/operators/multiply.h | 16 + HeishaMon/src/rules/operators/ne.cpp | 99 + HeishaMon/src/rules/operators/ne.h | 16 + HeishaMon/src/rules/operators/or.cpp | 112 + HeishaMon/src/rules/operators/or.h | 16 + HeishaMon/src/rules/operators/plus.cpp | 116 + HeishaMon/src/rules/operators/plus.h | 16 + HeishaMon/src/rules/operators/power.cpp | 116 + HeishaMon/src/rules/operators/power.h | 16 + HeishaMon/src/rules/rules.cpp | 4619 ++++++++++++++++++++ HeishaMon/src/rules/rules.h | 256 ++ HeishaMon/version.h | 2 +- HeishaMon/webfunctions.cpp | 130 +- HeishaMon/webfunctions.h | 9 + README.md | 180 + 72 files changed, 10885 insertions(+), 374 deletions(-) create mode 100755 HeishaMon/rules.cpp create mode 100755 HeishaMon/rules.h create mode 100755 HeishaMon/src/common/base64.cpp create mode 100755 HeishaMon/src/common/base64.h create mode 100755 HeishaMon/src/common/log.cpp create mode 100755 HeishaMon/src/common/log.h create mode 100755 HeishaMon/src/common/sha1.cpp create mode 100755 HeishaMon/src/common/sha1.h create mode 100755 HeishaMon/src/common/stricmp.cpp create mode 100755 HeishaMon/src/common/stricmp.h create mode 100755 HeishaMon/src/common/strnicmp.cpp create mode 100755 HeishaMon/src/common/strnicmp.h mode change 100755 => 100644 HeishaMon/src/common/webserver.h create mode 100755 HeishaMon/src/rules/function.cpp create mode 100755 HeishaMon/src/rules/function.h create mode 100755 HeishaMon/src/rules/functions/coalesce.cpp create mode 100755 HeishaMon/src/rules/functions/coalesce.h create mode 100755 HeishaMon/src/rules/functions/isset.cpp create mode 100755 HeishaMon/src/rules/functions/isset.h create mode 100755 HeishaMon/src/rules/functions/max.cpp create mode 100755 HeishaMon/src/rules/functions/max.h create mode 100755 HeishaMon/src/rules/functions/min.cpp create mode 100755 HeishaMon/src/rules/functions/min.h create mode 100755 HeishaMon/src/rules/functions/round.cpp create mode 100755 HeishaMon/src/rules/functions/round.h create mode 100755 HeishaMon/src/rules/functions/settimer.cpp create mode 100755 HeishaMon/src/rules/functions/settimer.h create mode 100755 HeishaMon/src/rules/operator.cpp create mode 100755 HeishaMon/src/rules/operator.h create mode 100755 HeishaMon/src/rules/operators/and.cpp create mode 100755 HeishaMon/src/rules/operators/and.h create mode 100755 HeishaMon/src/rules/operators/divide.cpp create mode 100755 HeishaMon/src/rules/operators/divide.h create mode 100755 HeishaMon/src/rules/operators/eq.cpp create mode 100755 HeishaMon/src/rules/operators/eq.h create mode 100755 HeishaMon/src/rules/operators/ge.cpp create mode 100755 HeishaMon/src/rules/operators/ge.h create mode 100755 HeishaMon/src/rules/operators/gt.cpp create mode 100755 HeishaMon/src/rules/operators/gt.h create mode 100755 HeishaMon/src/rules/operators/le.cpp create mode 100755 HeishaMon/src/rules/operators/le.h create mode 100755 HeishaMon/src/rules/operators/lt.cpp create mode 100755 HeishaMon/src/rules/operators/lt.h create mode 100755 HeishaMon/src/rules/operators/minus.cpp create mode 100755 HeishaMon/src/rules/operators/minus.h create mode 100755 HeishaMon/src/rules/operators/mod.cpp create mode 100755 HeishaMon/src/rules/operators/mod.h create mode 100755 HeishaMon/src/rules/operators/multiply.cpp create mode 100755 HeishaMon/src/rules/operators/multiply.h create mode 100755 HeishaMon/src/rules/operators/ne.cpp create mode 100755 HeishaMon/src/rules/operators/ne.h create mode 100755 HeishaMon/src/rules/operators/or.cpp create mode 100755 HeishaMon/src/rules/operators/or.h create mode 100755 HeishaMon/src/rules/operators/plus.cpp create mode 100755 HeishaMon/src/rules/operators/plus.h create mode 100755 HeishaMon/src/rules/operators/power.cpp create mode 100755 HeishaMon/src/rules/operators/power.h create mode 100755 HeishaMon/src/rules/rules.cpp create mode 100755 HeishaMon/src/rules/rules.h diff --git a/HeishaMon/HeishaMon.ino b/HeishaMon/HeishaMon.ino index 649d72ea..2dbd58a3 100755 --- a/HeishaMon/HeishaMon.ino +++ b/HeishaMon/HeishaMon.ino @@ -12,9 +12,14 @@ #include "lwip/apps/sntp.h" #include "src/common/timerqueue.h" +#include "src/common/stricmp.h" +#include "src/common/log.h" +#include "src/rules/rules.h" + #include "webfunctions.h" #include "decode.h" #include "commands.h" +#include "rules.h" DNSServer dnsServer; @@ -34,8 +39,6 @@ const byte DNS_PORT = 53; #define SERIALTIMEOUT 2000 // wait until all 203 bytes are read, must not be too long to avoid blocking the code -WebSocketsServer webSocket = WebSocketsServer(81); - settingsStruct heishamonSettings; bool sending = false; // mutex for sending data @@ -48,6 +51,7 @@ unsigned long lastMqttReconnectAttempt = 0; unsigned long lastWifiRetryTimer = 0; unsigned long lastRunTime = 0; +unsigned long lastOptionalPCBRunTime = 0; unsigned long sendCommandReadTime = 0; //set to millis value during send, allow to wait millis for answer unsigned long goodreads = 0; @@ -63,10 +67,10 @@ static int uploadpercentage = 0; // instead of passing array pointers between functions we just define this in the global scope #define MAXDATASIZE 255 char data[MAXDATASIZE] = { '\0' }; -byte data_length = 0; +byte data_length = 0; -// store actual data -#define DATASIZE 203 +// store actual data +String openTherm[2]; char actData[DATASIZE] = { '\0' }; #define OPTDATASIZE 20 char actOptData[OPTDATASIZE] = { '\0' }; @@ -125,7 +129,7 @@ void check_wifi() also, do not disconnect if wifi network scan is active */ if ((heishamonSettings.wifi_ssid[0] != '\0') && (WiFi.status() != WL_DISCONNECTED) && (WiFi.scanComplete() != -1) && (WiFi.softAPgetStationNum() > 0)) { - log_message((char *)"WiFi lost, but softAP station connecting, so stop trying to connect to configured ssid..."); + log_message(F("WiFi lost, but softAP station connecting, so stop trying to connect to configured ssid...")); WiFi.disconnect(true); } @@ -135,24 +139,24 @@ void check_wifi() if ((heishamonSettings.wifi_ssid[0] != '\0') && ((unsigned long)(millis() - lastWifiRetryTimer) > WIFIRETRYTIMER ) ) { lastWifiRetryTimer = millis(); if (WiFi.softAPSSID() == "") { - log_message((char *)"WiFi lost, starting setup hotspot..."); + log_message(F("WiFi lost, starting setup hotspot...")); WiFi.softAP((char*)"HeishaMon-Setup"); } if ((WiFi.status() == WL_DISCONNECTED) && (WiFi.softAPgetStationNum() == 0 )) { - log_message((char *)"Retrying configured WiFi, ..."); + log_message(F("Retrying configured WiFi, ...")); if (heishamonSettings.wifi_password[0] == '\0') { WiFi.begin(heishamonSettings.wifi_ssid); } else { WiFi.begin(heishamonSettings.wifi_ssid, heishamonSettings.wifi_password); } } else { - log_message((char *)"Reconnecting to WiFi failed. Waiting a few seconds before trying again."); + log_message(F("Reconnecting to WiFi failed. Waiting a few seconds before trying again.")); WiFi.disconnect(true); } } } else { //WiFi connected if (WiFi.softAPSSID() != "") { - log_message((char *)"WiFi (re)connected, shutting down hotspot..."); + log_message(F("WiFi (re)connected, shutting down hotspot...")); WiFi.softAPdisconnect(true); MDNS.notifyAPChange(); } @@ -166,7 +170,7 @@ void check_wifi() experimental::ESP8266WiFiGratuitous::stationKeepAliveSetIntervalMs(5000); //necessary for some users with bad wifi routers if (heishamonSettings.wifi_ssid[0] == '\0') { - log_message((char *)"WiFi connected without SSID and password in settings. Must come from persistent memory. Storing in settings."); + log_message(F("WiFi connected without SSID and password in settings. Must come from persistent memory. Storing in settings.")); WiFi.SSID().toCharArray(heishamonSettings.wifi_ssid, 40); WiFi.psk().toCharArray(heishamonSettings.wifi_password, 40); DynamicJsonDocument jsonDoc(1024); @@ -193,13 +197,14 @@ void mqtt_reconnect() unsigned long now = millis(); if ((lastMqttReconnectAttempt == 0) || ((unsigned long)(now - lastMqttReconnectAttempt) > MQTTRECONNECTTIMER)) { //only try reconnect each MQTTRECONNECTTIMER seconds or on boot when lastMqttReconnectAttempt is still 0 lastMqttReconnectAttempt = now; - log_message((char*)"Reconnecting to mqtt server ..."); + log_message(F("Reconnecting to mqtt server ...")); char topic[256]; sprintf(topic, "%s/%s", heishamonSettings.mqtt_topic_base, mqtt_willtopic); if (mqtt_client.connect(heishamonSettings.wifi_hostname, heishamonSettings.mqtt_username, heishamonSettings.mqtt_password, topic, 1, true, "Offline")) { mqttReconnects++; + mqtt_client.subscribe("panasonic_heat_pump/opentherm/#"); sprintf(topic, "%s/%s/#", heishamonSettings.mqtt_topic_base, mqtt_topic_commands); mqtt_client.subscribe(topic); sprintf(topic, "%s/%s", heishamonSettings.mqtt_topic_base, mqtt_send_raw_value_topic); @@ -223,6 +228,20 @@ void mqtt_reconnect() } } +void log_message(const __FlashStringHelper *msg) { + PGM_P p = (PGM_P)msg; + int len = strlen_P((const char *)p); + char *str = (char *)MALLOC(len+1); + if(str == NULL) { + OUT_OF_MEMORY + } + strcpy_P(str, p); + + log_message(str); + + FREE(str); +} + void log_message(char* string) { time_t rawtime; @@ -251,9 +270,7 @@ void log_message(char* string) mqtt_client.disconnect(); } } - if (webSocket.connectedClients() > 0) { - webSocket.broadcastTXT(log_line, strlen(log_line)); - } + websocket_write_all(log_line, strlen(log_line)); free(log_line); } @@ -265,7 +282,7 @@ void logHex(char *hex, byte hex_len) { for (int j = 0; ((j < LOGHEXBYTESPERLINE) && ((i + j) < hex_len)); j++) { sprintf(&buffer[3 * j], "%02X ", hex[i + j]); } - sprintf(log_msg, "data: %s", buffer ); log_message(log_msg); + sprintf_P(log_msg, PSTR("data: %s"), buffer ); log_message(log_msg); } } @@ -293,7 +310,7 @@ bool readSerial() data[data_length+len] = Serial.read(); //read available data and place it after the last received data len++; if (data[0] != 113) { //wrong header received! - log_message((char*)"Received bad header. Ignoring this data!"); + log_message(F("Received bad header. Ignoring this data!")); if (heishamonSettings.logHexdump) logHex(data, len); badheaderread++; data_length = 0; @@ -307,7 +324,7 @@ bool readSerial() if (data_length > 1) { //should have received length part of header now if ((data_length > (data[1] + 3)) || (data_length >= MAXDATASIZE) ) { - log_message((char*)"Received more data than header suggests! Ignoring this as this is bad data."); + log_message(F("Received more data than header suggests! Ignoring this as this is bad data.")); if (heishamonSettings.logHexdump) logHex(data, data_length); data_length = 0; toolongread++; @@ -315,16 +332,16 @@ bool readSerial() } if (data_length == (data[1] + 3)) { //we received all data (data[1] is header length field) - sprintf(log_msg, "Received %d bytes data", data_length); log_message(log_msg); + sprintf_P(log_msg, PSTR("Received %d bytes data"), data_length); log_message(log_msg); sending = false; //we received an answer after our last command so from now on we can start a new send request again if (heishamonSettings.logHexdump) logHex(data, data_length); if (! isValidReceiveChecksum() ) { - log_message((char*)"Checksum received false!"); + log_message(F("Checksum received false!")); data_length = 0; //for next attempt badcrcread++; return false; } - log_message((char*)"Checksum and header received ok!"); + log_message(F("Checksum and header received ok!")); goodreads++; if (data_length == DATASIZE) { //decode the normal data @@ -339,14 +356,14 @@ bool readSerial() return true; } else if (data_length == OPTDATASIZE ) { //optional pcb acknowledge answer - log_message((char*)"Received optional PCB ack answer. Decoding this in OPT topics."); + log_message(F("Received optional PCB ack answer. Decoding this in OPT topics.")); decode_optional_heatpump_data(data, actOptData, mqtt_client, log_message, heishamonSettings.mqtt_topic_base, heishamonSettings.updateAllTime); memcpy(actOptData, data, OPTDATASIZE); data_length = 0; return true; } else { - log_message((char*)"Received a shorter datagram. Can't decode this yet."); + log_message(F("Received a shorter datagram. Can't decode this yet.")); data_length = 0; return false; } @@ -366,7 +383,7 @@ void popCommandBuffer() { void pushCommandBuffer(byte* command, int length) { if (cmdnrel + 1 > MAXCOMMANDSINBUFFER) { - log_message((char *)"Too much commands already in buffer. Ignoring this commands.\n"); + log_message(F("Too much commands already in buffer. Ignoring this commands.\n")); return; } cmdbuffer[cmdend].length = length; @@ -377,11 +394,11 @@ void pushCommandBuffer(byte* command, int length) { bool send_command(byte* command, int length) { if ( heishamonSettings.listenonly ) { - log_message((char*)"Not sending this command. Heishamon in listen only mode!"); + log_message(F("Not sending this command. Heishamon in listen only mode!")); return false; } if ( sending ) { - log_message((char*)"Already sending data. Buffering this send request"); + log_message(F("Already sending data. Buffering this send request")); pushCommandBuffer(command, length); return false; } @@ -401,7 +418,7 @@ bool send_command(byte* command, int length) { // Callback function that is called when a message has been pushed to one of your topics. void mqtt_callback(char* topic, byte* payload, unsigned int length) { if (mqttcallbackinprogress) { - log_message((char*)"Already processing another mqtt callback. Ignoring this one"); + log_message(F("Already processing another mqtt callback. Ignoring this one")); } else { mqttcallbackinprogress = true; //simple semaphore to make sure we don't have two callbacks at the same time @@ -431,12 +448,25 @@ void mqtt_callback(char* topic, byte* payload, unsigned int length) { char mqtt_topic[256]; sprintf(mqtt_topic, "%s", topic); if (mqtt_client.unsubscribe(mqtt_topic)) { - log_message((char*)"Unsubscribed from S0 watthour restore topic"); + log_message(F("Unsubscribed from S0 watthour restore topic")); } } else if (strncmp(topic_command, mqtt_topic_commands, 8) == 0) // check for optional pcb commands { char* topic_sendcommand = topic_command + 9; //strip the first 9 "commands/" from the topic to get what we need send_heatpump_command(topic_sendcommand, msg, send_command, log_message, heishamonSettings.optionalPCB); + } else if (stricmp((char const *)topic, "panasonic_heat_pump/opentherm/Temperature") == 0) { + char cpy[length + 1]; + memset(&cpy, 0, length + 1); + strncpy(cpy, (char *)payload, length); + openTherm[0] = cpy; + rules_event_cb("temperature"); + } else if (stricmp((char const *)topic, "panasonic_heat_pump/opentherm/Setpoint") == 0) { + char cpy[length + 1]; + memset(&cpy, 0, length + 1); + strncpy(cpy, (char *)payload, length); + openTherm[1] = cpy; + + rules_event_cb("setpoint"); } mqttcallbackinprogress = false; } @@ -468,49 +498,61 @@ void setupOTA() { int8_t webserver_cb(struct webserver_t *client, void *dat) { switch (client->step) { case WEBSERVER_CLIENT_REQUEST_METHOD: { - if (strcmp((char *)dat, "POST") == 0) { + if (strcmp_P((char *)dat, PSTR("POST")) == 0) { client->route = 110; } return 0; } break; case WEBSERVER_CLIENT_REQUEST_URI: { - if (strcmp((char *)dat, "/") == 0) { + if (strcmp_P((char *)dat, PSTR("/")) == 0) { client->route = 1; - } else if (strcmp((char *)dat, "/tablerefresh") == 0) { + } else if (strcmp_P((char *)dat, PSTR("/tablerefresh")) == 0) { client->route = 10; - } else if (strcmp((char *)dat, "/json") == 0) { + } else if (strcmp_P((char *)dat, PSTR("/json")) == 0) { client->route = 20; - } else if (strcmp((char *)dat, "/reboot") == 0) { + } else if (strcmp_P((char *)dat, PSTR("/reboot")) == 0) { client->route = 30; - } else if (strcmp((char *)dat, "/debug") == 0) { + } else if (strcmp_P((char *)dat, PSTR("/debug")) == 0) { client->route = 40; - log_message((char*)"Debug URL requested"); - } else if (strcmp((char *)dat, "/wifiscan") == 0) { + log_message(F("Debug URL requested")); + } else if (strcmp_P((char *)dat, PSTR("/wifiscan")) == 0) { client->route = 50; - } else if (strcmp((char *)dat, "/togglelog") == 0) { + } else if (strcmp_P((char *)dat, PSTR("/togglelog")) == 0) { client->route = 1; - log_message((char*)"Toggled mqtt log flag"); + log_message(F("Toggled mqtt log flag")); heishamonSettings.logMqtt ^= true; - } else if (strcmp((char *)dat, "/togglehexdump") == 0) { + } else if (strcmp_P((char *)dat, PSTR("/togglehexdump")) == 0) { client->route = 1; - log_message((char*)"Toggled hexdump log flag"); + log_message(F("Toggled hexdump log flag")); heishamonSettings.logHexdump ^= true; - } else if (strcmp((char *)dat, "/hotspot-detect.html") == 0 || - strcmp((char *)dat, "/fwlink") == 0 || - strcmp((char *)dat, "/generate_204") == 0 || - strcmp((char *)dat, "/gen_204") == 0 || - strcmp((char *)dat, "/popup") == 0) { + } else if (strcmp_P((char *)dat, PSTR("/hotspot-detect.html")) == 0 || + strcmp_P((char *)dat, PSTR("/fwlink")) == 0 || + strcmp_P((char *)dat, PSTR("/generate_204")) == 0 || + strcmp_P((char *)dat, PSTR("/gen_204")) == 0 || + strcmp_P((char *)dat, PSTR("/popup")) == 0) { client->route = 80; - } else if (strcmp((char *)dat, "/factoryreset") == 0) { + } else if (strcmp_P((char *)dat, PSTR("/factoryreset")) == 0) { client->route = 90; - } else if (strcmp((char *)dat, "/command") == 0) { - RESTmsg.clear(); + } else if (strcmp_P((char *)dat, PSTR("/command")) == 0) { + if((client->userdata = malloc(1)) == NULL) { + Serial1.printf(PSTR("Out of memory %s:#%d\n"), __FUNCTION__, __LINE__); + ESP.restart(); + exit(-1); + } + ((char *)client->userdata)[0] = 0; client->route = 100; } else if (client->route == 110) { // Only accept settings POST requests - if (strcmp((char *)dat, "/savesettings") == 0) { + if (strcmp_P((char *)dat, PSTR("/savesettings")) == 0) { client->route = 110; - } else if (strcmp((char *)dat, "/firmware") == 0) { + } else if (strcmp_P((char *)dat, PSTR("/saverules")) == 0) { + client->route = 170; + + if (LittleFS.begin()) { + LittleFS.remove("/rules.new"); + client->userdata = new File(LittleFS.open("/rules.new", "a+")); + } + } else if (strcmp_P((char *)dat, PSTR("/firmware")) == 0) { if (!Update.isRunning()) { Update.runAsync(true); if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { @@ -520,19 +562,21 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { client->route = 150; } } else { - Serial1.println("New firmware update client, while previous isn't finished yet! Assume broken connection, abort!"); + Serial1.println(PSTR("New firmware update client, while previous isn't finished yet! Assume broken connection, abort!")); Update.end(); return -1; } } else { return -1; } - } else if (strcmp((char *)dat, "/settings") == 0) { + } else if (strcmp_P((char *)dat, PSTR("/settings")) == 0) { client->route = 120; - } else if (strcmp((char *)dat, "/getsettings") == 0) { + } else if (strcmp_P((char *)dat, PSTR("/getsettings")) == 0) { client->route = 130; - } else if (strcmp((char *)dat, "/firmware") == 0) { + } else if (strcmp_P((char *)dat, PSTR("/firmware")) == 0) { client->route = 140; + } else if (strcmp_P((char *)dat, PSTR("/rules")) == 0) { + client->route = 160; } else { client->route = 0; } @@ -543,9 +587,9 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { struct arguments_t *args = (struct arguments_t *)dat; switch (client->route) { case 10: { - if (strcmp((char *)args->name, "1wire") == 0) { + if (strcmp_P((char *)args->name, PSTR("1wire")) == 0) { client->route = 11; - } else if (strcmp((char *)args->name, "s0") == 0) { + } else if (strcmp_P((char *)args->name, PSTR("s0")) == 0) { client->route = 12; } } break; @@ -559,9 +603,17 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { snprintf((char *)&cpy, args->len + 1, "%.*s", args->len, args->value); for (uint8_t x = 0; x < sizeof(commands) / sizeof(commands[0]); x++) { - if (strcmp((char *)args->name, commands[x].name) == 0) { - len = commands[x].func(cpy, cmd, log_msg); - RESTmsg = RESTmsg + log_msg + "\n"; + cmdStruct tmp; + memcpy_P(&tmp, &commands[x], sizeof(tmp)); + if (strcmp((char *)args->name, tmp.name) == 0) { + len = tmp.func(cpy, cmd, log_msg); + if ((client->userdata = realloc(client->userdata, strlen((char *)client->userdata) + strlen(log_msg) + 2)) == NULL) { + Serial1.printf(PSTR("Out of memory %s:#%d\n"), __FUNCTION__, __LINE__); + ESP.restart(); + exit(-1); + } + strcat((char *)client->userdata, log_msg); + strcat((char *)client->userdata, "\n"); log_message(log_msg); send_command(cmd, len); } @@ -573,13 +625,33 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { if (heishamonSettings.optionalPCB) { //optional commands for (uint8_t x = 0; x < sizeof(optionalCommands) / sizeof(optionalCommands[0]); x++) { - if (strcmp((char *)args->name, optionalCommands[x].name) == 0) { - len = optionalCommands[x].func(cpy, log_msg); - RESTmsg = RESTmsg + log_msg + "\n"; + optCmdStruct tmp; + memcpy_P(&tmp, &optionalCommands[x], sizeof(tmp)); + if (strcmp((char *)args->name, tmp.name) == 0) { + len = tmp.func(cpy, log_msg); + if ((client->userdata = realloc(client->userdata, strlen((char *)client->userdata) + strlen(log_msg) + 2)) == NULL) { + Serial1.printf(PSTR("Out of memory %s:#%d\n"), __FUNCTION__, __LINE__); + ESP.restart(); + exit(-1); + } + strcat((char *)client->userdata, log_msg); + strcat((char *)client->userdata, "\n"); log_message(log_msg); } } } + + if (stricmp((char const *)args->name, "temperature") == 0) { + char cpy[args->len + 1]; + strcpy(cpy, (char *)args->value); + openTherm[0] = cpy; + rules_event_cb("temperature"); + } else if (stricmp((char const *)args->name, "setpoint") == 0) { + char cpy[args->len + 1]; + strcpy(cpy, (char *)args->value); + openTherm[1] = cpy; + rules_event_cb("setpoint"); + } } break; case 110: { return cacheSettings(client, args); @@ -593,7 +665,7 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { sprintf_P(log_msg, PSTR("Firmware MD5 expected: %s"), md5); log_message(log_msg); if (!Update.setMD5(md5)) { - log_message((char *)"Failed to set expected update file MD5!"); + log_message(F("Failed to set expected update file MD5!")); Update.end(false); } } else if (strcmp((char *)args->name, "firmware") == 0) { @@ -608,6 +680,16 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { } } } + } else { + log_message((char*)"New firmware POST data but update not running anymore!"); + } + } break; + case 170: { + File *f = (File *)client->userdata; + if (!f || !*f) { + client->route = 160; + } else { + f->write(args->value, args->len); } } break; } @@ -619,7 +701,10 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { case WEBSERVER_CLIENT_WRITE: { switch (client->route) { case 0: { - webserver_send(client, 404, (char *)"text/plain", 0); + if(client->content == 0) { + webserver_send(client, 404, (char *)"text/plain", 13); + webserver_send_content_P(client, PSTR("404 Not found"), 13); + } return 0; } break; case 1: { @@ -651,9 +736,10 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { case 100: { if (client->content == 0) { webserver_send(client, 200, (char *)"text/plain", 0); - char *str = (char *)RESTmsg.c_str(); - webserver_send_content(client, (char *)str, strlen(str)); - RESTmsg.clear(); + char *RESTmsg = (char *)client->userdata; + webserver_send_content(client, (char *)RESTmsg, strlen(RESTmsg)); + free(RESTmsg); + client->userdata = NULL; } return 0; } break; @@ -688,6 +774,7 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { return showFirmware(client); } break; case 150: { + log_message((char*)"In /firmware client write part"); if (Update.isRunning()) { if (Update.end(true)) { log_message((char*)"Firmware update success"); @@ -700,6 +787,21 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { } return 0; } break; + case 160: { + return showRules(client); + } break; + case 170: { + File *f = (File *)client->userdata; + if (f) { + if (*f) { + f->close(); + } + delete f; + } + client->userdata = NULL; + timerqueue_insert(0, 1, -4); + webserver_send(client, 301, (char *)"text/plain", 0); + } break; default: { webserver_send(client, 301, (char *)"text/plain", 0); } break; @@ -710,34 +812,66 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) { struct header_t *header = (struct header_t *)dat; switch (client->route) { case 113: { - header->ptr += sprintf((char *)header->buffer, "Location: /settings"); + header->ptr += sprintf_P((char *)header->buffer, PSTR("Location: /settings")); return -1; } break; - case 0: case 60: case 70: { - header->ptr += sprintf((char *)header->buffer, "Location: /"); + header->ptr += sprintf_P((char *)header->buffer, PSTR("Location: /")); + return -1; + } break; + case 170: { + header->ptr += sprintf_P((char *)header->buffer, PSTR("Location: /rules")); return -1; } break; default: { - header->ptr += sprintf((char *)header->buffer, "Access-Control-Allow-Origin: *"); + if(client->route != 0) { + header->ptr += sprintf_P((char *)header->buffer, PSTR("Access-Control-Allow-Origin: *")); + } } break; } return 0; } break; + case WEBSERVER_CLIENT_CLOSE: { + switch (client->route) { + case 100: { + if (client->userdata != NULL) { + free(client->userdata); + } + } break; + case 110: { + struct websettings_t *tmp = NULL; + while (client->userdata) { + tmp = (struct websettings_t *)client->userdata; + client->userdata = ((struct websettings_t *)(client->userdata))->next; + free(tmp); + } + } break; + case 160: + case 170: { + if (client->userdata != NULL) { + File *f = (File *)client->userdata; + if (f) { + if (*f) { + f->close(); + } + delete f; + } + } + } break; + } + client->userdata = NULL; + } break; default: { return 0; } break; } + return 0; } void setupHttp() { webserver_start(80, &webserver_cb, 0); - - webSocket.begin(); - webSocket.onEvent(webSocketEvent); - webSocket.enableHeartbeat(3000, 3000, 2); } void doubleResetDetect() { @@ -810,13 +944,14 @@ void setupConditionals() { //load optional PCB data from flash if (heishamonSettings.optionalPCB) { if (loadOptionalPCB(optionalPCBQuery, OPTIONALPCBQUERYSIZE)) { - log_message((char*)"Succesfully loaded optional PCB data from saved flash!"); + log_message(F("Succesfully loaded optional PCB data from saved flash!")); } else { - log_message((char*)"Failed to load optional PCB data from flash!"); + log_message(F("Failed to load optional PCB data from flash!")); } delay(1500); //need 1.5 sec delay before sending first datagram send_optionalpcb_query(); //send one datagram already at start + lastOptionalPCBRunTime = millis(); } //these two after optional pcb because it needs to send a datagram fast after boot @@ -825,11 +960,8 @@ void setupConditionals() { } void timer_cb(int nr) { - sprintf_P(log_msg, PSTR("%d seconds timer interval"), nr); - log_message(log_msg); - - if (nr > 0) { - timerqueue_insert(nr, 0, nr); + if(nr > 0) { + rules_timer_cb(nr); } else { switch (nr) { case -1: { @@ -844,6 +976,18 @@ void timer_cb(int nr) { case -3: { setupWifi(&heishamonSettings); } break; + case -4: { + if(rules_parse("/rules.new") == -1) { + logprintln_P(F("new ruleset failed to parse, using previous ruleset")); + rules_parse("/rules.txt"); + } else { + logprintln_P(F("new ruleset successfully parsed")); + if(LittleFS.begin()) { + LittleFS.rename("/rules.new", "/rules.txt"); + } + } + rules_boot(); + } break; } } @@ -890,37 +1034,44 @@ void setup() { dnsServer.setErrorReplyCode(DNSReplyCode::NoError); dnsServer.start(DNS_PORT, "*", apIP); - //timerqueue tests - //timerqueue_insert(1, 0, 1); - //timerqueue_insert(5, 0, 5); - //timerqueue_insert(60, 0, 60); - //maybe necessary but for now disable. CZ-TAW1 sends this query on boot //if (!heishamonSettings.listenonly) send_initial_query(); + + rst_info *resetInfo = ESP.getResetInfoPtr(); + Serial1.printf(PSTR("Reset reason: %d, exception cause: %d\n"), resetInfo->reason, resetInfo->exccause); + + if(resetInfo->reason > 0 && resetInfo->reason < 4) { + if(LittleFS.begin()) { + LittleFS.rename("/rules.txt", "/rules.old"); + } + rules_setup(); + if(LittleFS.begin()) { + LittleFS.rename("/rules.old", "/rules.txt"); + } + } else { + rules_setup(); + } } void send_initial_query() { - String message = F("Requesting initial start query"); - log_message((char*)message.c_str()); + log_message(F("Requesting initial start query")); send_command(initialQuery, INITIALQUERYSIZE); } void send_panasonic_query() { - String message = F("Requesting new panasonic data"); - log_message((char*)message.c_str()); + log_message(F("Requesting new panasonic data")); send_command(panasonicQuery, PANASONICQUERYSIZE); } void send_optionalpcb_query() { - String message = F("Sending optional PCB data"); - log_message((char*)message.c_str()); + log_message(F("Sending optional PCB data")); send_command(optionalPCBQuery, OPTIONALPCBQUERYSIZE); } void read_panasonic_data() { if (sending && ((unsigned long)(millis() - sendCommandReadTime) > SERIALTIMEOUT)) { - log_message((char*)"Previous read data attempt failed due to timeout!"); + log_message(F("Previous read data attempt failed due to timeout!")); sprintf_P(log_msg, PSTR("Received %d bytes data"), data_length); log_message(log_msg); if (heishamonSettings.logHexdump) logHex(data, data_length); @@ -943,15 +1094,13 @@ void loop() { check_wifi(); // Handle OTA first. ArduinoOTA.handle(); - // handle Websockets - webSocket.loop(); mqtt_client.loop(); read_panasonic_data(); if ((!sending) && (cmdnrel > 0)) { //check if there is a send command in the buffer - log_message((char *)"Sending command from buffer"); + log_message(F("Sending command from buffer")); popCommandBuffer(); } @@ -959,7 +1108,10 @@ void loop() { if (heishamonSettings.use_s0) s0Loop(mqtt_client, log_message, heishamonSettings.mqtt_topic_base, heishamonSettings.s0Settings); - if ((!sending) && (!heishamonSettings.listenonly) && (heishamonSettings.optionalPCB)) send_optionalpcb_query(); //send this as fast as possible or else we could get warnings on heatpump + if ((!sending) && (!heishamonSettings.listenonly) && (heishamonSettings.optionalPCB) && ((unsigned long)(millis() - lastOptionalPCBRunTime) > OPTIONALPCBQUERYTIME) ) { + lastOptionalPCBRunTime = millis(); + send_optionalpcb_query(); + } // run the data query only each WAITTIME if ((unsigned long)(millis() - lastRunTime) > (1000 * heishamonSettings.waitTime)) { @@ -967,19 +1119,25 @@ void loop() { //check mqtt if ( (WiFi.isConnected()) && (!mqtt_client.connected()) ) { - log_message((char *)"Lost MQTT connection!"); + log_message(F("Lost MQTT connection!")); mqtt_reconnect(); } //log stats if (totalreads > 0 ) readpercentage = (((float)goodreads / (float)totalreads) * 100); - String message = F("Heishamon stats: Uptime: "); + String message; + message.reserve(384); + message += F("Heishamon stats: Uptime: "); char *up = getUptime(); message += up; free(up); message += F(" ## Free memory: "); message += getFreeMemory(); - message += F("% "); + message += F("% ## Heap fragmentation: "); + message += ESP.getHeapFragmentation(); + message += F("% ## Max free block: "); + message += ESP.getMaxFreeBlockSize(); + message += F(" bytes ## Free heap: "); message += ESP.getFreeHeap(); message += F(" bytes ## Wifi: "); message += getWifiQuality(); @@ -992,7 +1150,9 @@ void loop() { message += F("%"); log_message((char*)message.c_str()); - String stats = F("{\"uptime\":"); + String stats; + stats.reserve(384); + stats += F("{\"uptime\":"); stats += String(millis()); stats += F(",\"voltage\":"); stats += ESP.getVcc() / 1024.0; @@ -1019,14 +1179,14 @@ void loop() { stats += F(",\"timeout reads\":"); stats += timeoutread; stats += F("}"); - sprintf(mqtt_topic, "%s/stats", heishamonSettings.mqtt_topic_base); + sprintf_P(mqtt_topic, PSTR("%s/stats"), heishamonSettings.mqtt_topic_base); mqtt_client.publish(mqtt_topic, stats.c_str(), MQTT_RETAIN_VALUES); //get new data if (!heishamonSettings.listenonly) send_panasonic_query(); //Make sure the LWT is set to Online, even if the broker have marked it dead. - sprintf(mqtt_topic, "%s/%s", heishamonSettings.mqtt_topic_base, mqtt_willtopic); + sprintf_P(mqtt_topic, PSTR("%s/%s"), heishamonSettings.mqtt_topic_base, mqtt_willtopic); mqtt_client.publish(mqtt_topic, "Online"); if (WiFi.isConnected()) { diff --git a/HeishaMon/commands.cpp b/HeishaMon/commands.cpp index 77c00f31..068b5693 100644 --- a/HeishaMon/commands.cpp +++ b/HeishaMon/commands.cpp @@ -749,8 +749,10 @@ void send_heatpump_command(char* topic, char *msg, bool (*send_command)(byte*, i unsigned int len = 0; for (unsigned int i = 0; i < sizeof(commands) / sizeof(commands[0]); i++) { - if (strcmp(topic, commands[i].name) == 0) { - len = commands[i].func(msg, cmd, log_msg); + cmdStruct tmp; + memcpy_P(&tmp, &commands[i], sizeof(tmp)); + if (strcmp(topic, tmp.name) == 0) { + len = tmp.func(msg, cmd, log_msg); log_message(log_msg); send_command(cmd, len); } @@ -759,8 +761,10 @@ void send_heatpump_command(char* topic, char *msg, bool (*send_command)(byte*, i if (optionalPCB) { //run for optional pcb commands for (unsigned int i = 0; i < sizeof(optionalCommands) / sizeof(optionalCommands[0]); i++) { - if (strcmp(topic, optionalCommands[i].name) == 0) { - len = optionalCommands[i].func(msg, log_msg); + optCmdStruct tmp; + memcpy_P(&tmp, &optionalCommands[i], sizeof(tmp)); + if (strcmp(topic, tmp.name) == 0) { + len = tmp.func(msg, log_msg); log_message(log_msg); if ((unsigned long)(millis() - lastOptionalPCBSave) > (1000 * OPTIONALPCBSAVETIME)) { // only save each 5 minutes lastOptionalPCBSave = millis(); diff --git a/HeishaMon/commands.h b/HeishaMon/commands.h index 0bb24194..853a5e58 100644 --- a/HeishaMon/commands.h +++ b/HeishaMon/commands.h @@ -3,16 +3,19 @@ #include #include - +#define DATASIZE 203 #define INITIALQUERYSIZE 7 extern byte initialQuery[INITIALQUERYSIZE]; #define PANASONICQUERYSIZE 110 extern byte panasonicQuery[PANASONICQUERYSIZE]; + +#define OPTIONALPCBQUERYTIME 1000 //send optional pcb query each second #define OPTIONALPCBQUERYSIZE 19 #define OPTIONALPCBSAVETIME 300 //save each 5 minutes the current optional pcb state into flash to have valid values during reboot extern byte optionalPCBQuery[OPTIONALPCBQUERYSIZE]; + extern const char* mqtt_topic_values; extern const char* mqtt_topic_commands; extern const char* mqtt_topic_pcbvalues; @@ -66,10 +69,12 @@ unsigned int set_z2_water_temp(char *msg, char *log_msg); unsigned int set_solar_temp(char *msg, char *log_msg); unsigned int set_byte_9(char *msg, char *log_msg); -struct { - const char *name; +struct cmdStruct { + char name[28]; unsigned int (*func)(char *msg, unsigned char *cmd, char *log_msg); -} commands[] PROGMEM = { +}; + +const cmdStruct commands[] PROGMEM = { // set heatpump state to on by sending 1 { "SetHeatpump", set_heatpump_state }, // set pump state to on by sending 1 @@ -114,10 +119,12 @@ struct { { "SetMainSchedule", set_main_schedule }, }; -struct { - const char *name; +struct optCmdStruct{ + char name[28]; unsigned int (*func)(char *msg, char *log_msg); -} optionalCommands[] PROGMEM = { +}; + +const optCmdStruct optionalCommands[] PROGMEM = { // optional PCB { "SetHeatCoolMode", set_heat_cool_mode }, { "SetCompressorState", set_compressor_state }, diff --git a/HeishaMon/dallas.cpp b/HeishaMon/dallas.cpp index 7befa671..1bfe03ac 100644 --- a/HeishaMon/dallas.cpp +++ b/HeishaMon/dallas.cpp @@ -3,6 +3,7 @@ #include #include "commands.h" #include "dallas.h" +#include "rules.h" #define MQTT_RETAIN_VALUES 1 // do we retain 1wire values? @@ -74,7 +75,7 @@ void readNewDallasTemp(PubSubClient &mqtt_client, void (*log_message)(char*), ch for (int i = 0; i < dallasDevicecount; i++) { float temp = DS18B20.getTempC(actDallasData[i].sensor); if (temp < -120.0) { - sprintf(log_msg, "Error 1wire sensor offline: %s", actDallasData[i].address); log_message(log_msg); + sprintf_P(log_msg, PSTR("Error 1wire sensor offline: %s"), actDallasData[i].address); log_message(log_msg); } else { float allowedtempdiff = (((millis() - actDallasData[i].lastgoodtime)) / 1000.0) * MAXTEMPDIFFPERSEC; if ((actDallasData[i].temperature != -127.0) and ((temp > (actDallasData[i].temperature + allowedtempdiff)) or (temp < (actDallasData[i].temperature - allowedtempdiff)))) { @@ -86,8 +87,9 @@ void readNewDallasTemp(PubSubClient &mqtt_client, void (*log_message)(char*), ch actDallasData[i].temperature = temp; sprintf(log_msg, PSTR("Received 1wire sensor temperature (%s): %.2f"), actDallasData[i].address, actDallasData[i].temperature); log_message(log_msg); - sprintf(valueStr, "%.2f", actDallasData[i].temperature); - sprintf(mqtt_topic, "%s/%s/%s", mqtt_topic_base, mqtt_topic_1wire, actDallasData[i].address); mqtt_client.publish(mqtt_topic, valueStr, MQTT_RETAIN_VALUES); + sprintf_P(valueStr, PSTR("%.2f"), actDallasData[i].temperature); + sprintf_P(mqtt_topic, PSTR("%s/%s/%s"), mqtt_topic_base, mqtt_topic_1wire, actDallasData[i].address); mqtt_client.publish(mqtt_topic, valueStr, MQTT_RETAIN_VALUES); + rules_event_cb(actDallasData[i].address); } } } diff --git a/HeishaMon/dallas.h b/HeishaMon/dallas.h index b1c9055d..76393041 100644 --- a/HeishaMon/dallas.h +++ b/HeishaMon/dallas.h @@ -1,3 +1,6 @@ +#ifndef _DALLAS_H_ +#define _DALLAS_H_ + #include #include #include @@ -18,3 +21,5 @@ void dallasLoop(PubSubClient &mqtt_client, void (*log_message)(char*), char* mqt void initDallasSensors(void (*log_message)(char*), unsigned int updataAllDallasTimeSettings, unsigned int dallasTimerWaitSettings, unsigned int dallasResolution); void dallasJsonOutput(struct webserver_t *client); void dallasTableOutput(struct webserver_t *client); + +#endif \ No newline at end of file diff --git a/HeishaMon/decode.cpp b/HeishaMon/decode.cpp index dc02304a..a45a1c73 100644 --- a/HeishaMon/decode.cpp +++ b/HeishaMon/decode.cpp @@ -1,5 +1,6 @@ #include "decode.h" #include "commands.h" +#include "rules.h" unsigned long lastalldatatime = 0; unsigned long lastalloptdatatime = 0; @@ -24,7 +25,6 @@ String getBit3and4and5(byte input) { return String(((input >> 3) & 0b111) - 1); } - String getLeft5bits(byte input) { return String((input >> 3) - 1); } @@ -47,11 +47,13 @@ String getIntMinus1Div5(byte input) { return String((((float)input - 1) / 5), 1); } + String getIntMinus1Times10(byte input) { int value = (int)input - 1; return (String)(value * 10); } + String getIntMinus1Times50(byte input) { int value = (int)input - 1; return (String)(value * 50); @@ -97,8 +99,7 @@ String getModel(char* data) { // TOP92 // return String(modelResult); } -String getEnergy(byte input) -{ +String getEnergy(byte input) { int value = ((int)input - 1) * 200; return (String)value; } @@ -215,8 +216,9 @@ void decode_heatpump_data(char* data, char* actData, PubSubClient &mqtt_client, char mqtt_topic[256]; sprintf_P(log_msg, PSTR("received TOP%d %s: %s"), Topic_Number, topics[Topic_Number], Topic_Value.c_str()); log_message(log_msg); - sprintf(mqtt_topic, "%s/%s/%s", mqtt_topic_base, mqtt_topic_values, topics[Topic_Number]); + sprintf_P(mqtt_topic, PSTR("%s/%s/%s"), mqtt_topic_base, mqtt_topic_values, topics[Topic_Number]); mqtt_client.publish(mqtt_topic, Topic_Value.c_str(), MQTT_RETAIN_VALUES); + rules_new_event(topics[Topic_Number]); } } } @@ -236,8 +238,9 @@ void decode_optional_heatpump_data(char* data, char* actOptData, PubSubClient & char mqtt_topic[256]; sprintf_P(log_msg, PSTR("received OPT%d %s: %s"), Topic_Number, optTopics[Topic_Number], Topic_Value.c_str()); log_message(log_msg); - sprintf(mqtt_topic, "%s/%s/%s", mqtt_topic_base, mqtt_topic_pcbvalues, optTopics[Topic_Number]); + sprintf_P(mqtt_topic, PSTR("%s/%s/%s"), mqtt_topic_base, mqtt_topic_pcbvalues, optTopics[Topic_Number]); mqtt_client.publish(mqtt_topic, Topic_Value.c_str(), MQTT_RETAIN_VALUES); + rules_new_event(optTopics[Topic_Number]); } } //response to heatpump should contain the data from heatpump on byte 4 and 5 diff --git a/HeishaMon/decode.h b/HeishaMon/decode.h index ac34bf5e..18d83df5 100644 --- a/HeishaMon/decode.h +++ b/HeishaMon/decode.h @@ -86,6 +86,7 @@ static const byte knownModels[sizeof(Model) / sizeof(Model[0])][10] PROGMEM = { #define NUMBER_OF_TOPICS 107 //last topic number + 1 #define NUMBER_OF_OPT_TOPICS 7 //last topic number + 1 +#define MAX_TOPIC_LEN 41 // max length + 1 static const char optTopics[][20] PROGMEM = { "Z1_Water_Pump", // OPT0 @@ -97,7 +98,7 @@ static const char optTopics[][20] PROGMEM = { "Alarm_State", // OPT6 }; -static const char topics[][40] PROGMEM = { +static const char topics[][MAX_TOPIC_LEN] PROGMEM = { "Heatpump_State", //TOP0 "Pump_Flow", //TOP1 "Force_DHW_State", //TOP2 @@ -439,19 +440,19 @@ static const char *OpModeDesc[] PROGMEM = {"9", "Heat", "Cool", "Auto(heat)", "D static const char *Powerfulmode[] PROGMEM = {"4", "Off", "30min", "60min", "90min"}; static const char *Quietmode[] PROGMEM = {"4", "Off", "Level 1", "Level 2", "Level 3"}; static const char *Valve[] PROGMEM = {"2", "Room", "DHW"}; -static const char *LitersPerMin[] PROGMEM = {"value", "l/min"}; -static const char *RotationsPerMin[] PROGMEM = {"value", "r/min"}; -static const char *Pressure[] PROGMEM = {"value", "Kgf/cm2"}; -static const char *Celsius[] PROGMEM = {"value", "°C"}; -static const char *Kelvin[] PROGMEM = {"value", "K"}; -static const char *Hertz[] PROGMEM = {"value", "Hz"}; -static const char *Counter[] PROGMEM = {"value", "count"}; -static const char *Hours[] PROGMEM = {"value", "hours"}; -static const char *Watt[] PROGMEM = {"value", "Watt"}; -static const char *ErrorState[] PROGMEM = {"value", "Error"}; -static const char *Ampere[] PROGMEM = {"value", "Ampere"}; -static const char *Minutes[] PROGMEM = {"value", "Minutes"}; -static const char *Duty[] PROGMEM = {"value", "Duty"}; +static const char *LitersPerMin[] PROGMEM = {"0", "l/min"}; +static const char *RotationsPerMin[] PROGMEM = {"0", "r/min"}; +static const char *Pressure[] PROGMEM = {"0", "Kgf/cm2"}; +static const char *Celsius[] PROGMEM = {"0", "°C"}; +static const char *Kelvin[] PROGMEM = {"0", "K"}; +static const char *Hertz[] PROGMEM = {"0", "Hz"}; +static const char *Counter[] PROGMEM = {"0", "count"}; +static const char *Hours[] PROGMEM = {"0", "hours"}; +static const char *Watt[] PROGMEM = {"0", "Watt"}; +static const char *ErrorState[] PROGMEM = {"0", "Error"}; +static const char *Ampere[] PROGMEM = {"0", "Ampere"}; +static const char *Minutes[] PROGMEM = {"0", "Minutes"}; +static const char *Duty[] PROGMEM = {"0", "Duty"}; static const char *ZonesState[] PROGMEM = {"3", "Zone1 active", "Zone2 active", "Zone1 and zone2 active"}; static const char *HeatCoolModeDesc[] PROGMEM = {"2", "Comp. Curve", "Direct"}; static const char *SolarModeDesc[] PROGMEM = {"3", "Disabled", "Buffer", "DHW"}; diff --git a/HeishaMon/htmlcode.h b/HeishaMon/htmlcode.h index 34b052ec..9c5d9454 100644 --- a/HeishaMon/htmlcode.h +++ b/HeishaMon/htmlcode.h @@ -29,10 +29,10 @@ static const char websocketJS[] PROGMEM = " var bConnected = false;" " function startWebsockets() {" " if(typeof MozWebSocket != \"undefined\") {" - " oWebsocket = new MozWebSocket(\"ws://\" + location.host + \":81\");" + " oWebsocket = new MozWebSocket(\"ws://\" + location.host + \":80\");" " } else if(typeof WebSocket != \"undefined\") {" " /* The characters after the trailing slash are needed for a wierd IE 10 bug */" - " oWebsocket = new WebSocket(\"ws://\" + location.host + \":81/ws\");" + " oWebsocket = new WebSocket(\"ws://\" + location.host + \":80/ws\");" " }" "" " if(oWebsocket) {" @@ -70,9 +70,9 @@ static const char refreshJS[] PROGMEM = " };" " function loadContent(id, url, func) {" " var xhr = new XMLHttpRequest();" - " xhr.open('GET', url, true);" + " xhr.open('GET', url, false);" //sync request to not overload the webserver " xhr.send();" - " xhr.onload = function() {" + //" xhr.onload = function() {" //sync request to not overload the webserver " if(xhr.status == 200) {" " let obj = document.getElementById(id);" " if(obj) {" @@ -80,7 +80,7 @@ static const char refreshJS[] PROGMEM = " func();" " }" " }" - " }" + //" }" //sync request to not overload the webserver " }" " function refreshTable(tableName){" @@ -181,6 +181,7 @@ static const char webBodyRoot1[] PROGMEM = "Reboot" "Firmware" "Settings" + "Rules" "Toggle mqtt log" "Toggle hexdump log" "
Version: "; @@ -223,6 +224,26 @@ static const char webBodyRootConsole[] PROGMEM = "

Console output

" "
Enable autoscroll
"; +static const char showRulesPage1[] PROGMEM = + "" + "
" + "

Rules

" + "
" + "
" + " " + "
" + "
"; + static const char webBodyFactoryResetWarning[] PROGMEM = "
" "

Removing configuration. To reconfigure please connect to WiFi hotspot after reset.

" @@ -258,84 +279,11 @@ static const char webBodySettings1[] PROGMEM = "Home" "Reboot" "Firmware" + "Rules" "Toggle mqtt log" "Toggle hexdump log" "
"; -static const char webBodySmartcontrol1[] PROGMEM = - ""; - -static const char webBodySmartcontrol2[] PROGMEM = - "
" - "" - // "" - "
"; - -static const char webBodySmartcontrolHeatingcurve1[] PROGMEM = - "
" - "

Heating curve setting

"; - -static const char webBodySmartcontrolHeatingcurve2[] PROGMEM = - "

" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "
Outside TemperatureTarget Setpoint
...Loading...
"; - -static const char webBodySmartcontrolHeatingcurveSVG[] PROGMEM = - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "-20" - "-20" - "15" - "15" - "" - "" - "60" - "60" - "20" - "20" - "" - "" - "Outside temperature" - "" - "" - "Target setpoint" - "" - "" - ""; - -//static const char webBodySmartcontrolOtherexample[] PROGMEM = -// "
" -// "

Other example

"; - static const char webCSS[] PROGMEM = "