diff --git a/.github/workflows/compile_zero-export.yml b/.github/workflows/compile_zero-export.yml new file mode 100644 index 00000000..a609ff09 --- /dev/null +++ b/.github/workflows/compile_zero-export.yml @@ -0,0 +1,179 @@ +name: Ahoy Zero-Export + +on: + push: + branches: zero-export + paths-ignore: + - '**.md' # Do no build on *.md changes + +jobs: + check: + name: Check Repository + runs-on: ubuntu-latest + if: github.repository == 'lumapu/ahoy' && github.ref_name == 'zero-export' + continue-on-error: true + steps: + - uses: actions/checkout@v4 + + build-en: + name: Build Environments (English) + needs: check + runs-on: ubuntu-latest + continue-on-error: true + strategy: + matrix: + variant: + - esp32-wroom32 + - esp32-wroom32-prometheus + - esp32-wroom32-ethernet + - esp32-s2-mini + - esp32-c3-mini + - opendtufusion + - opendtufusion-ethernet + steps: + - uses: actions/checkout@v4 + - uses: benjlevesque/short-sha@v3.0 + id: short-sha + with: + length: 7 + + - name: Cache Pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Cache PlatformIO + uses: actions/cache@v4 + with: + path: ~/.platformio + key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install PlatformIO + run: | + python -m pip install setuptools --upgrade pip + pip install --upgrade platformio + + - name: Run PlatformIO + run: pio run -d src -e ${{ matrix.variant }} + + - name: Rename Firmware + run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT + + - name: Create Artifact + uses: actions/upload-artifact@v4 + with: + name: zero-${{ matrix.variant }} + path: firmware/* + + build-de: + name: Build Environments (German) + needs: check + runs-on: ubuntu-latest + continue-on-error: true + strategy: + matrix: + variant: + - esp32-wroom32-de + - esp32-wroom32-prometheus-de + - esp32-wroom32-ethernet-de + - esp32-s2-mini-de + - esp32-c3-mini-de + - opendtufusion-de + - opendtufusion-ethernet-de + steps: + - uses: actions/checkout@v4 + - uses: benjlevesque/short-sha@v3.0 + id: short-sha + with: + length: 7 + + - name: Cache Pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Cache PlatformIO + uses: actions/cache@v4 + with: + path: ~/.platformio + key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install PlatformIO + run: | + python -m pip install setuptools --upgrade pip + pip install --upgrade platformio + + - name: Run PlatformIO + run: pio run -d src -e ${{ matrix.variant }} + + - name: Rename Firmware + run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT + + - name: Create Artifact + uses: actions/upload-artifact@v4 + with: + name: zero-${{ matrix.variant }} + path: firmware/* + + combine: + name: Combine Artifacts + needs: [build-en, build-de] + runs-on: ubuntu-latest + continue-on-error: false + steps: + - uses: actions/checkout@v4 + #- name: Copy boot_app0.bin + # run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin + + - name: Get Artifacts + uses: actions/download-artifact@v4 + with: + merge-multiple: true + path: firmware + + - name: Get Version from code + id: version_name + run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT + + - name: Set Version + uses: cschleiden/replace-tokens@v1 + with: + files: manual/User_Manual.md + env: + VERSION: ${{ steps.version_name.outputs.name }} + + - name: Rename firmware directory + run: mv firmware ${{ steps.version_name.outputs.name }} + + - name: Zip Artifacts + uses: vimtor/action-zip@v1.2 + with: + files: ${{ steps.version_name.outputs.name }} manual/User_Manual.md manual/Getting_Started.md + dest: '${{ steps.version_name.outputs.name }}.zip' + + - name: delete environment Artifacts + uses: geekyeggo/delete-artifact@v4 + with: + name: zero-* + + - name: Create Artifact + uses: actions/upload-artifact@v4 + with: + name: zero-${{ steps.version_name.outputs.name }} + path: '${{ steps.version_name.outputs.name }}.zip' diff --git a/ahoy.code-workspace b/ahoy.code-workspace index 20d5909e..a1baeb11 100644 --- a/ahoy.code-workspace +++ b/ahoy.code-workspace @@ -2,9 +2,6 @@ "folders": [ { "path": "." - }, - { - "path": "src" } ], "settings": { diff --git a/src/app.cpp b/src/app.cpp index 0bead49e..3803b48a 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -14,7 +14,7 @@ //----------------------------------------------------------------------------- app::app() : ah::Scheduler {} { - memset(mVersion, 0, sizeof(char) * 12); + memset(mVersion, 0, sizeof(char) * 17); memset(mVersionModules, 0, sizeof(char) * 12); } @@ -124,6 +124,11 @@ void app::setup() { mPubSerial.setup(mConfig, &mSys, &mTimestamp); + // ZeroExport + if (mConfig->plugin.zexport.enabled) { + mzExport.setup(&mConfig->plugin.zexport, &mSys, mConfig); + } + #if !defined(ETHERNET) //mImprov.setup(this, mConfig->sys.deviceName, mVersion); #endif @@ -191,6 +196,13 @@ void app::regularTickers(void) { if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type) everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp"); #endif + + // ZeroExport + #if defined(PLUGIN_ZEROEXPORT) + if (mConfig->plugin.zexport.enabled) + everySec(std::bind(&ZeroExportType::tickerSecond, &mzExport), "zExport"); + #endif + every(std::bind(&PubSerialType::tick, &mPubSerial), 5, "uart"); #if !defined(ETHERNET) //everySec([this]() { mImprov.tickSerial(); }, "impro"); @@ -445,6 +457,10 @@ void app::tickSend(void) { else mCommunication.add(iv, cmd); }); + + #if defined(ESP32) + if(mConfig->nrf.enabled || mConfig->cmt.enabled) zeroexport(); + #endif } } @@ -506,7 +522,7 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) { //----------------------------------------------------------------------------- void app::resetSystem(void) { - snprintf(mVersion, sizeof(mVersion), "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); + snprintf(mVersion, sizeof(mVersion), "zero-%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); snprintf(mVersionModules, sizeof(mVersionModules), "%s", #ifdef ENABLE_PROMETHEUS_EP "P" @@ -619,3 +635,47 @@ void app::updateLed(void) { analogWrite(mConfig->led.led[2], led_off); } } +//----------------------------------------------------------------------------- +#if defined(ESP32) +void app::zeroexport() { + if (!mConfig->plugin.zexport.enabled || + !mSys.getInverterByPos(mConfig->plugin.zexport.Iv)->isProducing()) { // check if plugin is enabled && indicate to send new value + mConfig->plugin.zexport.lastTime = millis(); // set last timestamp + return; + } + + if (millis() - mConfig->plugin.zexport.lastTime > mConfig->plugin.zexport.count_avg * 1000UL) + { + Inverter<> *iv = mSys.getInverterByPos(mConfig->plugin.zexport.Iv); + + DynamicJsonDocument doc(512); + JsonObject object = doc.to(); + + double nValue = round(mzExport.getPowertoSetnewValue()); + double twoPerVal = nValue <= (iv->getMaxPower() / 100 * 2 ); + + if(mConfig->plugin.zexport.two_percent && (nValue <= twoPerVal)) + nValue = twoPerVal; + + if(mConfig->plugin.zexport.max_power <= nValue) + nValue = mConfig->plugin.zexport.max_power; + + if(iv->actPowerLimit == nValue) { + mConfig->plugin.zexport.lastTime = millis(); // set last timestamp + return; // if PowerLimit same as befor, then skip + } + + object["val"] = nValue; + object["id"] = mConfig->plugin.zexport.Iv; + object["path"] = "ctrl"; + object["cmd"] = "limit_nonpersistent_absolute"; + + String data; + serializeJsonPretty(object, data); + DPRINTLN(DBG_INFO, data); + mApi.ctrlRequest(object); + + mConfig->plugin.zexport.lastTime = millis(); // set last timestamp + } +} +#endif diff --git a/src/app.h b/src/app.h index b16d7aeb..0f51eebe 100644 --- a/src/app.h +++ b/src/app.h @@ -77,6 +77,11 @@ typedef Simulator SimulatorType; typedef Display DisplayType; #endif +#if defined(PLUGIN_ZEROEXPORT) +#include "plugins/zeroExport/zeroExport.h" +typedef ZeroExport ZeroExportType; +#endif + class app : public IApp, public ah::Scheduler { public: app(); @@ -348,6 +353,10 @@ class app : public IApp, public ah::Scheduler { void setupLed(); void updateLed(); + #if defined(ESP32) + void zeroexport(); + #endif + void tickReboot(void) { DPRINTLN(DBG_INFO, F("Rebooting...")); ah::Scheduler::resetTicker(); @@ -414,7 +423,7 @@ class app : public IApp, public ah::Scheduler { CmtRadio<> mCmtRadio; #endif - char mVersion[12]; + char mVersion[17]; char mVersionModules[12]; settings mSettings; settings_t *mConfig = nullptr; @@ -450,6 +459,10 @@ class app : public IApp, public ah::Scheduler { #if defined(ENABLE_SIMULATOR) SimulatorType mSimulator; #endif /*ENABLE_SIMULATOR*/ + + #if defined(PLUGIN_ZEROEXPORT) + ZeroExportType mzExport; + #endif }; #endif /*__APP_H__*/ diff --git a/src/config/config.h b/src/config/config.h index 9e59f146..df8cec63 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -235,6 +235,9 @@ // default MQTT broker uri #define DEF_MQTT_BROKER "\0" +// default zero-export uri +#define DEF_ZEXPORT "\0" + // default MQTT port #define DEF_MQTT_PORT 1883 diff --git a/src/config/settings.h b/src/config/settings.h index 18725b48..0a462e06 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -139,6 +139,24 @@ typedef struct { uint16_t interval; } cfgMqtt_t; +/* Zero Export section */ +#if defined(ESP32) +typedef struct { + char monitor_url[ZEXPORT_ADDR_LEN]; + char json_path[ZEXPORT_ADDR_LEN]; + uint8_t query_device; // 0 - Tibber, 1 - Shelly, 2 - other (rs232?) + uint8_t Iv; // saves the inverter that is used for regulation + bool enabled; + float power_avg; + uint8_t count_avg; + double total_power; + unsigned long lastTime; // tic toc + double max_power; + bool two_percent; // ask if not go lower then 2% + char tibber_pw[10]; // needed for tibber QWGH-ED12 +} cfgzeroExport_t; +#endif + typedef struct { bool enabled; char name[MAX_NAME_LENGTH]; @@ -188,6 +206,9 @@ typedef struct { display_t display; char customLink[MAX_CUSTOM_LINK_LEN]; char customLinkText[MAX_CUSTOM_LINK_TEXT_LEN]; + #if defined(ESP32) + cfgzeroExport_t zexport; + #endif } plugins_t; typedef struct { @@ -291,6 +312,7 @@ class settings { if(root.containsKey(F("nrf"))) jsonNrf(root[F("nrf")]); #if defined(ESP32) if(root.containsKey(F("cmt"))) jsonCmt(root[F("cmt")]); + if(root.containsKey(F("zeroExport"))) jsonzeroExport(root[F("zeroExport")]); #endif if(root.containsKey(F("ntp"))) jsonNtp(root[F("ntp")]); if(root.containsKey(F("sun"))) jsonSun(root[F("sun")]); @@ -320,6 +342,7 @@ class settings { jsonNrf(root[F("nrf")].to(), true); #if defined(ESP32) jsonCmt(root[F("cmt")].to(), true); + jsonzeroExport(root.createNestedObject(F("zeroExport")), true); #endif jsonNtp(root[F("ntp")].to(), true); jsonSun(root[F("sun")].to(), true); @@ -329,6 +352,7 @@ class settings { jsonPlugin(root[F("plugin")].to(), true); jsonInst(root[F("inst")].to(), true); + DPRINT(DBG_INFO, F("memory usage: ")); DBGPRINTLN(String(json.memoryUsage())); DPRINT(DBG_INFO, F("capacity: ")); @@ -446,7 +470,22 @@ class settings { snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC); mCfg.mqtt.interval = 0; // off - mCfg.inst.sendInterval = SEND_INTERVAL; + // Zero-Export + #if defined(ESP32) + snprintf(mCfg.plugin.zexport.monitor_url, ZEXPORT_ADDR_LEN, "%s", DEF_ZEXPORT); + snprintf(mCfg.plugin.zexport.tibber_pw, ZEXPORT_ADDR_LEN, "%s", DEF_ZEXPORT); + snprintf(mCfg.plugin.zexport.json_path, ZEXPORT_ADDR_LEN, "%s", DEF_ZEXPORT); + mCfg.plugin.zexport.enabled = false; + mCfg.plugin.zexport.count_avg = 10; + mCfg.plugin.zexport.lastTime = millis(); // do not change! + + mCfg.plugin.zexport.query_device = 1; // Standard shelly + mCfg.plugin.zexport.power_avg = 10; + mCfg.plugin.zexport.Iv = 0; + mCfg.plugin.zexport.max_power = 600; // Max 600W to stay safe + mCfg.plugin.zexport.two_percent = true; + #endif + mCfg.inst.rstYieldMidNight = false; mCfg.inst.rstValsNotAvail = false; mCfg.inst.rstValsCommStop = false; @@ -694,6 +733,40 @@ class settings { } } + #if defined(ESP32) + void jsonzeroExport(JsonObject obj, bool set = false) { + if(set) { + obj[F("en_zeroexport")] = (bool) mCfg.plugin.zexport.enabled; + obj[F("monitor_url")] = mCfg.plugin.zexport.monitor_url; + obj[F("json_path")] = mCfg.plugin.zexport.json_path; + obj[F("Iv")] = mCfg.plugin.zexport.Iv; + obj[F("power_avg")] = mCfg.plugin.zexport.power_avg; + obj[F("query_device")] = mCfg.plugin.zexport.query_device; + obj[F("count_avg")] = mCfg.plugin.zexport.count_avg; + obj[F("max_power")] = mCfg.plugin.zexport.max_power; + obj[F("total_power")] = mCfg.plugin.zexport.total_power; + obj[F("two_percent")] = (bool)mCfg.plugin.zexport.two_percent; + } + else + { + getVal(obj, F("en_zeroexport"), &mCfg.plugin.zexport.enabled); + getVal(obj, F("two_percent"), &mCfg.plugin.zexport.two_percent); + + getChar(obj, F("monitor_url"), mCfg.plugin.zexport.monitor_url, ZEXPORT_ADDR_LEN); + getChar(obj, F("json_path"), mCfg.plugin.zexport.json_path, ZEXPORT_ADDR_LEN); + + getVal(obj, F("Iv"), &mCfg.plugin.zexport.Iv); + getVal(obj, F("count_avg"), &mCfg.plugin.zexport.count_avg); + getVal(obj, F("max_power"), &mCfg.plugin.zexport.max_power); + + getVal(obj, F("power_avg"), &mCfg.plugin.zexport.power_avg); + getVal(obj, F("query_device"), &mCfg.plugin.zexport.query_device); + + getVal(obj, F("total_power"), &mCfg.plugin.zexport.total_power); + } + } + #endif + void jsonLed(JsonObject obj, bool set = false) { if(set) { obj[F("0")] = mCfg.led.led[0]; diff --git a/src/defines.h b/src/defines.h index edcde7a0..b585fcba 100644 --- a/src/defines.h +++ b/src/defines.h @@ -98,6 +98,8 @@ enum { #define DEVNAME_LEN 16 #define NTP_ADDR_LEN 32 // DNS Name +#define ZEXPORT_ADDR_LEN 100 // Zero-Export Address + #define MQTT_ADDR_LEN 64 // DNS Name #define MQTT_CLIENTID_LEN 22 // number of chars is limited to 23 up to v3.1 of MQTT #define MQTT_USER_LEN 65 // there is another byte necessary for \0 @@ -106,6 +108,7 @@ enum { #define MQTT_MAX_PACKET_SIZE 384 +#define PLUGIN_ZEROEXPORT typedef struct { uint32_t rxFail; diff --git a/src/plugins/zeroExport/SML.cpp b/src/plugins/zeroExport/SML.cpp new file mode 100644 index 00000000..7a378f63 --- /dev/null +++ b/src/plugins/zeroExport/SML.cpp @@ -0,0 +1,405 @@ +#include +#include + +#include "sml.h" +#include "smlCrcTable.h" + +#ifdef SML_DEBUG +char logBuff[200]; + +#ifdef SML_NATIVE +#define SML_LOG(...) \ + do { \ + printf(__VA_ARGS__); \ + } while (0) +#define SML_TREELOG(level, ...) \ + do { \ + printf("%.*s", level, " "); \ + printf(__VA_ARGS__); \ + } while (0) +#elif ARDUINO +#include +#define SML_LOG(...) \ + do { \ + sprintf(logBuff, __VA_ARGS__); \ + Serial.print(logBuff); \ + } while (0) +#define SML_TREELOG(level, ...) \ + do { \ + sprintf(logBuff, __VA_ARGS__); \ + Serial.print(logBuff); \ + } while (0) +#endif + +#else +#define SML_LOG(...) \ + do { \ + } while (0) +#define SML_TREELOG(level, ...) \ + do { \ + } while (0) +#endif + +#define MAX_LIST_SIZE 80 +#define MAX_TREE_SIZE 10 + +static sml_states_t currentState = SML_START; +static char nodes[MAX_TREE_SIZE]; +static unsigned char currentLevel = 0; +static unsigned short crc = 0xFFFF; +static signed char sc; +static unsigned short crcMine = 0xFFFF; +static unsigned short crcReceived = 0x0000; +static unsigned char len = 4; +static unsigned char listBuffer[MAX_LIST_SIZE]; /* keeps a list + as length + state + data */ +static unsigned char listPos = 0; + +void crc16(unsigned char &byte) +{ +#ifdef ARDUINO + crc = + pgm_read_word_near(&smlCrcTable[(byte ^ crc) & 0xff]) ^ (crc >> 8 & 0xff); +#else + crc = smlCrcTable[(byte ^ crc) & 0xff] ^ (crc >> 8 & 0xff); +#endif +} + +void setState(sml_states_t state, int byteLen) +{ + currentState = state; + len = byteLen; +} + +void pushListBuffer(unsigned char byte) +{ + if (listPos < MAX_LIST_SIZE) { + listBuffer[listPos++] = byte; + } +} + +void reduceList() +{ + if (currentLevel <= MAX_TREE_SIZE && nodes[currentLevel] > 0) + nodes[currentLevel]--; +} + +void smlNewList(unsigned char size) +{ + reduceList(); + if (currentLevel < MAX_TREE_SIZE) + currentLevel++; + nodes[currentLevel] = size; + SML_TREELOG(currentLevel, "LISTSTART on level %i with %i nodes\n", + currentLevel, size); + setState(SML_LISTSTART, size); + // @todo workaround for lists inside obis lists + if (size > 5) { + listPos = 0; + memset(listBuffer, '\0', MAX_LIST_SIZE); + } + else { + pushListBuffer(size); + pushListBuffer(currentState); + } +} + +void checkMagicByte(unsigned char &byte) +{ + unsigned int size = 0; + while (currentLevel > 0 && nodes[currentLevel] == 0) { + /* go back in tree if no nodes remaining */ + SML_TREELOG(currentLevel, "back to previous list\n"); + currentLevel--; + } + if (byte > 0x70 && byte <= 0x7F) { + /* new list */ + size = byte & 0x0F; + smlNewList(size); + } + else if (byte >= 0x01 && byte <= 0x6F && nodes[currentLevel] > 0) { + if (byte == 0x01) { + /* no data, get next */ + SML_TREELOG(currentLevel, " Data %i (empty)\n", nodes[currentLevel]); + pushListBuffer(0); + pushListBuffer(currentState); + if (nodes[currentLevel] == 1) { + setState(SML_LISTEND, 1); + SML_TREELOG(currentLevel, "LISTEND\n"); + } + else { + setState(SML_NEXT, 1); + } + } + else { + size = (byte & 0x0F) - 1; + setState(SML_DATA, size); + if ((byte & 0xF0) == 0x50) { + setState(SML_DATA_SIGNED_INT, size); + } + else if ((byte & 0xF0) == 0x60) { + setState(SML_DATA_UNSIGNED_INT, size); + } + else if ((byte & 0xF0) == 0x00) { + setState(SML_DATA_OCTET_STRING, size); + } + SML_TREELOG(currentLevel, + " Data %i (length = %i%s): ", nodes[currentLevel], size, + (currentState == SML_DATA_SIGNED_INT) ? ", signed int" + : (currentState == SML_DATA_UNSIGNED_INT) ? ", unsigned int" + : (currentState == SML_DATA_OCTET_STRING) ? ", octet string" + : ""); + pushListBuffer(size); + pushListBuffer(currentState); + } + reduceList(); + } + else if (byte == 0x00) { + /* end of block */ + reduceList(); + SML_TREELOG(currentLevel, "End of block at level %i\n", currentLevel); + if (currentLevel == 0) { + setState(SML_NEXT, 1); + } + else { + setState(SML_BLOCKEND, 1); + } + } + else if (byte & 0x80) { + // MSB bit is set, another TL byte will follow + if (byte >= 0x80 && byte <= 0x8F) { + // Datatype Octet String + setState(SML_HDATA, (byte & 0x0F) << 4); + } + else if (byte >= 0xF0 /*&& byte <= 0xFF*/) { + /* Datatype List of ...*/ + setState(SML_LISTEXTENDED, (byte & 0x0F) << 4); + } + } + else if (byte == 0x1B && currentLevel == 0) { + /* end sequence */ + setState(SML_END, 3); + } + else { + /* Unexpected Byte */ + SML_TREELOG(currentLevel, + "UNEXPECTED magicbyte >%02X< at currentLevel %i\n", byte, + currentLevel); + setState(SML_UNEXPECTED, 4); + } +} + +sml_states_t smlState(unsigned char ¤tByte) +{ + unsigned char size; + if (len > 0) + len--; + crc16(currentByte); + switch (currentState) { + case SML_UNEXPECTED: + case SML_CHECKSUM_ERROR: + case SML_FINAL: + case SML_START: + currentState = SML_START; + currentLevel = 0; // Reset current level at the begin of a new transmission + // to prevent problems + if (currentByte != 0x1b) + setState(SML_UNEXPECTED, 4); + if (len == 0) { + SML_TREELOG(0, "START\n"); + /* completely clean any garbage from crc checksum */ + crc = 0xFFFF; + currentByte = 0x1b; + crc16(currentByte); + crc16(currentByte); + crc16(currentByte); + crc16(currentByte); + setState(SML_VERSION, 4); + } + break; + case SML_VERSION: + if (currentByte != 0x01) + setState(SML_UNEXPECTED, 4); + if (len == 0) { + setState(SML_BLOCKSTART, 1); + } + break; + case SML_END: + if (currentByte != 0x1b) { + SML_LOG("UNEXPECTED char >%02X< at SML_END\n", currentByte); + setState(SML_UNEXPECTED, 4); + } + if (len == 0) { + setState(SML_CHECKSUM, 4); + } + break; + case SML_CHECKSUM: + // SML_LOG("CHECK: %02X\n", currentByte); + if (len == 2) { + crcMine = crc ^ 0xFFFF; + } + if (len == 1) { + crcReceived += currentByte; + } + if (len == 0) { + crcReceived = crcReceived | (currentByte << 8); + SML_LOG("Received checksum: %02X\n", crcReceived); + SML_LOG("Calculated checksum: %02X\n", crcMine); + if (crcMine == crcReceived) { + setState(SML_FINAL, 4); + } + else { + setState(SML_CHECKSUM_ERROR, 4); + } + crc = 0xFFFF; + crcReceived = 0x000; /* reset CRC */ + } + break; + case SML_HDATA: + size = len + currentByte - 1; + setState(SML_DATA, size); + pushListBuffer(size); + pushListBuffer(currentState); + SML_TREELOG(currentLevel, " Data (length = %i): ", size); + break; + case SML_LISTEXTENDED: + size = len + (currentByte & 0x0F); + SML_TREELOG(currentLevel, "Extended List with Size=%i\n", size); + smlNewList(size); + break; + case SML_DATA: + case SML_DATA_SIGNED_INT: + case SML_DATA_UNSIGNED_INT: + case SML_DATA_OCTET_STRING: + SML_LOG("%02X ", currentByte); + pushListBuffer(currentByte); + if (nodes[currentLevel] == 0 && len == 0) { + SML_LOG("\n"); + SML_TREELOG(currentLevel, "LISTEND on level %i\n", currentLevel); + currentState = SML_LISTEND; + } + else if (len == 0) { + currentState = SML_DATAEND; + SML_LOG("\n"); + } + break; + case SML_DATAEND: + case SML_NEXT: + case SML_LISTSTART: + case SML_LISTEND: + case SML_BLOCKSTART: + case SML_BLOCKEND: + checkMagicByte(currentByte); + break; + } + return currentState; +} + +bool smlOBISCheck(const unsigned char *obis) +{ + return (memcmp(obis, &listBuffer[2], 6) == 0); +} + +void smlOBISManufacturer(unsigned char *str, int maxSize) +{ + int i = 0, pos = 0, size = 0; + while (i < listPos) { + size = (int)listBuffer[i]; + i++; + pos++; + if (pos == 6) { + /* get manufacturer at position 6 in list */ + size = (size > maxSize - 1) ? maxSize : size; + memcpy(str, &listBuffer[i + 1], size); + str[size + 1] = 0; + } + i += size + 1; + } +} + +void smlPow(double &val, signed char &scaler) +{ + if (scaler < 0) { + while (scaler++) { + val /= 10; + } + } + else { + while (scaler--) { + val *= 10; + } + } +} + +void smlOBISByUnit(long long int &val, signed char &scaler, sml_units_t unit) +{ + unsigned char i = 0, pos = 0, size = 0, y = 0, skip = 0; + sml_states_t type; + val = -1; /* unknown or error */ + while (i < listPos) { + pos++; + size = (int)listBuffer[i++]; + type = (sml_states_t)listBuffer[i++]; + if (type == SML_LISTSTART && size > 0) { + // skip a list inside an obis list + skip = size; + while (skip > 0) { + size = (int)listBuffer[i++]; + type = (sml_states_t)listBuffer[i++]; + i += size; + skip--; + } + size = 0; + } + if (pos == 4 && listBuffer[i] != unit) { + /* return unknown (-1) if unit does not match */ + return; + } + if (pos == 5) { + scaler = listBuffer[i]; + } + if (pos == 6) { + y = size; + // initialize 64bit signed integer based on MSB from received value + val = + (type == SML_DATA_SIGNED_INT && (listBuffer[i] & (1 << 7))) ? ~0 : 0; + for (y = 0; y < size; y++) { + // left shift received bytes to 64 bit signed integer + val = (val << 8) | listBuffer[i + y]; + } + } + i += size; + } +} + +void smlOBISWh(double &wh) +{ + long long int val; + smlOBISByUnit(val, sc, SML_WATT_HOUR); + wh = val; + smlPow(wh, sc); +} + +void smlOBISW(double &w) +{ + long long int val; + smlOBISByUnit(val, sc, SML_WATT); + w = val; + smlPow(w, sc); +} + +void smlOBISVolt(double &v) +{ + long long int val; + smlOBISByUnit(val, sc, SML_VOLT); + v = val; + smlPow(v, sc); +} + +void smlOBISAmpere(double &a) +{ + long long int val; + smlOBISByUnit(val, sc, SML_AMPERE); + a = val; + smlPow(a, sc); +} diff --git a/src/plugins/zeroExport/SML.h b/src/plugins/zeroExport/SML.h new file mode 100644 index 00000000..ac6405df --- /dev/null +++ b/src/plugins/zeroExport/SML.h @@ -0,0 +1,106 @@ +#ifndef SML_H +#define SML_H + +#include + +typedef enum { + SML_START, + SML_END, + SML_VERSION, + SML_NEXT, + SML_LISTSTART, + SML_LISTEND, + SML_LISTEXTENDED, + SML_DATA, + SML_HDATA, + SML_DATAEND, + SML_BLOCKSTART, + SML_BLOCKEND, + SML_CHECKSUM, + SML_CHECKSUM_ERROR, /* calculated checksum does not match */ + SML_UNEXPECTED, /* unexpected byte received */ + SML_FINAL, /* final state, checksum OK */ + SML_DATA_SIGNED_INT, + SML_DATA_UNSIGNED_INT, + SML_DATA_OCTET_STRING, +} sml_states_t; + +typedef enum { + SML_YEAR = 1, + SML_MONTH = 2, + SML_WEEK = 3, + SML_DAY = 4, + SML_HOUR = 5, + SML_MIN = 6, + SML_SECOND = 7, + SML_DEGREE = 8, + SML_DEGREE_CELSIUS = 9, + SML_CURRENCY = 10, + SML_METRE = 11, + SML_METRE_PER_SECOND = 12, + SML_CUBIC_METRE = 13, + SML_CUBIC_METRE_CORRECTED = 14, + SML_CUBIC_METRE_PER_HOUR = 15, + SML_CUBIC_METRE_PER_HOUR_CORRECTED = 16, + SML_CUBIC_METRE_PER_DAY = 17, + SML_CUBIC_METRE_PER_DAY_CORRECTED = 18, + SML_LITRE = 19, + SML_KILOGRAM = 20, + SML_NEWTON = 21, + SML_NEWTONMETER = 22, + SML_PASCAL = 23, + SML_BAR = 24, + SML_JOULE = 25, + SML_JOULE_PER_HOUR = 26, + SML_WATT = 27, + SML_VOLT_AMPERE = 28, + SML_VAR = 29, + SML_WATT_HOUR = 30, + SML_VOLT_AMPERE_HOUR = 31, + SML_VAR_HOUR = 32, + SML_AMPERE = 33, + SML_COULOMB = 34, + SML_VOLT = 35, + SML_VOLT_PER_METRE = 36, + SML_FARAD = 37, + SML_OHM = 38, + SML_OHM_METRE = 39, + SML_WEBER = 40, + SML_TESLA = 41, + SML_AMPERE_PER_METRE = 42, + SML_HENRY = 43, + SML_HERTZ = 44, + SML_ACTIVE_ENERGY_METER_CONSTANT_OR_PULSE_VALUE = 45, + SML_REACTIVE_ENERGY_METER_CONSTANT_OR_PULSE_VALUE = 46, + SML_APPARENT_ENERGY_METER_CONSTANT_OR_PULSE_VALUE = 47, + SML_VOLT_SQUARED_HOURS = 48, + SML_AMPERE_SQUARED_HOURS = 49, + SML_KILOGRAM_PER_SECOND = 50, + SML_KELVIN = 52, + SML_VOLT_SQUARED_HOUR_METER_CONSTANT_OR_PULSE_VALUE = 53, + SML_AMPERE_SQUARED_HOUR_METER_CONSTANT_OR_PULSE_VALUE = 54, + SML_METER_CONSTANT_OR_PULSE_VALUE = 55, + SML_PERCENTAGE = 56, + SML_AMPERE_HOUR = 57, + SML_ENERGY_PER_VOLUME = 60, + SML_CALORIFIC_VALUE = 61, + SML_MOLE_PERCENT = 62, + SML_MASS_DENSITY = 63, + SML_PASCAL_SECOND = 64, + SML_RESERVED = 253, + SML_OTHER_UNIT = 254, + SML_COUNT = 255 +} sml_units_t; + +sml_states_t smlState(unsigned char &byte); +bool smlOBISCheck(const unsigned char *obis); +void smlOBISManufacturer(unsigned char *str, int maxSize); +void smlOBISByUnit(long long int &wh, signed char &scaler, sml_units_t unit); + +// Be aware that double on Arduino UNO is just 32 bit +void smlOBISWh(double &wh); +void smlOBISW(double &w); +void smlOBISVolt(double &v); +void smlOBISAmpere(double &a); + +#endif diff --git a/src/plugins/zeroExport/SMLCRCTable.h b/src/plugins/zeroExport/SMLCRCTable.h new file mode 100644 index 00000000..9aee2987 --- /dev/null +++ b/src/plugins/zeroExport/SMLCRCTable.h @@ -0,0 +1,50 @@ +#ifndef SML_CRC_TABLE_H +#define SML_CRC_TABLE_H + +#include + +#ifdef ARDUINO +#include + + +/* + * This mysterious table is just the CRC of each possible byte. It can be + * computed using the standard bit-at-a-time methods. The polynomial can + * be seen in entry 128, 0x8408. This corresponds to x^0 + x^5 + x^12. + * Add the implicit x^16, and you have the standard CRC-CCITT. + */ +static const uint16_t smlCrcTable[256] PROGMEM = +#else +static const uint16_t smlCrcTable[256] = +#endif + {0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF, 0x8C48, + 0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7, 0x1081, 0x0108, + 0x3393, 0x221A, 0x56A5, 0x472C, 0x75B7, 0x643E, 0x9CC9, 0x8D40, 0xBFDB, + 0xAE52, 0xDAED, 0xCB64, 0xF9FF, 0xE876, 0x2102, 0x308B, 0x0210, 0x1399, + 0x6726, 0x76AF, 0x4434, 0x55BD, 0xAD4A, 0xBCC3, 0x8E58, 0x9FD1, 0xEB6E, + 0xFAE7, 0xC87C, 0xD9F5, 0x3183, 0x200A, 0x1291, 0x0318, 0x77A7, 0x662E, + 0x54B5, 0x453C, 0xBDCB, 0xAC42, 0x9ED9, 0x8F50, 0xFBEF, 0xEA66, 0xD8FD, + 0xC974, 0x4204, 0x538D, 0x6116, 0x709F, 0x0420, 0x15A9, 0x2732, 0x36BB, + 0xCE4C, 0xDFC5, 0xED5E, 0xFCD7, 0x8868, 0x99E1, 0xAB7A, 0xBAF3, 0x5285, + 0x430C, 0x7197, 0x601E, 0x14A1, 0x0528, 0x37B3, 0x263A, 0xDECD, 0xCF44, + 0xFDDF, 0xEC56, 0x98E9, 0x8960, 0xBBFB, 0xAA72, 0x6306, 0x728F, 0x4014, + 0x519D, 0x2522, 0x34AB, 0x0630, 0x17B9, 0xEF4E, 0xFEC7, 0xCC5C, 0xDDD5, + 0xA96A, 0xB8E3, 0x8A78, 0x9BF1, 0x7387, 0x620E, 0x5095, 0x411C, 0x35A3, + 0x242A, 0x16B1, 0x0738, 0xFFCF, 0xEE46, 0xDCDD, 0xCD54, 0xB9EB, 0xA862, + 0x9AF9, 0x8B70, 0x8408, 0x9581, 0xA71A, 0xB693, 0xC22C, 0xD3A5, 0xE13E, + 0xF0B7, 0x0840, 0x19C9, 0x2B52, 0x3ADB, 0x4E64, 0x5FED, 0x6D76, 0x7CFF, + 0x9489, 0x8500, 0xB79B, 0xA612, 0xD2AD, 0xC324, 0xF1BF, 0xE036, 0x18C1, + 0x0948, 0x3BD3, 0x2A5A, 0x5EE5, 0x4F6C, 0x7DF7, 0x6C7E, 0xA50A, 0xB483, + 0x8618, 0x9791, 0xE32E, 0xF2A7, 0xC03C, 0xD1B5, 0x2942, 0x38CB, 0x0A50, + 0x1BD9, 0x6F66, 0x7EEF, 0x4C74, 0x5DFD, 0xB58B, 0xA402, 0x9699, 0x8710, + 0xF3AF, 0xE226, 0xD0BD, 0xC134, 0x39C3, 0x284A, 0x1AD1, 0x0B58, 0x7FE7, + 0x6E6E, 0x5CF5, 0x4D7C, 0xC60C, 0xD785, 0xE51E, 0xF497, 0x8028, 0x91A1, + 0xA33A, 0xB2B3, 0x4A44, 0x5BCD, 0x6956, 0x78DF, 0x0C60, 0x1DE9, 0x2F72, + 0x3EFB, 0xD68D, 0xC704, 0xF59F, 0xE416, 0x90A9, 0x8120, 0xB3BB, 0xA232, + 0x5AC5, 0x4B4C, 0x79D7, 0x685E, 0x1CE1, 0x0D68, 0x3FF3, 0x2E7A, 0xE70E, + 0xF687, 0xC41C, 0xD595, 0xA12A, 0xB0A3, 0x8238, 0x93B1, 0x6B46, 0x7ACF, + 0x4854, 0x59DD, 0x2D62, 0x3CEB, 0x0E70, 0x1FF9, 0xF78F, 0xE606, 0xD49D, + 0xC514, 0xB1AB, 0xA022, 0x92B9, 0x8330, 0x7BC7, 0x6A4E, 0x58D5, 0x495C, + 0x3DE3, 0x2C6A, 0x1EF1, 0x0F78}; + +#endif \ No newline at end of file diff --git a/src/plugins/zeroExport/zeroExport.cpp b/src/plugins/zeroExport/zeroExport.cpp new file mode 100644 index 00000000..e69de29b diff --git a/src/plugins/zeroExport/zeroExport.h b/src/plugins/zeroExport/zeroExport.h new file mode 100644 index 00000000..192b8d63 --- /dev/null +++ b/src/plugins/zeroExport/zeroExport.h @@ -0,0 +1,122 @@ +#if defined(ESP32) + +#ifndef __ZEROEXPORT__ +#define __ZEROEXPORT__ + +#include +#include +#include "AsyncJson.h" + +#include "SML.h" +template + +class ZeroExport { + public: + ZeroExport() { } + + void setup(cfgzeroExport_t *cfg, HMSYSTEM *sys, settings_t *config) { + mCfg = cfg; + mSys = sys; + mConfig = config; + } + + void tickerSecond() { + //DPRINTLN(DBG_INFO, (F("tickerSecond()"))); + if (millis() - mCfg->lastTime < mCfg->count_avg * 1000UL) { + zero(); // just refresh when it is needed. To get cpu load low. + } + } + + // Sums up the power values ​​of all phases and returns them. + // If the value is negative, all power values ​​from the inverter are taken into account + double getPowertoSetnewValue() + { + float ivPower = 0; + Inverter<> *iv; + record_t<> *rec; + for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { + iv = mSys->getInverterByPos(i); + rec = iv->getRecordStruct(RealTimeRunData_Debug); + if (iv == NULL) + continue; + ivPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); + } + + return ((unsigned)(mCfg->total_power - mCfg->power_avg) >= mCfg->power_avg) ? ivPower + mCfg->total_power : ivPower - mCfg->total_power; + } + //C2T2-B91B + private: + HTTPClient httpClient; + + // TODO: Need to improve here. 2048 for a JSON Obj is to big!? + bool zero() + { + httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + httpClient.setUserAgent("Ahoy-Agent"); + httpClient.setConnectTimeout(1000); + httpClient.setTimeout(1000); + httpClient.addHeader("Content-Type", "application/json"); + httpClient.addHeader("Accept", "application/json"); + + if (!httpClient.begin(mCfg->monitor_url)) { + DPRINTLN(DBG_INFO, "httpClient.begin failed"); + httpClient.end(); + return false; + } + + if(String(mCfg->monitor_url).endsWith("data.json?node_id=1")){ + httpClient.setAuthorization("admin", mCfg->tibber_pw); + } + + int httpCode = httpClient.GET(); + if (httpCode == HTTP_CODE_OK) + { + String responseBody = httpClient.getString(); + DynamicJsonDocument json(2048); + DeserializationError err = deserializeJson(json, responseBody); + + // Parse succeeded? + if (err) { + DPRINTLN(DBG_INFO, (F("ZeroExport() JSON error returned: "))); + DPRINTLN(DBG_INFO, String(err.f_str())); + } + + // check if it HICHI + if(json.containsKey(F("StatusSNS")) ) { + int index = responseBody.indexOf(String(mCfg->json_path)); // find first match position + responseBody = responseBody.substring(index); // cut it and store it in value + index = responseBody.indexOf(","); // find the first seperation - Bingo!? + + mCfg->total_power = responseBody.substring(responseBody.indexOf(":"), index).toDouble(); + } else if(json.containsKey(F("emeters"))) { + mCfg->total_power = (double)json[F("total_power")]; + } else if(String(mCfg->monitor_url).endsWith("data.json?node_id=1") ) { + tibber_parse(); + } else { + DPRINTLN(DBG_INFO, (F("ZeroExport() json error: cant find value in this query: ") + responseBody)); + return false; + } + } + else + { + DPRINTLN(DBG_INFO, F("ZeroExport(): Error ") + String(httpCode)); + return false; + } + httpClient.end(); + return true; + } + + void tibber_parse() + { + + } + + // private member variables + cfgzeroExport_t *mCfg; + settings_t *mConfig; + HMSYSTEM *mSys; +}; + +#endif /*__ZEROEXPORT__*/ + +#endif /* #if defined(ESP32) */ \ No newline at end of file diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 09bba06e..9886d15f 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -615,6 +615,22 @@ class RestApi { obj[F("interval")] = String(mConfig->mqtt.interval); } + #if defined(ESP32) + void getzeroExport(JsonObject obj) { + obj[F("en_zeroexport")] = (bool) mConfig->plugin.zexport.enabled; + obj[F("two_percent")] = (bool) mConfig->plugin.zexport.two_percent; + obj[F("monitor_url")] = String(mConfig->plugin.zexport.monitor_url); + obj[F("json_path")] = String(mConfig->plugin.zexport.json_path); + obj[F("count_avg")] = (uint8_t)mConfig->plugin.zexport.count_avg; + obj[F("max_power")] = (double)mConfig->plugin.zexport.max_power; + obj[F("Iv")] = (uint8_t)mConfig->plugin.zexport.Iv; + obj[F("power_avg")] = (float)mConfig->plugin.zexport.power_avg; + obj[F("query_device")] = (float)mConfig->plugin.zexport.query_device; + obj[F("total_power")] = (double)mConfig->plugin.zexport.total_power; + //obj[F("device")] = (uint8_t)mCfg.plugin.zexport.device; + } + #endif + void getNtp(JsonObject obj) { obj[F("addr")] = String(mConfig->ntp.addr); obj[F("port")] = String(mConfig->ntp.port); @@ -775,6 +791,10 @@ class RestApi { getSerial(obj.createNestedObject(F("serial"))); getStaticIp(obj.createNestedObject(F("static_ip"))); getDisplay(obj.createNestedObject(F("display"))); + + #if defined(ESP32) + getzeroExport(obj.createNestedObject(F("zeroExport"))); + #endif } #if !defined(ETHERNET) diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 47d935b9..864e2ff4 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -164,6 +164,7 @@ +
@@ -218,6 +219,7 @@
+
@@ -306,6 +308,51 @@
+ + +
+
+ Zero Export +
+ +
+
Enable zero export
+
+

