mirror of https://github.com/lumapu/ahoy.git
				
				
			
				 39 changed files with 3052 additions and 2373 deletions
			
			
		| @ -1,13 +1,71 @@ | |||||
| # Changelog | # Changelog | ||||
| 
 | 
 | ||||
| * fix browser sync NTP button | ## 0.5.52 | ||||
| * added login feature (protect web ui) | * improved ahoyWifi class | ||||
| * added static IP option | * added interface class for app | ||||
| * improved initial boot - don't connect to `YOUR_WIFI_SSID` any more, directly boot into AP mode | * refactored web and webApi -> RestApi | ||||
| * added status LED support | * fix calcSunrise was not called every day | ||||
| * improved MQTT handling (boot, periodic updates, no zero values any more) | * added MQTT RX counter to index.html | ||||
| * replaced deprecated workflow functions | * all values are displayed on /live even if they are 0 | ||||
| * refactored code to make it more clearly | * added MQTT <TOPIC>/status to show status over all inverters | ||||
| * added scheduler to register functions which need to be run each second / minute / ... | 
 | ||||
| * changed settings to littlefs (-> no currupt settings in future on memory layout changes) | ## 0.5.51 | ||||
| * added a lot of system infos to `System` page for support | * improved scheduler, @beegee3 #483 | ||||
|  | * refactored get NTP time, @beegee3 #483 | ||||
|  | * generate `bin.gz` only for 1M device ESP8285 | ||||
|  | * fix calcSunrise was not called every day | ||||
|  | * incresed number of allowed characters for MQTT user, broker and password, @DanielR92 | ||||
|  | * added NRF24 info to Systeminfo, @DanielR92 | ||||
|  | * added timezone for monochrome displays, @gh-fx2 | ||||
|  | * added support for second inverter for monochrome displays, @gh-fx2 | ||||
|  | 
 | ||||
|  | ## 0.5.50 | ||||
|  | * fixed scheduler, uptime and timestamp counted too fast | ||||
|  | * added / renamed automatically build outputs | ||||
|  | * fixed MQTT ESP uptime on reconnect (not zero any more) | ||||
|  | * changed uptime on index.html to count each second, synced with ESP each 10 seconds | ||||
|  | 
 | ||||
|  | ## 0.5.49 | ||||
|  | * fixed AP mode on brand new ESP modules | ||||
|  | * fixed `last_success` MQTT message | ||||
|  | * fixed MQTT inverter available status at sunset | ||||
|  | * reordered enqueue commands after boot up to prevent same payload length for successive commands | ||||
|  | * added automatic build for Nokia5110 and SSD1306 displays (ESP8266) | ||||
|  | 
 | ||||
|  | ## 0.5.48 | ||||
|  | * added MQTT message send at sunset | ||||
|  | * added monochrome display support | ||||
|  | * added `once` and `onceAt` to scheduler to make code cleaner | ||||
|  | * improved sunrise / sunset calculation | ||||
|  | 
 | ||||
|  | ## 0.5.47 | ||||
|  | * refactored ahoyWifi class: AP is opened on every boot, once station connection is successful the AP will be closed | ||||
|  | * improved NTP sync after boot, faster sync | ||||
|  | * fix NRF24 details only on valid SPI connection | ||||
|  | 
 | ||||
|  | ## 0.5.46 | ||||
|  | * fix sunrise / sunset calculation | ||||
|  | * improved setup.html: `reboot on save` is checked as default | ||||
|  | 
 | ||||
|  | ## 0.5.45 | ||||
|  | * changed MQTT last will topic from `status` to `mqtt` | ||||
|  | * fix sunrise / sunset calculation | ||||
|  | * fix time of serial web console | ||||
|  | 
 | ||||
|  | ## 0.5.44 | ||||
|  | * marked some MQTT messages as retained | ||||
|  | * moved global functions to global location (no duplicates) | ||||
|  | * changed index.html inverval to static 10 seconds | ||||
|  | * fix static IP | ||||
|  | * fix NTP with static IP | ||||
|  | * print MQTT info only if MQTT was configured | ||||
|  | 
 | ||||
|  | ## 0.5.43 | ||||
|  | * updated REST API and MQTT (both of them use the same functionality) | ||||
|  | * added ESP-heap information as MQTT message | ||||
|  | * changed output name of automatic development build to fixed name (to have a static link from https://ahoydtu.de) | ||||
|  | * updated user manual to latest MQTT and API changes | ||||
|  | 
 | ||||
|  | ## 0.5.42 | ||||
|  | * fix web logout (auto logout) | ||||
|  | * switched MQTT library | ||||
|  | |||||
| @ -0,0 +1,41 @@ | |||||
|  | //-----------------------------------------------------------------------------
 | ||||
|  | // 2022 Ahoy, https://ahoydtu.de
 | ||||
|  | // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
 | ||||
|  | //-----------------------------------------------------------------------------
 | ||||
|  | 
 | ||||
|  | #ifndef __IAPP_H__ | ||||
|  | #define __IAPP_H__ | ||||
|  | 
 | ||||
|  | #include "defines.h" | ||||
|  | 
 | ||||
|  | // abstract interface to App. Make members of App accessible from child class
 | ||||
|  | // like web or API without forward declaration
 | ||||
|  | class IApp { | ||||
|  |     public: | ||||
|  |         virtual ~IApp() {} | ||||
|  |         virtual bool saveSettings() = 0; | ||||
|  |         virtual bool eraseSettings(bool eraseWifi) = 0; | ||||
|  |         virtual void setRebootFlag() = 0; | ||||
|  |         virtual const char *getVersion() = 0; | ||||
|  |         virtual statistics_t *getStatistics() = 0; | ||||
|  |         virtual void scanAvailNetworks() = 0; | ||||
|  |         virtual void getAvailNetworks(JsonObject obj) = 0; | ||||
|  | 
 | ||||
|  |         virtual uint32_t getUptime() = 0; | ||||
|  |         virtual uint32_t getTimestamp() = 0; | ||||
|  |         virtual uint32_t getSunrise() = 0; | ||||
|  |         virtual uint32_t getSunset() = 0; | ||||
|  |         virtual void setTimestamp(uint32_t newTime) = 0; | ||||
|  |         virtual String getTimeStr(uint32_t offset) = 0; | ||||
|  |         virtual uint32_t getTimezoneOffset() = 0; | ||||
|  | 
 | ||||
|  |         virtual bool getRebootRequestState() = 0; | ||||
|  |         virtual bool getSettingsValid() = 0; | ||||
|  |         virtual void setMqttDiscoveryFlag() = 0; | ||||
|  | 
 | ||||
|  |         virtual bool getMqttIsConnected() = 0; | ||||
|  |         virtual uint32_t getMqttRxCnt() = 0; | ||||
|  |         virtual uint32_t getMqttTxCnt() = 0; | ||||
|  | }; | ||||
|  | 
 | ||||
|  | #endif /*__IAPP_H__*/ | ||||
| @ -0,0 +1,307 @@ | |||||
|  | #ifndef __MONOCHROME_DISPLAY__ | ||||
|  | #define __MONOCHROME_DISPLAY__ | ||||
|  | 
 | ||||
|  | #if defined(ENA_NOKIA) || defined(ENA_SSD1306) | ||||
|  | #ifdef ENA_NOKIA | ||||
|  |     #include <U8g2lib.h> | ||||
|  |     #define DISP_PROGMEM U8X8_PROGMEM | ||||
|  | #else // ENA_SSD1306
 | ||||
|  |     /* esp8266 : SCL = 5, SDA = 4 */ | ||||
|  |     /* ewsp32  : SCL = 22, SDA = 21 */ | ||||
|  |     #include <Wire.h> | ||||
|  |     #include <SSD1306Wire.h> | ||||
|  |     #define DISP_PROGMEM PROGMEM | ||||
|  | #endif | ||||
|  | 
 | ||||
|  | #include <Timezone.h> | ||||
|  | 
 | ||||
|  | #include "../../utils/helper.h" | ||||
|  | #include "../../hm/hmSystem.h" | ||||
|  | 
 | ||||
|  | static uint8_t bmp_arrow[] DISP_PROGMEM = { | ||||
|  |     B00000000, B00011100, B00011100, B00001110, B00001110, B11111110, B01111111, | ||||
|  |     B01110000, B01110000, B00110000, B00111000, B00011000, B01111111, B00111111, | ||||
|  |     B00011110, B00001110, B00000110, B00000000, B00000000, B00000000, B00000000}; | ||||
|  | 
 | ||||
|  | static TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};     // Central European Summer Time
 | ||||
|  | static TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};       // Central European Standard Tim
 | ||||
|  | 
 | ||||
