diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..9fcb3325
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+patches/GxEPD2_HAL.patch eol=lf
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/report.yaml b/.github/ISSUE_TEMPLATE/report.yaml
index 9a2824c4..99a7a7a5 100644
--- a/.github/ISSUE_TEMPLATE/report.yaml
+++ b/.github/ISSUE_TEMPLATE/report.yaml
@@ -1,9 +1,6 @@
-name: "AhoyDTU bug"
-description: "File a bug report"
-title: "[Bug]"
-labels: ["bug", "needs-triage"]
-assignees:
-  - lumapu
+name: "issue report"
+description: "issue report"
+labels: ["new", "needs-triage"]
 body:
   - type: markdown
     attributes:
@@ -12,7 +9,7 @@ body:
         Wir lesen auch gerne Deutsch, bitte fülle die u.a. Fragen aus damit wir Dir bestmöglich helfen können Danke!
         Bitte unser FAQ als Hilfestellung prüfen: https://ahoydtu.de/faq
         
-        Please read, copy & fill in the template from our Posting Guide lines into your Support Forum post.
+        Please read, then copy & fill in the template from our Posting Guide lines into your Support Forum post.
         We do enjoy the english language, but we need a couple of things to best support you in your goal, please fill in all / most of the details given below. Thanks!
         Check our FAQ: https://ahoydtu.de/faq
   - type: markdown
@@ -35,7 +32,7 @@ body:
       label: Assembly
       description:
       options:
-        - I did the assebly by myself
+        - I did the assembly by myself
         - the DTU was already assembled
     validations:
       required: true
@@ -84,7 +81,7 @@ body:
       label: Connection picture
       description: 
       options:
-        - label: I will attach/upload an Image of my wiring
+        - label: I will attach/upload an image of my wiring
     validations:
       required: true
   - type: markdown
diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml
index c15bd883..cdd2cdf3 100644
--- a/.github/workflows/compile_development.yml
+++ b/.github/workflows/compile_development.yml
@@ -16,23 +16,25 @@ jobs:
     - uses: actions/checkout@v4
 
   build-en:
-    name: Build Environments (English)
+    name: Build (EN)
     needs: check
     runs-on: ubuntu-latest
     continue-on-error: true
     strategy:
       matrix:
         variant:
+          - opendtufusion
+          - opendtufusion-16MB
           - esp8266
+          - esp8266-all
+          - esp8266-minimal
           - esp8266-prometheus
           - esp8285
           - esp32-wroom32
+          - esp32-wroom32-minimal
           - 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
@@ -67,6 +69,11 @@ jobs:
     - name: Run PlatformIO
       run:  pio run -d src -e ${{ matrix.variant }}
 
+    - name: Compress .elf
+      uses: edgarrc/action-7z@v1
+      with:
+        args: 7z a -t7z -mx=9 src/.pio/build/${{ matrix.variant }}/firmware.elf.7z ./src/.pio/build/${{ matrix.variant }}/firmware.elf
+
     - name: Rename Firmware
       run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
 
@@ -77,23 +84,23 @@ jobs:
         path: firmware/*
 
   build-de:
-    name: Build Environments (German)
+    name: Build (DE)
     needs: check
     runs-on: ubuntu-latest
     continue-on-error: true
     strategy:
       matrix:
         variant:
+          - opendtufusion-de
+          #- opendtufusion-16MB-de #not needed, only the partions.bin is different and can be used from english build
           - esp8266-de
+          - esp8266-all-de
           - esp8266-prometheus-de
           - esp8285-de
           - 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
@@ -128,6 +135,11 @@ jobs:
     - name: Run PlatformIO
       run:  pio run -d src -e ${{ matrix.variant }}
 
+    - name: Compress .elf
+      uses: edgarrc/action-7z@v1
+      with:
+        args: 7z a -t7z -mx=9 src/.pio/build/${{ matrix.variant }}/firmware.elf.7z ./src/.pio/build/${{ matrix.variant }}/firmware.elf
+
     - name: Rename Firmware
       run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
 
@@ -138,7 +150,7 @@ jobs:
         path: firmware/*
 
   deploy:
-    name: Deploy Environments
+    name: Update Artifacts / Deploy
     needs: [build-en, build-de]
     runs-on: ubuntu-latest
     continue-on-error: false
@@ -164,9 +176,26 @@ jobs:
       env:
           VERSION: ${{ steps.version_name.outputs.name }}
 
+
+    - name: Create ESP Web Tools Manifest
+      working-directory: src
+      run: python ../scripts/buildManifest.py
+
+    - name: Copy install html
+      run: mv scripts/gh-action-dev-build-flash.html firmware/install.html
+
+    - name: Copy Changes.md
+      run: mv src/CHANGES.md firmware/CHANGES.md
+
+
     - name: Rename firmware directory
       run: mv firmware ${{ steps.version_name.outputs.name }}
 
+    - name: delete environment Artifacts
+      uses: geekyeggo/delete-artifact@v4
+      with:
+        name: dev-*
+
     - name: Deploy
       uses: nogsantos/scp-deploy@master
       with:
@@ -176,3 +205,17 @@ jobs:
         port: ${{ secrets.FW_SSH_PORT }}
         user: ${{ secrets.FW_SSH_USER }}
         key: ${{ secrets.FW_SSH_KEY }}
+
+    - name: Clean elf files (7z compressed) for Artifact
+      run: |
+        rm -f \
+          ${{ steps.version_name.outputs.name }}/*/*.elf.7z
+
+    - name: Create Artifact
+      uses: actions/upload-artifact@v4
+      with:
+        name: dev-${{ steps.version_name.outputs.name }}
+        path: |
+          ${{ steps.version_name.outputs.name }}/*
+          manual/User_Manual.md
+          manual/Getting_Started.md
diff --git a/.github/workflows/compile_release.yml b/.github/workflows/compile_release.yml
index 59adf28c..8ba1c06a 100644
--- a/.github/workflows/compile_release.yml
+++ b/.github/workflows/compile_release.yml
@@ -5,36 +5,103 @@ on:
     branches: main
     paths-ignore:
     - '**.md' # Do no build on *.md changes
+    - '**.yaml' # e.g. issue report
 
 jobs:
-  build:
-    name: Build Environments
+  check:
+    name: Check Repository
     runs-on: ubuntu-latest
     if: github.repository == 'lumapu/ahoy' && github.ref_name == 'main'
     continue-on-error: false
+    steps:
+    - uses: actions/checkout@v4
+
+  build-en:
+    name: Build (EN)
+    needs: check
+    runs-on: ubuntu-latest
+    continue-on-error: false
     strategy:
       matrix:
         variant:
+          - opendtufusion
+          - opendtufusion-16MB
           - esp8266
+          - esp8266-all
+          - esp8266-minimal
           - esp8266-prometheus
           - esp8285
           - esp32-wroom32
+          - esp32-wroom32-minimal
           - 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: Compress .elf
+      uses: edgarrc/action-7z@v1
+      with:
+        args: 7z a -t7z -mx=9 src/.pio/build/${{ matrix.variant }}/firmware.elf.7z ./src/.pio/build/${{ matrix.variant }}/firmware.elf
+
+    - name: Rename Firmware
+      run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
+
+    - name: Create Artifact
+      uses: actions/upload-artifact@v4
+      with:
+        name: ${{ matrix.variant }}
+        path: firmware/*
+
+  build-de:
+    name: Build (DE)
+    needs: check
+    runs-on: ubuntu-latest
+    continue-on-error: false
+    strategy:
+      matrix:
+        variant:
+          - opendtufusion-de
+          #- opendtufusion-16MB-de #not needed, only the partions.bin is different and can be used from english build
           - esp8266-de
+          - esp8266-all-de
           - esp8266-prometheus-de
           - esp8285-de
           - 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
@@ -69,6 +136,11 @@ jobs:
     - name: Run PlatformIO
       run:  pio run -d src -e ${{ matrix.variant }}
 
+    - name: Compress .elf
+      uses: edgarrc/action-7z@v1
+      with:
+        args: 7z a -t7z -mx=9 src/.pio/build/${{ matrix.variant }}/firmware.elf.7z ./src/.pio/build/${{ matrix.variant }}/firmware.elf
+
     - name: Rename Firmware
       run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
 
@@ -78,11 +150,10 @@ jobs:
         name: ${{ matrix.variant }}
         path: firmware/*
 
-
   release:
     name: Create Release
+    needs: [build-en, build-de]
     runs-on: ubuntu-latest
-    needs: [build]
     continue-on-error: false
     permissions:
       contents: write
@@ -140,7 +211,7 @@ jobs:
 
   deploy:
     name: Deploy Environments to fw.ahoydtu.de
-    needs: [build, release]
+    needs: [build-en, build-de, release]
     runs-on: ubuntu-latest
     continue-on-error: false
     steps:
diff --git a/.gitignore b/.gitignore
index 21ae2a57..0df18078 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@
 src/config/config_override.h
 src/web/html/h/*
 src/web/html/tmp/*
+src/data/*
 /**/Debug
 /**/v16/*
 *.db
@@ -15,3 +16,4 @@ src/web/html/tmp/*
 src/output.map
 
 /.venv
+/scripts/__pycache__/*
diff --git a/README.md b/README.md
index 16b2a256..07d60002 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@ This repository provides hardware and software solutions for communicating with
 
 ## Changelog
 [latest Release](https://github.com/lumapu/ahoy/blob/main/src/CHANGES.md)
+
 [Development Version](https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md)
 
 
@@ -31,10 +32,9 @@ Table of approaches:
 
 | Board  | MI | HM | HMS/HMT | comment | HowTo start |
 | ------ | -- | -- | ------- | ------- | ---------- |
-| [ESP8266/ESP32, C++](manual/Getting_Started.md) | ✔️ | ✔️ | ✔️ |  👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) |
-| [Arduino Nano, C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | |
-| [Raspberry Pi, Python](tools/rpi/) | ❌ | ✔️ | ❌ | |
-| [Others, C/C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ |  |
+| [ESP32, C++](manual/Getting_Started.md) | ✔️ | ✔️ | ✔️ |  [create your own DTU](https://ahoydtu.de/getting_started/) |
+| ESP8266, C++ | ✔️ | ✔️ | ❌ | ⚠️ not recommended for new DTU |
+
 
 ⚠️ **Warning: HMS-XXXXW-2T WiFi inverters are not supported. They have a 'W' in their name and a DTU serial number on its sticker**
 
@@ -48,6 +48,8 @@ Table of approaches:
 ## Our Website
 [https://ahoydtu.de](https://ahoydtu.de)
 
+[Firmware Archive https://fw.ahoydtu.de](https://fw.ahoydtu.de)
+
 ## Success Stories
 - [Getting the data into influxDB and visualize them in a Grafana Dashboard](https://grafana.com/grafana/dashboards/16850-pv-power-ahoy/) (thx @Carl)
 
diff --git a/manual/Getting_Started.md b/manual/Getting_Started.md
index b1ef7995..33637e6f 100644
--- a/manual/Getting_Started.md
+++ b/manual/Getting_Started.md
@@ -17,6 +17,7 @@ Hoymiles Inverters
 | ✔️ | HMS | 350, 500, 800, 1000, 1600, 1800, 2000 | |
 | ✔️ | HMT | 1600, 1800, 2250 | |
 | ⚠️ | TSUN | [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) | others may work as well (need to be verified). |
+| 🟡 | HERF | (supported) | |
 
 ## Table of Contents
 
@@ -25,16 +26,16 @@ Hoymiles Inverters
 - [Things needed](#things-needed)
     - [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there)
 - [Wiring things up](#wiring-things-up)
+    - [ESP32 wiring example](#esp32-wiring-example)
+      - [Schematic](#schematic-1)
+      - [Symbolic view](#symbolic-view-1)
+      - [ESP32 GPIO settings](#esp32-gpio-settings)
     - [ESP8266 wiring example on WEMOS D1](#esp8266-wiring-example)
       - [Schematic](#schematic)
       - [Symbolic view](#symbolic-view)
     - [ESP8266 wiring example on 30pin Lolin NodeMCU v3](#esp8266-wiring-example-2)
       - [Schematic](#schematic-2)
       - [Symbolic view](#symbolic-view-2)
-    - [ESP32 wiring example](#esp32-wiring-example)
-      - [Schematic](#schematic-1)
-      - [Symbolic view](#symbolic-view-1)
-      - [ESP32 GPIO settings](#esp32-gpio-settings)
 - [Flash the Firmware on your Ahoy DTU Hardware](#flash-the-firmware-on-your-ahoy-dtu-hardware)
     - [Compiling your own Version](#compiling-your-own-version)
     - [Using a ready-to-flash binary using nodemcu-pyflasher](#using-a-ready-to-flash-binary-using-nodemcu-pyflasher)
@@ -56,9 +57,10 @@ Solenso Inverters:
 
 To build your own AhoyDTU, you only need a few things. Remember that the maker community is always developing new and innovative options that we may not have covered in this readme.
 
-Start with an ESP8266 or ESP32, and combine it with an NRF24L01+ breakout board. Other ESP boards with at least 4MBytes of ROM may also be suitable.
+Start with an ESP32 or ESP8266 (not recommended), and combine it with an NRF24L01+ breakout board for HM-series inverters. To communicate with a HMS or HMT inverter you need to use a CMT2300A radio module.
+Other ESP boards with at least 4MBytes of ROM may also be suitable.
 
-Make sure to choose an NRF24L01+ module that includes the '+' in its name. This is important because we need the 250kbps features that are only available in the plus-variant.
+Note for NRF24 radio module: Make sure to choose an NRF24L01+ module that includes the '+' in its name. This is important because we need the 250kbps features that are only available in the plus-variant.
 
 **Attention**: The NRF24L01+ can only communicate with the MI/HM/TSUN inverter. For the HMS/HMT it is needed to use a CMT2300A!
 
diff --git a/manual/factory_firmware.md b/manual/factory_firmware.md
new file mode 100644
index 00000000..047dd4b0
--- /dev/null
+++ b/manual/factory_firmware.md
@@ -0,0 +1,56 @@
+# Generate factory firmware (ESP32)
+
+If the firmware should already contain predefined settings this guide will help you to compile these into a single binary file.
+
+## Generate default settings
+
+First install on the requested platform the standard firmware and configure everything to your needs. Once you did all changes store them and export them to a `json` file.
+
+## Further prepare default settings
+
+First create a directory `data` inside the following project path: `src/`.
+
+As the export removes all your passwords you need to add them again to the `json` file. Open the `json` file with a text editor and search for all the `"pwd":""` sections. Between the second bunch of quotation marks you have to place the password.
+
+*Note: It's recommended to keep all information in one line to save space on the ESP littlefs partition*
+
+Next rename your export file to `settings.json` and move it to the new created directory. It should be look similar to this:
+
+```
+ahoy
+  |-- src
+        |-- data
+              |-- settings.json
+        |-- config
+        |-- network
+        ...
+```
+
+## build firmware
+
+Choose your prefered environment and build firmware as usual. Once the process is finished you should find along with the standard `firmware.bin` an additional file called `firmware.factory.bin`. Both files are located here: `src/.pio/build/[ENVIRONMENT]/`
+
+## Upload to device
+
+Navigate to the firmware output directory `src/.pio/build/[ENVIRONMENT]/` and open a terminal or vice versa.
+
+Python:
+`esptool.py -b 921600 write_flash --flash_mode dio --flash_size detect 0x0 firmware.factory.bin`
+
+Windows:
+`esptool.exe -b 921600 write_flash --flash_mode dio --flash_size detect 0x0 firmware.factory.bin`
+
+The upload should be finished within one minute.
+
+## Testing
+
+Reboot your ESP an check if all your settings are present.
+
+## Get updated with 'Mainline'
+
+From time to time a new version of AhoyDTU will be published. To get the changes into your already prepared factory binary generation environment you have to do only a few steps:
+
+1. pull new changes from remote: `git pull`
+2. check if the `data` folder is still there and contains the `settings.json`
+3. build and upload
+4. enjoy
diff --git a/patches/AsyncWeb_Prometheus.patch b/patches/AsyncWeb_Prometheus.patch
index 21fe22cd..3c7deac4 100644
--- a/patches/AsyncWeb_Prometheus.patch
+++ b/patches/AsyncWeb_Prometheus.patch
@@ -1,26 +1,26 @@
 diff --git a/src/AsyncWebSocket.cpp b/src/AsyncWebSocket.cpp
-index 12be5f8..cffeed7 100644
+index 6e88da9..09359c3 100644
 --- a/src/AsyncWebSocket.cpp
 +++ b/src/AsyncWebSocket.cpp
-@@ -737,7 +737,7 @@ void AsyncWebSocketClient::binary(const __FlashStringHelper *data, size_t len)
- IPAddress AsyncWebSocketClient::remoteIP() const
- {
-     if (!_client)
--        return IPAddress(0U);
-+        return IPAddress();
+@@ -827,7 +827,7 @@ void AsyncWebSocketClient::binary(AsyncWebSocketMessageBuffer * buffer)
  
+ IPAddress AsyncWebSocketClient::remoteIP() {
+     if(!_client) {
+-        return IPAddress((uint32_t)0);
++        return IPAddress();
+     }
      return _client->remoteIP();
  }
 diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp
-index 22a549f..e0b36b3 100644
+index a22e991..babef18 100644
 --- a/src/WebResponses.cpp
 +++ b/src/WebResponses.cpp
-@@ -318,7 +318,7 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u
+@@ -317,7 +317,7 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u
            free(buf);
            return 0;
        }
--      outLen = sprintf_P((char*)buf+headLen, PSTR("%x"), readLen) + headLen;
-+      outLen = sprintf_P((char*)buf+headLen, PSTR("%04x"), readLen) + headLen;
+-      outLen = sprintf((char*)buf+headLen, "%x", readLen) + headLen;
++      outLen = sprintf((char*)buf+headLen, "%04x", readLen) + headLen;
        while(outLen < headLen + 4) buf[outLen++] = ' ';
        buf[outLen++] = '\r';
        buf[outLen++] = '\n';
diff --git a/patches/GxEPD2_HAL.patch b/patches/GxEPD2_HAL.patch
new file mode 100644
index 00000000..d7b394eb
--- /dev/null
+++ b/patches/GxEPD2_HAL.patch
@@ -0,0 +1,392 @@
+diff --git a/src/GxEPD2_EPD.cpp b/src/GxEPD2_EPD.cpp
+index 8df8bef..e9dfb19 100644
+--- a/src/GxEPD2_EPD.cpp
++++ b/src/GxEPD2_EPD.cpp
+@@ -17,11 +17,10 @@
+ #include <avr/pgmspace.h>
+ #endif
+ 
+-GxEPD2_EPD::GxEPD2_EPD(int16_t cs, int16_t dc, int16_t rst, int16_t busy, int16_t busy_level, uint32_t busy_timeout,
++GxEPD2_EPD::GxEPD2_EPD(GxEPD2_HalInterface *hal, int16_t busy_level, uint32_t busy_timeout,
+                        uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu) :
+   WIDTH(w), HEIGHT(h), panel(p), hasColor(c), hasPartialUpdate(pu), hasFastPartialUpdate(fpu),
+-  _cs(cs), _dc(dc), _rst(rst), _busy(busy), _busy_level(busy_level), _busy_timeout(busy_timeout), _diag_enabled(false),
+-  _pSPIx(&SPI), _spi_settings(4000000, MSBFIRST, SPI_MODE0)
++  _hal(hal), _busy_level(busy_level), _busy_timeout(busy_timeout), _diag_enabled(false)
+ {
+   _initial_write = true;
+   _initial_refresh = true;
+@@ -54,44 +53,10 @@ void GxEPD2_EPD::init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset
+     Serial.begin(serial_diag_bitrate);
+     _diag_enabled = true;
+   }
+-  if (_cs >= 0)
+-  {
+-    digitalWrite(_cs, HIGH); // preset (less glitch for any analyzer)
+-    pinMode(_cs, OUTPUT);
+-    digitalWrite(_cs, HIGH); // set (needed e.g. for RP2040)
+-  }
+-  if (_dc >= 0)
+-  {
+-    digitalWrite(_dc, HIGH); // preset (less glitch for any analyzer)
+-    pinMode(_dc, OUTPUT);
+-    digitalWrite(_dc, HIGH); // set (needed e.g. for RP2040)
+-  }
+-  _reset();
+-  if (_busy >= 0)
+-  {
+-    pinMode(_busy, INPUT);
+-  }
+-  _pSPIx->begin();
+-  if (_busy == MISO) // may be overridden
+-  {
+-    pinMode(_busy, INPUT);
+-  }
+-  if (_dc == MISO) // may be overridden, TTGO T5 V2.66
+-  {
+-    pinMode(_dc, OUTPUT);
+-  }
+-  if (_cs == MISO) // may be overridden
+-  {
+-    pinMode(_cs, INPUT);
+-  }
+ }
+ 
+ void GxEPD2_EPD::end()
+ {
+-  _pSPIx->end();
+-  if (_cs >= 0) pinMode(_cs, INPUT);
+-  if (_dc >= 0) pinMode(_dc, INPUT);
+-  if (_rst >= 0) pinMode(_rst, INPUT);
+ }
+ 
+ void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void* busy_callback_parameter)
+@@ -100,34 +65,27 @@ void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void*
+   _busy_callback_parameter = busy_callback_parameter;
+ }
+ 
+-void GxEPD2_EPD::selectSPI(SPIClass& spi, SPISettings spi_settings)
+-{
+-  _pSPIx = &spi;
+-  _spi_settings = spi_settings;
+-}
+-
+ void GxEPD2_EPD::_reset()
+ {
+-  if (_rst >= 0)
+   {
+     if (_pulldown_rst_mode)
+     {
+-      digitalWrite(_rst, LOW);
+-      pinMode(_rst, OUTPUT);
+-      digitalWrite(_rst, LOW);
++      _hal->rst(LOW);
++      _hal->rstMode(OUTPUT);
++      _hal->rst(LOW);
+       delay(_reset_duration);
+-      pinMode(_rst, INPUT_PULLUP);
++      _hal->rstMode(INPUT_PULLUP);
+       delay(_reset_duration > 10 ? _reset_duration : 10);
+     }
+     else
+     {
+-      digitalWrite(_rst, HIGH); // NEEDED for Waveshare "clever" reset circuit, power controller before reset pulse, preset (less glitch for any analyzer)
+-      pinMode(_rst, OUTPUT);
+-      digitalWrite(_rst, HIGH); // NEEDED for Waveshare "clever" reset circuit, power controller before reset pulse, set (needed e.g. for RP2040)
++      _hal->rst(HIGH); // NEEDED for Waveshare "clever" reset circuit, power controller before reset pulse, preset (less glitch for any analyzer)
++      _hal->rstMode(OUTPUT);
++      _hal->rst(HIGH); // NEEDED for Waveshare "clever" reset circuit, power controller before reset pulse, set (needed e.g. for RP2040)
+       delay(10); // NEEDED for Waveshare "clever" reset circuit, at least delay(2);
+-      digitalWrite(_rst, LOW);
++      _hal->rst(LOW);
+       delay(_reset_duration);
+-      digitalWrite(_rst, HIGH);
++      _hal->rst(HIGH);
+       delay(_reset_duration > 10 ? _reset_duration : 10);
+     }
+     _hibernating = false;
+@@ -136,16 +94,15 @@ void GxEPD2_EPD::_reset()
+ 
+ void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time)
+ {
+-  if (_busy >= 0)
+   {
+     delay(1); // add some margin to become active
+     unsigned long start = micros();
+     while (1)
+     {
+-      if (digitalRead(_busy) != _busy_level) break;
++      if (_hal->getBusy() != _busy_level) break;
+       if (_busy_callback) _busy_callback(_busy_callback_parameter);
+       else delay(1);
+-      if (digitalRead(_busy) != _busy_level) break;
++      if (_hal->getBusy() != _busy_level) break;
+       if (micros() - start > _busy_timeout)
+       {
+         Serial.println("Busy Timeout!");
+@@ -169,120 +126,59 @@ void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time)
+     }
+     (void) start;
+   }
+-  else delay(busy_time);
+ }
+ 
+ void GxEPD2_EPD::_writeCommand(uint8_t c)
+ {
+-  _pSPIx->beginTransaction(_spi_settings);
+-  if (_dc >= 0) digitalWrite(_dc, LOW);
+-  if (_cs >= 0) digitalWrite(_cs, LOW);
+-  _pSPIx->transfer(c);
+-  if (_cs >= 0) digitalWrite(_cs, HIGH);
+-  if (_dc >= 0) digitalWrite(_dc, HIGH);
+-  _pSPIx->endTransaction();
++  _hal->writeCmd(c);
+ }
+ 
+ void GxEPD2_EPD::_writeData(uint8_t d)
+ {
+-  _pSPIx->beginTransaction(_spi_settings);
+-  if (_cs >= 0) digitalWrite(_cs, LOW);
+-  _pSPIx->transfer(d);
+-  if (_cs >= 0) digitalWrite(_cs, HIGH);
+-  _pSPIx->endTransaction();
++  _hal->write(d);
+ }
+ 
+ void GxEPD2_EPD::_writeData(const uint8_t* data, uint16_t n)
+ {
+-  _pSPIx->beginTransaction(_spi_settings);
+-  if (_cs >= 0) digitalWrite(_cs, LOW);
+-  for (uint16_t i = 0; i < n; i++)
+-  {
+-    _pSPIx->transfer(*data++);
+-  }
+-  if (_cs >= 0) digitalWrite(_cs, HIGH);
+-  _pSPIx->endTransaction();
++  _hal->write(data, n);
+ }
+ 
+ void GxEPD2_EPD::_writeDataPGM(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes)
+ {
+-  _pSPIx->beginTransaction(_spi_settings);
+-  if (_cs >= 0) digitalWrite(_cs, LOW);
+-  for (uint16_t i = 0; i < n; i++)
+-  {
+-    _pSPIx->transfer(pgm_read_byte(&*data++));
+-  }
+-  while (fill_with_zeroes > 0)
+-  {
+-    _pSPIx->transfer(0x00);
+-    fill_with_zeroes--;
+-  }
+-  if (_cs >= 0) digitalWrite(_cs, HIGH);
+-  _pSPIx->endTransaction();
++  _hal->write(data, n, fill_with_zeroes);
+ }
+ 
+ void GxEPD2_EPD::_writeDataPGM_sCS(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes)
+ {
+-  _pSPIx->beginTransaction(_spi_settings);
+-  for (uint8_t i = 0; i < n; i++)
+-  {
+-    if (_cs >= 0) digitalWrite(_cs, LOW);
+-    _pSPIx->transfer(pgm_read_byte(&*data++));
+-    if (_cs >= 0) digitalWrite(_cs, HIGH);
+-  }
+-  while (fill_with_zeroes > 0)
+-  {
+-    if (_cs >= 0) digitalWrite(_cs, LOW);
+-    _pSPIx->transfer(0x00);
+-    fill_with_zeroes--;
+-    if (_cs >= 0) digitalWrite(_cs, HIGH);
++  _hal->write(data, n);
++  if (fill_with_zeroes > 0) {
++    uint8_t buf[fill_with_zeroes];
++    memset(buf, 0, fill_with_zeroes);
++    _hal->write(buf, fill_with_zeroes);
+   }
+-  _pSPIx->endTransaction();
+ }
+ 
+ void GxEPD2_EPD::_writeCommandData(const uint8_t* pCommandData, uint8_t datalen)
+ {
+-  _pSPIx->beginTransaction(_spi_settings);
+-  if (_dc >= 0) digitalWrite(_dc, LOW);
+-  if (_cs >= 0) digitalWrite(_cs, LOW);
+-  _pSPIx->transfer(*pCommandData++);
+-  if (_dc >= 0) digitalWrite(_dc, HIGH);
+-  for (uint8_t i = 0; i < datalen - 1; i++)  // sub the command
+-  {
+-    _pSPIx->transfer(*pCommandData++);
+-  }
+-  if (_cs >= 0) digitalWrite(_cs, HIGH);
+-  _pSPIx->endTransaction();
++  _hal->writeCmd(pCommandData, datalen, false);
+ }
+ 
+ void GxEPD2_EPD::_writeCommandDataPGM(const uint8_t* pCommandData, uint8_t datalen)
+ {
+-  _pSPIx->beginTransaction(_spi_settings);
+-  if (_dc >= 0) digitalWrite(_dc, LOW);
+-  if (_cs >= 0) digitalWrite(_cs, LOW);
+-  _pSPIx->transfer(pgm_read_byte(&*pCommandData++));
+-  if (_dc >= 0) digitalWrite(_dc, HIGH);
+-  for (uint8_t i = 0; i < datalen - 1; i++)  // sub the command
+-  {
+-    _pSPIx->transfer(pgm_read_byte(&*pCommandData++));
+-  }
+-  if (_cs >= 0) digitalWrite(_cs, HIGH);
+-  _pSPIx->endTransaction();
++  _hal->writeCmd(pCommandData, datalen, true);
+ }
+ 
+ void GxEPD2_EPD::_startTransfer()
+ {
+-  _pSPIx->beginTransaction(_spi_settings);
+-  if (_cs >= 0) digitalWrite(_cs, LOW);
++  _hal->startTransfer();
+ }
+ 
+ void GxEPD2_EPD::_transfer(uint8_t value)
+ {
+-  _pSPIx->transfer(value);
++  _hal->transfer(value);
+ }
+ 
+ void GxEPD2_EPD::_endTransfer()
+ {
+-  if (_cs >= 0) digitalWrite(_cs, HIGH);
+-  _pSPIx->endTransaction();
++  _hal->endTransfer();
+ }
+diff --git a/src/GxEPD2_EPD.h b/src/GxEPD2_EPD.h
+index 34c1145..1e8ea64 100644
+--- a/src/GxEPD2_EPD.h
++++ b/src/GxEPD2_EPD.h
+@@ -13,9 +13,9 @@
+ #define _GxEPD2_EPD_H_
+ 
+ #include <Arduino.h>
+-#include <SPI.h>
+ 
+ #include <GxEPD2.h>
++#include <GxEPD2_Hal.h>
+ 
+ #pragma GCC diagnostic ignored "-Wunused-parameter"
+ //#pragma GCC diagnostic ignored "-Wsign-compare"
+@@ -31,7 +31,7 @@ class GxEPD2_EPD
+     const bool hasPartialUpdate;
+     const bool hasFastPartialUpdate;
+     // constructor
+-    GxEPD2_EPD(int16_t cs, int16_t dc, int16_t rst, int16_t busy, int16_t busy_level, uint32_t busy_timeout,
++    GxEPD2_EPD(GxEPD2_HalInterface *hal, int16_t busy_level, uint32_t busy_timeout,
+                uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu);
+     virtual void init(uint32_t serial_diag_bitrate = 0); // serial_diag_bitrate = 0 : disabled
+     virtual void init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 10, bool pulldown_rst_mode = false);
+@@ -97,7 +97,6 @@ class GxEPD2_EPD
+     {
+       return (a > b ? a : b);
+     };
+-    void selectSPI(SPIClass& spi, SPISettings spi_settings);
+   protected:
+     void _reset();
+     void _waitWhileBusy(const char* comment = 0, uint16_t busy_time = 5000);
+@@ -112,16 +111,15 @@ class GxEPD2_EPD
+     void _transfer(uint8_t value);
+     void _endTransfer();
+   protected:
+-    int16_t _cs, _dc, _rst, _busy, _busy_level;
++    GxEPD2_HalInterface *_hal;
++    int16_t _busy_level;
+     uint32_t _busy_timeout;
+     bool _diag_enabled, _pulldown_rst_mode;
+-    SPIClass* _pSPIx;
+-    SPISettings _spi_settings;
+     bool _initial_write, _initial_refresh;
+     bool _power_is_on, _using_partial_mode, _hibernating;
+     bool _init_display_done;
+     uint16_t _reset_duration;
+-    void (*_busy_callback)(const void*); 
++    void (*_busy_callback)(const void*);
+     const void* _busy_callback_parameter;
+ };
+ 
+diff --git a/src/GxEPD2_Hal.h b/src/GxEPD2_Hal.h
+new file mode 100644
+index 0000000..13424b6
+--- /dev/null
++++ b/src/GxEPD2_Hal.h
+@@ -0,0 +1,19 @@
++#pragma once
++
++class GxEPD2_HalInterface {
++    public:
++        virtual void rstMode(uint8_t mode) = 0;
++        virtual void rst(bool level) = 0;
++        virtual int getBusy(void) = 0;
++        virtual bool isRst(void) = 0;
++
++        virtual void write(uint8_t buf) = 0;
++        virtual void write(const uint8_t *buf, uint16_t n) = 0;
++        virtual void write(const uint8_t *buf, uint16_t n, int16_t fill_with_zeroes) = 0;
++        virtual void writeCmd(const uint8_t val) = 0;
++        virtual void writeCmd(const uint8_t* pCommandData, uint8_t datalen, bool isPGM) = 0;
++
++        virtual void startTransfer(void) = 0;
++        virtual void endTransfer(void) = 0;
++        virtual void transfer(const uint8_t val) = 0;
++};
+diff --git a/src/epd/GxEPD2_150_BN.cpp b/src/epd/GxEPD2_150_BN.cpp
+index bfb3ddf..dba3d78 100644
+--- a/src/epd/GxEPD2_150_BN.cpp
++++ b/src/epd/GxEPD2_150_BN.cpp
+@@ -14,8 +14,8 @@
+ 
+ #include "GxEPD2_150_BN.h"
+ 
+-GxEPD2_150_BN::GxEPD2_150_BN(int16_t cs, int16_t dc, int16_t rst, int16_t busy) :
+-  GxEPD2_EPD(cs, dc, rst, busy, HIGH, 10000000, WIDTH, HEIGHT, panel, hasColor, hasPartialUpdate, hasFastPartialUpdate)
++GxEPD2_150_BN::GxEPD2_150_BN(GxEPD2_HalInterface *hal) :
++  GxEPD2_EPD(hal, HIGH, 10000000, WIDTH, HEIGHT, panel, hasColor, hasPartialUpdate, hasFastPartialUpdate)
+ {
+ }
+ 
+@@ -269,7 +269,7 @@ void GxEPD2_150_BN::refresh(int16_t x, int16_t y, int16_t w, int16_t h)
+   int16_t y1 = y < 0 ? 0 : y; // limit
+   w1 = x1 + w1 < int16_t(WIDTH) ? w1 : int16_t(WIDTH) - x1; // limit
+   h1 = y1 + h1 < int16_t(HEIGHT) ? h1 : int16_t(HEIGHT) - y1; // limit
+-  if ((w1 <= 0) || (h1 <= 0)) return; 
++  if ((w1 <= 0) || (h1 <= 0)) return;
+   // make x1, w1 multiple of 8
+   w1 += x1 % 8;
+   if (w1 % 8 > 0) w1 += 8 - w1 % 8;
+@@ -287,7 +287,7 @@ void GxEPD2_150_BN::powerOff()
+ void GxEPD2_150_BN::hibernate()
+ {
+   _PowerOff();
+-  if (_rst >= 0)
++  if (_hal->isRst())
+   {
+     _writeCommand(0x10); // deep sleep mode
+     _writeData(0x1);     // enter deep sleep
+diff --git a/src/epd/GxEPD2_150_BN.h b/src/epd/GxEPD2_150_BN.h
+index bc46a45..954b9c4 100644
+--- a/src/epd/GxEPD2_150_BN.h
++++ b/src/epd/GxEPD2_150_BN.h
+@@ -16,6 +16,7 @@
+ #define _GxEPD2_150_BN_H_
+ 
+ #include "../GxEPD2_EPD.h"
++#include "../GxEPD2_Hal.h"
+ 
+ class GxEPD2_150_BN : public GxEPD2_EPD
+ {
+@@ -33,7 +34,7 @@ class GxEPD2_150_BN : public GxEPD2_EPD
+     static const uint16_t full_refresh_time = 4000; // ms, e.g. 3825000us
+     static const uint16_t partial_refresh_time = 800; // ms, e.g. 736000us
+     // constructor
+-    GxEPD2_150_BN(int16_t cs, int16_t dc, int16_t rst, int16_t busy);
++    GxEPD2_150_BN(GxEPD2_HalInterface *hal);
+     // methods (virtual)
+     //  Support for Bitmaps (Sprites) to Controller Buffer and to Screen
+     void clearScreen(uint8_t value = 0xFF); // init controller memory and screen (default white)
diff --git a/patches/GxEPD2_SW_SPI.patch b/patches/GxEPD2_SW_SPI.patch
deleted file mode 100644
index dc3fa9ca..00000000
--- a/patches/GxEPD2_SW_SPI.patch
+++ /dev/null
@@ -1,362 +0,0 @@
-diff --git a/src/GxEPD2_EPD.cpp b/src/GxEPD2_EPD.cpp
-index 8df8bef..91d7f49 100644
---- a/src/GxEPD2_EPD.cpp
-+++ b/src/GxEPD2_EPD.cpp
-@@ -19,9 +19,9 @@
- 
- GxEPD2_EPD::GxEPD2_EPD(int16_t cs, int16_t dc, int16_t rst, int16_t busy, int16_t busy_level, uint32_t busy_timeout,
-                        uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu) :
--  WIDTH(w), HEIGHT(h), panel(p), hasColor(c), hasPartialUpdate(pu), hasFastPartialUpdate(fpu),
-+  WIDTH(w), HEIGHT(h), panel(p), hasColor(c), hasPartialUpdate(pu), hasFastPartialUpdate(fpu), _sck(-1), _mosi(-1),
-   _cs(cs), _dc(dc), _rst(rst), _busy(busy), _busy_level(busy_level), _busy_timeout(busy_timeout), _diag_enabled(false),
--  _pSPIx(&SPI), _spi_settings(4000000, MSBFIRST, SPI_MODE0)
-+  _spi_settings(4000000, MSBFIRST, SPI_MODE0)
- {
-   _initial_write = true;
-   _initial_refresh = true;
-@@ -71,27 +71,30 @@ void GxEPD2_EPD::init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset
-   {
-     pinMode(_busy, INPUT);
-   }
--  _pSPIx->begin();
--  if (_busy == MISO) // may be overridden
--  {
--    pinMode(_busy, INPUT);
--  }
--  if (_dc == MISO) // may be overridden, TTGO T5 V2.66
--  {
--    pinMode(_dc, OUTPUT);
--  }
--  if (_cs == MISO) // may be overridden
-+  if (_sck < 0) SPI.begin();
-+}
-+
-+void GxEPD2_EPD::init(int16_t sck, int16_t mosi, uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration, bool pulldown_rst_mode)
-+{
-+  if ((sck >= 0) && (mosi >= 0))
-   {
--    pinMode(_cs, INPUT);
--  }
-+    _sck = sck;
-+    _mosi = mosi;
-+    digitalWrite(_sck, LOW);
-+    digitalWrite(_mosi, LOW);
-+    pinMode(_sck, OUTPUT);
-+    pinMode(_mosi, OUTPUT);
-+  } else _sck = -1;
-+  init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode);
- }
- 
- void GxEPD2_EPD::end()
- {
--  _pSPIx->end();
-   if (_cs >= 0) pinMode(_cs, INPUT);
-   if (_dc >= 0) pinMode(_dc, INPUT);
-   if (_rst >= 0) pinMode(_rst, INPUT);
-+  if (_sck >= 0) pinMode(_sck, INPUT);
-+  if (_mosi >= 0) pinMode(_mosi, INPUT);
- }
- 
- void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void* busy_callback_parameter)
-@@ -100,12 +103,6 @@ void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void*
-   _busy_callback_parameter = busy_callback_parameter;
- }
- 
--void GxEPD2_EPD::selectSPI(SPIClass& spi, SPISettings spi_settings)
--{
--  _pSPIx = &spi;
--  _spi_settings = spi_settings;
--}
--
- void GxEPD2_EPD::_reset()
- {
-   if (_rst >= 0)
-@@ -174,115 +171,201 @@ void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time)
- 
- void GxEPD2_EPD::_writeCommand(uint8_t c)
- {
--  _pSPIx->beginTransaction(_spi_settings);
-+  _beginTransaction(_spi_settings);
-   if (_dc >= 0) digitalWrite(_dc, LOW);
-   if (_cs >= 0) digitalWrite(_cs, LOW);
--  _pSPIx->transfer(c);
-+  _spi_write(c);
-   if (_cs >= 0) digitalWrite(_cs, HIGH);
-   if (_dc >= 0) digitalWrite(_dc, HIGH);
--  _pSPIx->endTransaction();
-+  _endTransaction();
- }
- 
- void GxEPD2_EPD::_writeData(uint8_t d)
- {
--  _pSPIx->beginTransaction(_spi_settings);
-+  _beginTransaction(_spi_settings);
-   if (_cs >= 0) digitalWrite(_cs, LOW);
--  _pSPIx->transfer(d);
-+  _spi_write(d);
-   if (_cs >= 0) digitalWrite(_cs, HIGH);
--  _pSPIx->endTransaction();
-+  _endTransaction();
- }
- 
- void GxEPD2_EPD::_writeData(const uint8_t* data, uint16_t n)
- {
--  _pSPIx->beginTransaction(_spi_settings);
-+  _beginTransaction(_spi_settings);
-   if (_cs >= 0) digitalWrite(_cs, LOW);
--  for (uint16_t i = 0; i < n; i++)
-+  for (uint8_t i = 0; i < n; i++)
-   {
--    _pSPIx->transfer(*data++);
-+    _spi_write(*data++);
-   }
-   if (_cs >= 0) digitalWrite(_cs, HIGH);
--  _pSPIx->endTransaction();
-+  _endTransaction();
- }
- 
- void GxEPD2_EPD::_writeDataPGM(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes)
- {
--  _pSPIx->beginTransaction(_spi_settings);
-+  _beginTransaction(_spi_settings);
-   if (_cs >= 0) digitalWrite(_cs, LOW);
--  for (uint16_t i = 0; i < n; i++)
-+  for (uint8_t i = 0; i < n; i++)
-   {
--    _pSPIx->transfer(pgm_read_byte(&*data++));
-+    _spi_write(pgm_read_byte(&*data++));
-   }
-   while (fill_with_zeroes > 0)
-   {
--    _pSPIx->transfer(0x00);
-+    _spi_write(0x00);
-     fill_with_zeroes--;
-   }
-   if (_cs >= 0) digitalWrite(_cs, HIGH);
--  _pSPIx->endTransaction();
-+  _endTransaction();
- }
- 
- void GxEPD2_EPD::_writeDataPGM_sCS(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes)
- {
--  _pSPIx->beginTransaction(_spi_settings);
-+  _beginTransaction(_spi_settings);
-   for (uint8_t i = 0; i < n; i++)
-   {
-     if (_cs >= 0) digitalWrite(_cs, LOW);
--    _pSPIx->transfer(pgm_read_byte(&*data++));
-+    _spi_write(pgm_read_byte(&*data++));
-     if (_cs >= 0) digitalWrite(_cs, HIGH);
-   }
-   while (fill_with_zeroes > 0)
-   {
-     if (_cs >= 0) digitalWrite(_cs, LOW);
--    _pSPIx->transfer(0x00);
-+    _spi_write(0x00);
-     fill_with_zeroes--;
-     if (_cs >= 0) digitalWrite(_cs, HIGH);
-   }
--  _pSPIx->endTransaction();
-+  _endTransaction();
- }
- 
- void GxEPD2_EPD::_writeCommandData(const uint8_t* pCommandData, uint8_t datalen)
- {
--  _pSPIx->beginTransaction(_spi_settings);
-+  _beginTransaction(_spi_settings);
-   if (_dc >= 0) digitalWrite(_dc, LOW);
-   if (_cs >= 0) digitalWrite(_cs, LOW);
--  _pSPIx->transfer(*pCommandData++);
-+  _spi_write(*pCommandData++);
-   if (_dc >= 0) digitalWrite(_dc, HIGH);
-   for (uint8_t i = 0; i < datalen - 1; i++)  // sub the command
-   {
--    _pSPIx->transfer(*pCommandData++);
-+    _spi_write(*pCommandData++);
-   }
-   if (_cs >= 0) digitalWrite(_cs, HIGH);
--  _pSPIx->endTransaction();
-+  _endTransaction();
- }
- 
- void GxEPD2_EPD::_writeCommandDataPGM(const uint8_t* pCommandData, uint8_t datalen)
- {
--  _pSPIx->beginTransaction(_spi_settings);
-+  _beginTransaction(_spi_settings);
-   if (_dc >= 0) digitalWrite(_dc, LOW);
-   if (_cs >= 0) digitalWrite(_cs, LOW);
--  _pSPIx->transfer(pgm_read_byte(&*pCommandData++));
-+  _spi_write(pgm_read_byte(&*pCommandData++));
-   if (_dc >= 0) digitalWrite(_dc, HIGH);
-   for (uint8_t i = 0; i < datalen - 1; i++)  // sub the command
-   {
--    _pSPIx->transfer(pgm_read_byte(&*pCommandData++));
-+    _spi_write(pgm_read_byte(&*pCommandData++));
-   }
-   if (_cs >= 0) digitalWrite(_cs, HIGH);
--  _pSPIx->endTransaction();
-+  _endTransaction();
- }
- 
- void GxEPD2_EPD::_startTransfer()
- {
--  _pSPIx->beginTransaction(_spi_settings);
-+  _beginTransaction(_spi_settings);
-   if (_cs >= 0) digitalWrite(_cs, LOW);
- }
- 
- void GxEPD2_EPD::_transfer(uint8_t value)
- {
--  _pSPIx->transfer(value);
-+  _spi_write(value);
- }
- 
- void GxEPD2_EPD::_endTransfer()
- {
-   if (_cs >= 0) digitalWrite(_cs, HIGH);
--  _pSPIx->endTransaction();
-+  _endTransaction();
-+}
-+
-+void GxEPD2_EPD::_beginTransaction(const SPISettings& settings)
-+{
-+  if (_sck < 0) SPI.beginTransaction(settings);
-+}
-+
-+void GxEPD2_EPD::_spi_write(uint8_t data)
-+{
-+  if (_sck < 0) SPI.transfer(data);
-+  else
-+  {
-+#if defined (ESP8266)
-+    yield();
-+#endif
-+    for (int i = 0; i < 8; i++)
-+    {
-+      digitalWrite(_mosi, (data & 0x80) ? HIGH : LOW);
-+      data <<= 1;
-+      digitalWrite(_sck, HIGH);
-+      digitalWrite(_sck, LOW);
-+    }
-+  }
-+}
-+
-+void GxEPD2_EPD::_endTransaction()
-+{
-+  if (_sck < 0) SPI.endTransaction();
-+}
-+
-+uint8_t GxEPD2_EPD::_readData()
-+{
-+  uint8_t data = 0;
-+  _beginTransaction(_spi_settings);
-+  if (_cs >= 0) digitalWrite(_cs, LOW);
-+  if (_sck < 0)
-+  {
-+    data = SPI.transfer(0);
-+  }
-+  else
-+  {
-+    pinMode(_mosi, INPUT);
-+    for (int i = 0; i < 8; i++)
-+    {
-+      data <<= 1;
-+      digitalWrite(_sck, HIGH);
-+      data |= digitalRead(_mosi);
-+      digitalWrite(_sck, LOW);
-+    }
-+    pinMode(_mosi, OUTPUT);
-+  }
-+  if (_cs >= 0) digitalWrite(_cs, HIGH);
-+  _endTransaction();
-+  return data;
-+}
-+
-+void GxEPD2_EPD::_readData(uint8_t* data, uint16_t n)
-+{
-+  _beginTransaction(_spi_settings);
-+  if (_cs >= 0) digitalWrite(_cs, LOW);
-+  if (_sck < 0)
-+  {
-+    for (uint8_t i = 0; i < n; i++)
-+    {
-+      *data++ = SPI.transfer(0);
-+    }
-+  }
-+  else
-+  {
-+    pinMode(_mosi, INPUT);
-+    for (uint8_t i = 0; i < n; i++)
-+    {
-+      *data = 0;
-+      for (int i = 0; i < 8; i++)
-+      {
-+        *data <<= 1;
-+        digitalWrite(_sck, HIGH);
-+        *data |= digitalRead(_mosi);
-+        digitalWrite(_sck, LOW);
-+      }
-+      data++;
-+    }
-+    pinMode(_mosi, OUTPUT);
-+  }
-+  if (_cs >= 0) digitalWrite(_cs, HIGH);
-+  _endTransaction();
- }
-diff --git a/src/GxEPD2_EPD.h b/src/GxEPD2_EPD.h
-index 34c1145..c480b7d 100644
---- a/src/GxEPD2_EPD.h
-+++ b/src/GxEPD2_EPD.h
-@@ -8,6 +8,10 @@
- // Version: see library.properties
- //
- // Library: https://github.com/ZinggJM/GxEPD2
-+// To use SW SPI with GxEPD2:
-+//   add the special call to the added init method BEFORE the normal init method:
-+//   display.epd2.init(SW_SCK, SW_MOSI, 115200, true, 20, false); // define or replace SW_SCK, SW_MOSI
-+//   display.init(115200); // needed to init upper level
- 
- #ifndef _GxEPD2_EPD_H_
- #define _GxEPD2_EPD_H_
-@@ -35,6 +39,7 @@ class GxEPD2_EPD
-                uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu);
-     virtual void init(uint32_t serial_diag_bitrate = 0); // serial_diag_bitrate = 0 : disabled
-     virtual void init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 10, bool pulldown_rst_mode = false);
-+    virtual void init(int16_t sck, int16_t mosi, uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 20, bool pulldown_rst_mode = false);
-     virtual void end(); // release SPI and control pins
-     //  Support for Bitmaps (Sprites) to Controller Buffer and to Screen
-     virtual void clearScreen(uint8_t value) = 0; // init controller memory and screen (default white)
-@@ -97,7 +102,6 @@ class GxEPD2_EPD
-     {
-       return (a > b ? a : b);
-     };
--    void selectSPI(SPIClass& spi, SPISettings spi_settings);
-   protected:
-     void _reset();
-     void _waitWhileBusy(const char* comment = 0, uint16_t busy_time = 5000);
-@@ -111,17 +115,22 @@ class GxEPD2_EPD
-     void _startTransfer();
-     void _transfer(uint8_t value);
-     void _endTransfer();
-+    void _beginTransaction(const SPISettings& settings);
-+    void _spi_write(uint8_t data);
-+    void _endTransaction();
-+  public:
-+    uint8_t _readData();
-+    void _readData(uint8_t* data, uint16_t n);
-   protected:
--    int16_t _cs, _dc, _rst, _busy, _busy_level;
-+    int16_t _cs, _dc, _rst, _busy, _busy_level, _sck, _mosi;;
-     uint32_t _busy_timeout;
-     bool _diag_enabled, _pulldown_rst_mode;
--    SPIClass* _pSPIx;
-     SPISettings _spi_settings;
-     bool _initial_write, _initial_refresh;
-     bool _power_is_on, _using_partial_mode, _hibernating;
-     bool _init_display_done;
-     uint16_t _reset_duration;
--    void (*_busy_callback)(const void*); 
-+    void (*_busy_callback)(const void*);
-     const void* _busy_callback_parameter;
- };
- 
diff --git a/scripts/add_littlefs_binary.py b/scripts/add_littlefs_binary.py
new file mode 100644
index 00000000..ffc948cb
--- /dev/null
+++ b/scripts/add_littlefs_binary.py
@@ -0,0 +1,79 @@
+import os
+import subprocess
+import shutil
+from SCons.Script import DefaultEnvironment
+Import("env")
+
+
+def build_littlefs():
+    if os.path.isfile('data/settings.json') == False:
+        return # nothing to do
+
+    result = subprocess.run(["pio", "run", "--target", "buildfs", "--environment", env['PIOENV']])
+    if result.returncode != 0:
+        print("Error building LittleFS:")
+        exit(1)
+    else:
+        print("LittleFS build successful")
+
+def merge_bins():
+    if os.path.isfile('data/settings.json') == False:
+        return # nothing to do
+
+    BOOTLOADER_OFFSET = 0x0000
+    PARTITIONS_OFFSET = 0x8000
+    FIRMWARE_OFFSET   = 0x10000
+
+    if env['PIOENV'][:13] == "esp32-wroom32":
+        BOOTLOADER_OFFSET = 0x1000
+
+    flash_size = int(env.BoardConfig().get("upload.maximum_size", "1310720")) # 0x140000
+    app0_offset = 0x10000
+    if env['PIOENV'][:7] == "esp8266":
+        app0_offset = 0
+    elif env['PIOENV'][:7] == "esp8285":
+        app0_offset = 0
+
+    littlefs_offset = 0x290000
+    if flash_size == 0x330000:
+        littlefs_offset = 0x670000
+    elif flash_size == 0x640000:
+        littlefs_offset = 0xc90000
+
+    # save current wd
+    start = os.getcwd()
+    os.chdir('.pio/build/' + env['PIOENV'] + '/')
+
+    with open("bootloader.bin", "rb") as bootloader_file:
+        bootloader_data = bootloader_file.read()
+
+    with open("partitions.bin", "rb") as partitions_file:
+        partitions_data = partitions_file.read()
+
+    with open("firmware.bin", "rb") as firmware_file:
+        firmware_data = firmware_file.read()
+
+    with open("littlefs.bin", "rb") as littlefs_file:
+        littlefs_data = littlefs_file.read()
+
+    with open("firmware.factory.bin", "wb") as merged_file:
+        merged_file.write(b'\xFF' * BOOTLOADER_OFFSET)
+        merged_file.write(bootloader_data)
+
+        merged_file.write(b'\xFF' * (PARTITIONS_OFFSET - (BOOTLOADER_OFFSET + len(bootloader_data))))
+        merged_file.write(partitions_data)
+
+        merged_file.write(b'\xFF' * (FIRMWARE_OFFSET - (PARTITIONS_OFFSET + len(partitions_data))))
+        merged_file.write(firmware_data)
+
+        merged_file.write(b'\xFF' * (littlefs_offset - (FIRMWARE_OFFSET + len(firmware_data))))
+        merged_file.write(littlefs_data)
+
+    os.chdir(start)
+
+def main(target, source, env):
+    build_littlefs()
+    merge_bins()
+
+# ensure that script is called once firmeware was compiled
+env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", main)
diff --git a/scripts/applyPatches.py b/scripts/applyPatches.py
index 147fb0f3..1672ab2f 100644
--- a/scripts/applyPatches.py
+++ b/scripts/applyPatches.py
@@ -26,9 +26,10 @@ def applyPatch(libName, patchFile):
 
 
 # list of patches to apply (relative to /src)
-if env['PIOENV'][:22] != "opendtufusion-ethernet":
-    applyPatch("ESP Async WebServer", "../patches/AsyncWeb_Prometheus.patch")
+applyPatch("ESPAsyncWebServer-esphome", "../patches/AsyncWeb_Prometheus.patch")
 
-if env['PIOENV'][:13] == "opendtufusion":
-    applyPatch("GxEPD2", "../patches/GxEPD2_SW_SPI.patch")
+if (env['PIOENV'][:5] == "esp32") or (env['PIOENV'][:13] == "opendtufusion"):
+    applyPatch("GxEPD2", "../patches/GxEPD2_HAL.patch")
+
+if (env['PIOENV'][:13] == "opendtufusion") or (env['PIOENV'][:5] == "esp32"):
     applyPatch("RF24", "../patches/RF24_Hal.patch")
diff --git a/scripts/buildManifest.py b/scripts/buildManifest.py
index 2664a39f..b91145cd 100644
--- a/scripts/buildManifest.py
+++ b/scripts/buildManifest.py
@@ -36,9 +36,27 @@ def buildManifest(path, infile, outfile):
     esp32["parts"].append({"path": "ESP32/bootloader.bin", "offset": 4096})
     esp32["parts"].append({"path": "ESP32/partitions.bin", "offset": 32768})
     esp32["parts"].append({"path": "ESP32/ota.bin", "offset": 57344})
-    esp32["parts"].append({"path": "ESP32/" + version[1] + "_" + sha + "_esp32.bin", "offset": 65536})
+    esp32["parts"].append({"path": "ESP32/" + version[1] + "_" + sha + "_esp32-wroom32.bin", "offset": 65536})
     data["builds"].append(esp32)
 
+    esp32s2 = {}
+    esp32s2["chipFamily"] = "ESP32-S2"
+    esp32s2["parts"] = []
+    esp32s2["parts"].append({"path": "ESP32-S2/bootloader.bin", "offset": 4096})
+    esp32s2["parts"].append({"path": "ESP32-S2/partitions.bin", "offset": 32768})
+    esp32s2["parts"].append({"path": "ESP32-S2/ota.bin", "offset": 57344})
+    esp32s2["parts"].append({"path": "ESP32-S2/" + version[1] + "_" + sha + "_esp32-s2-mini.bin", "offset": 65536})
+    data["builds"].append(esp32s2)
+
+    esp32s3 = {}
+    esp32s3["chipFamily"] = "ESP32-S3"
+    esp32s3["parts"] = []
+    esp32s3["parts"].append({"path": "ESP32/bootloader.bin", "offset": 4096})
+    esp32s3["parts"].append({"path": "ESP32/partitions.bin", "offset": 32768})
+    esp32s3["parts"].append({"path": "ESP32/ota.bin", "offset": 57344})
+    esp32s3["parts"].append({"path": "ESP32-S3/" + version[1] + "_" + sha + "_opendtufusion.bin", "offset": 65536})
+    data["builds"].append(esp32s3)
+
     esp8266 = {}
     esp8266["chipFamily"] = "ESP8266"
     esp8266["parts"] = []
@@ -47,7 +65,7 @@ def buildManifest(path, infile, outfile):
 
     jsonString = json.dumps(data, indent=2)
 
-    fp = open(path + "firmware/" + outfile, "w")
+    fp = open(path + "../firmware/" + outfile, "w")
     fp.write(jsonString)
     fp.close()
 
diff --git a/scripts/convertHtml.py b/scripts/convertHtml.py
index c39e95ac..9a83d081 100644
--- a/scripts/convertHtml.py
+++ b/scripts/convertHtml.py
@@ -7,8 +7,58 @@ import json
 from datetime import date
 from pathlib import Path
 import subprocess
+import configparser
 Import("env")
-
+build_flags = []
+
+import htmlPreprocessorDefines as prepro
+
+def getFlagsOfEnv(env):
+    config = configparser.ConfigParser()
+    config.read('platformio.ini')
+    global build_flags
+    flags = config[env]['build_flags'].split('\n')
+
+    for i in range(len(flags)):
+        if flags[i][:2] == "-D" or flags[i][:2] == "${":
+            flags[i] = flags[i][2:]
+        if flags[i][-13:-1] == ".build_flags":
+            getFlagsOfEnv(flags[i].split(".build_flags")[0])
+        elif len(flags[i]) > 0:
+            build_flags = build_flags + [flags[i]]
+
+def parseDefinesH():
+    global build_flags
+    pattern = r'^\s*#\s*define\s+(\w+)'
+
+    with open("defines.h", "r") as f:
+        for line in f:
+            match = re.match(pattern, line)
+            if match:
+                build_flags += [match.group(1)]
+
+
+def get_build_flags():
+    getFlagsOfEnv("env:" + env['PIOENV'])
+    config = configparser.ConfigParser()
+    config.read('platformio.ini')
+    parseDefinesH()
+
+    # translate board
+    board = config["env:" + env['PIOENV']]['board']
+    if board == "esp12e" or board == "esp8285":
+        build_flags.append("ESP8266")
+    elif board == "lolin_d32":
+        build_flags.append("ESP32")
+    elif board == "lolin_s2_mini":
+        build_flags.append("ESP32")
+        build_flags.append("ESP32-S2")
+    elif board == "lolin_c3_mini":
+        build_flags.append("ESP32")
+        build_flags.append("ESP32-C3")
+    elif board == "esp32-s3-devkitc-1":
+        build_flags.append("ESP32")
+        build_flags.append("ESP32-S3")
 
 def get_git_sha():
     try:
@@ -50,38 +100,46 @@ def readVersionFull(path):
     return version
 
 def htmlParts(file, header, nav, footer, versionPath, lang):
-    p = "";
     f = open(file, "r")
     lines = f.readlines()
     f.close();
 
     f = open(header, "r")
-    h = f.read().strip()
+    h = f.readlines()
     f.close()
 
     f = open(nav, "r")
-    n = f.read().strip()
+    n = f.readlines()
     f.close()
 
     f = open(footer, "r")
-    fo = f.read().strip()
+    fo = f.readlines()
     f.close()
 
+    linesExt = []
     for line in lines:
-        line = line.replace("{#HTML_HEADER}", h)
-        line = line.replace("{#HTML_NAV}", n)
-        line = line.replace("{#HTML_FOOTER}", fo)
-        p += line
+        if line.find("{#HTML_HEADER}") != -1:
+            linesExt.extend(h)
+        elif line.find("{#HTML_NAV}") != -1:
+            linesExt.extend(n)
+        elif line.find("{#HTML_FOOTER}") != -1:
+            linesExt.extend(fo)
+        else:
+            linesExt.append(line)
+
+    linesMod = prepro.conv(linesExt, build_flags)
 
     #placeholders
     version = readVersion(versionPath);
     link = '<a target="_blank" href="https://github.com/lumapu/ahoy/commits/' + get_git_sha() + '">GIT SHA: ' + get_git_sha() + ' :: ' + version + '</a>'
+    p = ""
+    for line in linesMod:
+        p += line
+
     p = p.replace("{#VERSION}", version)
     p = p.replace("{#VERSION_FULL}", readVersionFull(versionPath))
     p = p.replace("{#VERSION_GIT}", link)
 
-    # remove if - endif ESP32
-    p = checkIf(p)
     p = translate(file, p, lang)
     p = translate("general", p, lang) # menu / header / footer
 
@@ -90,30 +148,6 @@ def htmlParts(file, header, nav, footer, versionPath, lang):
     f.close();
     return p
 
-def checkIf(data):
-    if (env['PIOENV'][0:5] == "esp32") or env['PIOENV'][0:4] == "open":
-        data = data.replace("<!--IF_ESP32-->", "")
-        data = data.replace("<!--ENDIF_ESP32-->", "")
-        data = data.replace("/*IF_ESP32*/", "")
-        data = data.replace("/*ENDIF_ESP32*/", "")
-    else:
-        while 1:
-            start = data.find("<!--IF_ESP32-->")
-            end = data.find("<!--ENDIF_ESP32-->")+18
-            if -1 == start:
-                break
-            else:
-                data = data[0:start] + data[end:]
-        while 1:
-            start = data.find("/*IF_ESP32*/")
-            end = data.find("/*ENDIF_ESP32*/")+15
-            if -1 == start:
-                break
-            else:
-                data = data[0:start] + data[end:]
-
-    return data
-
 def findLang(file):
     with open('../lang.json') as j:
         lang = json.load(j)
@@ -189,33 +223,41 @@ def convert2Header(inFile, versionPath, lang):
     f.write("#endif /*__{}_{}_H__*/\n".format(define, define2))
     f.close()
 
-# delete all files in the 'h' dir
-wd = 'web/html/h'
-
-if os.path.exists(wd):
-    for f in os.listdir(wd):
-        os.remove(os.path.join(wd, f))
-wd += "/tmp"
-if os.path.exists(wd):
-    for f in os.listdir(wd):
-        os.remove(os.path.join(wd, f))
-
-# grab all files with following extensions
-os.chdir('./web/html')
-types = ('*.html', '*.css', '*.js', '*.ico', '*.json') # the tuple of file types
-files_grabbed = []
-for files in types:
-    files_grabbed.extend(glob.glob(files))
-
-Path("h").mkdir(exist_ok=True)
-Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with all replacements
-shutil.copyfile("style.css", "tmp/style.css")
-
-# get language from environment
-lang = "en"
-if env['PIOENV'][-3:] == "-de":
-    lang = "de"
-
-# go throw the array
-for val in files_grabbed:
-    convert2Header(val, "../../defines.h", lang)
+
+def main():
+    get_build_flags()
+
+    # delete all files in the 'h' dir
+    wd = 'web/html/h'
+
+    if os.path.exists(wd):
+        for f in os.listdir(wd):
+            os.remove(os.path.join(wd, f))
+    wd += "/tmp"
+    if os.path.exists(wd):
+        for f in os.listdir(wd):
+            os.remove(os.path.join(wd, f))
+
+    # grab all files with following extensions
+    os.chdir('./web/html')
+    types = ('*.html', '*.css', '*.js', '*.ico', '*.json') # the tuple of file types
+    files_grabbed = []
+    for files in types:
+        files_grabbed.extend(glob.glob(files))
+
+    Path("h").mkdir(exist_ok=True)
+    Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with all replacements
+    shutil.copyfile("style.css", "tmp/style.css")
+
+    # get language from environment
+    lang = "en"
+    if env['PIOENV'][-3:] == "-de":
+        lang = "de"
+
+
+    # go throw the array
+    for val in files_grabbed:
+        convert2Header(val, "../../defines.h", lang)
+
+
+main()
diff --git a/scripts/getVersion.py b/scripts/getVersion.py
index a60a772d..f579b56a 100644
--- a/scripts/getVersion.py
+++ b/scripts/getVersion.py
@@ -76,8 +76,9 @@ def renameFw(path_define, env):
     fname = version[:-1] + "_" + sha + "_" + env + ".bin"
 
     os.rename("src/.pio/build/" + env + "/firmware.bin", dst + fname)
+    os.rename("src/.pio/build/" + env + "/firmware.elf.7z", dst + fname[:-3] + "elf.7z")
 
-    if env[:5] == "esp32":
+    if env[:5] == "esp32" or env[:4] == "open":
         os.rename("src/.pio/build/" + env + "/bootloader.bin", dst + "bootloader.bin")
         os.rename("src/.pio/build/" + env + "/partitions.bin", dst + "partitions.bin")
         genOtaBin(dst)
diff --git a/scripts/htmlPreprocessorDefines.py b/scripts/htmlPreprocessorDefines.py
new file mode 100644
index 00000000..f5d7cc31
--- /dev/null
+++ b/scripts/htmlPreprocessorDefines.py
@@ -0,0 +1,40 @@
+import re
+import os
+import queue
+
+def error(msg):
+    print("ERROR: " + msg)
+    exit()
+
+def check(inp, lst, pattern):
+    q = queue.LifoQueue()
+    out = []
+    keep = True
+    for line in inp:
+        x = re.findall(pattern, line)
+        if len(x) > 0:
+            if line.find("ENDIF_") != -1:
+                if not q.empty():
+                    e = q.get()
+                    if e[0] == x[0]:
+                        keep = e[1]
+            elif line.find("IF_") != -1:
+                q.put((x[0], keep))
+                if keep is True:
+                    keep = x[0] in lst
+            elif line.find("E") != -1:
+                if q.empty():
+                    error("(ELSE) missing open statement!")
+                e = q.get()
+                q.put(e)
+                if e[1] is True:
+                    keep = not keep
+        else:
+            if keep is True:
+                out.append(line)
+    return out
+
+def conv(inp, lst):
+    #print(lst)
+    out = check(inp, lst, r'\/\*(?:IF_|ELS|ENDIF_)([A-Z0-9\-_]+)?\*\/')
+    return check(out, lst, r'\<\!\-\-(?:IF_|ELS|ENDIF_)([A-Z0-9\-_]+)?\-\-\>')
diff --git a/src/.gitignore b/src/.gitignore
index 89cc49cb..30f1d1ca 100644
--- a/src/.gitignore
+++ b/src/.gitignore
@@ -3,3 +3,5 @@
 .vscode/c_cpp_properties.json
 .vscode/launch.json
 .vscode/ipch
+scripts/__pycache__/*
+*.pyc
diff --git a/src/CHANGES.md b/src/CHANGES.md
index 5fd4fc31..cede20cb 100644
--- a/src/CHANGES.md
+++ b/src/CHANGES.md
@@ -1,41 +1,54 @@
-Changelog v0.8.83
+Changelog v0.8.140
 
-* added German translations for all variants
-* added reading grid profile
-* added decimal place for active power control (APC aka power limit)
-* added information about working IRQ for NRF24 and CMT2300A to `/system`
-* added loss rate to `/visualization` in the statistics window and MqTT
-* added optional output to display whether it's night time or not. Can be reused as output to control battery system or mapped to a LED
-* added timestamp for `max ac power` as tooltip
-* added wizard for initial WiFi connection
-* added history graph (still under development)
-* added simulator (must be activated before compile, standard: off)
-* added minimal version (without: MqTT, Display, History), WebUI is not changed! (not compiled automatically)
-* added info about installed binary to `/update`
-* added protection to prevent update to wrong firmware (environment check)
-* added optional custom link to the menu
-* added support for other regions (USA, Indonesia)
-* added warning for WiFi channel 12-14 (ESP8266 only)
-* added `max_power` to MqTT total values
-* added API-Token authentification for external scripts
-* improved MqTT by marking sent data and improved `last_success` resends
-* improved communication for HM and MI inverters
-* improved reading live data from inverter
-* improved sending active power control command faster
-* improved `/settings`: pinout has an own subgroup
-* improved export by saving settings before they are exported (to have everything in JSON)
-* improved code quality (cppcheck)
-* seperated sunrise and sunset offset to two fields
-* fix MqTT night communication
-* fix missing favicon to html header
-* fix build on Windows of `opendtufusion` environments (git: trailing whitespaces)
-* fix generation of DTU-ID
-* fix: protect commands from popup in `/live` if password is set
-* fix: prevent sending commands to inverter which isn't active
-* combined firmware and hardware version to JSON topics (MqTT)
-* updated Prometheus with latest changes
-* upgraded most libraries to newer versions
-* beautified typography, added spaces between value and unit for `/visualization`
-* removed add to total (MqTT) inverter setting
+* added HMS-400-1T support (serial number 1125...)
+* added further ESP8266 versions (-all, -minimal) because of small ressources on ESP8266
+* added some Gridprofiles
+* added support for characters in serial number of inverter (A-F)
+* added default coordinates on fresh install, needed for history graph on display and WebUI
+* added option to reset values on communication start (sunrise)
+* added max inverter temperature to WebUI
+* added yield day to history graph
+* added script and [instructions](../manual/factory_firmware.md) how to generate factory firmware which includes predefined settings
+* added button for downloading coredump (ESP32 variants only) to `/system`. Once a crash happens the reason can be checked afterwards (even after a reboot)
+* added support of HERF inverters, serial number is converted in Javascript
+* added device name to HTML title
+* added feature to restart Ahoy using MqTT
+* added feature to publish MqTT messages as JSON as well (new setting)
+* add timestamp to JSON output
+* improved communication to inverter
+* improved translation to German
+* improved HTML pages, reduced in size by only including relevant contents depending by chip type
+* improved history graph in WebUI
+* improved network routines
+* improved Wizard
+* improved WebUI by disabling upload and import buttons when no file is selected
+* improved queue, only add new object once they not exist in queue
+* improved MqTT `OnMessage` (threadsafe)
+* improved read of alarms, prevent duplicates, update alarm time if there is an update
+* improved alarms are now sorted in ascending direction
+* improved by prevent add inverter multiple times
+* improved sending active power controll commands
+* improved refresh routine of ePaper, full refresh each 12h
+* redesigned WebUI on `/system`
+* changed MqTT retained flags
+* change MqTT return value of power limit acknowledge from `boolean` to `float`. The value returned is the same as it was set to confirm reception (not the read back value)
+* converted ePaper and Ethernet to hal-SPI
+* combined Ethernet and WiFi variants - Ethernet is now always included, but needs to be enabled if needed
+* changed: Ethernet variants (W5500) now support WiFi as fall back / configuration
+* switch AsyncWebserver library
+* fixed autodiscovery for homeassistant
+* fix reset values functionality
+* fix read back of active power control value, now it has one decimal place
+* fix NTP issues
+* fixed MqTT discovery field `ALARM_MES_ID`
+* fix close button color of modal windows in dark mode
+* fixed calculation of max AC power
+* fixed reset values at midnight if WiFi isn't available
+* fixed HMT-1800-4T number of inputs
+* fix crash if invalid serial number was set -> inverter will be disabled automatically
+* fixed ESP8266, ESP32 static IP
+* fixed ethernet MAC address read back
+* update several libraries to more recent versions
+* removed `yield efficiency` because the inverter already calculates correct
 
 full version log: [Development Log](https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md)
diff --git a/src/app.cpp b/src/app.cpp
index 0bead49e..c6a689ee 100644
--- a/src/app.cpp
+++ b/src/app.cpp
@@ -33,44 +33,33 @@ void app::setup() {
     resetSystem();
     esp_task_wdt_reset();
 
-    mSettings.setup();
-    mSettings.getPtr(mConfig);
+    mSettings.setup(mConfig);
     ah::Scheduler::setup(mConfig->inst.startWithoutTime);
     DPRINT(DBG_INFO, F("Settings valid: "));
-    DSERIAL.flush();
-    if (mSettings.getValid())
-        DBGPRINTLN(F("true"));
-    else
-        DBGPRINTLN(F("false"));
+    DBGPRINTLN(mConfig->valid ? F("true") : F("false"));
 
     esp_task_wdt_reset();
 
-    if(mConfig->nrf.enabled) {
-        mNrfRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso);
-    }
+    mNrfRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, &mConfig->nrf);
     #if defined(ESP32)
-    if(mConfig->cmt.enabled) {
-        mCmtRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->cmt.pinSclk, mConfig->cmt.pinSdio, mConfig->cmt.pinCsb, mConfig->cmt.pinFcsb, mConfig->sys.region);
-    }
+    mCmtRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, &mConfig->cmt, mConfig->sys.region);
     #endif
+
     #ifdef ETHERNET
         delay(1000);
-        mEth.setup(mConfig, &mTimestamp, [this](bool gotIp) { this->onNetwork(gotIp); }, [this](bool gotTime) { this->onNtpUpdate(gotTime); });
-    #endif // ETHERNET
-
-    #if !defined(ETHERNET)
-        mWifi.setup(mConfig, &mTimestamp, std::bind(&app::onNetwork, this, std::placeholders::_1));
-        #if !defined(AP_ONLY)
-            everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL");
-        #endif
-    #endif /* defined(ETHERNET) */
+        mNetwork = static_cast<AhoyNetwork*>(new AhoyEthernet());
+    #else
+        mNetwork = static_cast<AhoyNetwork*>(new AhoyWifi());
+    #endif
+    mNetwork->setup(mConfig, &mTimestamp, [this](bool gotIp) { this->onNetwork(gotIp); }, [this](bool gotTime) { this->onNtpUpdate(gotTime); });
+    mNetwork->begin();
 
     esp_task_wdt_reset();
 
     mCommunication.setup(&mTimestamp, &mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace);
-    mCommunication.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2));
+    mCommunication.addPayloadListener([this] (uint8_t cmd, Inverter<> *iv) { payloadEventListener(cmd, iv); });
     #if defined(ENABLE_MQTT)
-    mCommunication.addPowerLimitAckListener([this] (Inverter<> *iv) { mMqtt.setPowerLimitAck(iv); });
+        mCommunication.addPowerLimitAckListener([this] (Inverter<> *iv) { mMqtt.setPowerLimitAck(iv); });
     #endif
     mSys.setup(&mTimestamp, &mConfig->inst, this);
     for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
@@ -85,16 +74,14 @@ void app::setup() {
     esp_task_wdt_reset();
 
     // when WiFi is in client mode, then enable mqtt broker
-    #if !defined(AP_ONLY)
     #if defined(ENABLE_MQTT)
     mMqttEnabled = (mConfig->mqtt.broker[0] > 0);
     if (mMqttEnabled) {
-        mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp, &mUptime);
+        mMqtt.setup(this, &mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp, &mUptime);
         mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1));
         mCommunication.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); });
     }
     #endif
-    #endif
     setupLed();
 
     esp_task_wdt_reset();
@@ -107,6 +94,7 @@ void app::setup() {
     mDbgSyslog.setup(mConfig); // be sure to init after mWeb.setup (webSerial uses also debug callback)
     #endif
     // Plugins
+    mMaxPower.setup(&mTimestamp, mConfig->inst.sendInterval);
     #if defined(PLUGIN_DISPLAY)
     if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
         #if defined(ESP32)
@@ -124,18 +112,13 @@ void app::setup() {
 
     mPubSerial.setup(mConfig, &mSys, &mTimestamp);
 
-    #if !defined(ETHERNET)
     //mImprov.setup(this, mConfig->sys.deviceName, mVersion);
-    #endif
 
     #if defined(ENABLE_SIMULATOR)
     mSimulator.setup(&mSys, &mTimestamp, 0);
-    mSimulator.addPayloadListener([this](uint8_t cmd, Inverter<> *iv) {
-        payloadEventListener(cmd, iv);
-    });
+    mSimulator.addPayloadListener([this](uint8_t cmd, Inverter<> *iv) { payloadEventListener(cmd, iv); });
     #endif /*ENABLE_SIMULATOR*/
 
-
     esp_task_wdt_reset();
     regularTickers();
 }
@@ -144,12 +127,10 @@ void app::setup() {
 void app::loop(void) {
     esp_task_wdt_reset();
 
-    if(mConfig->nrf.enabled)
-        mNrfRadio.loop();
+    mNrfRadio.loop();
 
     #if defined(ESP32)
-    if(mConfig->cmt.enabled)
-        mCmtRadio.loop();
+    mCmtRadio.loop();
     #endif
 
     ah::Scheduler::loop();
@@ -159,25 +140,24 @@ void app::loop(void) {
     if (mMqttEnabled && mNetworkConnected)
         mMqtt.loop();
     #endif
+
+    #if defined(PLUGIN_DISPLAY)
+    mDisplay.loop();
+    #endif
     yield();
 }
 
 //-----------------------------------------------------------------------------
 void app::onNetwork(bool gotIp) {
-    DPRINTLN(DBG_DEBUG, F("onNetwork"));
     mNetworkConnected = gotIp;
-    ah::Scheduler::resetTicker();
-    regularTickers(); //reinstall regular tickers
-    every(std::bind(&app::tickSend, this), mConfig->inst.sendInterval, "tSend");
-    mMqttReconnect = true;
-    mSunrise = 0;  // needs to be set to 0, to reinstall sunrise and ivComm tickers!
-    once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2");
-    #if !defined(ETHERNET)
-    if (WIFI_AP == WiFi.getMode()) {
-        mMqttEnabled = false;
+    if(gotIp) {
+        ah::Scheduler::resetTicker();
+        regularTickers(); //reinstall regular tickers
+        every(std::bind(&app::tickSend, this), mConfig->inst.sendInterval, "tSend");
+        mTickerInstallOnce = true;
+        mSunrise = 0;  // needs to be set to 0, to reinstall sunrise and ivComm tickers!
+        once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2");
     }
-    everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL");
-    #endif /* !defined(ETHERNET) */
 }
 
 //-----------------------------------------------------------------------------
@@ -185,6 +165,10 @@ void app::regularTickers(void) {
     DPRINTLN(DBG_DEBUG, F("regularTickers"));
     everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc");
     everySec([this]() { mProtection->tickSecond(); }, "prot");
+    everySec([this]() {mNetwork->tickNetworkLoop(); }, "net");
+
+    if(mConfig->inst.startWithoutTime && !mNetworkConnected)
+        every(std::bind(&app::tickSend, this), mConfig->inst.sendInterval, "tSend");
 
     // Plugins
     #if defined(PLUGIN_DISPLAY)
@@ -192,9 +176,7 @@ void app::regularTickers(void) {
         everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp");
     #endif
     every(std::bind(&PubSerialType::tick, &mPubSerial), 5, "uart");
-    #if !defined(ETHERNET)
     //everySec([this]() { mImprov.tickSerial(); }, "impro");
-    #endif
 
     #if defined(ENABLE_HISTORY)
     everySec(std::bind(&HistoryType::tickerSecond, &mHistory), "hist");
@@ -205,75 +187,59 @@ void app::regularTickers(void) {
     #endif /*ENABLE_SIMULATOR*/
 }
 
-#if defined(ETHERNET)
+//-----------------------------------------------------------------------------
 void app::onNtpUpdate(bool gotTime) {
     mNtpReceived = true;
-}
-#endif /* defined(ETHERNET) */
-
-//-----------------------------------------------------------------------------
-void app::updateNtp(void) {
-    #if defined(ENABLE_MQTT)
-    if (mMqttReconnect && mMqttEnabled) {
-        mMqtt.tickerSecond();
-        everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS");
-        everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM");
+    if ((0 == mSunrise) && (0.0 != mConfig->sun.lat) && (0.0 != mConfig->sun.lon)) {
+        mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600;
+        tickCalcSunrise();
     }
-    #endif /*ENABLE_MQTT*/
 
-    // only install schedulers once even if NTP wasn't successful in first loop
-    if (mMqttReconnect) {  // @TODO: mMqttReconnect is variable which scope has changed
+    if (mTickerInstallOnce) {
+        mTickerInstallOnce = false;
+        #if defined(ENABLE_MQTT)
+        if (mMqttEnabled) {
+            mMqtt.tickerSecond();
+            everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS");
+            everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM");
+        }
+        #endif /*ENABLE_MQTT*/
+
         if (mConfig->inst.rstValsNotAvail)
             everyMin(std::bind(&app::tickMinute, this), "tMin");
 
-        uint32_t localTime = gTimezone.toLocal(mTimestamp);
-        uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400);  // next midnight local time
-        onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi");
+        if(mNtpReceived) {
+            uint32_t localTime = gTimezone.toLocal(mTimestamp);
+            uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400);  // next midnight local time
+            onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi");
 
-        if (mConfig->sys.schedReboot) {
-            uint32_t rebootTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86410);  // reboot 10 secs after midnght
-            if (rebootTrig <= mTimestamp) { //necessary for times other than midnight to prevent reboot loop
-               rebootTrig += 86400;
+            if (mConfig->sys.schedReboot) {
+                uint32_t rebootTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86410);  // reboot 10 secs after midnght
+                onceAt(std::bind(&app::tickReboot, this), rebootTrig, "midRe");
             }
-            onceAt(std::bind(&app::tickReboot, this), rebootTrig, "midRe");
         }
     }
+}
 
-    if ((0 == mSunrise) && (0.0 != mConfig->sun.lat) && (0.0 != mConfig->sun.lon)) {
-        mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600;
-        tickCalcSunrise();
-    }
-
-    mMqttReconnect = false;
+//-----------------------------------------------------------------------------
+void app::updateNtp(void) {
+    if(mNtpReceived)
+        onNtpUpdate(true);
 }
 
 //-----------------------------------------------------------------------------
 void app::tickNtpUpdate(void) {
     uint32_t nxtTrig = 5;  // default: check again in 5 sec
-    bool isOK = false;
 
-    #if defined(ETHERNET)
     if (!mNtpReceived)
-        mEth.updateNtpTime();
+        mNetwork->updateNtpTime();
     else {
+        nxtTrig = mConfig->ntp.interval * 60;  // check again in configured interval
         mNtpReceived = false;
-        isOK = true;
     }
-    #else
-    isOK = mWifi.getNtpTime();
-    #endif
-    if (isOK) {
-        this->updateNtp();
-        nxtTrig = mConfig->ntp.interval * 60;  // check again in 12h
-
-        // immediately start communicating
-        if (mSendFirst) {
-            mSendFirst = false;
-            once(std::bind(&app::tickSend, this), 1, "senOn");
-        }
 
-        mMqttReconnect = false;
-    }
+    updateNtp();
+
     once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp");
 }
 
@@ -315,6 +281,8 @@ void app::tickIVCommunication(void) {
                 if (mTimestamp >= (mSunset + mConfig->sun.offsetSecEvening)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise
                     nxtTrig = 0;
                 } else { // current time lies within communication start/stop time, set next trigger to communication stop
+                    if((!iv->commEnabled) && mConfig->inst.rstValsCommStart)
+                        zeroValues = true;
                     iv->commEnabled = true;
                     nxtTrig = mSunset + mConfig->sun.offsetSecEvening;
                 }
@@ -387,18 +355,9 @@ void app::tickMidnight(void) {
         // reset alarms
         if(InverterStatus::OFF == iv->getStatus())
             iv->resetAlarms();
-
-        // clear max values
-        if(mConfig->inst.rstMaxValsMidNight) {
-            record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
-            for(uint8_t i = 0; i <= iv->channels; i++) {
-                uint8_t pos = iv->getPosByChFld(i, FLD_MP, rec);
-                iv->setValue(pos, rec, 0.0f);
-            }
-        }
     }
 
-    if (mConfig->inst.rstYieldMidNight) {
+    if (mConfig->inst.rstValsAtMidNight) {
         zeroIvValues(!CHECK_AVAIL, !SKIP_YIELD_DAY);
 
         #if defined(ENABLE_MQTT)
@@ -423,29 +382,8 @@ void app::tickSend(void) {
 
     for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
         Inverter<> *iv = mSys.getInverterByPos(i);
-        if(NULL == iv)
-            continue;
-
-        if(iv->config->enabled) {
-            if(!iv->commEnabled) {
-                DPRINT_IVID(DBG_INFO, iv->id);
-                DBGPRINTLN(F("no communication to the inverter (night time)"));
-                continue;
-            }
-
-            if(!iv->radio->isChipConnected())
-                continue;
-
-            if(InverterStatus::OFF != iv->status)
-                notAvail = false;
-
-            iv->tickSend([this, iv](uint8_t cmd, bool isDevControl) {
-                if(isDevControl)
-                    mCommunication.addImportant(iv, cmd);
-                else
-                    mCommunication.add(iv, cmd);
-            });
-        }
+        if(!sendIv(iv))
+            notAvail = false;
     }
 
     if(mAllIvNotAvail != notAvail)
@@ -455,10 +393,44 @@ void app::tickSend(void) {
     updateLed();
 }
 
+//-----------------------------------------------------------------------------
+bool app::sendIv(Inverter<> *iv) {
+    if(NULL == iv)
+        return true;
+
+    if(!iv->config->enabled)
+        return true;
+
+    if(!iv->commEnabled) {
+        DPRINT_IVID(DBG_INFO, iv->id);
+        DBGPRINTLN(F("no communication to the inverter (night time)"));
+        return true;
+    }
+
+    if(!iv->radio->isChipConnected())
+        return true;
+
+    bool notAvail = true;
+    if(InverterStatus::OFF != iv->status)
+        notAvail = false;
+
+    iv->tickSend([this, iv](uint8_t cmd, bool isDevControl) {
+        if(isDevControl)
+            mCommunication.addImportant(iv, cmd);
+        else
+            mCommunication.add(iv, cmd);
+    });
+
+    return notAvail;
+}
+
 //-----------------------------------------------------------------------------
 void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
     Inverter<> *iv;
     bool changed = false;
+
+    mMaxPower.reset();
+
     // set values to zero, except yields
     for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
         iv = mSys.getInverterByPos(id);
@@ -468,10 +440,11 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
             continue;  // skip to next inverter
 
         if (checkAvail) {
-            if (!iv->isAvailable())
+            if (iv->isAvailable())
                 continue;
         }
 
+        changed = true;
         record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
         for(uint8_t ch = 0; ch <= iv->channels; ch++) {
             uint8_t pos = 0;
@@ -488,20 +461,21 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
                 pos = iv->getPosByChFld(ch, fld, rec);
                 iv->setValue(pos, rec, 0.0f);
             }
-            // zero max power
-            if(!skipYieldDay) {
+            // zero max power and max temperature
+            if(mConfig->inst.rstIncludeMaxVals) {
                 pos = iv->getPosByChFld(ch, FLD_MP, rec);
                 iv->setValue(pos, rec, 0.0f);
-            }
-            iv->resetAlarms();
-
+                pos = iv->getPosByChFld(ch, FLD_MT, rec);
+                iv->setValue(pos, rec, 0.0f);
+                iv->resetAlarms(true);
+            } else
+                iv->resetAlarms();
             iv->doCalculations();
         }
-        changed = true;
     }
 
     if(changed)
-        payloadEventListener(RealTimeRunData_Debug, NULL);
+        payloadEventListener(RealTimeRunData_Debug, nullptr);
 }
 
 //-----------------------------------------------------------------------------
@@ -548,7 +522,6 @@ void app::resetSystem(void) {
     mTimestamp = 1;
 #endif
 
-    mSendFirst = true;
     mAllIvNotAvail = true;
 
     mSunrise = 0;
@@ -562,10 +535,8 @@ void app::resetSystem(void) {
     mSaveReboot = false;
 
     mNetworkConnected = false;
-
-#if defined(ETHERNET)
     mNtpReceived = false;
-#endif
+    mTickerInstallOnce = false;
 }
 
 //-----------------------------------------------------------------------------
diff --git a/src/app.h b/src/app.h
index b16d7aeb..d23f307e 100644
--- a/src/app.h
+++ b/src/app.h
@@ -17,9 +17,9 @@
 #include "defines.h"
 #include "appInterface.h"
 #include "hm/hmSystem.h"
-#include "hm/hmRadio.h"
+#include "hm/NrfRadio.h"
 #if defined(ESP32)
-#include "hms/hmsRadio.h"
+#include "hms/CmtRadio.h"
 #endif
 #if defined(ENABLE_MQTT)
 #include "publisher/pubMqtt.h"
@@ -31,17 +31,22 @@
 #include "utils/syslog.h"
 #include "web/RestApi.h"
 #include "web/Protection.h"
+#include "plugins/MaxPower.h"
 #if defined(ENABLE_HISTORY)
 #include "plugins/history.h"
 #endif /*ENABLE_HISTORY*/
 #include "web/web.h"
 #include "hm/Communication.h"
 #if defined(ETHERNET)
-    #include "eth/ahoyeth.h"
+    #include "network/AhoyEthernet.h"
 #else /* defined(ETHERNET) */
-    #include "wifi/ahoywifi.h"
-    #include "utils/improv.h"
+    #if defined(ESP32)
+        #include "network/AhoyWifiEsp32.h"
+    #else
+        #include "network/AhoyWifiEsp8266.h"
+    #endif
 #endif /* defined(ETHERNET) */
+#include "utils/improv.h"
 
 #if defined(ENABLE_SIMULATOR)
     #include "hm/simulator.h"
@@ -162,32 +167,33 @@ class app : public IApp, public ah::Scheduler {
             return mSaveReboot;
         }
 
-        #if !defined(ETHERNET)
-        void scanAvailNetworks() override {
-            mWifi.scanAvailNetworks();
-        }
-
         bool getAvailNetworks(JsonObject obj) override {
-            return mWifi.getAvailNetworks(obj);
+            return mNetwork->getAvailNetworks(obj, this);
         }
 
         void setupStation(void) override {
-            mWifi.setupStation();
+            mNetwork->begin();
         }
 
-        void setStopApAllowedMode(bool allowed) override {
-            mWifi.setStopApAllowedMode(allowed);
+        bool getWasInCh12to14(void) const override {
+            #if defined(ESP8266)
+            return mNetwork->getWasInCh12to14();
+            #else
+            return false;
+            #endif
         }
 
-        String getStationIp(void) override {
-            return mWifi.getStationIp();
+        String getIp(void) override {
+            return mNetwork->getIp();
         }
 
-        bool getWasInCh12to14(void) const override {
-            return mWifi.getWasInCh12to14();
+        String getMac(void) override {
+            return mNetwork->getMac();
         }
 
-        #endif /* !defined(ETHERNET) */
+        bool isApActive(void) override {
+            return mNetwork->isApActive();
+        }
 
         void setRebootFlag() override {
             once(std::bind(&app::tickReboot, this), 3, "rboot");
@@ -201,6 +207,10 @@ class app : public IApp, public ah::Scheduler {
             return mVersionModules;
         }
 
+        void addOnce(ah::scdCb c, uint32_t timeout, const char *name) override {
+            once(c, timeout, name);
+        }
+
         uint32_t getSunrise() override {
             return mSunrise;
         }
@@ -210,7 +220,7 @@ class app : public IApp, public ah::Scheduler {
         }
 
         bool getSettingsValid() override {
-            return mSettings.getValid();
+            return mConfig->valid;
         }
 
         bool getRebootRequestState() override {
@@ -247,6 +257,12 @@ class app : public IApp, public ah::Scheduler {
             #endif
         }
 
+        #if defined(ETHERNET)
+        bool isWiredConnection() override {
+            return mNetwork->isWiredConnection();
+        }
+        #endif
+
         void lock(bool fromWeb) override {
             mProtection->lock(fromWeb);
         }
@@ -295,15 +311,15 @@ class app : public IApp, public ah::Scheduler {
             DPRINT(DBG_DEBUG, F("setTimestamp: "));
             DBGPRINTLN(String(newTime));
             if(0 == newTime)
-            {
-                #if defined(ETHERNET)
-                mEth.updateNtpTime();
-                #else /* defined(ETHERNET) */
-                mWifi.getNtpTime();
-                #endif /* defined(ETHERNET) */
-            }
-            else
+                mNetwork->updateNtpTime();
+            else {
                 Scheduler::setTimestamp(newTime);
+                onNtpUpdate(false);
+            }
+        }
+
+        float getTotalMaxPower(void) override {
+            return mMaxPower.getTotalMaxPower();
         }
 
         uint16_t getHistoryValue(uint8_t type, uint16_t i) override {
@@ -314,6 +330,14 @@ class app : public IApp, public ah::Scheduler {
             #endif
         }
 
+        uint32_t getHistoryPeriod(uint8_t type) override {
+            #if defined(ENABLE_HISTORY)
+                return mHistory.getPeriod((HistoryStorageType)type);
+            #else
+                return 0;
+            #endif
+        }
+
         uint16_t getHistoryMaxDay() override {
             #if defined(ENABLE_HISTORY)
                 return mHistory.getMaximumDay();
@@ -322,6 +346,21 @@ class app : public IApp, public ah::Scheduler {
             #endif
         }
 
+        uint32_t getHistoryLastValueTs(uint8_t type) override {
+            #if defined(ENABLE_HISTORY)
+                return mHistory.getLastValueTs((HistoryStorageType)type);
+            #else
+                return 0;
+            #endif
+        }
+        #if defined(ENABLE_HISTORY_LOAD_DATA)
+        void addValueToHistory(uint8_t historyType, uint8_t valueType, uint32_t value) override {
+            #if defined(ENABLE_HISTORY)
+                return mHistory.addValue((HistoryStorageType)historyType, valueType, value);
+            #endif
+        }
+        #endif
+
     private:
         #define CHECK_AVAIL     true
         #define SKIP_YIELD_DAY  true
@@ -330,15 +369,14 @@ class app : public IApp, public ah::Scheduler {
         void zeroIvValues(bool checkAvail = false, bool skipYieldDay = true);
 
         void payloadEventListener(uint8_t cmd, Inverter<> *iv) {
-            #if !defined(AP_ONLY)
+            mMaxPower.payloadEvent(cmd, iv);
             #if defined(ENABLE_MQTT)
                 if (mMqttEnabled)
                     mMqtt.payloadEventListener(cmd, iv);
-            #endif /*ENABLE_MQTT*/
             #endif
             #if defined(PLUGIN_DISPLAY)
-            if(DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
-               mDisplay.payloadEventListener(cmd);
+                if(DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
+                   mDisplay.payloadEventListener(cmd);
             #endif
            updateLed();
         }
@@ -366,14 +404,14 @@ class app : public IApp, public ah::Scheduler {
         }
 
         void tickNtpUpdate(void);
-        #if defined(ETHERNET)
         void onNtpUpdate(bool gotTime);
         bool mNtpReceived = false;
-        #endif /* defined(ETHERNET) */
         void updateNtp(void);
 
-        void triggerTickSend() override {
-            once(std::bind(&app::tickSend, this), 0, "tSend");
+        void triggerTickSend(uint8_t id) override {
+            once([this, id]() {
+                sendIv(mSys.getInverterByPos(id));
+            }, 0, "devct");
         }
 
         void tickCalcSunrise(void);
@@ -382,34 +420,28 @@ class app : public IApp, public ah::Scheduler {
         void tickSunrise(void);
         void tickComm(void);
         void tickSend(void);
+        bool sendIv(Inverter<> *iv);
         void tickMinute(void);
         void tickZeroValues(void);
         void tickMidnight(void);
         void notAvailChanged(void);
 
         HmSystemType mSys;
-        HmRadio<> mNrfRadio;
+        NrfRadio<> mNrfRadio;
         Communication mCommunication;
 
         bool mShowRebootRequest = false;
 
-        #if defined(ETHERNET)
-        ahoyeth mEth;
-        #else /* defined(ETHERNET) */
-        ahoywifi mWifi;
-        #endif /* defined(ETHERNET) */
+        AhoyNetwork *mNetwork = nullptr;
         WebType mWeb;
         RestApiType mApi;
         Protection *mProtection = nullptr;
         #ifdef ENABLE_SYSLOG
         DbgSyslog mDbgSyslog;
         #endif
-        //PayloadType mPayload;
-        //MiPayloadType mMiPayload;
+
         PubSerialType mPubSerial;
-        #if !defined(ETHERNET)
         //Improv mImprov;
-        #endif
         #ifdef ESP32
         CmtRadio<> mCmtRadio;
         #endif
@@ -422,16 +454,14 @@ class app : public IApp, public ah::Scheduler {
         bool mSaveReboot = false;
 
         uint8_t mSendLastIvId = 0;
-        bool mSendFirst = false;
         bool mAllIvNotAvail = false;
 
         bool mNetworkConnected = false;
 
-        // mqtt
         #if defined(ENABLE_MQTT)
         PubMqttType mMqtt;
-        #endif /*ENABLE_MQTT*/
-        bool mMqttReconnect = false;
+        #endif
+        bool mTickerInstallOnce = false;
         bool mMqttEnabled = false;
 
         // sun
@@ -439,6 +469,7 @@ class app : public IApp, public ah::Scheduler {
         uint32_t mSunrise = 0, mSunset = 0;
 
         // plugins
+        MaxPower<float> mMaxPower;
         #if defined(PLUGIN_DISPLAY)
         DisplayType mDisplay;
         DisplayData mDispData;
diff --git a/src/appInterface.h b/src/appInterface.h
index 536455e0..d49f907e 100644
--- a/src/appInterface.h
+++ b/src/appInterface.h
@@ -7,11 +7,8 @@
 #define __IAPP_H__
 
 #include "defines.h"
-#if defined(ETHERNET)
-#include "AsyncWebServer_ESP32_W5500.h"
-#else
 #include "ESPAsyncWebServer.h"
-#endif
+#include "utils/scheduler.h"
 
 // abstract interface to App. Make members of App accessible from child class
 // like web or API without forward declaration
@@ -29,14 +26,14 @@ class IApp {
         virtual const char *getVersion() = 0;
         virtual const char *getVersionModules() = 0;
 
-        #if !defined(ETHERNET)
-        virtual void scanAvailNetworks() = 0;
+        virtual void addOnce(ah::scdCb c, uint32_t timeout, const char *name) = 0;
+
         virtual bool getAvailNetworks(JsonObject obj) = 0;
         virtual void setupStation(void) = 0;
-        virtual void setStopApAllowedMode(bool allowed) = 0;
-        virtual String getStationIp(void) = 0;
         virtual bool getWasInCh12to14(void) const = 0;
-        #endif /* defined(ETHERNET) */
+        virtual String getIp(void) = 0;
+        virtual String getMac(void) = 0;
+        virtual bool isApActive(void) = 0;
 
         virtual uint32_t getUptime() = 0;
         virtual uint32_t getTimestamp() = 0;
@@ -48,7 +45,7 @@ class IApp {
         virtual void getSchedulerInfo(uint8_t *max) = 0;
         virtual void getSchedulerNames() = 0;
 
-        virtual void triggerTickSend() = 0;
+        virtual void triggerTickSend(uint8_t id) = 0;
 
         virtual bool getRebootRequestState() = 0;
         virtual bool getSettingsValid() = 0;
@@ -61,14 +58,23 @@ class IApp {
         virtual uint32_t getMqttRxCnt() = 0;
         virtual uint32_t getMqttTxCnt() = 0;
 
+        #if defined(ETHERNET)
+        virtual bool isWiredConnection() = 0;
+        #endif
+
         virtual void lock(bool fromWeb) = 0;
         virtual char *unlock(const char *clientIp, bool loginFromWeb) = 0;
         virtual void resetLockTimeout(void) = 0;
         virtual bool isProtected(const char *clientIp, const char *token, bool askedFromWeb) const = 0;
 
+        virtual float getTotalMaxPower(void) = 0;
         virtual uint16_t getHistoryValue(uint8_t type, uint16_t i) = 0;
+        virtual uint32_t getHistoryPeriod(uint8_t type) = 0;
         virtual uint16_t getHistoryMaxDay() = 0;
-
+        virtual uint32_t getHistoryLastValueTs(uint8_t type) = 0;
+        #if defined(ENABLE_HISTORY_LOAD_DATA)
+        virtual void addValueToHistory(uint8_t historyType, uint8_t valueType, uint32_t value) = 0;
+        #endif
         virtual void* getRadioObj(bool nrf) = 0;
 };
 
diff --git a/src/config/config.h b/src/config/config.h
index 9e59f146..7a3dbb64 100644
--- a/src/config/config.h
+++ b/src/config/config.h
@@ -16,9 +16,8 @@
 //-------------------------------------
 
 // Fallback WiFi Info
-#define FB_WIFI_SSID    "YOUR_WIFI_SSID"
-#define FB_WIFI_PWD     "YOUR_WIFI_PWD"
-
+#define FB_WIFI_SSID    ""
+#define FB_WIFI_PWD     ""
 
 // Access Point Info
 // In case there is no WiFi Network or Ahoy can not connect to it, it will act as an Access Point
@@ -28,6 +27,11 @@
 
 // If the next line is uncommented, Ahoy will stay in access point mode all the time
 //#define AP_ONLY
+#if defined(AP_ONLY)
+    #if defined(ENABLE_MQTT)
+        #undef ENABLE_MQTT
+    #endif
+#endif
 
 // timeout for automatic logoff (20 minutes)
 #define LOGOUT_TIMEOUT      (20 * 60)
@@ -77,6 +81,9 @@
     #ifndef DEF_ETH_CS_PIN
         #define DEF_ETH_CS_PIN      15
     #endif
+    #ifndef DEF_ETH_RST_PIN
+        #define DEF_ETH_RST_PIN     DEF_PIN_OFF
+    #endif
 #else /* defined(ETHERNET) */
 // time in seconds how long the station info (ssid + pwd) will be tried
 #define WIFI_TRY_CONNECT_TIME   30
@@ -142,7 +149,7 @@
     #ifndef DEF_MOTION_SENSOR_PIN
         #define DEF_MOTION_SENSOR_PIN   DEF_PIN_OFF
     #endif
-#else
+#else // ESP8266
     #ifndef DEF_NRF_CS_PIN
         #define DEF_NRF_CS_PIN          15
     #endif
diff --git a/src/config/config_override_example.h b/src/config/config_override_example.h
index 44623c1f..a84c1e8a 100644
--- a/src/config/config_override_example.h
+++ b/src/config/config_override_example.h
@@ -6,9 +6,6 @@
 #ifndef __CONFIG_OVERRIDE_H__
 #define __CONFIG_OVERRIDE_H__
 
-// override fallback WiFi info
-#define FB_WIFI_OVERRIDDEN
-
 // each override must be preceded with an #undef statement
 #undef FB_WIFI_SSID
 #define FB_WIFI_SSID    "MY_SSID"
diff --git a/src/config/settings.h b/src/config/settings.h
index 18725b48..0d8970d5 100644
--- a/src/config/settings.h
+++ b/src/config/settings.h
@@ -33,7 +33,6 @@
 
 #define CONFIG_VERSION      11
 
-
 #define PROT_MASK_INDEX     0x0001
 #define PROT_MASK_LIVE      0x0002
 #define PROT_MASK_SERIAL    0x0004
@@ -55,6 +54,20 @@
 #define DEF_PROT_MQTT       0x0000
 
 
+#define SSID_LEN                32
+#define PWD_LEN                 64
+#define DEVNAME_LEN             16
+#define NTP_ADDR_LEN            32 // DNS Name
+
+#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
+#define MQTT_PWD_LEN            65
+#define MQTT_TOPIC_LEN          65
+
+#define MQTT_MAX_PACKET_SIZE    384
+
+
 typedef struct {
     uint8_t ip[4];      // ip address
     uint8_t mask[4];    // sub mask
@@ -63,6 +76,19 @@ typedef struct {
     uint8_t gateway[4]; // standard gateway
 } cfgIp_t;
 
+
+#if defined(ETHERNET)
+typedef struct {
+    bool enabled;
+    uint8_t pinCs;
+    uint8_t pinSclk;
+    uint8_t pinMiso;
+    uint8_t pinMosi;
+    uint8_t pinIrq;
+    uint8_t pinRst;
+} cfgEth_t;
+#endif
+
 typedef struct {
     char deviceName[DEVNAME_LEN];
     char adminPwd[PWD_LEN];
@@ -72,13 +98,14 @@ typedef struct {
     uint8_t region;
     int8_t timezone;
 
-#if !defined(ETHERNET)
+    char apPwd[PWD_LEN];
     // wifi
     char stationSsid[SSID_LEN];
     char stationPwd[PWD_LEN];
-    char apPwd[PWD_LEN];
     bool isHidden;
-#endif /* !defined(ETHERNET) */
+    #if defined(ETHERNET)
+    cfgEth_t eth;
+    #endif
 
     cfgIp_t ip;
 } cfgSys_t;
@@ -136,7 +163,9 @@ typedef struct {
     char user[MQTT_USER_LEN];
     char pwd[MQTT_PWD_LEN];
     char topic[MQTT_TOPIC_LEN];
+    bool json;
     uint16_t interval;
+    bool enableRetain;
 } cfgMqtt_t;
 
 typedef struct {
@@ -152,16 +181,16 @@ typedef struct {
 } cfgIv_t;
 
 typedef struct {
-    bool enabled;
+//    bool enabled;
     cfgIv_t iv[MAX_NUM_INVERTERS];
 
     uint16_t sendInterval;
-    bool rstYieldMidNight;
+    bool rstValsAtMidNight;
     bool rstValsNotAvail;
     bool rstValsCommStop;
-    bool rstMaxValsMidNight;
+    bool rstValsCommStart;
+    bool rstIncludeMaxVals;
     bool startWithoutTime;
-    float yieldEffiency;
     bool readGrid;
 } cfgInst_t;
 
@@ -211,8 +240,9 @@ class settings {
             std::fill(reinterpret_cast<char*>(&mCfg), reinterpret_cast<char*>(&mCfg) + sizeof(mCfg), 0);
         }
 
-        void setup() {
+        void setup(settings_t *&c) {
             DPRINTLN(DBG_INFO, F("Initializing FS .."));
+            c = &mCfg;
 
             mCfg.valid = false;
             #if !defined(ESP32)
@@ -248,31 +278,11 @@ class settings {
             DPRINTLN(DBG_INFO, F("FS stopped"));
         }
 
-        void getPtr(settings_t *&cfg) {
-            cfg = &mCfg;
-        }
-
-        bool getValid(void) {
-            return mCfg.valid;
-        }
-
         inline bool getLastSaveSucceed() {
             return mLastSaveSucceed;
         }
 
-        void getInfo(uint32_t *used, uint32_t *size) {
-            #if !defined(ESP32)
-                FSInfo info;
-                LittleFS.info(info);
-                *used = info.usedBytes;
-                *size = info.totalBytes;
 
-                DPRINTLN(DBG_INFO, F("-- FILESYSTEM INFO --"));
-                DPRINTLN(DBG_INFO, String(info.usedBytes) + F(" of ") + String(info.totalBytes)  + F(" used"));
-            #else
-                DPRINTLN(DBG_WARN, F("not supported by ESP32"));
-            #endif
-        }
 
         bool readSettings(const char* path) {
             loadDefaults();
@@ -387,14 +397,26 @@ class settings {
             // restore temp settings
             if(keepWifi)
                 memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t));
-            #if !defined(ETHERNET)
             else {
-                snprintf(mCfg.sys.stationSsid, SSID_LEN, FB_WIFI_SSID);
-                snprintf(mCfg.sys.stationPwd,  PWD_LEN,  FB_WIFI_PWD);
-                snprintf(mCfg.sys.apPwd,       PWD_LEN,  WIFI_AP_PWD);
+                mCfg.sys.stationSsid[0] = '\0';
+                mCfg.sys.stationPwd[0] = '\0';
                 mCfg.sys.isHidden = false;
             }
-            #endif /* !defined(ETHERNET) */
+            snprintf(mCfg.sys.apPwd,       PWD_LEN,  WIFI_AP_PWD);
+
+            #if defined(ETHERNET)
+                #if defined(DEF_ETH_ENABLED)
+                mCfg.sys.eth.enabled = true;
+                #else
+                mCfg.sys.eth.enabled = false;
+                #endif
+            mCfg.sys.eth.pinCs   = DEF_ETH_CS_PIN;
+            mCfg.sys.eth.pinSclk = DEF_ETH_SCK_PIN;
+            mCfg.sys.eth.pinMiso = DEF_ETH_MISO_PIN;
+            mCfg.sys.eth.pinMosi = DEF_ETH_MOSI_PIN;
+            mCfg.sys.eth.pinIrq  = DEF_ETH_IRQ_PIN;
+            mCfg.sys.eth.pinRst  = DEF_ETH_RST_PIN;
+            #endif
 
             snprintf(mCfg.sys.deviceName,  DEVNAME_LEN, DEF_DEVICE_NAME);
             mCfg.sys.region   = 0; // Europe
@@ -407,7 +429,11 @@ class settings {
             mCfg.nrf.pinMosi           = DEF_NRF_MOSI_PIN;
             mCfg.nrf.pinSclk           = DEF_NRF_SCLK_PIN;
 
+            #if defined(ETHERNET)
+            mCfg.nrf.enabled           = false;
+            #else
             mCfg.nrf.enabled           = true;
+            #endif
 
             #if defined(ESP32)
             mCfg.cmt.pinSclk           = DEF_CMT_SCLK;
@@ -428,8 +454,8 @@ class settings {
             mCfg.ntp.port = DEF_NTP_PORT;
             mCfg.ntp.interval = 720;
 
-            mCfg.sun.lat         = 0.0;
-            mCfg.sun.lon         = 0.0;
+            mCfg.sun.lat         = 51.1; // mid of Germany
+            mCfg.sun.lon         = 10.5; // mid of Germany
             mCfg.sun.offsetSecMorning = 0;
             mCfg.sun.offsetSecEvening = 0;
 
@@ -445,15 +471,17 @@ class settings {
             snprintf(mCfg.mqtt.pwd,    MQTT_PWD_LEN,   "%s", DEF_MQTT_PWD);
             snprintf(mCfg.mqtt.topic,  MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC);
             mCfg.mqtt.interval = 0; // off
-
-            mCfg.inst.sendInterval     = SEND_INTERVAL;
-            mCfg.inst.rstYieldMidNight = false;
-            mCfg.inst.rstValsNotAvail  = false;
-            mCfg.inst.rstValsCommStop  = false;
-            mCfg.inst.startWithoutTime = false;
-            mCfg.inst.rstMaxValsMidNight = false;
-            mCfg.inst.yieldEffiency    = 1.0f;
-            mCfg.inst.readGrid         = true;
+            mCfg.mqtt.json = false; // off
+            mCfg.mqtt.enableRetain = true;
+
+            mCfg.inst.sendInterval       = SEND_INTERVAL;
+            mCfg.inst.rstValsAtMidNight   = false;
+            mCfg.inst.rstValsNotAvail    = false;
+            mCfg.inst.rstValsCommStop    = false;
+            mCfg.inst.rstValsCommStart   = false;
+            mCfg.inst.startWithoutTime   = false;
+            mCfg.inst.rstIncludeMaxVals = false;
+            mCfg.inst.readGrid           = true;
 
             for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
                 mCfg.inst.iv[i].powerLevel  = 0xff; // impossible high value
@@ -528,12 +556,10 @@ class settings {
         void jsonNetwork(JsonObject obj, bool set = false) {
             if(set) {
                 char buf[16];
-                #if !defined(ETHERNET)
                 obj[F("ssid")] = mCfg.sys.stationSsid;
                 obj[F("pwd")]  = mCfg.sys.stationPwd;
                 obj[F("ap_pwd")]  = mCfg.sys.apPwd;
                 obj[F("hidd")] = (bool) mCfg.sys.isHidden;
-                #endif /* !defined(ETHERNET) */
                 obj[F("dev")]  = mCfg.sys.deviceName;
                 obj[F("adm")]  = mCfg.sys.adminPwd;
                 obj[F("prot_mask")] = mCfg.sys.protectionMask;
@@ -546,13 +572,21 @@ class settings {
                 ah::ip2Char(mCfg.sys.ip.dns1, buf);    obj[F("dns1")] = String(buf);
                 ah::ip2Char(mCfg.sys.ip.dns2, buf);    obj[F("dns2")] = String(buf);
                 ah::ip2Char(mCfg.sys.ip.gateway, buf); obj[F("gtwy")] = String(buf);
+
+                #if defined(ETHERNET)
+                obj[F("en")]   = mCfg.sys.eth.enabled;
+                obj[F("cs")]   = mCfg.sys.eth.pinCs;
+                obj[F("sclk")] = mCfg.sys.eth.pinSclk;
+                obj[F("miso")] = mCfg.sys.eth.pinMiso;
+                obj[F("mosi")] = mCfg.sys.eth.pinMosi;
+                obj[F("irq")]  = mCfg.sys.eth.pinIrq;
+                obj[F("rst")]  = mCfg.sys.eth.pinRst;
+                #endif
             } else {
-                #if !defined(ETHERNET)
                 getChar(obj, F("ssid"), mCfg.sys.stationSsid, SSID_LEN);
                 getChar(obj, F("pwd"), mCfg.sys.stationPwd, PWD_LEN);
                 getChar(obj, F("ap_pwd"), mCfg.sys.apPwd, PWD_LEN);
                 getVal<bool>(obj, F("hidd"), &mCfg.sys.isHidden);
-                #endif /* !defined(ETHERNET) */
                 getChar(obj, F("dev"), mCfg.sys.deviceName, DEVNAME_LEN);
                 getChar(obj, F("adm"), mCfg.sys.adminPwd, PWD_LEN);
                 getVal<uint16_t>(obj, F("prot_mask"), &mCfg.sys.protectionMask);
@@ -569,6 +603,16 @@ class settings {
                 if(mCfg.sys.protectionMask == 0)
                     mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP
                                             | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT | DEF_PROT_HISTORY;
+
+                #if defined(ETHERNET)
+                getVal<bool>(obj, F("en"), &mCfg.sys.eth.enabled);
+                getVal<uint8_t>(obj, F("cs"), &mCfg.sys.eth.pinCs);
+                getVal<uint8_t>(obj, F("sclk"), &mCfg.sys.eth.pinSclk);
+                getVal<uint8_t>(obj, F("miso"), &mCfg.sys.eth.pinMiso);
+                getVal<uint8_t>(obj, F("mosi"), &mCfg.sys.eth.pinMosi);
+                getVal<uint8_t>(obj, F("irq"), &mCfg.sys.eth.pinIrq);
+                getVal<uint8_t>(obj, F("rst"), &mCfg.sys.eth.pinRst);
+                #endif
             }
         }
 
@@ -681,16 +725,20 @@ class settings {
                 obj[F("user")]     = mCfg.mqtt.user;
                 obj[F("pwd")]      = mCfg.mqtt.pwd;
                 obj[F("topic")]    = mCfg.mqtt.topic;
+                obj[F("json")]     = mCfg.mqtt.json;
                 obj[F("intvl")]    = mCfg.mqtt.interval;
+                obj[F("retain")]   = mCfg.mqtt.enableRetain;
 
             } else {
                 getVal<uint16_t>(obj, F("port"), &mCfg.mqtt.port);
                 getVal<uint16_t>(obj, F("intvl"), &mCfg.mqtt.interval);
+                getVal<bool>(obj, F("json"), &mCfg.mqtt.json);
                 getChar(obj, F("broker"), mCfg.mqtt.broker, MQTT_ADDR_LEN);
                 getChar(obj, F("user"), mCfg.mqtt.user, MQTT_USER_LEN);
                 getChar(obj, F("clientId"), mCfg.mqtt.clientId, MQTT_CLIENTID_LEN);
                 getChar(obj, F("pwd"), mCfg.mqtt.pwd, MQTT_PWD_LEN);
                 getChar(obj, F("topic"), mCfg.mqtt.topic, MQTT_TOPIC_LEN);
+                getVal<bool>(obj, F("retain"), &mCfg.mqtt.enableRetain);
             }
         }
 
@@ -757,30 +805,25 @@ class settings {
         void jsonInst(JsonObject obj, bool set = false) {
             if(set) {
                 obj[F("intvl")]          = mCfg.inst.sendInterval;
-                obj[F("en")] = (bool)mCfg.inst.enabled;
-                obj[F("rstMidNight")]    = (bool)mCfg.inst.rstYieldMidNight;
+//                obj[F("en")] = (bool)mCfg.inst.enabled;
+                obj[F("rstMidNight")]    = (bool)mCfg.inst.rstValsAtMidNight;
                 obj[F("rstNotAvail")]    = (bool)mCfg.inst.rstValsNotAvail;
                 obj[F("rstComStop")]     = (bool)mCfg.inst.rstValsCommStop;
+                obj[F("rstComStart")]    = (bool)mCfg.inst.rstValsCommStart;
                 obj[F("strtWthtTime")]   = (bool)mCfg.inst.startWithoutTime;
-                obj[F("rstMaxMidNight")] = (bool)mCfg.inst.rstMaxValsMidNight;
-                obj[F("yldEff")]         = mCfg.inst.yieldEffiency;
+                obj[F("rstMaxMidNight")] = (bool)mCfg.inst.rstIncludeMaxVals;
                 obj[F("rdGrid")]         = (bool)mCfg.inst.readGrid;
             }
             else {
                 getVal<uint16_t>(obj, F("intvl"), &mCfg.inst.sendInterval);
-                getVal<bool>(obj, F("en"), &mCfg.inst.enabled);
-                getVal<bool>(obj, F("rstMidNight"), &mCfg.inst.rstYieldMidNight);
+//                getVal<bool>(obj, F("en"), &mCfg.inst.enabled);
+                getVal<bool>(obj, F("rstMidNight"), &mCfg.inst.rstValsAtMidNight);
                 getVal<bool>(obj, F("rstNotAvail"), &mCfg.inst.rstValsNotAvail);
                 getVal<bool>(obj, F("rstComStop"), &mCfg.inst.rstValsCommStop);
+                getVal<bool>(obj, F("rstComStart"), &mCfg.inst.rstValsCommStart);
                 getVal<bool>(obj, F("strtWthtTime"), &mCfg.inst.startWithoutTime);
-                getVal<bool>(obj, F("rstMaxMidNight"), &mCfg.inst.rstMaxValsMidNight);
-                getVal<float>(obj, F("yldEff"), &mCfg.inst.yieldEffiency);
+                getVal<bool>(obj, F("rstMaxMidNight"), &mCfg.inst.rstIncludeMaxVals);
                 getVal<bool>(obj, F("rdGrid"), &mCfg.inst.readGrid);
-
-                if(mCfg.inst.yieldEffiency < 0.5)
-                    mCfg.inst.yieldEffiency = 1.0f;
-                else if(mCfg.inst.yieldEffiency > 1.0f)
-                    mCfg.inst.yieldEffiency = 1.0f;
             }
 
             JsonArray ivArr;
@@ -854,6 +897,7 @@ class settings {
         }
     #endif
 
+    private:
         settings_t mCfg;
         bool mLastSaveSucceed = 0;
 };
diff --git a/src/defines.h b/src/defines.h
index bf111d99..94674315 100644
--- a/src/defines.h
+++ b/src/defines.h
@@ -13,8 +13,7 @@
 //-------------------------------------
 #define VERSION_MAJOR       0
 #define VERSION_MINOR       8
-#define VERSION_PATCH       83
-
+#define VERSION_PATCH       140
 //-------------------------------------
 typedef struct {
     uint8_t ch;
@@ -24,41 +23,6 @@ typedef struct {
     uint16_t millis;
 } packet_t;
 
-typedef enum {
-    InverterDevInform_Simple = 0,  // 0x00
-    InverterDevInform_All    = 1,  // 0x01
-    GridOnProFilePara        = 2,  // 0x02
-    HardWareConfig           = 3,  // 0x03
-    SimpleCalibrationPara    = 4,  // 0x04
-    SystemConfigPara         = 5,  // 0x05
-    RealTimeRunData_Debug    = 11, // 0x0b
-    RealTimeRunData_Reality  = 12, // 0x0c
-    RealTimeRunData_A_Phase  = 13, // 0x0d
-    RealTimeRunData_B_Phase  = 14, // 0x0e
-    RealTimeRunData_C_Phase  = 15, // 0x0f
-    AlarmData                = 17, // 0x11, Alarm data - all unsent alarms
-    AlarmUpdate              = 18, // 0x12, Alarm data - all pending alarms
-    RecordData               = 19, // 0x13
-    InternalData             = 20, // 0x14
-    GetLossRate              = 21, // 0x15
-    GetSelfCheckState        = 30, // 0x1e
-    InitDataState            = 0xff
-} InfoCmdType;
-
-typedef enum {
-    TurnOn                  = 0,  // 0x00
-    TurnOff                 = 1,  // 0x01
-    Restart                 = 2,  // 0x02
-    Lock                    = 3,  // 0x03
-    Unlock                  = 4,  // 0x04
-    ActivePowerContr        = 11, // 0x0b
-    ReactivePowerContr      = 12, // 0x0c
-    PFSet                   = 13, // 0x0d
-    CleanState_LockAndAlarm = 20, // 0x14
-    SelfInspection          = 40, // 0x28, self-inspection of grid-connected protection files
-    Init                    = 0xff
-} DevControlCmdType;
-
 typedef enum {
     AbsolutNonPersistent  = 0UL,    // 0x0000
     RelativNonPersistent  = 1UL,    // 0x0001
@@ -71,13 +35,6 @@ union serial_u {
     uint8_t  b[8];
 };
 
-#define MIN_SERIAL_INTERVAL     2 // 5
-#define MIN_SEND_INTERVAL       15
-#define MIN_MQTT_INTERVAL       60
-
-
-enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE};
-
 enum {
     DISP_TYPE_T0_NONE           = 0,
     DISP_TYPE_T1_SSD1306_128X64 = 1,
@@ -89,27 +46,9 @@ enum {
     DISP_TYPE_T10_EPAPER        = 10
 };
 
-
-//-------------------------------------
-// EEPROM
-//-------------------------------------
-#define SSID_LEN                32
-#define PWD_LEN                 64
-#define DEVNAME_LEN             16
-#define NTP_ADDR_LEN            32 // DNS Name
-
-#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
-#define MQTT_PWD_LEN            65
-#define MQTT_TOPIC_LEN          65
-
-#define MQTT_MAX_PACKET_SIZE    384
-
-
 typedef struct {
     uint32_t rxFail;
-    uint32_t rxFailNoAnser;
+    uint32_t rxFailNoAnswer;
     uint32_t rxSuccess;
     uint32_t frmCnt;
     uint32_t txCnt;
diff --git a/src/eth/ahoyeth.cpp b/src/eth/ahoyeth.cpp
deleted file mode 100644
index 2226fce6..00000000
--- a/src/eth/ahoyeth.cpp
+++ /dev/null
@@ -1,261 +0,0 @@
-//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
-// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
-//-----------------------------------------------------------------------------
-
-#if defined(ETHERNET)
-
-#if defined(ESP32) && defined(F)
-  #undef F
-  #define F(sl) (sl)
-#endif
-#include "ahoyeth.h"
-#include <ESPmDNS.h>
-
-//-----------------------------------------------------------------------------
-ahoyeth::ahoyeth()
-{
-    // WiFi.onEvent(ESP32_W5500_event);
-}
-
-
-//-----------------------------------------------------------------------------
-void ahoyeth::setup(settings_t *config, uint32_t *utcTimestamp, OnNetworkCB onNetworkCB, OnTimeCB onTimeCB) {
-    mConfig = config;
-    mUtcTimestamp = utcTimestamp;
-    mOnNetworkCB = onNetworkCB;
-    mOnTimeCB = onTimeCB;
-
-    Serial.flush();
-    WiFi.onEvent([this](WiFiEvent_t event, arduino_event_info_t info) -> void { this->onEthernetEvent(event, info); });
-
-    Serial.flush();
-    #if defined(CONFIG_IDF_TARGET_ESP32S3)
-    mEthSpi.begin(DEF_ETH_MISO_PIN, DEF_ETH_MOSI_PIN, DEF_ETH_SCK_PIN, DEF_ETH_CS_PIN, DEF_ETH_IRQ_PIN, DEF_ETH_RST_PIN);
-    #else
-    ETH.begin(DEF_ETH_MISO_PIN, DEF_ETH_MOSI_PIN, DEF_ETH_SCK_PIN, DEF_ETH_CS_PIN, DEF_ETH_IRQ_PIN, ETH_SPI_CLOCK_MHZ, ETH_SPI_HOST);
-    #endif
-
-    if(mConfig->sys.ip.ip[0] != 0) {
-        IPAddress ip(mConfig->sys.ip.ip);
-        IPAddress mask(mConfig->sys.ip.mask);
-        IPAddress dns1(mConfig->sys.ip.dns1);
-        IPAddress dns2(mConfig->sys.ip.dns2);
-        IPAddress gateway(mConfig->sys.ip.gateway);
-        if(!ETH.config(ip, gateway, mask, dns1, dns2))
-            DPRINTLN(DBG_ERROR, F("failed to set static IP!"));
-    }
-}
-
-
-//-----------------------------------------------------------------------------
-bool ahoyeth::updateNtpTime(void) {
-    DPRINTLN(DBG_DEBUG, F(__FUNCTION__)); Serial.flush();
-    Serial.printf("ETH.linkUp()=%s\n", ETH.linkUp() ? "up" : "down");
-    Serial.print("ETH.localIP()=");
-    Serial.println(ETH.localIP());
-    Serial.printf("Go on? %s\n", (!ETH.localIP()) ? "No..." : "Yes...");
-    if (!ETH.localIP())
-        return false;
-
-    DPRINTLN(DBG_DEBUG, F("updateNtpTime: checking udp \"connection\"...")); Serial.flush();
-    if (!mUdp.connected()) {
-        DPRINTLN(DBG_DEBUG, F("updateNtpTime: About to (re)connect...")); Serial.flush();
-        IPAddress timeServer;
-        if (!WiFi.hostByName(mConfig->ntp.addr, timeServer))
-            return false;
-
-        if (!mUdp.connect(timeServer, mConfig->ntp.port))
-            return false;
-
-        DPRINTLN(DBG_DEBUG, F("updateNtpTime: Connected...")); Serial.flush();
-        mUdp.onPacket([this](AsyncUDPPacket packet) {
-            DPRINTLN(DBG_DEBUG, F("updateNtpTime: about to handle ntp packet...")); Serial.flush();
-            this->handleNTPPacket(packet);
-        });
-    }
-
-    DPRINTLN(DBG_DEBUG, F("updateNtpTime: prepare packet...")); Serial.flush();
-
-    // set all bytes in the buffer to 0
-    memset(mUdpPacketBuffer, 0, NTP_PACKET_SIZE);
-    // Initialize values needed to form NTP request
-    // (see URL above for details on the packets)
-
-    mUdpPacketBuffer[0]   = 0b11100011;   // LI, Version, Mode
-    mUdpPacketBuffer[1]   = 0;     // Stratum, or type of clock
-    mUdpPacketBuffer[2]   = 6;     // Polling Interval
-    mUdpPacketBuffer[3]   = 0xEC;  // Peer Clock Precision
-
-    // 8 bytes of zero for Root Delay & Root Dispersion
-    mUdpPacketBuffer[12]  = 49;
-    mUdpPacketBuffer[13]  = 0x4E;
-    mUdpPacketBuffer[14]  = 49;
-    mUdpPacketBuffer[15]  = 52;
-
-    //Send unicast
-    DPRINTLN(DBG_DEBUG, F("updateNtpTime: send packet...")); Serial.flush();
-    mUdp.write(mUdpPacketBuffer, sizeof(mUdpPacketBuffer));
-
-    return true;
-}
-
-//-----------------------------------------------------------------------------
-void ahoyeth::handleNTPPacket(AsyncUDPPacket packet) {
-    char       buf[80];
-
-    memcpy(buf, packet.data(), sizeof(buf));
-
-    unsigned long highWord = word(buf[40], buf[41]);
-    unsigned long lowWord = word(buf[42], buf[43]);
-
-    // combine the four bytes (two words) into a long integer
-    // this is NTP time (seconds since Jan 1 1900):
-    unsigned long secsSince1900 = highWord << 16 | lowWord;
-
-    *mUtcTimestamp = secsSince1900 - 2208988800UL; // UTC time
-    DPRINTLN(DBG_INFO, "[NTP]: " + ah::getDateTimeStr(*mUtcTimestamp) + " UTC");
-    mOnTimeCB(true);
-}
-
-//-----------------------------------------------------------------------------
-void ahoyeth::welcome(String ip, String mode) {
-    DBGPRINTLN(F("\n\n--------------------------------"));
-    DBGPRINTLN(F("Welcome to AHOY!"));
-    DBGPRINT(F("\npoint your browser to http://"));
-    DBGPRINT(ip);
-    DBGPRINTLN(mode);
-    DBGPRINTLN(F("to configure your device"));
-    DBGPRINTLN(F("--------------------------------\n"));
-}
-
-void ahoyeth::onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info) {
-    AWS_LOG(F("[ETH]: Got event..."));
-    switch (event) {
-#if ( ( defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 2) ) && ( ARDUINO_ESP32_GIT_VER != 0x46d5afb1 ) )
-    // For breaking core v2.0.0
-    // Why so strange to define a breaking enum arduino_event_id_t in WiFiGeneric.h
-    // compared to the old system_event_id_t, now in tools/sdk/esp32/include/esp_event/include/esp_event_legacy.h
-    // You can preserve the old enum order and just adding new items to do no harm
-    case ARDUINO_EVENT_ETH_START:
-        AWS_LOG(F("\nETH Started"));
-        //set eth hostname here
-        if(String(mConfig->sys.deviceName) != "")
-            ETH.setHostname(mConfig->sys.deviceName);
-        else
-            ETH.setHostname("ESP32_W5500");
-        break;
-
-    case ARDUINO_EVENT_ETH_CONNECTED:
-        AWS_LOG(F("ETH Connected"));
-        break;
-
-    case ARDUINO_EVENT_ETH_GOT_IP:
-        if (!ESP32_W5500_eth_connected) {
-            #if defined (CONFIG_IDF_TARGET_ESP32S3)
-            AWS_LOG3(F("ETH MAC: "), mEthSpi.macAddress(), F(", IPv4: "), ETH.localIP());
-            #else
-            AWS_LOG3(F("ETH MAC: "), ETH.macAddress(), F(", IPv4: "), ETH.localIP());
-            #endif
-
-            if (ETH.fullDuplex()) {
-                AWS_LOG0(F("FULL_DUPLEX, "));
-            } else {
-                AWS_LOG0(F("HALF_DUPLEX, "));
-            }
-
-            AWS_LOG1(ETH.linkSpeed(), F("Mbps"));
-
-            ESP32_W5500_eth_connected = true;
-            mOnNetworkCB(true);
-        }
-        if (!MDNS.begin(mConfig->sys.deviceName)) {
-            DPRINTLN(DBG_ERROR, F("Error setting up MDNS responder!"));
-        } else {
-            DBGPRINT(F("[WiFi] mDNS established: "));
-            DBGPRINT(mConfig->sys.deviceName);
-            DBGPRINTLN(F(".local"));
-        }
-        break;
-
-    case ARDUINO_EVENT_ETH_DISCONNECTED:
-      AWS_LOG("ETH Disconnected");
-      ESP32_W5500_eth_connected = false;
-      mUdp.close();
-      mOnNetworkCB(false);
-      break;
-
-    case ARDUINO_EVENT_ETH_STOP:
-      AWS_LOG("\nETH Stopped");
-      ESP32_W5500_eth_connected = false;
-      mUdp.close();
-      mOnNetworkCB(false);
-      break;
-
-#else
-
-    // For old core v1.0.6-
-    // Core v2.0.0 defines a stupid enum arduino_event_id_t, breaking any code for ESP32_W5500 written for previous core
-    // Why so strange to define a breaking enum arduino_event_id_t in WiFiGeneric.h
-    // compared to the old system_event_id_t, now in tools/sdk/esp32/include/esp_event/include/esp_event_legacy.h
-    // You can preserve the old enum order and just adding new items to do no harm
-    case SYSTEM_EVENT_ETH_START:
-        AWS_LOG(F("\nETH Started"));
-        //set eth hostname here
-        if(String(mConfig->sys.deviceName) != "")
-            ETH.setHostname(mConfig->sys.deviceName);
-        else
-            ETH.setHostname("ESP32_W5500");
-        break;
-
-    case SYSTEM_EVENT_ETH_CONNECTED:
-        AWS_LOG(F("ETH Connected"));
-        break;
-
-    case SYSTEM_EVENT_ETH_GOT_IP:
-        if (!ESP32_W5500_eth_connected) {
-            AWS_LOG3(F("ETH MAC: "), ETH.macAddress(), F(", IPv4: "), ETH.localIP());
-
-            if (ETH.fullDuplex()) {
-                AWS_LOG0(F("FULL_DUPLEX, "));
-            } else {
-                AWS_LOG0(F("HALF_DUPLEX, "));
-            }
-
-            AWS_LOG1(ETH.linkSpeed(), F("Mbps"));
-
-            ESP32_W5500_eth_connected = true;
-            mOnNetworkCB(true);
-        }
-        if (!MDNS.begin(mConfig->sys.deviceName)) {
-            DPRINTLN(DBG_ERROR, F("Error setting up MDNS responder!"));
-        } else {
-            DBGPRINT(F("[WiFi] mDNS established: "));
-            DBGPRINT(mConfig->sys.deviceName);
-            DBGPRINTLN(F(".local"));
-        }
-        break;
-
-    case SYSTEM_EVENT_ETH_DISCONNECTED:
-        AWS_LOG("ETH Disconnected");
-        ESP32_W5500_eth_connected = false;
-        mUdp.close();
-        mOnNetworkCB(false);
-        break;
-
-    case SYSTEM_EVENT_ETH_STOP:
-        AWS_LOG("\nETH Stopped");
-        ESP32_W5500_eth_connected = false;
-        mUdp.close();
-        mOnNetworkCB(false);
-        break;
-#endif
-
-    default:
-
-      break;
-  }
-
-}
-
-#endif /* defined(ETHERNET) */
diff --git a/src/eth/ahoyeth.h b/src/eth/ahoyeth.h
deleted file mode 100644
index ebd91c67..00000000
--- a/src/eth/ahoyeth.h
+++ /dev/null
@@ -1,64 +0,0 @@
-//-----------------------------------------------------------------------------
-// 2024 Ahoy, https://github.com/lumpapu/ahoy
-// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
-//-----------------------------------------------------------------------------
-
-#if defined(ETHERNET)
-#ifndef __AHOYETH_H__
-#define __AHOYETH_H__
-
-#include <functional>
-
-#include <Arduino.h>
-#include <AsyncUDP.h>
-#include <DNSServer.h>
-
-#include "ethSpi.h"
-#include "../utils/dbg.h"
-#include "../config/config.h"
-#include "../config/settings.h"
-
-#include "AsyncWebServer_ESP32_W5500.h"
-
-
-class app;
-
-#define NTP_PACKET_SIZE 48
-
-class ahoyeth {
-    public: /* types */
-        typedef std::function<void(bool)> OnNetworkCB;
-        typedef std::function<void(bool)> OnTimeCB;
-
-    public:
-        ahoyeth();
-
-        void setup(settings_t *config, uint32_t *utcTimestamp, OnNetworkCB onNetworkCB, OnTimeCB onTimeCB);
-        bool updateNtpTime(void);
-
-    private:
-        void setupEthernet();
-
-        void handleNTPPacket(AsyncUDPPacket packet);
-
-        void welcome(String ip, String mode);
-
-        void onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info);
-
-    private:
-        #if defined(CONFIG_IDF_TARGET_ESP32S3)
-        EthSpi mEthSpi;
-        #endif
-        settings_t *mConfig = nullptr;
-
-        uint32_t *mUtcTimestamp;
-        AsyncUDP mUdp; // for time server
-        byte mUdpPacketBuffer[NTP_PACKET_SIZE];   // buffer to hold incoming and outgoing packets
-
-        OnNetworkCB mOnNetworkCB;
-        OnTimeCB mOnTimeCB;
-
-};
-
-#endif /*__AHOYETH_H__*/
-#endif /* defined(ETHERNET) */
diff --git a/src/hm/CommQueue.h b/src/hm/CommQueue.h
index 328309ac..bf6f6861 100644
--- a/src/hm/CommQueue.h
+++ b/src/hm/CommQueue.h
@@ -19,13 +19,19 @@ template <uint8_t N=100>
 class CommQueue {
     public:
         void addImportant(Inverter<> *iv, uint8_t cmd) {
-            dec(&mRdPtr);
-            mQueue[mRdPtr] = queue_s(iv, cmd, true);
+            queue_s q(iv, cmd, true);
+            if(!isIncluded(&q)) {
+                dec(&mRdPtr);
+                mQueue[mRdPtr] = q;
+            }
         }
 
         void add(Inverter<> *iv, uint8_t cmd) {
-            mQueue[mWrPtr] = queue_s(iv, cmd, false);
-            inc(&mWrPtr);
+            queue_s q(iv, cmd, false);
+            if(!isIncluded(&q)) {
+                mQueue[mWrPtr] = q;
+                inc(&mWrPtr);
+            }
         }
 
         void chgCmd(Inverter<> *iv, uint8_t cmd) {
@@ -117,6 +123,19 @@ class CommQueue {
                 --(*ptr);
         }
 
+    private:
+        bool isIncluded(const queue_s *q) {
+            uint8_t ptr = mRdPtr;
+            while (ptr != mWrPtr) {
+                if(mQueue[ptr].cmd == q->cmd) {
+                    if(mQueue[ptr].iv->id == q->iv->id)
+                        return true;
+                }
+                inc(&ptr);
+            }
+            return false;
+        }
+
     protected:
         std::array<queue_s, N> mQueue;
         uint8_t mWrPtr = 0;
diff --git a/src/hm/Communication.h b/src/hm/Communication.h
index a37bcdb2..3192c513 100644
--- a/src/hm/Communication.h
+++ b/src/hm/Communication.h
@@ -117,7 +117,7 @@ class Communication : public CommQueue<> {
 
                     //q->iv->radioStatistics.txCnt++;
                     q->iv->radio->mRadioWaitTime.startTimeMonitor(mTimeout);
-                    if(!mIsRetransmit && (q->cmd == AlarmData) || (q->cmd == GridOnProFilePara))
+                    if((!mIsRetransmit && (q->cmd == AlarmData)) || (q->cmd == GridOnProFilePara))
                         incrAttempt((q->cmd == AlarmData)? MORE_ATTEMPS_ALARMDATA : MORE_ATTEMPS_GRIDONPROFILEPARA);
 
                     mIsRetransmit    = false;
@@ -159,8 +159,6 @@ class Communication : public CommQueue<> {
                                         mFirstTry = false;
                                     mHeu.evalTxChQuality(q->iv, false, 0, 0);
                                     mHeu.getTxCh(q->iv);
-                                    //q->iv->radioStatistics.rxFailNoAnser++;  // should only be one of fail or retransmit.
-                                    //q->iv->radioStatistics.txCnt--;
                                     q->iv->radioStatistics.retransmits++;
                                     q->iv->radio->mRadioWaitTime.stopTimeMonitor();
                                     mState = States::START;
@@ -276,7 +274,7 @@ class Communication : public CommQueue<> {
                                     DBGPRINT(F(" frames missing "));
                                     DBGPRINTLN(F("-> complete retransmit"));
                                 }
-                                mHeu.evalTxChQuality(q->iv, false, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt);
+                                mHeu.evalTxChQuality(q->iv, false, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt, true);
                                 q->iv->radioStatistics.txCnt--;
                                 q->iv->radioStatistics.retransmits++;
                                 mCompleteRetry = true;
@@ -622,7 +620,7 @@ class Communication : public CommQueue<> {
             else if(q->iv->mGotFragment || mCompleteRetry)
                 q->iv->radioStatistics.rxFail++; // got no complete payload
             else
-                q->iv->radioStatistics.rxFailNoAnser++; // got nothing
+                q->iv->radioStatistics.rxFailNoAnswer++; // got nothing
             mWaitTime.startTimeMonitor(1); // maybe remove, side effects unknown
 
             bool keep = false;
@@ -893,50 +891,60 @@ class Communication : public CommQueue<> {
 
             uint16_t prntsts = (statusMi == 3) ? 1 : statusMi;
             bool stsok = true;
-            if ( prntsts != rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)] ) { //sth.'s changed?
-                q->iv->alarmCnt = 1; // minimum...
+            bool changedStatus = false;  //if true, raise alarms and send via mqtt (might affect single channel only)
+            uint8_t oldState = rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)];
+            if ( prntsts != oldState ) { // sth.'s changed?
                 stsok = false;
-                //sth is or was wrong?
-                if ((q->iv->type != INV_TYPE_1CH)
-                    && ((statusMi != 3)
-                        || ((q->iv->lastAlarm[stschan].code) && (q->iv->lastAlarm[stschan].code != 1)))
-                   ) {
-                    q->iv->lastAlarm[stschan+q->iv->type==INV_TYPE_2CH ? 2: 4] = alarm_t(q->iv->lastAlarm[stschan].code, q->iv->lastAlarm[stschan].start,q->ts);
-                    q->iv->lastAlarm[stschan] = alarm_t(prntsts, q->ts,0);
-                    q->iv->alarmCnt = q->iv->type == INV_TYPE_2CH ? 3 : 5;
-                } else if ((q->iv->type == INV_TYPE_1CH)
-                    && ( (statusMi != 3)
-                        || ((q->iv->lastAlarm[stschan].code) && (q->iv->lastAlarm[stschan].code != 1)))
-                   ) {
-                    q->iv->lastAlarm[stschan] = alarm_t(q->iv->lastAlarm[0].code, q->iv->lastAlarm[0].start,q->ts);
-                } else if (q->iv->type == INV_TYPE_1CH)
-                    stsok = true;
-
-                q->iv->alarmLastId = prntsts; //iv->alarmMesIndex;
-
-                if (q->iv->alarmCnt > 1) { //more than one channel
-                    for (uint8_t ch = 0; ch < (q->iv->alarmCnt); ++ch) { //start with 1
-                        if (q->iv->lastAlarm[ch].code == 1) {
-                            stsok = true;
-                            break;
+                if( (!oldState) || (!q->iv->alarmCnt) ) {          // initial zero value? => just write this channel to main state and raise changed flags
+                    changedStatus = true;
+                    q->iv->alarmCnt = 1; // minimum...
+                } else {
+                    //sth is or was wrong?
+                    if (q->iv->type == INV_TYPE_1CH) {
+                        changedStatus = true;
+                        if(q->iv->alarmCnt == 2) // we had sth. other than "producing" in the past
+                            q->iv->lastAlarm[1].end = q->ts;
+                        else {                   // copy old state and mark as ended
+                            q->iv->lastAlarm[1] = alarm_t(q->iv->lastAlarm[0].code, q->iv->lastAlarm[0].start,q->ts);
+                            q->iv->alarmCnt = 2;
+                        }
+                    } else if((prntsts != 1) || (q->iv->alarmCnt > 1) ) { // we had sth. other than "producing" in the past in at least one channel (2 and 4 ch types)
+                        if (q->iv->alarmCnt == 1)
+                            q->iv->alarmCnt = (q->iv->type == INV_TYPE_2CH) ? 5 : 9;
+                        if(q->iv->lastAlarm[stschan].code != prntsts) { // changed?
+                            changedStatus = true;
+                            if(q->iv->lastAlarm[stschan].code) // copy old data and mark as ended (if any)
+                                q->iv->lastAlarm[(stschan + (q->iv->type==INV_TYPE_2CH ? 2 : 4))] = alarm_t(q->iv->lastAlarm[stschan].code, q->iv->lastAlarm[stschan].start,q->ts);
+                            q->iv->lastAlarm[stschan] = alarm_t(prntsts, q->ts,0);
+                        }
+                        if(changedStatus) {
+                            for (uint8_t i = 1; i <= q->iv->channels; i++) { //start with 1
+                                if (q->iv->lastAlarm[i].code == 1) {
+                                    stsok = true;
+                                    break;
+                                }
+                            }
                         }
                     }
                 }
-                if(*mSerialDebug) {
-                    DPRINT(DBG_WARN, F("New state on CH"));
-                    DBGPRINT(String(stschan)); DBGPRINT(F(" ("));
-                    DBGPRINT(String(prntsts)); DBGPRINT(F("): "));
-                    DBGPRINTLN(q->iv->getAlarmStr(prntsts));
-                }
-                if(!q->iv->miMultiParts)
-                    q->iv->miMultiParts = 1; // indicate we got status info (1+2 ch types)
             }
 
             if (!stsok) {
                 q->iv->setValue(q->iv->getPosByChFld(0, FLD_EVT, rec), rec, prntsts);
                 q->iv->lastAlarm[0] = alarm_t(prntsts, q->ts, 0);
+            }
+            if (changedStatus || !stsok) {
                 rec->ts = q->ts;
                 rec->mqttSentStatus = MqttSentStatus::NEW_DATA;
+                q->iv->alarmLastId = prntsts; //iv->alarmMesIndex;
+                if (NULL != mCbAlarm)
+                    (mCbAlarm)(q->iv);
+                if(*mSerialDebug) {
+                    DPRINT(DBG_WARN, F("New state on CH"));
+                    DBGPRINT(String(stschan)); DBGPRINT(F(" ("));
+                    DBGPRINT(String(prntsts)); DBGPRINT(F("): "));
+                    DBGPRINTLN(q->iv->getAlarmStr(prntsts));
+                }
             }
 
             if (q->iv->alarmMesIndex < rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)]) {
@@ -947,6 +955,8 @@ class Communication : public CommQueue<> {
                     DBGPRINTLN(String(q->iv->alarmMesIndex));
                 }
             }
+            if(!q->iv->miMultiParts)
+                q->iv->miMultiParts = 1; // indicate we got status info (1+2 ch types)
         }
 
 
@@ -986,10 +996,8 @@ class Communication : public CommQueue<> {
                     ac_pow += iv->getValue(iv->getPosByChFld(1, FLD_PDC, rec), rec);
             } else {
                 for(uint8_t i = 1; i <= iv->channels; i++) {
-                    if ((!iv->lastAlarm[i].code) || (iv->lastAlarm[i].code == 1)) {
-                        uint8_t pos = iv->getPosByChFld(i, FLD_PDC, rec);
-                        ac_pow += iv->getValue(pos, rec);
-                    }
+                    if ((!iv->lastAlarm[i].code) || (iv->lastAlarm[i].code == 1))
+                        ac_pow += iv->getValue(iv->getPosByChFld(i, FLD_PDC, rec), rec);
                 }
             }
             ac_pow = (int) (ac_pow*9.5);
diff --git a/src/hm/Heuristic.h b/src/hm/Heuristic.h
index ecf82aa7..1220692e 100644
--- a/src/hm/Heuristic.h
+++ b/src/hm/Heuristic.h
@@ -38,6 +38,8 @@ class Heuristic {
                     ih->txRfChId = curId;
                 curId = (curId + 1) % RF_MAX_CHANNEL_ID;
             }
+            if(ih->txRfQuality[ih->txRfChId] == RF_MIN_QUALTIY) // all channels are bad, reset...
+                ih->clear();
 
             if(ih->testPeriodSendCnt < 0xff)
                 ih->testPeriodSendCnt++;
@@ -71,7 +73,7 @@ class Heuristic {
             return id2Ch(ih->txRfChId);
         }
 
-        void evalTxChQuality(Inverter<> *iv, bool crcPass, uint8_t retransmits, uint8_t rxFragments) {
+        void evalTxChQuality(Inverter<> *iv, bool crcPass, uint8_t retransmits, uint8_t rxFragments, bool quotaMissed = false) {
             HeuristicInv *ih = &iv->heuristics;
 
             #if (DBG_DEBUG == DEBUG_LEVEL)
@@ -84,8 +86,10 @@ class Heuristic {
             DBGPRINT(", ");
             DBGPRINTLN(String(ih->lastRxFragments));
             #endif
+            if(quotaMissed)                             // we got not enough frames on this attempt, but iv was answering
+                updateQuality(ih, (rxFragments > 3 ? RF_TX_CHAN_QUALITY_GOOD : (rxFragments > 1 ?  RF_TX_CHAN_QUALITY_OK : RF_TX_CHAN_QUALITY_LOW)));
 
-            if(ih->lastRxFragments == rxFragments) {
+            else if(ih->lastRxFragments == rxFragments) {
                 if(crcPass)
                     updateQuality(ih, RF_TX_CHAN_QUALITY_GOOD);
                 else if(!retransmits || isNewTxCh(ih)) { // nothing received: send probably lost
@@ -149,7 +153,7 @@ class Heuristic {
             DBGPRINT(F(", f: "));
             DBGPRINT(String(iv->radioStatistics.rxFail));
             DBGPRINT(F(", n: "));
-            DBGPRINT(String(iv->radioStatistics.rxFailNoAnser));
+            DBGPRINT(String(iv->radioStatistics.rxFailNoAnswer));
             DBGPRINT(F(" | p: "));                                  // better debugging for helpers...
             if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen))
                 DBGPRINTLN(String(iv->config->powerLevel-10));
@@ -213,15 +217,12 @@ class Heuristic {
         }
 
         inline uint8_t id2Ch(uint8_t id) {
-            switch(id) {
-                case 0: return 3;
-                case 1: return 23;
-                case 2: return 40;
-                case 3: return 61;
-                case 4: return 75;
-            }
-            return 3; // standard
+            if (id < RF_MAX_CHANNEL_ID)
+                return mChList[id];
+            else
+                return 3; // standard
         }
+        uint8_t mChList[RF_MAX_CHANNEL_ID] = {03, 23, 40, 61, 75};
 };
 
 
diff --git a/src/hm/hmRadio.h b/src/hm/NrfRadio.h
similarity index 90%
rename from src/hm/hmRadio.h
rename to src/hm/NrfRadio.h
index d1d24364..21d0c676 100644
--- a/src/hm/hmRadio.h
+++ b/src/hm/NrfRadio.h
@@ -8,9 +8,10 @@
 
 #include <RF24.h>
 #include "SPI.h"
-#include "radio.h"
+#include "Radio.h"
 #include "../config/config.h"
-#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
+#include "../config/settings.h"
+#if defined(SPI_HAL)
 #include "nrfHal.h"
 #endif
 
@@ -28,24 +29,30 @@ const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"};
 //-----------------------------------------------------------------------------
 // HM Radio class
 //-----------------------------------------------------------------------------
-template <uint8_t IRQ_PIN = DEF_NRF_IRQ_PIN, uint8_t CE_PIN = DEF_NRF_CE_PIN, uint8_t CS_PIN = DEF_NRF_CS_PIN, uint8_t AMP_PWR = RF24_PA_LOW, uint8_t SCLK_PIN = DEF_NRF_SCLK_PIN, uint8_t MOSI_PIN = DEF_NRF_MOSI_PIN, uint8_t MISO_PIN = DEF_NRF_MISO_PIN, uint32_t DTU_SN = 0x81001765>
-class HmRadio : public Radio {
+template <uint32_t DTU_SN = 0x81001765>
+class NrfRadio : public Radio {
     public:
-        HmRadio() {
+        NrfRadio() {
             mDtuSn   = DTU_SN;
             mIrqRcvd = false;
-            #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
+            #if defined(SPI_HAL)
             //mNrf24.reset(new RF24());
             #else
-            mNrf24.reset(new RF24(CE_PIN, CS_PIN, SPI_SPEED));
+            mNrf24.reset(new RF24(DEF_NRF_CE_PIN, DEF_NRF_CS_PIN, SPI_SPEED));
             #endif
         }
-        ~HmRadio() {}
+        ~NrfRadio() {}
 
-        void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN, uint8_t sclk = SCLK_PIN, uint8_t mosi = MOSI_PIN, uint8_t miso = MISO_PIN) {
-            DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup"));
+        void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, cfgNrf24_t *cfg) {
+            DPRINTLN(DBG_VERBOSE, F("NrfRadio::setup"));
 
-            pinMode(irq, INPUT_PULLUP);
+            mCfg = cfg;
+            //uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN, uint8_t sclk = SCLK_PIN, uint8_t mosi = MOSI_PIN, uint8_t miso = MISO_PIN
+
+            if(!mCfg->enabled)
+                return;
+
+            pinMode(mCfg->pinIrq, INPUT_PULLUP);
 
             mSerialDebug     = serialDebug;
             mPrivacyMode     = privacyMode;
@@ -55,8 +62,8 @@ class HmRadio : public Radio {
             mDtuRadioId = ((uint64_t)(((mDtuSn >> 24) & 0xFF) | ((mDtuSn >> 8) & 0xFF00) | ((mDtuSn << 8) & 0xFF0000) | ((mDtuSn << 24) & 0xFF000000)) << 8) | 0x01;
 
             #ifdef ESP32
-                #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
-                    mNrfHal.init(mosi, miso, sclk, cs, ce, SPI_SPEED);
+                #if defined(SPI_HAL)
+                    mNrfHal.init(mCfg->pinMosi, mCfg->pinMiso, mCfg->pinSclk, mCfg->pinCs, mCfg->pinCe, SPI_SPEED);
                     mNrf24.reset(new RF24(&mNrfHal));
                 #else
                     #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
@@ -64,7 +71,7 @@ class HmRadio : public Radio {
                     #else
                         mSpi.reset(new SPIClass(VSPI));
                     #endif
-                    mSpi->begin(sclk, miso, mosi, cs);
+                    mSpi->begin(mCfg->pinSclk, mCfg->pinMiso, mCfg->pinMosi, mCfg->pinCs);
                 #endif
             #else
                 //the old ESP82xx cannot freely place their SPI pins
@@ -72,12 +79,12 @@ class HmRadio : public Radio {
                 mSpi->begin();
             #endif
 
-            #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
+            #if defined(SPI_HAL)
                 mNrf24->begin();
             #else
-                mNrf24->begin(mSpi.get(), ce, cs);
+                mNrf24->begin(mSpi.get(), mCfg->pinCe, mCfg->pinCs);
             #endif
-            mNrf24->setRetries(3, 9); // wait 3*250 = 750us, 16 * 250us -> 4000us = 4ms
+            mNrf24->setRetries(3, 15); // wait 3*250 = 750us, 16 * 250us -> 4000us = 4ms
 
             mNrf24->setDataRate(RF24_250KBPS);
             //mNrf24->setAutoAck(true); // enabled by default
@@ -99,21 +106,24 @@ class HmRadio : public Radio {
         }
 
         // returns true if communication is active
-        bool loop(void) override {
+        void loop(void) {
+            if(!mCfg->enabled)
+                return;
+
             if (!mIrqRcvd && !mNRFisInRX)
-                return false; // first quick check => nothing to do at all here
+                return; // first quick check => nothing to do at all here
 
             if(NULL == mLastIv) // prevent reading on NULL object!
-                return false;
+                return;
 
             if(!mIrqRcvd) {     // no news from nRF, check timers
                 if ((millis() - mTimeslotStart) < innerLoopTimeout)
-                    return true; // nothing to do, still waiting
+                    return; // nothing to do, still waiting
 
                 if (mRadioWaitTime.isTimeout()) { // timeout reached!
                     mNRFisInRX = false;
                     rx_ready = false;
-                    return false;
+                    return;
                 }
 
                 // otherwise switch to next RX channel
@@ -132,7 +142,7 @@ class HmRadio : public Radio {
                 mNrf24->setChannel(mRfChLst[tempRxChIdx]);
                 isRxInit = false;
 
-                return true; // communicating, but changed RX channel
+                return; // communicating, but changed RX channel
             } else {
                 // here we got news from the nRF
                 mIrqRcvd     = false;
@@ -145,7 +155,7 @@ class HmRadio : public Radio {
 
                     if(mNRFisInRX) {
                         DPRINTLN(DBG_WARN, F("unexpected tx irq!"));
-                        return false;
+                        return;
                     }
 
                     mNRFisInRX = true;
@@ -159,7 +169,7 @@ class HmRadio : public Radio {
                     mTimeslotStart = millis();
                     tempRxChIdx = mRxChIdx;  // might be better to start off with one channel less?
                     mRxPendular = false;
-                    mNRFloopChannels = (mLastIv->mCmd == MI_REQ_CH1);
+                    mNRFloopChannels = (mLastIv->mCmd == MI_REQ_CH1 || mLastIv->mCmd == MI_REQ_CH2);
                     innerLoopTimeout = DURATION_LISTEN_MIN;
                 }
 
@@ -181,18 +191,23 @@ class HmRadio : public Radio {
                         }
                     }
                     rx_ready = false; // reset
-                    return mNRFisInRX;
+                    return;
                 }
             }
 
-            return false;
+            return;
         }
 
         bool isChipConnected(void) const override {
+            if(!mCfg->enabled)
+                return false;
             return mNrf24->isChipConnected();
         }
 
         void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) override {
+            if(!mCfg->enabled)
+                return;
+
             DPRINT_IVID(DBG_INFO, iv->id);
             DBGPRINT(F("sendControlPacket cmd: "));
             DBGHEXLN(cmd);
@@ -279,13 +294,14 @@ class HmRadio : public Radio {
         }
 
         uint8_t getDataRate(void) const {
-            if(!mNrf24->isChipConnected())
+            if(!isChipConnected())
                 return 3; // unknown
             return mNrf24->getDataRate();
         }
 
         bool isPVariant(void) const {
-            return mNrf24->isPVariant();
+            if(!isChipConnected())
+                return mNrf24->isPVariant();
         }
 
     private:
@@ -413,6 +429,7 @@ class HmRadio : public Radio {
         }
 
         uint64_t mDtuRadioId = 0ULL;
+        cfgNrf24_t *mCfg = nullptr;
         const uint8_t mRfChLst[RF_CHANNELS] = {03, 23, 40, 61, 75}; // channel List:2403, 2423, 2440, 2461, 2475MHz
         uint8_t mTxChIdx = 0;
         uint8_t mRxChIdx = 0;
@@ -432,7 +449,7 @@ class HmRadio : public Radio {
 
         std::unique_ptr<SPIClass> mSpi;
         std::unique_ptr<RF24> mNrf24;
-        #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
+        #if defined(SPI_HAL)
         nrfHal mNrfHal;
         #endif
         Inverter<> *mLastIv = NULL;
diff --git a/src/hm/radio.h b/src/hm/Radio.h
similarity index 99%
rename from src/hm/radio.h
rename to src/hm/Radio.h
index 31643980..12e80850 100644
--- a/src/hm/radio.h
+++ b/src/hm/Radio.h
@@ -33,7 +33,7 @@ class Radio {
         virtual uint16_t getBaseFreqMhz() { return 0; }
         virtual uint16_t getBootFreqMhz() { return 0; }
         virtual std::pair<uint16_t,uint16_t> getFreqRangeMhz(void) { return std::make_pair(0, 0); }
-        virtual bool loop(void) = 0;
+        virtual void loop(void) = 0;
 
         Radio() : mTxBuf{} {}
 
diff --git a/src/hm/hmDefines.h b/src/hm/hmDefines.h
index 6ba92774..4287d26e 100644
--- a/src/hm/hmDefines.h
+++ b/src/hm/hmDefines.h
@@ -9,6 +9,41 @@
 #include "../utils/dbg.h"
 #include <cstdint>
 
+typedef enum {
+    InverterDevInform_Simple = 0,  // 0x00
+    InverterDevInform_All    = 1,  // 0x01
+    GridOnProFilePara        = 2,  // 0x02
+    HardWareConfig           = 3,  // 0x03
+    SimpleCalibrationPara    = 4,  // 0x04
+    SystemConfigPara         = 5,  // 0x05
+    RealTimeRunData_Debug    = 11, // 0x0b
+    RealTimeRunData_Reality  = 12, // 0x0c
+    RealTimeRunData_A_Phase  = 13, // 0x0d
+    RealTimeRunData_B_Phase  = 14, // 0x0e
+    RealTimeRunData_C_Phase  = 15, // 0x0f
+    AlarmData                = 17, // 0x11, Alarm data - all unsent alarms
+    AlarmUpdate              = 18, // 0x12, Alarm data - all pending alarms
+    RecordData               = 19, // 0x13
+    InternalData             = 20, // 0x14
+    GetLossRate              = 21, // 0x15
+    GetSelfCheckState        = 30, // 0x1e
+    InitDataState            = 0xff
+} InfoCmdType;
+
+typedef enum {
+    TurnOn                  = 0,  // 0x00
+    TurnOff                 = 1,  // 0x01
+    Restart                 = 2,  // 0x02
+    Lock                    = 3,  // 0x03
+    Unlock                  = 4,  // 0x04
+    ActivePowerContr        = 11, // 0x0b
+    ReactivePowerContr      = 12, // 0x0c
+    PFSet                   = 13, // 0x0d
+    CleanState_LockAndAlarm = 20, // 0x14
+    SelfInspection          = 40, // 0x28, self-inspection of grid-connected protection files
+    Init                    = 0xff
+} DevControlCmdType;
+
 // inverter generations
 enum {IV_MI = 0, IV_HM, IV_HMS, IV_HMT, IV_UNKNOWN};
 const char* const generationNames[] = {"MI", "HM", "HMS", "HMT", "UNKNOWN"};
@@ -24,20 +59,20 @@ enum {FLD_UDC = 0, FLD_IDC, FLD_PDC, FLD_YD, FLD_YW, FLD_YT,
         FLD_IRR, FLD_Q, FLD_EVT, FLD_FW_VERSION, FLD_FW_BUILD_YEAR,
         FLD_FW_BUILD_MONTH_DAY, FLD_FW_BUILD_HOUR_MINUTE, FLD_BOOTLOADER_VER,
         FLD_ACT_ACTIVE_PWR_LIMIT, FLD_PART_NUM, FLD_HW_VERSION, FLD_GRID_PROFILE_CODE,
-        FLD_GRID_PROFILE_VERSION,  /*FLD_ACT_REACTIVE_PWR_LIMIT, FLD_ACT_PF,*/ FLD_LAST_ALARM_CODE, FLD_MP};
+        FLD_GRID_PROFILE_VERSION,  /*FLD_ACT_REACTIVE_PWR_LIMIT, FLD_ACT_PF,*/ FLD_LAST_ALARM_CODE, FLD_MP, FLD_MT};
 
 const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "YieldTotal",
-        "U_AC", "U_AC_1N", "U_AC_2N", "U_AC_3N", "UAC_12", "UAC_23", "UAC_31", "I_AC",
-        "IAC_1", "I_AC_2", "I_AC_3", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC",
+        "U_AC", "U_AC_1N", "U_AC_2N", "U_AC_3N", "U_AC_12", "U_AC_23", "U_AC_31", "I_AC",
+        "I_AC_1", "I_AC_2", "I_AC_3", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC",
         "ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","FWBuildHourMinute","BootloaderVersion",
         "active_PowerLimit", "HWPartNumber", "HWVersion", "GridProfileCode",
-        "GridProfileVersion", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode", "MaxPower"};
+        "GridProfileVersion", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode", "MaxPower", "MaxTemp"};
 const char* const notAvail = "n/a";
 
 const uint8_t fieldUnits[] = {UNIT_V, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_KWH,
         UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_A, UNIT_A, UNIT_A, UNIT_A,
         UNIT_W, UNIT_HZ, UNIT_C, UNIT_NONE, UNIT_PCT, UNIT_PCT, UNIT_VAR,
-        UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_PCT, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_W};
+        UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_PCT, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_W, UNIT_C};
 
 // mqtt discovery device classes
 enum {DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, DEVICE_CLS_TEMP};
@@ -68,7 +103,7 @@ const byteAssign_fieldDeviceClass deviceFieldAssignment[] = {
 #define DEVICE_CLS_ASSIGN_LIST_LEN     (sizeof(deviceFieldAssignment) / sizeof(byteAssign_fieldDeviceClass))
 
 // indices to calculation functions, defined in hmInverter.h
-enum {CALC_YT_CH0 = 0, CALC_YD_CH0, CALC_UDC_CH, CALC_PDC_CH0, CALC_EFF_CH0, CALC_IRR_CH, CALC_MPAC_CH0, CALC_MPDC_CH};
+enum {CALC_YT_CH0 = 0, CALC_YD_CH0, CALC_UDC_CH, CALC_PDC_CH0, CALC_EFF_CH0, CALC_IRR_CH, CALC_MPAC_CH0, CALC_MPDC_CH, CALC_MT_CH0};
 enum {CMD_CALC = 0xffff};
 
 
@@ -173,7 +208,8 @@ const byteAssign_t hm1chAssignment[] = {
     { FLD_YT,  UNIT_KWH,  CH0, CALC_YT_CH0,   0, CMD_CALC },
     { FLD_PDC, UNIT_W,    CH0, CALC_PDC_CH0,  0, CMD_CALC },
     { FLD_EFF, UNIT_PCT,  CH0, CALC_EFF_CH0,  0, CMD_CALC },
-    { FLD_MP,  UNIT_W,    CH0, CALC_MPAC_CH0, 0, CMD_CALC }
+    { FLD_MP,  UNIT_W,    CH0, CALC_MPAC_CH0, 0, CMD_CALC },
+    { FLD_MT,  UNIT_C,    CH0, CALC_MT_CH0,   0, CMD_CALC }
 };
 #define HM1CH_LIST_LEN      (sizeof(hm1chAssignment) / sizeof(byteAssign_t))
 #define HM1CH_PAYLOAD_LEN   30
@@ -211,7 +247,8 @@ const byteAssign_t hm2chAssignment[] = {
     { FLD_YT,  UNIT_KWH,  CH0, CALC_YT_CH0,   0, CMD_CALC },
     { FLD_PDC, UNIT_W,    CH0, CALC_PDC_CH0,  0, CMD_CALC },
     { FLD_EFF, UNIT_PCT,  CH0, CALC_EFF_CH0,  0, CMD_CALC },
-    { FLD_MP,  UNIT_W,    CH0, CALC_MPAC_CH0, 0, CMD_CALC }
+    { FLD_MP,  UNIT_W,    CH0, CALC_MPAC_CH0, 0, CMD_CALC },
+    { FLD_MT,  UNIT_C,    CH0, CALC_MT_CH0,   0, CMD_CALC }
 
 };
 #define HM2CH_LIST_LEN      (sizeof(hm2chAssignment) / sizeof(byteAssign_t))
@@ -266,7 +303,8 @@ const byteAssign_t hm4chAssignment[] = {
     { FLD_YT,  UNIT_KWH,  CH0, CALC_YT_CH0,   0, CMD_CALC },
     { FLD_PDC, UNIT_W,    CH0, CALC_PDC_CH0,  0, CMD_CALC },
     { FLD_EFF, UNIT_PCT,  CH0, CALC_EFF_CH0,  0, CMD_CALC },
-    { FLD_MP,  UNIT_W,    CH0, CALC_MPAC_CH0, 0, CMD_CALC }
+    { FLD_MP,  UNIT_W,    CH0, CALC_MPAC_CH0, 0, CMD_CALC },
+    { FLD_MT,  UNIT_C,    CH0, CALC_MT_CH0,   0, CMD_CALC }
 };
 #define HM4CH_LIST_LEN      (sizeof(hm4chAssignment) / sizeof(byteAssign_t))
 #define HM4CH_PAYLOAD_LEN   62
@@ -351,8 +389,11 @@ const devInfo_t devInfo[] = {
     { 0x102271, 2000 }, // v2 black backplane, 16A
 
     // HMT
-    { 0x103311, 1800 },
-    { 0x103331, 2250 }
+    { 0x103241, 1600 }, // -4T
+    { 0x103251, 1800 }, // -4T
+    { 0x103271, 2000 }, // -4T
+    { 0x103311, 1800 }, // -6T
+    { 0x103331, 2250 }  // -6T
 };
 
 #define MI_REQ_CH1 0x09
diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h
index 50b4267c..026bf6d8 100644
--- a/src/hm/hmInverter.h
+++ b/src/hm/hmInverter.h
@@ -22,7 +22,7 @@
 #include <functional>
 #include "../config/settings.h"
 
-#include "radio.h"
+#include "Radio.h"
 /**
  * For values which are of interest and not transmitted by the inverter can be
  * calculated automatically.
@@ -33,28 +33,31 @@
 
 // prototypes
 template<class T=float>
-static T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0);
+T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0);
 
 template<class T=float>
-static T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0);
+T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0);
 
 template<class T=float>
-static T calcUdcCh(Inverter<> *iv, uint8_t arg0);
+T calcUdcCh(Inverter<> *iv, uint8_t arg0);
 
 template<class T=float>
-static T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0);
+T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0);
 
 template<class T=float>
-static T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0);
+T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0);
 
 template<class T=float>
-static T calcIrradiation(Inverter<> *iv, uint8_t arg0);
+T calcIrradiation(Inverter<> *iv, uint8_t arg0);
 
 template<class T=float>
-static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0);
+T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0);
 
 template<class T=float>
-static T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0);
+T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0);
+
+template<class T=float>
+T calcMaxTemperature(Inverter<> *iv, uint8_t arg0);
 
 template<class T=float>
 using func_t = T (Inverter<> *, uint8_t);
@@ -84,7 +87,7 @@ struct record_t {
     byteAssign_t* assign = nullptr; // assignment of bytes in payload
     uint8_t length = 0;             // length of the assignment list
     T *record = nullptr;            // data pointer
-    uint32_t ts = 0;                // timestamp of last received payload
+    uint32_t ts = 0;                // Timestamp of last received payload
     uint8_t pyldLen = 0;            // expected payload length for plausibility check
     MqttSentStatus mqttSentStatus = MqttSentStatus:: NEW_DATA; // indicates the current MqTT sent status
 };
@@ -100,14 +103,15 @@ struct alarm_t {
 // list of all available functions, mapped in hmDefines.h
 template<class T=float>
 const calcFunc_t<T> calcFunctions[] = {
-    { CALC_YT_CH0,   &calcYieldTotalCh0 },
-    { CALC_YD_CH0,   &calcYieldDayCh0   },
-    { CALC_UDC_CH,   &calcUdcCh         },
-    { CALC_PDC_CH0,  &calcPowerDcCh0    },
-    { CALC_EFF_CH0,  &calcEffiencyCh0   },
-    { CALC_IRR_CH,   &calcIrradiation   },
-    { CALC_MPAC_CH0, &calcMaxPowerAcCh0 },
-    { CALC_MPDC_CH,  &calcMaxPowerDc    }
+    { CALC_YT_CH0,   &calcYieldTotalCh0  },
+    { CALC_YD_CH0,   &calcYieldDayCh0    },
+    { CALC_UDC_CH,   &calcUdcCh          },
+    { CALC_PDC_CH0,  &calcPowerDcCh0     },
+    { CALC_EFF_CH0,  &calcEffiencyCh0    },
+    { CALC_IRR_CH,   &calcIrradiation    },
+    { CALC_MPAC_CH0, &calcMaxPowerAcCh0  },
+    { CALC_MPDC_CH,  &calcMaxPowerDc     },
+    { CALC_MT_CH0,   &calcMaxTemperature }
 };
 
 template <class REC_TYP>
@@ -146,7 +150,8 @@ class Inverter {
         statistics_t  radioStatistics;                      // information about transmitted, failed, ... packets
         HeuristicInv  heuristics;                           // heuristic information / logic
         uint8_t       curCmtFreq = 0;                       // current used CMT frequency, used to check if freq. was changed during runtime
-        uint32_t      tsMaxAcPower = 0;                     // holds the timestamp when the MaxAC power was seen
+        uint32_t      tsMaxAcPower = 0;                     // holds the Timestamp when the MaxAC power was seen
+        uint32_t      tsMaxTemperature = 0;                 // holds the Timestamp when the max temperature was seen
         bool          commEnabled = true;                   // 'pause night communication' sets this field to false
 
     public:
@@ -189,7 +194,7 @@ class Inverter {
                     cb(InverterDevInform_Simple, false); // get hardware version
                 } else if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0)) {
                     cb(AlarmData, false);                // get last alarms
-                } else if((0 == mGridLen) && generalConfig->readGrid) { // read grid profile
+                } else if((0 == mGridLen) && GeneralConfig->readGrid) { // read grid profile
                     cb(GridOnProFilePara, false);
                 } else if (mGetLossInterval > AHOY_GET_LOSS_INTERVAL) { // get loss rate
                     mGetLossInterval = 1;
@@ -213,7 +218,7 @@ class Inverter {
                         if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0) {
                             cb(0x0f, false); // hard- and firmware version for missing HW part nr, delivered by frame 1
                             mIvRxCnt +=2;
-                        } else if((getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec) == 0) && generalConfig->readGrid) // read grid profile
+                        } else if((getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec) == 0) && GeneralConfig->readGrid) // read grid profile
                             cb(0x10, false); // legacy GPF command
                     }
                 }
@@ -229,19 +234,20 @@ class Inverter {
             initAssignment(&recordAlarm, AlarmData);
             toRadioId();
             curCmtFreq = this->config->frequency; // update to frequency read from settings
+            resetAlarms(true);
         }
 
         uint8_t getPosByChFld(uint8_t channel, uint8_t fieldId, record_t<> *rec) {
             DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getPosByChFld"));
-            if(NULL != rec) {
-                uint8_t pos = 0;
-                for(; pos < rec->length; pos++) {
-                    if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId))
-                        break;
-                }
-                return (pos >= rec->length) ? 0xff : pos;
-            } else
+            if(nullptr == rec)
                 return 0xff;
+
+            uint8_t pos = 0;
+            for(; pos < rec->length; pos++) {
+                if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId))
+                    break;
+            }
+            return (pos >= rec->length) ? 0xff : pos;
         }
 
         byteAssign_t *getByteAssign(uint8_t pos, record_t<> *rec) {
@@ -270,15 +276,18 @@ class Inverter {
             if(InverterStatus::OFF != status) {
                 mDevControlRequest = true;
                 devControlCmd = cmd;
-                //app->triggerTickSend(); // done in RestApi.h, because of "chicken-and-egg problem ;-)"
+                assert(App);
+                App->triggerTickSend(id);
+                return true;
             }
-            return (InverterStatus::OFF != status);
+            return false;
         }
 
         bool setDevCommand(uint8_t cmd) {
-            if(InverterStatus::OFF != status)
+            bool retval = (InverterStatus::OFF != status);
+            if(retval)
                 devControlCmd = cmd;
-            return (InverterStatus::OFF != status);
+            return retval;
         }
 
         void addValue(uint8_t pos, const uint8_t buf[], record_t<> *rec) {
@@ -288,32 +297,30 @@ class Inverter {
                 uint8_t  end = ptr + rec->assign[pos].num;
                 uint16_t div = rec->assign[pos].div;
 
-                if(NULL != rec) {
-                    if(CMD_CALC != div) {
-                        uint32_t val = 0;
-                        do {
-                            val <<= 8;
-                            val |= buf[ptr];
-                        } while(++ptr != end);
-
-                        if ((FLD_T == rec->assign[pos].fieldId) || (FLD_Q == rec->assign[pos].fieldId) || (FLD_PF == rec->assign[pos].fieldId)) {
-                            // temperature, Qvar, and power factor are a signed values
-                            rec->record[pos] = ((REC_TYP)((int16_t)val)) / (REC_TYP)(div);
-                        } else if (FLD_YT == rec->assign[pos].fieldId) {
-                            rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]);
-                        } else if (FLD_YD == rec->assign[pos].fieldId) {
-                            float actYD = (REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency;
-                            uint8_t idx = rec->assign[pos].ch - 1;
-                            if (mLastYD[idx] > actYD)
-                                mOffYD[idx] += mLastYD[idx];
-                            mLastYD[idx] = actYD;
-                            rec->record[pos] = mOffYD[idx] + actYD;
-                        } else {
-                            if ((REC_TYP)(div) > 1)
-                                rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div);
-                            else
-                                rec->record[pos] = (REC_TYP)(val);
-                        }
+                if(CMD_CALC != div) {
+                    uint32_t val = 0;
+                    do {
+                        val <<= 8;
+                        val |= buf[ptr];
+                    } while(++ptr != end);
+
+                    if ((FLD_T == rec->assign[pos].fieldId) || (FLD_Q == rec->assign[pos].fieldId) || (FLD_PF == rec->assign[pos].fieldId)) {
+                        // temperature, Qvar, and power factor are a signed values
+                        rec->record[pos] = ((REC_TYP)((int16_t)val)) / (REC_TYP)(div);
+                    } else if (FLD_YT == rec->assign[pos].fieldId) {
+                        rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div)) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]);
+                    } else if (FLD_YD == rec->assign[pos].fieldId) {
+                        float actYD = (REC_TYP)(val) / (REC_TYP)(div);
+                        uint8_t idx = rec->assign[pos].ch - 1;
+                        if (mLastYD[idx] > actYD)
+                            mOffYD[idx] += mLastYD[idx];
+                        mLastYD[idx] = actYD;
+                        rec->record[pos] = mOffYD[idx] + actYD;
+                    } else {
+                        if ((REC_TYP)(div) > 1)
+                            rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div);
+                        else
+                            rec->record[pos] = (REC_TYP)(val);
                     }
                 }
 
@@ -337,7 +344,7 @@ class Inverter {
                         // eg. hw version ...
                     } else if (rec->assign == SystemConfigParaAssignment) {
                         DPRINTLN(DBG_DEBUG, "add config");
-                        if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){
+                        if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos) {
                             actPowerLimit = rec->record[pos];
                             DPRINT(DBG_DEBUG, F("Inverter actual power limit: "));
                             DPRINTLN(DBG_DEBUG, String(actPowerLimit, 1));
@@ -356,7 +363,7 @@ class Inverter {
 
         bool setValue(uint8_t pos, record_t<> *rec, REC_TYP val) {
             DPRINTLN(DBG_VERBOSE, F("hmInverter.h:setValue"));
-            if(NULL == rec)
+            if(nullptr == rec)
                 return false;
             if(pos > rec->length)
                 return false;
@@ -407,23 +414,17 @@ class Inverter {
         bool isAvailable() {
             bool avail = false;
 
-            if((recordMeas.ts == 0) && (recordInfo.ts == 0) && (recordConfig.ts == 0) && (recordAlarm.ts == 0))
+            if(recordMeas.ts == 0)
                 return false;
 
-            if((*timestamp - recordMeas.ts) < INVERTER_INACT_THRES_SEC)
-                avail = true;
-            if((*timestamp - recordInfo.ts) < INVERTER_INACT_THRES_SEC)
-                avail = true;
-            if((*timestamp - recordConfig.ts) < INVERTER_INACT_THRES_SEC)
-                avail = true;
-            if((*timestamp - recordAlarm.ts) < INVERTER_INACT_THRES_SEC)
+            if(((*Timestamp) - recordMeas.ts) < INVERTER_INACT_THRES_SEC)
                 avail = true;
 
             if(avail) {
                 if(status < InverterStatus::PRODUCING)
                     status = InverterStatus::STARTING;
             } else {
-                if((*timestamp - recordMeas.ts) > INVERTER_OFF_THRES_SEC) {
+                if(((*Timestamp) - recordMeas.ts) > INVERTER_OFF_THRES_SEC) {
                     if(status != InverterStatus::OFF) {
                         status = InverterStatus::OFF;
                         actPowerLimit = 0xffff; // power limit will be read once inverter becomes available
@@ -537,6 +538,10 @@ class Inverter {
                             rec->length  = (uint8_t)(HMS4CH_LIST_LEN);
                             rec->assign  = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(hms4chAssignment));
                             rec->pyldLen = HMS4CH_PAYLOAD_LEN;
+                        } else if(IV_HMT == ivGen){
+                            rec->length  = (uint8_t)(HMT4CH_LIST_LEN);
+                            rec->assign  = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(hmt4chAssignment));
+                            rec->pyldLen = HMT4CH_PAYLOAD_LEN;
                         }
                         channels = 4;
                     }
@@ -584,7 +589,7 @@ class Inverter {
             }
         }
 
-        void resetAlarms() {
+        void resetAlarms(bool clearTs = false) {
             lastAlarm.fill({0, 0, 0});
             mAlarmNxtWrPos = 0;
             alarmCnt = 0;
@@ -592,6 +597,11 @@ class Inverter {
 
             memset(mOffYD, 0, sizeof(float) * 6);
             memset(mLastYD, 0, sizeof(float) * 6);
+
+            if(clearTs) {
+                tsMaxAcPower = *Timestamp;
+                tsMaxTemperature = *Timestamp;
+            }
         }
 
         bool parseGetLossRate(const uint8_t pyld[], uint8_t len) {
@@ -660,7 +670,6 @@ class Inverter {
             DPRINTLN(DBG_DEBUG, "Alarm #" + String(pyld[startOff+1]) + " '" + String(getAlarmStr(pyld[startOff+1])) + "' start: " + ah::getTimeStr(start) + ", end: " + ah::getTimeStr(endTime));
             addAlarm(pyld[startOff+1], start, endTime);
 
-            alarmCnt++;
             alarmLastId = alarmMesIndex;
 
             return pyld[startOff+1];
@@ -808,6 +817,26 @@ class Inverter {
 
     private:
         inline void addAlarm(uint16_t code, uint32_t start, uint32_t end) {
+            uint8_t i = 0;
+
+            if(start > end)
+                end = 0;
+
+            for(; i < 10; i++) {
+                ++mAlarmNxtWrPos;
+                mAlarmNxtWrPos = mAlarmNxtWrPos % 10;
+
+                if(lastAlarm[mAlarmNxtWrPos].code == code && lastAlarm[mAlarmNxtWrPos].start == start) {
+                    // replace with same or update end time
+                    if(lastAlarm[mAlarmNxtWrPos].end == 0 || lastAlarm[mAlarmNxtWrPos].end == end) {
+                        break;
+                    }
+                }
+            }
+
+            if(alarmCnt < 10 && alarmCnt <= mAlarmNxtWrPos)
+                alarmCnt = mAlarmNxtWrPos + 1;
+
             lastAlarm[mAlarmNxtWrPos] = alarm_t(code, start, end);
             if(++mAlarmNxtWrPos >= 10) // rolling buffer
                 mAlarmNxtWrPos = 0;
@@ -824,8 +853,9 @@ class Inverter {
         }
 
     public:
-        static uint32_t  *timestamp;     // system timestamp
-        static cfgInst_t *generalConfig; // general inverter configuration from setup
+        static uint32_t  *Timestamp;     // system timestamp
+        static cfgInst_t *GeneralConfig; // general inverter configuration from setup
+        static IApp *App;
 
         uint16_t mDtuRxCnt = 0;
         uint16_t mDtuTxCnt = 0;
@@ -843,9 +873,11 @@ class Inverter {
 };
 
 template <class REC_TYP>
-uint32_t *Inverter<REC_TYP>::timestamp {0};
+uint32_t *Inverter<REC_TYP>::Timestamp {0};
 template <class REC_TYP>
-cfgInst_t *Inverter<REC_TYP>::generalConfig {0};
+cfgInst_t *Inverter<REC_TYP>::GeneralConfig {0};
+template <class REC_TYP>
+IApp *Inverter<REC_TYP>::App {nullptr};
 
 
 /**
@@ -855,7 +887,7 @@ cfgInst_t *Inverter<REC_TYP>::generalConfig {0};
  */
 
 template<class T=float>
-static T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0) {
+T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0) {
     DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcYieldTotalCh0"));
     if(NULL != iv) {
         record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
@@ -869,7 +901,7 @@ static T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0) {
 }
 
 template<class T=float>
-static T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0) {
+T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0) {
     DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcYieldDayCh0"));
     if(NULL != iv) {
         record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
@@ -883,7 +915,7 @@ static T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0) {
 }
 
 template<class T=float>
-static T calcUdcCh(Inverter<> *iv, uint8_t arg0) {
+T calcUdcCh(Inverter<> *iv, uint8_t arg0) {
     DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcUdcCh"));
     // arg0 = channel of source
     record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
@@ -897,7 +929,7 @@ static T calcUdcCh(Inverter<> *iv, uint8_t arg0) {
 }
 
 template<class T=float>
-static T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0) {
+T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0) {
     DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcPowerDcCh0"));
     if(NULL != iv) {
         record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
@@ -911,7 +943,7 @@ static T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0) {
 }
 
 template<class T=float>
-static T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0) {
+T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0) {
     DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcEfficiencyCh0"));
     if(NULL != iv) {
         record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
@@ -927,7 +959,7 @@ static T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0) {
 }
 
 template<class T=float>
-static T calcIrradiation(Inverter<> *iv, uint8_t arg0) {
+T calcIrradiation(Inverter<> *iv, uint8_t arg0) {
     DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcIrradiation"));
     // arg0 = channel
     if(NULL != iv) {
@@ -939,7 +971,7 @@ static T calcIrradiation(Inverter<> *iv, uint8_t arg0) {
 }
 
 template<class T=float>
-static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) {
+T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) {
     DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcMaxPowerAcCh0"));
     T acMaxPower = 0.0;
     if(NULL != iv) {
@@ -952,7 +984,7 @@ static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) {
             }
         }
         if(acPower > acMaxPower) {
-            iv->tsMaxAcPower = *iv->timestamp;
+            iv->tsMaxAcPower = *iv->Timestamp;
             return acPower;
         }
     }
@@ -960,7 +992,7 @@ static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) {
 }
 
 template<class T=float>
-static T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0) {
+T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0) {
     DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcMaxPowerDc"));
     // arg0 = channel
     T dcMaxPower = 0.0;
@@ -979,4 +1011,22 @@ static T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0) {
     return dcMaxPower;
 }
 
+template<class T=float>
+T calcMaxTemperature(Inverter<> *iv, uint8_t arg0) {
+    DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcMaxTemperature"));
+    // arg0 = channel
+    if(NULL != iv) {
+        record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
+        T temp = iv->getChannelFieldValue(arg0, FLD_T, rec);
+        T maxTemp = iv->getChannelFieldValue(arg0, FLD_MT, rec);
+
+        if(temp > maxTemp) {
+            iv->tsMaxTemperature = *iv->Timestamp;
+            return temp;
+        }
+        return maxTemp;
+    }
+    return 0;
+}
+
 #endif /*__HM_INVERTER_H__*/
diff --git a/src/hm/hmSystem.h b/src/hm/hmSystem.h
index 7e79f30a..81507105 100644
--- a/src/hm/hmSystem.h
+++ b/src/hm/hmSystem.h
@@ -16,8 +16,9 @@ class HmSystem {
         HmSystem() {}
 
         void setup(uint32_t *timestamp, cfgInst_t *config, IApp *app) {
-            mInverter[0].timestamp     = timestamp;
-            mInverter[0].generalConfig = config;
+            INVERTERTYPE::Timestamp = timestamp;
+            INVERTERTYPE::GeneralConfig = config;
+            INVERTERTYPE::App = app;
             //mInverter[0].app           = app;
         }
 
@@ -25,7 +26,7 @@ class HmSystem {
             DPRINTLN(DBG_VERBOSE, F("hmSystem.h:addInverter"));
             INVERTERTYPE *iv = &mInverter[id];
             iv->id         = id;
-            iv->config     = &mInverter[0].generalConfig->iv[id];
+            iv->config     = &INVERTERTYPE::GeneralConfig->iv[id];
             DPRINT(DBG_VERBOSE, "SERIAL: " + String(iv->config->serial.b[5], HEX));
             DPRINTLN(DBG_VERBOSE, " " + String(iv->config->serial.b[4], HEX));
             if((iv->config->serial.b[5] == 0x11) || (iv->config->serial.b[5] == 0x10)) {
@@ -35,6 +36,8 @@ class HmSystem {
                     case 0x21: iv->type = INV_TYPE_1CH;
                         break;
 
+                    case 0x25: // HMS-400 - 1 channel but payload like 2ch
+
                     case 0x44: // HMS-1000
                     case 0x42:
                     case 0x41: iv->type = INV_TYPE_2CH;
@@ -51,15 +54,14 @@ class HmSystem {
                 }
 
                 if(iv->config->serial.b[5] == 0x11) {
-                    if((iv->config->serial.b[4] & 0x0f) == 0x04) {
+                    if(((iv->config->serial.b[4] & 0x0f) == 0x04) || ((iv->config->serial.b[4] & 0x0f) == 0x05)) {
                         iv->ivGen = IV_HMS;
                         iv->ivRadioType = INV_RADIO_TYPE_CMT;
                     } else {
                         iv->ivGen = IV_HM;
                         iv->ivRadioType = INV_RADIO_TYPE_NRF;
                     }
-                }
-                else if((iv->config->serial.b[4] & 0x03) == 0x02) { // MI 3rd Gen -> same as HM
+                } else if((iv->config->serial.b[4] & 0x03) == 0x02) { // MI 3rd Gen -> same as HM
                     iv->ivGen = IV_HM;
                     iv->ivRadioType = INV_RADIO_TYPE_NRF;
                 } else {  // MI 2nd Gen
@@ -67,11 +69,16 @@ class HmSystem {
                     iv->ivRadioType = INV_RADIO_TYPE_NRF;
                 }
             } else if(iv->config->serial.b[5] == 0x13) {
-                    iv->ivGen = IV_HMT;
+                iv->ivGen = IV_HMT;
+                if(iv->config->serial.b[4] == 0x61)
+                    iv->type = INV_TYPE_4CH;
+                else
                     iv->type = INV_TYPE_6CH;
-                    iv->ivRadioType = INV_RADIO_TYPE_CMT;
+
+                iv->ivRadioType = INV_RADIO_TYPE_CMT;
             } else if(iv->config->serial.u64 != 0ULL) {
                 DPRINTLN(DBG_ERROR, F("inverter type can't be detected!"));
+                iv->config->enabled = false;
                 return;
             } else
                 iv->ivGen = IV_UNKNOWN;
@@ -82,7 +89,7 @@ class HmSystem {
 
             DPRINT(DBG_INFO, "added inverter ");
             if(iv->config->serial.b[5] == 0x11) {
-                if((iv->config->serial.b[4] & 0x0f) == 0x04)
+                if(((iv->config->serial.b[4] & 0x0f) == 0x04) || ((iv->config->serial.b[4] & 0x0f) == 0x05))
                     DBGPRINT("HMS");
                 else
                     DBGPRINT("HM");
@@ -114,6 +121,8 @@ class HmSystem {
             DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos"));
             if(pos >= MAX_INVERTER)
                 return nullptr;
+            else if(nullptr == mInverter[pos].config)
+                return nullptr;
             else if((mInverter[pos].config->serial.u64 != 0ULL) || (false == check))
                 return &mInverter[pos];
             else
diff --git a/src/hm/nrfHal.h b/src/hm/nrfHal.h
index b9265626..a838f8bc 100644
--- a/src/hm/nrfHal.h
+++ b/src/hm/nrfHal.h
@@ -9,18 +9,15 @@
 #pragma once
 
 #include "../utils/spiPatcher.h"
-
 #include <esp_rom_gpio.h>
-#include <RF24_hal.h>
+#include <RF24.h>
 
 #define NRF_MAX_TRANSFER_SZ 64
 #define NRF_DEFAULT_SPI_SPEED 10000000 // 10 MHz
 
 class nrfHal: public RF24_hal, public SpiPatcherHandle {
     public:
-        nrfHal() {
-            mSpiPatcher = SpiPatcher::getInstance(SPI2_HOST);
-        }
+        nrfHal() {}
 
         void patch() override {
             esp_rom_gpio_connect_out_signal(mPinMosi, spi_periph_signal[mHostDevice].spid_out, false, false);
@@ -42,7 +39,13 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle {
             mPinEn = static_cast<gpio_num_t>(en);
             mSpiSpeed = speed;
 
-            mHostDevice = mSpiPatcher->getDevice();
+            #if defined(CONFIG_IDF_TARGET_ESP32S3)
+            mHostDevice = SPI2_HOST;
+            #else
+            mHostDevice = (14 == sclk) ? SPI2_HOST : SPI_HOST_OTHER;
+            #endif
+
+            mSpiPatcher = SpiPatcher::getInstance(mHostDevice);
 
             gpio_reset_pin(mPinMosi);
             gpio_set_direction(mPinMosi, GPIO_MODE_OUTPUT);
@@ -56,6 +59,7 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle {
             gpio_set_level(mPinClk, 0);
 
             gpio_reset_pin(mPinCs);
+            request_spi();
             spi_device_interface_config_t devcfg = {
                 .command_bits = 0,
                 .address_bits = 0,
@@ -72,14 +76,14 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle {
                 .pre_cb = nullptr,
                 .post_cb = nullptr
             };
-            ESP_ERROR_CHECK(spi_bus_add_device(mHostDevice, &devcfg, &spi));
+            mSpiPatcher->addDevice(mHostDevice, &devcfg, &spi);
+            release_spi();
 
             gpio_reset_pin(mPinEn);
             gpio_set_direction(mPinEn, GPIO_MODE_OUTPUT);
             gpio_set_level(mPinEn, 0);
         }
 
-
         bool begin() override {
             return true;
         }
diff --git a/src/hms/hmsRadio.h b/src/hms/CmtRadio.h
similarity index 90%
rename from src/hms/hmsRadio.h
rename to src/hms/CmtRadio.h
index d074442b..b6405329 100644
--- a/src/hms/hmsRadio.h
+++ b/src/hms/CmtRadio.h
@@ -7,7 +7,7 @@
 #define __HMS_RADIO_H__
 
 #include "cmt2300a.h"
-#include "../hm/radio.h"
+#include "../hm/Radio.h"
 
 //#define CMT_SWITCH_CHANNEL_CYCLE    5
 
@@ -15,25 +15,34 @@ template<uint32_t DTU_SN = 0x81001765>
 class CmtRadio : public Radio {
     typedef Cmt2300a CmtType;
     public:
-        void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, uint8_t pinSclk, uint8_t pinSdio, uint8_t pinCsb, uint8_t pinFcsb, uint8_t region = 0, bool genDtuSn = true) {
-            mCmt.setup(pinSclk, pinSdio, pinCsb, pinFcsb);
-            reset(genDtuSn, static_cast<RegionCfg>(region));
+        void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, cfgCmt_t *cfg, uint8_t region = 0, bool genDtuSn = true) {
+            mCfg = cfg;
+
+            if(!cfg->enabled)
+                return;
+
             mPrivacyMode = privacyMode;
             mSerialDebug = serialDebug;
             mPrintWholeTrace = printWholeTrace;
             mTxBuf.fill(0);
+
+            mCmt.setup(cfg->pinSclk, cfg->pinSdio, cfg->pinCsb, cfg->pinFcsb);
+            reset(genDtuSn, static_cast<RegionCfg>(region));
         }
 
-        bool loop() override {
+        void loop() override {
+            if(!mCfg->enabled)
+                return;
+
             mCmt.loop();
             if((!mIrqRcvd) && (!mRqstGetRx))
-                return false;
+                return;
             getRx();
             if(CmtStatus::SUCCESS == mCmt.goRx()) {
                 mIrqRcvd   = false;
                 mRqstGetRx = false;
             }
-            return false;
+            return;
         }
 
         bool isChipConnected(void) const override {
@@ -41,6 +50,9 @@ class CmtRadio : public Radio {
         }
 
         void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) override {
+            if(!mCfg->enabled)
+                return;
+
             DPRINT(DBG_INFO, F("sendControlPacket cmd: "));
             DBGHEXLN(cmd);
             initPacket(iv->radioId.u64, TX_REQ_DEVCONTROL, SINGLE_FRAME);
@@ -59,6 +71,9 @@ class CmtRadio : public Radio {
         }
 
         bool switchFrequency(Inverter<> *iv, uint32_t fromkHz, uint32_t tokHz) override {
+            if(!isChipConnected())
+                return false;
+
             uint8_t fromCh = mCmt.freq2Chan(fromkHz);
             uint8_t toCh = mCmt.freq2Chan(tokHz);
 
@@ -68,6 +83,8 @@ class CmtRadio : public Radio {
         bool switchFrequencyCh(Inverter<> *iv, uint8_t fromCh, uint8_t toCh) override {
             if((0xff == fromCh) || (0xff == toCh))
                 return false;
+            if(!isChipConnected())
+                return false;
 
             mCmt.switchChannel(fromCh);
             sendSwitchChCmd(iv, toCh);
@@ -183,11 +200,12 @@ class CmtRadio : public Radio {
 
             if(p.packet[9] > ALL_FRAMES) { // indicates last frame
                 setExpectedFrames(p.packet[9] - ALL_FRAMES);
-                mRadioWaitTime.startTimeMonitor(DURATION_PAUSE_LASTFR); // let the inverter first get back to rx mode?
+                mRadioWaitTime.startTimeMonitor(2); // let the inverter first get back to rx mode?
             }
         }
 
         CmtType mCmt;
+        cfgCmt_t *mCfg = nullptr;
         bool mCmtAvail = false;
         bool mRqstGetRx = false;
         uint32_t mMillis = 0;
diff --git a/src/hms/cmt2300a.h b/src/hms/cmt2300a.h
index 23911b15..ed3aab54 100644
--- a/src/hms/cmt2300a.h
+++ b/src/hms/cmt2300a.h
@@ -6,7 +6,7 @@
 #ifndef __CMT2300A_H__
 #define __CMT2300A_H__
 
-#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
+#if defined(SPI_HAL)
 #include "cmtHal.h"
 #else
 #include "esp32_3wSpi.h"
@@ -545,7 +545,7 @@ class Cmt2300a {
         }
 
     private:
-        #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
+        #if defined(SPI_HAL)
         cmtHal mSpi;
         #else
         esp32_3wSpi mSpi;
diff --git a/src/hms/cmtHal.h b/src/hms/cmtHal.h
index a4bec587..8556a043 100644
--- a/src/hms/cmtHal.h
+++ b/src/hms/cmtHal.h
@@ -16,9 +16,7 @@
 
 class cmtHal : public SpiPatcherHandle {
     public:
-        cmtHal() {
-            mSpiPatcher = SpiPatcher::getInstance(DEF_CMT_SPI_HOST);
-        }
+        cmtHal() {}
 
         void patch() override {
             esp_rom_gpio_connect_out_signal(mPinSdio, spi_periph_signal[mHostDevice].spid_out, false, false);
@@ -39,7 +37,13 @@ class cmtHal : public SpiPatcherHandle {
             mPinFcs   = static_cast<gpio_num_t>(fcs);
             mSpiSpeed = speed;
 
-            mHostDevice = mSpiPatcher->getDevice();
+            #if defined(CONFIG_IDF_TARGET_ESP32S3)
+            mHostDevice = SPI2_HOST;
+            #else
+            mHostDevice = (14 == clk) ? SPI2_HOST : SPI_HOST_OTHER;
+            #endif
+
+            mSpiPatcher = SpiPatcher::getInstance(mHostDevice);
 
             gpio_reset_pin(mPinSdio);
             gpio_set_direction(mPinSdio, GPIO_MODE_INPUT_OUTPUT);
@@ -50,6 +54,7 @@ class cmtHal : public SpiPatcherHandle {
             gpio_set_level(mPinClk, 0);
 
             gpio_reset_pin(mPinCs);
+            request_spi();
             spi_device_interface_config_t devcfg_reg = {
                 .command_bits = 1,
                 .address_bits = 7,
@@ -66,7 +71,8 @@ class cmtHal : public SpiPatcherHandle {
                 .pre_cb = nullptr,
                 .post_cb = nullptr
             };
-            ESP_ERROR_CHECK(spi_bus_add_device(mHostDevice, &devcfg_reg, &spi_reg));
+            mSpiPatcher->addDevice(mHostDevice, &devcfg_reg, &spi_reg);
+            release_spi();
 
             gpio_reset_pin(mPinFcs);
             spi_device_interface_config_t devcfg_fifo = {
diff --git a/src/hms/hmsDefines.h b/src/hms/hmsDefines.h
index 61275dc1..07aec682 100644
--- a/src/hms/hmsDefines.h
+++ b/src/hms/hmsDefines.h
@@ -33,7 +33,8 @@ const byteAssign_t hms1chAssignment[] = {
     { FLD_YT,  UNIT_KWH,  CH0, CALC_YT_CH0,   0, CMD_CALC },
     { FLD_PDC, UNIT_W,    CH0, CALC_PDC_CH0,  0, CMD_CALC },
     { FLD_EFF, UNIT_PCT,  CH0, CALC_EFF_CH0,  0, CMD_CALC },
-    { FLD_MP,  UNIT_W,    CH0, CALC_MPAC_CH0, 0, CMD_CALC }
+    { FLD_MP,  UNIT_W,    CH0, CALC_MPAC_CH0, 0, CMD_CALC },
+    { FLD_MT,  UNIT_C,    CH0, CALC_MT_CH0,   0, CMD_CALC }
 };
 #define HMS1CH_LIST_LEN      (sizeof(hms1chAssignment) / sizeof(byteAssign_t))
 #define HMS1CH_PAYLOAD_LEN   30
@@ -70,7 +71,8 @@ const byteAssign_t hms2chAssignment[] = {
     { FLD_YT,  UNIT_KWH,  CH0, CALC_YT_CH0,  0, CMD_CALC },
     { FLD_PDC, UNIT_W,    CH0, CALC_PDC_CH0, 0, CMD_CALC },
     { FLD_EFF, UNIT_PCT,  CH0, CALC_EFF_CH0, 0, CMD_CALC },
-    { FLD_MP,  UNIT_W,    CH0, CALC_MPAC_CH0, 0, CMD_CALC }
+    { FLD_MP,  UNIT_W,    CH0, CALC_MPAC_CH0, 0, CMD_CALC },
+    { FLD_MT,  UNIT_C,    CH0, CALC_MT_CH0,   0, CMD_CALC }
 };
 #define HMS2CH_LIST_LEN      (sizeof(hms2chAssignment) / sizeof(byteAssign_t))
 #define HMS2CH_PAYLOAD_LEN   42
@@ -123,11 +125,73 @@ const byteAssign_t hms4chAssignment[] = {
     { FLD_YT,  UNIT_KWH,  CH0, CALC_YT_CH0,   0, CMD_CALC },
     { FLD_PDC, UNIT_W,    CH0, CALC_PDC_CH0,  0, CMD_CALC },
     { FLD_EFF, UNIT_PCT,  CH0, CALC_EFF_CH0,  0, CMD_CALC },
-    { FLD_MP,  UNIT_W,    CH0, CALC_MPAC_CH0, 0, CMD_CALC }
+    { FLD_MP,  UNIT_W,    CH0, CALC_MPAC_CH0, 0, CMD_CALC },
+    { FLD_MT,  UNIT_C,    CH0, CALC_MT_CH0,   0, CMD_CALC }
 };
 #define HMS4CH_LIST_LEN      (sizeof(hms4chAssignment) / sizeof(byteAssign_t))
 #define HMS4CH_PAYLOAD_LEN   66
 
+//-------------------------------------
+// HMT-1600, HMT-1800, HMT-2000
+//-------------------------------------
+const byteAssign_t hmt4chAssignment[] = {
+    { FLD_UDC, UNIT_V,   CH1,  2, 2,   10 },
+    { FLD_IDC, UNIT_A,   CH1,  4, 2,  100 },
+    { FLD_PDC, UNIT_W,   CH1,  8, 2,   10 },
+    { FLD_YT,  UNIT_KWH, CH1, 12, 4, 1000 },
+    { FLD_YD,  UNIT_WH,  CH1, 20, 2,    1 },
+    { FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC },
+    { FLD_MP,  UNIT_W,   CH1, CALC_MPDC_CH, CH1, CMD_CALC },
+
+    { FLD_UDC, UNIT_V,   CH2, CALC_UDC_CH, CH1, CMD_CALC },
+    { FLD_IDC, UNIT_A,   CH2,  6, 2,  100 },
+    { FLD_PDC, UNIT_W,   CH2, 10, 2,   10 },
+    { FLD_YT,  UNIT_KWH, CH2, 16, 4, 1000 },
+    { FLD_YD,  UNIT_WH,  CH2, 22, 2,    1 },
+    { FLD_IRR, UNIT_PCT, CH2, CALC_IRR_CH, CH2, CMD_CALC },
+    { FLD_MP,  UNIT_W,   CH2, CALC_MPDC_CH, CH2, CMD_CALC },
+
+    { FLD_UDC, UNIT_V,   CH3, 24, 2,   10 },
+    { FLD_IDC, UNIT_A,   CH3, 26, 2,  100 },
+    { FLD_PDC, UNIT_W,   CH3, 30, 2,   10 },
+    { FLD_YT,  UNIT_KWH, CH3, 34, 4, 1000 },
+    { FLD_YD,  UNIT_WH,  CH3, 42, 2,    1 },
+    { FLD_IRR, UNIT_PCT, CH3, CALC_IRR_CH, CH3, CMD_CALC },
+    { FLD_MP,  UNIT_W,   CH3, CALC_MPDC_CH, CH3, CMD_CALC },
+
+    { FLD_UDC, UNIT_V,   CH4, CALC_UDC_CH, CH3, CMD_CALC },
+    { FLD_IDC, UNIT_A,   CH4, 28, 2,  100 },
+    { FLD_PDC, UNIT_W,   CH4, 32, 2,   10 },
+    { FLD_YT,  UNIT_KWH, CH4, 38, 4, 1000 },
+    { FLD_YD,  UNIT_WH,  CH4, 44, 2,    1 },
+    { FLD_IRR, UNIT_PCT, CH4, CALC_IRR_CH, CH4, CMD_CALC },
+    { FLD_MP,  UNIT_W,   CH4, CALC_MPDC_CH, CH4, CMD_CALC },
+
+    { FLD_UAC_1N,   UNIT_V,    CH0, 68, 2,   10 },
+    { FLD_UAC_2N,   UNIT_V,    CH0, 70, 2,   10 },
+    { FLD_UAC_3N,   UNIT_V,    CH0, 72, 2,   10 },
+    { FLD_UAC_12,   UNIT_V,    CH0, 74, 2,   10 },
+    { FLD_UAC_23,   UNIT_V,    CH0, 76, 2,   10 },
+    { FLD_UAC_31,   UNIT_V,    CH0, 78, 2,   10 },
+    { FLD_F,        UNIT_HZ,   CH0, 80, 2,  100 },
+    { FLD_PAC,      UNIT_W,    CH0, 82, 2,   10 },
+    { FLD_Q,        UNIT_VAR,  CH0, 84, 2,   10 },
+    { FLD_IAC_1,    UNIT_A,    CH0, 86, 2,  100 },
+    { FLD_IAC_2,    UNIT_A,    CH0, 88, 2,  100 },
+    { FLD_IAC_3,    UNIT_A,    CH0, 90, 2,  100 },
+    { FLD_PF,       UNIT_NONE, CH0, 92, 2, 1000 },
+    { FLD_T,        UNIT_C,    CH0, 94, 2,   10 },
+    { FLD_EVT,      UNIT_NONE, CH0, 96, 2,    1 },
+    { FLD_YD,       UNIT_WH,   CH0, CALC_YD_CH0,   0, CMD_CALC },
+    { FLD_YT,       UNIT_KWH,  CH0, CALC_YT_CH0,   0, CMD_CALC },
+    { FLD_PDC,      UNIT_W,    CH0, CALC_PDC_CH0,  0, CMD_CALC },
+    { FLD_EFF,      UNIT_PCT,  CH0, CALC_EFF_CH0,  0, CMD_CALC },
+    { FLD_MP,       UNIT_W,    CH0, CALC_MPAC_CH0, 0, CMD_CALC },
+    { FLD_MT,       UNIT_C,    CH0, CALC_MT_CH0,   0, CMD_CALC }
+};
+#define HMT4CH_LIST_LEN      (sizeof(hmt4chAssignment) / sizeof(byteAssign_t))
+#define HMT4CH_PAYLOAD_LEN   98
+
 //-------------------------------------
 // HMT-1800, HMT-2250
 //-------------------------------------
@@ -199,7 +263,8 @@ const byteAssign_t hmt6chAssignment[] = {
     { FLD_YT,       UNIT_KWH,  CH0, CALC_YT_CH0,   0, CMD_CALC },
     { FLD_PDC,      UNIT_W,    CH0, CALC_PDC_CH0,  0, CMD_CALC },
     { FLD_EFF,      UNIT_PCT,  CH0, CALC_EFF_CH0,  0, CMD_CALC },
-    { FLD_MP,       UNIT_W,    CH0, CALC_MPAC_CH0, 0, CMD_CALC }
+    { FLD_MP,       UNIT_W,    CH0, CALC_MPAC_CH0, 0, CMD_CALC },
+    { FLD_MT,       UNIT_C,    CH0, CALC_MT_CH0,   0, CMD_CALC }
 };
 #define HMT6CH_LIST_LEN      (sizeof(hmt6chAssignment) / sizeof(byteAssign_t))
 #define HMT6CH_PAYLOAD_LEN   98
diff --git a/src/network/AhoyEthernet.h b/src/network/AhoyEthernet.h
new file mode 100644
index 00000000..9ed2de98
--- /dev/null
+++ b/src/network/AhoyEthernet.h
@@ -0,0 +1,151 @@
+//-----------------------------------------------------------------------------
+// 2024 Ahoy, https://ahoydtu.de
+// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
+//-----------------------------------------------------------------------------
+
+#ifndef __AHOY_ETHERNET_H__
+#define __AHOY_ETHERNET_H__
+
+#if defined(ETHERNET)
+#include <functional>
+#include <AsyncUDP.h>
+#include <ETH.h>
+#include "AhoyEthernetSpi.h"
+#include "AhoyNetwork.h"
+#include "AhoyWifiEsp32.h"
+
+class AhoyEthernet : public AhoyWifi {
+    private:
+        enum class Mode {
+            WIRED,
+            WIRELESS
+        };
+
+    public:
+        AhoyEthernet()
+            : mMode (Mode::WIRELESS) {}
+
+        virtual void begin() override {
+            mMode = Mode::WIRELESS;
+            mAp.enable();
+            AhoyWifi::begin();
+
+            if(!mConfig->sys.eth.enabled)
+                return;
+
+            mEthSpi.begin(mConfig->sys.eth.pinMiso, mConfig->sys.eth.pinMosi, mConfig->sys.eth.pinSclk, mConfig->sys.eth.pinCs, mConfig->sys.eth.pinIrq, mConfig->sys.eth.pinRst);
+            ETH.setHostname(mConfig->sys.deviceName);
+        }
+
+        virtual String getIp(void) override {
+            if(Mode::WIRELESS == mMode)
+                return AhoyWifi::getIp();
+            else
+                return ETH.localIP().toString();
+        }
+
+        virtual String getMac(void) override {
+            if(Mode::WIRELESS == mMode)
+                return AhoyWifi::getMac();
+            else
+                return mEthSpi.macAddress();
+        }
+
+        virtual bool isWiredConnection() override {
+            return (Mode::WIRED == mMode);
+        }
+
+    private:
+        virtual void OnEvent(WiFiEvent_t event) override {
+            switch(event) {
+                case ARDUINO_EVENT_ETH_CONNECTED:
+                    mMode = Mode::WIRED; // needed for static IP
+                    [[fallthrough]];
+                case SYSTEM_EVENT_STA_CONNECTED:
+                    mWifiConnecting = false;
+                    if(NetworkState::CONNECTED != mStatus) {
+                        if(ARDUINO_EVENT_ETH_CONNECTED == event)
+                            WiFi.disconnect();
+
+                        mStatus = NetworkState::CONNECTED;
+                        DPRINTLN(DBG_INFO, F("Network connected"));
+                        setStaticIp();
+                    }
+                    break;
+
+                case SYSTEM_EVENT_STA_GOT_IP:
+                    mStatus = NetworkState::GOT_IP;
+                    if(mAp.isEnabled())
+                        mAp.disable();
+
+                    mMode = Mode::WIRELESS;
+                    if(!mConnected) {
+                        mConnected = true;
+                        ah::welcome(WiFi.localIP().toString(), F("Station WiFi"));
+                        MDNS.begin(mConfig->sys.deviceName);
+                        mOnNetworkCB(true);
+                    }
+                    break;
+
+                case ARDUINO_EVENT_ETH_GOT_IP:
+                    mStatus = NetworkState::GOT_IP;
+                    mMode = Mode::WIRED;
+                    if(!mConnected) {
+                        mAp.disable();
+                        mConnected = true;
+                        ah::welcome(ETH.localIP().toString(), F("Station Ethernet"));
+                        MDNS.begin(mConfig->sys.deviceName);
+                        mOnNetworkCB(true);
+                        WiFi.disconnect();
+                    }
+                    break;
+
+                case ARDUINO_EVENT_ETH_STOP:
+                    [[fallthrough]];
+                case ARDUINO_EVENT_ETH_DISCONNECTED:
+                    mStatus = NetworkState::DISCONNECTED;
+                    if(mConnected) {
+                        mMode = Mode::WIRELESS;
+                        mConnected = false;
+                        mOnNetworkCB(false);
+                        MDNS.end();
+                        AhoyWifi::begin();
+                    }
+                    break;
+
+                case ARDUINO_EVENT_WIFI_STA_LOST_IP:
+                    [[fallthrough]];
+                case ARDUINO_EVENT_WIFI_STA_STOP:
+                    [[fallthrough]];
+                case SYSTEM_EVENT_STA_DISCONNECTED:
+                    mStatus = NetworkState::DISCONNECTED;
+                    if(mConnected && (Mode::WIRELESS == mMode)) {
+                        mConnected = false;
+                        mOnNetworkCB(false);
+                        MDNS.end();
+                        AhoyWifi::begin();
+                    }
+                    break;
+
+                default:
+                    break;
+            }
+        }
+
+        void setStaticIp() override {
+            setupIp([this](IPAddress ip, IPAddress gateway, IPAddress mask, IPAddress dns1, IPAddress dns2) -> bool {
+                if(Mode::WIRELESS == mMode)
+                    return WiFi.config(ip, gateway, mask, dns1, dns2);
+                else
+                    return ETH.config(ip, gateway, mask, dns1, dns2);
+            });
+        }
+
+    private:
+        AhoyEthernetSpi mEthSpi;
+        Mode mMode;
+
+};
+
+#endif /*ETHERNET*/
+#endif /*__AHOY_ETHERNET_H__*/
diff --git a/src/eth/ethSpi.h b/src/network/AhoyEthernetSpi.h
similarity index 70%
rename from src/eth/ethSpi.h
rename to src/network/AhoyEthernetSpi.h
index d0ef9487..ec750592 100644
--- a/src/eth/ethSpi.h
+++ b/src/network/AhoyEthernetSpi.h
@@ -1,10 +1,8 @@
 //-----------------------------------------------------------------------------
-// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
-// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
+// 2024 Ahoy, https://ahoydtu.de
+// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
 //-----------------------------------------------------------------------------
 
-
-#if defined(CONFIG_IDF_TARGET_ESP32S3)
 #if defined(ETHERNET)
 #ifndef __ETH_SPI_H__
 #define __ETH_SPI_H__
@@ -14,23 +12,25 @@
 #include <Arduino.h>
 #include <esp_netif.h>
 #include <WiFiGeneric.h>
-#include <driver/spi_master.h>
+#include "../utils/spiPatcher.h"
 
 // Functions from WiFiGeneric
 void tcpipInit();
 void add_esp_interface_netif(esp_interface_t interface, esp_netif_t* esp_netif);
 
-class EthSpi {
+class AhoyEthernetSpi {
     public:
 
-        EthSpi() :
+        AhoyEthernetSpi() :
             eth_handle(nullptr),
             eth_netif(nullptr) {}
 
         void begin(int8_t pin_miso, int8_t pin_mosi, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst) {
-            gpio_reset_pin(static_cast<gpio_num_t>(pin_rst));
-            gpio_set_direction(static_cast<gpio_num_t>(pin_rst), GPIO_MODE_OUTPUT);
-            gpio_set_level(static_cast<gpio_num_t>(pin_rst), 0);
+            if(-1 != pin_rst) {
+                gpio_reset_pin(static_cast<gpio_num_t>(pin_rst));
+                gpio_set_direction(static_cast<gpio_num_t>(pin_rst), GPIO_MODE_OUTPUT);
+                gpio_set_level(static_cast<gpio_num_t>(pin_rst), 0);
+            }
 
             gpio_reset_pin(static_cast<gpio_num_t>(pin_sclk));
             gpio_reset_pin(static_cast<gpio_num_t>(pin_mosi));
@@ -44,22 +44,14 @@ class EthSpi {
             gpio_reset_pin(static_cast<gpio_num_t>(pin_int));
             gpio_set_pull_mode(static_cast<gpio_num_t>(pin_int), GPIO_PULLUP_ONLY);
 
-            spi_bus_config_t buscfg = {
-                .mosi_io_num = pin_mosi,
-                .miso_io_num = pin_miso,
-                .sclk_io_num = pin_sclk,
-                .quadwp_io_num = -1,
-                .quadhd_io_num = -1,
-                .data4_io_num = -1,
-                .data5_io_num = -1,
-                .data6_io_num = -1,
-                .data7_io_num = -1,
-                .max_transfer_sz = 0, // uses default value internally
-                .flags = 0,
-                .intr_flags = 0
-            };
+            #if defined(CONFIG_IDF_TARGET_ESP32S3)
+            mHostDevice = SPI3_HOST;
+            #else
+            mHostDevice = (14 == pin_sclk) ? SPI2_HOST : SPI3_HOST;
+            #endif
 
-            ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO));
+            mSpiPatcher = SpiPatcher::getInstance(mHostDevice, false);
+            mSpiPatcher->initBus(pin_mosi, pin_miso, pin_sclk, SPI_DMA_CH_AUTO);
 
             spi_device_interface_config_t devcfg = {
                 .command_bits = 16, // actually address phase
@@ -78,13 +70,14 @@ class EthSpi {
                 .post_cb = nullptr
             };
 
-            spi_device_handle_t spi;
-            ESP_ERROR_CHECK(spi_bus_add_device(SPI3_HOST, &devcfg, &spi));
+            mSpiPatcher->addDevice(mHostDevice, &devcfg, &spi);
 
             // Reset sequence
-            delayMicroseconds(500);
-            gpio_set_level(static_cast<gpio_num_t>(pin_rst), 1);
-            delayMicroseconds(1000);
+            if(-1 != pin_rst) {
+                delayMicroseconds(500);
+                gpio_set_level(static_cast<gpio_num_t>(pin_rst), 1);
+                delayMicroseconds(1000);
+            }
 
             // Arduino function to start networking stack if not already started
             tcpipInit();
@@ -123,10 +116,14 @@ class EthSpi {
         }
 
         String macAddress() {
-            uint8_t mac_addr[6] = {0, 0, 0, 0, 0, 0};
+            uint8_t mac_addr[6];
             esp_eth_ioctl(eth_handle, ETH_CMD_G_MAC_ADDR, mac_addr);
-            char mac_addr_str[24];
-            snprintf(mac_addr_str, sizeof(mac_addr_str), "%02X:%02X:%02X:%02X:%02X:%02X", mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
+            char mac_addr_str[19];
+            for(uint8_t i = 0; i < 6; i++) {
+                snprintf(&mac_addr_str[i*3], sizeof(mac_addr_str), "%02X", mac_addr[i]);
+                mac_addr_str[i*3+2] = ':';
+            }
+            mac_addr_str[17] = '\0';
             return String(mac_addr_str);
         }
 
@@ -134,8 +131,10 @@ class EthSpi {
     private:
         esp_eth_handle_t eth_handle;
         esp_netif_t *eth_netif;
+        spi_host_device_t mHostDevice;
+        spi_device_handle_t spi;
+        SpiPatcher *mSpiPatcher;
 };
 
 #endif /*__ETH_SPI_H__*/
 #endif /*ETHERNET*/
-#endif /*CONFIG_IDF_TARGET_ESP32S3*/
diff --git a/src/network/AhoyNetwork.h b/src/network/AhoyNetwork.h
new file mode 100644
index 00000000..e64a13b1
--- /dev/null
+++ b/src/network/AhoyNetwork.h
@@ -0,0 +1,254 @@
+//-----------------------------------------------------------------------------
+// 2024 Ahoy, https://ahoydtu.de
+// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
+//-----------------------------------------------------------------------------
+
+#ifndef __AHOY_NETWORK_H__
+#define __AHOY_NETWORK_H__
+
+#include "AhoyNetworkHelper.h"
+#include "../config/settings.h"
+#include "../utils/helper.h"
+#include "AhoyWifiAp.h"
+#include "AsyncJson.h"
+
+#define NTP_PACKET_SIZE 48
+
+class AhoyNetwork {
+    public:
+        typedef std::function<void(bool)> OnNetworkCB;
+        typedef std::function<void(bool)> OnTimeCB;
+
+    public:
+        void setup(settings_t *config, uint32_t *utcTimestamp, OnNetworkCB onNetworkCB, OnTimeCB onTimeCB) {
+            mConfig = config;
+            mUtcTimestamp = utcTimestamp;
+            mOnNetworkCB = onNetworkCB;
+            mOnTimeCB = onTimeCB;
+
+            if('\0' == mConfig->sys.deviceName[0])
+                snprintf(mConfig->sys.deviceName, DEVNAME_LEN, "%s", DEF_DEVICE_NAME);
+
+            mAp.setup(&mConfig->sys);
+
+            #if defined(ESP32)
+            WiFi.onEvent([this](WiFiEvent_t event, arduino_event_info_t info) -> void {
+                OnEvent(event);
+            });
+            #else
+            wifiConnectHandler = WiFi.onStationModeConnected(
+                [this](const WiFiEventStationModeConnected& event) -> void {
+                OnEvent((WiFiEvent_t)SYSTEM_EVENT_STA_CONNECTED);
+            });
+            wifiGotIPHandler = WiFi.onStationModeGotIP(
+                [this](const WiFiEventStationModeGotIP& event) -> void {
+                OnEvent((WiFiEvent_t)SYSTEM_EVENT_STA_GOT_IP);
+            });
+            wifiDisconnectHandler = WiFi.onStationModeDisconnected(
+                [this](const WiFiEventStationModeDisconnected& event) -> void {
+                OnEvent((WiFiEvent_t)SYSTEM_EVENT_STA_DISCONNECTED);
+            });
+            #endif
+        }
+
+        bool isConnected() const {
+            return (mStatus == NetworkState::CONNECTED);
+        }
+
+        bool updateNtpTime(void) {
+            if(NetworkState::GOT_IP != mStatus)
+                return false;
+
+            if (!mUdp.connected()) {
+                IPAddress timeServer;
+                if (!WiFi.hostByName(mConfig->ntp.addr, timeServer))
+                    return false;
+                if (!mUdp.connect(timeServer, mConfig->ntp.port))
+                    return false;
+            }
+
+            mUdp.onPacket([this](AsyncUDPPacket packet) {
+                this->handleNTPPacket(packet);
+            });
+            sendNTPpacket();
+
+            return true;
+        }
+
+    public:
+        virtual void begin() = 0;
+        virtual void tickNetworkLoop() = 0;
+        virtual String getIp(void) = 0;
+        virtual String getMac(void) = 0;
+
+        virtual bool getWasInCh12to14() {
+            return false;
+        }
+
+        virtual bool isWiredConnection() {
+            return false;
+        }
+
+        bool isApActive() {
+            return mAp.isEnabled();
+        }
+
+        bool getAvailNetworks(JsonObject obj, IApp *app) {
+            if(!mScanActive) {
+                app->addOnce([this]() {scan();}, 1, "scan");
+                return false;
+            }
+
+            int n = WiFi.scanComplete();
+            if (WIFI_SCAN_RUNNING == n)
+                return false;
+
+            if(n > 0) {
+                JsonArray nets = obj.createNestedArray(F("networks"));
+                int sort[n];
+                sortRSSI(&sort[0], n);
+                for (int i = 0; i < n; ++i) {
+                    nets[i][F("ssid")] = WiFi.SSID(sort[i]);
+                    nets[i][F("rssi")] = WiFi.RSSI(sort[i]);
+                }
+            }
+            mScanActive = false;
+            WiFi.scanDelete();
+
+            return true;
+        }
+
+        void scan(void) {
+            mScanActive = true;
+            if(mWifiConnecting) {
+                mWifiConnecting = false;
+                WiFi.disconnect();
+            }
+            WiFi.scanNetworks(true, true);
+        }
+
+    protected:
+        virtual void setStaticIp() = 0;
+
+        void setupIp(std::function<bool(IPAddress ip, IPAddress gateway, IPAddress mask, IPAddress dns1, IPAddress dns2)> cb) {
+            if(mConfig->sys.ip.ip[0] != 0) {
+                IPAddress ip(mConfig->sys.ip.ip);
+                IPAddress mask(mConfig->sys.ip.mask);
+                IPAddress dns1(mConfig->sys.ip.dns1);
+                IPAddress dns2(mConfig->sys.ip.dns2);
+                IPAddress gateway(mConfig->sys.ip.gateway);
+                if(cb(ip, gateway, mask, dns1, dns2))
+                    DPRINTLN(DBG_ERROR, F("failed to set static IP!"));
+            }
+        }
+
+        virtual void OnEvent(WiFiEvent_t event) {
+            switch(event) {
+                case SYSTEM_EVENT_STA_CONNECTED:
+                    [[fallthrough]];
+                case ARDUINO_EVENT_ETH_CONNECTED:
+                    if(NetworkState::CONNECTED != mStatus) {
+                        mStatus = NetworkState::CONNECTED;
+                        DPRINTLN(DBG_INFO, F("Network connected"));
+                    }
+                    break;
+
+                case SYSTEM_EVENT_STA_GOT_IP:
+                    [[fallthrough]];
+                case ARDUINO_EVENT_ETH_GOT_IP:
+                    mStatus = NetworkState::GOT_IP;
+                    break;
+
+                case ARDUINO_EVENT_WIFI_STA_LOST_IP:
+                    [[fallthrough]];
+                case ARDUINO_EVENT_WIFI_STA_STOP:
+                    [[fallthrough]];
+                case SYSTEM_EVENT_STA_DISCONNECTED:
+                    [[fallthrough]];
+                case ARDUINO_EVENT_ETH_STOP:
+                    [[fallthrough]];
+                case ARDUINO_EVENT_ETH_DISCONNECTED:
+                    mStatus = NetworkState::DISCONNECTED;
+                    break;
+
+                default:
+                    break;
+            }
+        }
+
+        void sortRSSI(int *sort, int n) {
+            for (int i = 0; i < n; i++)
+                sort[i] = i;
+            for (int i = 0; i < n; i++)
+                for (int j = i + 1; j < n; j++)
+                    if (WiFi.RSSI(sort[j]) > WiFi.RSSI(sort[i]))
+                        std::swap(sort[i], sort[j]);
+        }
+
+    private:
+        void sendNTPpacket(void) {
+            uint8_t buf[NTP_PACKET_SIZE];
+            memset(buf, 0, NTP_PACKET_SIZE);
+
+            buf[0] = 0b11100011; // LI, Version, Mode
+            buf[1] = 0;          // Stratum
+            buf[2] = 6;          // Max Interval between messages in seconds
+            buf[3] = 0xEC;       // Clock Precision
+            // bytes 4 - 11 are for Root Delay and Dispersion and were set to 0 by memset
+            buf[12] = 49;        // four-byte reference ID identifying
+            buf[13] = 0x4E;
+            buf[14] = 49;
+            buf[15] = 52;
+
+            mUdp.write(buf, NTP_PACKET_SIZE);
+        }
+
+        void handleNTPPacket(AsyncUDPPacket packet) {
+            char buf[80];
+
+            memcpy(buf, packet.data(), sizeof(buf));
+
+            unsigned long highWord = word(buf[40], buf[41]);
+            unsigned long lowWord = word(buf[42], buf[43]);
+
+            // combine the four bytes (two words) into a long integer
+            // this is NTP time (seconds since Jan 1 1900):
+            unsigned long secsSince1900 = highWord << 16 | lowWord;
+
+            *mUtcTimestamp = secsSince1900 - 2208988800UL; // UTC time
+            DPRINTLN(DBG_INFO, "[NTP]: " + ah::getDateTimeStr(*mUtcTimestamp) + " UTC");
+            mOnTimeCB(true);
+            mUdp.close();
+        }
+
+    protected:
+        enum class NetworkState : uint8_t {
+            DISCONNECTED,
+            CONNECTED,
+            GOT_IP,
+            SCAN_READY, // ESP8266
+            CONNECTING // ESP8266
+        };
+
+    protected:
+        settings_t *mConfig = nullptr;
+        uint32_t *mUtcTimestamp = nullptr;
+        bool mConnected = false;
+        bool mScanActive = false;
+        bool mWifiConnecting = false;
+
+        OnNetworkCB mOnNetworkCB;
+        OnTimeCB mOnTimeCB;
+
+        NetworkState mStatus = NetworkState::DISCONNECTED;
+
+        AhoyWifiAp mAp;
+        DNSServer mDns;
+
+        AsyncUDP mUdp; // for time server
+        #if defined(ESP8266)
+            WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler, wifiGotIPHandler;
+        #endif
+};
+
+#endif /*__AHOY_NETWORK_H__*/
diff --git a/src/network/AhoyNetworkHelper.cpp b/src/network/AhoyNetworkHelper.cpp
new file mode 100644
index 00000000..09678023
--- /dev/null
+++ b/src/network/AhoyNetworkHelper.cpp
@@ -0,0 +1,20 @@
+//-----------------------------------------------------------------------------
+// 2024 Ahoy, https://ahoydtu.de
+// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
+//-----------------------------------------------------------------------------
+
+#include "AhoyNetworkHelper.h"
+
+namespace ah {
+    void welcome(String ip, String info) {
+        DBGPRINTLN(F("\n\n-------------------"));
+        DBGPRINTLN(F("Welcome to AHOY!"));
+        DBGPRINT(F("\npoint your browser to http://"));
+        DBGPRINT(ip);
+        DBGPRINT(" (");
+        DBGPRINT(info);
+        DBGPRINTLN(")");
+        DBGPRINTLN(F("to configure your device"));
+        DBGPRINTLN(F("-------------------\n"));
+    }
+}
diff --git a/src/network/AhoyNetworkHelper.h b/src/network/AhoyNetworkHelper.h
new file mode 100644
index 00000000..378ba033
--- /dev/null
+++ b/src/network/AhoyNetworkHelper.h
@@ -0,0 +1,39 @@
+//-----------------------------------------------------------------------------
+// 2024 Ahoy, https://ahoydtu.de
+// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
+//-----------------------------------------------------------------------------
+
+#ifndef __AHOY_NETWORK_HELPER_H__
+#define __AHOY_NETWORK_HELPER_H__
+
+#include "../utils/dbg.h"
+#include <Arduino.h>
+#if defined(ESP32)
+    #include "ESPAsyncWebServer.h"
+    #include <WiFiType.h>
+    #include <ESPmDNS.h>
+#else
+    #include <ESP8266WiFi.h>
+    #include <ESP8266mDNS.h>
+    //#include <WiFiUdp.h>
+    #include "ESPAsyncUDP.h"
+
+    enum {
+        SYSTEM_EVENT_STA_CONNECTED = 1,
+        ARDUINO_EVENT_ETH_CONNECTED,
+        SYSTEM_EVENT_STA_GOT_IP,
+        ARDUINO_EVENT_ETH_GOT_IP,
+        ARDUINO_EVENT_WIFI_STA_LOST_IP,
+        ARDUINO_EVENT_WIFI_STA_STOP,
+        SYSTEM_EVENT_STA_DISCONNECTED,
+        ARDUINO_EVENT_ETH_STOP,
+        ARDUINO_EVENT_ETH_DISCONNECTED
+    };
+#endif
+#include <DNSServer.h>
+
+namespace ah {
+    void welcome(String ip, String info);
+}
+
+#endif /*__AHOY_NETWORK_HELPER_H__*/
diff --git a/src/network/AhoyWifiAp.h b/src/network/AhoyWifiAp.h
new file mode 100644
index 00000000..ed1ad3a3
--- /dev/null
+++ b/src/network/AhoyWifiAp.h
@@ -0,0 +1,76 @@
+//-----------------------------------------------------------------------------
+// 2024 Ahoy, https://ahoydtu.de
+// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
+//-----------------------------------------------------------------------------
+
+#ifndef __AHOY_WIFI_AP_H__
+#define __AHOY_WIFI_AP_H__
+
+#include "../utils/dbg.h"
+#include <Arduino.h>
+#include "../config/settings.h"
+#include "AhoyNetworkHelper.h"
+
+class AhoyWifiAp {
+    public:
+        AhoyWifiAp() : mIp(192, 168, 4, 1) {}
+
+        void setup(cfgSys_t *cfg) {
+            mCfg = cfg;
+        }
+
+        void tickLoop() {
+            if(mEnabled)
+                mDns.processNextRequest();
+
+            if (WiFi.softAPgetStationNum() != mLast) {
+                mLast = WiFi.softAPgetStationNum();
+                if(mLast > 0)
+                    DBGPRINTLN(F("AP client connected"));
+            }
+        }
+
+        void enable() {
+            if(mEnabled)
+                return;
+
+            ah::welcome(mIp.toString(), String(F("Password: ") + String(mCfg->apPwd)));
+
+            WiFi.mode(WIFI_AP_STA);
+            WiFi.softAPConfig(mIp, mIp, IPAddress(255, 255, 255, 0));
+            WiFi.softAP(WIFI_AP_SSID, mCfg->apPwd);
+
+            mDns.setErrorReplyCode(DNSReplyCode::NoError);
+            mDns.start(53, "*", mIp);
+
+            mEnabled = true;
+            tickLoop();
+        }
+
+        void disable() {
+            if(!mEnabled)
+                return;
+
+            if(WiFi.softAPgetStationNum() > 0)
+                return;
+
+            mDns.stop();
+            WiFi.softAPdisconnect();
+            WiFi.mode(WIFI_STA);
+
+            mEnabled = false;
+        }
+
+        bool isEnabled() const {
+            return mEnabled;
+        }
+
+    private:
+        cfgSys_t *mCfg = nullptr;
+        DNSServer mDns;
+        IPAddress mIp;
+        bool mEnabled = false;
+        uint8_t mLast = 0;
+};
+
+#endif /*__AHOY_WIFI_AP_H__*/
diff --git a/src/network/AhoyWifiEsp32.h b/src/network/AhoyWifiEsp32.h
new file mode 100644
index 00000000..70017518
--- /dev/null
+++ b/src/network/AhoyWifiEsp32.h
@@ -0,0 +1,103 @@
+//-----------------------------------------------------------------------------
+// 2024 Ahoy, https://ahoydtu.de
+// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
+//-----------------------------------------------------------------------------
+
+#ifndef __AHOY_WIFI_ESP32_H__
+#define __AHOY_WIFI_ESP32_H__
+
+#if defined(ESP32)
+#include <functional>
+#include <AsyncUDP.h>
+#include "AhoyNetwork.h"
+#include "ESPAsyncWebServer.h"
+
+class AhoyWifi : public AhoyNetwork {
+    public:
+        virtual void begin() override {
+            mAp.enable();
+
+            if(strlen(mConfig->sys.stationSsid) == 0)
+                return; // no station wifi defined
+
+
+            WiFi.disconnect(); // clean up
+            WiFi.setHostname(mConfig->sys.deviceName);
+            #if !defined(AP_ONLY)
+                WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
+                WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL);
+                setStaticIp();
+                WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, WIFI_ALL_CHANNEL_SCAN);
+                mWifiConnecting = true;
+
+                DBGPRINT(F("connect to network '"));
+                DBGPRINT(mConfig->sys.stationSsid);
+                DBGPRINTLN(F("'"));
+            #endif
+        }
+
+        void tickNetworkLoop() override {
+            if(mAp.isEnabled())
+                mAp.tickLoop();
+        }
+
+        virtual String getIp(void) override {
+            return WiFi.localIP().toString();
+        }
+
+        virtual String getMac(void) override {
+            return WiFi.macAddress();
+        }
+
+    private:
+        virtual void OnEvent(WiFiEvent_t event) override {
+            switch(event) {
+                case SYSTEM_EVENT_STA_CONNECTED:
+                    if(NetworkState::CONNECTED != mStatus) {
+                        mStatus = NetworkState::CONNECTED;
+                        mWifiConnecting = false;
+                        DPRINTLN(DBG_INFO, F("Network connected"));
+                    }
+                    break;
+
+                case SYSTEM_EVENT_STA_GOT_IP:
+                    mStatus = NetworkState::GOT_IP;
+                    if(mAp.isEnabled())
+                        mAp.disable();
+
+                    if(!mConnected) {
+                        mConnected = true;
+                        ah::welcome(WiFi.localIP().toString(), F("Station"));
+                        MDNS.begin(mConfig->sys.deviceName);
+                        mOnNetworkCB(true);
+                    }
+                    break;
+
+                case ARDUINO_EVENT_WIFI_STA_LOST_IP:
+                    [[fallthrough]];
+                case ARDUINO_EVENT_WIFI_STA_STOP:
+                    [[fallthrough]];
+                case SYSTEM_EVENT_STA_DISCONNECTED:
+                    mStatus = NetworkState::DISCONNECTED;
+                    if(mConnected) {
+                        mConnected = false;
+                        mOnNetworkCB(false);
+                        MDNS.end();
+                        begin();
+                    }
+                    break;
+
+                default:
+                    break;
+            }
+        }
+
+        virtual void setStaticIp() override {
+            setupIp([this](IPAddress ip, IPAddress gateway, IPAddress mask, IPAddress dns1, IPAddress dns2) -> bool {
+                return WiFi.config(ip, gateway, mask, dns1, dns2);
+            });
+        }
+};
+
+#endif /*ESP32*/
+#endif /*__AHOY_WIFI_ESP32_H__*/
diff --git a/src/network/AhoyWifiEsp8266.h b/src/network/AhoyWifiEsp8266.h
new file mode 100644
index 00000000..c72f06b5
--- /dev/null
+++ b/src/network/AhoyWifiEsp8266.h
@@ -0,0 +1,170 @@
+//-----------------------------------------------------------------------------
+// 2024 Ahoy, https://ahoydtu.de
+// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
+//-----------------------------------------------------------------------------
+
+#ifndef __AHOY_WIFI_ESP8266_H__
+#define __AHOY_WIFI_ESP8266_H__
+
+#if defined(ESP8266)
+#include <functional>
+#include <list>
+#include <WiFiUdp.h>
+#include "AhoyNetwork.h"
+#include "ESPAsyncWebServer.h"
+
+class AhoyWifi : public AhoyNetwork {
+    public:
+        void begin() override {
+            mAp.enable();
+
+            WiFi.setHostname(mConfig->sys.deviceName);
+            mBSSIDList.clear();
+        }
+
+        void tickNetworkLoop() override {
+            if(mAp.isEnabled())
+                mAp.tickLoop();
+
+            mCnt++;
+
+            switch(mStatus) {
+                case NetworkState::DISCONNECTED:
+                    if(mConnected) {
+                        mConnected = false;
+                        mWifiConnecting = false;
+                        mOnNetworkCB(false);
+                        mAp.enable();
+                        MDNS.end();
+                    }
+
+                    if (WiFi.softAPgetStationNum() > 0) {
+                        DBGPRINTLN(F("AP client connected"));
+                    }
+                    #if !defined(AP_ONLY)
+                    else if (!mScanActive) {
+                        DBGPRINT(F("scanning APs with SSID "));
+                        DBGPRINTLN(String(mConfig->sys.stationSsid));
+                        mScanCnt = 0;
+                        mCnt = 0;
+                        mScanActive = true;
+                        WiFi.scanNetworks(true, true, 0U, ([this]() {
+                            if (mConfig->sys.isHidden)
+                                return (uint8_t*)NULL;
+                            return (uint8_t*)(mConfig->sys.stationSsid);
+                        })());
+                    } else if(getBSSIDs()) {
+                        mStatus = NetworkState::SCAN_READY;
+                        DBGPRINT(F("connect to network '")); Serial.flush();
+                        DBGPRINTLN(mConfig->sys.stationSsid);
+                    }
+                    #endif
+                    break;
+
+                case NetworkState::SCAN_READY:
+                    mStatus = NetworkState::CONNECTING;
+                    DBGPRINT(F("try to connect to BSSID:"));
+                    uint8_t bssid[6];
+                    for (int j = 0; j < 6; j++) {
+                        bssid[j] = mBSSIDList.front();
+                        mBSSIDList.pop_front();
+                        DBGPRINT(" "  + String(bssid[j], HEX));
+                    }
+                    DBGPRINTLN("");
+                    setStaticIp();
+                    WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, 0, &bssid[0]);
+                    mWifiConnecting = true;
+                    break;
+
+                case NetworkState::CONNECTING:
+                    if (isTimeout(TIMEOUT)) {
+                        WiFi.disconnect();
+                        mWifiConnecting = false;
+                        mStatus = mBSSIDList.empty() ? NetworkState::DISCONNECTED : NetworkState::SCAN_READY;
+                    }
+                    break;
+
+                case NetworkState::CONNECTED:
+                    break;
+
+                case NetworkState::GOT_IP:
+                    if(!mConnected) {
+                        mAp.disable();
+                        mConnected = true;
+                        ah::welcome(WiFi.localIP().toString(), F("Station"));
+                        MDNS.begin(mConfig->sys.deviceName);
+                        MDNSResponder::hMDNSService hRes = MDNS.addService(NULL, "http", "tcp", 80);
+                        MDNS.addServiceTxt(hRes, "path", "/");
+                        MDNS.announce();
+                        mOnNetworkCB(true);
+                    }
+
+                    MDNS.update();
+
+                    if(WiFi.channel() > 11)
+                        mWasInCh12to14 = true;
+                    break;
+            }
+        }
+
+        String getIp(void) override {
+            return WiFi.localIP().toString();
+        }
+
+        String getMac(void) override {
+            return WiFi.macAddress();
+        }
+
+        bool getWasInCh12to14() override {
+            return mWasInCh12to14;
+        }
+
+    private:
+        void setStaticIp() override {
+            setupIp([this](IPAddress ip, IPAddress gateway, IPAddress mask, IPAddress dns1, IPAddress dns2) -> bool {
+                return WiFi.config(ip, gateway, mask, dns1, dns2);
+            });
+        }
+
+        bool getBSSIDs() {
+            bool result = false;
+            int n = WiFi.scanComplete();
+            if (n < 0) {
+                if (++mScanCnt < 20)
+                    return false;
+            }
+            if(n > 0) {
+                mBSSIDList.clear();
+                int sort[n];
+                sortRSSI(&sort[0], n);
+                for (int i = 0; i < n; i++) {
+                    DBGPRINT("BSSID " + String(i) + ":");
+                    uint8_t *bssid = WiFi.BSSID(sort[i]);
+                    for (int j = 0; j < 6; j++){
+                        DBGPRINT(" " + String(bssid[j], HEX));
+                        mBSSIDList.push_back(bssid[j]);
+                    }
+                    DBGPRINTLN("");
+                }
+                result = true;
+            }
+            mScanActive = false;
+            WiFi.scanDelete();
+            return result;
+        }
+
+        bool isTimeout(uint8_t timeout) {
+            return ((mCnt % timeout) == 0);
+        }
+
+    private:
+        uint8_t mCnt = 0;
+        uint8_t mScanCnt = 0;
+        std::list<uint8_t> mBSSIDList;
+        bool mWasInCh12to14 = false;
+        static constexpr uint8_t TIMEOUT = 20;
+        static constexpr uint8_t SCAN_TIMEOUT = 10;
+};
+
+#endif /*ESP8266*/
+#endif /*__AHOY_WIFI_ESP8266_H__*/
diff --git a/src/platformio.ini b/src/platformio.ini
index f949aa37..0639d16f 100644
--- a/src/platformio.ini
+++ b/src/platformio.ini
@@ -23,16 +23,18 @@ extra_scripts =
     pre:../scripts/convertHtml.py
     pre:../scripts/applyPatches.py
     pre:../scripts/reduceGxEPD2.py
+    post:../scripts/add_littlefs_binary.py
 
 lib_deps =
-    https://github.com/yubox-node-org/ESPAsyncWebServer
-    https://github.com/nRF24/RF24 @ 1.4.8
+    https://github.com/esphome/ESPAsyncWebServer @ ^3.2.2
+    https://github.com/nRF24/RF24.git#v1.4.8
     paulstoffregen/Time @ ^1.6.1
-    https://github.com/bertmelis/espMqttClient#v1.6.0
-    bblanchon/ArduinoJson @ ^6.21.3
+    https://github.com/bertmelis/espMqttClient#v1.7.0
+    bblanchon/ArduinoJson @ ^6.21.5
     https://github.com/JChristensen/Timezone @ ^1.2.4
     olikraus/U8g2 @ ^2.35.9
     https://github.com/zinggjm/GxEPD2#1.5.3
+
 build_flags =
     -std=c++17
     -std=gnu++17
@@ -40,67 +42,78 @@ build_unflags =
     -std=gnu++11
 
 
-[env:esp8266]
+[env:esp8266-minimal]
 platform = espressif8266
 board = esp12e
 board_build.f_cpu = 80000000L
+lib_deps =
+    ${env.lib_deps}
+    https://github.com/me-no-dev/ESPAsyncUDP
 build_flags = ${env.build_flags}
     -DEMC_MIN_FREE_MEMORY=4096
-    -DENABLE_MQTT
-    -DPLUGIN_DISPLAY
-    -DENABLE_HISTORY
     ;-Wl,-Map,output.map
 monitor_filters =
     esp8266_exception_decoder
 
+[env:esp8266]
+platform = espressif8266
+board = esp12e
+board_build.f_cpu = 80000000L
+lib_deps = ${env:esp8266-minimal.lib_deps}
+    https://github.com/me-no-dev/ESPAsyncUDP
+build_flags = ${env:esp8266-minimal.build_flags}
+    -DENABLE_MQTT
+monitor_filters =
+    esp8266_exception_decoder
+
 [env:esp8266-de]
 platform = espressif8266
 board = esp12e
 board_build.f_cpu = 80000000L
-build_flags = ${env.build_flags}
-    -DEMC_MIN_FREE_MEMORY=4096
+lib_deps = ${env:esp8266.lib_deps}
+build_flags = ${env:esp8266.build_flags}
     -DLANG_DE
-    -DENABLE_MQTT
-    -DPLUGIN_DISPLAY
-    -DENABLE_HISTORY
-    ;-Wl,-Map,output.map
 monitor_filters =
     esp8266_exception_decoder
 
-[env:esp8266-prometheus]
+[env:esp8266-all]
 platform = espressif8266
 board = esp12e
 board_build.f_cpu = 80000000L
-build_flags = ${env.build_flags}
-    -DEMC_MIN_FREE_MEMORY=4096
-    -DENABLE_PROMETHEUS_EP
-    -DENABLE_MQTT
+lib_deps = ${env:esp8266.lib_deps}
+build_flags = ${env:esp8266.build_flags}
     -DPLUGIN_DISPLAY
     -DENABLE_HISTORY
 monitor_filters =
     esp8266_exception_decoder
 
-[env:esp8266-prometheus-de]
+[env:esp8266-all-de]
 platform = espressif8266
 board = esp12e
 board_build.f_cpu = 80000000L
-build_flags = ${env.build_flags}
-    -DEMC_MIN_FREE_MEMORY=4096
-    -DENABLE_PROMETHEUS_EP
+lib_deps = ${env:esp8266.lib_deps}
+build_flags = ${env:esp8266-all.build_flags}
     -DLANG_DE
-    -DENABLE_MQTT
-    -DPLUGIN_DISPLAY
-    -DENABLE_HISTORY
 monitor_filters =
     esp8266_exception_decoder
 
-[env:esp8266-minimal]
+[env:esp8266-prometheus]
 platform = espressif8266
 board = esp12e
 board_build.f_cpu = 80000000L
-build_flags = ${env.build_flags}
-    -DEMC_MIN_FREE_MEMORY=4096
-    ;-Wl,-Map,output.map
+lib_deps = ${env:esp8266.lib_deps}
+build_flags = ${env:esp8266-all.build_flags}
+    -DENABLE_PROMETHEUS_EP
+monitor_filters =
+    esp8266_exception_decoder
+
+[env:esp8266-prometheus-de]
+platform = espressif8266
+board = esp12e
+board_build.f_cpu = 80000000L
+lib_deps = ${env:esp8266.lib_deps}
+build_flags = ${env:esp8266-prometheus.build_flags}
+    -DLANG_DE
 monitor_filters =
     esp8266_exception_decoder
 
@@ -111,6 +124,7 @@ platform = espressif8266
 board = esp8285
 board_build.ldscript = eagle.flash.1m64.ld
 board_build.f_cpu = 80000000L
+lib_deps = ${env:esp8266.lib_deps}
 build_flags = ${env.build_flags}
     -DEMC_MIN_FREE_MEMORY=4096
     -DENABLE_MQTT
@@ -124,6 +138,7 @@ platform = espressif8266
 board = esp8285
 board_build.ldscript = eagle.flash.1m64.ld
 board_build.f_cpu = 80000000L
+lib_deps = ${env:esp8266.lib_deps}
 build_flags = ${env.build_flags}
     -DEMC_MIN_FREE_MEMORY=4096
     -DLANG_DE
@@ -133,114 +148,73 @@ build_flags = ${env.build_flags}
 monitor_filters =
     esp8266_exception_decoder
 
-[env:esp32-wroom32]
-platform = espressif32@6.5.0
+[env:esp32-wroom32-minimal]
+platform = espressif32@6.7.0
 board = lolin_d32
 build_flags = ${env.build_flags}
-    -DUSE_HSPI_FOR_EPD
-    -DENABLE_MQTT
-    -DPLUGIN_DISPLAY
-    -DENABLE_HISTORY
+    -DSPI_HAL
 monitor_filters =
     esp32_exception_decoder
 
-[env:esp32-wroom32-minimal]
-platform = espressif32@6.5.0
+[env:esp32-wroom32]
+platform = espressif32@6.7.0
 board = lolin_d32
-build_flags = ${env.build_flags}
+build_flags = ${env:esp32-wroom32-minimal.build_flags}
     -DUSE_HSPI_FOR_EPD
+    -DENABLE_MQTT
+    -DPLUGIN_DISPLAY
+    -DENABLE_HISTORY
+    -DETHERNET
+    -DDEF_ETH_CS_PIN=15
+    -DDEF_ETH_SCK_PIN=14
+    -DDEF_ETH_MISO_PIN=12
+    -DDEF_ETH_MOSI_PIN=13
+    -DDEF_ETH_IRQ_PIN=4
+    -DDEF_ETH_RST_PIN=255
+    -DDEF_NRF_CS_PIN=5
+    -DDEF_NRF_CE_PIN=17
+    -DDEF_NRF_IRQ_PIN=16
+    -DDEF_NRF_MISO_PIN=19
+    -DDEF_NRF_MOSI_PIN=23
+    -DDEF_NRF_SCLK_PIN=18
+    -DDEF_CMT_CSB=27
+    -DDEF_CMT_FCSB=26
+    -DDEF_CMT_IRQ=34
+    -DDEF_CMT_SDIO=14
+    -DDEF_CMT_SCLK=12
 monitor_filters =
     esp32_exception_decoder
 
 [env:esp32-wroom32-de]
-platform = espressif32@6.5.0
+platform = espressif32@6.7.0
 board = lolin_d32
-build_flags = ${env.build_flags}
-    -DUSE_HSPI_FOR_EPD
-    -DENABLE_MQTT
-    -DPLUGIN_DISPLAY
-    -DENABLE_HISTORY
+build_flags = ${env:esp32-wroom32.build_flags}
     -DLANG_DE
 monitor_filters =
     esp32_exception_decoder
 
 [env:esp32-wroom32-prometheus]
-platform = espressif32@6.5.0
+platform = espressif32@6.7.0
 board = lolin_d32
-build_flags = ${env.build_flags}
-    -DUSE_HSPI_FOR_EPD
+build_flags = ${env:esp32-wroom32.build_flags}
     -DENABLE_PROMETHEUS_EP
-    -DENABLE_MQTT
-    -DPLUGIN_DISPLAY
-    -DENABLE_HISTORY
 monitor_filters =
     esp32_exception_decoder
 
 [env:esp32-wroom32-prometheus-de]
-platform = espressif32@6.5.0
+platform = espressif32@6.7.0
 board = lolin_d32
-build_flags = ${env.build_flags}
-    -DUSE_HSPI_FOR_EPD
-    -DLANG_DE
-    -DENABLE_PROMETHEUS_EP
-    -DENABLE_MQTT
-    -DPLUGIN_DISPLAY
-    -DENABLE_HISTORY
-monitor_filters =
-    esp32_exception_decoder
-
-[env:esp32-wroom32-ethernet]
-platform = espressif32
-board = esp32dev
-lib_deps =
-    khoih-prog/AsyncWebServer_ESP32_W5500
-    khoih-prog/AsyncUDP_ESP32_W5500
-    https://github.com/nRF24/RF24 @ ^1.4.8
-    paulstoffregen/Time @ ^1.6.1
-    https://github.com/bertmelis/espMqttClient#v1.6.0
-    bblanchon/ArduinoJson @ ^6.21.3
-    https://github.com/JChristensen/Timezone @ ^1.2.4
-    olikraus/U8g2 @ ^2.35.9
-    https://github.com/zinggjm/GxEPD2#1.5.3
-build_flags = ${env.build_flags}
-    -D ETHERNET
-    -DRELEASE
-    -DUSE_HSPI_FOR_EPD
-    -DENABLE_MQTT
-    -DPLUGIN_DISPLAY
-    -DENABLE_HISTORY
-monitor_filters =
-    esp32_exception_decoder
-
-[env:esp32-wroom32-ethernet-de]
-platform = espressif32
-board = esp32dev
-lib_deps =
-    khoih-prog/AsyncWebServer_ESP32_W5500
-    khoih-prog/AsyncUDP_ESP32_W5500
-    https://github.com/nRF24/RF24 @ ^1.4.8
-    paulstoffregen/Time @ ^1.6.1
-    https://github.com/bertmelis/espMqttClient#v1.6.0
-    bblanchon/ArduinoJson @ ^6.21.3
-    https://github.com/JChristensen/Timezone @ ^1.2.4
-    olikraus/U8g2 @ ^2.35.9
-    https://github.com/zinggjm/GxEPD2#1.5.3
-build_flags = ${env.build_flags}
-    -D ETHERNET
-    -DRELEASE
-    -DUSE_HSPI_FOR_EPD
+build_flags = ${env:esp32-wroom32-prometheus.build_flags}
     -DLANG_DE
-    -DENABLE_MQTT
-    -DPLUGIN_DISPLAY
-    -DENABLE_HISTORY
 monitor_filters =
     esp32_exception_decoder
 
 [env:esp32-s2-mini]
-platform = espressif32@6.5.0
+platform = espressif32@6.7.0
 board = lolin_s2_mini
 build_flags = ${env.build_flags}
     -DUSE_HSPI_FOR_EPD
+    -DSPI_HAL
     -DENABLE_MQTT
     -DPLUGIN_DISPLAY
     -DENABLE_HISTORY
@@ -259,33 +233,19 @@ monitor_filters =
     esp32_exception_decoder
 
 [env:esp32-s2-mini-de]
-platform = espressif32@6.5.0
+platform = espressif32@6.7.0
 board = lolin_s2_mini
-build_flags = ${env.build_flags}
-    -DUSE_HSPI_FOR_EPD
-    -DENABLE_MQTT
-    -DPLUGIN_DISPLAY
-    -DENABLE_HISTORY
-    -DDEF_NRF_CS_PIN=12
-    -DDEF_NRF_CE_PIN=3
-    -DDEF_NRF_IRQ_PIN=5
-    -DDEF_NRF_MISO_PIN=9
-    -DDEF_NRF_MOSI_PIN=11
-    -DDEF_NRF_SCLK_PIN=7
-    -DDEF_CMT_CSB=16
-    -DDEF_CMT_FCSB=18
-    -DDEF_CMT_IRQ=33
-    -DDEF_CMT_SDIO=35
-    -DDEF_CMT_SCLK=37
+build_flags = ${env:esp32-s2-mini.build_flags}
     -DLANG_DE
 monitor_filters =
     esp32_exception_decoder
 
 [env:esp32-c3-mini]
-platform = espressif32@6.5.0
+platform = espressif32@6.7.0
 board = lolin_c3_mini
 build_flags = ${env.build_flags}
     -DUSE_HSPI_FOR_EPD
+    -DSPI_HAL
     -DENABLE_MQTT
     -DPLUGIN_DISPLAY
     -DENABLE_HISTORY
@@ -304,36 +264,18 @@ monitor_filters =
     esp32_exception_decoder
 
 [env:esp32-c3-mini-de]
-platform = espressif32@6.5.0
+platform = espressif32@6.7.0
 board = lolin_c3_mini
-build_flags = ${env.build_flags}
-    -DUSE_HSPI_FOR_EPD
-    -DENABLE_MQTT
-    -DPLUGIN_DISPLAY
-    -DENABLE_HISTORY
-    -DDEF_NRF_CS_PIN=5
-    -DDEF_NRF_CE_PIN=0
-    -DDEF_NRF_IRQ_PIN=1
-    -DDEF_NRF_MISO_PIN=3
-    -DDEF_NRF_MOSI_PIN=4
-    -DDEF_NRF_SCLK_PIN=2
-    -DDEF_CMT_CSB=255
-    -DDEF_CMT_FCSB=255
-    -DDEF_CMT_IRQ=255
-    -DDEF_CMT_SDIO=255
-    -DDEF_CMT_SCLK=255
+build_flags = ${env:esp32-c3-mini.build_flags}
     -DLANG_DE
 monitor_filters =
     esp32_exception_decoder
 
-[env:opendtufusion]
-platform = espressif32@6.5.0
+[env:opendtufusion-minimal]
+platform = espressif32@6.7.0
 board = esp32-s3-devkitc-1
 upload_protocol = esp-builtin
 build_flags = ${env.build_flags}
-    -DENABLE_MQTT
-    -DPLUGIN_DISPLAY
-    -DENABLE_HISTORY
     -DSPI_HAL
     -DDEF_NRF_CS_PIN=37
     -DDEF_NRF_CE_PIN=38
@@ -354,145 +296,48 @@ build_flags = ${env.build_flags}
 monitor_filters =
     esp32_exception_decoder, colorize
 
-[env:opendtufusion-de]
-platform = espressif32@6.5.0
+[env:opendtufusion]
+platform = espressif32@6.7.0
 board = esp32-s3-devkitc-1
 upload_protocol = esp-builtin
-build_flags = ${env.build_flags}
-    -DLANG_DE
+build_flags = ${env:opendtufusion-minimal.build_flags}
+    -DETHERNET
     -DENABLE_MQTT
     -DPLUGIN_DISPLAY
     -DENABLE_HISTORY
-    -DSPI_HAL
-    -DDEF_NRF_CS_PIN=37
-    -DDEF_NRF_CE_PIN=38
-    -DDEF_NRF_IRQ_PIN=47
-    -DDEF_NRF_MISO_PIN=48
-    -DDEF_NRF_MOSI_PIN=35
-    -DDEF_NRF_SCLK_PIN=36
-    -DDEF_CMT_CSB=4
-    -DDEF_CMT_FCSB=21
-    -DDEF_CMT_IRQ=8
-    -DDEF_CMT_SDIO=5
-    -DDEF_CMT_SCLK=6
-    -DDEF_LED0=18
-    -DDEF_LED1=17
-    -DLED_ACTIVE_HIGH
-    -DARDUINO_USB_MODE=1
+    -DDEF_ETH_CS_PIN=42
+    -DDEF_ETH_SCK_PIN=39
+    -DDEF_ETH_MISO_PIN=41
+    -DDEF_ETH_MOSI_PIN=40
+    -DDEF_ETH_IRQ_PIN=44
+    -DDEF_ETH_RST_PIN=43
 monitor_filters =
     esp32_exception_decoder, colorize
 
-[env:opendtufusion-minimal]
-platform = espressif32@6.5.0
+[env:opendtufusion-de]
+platform = espressif32@6.7.0
 board = esp32-s3-devkitc-1
 upload_protocol = esp-builtin
-build_flags = ${env.build_flags}
-    -DSPI_HAL
-    -DDEF_NRF_CS_PIN=37
-    -DDEF_NRF_CE_PIN=38
-    -DDEF_NRF_IRQ_PIN=47
-    -DDEF_NRF_MISO_PIN=48
-    -DDEF_NRF_MOSI_PIN=35
-    -DDEF_NRF_SCLK_PIN=36
-    -DDEF_CMT_CSB=4
-    -DDEF_CMT_FCSB=21
-    -DDEF_CMT_IRQ=8
-    -DDEF_CMT_SDIO=5
-    -DDEF_CMT_SCLK=6
-    -DDEF_LED0=18
-    -DDEF_LED1=17
-    -DLED_ACTIVE_HIGH
-    -DARDUINO_USB_MODE=1
+build_flags = ${env:opendtufusion.build_flags}
+    -DLANG_DE
 monitor_filters =
     esp32_exception_decoder, colorize
 
-[env:opendtufusion-ethernet]
-platform = espressif32@6.5.0
+[env:opendtufusion-16MB]
+platform = espressif32@6.7.0
 board = esp32-s3-devkitc-1
-lib_deps =
-    khoih-prog/AsyncWebServer_ESP32_W5500
-    khoih-prog/AsyncUDP_ESP32_W5500
-    https://github.com/nrf24/RF24 @ ^1.4.8
-    paulstoffregen/Time @ ^1.6.1
-    https://github.com/bertmelis/espMqttClient#v1.6.0
-    bblanchon/ArduinoJson @ ^6.21.3
-    https://github.com/JChristensen/Timezone @ ^1.2.4
-    olikraus/U8g2 @ ^2.35.9
-    https://github.com/zinggjm/GxEPD2#1.5.3
+board_upload.flash_size = 16MB
+board_build.partitions = default_16MB.csv
 upload_protocol = esp-builtin
-build_flags = ${env.build_flags}
-    -DETHERNET
-    -DSPI_HAL
-    -DENABLE_MQTT
-    -DPLUGIN_DISPLAY
-    -DENABLE_HISTORY
-    -DDEF_ETH_CS_PIN=42
-    -DDEF_ETH_SCK_PIN=39
-    -DDEF_ETH_MISO_PIN=41
-    -DDEF_ETH_MOSI_PIN=40
-    -DDEF_ETH_IRQ_PIN=44
-    -DDEF_ETH_RST_PIN=43
-    -DDEF_NRF_CS_PIN=37
-    -DDEF_NRF_CE_PIN=38
-    -DDEF_NRF_IRQ_PIN=47
-    -DDEF_NRF_MISO_PIN=48
-    -DDEF_NRF_MOSI_PIN=35
-    -DDEF_NRF_SCLK_PIN=36
-    -DDEF_CMT_CSB=4
-    -DDEF_CMT_FCSB=21
-    -DDEF_CMT_IRQ=8
-    -DDEF_CMT_SDIO=5
-    -DDEF_CMT_SCLK=6
-    -DDEF_LED0=18
-    -DDEF_LED1=17
-    -DLED_ACTIVE_HIGH
-    -DARDUINO_USB_MODE=1
-    #-DARDUINO_USB_CDC_ON_BOOT=1
+build_flags = ${env:opendtufusion.build_flags}
 monitor_filters =
     esp32_exception_decoder, colorize
 
-[env:opendtufusion-ethernet-de]
-platform = espressif32@6.5.0
+[env:opendtufusion-16MB-de]
+platform = espressif32@6.7.0
 board = esp32-s3-devkitc-1
-lib_deps =
-    khoih-prog/AsyncWebServer_ESP32_W5500
-    khoih-prog/AsyncUDP_ESP32_W5500
-    https://github.com/nrf24/RF24 @ ^1.4.8
-    paulstoffregen/Time @ ^1.6.1
-    https://github.com/bertmelis/espMqttClient#v1.6.0
-    bblanchon/ArduinoJson @ ^6.21.3
-    https://github.com/JChristensen/Timezone @ ^1.2.4
-    olikraus/U8g2 @ ^2.35.9
-    https://github.com/zinggjm/GxEPD2#1.5.3
 upload_protocol = esp-builtin
-build_flags = ${env.build_flags}
-    -DETHERNET
-    -DSPI_HAL
+build_flags = ${env:opendtufusion-16MB.build_flags}
     -DLANG_DE
-    -DENABLE_MQTT
-    -DPLUGIN_DISPLAY
-    -DENABLE_HISTORY
-    -DDEF_ETH_CS_PIN=42
-    -DDEF_ETH_SCK_PIN=39
-    -DDEF_ETH_MISO_PIN=41
-    -DDEF_ETH_MOSI_PIN=40
-    -DDEF_ETH_IRQ_PIN=44
-    -DDEF_ETH_RST_PIN=43
-    -DDEF_NRF_CS_PIN=37
-    -DDEF_NRF_CE_PIN=38
-    -DDEF_NRF_IRQ_PIN=47
-    -DDEF_NRF_MISO_PIN=48
-    -DDEF_NRF_MOSI_PIN=35
-    -DDEF_NRF_SCLK_PIN=36
-    -DDEF_CMT_CSB=4
-    -DDEF_CMT_FCSB=21
-    -DDEF_CMT_IRQ=8
-    -DDEF_CMT_SDIO=5
-    -DDEF_CMT_SCLK=6
-    -DDEF_LED0=18
-    -DDEF_LED1=17
-    -DLED_ACTIVE_HIGH
-    -DARDUINO_USB_MODE=1
-    #-DARDUINO_USB_CDC_ON_BOOT=1
 monitor_filters =
     esp32_exception_decoder, colorize
diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h
index b2c88b05..4654a4f4 100644
--- a/src/plugins/Display/Display.h
+++ b/src/plugins/Display/Display.h
@@ -7,8 +7,9 @@
 #include <U8g2lib.h>
 
 #include "../../hm/hmSystem.h"
-#include "../../hm/hmRadio.h"
+#include "../../hm/NrfRadio.h"
 #include "../../utils/helper.h"
+#include "../plugin_lang.h"
 #include "Display_Mono.h"
 #include "Display_Mono_128X32.h"
 #include "Display_Mono_128X64.h"
@@ -24,9 +25,9 @@ class Display {
             mMono = NULL;
         }
 
-        void setup(IApp *app, display_t *cfg, HMSYSTEM *sys, RADIO *hmradio, RADIO *hmsradio, uint32_t *utcTs) {
+        void setup(IApp *app, display_t *cfg, HMSYSTEM *sys, RADIO *nrfRadio, RADIO *hmsradio, uint32_t *utcTs) {
             mApp = app;
-            mHmRadio  = hmradio;
+            mNrfRadio  = nrfRadio;
             mHmsRadio = hmsradio;
             mCfg = cfg;
             mSys = sys;
@@ -44,7 +45,7 @@ class Display {
                 case DISP_TYPE_T4_SSD1306_128X32:   mMono = new DisplayMono128X32(); break; // SSD1306_128X32 (0.91")
                 case DISP_TYPE_T5_SSD1306_64X48:    mMono = new DisplayMono64X48();  break; // SSD1306_64X48 (0.66" - Wemos OLED Shield)
                 case DISP_TYPE_T6_SSD1309_128X64:   mMono = new DisplayMono128X64(); break; // SSD1309_128X64 (2.42")
-    #if defined(ESP32) && !defined(ETHERNET)
+    #if defined(ESP32)
                 case DISP_TYPE_T10_EPAPER:
                     mMono = NULL;   // ePaper does not use this
                     mRefreshCycle = 0;
@@ -71,6 +72,14 @@ class Display {
 
         }
 
+        void loop() {
+            #if defined(ESP32)
+            if ((nullptr != mCfg) && (DISP_TYPE_T10_EPAPER == mCfg->type)) {
+                mEpaper.refreshLoop();
+            }
+            #endif
+        }
+
         void payloadEventListener(uint8_t cmd) {
             mNewPayload = true;
         }
@@ -78,16 +87,25 @@ class Display {
         void tickerSecond() {
             bool request_refresh = false;
 
-            if (mMono != NULL)
+            if (mMono != NULL) {
+                // maintain LCD and OLED displays with pixel shift screensavers, at least every 5 seconds
                 request_refresh = mMono->loop(motionSensorActive());
-
-            if (mNewPayload || (((++mLoopCnt) % 5) == 0) || request_refresh) {
-                DataScreen();
-                mNewPayload = false;
-                mLoopCnt = 0;
+                if (mNewPayload || (((++mLoopCnt) % 5) == 0) || request_refresh) {
+                    DataScreen();
+                    mNewPayload = false;
+                    mLoopCnt = 0;
+                }
             }
-            #if defined(ESP32) && !defined(ETHERNET)
+            #if defined(ESP32)
+            else if (DISP_TYPE_T10_EPAPER == mCfg->type) {
+                // maintain ePaper at least every 15 seconds
+                if (mNewPayload || (((++mLoopCnt) % 15) == 0)) {
+                    DataScreen();
+                    mNewPayload = false;
+                    mLoopCnt = 0;
+                }
                 mEpaper.tickerSecond();
+            }
             #endif
         }
 
@@ -148,7 +166,7 @@ class Display {
             mDisplayData.totalYieldDay = totalYieldDay;
             mDisplayData.totalYieldTotal = totalYieldTotal;
             bool nrf_en = mApp->getNrfEnabled();
-            bool nrf_ok = nrf_en && mHmRadio->isChipConnected();
+            bool nrf_ok = nrf_en && mNrfRadio->isChipConnected();
             #if defined(ESP32)
             bool cmt_en = mApp->getCmtEnabled();
             bool cmt_ok = cmt_en && mHmsRadio->isChipConnected();
@@ -175,16 +193,18 @@ class Display {
             if (mMono ) {
                 mMono->disp();
             }
-    #if defined(ESP32) && !defined(ETHERNET)
+    #if defined(ESP32)
             else if (DISP_TYPE_T10_EPAPER == mCfg->type) {
                 mEpaper.loop((totalPower), totalYieldDay, totalYieldTotal, nrprod);
                 mRefreshCycle++;
-            }
 
-            if (mRefreshCycle > 480) {
-                mEpaper.fullRefresh();
-                mRefreshCycle = 0;
+                if (mRefreshCycle > 2880) { // 15 * 2280 = 44300s = 12h
+                    mEpaper.fullRefresh();
+                    mRefreshCycle = 0;
+                }
+
             }
+
     #endif
         }
 
@@ -230,11 +250,11 @@ class Display {
         uint32_t *mUtcTs = nullptr;
         display_t *mCfg = nullptr;
         HMSYSTEM *mSys = nullptr;
-        RADIO *mHmRadio = nullptr;
+        RADIO *mNrfRadio = nullptr;
         RADIO *mHmsRadio = nullptr;
         uint16_t mRefreshCycle = 0;
 
-    #if defined(ESP32) && !defined(ETHERNET)
+    #if defined(ESP32)
         DisplayEPaper mEpaper;
     #endif
         DisplayMono *mMono = nullptr;
diff --git a/src/plugins/Display/Display_Mono_128X32.h b/src/plugins/Display/Display_Mono_128X32.h
index 6262e3f6..def87507 100644
--- a/src/plugins/Display/Display_Mono_128X32.h
+++ b/src/plugins/Display/Display_Mono_128X32.h
@@ -40,20 +40,20 @@ class DisplayMono128X32 : public DisplayMono {
 
                 printText(mFmtText, 0);
             } else {
-                printText("offline", 0);
+                printText(STR_OFFLINE, 0);
             }
 
-            snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", mDisplayData->totalYieldDay);
+            snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %4.0f Wh", STR_TODAY, mDisplayData->totalYieldDay);
             printText(mFmtText, 1);
 
-            snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", mDisplayData->totalYieldTotal);
+            snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %.1f kWh", STR_TOTAL, mDisplayData->totalYieldTotal);
             printText(mFmtText, 2);
 
             IPAddress ip = WiFi.localIP();
             if (!(mExtra % 10) && (ip))
                 printText(ip.toString().c_str(), 3);
             else if (!(mExtra % 5)) {
-                snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", mDisplayData->nrProducing);
+                snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %d", STR_ACTIVE_INVERTERS, mDisplayData->nrProducing);
                 printText(mFmtText, 3);
             } else if (0 != mDisplayData->utcTs)
                 printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3);
diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h
index c63f0b22..c93c5c1a 100644
--- a/src/plugins/Display/Display_Mono_128X64.h
+++ b/src/plugins/Display/Display_Mono_128X64.h
@@ -93,7 +93,7 @@ class DisplayMono128X64 : public DisplayMono {
 
             // print Date and time
             if (0 != mDisplayData->utcTs)
-                printText(ah::getDateTimeStrShort(mDisplayData->utcTs).c_str(), l_Time, 0xff);
+                printText(ah::getDateTimeStrShort_i18n(mDisplayData->utcTs).c_str(), l_Time, 0xff);
 
             if (showLine(l_Status)) {
                 // alternatively:
@@ -108,7 +108,7 @@ class DisplayMono128X64 : public DisplayMono {
                     int8_t moon_pos = -1;
                     setLineFont(l_Status);
                     if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing)
-                        snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter");
+                        snprintf(mFmtText, DISP_FMT_TEXT_LEN, STR_NO_INVERTER);
                     else if (0 == mDisplayData->nrSleeping) {
                         snprintf(mFmtText, DISP_FMT_TEXT_LEN, "  ");
                         sun_pos = 0;
@@ -145,7 +145,7 @@ class DisplayMono128X64 : public DisplayMono {
 
                     printText(mFmtText, l_TotalPower, 0xff);
                 } else {
-                    printText("offline", l_TotalPower, 0xff);
+                    printText(STR_OFFLINE, l_TotalPower, 0xff);
                 }
             }
 
diff --git a/src/plugins/Display/Display_Mono_64X48.h b/src/plugins/Display/Display_Mono_64X48.h
index 7f98cae5..799d787e 100644
--- a/src/plugins/Display/Display_Mono_64X48.h
+++ b/src/plugins/Display/Display_Mono_64X48.h
@@ -42,7 +42,7 @@ class DisplayMono64X48 : public DisplayMono {
 
                 printText(mFmtText, 0);
             } else {
-                printText("offline", 0);
+                printText(STR_OFFLINE, 0);
             }
 
             snprintf(mFmtText, DISP_FMT_TEXT_LEN, "D: %4.0f Wh", mDisplayData->totalYieldDay);
@@ -55,7 +55,7 @@ class DisplayMono64X48 : public DisplayMono {
             if (!(mExtra % 10) && (ip))
                 printText(ip.toString().c_str(), 3);
             else if (!(mExtra % 5)) {
-                snprintf(mFmtText, DISP_FMT_TEXT_LEN, "active Inv: %d", mDisplayData->nrProducing);
+                snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %d", STR_ACTIVE_INVERTERS, mDisplayData->nrProducing);
                 printText(mFmtText, 3);
             } else if (0 != mDisplayData->utcTs)
                 printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3);
diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h
index b5daacd5..ccbc8083 100644
--- a/src/plugins/Display/Display_Mono_84X48.h
+++ b/src/plugins/Display/Display_Mono_84X48.h
@@ -78,7 +78,7 @@ class DisplayMono84X48 : public DisplayMono {
 
             // print Date and time
             if (0 != mDisplayData->utcTs)
-                printText(ah::getDateTimeStrShort(mDisplayData->utcTs).c_str(), l_Time, 0xff);
+                printText(ah::getDateTimeStrShort_i18n(mDisplayData->utcTs).c_str(), l_Time, 0xff);
 
             if (showLine(l_Status)) {
                 // alternatively:
@@ -90,7 +90,7 @@ class DisplayMono84X48 : public DisplayMono {
                 // print status of inverters
                 else {
                     if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing)
-                        snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter");
+                        snprintf(mFmtText, DISP_FMT_TEXT_LEN, STR_NO_INVERTER);
                     else if (0 == mDisplayData->nrSleeping)
                         snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x86");      // sun symbol
                     else if (0 == mDisplayData->nrProducing)
@@ -110,9 +110,8 @@ class DisplayMono84X48 : public DisplayMono {
                         snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower);
 
                     printText(mFmtText, l_TotalPower, 0xff);
-                } else {
-                    printText("offline", l_TotalPower, 0xff);
-                }
+                } else
+                    printText(STR_OFFLINE, l_TotalPower, 0xff);
             }
 
             if (showLine(l_YieldDay)) {
diff --git a/src/plugins/Display/Display_ePaper.cpp b/src/plugins/Display/Display_ePaper.cpp
index 087d784b..654627d2 100644
--- a/src/plugins/Display/Display_ePaper.cpp
+++ b/src/plugins/Display/Display_ePaper.cpp
@@ -1,15 +1,11 @@
 #include "Display_ePaper.h"
 
-#ifdef ESP8266
-#include <ESP8266WiFi.h>
-#elif defined(ESP32)
+#if defined(ESP32)
 #include <WiFi.h>
-#endif
 #include "../../utils/helper.h"
 #include "imagedata.h"
 #include "defines.h"
-
-#if defined(ESP32)
+#include "../plugin_lang.h"
 
 static const uint32_t spiClk = 4000000;  // 4 MHz
 
@@ -30,20 +26,26 @@ void DisplayEPaper::init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, u
 
     mRefreshState = RefreshStatus::LOGO;
     mSecondCnt = 0;
+    mLogoDisplayed = false;
 
     if (DISP_TYPE_T10_EPAPER == type) {
-        Serial.begin(115200);
-        _display = new GxEPD2_BW<GxEPD2_150_BN, GxEPD2_150_BN::HEIGHT>(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY));
-
-#if defined(ESP32) && defined(USE_HSPI_FOR_EPD)
-        hspi.begin(_SCK, _BUSY, _MOSI, _CS);
-        _display->epd2.selectSPI(hspi, SPISettings(spiClk, MSBFIRST, SPI_MODE0));
-#elif defined(ESP32)
-        _display->epd2.init(_SCK, _MOSI, 115200, true, 20, false);
-#endif
-        _display->init(115200, true, 20, false);
+        #if defined(SPI_HAL)
+            hal.init(_MOSI, _DC, _SCK, _CS, _RST, _BUSY);
+            _display = new GxEPD2_BW<GxEPD2_150_BN, GxEPD2_150_BN::HEIGHT>(GxEPD2_150_BN(&hal));
+        #else
+            _display = new GxEPD2_BW<GxEPD2_150_BN, GxEPD2_150_BN::HEIGHT>(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY));
+            #if defined(USE_HSPI_FOR_EPD)
+            hspi.begin(_SCK, _BUSY, _MOSI, _CS);
+            _display->epd2.selectSPI(hspi, SPISettings(spiClk, MSBFIRST, SPI_MODE0));
+            #elif defined(PLUGIN_DISPLAY)
+            _display->epd2.init(_SCK, _MOSI, 115200, true, 20, false);
+            #endif
+        #endif
+        _display->init(0, true, 20, false);
         _display->setRotation(mDisplayRotation);
         _display->setFullWindow();
+        _display->setTextColor(GxEPD_BLACK);
+        _display->firstPage();
         _version = version;
     }
 }
@@ -57,7 +59,8 @@ void DisplayEPaper::config(uint8_t rotation, bool enPowerSave) {
 void DisplayEPaper::fullRefresh() {
     if(RefreshStatus::DONE != mRefreshState)
         return;
-    mSecondCnt = 2;
+    if(mLogoDisplayed)
+        return; // no refresh during logo display
     mRefreshState = RefreshStatus::BLACK;
 }
 
@@ -65,40 +68,42 @@ void DisplayEPaper::fullRefresh() {
 void DisplayEPaper::refreshLoop() {
     switch(mRefreshState) {
         case RefreshStatus::LOGO:
-            _display->fillScreen(GxEPD_BLACK);
-            _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE);
-            mSecondCnt = 2;
-            mNextRefreshState = RefreshStatus::PARTITIALS;
-            mRefreshState = RefreshStatus::WAIT;
+            _display->fillScreen(GxEPD_WHITE);
+            _display->drawInvertedBitmap(0, 0, logo, 200, 200, GxEPD_BLACK);
+            if(_display->nextPage())
+                break;
+            mSecondCnt = 10;
+            _display->powerOff();
+            mRefreshState = RefreshStatus::LOGO_WAIT;
+            break;
+
+        case RefreshStatus::LOGO_WAIT:
+            if(0 != mSecondCnt)
+                break;
+            mRefreshState = RefreshStatus::WHITE;
+            _display->firstPage();
             break;
 
         case RefreshStatus::BLACK:
             _display->fillScreen(GxEPD_BLACK);
-            mNextRefreshState = RefreshStatus::WHITE;
-            mRefreshState = RefreshStatus::WAIT;
+            if(_display->nextPage())
+                break;
+            mRefreshState = RefreshStatus::WHITE;
+            _display->firstPage();
             break;
 
         case RefreshStatus::WHITE:
-            if(0 != mSecondCnt)
-                break;
             _display->fillScreen(GxEPD_WHITE);
-            mNextRefreshState = RefreshStatus::PARTITIALS;
-            mRefreshState = RefreshStatus::WAIT;
-            break;
-
-        case RefreshStatus::WAIT:
-            if(!_display->nextPage())
-                mRefreshState = mNextRefreshState;
+            if(_display->nextPage())
+                break;
+            mRefreshState = RefreshStatus::PARTITIALS;
+            _display->firstPage();
             break;
 
         case RefreshStatus::PARTITIALS:
-            if(0 != mSecondCnt)
-                break;
             headlineIP();
             versionFooter();
-            mSecondCnt = 4; // display Logo time during boot up
-            mNextRefreshState = RefreshStatus::DONE;
-            mRefreshState = RefreshStatus::WAIT;
+            mRefreshState = RefreshStatus::DONE;
             break;
 
         default: // RefreshStatus::DONE
@@ -120,7 +125,7 @@ void DisplayEPaper::headlineIP() {
         if ((WiFi.isConnected() == true) && (WiFi.localIP() > 0)) {
             snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%s", WiFi.localIP().toString().c_str());
         } else {
-            snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "WiFi not connected");
+            snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, STR_NO_WIFI);
         }
         _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
         uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@@ -162,7 +167,7 @@ void DisplayEPaper::versionFooter() {
     _display->setPartialWindow(0, _display->height() - mHeadFootPadding, _display->width(), mHeadFootPadding);
     _display->fillScreen(GxEPD_BLACK);
     do {
-        snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "Version: %s", _version);
+        snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%s: %s", STR_VERSION, _version);
 
         _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
         uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@@ -183,7 +188,7 @@ void DisplayEPaper::offlineFooter() {
     _display->fillScreen(GxEPD_BLACK);
     do {
         if (NULL != mUtcTs) {
-            snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "offline");
+            snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, STR_OFFLINE);
 
             _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
             uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@@ -213,12 +218,17 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa
             snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%.0f W", totalPower);
             _changed = true;
         } else
-            snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "offline");
+            snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, STR_OFFLINE);
 
         if ((totalPower == 0) && (mEnPowerSave)) {
             _display->fillRect(0, mHeadFootPadding, 200, 200, GxEPD_BLACK);
             _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE);
+            mLogoDisplayed = true;
         } else {
+            if(mLogoDisplayed) {
+                mLogoDisplayed = false;
+                fullRefresh();
+            }
             _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
             x = ((_display->width() - tbw) / 2) - tbx;
             _display->setCursor(x, mHeadFootPadding + tbh + 10);
@@ -268,7 +278,7 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa
             // Inverter online
             _display->setFont(&FreeSans12pt7b);
             y = _display->height() - (mHeadFootPadding + 10);
-            snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, " %d online", isprod);
+            snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, " %d %s", isprod, STR_ONLINE);
             _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
             _display->drawInvertedBitmap(10, y - tbh, myWR, 20, 20, GxEPD_BLACK);
             x = ((_display->width() - tbw - 20) / 2) - tbx;
@@ -305,8 +315,9 @@ void DisplayEPaper::loop(float totalPower, float totalYieldDay, float totalYield
 
 //***************************************************************************
 void DisplayEPaper::tickerSecond() {
-    if(mSecondCnt != 0)
-        mSecondCnt--;
-    refreshLoop();
+    if(RefreshStatus::LOGO_WAIT == mRefreshState) {
+        if(mSecondCnt > 0)
+            mSecondCnt--;
+    }
 }
 #endif  // ESP32
diff --git a/src/plugins/Display/Display_ePaper.h b/src/plugins/Display/Display_ePaper.h
index c26d3b42..2106c492 100644
--- a/src/plugins/Display/Display_ePaper.h
+++ b/src/plugins/Display/Display_ePaper.h
@@ -12,7 +12,11 @@
 #define EPAPER_MAX_TEXT_LEN     35
 
 #include <GxEPD2_BW.h>
+#if defined(SPI_HAL)
+#include "epdHal.h"
+#else
 #include <SPI.h>
+#endif
 
 // FreeFonts from Adafruit_GFX
 #include <Fonts/FreeSans12pt7b.h>
@@ -44,9 +48,9 @@ class DisplayEPaper {
             DONE,
             BLACK,
             WHITE,
-            WAIT,
             PARTITIALS,
-            LOGO
+            LOGO,
+            LOGO_WAIT
         };
 
         uint8_t mDisplayRotation;
@@ -58,8 +62,13 @@ class DisplayEPaper {
         uint32_t* mUtcTs;
         bool mEnPowerSave;
         const char* _version;
-        RefreshStatus mRefreshState, mNextRefreshState;
+        RefreshStatus mRefreshState;
+
         uint8_t mSecondCnt;
+        bool mLogoDisplayed;
+        #if defined(SPI_HAL)
+        epdHal hal;
+        #endif
 };
 
 #endif  // ESP32
diff --git a/src/plugins/Display/epdHal.h b/src/plugins/Display/epdHal.h
new file mode 100644
index 00000000..1718b838
--- /dev/null
+++ b/src/plugins/Display/epdHal.h
@@ -0,0 +1,304 @@
+//-----------------------------------------------------------------------------
+// 2024 Ahoy, https://github.com/lumpapu/ahoy
+// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
+//-----------------------------------------------------------------------------
+
+#ifndef __EPD_HAL_H__
+#define __EPD_HAL_H__
+
+#pragma once
+#include "../../utils/spiPatcher.h"
+#include <esp_rom_gpio.h>
+#include <GxEPD2_BW.h>
+
+
+#define EPD_DEFAULT_SPI_SPEED   4000000 // 4 MHz
+
+class epdHal: public GxEPD2_HalInterface, public SpiPatcherHandle {
+    public:
+        epdHal() {}
+
+        void patch() override {
+            esp_rom_gpio_connect_out_signal(mPinMosi, spi_periph_signal[mHostDevice].spid_out, false, false);
+            esp_rom_gpio_connect_in_signal(mPinBusy, spi_periph_signal[mHostDevice].spiq_in, false);
+            esp_rom_gpio_connect_out_signal(mPinClk, spi_periph_signal[mHostDevice].spiclk_out, false, false);
+        }
+
+        void unpatch() override {
+            esp_rom_gpio_connect_out_signal(mPinMosi, SIG_GPIO_OUT_IDX, false, false);
+            esp_rom_gpio_connect_in_signal(mPinBusy, GPIO_MATRIX_CONST_ZERO_INPUT, false);
+            esp_rom_gpio_connect_out_signal(mPinClk, SIG_GPIO_OUT_IDX, false, false);
+        }
+
+        void init(int8_t mosi, int8_t dc, int8_t sclk, int8_t cs, int8_t rst, int8_t busy, int32_t speed = EPD_DEFAULT_SPI_SPEED) {
+            mPinMosi = static_cast<gpio_num_t>(mosi);
+            mPinDc = static_cast<gpio_num_t>(dc);
+            mPinClk = static_cast<gpio_num_t>(sclk);
+            mPinCs = static_cast<gpio_num_t>(cs);
+            mPinRst = static_cast<gpio_num_t>(rst);
+            mPinBusy = static_cast<gpio_num_t>(busy);
+            mSpiSpeed = speed;
+
+            #if defined(CONFIG_IDF_TARGET_ESP32S3)
+            mHostDevice = SPI3_HOST;
+            #else
+            mHostDevice = (14 == sclk) ? SPI2_HOST : SPI_HOST_OTHER;
+            #endif
+
+            mSpiPatcher = SpiPatcher::getInstance(mHostDevice);
+
+            gpio_reset_pin(mPinMosi);
+            gpio_set_direction(mPinMosi, GPIO_MODE_OUTPUT);
+            gpio_set_level(mPinMosi, 1);
+
+            gpio_reset_pin(mPinClk);
+            gpio_set_direction(mPinClk, GPIO_MODE_OUTPUT);
+            gpio_set_level(mPinClk, 0);
+
+            gpio_reset_pin(mPinCs);
+            spi_device_interface_config_t devcfg = {
+                .command_bits = 0,
+                .address_bits = 0,
+                .dummy_bits = 0,
+                .mode = 0,
+                .duty_cycle_pos = 0,
+                .cs_ena_pretrans = 0,
+                .cs_ena_posttrans = 0,
+                .clock_speed_hz = mSpiSpeed,
+                .input_delay_ns = 0,
+                .spics_io_num = mPinCs,
+                .flags = 0,
+                .queue_size = 1,
+                .pre_cb = nullptr,
+                .post_cb = nullptr
+            };
+            mSpiPatcher->addDevice(mHostDevice, &devcfg, &spi);
+
+            if(GPIO_NUM_NC != mPinRst) {
+                gpio_reset_pin(mPinRst);
+                gpio_set_direction(mPinRst, GPIO_MODE_OUTPUT);
+                gpio_set_level(mPinRst, HIGH);
+            }
+
+            gpio_reset_pin(mPinDc);
+            gpio_set_direction(mPinDc, GPIO_MODE_OUTPUT);
+            gpio_set_level(mPinDc, HIGH);
+
+            //gpio_reset_pin(mPinBusy);
+            //gpio_set_direction(mPinBusy, GPIO_MODE_INPUT);
+        }
+
+        void rstMode(uint8_t mode) override {
+            if(GPIO_NUM_NC != mPinRst)
+                gpio_set_direction(mPinRst, static_cast<gpio_mode_t>(mode));
+        }
+
+        void rst(bool level) override {
+            if(GPIO_NUM_NC != mPinRst)
+                gpio_set_level(mPinRst, level);
+        }
+
+        int getBusy(void) override {
+            return gpio_get_level(mPinBusy);
+        }
+
+        bool isRst(void) override {
+            return (GPIO_NUM_NC != mPinRst);
+        }
+
+        void write(uint8_t buf) override {
+            uint8_t data[1];
+            data[0] = buf;
+            request_spi();
+
+            size_t spiLen = static_cast<size_t>(1u) << 3;
+            spi_transaction_t t = {
+                .flags = 0,
+                .cmd = 0,
+                .addr = 0,
+                .length = spiLen,
+                .rxlength = spiLen,
+                .user = NULL,
+                .tx_buffer = data,
+                .rx_buffer = data
+            };
+            ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t));
+
+            release_spi();
+        }
+
+        void write(const uint8_t *buf, uint16_t n) override {
+            uint8_t data[n];
+            std::copy(&buf[0], &buf[n], &data[0]);
+
+            request_spi();
+
+            size_t spiLen = static_cast<size_t>(n) << 3;
+            spi_transaction_t t = {
+                .flags = 0,
+                .cmd = 0,
+                .addr = 0,
+                .length = spiLen,
+                .rxlength = spiLen,
+                .user = NULL,
+                .tx_buffer = data,
+                .rx_buffer = data
+            };
+            ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t));
+
+            release_spi();
+        }
+
+        void write(const uint8_t *buf, uint16_t n, int16_t fill_with_zeroes) override {
+            uint8_t data[n + fill_with_zeroes];
+            memset(data, 0, (n + fill_with_zeroes));
+            for (uint16_t i = 0; i < n; i++) {
+                data[i] = pgm_read_byte(&*buf++);
+            }
+
+            request_spi();
+            spi_transaction_t t = {
+                .flags = SPI_TRANS_CS_KEEP_ACTIVE,
+                .cmd = 0,
+                .addr = 0,
+                .length = 1u,
+                .rxlength = 1u,
+                .user = NULL,
+                .tx_buffer = data,
+                .rx_buffer = data
+            };
+
+            size_t offs = 0;
+            spi_device_acquire_bus(spi, portMAX_DELAY);
+            while(offs < (n + fill_with_zeroes)) {
+                t.length = (64u << 3);
+                t.rxlength = t.length;
+                t.tx_buffer = &data[offs];
+                offs += 64;
+                ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t));
+            }
+            spi_device_release_bus(spi);
+
+            release_spi();
+        }
+
+        void writeCmd(const uint8_t val) override {
+            uint8_t data[1];
+            data[0] = val;
+
+            request_spi();
+            gpio_set_level(mPinDc, LOW);
+
+            size_t spiLen = static_cast<size_t>(1u) << 3;
+            spi_transaction_t t = {
+                .flags = 0,
+                .cmd = 0,
+                .addr = 0,
+                .length = spiLen,
+                .rxlength = spiLen,
+                .user = NULL,
+                .tx_buffer = data,
+                .rx_buffer = data
+            };
+            ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t));
+            gpio_set_level(mPinDc, HIGH);
+
+            release_spi();
+        }
+
+        void writeCmd(const uint8_t *buf, uint8_t n, bool isPGM) override {
+            uint8_t data[n-1];
+            data[0] = (isPGM) ? pgm_read_byte(&*buf++) : buf[0];
+
+            request_spi();
+            gpio_set_level(mPinDc, LOW);
+            spi_device_acquire_bus(spi, portMAX_DELAY);
+
+            size_t spiLen = static_cast<size_t>(1u) << 3;
+            spi_transaction_t t = {
+                .flags = SPI_TRANS_CS_KEEP_ACTIVE,
+                .cmd = 0,
+                .addr = 0,
+                .length = spiLen,
+                .rxlength = spiLen,
+                .user = NULL,
+                .tx_buffer = data,
+                .rx_buffer = data
+            };
+            ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t));
+            gpio_set_level(mPinDc, HIGH);
+
+            if(isPGM) {
+                for (uint16_t i = 0; i < n; i++) {
+                    data[i] = pgm_read_byte(&*buf++);
+                }
+            } else
+                std::copy(&buf[1], &buf[n], &data[0]);
+
+            spiLen = static_cast<size_t>(n-1) << 3;
+            spi_transaction_t t1 = {
+                .flags = SPI_TRANS_CS_KEEP_ACTIVE,
+                .cmd = 0,
+                .addr = 0,
+                .length = spiLen,
+                .rxlength = spiLen,
+                .user = NULL,
+                .tx_buffer = data,
+                .rx_buffer = data
+            };
+            ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t1));
+            spi_device_release_bus(spi);
+
+            release_spi();
+        }
+
+        void startTransfer(void) override {
+            request_spi();
+        }
+
+        void endTransfer(void) override {
+            release_spi();
+        }
+
+        void transfer(const uint8_t val) override {
+            uint8_t data[1];
+            data[0] = val;
+
+            size_t spiLen = static_cast<size_t>(1u) << 3;
+            spi_transaction_t t = {
+                .flags = 0,
+                .cmd = 0,
+                .addr = 0,
+                .length = spiLen,
+                .rxlength = spiLen,
+                .user = NULL,
+                .tx_buffer = data,
+                .rx_buffer = data
+            };
+            ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t));
+        }
+
+    private:
+        inline void request_spi() {
+            mSpiPatcher->request(this);
+        }
+
+        inline void release_spi() {
+            mSpiPatcher->release();
+        }
+
+    private:
+        gpio_num_t mPinMosi = GPIO_NUM_NC;
+        gpio_num_t mPinDc = GPIO_NUM_NC;
+        gpio_num_t mPinClk = GPIO_NUM_NC;
+        gpio_num_t mPinCs = GPIO_NUM_NC;
+        gpio_num_t mPinRst = GPIO_NUM_NC;
+        gpio_num_t mPinBusy = GPIO_NUM_NC;
+        int32_t mSpiSpeed = EPD_DEFAULT_SPI_SPEED;
+
+        spi_host_device_t mHostDevice;
+        spi_device_handle_t spi;
+        SpiPatcher *mSpiPatcher;
+};
+
+#endif /*__EPD_HAL_H__*/
diff --git a/src/plugins/MaxPower.h b/src/plugins/MaxPower.h
new file mode 100644
index 00000000..68665d61
--- /dev/null
+++ b/src/plugins/MaxPower.h
@@ -0,0 +1,67 @@
+//-----------------------------------------------------------------------------
+// 2024 Ahoy, https://github.com/lumpapu/ahoy
+// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
+//-----------------------------------------------------------------------------
+
+#ifndef __MAX_VALUE__
+#define __MAX_VALUE__
+#pragma once
+
+#include <array>
+#include <utility>
+#include "../hm/hmDefines.h"
+
+template<class T=float>
+class MaxPower {
+    public:
+        MaxPower() {
+            mTs = nullptr;
+            mMaxDiff = 60;
+            reset();
+        }
+
+        void setup(uint32_t *ts, uint16_t interval) {
+            mTs = ts;
+            mMaxDiff = interval * 4;
+        }
+
+        void reset(void) {
+            mValues.fill(std::make_pair(0, 0.0));
+            mLast = 0.0;
+        }
+
+        void payloadEvent(uint8_t cmd, Inverter<> *iv) {
+            if(RealTimeRunData_Debug != cmd)
+                return;
+
+            if(nullptr == iv)
+                return;
+
+            if(iv->id >= MAX_NUM_INVERTERS)
+                return;
+
+            record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
+            mValues[iv->id] = std::make_pair(*mTs, iv->getChannelFieldValue(CH0, FLD_PAC, rec));
+        }
+
+        T getTotalMaxPower(void) {
+            T val = 0;
+            for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
+                if((mValues[i].first + mMaxDiff) >= *mTs)
+                    val += mValues[i].second;
+                else if(mValues[i].first > 0)
+                    break; // old data
+            }
+            if(val > mLast)
+                mLast = val;
+            return mLast;
+        }
+
+    private:
+        uint32_t *mTs;
+        uint32_t mMaxDiff;
+        float mLast;
+        std::array<std::pair<uint32_t, T>, MAX_NUM_INVERTERS> mValues;
+};
+
+#endif
diff --git a/src/plugins/history.h b/src/plugins/history.h
index 5076e295..bffbc6d5 100644
--- a/src/plugins/history.h
+++ b/src/plugins/history.h
@@ -17,6 +17,7 @@
 
 enum class HistoryStorageType : uint8_t {
     POWER,
+    POWER_DAY,
     YIELD
 };
 
@@ -27,12 +28,14 @@ class HistoryData {
             uint16_t refreshCycle = 0;
             uint16_t loopCnt = 0;
             uint16_t listIdx = 0; // index for next Element to write into WattArr
-            uint16_t dispIdx = 0; // index for 1st Element to display from WattArr
-            bool wrapped = false;
             // ring buffer for watt history
             std::array<uint16_t, (HISTORY_DATA_ARR_LENGTH + 1)> data;
 
-            storage_t() { data.fill(0); }
+            void reset() {
+                loopCnt = 0;
+                listIdx = 0;
+                data.fill(0);
+            }
         };
 
     public:
@@ -43,33 +46,52 @@ class HistoryData {
             mTs = ts;
 
             mCurPwr.refreshCycle = mConfig->inst.sendInterval;
-            //mYieldDay.refreshCycle = 60;
+            mCurPwrDay.refreshCycle = mConfig->inst.sendInterval;
+            #if defined(ENABLE_HISTORY_YIELD_PER_DAY)
+            mYieldDay.refreshCycle = 60;
+            #endif
+            mLastValueTs = 0;
+            mPgPeriod=0;
+            mMaximumDay = 0;
         }
 
         void tickerSecond() {
-            ;
             float curPwr = 0;
-            float maxPwr = 0;
             float yldDay = -0.1;
+            uint32_t ts = 0;
+
             for (uint8_t i = 0; i < mSys->getNumInverters(); i++) {
                 Inverter<> *iv = mSys->getInverterByPos(i);
-                record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
                 if (iv == NULL)
                     continue;
+                record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
                 curPwr += iv->getChannelFieldValue(CH0, FLD_PAC, rec);
-                maxPwr += iv->getChannelFieldValue(CH0, FLD_MP, rec);
                 yldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
+                if (rec->ts > ts)
+                    ts = rec->ts;
             }
 
             if ((++mCurPwr.loopCnt % mCurPwr.refreshCycle) == 0) {
                 mCurPwr.loopCnt = 0;
-                if (curPwr > 0)
+                if (curPwr > 0) {
+                    mLastValueTs = ts;
                     addValue(&mCurPwr, roundf(curPwr));
-                if (maxPwr > 0)
-                    mMaximumDay = roundf(maxPwr);
+                    if (curPwr > mMaximumDay)
+                        mMaximumDay = roundf(curPwr);
+                }
             }
 
-            /*if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) {
+            if ((++mCurPwrDay.loopCnt % mCurPwrDay.refreshCycle) == 0) {
+                mCurPwrDay.loopCnt = 0;
+                if (curPwr > 0) {
+                    mLastValueTs = ts;
+                    addValueDay(&mCurPwrDay, roundf(curPwr));
+                }
+            }
+
+            #if defined(ENABLE_HISTORY_YIELD_PER_DAY)
+            if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) {
+                mYieldDay.loopCnt = 0;
                 if (*mTs > mApp->getSunset()) {
                     if ((!mDayStored) && (yldDay > 0)) {
                         addValue(&mYieldDay, roundf(yldDay));
@@ -77,28 +99,172 @@ class HistoryData {
                     }
                 } else if (*mTs > mApp->getSunrise())
                     mDayStored = false;
-            }*/
+            }
+            #endif
         }
 
         uint16_t valueAt(HistoryStorageType type, uint16_t i) {
-            //storage_t *s = (HistoryStorageType::POWER == type) ? &mCurPwr : &mYieldDay;
-            storage_t *s = &mCurPwr;
-            uint16_t idx = (s->dispIdx + i) % HISTORY_DATA_ARR_LENGTH;
-            return s->data[idx];
+            storage_t *s = nullptr;
+            uint16_t idx=i;
+            DPRINTLN(DBG_VERBOSE, F("valueAt ") + String((uint8_t)type) + " i=" + String(i));
+
+            switch (type) {
+                default:
+                    [[fallthrough]];
+                case HistoryStorageType::POWER:
+                    s = &mCurPwr;
+                    idx = (s->listIdx + i) % HISTORY_DATA_ARR_LENGTH;
+                    break;
+                case HistoryStorageType::POWER_DAY:
+                    s = &mCurPwrDay;
+                    break;
+                #if defined(ENABLE_HISTORY_YIELD_PER_DAY)
+                case HistoryStorageType::YIELD:
+                    s = &mYieldDay;
+                    idx = (s->listIdx + i) % HISTORY_DATA_ARR_LENGTH;
+                    break;
+                #endif
+            }
+
+            return (nullptr == s) ? 0 : s->data[idx];
         }
 
         uint16_t getMaximumDay() {
             return mMaximumDay;
         }
 
+        uint32_t getLastValueTs(HistoryStorageType type) {
+            DPRINTLN(DBG_VERBOSE, F("getLastValueTs ") + String((uint8_t)type));
+            if (type == HistoryStorageType::POWER_DAY)
+                return mPgEndTime;
+            return mLastValueTs;
+        }
+
+        uint32_t getPeriod(HistoryStorageType type) {
+            DPRINTLN(DBG_VERBOSE, F("getPeriode ") + String((uint8_t)type));
+            switch (type) {
+                case HistoryStorageType::POWER:
+                    return mCurPwr.refreshCycle;
+                    break;
+                case HistoryStorageType::POWER_DAY:
+                    return mPgPeriod / HISTORY_DATA_ARR_LENGTH;
+                    break;
+                case HistoryStorageType::YIELD:
+                    return (60 * 60 * 24);  // 1 day
+                    break;
+            }
+            return 0;
+        }
+
+        bool isDataValid(void) {
+            return ((0 != mPgStartTime) && (0 != mPgEndTime));
+        }
+
+        #if defined(ENABLE_HISTORY_LOAD_DATA)
+        void addValue(HistoryStorageType historyType, uint8_t valueType, uint32_t value) {
+            if (valueType < 2) {
+                storage_t *s = NULL;
+                switch (historyType) {
+                    default:
+                        [[fallthrough]];
+                    case HistoryStorageType::POWER:
+                        s = &mCurPwr;
+                        break;
+                    case HistoryStorageType::POWER_DAY:
+                        s = &mCurPwrDay;
+                        break;
+                    #if defined(ENABLE_HISTORY_YIELD_PER_DAY)
+                    case HistoryStorageType::YIELD:
+                        s = &mYieldDay;
+                        break;
+                    #endif
+                }
+                if (s) {
+                    if (0 == valueType)
+                        addValue(s, value);
+                    else {
+                        if (historyType == HistoryStorageType::POWER)
+                            s->refreshCycle = value;
+                        if (historyType == HistoryStorageType::POWER_DAY)
+                            mPgPeriod = value * HISTORY_DATA_ARR_LENGTH;
+                    }
+                }
+                return;
+            }
+            if (2 == valueType) {
+                if (historyType == HistoryStorageType::POWER)
+                    mLastValueTs = value;
+                if (historyType == HistoryStorageType::POWER_DAY)
+                    mPgEndTime = value;
+            }
+        }
+        #endif
+
     private:
         void addValue(storage_t *s, uint16_t value) {
-            if (s->wrapped) // after 1st time array wrap we have to increase the display index
-                s->dispIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH);
             s->data[s->listIdx] = value;
             s->listIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH);
-            if (s->listIdx == 0)
-                s->wrapped = true;
+        }
+
+        void addValueDay(storage_t *s, uint16_t value) {
+            DPRINTLN(DBG_VERBOSE, F("addValueDay ") + String(value));
+            bool storeStartEndTimes = false;
+            bool store_entry = false;
+            uint32_t pGraphStartTime = mApp->getSunrise();
+            uint32_t pGraphEndTime = mApp->getSunset();
+            uint32_t utcTs = mApp->getTimestamp();
+            switch (mPgState) {
+                case PowerGraphState::NO_TIME_SYNC:
+                    if ((pGraphStartTime > 0)
+                        && (pGraphEndTime > 0)      // wait until period data is available ...
+                        && (utcTs >= pGraphStartTime)
+                        && (utcTs < pGraphEndTime)) // and current time is in period
+                    {
+                        storeStartEndTimes = true;  // period was received -> store
+                        store_entry = true;
+                        mPgState = PowerGraphState::IN_PERIOD;
+                    }
+                    break;
+                case PowerGraphState::IN_PERIOD:
+                    if (utcTs > mPgEndTime)                             // check if end of day is reached ...
+                        mPgState = PowerGraphState::WAIT_4_NEW_PERIOD;  // then wait for new period setting
+                    else
+                        store_entry = true;
+                    break;
+                case PowerGraphState::WAIT_4_NEW_PERIOD:
+                    if ((mPgStartTime != pGraphStartTime) || (mPgEndTime != pGraphEndTime)) { // wait until new time period was received ...
+                        storeStartEndTimes = true;                                            // and store it for next period
+                        mPgState = PowerGraphState::WAIT_4_RESTART;
+                    }
+                    break;
+                case PowerGraphState::WAIT_4_RESTART:
+                    if ((utcTs >= mPgStartTime) && (utcTs < mPgEndTime)) { // wait until current time is in period again ...
+                        mCurPwrDay.reset();                                // then reset power graph data
+                        store_entry = true;
+                        mPgState = PowerGraphState::IN_PERIOD;
+                        mCurPwr.reset(); // also reset "last values" graph
+                        mMaximumDay = 0; // and the maximum of the (last) day
+                    }
+                    break;
+            }
+
+            // store start and end times of current time period and calculate period length
+            if (storeStartEndTimes) {
+                mPgStartTime = pGraphStartTime;
+                mPgEndTime = pGraphEndTime;
+                mPgPeriod = pGraphEndTime - pGraphStartTime;  // time period of power graph in sec for scaling of x-axis
+            }
+
+            if (store_entry) {
+                DPRINTLN(DBG_VERBOSE, F("addValueDay store_entry") + String(value));
+                if (mPgPeriod) {
+                    uint16_t pgPos = (utcTs - mPgStartTime) * (HISTORY_DATA_ARR_LENGTH - 1) / mPgPeriod;
+                    s->listIdx = std::min(pgPos, (uint16_t)(HISTORY_DATA_ARR_LENGTH - 1));
+                } else
+                    s->listIdx = 0;
+                DPRINTLN(DBG_VERBOSE, F("addValueDay store_entry idx=") + String(s->listIdx));
+                s->data[s->listIdx] = std::max(s->data[s->listIdx], value);  // update current datapoint to maximum of all seen values
+            }
         }
 
     private:
@@ -109,8 +275,23 @@ class HistoryData {
         uint32_t *mTs = nullptr;
 
         storage_t mCurPwr;
+        storage_t mCurPwrDay;
+        #if defined(ENABLE_HISTORY_YIELD_PER_DAY)
+        storage_t mYieldDay;
+        #endif
         bool mDayStored = false;
         uint16_t mMaximumDay = 0;
+        uint32_t mLastValueTs = 0;
+        enum class PowerGraphState {
+            NO_TIME_SYNC,
+            IN_PERIOD,
+            WAIT_4_NEW_PERIOD,
+            WAIT_4_RESTART
+        };
+        PowerGraphState mPgState = PowerGraphState::NO_TIME_SYNC;
+        uint32_t mPgStartTime = 0;
+        uint32_t mPgEndTime = 0;
+        uint32_t mPgPeriod = 0;  // seconds
 };
 
 #endif /*ENABLE_HISTORY*/
diff --git a/src/plugins/plugin_lang.h b/src/plugins/plugin_lang.h
new file mode 100644
index 00000000..8d7a987f
--- /dev/null
+++ b/src/plugins/plugin_lang.h
@@ -0,0 +1,44 @@
+//-----------------------------------------------------------------------------
+// 2024 Ahoy, https://ahoydtu.de
+// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
+//-----------------------------------------------------------------------------
+
+#ifndef __PLUGIN_LANG_H__
+#define __PLUGIN_LANG_H__
+
+#ifdef LANG_DE
+    #define STR_MONTHNAME_3_CHAR_LIST "ErrJanFebMrzAprMaiJunJulAugSepOktNovDez"
+    #define STR_DAYNAME_3_CHAR_LIST   "ErrSonMonDieMitDonFreSam"
+    #define STR_OFFLINE               "aus"
+    #define STR_ONLINE                "aktiv"
+    #define STR_NO_INVERTER           "kein inverter"
+    #define STR_NO_WIFI               "WLAN nicht verbunden"
+    #define STR_VERSION               "Version"
+    #define STR_ACTIVE_INVERTERS      "aktive WR"
+    #define STR_TODAY                 "heute"
+    #define STR_TOTAL                 "Gesamt"
+#elif LANG_FR
+    #define STR_MONTHNAME_3_CHAR_LIST "ErrJanFevMarAvrMaiJunJulAouSepOctNovDec"
+    #define STR_DAYNAME_3_CHAR_LIST   "ErrDimLunMarMerJeuVenSam"
+    #define STR_OFFLINE               "eteint"
+    #define STR_ONLINE                "online"
+    #define STR_NO_INVERTER           "pas d'onduleur"
+    #define STR_NO_WIFI               "WiFi not connected"
+    #define STR_VERSION               "Version"
+    #define STR_ACTIVE_INVERTERS      "active Inv"
+    #define STR_TODAY                 "today"
+    #define STR_TOTAL                 "total"
+#else
+    #define STR_MONTHNAME_3_CHAR_LIST "ErrJanFebMarAprMayJunJulAugSepOctNovDec"
+    #define STR_DAYNAME_3_CHAR_LIST   "ErrSunMonTueWedThuFriSat"
+    #define STR_OFFLINE               "offline"
+    #define STR_ONLINE                "online"
+    #define STR_NO_INVERTER           "no inverter"
+    #define STR_NO_WIFI               "WiFi not connected"
+    #define STR_VERSION               "Version"
+    #define STR_ACTIVE_INVERTERS      "active Inv"
+    #define STR_TODAY                 "today"
+    #define STR_TOTAL                 "total"
+#endif
+
+#endif /*__PLUGIN_LANG_H__*/
diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h
index 369d9ead..6e021bed 100644
--- a/src/publisher/pubMqtt.h
+++ b/src/publisher/pubMqtt.h
@@ -11,6 +11,8 @@
 #if defined(ENABLE_MQTT)
 #ifdef ESP8266
     #include <ESP8266WiFi.h>
+    #define xSemaphoreTake(a, b) { while(a) { yield(); } a = true; }
+    #define xSemaphoreGive(a) { a = false; }
 #elif defined(ESP32)
     #include <WiFi.h>
 #endif
@@ -39,6 +41,13 @@ template<class HMSYSTEM>
 class PubMqtt {
     public:
         PubMqtt() : SendIvData() {
+            #if defined(ESP32)
+                mutex = xSemaphoreCreateBinaryStatic(&mutexBuffer);
+                xSemaphoreGive(mutex);
+            #else
+                mutex = false;
+            #endif
+
             mLastIvState.fill(InverterStatus::OFF);
             mIvLastRTRpub.fill(0);
 
@@ -50,9 +59,14 @@ class PubMqtt {
             mSendAlarm.fill(false);
         }
 
-        ~PubMqtt() { }
+        ~PubMqtt() {
+            #if defined(ESP32)
+            vSemaphoreDelete(mutex);
+            #endif
+        }
 
-        void setup(cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs, uint32_t *uptime) {
+        void setup(IApp *app, cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs, uint32_t *uptime) {
+            mApp             = app;
             mCfgMqtt         = cfg_mqtt;
             mDevName         = devName;
             mVersion         = version;
@@ -61,7 +75,7 @@ class PubMqtt {
             mUptime          = uptime;
             mIntervalTimeout = 1;
 
-            SendIvData.setup(sys, utcTs, &mSendList);
+            SendIvData.setup(app, sys, cfg_mqtt, utcTs, &mSendList);
             SendIvData.setPublishFunc([this](const char *subTopic, const char *payload, bool retained, uint8_t qos) {
                 publish(subTopic, payload, retained, true, qos);
             });
@@ -95,6 +109,17 @@ class PubMqtt {
         }
 
         void loop() {
+            std::queue<message_s> queue;
+            xSemaphoreTake(mutex, portMAX_DELAY);
+            queue.swap(mReceiveQueue);
+            xSemaphoreGive(mutex);
+
+            while (!queue.empty()) {
+                message_s *entry = &queue.front();
+                handleMessage(entry->topic, entry->payload, entry->len, entry->index, entry->total);
+                queue.pop();
+            }
+
             SendIvData.loop();
 
             #if defined(ESP8266)
@@ -204,6 +229,9 @@ class PubMqtt {
             else
                 snprintf(mTopic.data(), mTopic.size(), "%s", subTopic);
 
+            if(!mCfgMqtt->enableRetain)
+                retained = false;
+
             mClient.publish(mTopic.data(), qos, retained, payload);
             yield();
             mTxCnt++;
@@ -242,7 +270,8 @@ class PubMqtt {
         void setPowerLimitAck(Inverter<> *iv) {
             if (NULL != iv) {
                 snprintf(mSubTopic.data(), mSubTopic.size(), "%s/%s", iv->config->name, subtopics[MQTT_ACK_PWR_LMT]);
-                publish(mSubTopic.data(), "true", true, true, QOS_2);
+                snprintf(mVal.data(), mVal.size(), "%.1f", iv->powerLimit[0]/10.0);
+                publish(mSubTopic.data(), mVal.data(), true, true, QOS_2);
             }
         }
 
@@ -250,16 +279,15 @@ class PubMqtt {
         void onConnect(bool sessionPreset) {
             DPRINTLN(DBG_INFO, F("MQTT connected"));
 
-            publish(subtopics[MQTT_VERSION], mVersion, true);
-            publish(subtopics[MQTT_DEVICE], mDevName, true);
-            #if defined(ETHERNET)
-            publish(subtopics[MQTT_IP_ADDR], ETH.localIP().toString().c_str(), true);
-            #else
-            publish(subtopics[MQTT_IP_ADDR], WiFi.localIP().toString().c_str(), true);
-            #endif
+            publish(subtopics[MQTT_VERSION], mVersion, false);
+            publish(subtopics[MQTT_DEVICE], mDevName, false);
+            publish(subtopics[MQTT_IP_ADDR], mApp->getIp().c_str(), true);
             tickerMinute();
             publish(mLwtTopic.data(), mqttStr[MQTT_STR_LWT_CONN], true, false);
 
+            snprintf(mVal.data(), mVal.size(), "ctrl/restart_ahoy");
+            subscribe(mVal.data());
+
             for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
                 snprintf(mVal.data(), mVal.size(), "ctrl/limit/%d", i);
                 subscribe(mVal.data(), QOS_2);
@@ -300,6 +328,14 @@ class PubMqtt {
         void onMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) {
             if(len == 0)
                 return;
+
+            xSemaphoreTake(mutex, portMAX_DELAY);
+            mReceiveQueue.push(message_s(topic, payload, len, index, total));
+            xSemaphoreGive(mutex);
+
+        }
+
+        inline void handleMessage(const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) {
             DPRINT(DBG_INFO, mqttStr[MQTT_STR_GOT_TOPIC]);
             DBGPRINTLN(String(topic));
             if(NULL == mSubscriptionCb)
@@ -353,9 +389,9 @@ class PubMqtt {
                 pos++;
             }
 
-            /*char out[128];
+            char out[128];
             serializeJson(root, out, 128);
-            DPRINTLN(DBG_INFO, "json: " + String(out));*/
+            DPRINTLN(DBG_INFO, "json: " + String(out));
             (mSubscriptionCb)(root);
 
             mRxCnt++;
@@ -397,6 +433,10 @@ class PubMqtt {
                 std::array<char, 32> name;
                 std::array<char, 32> uniq_id;
                 std::array<char, 350> buf;
+                topic.fill(0);
+                name.fill(0);
+                uniq_id.fill(0);
+                buf.fill(0);
                 const char *devCls, *stateCls;
                 if (!total) {
                     if (rec->assign[mDiscovery.sub].ch == CH0)
@@ -419,8 +459,8 @@ class PubMqtt {
                 }
 
                 DynamicJsonDocument doc2(512);
-                constexpr static char* unitTotal[] = {"W", "kWh", "Wh", "W"};
-                doc2[F("name")] = name;
+                constexpr static const char* unitTotal[] = {"W", "kWh", "Wh", "W"};
+                doc2[F("name")] = String(name.data());
                 doc2[F("stat_t")] = String(mCfgMqtt->topic) + "/" + ((!total) ? String(iv->config->name) : "total" ) + String(topic.data());
                 doc2[F("unit_of_meas")] = ((!total) ? (iv->getUnit(mDiscovery.sub, rec)) : (unitTotal[mDiscovery.sub]));
                 doc2[F("uniq_id")] = ((!total) ? (String(iv->config->serial.u64, HEX)) : (node_id)) + "_" + uniq_id.data();
@@ -437,9 +477,9 @@ class PubMqtt {
                 else // total values
                     snprintf(topic.data(), topic.size(), "%s/sensor/%s/total_%s/config", MQTT_DISCOVERY_PREFIX, node_id.c_str(), fields[fldTotal[mDiscovery.sub]]);
                 size_t size = measureJson(doc2) + 1;
-                buf.fill(0);
                 serializeJson(doc2, buf.data(), size);
-                publish(topic.data(), buf.data(), true, false);
+                if(FLD_EVT != rec->assign[mDiscovery.sub].fieldId)
+                    publish(topic.data(), buf.data(), true, false);
 
                 if(++mDiscovery.sub == ((!total) ? (rec->length) : 4)) {
                     mDiscovery.sub = 0;
@@ -559,6 +599,9 @@ class PubMqtt {
         }
 
         void sendData(Inverter<> *iv, uint8_t curInfoCmd) {
+            if (mCfgMqtt->json)
+                return;
+
             record_t<> *rec = iv->getRecordStruct(curInfoCmd);
 
             uint32_t lastTs = iv->getLastTs(rec);
@@ -602,10 +645,80 @@ class PubMqtt {
             mLastAnyAvail = anyAvail;
         }
 
+    private:
+        enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE};
+
+        struct message_s
+        {
+            char *topic;
+            uint8_t *payload;
+            size_t len;
+            size_t index;
+            size_t total;
+
+            message_s() 
+            : topic { nullptr }
+            , payload { nullptr }
+            , len { 0 }
+            , index { 0 }
+            , total { 0 }
+            {}
+
+            message_s(const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
+            {
+                uint8_t topic_len = strlen(topic) + 1;
+                this->topic = new char[topic_len];
+                this->payload = new uint8_t[len];
+
+                memcpy(this->topic, topic, topic_len);
+                memcpy(this->payload, payload, len);
+                this->len = len;
+                this->index = index;
+                this->total = total;
+            }
+
+            message_s(const message_s &) = delete;
+
+            message_s(message_s && other) : message_s {}
+            {
+                this->swap( other );
+            }
+
+            ~message_s()
+            {
+                delete[] this->topic;
+                delete[] this->payload;
+            }
+
+            message_s  &operator = (const message_s &) = delete;
+
+            message_s  &operator = (message_s &&other)
+            {
+                this->swap(other);
+                return *this;
+            }
+
+            void swap(message_s &other)
+            {
+                std::swap(this->topic, other.topic);
+                std::swap(this->payload, other.payload);
+                std::swap(this->len, other.len);
+                std::swap(this->index, other.index);
+                std::swap(this->total, other.total);
+            }
+
+        };
+
+    private:
         espMqttClient mClient;
         cfgMqtt_t *mCfgMqtt = nullptr;
+        IApp *mApp;
         #if defined(ESP8266)
         WiFiEventHandler mHWifiCon, mHWifiDiscon;
+        volatile bool mutex;
+        #else
+        SemaphoreHandle_t mutex;
+        StaticSemaphore_t mutexBuffer;
         #endif
 
         HMSYSTEM *mSys = nullptr;
@@ -621,6 +734,8 @@ class PubMqtt {
         std::array<uint32_t, MAX_NUM_INVERTERS> mIvLastRTRpub;
         uint16_t mIntervalTimeout = 0;
 
+        std::queue<message_s> mReceiveQueue;
+
         // last will topic and payload must be available through lifetime of 'espMqttClient'
         std::array<char, (MQTT_TOPIC_LEN + 5)> mLwtTopic;
         const char *mDevName = nullptr, *mVersion = nullptr;
diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h
index c3ae3814..5ba21f6d 100644
--- a/src/publisher/pubMqttIvData.h
+++ b/src/publisher/pubMqttIvData.h
@@ -24,8 +24,10 @@ class PubMqttIvData {
     public:
         PubMqttIvData() : mTotal{}, mSubTopic{}, mVal{} {}
 
-        void setup(HMSYSTEM *sys, uint32_t *utcTs, std::queue<sendListCmdIv> *sendList) {
+        void setup(IApp *app, HMSYSTEM *sys, cfgMqtt_t *cfg_mqtt, uint32_t *utcTs, std::queue<sendListCmdIv> *sendList) {
+            mApp           = app;
             mSys           = sys;
+            mCfg           = cfg_mqtt;
             mUtcTimestamp  = utcTs;
             mSendList      = sendList;
             mState         = IDLE;
@@ -75,6 +77,7 @@ class PubMqttIvData {
             mTotalFound = false;
             mSendTotalYd = true;
             mAllTotalFound = true;
+            mAtLeastOneWasntSent = false;
             if(!mSendList->empty()) {
                 mCmd = mSendList->front().cmd;
                 mIvSend = mSendList->front().iv;
@@ -113,7 +116,7 @@ class PubMqttIvData {
                     mPublish(mSubTopic.data(), mVal.data(), true, QOS_0);
 
                     if((mIv->ivGen == IV_HMS) || (mIv->ivGen == IV_HMT)) {
-                        snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch0/rssi", mIv->config->name);
+                        snprintf(mSubTopic.data(), mSubTopic.size(), "%s/rssi", mIv->config->name);
                         snprintf(mVal.data(), mVal.size(), "%d", mIv->rssi);
                         mPublish(mSubTopic.data(), mVal.data(), false, QOS_0);
                     }
@@ -122,7 +125,7 @@ class PubMqttIvData {
 
                 mIv->isProducing(); // recalculate status
                 mState = SEND_DATA;
-            } else if(mSendTotals && mTotalFound) {
+            } else if(mSendTotals && mTotalFound && mAtLeastOneWasntSent) {
                 if(mYldTotalStore > mTotal[2])
                     mSendTotalYd = false; // don't send yield total if last value was greater
                 else
@@ -167,9 +170,6 @@ class PubMqttIvData {
                                 case FLD_PDC:
                                     mTotal[3] += mIv->getValue(mPos, rec);
                                     break;
-                                case FLD_MP:
-                                    mTotal[4] += mIv->getValue(mPos, rec);
-                                    break;
                             }
                         } else
                             mAllTotalFound = false;
@@ -177,6 +177,7 @@ class PubMqttIvData {
                 }
 
                 if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) {
+                    mAtLeastOneWasntSent = true;
                     if(InverterDevInform_All == mCmd) {
                         snprintf(mSubTopic.data(), mSubTopic.size(), "%s/firmware", mIv->config->name);
                         snprintf(mVal.data(), mVal.size(), "{\"version\":%d,\"build_year\":\"%d\",\"build_month_day\":%d,\"build_hour_min\":%d,\"bootloader\":%d}",
@@ -185,7 +186,6 @@ class PubMqttIvData {
                             static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_FW_BUILD_MONTH_DAY, rec)),
                             static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_FW_BUILD_HOUR_MINUTE, rec)),
                             static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_BOOTLOADER_VER, rec)));
-                        retained = true;
                     } else if(InverterDevInform_Simple == mCmd) {
                         snprintf(mSubTopic.data(), mSubTopic.size(), "%s/hardware", mIv->config->name);
                         snprintf(mVal.data(), mVal.size(), "{\"part\":%d,\"version\":\"%d\",\"grid_profile_code\":%d,\"grid_profile_version\":%d}",
@@ -193,20 +193,54 @@ class PubMqttIvData {
                             static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_HW_VERSION, rec)),
                             static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec)),
                             static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_GRID_PROFILE_VERSION, rec)));
-                        retained = true;
                     } else {
-                        snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]);
-                        snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mIv->getValue(mPos, rec)));
+                        if (!mCfg->json) {
+                            snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]);
+                            snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mIv->getValue(mPos, rec)));
+                        } else {
+                            if (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) {
+                                uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0;
+                                snprintf(mSubTopic.data(), mSubTopic.size(), "%s/%s", mIv->config->name, fields[rec->assign[mPos].fieldId]);
+                                snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mIv->getValue(mPos, rec)));
+                                mPublish(mSubTopic.data(), mVal.data(), retained, qos);
+                            }
+                        }
                     }
 
-                    uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0;
-                    if((FLD_EVT != rec->assign[mPos].fieldId)
-                        && (FLD_LAST_ALARM_CODE != rec->assign[mPos].fieldId))
-                        mPublish(mSubTopic.data(), mVal.data(), retained, qos);
+                    if ((InverterDevInform_All == mCmd) || (InverterDevInform_Simple == mCmd) || !mCfg->json)
+                    {
+                        uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0;
+                        if((FLD_EVT != rec->assign[mPos].fieldId) && (FLD_LAST_ALARM_CODE != rec->assign[mPos].fieldId))
+                            mPublish(mSubTopic.data(), mVal.data(), retained, qos);
+                    }
                 }
                 mPos++;
             } else {
                 if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) {
+                    if (mCfg->json && (RealTimeRunData_Debug == mCmd)) {
+                        DynamicJsonDocument doc(300);
+
+                        for (mPos = 0; mPos < rec->length; mPos++) {
+                            doc[fields[rec->assign[mPos].fieldId]] = ah::round3(mIv->getValue(mPos, rec));
+
+                            bool publish = false;
+                            if (mPos != (rec->length - 1)) { // not last one
+                                if (rec->assign[mPos].ch != rec->assign[mPos+1].ch)
+                                    publish = true;
+                            } else
+                                publish = true;
+
+                            if (publish) {
+                                doc[F("ts")] = rec->ts;
+                                // if next channel or end->publish
+                                serializeJson(doc, mVal.data(), mVal.size());
+                                snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch%d", mIv->config->name, rec->assign[mPos].ch);
+                                mPublish(mSubTopic.data(), mVal.data(), false, QOS_0);
+                                doc.clear();
+                            }
+                        }
+                    }
+
                     sendRadioStat(rec->length);
                     rec->mqttSentStatus = MqttSentStatus::DATA_SENT;
                 }
@@ -220,7 +254,7 @@ class PubMqttIvData {
                 mIv->radioStatistics.txCnt,
                 mIv->radioStatistics.rxSuccess,
                 mIv->radioStatistics.rxFail,
-                mIv->radioStatistics.rxFailNoAnser,
+                mIv->radioStatistics.rxFailNoAnswer,
                 mIv->radioStatistics.retransmits,
                 mIv->radioStatistics.ivLoss,
                 mIv->radioStatistics.ivSent,
@@ -261,19 +295,40 @@ class PubMqttIvData {
                     case 4:
                         fieldId = FLD_MP;
                         retained = false;
+                        mTotal[4] = mApp->getTotalMaxPower();
                         break;
                 }
-                snprintf(mSubTopic.data(), mSubTopic.size(), "total/%s", fields[fieldId]);
-                snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mTotal[mPos]));
-                mPublish(mSubTopic.data(), mVal.data(), retained, QOS_0);
+                if (!mCfg->json) {
+                    snprintf(mSubTopic.data(), mSubTopic.size(), "total/%s", fields[fieldId]);
+                    snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mTotal[mPos]));
+                    mPublish(mSubTopic.data(), mVal.data(), retained, QOS_0);
+                }
                 mPos++;
             } else {
+                if (mCfg->json) {
+                    int type[5] = {FLD_PAC, FLD_YT, FLD_YD, FLD_PDC, FLD_MP};
+                    snprintf(mVal.data(), mVal.size(), "{");
+
+                    for (mPos = 0; mPos < 5; mPos++) {
+                        snprintf(mSubTopic.data(), mSubTopic.size(), "\"%s\":%g", fields[type[mPos]], ah::round3(mTotal[mPos]));
+                        strcat(mVal.data(), mSubTopic.data());
+                        if (mPos < 4)
+                            strcat(mVal.data(), ",");
+                        else
+                            strcat(mVal.data(), "}");
+                    }
+                    mPublish("total", mVal.data(), true, QOS_0);
+                }
                 mSendList->pop();
                 mSendTotals = false;
                 mState = IDLE;
             }
         }
 
+    private:
+        IApp *mApp = nullptr;
+        cfgMqtt_t *mCfg = nullptr;
+
         HMSYSTEM *mSys = nullptr;
         uint32_t *mUtcTimestamp = nullptr;
         pubMqttPublisherType mPublish;
@@ -282,7 +337,8 @@ class PubMqttIvData {
 
         uint8_t mCmd = 0;
         uint8_t mLastIvId = 0;
-        bool mSendTotals = false, mTotalFound = false, mAllTotalFound = false, mSendTotalYd = false;
+        bool mSendTotals = false, mTotalFound = false, mAllTotalFound = false;
+        bool mSendTotalYd = false, mAtLeastOneWasntSent = false;
         float mTotal[5], mYldTotalStore = 0;
 
         Inverter<> *mIv = nullptr, *mIvSend = nullptr;
@@ -290,7 +346,7 @@ class PubMqttIvData {
         bool mRTRDataHasBeenSent = false;
 
         std::array<char, (32 + MAX_NAME_LENGTH + 1)> mSubTopic;
-        std::array<char, 140> mVal;
+        std::array<char, 300> mVal;
 
         std::queue<sendListCmdIv> *mSendList = nullptr;
 };
diff --git a/src/utils/dbg.h b/src/utils/dbg.h
index 9e754ba6..f6dd8012 100644
--- a/src/utils/dbg.h
+++ b/src/utils/dbg.h
@@ -110,7 +110,7 @@
 
 #if DEBUG_LEVEL >= DBG_ERROR
     #define PERR(str) DBGPRINT(F("E: ")); DBGPRINT(str);
-    #define PERRLN(str) DBGPRINT(F("E: ")); DBGPRINTLN(str);
+    #define PERRLN(str) DBGPRINT(F("E: ")); DBGPRINTLN(str); DSERIAL.flush();
 #else
     #define PERR(str)
     #define PERRLN(str)
diff --git a/src/utils/helper.cpp b/src/utils/helper.cpp
index 24a4d9ee..edb9b9b9 100644
--- a/src/utils/helper.cpp
+++ b/src/utils/helper.cpp
@@ -5,6 +5,12 @@
 
 #include "helper.h"
 #include "dbg.h"
+#include "../plugins/plugin_lang.h"
+
+#define dt_SHORT_STR_LEN_i18n  3 // the length of short strings
+static char buffer_i18n[dt_SHORT_STR_LEN_i18n + 1];  // must be big enough for longest string and the terminating null
+const char monthShortNames_P[] PROGMEM = STR_MONTHNAME_3_CHAR_LIST;
+const char dayShortNames_P[] PROGMEM = STR_DAYNAME_3_CHAR_LIST;
 
 namespace ah {
     void ip2Arr(uint8_t ip[], const char *ipStr) {
@@ -28,6 +34,10 @@ namespace ah {
             snprintf(str, 16, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
     }
 
+    double round1(double value) {
+        return (int)(value * 10 + 0.5) / 10.0;
+    }
+
     double round3(double value) {
         return (int)(value * 1000 + 0.5) / 1000.0;
     }
@@ -82,6 +92,31 @@ namespace ah {
         return String(str);
     }
 
+    static char* monthShortStr_i18n(uint8_t month) {
+        for (int i=0; i < dt_SHORT_STR_LEN_i18n; i++)
+            buffer_i18n[i] = pgm_read_byte(&(monthShortNames_P[i + month * dt_SHORT_STR_LEN_i18n]));
+        buffer_i18n[dt_SHORT_STR_LEN_i18n] = 0;
+        return buffer_i18n;
+    }
+
+    static char* dayShortStr_i18n(uint8_t day) {
+        for (int i=0; i < dt_SHORT_STR_LEN_i18n; i++)
+            buffer_i18n[i] = pgm_read_byte(&(dayShortNames_P[i + day * dt_SHORT_STR_LEN_i18n]));
+        buffer_i18n[dt_SHORT_STR_LEN_i18n] = 0;
+        return buffer_i18n;
+    }
+
+    String getDateTimeStrShort_i18n(time_t t) {
+        char str[20];
+        if(0 == t)
+            sprintf(str, "n/a");
+        else {
+            sprintf(str, "%3s ", dayShortStr_i18n(dayOfWeek(t)));
+            sprintf(str+4, "%2d.%3s %02d:%02d", day(t), monthShortStr_i18n(month(t)), hour(t), minute(t));
+        }
+        return String(str);
+    }
+
     uint64_t Serial2u64(const char *val) {
         char tmp[3];
         uint64_t ret = 0ULL;
diff --git a/src/utils/helper.h b/src/utils/helper.h
index 1dbba3d9..ff1a9aed 100644
--- a/src/utils/helper.h
+++ b/src/utils/helper.h
@@ -39,9 +39,11 @@ static Timezone gTimezone(CEST, CET);
 namespace ah {
     void ip2Arr(uint8_t ip[], const char *ipStr);
     void ip2Char(uint8_t ip[], char *str);
+    double round1(double value);
     double round3(double value);
     String getDateTimeStr(time_t t);
     String getDateTimeStrShort(time_t t);
+    String getDateTimeStrShort_i18n(time_t t);
     String getDateTimeStrFile(time_t t);
     String getTimeStr(time_t t);
     String getTimeStrMs(uint64_t t);
diff --git a/src/utils/improv.h b/src/utils/improv.h
index 20b2bcad..d2ccc0c3 100644
--- a/src/utils/improv.h
+++ b/src/utils/improv.h
@@ -147,10 +147,12 @@ class Improv {
         }
 
         void getNetworks(void) {
-            if(!mScanRunning)
-                mApp->scanAvailNetworks();
-
             JsonObject obj;
+            if(!mScanRunning) {
+                mApp->getAvailNetworks(obj);
+                return;
+            }
+
             if(!mApp->getAvailNetworks(obj))
                 return;
 
diff --git a/src/utils/scheduler.h b/src/utils/scheduler.h
index 16009778..5ce43a36 100644
--- a/src/utils/scheduler.h
+++ b/src/utils/scheduler.h
@@ -35,14 +35,21 @@ namespace ah {
                 mMax        = 0;
                 mPrevMillis = millis();
                 mTsMillis   = mPrevMillis % 1000;
+                mFastTicker = false;
                 resetTicker();
             }
 
             virtual void loop(void) {
                 mMillis = millis();
                 mDiff = mMillis - mPrevMillis;
-                if (mDiff < 1000)
+                if (mDiff < 1000) {
+                    if (mFastTicker) {
+                        mDiffSeconds = 0;
+                        checkTicker();
+                        mFastTicker = false;
+                    }
                     return;
+                }
 
                 mDiffSeconds = 1;
                 if (mDiff < 2000)
@@ -125,8 +132,10 @@ namespace ah {
                         mTicker[i].timeout = timeout;
                         mTicker[i].reload = reload;
                         mTicker[i].isTimestamp = isTimestamp;
-                        memset(mTicker[i].name, 0, 6);
-                        strncpy(mTicker[i].name, name, (strlen(name) < 6) ? strlen(name) : 5);
+                        strncpy(mTicker[i].name, name, 5);
+                        mTicker[i].name[5]=0;
+                        if (timeout == 0 && reload == false)
+                            mFastTicker = true;
                         if(mMax == i)
                             mMax = i + 1;
                         return i;
@@ -162,6 +171,7 @@ namespace ah {
             uint32_t mMillis = 0, mPrevMillis = 0, mDiff = 0;
             uint8_t mDiffSeconds = 0;
             uint8_t mMax = 0;
+            bool mFastTicker;
     };
 }
 
diff --git a/src/utils/spiPatcher.cpp b/src/utils/spiPatcher.cpp
index 3b7b5681..b3d27482 100644
--- a/src/utils/spiPatcher.cpp
+++ b/src/utils/spiPatcher.cpp
@@ -5,5 +5,6 @@
 
 #if defined(ESP32)
 #include "spiPatcher.h"
-SpiPatcher *SpiPatcher::mInstance = nullptr;
+SpiPatcher *SpiPatcher::InstanceHost2 = nullptr;
+SpiPatcher *SpiPatcher::InstanceHost3 = nullptr;
 #endif
diff --git a/src/utils/spiPatcher.h b/src/utils/spiPatcher.h
index 210b2a09..c8f0ba3c 100644
--- a/src/utils/spiPatcher.h
+++ b/src/utils/spiPatcher.h
@@ -9,23 +9,38 @@
 
 #if defined(ESP32)
 
+#include "dbg.h"
 #include "spiPatcherHandle.h"
 
 #include <driver/spi_master.h>
 #include <freertos/semphr.h>
 
+#if (SOC_SPI_PERIPH_NUM > 2)
+    #define SPI_HOST_OTHER SPI3_HOST
+#else
+    #define SPI_HOST_OTHER SPI2_HOST
+#endif
+
 class SpiPatcher {
     protected:
         explicit SpiPatcher(spi_host_device_t dev) :
-            mHostDevice(dev), mCurHandle(nullptr) {
+            mCurHandle(nullptr) {
             // Use binary semaphore instead of mutex for performance reasons
             mutex = xSemaphoreCreateBinaryStatic(&mutex_buffer);
             xSemaphoreGive(mutex);
+            mDev = dev;
+            mBusState = ESP_FAIL;
+        }
 
-            spi_bus_config_t buscfg = {
-                .mosi_io_num = -1,
-                .miso_io_num = -1,
-                .sclk_io_num = -1,
+    public:
+        SpiPatcher(const SpiPatcher &other) = delete;
+        void operator=(const SpiPatcher &) = delete;
+
+        esp_err_t initBus(int mosi = -1, int miso = -1, int sclk = -1, spi_common_dma_t dmaType = SPI_DMA_DISABLED) {
+            mBusConfig = spi_bus_config_t {
+                .mosi_io_num = mosi,
+                .miso_io_num = miso,
+                .sclk_io_num = sclk,
                 .quadwp_io_num = -1,
                 .quadhd_io_num = -1,
                 .data4_io_num = -1,
@@ -36,26 +51,48 @@ class SpiPatcher {
                 .flags = 0,
                 .intr_flags = 0
             };
-            ESP_ERROR_CHECK(spi_bus_initialize(mHostDevice, &buscfg, SPI_DMA_DISABLED));
-        }
+            ESP_ERROR_CHECK((mBusState = spi_bus_initialize(mDev, &mBusConfig, dmaType)));
 
-    public:
-        SpiPatcher(SpiPatcher &other) = delete;
-        void operator=(const SpiPatcher &) = delete;
+            return mBusState;
+        }
 
-        static SpiPatcher* getInstance(spi_host_device_t dev) {
-            if(nullptr == mInstance)
-                mInstance = new SpiPatcher(dev);
-            return mInstance;
+        static SpiPatcher* getInstance(spi_host_device_t dev, bool initialize = true) {
+            if(SPI2_HOST == dev) {
+                if(nullptr == InstanceHost2) {
+                    InstanceHost2 = new SpiPatcher(dev);
+                    if(initialize)
+                        InstanceHost2->initBus();
+                }
+                return InstanceHost2;
+            } else { // SPI3_HOST
+                if(nullptr == InstanceHost3) {
+                    InstanceHost3 = new SpiPatcher(dev);
+                    if(initialize)
+                        InstanceHost3->initBus();
+                }
+                return InstanceHost3;
+            }
         }
 
         ~SpiPatcher() { vSemaphoreDelete(mutex); }
 
-        spi_host_device_t getDevice() {
-            return mHostDevice;
+        inline void addDevice(spi_host_device_t host_id, const spi_device_interface_config_t *dev_config, spi_device_handle_t *handle) {
+            assert(mBusState == ESP_OK);
+            if(SPI2_HOST == host_id)
+                mHost2Cnt++;
+            #if (SOC_SPI_PERIPH_NUM > 2)
+            if(SPI3_HOST == host_id)
+                mHost3Cnt++;
+            #endif
+
+            if((mHost2Cnt > 3) || (mHost3Cnt > 3))
+                DPRINTLN(DBG_ERROR, F("maximum number of SPI devices reached (3)"));
+
+            ESP_ERROR_CHECK(spi_bus_add_device(host_id, dev_config, handle));
         }
 
         inline void request(SpiPatcherHandle* handle) {
+            assert(mBusState == ESP_OK);
             xSemaphoreTake(mutex, portMAX_DELAY);
 
             if (mCurHandle != handle) {
@@ -70,17 +107,22 @@ class SpiPatcher {
         }
 
         inline void release() {
+            assert(mBusState == ESP_OK);
             xSemaphoreGive(mutex);
         }
 
     protected:
-        static SpiPatcher *mInstance;
+        static SpiPatcher *InstanceHost2;
+        static SpiPatcher *InstanceHost3;
 
     private:
-        const spi_host_device_t mHostDevice;
         SpiPatcherHandle* mCurHandle;
         SemaphoreHandle_t mutex;
         StaticSemaphore_t mutex_buffer;
+        uint8_t mHost2Cnt = 0, mHost3Cnt = 0;
+        spi_host_device_t mDev = SPI2_HOST;
+        esp_err_t mBusState = ESP_FAIL;
+        spi_bus_config_t mBusConfig;
 };
 
 #endif /*ESP32*/
diff --git a/src/web/Protection.h b/src/web/Protection.h
index 74f04b52..e41249ac 100644
--- a/src/web/Protection.h
+++ b/src/web/Protection.h
@@ -24,7 +24,7 @@ class Protection {
         }
 
     public:
-        Protection(Protection &other) = delete;
+        Protection(const Protection &other) = delete;
         void operator=(const Protection &) = delete;
 
         static Protection* getInstance(const char *pwd) {
diff --git a/src/web/RestApi.h b/src/web/RestApi.h
index 09bba06e..eac91cb9 100644
--- a/src/web/RestApi.h
+++ b/src/web/RestApi.h
@@ -17,11 +17,7 @@
 #include "../utils/helper.h"
 #include "lang.h"
 #include "AsyncJson.h"
-#if defined(ETHERNET)
-#include "AsyncWebServer_ESP32_W5500.h"
-#else
 #include "ESPAsyncWebServer.h"
-#endif
 
 #include "plugins/history.h"
 
@@ -44,16 +40,24 @@ class RestApi {
             mApp      = app;
             mSrv      = srv;
             mSys      = sys;
-            mRadioNrf = (HmRadio<>*)mApp->getRadioObj(true);
+            mRadioNrf = (NrfRadio<>*)mApp->getRadioObj(true);
             #if defined(ESP32)
             mRadioCmt = (CmtRadio<>*)mApp->getRadioObj(false);
             #endif
             mConfig   = config;
+            #if defined(ENABLE_HISTORY_LOAD_DATA)
+            mSrv->on("/api/addYDHist",
+                             HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1),
+                                        std::bind(&RestApi::onApiPostYDHist,this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
+            #endif
             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("/api", HTTP_GET,  std::bind(&RestApi::onApi,         this, std::placeholders::_1));
 
             mSrv->on("/get_setup", HTTP_GET,  std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1));
+            #if defined(ESP32)
+            mSrv->on("/coredump", HTTP_GET,  std::bind(&RestApi::getCoreDump, this, std::placeholders::_1));
+            #endif
         }
 
         uint32_t getTimezoneOffset(void) {
@@ -77,9 +81,19 @@ class RestApi {
             #ifndef ESP32
             mHeapFreeBlk = ESP.getMaxFreeBlockSize();
             mHeapFrag = ESP.getHeapFragmentation();
+            #else
+            mHeapFreeBlk = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
+            if(mHeapFree > 0)
+                mHeapFrag = 100 - ((mHeapFreeBlk * 100) / mHeapFree);
+            else
+                mHeapFrag = 0;
             #endif
 
+            #if defined(ESP32)
+            AsyncJsonResponse* response = new AsyncJsonResponse(false, 8000);
+            #else
             AsyncJsonResponse* response = new AsyncJsonResponse(false, 6000);
+            #endif
             JsonObject root = response->getRoot();
 
             String path = request->url().substring(5);
@@ -97,12 +111,13 @@ class RestApi {
             else if(path == "inverter/list")  getInverterList(root);
             else if(path == "index")          getIndex(request, root);
             else if(path == "setup")          getSetup(request, root);
-            #if !defined(ETHERNET)
             else if(path == "setup/networks") getNetworks(root);
-            else if(path == "setup/getip")    getWifiIp(root);
-            #endif /* !defined(ETHERNET) */
+            else if(path == "setup/getip")    getIp(root);
             else if(path == "live")           getLive(request,root);
-            else if (path == "powerHistory")  getPowerHistory(request, root);
+            #if defined(ENABLE_HISTORY)
+            else if (path == "powerHistory")  getPowerHistory(request, root, HistoryStorageType::POWER);
+            else if (path == "powerHistoryDay")  getPowerHistory(request, root, HistoryStorageType::POWER_DAY);
+            #endif /*ENABLE_HISTORY*/
             else {
                 if(path.substring(0, 12) == "inverter/id/")
                     getInverter(root, request->url().substring(17).toInt());
@@ -137,11 +152,98 @@ class RestApi {
             #endif
         }
 
-        void onApiPostBody(AsyncWebServerRequest *request, const uint8_t *data, size_t len, size_t index, size_t total) {
+        #if defined(ENABLE_HISTORY_LOAD_DATA)
+        // VArt67: For debugging history graph. Loading data into graph
+        void onApiPostYDHist(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, size_t final) {
+            uint32_t total = request->contentLength();
+            DPRINTLN(DBG_DEBUG, "[onApiPostYieldDHistory ] " + filename + " index:" + index + " len:" + len + " total:" + total + " final:" + final);
+
+            if (0 == index) {
+                if (NULL != mTmpBuf)
+                    delete[] mTmpBuf;
+                mTmpBuf = new uint8_t[total + 1];
+                mTmpSize = total;
+            }
+            if (mTmpSize >= (len + index))
+                memcpy(&mTmpBuf[index], data, len);
+
+            if (!final)
+                return;  // not last frame - nothing to do
+
+            mTmpSize = len + index;  // correct the total size
+            mTmpBuf[mTmpSize] = 0;
+
+            #ifndef ESP32
+            DynamicJsonDocument json(ESP.getMaxFreeBlockSize() - 512);  // need some memory on heap
+            #else
+            DynamicJsonDocument json(12000);  // does this work? I have no ESP32 :-(
+            #endif
+            DeserializationError err = deserializeJson(json, static_cast<const char *>(mTmpBuf, mTmpSize));
+            json.shrinkToFit();
+            JsonObject obj = json.as<JsonObject>();
+
+            // Debugging
+            // mTmpBuf[mTmpSize] = 0;
+            // DPRINTLN(DBG_DEBUG, (const char *)mTmpBuf);
+
+            if (!err && obj) {
+                // insert data into yieldDayHistory object
+                HistoryStorageType dataType;
+                if (obj["maxDay"] > 0)  // this is power history data
+                {
+                    dataType = HistoryStorageType::POWER;
+                    if (obj["refresh"] > 60)
+                        dataType = HistoryStorageType::POWER_DAY;
+
+                }
+                else
+                    dataType = HistoryStorageType::YIELD;
+
+                size_t cnt = obj[F("value")].size();
+                DPRINTLN(DBG_DEBUG, "ArraySize: " + String(cnt));
+
+                for (uint16_t i = 0; i < cnt; i++) {
+                    uint16_t val = obj[F("value")][i];
+                    mApp->addValueToHistory((uint8_t)dataType, 0, val);
+                    // DPRINT(DBG_VERBOSE, "value " + String(i) + ": " + String(val) + ", ");
+                }
+                uint32_t refresh = obj[F("refresh")];
+                mApp->addValueToHistory((uint8_t)dataType, 1, refresh);
+                if (dataType != HistoryStorageType::YIELD) {
+                    uint32_t ts = obj[F("lastValueTs")];
+                    mApp->addValueToHistory((uint8_t)dataType, 2, ts);
+                }
+
+            } else {
+                switch (err.code()) {
+                    case DeserializationError::Ok:
+                        break;
+                    case DeserializationError::IncompleteInput:
+                        DPRINTLN(DBG_DEBUG, F("Incomplete input"));
+                        break;
+                    case DeserializationError::InvalidInput:
+                        DPRINTLN(DBG_DEBUG, F("Invalid input"));
+                        break;
+                    case DeserializationError::NoMemory:
+                        DPRINTLN(DBG_DEBUG, F("Not enough memory ") + String(json.capacity()) + " bytes");
+                        break;
+                    default:
+                        DPRINTLN(DBG_DEBUG, F("Deserialization failed"));
+                        break;
+                }
+            }
+
+            request->send(204);  // Success with no page load
+            delete[] mTmpBuf;
+            mTmpBuf = NULL;
+        }
+        #endif
+
+        void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
             DPRINTLN(DBG_VERBOSE, "onApiPostBody");
 
             if(0 == index) {
-                if(NULL != mTmpBuf)
+                if(nullptr != mTmpBuf)
                     delete[] mTmpBuf;
                 mTmpBuf = new uint8_t[total+1];
                 mTmpSize = total;
@@ -154,36 +256,40 @@ class RestApi {
 
             DynamicJsonDocument json(1000);
 
-            DeserializationError err = deserializeJson(json, reinterpret_cast<const char*>(mTmpBuf), mTmpSize);
-            JsonObject obj = json.as<JsonObject>();
-
             AsyncJsonResponse* response = new AsyncJsonResponse(false, 200);
             JsonObject root = response->getRoot();
-            root[F("success")] = (err) ? false : true;
-            if(!err) {
-                String path = request->url().substring(5);
-                if(path == "ctrl")
-                    root[F("success")] = setCtrl(obj, root, request->client()->remoteIP().toString().c_str());
-                else if(path == "setup")
-                    root[F("success")] = setSetup(obj, root, request->client()->remoteIP().toString().c_str());
-                else {
-                    root[F("success")] = false;
-                    root[F("error")]   = F(PATH_NOT_FOUND) + path;
-                }
-            } else {
-                switch (err.code()) {
-                    case DeserializationError::Ok: break;
-                    case DeserializationError::IncompleteInput: root[F("error")] = F(INCOMPLETE_INPUT); break;
-                    case DeserializationError::InvalidInput:    root[F("error")] = F(INVALID_INPUT);    break;
-                    case DeserializationError::NoMemory:        root[F("error")] = F(NOT_ENOUGH_MEM);   break;
-                    default:                                    root[F("error")] = F(DESER_FAILED);     break;
+            DeserializationError err = deserializeJson(json, reinterpret_cast<const char*>(mTmpBuf), mTmpSize);
+            if(!json.is<JsonObject>())
+                root[F("error")] = F(DESER_FAILED);
+            else {
+                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, request->client()->remoteIP().toString().c_str());
+                    else if(path == "setup")
+                        root[F("success")] = setSetup(obj, root, request->client()->remoteIP().toString().c_str());
+                    else {
+                        root[F("success")] = false;
+                        root[F("error")]   = F(PATH_NOT_FOUND) + path;
+                    }
+                } else {
+                    switch (err.code()) {
+                        case DeserializationError::Ok: break;
+                        case DeserializationError::IncompleteInput: root[F("error")] = F(INCOMPLETE_INPUT); break;
+                        case DeserializationError::InvalidInput:    root[F("error")] = F(INVALID_INPUT);    break;
+                        case DeserializationError::NoMemory:        root[F("error")] = F(NOT_ENOUGH_MEM);   break;
+                        default:                                    root[F("error")] = F(DESER_FAILED);     break;
+                    }
                 }
             }
 
             response->setLength();
             request->send(response);
             delete[] mTmpBuf;
-            mTmpBuf = NULL;
+            mTmpBuf = nullptr;
         }
 
         void getNotFound(JsonObject obj, String url) {
@@ -195,14 +301,13 @@ class RestApi {
             ep[F("generic")]          = url + F("generic");
             ep[F("index")]            = url + F("index");
             ep[F("setup")]            = url + F("setup");
-            #if !defined(ETHERNET)
             ep[F("setup/networks")]   = url + F("setup/networks");
             ep[F("setup/getip")]      = url + F("setup/getip");
-            #endif /* !defined(ETHERNET) */
             ep[F("system")]           = url + F("system");
             ep[F("live")]             = url + F("live");
             #if defined(ENABLE_HISTORY)
             ep[F("powerHistory")]     = url + F("powerHistory");
+            ep[F("powerHistoryDay")]  = url + F("powerHistoryDay");
             #endif
         }
 
@@ -236,7 +341,7 @@ class RestApi {
                     if(-1 != i) {
                         i+=5;
                         String sn = tmp.substring(i, tmp.indexOf("\"", i)-1);
-                        tmp.replace(sn, String(atoll(sn.c_str()), HEX));
+                        tmp.replace(sn, String(sn) + ",\"note\":\"" + String(atoll(sn.c_str()),  HEX) + "\"");
                     }
                 }
                 response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp);
@@ -252,9 +357,38 @@ class RestApi {
             fp.close();
         }
 
+        #if defined(ESP32)
+        void getCoreDump(AsyncWebServerRequest *request) {
+            const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_COREDUMP, "coredump");
+            if (partition != NULL) {
+                size_t size = partition->size;
+
+                AsyncWebServerResponse *response = request->beginResponse("application/octet-stream", size, [size, partition](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
+                    if((index + maxLen) > size)
+                        maxLen = size - index;
+
+                    if (ESP_OK != esp_partition_read(partition, index, buffer, maxLen))
+                        DPRINTLN(DBG_ERROR, F("can't read partition"));
+
+                    return maxLen;
+                });
+
+                String filename = ah::getDateTimeStrFile(gTimezone.toLocal(mApp->getTimestamp()));
+                filename += "_v" + String(mApp->getVersion());
+                filename += "_" + String(ENV_NAME);
+
+                response->addHeader("Content-Description", "File Transfer");
+                response->addHeader("Content-Disposition", "attachment; filename=" + filename + "_coredump.bin");
+                request->send(response);
+            } else {
+                AsyncWebServerResponse *response = request->beginResponse(200, F("application/json; charset=utf-8"), "{}");
+                request->send(response);
+            }
+        }
+        #endif
+
         void getGeneric(AsyncWebServerRequest *request, JsonObject obj) {
             mApp->resetLockTimeout();
-
             obj[F("wifi_rssi")]   = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI();
             obj[F("ts_uptime")]   = mApp->getUptime();
             obj[F("ts_now")]      = mApp->getTimestamp();
@@ -262,6 +396,7 @@ class RestApi {
             obj[F("modules")]     = String(mApp->getVersionModules());
             obj[F("build")]       = String(AUTO_GIT_HASH);
             obj[F("env")]         = String(ENV_NAME);
+            obj[F("host")]        = mConfig->sys.deviceName;
             obj[F("menu_prot")]   = mApp->isProtected(request->client()->remoteIP().toString().c_str(), "", true);
             obj[F("menu_mask")]   = (uint16_t)(mConfig->sys.protectionMask );
             obj[F("menu_protEn")] = (bool) (mConfig->sys.adminPwd[0] != '\0');
@@ -278,67 +413,38 @@ class RestApi {
         }
 
         void getSysInfo(AsyncWebServerRequest *request, JsonObject obj) {
-            #if !defined(ETHERNET)
-            obj[F("ssid")]         = mConfig->sys.stationSsid;
-            obj[F("ap_pwd")]       = mConfig->sys.apPwd;
-            obj[F("hidd")]         = mConfig->sys.isHidden;
-            #endif /* !defined(ETHERNET) */
             obj[F("device_name")]  = mConfig->sys.deviceName;
             obj[F("dark_mode")]    = (bool)mConfig->sys.darkMode;
             obj[F("sched_reboot")] = (bool)mConfig->sys.schedReboot;
 
-            obj[F("mac")]          = WiFi.macAddress();
-            obj[F("hostname")]     = mConfig->sys.deviceName;
             obj[F("pwd_set")]      = (strlen(mConfig->sys.adminPwd) > 0);
             obj[F("prot_mask")]    = mConfig->sys.protectionMask;
 
-            obj[F("sdk")]          = ESP.getSdkVersion();
-            obj[F("cpu_freq")]     = ESP.getCpuFreqMHz();
-            obj[F("heap_free")]    = mHeapFree;
-            obj[F("sketch_total")] = ESP.getFreeSketchSpace();
-            obj[F("sketch_used")]  = ESP.getSketchSize() / 1024; // in kb
-            getGeneric(request, obj);
-
+            getGeneric(request, obj.createNestedObject(F("generic")));
+            getChipInfo(obj.createNestedObject(F("chip")));
             getRadioNrf(obj.createNestedObject(F("radioNrf")));
+            getMqttInfo(obj.createNestedObject(F("mqtt")));
+            getNetworkInfo(obj.createNestedObject(F("network")));
+            getMemoryInfo(obj.createNestedObject(F("memory")));
             #if defined(ESP32)
             getRadioCmtInfo(obj.createNestedObject(F("radioCmt")));
             #endif
-            getMqttInfo(obj.createNestedObject(F("mqtt")));
-
-        #if defined(ESP32)
-            obj[F("chip_revision")] = ESP.getChipRevision();
-            obj[F("chip_model")]    = ESP.getChipModel();
-            obj[F("chip_cores")]    = ESP.getChipCores();
-            obj[F("heap_total")]    = ESP.getHeapSize();
-            //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("heap_frag")]     = mHeapFrag;
-            obj[F("max_free_blk")]  = mHeapFreeBlk;
-            obj[F("core_version")]  = ESP.getCoreVersion();
-            obj[F("flash_size")]    = ESP.getFlashChipRealSize() / 1024; // in kb
-            obj[F("reboot_reason")] = ESP.getResetReason();
-        #endif
-            //obj[F("littlefs_total")] = LittleFS.totalBytes();
-            //obj[F("littlefs_used")] = LittleFS.usedBytes();
 
-            uint8_t max;
+            /*uint8_t max;
             mApp->getSchedulerInfo(&max);
-            obj[F("schMax")] = max;
+            obj[F("schMax")] = max;*/
         }
 
         void getHtmlSystem(AsyncWebServerRequest *request, JsonObject obj) {
             getSysInfo(request, obj.createNestedObject(F("system")));
             getGeneric(request, obj.createNestedObject(F("generic")));
+            #if defined(ESP32)
+            char tmp[300];
+            snprintf(tmp, 300, "<a href=\"/factory\" class=\"btn\">%s</a><br/><br/><a href=\"/reboot\" class=\"btn\">%s</a><br/><br/><a href=\"/coredump\" class=\"btn\">%s</a>", FACTORY_RESET, BTN_REBOOT, BTN_COREDUMP);
+            #else
             char tmp[200];
             snprintf(tmp, 200, "<a href=\"/factory\" class=\"btn\">%s</a><br/><br/><a href=\"/reboot\" class=\"btn\">%s</a>", FACTORY_RESET, BTN_REBOOT);
+            #endif
             obj[F("html")] = String(tmp);
         }
 
@@ -351,8 +457,8 @@ class RestApi {
 
         void getHtmlReboot(AsyncWebServerRequest *request, JsonObject obj) {
             getGeneric(request, obj.createNestedObject(F("generic")));
-            #if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3)
-            obj[F("refresh")] = 5;
+            #if defined(ETHERNET)
+            obj[F("refresh")] = (mConfig->sys.eth.enabled) ? 5 : 20;
             #else
             obj[F("refresh")] = 20;
             #endif
@@ -365,8 +471,8 @@ class RestApi {
             obj[F("pending")] = (bool)mApp->getSavePending();
             obj[F("success")] = (bool)mApp->getLastSaveSucceed();
             obj[F("reboot")] = (bool)mApp->getShouldReboot();
-            #if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3)
-            obj[F("reload")] = 5;
+            #if defined(ETHERNET)
+            obj[F("reload")] = (mConfig->sys.eth.enabled) ? 5 : 20;
             #else
             obj[F("reload")] = 20;
             #endif
@@ -383,7 +489,7 @@ class RestApi {
             mApp->setRebootFlag();
             obj[F("html")] = F("Erase settings: success");
             #if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3)
-            obj[F("reload")] = 5;
+            obj[F("reload")] = (mConfig->sys.eth.enabled) ? 5 : 20;
             #else
             obj[F("reload")] = 20;
             #endif
@@ -391,7 +497,9 @@ class RestApi {
 
         void getHtmlFactory(AsyncWebServerRequest *request, JsonObject obj) {
             getGeneric(request, obj.createNestedObject(F("generic")));
-            obj[F("html")] = F("Factory reset? <a class=\"btn\" href=\"/factorytrue\">yes</a> <a class=\"btn\" href=\"/\">no</a>");
+            char tmp[200];
+            snprintf(tmp, 200, "%s <a class=\"btn\" href=\"/factorytrue\">%s</a> <a class=\"btn\" href=\"/\">%s</a>", FACTORY_RESET, BTN_YES, BTN_NO);
+            obj[F("html")] = tmp;
         }
 
         void getHtmlFactoryTrue(AsyncWebServerRequest *request, JsonObject obj) {
@@ -399,8 +507,8 @@ class RestApi {
             mApp->eraseSettings(true);
             mApp->setRebootFlag();
             obj[F("html")] = F("Factory reset: success");
-            #if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3)
-            obj[F("reload")] = 5;
+            #if defined(ETHERNET)
+            obj[F("reload")] = (mConfig->sys.eth.enabled) ? 5 : 20;
             #else
             obj[F("reload")] = 20;
             #endif
@@ -422,7 +530,7 @@ class RestApi {
             obj[F("name")]           = String(iv->config->name);
             obj[F("rx_success")]     = iv->radioStatistics.rxSuccess;
             obj[F("rx_fail")]        = iv->radioStatistics.rxFail;
-            obj[F("rx_fail_answer")] = iv->radioStatistics.rxFailNoAnser;
+            obj[F("rx_fail_answer")] = iv->radioStatistics.rxFailNoAnswer;
             obj[F("frame_cnt")]      = iv->radioStatistics.frmCnt;
             obj[F("tx_cnt")]         = iv->radioStatistics.txCnt;
             obj[F("retransmits")]    = iv->radioStatistics.retransmits;
@@ -474,13 +582,13 @@ class RestApi {
             }
             obj[F("interval")]          = String(mConfig->inst.sendInterval);
             obj[F("max_num_inverters")] = MAX_NUM_INVERTERS;
-            obj[F("rstMid")]            = (bool)mConfig->inst.rstYieldMidNight;
+            obj[F("rstMid")]            = (bool)mConfig->inst.rstValsAtMidNight;
             obj[F("rstNotAvail")]       = (bool)mConfig->inst.rstValsNotAvail;
             obj[F("rstComStop")]        = (bool)mConfig->inst.rstValsCommStop;
+            obj[F("rstComStart")]       = (bool)mConfig->inst.rstValsCommStart;
             obj[F("strtWthtTm")]        = (bool)mConfig->inst.startWithoutTime;
             obj[F("rdGrid")]            = (bool)mConfig->inst.readGrid;
-            obj[F("rstMaxMid")]         = (bool)mConfig->inst.rstMaxValsMidNight;
-            obj[F("yldEff")]            = mConfig->inst.yieldEffiency;
+            obj[F("rstMaxMid")]         = (bool)mConfig->inst.rstIncludeMaxVals;
         }
 
         void getInverter(JsonObject obj, uint8_t id) {
@@ -496,7 +604,7 @@ class RestApi {
             obj[F("name")]             = String(iv->config->name);
             obj[F("serial")]           = String(iv->config->serial.u64, HEX);
             obj[F("version")]          = String(iv->getFwVersion());
-            obj[F("power_limit_read")] = iv->actPowerLimit;
+            obj[F("power_limit_read")] = ah::round1(iv->getChannelFieldValue(CH0, FLD_ACT_ACTIVE_PWR_LIMIT, iv->getRecordStruct(SystemConfigPara)));
             obj[F("power_limit_ack")]  = iv->powerLimitAck;
             obj[F("max_pwr")]          = iv->getMaxPower();
             obj[F("ts_last_success")]  = rec->ts;
@@ -505,6 +613,7 @@ class RestApi {
             obj[F("alarm_cnt")]        = iv->alarmCnt;
             obj[F("rssi")]             = iv->rssi;
             obj[F("ts_max_ac_pwr")]    = iv->tsMaxAcPower;
+            obj[F("ts_max_temp")]      = iv->tsMaxTemperature;
 
             JsonArray ch = obj.createNestedArray("ch");
 
@@ -561,11 +670,23 @@ class RestApi {
             obj[F("last_id")] = iv->getChannelFieldValue(CH0, FLD_EVT, rec);
 
             JsonArray alarm = obj.createNestedArray(F("alarm"));
+
+            // find oldest alarm
+            uint8_t offset = 0;
+            uint32_t oldestStart = 0xffffffff;
             for(uint8_t i = 0; i < 10; i++) {
-                alarm[i][F("code")]  = iv->lastAlarm[i].code;
-                alarm[i][F("str")]   = iv->getAlarmStr(iv->lastAlarm[i].code);
-                alarm[i][F("start")] = iv->lastAlarm[i].start;
-                alarm[i][F("end")]   = iv->lastAlarm[i].end;
+                if((iv->lastAlarm[i].start != 0) && (iv->lastAlarm[i].start < oldestStart)) {
+                    offset = i;
+                    oldestStart = iv->lastAlarm[i].start;
+                }
+            }
+
+            for(uint8_t i = 0; i < 10; i++) {
+                uint8_t pos = (i + offset) % 10;
+                alarm[pos][F("code")]  = iv->lastAlarm[pos].code;
+                alarm[pos][F("str")]   = iv->getAlarmStr(iv->lastAlarm[pos].code);
+                alarm[pos][F("start")] = iv->lastAlarm[pos].start;
+                alarm[pos][F("end")]   = iv->lastAlarm[pos].end;
             }
         }
 
@@ -612,7 +733,9 @@ class RestApi {
             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);
+            obj[F("json")]       = (bool) mConfig->mqtt.json;
             obj[F("interval")]   = String(mConfig->mqtt.interval);
+            obj[F("retain")]     = (bool)mConfig->mqtt.enableRetain;
         }
 
         void getNtp(JsonObject obj) {
@@ -665,6 +788,116 @@ class RestApi {
         }
         #endif
 
+        #if defined(ETHERNET)
+        void getEthernet(JsonObject obj) {
+            obj[F("en")]           = mConfig->sys.eth.enabled;
+            obj[F("cs")]           = mConfig->sys.eth.pinCs;
+            obj[F("sclk")]         = mConfig->sys.eth.pinSclk;
+            obj[F("miso")]         = mConfig->sys.eth.pinMiso;
+            obj[F("mosi")]         = mConfig->sys.eth.pinMosi;
+            obj[F("irq")]          = mConfig->sys.eth.pinIrq;
+            obj[F("reset")]        = mConfig->sys.eth.pinRst;
+        }
+        #endif
+
+        void getNetworkInfo(JsonObject obj) {
+            #if defined(ETHERNET)
+            bool isWired = mApp->isWiredConnection();
+            if(!isWired)
+                obj[F("wifi_channel")] = WiFi.channel();
+
+            obj[F("wired")] = isWired;
+            #else
+                obj[F("wired")]        = false;
+                obj[F("wifi_channel")] = WiFi.channel();
+            #endif
+
+            obj[F("ap_pwd")] = mConfig->sys.apPwd;
+            obj[F("ssid")]   = mConfig->sys.stationSsid;
+            obj[F("hidd")]   = mConfig->sys.isHidden;
+            obj[F("mac")]    = mApp->getMac();
+            obj[F("ip")]     = mApp->getIp();
+        }
+
+        void getChipInfo(JsonObject obj) {
+            obj[F("cpu_freq")]     = ESP.getCpuFreqMHz();
+            obj[F("sdk")]          = ESP.getSdkVersion();
+            #if defined(ESP32)
+                obj[F("revision")] = ESP.getChipRevision();
+                obj[F("model")]    = ESP.getChipModel();
+                obj[F("cores")]    = ESP.getChipCores();
+
+                switch (esp_reset_reason()) {
+                    default:
+                        [[fallthrough]];
+                    case ESP_RST_UNKNOWN:
+                        obj[F("reboot_reason")] = F("Unknown");
+                        break;
+                    case ESP_RST_POWERON:
+                        obj[F("reboot_reason")] = F("Power on");
+                        break;
+                    case ESP_RST_EXT:
+                        obj[F("reboot_reason")] = F("External");
+                        break;
+                    case ESP_RST_SW:
+                        obj[F("reboot_reason")] = F("Software");
+                        break;
+                    case ESP_RST_PANIC:
+                        obj[F("reboot_reason")] = F("Panic");
+                        break;
+                    case ESP_RST_INT_WDT:
+                        obj[F("reboot_reason")] = F("Interrupt Watchdog");
+                        break;
+                    case ESP_RST_TASK_WDT:
+                        obj[F("reboot_reason")] = F("Task Watchdog");
+                        break;
+                    case ESP_RST_WDT:
+                        obj[F("reboot_reason")] = F("Watchdog");
+                        break;
+                    case ESP_RST_DEEPSLEEP:
+                        obj[F("reboot_reason")] = F("Deepsleep");
+                        break;
+                    case ESP_RST_BROWNOUT:
+                        obj[F("reboot_reason")] = F("Brownout");
+                        break;
+                    case ESP_RST_SDIO:
+                        obj[F("reboot_reason")] = F("SDIO");
+                        break;
+                }
+            #else
+                obj[F("core_version")]  = ESP.getCoreVersion();
+                obj[F("reboot_reason")] = ESP.getResetReason();
+            #endif
+        }
+
+        void getMemoryInfo(JsonObject obj) {
+            obj[F("heap_frag")]         = mHeapFrag;
+            obj[F("heap_max_free_blk")] = mHeapFreeBlk;
+            obj[F("heap_free")]         = mHeapFree;
+
+            obj[F("par_size_app0")] = ESP.getFreeSketchSpace();
+            obj[F("par_used_app0")] = ESP.getSketchSize();
+
+            #if defined(ESP32)
+                obj[F("heap_total")] = ESP.getHeapSize();
+
+                const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_COREDUMP, "coredump");
+                if (partition != NULL)
+                    obj[F("flash_size")] = partition->address + partition->size;
+
+                obj[F("par_size_spiffs")] = LittleFS.totalBytes();
+                obj[F("par_used_spiffs")] = LittleFS.usedBytes();
+            #else
+                obj[F("flash_size")] = ESP.getFlashChipRealSize();
+
+                FSInfo info;
+                LittleFS.info(info);
+                obj[F("par_used_spiffs")] = info.usedBytes;
+                obj[F("par_size_spiffs")] = info.totalBytes;
+                obj[F("heap_total")] = 24*1014; // FIXME: don't know correct value
+            #endif
+        }
+
         void getRadioNrf(JsonObject obj) {
             obj[F("en")] = (bool) mConfig->nrf.enabled;
             if(mConfig->nrf.enabled) {
@@ -752,11 +985,9 @@ class RestApi {
                 warn.add(F(REBOOT_ESP_APPLY_CHANGES));
             if(0 == mApp->getTimestamp())
                 warn.add(F(TIME_NOT_SET));
-            #if !defined(ETHERNET)
-                #if !defined(ESP32)
-                if(mApp->getWasInCh12to14())
-                    warn.add(F(WAS_IN_CH_12_TO_14));
-                #endif
+            #if !defined(ESP32)
+            if(mApp->getWasInCh12to14())
+                warn.add(F(WAS_IN_CH_12_TO_14));
             #endif
         }
 
@@ -771,24 +1002,28 @@ class RestApi {
             #if defined(ESP32)
             getRadioCmt(obj.createNestedObject(F("radioCmt")));
             #endif
+            #if defined(ETHERNET)
+            getEthernet(obj.createNestedObject(F("eth")));
+            #endif
             getRadioNrf(obj.createNestedObject(F("radioNrf")));
             getSerial(obj.createNestedObject(F("serial")));
             getStaticIp(obj.createNestedObject(F("static_ip")));
             getDisplay(obj.createNestedObject(F("display")));
         }
 
-        #if !defined(ETHERNET)
         void getNetworks(JsonObject obj) {
-            mApp->getAvailNetworks(obj);
+            obj[F("success")] = mApp->getAvailNetworks(obj);
+            obj[F("ip")] = mApp->getIp();
         }
-        void getWifiIp(JsonObject obj) {
-            obj[F("ip")] = mApp->getStationIp();
+
+        void getIp(JsonObject obj) {
+            obj[F("ip")] = mApp->getIp();
         }
-        #endif /* !defined(ETHERNET) */
 
         void getLive(AsyncWebServerRequest *request, JsonObject obj) {
             getGeneric(request, obj.createNestedObject(F("generic")));
             obj[F("refresh")] = mConfig->inst.sendInterval;
+            obj[F("max_total_pwr")] = ah::round3(mApp->getTotalMaxPower());
 
             for (uint8_t fld = 0; fld < sizeof(acList); fld++) {
                 obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]);
@@ -809,23 +1044,50 @@ class RestApi {
             }
         }
 
-        void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj) {
+        #if defined(ENABLE_HISTORY)
+        void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj, HistoryStorageType type) {
             getGeneric(request, obj.createNestedObject(F("generic")));
-            #if defined(ENABLE_HISTORY)
-            obj[F("refresh")] = mConfig->inst.sendInterval;
+            obj[F("refresh")] = mApp->getHistoryPeriod(static_cast<uint8_t>(type));
+
             uint16_t max = 0;
             for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) {
-                uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER, fld);
+                uint16_t value = mApp->getHistoryValue(static_cast<uint8_t>(type), fld);
+                obj[F("value")][fld] = value;
+                if (value > max)
+                    max = value;
+            }
+            obj[F("max")] = max;
+
+            if(HistoryStorageType::POWER_DAY == type) {
+                float yldDay = 0;
+                for (uint8_t i = 0; i < mSys->getNumInverters(); i++) {
+                    Inverter<> *iv = mSys->getInverterByPos(i);
+                    if (iv == NULL)
+                        continue;
+                    record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
+                    yldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
+                }
+                obj[F("yld")] = ah::round3(yldDay / 1000.0);
+            }
+
+            obj[F("lastValueTs")] = mApp->getHistoryLastValueTs(static_cast<uint8_t>(type));
+        }
+        #endif /*ENABLE_HISTORY*/
+
+
+        #if defined(ENABLE_HISTORY_YIELD_PER_DAY)
+        void getYieldDayHistory(AsyncWebServerRequest *request, JsonObject obj) {
+            obj[F("refresh")] = mApp->getHistoryPeriod((uint8_t)HistoryStorageType::YIELD);
+            uint16_t max = 0;
+            for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) {
+                uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::YIELD, fld);
                 obj[F("value")][fld] = value;
                 if (value > max)
                     max = value;
             }
             obj[F("max")] = max;
-            obj[F("maxDay")] = mApp->getHistoryMaxDay();
-            #else
-            obj[F("refresh")] = 86400;  // 1 day;
-            #endif /*ENABLE_HISTORY*/
         }
+        #endif /*ENABLE_HISTORY_YIELD_PER_DAY*/
 
         bool setCtrl(JsonObject jsonIn, JsonObject jsonOut, const char *clientIP) {
             if(jsonIn.containsKey(F("auth"))) {
@@ -867,11 +1129,11 @@ class RestApi {
                     iv->powerLimit[1] = AbsolutNonPersistent;
 
                 accepted = iv->setDevControlRequest(ActivePowerContr);
-                if(accepted)
-                    mApp->triggerTickSend();
             } else if(F("dev") == jsonIn[F("cmd")]) {
                 DPRINTLN(DBG_INFO, F("dev cmd"));
                 iv->setDevCommand(jsonIn[F("val")].as<int>());
+            } else if(F("restart_ahoy") == jsonIn[F("cmd")]) {
+                mApp->setRebootFlag();
             } else {
                 jsonOut[F("error")] = F("ERR_UNKNOWN_CMD");
                 return false;
@@ -889,11 +1151,6 @@ class RestApi {
             if(isProtected(jsonIn, jsonOut, clientIP))
                 return false;
 
-            #if !defined(ETHERNET)
-            if(F("scan_wifi") == jsonIn[F("cmd")])
-                mApp->scanAvailNetworks();
-            else
-            #endif /* !defined(ETHERNET) */
             if(F("set_time") == jsonIn[F("cmd")])
                 mApp->setTimestamp(jsonIn[F("val")]);
             else if(F("sync_ntp") == jsonIn[F("cmd")])
@@ -902,17 +1159,38 @@ class RestApi {
                 mTimezoneOffset = jsonIn[F("val")];
             else if(F("discovery_cfg") == jsonIn[F("cmd")])
                 mApp->setMqttDiscoveryFlag(); // for homeassistant
-            #if !defined(ETHERNET)
             else if(F("save_wifi") == jsonIn[F("cmd")]) {
                 snprintf(mConfig->sys.stationSsid, SSID_LEN, "%s", jsonIn[F("ssid")].as<const char*>());
                 snprintf(mConfig->sys.stationPwd, PWD_LEN, "%s", jsonIn[F("pwd")].as<const char*>());
                 mApp->saveSettings(false); // without reboot
-                mApp->setStopApAllowedMode(false);
                 mApp->setupStation();
             }
-            #endif /* !defined(ETHERNET */
+            #if defined(ETHERNET)
+            else if(F("save_eth") == jsonIn[F("cmd")]) {
+                mConfig->sys.eth.enabled = jsonIn[F("en")].as<bool>();
+                mConfig->sys.eth.pinCs = jsonIn[F("cs")].as<uint8_t>();
+                mConfig->sys.eth.pinSclk = jsonIn[F("sclk")].as<uint8_t>();
+                mConfig->sys.eth.pinMiso = jsonIn[F("miso")].as<uint8_t>();
+                mConfig->sys.eth.pinMosi = jsonIn[F("mosi")].as<uint8_t>();
+                mConfig->sys.eth.pinIrq = jsonIn[F("irq")].as<uint8_t>();
+                mConfig->sys.eth.pinRst = jsonIn[F("reset")].as<uint8_t>();
+                mApp->saveSettings(true);
+            }
+            #endif
             else if(F("save_iv") == jsonIn[F("cmd")]) {
-                Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")], false);
+                Inverter<> *iv;
+
+                for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
+                    iv = mSys->getInverterByPos(i, true);
+                    if(nullptr != iv) {
+                        if((i != jsonIn[F("id")]) && (iv->config->serial.u64 == jsonIn[F("ser")])) {
+                            jsonOut[F("error")] = F("ERR_DUPLICATE_INVERTER");
+                            return false;
+                        }
+                    }
+                }
+
+                iv = mSys->getInverterByPos(jsonIn[F("id")], false);
                 iv->config->enabled = jsonIn[F("en")];
                 iv->config->serial.u64 = jsonIn[F("ser")];
                 snprintf(iv->config->name, MAX_NAME_LENGTH, "%s", jsonIn[F("name")].as<const char*>());
@@ -956,15 +1234,15 @@ class RestApi {
 
     private:
         constexpr static uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT,
-            FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP};
+            FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP, FLD_MT};
         constexpr static uint8_t acListHmt[] = {FLD_UAC_1N, FLD_IAC_1, FLD_PAC, FLD_F, FLD_PF, FLD_T,
-            FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP};
+            FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP, FLD_MT};
         constexpr static uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR, FLD_MP};
 
     private:
         IApp *mApp = nullptr;
         HMSYSTEM *mSys = nullptr;
-        HmRadio<> *mRadioNrf = nullptr;
+        NrfRadio<> *mRadioNrf = nullptr;
         #if defined(ESP32)
         CmtRadio<> *mRadioCmt = nullptr;
         #endif
@@ -974,7 +1252,7 @@ class RestApi {
         uint32_t mTimezoneOffset = 0;
         uint32_t mHeapFree = 0, mHeapFreeBlk = 0;
         uint8_t mHeapFrag = 0;
-        uint8_t *mTmpBuf = NULL;
+        uint8_t *mTmpBuf = nullptr;
         uint32_t mTmpSize = 0;
 };
 
diff --git a/src/web/html/about.html b/src/web/html/about.html
index c0eb8c5e..1b27ac9d 100644
--- a/src/web/html/about.html
+++ b/src/web/html/about.html
@@ -14,7 +14,7 @@
                         <div class="p-2">Used Libraries</div>
                     </div>
                     <div class="row"><a href="https://github.com/bertmelis/espMqttClient" target="_blank">bertmelis/espMqttClient</a></div>
-                    <div class="row"><a href="https://github.com/yubox-node-org/ESPAsyncWebServer" target="_blank">yubox-node-org/ESPAsyncWebServer</a></div>
+                    <div class="row"><a href="https://github.com/esphome/ESPAsyncWebServer" target="_blank">esphome/ESPAsyncWebServer</a></div>
                     <div class="row"><a href="https://github.com/bblanchon/ArduinoJson" target="_blank">bblanchon/ArduinoJson</a></div>
                     <div class="row"><a href="https://github.com/nrf24/RF24" target="_blank">nrf24/RF24</a></div>
                     <div class="row"><a href="https://github.com/paulstoffregen/Time" target="_blank">paulstoffregen/Time</a></div>
diff --git a/src/web/html/api.js b/src/web/html/api.js
index 5cce4206..12ab239b 100644
--- a/src/web/html/api.js
+++ b/src/web/html/api.js
@@ -61,6 +61,23 @@ function ml(tagName, ...args) {
     return nester(el, args[1])
 }
 
+function mlNs(tagName, ...args) {
+    var el = document.createElementNS("http://www.w3.org/2000/svg", tagName);
+    if(args[0]) {
+        for(var name in args[0]) {
+            if(name.indexOf("on") === 0) {
+                el.addEventListener(name.substr(2).toLowerCase(), args[0][name], false)
+            } else {
+                el.setAttribute(name, args[0][name]);
+            }
+        }
+    }
+    if (!args[1]) {
+        return el;
+    }
+    return nester(el, args[1])
+}
+
 function nester(el, n) {
     if (typeof n === "string") {
         el.innerHTML = n;
@@ -84,10 +101,12 @@ function topnav() {
 }
 
 function parseNav(obj) {
-    for(i = 0; i < 13; i++) {
+    for(i = 0; i < 14; i++) {
         if(i == 2)
             continue;
         var l = document.getElementById("nav"+i);
+        if(null == l)
+            continue
         if(12 == i) {
             if(obj.cst_lnk.length > 0) {
                 l.href = obj.cst_lnk
@@ -124,7 +143,7 @@ function parseVersion(obj) {
 
 function parseESP(obj) {
     document.getElementById("esp_type").replaceChildren(
-        document.createTextNode("Board: " + obj["esp_type"])
+        document.createTextNode("Board: " + obj.esp_type)
     );
 }
 
@@ -134,7 +153,11 @@ function parseRssi(obj) {
         icon = iconWifi1;
     else if(obj["wifi_rssi"] <= -70)
         icon = iconWifi2;
-    document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "icon-fg2", obj["wifi_rssi"]));
+    document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "icon-fg2", obj.wifi_rssi));
+}
+
+function parseTitle(obj) {
+    document.title = obj.host + " - " + document.title
 }
 
 function toIsoDateStr(d) {
@@ -221,6 +244,10 @@ function badge(success, text, second="error") {
     return ml("span", {class: "badge badge-" + ((success) ? "success" : second)}, text);
 }
 
+function progress(val) {
+    return ml("div", {class: "progress"}, ml("div", {class: "progress-bar", style: "width: " + val + "%"}, null))
+}
+
 function tabChange(id) {
     var els = document.getElementsByClassName("nav-link");
     [].forEach.call(els, function(e) {
diff --git a/src/web/html/colorBright.css b/src/web/html/colorBright.css
index 2e676029..ebf4f12a 100644
--- a/src/web/html/colorBright.css
+++ b/src/web/html/colorBright.css
@@ -12,6 +12,7 @@
 
     --nav-bg: #333;
     --primary: #006ec0;
+    --primary-disabled: #ccc;
     --primary-hover: #044e86;
     --secondary: #0072c8;
     --nav-active: #555;
@@ -30,4 +31,8 @@
     --ch-head-bg: #006ec0;
     --ts-head: #333;
     --ts-bg: #555;
+
+    --chart-cont: #fbfbfb;
+    --chart-bg: #f9f9f9;
+    --chart-text: #000000;
 }
diff --git a/src/web/html/colorDark.css b/src/web/html/colorDark.css
index 40bd4cf3..23e7a2cf 100644
--- a/src/web/html/colorDark.css
+++ b/src/web/html/colorDark.css
@@ -12,6 +12,7 @@
 
     --nav-bg: #333;
     --primary: #004d87;
+    --primary-disabled: #ccc;
     --primary-hover: #023155;
     --secondary: #0072c8;
     --nav-active: #555;
@@ -30,4 +31,8 @@
     --ch-head-bg: #236;
     --ts-head: #333;
     --ts-bg: #555;
+
+    --chart-cont: #0b0b0b;
+    --chart-bg: #090909;
+    --chart-text: #FFFFFF;
 }
diff --git a/src/web/html/grid_info.json b/src/web/html/grid_info.json
index 12760ccf..5b5d4186 100644
--- a/src/web/html/grid_info.json
+++ b/src/web/html/grid_info.json
@@ -10,10 +10,13 @@
         {"0x0908": "France_VFR2014"},
         {"0x0a00": "DE NF_EN_50549-1:2019"},
         {"0x0c00": "AT_TOR_Erzeuger_default"},
+        {"0x0c03": "AT_TOR_Erzeuger_cosphi=1"},
+        {"0x0c04": "AT_TOR_Erzeuger_default"},
         {"0x0d00": "FR_VFR2019"},
         {"0x0d04": "NF_EN_50549-1:2019"},
         {"0x1000": "ES_RD1699"},
         {"0x1200": "EU_EN50438"},
+        {"0x1300": "MEX_NOM_220V"},
         {"0x2600": "BE_C10_26"},
         {"0x2900": "NL_NEN-EN50549-1_2019"},
         {"0x2a00": "PL_PN-EN 50549-1:2019"},
@@ -35,6 +38,44 @@
         {"0xb0": "Watt Power Factor"}
     ],
     "group": [
+        {
+            "0x0000": [
+                {
+                    "name": "Nominal Voltage",
+                    "div": 10,
+                    "def": 220,
+                    "unit": "V"
+                },
+                {
+                    "name": "Low Voltage 1",
+                    "div": 10,
+                    "min": 170,
+                    "max": 195.5,
+                    "def": 184,
+                    "unit": "V"
+                },
+                {
+                    "name": "LV1 Maximum Trip Time",
+                    "div": 10,
+                    "def": 0.1,
+                    "unit": "s"
+                },
+                {
+                    "name": "High Voltage 1",
+                    "div": 10,
+                    "min": 253,
+                    "max": 275,
+                    "def": 270,
+                    "unit": "V"
+                },
+                {
+                    "name": "HV1 Maximum Trip Time",
+                    "div": 10,
+                    "def": 0.1,
+                    "unit": "s"
+                }
+            ]
+        },
         {
             "0x0003": [
                 {
@@ -213,6 +254,78 @@
                 }
             ]
         },
+        {
+            "0x000b": [
+                {
+                    "name": "Nominal Voltage",
+                    "div": 10,
+                    "def": 230,
+                    "unit": "V"
+                },
+                {
+                    "name": "Low Voltage 1",
+                    "div": 10,
+                    "min": 170,
+                    "max": 184,
+                    "def": 184,
+                    "unit": "V"
+                },
+                {
+                    "name": "LV1 Maximum Trip Time",
+                    "div": 10,
+                    "def": 1.5,
+                    "unit": "s"
+                },
+                {
+                    "name": "High Voltage 1",
+                    "div": 10,
+                    "min": 253,
+                    "max": 270,
+                    "def": 255.3,
+                    "unit": "V"
+                },
+                {
+                    "name": "HV1 Maximum Trip Time",
+                    "div": 10,
+                    "def": 0.1,
+                    "unit": "s"
+                },
+                {
+                    "name": "Low Voltage 2",
+                    "div": 10,
+                    "def": 57.5,
+                    "unit": "V"
+                },
+                {
+                    "name": "LV2 Maximum Trip Time",
+                    "div": 100,
+                    "def": 0.5,
+                    "unit": "s"
+                },
+                {
+                    "name": "High Voltage 2",
+                    "div": 10,
+                    "min": 264.5,
+                    "max": 275,
+                    "def": 264.5,
+                    "unit": "V"
+                },
+                {
+                    "name": "HV2 Maximum Trip Time",
+                    "div": 100,
+                    "def": 0.08,
+                    "unit": "s"
+                },
+                {
+                    "name": "10mins Average High Voltage",
+                    "div": 10,
+                    "min": 245,
+                    "max": 255.3,
+                    "def": 255.3,
+                    "unit": "V"
+                }
+            ]
+        },
         {
             "0x000c": [
                 {
@@ -766,6 +879,35 @@
                 }
             ]
         },
+        {
+            "0x6004": [
+                {
+                    "name": "VW Function Activated",
+                    "div": 1,
+                    "min": 0,
+                    "max": 1,
+                    "def": 1
+                },
+                {
+                    "name": "Start of Voltage Watt Droop",
+                    "div": 10,
+                    "def": 253,
+                    "unit": "V"
+                },
+                {
+                    "name": "End of Voltage Watt Droop",
+                    "div": 10,
+                    "def": 257.6,
+                    "unit": "V"
+                },
+                {
+                    "name": "VW Droop Slope",
+                    "div": 100,
+                    "def": 21.74,
+                    "unit": "Pn%/V"
+                }
+            ]
+        },
         {
             "0x7000": [
                 {
diff --git a/src/web/html/history.html b/src/web/html/history.html
index 7e317b59..8b3b63c7 100644
--- a/src/web/html/history.html
+++ b/src/web/html/history.html
@@ -13,82 +13,178 @@
         <div id="wrapper">
             <div id="content">
                 <h3>{#TOTAL_POWER}</h3>
-                <div>
-                    <div class="chartDiv" id="pwrChart">  </div>
-                    <p>
-                        {#MAX_DAY}: <span id="pwrMaxDay"></span> W. {#LAST_VALUE}: <span id="pwrLast"></span> W.<br />
-                        {#MAXIMUM}: <span id="pwrMax"></span> W. {#UPDATED} <span id="pwrRefresh"></span> {#SECONDS}
-                    </p>
-                </div>
+                <div class="chartDiv" id="pwrChart"></div>
+                <h3>{#TOTAL_POWER_DAY}</h3>
+                <div class="chartDiv" id="pwrDayChart"></div>
+                <!--IF_ENABLE_HISTORY_YIELD_PER_DAY-->
+                <h3>{#TOTAL_YIELD_PER_DAY}</h3>
+                <div class="chartDiv" id="ydChart"></div>
+                <!--ENDIF_ENABLE_HISTORY_YIELD_PER_DAY-->
+                <!--IF_ENABLE_HISTORY_LOAD_DATA-->
+                <h4 style="margin-bottom:0px;">Insert data into Yield per day history</h4>
+                <fieldset style="padding: 1px;">
+                    <legend class="des" style="margin-top: 0px;">Insert data (*.json) i.e. from a saved "/api/yieldDayHistory" call
+                    </legend>
+                    <form id="form" method="POST" action="/api/addYDHist" enctype="multipart/form-data"
+                        accept-charset="utf-8">
+                        <input type="button" class="btn my-4" style="padding: 3px;margin: 3px;" value="Insert" onclick="submit()">
+                        <input type="file" name="insert" style="width: 80%;">
+                    </form>
+                </fieldset>
+                <!--ENDIF_ENABLE_HISTORY_LOAD_DATA-->
             </div>
         </div>
         {#HTML_FOOTER}
 
         <script type="text/javascript">
-            const svgns = "http://www.w3.org/2000/svg";
-            var pwrExeOnce = true;
-            var ydExeOnce = true;
-            // make a simple rectangle
-            var mRefresh = 60;
-            var mLastValue = 0;
-            const mChartHeight = 250;
-
-            function parseHistory(obj, namePrefix, execOnce) {
-                mRefresh = obj.refresh
-                var data = Object.assign({}, obj.value)
-                numDataPts = Object.keys(data).length
-
-                if (true == execOnce) {
-                    let s = document.createElementNS(svgns, "svg");
-                    s.setAttribute("class", "chart");
-                    s.setAttribute("width", (numDataPts + 2) * 2);
-                    s.setAttribute("height", mChartHeight);
-                    s.setAttribute("role", "img");
-
-                    let g = document.createElementNS(svgns, "g");
-                    s.appendChild(g);
-                    for (var i = 0; i < numDataPts; i++) {
-                        val = data[i];
-                        let rect = document.createElementNS(svgns, "rect");
-                        rect.setAttribute("id", namePrefix+"Rect" + i);
-                        rect.setAttribute("x", i * 2);
-                        rect.setAttribute("width", 2);
-                        g.appendChild(rect);
+            const height = 250
+            var once = true
+
+            function calcScale(obj) {
+                let s = {}
+                s.x_mul = 60
+                s.ts_dur = obj.refresh * obj.value.length
+                s.ts_start = obj.lastValueTs - s.ts_dur
+                while(s.x_mul * 10 <= s.ts_dur)
+                    s.x_mul += (s.x_mul == 60) ? 240 : ((s.x_mul < 1800) ? 300 : 1800)
+
+                s.y_mul = 10
+                while(s.y_mul * 10 <= obj.max)
+                    s.y_mul += (s.y_mul < 100) ? 10 : 100
+                s.y_step = Math.ceil(obj.max / s.y_mul)
+                s.y_max = s.y_mul * s.y_step
+                return s
+            }
+
+            function setupSvg(id, obj) {
+                let scale = calcScale(obj)
+                let n = obj.value.length
+                return mlNs("svg", {class: "container", id: id+"_svg", viewBox: "0 0 "+String(n*2+50)+" "+String(height+20), width: "100%", height: "100%"}, [
+                    mlNs("defs", {}, [
+                        mlNs("linearGradient", {id: "gLine", x1: 0, y1: 0, x2: 0, y2: "100%"}, [
+                            mlNs("stop", {offset: 0, "stop-color": "#006ec0"}),
+                            mlNs("stop", {offset: "80%", "stop-color": "#5050ff"}),
+                            mlNs("stop", {offset: "100%", "stop-color": "gray"})
+                        ]),
+                        mlNs("linearGradient", {id: "gFill", x1: 0, y1: 0, x2: 0, y2: "100%"}, [
+                            mlNs("stop", {offset: 0, "stop-color": "#006ec0"}),
+                            mlNs("stop", {offset: "50%", "stop-color": "#0034c0"}),
+                            mlNs("stop", {offset: "100%", "stop-color": "#e0e0e0"})
+                        ])
+                    ]),
+                    ...gridText(n*2, scale),
+                    mlNs("g", {transform: "translate(30, 5)"}, [
+                        ...grid(n*2, scale),
+                        ...poly(n*2, obj, scale)
+                    ])
+                ])
+            }
+
+            function gridText(x2, scale) {
+                let g = []
+                let div = height / scale.y_max
+                for(let i = 0; i <= scale.y_max; i += scale.y_mul) {
+                    g.push(mlNs("text", {x: 0, y: height-(i*div)+9}, String(i)))
+                }
+                div = x2 / scale.ts_dur
+                for(let i = 0; i < scale.ts_dur; i++) {
+                    if((i + scale.ts_start) % scale.x_mul == 0) {
+                        let d = new Date((scale.ts_start + i) * 1000)
+                        g.push(mlNs("text", {x: (i*div)+17, y: height+20}, ("0"+d.getHours()).slice(-2) + ":" + ("0"+d.getMinutes()).slice(-2)))
                     }
-                    document.getElementById(namePrefix+"Chart").appendChild(s);
                 }
+                return g
+            }
 
-                // normalize data to chart
-                let divider = obj.max / mChartHeight;
-                if (divider == 0)
-                    divider = 1;
-                for (var i = 0; i < numDataPts; i++) {
-                    val = data[i];
-                    if (val > 0)
-                        mLastValue = val
-                    val = val / divider
-                    rect = document.getElementById(namePrefix+"Rect" + i);
-                    rect.setAttribute("height", val);
-                    rect.setAttribute("y", mChartHeight - val);
+            function grid(x2, scale) {
+                let g = []
+                let div = height / scale.y_max
+                for(let i = 0; i <= scale.y_max; i += scale.y_mul) {
+                    g.push(mlNs("line", {x1: 0, x2: x2, y1: height-i*div, y2: height-i*div, "stroke-width": 1, "stroke-dasharray": "1,3", stroke: "#aaa"}))
+                }
+                div = x2 / scale.ts_dur
+                for(let i = 0; i <= scale.ts_dur; i++) {
+                    if((i + scale.ts_start) % scale.x_mul == 0) {
+                        g.push(mlNs("line", {x1: (i*div), x2: (i*div), y1: 0, y2: height, "stroke-width": 1, "stroke-dasharray": "1,3", stroke: "#aaa"}))
+                    }
                 }
-                document.getElementById(namePrefix + "Max").innerHTML = obj.max;
-                if (mRefresh < 5)
-                    mRefresh = 5;
-                document.getElementById(namePrefix + "Refresh").innerHTML = mRefresh;
+                return g
             }
 
+            function poly(x2, obj, scale) {
+                let pts = ""
+                let i = 0, first = -1, last = -1, lastVal = 0
+                let div = scale.y_max / height
+                if(div == 0)
+                    div = 1
+                for (val of obj.value) {
+                    if(val > 0) {
+                        lastVal = val
+                        pts += " " + String(i) + "," + String(height - val / div)
+                        if(first < 0)
+                            first = i
+                        last = i
+                    }
+                    i += 2
+                }
+                let pts2 = pts + " " + String(last) + "," + String(height)
+                pts2 += " " + String(first) + "," + String(height)
+                elm = [
+                    mlNs("polyline", {stroke: "url(#gLine)", fill: "none", points: pts}),
+                    mlNs("polyline", {stroke: "none", fill: "url(#gFill)", points: pts2}),
+                    mlNs("text", {x: i*.8, y: 10}, "{#MAXIMUM}: " + String(obj.max) + "W"),
+                    mlNs("text", {x: i*.8, y: 25}, "{#LAST_VALUE}: " + String(lastVal) + "W")
+                ]
+
+                if(undefined !== obj.yld)
+                    elm.push(mlNs("text", {x: i*.8, y: 40}, "{#YIELD_DAY}: " + String(obj.yld) + "kWh"))
+
+                return elm;
+            }
+
+
             function parsePowerHistory(obj){
+                if(once) {
+                    once = false
+                    parseNav(obj.generic)
+                    parseESP(obj.generic)
+                    parseRssi(obj.generic)
+                    parseTitle(obj.generic)
+                    window.setInterval("getAjax('/api/powerHistory', parsePowerHistory)", obj.refresh * 1000)
+                    setTimeout(() => {
+                        window.setInterval("getAjax('/api/powerHistoryDay', parsePowerHistoryDay)", obj.refresh * 1000)
+                    }, 200)
+                    /*IF_ENABLE_HISTORY_YIELD_PER_DAY*/
+                    setTimeout(() => {
+                        window.setInterval("getAjax('/api/yieldDayHistory', parseYieldDayHistory)", obj.refresh * 1000)
+                    }, 400)
+                    /*ENDIF_ENABLE_HISTORY_YIELD_PER_DAY*/
+                }
+                if (null != obj) {
+                    let svg = setupSvg("ph", obj);
+                    document.getElementById("pwrChart").replaceChildren(svg);
+                    setTimeout(() => { getAjax("/api/powerHistoryDay", parsePowerHistoryDay) }, 50);
+                }
+            }
+
+            function parsePowerHistoryDay(obj) {
                 if (null != obj) {
-                    parseNav(obj.generic);
-                    parseHistory(obj,"pwr", pwrExeOnce)
-                    document.getElementById("pwrLast").innerHTML = mLastValue
-                    document.getElementById("pwrMaxDay").innerHTML = obj.maxDay
+                    let svg = setupSvg("phDay", obj);
+                    document.getElementById("pwrDayChart").replaceChildren(svg);
+                    /*IF_ENABLE_HISTORY_YIELD_PER_DAY*/
+                    setTimeout(() => { getAjax("/api/yieldDayHistory", parseYieldDayHistory) }, 50);
+                    /*ENDIF_ENABLE_HISTORY_YIELD_PER_DAY*/
                 }
-                if (pwrExeOnce) {
-                    pwrExeOnce = false;
-                    window.setInterval("getAjax('/api/powerHistory', parsePowerHistory)", mRefresh * 1000);
+            }
+
+            /*IF_ENABLE_HISTORY_YIELD_PER_DAY*/
+            function parseYieldDayHistory(obj) {
+                if (null != obj) {
+                    let svg = setupSvg("phDay", obj);
+                    document.getElementById("pwrDayChart").replaceChildren(svg);
                 }
             }
+            /*ENDIF_ENABLE_HISTORY_YIELD_PER_DAY*/
 
             getAjax("/api/powerHistory", parsePowerHistory);
         </script>
diff --git a/src/web/html/includes/footer.html b/src/web/html/includes/footer.html
index 6aa89673..ce0c6852 100644
--- a/src/web/html/includes/footer.html
+++ b/src/web/html/includes/footer.html
@@ -1,6 +1,6 @@
 <div id="footer">
         <div class="left">
-        <a href="https://ahoydtu.de" target="_blank">AhoyDTU &copy 2024</a>
+        <a href="https://ahoydtu.de" target="_blank">AhoyDTU &copy; 2024</a>
         <ul>
             <li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li>
             <li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li>
diff --git a/src/web/html/includes/nav.html b/src/web/html/includes/nav.html
index 447bf411..4718e257 100644
--- a/src/web/html/includes/nav.html
+++ b/src/web/html/includes/nav.html
@@ -7,18 +7,21 @@
     </a>
     <div id="topnav" class="mobile">
         <a id="nav3" class="hide" href="/live?v={#VERSION}">{#NAV_LIVE}</a>
+        <!--IF_ENABLE_HISTORY-->
         <a id="nav11" class="acitve" href="/history?v={#VERSION}">{#NAV_HISTORY}</a>
+        <!--ENDIF_ENABLE_HISTORY-->
         <a id="nav4" class="hide" href="/serial?v={#VERSION}">{#NAV_WEBSERIAL}</a>
         <a id="nav5" class="hide" href="/setup?v={#VERSION}">{#NAV_SETTINGS}</a>
-        <span class="seperator"></span>
+        <span class="separator"></span>
         <a id="nav6" class="hide" href="/update?v={#VERSION}">Update</a>
         <a id="nav7" class="hide" href="/system?v={#VERSION}">System</a>
-        <span class="seperator"></span>
+        <span class="separator"></span>
         <a id="nav8" href="/api" target="_blank">REST API</a>
-        <a id="nav9" href="https://ahoydtu.de" target="_blank">{#NAV_DOCUMENTATION}</a>
+        <a id="nav9" href="https://docs.ahoydtu.de" target="_blank">{#NAV_DOCUMENTATION}</a>
+        <a id="nav13" href="https://ahoydtu.de" target="_blank">Website</a>
         <a id="nav10" href="/about?v={#VERSION}">{#NAV_ABOUT}</a>
         <a id="nav12" href="#" class="hide" target="_blank">Custom Link</a>
-        <span class="seperator"></span>
+        <span class="separator"></span>
         <a id="nav0" class="hide" href="/login">Login</a>
         <a id="nav1" class="hide" href="/logout">Logout</a>
     </div>
diff --git a/src/web/html/index.html b/src/web/html/index.html
index 2611db5b..ee78ac8e 100644
--- a/src/web/html/index.html
+++ b/src/web/html/index.html
@@ -14,6 +14,7 @@
                 </p>
                 <p>
                     <span class="des">System Infos:</span>
+                    <div id="total"></div>
                     <div id="iv"></div>
                     <div class="hr"></div>
                     <div id="warn_info"></div>
@@ -23,9 +24,9 @@
                     <h3>{#SUPPORT}:</h3>
                     <ul>
                         <li><a href="https://github.com/lumapu/ahoy/blob/main/src/CHANGES.md" target="_blank">{#CHANGELOG}</a></li>
-                        <li>{#DISCUSS} <a href="https://discord.gg/WzhxEY62mB">Discord</a></li>
+                        <li>{#DISCUSS} <a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li>
                         <li>{#REPORT} <a href="https://github.com/lumapu/ahoy/issues" target="_blank">{#ISSUES}</a></li>
-                        <li>{#CONTRIBUTE} <a href="https://github.com/lumapu/ahoy/blob/main/User_Manual.md"  target="_blank">{#DOCUMENTATION}</a></li>
+                        <li>{#CONTRIBUTE} <a href="https://docs.ahoydtu.de"  target="_blank">{#DOCUMENTATION}</a></li>
                         <li><a href="https://fw.ahoydtu.de/fw/dev/" target="_blank">Download</a> & Test {#DEV_FIRMWARE}, <a href="https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md" target="_blank">{#DEV_CHANGELOG}</a></li>
                         <li>{#DON_MAKE} <a href="https://paypal.me/lupusch"  target="_blank">{#DONATION}</a></li>
                     </ul>
@@ -56,8 +57,10 @@
             }
 
             function parseGeneric(obj) {
-                if(exeOnce)
+                if(exeOnce) {
                     parseESP(obj)
+                    parseTitle(obj)
+                }
                 parseRssi(obj)
             }
 
@@ -70,9 +73,9 @@
                 var min  = parseInt(up / 60) % 60;
                 var sec  = up % 60;
                 var e = document.getElementById("uptime");
-                e.innerHTML = days + " Day";
+                e.innerHTML = days + " {#DAY}";
                 if(1 != days)
-                    e.innerHTML += "s";
+                    e.innerHTML += "{#S}";
                 e.innerHTML += ", " + ("0"+hrs).substr(-2) + ":"
                    + ("0"+min).substr(-2) + ":"
                    + ("0"+sec).substr(-2);
@@ -111,6 +114,10 @@
 
             function parseIv(obj, ts) {
                 var p = div(["none"]);
+                var total = 0;
+                var count = 0;
+                var mobile = window.screen.width < 470;
+
                 for(var i of obj) {
                     var icon = iconSuccess;
                     var cl = "icon-success";
@@ -126,20 +133,28 @@
                     } else if(0 == i["ts_last_success"]) {
                         avail = "{#AVAIL_NO_DATA}";
                     } else {
-                        avail = "{#AVAIL} ";
+                        if (!mobile)
+                            avail = "{#AVAIL} ";
                         if(false == i["is_producing"])
                             avail += "{#NOT_PRODUCING}";
                         else {
                             icon = iconSuccessFull;
-                            avail += "{#PRODUCING} " + i.cur_pwr + "W";
+                            avail += "{#PRODUCING} " + i.cur_pwr + " W";
+                            total += i.cur_pwr;
+                            count += 1;
                         }
                     }
 
+                    var text;
+                        if (mobile)
+                            text = "#";
+                        else
+                            text = "{#INVERTER} #";
                     p.append(
-                        svg(icon, 30, 30, "icon " + cl),
-                        span("{#INVERTER} #" + i["id"] + ": " + i["name"] + " {#IS} " + avail),
-                        br()
-                    );
+                            svg(icon, 30, 30, "icon " + cl),
+                            span(text + i["id"] + ": " + i["name"] + " {#IS} " + avail),
+                            br()
+                        );
 
                     if(false == i["is_avail"]) {
                         if(i["ts_last_success"] > 0) {
@@ -149,6 +164,13 @@
                     }
                 }
                 document.getElementById("iv").replaceChildren(p);
+
+                if (count > 1) {
+                    var t = div(["none"]);
+                    t.append(svg(iconInfo, 30, 30, "icon icon-info"), span("Total: " + Math.round(total).toLocaleString() + " W"), br());
+                    document.getElementById("total").replaceChildren(t);
+                    document.getElementById("total").appendChild(div(["hr"]));
+                }
             }
 
             function parseWarn(warn) {
@@ -165,7 +187,7 @@
                         p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("{#UPDATE_AVAIL}: " + release), br());
                     else if(getVerInt("{#VERSION}") > getVerInt(release))
                         p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("{#USING_DEV_VERSION} {#VERSION}. {#DEV_ISSUE_RELEASE_VERSION}: " + release), br());
-                                       else 
+                                       else
                         p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("{#RELEASE_INSTALLED}: " + release), br());
                 }
 
diff --git a/src/web/html/serial.html b/src/web/html/serial.html
index 83e614c8..39ba0ac2 100644
--- a/src/web/html/serial.html
+++ b/src/web/html/serial.html
@@ -35,15 +35,16 @@
                 var hrs  = parseInt(up / 3600) % 24;
                 var min  = parseInt(up / 60) % 60;
                 var sec  = up % 60;
-                document.getElementById("uptime").innerHTML = days + " Days, "
+                document.getElementById("uptime").innerHTML = days + " {#DAYS}, "
                     + ("0"+hrs).substr(-2) + ":"
                     + ("0"+min).substr(-2) + ":"
                     + ("0"+sec).substr(-2);
 
-                parseRssi(obj);
+                parseRssi(obj)
                 if(true == exeOnce) {
-                    parseNav(obj);
-                    parseESP(obj);
+                    parseNav(obj)
+                    parseESP(obj)
+                    parseTitle(obj)
                     window.setInterval("getAjax('/api/generic', parseGeneric)", 5000);
                     exeOnce = false;
                     setTimeOffset();
@@ -65,7 +66,7 @@
             });
             document.getElementById("scroll").addEventListener("click", function() {
                 mAutoScroll = !mAutoScroll;
-                this.value = (mAutoScroll) ? "autoscroll" : "manual scroll";
+                this.value = (mAutoScroll) ? "{#BTN_AUTOSCROLL}" : "{#BTN_MANUALSCROLL}";
             });
             document.getElementById("copy").addEventListener("click", function() {
                 con.value = version + " - " + build + "\n---------------\n" + con.value;
@@ -80,10 +81,10 @@
                     try {
                         return document.execCommand("copy");  // Security exception may be thrown by some browsers.
                     } catch (ex) {
-                        alert("Copy to clipboard failed" + ex);
+                        alert("{#CLIPBOARD_FAILED} " + ex);
                     } finally {
                         document.body.removeChild(ta);
-                        alert("Copied to clipboard");
+                        alert("{#COPIED_TO_CLIPBOARD}");
                     }
                 }
             });
diff --git a/src/web/html/setup.html b/src/web/html/setup.html
index 47d935b9..2b59146c 100644
--- a/src/web/html/setup.html
+++ b/src/web/html/setup.html
@@ -52,28 +52,17 @@
                     <div class="s_content">
                         <fieldset class="mb-2">
                             <legend class="des">WiFi</legend>
-
                             <div class="row mb-3">
                                 <div class="col-12 col-sm-3 my-2">{#AP_PWD}</div>
                                 <div class="col-12 col-sm-9"><input type="text" name="ap_pwd" minlength="8" /></div>
                             </div>
-                            <div class="row mb-3">
-                                <div class="col-12 col-sm-3 my-2">{#SEARCH_NETWORKS}</div>
-                                <div class="col-12 col-sm-9"><input type="button" name="scanbtn" id="scanbtn" class="btn" value="{#BTN_SCAN}" onclick="scan()"/></div>
-                            </div>
-
                             <div class="row mb-2 mb-sm-3">
-                                <div class="col-12 col-sm-3 my-2">{#AVAIL_NETWORKS}</div>
+                                <div class="col-12 col-sm-3 my-2">SSID</div>
                                 <div class="col-12 col-sm-9">
-                                    <select name="networks" id="networks" onChange="selNet()">
-                                        <option value="-1" selected disabled hidden>{#NETWORK_NOT_SCANNED}</option>
-                                    </select>
+                                    <input type="text" name="ssid"/><br/>
+                                    <a href="/wizard">{#SCAN_WIFI}</a>
                                 </div>
                             </div>
-                            <div class="row mb-2 mb-sm-3">
-                                <div class="col-12 col-sm-3 my-2">SSID</div>
-                                <div class="col-12 col-sm-9"><input type="text" name="ssid"/></div>
-                            </div>
                             <div class="row mb-2 mb-sm-3">
                                 <div class="col-12 col-sm-3">{#SSID_HIDDEN}</div>
                                 <div class="col-12 col-sm-9"><input type="checkbox" name="hidd"/></div>
@@ -138,7 +127,11 @@
                                 <div class="col-4"><input type="checkbox" name="invRstMid"/></div>
                             </div>
                             <div class="row mb-3">
-                                <div class="col-8 mb-2">{#INV_PAUSE_SUNSET}</div>
+                                <div class="col-8 mb-2">{#INV_RESET_SUNRISE}</div>
+                                <div class="col-4"><input type="checkbox" name="invRstComStart"/></div>
+                            </div>
+                            <div class="row mb-3">
+                                <div class="col-8 mb-2">{#INV_RESET_SUNSET}</div>
                                 <div class="col-4"><input type="checkbox" name="invRstComStop"/></div>
                             </div>
                             <div class="row mb-3">
@@ -146,7 +139,7 @@
                                 <div class="col-4"><input type="checkbox" name="invRstNotAvail"/></div>
                             </div>
                             <div class="row mb-3">
-                                <div class="col-8">{#INV_RESET_MAX_MIDNIGHT}</div>
+                                <div class="col-8">{#INV_RESET_MAX_VALUES}</div>
                                 <div class="col-4"><input type="checkbox" name="invRstMaxMid"/></div>
                             </div>
                             <div class="row mb-3">
@@ -157,10 +150,6 @@
                                 <div class="col-8">{#INV_READ_GRID_PROFILE}</div>
                                 <div class="col-4"><input type="checkbox" name="rdGrid"/></div>
                             </div>
-                            <div class="row mb-3">
-                                <div class="col-8">{#INV_YIELD_EFF}</div>
-                                <div class="col-4"><input type="number" name="yldEff" step="any"/></div>
-                            </div>
                         </fieldset>
                     </div>
 
@@ -246,6 +235,10 @@
                                 <div class="col-12 col-sm-3 my-2">Topic</div>
                                 <div class="col-12 col-sm-9"><input type="text" name="mqttTopic" pattern="[\-\+A-Za-z0-9\.\/#\$%&=_]+" title="Invalid input" /></div>
                             </div>
+                            <div class="row mb-3">
+                                <div class="col-12 col-sm-3 my-2">{#MQTT_JSON}</div>
+                                <div class="col-12 col-sm-9"><input type="checkbox" name="mqttJson" /></div>
+                            </div>
                             <p class="des">{#MQTT_NOTE}</p>
                             <div class="row mb-3">
                                 <div class="col-12 col-sm-3 my-2">{#INTERVAL}</div>
@@ -258,6 +251,10 @@
                                     <span id="apiResultMqtt"></span>
                                 </div>
                             </div>
+                            <div class="row mb-3">
+                                <div class="col-8 col-sm-3">{#RETAIN}</div>
+                                <div class="col-4 col-sm-9"><input type="checkbox" name="retain"/></div>
+                            </div>
                         </fieldset>
                     </div>
 
@@ -274,9 +271,13 @@
                             <p class="des">{#RADIO} (CMT2300A)</p>
                             <div id="cmt"></div>
                             <!--ENDIF_ESP32-->
+                            <!--IF_ETHERNET-->
+                            <p class="des">Ethernet</p>
+                            <div id="eth"></div>
+                            <!--ENDIF_ETHERNET-->
                         </fieldset>
                     </div>
-
+                    <!--IF_PLUGIN_DISPLAY-->
                     <button type="button" class="s_collapsible">{#DISPLAY_CONFIG}</button>
                     <div class="s_content">
                         <fieldset class="mb-4">
@@ -290,7 +291,7 @@
                             <div id="screenSaver"></div>
                             <div class="row mb-3" id="luminanceOption">
                                 <div class="col-12 col-sm-3 my-2">{#DISP_LUMINANCE}</div>
-                                <div class="col-12 col-sm-9"><input type="number" name="disp_cont" min="0" max="255"></select></div>
+                                <div class="col-12 col-sm-9"><input type="number" name="disp_cont" min="0" max="255"></div>
                             </div>
                             <p class="des">{#DISP_PINOUT}</p>
                             <div id="dispPins"></div>
@@ -299,12 +300,13 @@
                                 <p class="des">{#GRAPH_OPTIONS}</p>
                                 <div class="row mb-3">
                                     <div class="col-12 col-sm-3 my-2">{#GRAPH_SHOW_RATIO}</div>
-                                    <div class="col-12 col-sm-9"><input type="number" name="disp_graph_ratio" min="0" max="100"></select></div>
+                                    <div class="col-12 col-sm-9"><input type="number" name="disp_graph_ratio" min="0" max="100"></div>
                                 </div>
                                 <div id="graphSize"></div>
                             </div>
                         </fieldset>
                     </div>
+                    <!--ENDIF_PLUGIN_DISPLAY-->
 
                     <div class="row mb-4 mt-4">
                         <div class="col-8 col-sm-3">{#BTN_REBOOT_SUCCESSFUL_SAVE}</div>
@@ -324,8 +326,8 @@
                             <div class="col-12 col-sm-9">
                                 <form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8">
                                     <div class="row">
-                                        <div class="col-12 col-sm-8 my-2"><input type="file" name="upload"></div>
-                                        <div class="col-12 col-sm-4 my-2"><input type="button" class="btn" value="Import" onclick="hide()"></div>
+                                        <div class="col-12 col-sm-8 my-2"><input type="file" id="importFileInput" name="upload"></div>
+                                        <div class="col-12 col-sm-4 my-2"><input type="button" id="importButton" class="btn" value="Import" onclick="hide()"></div>
                                     </div>
                                 </form>
                             </div>
@@ -345,6 +347,7 @@
             var maxInv = 0;
             var ts = 0;
 
+            /*IF_ESP8266*/
             var esp8266pins = [
                 [255, "{#PIN_OFF}"],
                 [0, "D3 (GPIO0)"],
@@ -365,6 +368,7 @@
                 [15, "D8 (GPIO15)"],
                 [16, "D0 (GPIO16 - {#PIN_NO_IRQ})"]
             ];
+            /*ENDIF_ESP8266*/
 
             /*IF_ESP32*/
             var esp32pins = [
@@ -396,6 +400,7 @@
                 [36, "VP (GPIO36, {#PIN_INPUT_ONLY})"],
                 [39, "VN (GPIO39, {#PIN_INPUT_ONLY})"]
             ];
+            /*IF_ESP32-S2*/
             var esp32sXpins = [
                 [255, "off / default"],
                 [0,  "GPIO0 ({#PIN_DONT_USE} - BOOT)"],
@@ -444,6 +449,58 @@
                 [47, "GPIO47"],
                 [48, "GPIO48"],
             ];
+            /*ENDIF_ESP32-S2*/
+            /*IF_ESP32-S3*/
+            var esp32sXpins = [
+                [255, "off / default"],
+                [0,  "GPIO0 ({#PIN_DONT_USE} - BOOT)"],
+                [1,  "GPIO1"],
+                [2,  "GPIO2"],
+                [3,  "GPIO3"],
+                [4,  "GPIO4 (CMT CSB)"],
+                [5,  "GPIO5 (CMT SDIO)"],
+                [6,  "GPIO6 (CMT SCLK)"],
+                [7,  "GPIO7"],
+                [8,  "GPIO8 (CMT GPIO3)"],
+                [9,  "GPIO9 (DATA display)"],
+                [10, "GPIO10 (SCK display)"],
+                [11, "GPIO11 (CS display)"],
+                [12, "GPIO12 (DC display)"],
+                [13, "GPIO13 (RST display)"],
+                [14, "GPIO14 (BUSY display)"],
+                [15, "GPIO15"],
+                [16, "GPIO16"],
+                [17, "GPIO17"],
+                [18, "GPIO18"],
+                [19, "GPIO19 ({#PIN_DONT_USE} - USB-)"],
+                [20, "GPIO20 ({#PIN_DONT_USE} - USB+)"],
+                [21, "GPIO21 (CMT FCSB)"],
+                [26, "GPIO26 (PSRAM - {#PIN_NOT_AVAIL})"],
+                [27, "GPIO27 (FLASH - {#PIN_NOT_AVAIL})"],
+                [28, "GPIO28 (FLASH - {#PIN_NOT_AVAIL})"],
+                [29, "GPIO29 (FLASH - {#PIN_NOT_AVAIL})"],
+                [30, "GPIO30 (FLASH - {#PIN_NOT_AVAIL})"],
+                [31, "GPIO31 (FLASH - {#PIN_NOT_AVAIL})"],
+                [32, "GPIO32 (FLASH - {#PIN_NOT_AVAIL})"],
+                [33, "GPIO33 (not exposed on S3-WROOM modules)"],
+                [34, "GPIO34 (not exposed on S3-WROOM modules)"],
+                [35, "GPIO35 (MOSI NRF24)"],
+                [36, "GPIO36 (SCK NRF24)"],
+                [37, "GPIO37 (CSN NRF24)"],
+                [38, "GPIO38 (CE NRF24)"],
+                [39, "GPIO39 (SCK ETH)"],
+                [40, "GPIO40 (MOSI ETH)"],
+                [41, "GPIO41 (MISO ETH)"],
+                [42, "GPIO42 (CS ETH)"],
+                [43, "GPIO43 (RST ETH)"],
+                [44, "GPIO44 (INT ETH)"],
+                [45, "GPIO45 ({#PIN_DONT_USE} - STRAPPING PIN)"],
+                [46, "GPIO46 ({#PIN_DONT_USE} - STRAPPING PIN)"],
+                [47, "GPIO47 (IRQ NRF24)"],
+                [48, "GPIO48 (MISO NRF24)"],
+            ];
+            /*ENDIF_ESP32-S3*/
+            /*IF_ESP32-C3*/
             var esp32c3pins = [
                 [255, "off / default"],
                 [0,  "GPIO0"],
@@ -469,6 +526,7 @@
                 [20, "GPIO20 (RX)"],
                 [21, "GPIO21 (TX)"],
             ];
+            /*ENDIF_ESP32-C3*/
             /*ENDIF_ESP32*/
             var nrfPa = [
                 [0, "MIN ({#PIN_RECOMMENDED})"],
@@ -549,12 +607,6 @@
                 setTimeout(function() {getAjax('/api/index', apiCbNtp2)}, 2000)
             }
 
-            function scan() {
-                var obj = {cmd: "scan_wifi", token: "*"}
-                getAjax("/api/setup", apiCbWifi, "POST", JSON.stringify(obj));
-                setTimeout(function() {getAjax('/api/setup/networks', listNetworks)}, 5000);
-            }
-
             function syncTime() {
                 var obj = {cmd: "sync_ntp", token: "*"}
                 getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj))
@@ -566,6 +618,22 @@
                 getAjax("/api/setup", apiCbMqtt, "POST", JSON.stringify(obj));
             }
 
+            document.addEventListener('DOMContentLoaded', () => {
+                const fileInput = document.querySelector('#importFileInput');
+                const button = document.querySelector('#importButton');
+                button.disabled = true;
+                button.title = "Please select a file first";
+                fileInput.addEventListener('change', () => {
+                    if (fileInput.value) {
+                        button.disabled = false;
+                        button.title = "";
+                    } else {
+                        button.disabled = true;
+                        button.title = "Please select a file first";
+                    }
+                });
+            });
+
             function hide() {
                 document.getElementById("form").submit();
                 var e = document.getElementById("content");
@@ -605,53 +673,55 @@
             }
 
             function ivGlob(obj) {
-                for(var i of [["invInterval", "interval"], ["yldEff", "yldEff"]])
+                for(var i of [["invInterval", "interval"]])
                     document.getElementsByName(i[0])[0].value = obj[i[1]];
-                for(var i of ["Mid", "ComStop", "NotAvail", "MaxMid"])
+                for(var i of ["Mid", "ComStop", "ComStart", "NotAvail", "MaxMid"])
                     document.getElementsByName("invRst"+i)[0].checked = obj["rst" + i];
                 document.getElementsByName("strtWthtTm")[0].checked = obj["strtWthtTm"];
                 document.getElementsByName("rdGrid")[0].checked = obj["rdGrid"];
             }
 
             function parseSys(obj) {
-                for(var i of [["device", "device_name"], ["ssid", "ssid"], ["ap_pwd", "ap_pwd"]])
-                    document.getElementsByName(i[0])[0].value = obj[i[1]];
-                document.getElementsByName("hidd")[0].checked = obj["hidd"];
-                document.getElementsByName("darkMode")[0].checked = obj["dark_mode"];
-                document.getElementsByName("schedReboot")[0].checked = obj["sched_reboot"];
+                document.getElementsByName("device")[0].value = obj.device_name;
+                for(var i of [["ssid", "ssid"], ["ap_pwd", "ap_pwd"]])
+                    document.getElementsByName(i[0])[0].value = obj.network[i[1]];
+                document.getElementsByName("hidd")[0].checked = obj.network.hidd;
+
+                document.getElementsByName("darkMode")[0].checked = obj.dark_mode;
+                document.getElementsByName("schedReboot")[0].checked = obj.sched_reboot;
                 e = document.getElementsByName("adminpwd")[0];
-                if(!obj["pwd_set"])
+                if(!obj.pwd_set)
                     e.value = "";
                 var d = document.getElementById("prot_mask");
                 var a = ["Index", "{#NAV_LIVE}", "{#NAV_WEBSERIAL}", "{#NAV_SETTINGS}", "Update", "System", "{#NAV_HISTORY}"];
                 var el = [];
                 for(var i = 0; i < 7; i++) {
-                    var chk = ((obj["prot_mask"] & (1 << i)) == (1 << i));
+                    var chk = ((obj.prot_mask & (1 << i)) == (1 << i));
                     el.push(mlCb("protMask" + i, a[i], chk))
                 }
                 d.append(...el);
-
-                var tz = []
-                for(i = 0; i < 24; i += 0.5)
-                    tz.push([i, ((i-12 > 0) ? "+" : "") + String(i-12)]);
-                document.getElementById("timezone").append(sel("timezone", tz, obj.timezone + 12))
-                var region = [[0, "Europe (860 - 870 MHz)"], [1, "USA, Indonesia (905 - 925 MHz)"], [2, "Brazil (915 - 928 MHz)"]]
-                document.getElementById("region").append(sel("region", region, obj.region))
-
             }
 
             function parseGeneric(obj) {
-                parseNav(obj);
-                parseESP(obj);
-                parseRssi(obj);
+                parseNav(obj)
+                parseESP(obj)
+                parseRssi(obj)
+                parseTitle(obj)
 
                 if(0 != obj.cst_lnk.length) {
                     document.getElementsByName("cstLnk")[0].value = obj.cst_lnk
                     document.getElementsByName("cstLnkTxt")[0].value = obj.cst_lnk_txt
                 }
 
-                ts = obj["ts_now"];
+                ts = obj.ts_now;
                 window.setInterval("tick()", 1000);
+
+                var tz = []
+                for(i = 0; i < 24; i += 0.5)
+                    tz.push([i, ((i-12 > 0) ? "+" : "") + String(i-12)]);
+                document.getElementById("timezone").append(sel("timezone", tz, obj.timezone + 12))
+                var region = [[0, "Europe (860 - 870 MHz)"], [1, "USA, Indonesia (905 - 925 MHz)"], [2, "Brazil (915 - 928 MHz)"]]
+                document.getElementById("region").append(sel("region", region, obj.region))
             }
 
             function parseStaticIp(obj) {
@@ -730,7 +800,7 @@
                 cbEn.checked = (obj.enabled);
                 cbDisNightCom.checked = (obj.disnightcom);
 
-                var ser = ml("input", {name: "ser", class: "text", type: "number", max: 138999999999, value: obj.serial}, null);
+                var ser = ml("input", {name: "ser", class: "text", type: "text", pattern: "[0-9a-fA-F]{12}", value: obj.serial}, null);
                 var html = ml("div", {}, [
                     tabs(["{#TAB_GENERAL}", "{#TAB_INPUTS}", "{#TAB_RADIO}", "{#TAB_ADVANCED}"]),
                     ml("div", {id: "div{#TAB_GENERAL}", class: "tab-content"}, [
@@ -786,7 +856,8 @@
                             case 0x1000: nrf = true; break;
                             case 0x1100:
                                 switch(sn & 0x000f) {
-                                    case 0x0004: nrf = false; break;
+                                    case 0x0004:
+                                    case 0x0005: nrf = false; break;
                                     default:     nrf = true; break;
                                 }
                                 break;
@@ -802,11 +873,16 @@
                 ser.dispatchEvent(new Event('change'));
 
                 function ivSave() {
-                    var o = new Object();
+                    var o = {}
                     o.cmd  = "save_iv"
                     o.token = "*"
                     o.id   = obj.id
-                    o.ser  = parseInt(document.getElementsByName("ser")[0].value, 16);
+
+                    let sn = document.getElementsByName("ser")[0].value
+                    if(sn[0] == 'A')
+                        sn = convHerf(sn)
+                    o.ser  = parseInt(sn, 16)
+
                     o.name = document.getElementsByName("name")[0].value;
                     o.en   = document.getElementsByName("enable")[0].checked;
                     o.ch = [];
@@ -826,6 +902,30 @@
                     getAjax("/api/setup", cb, "POST", JSON.stringify(o));
                 }
 
+                function convHerf(sn) {
+                    let sn_int = 0n;
+                    const CHARS = "0123456789ABCDEFGHJKLMNPRSTUVWXY";
+
+                    for (let i = 0; i < 9; ++i) {
+                        const pos = CHARS.indexOf(sn[i])
+                        const shift = 42 - 5 * i - (i <= 2 ? 0 : 2)
+                        sn_int |= BigInt(pos) << BigInt(shift)
+                    }
+
+                    let first4Hex = (sn_int >> 32n) & 0xFFFFn
+
+                    if (first4Hex === 0x2841n)
+                        first4Hex = 0x1121n
+                    else if (first4Hex === 0x2821n)
+                        first4Hex = 0x1141n
+                    else if (first4Hex === 0x2801n)
+                        first4Hex = 0x1161n
+
+                    sn_int = (sn_int & ~(0xFFFFn << 32n)) | (first4Hex << 32n);
+
+                    return sn_int.toString(16)
+                }
+
                 function cb(obj2) {
                     var e = document.getElementById("res");
                     if(!obj2.success)
@@ -847,6 +947,7 @@
                 function del() {
                     var o = new Object();
                     o.cmd  = "save_iv";
+                    o.token = "*"
                     o.id   = obj.id;
                     o.ser  = 0;
                     o.name = "";
@@ -873,6 +974,8 @@
             function parseMqtt(obj) {
                 for(var i of [["Addr", "broker"], ["Port", "port"], ["ClientId", "clientId"], ["User", "user"], ["Pwd", "pwd"], ["Topic", "topic"], ["Interval", "interval"]])
                     document.getElementsByName("mqtt"+i[0])[0].value = obj[i[1]];
+                document.getElementsByName("mqttJson")[0].checked = obj["json"];
+                document.getElementsByName("retain")[0].checked = obj.retain
             }
 
             function parseNtp(obj) {
@@ -891,13 +994,21 @@
                 }
             }
 
-            function parsePinout(obj, type, system) {
+            function parsePinout(obj) {
                 var e = document.getElementById("pinout");
-                var pinList = esp8266pins;
                 /*IF_ESP32*/
                 var pinList = esp32pins;
-                if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins;
-                else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins;
+                /*IF_ESP32-S2*/
+                pinList = esp32sXpins;
+                /*ENDIF_ESP32-S2*/
+                /*IF_ESP32-S3*/
+                pinList = esp32sXpins;
+                /*ENDIF_ESP32-S3*/
+                /*IF_ESP32-C3*/
+                pinList = esp32c3pins;
+                /*ENDIF_ESP32-C3*/
+                /*ELSE*/
+                var pinList = esp8266pins;
                 /*ENDIF_ESP32*/
                 pins = [['led0', 'pinLed0', '{#LED_AT_LEAST_ONE_PRODUCING}'], ['led1', 'pinLed1', '{#LED_MQTT_CONNECTED}'], ['led2', 'pinLed2', '{#LED_NIGHT_TIME}']];
                 for(p of pins) {
@@ -924,16 +1035,24 @@
                 )
             }
 
-            function parseNrfRadio(obj, objPin, type, system) {
+            function parseNrfRadio(obj, objPin) {
                 var e = document.getElementById("rf24");
                 var en = inp("nrfEnable", null, null, ["cb"], "nrfEnable", "checkbox");
                 en.checked = obj["en"];
 
-                var pinList = esp8266pins;
                 /*IF_ESP32*/
                 var pinList = esp32pins;
-                if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins;
-                else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins;
+                /*IF_ESP32-S2*/
+                pinList = esp32sXpins;
+                /*ENDIF_ESP32-S2*/
+                /*IF_ESP32-S3*/
+                pinList = esp32sXpins;
+                /*ENDIF_ESP32-S3*/
+                /*IF_ESP32-C3*/
+                pinList = esp32c3pins;
+                /*ENDIF_ESP32-C3*/
+                /*ELSE*/
+                var pinList = esp8266pins;
                 /*ENDIF_ESP32*/
 
                 e.replaceChildren (
@@ -943,11 +1062,11 @@
                     ])
                 );
 
-                if ("ESP8266" == type) {
-                    pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq']];
-                } else {
-                    pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['sclk', 'pinSclk'], ['mosi', 'pinMosi'], ['miso', 'pinMiso']];
-                }
+                /*IF_ESP32*/
+                var pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['sclk', 'pinSclk'], ['mosi', 'pinMosi'], ['miso', 'pinMiso']];
+                /*ELSE*/
+                var pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq']];
+                /*ENDIF_ESP32*/
                 for(p of pins) {
                     e.append(
                         ml("div", {class: "row mb-3"}, [
@@ -961,15 +1080,21 @@
             }
 
             /*IF_ESP32*/
-            function parseCmtRadio(obj, type, system) {
+            function parseCmtRadio(obj) {
                 var e = document.getElementById("cmt");
                 var en = inp("cmtEnable", null, null, ["cb"], "cmtEnable", "checkbox");
                 var pinList = esp32pins;
-                if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins;
-                else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins;
+                /*IF_ESP32-S2*/
+                pinList = esp32sXpins;
+                /*ENDIF_ESP32-S2*/
+                /*IF_ESP32-S3*/
+                pinList = esp32sXpins;
+                /*ENDIF_ESP32-S3*/
+                /*IF_ESP32-C3*/
+                pinList = esp32c3pins;
+                /*ENDIF_ESP32-C3*/
 
                 en.checked = obj["en"];
-
                 e.replaceChildren (
                     ml("div", {class: "row mb-3"}, [
                         ml("div", {class: "col-8 col-sm-3 my-2"}, "{#CMT_ENABLE}"),
@@ -996,6 +1121,42 @@
             }
             /*ENDIF_ESP32*/
 
+            /*IF_ETHERNET*/
+            function parseEth(obj) {
+                var e = document.getElementById("eth");
+                var en = inp("ethEn", null, null, ["cb"], "ethEn", "checkbox");
+                var pinList = esp32pins;
+                /*IF_ESP32-S2*/
+                pinList = esp32sXpins;
+                /*ENDIF_ESP32-S2*/
+                /*IF_ESP32-S3*/
+                pinList = esp32sXpins;
+                /*ENDIF_ESP32-S3*/
+                /*IF_ESP32-C3*/
+                pinList = esp32c3pins;
+                /*ENDIF_ESP32-C3*/
+
+                en.checked = obj["en"];
+                e.replaceChildren (
+                    ml("div", {class: "row mb-3"}, [
+                        ml("div", {class: "col-8 col-sm-3 my-2"}, "{#ETH_ENABLE}"),
+                        ml("div", {class: "col-4 col-sm-9"}, en)
+                    ])
+                );
+                pins = [['cs', 'ethCs'], ['sclk', 'ethSclk'], ['miso', 'ethMiso'], ['mosi', 'ethMosi'], ['irq', 'ethIrq'], ['reset', 'ethRst']];
+                for(p of pins) {
+                    e.append(
+                        ml("div", {class: "row mb-3"}, [
+                            ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
+                            ml("div", {class: "col-12 col-sm-9"},
+                                sel(p[1], pinList, obj[p[0]])
+                            )
+                        ])
+                    );
+                }
+            }
+            /*ENDIF_ETHERNET*/
+
             function parseSerial(obj) {
                 var e = document.getElementById("serialCb")
                 var l = [["serEn", "show_live_data", "{#LOG_PRINT_INVERTER_DATA}"], ["serDbg", "debug", "{#LOG_SERIAL_DEBUG}"], ["priv", "priv", "{#LOG_PRIVACY_MODE}"], ["wholeTrace", "wholeTrace", "{#LOG_PRINT_TRACES}"], ["log2mqtt", "log2mqtt", "{#LOG_TO_MQTT}"]]
@@ -1011,12 +1172,21 @@
                 }
             }
 
+            /*IF_PLUGIN_DISPLAY*/
             function parseDisplay(obj, type, system) {
-                var pinList = esp8266pins;
                 /*IF_ESP32*/
                 var pinList = esp32pins;
-                if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins;
-                else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins;
+                /*IF_ESP32-S2*/
+                pinList = esp32sXpins;
+                /*ENDIF_ESP32-S2*/
+                /*IF_ESP32-S3*/
+                pinList = esp32sXpins;
+                /*ENDIF_ESP32-S3*/
+                /*IF_ESP32-C3*/
+                pinList = esp32c3pins;
+                /*ENDIF_ESP32-C3*/
+                /*ELSE*/
+                var pinList = esp8266pins;
                 /*ENDIF_ESP32*/
 
                 for(var i of ["disp_pwr"])
@@ -1152,6 +1322,7 @@
                 setHide("screenSaver",     !optionsMap.get(dispType)[2]);
                 setHide("pirPin",          !(optionsMap.get(dispType)[2] && (screenSaver==2))); // show pir pin only for motion screensaver
             }
+            /*ENDIF_PLUGIN_DISPLAY*/
 
             function tick() {
                 document.getElementById("date").innerHTML = toIsoDateStr((new Date((++ts) * 1000)));
@@ -1159,42 +1330,28 @@
 
             function parse(root) {
                 if(null != root) {
-                    parseGeneric(root["generic"]);
-                    parseSys(root["system"]);
-                    parseStaticIp(root["static_ip"]);
-                    parseMqtt(root["mqtt"]);
-                    parseNtp(root["ntp"]);
-                    parseSun(root["sun"]);
-                    parsePinout(root["pinout"], root["system"]["esp_type"], root["system"]);
-                    parseNrfRadio(root["radioNrf"], root["pinout"], root["system"]["esp_type"], root["system"]);
+                    parseGeneric(root.generic);
+                    parseSys(root.system);
+                    parseStaticIp(root.static_ip);
+                    parseMqtt(root.mqtt);
+                    parseNtp(root.ntp);
+                    parseSun(root.sun);
+                    parsePinout(root.pinout);
+                    parseNrfRadio(root.radioNrf, root.pinout);
                     /*IF_ESP32*/
-                    parseCmtRadio(root["radioCmt"], root["system"]["esp_type"], root["system"]);
+                    parseCmtRadio(root.radioCmt);
                     /*ENDIF_ESP32*/
-                    parseSerial(root["serial"]);
-                    parseDisplay(root["display"], root["system"]["esp_type"], root["system"]);
+                    /*IF_ETHERNET*/
+                    parseEth(root.eth)
+                    /*ENDIF_ETHERNET*/
+                    parseSerial(root.serial);
+                    /*IF_PLUGIN_DISPLAY*/
+                    parseDisplay(root.display, root.system.esp_type, root.system);
+                    /*ENDIF_PLUGIN_DISPLAY*/
                     getAjax("/api/inverter/list", parseIv);
                 }
             }
 
-            function listNetworks(root) {
-                var s = document.getElementById("networks");
-                selDelAllOpt(s);
-                if(root["networks"].length > 0) {
-                    s.appendChild(opt("-1", "{#NETWORK_PLEASE_SELECT}"));
-                    for(i = 0; i < root["networks"].length; i++) {
-                        s.appendChild(opt(root["networks"][i]["ssid"], root["networks"][i]["ssid"] + " (" + root["networks"][i]["rssi"] + " dBm)"));
-                    }
-                } else
-                    s.appendChild(opt("-1", "{#NO_NETWORK_FOUND}"));
-            }
-
-            function selNet() {
-                var s = document.getElementById("networks");
-                var e = document.getElementsByName("ssid")[0];
-                if(-1 != s.value)
-                    e.value = s.value;
-            }
-
             getAjax("/api/setup", parse);
         </script>
     </body>
diff --git a/src/web/html/style.css b/src/web/html/style.css
index 2d6a03c7..6f05ea77 100644
--- a/src/web/html/style.css
+++ b/src/web/html/style.css
@@ -33,13 +33,17 @@ textarea {
     color: var(--fg2);
 }
 
-svg rect {fill: #00A;}
-svg.chart {
-    background: #f2f2f2;
-    border: 2px solid gray;
-    padding: 1px;
+svg polyline {
+	fill-opacity: .5;
+	stroke-width:	1;
+}
+
+svg text {
+	font-size: x-small;
+	fill: var(--chart-text);
 }
 
+
 div.chartDivContainer {
     padding: 1px;
     margin: 1px;
@@ -139,7 +143,7 @@ svg.icon {
     background-color: var(--nav-active);
 }
 
-span.seperator {
+span.separator {
     width: 100%;
     height: 1px;
     margin: 5px 0 5px;
@@ -559,7 +563,13 @@ input.btn {
     cursor: pointer;
 }
 
-input.btn:hover {
+input.btn:disabled {
+    background-color: var(--primary-disabled);
+    color: #888;
+    cursor: not-allowed;
+}
+
+input.btn:not(:disabled):hover {
     background-color: #044e86;
 }
 
@@ -677,7 +687,7 @@ div.hr {
     border-radius: 3px;
     display: inline-block;
     position: absolute;
-    transform: translate(-50%,-100%);
+    transform: translate(-50%,-50%);
     margin:0 auto;
     color: var(--fg2);
     min-width: 100px;
@@ -748,6 +758,7 @@ div.hr {
     font-family: inherit;
     cursor: pointer;
     padding: 0;
+    color: var(--fg);
 }
 
 button.close {
@@ -848,3 +859,16 @@ ul {
     height: 100%;
     overflow: auto;
 }
+
+.progress {
+    display: flex;
+    height: 1rem;
+    overflow: hidden;
+    background-color: #e9ecef;
+    border-radius: .25rem;
+}
+
+.progress-bar {
+    display: flex;
+    background-color: var(--primary);
+}
diff --git a/src/web/html/system.html b/src/web/html/system.html
index a646e8b8..477b3a7e 100644
--- a/src/web/html/system.html
+++ b/src/web/html/system.html
@@ -8,29 +8,44 @@
         {#HTML_NAV}
         <div id="wrapper">
             <div id="content">
-                <div id="info" class="col-sm-12 col-md-6 mt-3"></div>
+                <div id="info" class="col-sm-12 col-md-10 mt-3"></div>
                 <div id="html" class="mt-3 mb-3"></div>
             </div>
         </div>
         {#HTML_FOOTER}
         <script type="text/javascript">
             function parseGeneric(obj) {
-                parseNav(obj);
-                parseESP(obj);
-                parseRssi(obj);
+                parseNav(obj)
+                parseESP(obj)
+                parseRssi(obj)
+                parseTitle(obj)
+            }
+
+            function parseUptime(up) {
+                var days = parseInt(up / 86400) % 365
+                var hrs  = parseInt(up / 3600) % 24
+                var min  = parseInt(up / 60) % 60
+                var sec  = up % 60
+                var str = days + " day"
+                if(1 != days)
+                    str += "s"
+                str += ", " + ("0"+hrs).substr(-2) + ":"
+                   + ("0"+min).substr(-2) + ":"
+                   + ("0"+sec).substr(-2)
+
+                return  ml("span", {}, str)
             }
 
             function parseSysInfo(obj) {
-                const data = ["sdk", "cpu_freq", "chip_revision",
-                    "chip_model", "chip_cores", "esp_type", "mac", "wifi_rssi", "ts_uptime",
-                    "flash_size", "sketch_used", "heap_total", "heap_free", "heap_frag",
-                    "max_free_blk", "version", "modules", "env", "core_version", "reboot_reason"];
-
-                lines = [];
-                for (const [key, value] of Object.entries(obj)) {
-                    if(!data.includes(key) || (typeof value == 'undefined')) continue;
-                    lines.push(tr(key.replace('_', ' '), value));
-                }
+                lines = [
+                    tr("{#DEVICE_NAME}", obj.device_name),
+                    tr("{#UPTIME}", parseUptime(obj.generic.ts_uptime)),
+                    tr("{#REBOOT_REASON}", obj.chip.reboot_reason),
+                    tr("{#ENVIRONMENT}", obj.generic.env + " ({#BUILD_OPTIONS}: " + obj.generic.modules + ")"),
+                    tr("Version", obj.generic.version + " - " + obj.generic.build),
+                    tr("Chip", "CPU: " + obj.chip.cpu_freq + "MHz, " + obj.chip.cores + " Core(s)"),
+                    tr("Chip Model", obj.chip.model)
+                ]
 
                 document.getElementById("info").append(
                     headline("System Information"),
@@ -46,10 +61,10 @@
 
             function irqBadge(state) {
                 switch(state) {
-                    case 0: return badge(false, "{#UNKNOWN}", "warning"); break;
-                    case 1: return badge(true, "{#TRUE}"); break;
-                    default: return badge(false, "{#FALSE}"); break;
-                }                    
+                    case 0: return badge(false, "unknown", "warning"); break;
+                    case 1: return badge(true, "true"); break;
+                    default: return badge(false, "false"); break;
+                }
             }
 
             function parseRadio(obj) {
@@ -58,15 +73,15 @@
                 if(obj.radioNrf.en) {
                     lines = [
                         tr("NRF24L01", badge(obj.radioNrf.isconnected, ((obj.radioNrf.isconnected) ? "" : "{#NOT} ") + "{#CONNECTED}")),
-                        tr("{#IRQ_WORKING}", irqBadge(obj.radioNrf.irqOk)),
-                        tr("{#NRF24_DATA_RATE}", dr[obj.radioNrf.dataRate] + "bps"),
-                        tr("DTU Radio ID", obj.radioNrf.sn)
+                        tr("{#INTR_PIN_WORKING}", irqBadge(obj.radioNrf.irqOk)),
+                        tr("NRF24 {#DATA_RATE}", dr[obj.radioNrf.dataRate] + "bps"),
+                        tr("DTU {#RADIO} ID", obj.radioNrf.sn)
                     ];
                 } else
-                    lines = [tr("NRF24L01", badge(false, "{#NOT_ENABLED}"))];
+                    lines = [tr("NRF24L01", badge(false, "{#NOT} {#ENABLED}"))];
 
                 document.getElementById("info").append(
-                    headline("{#NRF24_RADIO}"),
+                    headline("{#RADIO} NRF24"),
                     ml("table", {class: "table"},
                         ml("tbody", {}, lines)
                     )
@@ -76,14 +91,14 @@
                 if(obj.radioCmt.en) {
                     cmt = [
                         tr("CMT2300A", badge(obj.radioCmt.isconnected, ((obj.radioCmt.isconnected) ? "" : "{#NOT} ") + "{#CONNECTED}")),
-                        tr("{#IRQ_WORKING}", irqBadge(obj.radioCmt.irqOk)),
-                        tr("DTU Radio ID", obj.radioCmt.sn)
+                        tr("{#INTR_PIN_WORKING}", irqBadge(obj.radioCmt.irqOk)),
+                        tr("DTU {#RADIO} ID", obj.radioCmt.sn)
                     ];
                 } else
-                    cmt = [tr("CMT2300A", badge(false, "{#NOT_ENABLED}"))];
+                    cmt = [tr("CMT2300A", badge(false, "{#NOT} {#ENABLED}"))];
 
                 document.getElementById("info").append(
-                    headline("{#CMT_RADIO}"),
+                    headline("{#RADIO} CMT"),
                     ml("table", {class: "table"},
                         ml("tbody", {}, cmt)
                     )
@@ -91,16 +106,32 @@
                 /*ENDIF_ESP32*/
             }
 
+            function parseNetwork(obj, gen) {
+                lines = [
+                    tr("{#CONNECTION}", ((obj.wired) ? "{#WIRED}" : "{#WIFI} (SSID: " + obj.ssid + ", RSSI: " + gen.wifi_rssi + ", CH: " + obj.wifi_channel + ")")),
+                    tr("Hostname", gen.host),
+                    tr("IP {#ADDRESS}", obj.ip),
+                    tr("MAC {#ADDRESS}", obj.mac)
+                ]
+
+                document.getElementById("info").append(
+                    headline("{#NETWORK}"),
+                    ml("table", {class: "table"},
+                        ml("tbody", {}, lines)
+                    )
+                );
+            }
+
             function parseMqtt(obj) {
                 if(obj.enabled) {
                     lines = [
-                        tr("{#CONNECTED}", badge(obj.connected, ((obj.connected) ? "{#TRUE}" : "{#FALSE}"))),
+                        tr("{#CONNECTED}", badge(obj.connected, ((obj.connected) ? "true" : "false"))),
                         tr("#TX", obj.tx_cnt),
                         tr("#RX", obj.rx_cnt)
-                    ];
+                    ]
 
                 } else
-                    lines = tr("{#ENABLED}", badge(false, "{#FALSE}"));
+                    lines = tr("enabled", badge(false, "false"));
 
                 document.getElementById("info").append(
                     headline("MqTT"),
@@ -110,17 +141,34 @@
                 );
             }
 
+            function parseMemory(obj) {
+                lines = [
+                    tr("{#FLASH_SIZE}", obj.flash_size / 1024 / 1024 + "MB"),
+                    tr("{#CONFIG_PARTITION} (" + Math.round(obj.par_used_spiffs / 1024) + "kB of " + obj.par_size_spiffs / 1024 + "kB)", progress(obj.par_used_spiffs / obj.par_size_spiffs * 100)),
+                    tr("{#FIRMWARE_PARTITION} (" + Math.round(obj.par_used_app0 / 1024) + "kB of " + obj.par_size_app0 / 1024 + "kB)", progress(obj.par_used_app0 / obj.par_size_app0 * 100)),
+                    tr("Heap (" + Math.round(obj.heap_free / 1024) + "kB of " + Math.round(obj.heap_total / 1024) + "kB)", progress(obj.heap_free / obj.heap_total * 100)),
+                    tr("Heap {#MAX_FREE_BLOCK}", Math.round(obj.heap_max_free_blk / 1024) + "kB (Fragmentation: " + obj.heap_frag + ")")
+                ]
+
+                document.getElementById("info").append(
+                    headline("{#MEMORY}"),
+                    ml("table", {class: "table"},
+                        ml("tbody", {}, lines)
+                    )
+                );
+            }
+
             function parseIndex(obj) {
                 if(obj.ts_sunrise > 0) {
                     document.getElementById("info").append(
-                        headline("{#SUN}"),
+                        headline("Sun"),
                         ml("table", {class: "table"},
                             ml("tbody", {}, [
                                 tr("{#SUNRISE}", new Date(obj.ts_sunrise * 1000).toLocaleString('de-DE')),
                                 tr("{#SUNSET}", new Date(obj.ts_sunset * 1000).toLocaleString('de-DE')),
                                 tr("{#COMMUNICATION_START}", new Date((obj.ts_sunrise + obj.ts_offsSr) * 1000).toLocaleString('de-DE')),
                                 tr("{#COMMUNICATION_STOP}", new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')),
-                                tr("{#NIGHT_BEHAVE}", badge(obj.disNightComm, ((obj.disNightComm) ? "{#NOT}" : "") + " {#COMMUNICATING}", "warning"))
+                                tr("{#NIGHT_BEHAVIOR}", badge(obj.disNightComm, ((obj.disNightComm) ? "{#NOT}" : "") + " {#COMMUNICATING}", "warning"))
                             ])
                         )
                     );
@@ -137,10 +185,12 @@
                         meta.content = obj.refresh + "; URL=" + obj.refresh_url;
                         document.getElementsByTagName('head')[0].appendChild(meta);
                     } else if(null != obj.system) {
-                        parseRadio(obj.system);
-                        parseMqtt(obj.system.mqtt);
-                        parseSysInfo(obj.system);
-                        getAjax('/api/index', parseIndex);
+                        parseRadio(obj.system)
+                        parseNetwork(obj.system.network, obj.system.generic)
+                        parseMqtt(obj.system.mqtt)
+                        parseMemory(obj.system.memory)
+                        parseSysInfo(obj.system)
+                        getAjax('/api/index', parseIndex)
                     }
                     document.getElementById("html").innerHTML = obj.html;
                 }
diff --git a/src/web/html/update.html b/src/web/html/update.html
index 52ace5f1..4b670d45 100644
--- a/src/web/html/update.html
+++ b/src/web/html/update.html
@@ -12,22 +12,39 @@
                     <legend class="des">{#SELECT_FILE} (*.bin)</legend>
                     <p>{#INSTALLED_VERSION}:<br/><span id="version" style="background-color: var(--input-bg); padding: 7px; display: block; margin: 3px;"></span></p>
                     <form id="form" method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8">
-                        <input type="file" name="update">
-                        <input type="button" class="btn my-4" value="{#BTN_UPDATE}" onclick="hide()">
+                        <input type="file" id="uploadFileInput" name="update">
+                        <input type="button" id="uploadButton" class="btn my-4" value="{#BTN_UPDATE}" onclick="hide()">
                     </form>
                 </fieldset>
                 <div class="row mt-4">
-                    <a href="https://fw.ahoydtu.de" target="_blank">{#DOWNLOADS}<a/>
+                    <a href="https://fw.ahoydtu.de" target="_blank">{#DOWNLOADS}</a>
                 </div>
             </div>
         </div>
         {#HTML_FOOTER}
         <script type="text/javascript">
+            document.addEventListener('DOMContentLoaded', () => {
+                const fileInput = document.querySelector('#uploadFileInput');
+                const button = document.querySelector('#uploadButton');
+                button.disabled = true;
+                button.title = "Please select a file first";
+                fileInput.addEventListener('change', () => {
+                    if (fileInput.value) {
+                        button.disabled = false;
+                        button.title = "";
+                    } else {
+                        button.disabled = true;
+                        button.title = "Please select a file first";
+                    }
+                });
+            });
+
             var env;
             function parseGeneric(obj) {
                 parseNav(obj)
                 parseESP(obj)
                 parseRssi(obj)
+                parseTitle(obj)
                 env = obj.env
                 document.getElementById("version").innerHTML = "{#VERSION_FULL}_" + obj.env + ".bin"
             }
diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html
index 81962add..507a6f9f 100644
--- a/src/web/html/visualization.html
+++ b/src/web/html/visualization.html
@@ -21,6 +21,7 @@
             var mNum = 0;
             var total = Array(6).fill(0);
             var tPwrAck;
+            var totalsRendered = false
 
             function getErrStr(code) {
                 if("ERR_AUTH") return "{#ERR_AUTH}"
@@ -33,10 +34,11 @@
 
             function parseGeneric(obj) {
                 if(true == exeOnce){
-                    parseNav(obj);
-                    parseESP(obj);
+                    parseNav(obj)
+                    parseESP(obj)
+                    parseTitle(obj)
                 }
-                parseRssi(obj);
+                parseRssi(obj)
             }
 
             function numBig(val, unit, des) {
@@ -54,11 +56,11 @@
                 ]);
             }
 
-            function numMid(val, unit, des, opt={class: "fs-6"}) {
+            function numMid(val, unit, des, opt={class: "row"}) {
                 return ml("div", {class: "col-6 col-sm-4 col-md-3 mb-2"}, [
-                    ml("div", {class: "row"},
+                    ml("div", opt,
                         ml("div", {class: "col"}, [
-                            ml("span", opt, String(Math.round(val * 100) / 100)),
+                            ml("span", {class: "fs-6"}, String(Math.round(val * 100) / 100)),
                             ml("span", {class: "fs-8 mx-1"}, unit)
                         ])
                     ),
@@ -74,6 +76,7 @@
                 for(var i = 0; i < 6; i++) {
                     total[i] = Math.round(total[i] * 100) / 100;
                 }
+                totalsRendered = true
 
                 return ml("div", {class: "row mt-3 mb-5"},
                     ml("div", {class: "col"}, [
@@ -104,7 +107,6 @@
                     total[4] += obj.ch[0][8]; // P_DC
                     total[5] += obj.ch[0][10]; // Q_AC
                 }
-                total[3] += obj.ch[0][11]; // MAX P_AC
                 total[1] += obj.ch[0][7]; // YieldDay
                 total[2] += obj.ch[0][6]; // YieldTotal
 
@@ -116,10 +118,11 @@
                 if(65535 != obj.power_limit_read) {
                     pwrLimit = obj.power_limit_read + "&nbsp;%";
                     if(0 != obj.max_pwr)
-                        pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + "&nbsp;W";
+                        pwrLimit += ", " + (obj.max_pwr * obj.power_limit_read / 100).toFixed(1) + "&nbsp;W";
                 }
 
-                var maxAcPwr = toIsoDateStr(new Date(obj.ts_max_ac_pwr * 1000));
+                var maxAcPwrDate = toIsoDateStr(new Date(obj.ts_max_ac_pwr * 1000))
+                var maxTempDate = toIsoDateStr(new Date(obj.ts_max_temp * 1000))
                 return ml("div", {class: "row mt-2"},
                     ml("div", {class: "col"}, [
                         ml("div", {class: "p-2 " + clh},
@@ -134,7 +137,7 @@
                                 ml("div", {class: "col a-c"}, ml("span", { class: "pointer", onclick: function() {
                                     getAjax("/api/inverter/alarm/" + obj.id, parseIvAlarm);
                                 }}, ("{#ALARMS}: " + obj.alarm_cnt))),
-                                ml("div", {class: "col a-r mx-2 mx-md-1"}, String(obj.ch[0][5].toFixed(1)) + t.innerText)
+                                ml("div", {class: "col a-r mx-2 mx-md-1 tooltip", data: (obj.ch[0][12] + t.innerText + "\n" + maxTempDate)}, String(obj.ch[0][5].toFixed(1)) + t.innerText)
                             ])
                         ),
                         ml("div", {class: "p-2 " + clbg}, [
@@ -145,7 +148,7 @@
                             ]),
                             ml("div", {class: "hr"}),
                             ml("div", {class: "row mt-2"},[
-                                numMid(obj.ch[0][11], "W", "{#MAX_AC_POWER}", {class: "fs-6 tooltip", data: maxAcPwr}),
+                                numMid(obj.ch[0][11], "W", "{#MAX_AC_POWER}", {class: "row tooltip", data: maxAcPwrDate}),
                                 numMid(obj.ch[0][8], "W", "{#DC_POWER}"),
                                 numMid(obj.ch[0][0], "V", "{#AC_VOLTAGE}"),
                                 numMid(obj.ch[0][1], "A", "{#AC_CURRENT}"),
@@ -240,20 +243,18 @@
                     ])
                 );
 
-
-                var last = true;
                 for(var i = obj.id + 1; i < ivEn.length; i++) {
                     if((i != ivEn.length) && ivEn[i]) {
-                        last = false;
                         getAjax("/api/inverter/id/" + i, parseIv);
-                        break;
+                        return
                     }
                 }
-                if(last) {
-                    if(mNum > 1)
+
+                if(mNum > 1) {
+                    if(!totalsRendered)
                         mIvHtml.unshift(totals());
-                    document.getElementById("live").replaceChildren(...mIvHtml);
                 }
+                document.getElementById("live").replaceChildren(...mIvHtml);
             }
 
             function parseIvAlarm(obj) {
@@ -275,7 +276,7 @@
                                 ml("div", {class: "col mt-3"}, String(a.str)),
                                 ml("div", {class: "col mt-3"}, String(a.code)),
                                 ml("div", {class: "col mt-3"}, String(toIsoTimeStr(new Date((a.start + offs) * 1000)))),
-                                ml("div", {class: "col mt-3"}, String(toIsoTimeStr(new Date((a.end + offs) * 1000))))
+                                ml("div", {class: "col mt-3"}, (a.end == 0) ? "-" : String(toIsoTimeStr(new Date((a.end + offs) * 1000))))
                             ])
                         );
                     }
@@ -514,7 +515,9 @@
                     ivEn = Object.values(Object.assign({}, obj["iv"]));
                     mIvHtml = [];
                     mNum = 0;
+                    totalsRendered = false
                     total.fill(0);
+                    total[3] = obj.max_total_pwr
                     for(var i = 0; i < obj.iv.length; i++) {
                         if(obj.iv[i]) {
                             getAjax("/api/inverter/id/" + i, parseIv);
diff --git a/src/web/html/wizard.html b/src/web/html/wizard.html
index 3df44dc4..199f66db 100644
--- a/src/web/html/wizard.html
+++ b/src/web/html/wizard.html
@@ -4,7 +4,7 @@
         <title>{#NAV_WIZARD}</title>
         {#HTML_HEADER}
     </head>
-    <body>
+    <body onload="init()">
         <div id="wrapper">
             <div class="container d-flex aic jc">
                 <div id="con"></div>
@@ -14,6 +14,166 @@
             var v;
             var found = false;
             var c = document.getElementById("con");
+            var redirIp = "http://192.168.4.1/index"
+
+            /*IF_ESP32*/
+            var pinList = [
+                [255, "{#PIN_OFF}"],
+                [0,  "GPIO0"],
+                [1,  "TX (GPIO1)"],
+                [2,  "GPIO2 (LED)"],
+                [3,  "RX (GPIO3)"],
+                [4,  "GPIO4"],
+                [5,  "GPIO5"],
+                [12, "GPIO12 (HSPI MISO)"],
+                [13, "GPIO13 (HSPI MOSI)"],
+                [14, "GPIO14 (HSPI SCLK)"],
+                [15, "GPIO15"],
+                [16, "GPIO16"],
+                [17, "GPIO17"],
+                [18, "GPIO18 (VSPI SCLK)"],
+                [19, "GPIO19 (VSPI MISO)"],
+                [21, "GPIO21 (SDA)"],
+                [22, "GPIO22 (SCL)"],
+                [23, "GPIO23 (VSPI MOSI)"],
+                [25, "GPIO25"],
+                [26, "GPIO26"],
+                [27, "GPIO27"],
+                [32, "GPIO32"],
+                [33, "GPIO33"],
+                [34, "GPIO34 ({#PIN_INPUT_ONLY})"],
+                [35, "GPIO35 ({#PIN_INPUT_ONLY})"],
+                [36, "VP (GPIO36, {#PIN_INPUT_ONLY})"],
+                [39, "VN (GPIO39, {#PIN_INPUT_ONLY})"]
+            ];
+            /*IF_ESP32-S2*/
+            pinList = [
+                [255, "off / default"],
+                [0,  "GPIO0 ({#PIN_DONT_USE} - BOOT)"],
+                [1,  "GPIO1"],
+                [2,  "GPIO2"],
+                [3,  "GPIO3"],
+                [4,  "GPIO4"],
+                [5,  "GPIO5"],
+                [6,  "GPIO6"],
+                [7,  "GPIO7"],
+                [8,  "GPIO8"],
+                [9,  "GPIO9"],
+                [10, "GPIO10"],
+                [11, "GPIO11"],
+                [12, "GPIO12"],
+                [13, "GPIO13"],
+                [14, "GPIO14"],
+                [15, "GPIO15"],
+                [16, "GPIO16"],
+                [17, "GPIO17"],
+                [18, "GPIO18"],
+                [19, "GPIO19 ({#PIN_DONT_USE} - USB-)"],
+                [20, "GPIO20 ({#PIN_DONT_USE} - USB+)"],
+                [21, "GPIO21"],
+                [26, "GPIO26 (PSRAM - {#PIN_NOT_AVAIL})"],
+                [27, "GPIO27 (FLASH - {#PIN_NOT_AVAIL})"],
+                [28, "GPIO28 (FLASH - {#PIN_NOT_AVAIL})"],
+                [29, "GPIO29 (FLASH - {#PIN_NOT_AVAIL})"],
+                [30, "GPIO30 (FLASH - {#PIN_NOT_AVAIL})"],
+                [31, "GPIO31 (FLASH - {#PIN_NOT_AVAIL})"],
+                [32, "GPIO32 (FLASH - {#PIN_NOT_AVAIL})"],
+                [33, "GPIO33 (not exposed on S3-WROOM modules)"],
+                [34, "GPIO34 (not exposed on S3-WROOM modules)"],
+                [35, "GPIO35"],
+                [36, "GPIO36"],
+                [37, "GPIO37"],
+                [38, "GPIO38"],
+                [39, "GPIO39"],
+                [40, "GPIO40"],
+                [41, "GPIO41"],
+                [42, "GPIO42"],
+                [43, "GPIO43"],
+                [44, "GPIO44"],
+                [45, "GPIO45 ({#PIN_DONT_USE} - STRAPPING PIN)"],
+                [46, "GPIO46 ({#PIN_DONT_USE} - STRAPPING PIN)"],
+                [47, "GPIO47"],
+                [48, "GPIO48"],
+            ];
+            /*ENDIF_ESP32-S2*/
+            /*IF_ESP32-S3*/
+            pinList = [
+                [255, "off / default"],
+                [0,  "GPIO0 ({#PIN_DONT_USE} - BOOT)"],
+                [1,  "GPIO1"],
+                [2,  "GPIO2"],
+                [3,  "GPIO3"],
+                [4,  "GPIO4"],
+                [5,  "GPIO5"],
+                [6,  "GPIO6"],
+                [7,  "GPIO7"],
+                [8,  "GPIO8"],
+                [9,  "GPIO9"],
+                [10, "GPIO10"],
+                [11, "GPIO11"],
+                [12, "GPIO12"],
+                [13, "GPIO13"],
+                [14, "GPIO14"],
+                [15, "GPIO15"],
+                [16, "GPIO16"],
+                [17, "GPIO17"],
+                [18, "GPIO18"],
+                [19, "GPIO19 ({#PIN_DONT_USE} - USB-)"],
+                [20, "GPIO20 ({#PIN_DONT_USE} - USB+)"],
+                [21, "GPIO21"],
+                [26, "GPIO26 (PSRAM - {#PIN_NOT_AVAIL})"],
+                [27, "GPIO27 (FLASH - {#PIN_NOT_AVAIL})"],
+                [28, "GPIO28 (FLASH - {#PIN_NOT_AVAIL})"],
+                [29, "GPIO29 (FLASH - {#PIN_NOT_AVAIL})"],
+                [30, "GPIO30 (FLASH - {#PIN_NOT_AVAIL})"],
+                [31, "GPIO31 (FLASH - {#PIN_NOT_AVAIL})"],
+                [32, "GPIO32 (FLASH - {#PIN_NOT_AVAIL})"],
+                [33, "GPIO33 (not exposed on S3-WROOM modules)"],
+                [34, "GPIO34 (not exposed on S3-WROOM modules)"],
+                [35, "GPIO35"],
+                [36, "GPIO36"],
+                [37, "GPIO37"],
+                [38, "GPIO38"],
+                [39, "GPIO39"],
+                [40, "GPIO40"],
+                [41, "GPIO41"],
+                [42, "GPIO42"],
+                [43, "GPIO43"],
+                [44, "GPIO44"],
+                [45, "GPIO45 ({#PIN_DONT_USE} - STRAPPING PIN)"],
+                [46, "GPIO46 ({#PIN_DONT_USE} - STRAPPING PIN)"],
+                [47, "GPIO47"],
+                [48, "GPIO48"],
+            ];
+            /*ENDIF_ESP32-S3*/
+            /*IF_ESP32-C3*/
+            pinList = [
+                [255, "off / default"],
+                [0,  "GPIO0"],
+                [1,  "GPIO1"],
+                [2,  "GPIO2"],
+                [3,  "GPIO3"],
+                [4,  "GPIO4"],
+                [5,  "GPIO5"],
+                [6,  "GPIO6"],
+                [7,  "GPIO7"],
+                [8,  "GPIO8"],
+                [9,  "GPIO9"],
+                [10, "GPIO10"],
+                [11, "GPIO11"],
+                [12, "GPIO12 (PSRAM/FLASH)"],
+                [13, "GPIO13 (PSRAM/FLASH)"],
+                [14, "GPIO14 (PSRAM/FLASH)"],
+                [15, "GPIO15 (PSRAM/FLASH)"],
+                [16, "GPIO16 (PSRAM/FLASH)"],
+                [17, "GPIO17 (PSRAM/FLASH)"],
+                [18, "GPIO18 ({#PIN_DONT_USE} - USB-)"],
+                [19, "GPIO19 ({#PIN_DONT_USE} - USB+)"],
+                [20, "GPIO20 (RX)"],
+                [21, "GPIO21 (TX)"],
+            ];
+            /*ENDIF_ESP32-C3*/
+            /*ENDIF_ESP32*/
 
             function sect(e1, e2) {
                 return ml("div", {class: "row"}, [
@@ -22,7 +182,36 @@
                 ])
             }
 
-            function wifi() {
+            /*IF_ETHERNET*/
+            var pins = ['cs', 'sclk', 'miso', 'mosi', 'irq', 'reset']
+            function step1(obj) {
+                console.log(obj)
+                lst = []
+                for(p of pins) {
+                    lst.push(
+                        ml("div", {class: "row mb-3"}, [
+                            ml("div", {class: "col-12 col-sm-3 my-2"}, p.toUpperCase()),
+                            ml("div", {class: "col-12 col-sm-9"},
+                                sel(p, pinList, obj[p])
+                            )
+                        ])
+                    )
+                }
+                let en = inp("en", null, null, ["cb"], "en", "checkbox");
+                en.checked = obj["en"];
+
+                return sect("{#NETWORK_SETUP}", [
+                    ml("div", {class: "row mb-3"}, [
+                        ml("div", {class: "col-8"}, "{#ETH_ENABLE}"),
+                        ml("div", {class: "col-4"}, en)
+                    ]),
+                    ...lst,
+                    ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn", value: "{#BTN_REBOOT}", onclick: () => {saveEth()}}, null))),
+                    ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {onclick: () => {redirect()}}, "{#STOP_WIZARD}")))
+                ])
+            }
+            /*ENDIF_ETHERNET*/
+            function step1Wifi() {
                 return ml("div", {}, [
                     ml("div", {class: "row my-5"}, ml("div", {class: "col"}, ml("span", {class: "fs-1"}, "{#WELCOME}"))),
                     ml("div", {class: "row"}, ml("div", {class: "col"}, ml("span", {class: "fs-5"}, "{#NETWORK_SETUP}"))),
@@ -30,7 +219,7 @@
                     sect("{#WIFI_MANUAL}", ml("input", {id: "man", type: "text"})),
                     sect("{#WIFI_PASSWORD}", ml("input", {id: "pwd", type: "password"})),
                     ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn", value: "{#BTN_NEXT}", onclick: () => {saveWifi()}}, null))),
-                    ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {href: "http://192.168.4.1/"}, "{#STOP_WIZARD}")))
+                    ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {onclick: () => {redirect()}}, "{#STOP_WIZARD}")))
                 ])
             }
 
@@ -40,23 +229,35 @@
                     ml("div", {class: "row"}, ml("div", {class: "col"}, ml("span", {class: "fs-5"}, "{#TEST_CONNECTION}"))),
                     sect("{#TRY_TO_CONNECT}", ml("span", {id: "state"}, "{#CONNECTING}")),
                     ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn hide", id: "btn", value: "{#BTN_FINISH}", onclick: () => {redirect()}}, null))),
-                    ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {href: "http://192.168.4.1/"}, "{#STOP_WIZARD}")))
+                    ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {onclick: () => {redirect()}}, "{#STOP_WIZARD}")))
                 )
-                v = setInterval(() => {getAjax('/api/setup/getip', printIp)}, 2500);
+                v = setInterval(() => {getAjax('/api/setup/getip', printIp)}, 1000);
             }
 
             function redirect() {
-                window.location.replace("http://192.168.4.1/")
+                window.location.replace(redirIp)
             }
 
             function printIp(obj) {
-                if("0.0.0.0" != obj["ip"]) {
+                if("0.0.0.0" != obj.ip) {
                     clearInterval(v)
                     setHide("btn", false)
                     document.getElementById("state").innerHTML = "{#NETWORK_SUCCESS}" + obj.ip
                 }
             }
 
+            /*IF_ETHERNET*/
+            function saveEth() {
+                let o = {
+                    cmd: "save_eth",
+                    en: document.getElementsByName("en")[0].checked
+                }
+                for(p of pins) {
+                    o[p] = document.getElementsByName(p)[0].value
+                }
+                getAjax("/api/setup", ((o) => {}), "POST", JSON.stringify(o));
+            }
+            /*ENDIF_ETHERNET*/
             function saveWifi() {
                 var ssid = document.getElementById("net").value;
                 if(-1 == ssid)
@@ -64,24 +265,36 @@
                 getAjax("/api/setup", ((o) => {if(!o.error) checkWifi()}), "POST", JSON.stringify({cmd: "save_wifi", ssid: ssid, pwd: document.getElementById("pwd").value}));
             }
 
-            function nets(obj) {
-                var e = document.getElementById("net");
-                if(obj.networks.length > 0) {
-                    var a = []
-                    a.push(ml("option", {value: -1}, obj.networks.length + " {#NUM_NETWORKS_FOUND}"))
-                    for(n of obj.networks) {
-                        a.push(ml("option", {value: n.ssid}, n.ssid + " (" + n.rssi + "dBm)"))
-                        found = true;
+            function init() {
+                /*IF_ETHERNET*/
+                getAjax("/api/setup", ((o) => c.append(step1(o.eth))));
+                /*ENDIF_ETHERNET*/
+                function nets(obj) {
+                    clearInterval(v)
+                    v = setInterval(() => {getAjax('/api/setup/networks', nets)}, 4000)
+
+                    if(!obj.success)
+                        return;
+
+                    var e = document.getElementById("net");
+                    if(obj.networks.length > 0) {
+                        var a = []
+                        a.push(ml("option", {value: -1}, obj.networks.length + " {#NUM_NETWORKS_FOUND}"))
+                        for(n of obj.networks) {
+                            a.push(ml("option", {value: n.ssid}, n.ssid + " (" + n.rssi + "dBm)"))
+                            found = true;
+                        }
+                        e.replaceChildren(...a)
                     }
-                    e.replaceChildren(...a)
+
+                    if("0.0.0.0" != obj.ip)
+                        redirIp = "http://" + obj.ip + "/index"
                 }
-                getAjax("/api/setup", ((o) => {}), "POST", JSON.stringify({cmd: "scan_wifi"}));
-            }
 
-            getAjax("/api/setup", ((o) => {}), "POST", JSON.stringify({cmd: "scan_wifi"}));
-            c.append(wifi())
+                c.append(step1Wifi())
+                getAjax('/api/setup/networks', nets)
+            }
 
-            v = setInterval(() => {getAjax('/api/setup/networks', nets)}, 2500);
         </script>
     </body>
 </html>
diff --git a/src/web/lang.h b/src/web/lang.h
index fb5506ee..dd6640b2 100644
--- a/src/web/lang.h
+++ b/src/web/lang.h
@@ -72,4 +72,28 @@
     #define BTN_REBOOT "Reboot"
 #endif
 
+#ifdef LANG_DE
+    #define BTN_REBOOT "Ahoy neustarten"
+#else /*LANG_EN*/
+    #define BTN_REBOOT "Reboot"
+#endif
+
+#ifdef LANG_DE
+    #define BTN_YES "ja"
+#else /*LANG_EN*/
+    #define BTN_YES "yes"
+#endif
+
+#ifdef LANG_DE
+    #define BTN_NO "nein"
+#else /*LANG_EN*/
+    #define BTN_NO "no"
+#endif
+
+#ifdef LANG_DE
+    #define BTN_COREDUMP "CoreDump herunterladen"
+#else /*LANG_EN*/
+    #define BTN_COREDUMP "download CoreDump"
+#endif
+
 #endif /*__LANG_H__*/
diff --git a/src/web/lang.json b/src/web/lang.json
index 066370c5..179c3e4e 100644
--- a/src/web/lang.json
+++ b/src/web/lang.json
@@ -6,7 +6,7 @@
                 {
                     "token": "NAV_WIZARD",
                     "en": "Setup Wizard",
-                    "de": "Daten"
+                    "de": "Einrichtungsassitent"
                 },
                 {
                     "token": "NAV_LIVE",
@@ -81,17 +81,22 @@
                 {
                     "token": "BTN_NEXT",
                     "en": "next >>",
-                    "de": "prüfen >>"
+                    "de": "speichern >>"
+                },
+                {
+                    "token": "BTN_REBOOT",
+                    "en": "reboot >>",
+                    "de": "Ahoy neustarten >>"
                 },
                 {
                     "token": "TEST_CONNECTION",
                     "en": "Test Connection",
-                    "de": "Verbindung wird überprüft"
+                    "de": "Verbindung wird &uuml;berpr&uuml;ft"
                 },
                 {
                     "token": "TRY_TO_CONNECT",
                     "en": "AhoyDTU is trying to connect to your WiFi",
-                    "de": "AhoyDTU versucht eine Verindung mit deinem Netzwerk herzustellen"
+                    "de": "AhoyDTU versucht eine Verbindung mit Deinem Netzwerk herzustellen"
                 },
                 {
                     "token": "CONNECTING",
@@ -101,7 +106,7 @@
                 {
                     "token": "NETWORK_SUCCESS",
                     "en": "success, got following IP in your network: ",
-                    "de": "Verindung erfolgreich. AhoyDTU hat die folgende IP bekommen: "
+                    "de": "Verbindung erfolgreich. AhoyDTU hat die folgende IP bekommen: "
                 },
                 {
                     "token": "BTN_FINISH",
@@ -112,6 +117,36 @@
                     "token": "NUM_NETWORKS_FOUND",
                     "en": "Network(s) found",
                     "de": "Netzwerk(e) gefunden"
+                },
+                {
+                    "token": "PIN_OFF",
+                    "en": "off / default",
+                    "de": "aus / Standard"
+                },
+                {
+                    "token": "PIN_NO_IRQ",
+                    "en": "no IRQ!",
+                    "de": "kein Interrupt!"
+                },
+                {
+                    "token": "PIN_INPUT_ONLY",
+                    "en": "in only",
+                    "de": "nur Eingang"
+                },
+                {
+                    "token": "PIN_DONT_USE",
+                    "en": "DONT USE",
+                    "de": "nicht benutzen"
+                },
+                {
+                    "token": "PIN_NOT_AVAIL",
+                    "en": "not available",
+                    "de": "nicht verf&uuml;gbar"
+                },
+                {
+                    "token": "ETH_ENABLE",
+                    "en": "Ethernet enable",
+                    "de": "Ethernet aktivieren"
                 }
             ]
         },
@@ -186,7 +221,7 @@
                 {
                     "token": "LOG_PRINT_INVERTER_DATA",
                     "en": "print inverter data",
-                    "de": "Livedaten ausgeben"
+                    "de": "Inverterwerte ausgeben"
                 },
                 {
                     "token": "LOG_SERIAL_DEBUG",
@@ -224,19 +259,9 @@
                     "de": "Netzwerke suchen"
                 },
                 {
-                    "token": "BTN_SCAN",
-                    "en": "scan",
-                    "de": "Suche starten"
-                },
-                {
-                    "token": "AVAIL_NETWORKS",
-                    "en": "Avail Networks",
-                    "de": "Verf&uuml;gbare Netzwerke"
-                },
-                {
-                    "token": "NETWORK_NOT_SCANNED",
-                    "en": "not scanned",
-                    "de": "nicht gesucht"
+                    "token": "SCAN_WIFI",
+                    "en": "scan for WiFi networks",
+                    "de": "nach WiFi Netzwerken suchen"
                 },
                 {
                     "token": "SSID_HIDDEN",
@@ -295,23 +320,28 @@
                 },
                 {
                     "token": "INV_RESET_MIDNIGHT",
-                    "en": "Reset values and YieldDay at midnight. ('Pause communication during night' need to be set)",
-                    "de": "Werte und Gesamtertrag um Mitternacht zur&uuml;cksetzen ('Kommunikation w&auml;hrend der Nacht pausieren' muss gesetzt sein)"
+                    "en": "Reset values and YieldDay at midnight",
+                    "de": "Werte und Gesamtertrag um Mitternacht zur&uuml;cksetzen"
                 },
                 {
-                    "token": "INV_PAUSE_SUNSET",
+                    "token": "INV_RESET_SUNSET",
                     "en": "Reset values at sunset",
                     "de": "Werte bei Sonnenuntergang zur&uuml;cksetzen"
                 },
+                {
+                    "token": "INV_RESET_SUNRISE",
+                    "en": "Reset values at sunrise",
+                    "de": "Werte bei Sonnenaufgang zur&uuml;cksetzen"
+                },
                 {
                     "token": "INV_RESET_NOT_AVAIL",
                     "en": "Reset values when inverter status is 'not available'",
                     "de": "Werte zur&uuml;cksetzen, sobald der Wechselrichter nicht erreichbar ist"
                 },
                 {
-                    "token": "INV_RESET_MAX_MIDNIGHT",
-                    "en": "Reset 'max' values at midnight",
-                    "de": "Maximalwerte mitternachts zur&uuml;cksetzen"
+                    "token": "INV_RESET_MAX_VALUES",
+                    "en": "Include reset 'max' values",
+                    "de": "Maximalwerte auch zur&uuml;cksetzen"
                 },
                 {
                     "token": "INV_START_WITHOUT_TIME",
@@ -323,11 +353,6 @@
                     "en": "Read Grid Profile",
                     "de": "Grid-Profil auslesen"
                 },
-                {
-                    "token": "INV_YIELD_EFF",
-                    "en": "Yield Efficiency (default 1.0)",
-                    "de": "Ertragseffizienz (Standard 1.0)"
-                },
                 {
                     "token": "NTP_INTERVAL",
                     "en": "NTP Interval (in minutes, min. 5 minutes)",
@@ -388,11 +413,21 @@
                     "en": "Password (optional)",
                     "de": "Passwort (optional)"
                 },
+                {
+                    "token": "MQTT_JSON",
+                    "en": "Payload as JSON",
+                    "de": "Ausgabe als JSON"
+                },
                 {
                     "token": "MQTT_NOTE",
                     "en": "Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)",
                     "de": "Wechselrichterdaten in fixem Intervall schicken, auch wenn es keine &Auml;nderung gab. Ein Wert von '0' deaktiviert das fixe Intervall, die Wechselrichterdaten werden &uuml;bertragen, sobald neue zur Verf&uuml;gung stehen. (Standard: 0)"
                 },
+                {
+                    "token": "RETAIN",
+                    "en": "enable retain flag",
+                    "de": "'Retain Flag' aktivieren"
+                },
                 {
                     "token": "DISPLAY_CONFIG",
                     "en": "Display Config",
@@ -601,7 +636,7 @@
                 {
                     "token": "BTN_INV_ADD",
                     "en": "add Inverter",
-                    "de": "Wechselrichter hinzufuegen"
+                    "de": "Wechselrichter hinzuf\u00FCgen"
                 },
                 {
                     "token": "INV_INPUT",
@@ -626,7 +661,7 @@
                 {
                     "token": "TAB_INPUTS",
                     "en": "Inputs",
-                    "de": "Eingaenge"
+                    "de": "Eing&auml;nge"
                 },
                 {
                     "token": "TAB_RADIO",
@@ -728,6 +763,11 @@
                     "en": "CMT2300A radio enable",
                     "de": "CMT2300A Funkmodul aktivieren"
                 },
+                {
+                    "token": "ETH_ENABLE",
+                    "en": "Ethernet enable",
+                    "de": "Ethernet aktivieren"
+                },
                 {
                     "token": "DISP_NONE",
                     "en": "None",
@@ -867,6 +907,151 @@
                     "token": "COMMUNICATING",
                     "en": "communicating",
                     "de": "aktiv"
+                },
+                {
+                    "token": "NETWORK",
+                    "en": "Network",
+                    "de": "Netzwerk"
+                },
+                {
+                    "token": "CONNECTION_TYPE",
+                    "en": "connection",
+                    "de": "Verbindung"
+                },
+                {
+                    "token": "WIRED",
+                    "en": "ethernet cable",
+                    "de": "Netzwerkkabel"
+                },
+                {
+                    "token": "WIFI",
+                    "en": "WiFi",
+                    "de": "WiFi"
+                },
+                {
+                    "token": "DEVICE_NAME",
+                    "en": "Device name",
+                    "de": "Ger&auml;tename"
+                },
+                {
+                    "token": "UPTIME",
+                    "en": "Uptime",
+                    "de": "Laufzeit"
+                },
+                {
+                    "token": "REBOOT_REASON",
+                    "en": "Reboot reason",
+                    "de": "Grund des Neustarts"
+                },
+                {
+                    "token": "ENVIRONMENT",
+                    "en": "Environment",
+                    "de": "Umgebung"
+                },
+                {
+                    "token": "BUILD_OPTIONS",
+                    "en": "build options",
+                    "de": "Module"
+                },
+                {
+                    "token": "ADDRESS",
+                    "en": "Address",
+                    "de": "Adresse"
+                },
+                {
+                    "token": "NETWORK",
+                    "en": "Network",
+                    "de": "Netzwerk"
+                },
+                {
+                    "token": "MEMORY",
+                    "en": "Memory",
+                    "de": "Speicher"
+                },
+                {
+                    "token": "CONFIG_PARTITION",
+                    "en": "Config Partition",
+                    "de": "Konfiguration"
+                },
+                {
+                    "token": "FIRMWARE_PARTITION",
+                    "en": "Firmware Partition",
+                    "de": "Firmware"
+                },
+                {
+                    "token": "INTR_PIN_WORKING",
+                    "en": "Interrupt Pin working",
+                    "de": "Interrupt Pin funktioniert"
+                },
+                {
+                    "token": "DATA_RATE",
+                    "en": "Data Rate",
+                    "de": "Datenrate"
+                },
+                {
+                    "token": "RADIO",
+                    "en": "Radio",
+                    "de": "Funkmodul"
+                },
+                {
+                    "token": "NOT",
+                    "en": "not",
+                    "de": "nicht"
+                },
+                {
+                    "token": "CONNECTED",
+                    "en": "connected",
+                    "de": "verbunden"
+                },
+                {
+                    "token": "ENABLED",
+                    "en": "enabled",
+                    "de": "aktiviert"
+                },
+                {
+                    "token": "CONNECTION",
+                    "en": "connection",
+                    "de": "Verbindung"
+                },
+                {
+                    "token": "FLASH_SIZE",
+                    "en": "Flash size",
+                    "de": "Speichergr&ouml;&szlig;e"
+                },
+                {
+                    "token": "MAX_FREE_BLOCK",
+                    "en": "max free block",
+                    "de": "maximale freie Blockgr&ouml;&szlig;e"
+                },
+                {
+                    "token": "SUNRISE",
+                    "en": "sunrise",
+                    "de": "Sonnenaufgang"
+                },
+                {
+                    "token": "SUNSET",
+                    "en": "sunset",
+                    "de": "Sonnenuntergang"
+                },
+                {
+                    "token": "COMMUNICATION_START",
+                    "en": "Communication start",
+                    "de": "Start der Kommunikation"
+                },
+                {
+                    "token": "COMMUNICATION_STOP",
+                    "en": "Communication stop",
+                    "de": "Ende der Kommunikation"
+                },
+                {
+                    "token": "NIGHT_BEHAVIOR",
+                    "en": "Night behavior",
+                    "de": "Verhalten bei Nacht"
+                },
+                {
+                    "token": "COMMUNICATING",
+                    "en": "communicating",
+                    "de": "kommunizierend"
                 }
             ]
         },
@@ -883,6 +1068,11 @@
                     "en": "autoscroll",
                     "de": "automatisch scrollen"
                 },
+                {
+                    "token": "BTN_MANUALSCROLL",
+                    "en": "manual scroll",
+                    "de": "manuell scrollen"
+                },
                 {
                     "token": "BTN_COPY",
                     "en": "copy",
@@ -897,6 +1087,21 @@
                     "token": "UPTIME",
                     "en": "uptime",
                     "de": "Laufzeit"
+                },
+                {
+                    "token": "DAYS",
+                    "en": "days",
+                    "de": "Tage"
+                },
+                {
+                    "token": "COPIED_TO_CLIPBOARD",
+                    "en": "Copied to clipboard",
+                    "de": "in die Zwischenablage kopiert"
+                },
+                {
+                    "token": "CLIPBOARD_FAILED",
+                    "en": "Copy failed",
+                    "de": "kopieren fehlgeschlagen"
                 }
             ]
         },
@@ -973,6 +1178,16 @@
                     "en": "Error",
                     "de": "Fehler"
                 },
+                {
+                    "token": "DAY",
+                    "en": "day",
+                    "de": "Tag"
+                },
+                {
+                    "token": "S",
+                    "en": "s",
+                    "de": "e"
+                },
                 {
                     "token": "NTP_UNREACH",
                     "en": "NTP timeserver unreachable",
@@ -986,7 +1201,7 @@
                 {
                     "token": "NIGHT_TIME",
                     "en": "Night time, inverter polling disabled",
-                    "de": "Wechselrichterabfrage deaktivert (Nacht)"
+                    "de": "Wechselrichterabfrage deaktiviert (Nacht)"
                 },
                 {
                     "token": "PAUSED_AT",
@@ -1503,35 +1718,30 @@
                     "en": "Total Power",
                     "de": "Gesamtleistung"
                 },
+                {
+                    "token": "TOTAL_POWER_DAY",
+                    "en": "Total Power Today",
+                    "de": "Gesamtleistung heute"
+                },
                 {
                     "token": "TOTAL_YIELD_PER_DAY",
                     "en": "Total Yield per day",
                     "de": "Gesamtertrag pro Tag"
                 },
-                {
-                    "token": "MAX_DAY",
-                    "en": "maximum day",
-                    "de": "Tagesmaximum"
-                },
-                {
-                    "token": "LAST_VALUE",
-                    "en": "last value",
-                    "de": "letzter Wert"
-                },
                 {
                     "token": "MAXIMUM",
-                    "en": "maximum value",
-                    "de": "Maximalwert"
+                    "en": "Maximum",
+                    "de": "Maximum"
                 },
                 {
-                    "token": "UPDATED",
-                    "en": "Updated every",
-                    "de": "aktualisiert alle"
+                    "token": "LAST_VALUE",
+                    "en": "Last value",
+                    "de": "Letzter Wert"
                 },
                 {
-                    "token": "SECONDS",
-                    "en": "seconds",
-                    "de": "Sekunden"
+                    "token": "YIELD_DAY",
+                    "en": "Yield day",
+                    "de": "Tagesertrag"
                 }
             ]
         }
diff --git a/src/web/web.h b/src/web/web.h
index 8495ba23..b337228a 100644
--- a/src/web/web.h
+++ b/src/web/web.h
@@ -16,11 +16,7 @@
 #include "../appInterface.h"
 #include "../hm/hmSystem.h"
 #include "../utils/helper.h"
-#if defined(ETHERNET)
-#include "AsyncWebServer_ESP32_W5500.h"
-#else /* defined(ETHERNET) */
 #include "ESPAsyncWebServer.h"
-#endif /* defined(ETHERNET) */
 #include "html/h/api_js.h"
 #include "html/h/colorBright_css.h"
 #include "html/h/colorDark_css.h"
@@ -41,14 +37,19 @@
 
 #define WEB_SERIAL_BUF_SIZE 2048
 
-const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", "pinLed1", "pinLed2", "pinLedHighActive", "pinLedLum", "pinCmtSclk", "pinSdio", "pinCsb", "pinFcsb", "pinGpio3"};
+const char* const pinArgNames[] = {
+    "pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0",
+    "pinLed1", "pinLed2", "pinLedHighActive", "pinLedLum", "pinCmtSclk",
+    "pinSdio", "pinCsb", "pinFcsb", "pinGpio3"
+    #if defined (ETHERNET)
+    , "ethCs", "ethSclk", "ethMiso", "ethMosi", "ethIrq", "ethRst"
+    #endif
+};
 
 template <class HMSYSTEM>
 class Web {
    public:
-        Web(void) : mWeb(80), mEvts("/events") {
-            memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
-        }
+        Web(void) : mWeb(80), mEvts("/events") {}
 
         void setup(IApp *app, HMSYSTEM *sys, settings_t *config) {
             mApp     = app;
@@ -56,7 +57,8 @@ class Web {
             mConfig  = config;
 
             DPRINTLN(DBG_VERBOSE, F("app::setup-on"));
-            mWeb.on("/",               HTTP_GET,  std::bind(&Web::onIndex,        this, std::placeholders::_1));
+            mWeb.on("/",               HTTP_GET,  std::bind(&Web::onIndex,        this, std::placeholders::_1, true));
+            mWeb.on("/index",          HTTP_GET,  std::bind(&Web::onIndex,        this, std::placeholders::_1, false));
             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("/colors.css",     HTTP_GET,  std::bind(&Web::onColor,        this, std::placeholders::_1));
@@ -74,6 +76,7 @@ class Web {
 
             mWeb.on("/setup",          HTTP_GET,  std::bind(&Web::onSetup,        this, std::placeholders::_1));
             mWeb.on("/wizard",         HTTP_GET,  std::bind(&Web::onWizard,       this, std::placeholders::_1));
+            mWeb.on("/generate_204",   HTTP_GET,  std::bind(&Web::onWizard,       this, std::placeholders::_1));   //Android captive portal
             mWeb.on("/save",           HTTP_POST, std::bind(&Web::showSave,       this, std::placeholders::_1));
 
             mWeb.on("/live",           HTTP_ANY,  std::bind(&Web::onLive,         this, std::placeholders::_1));
@@ -105,11 +108,17 @@ class Web {
 
         void tickSecond() {
             if (mSerialClientConnnected) {
+                if(nullptr == mSerialBuf)
+                    return;
+
                 if (mSerialBufFill > 0) {
                     mEvts.send(mSerialBuf, "serial", millis());
                     memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
                     mSerialBufFill = 0;
                 }
+            } else if(nullptr != mSerialBuf) {
+                delete[] mSerialBuf;
+                mSerialBuf = nullptr;
             }
         }
 
@@ -153,18 +162,14 @@ class Web {
             mUploadFp.write(data, len);
             if (final) {
                 mUploadFp.close();
-                #if !defined(ETHERNET)
                 char pwd[PWD_LEN];
                 strncpy(pwd, mConfig->sys.stationPwd, PWD_LEN); // backup WiFi PWD
-                #endif
                 if (!mApp->readSettings("/tmp.json")) {
                     mUploadFail = true;
                     DPRINTLN(DBG_ERROR, F("upload JSON error!"));
                 } else {
                     LittleFS.remove("/tmp.json");
-                    #if !defined(ETHERNET)
                     strncpy(mConfig->sys.stationPwd, pwd, PWD_LEN); // restore WiFi PWD
-                    #endif
                     for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
                         if((mConfig->inst.iv[i].serial.u64 != 0) && (mConfig->inst.iv[i].serial.u64 < 138999999999)) { // hexadecimal
                             mConfig->inst.iv[i].serial.u64 = ah::Serial2u64(String(mConfig->inst.iv[i].serial.u64).c_str());
@@ -181,6 +186,9 @@ class Web {
             if (!mSerialClientConnnected)
                 return;
 
+            if(nullptr == mSerialBuf)
+                return;
+
             msg.replace("\r\n", "<rn>");
             if (mSerialAddTime) {
                 if ((13 + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) {
@@ -260,8 +268,8 @@ class Web {
             bool reboot = (!Update.hasError());
 
             String html = F("<!doctype html><html><head><title>Update</title><meta http-equiv=\"refresh\" content=\"");
-            #if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3)
-                html += F("5");
+            #if defined(ETHERNET)
+                html += (mConfig->sys.eth.enabled) ? F("5") : F("20");
             #else
                 html += F("20");
             #endif
@@ -297,6 +305,10 @@ class Web {
         void onConnect(AsyncEventSourceClient *client) {
             DPRINTLN(DBG_VERBOSE, "onConnect");
 
+            if(nullptr == mSerialBuf) {
+                mSerialBuf = new char[WEB_SERIAL_BUF_SIZE];
+                memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
+            }
             mSerialClientConnnected = true;
 
             if (client->lastId())
@@ -305,7 +317,19 @@ class Web {
             client->send("hello!", NULL, millis(), 1000);
         }
 
-        void onIndex(AsyncWebServerRequest *request) {
+        void onIndex(AsyncWebServerRequest *request, bool checkAp = true) {
+            #if !defined(ETHERNET)
+            if(mApp->isApActive() && checkAp) {
+                onWizard(request);
+                return;
+            }
+            #else
+            // show wizard only if ethernet is not configured
+            if(mApp->isApActive() && checkAp && !mConfig->sys.eth.enabled) {
+                onWizard(request);
+                return;
+            }
+            #endif
             getPage(request, PROT_MASK_INDEX, index_html, index_html_len);
         }
 
@@ -388,6 +412,7 @@ class Web {
 
         void showNotFound(AsyncWebServerRequest *request) {
             checkProtection(request);
+            //DBGPRINTLN(request->url());
             request->redirect("/wizard");
         }
 
@@ -411,6 +436,13 @@ class Web {
         }
 
         void onWizard(AsyncWebServerRequest *request) {
+            #if defined(ETHERNET)
+            if(mConfig->sys.eth.enabled) {
+                getPage(request, PROT_MASK_INDEX, index_html, index_html_len);
+                return;
+            }
+            #endif
+
             AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), wizard_html, wizard_html_len);
             response->addHeader(F("Content-Encoding"), "gzip");
             response->addHeader(F("content-type"), "text/html; charset=UTF-8");
@@ -428,15 +460,14 @@ class Web {
             char buf[20] = {0};
 
             // general
-            #if !defined(ETHERNET)
             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);
+            mConfig->sys.isHidden = (request->arg("hidd") == "on");
+
             if (request->arg("ap_pwd") != "")
                 request->arg("ap_pwd").toCharArray(mConfig->sys.apPwd, PWD_LEN);
-            mConfig->sys.isHidden = (request->arg("hidd") == "on");
-            #endif /* !defined(ETHERNET) */
             if (request->arg("device") != "")
                 request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN);
             mConfig->sys.darkMode = (request->arg("darkMode") == "on");
@@ -477,17 +508,22 @@ class Web {
 
             if (request->arg("invInterval") != "")
                 mConfig->inst.sendInterval = request->arg("invInterval").toInt();
-            mConfig->inst.rstYieldMidNight = (request->arg("invRstMid") == "on");
+            mConfig->inst.rstValsAtMidNight = (request->arg("invRstMid") == "on");
             mConfig->inst.rstValsCommStop = (request->arg("invRstComStop") == "on");
+            mConfig->inst.rstValsCommStart = (request->arg("invRstComStart") == "on");
             mConfig->inst.rstValsNotAvail = (request->arg("invRstNotAvail") == "on");
             mConfig->inst.startWithoutTime = (request->arg("strtWthtTm") == "on");
             mConfig->inst.readGrid = (request->arg("rdGrid") == "on");
-            mConfig->inst.rstMaxValsMidNight = (request->arg("invRstMaxMid") == "on");
-            mConfig->inst.yieldEffiency = (request->arg("yldEff")).toFloat();
+            mConfig->inst.rstIncludeMaxVals = (request->arg("invRstMaxMid") == "on");
 
 
             // pinout
-            for (uint8_t i = 0; i < 16; i++) {
+            #if defined(ETHERNET)
+            for (uint8_t i = 0; i < 22; i++)
+            #else
+            for (uint8_t i = 0; i < 16; i++)
+            #endif
+            {
                 uint8_t pin = request->arg(String(pinArgNames[i])).toInt();
                 switch(i) {
                     case 0:  mConfig->nrf.pinCs    = ((pin != 0xff) ? pin : DEF_NRF_CS_PIN);  break;
@@ -506,11 +542,23 @@ class Web {
                     case 13: mConfig->cmt.pinCsb   = pin; break;
                     case 14: mConfig->cmt.pinFcsb  = pin; break;
                     case 15: mConfig->cmt.pinIrq   = pin; break;
+
+                    #if defined(ETHERNET)
+                    case 16: mConfig->sys.eth.pinCs   = pin; break;
+                    case 17: mConfig->sys.eth.pinSclk = pin; break;
+                    case 18: mConfig->sys.eth.pinMiso = pin; break;
+                    case 19: mConfig->sys.eth.pinMosi = pin; break;
+                    case 20: mConfig->sys.eth.pinIrq  = pin; break;
+                    case 21: mConfig->sys.eth.pinRst  = pin; break;
+                    #endif
                 }
             }
 
             mConfig->nrf.enabled = (request->arg("nrfEnable") == "on");
             mConfig->cmt.enabled = (request->arg("cmtEnable") == "on");
+            #if defined(ETHERNET)
+            mConfig->sys.eth.enabled = (request->arg("ethEn") == "on");
+            #endif
 
             // ntp
             if (request->arg("ntpAddr") != "") {
@@ -544,8 +592,10 @@ class Web {
             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.json = (request->arg("mqttJson") == "on");
             mConfig->mqtt.port = request->arg("mqttPort").toInt();
             mConfig->mqtt.interval = request->arg("mqttInterval").toInt();
+            mConfig->mqtt.enableRetain = (request->arg("retain") == "on");
 
             // serial console
             mConfig->serial.debug = (request->arg("serDbg") == "on");
@@ -662,7 +712,7 @@ class Web {
             { "max_power",            "gauge",   metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->getMaxPower();} },
             { "radio_rx_success",     "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxSuccess;} },
             { "radio_rx_fail",        "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFail;} },
-            { "radio_rx_fail_answer", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnser;} },
+            { "radio_rx_fail_answer", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnswer;} },
             { "radio_frame_cnt",      "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.frmCnt;} },
             { "radio_tx_cnt",         "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.txCnt;} },
             { "radio_retransmits",    "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.retransmits;} },
@@ -911,7 +961,7 @@ class Web {
         settings_t *mConfig = nullptr;
 
         bool mSerialAddTime = true;
-        char mSerialBuf[WEB_SERIAL_BUF_SIZE];
+        char *mSerialBuf = nullptr;
         uint16_t mSerialBufFill = 0;
         bool mSerialClientConnnected = false;
 
diff --git a/src/wifi/ahoywifi.cpp b/src/wifi/ahoywifi.cpp
deleted file mode 100644
index 9bcbdadc..00000000
--- a/src/wifi/ahoywifi.cpp
+++ /dev/null
@@ -1,488 +0,0 @@
-//-----------------------------------------------------------------------------
-// 2024 Ahoy, https://ahoydtu.de
-// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
-//-----------------------------------------------------------------------------
-
-#if !defined(ETHERNET)
-#if defined(ESP32) && defined(F)
-  #undef F
-  #define F(sl) (sl)
-#endif
-#include "ahoywifi.h"
-
-#if defined(ESP32)
-#include <ESPmDNS.h>
-#else
-#include <ESP8266mDNS.h>
-#endif
-
-// NTP CONFIG
-#define NTP_PACKET_SIZE     48
-
-//-----------------------------------------------------------------------------
-ahoywifi::ahoywifi() : mApIp(192, 168, 4, 1) {}
-
-
-/**
- * TODO: ESP32 has native strongest AP support!
- *       WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
-         WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL);
-*/
-
-//-----------------------------------------------------------------------------
-void ahoywifi::setup(settings_t *config, uint32_t *utcTimestamp, appWifiCb cb) {
-    mConfig = config;
-    mUtcTimestamp = utcTimestamp;
-    mAppWifiCb = cb;
-
-    mGotDisconnect = false;
-    mStaConn = DISCONNECTED;
-    mCnt        = 0;
-    mScanActive = false;
-    mScanCnt    = 0;
-    mStopApAllowed = true;
-
-    #if defined(ESP8266)
-    wifiConnectHandler = WiFi.onStationModeConnected(std::bind(&ahoywifi::onConnect, this, std::placeholders::_1));
-    wifiGotIPHandler = WiFi.onStationModeGotIP(std::bind(&ahoywifi::onGotIP, this, std::placeholders::_1));
-    wifiDisconnectHandler = WiFi.onStationModeDisconnected(std::bind(&ahoywifi::onDisconnect, this, std::placeholders::_1));
-    #else
-    WiFi.onEvent(std::bind(&ahoywifi::onWiFiEvent, this, std::placeholders::_1));
-    #endif
-
-    setupWifi(true);
-}
-
-
-//-----------------------------------------------------------------------------
-void ahoywifi::setupWifi(bool startAP = false) {
-    #if !defined(FB_WIFI_OVERRIDDEN)
-        if(startAP) {
-            setupAp();
-            delay(1000);
-        }
-    #endif
-    #if !defined(AP_ONLY)
-        #if defined(FB_WIFI_OVERRIDDEN)
-            snprintf(mConfig->sys.stationSsid, SSID_LEN, "%s", FB_WIFI_SSID);
-            snprintf(mConfig->sys.stationPwd, PWD_LEN, "%s", FB_WIFI_PWD);
-            setupStation();
-        #else
-            if(mConfig->valid) {
-                if(strncmp(mConfig->sys.stationSsid, FB_WIFI_SSID, 14) != 0)
-                    setupStation();
-            }
-        #endif
-    #endif
-}
-
-
-void ahoywifi::tickWifiLoop() {
-    static const uint8_t TIMEOUT = 20;
-    static const uint8_t SCAN_TIMEOUT = 10;
-    #if !defined(AP_ONLY)
-
-    mCnt++;
-
-    switch (mStaConn) {
-        case IN_STA_MODE:
-            // Nothing to do
-            if (mGotDisconnect) {
-                mStaConn = RESET;
-            }
-            #if !defined(ESP32)
-            MDNS.update();
-            if(WiFi.channel() > 11)
-                mWasInCh12to14 = true;
-            #endif
-            return;
-        case IN_AP_MODE:
-            if ((WiFi.softAPgetStationNum() == 0) || (!mStopApAllowed)) {
-                mCnt = 0;
-                mDns.stop();
-                WiFi.mode(WIFI_AP_STA);
-                mStaConn = DISCONNECTED;
-            } else {
-                mDns.processNextRequest();
-                return;
-            }
-            break;
-        case DISCONNECTED:
-            if ((WiFi.softAPgetStationNum() > 0) && (mStopApAllowed)) {
-                mStaConn = IN_AP_MODE;
-                // first time switch to AP Mode
-                if (mScanActive) {
-                    WiFi.scanDelete();
-                    mScanActive = false;
-                }
-                DBGPRINTLN(F("AP client connected"));
-                welcome(mApIp.toString(), "");
-                WiFi.mode(WIFI_AP);
-                mDns.start(53, "*", mApIp);
-                mAppWifiCb(true);
-                mDns.processNextRequest();
-                return;
-            } else if (!mScanActive) {
-                DBGPRINT(F("scanning APs with SSID "));
-                DBGPRINTLN(String(mConfig->sys.stationSsid));
-                mScanCnt = 0;
-                mCnt = 0;
-                mScanActive = true;
-#if defined(ESP8266)
-                WiFi.scanNetworks(true, true, 0U, ([this]() {
-                    if (mConfig->sys.isHidden)
-                        return (uint8_t*)NULL;
-                    return (uint8_t*)(mConfig->sys.stationSsid);
-                    })());
-#else
-                WiFi.scanNetworks(true, true, false, 300U, 0U, ([this]() {
-                    if (mConfig->sys.isHidden)
-                        return (char*)NULL;
-                    return (mConfig->sys.stationSsid);
-                    })());
-#endif
-                return;
-            } else if(getBSSIDs()) {
-                // Scan ready
-                mStaConn = SCAN_READY;
-            } else {
-                // In case of a timeout, what do we do?
-                // For now we start scanning again as the original code did.
-                // Would be better to into PA mode
-
-                if (isTimeout(SCAN_TIMEOUT)) {
-                    WiFi.scanDelete();
-                    mScanActive = false;
-                }
-            }
-            break;
-        case SCAN_READY:
-                mStaConn = CONNECTING;
-                mCnt = 0;
-                DBGPRINT(F("try to connect to AP with BSSID:"));
-                uint8_t bssid[6];
-                for (int j = 0; j < 6; j++) {
-                    bssid[j] = mBSSIDList.front();
-                    mBSSIDList.pop_front();
-                    DBGPRINT(" "  + String(bssid[j], HEX));
-                }
-                DBGPRINTLN("");
-                mGotDisconnect = false;
-                WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, 0, &bssid[0]);
-
-                break;
-        case CONNECTING:
-            if (isTimeout(TIMEOUT)) {
-                WiFi.disconnect();
-                mStaConn = mBSSIDList.empty() ? DISCONNECTED : SCAN_READY;
-            }
-            break;
-        case CONNECTED:
-            // Connection but no IP yet
-            if (isTimeout(TIMEOUT) || mGotDisconnect) {
-                mStaConn = RESET;
-            }
-            break;
-        case GOT_IP:
-            welcome(WiFi.localIP().toString(), F(" (Station)"));
-            if(mStopApAllowed) {
-                WiFi.softAPdisconnect();
-                WiFi.mode(WIFI_STA);
-                DBGPRINTLN(F("[WiFi] AP disabled"));
-                delay(100);
-            }
-            mAppWifiCb(true);
-            mGotDisconnect = false;
-            mStaConn = IN_STA_MODE;
-
-            if (!MDNS.begin(mConfig->sys.deviceName)) {
-                DPRINTLN(DBG_ERROR, F("Error setting up MDNS responder!"));
-            } else {
-                DBGPRINT(F("[WiFi] mDNS established: "));
-                DBGPRINT(mConfig->sys.deviceName);
-                DBGPRINTLN(F(".local"));
-            }
-
-            break;
-        case RESET:
-            mGotDisconnect = false;
-            mStaConn = DISCONNECTED;
-            mCnt = 5;     // try to reconnect in 5 sec
-            setupWifi();        // reconnect with AP / Station setup
-            mAppWifiCb(false);
-            DPRINTLN(DBG_INFO, "[WiFi] Connection Lost");
-            break;
-        default:
-            DBGPRINTLN(F("Unhandled status"));
-            break;
-    }
-
-#endif
-}
-
-//-----------------------------------------------------------------------------
-void ahoywifi::setupAp(void) {
-    DPRINTLN(DBG_VERBOSE, F("wifi::setupAp"));
-
-    DBGPRINTLN(F("\n---------\nAhoyDTU Info:"));
-    DBGPRINT(F("Version: "));
-    DBGPRINT(String(VERSION_MAJOR));
-    DBGPRINT(F("."));
-    DBGPRINT(String(VERSION_MINOR));
-    DBGPRINT(F("."));
-    DBGPRINTLN(String(VERSION_PATCH));
-    DBGPRINT(F("Github Hash: "));
-    DBGPRINTLN(String(AUTO_GIT_HASH));
-
-    DBGPRINT(F("\n---------\nAP MODE\nSSID: "));
-    DBGPRINTLN(WIFI_AP_SSID);
-    DBGPRINT(F("PWD: "));
-    DBGPRINTLN(mConfig->sys.apPwd);
-    DBGPRINT(F("IP Address: http://"));
-    DBGPRINTLN(mApIp.toString());
-    DBGPRINTLN(F("---------\n"));
-
-    if(String(mConfig->sys.deviceName) != "")
-        WiFi.hostname(mConfig->sys.deviceName);
-
-    WiFi.mode(WIFI_AP_STA);
-    WiFi.softAPConfig(mApIp, mApIp, IPAddress(255, 255, 255, 0));
-    WiFi.softAP(WIFI_AP_SSID, mConfig->sys.apPwd);
-}
-
-
-//-----------------------------------------------------------------------------
-void ahoywifi::setupStation(void) {
-    DPRINTLN(DBG_VERBOSE, F("wifi::setupStation"));
-    if(mConfig->sys.ip.ip[0] != 0) {
-        IPAddress ip(mConfig->sys.ip.ip);
-        IPAddress mask(mConfig->sys.ip.mask);
-        IPAddress dns1(mConfig->sys.ip.dns1);
-        IPAddress dns2(mConfig->sys.ip.dns2);
-        IPAddress gateway(mConfig->sys.ip.gateway);
-        if(!WiFi.config(ip, gateway, mask, dns1, dns2))
-            DPRINTLN(DBG_ERROR, F("failed to set static IP!"));
-    }
-    mBSSIDList.clear();
-    if(String(mConfig->sys.deviceName) != "")
-        WiFi.hostname(mConfig->sys.deviceName);
-    WiFi.mode(WIFI_AP_STA);
-
-    DBGPRINT(F("connect to network '"));
-    DBGPRINT(mConfig->sys.stationSsid);
-    DBGPRINTLN(F("' ..."));
-}
-
-
-//-----------------------------------------------------------------------------
-bool ahoywifi::getNtpTime(void) {
-    if(IN_STA_MODE != mStaConn)
-        return false;
-
-    IPAddress timeServer;
-    uint8_t buf[NTP_PACKET_SIZE];
-    uint8_t retry = 0;
-
-    if (WiFi.hostByName(mConfig->ntp.addr, timeServer) != 1)
-        return false;
-
-    mUdp.begin(mConfig->ntp.port);
-    sendNTPpacket(timeServer);
-
-    while(retry++ < 5) {
-        int wait = 150;
-        while(--wait) {
-            if(NTP_PACKET_SIZE <= mUdp.parsePacket()) {
-                uint64_t secsSince1900;
-                mUdp.read(buf, NTP_PACKET_SIZE);
-                secsSince1900  = ((uint64_t)buf[40] << 24);
-                secsSince1900 |= (buf[41] << 16);
-                secsSince1900 |= (buf[42] <<  8);
-                secsSince1900 |= (buf[43]      );
-
-                *mUtcTimestamp = secsSince1900 - 2208988800UL; // UTC time
-                DPRINTLN(DBG_INFO, "[NTP]: " + ah::getDateTimeStr(*mUtcTimestamp) + " UTC");
-                return true;
-            } else
-                delay(10);
-        }
-    }
-
-    DPRINTLN(DBG_INFO, F("[NTP]: getNtpTime failed"));
-    return false;
-}
-
-
-//-----------------------------------------------------------------------------
-void ahoywifi::sendNTPpacket(IPAddress& address) {
-    //DPRINTLN(DBG_VERBOSE, F("wifi::sendNTPpacket"));
-    uint8_t buf[NTP_PACKET_SIZE] = {0};
-
-    buf[0] = B11100011; // LI, Version, Mode
-    buf[1] = 0;         // Stratum
-    buf[2] = 6;         // Max Interval between messages in seconds
-    buf[3] = 0xEC;      // Clock Precision
-    // bytes 4 - 11 are for Root Delay and Dispersion and were set to 0 by memset
-    buf[12] = 49;       // four-byte reference ID identifying
-    buf[13] = 0x4E;
-    buf[14] = 49;
-    buf[15] = 52;
-
-    mUdp.beginPacket(address, 123); // NTP request, port 123
-    mUdp.write(buf, NTP_PACKET_SIZE);
-    mUdp.endPacket();
-}
-
-//-----------------------------------------------------------------------------
-void ahoywifi::sortRSSI(int *sort, int n) {
-    for (int i = 0; i < n; i++)
-        sort[i] = i;
-    for (int i = 0; i < n; i++)
-        for (int j = i + 1; j < n; j++)
-            if (WiFi.RSSI(sort[j]) > WiFi.RSSI(sort[i]))
-                std::swap(sort[i], sort[j]);
-}
-
-//-----------------------------------------------------------------------------
-void ahoywifi::scanAvailNetworks(void) {
-    if(!mScanActive) {
-        mScanActive = true;
-        if(WIFI_AP == WiFi.getMode())
-          WiFi.mode(WIFI_AP_STA);
-        WiFi.scanNetworks(true);
-    }
-}
-
-//-----------------------------------------------------------------------------
-bool ahoywifi::getAvailNetworks(JsonObject obj) {
-    JsonArray nets = obj.createNestedArray("networks");
-
-    int n = WiFi.scanComplete();
-    if (n < 0)
-        return false;
-    if(n > 0) {
-        int sort[n];
-        sortRSSI(&sort[0], n);
-        for (int i = 0; i < n; ++i) {
-            nets[i]["ssid"] = WiFi.SSID(sort[i]);
-            nets[i]["rssi"] = WiFi.RSSI(sort[i]);
-        }
-    }
-    mScanActive = false;
-    WiFi.scanDelete();
-    if(mStaConn == IN_AP_MODE)
-        WiFi.mode(WIFI_AP);
-
-    return true;
-}
-
-//-----------------------------------------------------------------------------
-bool ahoywifi::getBSSIDs() {
-    bool result = false;
-    int n = WiFi.scanComplete();
-    if (n < 0) {
-        if (++mScanCnt < 20)
-            return false;
-    }
-    if(n > 0) {
-        mBSSIDList.clear();
-        int sort[n];
-        sortRSSI(&sort[0], n);
-        for (int i = 0; i < n; i++) {
-            DBGPRINT("BSSID " + String(i) + ":");
-            uint8_t *bssid = WiFi.BSSID(sort[i]);
-            for (int j = 0; j < 6; j++){
-                DBGPRINT(" " + String(bssid[j], HEX));
-                mBSSIDList.push_back(bssid[j]);
-            }
-            DBGPRINTLN("");
-        }
-        result = true;
-    }
-    mScanActive = false;
-    WiFi.scanDelete();
-    return result;
-}
-
-//-----------------------------------------------------------------------------
-void ahoywifi::connectionEvent(WiFiStatus_t status) {
-    DPRINTLN(DBG_INFO, "connectionEvent");
-
-    switch(status) {
-        case CONNECTED:
-            if(mStaConn != CONNECTED) {
-                mStaConn = CONNECTED;
-                mGotDisconnect = false;
-                DBGPRINTLN(F("\n[WiFi] Connected"));
-            }
-            break;
-
-        case GOT_IP:
-            mStaConn = GOT_IP;
-            break;
-
-        case DISCONNECTED:
-            mGotDisconnect = true;
-            break;
-
-        default:
-            break;
-    }
-}
-
-
-//-----------------------------------------------------------------------------
-#if defined(ESP8266)
-    //-------------------------------------------------------------------------
-    void ahoywifi::onConnect(const WiFiEventStationModeConnected& event) {
-        connectionEvent(CONNECTED);
-    }
-
-    //-------------------------------------------------------------------------
-    void ahoywifi::onGotIP(const WiFiEventStationModeGotIP& event) {
-        connectionEvent(GOT_IP);
-    }
-
-    //-------------------------------------------------------------------------
-    void ahoywifi::onDisconnect(const WiFiEventStationModeDisconnected& event) {
-        connectionEvent(DISCONNECTED);
-    }
-
-#else
-    //-------------------------------------------------------------------------
-    void ahoywifi::onWiFiEvent(WiFiEvent_t event) {
-        DBGPRINT(F("Wifi event: "));
-        DBGPRINTLN(String(event));
-
-        switch(event) {
-            case SYSTEM_EVENT_STA_CONNECTED:
-                connectionEvent(CONNECTED);
-                break;
-
-            case SYSTEM_EVENT_STA_GOT_IP:
-                connectionEvent(GOT_IP);
-                break;
-
-            case SYSTEM_EVENT_STA_DISCONNECTED:
-                connectionEvent(DISCONNECTED);
-                break;
-
-            default:
-                break;
-        }
-    }
-#endif
-
-
-//-----------------------------------------------------------------------------
-void ahoywifi::welcome(String ip, String mode) {
-    DBGPRINTLN(F("\n\n--------------------------------"));
-    DBGPRINTLN(F("Welcome to AHOY!"));
-    DBGPRINT(F("\npoint your browser to http://"));
-    DBGPRINT(ip);
-    DBGPRINTLN(mode);
-    DBGPRINTLN(F("to configure your device"));
-    DBGPRINTLN(F("--------------------------------\n"));
-}
-
-#endif /* !defined(ETHERNET) */
diff --git a/src/wifi/ahoywifi.h b/src/wifi/ahoywifi.h
deleted file mode 100644
index d38701aa..00000000
--- a/src/wifi/ahoywifi.h
+++ /dev/null
@@ -1,97 +0,0 @@
-//------------------------------------//-----------------------------------------------------------------------------
-// 2024 Ahoy, https://github.com/lumpapu/ahoy
-// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
-//-----------------------------------------------------------------------------
-
-#if !defined(ETHERNET)
-#ifndef __AHOYWIFI_H__
-#define __AHOYWIFI_H__
-
-#include "../utils/dbg.h"
-#include <Arduino.h>
-#include <WiFiUdp.h>
-#include <DNSServer.h>
-#include "ESPAsyncWebServer.h"
-
-#include "../config/settings.h"
-
-class app;
-
-class ahoywifi {
-    public:
-        typedef std::function<void(bool)> appWifiCb;
-
-        ahoywifi();
-
-
-        void setup(settings_t *config, uint32_t *utcTimestamp, appWifiCb cb);
-        void tickWifiLoop(void);
-        bool getNtpTime(void);
-        void scanAvailNetworks(void);
-        bool getAvailNetworks(JsonObject obj);
-        void setStopApAllowedMode(bool allowed) {
-            mStopApAllowed = allowed;
-        }
-        String getStationIp(void) {
-            return WiFi.localIP().toString();
-        }
-        void setupStation(void);
-
-        bool getWasInCh12to14() const {
-            return mWasInCh12to14;
-        }
-
-    private:
-        typedef enum WiFiStatus {
-            DISCONNECTED = 0,
-            SCAN_READY,
-            CONNECTING,
-            CONNECTED,
-            IN_AP_MODE,
-            GOT_IP,
-            IN_STA_MODE,
-            RESET
-        } WiFiStatus_t;
-
-        void setupWifi(bool startAP);
-        void setupAp(void);
-        void sendNTPpacket(IPAddress& address);
-        void sortRSSI(int *sort, int n);
-        bool getBSSIDs(void);
-        void connectionEvent(WiFiStatus_t status);
-        bool isTimeout(uint8_t timeout) {  return (mCnt % timeout) == 0; }
-
-#if defined(ESP8266)
-        void onConnect(const WiFiEventStationModeConnected& event);
-        void onGotIP(const WiFiEventStationModeGotIP& event);
-        void onDisconnect(const WiFiEventStationModeDisconnected& event);
-        #else
-        void onWiFiEvent(WiFiEvent_t event);
-        #endif
-        void welcome(String ip, String mode);
-
-
-        settings_t *mConfig = nullptr;
-        appWifiCb mAppWifiCb;
-
-        DNSServer mDns;
-        IPAddress mApIp;
-        WiFiUDP mUdp; // for time server
-        #if defined(ESP8266)
-        WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler, wifiGotIPHandler;
-        #endif
-
-        WiFiStatus_t mStaConn = DISCONNECTED;
-        uint8_t mCnt = 0;
-        uint32_t *mUtcTimestamp = nullptr;
-
-        uint8_t mScanCnt = 0;
-        bool mScanActive = false;
-        bool mGotDisconnect = false;
-        std::list<uint8_t> mBSSIDList;
-        bool mStopApAllowed = false;
-        bool mWasInCh12to14 = false;
-};
-
-#endif /*__AHOYWIFI_H__*/
-#endif /* !defined(ETHERNET) */
diff --git a/tools/NodeRED/flows-mqtt-json-example.json b/tools/NodeRED/flows-mqtt-json-example.json
new file mode 100644
index 00000000..5e2e09a1
--- /dev/null
+++ b/tools/NodeRED/flows-mqtt-json-example.json
@@ -0,0 +1,466 @@
+[
+    {
+        "id": "67bced2c4e728783",
+        "type": "mqtt in",
+        "z": "5de5756d190f9086",
+        "name": "",
+        "topic": "hoymiles/+",
+        "qos": "0",
+        "datatype": "auto-detect",
+        "broker": "319864a4e0fd913f",
+        "nl": false,
+        "rap": true,
+        "rh": 0,
+        "inputs": 0,
+        "x": 80,
+        "y": 2100,
+        "wires": [
+            [
+                "a55632ad0dff0b69"
+            ]
+        ]
+    },
+    {
+        "id": "a7f0d307d7cf77e2",
+        "type": "mqtt in",
+        "z": "5de5756d190f9086",
+        "name": "",
+        "topic": "hoymiles/X/#",
+        "qos": "0",
+        "datatype": "auto-detect",
+        "broker": "319864a4e0fd913f",
+        "nl": false,
+        "rap": true,
+        "rh": 0,
+        "inputs": 0,
+        "x": 90,
+        "y": 2260,
+        "wires": [
+            [
+                "7e17e5a3f4df3011",
+                "1a8cca488d53394a"
+            ]
+        ]
+    },
+    {
+        "id": "7e17e5a3f4df3011",
+        "type": "debug",
+        "z": "5de5756d190f9086",
+        "name": "Inverter X",
+        "active": false,
+        "tosidebar": true,
+        "console": false,
+        "tostatus": false,
+        "complete": "payload",
+        "targetType": "msg",
+        "statusVal": "",
+        "statusType": "auto",
+        "x": 340,
+        "y": 2260,
+        "wires": []
+    },
+    {
+        "id": "fb7357db50501627",
+        "type": "change",
+        "z": "5de5756d190f9086",
+        "name": "Tags setzen",
+        "rules": [
+            {
+                "t": "set",
+                "p": "payload",
+                "pt": "msg",
+                "to": "(\t    $a := $split(topic, '/');\t    [\t        payload,\t        {\t            \"device\":$a[0],\t            \"name\":$a[1],\t            \"channel\":$a[2]\t        }\t    ]\t)\t",
+                "tot": "jsonata"
+            },
+            {
+                "t": "delete",
+                "p": "topic",
+                "pt": "msg"
+            }
+        ],
+        "action": "",
+        "property": "",
+        "from": "",
+        "to": "",
+        "reg": false,
+        "x": 610,
+        "y": 2360,
+        "wires": [
+            [
+                "91a4607dfda84b67"
+            ]
+        ]
+    },
+    {
+        "id": "670eb9fbb5c31b2c",
+        "type": "debug",
+        "z": "5de5756d190f9086",
+        "name": "InfluxDB",
+        "active": false,
+        "tosidebar": true,
+        "console": false,
+        "tostatus": false,
+        "complete": "payload",
+        "targetType": "msg",
+        "statusVal": "",
+        "statusType": "auto",
+        "x": 940,
+        "y": 2360,
+        "wires": []
+    },
+    {
+        "id": "1a8cca488d53394a",
+        "type": "switch",
+        "z": "5de5756d190f9086",
+        "name": "",
+        "property": "$split(topic, '/')[2]",
+        "propertyType": "jsonata",
+        "rules": [
+            {
+                "t": "eq",
+                "v": "available",
+                "vt": "str"
+            },
+            {
+                "t": "eq",
+                "v": "last_success",
+                "vt": "str"
+            },
+            {
+                "t": "regex",
+                "v": "(ch[0-6])\\b",
+                "vt": "str",
+                "case": false
+            },
+            {
+                "t": "eq",
+                "v": "radio_stat",
+                "vt": "str"
+            },
+            {
+                "t": "eq",
+                "v": "firmware",
+                "vt": "str"
+            },
+            {
+                "t": "eq",
+                "v": "hardware",
+                "vt": "str"
+            },
+            {
+                "t": "eq",
+                "v": "alarm",
+                "vt": "str"
+            }
+        ],
+        "checkall": "true",
+        "repair": false,
+        "outputs": 7,
+        "x": 330,
+        "y": 2380,
+        "wires": [
+            [
+                "845aeb93e39092c5"
+            ],
+            [
+                "241a8e70e9fde93c"
+            ],
+            [
+                "fb7357db50501627"
+            ],
+            [
+                "9d38f021308664c1"
+            ],
+            [
+                "a508355f0cc87966"
+            ],
+            [
+                "d2c9aa1a8978aca6"
+            ],
+            [
+                "b27032beb597d5a7"
+            ]
+        ]
+    },
+    {
+        "id": "845aeb93e39092c5",
+        "type": "debug",
+        "z": "5de5756d190f9086",
+        "name": "available",
+        "active": true,
+        "tosidebar": false,
+        "console": false,
+        "tostatus": true,
+        "complete": "payload",
+        "targetType": "msg",
+        "statusVal": "payload",
+        "statusType": "auto",
+        "x": 600,
+        "y": 2240,
+        "wires": []
+    },
+    {
+        "id": "241a8e70e9fde93c",
+        "type": "debug",
+        "z": "5de5756d190f9086",
+        "name": "last_success",
+        "active": true,
+        "tosidebar": false,
+        "console": false,
+        "tostatus": true,
+        "complete": "payload",
+        "targetType": "msg",
+        "statusVal": "payload",
+        "statusType": "auto",
+        "x": 610,
+        "y": 2300,
+        "wires": []
+    },
+    {
+        "id": "9d38f021308664c1",
+        "type": "debug",
+        "z": "5de5756d190f9086",
+        "name": "radio_stat",
+        "active": false,
+        "tosidebar": true,
+        "console": false,
+        "tostatus": false,
+        "complete": "payload",
+        "targetType": "msg",
+        "statusVal": "",
+        "statusType": "auto",
+        "x": 600,
+        "y": 2400,
+        "wires": []
+    },
+    {
+        "id": "a508355f0cc87966",
+        "type": "debug",
+        "z": "5de5756d190f9086",
+        "name": "firmware",
+        "active": false,
+        "tosidebar": true,
+        "console": false,
+        "tostatus": false,
+        "complete": "payload",
+        "targetType": "msg",
+        "statusVal": "",
+        "statusType": "auto",
+        "x": 600,
+        "y": 2440,
+        "wires": []
+    },
+    {
+        "id": "d2c9aa1a8978aca6",
+        "type": "debug",
+        "z": "5de5756d190f9086",
+        "name": "hardware",
+        "active": false,
+        "tosidebar": true,
+        "console": false,
+        "tostatus": false,
+        "complete": "payload",
+        "targetType": "msg",
+        "statusVal": "",
+        "statusType": "auto",
+        "x": 600,
+        "y": 2480,
+        "wires": []
+    },
+    {
+        "id": "b27032beb597d5a7",
+        "type": "debug",
+        "z": "5de5756d190f9086",
+        "name": "alarm",
+        "active": false,
+        "tosidebar": true,
+        "console": false,
+        "tostatus": false,
+        "complete": "payload",
+        "targetType": "msg",
+        "statusVal": "",
+        "statusType": "auto",
+        "x": 590,
+        "y": 2520,
+        "wires": []
+    },
+    {
+        "id": "d814738cf55ad663",
+        "type": "debug",
+        "z": "5de5756d190f9086",
+        "name": "total",
+        "active": false,
+        "tosidebar": true,
+        "console": false,
+        "tostatus": false,
+        "complete": "payload",
+        "targetType": "msg",
+        "statusVal": "",
+        "statusType": "auto",
+        "x": 590,
+        "y": 2160,
+        "wires": []
+    },
+    {
+        "id": "a55632ad0dff0b69",
+        "type": "switch",
+        "z": "5de5756d190f9086",
+        "name": "",
+        "property": "$split(topic, '/')[1]",
+        "propertyType": "jsonata",
+        "rules": [
+            {
+                "t": "eq",
+                "v": "uptime",
+                "vt": "str"
+            },
+            {
+                "t": "eq",
+                "v": "wifi_rssi",
+                "vt": "str"
+            },
+            {
+                "t": "eq",
+                "v": "status",
+                "vt": "str"
+            },
+            {
+                "t": "eq",
+                "v": "total",
+                "vt": "str"
+            }
+        ],
+        "checkall": "true",
+        "repair": false,
+        "outputs": 4,
+        "x": 330,
+        "y": 2100,
+        "wires": [
+            [
+                "1fbb0674d2576ee7"
+            ],
+            [
+                "e6be1c98ac55f511"
+            ],
+            [
+                "f9c2d3b30e34fdda"
+            ],
+            [
+                "d814738cf55ad663"
+            ]
+        ]
+    },
+    {
+        "id": "f9c2d3b30e34fdda",
+        "type": "debug",
+        "z": "5de5756d190f9086",
+        "name": "status",
+        "active": false,
+        "tosidebar": false,
+        "console": false,
+        "tostatus": true,
+        "complete": "payload",
+        "targetType": "msg",
+        "statusVal": "payload",
+        "statusType": "auto",
+        "x": 590,
+        "y": 2100,
+        "wires": []
+    },
+    {
+        "id": "e6be1c98ac55f511",
+        "type": "debug",
+        "z": "5de5756d190f9086",
+        "name": "wifi_rssi",
+        "active": false,
+        "tosidebar": false,
+        "console": false,
+        "tostatus": true,
+        "complete": "payload",
+        "targetType": "msg",
+        "statusVal": "payload",
+        "statusType": "auto",
+        "x": 600,
+        "y": 2040,
+        "wires": []
+    },
+    {
+        "id": "1fbb0674d2576ee7",
+        "type": "debug",
+        "z": "5de5756d190f9086",
+        "name": "uptime",
+        "active": false,
+        "tosidebar": false,
+        "console": false,
+        "tostatus": true,
+        "complete": "payload",
+        "targetType": "msg",
+        "statusVal": "payload",
+        "statusType": "auto",
+        "x": 590,
+        "y": 1980,
+        "wires": []
+    },
+    {
+        "id": "91a4607dfda84b67",
+        "type": "change",
+        "z": "5de5756d190f9086",
+        "name": "Lösche",
+        "rules": [
+            {
+                "t": "delete",
+                "p": "payload[0].YieldDay",
+                "pt": "msg"
+            },
+            {
+                "t": "delete",
+                "p": "payload[0].MaxPower",
+                "pt": "msg"
+            },
+            {
+                "t": "delete",
+                "p": "payload[0].ALARM_MES_ID",
+                "pt": "msg"
+            }
+        ],
+        "action": "",
+        "property": "",
+        "from": "",
+        "to": "",
+        "reg": false,
+        "x": 780,
+        "y": 2360,
+        "wires": [
+            [
+                "670eb9fbb5c31b2c"
+            ]
+        ]
+    },
+    {
+        "id": "319864a4e0fd913f",
+        "type": "mqtt-broker",
+        "name": "broker",
+        "broker": "localhost",
+        "port": "1883",
+        "clientid": "",
+        "autoConnect": true,
+        "usetls": false,
+        "protocolVersion": "4",
+        "keepalive": "60",
+        "cleansession": true,
+        "birthTopic": "",
+        "birthQos": "0",
+        "birthPayload": "",
+        "birthMsg": {},
+        "closeTopic": "",
+        "closeQos": "0",
+        "closePayload": "",
+        "closeMsg": {},
+        "willTopic": "",
+        "willQos": "0",
+        "willPayload": "",
+        "willMsg": {},
+        "userProps": "",
+        "sessionExpiry": ""
+    }
+]
\ No newline at end of file