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