|  | template<class HMSYSTEM> | ||||
|  | class MonochromeDisplay { | ||||
|  |     public: | ||||
|  |         #if defined(ENA_NOKIA) | ||||
|  |         MonochromeDisplay() : mDisplay(U8G2_R0, 5, 4, 16), mCE(CEST, CET) { | ||||
|  |             mNewPayload = false; | ||||
|  |             mExtra      = 0; | ||||
|  |         } | ||||
|  |         #else // ENA_SSD1306
 | ||||
|  |         MonochromeDisplay() : mDisplay(0x3c, SDA, SCL), mCE(CEST, CET) { | ||||
|  |             mNewPayload = false; | ||||
|  |             mExtra      = 0; | ||||
|  |             mRx         = 0; | ||||
|  |             mUp         = 1; | ||||
|  |         } | ||||
|  |         #endif | ||||
|  | 
 | ||||
|  |         void setup(HMSYSTEM *sys, uint32_t *utcTs) { | ||||
|  |             mSys   = sys; | ||||
|  |             mUtcTs = utcTs; | ||||
|  |             memset( mToday, 0, sizeof(float)*MAX_NUM_INVERTERS ); | ||||
|  |             memset( mTotal, 0, sizeof(float)*MAX_NUM_INVERTERS ); | ||||
|  |             mLastHour = 25; | ||||
|  |             #if defined(ENA_NOKIA) | ||||
|  |                 mDisplay.begin(); | ||||
|  |                 ShowInfoText("booting..."); | ||||
|  |             #else | ||||
|  |                 mDisplay.init(); | ||||
|  |                 mDisplay.flipScreenVertically(); | ||||
|  |                 mDisplay.setContrast(63); | ||||
|  |                 mDisplay.setBrightness(63); | ||||
|  | 
 | ||||
|  |                 mDisplay.clear(); | ||||
|  |                 mDisplay.setFont(ArialMT_Plain_24); | ||||
|  |                 mDisplay.setTextAlignment(TEXT_ALIGN_CENTER_BOTH); | ||||
|  | 
 | ||||
|  |                 mDisplay.drawString(64,22,"Starting..."); | ||||
|  |                 mDisplay.display(); | ||||
|  |                 mDisplay.setTextAlignment(TEXT_ALIGN_LEFT); | ||||
|  |             #endif | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void loop(void) { | ||||
|  | 
 | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void payloadEventListener(uint8_t cmd) { | ||||
|  |             mNewPayload = true; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void tickerSecond() { | ||||
|  |             static int cnt=1; | ||||
|  |             if(mNewPayload || !(cnt % 10)) { | ||||
|  |                 cnt=1; | ||||
|  |                 mNewPayload = false; | ||||
|  |                 DataScreen(); | ||||
|  |             } | ||||
|  |             else | ||||
|  |                cnt++; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |     private: | ||||
|  |         #if defined(ENA_NOKIA) | ||||
|  |         void ShowInfoText(const char *txt) { | ||||
|  |             /* u8g2_font_open_iconic_embedded_2x_t 'D' + 'G' + 'J' */ | ||||
|  |             mDisplay.clear(); | ||||
|  |             mDisplay.firstPage(); | ||||
|  |             do { | ||||
|  |                 const char *e; | ||||
|  |                 const char *p = txt; | ||||
|  |                 int y=10; | ||||
|  |                 mDisplay.setFont(u8g2_font_5x8_tr); | ||||
|  |                 while(1) { | ||||
|  |                     for(e=p+1; (*e && (*e != '\n')); e++); | ||||
|  |                     size_t len=e-p; | ||||
|  |                     mDisplay.setCursor(2,y); | ||||
|  |                     String res=((String)p).substring(0,len); | ||||
|  |                     mDisplay.print(res); | ||||
|  |                     if ( !*e ) | ||||
|  |                         break; | ||||
|  |                     p=e+1; | ||||
|  |                     y+=12; | ||||
|  |                 } | ||||
|  |                 mDisplay.sendBuffer(); | ||||
|  |             } while( mDisplay.nextPage() ); | ||||
|  |         } | ||||
|  |         #endif | ||||
|  | 
 | ||||
|  |         void DataScreen(void) { | ||||
|  |             String timeStr = ah::getDateTimeStr(mCE.toLocal(*mUtcTs)).substring(2, 22); | ||||
|  |             int hr = timeStr.substring(9,2).toInt(); | ||||
|  |             IPAddress ip = WiFi.localIP(); | ||||
|  |             float totalYield = 0.0, totalYieldToday = 0.0, totalActual = 0.0; | ||||
|  |             char fmtText[32]; | ||||
|  |             int  ucnt=0, num_inv=0; | ||||
|  |             unsigned int pow_i[ MAX_NUM_INVERTERS ]; | ||||
|  | 
 | ||||
|  |             memset( pow_i, 0, sizeof(unsigned int)* MAX_NUM_INVERTERS ); | ||||
|  |             if ( hr < mLastHour )  // next day ? reset today-values
 | ||||
|  |                 memset( mToday, 0, sizeof(float)*MAX_NUM_INVERTERS ); | ||||
|  |             mLastHour = hr; | ||||
|  | 
 | ||||
|  |             for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { | ||||
|  |                 Inverter<> *iv = mSys->getInverterByPos(id); | ||||
|  |                 if (NULL != iv) { | ||||
|  |                     record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); | ||||
|  |                     uint8_t pos; | ||||
|  |                     uint8_t list[] = {FLD_PAC, FLD_YT, FLD_YD}; | ||||
|  | 
 | ||||
|  |                     for (uint8_t fld = 0; fld < 3; fld++) { | ||||
|  |                         pos = iv->getPosByChFld(CH0, list[fld],rec); | ||||
|  |                         int isprod = iv->isProducing(*mUtcTs,rec); | ||||
|  | 
 | ||||
|  |                         if(fld == 1) | ||||
|  |                         { | ||||
|  |                             if ( isprod ) | ||||
|  |                                 mTotal[num_inv] = iv->getValue(pos,rec); | ||||
|  |                             totalYield += mTotal[num_inv]; | ||||
|  |                         } | ||||
|  |                         if(fld == 2) | ||||
|  |                         { | ||||
|  |                             if ( isprod ) | ||||
|  |                                 mToday[num_inv] = iv->getValue(pos,rec); | ||||
|  |                             totalYieldToday += mToday[num_inv]; | ||||
|  |                         } | ||||
|  |                         if((fld == 0) && isprod ) | ||||
|  |                         { | ||||
|  |                             pow_i[num_inv] = iv->getValue(pos,rec); | ||||
|  |                             totalActual += iv->getValue(pos,rec); | ||||
|  |                             ucnt++; | ||||
|  |                         } | ||||
|  |                     } | ||||
|  |                     num_inv++; | ||||
|  |                 } | ||||
|  |             } | ||||
|  |             /* u8g2_font_open_iconic_embedded_2x_t 'D' + 'G' + 'J' */ | ||||
|  |             mDisplay.clear(); | ||||
|  |             #if defined(ENA_NOKIA) | ||||
|  |                 mDisplay.firstPage(); | ||||
|  |                 do { | ||||
|  |                     if(ucnt) { | ||||
|  |                         mDisplay.drawXBMP(10,1,8,17,bmp_arrow); | ||||
|  |                         mDisplay.setFont(u8g2_font_logisoso16_tr); | ||||
|  |                         mDisplay.setCursor(25,17); | ||||
|  |                         sprintf(fmtText,"%3.0f",totalActual); | ||||
|  |                         mDisplay.print(String(fmtText)+F(" W")); | ||||
|  |                     } | ||||
|  |                     else | ||||
|  |                     { | ||||
|  |                         mDisplay.setFont(u8g2_font_logisoso16_tr  ); | ||||
|  |                         mDisplay.setCursor(10,17); | ||||
|  |                         mDisplay.print(String(F("offline"))); | ||||
|  |                     } | ||||
|  |                     mDisplay.drawHLine(2,20,78); | ||||
|  |                     mDisplay.setFont(u8g2_font_5x8_tr); | ||||
|  |                     mDisplay.setCursor(5,29); | ||||
|  |                     if (( num_inv < 2 ) || !(mExtra%2)) | ||||
|  |                     { | ||||
|  |                         sprintf(fmtText,"%4.0f",totalYieldToday); | ||||
|  |                         mDisplay.print(F("today ")+String(fmtText)+F(" Wh")); | ||||
|  |                         mDisplay.setCursor(5,37); | ||||
|  |                         sprintf(fmtText,"%.1f",totalYield); | ||||
|  |                         mDisplay.print(F("total ")+String(fmtText)+F(" kWh")); | ||||
|  |                     } | ||||
|  |                     else | ||||
|  |                     { | ||||
|  |                         int id1=(mExtra/2)%(num_inv-1); | ||||
|  |                         if( pow_i[id1] ) | ||||
|  |                             mDisplay.print(F("#")+String(id1+1)+F("  ")+String(pow_i[id1])+F(" W")); | ||||
|  |                         else | ||||
|  |                             mDisplay.print(F("#")+String(id1+1)+F("  -----")); | ||||
|  |                         mDisplay.setCursor(5,37); | ||||
|  |                         if( pow_i[id1+1] ) | ||||
|  |                             mDisplay.print(F("#")+String(id1+2)+F("  ")+String(pow_i[id1+1])+F(" W")); | ||||
|  |                         else | ||||
|  |                             mDisplay.print(F("#")+String(id1+2)+F("  -----")); | ||||
|  |                     } | ||||
|  |                     if ( !(mExtra%10) && ip ) { | ||||
|  |                         mDisplay.setCursor(5,47); | ||||
|  |                         mDisplay.print(ip.toString()); | ||||
|  |                     } | ||||
|  |                     else { | ||||
|  |                         mDisplay.setCursor(0,47); | ||||
|  |                         mDisplay.print(timeStr); | ||||
|  |                     } | ||||
|  | 
 | ||||
|  |                     mDisplay.sendBuffer(); | ||||
|  |                 } while( mDisplay.nextPage() ); | ||||
|  |                 mExtra++; | ||||
|  |         #else // ENA_SSD1306
 | ||||
|  |             if(mUp) { | ||||
|  |                 mRx += 2; | ||||
|  |                 if(mRx >= 20) | ||||
|  |                 mUp = 0; | ||||
|  |             } else { | ||||
|  |                 mRx -= 2; | ||||
|  |                 if(mRx <= 0) | ||||
|  |                 mUp = 1; | ||||
|  |             } | ||||
|  |             int ex = 2*( mExtra % 5 ); | ||||
|  | 
 | ||||
|  |             if(ucnt) { | ||||
|  |                 mDisplay.setBrightness(63); | ||||
|  |                 mDisplay.drawXbm(10+ex,5,8,17,bmp_arrow); | ||||
|  |                 mDisplay.setFont(ArialMT_Plain_24); | ||||
|  |                 sprintf(fmtText,"%3.0f",totalActual); | ||||
|  |                 mDisplay.drawString(25+ex,0,String(fmtText)+F(" W")); | ||||
|  |             } | ||||
|  |             else | ||||
|  |             { | ||||
|  |                 mDisplay.setBrightness(1); | ||||
|  |                 mDisplay.setFont(ArialMT_Plain_24); | ||||
|  |                 mDisplay.drawString(25+ex,0,String(F("offline"))); | ||||
|  |             } | ||||
|  |             mDisplay.setFont(ArialMT_Plain_16); | ||||
|  | 
 | ||||
|  |             if (( num_inv < 2 ) || !(mExtra%2)) | ||||
|  |             { | ||||
|  |                 sprintf(fmtText,"%4.0f",totalYieldToday); | ||||
|  |                 mDisplay.drawString(5,22,F("today ")+String(fmtText)+F(" Wh")); | ||||
|  |                 sprintf(fmtText,"%.1f",totalYield); | ||||
|  |                 mDisplay.drawString(5,35,F("total  ")+String(fmtText)+F(" kWh")); | ||||
|  |             } | ||||
|  |             else | ||||
|  |             { | ||||
|  |                 int id1=(mExtra/2)%(num_inv-1); | ||||
|  |                 if( pow_i[id1] ) | ||||
|  |                     mDisplay.drawString(15,22,F("#")+String(id1+1)+F("  ")+String(pow_i[id1])+F(" W")); | ||||
|  |                 else | ||||
|  |                     mDisplay.drawString(15,22,F("#")+String(id1+1)+F("  -----")); | ||||
|  |                 if( pow_i[id1+1] ) | ||||
|  |                     mDisplay.drawString(15,35,F("#")+String(id1+2)+F("  ")+String(pow_i[id1+1])+F(" W")); | ||||
|  |                 else | ||||
|  |                     mDisplay.drawString(15,35,F("#")+String(id1+2)+F("  -----")); | ||||
|  |             } | ||||
|  |             mDisplay.drawLine(2,23,123,23); | ||||
|  | 
 | ||||
|  |             if ( (!(mExtra%10) && ip )|| (timeStr.length()<16)) | ||||
|  |             { | ||||
|  |                 mDisplay.drawString(5,49,ip.toString()); | ||||
|  |             } | ||||
|  |             else | ||||
|  |             { | ||||
|  |                 int w=mDisplay.getStringWidth(timeStr.c_str(),timeStr.length(),0); | ||||
|  |                 if ( w>127 ) | ||||
|  |                 { | ||||
|  |                     String tt=timeStr.substring(9,17); | ||||
|  |                     w=mDisplay.getStringWidth(tt.c_str(),tt.length(),0); | ||||
|  |                     mDisplay.drawString(127-w-mRx,49,tt); | ||||
|  |                 } | ||||
|  |                 else | ||||
|  |                     mDisplay.drawString(0,49,timeStr); | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             mDisplay.display(); | ||||
|  |             mExtra++; | ||||
|  |         #endif | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         // private member variables
 | ||||
|  |         #if defined(ENA_NOKIA) | ||||
|  |             U8G2_PCD8544_84X48_1_4W_HW_SPI mDisplay; | ||||
|  |         #else // ENA_SSD1306
 | ||||
|  |             SSD1306Wire mDisplay; | ||||
|  |             int mRx; | ||||
|  |             char mUp; | ||||
|  |         #endif | ||||
|  |         int mExtra; | ||||
|  |         bool mNewPayload; | ||||
|  |         float mTotal[ MAX_NUM_INVERTERS ]; | ||||
|  |         float mToday[ MAX_NUM_INVERTERS ]; | ||||
|  |         uint32_t *mUtcTs; | ||||
|  |         int mLastHour; | ||||
|  |         HMSYSTEM *mSys; | ||||
|  |         Timezone mCE; | ||||
|  | }; | ||||
|  | #endif | ||||
|  | 
 | ||||
|  | #endif /*__MONOCHROME_DISPLAY__*/ | ||||
| @ -0,0 +1,58 @@ | |||||
|  | //-----------------------------------------------------------------------------
 | ||||
|  | // 2022 Ahoy, https://github.com/lumpapu/ahoy
 | ||||
|  | // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
 | ||||
|  | //-----------------------------------------------------------------------------
 | ||||
|  | 
 | ||||
|  | #include "helper.h" | ||||
|  | 
 | ||||
|  | namespace ah { | ||||
|  |     void ip2Arr(uint8_t ip[], const char *ipStr) { | ||||
|  |         memset(ip, 0, 4); | ||||
|  |         char *tmp = new char[strlen(ipStr)+1]; | ||||
|  |         strncpy(tmp, ipStr, strlen(ipStr)+1); | ||||
|  |         char *p = strtok(tmp, "."); | ||||
|  |         uint8_t i = 0; | ||||
|  |         while(NULL != p) { | ||||
|  |             ip[i++] = atoi(p); | ||||
|  |             p = strtok(NULL, "."); | ||||
|  |         } | ||||
|  |         delete[] tmp; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     // note: char *str needs to be at least 16 bytes long
 | ||||
|  |     void ip2Char(uint8_t ip[], char *str) { | ||||
|  |         if(0 == ip[0]) | ||||
|  |             str[0] = '\0'; | ||||
|  |         else | ||||
|  |             snprintf(str, 16, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     double round3(double value) { | ||||
|  |         return (int)(value * 1000 + 0.5) / 1000.0; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     String getDateTimeStr(time_t t) { | ||||
|  |         char str[20]; | ||||
|  |         if(0 == t) | ||||
|  |             sprintf(str, "n/a"); | ||||
|  |         else | ||||
|  |             sprintf(str, "%04d-%02d-%02d %02d:%02d:%02d", year(t), month(t), day(t), hour(t), minute(t), second(t)); | ||||
|  |         return String(str); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     uint64_t Serial2u64(const char *val) { | ||||
|  |         char tmp[3]; | ||||
|  |         uint64_t ret = 0ULL; | ||||
|  |         uint64_t u64; | ||||
|  |         memset(tmp, 0, 3); | ||||
|  |         for(uint8_t i = 0; i < 6; i++) { | ||||
|  |             tmp[0] = val[i*2]; | ||||
|  |             tmp[1] = val[i*2 + 1]; | ||||
|  |             if((tmp[0] == '\0') || (tmp[1] == '\0')) | ||||
|  |                 break; | ||||
|  |             u64 = strtol(tmp, NULL, 16); | ||||
|  |             ret |= (u64 << ((5-i) << 3)); | ||||
|  |         } | ||||
|  |         return ret; | ||||
|  |     } | ||||
|  | } | ||||
| @ -0,0 +1,24 @@ | |||||
|  | //-----------------------------------------------------------------------------
 | ||||
|  | // 2022 Ahoy, https://ahoydtu.de
 | ||||
|  | // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
 | ||||
|  | //-----------------------------------------------------------------------------
 | ||||
|  | 
 | ||||
|  | #ifndef __HELPER_H__ | ||||
|  | #define __HELPER_H__ | ||||
|  | 
 | ||||
|  | #include <Arduino.h> | ||||
|  | #include <cstdint> | ||||
|  | #include <cstring> | ||||
|  | #include <stdio.h> | ||||
|  | #include <stdlib.h> | ||||
|  | #include <TimeLib.h> | ||||
|  | 
 | ||||
|  | namespace ah { | ||||
|  |     void ip2Arr(uint8_t ip[], const char *ipStr); | ||||
|  |     void ip2Char(uint8_t ip[], char *str); | ||||
|  |     double round3(double value); | ||||
|  |     String getDateTimeStr(time_t t); | ||||
|  |     uint64_t Serial2u64(const char *val); | ||||
|  | } | ||||
|  | 
 | ||||
|  | #endif /*__HELPER_H__*/ | ||||
| @ -0,0 +1,108 @@ | |||||
|  | //-----------------------------------------------------------------------------
 | ||||
|  | // 2022 Ahoy, https://ahoydtu.de
 | ||||
|  | // Lukas Pusch, lukas@lpusch.de
 | ||||
|  | // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
 | ||||
|  | //-----------------------------------------------------------------------------
 | ||||
|  | #ifndef __LIST_H__ | ||||
|  | #define __LIST_H__ | ||||
|  | 
 | ||||
|  | template<class T, class... Args> | ||||
|  | struct node_s { | ||||
|  |     typedef T dT; | ||||
|  |     node_s *pre; | ||||
|  |     node_s *nxt; | ||||
|  |     uint32_t id; | ||||
|  |     dT d; | ||||
|  |     node_s() : pre(NULL), nxt(NULL), d() {} | ||||
|  |     node_s(Args... args) : id(0), pre(NULL), nxt(NULL), d(args...) {} | ||||
|  | }; | ||||
|  | 
 | ||||
|  | template<int MAX_NUM, class T, class... Args> | ||||
|  | class llist { | ||||
|  |     typedef node_s<T, Args...> elmType; | ||||
|  |     typedef T dataType; | ||||
|  |     public: | ||||
|  |         llist() : root(mPool) { | ||||
|  |             root = NULL; | ||||
|  |             elmType *p = mPool; | ||||
|  |             for(uint32_t i = 0; i < MAX_NUM; i++) { | ||||
|  |                 p->id = i; | ||||
|  |                 p++; | ||||
|  |             } | ||||
|  |             mFill = mMax = 0; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         elmType *add(Args... args) { | ||||
|  |             elmType *p = root, *t; | ||||
|  |             if(NULL == (t = getFreeNode())) | ||||
|  |                 return NULL; | ||||
|  |             if(++mFill > mMax) | ||||
|  |                 mMax = mFill; | ||||
|  | 
 | ||||
|  |             if(NULL == root) { | ||||
|  |                 p = root = t; | ||||
|  |                 p->pre = p; | ||||
|  |                 p->nxt = p; | ||||
|  |             } | ||||
|  |             else { | ||||
|  |                 p = root->pre; | ||||
|  |                 t->pre = p; | ||||
|  |                 p->nxt->pre = t; | ||||
|  |                 t->nxt = p->nxt; | ||||
|  |                 p->nxt = t; | ||||
|  |             } | ||||
|  |             t->d = dataType(args...); | ||||
|  |             return p; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         elmType *getFront() { | ||||
|  |             return root; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         elmType *get(elmType *p) { | ||||
|  |             p = p->nxt; | ||||
|  |             return (p == root) ? NULL : p; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         elmType *rem(elmType *p) { | ||||
|  |             if(NULL == p) | ||||
|  |                 return NULL; | ||||
|  |             elmType *t = p->nxt; | ||||
|  |             p->nxt->pre = p->pre; | ||||
|  |             p->pre->nxt = p->nxt; | ||||
|  |             if(root == p) | ||||
|  |                 root = NULL; | ||||
|  |             p->nxt = NULL; | ||||
|  |             p->pre = NULL; | ||||
|  |             p = NULL; | ||||
|  |             mFill--; | ||||
|  |             return (NULL == root) ? NULL : ((t == root) ? NULL : t); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         uint16_t getFill(void) { | ||||
|  |             return mFill; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         uint16_t getMaxFill(void) { | ||||
|  |             return mMax; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |     protected: | ||||
|  |         elmType *root; | ||||
|  | 
 | ||||
|  |     private: | ||||
|  |         elmType *getFreeNode(void) { | ||||
|  |             elmType *n = mPool; | ||||
|  |             for(uint32_t i = 0; i < MAX_NUM; i++) { | ||||
|  |                 if(NULL == n->nxt) | ||||
|  |                     return n; | ||||
|  |                 n++; | ||||
|  |             } | ||||
|  |             return NULL; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         elmType mPool[MAX_NUM]; | ||||
|  |         uint16_t mFill, mMax; | ||||
|  | }; | ||||
|  | 
 | ||||
|  | #endif /*__LIST_H__*/ | ||||
| @ -0,0 +1,538 @@ | |||||
|  | #ifndef __WEB_API_H__ | ||||
|  | #define __WEB_API_H__ | ||||
|  | 
 | ||||
|  | #include "../utils/dbg.h" | ||||
|  | #ifdef ESP32 | ||||
|  |     #include "AsyncTCP.h" | ||||
|  | #else | ||||
|  |     #include "ESPAsyncTCP.h" | ||||
|  | #endif | ||||
|  | #include "ESPAsyncWebServer.h" | ||||
|  | #include "AsyncJson.h" | ||||
|  | #include "../hm/hmSystem.h" | ||||
|  | #include "../utils/helper.h" | ||||
|  | 
 | ||||
|  | #include "../appInterface.h" | ||||
|  | 
 | ||||
|  | template<class HMSYSTEM> | ||||
|  | class RestApi { | ||||
|  |     public: | ||||
|  |         RestApi() { | ||||
|  |             mTimezoneOffset = 0; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void setup(IApp *app, HMSYSTEM *sys, AsyncWebServer *srv, settings_t *config) { | ||||
|  |             mApp     = app; | ||||
|  |             mSrv     = srv; | ||||
|  |             mSys     = sys; | ||||
|  |             mConfig  = config; | ||||
|  |             mSrv->on("/api", HTTP_GET,  std::bind(&RestApi::onApi,         this, std::placeholders::_1)); | ||||
|  |             mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost,     this, std::placeholders::_1)).onBody( | ||||
|  |                                         std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); | ||||
|  | 
 | ||||
|  |             mSrv->on("/get_setup", HTTP_GET,  std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1)); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         uint32_t getTimezoneOffset(void) { | ||||
|  |             return mTimezoneOffset; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void ctrlRequest(JsonObject obj) { | ||||
|  |             /*char out[128];
 | ||||
|  |             serializeJson(obj, out, 128); | ||||
|  |             DPRINTLN(DBG_INFO, "RestApi: " + String(out));*/ | ||||
|  |             DynamicJsonDocument json(128); | ||||
|  |             JsonObject dummy = json.to<JsonObject>(); | ||||
|  |             if(obj[F("path")] == "ctrl") | ||||
|  |                 setCtrl(obj, dummy); | ||||
|  |             else if(obj[F("path")] == "setup") | ||||
|  |                 setSetup(obj, dummy); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |     private: | ||||
|  |         void onApi(AsyncWebServerRequest *request) { | ||||
|  |             AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192); | ||||
|  |             JsonObject root = response->getRoot(); | ||||
|  | 
 | ||||
|  |             Inverter<> *iv = mSys->getInverterByPos(0, false); | ||||
|  |             String path = request->url().substring(5); | ||||
|  |             if(path == "html/system")         getHtmlSystem(root); | ||||
|  |             else if(path == "html/logout")    getHtmlLogout(root); | ||||
|  |             else if(path == "html/save")      getHtmlSave(root); | ||||
|  |             else if(path == "system")         getSysInfo(root); | ||||
|  |             else if(path == "reboot")         getReboot(root); | ||||
|  |             else if(path == "statistics")     getStatistics(root); | ||||
|  |             else if(path == "inverter/list")  getInverterList(root); | ||||
|  |             else if(path == "menu")           getMenu(root); | ||||
|  |             else if(path == "index")          getIndex(root); | ||||
|  |             else if(path == "setup")          getSetup(root); | ||||
|  |             else if(path == "setup/networks") getNetworks(root); | ||||
|  |             else if(path == "live")           getLive(root); | ||||
|  |             else if(path == "record/info")    getRecord(root, iv->getRecordStruct(InverterDevInform_All)); | ||||
|  |             else if(path == "record/alarm")   getRecord(root, iv->getRecordStruct(AlarmData)); | ||||
|  |             else if(path == "record/config")  getRecord(root, iv->getRecordStruct(SystemConfigPara)); | ||||
|  |             else if(path == "record/live")    getRecord(root, iv->getRecordStruct(RealTimeRunData_Debug)); | ||||
|  |             else | ||||
|  |                 getNotFound(root, F("http://") + request->host() + F("/api/")); | ||||
|  | 
 | ||||
|  |             response->addHeader("Access-Control-Allow-Origin", "*"); | ||||
|  |             response->addHeader("Access-Control-Allow-Headers", "content-type"); | ||||
|  |             response->setLength(); | ||||
|  |             request->send(response); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void onApiPost(AsyncWebServerRequest *request) { | ||||
|  |             DPRINTLN(DBG_VERBOSE, "onApiPost"); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { | ||||
|  |             DPRINTLN(DBG_VERBOSE, "onApiPostBody"); | ||||
|  |             DynamicJsonDocument json(200); | ||||
|  |             AsyncJsonResponse* response = new AsyncJsonResponse(false, 200); | ||||
|  |             JsonObject root = response->getRoot(); | ||||
|  | 
 | ||||
|  |             DeserializationError err = deserializeJson(json, (const char *)data, len); | ||||
|  |             JsonObject obj = json.as<JsonObject>(); | ||||
|  |             root[F("success")] = (err) ? false : true; | ||||
|  |             if(!err) { | ||||
|  |                 String path = request->url().substring(5); | ||||
|  |                 if(path == "ctrl") | ||||
|  |                     root[F("success")] = setCtrl(obj, root); | ||||
|  |                 else if(path == "setup") | ||||
|  |                     root[F("success")] = setSetup(obj, root); | ||||
|  |                 else { | ||||
|  |                     root[F("success")] = false; | ||||
|  |                     root[F("error")]   = "Path not found: " + path; | ||||
|  |                 } | ||||
|  |             } | ||||
|  |             else { | ||||
|  |                 switch (err.code()) { | ||||
|  |                     case DeserializationError::Ok: break; | ||||
|  |                     case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input");          break; | ||||
|  |                     case DeserializationError::NoMemory:     root[F("error")] = F("Not enough memory");      break; | ||||
|  |                     default:                                 root[F("error")] = F("Deserialization failed"); break; | ||||
|  |                 } | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             response->setLength(); | ||||
|  |             request->send(response); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getNotFound(JsonObject obj, String url) { | ||||
|  |             JsonObject ep = obj.createNestedObject("avail_endpoints"); | ||||
|  |             ep[F("system")]        = url + F("system"); | ||||
|  |             ep[F("statistics")]    = url + F("statistics"); | ||||
|  |             ep[F("inverter/list")] = url + F("inverter/list"); | ||||
|  |             ep[F("index")]         = url + F("index"); | ||||
|  |             ep[F("setup")]         = url + F("setup"); | ||||
|  |             ep[F("live")]          = url + F("live"); | ||||
|  |             ep[F("record/info")]   = url + F("record/info"); | ||||
|  |             ep[F("record/alarm")]  = url + F("record/alarm"); | ||||
|  |             ep[F("record/config")] = url + F("record/config"); | ||||
|  |             ep[F("record/live")]   = url + F("record/live"); | ||||
|  |         } | ||||
|  |         void onDwnldSetup(AsyncWebServerRequest *request) { | ||||
|  |             AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192); | ||||
|  |             JsonObject root = response->getRoot(); | ||||
|  | 
 | ||||
|  |             getSetup(root); | ||||
|  | 
 | ||||
|  |             response->setLength(); | ||||
|  |             response->addHeader("Content-Type", "application/octet-stream"); | ||||
|  |             response->addHeader("Content-Description", "File Transfer"); | ||||
|  |             response->addHeader("Content-Disposition", "attachment; filename=ahoy_setup.json"); | ||||
|  |             request->send(response); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getSysInfo(JsonObject obj) { | ||||
|  |             obj[F("ssid")]         = mConfig->sys.stationSsid; | ||||
|  |             obj[F("device_name")]  = mConfig->sys.deviceName; | ||||
|  |             obj[F("version")]      = String(mApp->getVersion()); | ||||
|  |             obj[F("build")]        = String(AUTO_GIT_HASH); | ||||
|  | 
 | ||||
|  |             obj[F("ts_uptime")]    = mApp->getUptime(); | ||||
|  |             obj[F("ts_now")]       = mApp->getTimestamp(); | ||||
|  |             obj[F("ts_sunrise")]   = mApp->getSunrise(); | ||||
|  |             obj[F("ts_sunset")]    = mApp->getSunset(); | ||||
|  |             obj[F("wifi_rssi")]    = WiFi.RSSI(); | ||||
|  |             obj[F("mac")]          = WiFi.macAddress(); | ||||
|  |             obj[F("hostname")]     = WiFi.getHostname(); | ||||
|  |             obj[F("pwd_set")]      = (strlen(mConfig->sys.adminPwd) > 0); | ||||
|  | 
 | ||||
|  |             obj[F("sdk")]          = ESP.getSdkVersion(); | ||||
|  |             obj[F("cpu_freq")]     = ESP.getCpuFreqMHz(); | ||||
|  |             obj[F("heap_free")]    = ESP.getFreeHeap(); | ||||
|  |             obj[F("sketch_total")] = ESP.getFreeSketchSpace(); | ||||
|  |             obj[F("sketch_used")]  = ESP.getSketchSize() / 1024; // in kb
 | ||||
|  | 
 | ||||
|  | 
 | ||||
|  |             getRadio(obj.createNestedObject(F("radio"))); | ||||
|  | 
 | ||||
|  |         #if defined(ESP32) | ||||
|  |             obj[F("heap_total")]    = ESP.getHeapSize(); | ||||
|  |             obj[F("chip_revision")] = ESP.getChipRevision(); | ||||
|  |             obj[F("chip_model")]    = ESP.getChipModel(); | ||||
|  |             obj[F("chip_cores")]    = ESP.getChipCores(); | ||||
|  |             //obj[F("core_version")]  = F("n/a");
 | ||||
|  |             //obj[F("flash_size")]    = F("n/a");
 | ||||
|  |             //obj[F("heap_frag")]     = F("n/a");
 | ||||
|  |             //obj[F("max_free_blk")]  = F("n/a");
 | ||||
|  |             //obj[F("reboot_reason")] = F("n/a");
 | ||||
|  |         #else | ||||
|  |             //obj[F("heap_total")]    = F("n/a");
 | ||||
|  |             //obj[F("chip_revision")] = F("n/a");
 | ||||
|  |             //obj[F("chip_model")]    = F("n/a");
 | ||||
|  |             //obj[F("chip_cores")]    = F("n/a");
 | ||||
|  |             obj[F("core_version")]  = ESP.getCoreVersion(); | ||||
|  |             obj[F("flash_size")]    = ESP.getFlashChipRealSize() / 1024; // in kb
 | ||||
|  |             obj[F("heap_frag")]     = ESP.getHeapFragmentation(); | ||||
|  |             obj[F("max_free_blk")]  = ESP.getMaxFreeBlockSize(); | ||||
|  |             obj[F("reboot_reason")] = ESP.getResetReason(); | ||||
|  |         #endif | ||||
|  |             //obj[F("littlefs_total")] = LittleFS.totalBytes();
 | ||||
|  |             //obj[F("littlefs_used")] = LittleFS.usedBytes();
 | ||||
|  | 
 | ||||
|  |         #if defined(ESP32) | ||||
|  |             obj[F("esp_type")]    = F("ESP32"); | ||||
|  |         #else | ||||
|  |             obj[F("esp_type")]    = F("ESP8266"); | ||||
|  |         #endif | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getHtmlSystem(JsonObject obj) { | ||||
|  |             getMenu(obj.createNestedObject(F("menu"))); | ||||
|  |             getSysInfo(obj.createNestedObject(F("system"))); | ||||
|  |             obj[F("html")] = F("<a href=\"/factory\" class=\"btn\">Factory Reset</a><br/><br/><a href=\"/reboot\" class=\"btn\">Reboot</a>"); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getHtmlLogout(JsonObject obj) { | ||||
|  |             getMenu(obj.createNestedObject(F("menu"))); | ||||
|  |             getSysInfo(obj.createNestedObject(F("system"))); | ||||
|  |             obj[F("refresh")] = 3; | ||||
|  |             obj[F("refresh_url")] = "/"; | ||||
|  |             obj[F("html")] = F("succesfully logged out"); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getHtmlSave(JsonObject obj) { | ||||
|  |             getMenu(obj.createNestedObject(F("menu"))); | ||||
|  |             getSysInfo(obj.createNestedObject(F("system"))); | ||||
|  |             obj[F("refresh")] = 2; | ||||
|  |             obj[F("refresh_url")] = "/setup"; | ||||
|  |             obj[F("html")] = F("settings succesfully save"); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getReboot(JsonObject obj) { | ||||
|  |             getMenu(obj.createNestedObject(F("menu"))); | ||||
|  |             getSysInfo(obj.createNestedObject(F("system"))); | ||||
|  |             obj[F("refresh")] = 10; | ||||
|  |             obj[F("refresh_url")] = "/"; | ||||
|  |             obj[F("html")] = F("reboot. Autoreload after 10 seconds"); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getStatistics(JsonObject obj) { | ||||
|  |             statistics_t *stat = mApp->getStatistics(); | ||||
|  |             obj[F("rx_success")]     = stat->rxSuccess; | ||||
|  |             obj[F("rx_fail")]        = stat->rxFail; | ||||
|  |             obj[F("rx_fail_answer")] = stat->rxFailNoAnser; | ||||
|  |             obj[F("frame_cnt")]      = stat->frmCnt; | ||||
|  |             obj[F("tx_cnt")]         = mSys->Radio.mSendCnt; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getInverterList(JsonObject obj) { | ||||
|  |             JsonArray invArr = obj.createNestedArray(F("inverter")); | ||||
|  | 
 | ||||
|  |             Inverter<> *iv; | ||||
|  |             for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { | ||||
|  |                 iv = mSys->getInverterByPos(i); | ||||
|  |                 if(NULL != iv) { | ||||
|  |                     JsonObject obj2 = invArr.createNestedObject(); | ||||
|  |                     obj2[F("id")]       = i; | ||||
|  |                     obj2[F("name")]     = String(iv->config->name); | ||||
|  |                     obj2[F("serial")]   = String(iv->config->serial.u64, HEX); | ||||
|  |                     obj2[F("channels")] = iv->channels; | ||||
|  |                     obj2[F("version")]  = String(iv->fwVersion); | ||||
|  | 
 | ||||
|  |                     for(uint8_t j = 0; j < iv->channels; j ++) { | ||||
|  |                         obj2[F("ch_max_power")][j] = iv->config->chMaxPwr[j]; | ||||
|  |                         obj2[F("ch_name")][j] = iv->config->chName[j]; | ||||
|  |                     } | ||||
|  |                 } | ||||
|  |             } | ||||
|  |             obj[F("interval")]          = String(mConfig->nrf.sendInterval); | ||||
|  |             obj[F("retries")]           = String(mConfig->nrf.maxRetransPerPyld); | ||||
|  |             obj[F("max_num_inverters")] = MAX_NUM_INVERTERS; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getMqtt(JsonObject obj) { | ||||
|  |             obj[F("broker")] = String(mConfig->mqtt.broker); | ||||
|  |             obj[F("port")]   = String(mConfig->mqtt.port); | ||||
|  |             obj[F("user")]   = String(mConfig->mqtt.user); | ||||
|  |             obj[F("pwd")]    = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String(""); | ||||
|  |             obj[F("topic")]  = String(mConfig->mqtt.topic); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getNtp(JsonObject obj) { | ||||
|  |             obj[F("addr")] = String(mConfig->ntp.addr); | ||||
|  |             obj[F("port")] = String(mConfig->ntp.port); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getSun(JsonObject obj) { | ||||
|  |             obj[F("lat")] = mConfig->sun.lat ? String(mConfig->sun.lat, 5) : ""; | ||||
|  |             obj[F("lon")] = mConfig->sun.lat ? String(mConfig->sun.lon, 5) : ""; | ||||
|  |             obj[F("disnightcom")] = mConfig->sun.disNightCom; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getPinout(JsonObject obj) { | ||||
|  |             obj[F("cs")]  = mConfig->nrf.pinCs; | ||||
|  |             obj[F("ce")]  = mConfig->nrf.pinCe; | ||||
|  |             obj[F("irq")] = mConfig->nrf.pinIrq; | ||||
|  |             obj[F("led0")] = mConfig->led.led0; | ||||
|  |             obj[F("led1")] = mConfig->led.led1; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getRadio(JsonObject obj) { | ||||
|  |             obj[F("power_level")] = mConfig->nrf.amplifierPower; | ||||
|  |             obj[F("isconnected")] = mSys->Radio.isChipConnected(); | ||||
|  |             obj[F("DataRate")] = mSys->Radio.getDataRate(); | ||||
|  |             obj[F("isPVariant")] = mSys->Radio.isPVariant(); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getSerial(JsonObject obj) { | ||||
|  |             obj[F("interval")]       = (uint16_t)mConfig->serial.interval; | ||||
|  |             obj[F("show_live_data")] = mConfig->serial.showIv; | ||||
|  |             obj[F("debug")]          = mConfig->serial.debug; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getStaticIp(JsonObject obj) { | ||||
|  |             char buf[16]; | ||||
|  |             ah::ip2Char(mConfig->sys.ip.ip, buf);      obj[F("ip")]      = String(buf); | ||||
|  |             ah::ip2Char(mConfig->sys.ip.mask, buf);    obj[F("mask")]    = String(buf); | ||||
|  |             ah::ip2Char(mConfig->sys.ip.dns1, buf);    obj[F("dns1")]    = String(buf); | ||||
|  |             ah::ip2Char(mConfig->sys.ip.dns2, buf);    obj[F("dns2")]    = String(buf); | ||||
|  |             ah::ip2Char(mConfig->sys.ip.gateway, buf); obj[F("gateway")] = String(buf); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getMenu(JsonObject obj) { | ||||
|  |             obj["name"][0] = "Live"; | ||||
|  |             obj["link"][0] = "/live"; | ||||
|  |             obj["name"][1] = "Serial / Control"; | ||||
|  |             obj["link"][1] = "/serial"; | ||||
|  |             obj["name"][2] = "Settings"; | ||||
|  |             obj["link"][2] = "/setup"; | ||||
|  |             obj["name"][3] = "-"; | ||||
|  |             obj["name"][4] = "REST API"; | ||||
|  |             obj["link"][4] = "/api"; | ||||
|  |             obj["trgt"][4] = "_blank"; | ||||
|  |             obj["name"][5] = "-"; | ||||
|  |             obj["name"][6] = "Update"; | ||||
|  |             obj["link"][6] = "/update"; | ||||
|  |             obj["name"][7] = "System"; | ||||
|  |             obj["link"][7] = "/system"; | ||||
|  |             obj["name"][8] = "-"; | ||||
|  |             obj["name"][9] = "Documentation"; | ||||
|  |             obj["link"][9] = "https://ahoydtu.de"; | ||||
|  |             obj["trgt"][9] = "_blank"; | ||||
|  |             if(strlen(mConfig->sys.adminPwd) > 0) { | ||||
|  |                 obj["name"][10] = "-"; | ||||
|  |                 obj["name"][11] = "Logout"; | ||||
|  |                 obj["link"][11] = "/logout"; | ||||
|  |             } | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getIndex(JsonObject obj) { | ||||
|  |             getMenu(obj.createNestedObject(F("menu"))); | ||||
|  |             getSysInfo(obj.createNestedObject(F("system"))); | ||||
|  |             getRadio(obj.createNestedObject(F("radio"))); | ||||
|  |             getStatistics(obj.createNestedObject(F("statistics"))); | ||||
|  |             obj["refresh_interval"] = mConfig->nrf.sendInterval; | ||||
|  | 
 | ||||
|  |             JsonArray inv = obj.createNestedArray(F("inverter")); | ||||
|  |             Inverter<> *iv; | ||||
|  |             for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { | ||||
|  |                 iv = mSys->getInverterByPos(i); | ||||
|  |                 if(NULL != iv) { | ||||
|  |                     record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); | ||||
|  |                     JsonObject invObj = inv.createNestedObject(); | ||||
|  |                     invObj[F("id")]              = i; | ||||
|  |                     invObj[F("name")]            = String(iv->config->name); | ||||
|  |                     invObj[F("version")]         = String(iv->fwVersion); | ||||
|  |                     invObj[F("is_avail")]        = iv->isAvailable(mApp->getTimestamp(), rec); | ||||
|  |                     invObj[F("is_producing")]    = iv->isProducing(mApp->getTimestamp(), rec); | ||||
|  |                     invObj[F("ts_last_success")] = iv->getLastTs(rec); | ||||
|  |                 } | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             JsonArray warn = obj.createNestedArray(F("warnings")); | ||||
|  |             if(!mSys->Radio.isChipConnected()) | ||||
|  |                 warn.add(F("your NRF24 module can't be reached, check the wiring and pinout")); | ||||
|  |             else if(!mSys->Radio.isPVariant()) | ||||
|  |                 warn.add(F("your NRF24 module isn't a plus version(+), maybe incompatible!")); | ||||
|  | 
 | ||||
|  |             if((!mApp->getMqttIsConnected()) && (String(mConfig->mqtt.broker).length() > 0)) | ||||
|  |                 warn.add(F("MQTT is not connected")); | ||||
|  | 
 | ||||
|  |             JsonArray info = obj.createNestedArray(F("infos")); | ||||
|  |             if(mApp->getRebootRequestState()) | ||||
|  |                 info.add(F("reboot your ESP to apply all your configuration changes!")); | ||||
|  |             if(!mApp->getSettingsValid()) | ||||
|  |                 info.add(F("your settings are invalid")); | ||||
|  |             if(mApp->getMqttIsConnected()) | ||||
|  |                 info.add(F("MQTT is connected, ") + String(mApp->getMqttTxCnt()) + F(" packets sent, ") + String(mApp->getMqttRxCnt()) + F(" packets received")); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getSetup(JsonObject obj) { | ||||
|  |             getMenu(obj.createNestedObject(F("menu"))); | ||||
|  |             getSysInfo(obj.createNestedObject(F("system"))); | ||||
|  |             getInverterList(obj.createNestedObject(F("inverter"))); | ||||
|  |             getMqtt(obj.createNestedObject(F("mqtt"))); | ||||
|  |             getNtp(obj.createNestedObject(F("ntp"))); | ||||
|  |             getSun(obj.createNestedObject(F("sun"))); | ||||
|  |             getPinout(obj.createNestedObject(F("pinout"))); | ||||
|  |             getRadio(obj.createNestedObject(F("radio"))); | ||||
|  |             getSerial(obj.createNestedObject(F("serial"))); | ||||
|  |             getStaticIp(obj.createNestedObject(F("static_ip"))); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getNetworks(JsonObject obj) { | ||||
|  |             mApp->getAvailNetworks(obj); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getLive(JsonObject obj) { | ||||
|  |             getMenu(obj.createNestedObject(F("menu"))); | ||||
|  |             getSysInfo(obj.createNestedObject(F("system"))); | ||||
|  |             JsonArray invArr = obj.createNestedArray(F("inverter")); | ||||
|  |             obj["refresh_interval"] = mConfig->nrf.sendInterval; | ||||
|  | 
 | ||||
|  |             uint8_t list[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q}; | ||||
|  | 
 | ||||
|  |             Inverter<> *iv; | ||||
|  |             uint8_t pos; | ||||
|  |             for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { | ||||
|  |                 iv = mSys->getInverterByPos(i); | ||||
|  |                 if(NULL != iv) { | ||||
|  |                     record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); | ||||
|  |                     JsonObject obj2 = invArr.createNestedObject(); | ||||
|  |                     obj2[F("name")]               = String(iv->config->name); | ||||
|  |                     obj2[F("channels")]           = iv->channels; | ||||
|  |                     obj2[F("power_limit_read")]   = ah::round3(iv->actPowerLimit); | ||||
|  |                     obj2[F("last_alarm")]         = String(iv->lastAlarmMsg); | ||||
|  |                     obj2[F("ts_last_success")]    = rec->ts; | ||||
|  | 
 | ||||
|  |                     JsonArray ch = obj2.createNestedArray("ch"); | ||||
|  |                     JsonArray ch0 = ch.createNestedArray(); | ||||
|  |                     obj2[F("ch_names")][0] = "AC"; | ||||
|  |                     for (uint8_t fld = 0; fld < sizeof(list); fld++) { | ||||
|  |                         pos = (iv->getPosByChFld(CH0, list[fld], rec)); | ||||
|  |                         ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; | ||||
|  |                         obj[F("ch0_fld_units")][fld] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; | ||||
|  |                         obj[F("ch0_fld_names")][fld] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; | ||||
|  |                     } | ||||
|  | 
 | ||||
|  |                     for(uint8_t j = 1; j <= iv->channels; j ++) { | ||||
|  |                         obj2[F("ch_names")][j] = String(iv->config->chName[j-1]); | ||||
|  |                         JsonArray cur = ch.createNestedArray(); | ||||
|  |                         for (uint8_t k = 0; k < 6; k++) { | ||||
|  |                             switch(k) { | ||||
|  |                                 default: pos = (iv->getPosByChFld(j, FLD_UDC, rec)); break; | ||||
|  |                                 case 1:  pos = (iv->getPosByChFld(j, FLD_IDC, rec)); break; | ||||
|  |                                 case 2:  pos = (iv->getPosByChFld(j, FLD_PDC, rec)); break; | ||||
|  |                                 case 3:  pos = (iv->getPosByChFld(j, FLD_YD, rec));  break; | ||||
|  |                                 case 4:  pos = (iv->getPosByChFld(j, FLD_YT, rec));  break; | ||||
|  |                                 case 5:  pos = (iv->getPosByChFld(j, FLD_IRR, rec)); break; | ||||
|  |                             } | ||||
|  |                             cur[k] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; | ||||
|  |                             if(1 == j) { | ||||
|  |                                 obj[F("fld_units")][k] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; | ||||
|  |                                 obj[F("fld_names")][k] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; | ||||
|  |                             } | ||||
|  |                         } | ||||
|  |                     } | ||||
|  |                 } | ||||
|  |             } | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         void getRecord(JsonObject obj, record_t<> *rec) { | ||||
|  |             JsonArray invArr = obj.createNestedArray(F("inverter")); | ||||
|  | 
 | ||||
|  |             Inverter<> *iv; | ||||
|  |             uint8_t pos; | ||||
|  |             for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { | ||||
|  |                 iv = mSys->getInverterByPos(i); | ||||
|  |                 if(NULL != iv) { | ||||
|  |                     JsonArray obj2 = invArr.createNestedArray(); | ||||
|  |                     for(uint8_t j = 0; j < rec->length; j++) { | ||||
|  |                         byteAssign_t *assign = iv->getByteAssign(j, rec); | ||||
|  |                         pos = (iv->getPosByChFld(assign->ch, assign->fieldId, rec)); | ||||
|  |                         obj2[j]["fld"]  = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; | ||||
|  |                         obj2[j]["unit"] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; | ||||
|  |                         obj2[j]["val"]  = (0xff != pos) ? String(iv->getValue(pos, rec)) : notAvail; | ||||
|  |                     } | ||||
|  |                 } | ||||
|  |             } | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) { | ||||
|  |             Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]); | ||||
|  |             if(NULL == iv) { | ||||
|  |                 jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as<String>(); | ||||
|  |                 return false; | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             if(F("power") == jsonIn[F("cmd")]) { | ||||
|  |                 iv->devControlCmd = (jsonIn[F("val")] == 1) ? TurnOn : TurnOff; | ||||
|  |                 iv->devControlRequest = true; | ||||
|  |             } else if(F("restart") == jsonIn[F("restart")]) { | ||||
|  |                 iv->devControlCmd = Restart; | ||||
|  |                 iv->devControlRequest = true; | ||||
|  |             } | ||||
|  |             else if(0 == strncmp("limit_", jsonIn[F("cmd")].as<const char*>(), 6)) { | ||||
|  |                 iv->powerLimit[0] = jsonIn["val"]; | ||||
|  |                 if(F("limit_persistent_relative") == jsonIn[F("cmd")]) | ||||
|  |                     iv->powerLimit[1] = RelativPersistent; | ||||
|  |                 else if(F("limit_persistent_absolute") == jsonIn[F("cmd")]) | ||||
|  |                     iv->powerLimit[1] = AbsolutPersistent; | ||||
|  |                 else if(F("limit_nonpersistent_relative") == jsonIn[F("cmd")]) | ||||
|  |                     iv->powerLimit[1] = RelativNonPersistent; | ||||
|  |                 else if(F("limit_nonpersistent_absolute") == jsonIn[F("cmd")]) | ||||
|  |                     iv->powerLimit[1] = AbsolutNonPersistent; | ||||
|  |                 iv->devControlCmd = ActivePowerContr; | ||||
|  |                 iv->devControlRequest = true; | ||||
|  |             } | ||||
|  |             else { | ||||
|  |                 jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as<String>() + "'"; | ||||
|  |                 return false; | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             return true; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         bool setSetup(JsonObject jsonIn, JsonObject jsonOut) { | ||||
|  |             if(F("scan_wifi") == jsonIn[F("cmd")]) { | ||||
|  |                 mApp->scanAvailNetworks(); | ||||
|  |             } | ||||
|  |             else if(F("set_time") == jsonIn[F("cmd")]) | ||||
|  |                 mApp->setTimestamp(jsonIn[F("val")]); | ||||
|  |             else if(F("sync_ntp") == jsonIn[F("cmd")]) | ||||
|  |                 mApp->setTimestamp(0); // 0: update ntp flag
 | ||||
|  |             else if(F("serial_utc_offset") == jsonIn[F("cmd")]) | ||||
|  |                 mTimezoneOffset = jsonIn[F("val")]; | ||||
|  |             else if(F("discovery_cfg") == jsonIn[F("cmd")]) { | ||||
|  |                 mApp->setMqttDiscoveryFlag(); // for homeassistant
 | ||||
|  |             } | ||||
|  |             else { | ||||
|  |                 jsonOut[F("error")] = F("unknown cmd"); | ||||
|  |                 return false; | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             return true; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         IApp *mApp; | ||||
|  |         HMSYSTEM *mSys; | ||||
|  |         AsyncWebServer *mSrv; | ||||
|  |         settings_t *mConfig; | ||||
|  | 
 | ||||
|  |         uint32_t mTimezoneOffset; | ||||
|  | }; | ||||
|  | 
 | ||||
|  | #endif /*__WEB_API_H__*/ | ||||
| @ -1,726 +0,0 @@ | |||||
| //-----------------------------------------------------------------------------
 |  | ||||
| // 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
 |  | ||||
| // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| 
 |  | ||||
| #if defined(ESP32) && defined(F) |  | ||||
|   #undef F |  | ||||
|   #define F(sl) (sl) |  | ||||
| #endif |  | ||||
| 
 |  | ||||
| #include "web.h" |  | ||||
| 
 |  | ||||
| #include "../utils/ahoyTimer.h" |  | ||||
| 
 |  | ||||
| #include "html/h/index_html.h" |  | ||||
| #include "html/h/login_html.h" |  | ||||
| #include "html/h/style_css.h" |  | ||||
| #include "html/h/api_js.h" |  | ||||
| #include "html/h/favicon_ico.h" |  | ||||
| #include "html/h/setup_html.h" |  | ||||
| #include "html/h/visualization_html.h" |  | ||||
| #include "html/h/update_html.h" |  | ||||
| #include "html/h/serial_html.h" |  | ||||
| #include "html/h/system_html.h" |  | ||||
| 
 |  | ||||
| const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinLed0", "pinLed1"}; |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| web::web(app *main, settings_t *config, statistics_t *stat, char version[]) { |  | ||||
|     mMain    = main; |  | ||||
|     mConfig  = config; |  | ||||
|     mStat    = stat; |  | ||||
|     mVersion = version; |  | ||||
|     mWeb     = new AsyncWebServer(80); |  | ||||
|     mEvts    = new AsyncEventSource("/events"); |  | ||||
|     mApi     = new webApi(mWeb, main, config, stat, version); |  | ||||
| 
 |  | ||||
|     mProtected     = true; |  | ||||
|     mLogoutTimeout = 0; |  | ||||
| 
 |  | ||||
|     memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); |  | ||||
|     mSerialBufFill     = 0; |  | ||||
|     mWebSerialTicker   = 0; |  | ||||
|     mWebSerialInterval = 1000; // [ms]
 |  | ||||
|     mSerialAddTime     = true; |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::setup(void) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("app::setup-begin")); |  | ||||
|     mWeb->begin(); |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("app::setup-on")); |  | ||||
|     mWeb->on("/",               HTTP_GET,  std::bind(&web::onIndex,        this, std::placeholders::_1)); |  | ||||
|     mWeb->on("/login",          HTTP_ANY,  std::bind(&web::onLogin,        this, std::placeholders::_1)); |  | ||||
|     mWeb->on("/logout",         HTTP_GET,  std::bind(&web::onLogout,       this, std::placeholders::_1)); |  | ||||
|     mWeb->on("/style.css",      HTTP_GET,  std::bind(&web::onCss,          this, std::placeholders::_1)); |  | ||||
|     mWeb->on("/api.js",         HTTP_GET,  std::bind(&web::onApiJs,        this, std::placeholders::_1)); |  | ||||
|     mWeb->on("/favicon.ico",    HTTP_GET,  std::bind(&web::onFavicon,      this, std::placeholders::_1)); |  | ||||
|     mWeb->onNotFound (                     std::bind(&web::showNotFound,   this, std::placeholders::_1)); |  | ||||
|     mWeb->on("/reboot",         HTTP_ANY,  std::bind(&web::onReboot,       this, std::placeholders::_1)); |  | ||||
|     mWeb->on("/system",         HTTP_ANY,  std::bind(&web::onSystem,       this, std::placeholders::_1)); |  | ||||
|     mWeb->on("/erase",          HTTP_ANY,  std::bind(&web::showErase,      this, std::placeholders::_1)); |  | ||||
|     mWeb->on("/factory",        HTTP_ANY,  std::bind(&web::showFactoryRst, this, std::placeholders::_1)); |  | ||||
| 
 |  | ||||
|     mWeb->on("/setup",          HTTP_GET,  std::bind(&web::onSetup,        this, std::placeholders::_1)); |  | ||||
|     mWeb->on("/save",           HTTP_ANY,  std::bind(&web::showSave,       this, std::placeholders::_1)); |  | ||||
| 
 |  | ||||
|     mWeb->on("/live",           HTTP_ANY,  std::bind(&web::onLive,         this, std::placeholders::_1)); |  | ||||
|     mWeb->on("/api1",           HTTP_POST, std::bind(&web::showWebApi,     this, std::placeholders::_1)); |  | ||||
| 
 |  | ||||
| #ifdef ENABLE_JSON_EP |  | ||||
|     mWeb->on("/json",           HTTP_ANY,  std::bind(&web::showJson,       this, std::placeholders::_1)); |  | ||||
| #endif |  | ||||
| #ifdef ENABLE_PROMETHEUS_EP |  | ||||
|     mWeb->on("/metrics",        HTTP_ANY,  std::bind(&web::showMetrics,    this, std::placeholders::_1)); |  | ||||
| #endif |  | ||||
| 
 |  | ||||
|     mWeb->on("/update",         HTTP_GET,  std::bind(&web::onUpdate,       this, std::placeholders::_1)); |  | ||||
|     mWeb->on("/update",         HTTP_POST, std::bind(&web::showUpdate,     this, std::placeholders::_1), |  | ||||
|                                            std::bind(&web::showUpdate2,    this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); |  | ||||
|     mWeb->on("/serial",         HTTP_GET,  std::bind(&web::onSerial,       this, std::placeholders::_1)); |  | ||||
| 
 |  | ||||
| 
 |  | ||||
|     mEvts->onConnect(std::bind(&web::onConnect, this, std::placeholders::_1)); |  | ||||
|     mWeb->addHandler(mEvts); |  | ||||
| 
 |  | ||||
|     mApi->setup(); |  | ||||
| 
 |  | ||||
|     registerDebugCb(std::bind(&web::serialCb, this, std::placeholders::_1)); |  | ||||
| } |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::loop(void) { |  | ||||
|     mApi->loop(); |  | ||||
| 
 |  | ||||
|     if(ah::checkTicker(&mWebSerialTicker, mWebSerialInterval)) { |  | ||||
|         if(mSerialBufFill > 0) { |  | ||||
|             mEvts->send(mSerialBuf, "serial", millis()); |  | ||||
|             memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); |  | ||||
|             mSerialBufFill = 0; |  | ||||
|         } |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::tickSecond() { |  | ||||
|     if(0 != mLogoutTimeout) { |  | ||||
|         mLogoutTimeout -= 1; |  | ||||
|         if(0 == mLogoutTimeout) |  | ||||
|             mProtected = true; |  | ||||
| 
 |  | ||||
|         DPRINTLN(DBG_DEBUG, "auto logout in " + String(mLogoutTimeout)); |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::setProtection(bool protect) { |  | ||||
|     mProtected = protect; |  | ||||
| } |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::onConnect(AsyncEventSourceClient *client) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, "onConnect"); |  | ||||
| 
 |  | ||||
|     if(client->lastId()) |  | ||||
|         DPRINTLN(DBG_VERBOSE, "Client reconnected! Last message ID that it got is: " + String(client->lastId())); |  | ||||
| 
 |  | ||||
|     client->send("hello!", NULL, millis(), 1000); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::onIndex(AsyncWebServerRequest *request) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("onIndex")); |  | ||||
| 
 |  | ||||
|     if(mProtected) { |  | ||||
|         request->redirect("/login"); |  | ||||
|         return; |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), index_html, index_html_len); |  | ||||
|     response->addHeader(F("Content-Encoding"), "gzip"); |  | ||||
|     request->send(response); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::onLogin(AsyncWebServerRequest *request) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("onLogin")); |  | ||||
| 
 |  | ||||
|     if(request->args() > 0) { |  | ||||
|         if(String(request->arg("pwd")) == String(mConfig->sys.adminPwd)) { |  | ||||
|             mProtected      = false; |  | ||||
|             request->redirect("/"); |  | ||||
|         } |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), login_html, login_html_len); |  | ||||
|     response->addHeader(F("Content-Encoding"), "gzip"); |  | ||||
|     request->send(response); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::onCss(AsyncWebServerRequest *request) { |  | ||||
|     mLogoutTimeout = LOGOUT_TIMEOUT; |  | ||||
|     AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), style_css, style_css_len); |  | ||||
|     response->addHeader(F("Content-Encoding"), "gzip"); |  | ||||
|     request->send(response); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::onApiJs(AsyncWebServerRequest *request) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("onApiJs")); |  | ||||
| 
 |  | ||||
|     AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/javascript"), api_js, api_js_len); |  | ||||
|     response->addHeader(F("Content-Encoding"), "gzip"); |  | ||||
|     request->send(response); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::onFavicon(AsyncWebServerRequest *request) { |  | ||||
|     static const char favicon_type[] PROGMEM = "image/x-icon"; |  | ||||
|     AsyncWebServerResponse *response = request->beginResponse_P(200, favicon_type, favicon_ico, favicon_ico_len); |  | ||||
|     response->addHeader(F("Content-Encoding"), "gzip"); |  | ||||
|     request->send(response); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::showNotFound(AsyncWebServerRequest *request) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("showNotFound - ") + request->url()); |  | ||||
|     String msg = F("File Not Found\n\nURL: "); |  | ||||
|     msg += request->url(); |  | ||||
|     msg += F("\nMethod: "); |  | ||||
|     msg += ( request->method() == HTTP_GET ) ? "GET" : "POST"; |  | ||||
|     msg += F("\nArguments: "); |  | ||||
|     msg += request->args(); |  | ||||
|     msg += "\n"; |  | ||||
| 
 |  | ||||
|     for(uint8_t i = 0; i < request->args(); i++ ) { |  | ||||
|         msg += " " + request->argName(i) + ": " + request->arg(i) + "\n"; |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     request->send(404, F("text/plain"), msg); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::onReboot(AsyncWebServerRequest *request) { |  | ||||
|     mMain->mShouldReboot = true; |  | ||||
|     AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len); |  | ||||
|     response->addHeader(F("Content-Encoding"), "gzip"); |  | ||||
|     request->send(response); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::onSystem(AsyncWebServerRequest *request) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("onSystem")); |  | ||||
| 
 |  | ||||
|     if(mProtected) { |  | ||||
|         request->redirect("/login"); |  | ||||
|         return; |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len); |  | ||||
|     response->addHeader(F("Content-Encoding"), "gzip"); |  | ||||
|     request->send(response); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::onLogout(AsyncWebServerRequest *request) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("onLogout")); |  | ||||
| 
 |  | ||||
|     if(mProtected) { |  | ||||
|         request->redirect("/login"); |  | ||||
|         return; |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     mProtected = true; |  | ||||
| 
 |  | ||||
|     AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len); |  | ||||
|     response->addHeader(F("Content-Encoding"), "gzip"); |  | ||||
|     request->send(response); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::showErase(AsyncWebServerRequest *request) { |  | ||||
|     if(mProtected) { |  | ||||
|         request->redirect("/login"); |  | ||||
|         return; |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("showErase")); |  | ||||
|     mMain->eraseSettings(false); |  | ||||
|     onReboot(request); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::showFactoryRst(AsyncWebServerRequest *request) { |  | ||||
|     if(mProtected) { |  | ||||
|         request->redirect("/login"); |  | ||||
|         return; |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("showFactoryRst")); |  | ||||
|     String content = ""; |  | ||||
|     int refresh = 3; |  | ||||
|     if(request->args() > 0) { |  | ||||
|         if(request->arg("reset").toInt() == 1) { |  | ||||
|             refresh = 10; |  | ||||
|             if(mMain->eraseSettings(true)) |  | ||||
|                 content = F("factory reset: success\n\nrebooting ... "); |  | ||||
|             else |  | ||||
|                 content = F("factory reset: failed\n\nrebooting ... "); |  | ||||
|         } |  | ||||
|         else { |  | ||||
|             content = F("factory reset: aborted"); |  | ||||
|             refresh = 3; |  | ||||
|         } |  | ||||
|     } |  | ||||
|     else { |  | ||||
|         content = F("<h1>Factory Reset</h1>" |  | ||||
|             "<p><a href=\"/factory?reset=1\">RESET</a><br/><br/><a href=\"/factory?reset=0\">CANCEL</a><br/></p>"); |  | ||||
|         refresh = 120; |  | ||||
|     } |  | ||||
|     request->send(200, F("text/html"), F("<!doctype html><html><head><title>Factory Reset</title><meta http-equiv=\"refresh\" content=\"") + String(refresh) + F("; URL=/\"></head><body>") + content + F("</body></html>")); |  | ||||
|     if(refresh == 10) { |  | ||||
|         delay(1000); |  | ||||
|         ESP.restart(); |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::onSetup(AsyncWebServerRequest *request) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("onSetup")); |  | ||||
| 
 |  | ||||
|     if(mProtected) { |  | ||||
|         request->redirect("/login"); |  | ||||
|         return; |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), setup_html, setup_html_len); |  | ||||
|     response->addHeader(F("Content-Encoding"), "gzip"); |  | ||||
|     request->send(response); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::showSave(AsyncWebServerRequest *request) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("showSave")); |  | ||||
| 
 |  | ||||
|     if(mProtected) { |  | ||||
|         request->redirect("/login"); |  | ||||
|         return; |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     if(request->args() > 0) { |  | ||||
|         char buf[20] = {0}; |  | ||||
| 
 |  | ||||
|         // general
 |  | ||||
|         if(request->arg("ssid") != "") |  | ||||
|             request->arg("ssid").toCharArray(mConfig->sys.stationSsid, SSID_LEN); |  | ||||
|         if(request->arg("pwd") != "{PWD}") |  | ||||
|             request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN); |  | ||||
|         if(request->arg("device") != "") |  | ||||
|             request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN); |  | ||||
|         if(request->arg("adminpwd") != "{PWD}") { |  | ||||
|             request->arg("adminpwd").toCharArray(mConfig->sys.adminPwd, PWD_LEN); |  | ||||
|             mProtected = (strlen(mConfig->sys.adminPwd) > 0); |  | ||||
|         } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
|         // static ip
 |  | ||||
|         if(request->arg("ipAddr") != "") { |  | ||||
|             request->arg("ipAddr").toCharArray(buf, SSID_LEN); |  | ||||
|             ip2Arr(mConfig->sys.ip.ip, buf); |  | ||||
|             if(request->arg("ipMask") != "") { |  | ||||
|                 request->arg("ipMask").toCharArray(buf, SSID_LEN); |  | ||||
|                 ip2Arr(mConfig->sys.ip.mask, buf); |  | ||||
|             } |  | ||||
|             if(request->arg("ipDns1") != "") { |  | ||||
|                 request->arg("ipDns1").toCharArray(buf, SSID_LEN); |  | ||||
|                 ip2Arr(mConfig->sys.ip.dns1, buf); |  | ||||
|             } |  | ||||
|             if(request->arg("ipDns2") != "") { |  | ||||
|                 request->arg("ipDns2").toCharArray(buf, SSID_LEN); |  | ||||
|                 ip2Arr(mConfig->sys.ip.dns2, buf); |  | ||||
|             } |  | ||||
|             if(request->arg("ipGateway") != "") { |  | ||||
|                 request->arg("ipGateway").toCharArray(buf, SSID_LEN); |  | ||||
|                 ip2Arr(mConfig->sys.ip.gateway, buf); |  | ||||
|             } |  | ||||
|         } |  | ||||
|         else |  | ||||
|             memset(&mConfig->sys.ip.ip, 0, 4); |  | ||||
| 
 |  | ||||
| 
 |  | ||||
|         // inverter
 |  | ||||
|         Inverter<> *iv; |  | ||||
|         for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { |  | ||||
|             iv = mMain->mSys->getInverterByPos(i, false); |  | ||||
|             // address
 |  | ||||
|             request->arg("inv" + String(i) + "Addr").toCharArray(buf, 20); |  | ||||
|             if(strlen(buf) == 0) |  | ||||
|                 memset(buf, 0, 20); |  | ||||
|             iv->config->serial.u64 = mMain->Serial2u64(buf); |  | ||||
|             switch(iv->config->serial.b[4]) { |  | ||||
|                 case 0x21: iv->type = INV_TYPE_1CH; iv->channels = 1; break; |  | ||||
|                 case 0x41: iv->type = INV_TYPE_2CH; iv->channels = 2; break; |  | ||||
|                 case 0x61: iv->type = INV_TYPE_4CH; iv->channels = 4; break; |  | ||||
|                 default:  break; |  | ||||
|             } |  | ||||
| 
 |  | ||||
|             // name
 |  | ||||
|             request->arg("inv" + String(i) + "Name").toCharArray(iv->config->name, MAX_NAME_LENGTH); |  | ||||
| 
 |  | ||||
|             // max channel power / name
 |  | ||||
|             for(uint8_t j = 0; j < 4; j++) { |  | ||||
|                 iv->config->chMaxPwr[j] = request->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff; |  | ||||
|                 request->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->config->chName[j], MAX_NAME_LENGTH); |  | ||||
|             } |  | ||||
|             iv->initialized = true; |  | ||||
|         } |  | ||||
| 
 |  | ||||
|         if(request->arg("invInterval") != "") |  | ||||
|             mConfig->nrf.sendInterval = request->arg("invInterval").toInt(); |  | ||||
|         if(request->arg("invRetry") != "") |  | ||||
|             mConfig->nrf.maxRetransPerPyld = request->arg("invRetry").toInt(); |  | ||||
| 
 |  | ||||
|         // pinout
 |  | ||||
|         uint8_t pin; |  | ||||
|         for(uint8_t i = 0; i < 5; i ++) { |  | ||||
|             pin = request->arg(String(pinArgNames[i])).toInt(); |  | ||||
|             switch(i) { |  | ||||
|                 default: mConfig->nrf.pinCs    = ((pin != 0xff) ? pin : DEF_CS_PIN);  break; |  | ||||
|                 case 1:  mConfig->nrf.pinCe    = ((pin != 0xff) ? pin : DEF_CE_PIN);  break; |  | ||||
|                 case 2:  mConfig->nrf.pinIrq   = ((pin != 0xff) ? pin : DEF_IRQ_PIN); break; |  | ||||
|                 case 3:  mConfig->led.led0 = pin; break; |  | ||||
|                 case 4:  mConfig->led.led1 = pin; break; |  | ||||
|             } |  | ||||
|         } |  | ||||
| 
 |  | ||||
|         // nrf24 amplifier power
 |  | ||||
|         mConfig->nrf.amplifierPower = request->arg("rf24Power").toInt() & 0x03; |  | ||||
| 
 |  | ||||
|         // ntp
 |  | ||||
|         if(request->arg("ntpAddr") != "") { |  | ||||
|             request->arg("ntpAddr").toCharArray(mConfig->ntp.addr, NTP_ADDR_LEN); |  | ||||
|             mConfig->ntp.port = request->arg("ntpPort").toInt() & 0xffff; |  | ||||
|         } |  | ||||
| 
 |  | ||||
|         // sun
 |  | ||||
|         if(request->arg("sunLat") == "" || (request->arg("sunLon") == "")) { |  | ||||
|             mConfig->sun.lat = 0.0; |  | ||||
|             mConfig->sun.lon = 0.0; |  | ||||
|             mConfig->sun.disNightCom = false; |  | ||||
|         } else { |  | ||||
|             mConfig->sun.lat = request->arg("sunLat").toFloat(); |  | ||||
|             mConfig->sun.lon = request->arg("sunLon").toFloat(); |  | ||||
|             mConfig->sun.disNightCom = (request->arg("sunDisNightCom") == "on"); |  | ||||
|         } |  | ||||
| 
 |  | ||||
|         // mqtt
 |  | ||||
|         if(request->arg("mqttAddr") != "") { |  | ||||
|             String addr = request->arg("mqttAddr"); |  | ||||
|             addr.trim(); |  | ||||
|             addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN); |  | ||||
|             request->arg("mqttUser").toCharArray(mConfig->mqtt.user, MQTT_USER_LEN); |  | ||||
|             if(request->arg("mqttPwd") != "{PWD}") |  | ||||
|                 request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN); |  | ||||
|             request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN); |  | ||||
|             mConfig->mqtt.port = request->arg("mqttPort").toInt(); |  | ||||
|         } |  | ||||
| 
 |  | ||||
|         // serial console
 |  | ||||
|         if(request->arg("serIntvl") != "") { |  | ||||
|             mConfig->serial.interval = request->arg("serIntvl").toInt() & 0xffff; |  | ||||
| 
 |  | ||||
|             mConfig->serial.debug  = (request->arg("serDbg") == "on"); |  | ||||
|             mConfig->serial.showIv = (request->arg("serEn") == "on"); |  | ||||
|             // Needed to log TX buffers to serial console
 |  | ||||
|             mMain->mSys->Radio.mSerialDebug = mConfig->serial.debug; |  | ||||
|         } |  | ||||
|         mMain->saveSettings(); |  | ||||
| 
 |  | ||||
|         if(request->arg("reboot") == "on") |  | ||||
|             onReboot(request); |  | ||||
|         else { |  | ||||
|             AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len); |  | ||||
|             response->addHeader(F("Content-Encoding"), "gzip"); |  | ||||
|             request->send(response); |  | ||||
|         } |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::onLive(AsyncWebServerRequest *request) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("onLive")); |  | ||||
| 
 |  | ||||
|     if(mProtected) { |  | ||||
|         request->redirect("/login"); |  | ||||
|         return; |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), visualization_html, visualization_html_len); |  | ||||
|     response->addHeader(F("Content-Encoding"), "gzip"); |  | ||||
|     request->send(response); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::showWebApi(AsyncWebServerRequest *request) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("web::showWebApi")); |  | ||||
|     DPRINTLN(DBG_DEBUG, request->arg("plain")); |  | ||||
|     const size_t capacity = 200; // Use arduinojson.org/assistant to compute the capacity.
 |  | ||||
|     DynamicJsonDocument response(capacity); |  | ||||
| 
 |  | ||||
|     // Parse JSON object
 |  | ||||
|     deserializeJson(response, request->arg("plain")); |  | ||||
|     // ToDo: error handling for payload
 |  | ||||
|     uint8_t iv_id = response["inverter"]; |  | ||||
|     uint8_t cmd = response["cmd"]; |  | ||||
|     Inverter<> *iv = mMain->mSys->getInverterByPos(iv_id); |  | ||||
|     if (NULL != iv) { |  | ||||
|         if (response["tx_request"] == (uint8_t)TX_REQ_INFO) { |  | ||||
|             // if the AlarmData is requested set the Alarm Index to the requested one
 |  | ||||
|             if (cmd == AlarmData || cmd == AlarmUpdate) { |  | ||||
|                 // set the AlarmMesIndex for the request from user input
 |  | ||||
|                 iv->alarmMesIndex = response["payload"]; |  | ||||
|             } |  | ||||
|             DPRINTLN(DBG_INFO, F("Will make tx-request 0x15 with subcmd ") + String(cmd) + F(" and payload ") + String((uint16_t) response["payload"])); |  | ||||
|             // process payload from web request corresponding to the cmd
 |  | ||||
|             iv->enqueCommand<InfoCommand>(cmd); |  | ||||
|         } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
|         if (response["tx_request"] == (uint8_t)TX_REQ_DEVCONTROL) { |  | ||||
|             if (response["cmd"] == (uint8_t)ActivePowerContr) { |  | ||||
|                 uint16_t webapiPayload = response["payload"]; |  | ||||
|                 uint16_t webapiPayload2 = response["payload2"]; |  | ||||
|                 if (webapiPayload > 0 && webapiPayload < 10000) { |  | ||||
|                     iv->devControlCmd = ActivePowerContr; |  | ||||
|                     iv->powerLimit[0] = webapiPayload; |  | ||||
|                     if (webapiPayload2 > 0) |  | ||||
|                         iv->powerLimit[1] = webapiPayload2; // dev option, no sanity check
 |  | ||||
|                     else                                            // if not set, set it to 0x0000 default
 |  | ||||
|                         iv->powerLimit[1] = AbsolutNonPersistent; // payload will be seted temporary in Watt absolut
 |  | ||||
|                     if (iv->powerLimit[1] & 0x0001) |  | ||||
|                         DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("% via REST API")); |  | ||||
|                     else |  | ||||
|                         DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W via REST API")); |  | ||||
|                     iv->devControlRequest = true; // queue it in the request loop
 |  | ||||
|                 } |  | ||||
|             } |  | ||||
|             if (response["cmd"] == (uint8_t)TurnOff) { |  | ||||
|                 iv->devControlCmd = TurnOff; |  | ||||
|                 iv->devControlRequest = true; // queue it in the request loop
 |  | ||||
|             } |  | ||||
|             if (response["cmd"] == (uint8_t)TurnOn) { |  | ||||
|                 iv->devControlCmd = TurnOn; |  | ||||
|                 iv->devControlRequest = true; // queue it in the request loop
 |  | ||||
|             } |  | ||||
|             if (response["cmd"] == (uint8_t)CleanState_LockAndAlarm) { |  | ||||
|                 iv->devControlCmd = CleanState_LockAndAlarm; |  | ||||
|                 iv->devControlRequest = true; // queue it in the request loop
 |  | ||||
|             } |  | ||||
|             if (response["cmd"] == (uint8_t)Restart) { |  | ||||
|                 iv->devControlCmd = Restart; |  | ||||
|                 iv->devControlRequest = true; // queue it in the request loop
 |  | ||||
|             } |  | ||||
|         } |  | ||||
|     } |  | ||||
|     request->send(200, "text/json", "{success:true}"); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::onUpdate(AsyncWebServerRequest *request) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("onUpdate")); |  | ||||
| 
 |  | ||||
|     /*if(mProtected) {
 |  | ||||
|         request->redirect("/login"); |  | ||||
|         return; |  | ||||
|     }*/ |  | ||||
| 
 |  | ||||
|     AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), update_html, update_html_len); |  | ||||
|     response->addHeader(F("Content-Encoding"), "gzip"); |  | ||||
|     request->send(response); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::showUpdate(AsyncWebServerRequest *request) { |  | ||||
|     bool reboot = !Update.hasError(); |  | ||||
| 
 |  | ||||
|     String html = F("<!doctype html><html><head><title>Update</title><meta http-equiv=\"refresh\" content=\"20; URL=/\"></head><body>Update: "); |  | ||||
|     if(reboot) |  | ||||
|         html += "success"; |  | ||||
|     else |  | ||||
|         html += "failed"; |  | ||||
|     html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>"); |  | ||||
| 
 |  | ||||
|     AsyncWebServerResponse *response = request->beginResponse(200, F("text/html"), html); |  | ||||
|     response->addHeader("Connection", "close"); |  | ||||
|     request->send(response); |  | ||||
|     mMain->mShouldReboot = reboot; |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { |  | ||||
|     if(!index) { |  | ||||
|         Serial.printf("Update Start: %s\n", filename.c_str()); |  | ||||
| #ifndef ESP32 |  | ||||
|         Update.runAsync(true); |  | ||||
| #endif |  | ||||
|         if(!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { |  | ||||
|             Update.printError(Serial); |  | ||||
|         } |  | ||||
|     } |  | ||||
|     if(!Update.hasError()) { |  | ||||
|         if(Update.write(data, len) != len){ |  | ||||
|             Update.printError(Serial); |  | ||||
|         } |  | ||||
|     } |  | ||||
|     if(final) { |  | ||||
|         if(Update.end(true)) { |  | ||||
|             Serial.printf("Update Success: %uB\n", index+len); |  | ||||
|         } else { |  | ||||
|             Update.printError(Serial); |  | ||||
|         } |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::onSerial(AsyncWebServerRequest *request) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("onSerial")); |  | ||||
| 
 |  | ||||
|     if(mProtected) { |  | ||||
|         request->redirect("/login"); |  | ||||
|         return; |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), serial_html, serial_html_len); |  | ||||
|     response->addHeader(F("Content-Encoding"), "gzip"); |  | ||||
|     request->send(response); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::serialCb(String msg) { |  | ||||
|     msg.replace("\r\n", "<rn>"); |  | ||||
|     if(mSerialAddTime) { |  | ||||
|         if((9 + mSerialBufFill) <= WEB_SERIAL_BUF_SIZE) { |  | ||||
|             strncpy(&mSerialBuf[mSerialBufFill], mMain->getTimeStr(mApi->getTimezoneOffset()).c_str(), 9); |  | ||||
|             mSerialBufFill += 9; |  | ||||
|         } |  | ||||
|         else { |  | ||||
|             mSerialBufFill = 0; |  | ||||
|             mEvts->send("webSerial, buffer overflow!", "serial", millis()); |  | ||||
|         } |  | ||||
|         mSerialAddTime = false; |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     if(msg.endsWith("<rn>")) |  | ||||
|         mSerialAddTime = true; |  | ||||
| 
 |  | ||||
|     uint16_t length = msg.length(); |  | ||||
|     if((length + mSerialBufFill) <= WEB_SERIAL_BUF_SIZE) { |  | ||||
|         strncpy(&mSerialBuf[mSerialBufFill], msg.c_str(), length); |  | ||||
|         mSerialBufFill += length; |  | ||||
|     } |  | ||||
|     else { |  | ||||
|         mSerialBufFill = 0; |  | ||||
|         mEvts->send("webSerial, buffer overflow!", "serial", millis()); |  | ||||
|     } |  | ||||
| 
 |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| #ifdef ENABLE_JSON_EP |  | ||||
| void web::showJson(void) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("web::showJson")); |  | ||||
|     String modJson; |  | ||||
| 
 |  | ||||
|     modJson = F("{\n"); |  | ||||
|     for(uint8_t id = 0; id < mMain->mSys->getNumInverters(); id++) { |  | ||||
|         Inverter<> *iv = mMain->mSys->getInverterByPos(id); |  | ||||
|         if(NULL != iv) { |  | ||||
|             char topic[40], val[25]; |  | ||||
|             snprintf(topic, 30, "\"%s\": {\n", iv->name); |  | ||||
|             modJson += String(topic); |  | ||||
|             for(uint8_t i = 0; i < iv->listLen; i++) { |  | ||||
|                 snprintf(topic, 40, "\t\"ch%d/%s\"", iv->assign[i].ch, iv->getFieldName(i)); |  | ||||
|                 snprintf(val, 25, "[%.3f, \"%s\"]", iv->getValue(i), iv->getUnit(i)); |  | ||||
|                 modJson += String(topic) + ": " + String(val) + F(",\n"); |  | ||||
|             } |  | ||||
|             modJson += F("\t\"last_msg\": \"") + mMain->getDateTimeStr(iv->ts) + F("\"\n\t},\n"); |  | ||||
|         } |  | ||||
|     } |  | ||||
|     modJson += F("\"json_ts\": \"") + String(mMain->getDateTimeStr(mMain->mTimestamp)) + F("\"\n}\n"); |  | ||||
| 
 |  | ||||
|     mWeb->send(200, F("application/json"), modJson); |  | ||||
| } |  | ||||
| #endif |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| #ifdef ENABLE_PROMETHEUS_EP |  | ||||
| std::pair<String, String> web::convertToPromUnits(String shortUnit) { |  | ||||
| 
 |  | ||||
|     if(shortUnit == "A")    return {"ampere", "gauge"}; |  | ||||
|     if(shortUnit == "V")    return {"volt", "gauge"}; |  | ||||
|     if(shortUnit == "%")    return {"ratio", "gauge"}; |  | ||||
|     if(shortUnit == "W")    return {"watt", "gauge"}; |  | ||||
|     if(shortUnit == "Wh")   return {"watt_daily", "counter"}; |  | ||||
|     if(shortUnit == "kWh")  return {"watt_total", "counter"}; |  | ||||
|     if(shortUnit == "°C")   return {"celsius", "gauge"}; |  | ||||
| 
 |  | ||||
|     return {"", "gauge"}; |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void web::showMetrics(void) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, F("web::showMetrics")); |  | ||||
|     String metrics; |  | ||||
|     char headline[80]; |  | ||||
| 
 |  | ||||
|     snprintf(headline, 80, "ahoy_solar_info{version=\"%s\",image=\"\",devicename=\"%s\"} 1", mVersion, mconfig->sys.deviceName); |  | ||||
|     metrics += "# TYPE ahoy_solar_info gauge\n" + String(headline) + "\n"; |  | ||||
| 
 |  | ||||
|     for(uint8_t id = 0; id < mMain->mSys->getNumInverters(); id++) { |  | ||||
|         Inverter<> *iv = mMain->mSys->getInverterByPos(id); |  | ||||
|         if(NULL != iv) { |  | ||||
|             char type[60], topic[60], val[25]; |  | ||||
|             for(uint8_t i = 0; i < iv->listLen; i++) { |  | ||||
|                 uint8_t channel = iv->assign[i].ch; |  | ||||
|                 if(channel == 0) { |  | ||||
|                     String promUnit, promType; |  | ||||
|                     std::tie(promUnit, promType) = convertToPromUnits( iv->getUnit(i) ); |  | ||||
|                     snprintf(type, 60, "# TYPE ahoy_solar_%s_%s %s", iv->getFieldName(i), promUnit.c_str(), promType.c_str()); |  | ||||
|                     snprintf(topic, 60, "ahoy_solar_%s_%s{inverter=\"%s\"}", iv->getFieldName(i), promUnit.c_str(), iv->name); |  | ||||
|                     snprintf(val, 25, "%.3f", iv->getValue(i)); |  | ||||
|                     metrics += String(type) + "\n" + String(topic) + " " + String(val) + "\n"; |  | ||||
|                 } |  | ||||
|             } |  | ||||
|         } |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     mWeb->send(200, F("text/plain"), metrics); |  | ||||
| } |  | ||||
| #endif |  | ||||
| @ -1,581 +0,0 @@ | |||||
| //-----------------------------------------------------------------------------
 |  | ||||
| // 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
 |  | ||||
| // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| 
 |  | ||||
| #if defined(ESP32) && defined(F) |  | ||||
|   #undef F |  | ||||
|   #define F(sl) (sl) |  | ||||
| #endif |  | ||||
| 
 |  | ||||
| #include "webApi.h" |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| webApi::webApi(AsyncWebServer *srv, app *app, settings_t *config, statistics_t *stat, char version[]) { |  | ||||
|     mSrv = srv; |  | ||||
|     mApp = app; |  | ||||
|     mConfig  = config; |  | ||||
|     mStat    = stat; |  | ||||
|     mVersion = version; |  | ||||
| 
 |  | ||||
|     mTimezoneOffset = 0; |  | ||||
| } |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::setup(void) { |  | ||||
|     mSrv->on("/api", HTTP_GET,  std::bind(&webApi::onApi,         this, std::placeholders::_1)); |  | ||||
|     mSrv->on("/api", HTTP_POST, std::bind(&webApi::onApiPost,     this, std::placeholders::_1)).onBody( |  | ||||
|                                 std::bind(&webApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); |  | ||||
| 
 |  | ||||
|     mSrv->on("/get_setup", HTTP_GET,  std::bind(&webApi::onDwnldSetup,   this, std::placeholders::_1)); |  | ||||
| } |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::loop(void) { |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::onApi(AsyncWebServerRequest *request) { |  | ||||
|     AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192); |  | ||||
|     JsonObject root = response->getRoot(); |  | ||||
| 
 |  | ||||
|     Inverter<> *iv = mApp->mSys->getInverterByPos(0, false); |  | ||||
|     String path = request->url().substring(5); |  | ||||
|     if(path == "html/system")         getHtmlSystem(root); |  | ||||
|     else if(path == "html/logout")    getHtmlLogout(root); |  | ||||
|     else if(path == "html/save")      getHtmlSave(root); |  | ||||
|     else if(path == "system")         getSysInfo(root); |  | ||||
|     else if(path == "reboot")         getReboot(root); |  | ||||
|     else if(path == "statistics")     getStatistics(root); |  | ||||
|     else if(path == "inverter/list")  getInverterList(root); |  | ||||
|     else if(path == "menu")           getMenu(root); |  | ||||
|     else if(path == "index")          getIndex(root); |  | ||||
|     else if(path == "setup")          getSetup(root); |  | ||||
|     else if(path == "setup/networks") getNetworks(root); |  | ||||
|     else if(path == "live")           getLive(root); |  | ||||
|     else if(path == "record/info")    getRecord(root, iv->getRecordStruct(InverterDevInform_All)); |  | ||||
|     else if(path == "record/alarm")   getRecord(root, iv->getRecordStruct(AlarmData)); |  | ||||
|     else if(path == "record/config")  getRecord(root, iv->getRecordStruct(SystemConfigPara)); |  | ||||
|     else if(path == "record/live")    getRecord(root, iv->getRecordStruct(RealTimeRunData_Debug)); |  | ||||
|     else |  | ||||
|         getNotFound(root, F("http://") + request->host() + F("/api/")); |  | ||||
| 
 |  | ||||
|     response->addHeader("Access-Control-Allow-Origin", "*"); |  | ||||
|     response->addHeader("Access-Control-Allow-Headers", "content-type"); |  | ||||
|     response->setLength(); |  | ||||
|     request->send(response); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::onApiPost(AsyncWebServerRequest *request) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, "onApiPost"); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { |  | ||||
|     DPRINTLN(DBG_VERBOSE, "onApiPostBody"); |  | ||||
|     DynamicJsonDocument json(200); |  | ||||
|     AsyncJsonResponse* response = new AsyncJsonResponse(false, 200); |  | ||||
|     JsonObject root = response->getRoot(); |  | ||||
| 
 |  | ||||
|     DeserializationError err = deserializeJson(json, (const char *)data, len); |  | ||||
|     JsonObject obj = json.as<JsonObject>(); |  | ||||
|     root[F("success")] = (err) ? false : true; |  | ||||
|     if(!err) { |  | ||||
|         String path = request->url().substring(5); |  | ||||
|         if(path == "ctrl") |  | ||||
|             root[F("success")] = setCtrl(obj, root); |  | ||||
|         else if(path == "setup") |  | ||||
|             root[F("success")] = setSetup(obj, root); |  | ||||
|         else { |  | ||||
|             root[F("success")] = false; |  | ||||
|             root[F("error")]   = "Path not found: " + path; |  | ||||
|         } |  | ||||
|     } |  | ||||
|     else { |  | ||||
|         switch (err.code()) { |  | ||||
|             case DeserializationError::Ok: break; |  | ||||
|             case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input");          break; |  | ||||
|             case DeserializationError::NoMemory:     root[F("error")] = F("Not enough memory");      break; |  | ||||
|             default:                                 root[F("error")] = F("Deserialization failed"); break; |  | ||||
|         } |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     response->setLength(); |  | ||||
|     request->send(response); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getNotFound(JsonObject obj, String url) { |  | ||||
|     JsonObject ep = obj.createNestedObject("avail_endpoints"); |  | ||||
|     ep[F("system")]        = url + F("system"); |  | ||||
|     ep[F("statistics")]    = url + F("statistics"); |  | ||||
|     ep[F("inverter/list")] = url + F("inverter/list"); |  | ||||
|     ep[F("index")]         = url + F("index"); |  | ||||
|     ep[F("setup")]         = url + F("setup"); |  | ||||
|     ep[F("live")]          = url + F("live"); |  | ||||
|     ep[F("record/info")]   = url + F("record/info"); |  | ||||
|     ep[F("record/alarm")]  = url + F("record/alarm"); |  | ||||
|     ep[F("record/config")] = url + F("record/config"); |  | ||||
|     ep[F("record/live")]   = url + F("record/live"); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::onDwnldSetup(AsyncWebServerRequest *request) { |  | ||||
|     AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192); |  | ||||
|     JsonObject root = response->getRoot(); |  | ||||
| 
 |  | ||||
|     getSetup(root); |  | ||||
| 
 |  | ||||
|     response->setLength(); |  | ||||
|     response->addHeader("Content-Type", "application/octet-stream"); |  | ||||
|     response->addHeader("Content-Description", "File Transfer"); |  | ||||
|     response->addHeader("Content-Disposition", "attachment; filename=ahoy_setup.json"); |  | ||||
|     request->send(response); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getSysInfo(JsonObject obj) { |  | ||||
|     obj[F("ssid")]         = mConfig->sys.stationSsid; |  | ||||
|     obj[F("device_name")]  = mConfig->sys.deviceName; |  | ||||
|     obj[F("version")]      = String(mVersion); |  | ||||
|     obj[F("build")]        = String(AUTO_GIT_HASH); |  | ||||
|     obj[F("ts_uptime")]    = mApp->getUptime(); |  | ||||
|     obj[F("ts_now")]       = mApp->getTimestamp(); |  | ||||
|     obj[F("ts_sunrise")]   = mApp->getSunrise(); |  | ||||
|     obj[F("ts_sunset")]    = mApp->getSunset(); |  | ||||
|     obj[F("ts_sun_upd")]   = mApp->getLatestSunTimestamp(); |  | ||||
|     obj[F("wifi_rssi")]    = WiFi.RSSI(); |  | ||||
|     obj[F("mac")]          = WiFi.macAddress(); |  | ||||
|     obj[F("hostname")]     = WiFi.getHostname(); |  | ||||
|     obj[F("pwd_set")]      = (strlen(mConfig->sys.adminPwd) > 0); |  | ||||
| 
 |  | ||||
|     obj[F("sdk")]          = ESP.getSdkVersion(); |  | ||||
|     obj[F("cpu_freq")]     = ESP.getCpuFreqMHz(); |  | ||||
|     obj[F("heap_free")]    = ESP.getFreeHeap(); |  | ||||
|     obj[F("sketch_total")] = ESP.getFreeSketchSpace(); |  | ||||
|     obj[F("sketch_used")]  = ESP.getSketchSize() / 1024; // in kb
 |  | ||||
| 
 |  | ||||
| #if defined(ESP32) |  | ||||
|     obj[F("heap_total")]    = ESP.getHeapSize(); |  | ||||
|     obj[F("chip_revision")] = ESP.getChipRevision(); |  | ||||
|     obj[F("chip_model")]    = ESP.getChipModel(); |  | ||||
|     obj[F("chip_cores")]    = ESP.getChipCores(); |  | ||||
|     //obj[F("core_version")]  = F("n/a");
 |  | ||||
|     //obj[F("flash_size")]    = F("n/a");
 |  | ||||
|     //obj[F("heap_frag")]     = F("n/a");
 |  | ||||
|     //obj[F("max_free_blk")]  = F("n/a");
 |  | ||||
|     //obj[F("reboot_reason")] = F("n/a");
 |  | ||||
| #else |  | ||||
|     //obj[F("heap_total")]    = F("n/a");
 |  | ||||
|     //obj[F("chip_revision")] = F("n/a");
 |  | ||||
|     //obj[F("chip_model")]    = F("n/a");
 |  | ||||
|     //obj[F("chip_cores")]    = F("n/a");
 |  | ||||
|     obj[F("core_version")]  = ESP.getCoreVersion(); |  | ||||
|     obj[F("flash_size")]    = ESP.getFlashChipRealSize() / 1024; // in kb
 |  | ||||
|     obj[F("heap_frag")]     = ESP.getHeapFragmentation(); |  | ||||
|     obj[F("max_free_blk")]  = ESP.getMaxFreeBlockSize(); |  | ||||
|     obj[F("reboot_reason")] = ESP.getResetReason(); |  | ||||
| #endif |  | ||||
|     //obj[F("littlefs_total")] = LittleFS.totalBytes();
 |  | ||||
|     //obj[F("littlefs_used")] = LittleFS.usedBytes();
 |  | ||||
| 
 |  | ||||
| #if defined(ESP32) |  | ||||
|     obj[F("esp_type")]    = F("ESP32"); |  | ||||
| #else |  | ||||
|     obj[F("esp_type")]    = F("ESP8266"); |  | ||||
| #endif |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getHtmlSystem(JsonObject obj) { |  | ||||
|     getMenu(obj.createNestedObject(F("menu"))); |  | ||||
|     getSysInfo(obj.createNestedObject(F("system"))); |  | ||||
|     obj[F("html")] = F("<a href=\"/factory\" class=\"btn\">Factory Reset</a><br/><br/><a href=\"/reboot\" class=\"btn\">Reboot</a>"); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getHtmlLogout(JsonObject obj) { |  | ||||
|     getMenu(obj.createNestedObject(F("menu"))); |  | ||||
|     getSysInfo(obj.createNestedObject(F("system"))); |  | ||||
|     obj[F("refresh")] = 3; |  | ||||
|     obj[F("refresh_url")] = "/"; |  | ||||
|     obj[F("html")] = F("succesfully logged out"); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getHtmlSave(JsonObject obj) { |  | ||||
|     getMenu(obj.createNestedObject(F("menu"))); |  | ||||
|     getSysInfo(obj.createNestedObject(F("system"))); |  | ||||
|     obj[F("refresh")] = 2; |  | ||||
|     obj[F("refresh_url")] = "/setup"; |  | ||||
|     obj[F("html")] = F("settings succesfully save"); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getReboot(JsonObject obj) { |  | ||||
|     getMenu(obj.createNestedObject(F("menu"))); |  | ||||
|     getSysInfo(obj.createNestedObject(F("system"))); |  | ||||
|     obj[F("refresh")] = 10; |  | ||||
|     obj[F("refresh_url")] = "/"; |  | ||||
|     obj[F("html")] = F("reboot. Autoreload after 10 seconds"); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getStatistics(JsonObject obj) { |  | ||||
|     obj[F("rx_success")]     = mStat->rxSuccess; |  | ||||
|     obj[F("rx_fail")]        = mStat->rxFail; |  | ||||
|     obj[F("rx_fail_answer")] = mStat->rxFailNoAnser; |  | ||||
|     obj[F("frame_cnt")]      = mStat->frmCnt; |  | ||||
|     obj[F("tx_cnt")]         = mApp->mSys->Radio.mSendCnt; |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getInverterList(JsonObject obj) { |  | ||||
|     JsonArray invArr = obj.createNestedArray(F("inverter")); |  | ||||
| 
 |  | ||||
|     Inverter<> *iv; |  | ||||
|     for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { |  | ||||
|         iv = mApp->mSys->getInverterByPos(i); |  | ||||
|         if(NULL != iv) { |  | ||||
|             JsonObject obj2 = invArr.createNestedObject(); |  | ||||
|             obj2[F("id")]       = i; |  | ||||
|             obj2[F("name")]     = String(iv->config->name); |  | ||||
|             obj2[F("serial")]   = String(iv->config->serial.u64, HEX); |  | ||||
|             obj2[F("channels")] = iv->channels; |  | ||||
|             obj2[F("version")]  = String(iv->fwVersion); |  | ||||
| 
 |  | ||||
|             for(uint8_t j = 0; j < iv->channels; j ++) { |  | ||||
|                 obj2[F("ch_max_power")][j] = iv->config->chMaxPwr[j]; |  | ||||
|                 obj2[F("ch_name")][j] = iv->config->chName[j]; |  | ||||
|             } |  | ||||
|         } |  | ||||
|     } |  | ||||
|     obj[F("interval")]          = String(mConfig->nrf.sendInterval); |  | ||||
|     obj[F("retries")]           = String(mConfig->nrf.maxRetransPerPyld); |  | ||||
|     obj[F("max_num_inverters")] = MAX_NUM_INVERTERS; |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getMqtt(JsonObject obj) { |  | ||||
|     obj[F("broker")] = String(mConfig->mqtt.broker); |  | ||||
|     obj[F("port")]   = String(mConfig->mqtt.port); |  | ||||
|     obj[F("user")]   = String(mConfig->mqtt.user); |  | ||||
|     obj[F("pwd")]    = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String(""); |  | ||||
|     obj[F("topic")]  = String(mConfig->mqtt.topic); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getNtp(JsonObject obj) { |  | ||||
|     obj[F("addr")] = String(mConfig->ntp.addr); |  | ||||
|     obj[F("port")] = String(mConfig->ntp.port); |  | ||||
| } |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getSun(JsonObject obj) { |  | ||||
|     obj[F("lat")] = mConfig->sun.lat ? String(mConfig->sun.lat, 5) : ""; |  | ||||
|     obj[F("lon")] = mConfig->sun.lat ? String(mConfig->sun.lon, 5) : ""; |  | ||||
|     obj[F("disnightcom")] = mConfig->sun.disNightCom; |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getPinout(JsonObject obj) { |  | ||||
|     obj[F("cs")]  = mConfig->nrf.pinCs; |  | ||||
|     obj[F("ce")]  = mConfig->nrf.pinCe; |  | ||||
|     obj[F("irq")] = mConfig->nrf.pinIrq; |  | ||||
|     obj[F("led0")] = mConfig->led.led0; |  | ||||
|     obj[F("led1")] = mConfig->led.led1; |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getRadio(JsonObject obj) { |  | ||||
|     obj[F("power_level")] = mConfig->nrf.amplifierPower; |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getSerial(JsonObject obj) { |  | ||||
|     obj[F("interval")]       = (uint16_t)mConfig->serial.interval; |  | ||||
|     obj[F("show_live_data")] = mConfig->serial.showIv; |  | ||||
|     obj[F("debug")]          = mConfig->serial.debug; |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getStaticIp(JsonObject obj) { |  | ||||
|     if(mConfig->sys.ip.ip[0] != 0) { |  | ||||
|         obj[F("ip")]      = ip2String(mConfig->sys.ip.ip); |  | ||||
|         obj[F("mask")]    = ip2String(mConfig->sys.ip.mask); |  | ||||
|         obj[F("dns1")]     = ip2String(mConfig->sys.ip.dns1); |  | ||||
|         obj[F("dns2")]     = ip2String(mConfig->sys.ip.dns2); |  | ||||
|         obj[F("gateway")] = ip2String(mConfig->sys.ip.gateway); |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getMenu(JsonObject obj) { |  | ||||
|     obj["name"][0] = "Live"; |  | ||||
|     obj["link"][0] = "/live"; |  | ||||
|     obj["name"][1] = "Serial Console"; |  | ||||
|     obj["link"][1] = "/serial"; |  | ||||
|     obj["name"][2] = "Settings"; |  | ||||
|     obj["link"][2] = "/setup"; |  | ||||
|     obj["name"][3] = "-"; |  | ||||
|     obj["name"][4] = "REST API"; |  | ||||
|     obj["link"][4] = "/api"; |  | ||||
|     obj["trgt"][4] = "_blank"; |  | ||||
|     obj["name"][5] = "-"; |  | ||||
|     obj["name"][6] = "Update"; |  | ||||
|     obj["link"][6] = "/update"; |  | ||||
|     obj["name"][7] = "System"; |  | ||||
|     obj["link"][7] = "/system"; |  | ||||
|     if(strlen(mConfig->sys.adminPwd) > 0) { |  | ||||
|         obj["name"][8] = "-"; |  | ||||
|         obj["name"][9] = "Logout"; |  | ||||
|         obj["link"][9] = "/logout"; |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getIndex(JsonObject obj) { |  | ||||
|     getMenu(obj.createNestedObject(F("menu"))); |  | ||||
|     getSysInfo(obj.createNestedObject(F("system"))); |  | ||||
|     getStatistics(obj.createNestedObject(F("statistics"))); |  | ||||
|     obj["refresh_interval"] = mConfig->nrf.sendInterval; |  | ||||
| 
 |  | ||||
|     JsonArray inv = obj.createNestedArray(F("inverter")); |  | ||||
|     Inverter<> *iv; |  | ||||
|     for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { |  | ||||
|         iv = mApp->mSys->getInverterByPos(i); |  | ||||
|         if(NULL != iv) { |  | ||||
|             record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); |  | ||||
|             JsonObject invObj = inv.createNestedObject(); |  | ||||
|             invObj[F("id")]              = i; |  | ||||
|             invObj[F("name")]            = String(iv->config->name); |  | ||||
|             invObj[F("version")]         = String(iv->fwVersion); |  | ||||
|             invObj[F("is_avail")]        = iv->isAvailable(mApp->getTimestamp(), rec); |  | ||||
|             invObj[F("is_producing")]    = iv->isProducing(mApp->getTimestamp(), rec); |  | ||||
|             invObj[F("ts_last_success")] = iv->getLastTs(rec); |  | ||||
|         } |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     JsonArray warn = obj.createNestedArray(F("warnings")); |  | ||||
|     if(!mApp->mSys->Radio.isChipConnected()) |  | ||||
|         warn.add(F("your NRF24 module can't be reached, check the wiring and pinout")); |  | ||||
|     if(!mApp->mqttIsConnected()) |  | ||||
|         warn.add(F("MQTT is not connected")); |  | ||||
| 
 |  | ||||
|     JsonArray info = obj.createNestedArray(F("infos")); |  | ||||
|     if(mApp->getRebootRequestState()) |  | ||||
|         info.add(F("reboot your ESP to apply all your configuration changes!")); |  | ||||
|     if(!mApp->getSettingsValid()) |  | ||||
|         info.add(F("your settings are invalid")); |  | ||||
|     if(mApp->mqttIsConnected()) |  | ||||
|         info.add(F("MQTT is connected, ") + String(mApp->getMqttTxCnt()) + F(" packets sent")); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getSetup(JsonObject obj) { |  | ||||
|     getMenu(obj.createNestedObject(F("menu"))); |  | ||||
|     getSysInfo(obj.createNestedObject(F("system"))); |  | ||||
|     getInverterList(obj.createNestedObject(F("inverter"))); |  | ||||
|     getMqtt(obj.createNestedObject(F("mqtt"))); |  | ||||
|     getNtp(obj.createNestedObject(F("ntp"))); |  | ||||
|     getSun(obj.createNestedObject(F("sun"))); |  | ||||
|     getPinout(obj.createNestedObject(F("pinout"))); |  | ||||
|     getRadio(obj.createNestedObject(F("radio"))); |  | ||||
|     getSerial(obj.createNestedObject(F("serial"))); |  | ||||
|     getStaticIp(obj.createNestedObject(F("static_ip"))); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getNetworks(JsonObject obj) { |  | ||||
|     mApp->getAvailNetworks(obj); |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getLive(JsonObject obj) { |  | ||||
|     getMenu(obj.createNestedObject(F("menu"))); |  | ||||
|     getSysInfo(obj.createNestedObject(F("system"))); |  | ||||
|     JsonArray invArr = obj.createNestedArray(F("inverter")); |  | ||||
|     obj["refresh_interval"] = mConfig->nrf.sendInterval; |  | ||||
| 
 |  | ||||
|     uint8_t list[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q}; |  | ||||
| 
 |  | ||||
|     Inverter<> *iv; |  | ||||
|     uint8_t pos; |  | ||||
|     for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { |  | ||||
|         iv = mApp->mSys->getInverterByPos(i); |  | ||||
|         if(NULL != iv) { |  | ||||
|             record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); |  | ||||
|             JsonObject obj2 = invArr.createNestedObject(); |  | ||||
|             obj2[F("name")]               = String(iv->config->name); |  | ||||
|             obj2[F("channels")]           = iv->channels; |  | ||||
|             obj2[F("power_limit_read")]   = round3(iv->actPowerLimit); |  | ||||
|             obj2[F("last_alarm")]         = String(iv->lastAlarmMsg); |  | ||||
|             obj2[F("ts_last_success")]    = rec->ts; |  | ||||
| 
 |  | ||||
|             JsonArray ch = obj2.createNestedArray("ch"); |  | ||||
|             JsonArray ch0 = ch.createNestedArray(); |  | ||||
|             obj2[F("ch_names")][0] = "AC"; |  | ||||
|             for (uint8_t fld = 0; fld < sizeof(list); fld++) { |  | ||||
|                 pos = (iv->getPosByChFld(CH0, list[fld], rec)); |  | ||||
|                 ch0[fld] = (0xff != pos) ? round3(iv->getValue(pos, rec)) : 0.0; |  | ||||
|                 obj[F("ch0_fld_units")][fld] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; |  | ||||
|                 obj[F("ch0_fld_names")][fld] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; |  | ||||
|             } |  | ||||
| 
 |  | ||||
|             for(uint8_t j = 1; j <= iv->channels; j ++) { |  | ||||
|                 obj2[F("ch_names")][j] = String(iv->config->chName[j-1]); |  | ||||
|                 JsonArray cur = ch.createNestedArray(); |  | ||||
|                 for (uint8_t k = 0; k < 6; k++) { |  | ||||
|                     switch(k) { |  | ||||
|                         default: pos = (iv->getPosByChFld(j, FLD_UDC, rec)); break; |  | ||||
|                         case 1:  pos = (iv->getPosByChFld(j, FLD_IDC, rec)); break; |  | ||||
|                         case 2:  pos = (iv->getPosByChFld(j, FLD_PDC, rec)); break; |  | ||||
|                         case 3:  pos = (iv->getPosByChFld(j, FLD_YD, rec));  break; |  | ||||
|                         case 4:  pos = (iv->getPosByChFld(j, FLD_YT, rec));  break; |  | ||||
|                         case 5:  pos = (iv->getPosByChFld(j, FLD_IRR, rec)); break; |  | ||||
|                     } |  | ||||
|                     cur[k] = (0xff != pos) ? round3(iv->getValue(pos, rec)) : 0.0; |  | ||||
|                     if(1 == j) { |  | ||||
|                         obj[F("fld_units")][k] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; |  | ||||
|                         obj[F("fld_names")][k] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; |  | ||||
|                     } |  | ||||
|                 } |  | ||||
|             } |  | ||||
|         } |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| void webApi::getRecord(JsonObject obj, record_t<> *rec) { |  | ||||
|     JsonArray invArr = obj.createNestedArray(F("inverter")); |  | ||||
| 
 |  | ||||
|     Inverter<> *iv; |  | ||||
|     uint8_t pos; |  | ||||
|     for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { |  | ||||
|         iv = mApp->mSys->getInverterByPos(i); |  | ||||
|         if(NULL != iv) { |  | ||||
|             JsonArray obj2 = invArr.createNestedArray(); |  | ||||
|             for(uint8_t j = 0; j < rec->length; j++) { |  | ||||
|                 byteAssign_t *assign = iv->getByteAssign(j, rec); |  | ||||
|                 pos = (iv->getPosByChFld(assign->ch, assign->fieldId, rec)); |  | ||||
|                 obj2[j]["fld"]  = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; |  | ||||
|                 obj2[j]["unit"] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; |  | ||||
|                 obj2[j]["val"]  = (0xff != pos) ? String(iv->getValue(pos, rec)) : notAvail; |  | ||||
|             } |  | ||||
|         } |  | ||||
|     } |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| bool webApi::setCtrl(JsonObject jsonIn, JsonObject jsonOut) { |  | ||||
|     uint8_t cmd = jsonIn[F("cmd")]; |  | ||||
| 
 |  | ||||
|     // Todo: num is the inverter number 0-3. For better display in DPRINTLN
 |  | ||||
|     uint8_t num = jsonIn[F("inverter")]; |  | ||||
|     uint8_t tx_request = jsonIn[F("tx_request")]; |  | ||||
| 
 |  | ||||
|     if(TX_REQ_DEVCONTROL == tx_request) |  | ||||
|     { |  | ||||
|         DPRINTLN(DBG_INFO, F("devcontrol [") + String(num) + F("], cmd: 0x") + String(cmd, HEX)); |  | ||||
| 
 |  | ||||
|         Inverter<> *iv = getInverter(jsonIn, jsonOut); |  | ||||
|         JsonArray payload = jsonIn[F("payload")].as<JsonArray>(); |  | ||||
| 
 |  | ||||
|         if(NULL != iv) |  | ||||
|         { |  | ||||
|             switch (cmd) |  | ||||
|             { |  | ||||
|                 case TurnOn: |  | ||||
|                     iv->devControlCmd = TurnOn; |  | ||||
|                     iv->devControlRequest = true; |  | ||||
|                     break; |  | ||||
|                 case TurnOff: |  | ||||
|                     iv->devControlCmd = TurnOff; |  | ||||
|                     iv->devControlRequest = true; |  | ||||
|                     break; |  | ||||
|                 case CleanState_LockAndAlarm: |  | ||||
|                     iv->devControlCmd = CleanState_LockAndAlarm; |  | ||||
|                     iv->devControlRequest = true; |  | ||||
|                     break; |  | ||||
|                 case Restart: |  | ||||
|                     iv->devControlCmd = Restart; |  | ||||
|                     iv->devControlRequest = true; |  | ||||
|                     break; |  | ||||
|                 case ActivePowerContr: |  | ||||
|                     iv->devControlCmd = ActivePowerContr; |  | ||||
|                     iv->devControlRequest = true; |  | ||||
|                     iv->powerLimit[0] = payload[0]; |  | ||||
|                     iv->powerLimit[1] = payload[1]; |  | ||||
|                     break; |  | ||||
|                 default: |  | ||||
|                     jsonOut["error"] = "unknown 'cmd' = " + String(cmd); |  | ||||
|                     return false; |  | ||||
|             } |  | ||||
|         } else { |  | ||||
|             return false; |  | ||||
|         } |  | ||||
|     } |  | ||||
|     else { |  | ||||
|         jsonOut[F("error")] = F("unknown 'tx_request'"); |  | ||||
|         return false; |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     return true; |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| bool webApi::setSetup(JsonObject jsonIn, JsonObject jsonOut) { |  | ||||
|     if(F("scan_wifi") == jsonIn[F("cmd")]) |  | ||||
|         mApp->scanAvailNetworks(); |  | ||||
|     else if(F("set_time") == jsonIn[F("cmd")]) |  | ||||
|         mApp->setTimestamp(jsonIn[F("ts")]); |  | ||||
|     else if(F("sync_ntp") == jsonIn[F("cmd")]) |  | ||||
|         mApp->setTimestamp(0); // 0: update ntp flag
 |  | ||||
|     else if(F("serial_utc_offset") == jsonIn[F("cmd")]) |  | ||||
|         mTimezoneOffset = jsonIn[F("ts")]; |  | ||||
|     else if(F("discovery_cfg") == jsonIn[F("cmd")]) |  | ||||
|         mApp->mFlagSendDiscoveryConfig = true; // for homeassistant
 |  | ||||
|     else { |  | ||||
|         jsonOut[F("error")] = F("unknown cmd"); |  | ||||
|         return false; |  | ||||
|     } |  | ||||
| 
 |  | ||||
|     return true; |  | ||||
| } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| //-----------------------------------------------------------------------------
 |  | ||||
| Inverter<> *webApi::getInverter(JsonObject jsonIn, JsonObject jsonOut) { |  | ||||
|     uint8_t id = jsonIn[F("inverter")]; |  | ||||
|     Inverter<> *iv = mApp->mSys->getInverterByPos(id); |  | ||||
|     if(NULL == iv) |  | ||||
|         jsonOut[F("error")] = F("inverter index to high: ") + String(id); |  | ||||
|     return iv; |  | ||||
| } |  | ||||
| @ -1,82 +0,0 @@ | |||||
| #ifndef __WEB_API_H__ |  | ||||
| #define __WEB_API_H__ |  | ||||
| 
 |  | ||||
| #include "../utils/dbg.h" |  | ||||
| #ifdef ESP32 |  | ||||
|     #include "AsyncTCP.h" |  | ||||
| #else |  | ||||
|     #include "ESPAsyncTCP.h" |  | ||||
| #endif |  | ||||
| #include "ESPAsyncWebServer.h" |  | ||||
| #include "AsyncJson.h" |  | ||||
| #include "../app.h" |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| class app; |  | ||||
| 
 |  | ||||
| class webApi { |  | ||||
|     public:  |  | ||||
|         webApi(AsyncWebServer *srv, app *app, settings_t *config, statistics_t *stat, char version[]); |  | ||||
| 
 |  | ||||
|         void setup(void); |  | ||||
|         void loop(void); |  | ||||
| 
 |  | ||||
|         uint32_t getTimezoneOffset() { |  | ||||
|             return mTimezoneOffset; |  | ||||
|         } |  | ||||
| 
 |  | ||||
|     private: |  | ||||
|         void onApi(AsyncWebServerRequest *request); |  | ||||
|         void onApiPost(AsyncWebServerRequest *request); |  | ||||
|         void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); |  | ||||
|         void getNotFound(JsonObject obj, String url); |  | ||||
|         void onDwnldSetup(AsyncWebServerRequest *request); |  | ||||
| 
 |  | ||||
|         void getSysInfo(JsonObject obj); |  | ||||
|         void getHtmlSystem(JsonObject obj); |  | ||||
|         void getHtmlLogout(JsonObject obj); |  | ||||
|         void getHtmlSave(JsonObject obj); |  | ||||
|         void getReboot(JsonObject obj); |  | ||||
|         void getStatistics(JsonObject obj); |  | ||||
|         void getInverterList(JsonObject obj); |  | ||||
|         void getMqtt(JsonObject obj); |  | ||||
|         void getNtp(JsonObject obj); |  | ||||
|         void getSun(JsonObject obj); |  | ||||
|         void getPinout(JsonObject obj); |  | ||||
|         void getRadio(JsonObject obj); |  | ||||
|         void getSerial(JsonObject obj); |  | ||||
|         void getStaticIp(JsonObject obj); |  | ||||
| 
 |  | ||||
|         void getMenu(JsonObject obj); |  | ||||
|         void getIndex(JsonObject obj); |  | ||||
|         void getSetup(JsonObject obj); |  | ||||
|         void getNetworks(JsonObject obj); |  | ||||
|         void getLive(JsonObject obj); |  | ||||
|         void getRecord(JsonObject obj, record_t<> *rec); |  | ||||
| 
 |  | ||||
|         bool setCtrl(JsonObject jsonIn, JsonObject jsonOut); |  | ||||
|         bool setSetup(JsonObject jsonIn, JsonObject jsonOut); |  | ||||
| 
 |  | ||||
|         Inverter<> *getInverter(JsonObject jsonIn, JsonObject jsonOut); |  | ||||
| 
 |  | ||||
|         double round3(double value) { |  | ||||
|            return (int)(value * 1000 + 0.5) / 1000.0; |  | ||||
|         } |  | ||||
| 
 |  | ||||
|         String ip2String(uint8_t ip[]) { |  | ||||
|             char str[16]; |  | ||||
|             snprintf(str, 16, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); |  | ||||
|             return String(str); |  | ||||
|         } |  | ||||
| 
 |  | ||||
|         AsyncWebServer *mSrv; |  | ||||
|         app *mApp; |  | ||||
| 
 |  | ||||
|         settings_t *mConfig; |  | ||||
|         statistics_t *mStat; |  | ||||
|         char *mVersion; |  | ||||
| 
 |  | ||||
|         uint32_t mTimezoneOffset; |  | ||||
| }; |  | ||||
| 
 |  | ||||
| #endif /*__WEB_API_H__*/ |  | ||||
					Loading…
					
					
				
		Reference in new issue