Please select your favorite query interface:

+
+ +
+
Monitor IP:
+ + + + + + + +
+ A JSON-Format is required to work properly.
+ HICHI: http://IP_Address/cm?cmnd=status%208
+ +
Prio Inverter
+
Which Inverter should be regulated.
+ +
JSON Path:
+
Only for HICHI needed!
+ +
2% protection:
+
+
+
Max Power:
+
+
+
Refresh rate (sec.)
+
Power tolerances (Watt)
+
+

Total: n/a

+
+
+
{#BTN_REBOOT_SUCCESSFUL_SAVE}
@@ -1157,6 +1204,56 @@ document.getElementById("date").innerHTML = toIsoDateStr((new Date((++ts) * 1000))); } + function parsezeroExport(obj, type, ) { + if ("ESP8266" == type) { + var e = document.getElementById("zeroExport"); + e.remove(); + + var e = document.getElementById("zeroExport_button"); + e.textContent += " (only for ESP32 available)"; + e.disabled = true; + element.classList.add("disabled"); + + return; + } + + document.getElementsByName("en_zeroexport")[0].checked = obj["en_zeroexport"]; + document.getElementsByName("two_percent")[0].checked = obj["two_percent"]; + + document.getElementsByName("dev_Tibber")[0].checked = (obj["query_device"] == 1); + document.getElementsByName("dev_Shelly")[0].checked = (obj["query_device"] == 2); + document.getElementsByName("dev_Other")[0].checked = (obj["query_device"] == 3); + + + getAjax("/api/inverter/list", parseZeroIv); + + for(var i of [["monitor_url", "monitor_url"], ["power_avg", "power_avg"], ["count_avg", "count_avg"], ["json_path", "json_path"], ["max_power", "max_power"], ["query_device", "query_device"]]) + if(null != obj[i[1]]) + document.getElementsByName(i[0])[0].value = obj[i[1]]; + + document.getElementsByName("total_power")[0].innerHTML = "Total: " + obj["total_power"].toFixed(2) + "W"; + document.getElementById("Inv_ID").selectedIndex = obj["Iv"]; + } + + function parseZeroIv(root) + { + for(var i = 0; i < root.inverter.length; i++) + root.inverter[i]; + + select = document.getElementById('Inv_ID'); + parseInt(select.value) + + if(null == root) return; + root = root.inverter; + for(var i = 0; i < root.length; i++) { + inv = root[i]; + var opt = document.createElement('option'); + opt.value = inv.id; + opt.innerHTML = inv.name; + select.appendChild(opt); + } + } + function parse(root) { if(null != root) { parseGeneric(root["generic"]); @@ -1167,11 +1264,15 @@ parseSun(root["sun"]); parsePinout(root["pinout"], root["system"]["esp_type"], root["system"]); parseNrfRadio(root["radioNrf"], root["pinout"], root["system"]["esp_type"], root["system"]); + /*IF_ESP32*/ parseCmtRadio(root["radioCmt"], root["system"]["esp_type"], root["system"]); /*ENDIF_ESP32*/ + parsezeroExport(root["zeroExport"], root["system"]["esp_type"]); + parseSerial(root["serial"]); parseDisplay(root["display"], root["system"]["esp_type"], root["system"]); + getAjax("/api/inverter/list", parseIv); } } diff --git a/src/web/html/style.css b/src/web/html/style.css index 2d6a03c7..880ce759 100644 --- a/src/web/html/style.css +++ b/src/web/html/style.css @@ -470,6 +470,10 @@ p.lic, p.lic a { color: #fff; } +.disabled { + background-color: dimgray; +} + .s_content { display: none; overflow: hidden; diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index 81962add..f7d10ffa 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -11,6 +11,8 @@

{#EVERY} {#UPDATE_SECS}

+
+

Every seconds the values are updated

{#HTML_FOOTER} diff --git a/src/web/web.h b/src/web/web.h index 8495ba23..2e21ec58 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -547,6 +547,39 @@ class Web { mConfig->mqtt.port = request->arg("mqttPort").toInt(); mConfig->mqtt.interval = request->arg("mqttInterval").toInt(); + // zero-export + #if defined(ESP32) + mConfig->plugin.zexport.enabled = (request->arg("en_zeroexport") == "on"); + mConfig->plugin.zexport.two_percent = (request->arg("two_percent") == "on"); + mConfig->plugin.zexport.Iv = request->arg("Iv").toInt(); + mConfig->plugin.zexport.count_avg = request->arg("count_avg").toInt(); + mConfig->plugin.zexport.max_power = request->arg("max_power").toDouble(); + mConfig->plugin.zexport.power_avg = request->arg("power_avg").toFloat(); + mConfig->plugin.zexport.query_device = request->arg("query_device").toInt(); + mConfig->plugin.zexport.total_power = request->arg("total_power").toDouble(); + + if (request->arg("monitor_url") != "") { + String addr = request->arg("monitor_url"); + addr.trim(); + addr.toCharArray(mConfig->plugin.zexport.monitor_url, ZEXPORT_ADDR_LEN); + } else + mConfig->plugin.zexport.monitor_url[0] = '\0'; + + if (request->arg("json_path") != "") { + String addr = request->arg("json_path"); + addr.trim(); + addr.toCharArray(mConfig->plugin.zexport.json_path, ZEXPORT_ADDR_LEN); + } else + mConfig->plugin.zexport.json_path[0] = '\0'; + + if (request->arg("tibber_pw") != "") { + String addr = request->arg("tibber_pw"); + addr.trim(); + addr.toCharArray(mConfig->plugin.zexport.tibber_pw, 10); + } else + mConfig->plugin.zexport.tibber_pw[0] = '\0'; + #endif + // serial console mConfig->serial.debug = (request->arg("serDbg") == "on"); mConfig->serial.privacyLog = (request->arg("priv") == "on");