diff --git a/CanGrow.geany b/CanGrow.geany
new file mode 100644
index 0000000..bdea1c0
--- /dev/null
+++ b/CanGrow.geany
@@ -0,0 +1,61 @@
+[editor]
+line_wrapping=false
+line_break_column=72
+auto_continue_multiline=true
+
+[file_prefs]
+final_new_line=true
+ensure_convert_new_lines=false
+strip_trailing_spaces=false
+replace_tabs=false
+
+[indentation]
+indent_width=2
+indent_type=0
+indent_hard_tab_width=8
+detect_indent=false
+detect_indent_width=false
+indent_mode=2
+
+[project]
+name=CanGrow
+base_path=./
+description=
+file_patterns=.ino,;.h;
+
+[long line marker]
+long_line_behaviour=1
+long_line_column=72
+
+[files]
+current_page=0
+FILE_NAME_0=493;Sh;0;EUTF-8;0;1;0;.%2Fcangrow.sh;0;2
+FILE_NAME_1=0;Arduino;0;EUTF-8;0;1;0;.%2FCanGrow.ino;0;2
+FILE_NAME_2=0;C++;0;EUTF-8;0;1;0;.%2Finclude%2FCanGrow.h;0;2
+FILE_NAME_3=0;C++;0;EUTF-8;0;1;0;.%2Finclude%2FCanGrow_Core.h;0;2
+FILE_NAME_4=0;C++;0;EUTF-8;0;1;0;.%2Finclude%2FCanGrow_ESP32.h;0;2
+FILE_NAME_5=0;C++;0;EUTF-8;0;1;0;.%2Finclude%2FCanGrow_ESP8266.h;0;2
+FILE_NAME_6=0;C++;0;EUTF-8;0;1;0;.%2Finclude%2FCanGrow_LittleFS.h;0;2
+FILE_NAME_7=0;C++;0;EUTF-8;0;1;0;.%2Finclude%2FCanGrow_Logo.h;0;2
+FILE_NAME_8=0;C++;0;EUTF-8;0;1;0;.%2Finclude%2FCanGrow_Version.h;0;2
+
+[build-menu]
+C++FT_00_LB=_Compile
+C++FT_00_CM=cd .. ; ./cangrow.sh build
+C++FT_00_WD=
+filetypes=C++;Arduino;Sh;
+ArduinoFT_00_LB=_Build
+ArduinoFT_00_CM=./cangrow.sh build
+ArduinoFT_00_WD=
+ArduinoFT_01_LB=Build & Upload
+ArduinoFT_01_CM=./cangrow.sh upload
+ArduinoFT_01_WD=
+C++FT_01_LB=_Build & Upload
+C++FT_01_CM=cd .. ; ./cangrow.sh upload
+C++FT_01_WD=
+ShFT_00_LB=Build
+ShFT_00_CM=./cangrow.sh build
+ShFT_00_WD=
+ShFT_01_LB=Build & Upload
+ShFT_01_CM=./cangrow.sh upload
+ShFT_01_WD=
diff --git a/CanGrow.ino b/CanGrow.ino
new file mode 100644
index 0000000..23d1540
--- /dev/null
+++ b/CanGrow.ino
@@ -0,0 +1,216 @@
+/*
+ * 
+ * CanGrow - an OpenSource growcontroller firmware (for cannabis)
+ * 
+ * 
+ * MIT License
+ * 
+ * Copyright (c) 2024 DeltaLima
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to 
+ * deal in the Software without restriction, including without limitation the 
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 
+ * sell copies of the Software, and to permit persons to whom the Software is 
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in 
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
+ * THE SOFTWARE.
+ *
+ */
+
+
+/*
+ *  Libraries include
+ */
+
+#include "Arduino.h"
+
+// * ESP8266 *
+#ifdef ESP8266
+ #include <ESP8266WiFi.h>
+ #include <ESPAsyncTCP.h>
+ #include <ESP8266HTTPClient.h>
+ #include <WiFiClient.h>
+#endif
+
+// * ESP32 *
+#ifdef ESP32
+ #include <WiFi.h>
+ #include <AsyncTCP.h>
+ #include <Update.h>
+ #include <HTTPClient.h>
+#endif
+
+#include <WiFiUdp.h>
+
+// https://github.com/thijse/Arduino-Log/
+#include <ArduinoLog.h>
+
+// https://github.com/mathieucarbou/ESPAsyncWebServer
+#include <ESPAsyncWebServer.h>
+
+// LittleFS filesystem
+#include "FS.h"
+// arduino-core for esp8266 and esp32
+#include "LittleFS.h"
+
+//#include <SPI.h>
+#include <Wire.h>
+
+// https://github.com/bblanchon/ArduinoJson
+#include <ArduinoJson.h>
+#include "AsyncJson.h"
+
+// https://github.com/PaulStoffregen/Time
+#include <TimeLib.h>
+// https://github.com/arduino-libraries/NTPClient/
+#include <NTPClient.h>
+
+// https://github.com/nusabot-iot/NusabotSimpleTimer/
+#include <NusabotSimpleTimer.h>
+
+// https://github.com/adafruit/RTClib/
+#include "RTClib.h"
+
+/*
+ *  CanGrow includes
+ */
+
+/* main header file, where all variables, consts and structs get defined */
+#include "include/CanGrow.h"
+/* CanGrow platform specific includes */
+#include "include/Architecture/ESP8266.h"
+#include "include/Architecture/ESP32.h"
+#include "include/Architecture/ESP32_LOLIN_S2_MINI.h"
+#include "include/Architecture/ESP32_MAKERGO_C3_SUPERMINI.h"
+
+/* CanGrow header with all functions
+ * order is important - I need to learn how to do it right, so order is not important */
+#include "include/CanGrow_ConfigHelper.h"
+#include "include/CanGrow_Sensor.h"
+#include "include/CanGrow_Output.h"
+#include "include/CanGrow_Core.h"
+#include "include/CanGrow_Wifi.h"
+#include "include/CanGrow_LittleFS.h"
+
+#include "include/CanGrow_Control.h"
+#include "include/CanGrow_Timer.h"
+#include "include/CanGrow_Webserver.h"
+
+
+
+void setup() {
+  /* Measure start up time */
+  unsigned long millisFinish;
+  // define output for onboard LED/WIPE pin
+  pinMode(PinWIPE, OUTPUT);
+  
+  
+  // Start Serial
+  Serial.begin(115200);
+      
+  // Write a line before doing serious output, because before there is some garbage in serial
+  // whats get the cursor somewhere over the place
+  Serial.println("420");
+  
+  // initiate ArduinoLog
+  
+  Log.setPrefix(LogPrefix);
+  Log.begin(LOG_LEVEL_VERBOSE, &Serial);
+  // disable show loglevel, we do it in Prefix
+  Log.setShowLevel(false);
+  // set Log Location, to tell user at which part of the code we are
+  const char LogLoc[]  = "[SETUP]";
+  
+  //Serial.printf(".:: CanGrow firmware v%s build %s starting ::.\n", CANGROW_VER, CANGROW_BUILD);
+  Log.notice(F("CanGrow firmware v%s build %s starting ::" CR), CANGROW_VER, CANGROW_BUILD);
+    
+  Log.warning(F("%s To format / factory reset LittleFS, pull GPIO %d  (PinWIPE) to %d - NOW! (2 seconds left)" CR), LogLoc, PinWIPE, 1 - PinWIPE_default );
+  
+  // blink with the onboard LED on D4/GPIO2 (PinWIPE)
+  for(byte i = 0; i <= 6 ; i++) {
+    if(i % 2) {
+      digitalWrite(PinWIPE, 1 - PinWIPE_default);
+    } else {
+      digitalWrite(PinWIPE, PinWIPE_default);
+    }
+    delay(333);
+  }
+  
+  // set PinWIPE back to its default
+  digitalWrite(PinWIPE, PinWIPE_default);
+    
+  // read status from PinWIPE to WIPE
+  // when PinWIPE is set to LOW, format LittleFS
+  if(digitalRead(PinWIPE) != PinWIPE_default) {
+    LFS_Format();
+    Restart();
+  }
+  /* for ESP32-C3 supermini board compatibility, we initiate I2C here and not at the beginning
+   * ESP32-C3 supermini board shares GPIO 8 Internal LED with I2C SDA */
+  /* I2C init*/
+  Wire.begin(Pin_I2C_SDA, Pin_I2C_SCL);
+  
+  LFS_Init();
+  LoadConfig();
+  Wifi_Init();
+  Webserver_Init();
+  
+  Log.notice(F("%s Usable Pins: %d" CR), LogLoc, GPIOindex_length);
+  // List all available pins
+  for(byte i = 1; i <= GPIOindex_length; i++) {
+    Log.notice(F("%s Pin Index: %d, GPIO: %d, Notes: %s" CR), LogLoc, i , GPIOindex[i].gpio, GPIO_Index_note_descr[GPIOindex[i].note]);
+  }
+  
+  // time init 
+  Time_Init();
+  TimeR_Init();
+
+  
+  #ifdef ESP8266
+  /* set pwm frequency global for ESP8266. 
+   * ESP32 pwm frequency setting is done withing CanGrow_Output / Init */
+  analogWriteFreq(config.system.pwmFreq);
+  #endif
+  
+  Output_Init();
+  
+  Sensor_Init();
+
+  Log.notice(F("%s Done. Startup took : %u ms" CR), LogLoc, millis());
+}
+
+bool alrdySaved = false;
+
+void loop() {
+  const char LogLoc[]  = "[LOOP]";
+  
+  /* Execute main timer, runs Timer_1s, Timer_3s, Timer_5s by default */
+  timer.run();
+  
+  // if global var doRestart is true, perform a restart
+  if(doRestart == true) {
+    /* wait 100ms after Restart got triggered. This should workaround some crash problems with AsyncWebserver stuff 
+     * for example when updating the firmware by web upload */
+    Log.verbose(F("%s Restart got triggered. Waiting 100ms before doing it" CR), LogLoc);
+    timer.setTimeout(100, Restart);
+    //Restart();
+  }
+  
+  // does ntp offset need an update?
+  if(updateNtpOffset) {
+    /* doing ntp offset update here, because when doing it in the webserver:system function
+     * where the new value gets entered, it sometimes crashed */
+    NTP_OffsetUpdate();
+    updateNtpOffset = false;
+  }
+}
diff --git a/README.md b/README.md
index 4f25752..36449d7 100644
--- a/README.md
+++ b/README.md
@@ -1,94 +1,53 @@
-# CanGrow
+# CanGrow - An OpenSource grow controller firmware for ESP8266 / ESP32
+![Screenshot_montage.png](Screenshot_montage.png)
 
 
-![CanGrowLogo](Logo/CanGrow_Logo.png)
 
+## Build environment
+The helper script `cangrow.sh` is written for a Debian 12 system. 
 
-An easy to use DIY grow controller firmware (for cannabis).
+To install all dependencies you need for building the firmware, run the cangrow.sh setup:
 
-![Screenshot_WebUI_root.png](Arduino/CanGrow/Screenshot_WebUI_root.png)
-![CanGrow_PCB_Front.png](KiCad/CanGrow/CanGrow_PCB_Front_small.png)
+```sh
+$ ./cangrow.sh help
+./cangrow.sh [setup|build|upload|webupload|monitor]
+setup: setup build environment, download arduino-cli, install all dependencies for arduino ide
+build: build firmware binary. will be saved into build/
+upload: upload firmware by serial connection /dev/ttyUSB0
+webupload: upload firmware with webupload to 192.168.4.20
+monitor: serial monitor /dev/ttyUSB0
 
-# WORK IN PROGRESS
+# Install all dependencies for build environment
+$ ./cangrow.sh setup
+```
 
-## Motivation
-I havn't found an already existing grow controller project within the ESP / Arduino Core eco system which 
-met my personal requirements. 
-Those are an easy DIY, using low cost parts, Arduino Core sourcecode to hack own things together, having a WebUI, grab some Metrics for monitoring, standalone and my very special need that the Hardware should run completely with 12V. 
+The script installs [arduino-cli](https://github.com/arduino/arduino-cli) to `~/.local/bin/arduino-cli`. 
 
-### Update 14.09.2024 - Code Rewrite v0.2
+## Compile
 
-I took some "summer break" from the project, and had the opportunity to talk to different people about it.
-My conclusion at this point is, that the focus of this project is not the Hardware, it came out that it should be the software.
-So I decided to completely rewrite the code from 0 - with recycling some parts of it. 
-Goal of the Rewrite is that the Firmware becomes more independent of the hardware used. It has to support both ESP8266 and ESP32 
-and let the user decide at which pin which output, sensor or whatever will be connected to. Like done in the [Tasmota](https://github.com/arendst/Tasmota) Firmware, I also want to support "Hardware Templates" which come with presets for PCBs like the one I created.
+```sh
+# compile and output to build/CanGrow_v0.2...bin
+# Default Target is ESP8266 D1 Mini
+$ ./cangrow.sh build
 
-**Checklist for v0.2 Firmware**
-- Support ESP8266 and ESP32 
-- AsyncWebserver instead ESP8266Webserver
-- LittleFS instead of EEPROM()
-- deliver static HTML, dynamic Stuff with Javascript
-  - (or is there a better way? please tell me!)
-- Free configurable outputs
-  - Main outputs for Light, Air, Water 
-  - Support for Tasmota Wifi Plugs (and others?)
-  - No Limitation for Amount of outputs 
-  - Light
-    - support for I2C 0-10V Dimm control
-    - PWM dimm control
-  - Air
-    - support for I2C 0-10V Dimm control
-    - PWM dimm control
-    - Support for humidifier, heater (, CO2?)
-    - Read Fan RPM
-  - Water
-    - Usual watering
-    - Pump for fertilizer
-- Free configurable Inputs
-  - Support for various I2C devices
-    - All kind of sensors for Temp, Humidity, Moisture, and so on
-    - Support for ADCs to connect multiple analoge sensors
-  - Support for Analog inputs
-    - onboard ones or I2C (ADC)
-    - Analog Multiplexer support (like CD4051)
-  - Calibrate sensors 
-    - define 0% and 100% values
-    - Offsets
-- MQTT support
-- API
+# Compile for ESP32 D1 Mini
+$ export BOARD="esp32:esp32:d1_mini32"
+$ ./cangrow.sh build
 
-  
-  
-## Old v0.1 Features / ToDo List
+# Build and webupload to IP
+$ export IP="192.168.4.69"
+$ ./cangrow.sh build # need to make .bin first
+$ ./cangrow.sh webupload # upload
 
-- Measure values :white_check_mark:
-  - Humidity :white_check_mark:
-  - soil moisture :white_check_mark: 
-  - temperature :white_check_mark:
-  - water level for water tank :white_check_mark:
-- LED grow light control (on/off, dimming, max. 12V 50W load ) :white_check_mark:
-  - You can of course use a relais as well, if you want to drive 220V lights :white_check_mark:
-- fan control (on/off, (PWM?) max 1A) :white_check_mark:
-- pump control for automatic watering (max 1A) :large_blue_circle:
-- Web UI and REST API for data and controlling :large_blue_circle:
-  - simple web ui :white_check_mark:
-  - REST API :large_blue_circle:
-  - Send notifications with web call (e.g. for mastodon) :red_circle:
-  - predefined grow profiles :large_blue_circle:
-  - persistent data :white_check_mark:
-    - Start of Grow :white_check_mark:
-    - day of grow :large_blue_circle:
-    - grow profile 
-      - watering amount per week :large_blue_circle:
-      - light cycle :white_check_mark:
-    - wifi settings :white_check_mark:
-    - settings in general :white_check_mark:
-- Easy to build and use for beginners (i hope so!) :white_check_mark:
-  - PCB layout to order from manufacture (jlcpcb or pcbway) :white_check_mark:
-  - easy to build up on a perfboard :white_check_mark:
-  - easy to etch pcb :white_check_mark:
-  - easy to access and modify :white_check_mark:
-  - low cost as possible! :white_check_mark:
+# listen to serial monitor on /dev/ttyUSB2
+$ export TTY="/dev/ttyUSB2"
+./cangrow.sh monitor
+```
 
-:white_check_mark: Done - :large_blue_circle: In Progress - :red_circle: ToDo
+I wrote this project using [Geany IDE. ](https://www.geany.org/). The Geany Projectfile is also included, just run
+```sh
+$ geany CanGrow.geany
+```
+
+**F8 compiles** the project, **F9 uploads** firmware to /dev/ttyUSB0. You can change these settings for .ino and .h files
+in Project -> Settings -> Create/Make.
diff --git a/Screenshot_montage.png b/Screenshot_montage.png
new file mode 100644
index 0000000..7fbb2d6
Binary files /dev/null and b/Screenshot_montage.png differ
diff --git a/allbuild.sh b/allbuild.sh
new file mode 100755
index 0000000..1ecf7a4
--- /dev/null
+++ b/allbuild.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+#
+
+rm -Rf build/*
+for board in esp8266:esp8266:d1_mini_clone esp32:esp32:d1_mini32 esp32:esp32:makergo_c3_supermini esp32:esp32:lolin_s2_mini
+do
+	echo "Build firmware binary for $board"
+	echo "==================================================================="
+	BOARD="$board" ./cangrow.sh build
+done
diff --git a/arduino-cli.yml b/arduino-cli.yml
new file mode 100644
index 0000000..29db8f5
--- /dev/null
+++ b/arduino-cli.yml
@@ -0,0 +1,4 @@
+board_manager:
+  additional_urls:
+    - http://arduino.esp8266.com/stable/package_esp8266com_index.json
+    - https://espressif.github.io/arduino-esp32/package_esp32_index.json
diff --git a/cangrow.sh b/cangrow.sh
new file mode 100755
index 0000000..eda39dc
--- /dev/null
+++ b/cangrow.sh
@@ -0,0 +1,167 @@
+#!/bin/bash
+#
+
+test -z $TTY && TTY="/dev/ttyUSB0"
+test -z $IP && IP="192.168.4.20"
+test -z $VER && VER="$(grep "define CANGROW_VER" include/CanGrow.h | cut -d \" -f2 |sed -e 's/\"//g')" #VER="0.2-dev"
+test -z $BOARD && BOARD="esp8266:esp8266:d1_mini_clone"
+#test -z $BOARD && BOARD="esp32:esp32:d1_mini32"
+
+BUILD="$(git rev-parse --short HEAD)-$(echo $BOARD | cut -d : -f1)_$(echo $BOARD | cut -d : -f3)-$(date '+%Y%m%d%H%M%S')"
+
+# arduino-cli path and version
+ACLI="$HOME/.local/bin/arduino-cli"
+ACLI_VER="1.2.0"
+ACLI_CMD="$ACLI --config-file arduino-cli.yml"
+test -z $BUILDDIR && BUILDDIR="build"
+
+
+function help() {
+	echo "$0 [setup|build|upload|webupload|monitor]"
+	echo "setup: setup build environment, download arduino-cli, install all dependencies for arduino ide"
+	echo "build: build firmware binary. will be saved into ${BUILDDIR}/"
+	echo "upload: upload firmware by serial connection $TTY"
+	echo "webupload: upload firmware with webupload to $IP"
+	echo "monitor: serial monitor $TTY"
+	exit 1
+}
+
+function check_acli() {
+	if [ ! -x $ACLI ]
+	then
+		echo "$ACLI does not exist nor is executable. Please run '$0 setup' first"
+		exit 1
+	fi
+}
+
+test -z $1 && help
+
+case $1 in
+	s|setup)
+		ACLI_DIR="$(dirname $ACLI)"
+    ALIB_DIR="${HOME}/Arduino/libraries/"
+		declare -a CORES=(
+      "esp8266:esp8266@3.1.2"
+      "esp32:esp32@3.0.7"
+				)
+		declare -a LIBS=(
+      "Adafruit SSD1306@2.5.12"
+      "Adafruit BME280 Library@2.2.4"
+      "ArduinoJson@7.3.0"
+      "NTPClient@3.2.1"
+      "Time@1.6.1"
+      "ESP Async WebServer@3.6.0"
+      "Async TCP@3.3.2"
+      "Nusabot Simple Timer@1.0.0"
+      "ArduinoLog@1.1.1"
+      "RTClib@2.1.4"
+      "Adafruit BME680 Library@2.0.5"
+      "Adafruit ADS1X15@2.5.0"
+      "Adafruit SHT31 Library@2.2.2"
+      "Adafruit MCP4725@2.0.2"
+      "Adafruit TCS34725@1.4.4"
+      "Adafruit MLX90614 Library@2.1.5"
+      "I2CSoilMoistureSensor@1.1.4"
+      "DFRobot_GP8XXX@1.0.1"
+      "Adafruit CCS811 Library@1.1.3"
+    )
+    
+		echo ":: Setting up build environment for CanGrow Firmware."
+		echo "   This will download the binary for arduino-cli and install"
+		echo "   the packages for the arduino ide from the debian repository."
+		echo "   !! This script is meant to be executed on a Debian stable (bookworm) system !!"
+		echo ""
+		echo ":: Press Enter to continue"
+		read
+		echo ""
+		echo ":: Installing Arduino IDE packages with apt, please enter sudo password:"
+		sudo apt update || exit 1
+		sudo apt install arduino python3 python3-serial wget curl xxd || exit 1
+		echo ":: Ensure directory ${ACLI_DIR} is present"
+		test -d ${ACLI_DIR} || mkdir -p ${ACLI_DIR}
+		echo ":: Please ensure ${ACLI_DIR} is in your \$PATH, I wont do it."
+		echo ""
+		echo ":: Downloading arduino-cli ${ACLI_VER} into ${ACLI_DIR}/"
+		wget -O - "https://github.com/arduino/arduino-cli/releases/download/v${ACLI_VER}/arduino-cli_${ACLI_VER}_Linux_64bit.tar.gz" | tar -C ${ACLI_DIR} -zxvf - arduino-cli 
+		chmod +x ${ACLI}
+		echo ""
+		echo ":: Installing ESP8266 and ESP32 cores for Arduino"
+		for core in ${!CORES[@]}
+		do
+			${ACLI_CMD} core install ${CORES[$core]}
+		done
+		echo ":: Installing Arduino libraries"
+		${ACLI_CMD} lib update-index || exit 1
+		for lib in ${!LIBS[@]}
+		do
+			echo "     - ${LIBS[$lib]}"
+		done
+		
+		for lib in ${!LIBS[@]}
+		do
+			${ACLI_CMD} lib install "${LIBS[$lib]}" || exit 1
+		done
+		echo ""
+    
+    echo ":: fetching ESPAsyncTCP-esphome from GIT"
+    wget -q https://github.com/mathieucarbou/esphome-ESPAsyncTCP/archive/refs/tags/v2.0.0.tar.gz -O - | tar -xzf - -C $ALIB_DIR
+    mv $ALIB_DIR/esphome-ESPAsyncTCP-2.0.0 $ALIB_DIR/ESPAsyncTCP-esphome
+    echo ":: Patching ArduinoLog (https://github.com/thijse/Arduino-Log/pull/28/commits/57d350a25428376935b793a2138210320cf3801c)"
+    sed -i -e 's/register//g' $ALIB_DIR/ArduinoLog/ArduinoLog.cpp
+    
+		echo ":: Setup build environment done! You can now build the firmware"
+		echo "   with: $0 build"
+
+		;;
+	b|build)
+		check_acli
+		ACLI_CMD="${ACLI_CMD} --output-dir ${BUILDDIR}"
+		echo ":: Building firmware $VER $BUILD, target dir: ${BUILDDIR}/"
+		
+		test -d ${BUILDDIR} || mkdir ${BUILDDIR}
+
+
+		# esp8266 and esp32 compiler have to use different compile flags for VER and BUILD
+		if [ "$(echo $BOARD | cut -d : -f1)" == "esp8266" ]
+		then
+			${ACLI_CMD} --no-color compile -b ${BOARD} --build-property "build.extra_flags=-DCANGROW_VER=\"${VER}\" -DCANGROW_BUILD=\"${BUILD}\"" "CanGrow.ino"  || exit 1
+		elif [ "$(echo $BOARD | cut -d : -f1)" == "esp32" ]
+		then
+			${ACLI_CMD} --no-color compile -b ${BOARD} --build-property "build.defines=-DCANGROW_VER=\"${VER}\" -DCANGROW_BUILD=\"${BUILD}\"" "CanGrow.ino" || exit 1
+		fi
+
+		cp ${BUILDDIR}/CanGrow.ino.bin ${BUILDDIR}/CanGrow_v${VER}_${BUILD}.bin
+		;;
+	u|upload)
+		check_acli
+		echo ":: Build and upload firmware $VER $BUILD to $TTY"
+		
+		test -d build || mkdir build
+
+		# esp8266 and esp32 compiler have to use different compile flags for VER and BUILD
+		if [ "$(echo $BOARD | cut -d : -f1)" == "esp8266" ]
+		then
+			${ACLI_CMD} --no-color compile -b ${BOARD} --build-property "build.extra_flags=-DCANGROW_VER=\"${VER}\" -DCANGROW_BUILD=\"${BUILD}\""  ${ACLI_BUILD_OPTS} -u -p $TTY  "CanGrow.ino"
+		elif [ "$(echo $BOARD | cut -d : -f1)" == "esp32" ]
+		then
+			${ACLI_CMD} --no-color compile -b ${BOARD} --build-property "build.defines=-DCANGROW_VER=\"${VER}\" -DCANGROW_BUILD=\"${BUILD}\""  ${ACLI_BUILD_OPTS} -u -p $TTY  "CanGrow.ino"
+		fi
+
+		;;
+	w|webupload)
+		test -z "$2" && UPLOAD_FILE="${BUILDDIR}/CanGrow.ino.bin" 
+		test -n "$2" && UPLOAD_FILE="$2"
+
+		echo ":: Uploading $UPLOAD_FILE to $IP"
+		curl -v http://$IP/system/update -X POST -H 'Content-Type: multipart/form-data' -F "firmware=@${UPLOAD_FILE}"
+		echo
+		;;	
+	m|mon|monitor)
+		check_acli
+		echo ":: Open serial monitor $TTY"
+		${ACLI_CMD} monitor -c baudrate=115200 -b ${BOARD} -p $TTY
+		;;
+	*)
+		help
+		;;
+esac
diff --git a/include/Architecture/ESP32.h b/include/Architecture/ESP32.h
new file mode 100644
index 0000000..af3331c
--- /dev/null
+++ b/include/Architecture/ESP32.h
@@ -0,0 +1,69 @@
+/*
+ * 
+ * include/CanGrow_ESP32.h - ESP32 specific header file for generic ESP32_DEV board
+ * 
+ * 
+ * 
+ *
+ */
+#if defined(ARDUINO_ESP32_DEV) || defined(ARDUINO_D1_MINI32)
+
+#define PinWIPE 2
+#define PinWIPE_default LOW
+#define Pin_I2C_SCL 22
+#define Pin_I2C_SDA 21
+
+/* https://randomnerdtutorials.com/esp32-pinout-reference-gpios/
+ * 
+ * free usable pins
+ * - GPIO 0     PU    OK    outputs PWM signal at boot, must be LOW to enter flashing mode
+ * - GPIO 4     OK    OK    
+ * - GPIO 5     OK    OK    outputs PWM signal at boot, strapping pin
+ * - GPIO 12    OK    OK    boot fails if pulled high, strapping pin
+ * - GPIO 13    OK    OK    
+ * - GPIO 14    OK    OK    outputs PWM signal at boot
+ * - GPIO 15    OK    OK    outputs PWM signal at boot, strapping pin
+ * - GPIO 16    OK    OK    
+ * - GPIO 17    OK    OK    
+ * - GPIO 18    OK    OK    
+ * - GPIO 19    OK    OK    
+ * - GPIO 23    OK    OK    
+ * - GPIO 25    OK    OK    
+ * - GPIO 26    OK    OK    
+ * - GPIO 27    OK    OK    
+ * - GPIO 32    OK    OK    
+ * - GPIO 33    OK    OK    
+ * - GPIO 34    OK    input only
+ * - GPIO 35    OK    input only
+ * - GPIO 36    OK    input only
+ * - GPIO 39    OK    input only
+ */
+
+
+// 
+const byte GPIOindex_length = 21;
+// initialize pinIndex with all usable GPIOs
+GPIO_Index GPIOindex[] = {{ 255, 255 },
+                          { 0, FLASHMODE_LOW },
+                          { 4 },
+                          { 5 },
+                          { 12, BOOTFAILS_HIGH },
+                          { 13 },
+                          { 14 },
+                          { 15 },
+                          { 16 },
+                          { 17 },
+                          { 18 },
+                          { 19 },
+                          { 23 },
+                          { 25, INT_DAC },
+                          { 26, INT_DAC },
+                          { 27 },
+                          { 32, INT_ADC },
+                          { 33, INT_ADC },
+                          { 34, INPUT_ONLY },
+                          { 35, INPUT_ONLY },
+                          { 36, INPUT_ONLY },
+                          { 39, INPUT_ONLY }
+                            };
+#endif
diff --git a/include/Architecture/ESP32_LOLIN_S2_MINI.h b/include/Architecture/ESP32_LOLIN_S2_MINI.h
new file mode 100644
index 0000000..fedcbaf
--- /dev/null
+++ b/include/Architecture/ESP32_LOLIN_S2_MINI.h
@@ -0,0 +1,74 @@
+/*
+ * 
+ * include/CanGrow_ESP32.h - ESP32 specific header file for Lolin S2 Mini
+ * 
+ * 
+ *
+ */
+#ifdef ARDUINO_LOLIN_S2_MINI
+
+#define PinWIPE 15
+#define PinWIPE_default LOW
+#define Pin_I2C_SCL 33
+#define Pin_I2C_SDA 35
+
+
+/* https://done.land/components/microcontroller/families/esp/esp32/developmentboards/esp32-s2/s2mini/
+ * 
+ * free usable pins
+    Pin 	Remark 	Description
+    EN 	  	Reset button
+    3V3 	  	direct power supply to CPU
+    VBUS 	  	connected to ME6211C33 voltage regulator
+    0 	not exposed 	Boot button pulls it low
+    1-6 	  	general purpose: analog input (ADC1) and digital in/output
+    7 	SPI SCK 	general purpose: analog input (ADC1) and digital in/output
+    8 	  	general purpose: analog input (ADC1) and digital in/output
+    9 	SPI MISO 	general purpose: analog input (ADC1) and digital in/output
+    10 	  	general purpose: analog input (ADC1) and digital in/output
+    11 	SPI MOSI 	general purpose: analog input (ADC2) and digital in/output
+    12 	SPI SS 	general purpose: analog input (ADC2) and digital in/output
+    13-14 	  	general purpose: analog input (ADC2) and digital in/output
+    15 	internal LED 	general purpose: analog input (ADC2) and digital in/output
+    16 	  	general purpose: analog input (ADC2) and digital in/output
+    17 	DAC1 	general purpose: analog input (ADC2) and digital in/output
+    18 	DAC2 	general purpose: analog input (ADC2) and digital in/output
+    19, 20 	not exposed 	USB D1/D2, connected to the USB C connector
+    21 	  	general purpose digital in/output
+    33 	I2C SDA 	general purpose digital in/output
+    34 	  	general purpose digital in/output
+    35 	I2C SCL 	general purpose digital in/output
+    36-40 	  	general purpose digital in/output
+ */
+
+
+// 
+const byte GPIOindex_length = 24;
+// initialize pinIndex with all usable GPIOs
+GPIO_Index GPIOindex[] = {{ 255, 255 },
+                          { 1, INT_ADC },
+                          { 2, INT_ADC },
+                          { 3, INT_ADC },
+                          { 4, INT_ADC },
+                          { 5, INT_ADC },
+                          { 6, INT_ADC },
+                          { 7, INT_ADC },
+                          { 8, INT_ADC },
+                          { 9, INT_ADC },
+                          { 10, INT_ADC },
+                          { 11 },
+                          { 12 },
+                          { 13 },
+                          { 14 },
+                          { 16 },
+                          { 17, INT_DAC },
+                          { 18, INT_DAC },
+                          { 21 },
+                          { 34 },
+                          { 36 },
+                          { 37 },
+                          { 38 },
+                          { 39 },
+                          { 40 }
+                            };
+#endif
diff --git a/include/Architecture/ESP32_MAKERGO_C3_SUPERMINI.h b/include/Architecture/ESP32_MAKERGO_C3_SUPERMINI.h
new file mode 100644
index 0000000..2201151
--- /dev/null
+++ b/include/Architecture/ESP32_MAKERGO_C3_SUPERMINI.h
@@ -0,0 +1,48 @@
+/*
+ * 
+ * include/Platform/ESP32_MAKERGO_C3_SUPERMINI.h - ESP32 specific header file
+ * 
+ * 
+ *
+ */
+#ifdef ARDUINO_MAKERGO_C3_SUPERMINI
+
+#define PinWIPE 8
+#define PinWIPE_default HIGH
+#define Pin_I2C_SCL 9
+#define Pin_I2C_SDA 8
+
+/* https://www.sudo.is/docs/esphome/boards/esp32c3supermini/
+ * 
+ * free usable pins
+ 0	GPIO0	ADC1
+1	GPIO1	ADC1
+2	GPIO2	ADC1, boot mode / strapping pin
+3	GPIO3	ADC1
+4	GPIO4	ADC1, JTAG
+5	GPIO5	JTAG
+6	GPIO6	JTAG
+7	GPIO7	JTAG
+8	GPIO8	Blue status_led (inverted), boot mode / strapping pin
+9	GPIO9	Boot mode / strapping pin, boot button
+10	GPIO10	
+20	GPIO20	RX
+21	GPIO21	TX
+ */
+
+
+// 
+const byte GPIOindex_length = 9;
+// initialize pinIndex with all usable GPIOs
+GPIO_Index GPIOindex[] = {{ 255, 255 },
+                          { 0, INT_ADC },
+                          { 1, INT_ADC },
+                          { 2, INT_ADC },
+                          { 3, INT_ADC },
+                          { 4, INT_ADC },
+                          { 5 },
+                          { 6 },
+                          { 7 },
+                          { 10 }
+                            };
+#endif
diff --git a/include/Architecture/ESP8266.h b/include/Architecture/ESP8266.h
new file mode 100644
index 0000000..a061bbc
--- /dev/null
+++ b/include/Architecture/ESP8266.h
@@ -0,0 +1,51 @@
+/*
+ * 
+ * include/CanGrow_ESP8266.h - ESP8266 specific header file
+ * 
+ * 
+ *
+ */
+#ifdef ESP8266
+
+// GPIO 2 Boot fails if pulled to LOW
+#define PinWIPE 2
+#define PinWIPE_default HIGH
+#define Pin_I2C_SCL 5
+#define Pin_I2C_SDA 4
+
+/* https://randomnerdtutorials.com/esp8266-pinout-reference-gpios/
+ * 
+ * free usable pins
+ * - GPIO 0 / D3 boot fails if pulled LOW
+ * - GPIO 12 / D6
+ * - GPIO 13 / D7
+ * - GPIO 14 / D5
+ * - GPIO 15 / D8 Boot fails if pulled HIGH
+ * - GPIO 16 / D0
+ */
+
+const byte GPIOindex_length = 6;
+// initialize pinIndex with all usable GPIOs
+GPIO_Index GPIOindex[] = {{ 255, 255 },
+                          { 0, BOOTFAILS_LOW },
+                          { 12 },
+                          { 13 },
+                          { 14 },
+                          { 15, BOOTFAILS_HIGH },
+                          { 16, NO_PWM }  };
+
+#endif
+
+
+/* CanGrow 12V PCB v0.6 Pin assignment
+ * 
+ * 
+ * LED - D6 (GPIO 12)
+ * FAN1 - D5 (GPIO 14)
+ * FAN2 - D3 (GPIO 0)
+ * PUMP - D0 (GPIO 16)
+ * 
+ * WaterlevelVCC - D7 (GPIO 13)
+ * SoilmoistureVCC - D8 (GPIO 15)
+ * 
+ */
diff --git a/include/CanGrow.h b/include/CanGrow.h
new file mode 100644
index 0000000..7d405ab
--- /dev/null
+++ b/include/CanGrow.h
@@ -0,0 +1,411 @@
+/*
+ * 
+ * include/CanGrow.h - main header file
+ * 
+ * 
+ *
+ */
+
+/* If you need detailed debug output, uncomment the following lines. 
+ * DEBUG is less noisy messages
+ * DEBUG2 are noisy messages
+ * DEBUG3 are super noisy messages */
+//#define DEBUG
+//#define DEBUG2
+//#define DEBUG3
+
+/* ensure the code will also compile when CANGROW_VER and CANGROW_BUILD
+ * are not defined by the compiler arguments
+ * like -DCANGROW_VER="0.x-dev" or -DCANGROW_BUILD="commitid-core-timestamp"
+*/
+
+
+/*
+ * 
+ * 
+ * Constants
+ * 
+ * 
+ */
+
+#ifndef CANGROW_VER
+#define CANGROW_VER "0.2-dev2"
+#endif
+#ifndef CANGROW_BUILD
+#define CANGROW_BUILD "0420"
+#endif
+#ifndef CANGROW_BUILDTIME
+#define CANGROW_BUILDTIME "1711922400" // 1.4.2024
+#endif
+
+#define CANGROW_DEFAULT_WIFI_SSID "CanGrow-unconfigured"
+#define CANGROW_DEFAULT_WIFI_PASSWORD "letitgrow!"
+
+#define CANGROW_CFG "/config.json"
+#define TIME2FS "/time"
+
+/* define Max limits for outputs and sensors */
+const byte Max_Outputs = 16;
+const byte Max_Sensors = 16;
+/* How much values can a sensor contain  at max */
+const byte Max_Sensors_Read = 6;
+/* how much GPIOs a Sensor can use */
+const byte Max_Sensors_GPIO = 2;
+
+
+/* actual structure initialization for GPIO_Index is done within the header files
+ * for ESP32 and ESP8266
+ * 
+ * GPIO_Index.note explenation:
+ *   1 - BOOTFAILS_LOW: BootFails when LOW
+ *   2 - BOOTFAILS_HIGH: BootFails when HIGH
+ *   3 - FLASHMODE_LOW: FlashMode needs LOW to enter
+ *   4 - INPUT_ONLY: Input Only
+ *   5 - NO_PWM: No PWM output
+ *   6 - PWM_BOOT: PWM at boot time
+ *   7 - INT_ADC Pin for internal ADC (only ESP32, ESP8266 only has one Pin, A0)
+ */
+const byte BOOTFAILS_LOW = 1;
+const byte BOOTFAILS_HIGH = 2;
+const byte FLASHMODE_LOW = 3;
+const byte INPUT_ONLY = 4;
+const byte NO_PWM = 5;
+const byte HIGH_BOOT = 6;
+const byte INT_ADC = 7;
+const byte INT_DAC = 8;
+
+
+//const char signMessage[] PROGMEM = {"I AM PREDATOR,  UNSEEN COMBATANT. CREATED BY THE UNITED STATES DEPART"};
+
+
+const char BOOTFAILS_LOW_descr[] PROGMEM = {"BF_LOW"};
+const char BOOTFAILS_HIGH_descr[] PROGMEM = {"BF_HIGH"};
+const char FLASMODE_LOW_descr[] PROGMEM = {"FM_LOW"};
+const char INPUT_ONLY_descr[] PROGMEM = {"IN_ONLY"};
+const char NO_PWM_descr[] PROGMEM = {"NO_PWM"};
+const char HIGH_BOOT_descr[] PROGMEM = {"B_HIGH"};
+const char INT_ADC_descr[] PROGMEM = {"INT_ADC"};
+const char INT_DAC_descr[] PROGMEM = {"INT_DAC"};
+
+const char * GPIO_Index_note_descr[] = {
+  NULL, // 0 - no note
+  BOOTFAILS_LOW_descr, // 1
+  BOOTFAILS_HIGH_descr, // 2
+  FLASMODE_LOW_descr, // 3
+  INPUT_ONLY_descr, // 4
+  NO_PWM_descr, // 5
+  HIGH_BOOT_descr, // 6
+  INT_ADC_descr, // 7
+  INT_DAC_descr, // 8
+};
+
+
+/*
+ * RTCs available
+ */
+ 
+// 0 is unconfigured
+const byte RTCs_total = 4;
+
+const byte RTCs_DS1307 = 1;
+const byte RTCs_DS3231 = 2;
+const byte RTCs_PCF8523 = 3;
+const byte RTCs_PCF8563 = 4;
+
+const char RTCs_DS1307_descr[] PROGMEM = {"DS1307"};
+const char RTCs_DS3231_descr[] PROGMEM = {"DS3231"};
+const char RTCs_PCF8523_descr[] PROGMEM = {"PCF8523"};
+const char RTCs_PCF8563_descr[] PROGMEM = {"PCF8563"};
+
+const char * RTCs_descr[] = {
+  NULL, // unconfigured
+  RTCs_DS1307_descr,
+  RTCs_DS3231_descr,
+  RTCs_PCF8523_descr,
+  RTCs_PCF8563_descr,
+};
+
+
+/*
+ * Time scales
+ */
+ 
+// 0 is unconfigured
+const byte TIMESCALE_total = 7;
+
+const byte TIMESCALE_SECOND = 0;
+const byte TIMESCALE_MINUTE = 1;
+const byte TIMESCALE_HOUR = 2;
+const byte TIMESCALE_DAY = 3;
+const byte TIMESCALE_WEEK = 4;
+const byte TIMESCALE_MONTH = 5;
+const byte TIMESCALE_YEAR = 6;
+
+
+const char TIMESCALE_SECOND_descr[] PROGMEM = {"Second"};
+const char TIMESCALE_MINUTE_descr[] PROGMEM = {"Minute"};
+const char TIMESCALE_HOUR_descr[] PROGMEM = {"Hour"};
+const char TIMESCALE_DAY_descr[] PROGMEM = {"Day"};
+const char TIMESCALE_WEEK_descr[] PROGMEM = {"Week"};
+const char TIMESCALE_MONTH_descr[] PROGMEM = {"Month"};
+const char TIMESCALE_YEAR_descr[] PROGMEM = {"Year"};
+
+const char * Timescale_descr[] = {
+  TIMESCALE_SECOND_descr,
+  TIMESCALE_MINUTE_descr,
+  TIMESCALE_HOUR_descr,
+  TIMESCALE_DAY_descr,
+  TIMESCALE_WEEK_descr,
+  TIMESCALE_MONTH_descr,
+  TIMESCALE_YEAR_descr,
+};
+
+
+
+/* GPIO Index struct
+ * filled with CanGrow_ESP8266.h and CanGrow_ESP32.h
+ */
+
+struct GPIO_Index {
+  const byte gpio;
+  const byte note;
+};
+
+
+/*
+ * 
+ * Config
+ * 
+ * Note: when adding/removing/changing a saved Config variable
+ * you have to touch the config struct, LoadConfig() and SaveConfig() at least too!
+ */
+
+/*
+ * Config WiFi
+ */
+struct Config_WiFi {
+  char ssid[32];
+  char password[64];
+  bool dhcp;
+  byte ip[4] = {192,168,4,20};
+  byte netmask[4] = {255,255,255,0};
+  byte gateway[4] = {0,0,0,0};
+  byte dns[4] = {0,0,0,0};
+};
+
+
+/*
+ * Config System
+ */
+
+struct Config_System_Output {
+   
+  /*
+   * Config System Output
+   * 
+   * - type: output type like GPIO, I2C, URL
+   *   1 - GPIO
+   *   2 - I2C
+   *   3 - Web
+   * - device: what this output is connected to
+   *   1 - Light
+   *   2 - Fan
+   *   3 - Pump
+   *   4 - Humudifier
+   *   5 - Dehumidifier
+   *   6 - Heating
+   * - name: name of output
+   * - enabled: enable output
+   * - gpio: which gpio is used
+   * - invert: invert output
+   * - gpio_pwm: enable pwm for output
+   * - i2c: 
+   * - webcall_host: ip to smart plug (tasmota e.g.)
+   * - webcall_path_on: GET request path to turn ON
+   * - webcall_path_off: GET request path to turn OFF
+
+   * 
+   */
+  byte type[Max_Outputs];
+  byte device[Max_Outputs];
+  char name[Max_Outputs][32];
+  bool enabled[Max_Outputs];
+  byte gpio[Max_Outputs];
+  bool gpio_pwm[Max_Outputs];
+  bool invert[Max_Outputs];
+  byte i2c_type[Max_Outputs];
+  byte i2c_addr[Max_Outputs];
+  byte i2c_port[Max_Outputs];
+  char webcall_host[Max_Outputs][32];
+  char webcall_path_on[Max_Outputs][32];
+  char webcall_path_off[Max_Outputs][32];
+  char webcall_user[Max_Outputs][32];
+  char webcall_password[Max_Outputs][32];
+};
+
+struct Config_System_Sensor {
+  /*
+   * Config System Sensor
+   * - type: Index ID of SensorIndex, which Sensor to use (ADC, BME280, Chirp, ...)
+   * - name: nice name
+   * - gpio[]: gpio to use for RPM reading, builtin ADC, OneWire, TwoWire
+   */
+
+  byte type[Max_Sensors];
+  char name[Max_Sensors][32];
+  byte i2c_addr[Max_Sensors];
+  byte gpio[Max_Sensors][Max_Sensors_GPIO];
+  float offset[Max_Sensors][Max_Sensors_Read];
+  unsigned int low[Max_Sensors][Max_Sensors_Read];
+  unsigned int high[Max_Sensors][Max_Sensors_Read];
+  byte rawConvert[Max_Sensors][Max_Sensors_Read];
+};
+
+/* main System struct */
+struct Config_System {
+  bool ntp = true;
+  byte rtc;
+  bool time2fs;
+  short ntpOffset;
+  unsigned short maintenanceDuration;
+  char esp32cam[16];
+  char httpUser[32];
+  char httpPass[32];
+  bool httpLogSerial;
+  unsigned short schedulerInterval = 1000;
+  unsigned short pwmFreq = 13370;
+  Config_System_Output output;
+  Config_System_Sensor sensor;
+};
+
+
+
+/*
+ * Config Grow
+ */
+ 
+struct Config_Grow_Light {
+  bool configured[Max_Outputs];
+  byte output[Max_Outputs];
+  byte sunriseHourVeg[Max_Outputs];
+  byte sunriseMinuteVeg[Max_Outputs];
+  byte sunsetHourVeg[Max_Outputs];
+  byte sunsetMinuteVeg[Max_Outputs];
+  
+  byte sunriseHourBloom[Max_Outputs];
+  byte sunriseMinuteBloom[Max_Outputs];
+  byte sunsetHourBloom[Max_Outputs];
+  byte sunsetMinuteBloom[Max_Outputs];
+  
+  byte power[Max_Outputs];
+  bool fade[Max_Outputs];
+  byte fadeDuration[Max_Outputs];
+};
+
+struct Config_Grow_Air {
+  bool configured[Max_Outputs];
+  byte output[Max_Outputs];
+  byte power[Max_Sensors];
+  byte controlSensor[Max_Outputs];
+  byte controlRead[Max_Outputs];
+  byte controlMode[Max_Outputs];
+  float min[Max_Outputs];
+  float max[Max_Outputs];
+};
+
+struct Config_Grow_Water {
+  bool configured[Max_Outputs];
+  byte output[Max_Outputs];
+  byte controlSensor[Max_Outputs];
+  byte controlRead[Max_Outputs];
+  byte controlMode[Max_Outputs];
+  byte onTime[Max_Sensors];
+  byte min[Max_Sensors];
+  byte max[Max_Sensors];
+  byte interval[Max_Sensors];
+  byte intervalUnit[Max_Sensors];
+};
+
+struct Config_Grow_Dashboard {
+  bool configured[Max_Sensors][Max_Sensors_Read];
+  byte sensor[Max_Sensors][Max_Sensors_Read];
+};
+
+struct Config_Grow {
+  char name[64] = "CanGrow";
+  unsigned long start;
+  byte daysVeg = 42;
+  byte daysBloom = 69;
+  Config_Grow_Light light;
+  Config_Grow_Air air;
+  Config_Grow_Water water;
+  Config_Grow_Dashboard dashboard;
+  //unsigned short dayOfGrow;
+  //byte daysSeed;
+
+  //byte lightHoursVeg;
+  //byte lightHoursBloom;
+  //byte sunriseHour;
+  //byte sunriseMinute;
+  //bool sunFade;
+  //byte sunFadeDuration;
+};
+
+
+/*
+ * main Config struct
+ */
+struct Config {
+  char test[16] = "123";
+  Config_WiFi wifi;
+  Config_System system;
+  Config_Grow grow;
+  
+  
+};
+
+Config config;
+
+
+
+
+
+/*
+ * 
+ * 
+ * Global Runtime variables
+ * 
+ * 
+ */
+
+
+// do we need a restart? (e.g. after wifi settings change)
+bool needRestart = false;
+// this triggers Restart() from the main loop
+bool doRestart = false;
+// previous value of millis within the scheduler loop
+unsigned long schedulerPrevMillis = 0;
+/* in which time status is the system
+ *  0 - OK
+ *  1 - RTC fallback is used
+ *  2 - Time2FS fallback is used
+ */
+byte timeSrcStatus;
+
+/* rtcError - false no Error, true had error while init */
+bool rtcError = false;
+// did ntp offset got changed?
+bool updateNtpOffset = false;
+/* sensorStatus[] to keep track if sensor init succeeded  or not, true is  OK */ 
+bool sensorStatus[Max_Sensors];
+/* outputStatus[] to keep track if output init succeeded or not, true is OK */
+bool outputStatus[Max_Outputs];
+/* outputState[] gets read by Output_Update() */
+byte outputState[Max_Outputs];
+/* keep track how often a http call failed  */
+byte outputWebcallFailed[Max_Outputs];
+
+/* remember timestamp when pump was turned on to turn it off after config.grow.water.onTime */
+unsigned long controlWaterLastStarted[Max_Outputs];
+/* remember timestamp when last water cycle was done.*/
+unsigned long controlWaterLast[Max_Outputs];
diff --git a/include/CanGrow_ConfigHelper.h b/include/CanGrow_ConfigHelper.h
new file mode 100644
index 0000000..3e49a19
--- /dev/null
+++ b/include/CanGrow_ConfigHelper.h
@@ -0,0 +1,22 @@
+/*
+ * 
+ * include/CanGrow_Core.h - core stuff header file
+ * 
+ * 
+ *
+ */
+
+/* Give free grow.light id */
+byte Give_Free_Grow_LightId() {
+  byte freeId;
+  for(byte i = 0; i < Max_Outputs; i++) {
+    if(config.grow.light.configured[i] == true) {
+      // here i define that 255 stands for "no more free outputs" 
+      freeId = 255;
+    } else {
+      freeId = i;
+      break;
+    }
+  }
+  return freeId;
+}
diff --git a/include/CanGrow_Control.h b/include/CanGrow_Control.h
new file mode 100644
index 0000000..b05bd70
--- /dev/null
+++ b/include/CanGrow_Control.h
@@ -0,0 +1,407 @@
+/*
+ * 
+ * include/CanGrow_Control.h - control stuff for light,air,water header file
+ * 
+ * 
+ *
+ */
+
+
+/*
+ * 
+ * Light stuff
+ * 
+ */
+ 
+/* Light fade */
+byte Light_Power(byte id, unsigned int sunriseSec, unsigned int sunsetSec, unsigned int nowSec, bool shifted) {
+  const static char LogLoc[] PROGMEM = "[Control:Light_Power]";
+  if(config.grow.light.fade[id] == true) {
+    unsigned int fadeDurationSec = config.grow.light.fadeDuration[id] * 60;
+    byte power_tmp;
+    //byte power_tmp; // = (durationSec - ((sunriseSec + durationSec) - nowSec) * config.grow.light.power[id] / durationSec);
+    
+    /* rising sun */
+    if(nowSec <= sunriseSec + fadeDurationSec) {
+      /* calculate fade power value */
+      //power_tmp = ( ( (nowSec - sunriseSec) / (fadeDurationSec / 255) ) * config.grow.light.power[id] ) / 255;
+      power_tmp = (fadeDurationSec - ((sunriseSec + fadeDurationSec) - nowSec)) * config.grow.light.power[id] / fadeDurationSec;
+    /* setting sun */
+    } else if((nowSec >= sunsetSec - fadeDurationSec) && (nowSec <= sunsetSec)) {
+      /* calculate fade power value */
+      //power_tmp = ( ( (sunsetSec - nowSec) / (fadeDurationSec / 255) ) * config.grow.light.power[id] ) / 255;
+      power_tmp = (sunsetSec - nowSec) * config.grow.light.power[id] / fadeDurationSec;
+    } else {
+      /* otherwise just turn the light on with configured value */
+      power_tmp = config.grow.light.power[id];
+    }
+
+    //if(shifted == false) {
+      
+    //} else {
+      
+    //}
+    
+    #ifdef DEBUG
+    Log.verbose(F("%s Light %d - power_tmp %d" CR), LogLoc, id, power_tmp);
+    #endif
+    return power_tmp;
+  } else {
+    return config.grow.light.power[id];
+  }
+  
+  
+  //return 0;
+}
+/* Function to set light based on time */
+void Control_Light() {
+  const static char LogLoc[] PROGMEM = "[Control:Light]";
+  //Log.verbose(F("%s start %s %s" CR), LogLoc, Str_DateNow(), Str_TimeNow());
+  /* iterate through all configured lights */
+  for(byte i = 0; i < Max_Outputs; i++) {
+    if(config.grow.light.configured[i] == true) {
+      unsigned int nowSec = (hour() * 60 * 60) + (minute() * 60) + second();
+      unsigned int sunriseSec;
+      unsigned int sunsetSec;
+      
+      /* check if veg or bloom */
+      if((config.grow.start < 1) || (now() - config.grow.start <= config.grow.daysVeg * 24 * 60 * 60))  {
+        sunriseSec = (config.grow.light.sunriseHourVeg[i] * 60 * 60) + (config.grow.light.sunriseMinuteVeg[i] * 60);
+        sunsetSec = (config.grow.light.sunsetHourVeg[i] * 60 * 60) + (config.grow.light.sunsetMinuteVeg[i] * 60);
+        #ifdef DEBUG
+        Log.verbose(F("%s Veg" CR), LogLoc);
+        #endif
+      /* now > than veg = bloom */
+      } else if(now() - config.grow.start > config.grow.daysVeg * 24 * 60 * 60) {
+        sunriseSec = (config.grow.light.sunriseHourBloom[i] * 60 * 60) + (config.grow.light.sunriseMinuteBloom[i] * 60);
+        sunsetSec = (config.grow.light.sunsetHourBloom[i] * 60 * 60) + (config.grow.light.sunsetMinuteBloom[i] * 60);
+        #ifdef DEBUG
+        Log.verbose(F("%s Bloom" CR), LogLoc);
+        #endif
+      /* now > than veg+bloom = harvest*/
+      } //else if(now() - config.grow.start > (config.grow.daysVeg + config.grow.daysBloom) * 24 * 60 * 60)) {
+        //}
+      
+      
+      
+      
+      /* 
+       * Sunrise / Day
+       */
+      
+      /* when now is greater than sunrise AND sunsetTime is greater than sunrise */
+      if((nowSec >= sunriseSec) && (nowSec < sunsetSec) && (sunsetSec > sunriseSec)) {
+        //outputState[i] = config.grow.light.power[i];
+        outputState[i] = Light_Power(i, sunriseSec, sunsetSec, nowSec, false);
+        //Log.verbose(F("%s Light %d - nowSec %d - sunriseSec %d - sunsetSec %d - %s %s Day" CR), LogLoc, i, nowSec, sunriseSec, sunsetSec, Str_DateNow(), Str_TimeNow());
+        
+        
+      /* when now is greater than sunrise OR */
+      } else if(((nowSec >= sunriseSec) && (sunsetSec < sunriseSec)) ||
+                /* when now is smaller than sunset AND sunset is
+                 * smaller than sunrise - this is a shifted daytime */
+                ((nowSec <= sunsetSec) && (sunsetSec < sunriseSec))) {
+        
+        //outputState[i] = config.grow.light.power[i];
+        outputState[i] = Light_Power(i, sunriseSec, sunsetSec, nowSec, true);
+        //Log.verbose(F("%s Light %d - nowSec %d - sunriseSec %d - sunsetSec %d - %s %s Day (shifted)" CR), LogLoc, i, nowSec, sunriseSec, sunsetSec, Str_DateNow(), Str_TimeNow());
+        
+        
+      } else {
+        /* otherwise its night, turn off the light */
+        outputState[i] = 0;
+        //Log.verbose(F("%s Light %d - nowSec %d - sunriseSec %d - sunsetSec %d - %s %s Night" CR), LogLoc, i, nowSec, sunriseSec, sunsetSec, Str_DateNow(), Str_TimeNow());
+      }
+    }
+  }
+}
+
+
+/*
+ * 
+ * Air stuff
+ * 
+ */
+
+
+/*
+ * Output Device
+ */
+
+/* Air Mode definitions */
+// 0 is unconfigured
+const byte CONTROL_AIR_MODE__TOTAL = 3;
+
+const byte CONTROL_AIR_MODE_ONOFF = 1;
+const byte CONTROL_AIR_MODE_LINEAR = 2;
+const byte CONTROL_AIR_MODE_STEPS = 3;
+
+const char CONTROL_AIR_MODE_ONOFF_descr[] PROGMEM = {"On/Off"};
+const char CONTROL_AIR_MODE_LINEAR_descr[] PROGMEM = {"Linear"};
+const char CONTROL_AIR_MODE_STEPS_descr[] PROGMEM = {"Steps"};
+
+const char * Control_Air_Mode_descr[] = {
+  NULL, // 0 - no description because 0 means unconfigured
+  CONTROL_AIR_MODE_ONOFF_descr,
+  CONTROL_AIR_MODE_LINEAR_descr,
+  CONTROL_AIR_MODE_STEPS_descr,
+};
+
+
+/* Air control modes themselfs */ 
+
+byte Control_Air_Mode_OnOff(byte id) {
+  /* turns the output on or off, depending if the is within min and max */
+  
+  /* if only min is set (max = 0), turn on when above it */ 
+  if((config.grow.air.min[id] > 0) && (config.grow.air.max[id] == 0)) {
+    /* check if Sensor reading is above min value, then turn on */
+    if(Sensor_getCalibratedValue(config.grow.air.controlSensor[id], config.grow.air.controlRead[id]) >= config.grow.air.min[id]) {
+      return config.grow.air.power[id];
+    } else {
+      return 0;
+    }
+    
+  /* if only max is set (min = 0), turn off when above */
+  } else if((config.grow.air.min[id] == 0) && (config.grow.air.max[id] > 0)) {
+    /* check if Sensor reading is under max value, then turn on */
+    if(Sensor_getCalibratedValue(config.grow.air.controlSensor[id], config.grow.air.controlRead[id]) <= config.grow.air.max[id]) {
+      return config.grow.air.power[id];
+    } else {
+      return 0;
+    }
+  /* when min and max are set (> 0) turn output on when within the given values */
+  } else if((config.grow.air.min[id] > 0) && (config.grow.air.max[id] > 0)) {
+    if((Sensor_getCalibratedValue(config.grow.air.controlSensor[id], config.grow.air.controlRead[id]) >= config.grow.air.min[id]) && (Sensor_getCalibratedValue(config.grow.air.controlSensor[id], config.grow.air.controlRead[id]) <= config.grow.air.max[id]))  {
+      return config.grow.air.power[id];
+    } else {
+      return 0;
+    }
+  } 
+  
+  return 0; 
+}
+
+byte Control_Air_Mode_Linear(byte id) {
+  /* if min and max are set */
+  if((config.grow.air.min[id] > 0) && (config.grow.air.max[id] > 0)) {
+    /* return power value calculated with map() and contrain()
+     * multiply by 100 to "convert" the float to int. With constrain we prevent returning negative or out of range values */
+    return map(constrain(Sensor_getCalibratedValue(config.grow.air.controlSensor[id], config.grow.air.controlRead[id]) * 100, config.grow.air.min[id] * 100, config.grow.air.max[id] * 100),
+               config.grow.air.min[id] * 100,
+               config.grow.air.max[id] * 100,
+               0,
+               config.grow.air.power[id]);
+  } else {
+    return Control_Air_Mode_OnOff(id);
+  }
+  
+  return 0;
+}
+
+byte Control_Air_Mode_Steps(byte id) {
+/* if min and max are set */
+  if((config.grow.air.min[id] > 0) && (config.grow.air.max[id] > 0)) {
+    /* return power value calculated with map() and contrain()
+     * multiply by 100 to "convert" the float to int. With constrain we prevent returning negative or out of range values */
+    byte power_tmp = map(constrain(Sensor_getCalibratedValue(config.grow.air.controlSensor[id], config.grow.air.controlRead[id]) * 100, config.grow.air.min[id] * 100, config.grow.air.max[id] * 100),
+               config.grow.air.min[id] * 100,
+               config.grow.air.max[id] * 100,
+               0,
+               config.grow.air.power[id]);
+    if(power_tmp == 0) {
+      return 0;
+    } else if(power_tmp < 64) {
+      return 64;
+    } else if(power_tmp < 128) {
+      return 128;
+    } else if(power_tmp < 192) {
+      return 192;
+    } else if(power_tmp < 255) {
+      return 192;
+    } else {
+      return 255;
+    }
+  } else { 
+    return Control_Air_Mode_OnOff(id);
+  }
+  
+  return 0;
+}
+
+/* Function to set air devices  */
+void Control_Air() {
+  const static char LogLoc[] PROGMEM = "[Control:Air]";
+  
+  /* iterate through all configured air devices */
+  for(byte i = 0; i < Max_Outputs; i++) {
+    if(config.grow.air.configured[i] == true) {
+      /* check if a control Sensor reading is set. As SensorIndex starts by 0, 255 is "unset" */
+      if((config.grow.air.controlSensor[i] < 255) && (config.grow.air.controlRead[i] < 255)) {
+        /* switch for control modes */
+        switch(config.grow.air.controlMode[i]) {
+          
+          case CONTROL_AIR_MODE_ONOFF:
+            outputState[i] = Control_Air_Mode_OnOff(i);
+          break;
+          
+          case CONTROL_AIR_MODE_LINEAR:
+            outputState[i] = Control_Air_Mode_Linear(i);
+          break;
+          
+          case CONTROL_AIR_MODE_STEPS:
+            outputState[i] = Control_Air_Mode_Steps(i);
+          break;
+          
+        }
+        
+      /* if there is no control sensor reading selected, just set power */
+      } else {
+        outputState[i] = config.grow.air.power[i];
+      }
+    }
+  }
+}
+
+
+
+
+
+/*
+ * 
+ * Water stuff
+ * 
+ */
+
+
+/*
+ * Output Device
+ */
+
+/* Water Mode definitions */
+// 0 is unconfigured
+const byte CONTROL_WATER_MODE__TOTAL = 3;
+
+const byte CONTROL_WATER_MODE_TIMEINTERVAL = 1;
+const byte CONTROL_WATER_MODE_SENSOR_MIN_THRESHOLD = 2;
+const byte CONTROL_WATER_MODE_SENSMIN_TIMEINT_COMBINED = 3;
+
+const char CONTROL_WATER_MODE_TIMEINTERVAL_descr[] PROGMEM = {"Timeinterval"};
+const char CONTROL_WATER_MODE_SENSOR_MIN_THRESHOLD_descr[] PROGMEM = {"Sensor min threshold"};
+const char CONTROL_WATER_MODE_SENSMIN_TIMEINT_COMBINED_descr[] PROGMEM = {"Sensor min + Timeinterval"};
+
+const char * Control_Water_Mode_descr[] = {
+  NULL, // 0 - no description because 0 means unconfigured
+  CONTROL_WATER_MODE_TIMEINTERVAL_descr,
+  CONTROL_WATER_MODE_SENSOR_MIN_THRESHOLD_descr,
+  CONTROL_WATER_MODE_SENSMIN_TIMEINT_COMBINED_descr,
+};
+
+
+
+
+
+/* Function to set water devices  */
+void Control_Water() {
+  const static char LogLoc[] PROGMEM = "[Control:Water]";
+  
+  /* iterate through all configured water devices which have a control mode set */
+  for(byte i = 0; i < Max_Outputs; i++) {
+    if((config.grow.water.configured[i] == true) && (config.grow.water.controlMode[i] > 0)) {
+      
+      /* which mode was set in config.grow.water.controlMode */
+      switch(config.grow.water.controlMode[i]) {
+      
+        case CONTROL_WATER_MODE_TIMEINTERVAL:
+          // when diff of time now and time pumpLastOn is greater then water.interval, do some watering (Or manual watering)
+          if( (now() - controlWaterLast[i]) >= (config.grow.water.interval[i] * Timescale(config.grow.water.intervalUnit[i])) ) {
+            /* check if output is already on. If not so, we begin a watering cycle and remember the timestamp of it. */
+            if(outputState[i] == 0) {
+              controlWaterLastStarted[i] = now();
+            }
+            
+            /* when diff of now and controlWaterLastStarted is smaller than onTime, turn output on */
+            if((now() - controlWaterLastStarted[i]) < config.grow.water.onTime[i]) {
+              /* at the moment i think PWM is not necessary here, so we set the output to 255 */
+              outputState[i] = 255;
+            /* when onTime is exceeded, turn output off */  
+            } else {
+              outputState[i] = 0;
+              
+              /* remember when we finished watering */
+              controlWaterLast[i] = now();
+              
+              /* Todo, write controlWaterLast to LittleFS. */
+            }
+          } else {
+            /* turn output off when interval is not exceeded */
+            outputState[i] = 0;
+          }
+        break;
+        
+        case CONTROL_WATER_MODE_SENSOR_MIN_THRESHOLD:
+          /* when sensor reading config.grow.water.controlSensor is lower then config.grow.water.min , do some watering */
+          if( (Sensor_getCalibratedValue(config.grow.water.controlSensor[i], config.grow.water.controlRead[i]) < config.grow.water.min[i]) ||
+          /* or when the sensor value is larger than min but onTime has not exceeded yet */
+             ((Sensor_getCalibratedValue(config.grow.water.controlSensor[i], config.grow.water.controlRead[i]) >= config.grow.water.min[i]) && ( (now() - controlWaterLastStarted[i]) < config.grow.water.onTime[i]))
+            ) {
+            /* check if output is already on. If not so, we begin a watering cycle and remember the timestamp of it. */
+            if(outputState[i] == 0) {
+              controlWaterLastStarted[i] = now();
+            }
+            
+            /* when diff of now and controlWaterLastStarted is smaller than onTime, turn output on */
+            if((now() - controlWaterLastStarted[i]) < config.grow.water.onTime[i]) {
+              /* at the moment i think PWM is not necessary here, so we set the output to 255 */
+              outputState[i] = 255;
+            /* when onTime is exceeded, turn output off */  
+            } else {
+              outputState[i] = 0;
+            }
+          /* turn output off when water conditions are not met */
+          } else {
+            outputState[i] = 0;
+          }
+        break;
+        
+        case CONTROL_WATER_MODE_SENSMIN_TIMEINT_COMBINED:
+           // when diff of time now and time pumpLastOn is greater then water.interval AND sensor read value is below min
+          if( ( (now() - controlWaterLast[i]) >= (config.grow.water.interval[i] * Timescale(config.grow.water.intervalUnit[i])) )  &&
+              ( (Sensor_getCalibratedValue(config.grow.water.controlSensor[i], config.grow.water.controlRead[i]) < config.grow.water.min[i]) ||
+          /* or when the sensor value is larger than min but onTime has not exceeded yet */
+             ((Sensor_getCalibratedValue(config.grow.water.controlSensor[i], config.grow.water.controlRead[i]) >= config.grow.water.min[i]) && ( (now() - controlWaterLastStarted[i]) < config.grow.water.onTime[i])) )
+          ) {
+            /* check if output is already on. If not so, we begin a watering cycle and remember the timestamp of it. */
+            if(outputState[i] == 0) {
+              controlWaterLastStarted[i] = now();
+            }
+            
+            /* when diff of now and controlWaterLastStarted is smaller than onTime, turn output on */
+            if((now() - controlWaterLastStarted[i]) < config.grow.water.onTime[i]) {
+              /* at the moment i think PWM is not necessary here, so we set the output to 255 */
+              outputState[i] = 255;
+            /* when onTime is exceeded, turn output off */  
+            } else {
+              outputState[i] = 0;
+              
+              /* remember when we finished watering */
+              controlWaterLast[i] = now();
+              
+              /* Todo, write controlWaterLast to LittleFS. */
+            }
+          } else {
+            /* turn output off when interval is not exceeded */
+            outputState[i] = 0;
+          }
+        break;
+        
+        default:
+          /* when no mode is selected, turn output off */
+          outputState[i] = 0;
+        break;
+        
+      }
+    }
+  /* if neither configured or mode set, force output being off */
+  }
+}
diff --git a/include/CanGrow_Core.h b/include/CanGrow_Core.h
new file mode 100644
index 0000000..9ed08ec
--- /dev/null
+++ b/include/CanGrow_Core.h
@@ -0,0 +1,642 @@
+/*
+ * 
+ * include/CanGrow_Core.h - core stuff header file
+ * 
+ * 
+ *
+ */
+
+/*
+ * NTP Stuff
+ */
+ 
+WiFiUDP ntpUDP;
+NTPClient timeClient(ntpUDP);
+
+/*
+ * RTC Stuff
+ */
+
+/* I would more like not to define four individual globals for each RTC type
+ * but Adafruit lib seems to work only this way - and i am too lazyscared to use
+ * some other lib or do it myself - so i hope this will not eat up my ram */
+RTC_DS1307 rtc_ds1307;
+RTC_DS3231 rtc_ds3231;
+RTC_PCF8523 rtc_pcf8523;
+RTC_PCF8563 rtc_pcf8563;
+
+
+/*
+ * Timer stuff
+ */ 
+NusabotSimpleTimer timer;
+
+
+/*
+ * Logging stuff
+ * 
+ * Example Log call
+ *   const static char LogLoc[] PROGMEM= "[Some:Stuff:Happening]"
+ *   Log.notice(F("%s This is %d" CR), LogLoc, i);
+ * 
+ * LogLoc stands for "LogLocation"
+ */
+ 
+/* Logging prefix */
+void LogPrefix(Print* _logOutput, int logLevel) {
+  //_logOutput->print(":: TEST");
+  switch (logLevel)
+    {
+      default:
+      // silent
+      case 0:_logOutput->print("--" ); break;
+      // fatal
+      case 1:_logOutput->print("!!!! "  ); break;
+      // error
+      case 2:_logOutput->print("!! "  ); break;
+      // warning
+      case 3:_logOutput->print("!: "); break;
+      // info / notice
+      case 4:_logOutput->print(":: "   ); break;
+      // trace
+      case 5:_logOutput->print("T: "  ); break;
+      // verbose / debug
+      case 6:_logOutput->print("DB "); break;
+    }
+}
+
+/* System core stuff , like restart , give free Id of xy, .. */
+void Restart() {
+  const static char LogLoc[] PROGMEM = "[Core:Restart]";
+  Log.notice(F("%s got triggered, restarting in 2 seconds" CR), LogLoc);
+  
+  // blink fast with the built in LED in an infinite loop
+  byte i = 0;
+  while(i <= 16) {
+    if(i % 2) {
+      digitalWrite(PinWIPE, 1 - PinWIPE_default);
+      
+    } else {
+      digitalWrite(PinWIPE, PinWIPE_default);
+      
+    }
+    i++;
+    delay(125);
+  }
+  ESP.restart();
+
+}
+
+
+// IP2Char helper function to convert ip arrarys to char arrays
+char* IP2Char(IPAddress ipaddr){
+  // https://forum.arduino.cc/t/trouble-returning-char-array-string/473246/6
+  static char buffer[18];
+  sprintf(buffer, "%d.%d.%d.%d", ipaddr[0], ipaddr[1], ipaddr[2], ipaddr[3] );
+  return buffer;
+}
+
+byte Give_Free_OutputId() {
+  const static char LogLoc[] PROGMEM = "[Core:Give_Free_OutputId]";
+  byte outputId_free;
+  for(byte i=0; i < Max_Outputs; i++) {
+    if(config.system.output.type[i] > 0) {
+      // here i define that 255 stands for "no more free outputs" 
+      outputId_free = 255;
+    } else {
+      outputId_free = i;
+      break;
+    }
+  }
+  #ifdef DEBUG
+  Log.verbose(F("%s next free output id: %d" CR), LogLoc, outputId_free);
+  #endif
+  return outputId_free;
+}
+
+byte Give_Free_SensorId() {
+  const static char LogLoc[] PROGMEM = "[Core:Give_Free_SensorId]";
+  
+  byte sensorId_free;
+  for(byte i=0; i < Max_Sensors; i++) {
+    if(config.system.sensor.type[i] > 0) {
+      // here i define that 255 stands for "no more free outputs" 
+      sensorId_free = 255;
+    } else {
+      sensorId_free = i;
+      break;
+    }
+  }
+  #ifdef DEBUG
+  Log.verbose(F("%s next free sensor id: %d" CR), LogLoc, sensorId_free);
+  #endif
+  return sensorId_free;
+}
+
+
+
+// checks if GPIO is already in use by output or sensor
+bool Check_GPIOindex_Used(byte gpio) {
+  const static char LogLoc[] PROGMEM = "[Core:Check_GPIOindex_Used]";
+  
+  bool used;
+  
+  //Log.verbose(F("%s check GPIO: %d" CR), LogLoc, gpio);
+  
+  // go through each outputid
+  for(byte i=0; i < Max_Outputs; i++) {
+    
+    // check if output type is gpio
+    if(config.system.output.type[i] == OUTPUT_TYPE_GPIO) {
+      #ifdef DEBUG
+      Log.verbose(F("%s OutputId: %d is GPIO (type %d)" CR), LogLoc, i, config.system.output.type[i]);
+      #endif
+      // check if gpio id is already in use
+      if(config.system.output.gpio[i] == gpio) {
+        #ifdef DEBUG
+        Log.verbose(F("%s output.gpio[%d](%d) == GPIO %d" CR), LogLoc, i, config.system.output.gpio[i], gpio);
+        #endif
+        used = true;
+        break;        
+      } else {
+        used = false;
+      }
+    }
+  }
+  
+  if(used == false) {
+    for(byte i=0; i < Max_Sensors; i++) {
+
+      // check if sensor type uses gpio
+      if((SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_INTADC) || (SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_ONEWIRE) || (SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_TWOWIRE)) {
+        #ifdef DEBUG
+        Log.verbose(F("%s SensorId: %d is using GPIO (type %d)" CR), LogLoc, i, config.system.sensor.type[i]);
+        #endif
+        // check if gpio id is already in use
+        for(byte j = 0; j < Max_Sensors_GPIO; j++) {
+          if(config.system.sensor.gpio[i][j] == gpio) {
+            #ifdef DEBUG
+            Log.verbose(F("%s sensor.gpio[%d][%d](%d) == GPIO %d" CR), LogLoc, i, j, config.system.sensor.gpio[i][j], gpio);
+            #endif
+            used = true;
+            break;        
+          } else {
+            used = false;
+          }
+        }
+      }
+    }
+  }
+  #ifdef DEBUG
+  Log.verbose(F("%s GPIO: %d, used: %d" CR), LogLoc, gpio, used);
+  #endif
+  return used;
+}
+
+
+/*
+ * 
+ * Time related stuff
+ * 
+ */
+
+/*
+ * NTP stuff
+ */
+ 
+void NTP_OffsetUpdate() {
+  const static char LogLoc[] PROGMEM = "[Core:NTP_OffsetUpdate]";
+  #ifdef DEBUG
+  Log.verbose(F("%s updating time with offset %dh" CR), LogLoc, config.system.ntpOffset);
+  #endif
+  timeClient.setTimeOffset(config.system.ntpOffset * 60 * 60);
+  if( (config.system.ntp == true) && (timeSrcStatus < 1) ) {
+    timeClient.update();
+    setTime(timeClient.getEpochTime());
+  } 
+  #ifdef DEBUG
+  else {
+    Log.verbose(F("%s update requirements not met, timeSrcStatus %d > 0" CR), LogLoc, timeSrcStatus);
+  }
+  #endif
+}
+
+bool NTP_Init() {
+  const static char LogLoc[] PROGMEM = "[Core:NTP_Init]";
+  bool result;
+  timeClient.begin();
+  NTP_OffsetUpdate();
+  // when NTP update failes (e.g. no connection to internet)
+  Log.notice(F("%s updating " ), LogLoc);
+  
+  byte i = 0;
+  while( (! timeClient.isTimeSet()) && ( i < 5 )) {
+    timeClient.update();
+    delay(100);
+    Serial.print(".");
+    i++;
+  }
+  Serial.println();
+  
+  if( ! timeClient.isTimeSet()) {
+    Log.error(F("%s FAILED" CR), LogLoc);
+    //Serial.println("!! [Core:NTP_Init] update failed");
+    result = false;
+  } else {
+    
+    Log.notice(F("%s Success! Time: %s (%u), Offset: %d h" CR), LogLoc, timeClient.getFormattedTime(), timeClient.getEpochTime(), config.system.ntpOffset);
+    
+    result = true;
+  }
+  return result;
+}
+
+
+time_t NTP_getEpochTime() {
+  /* convert epoch from ntp (UL) to time_t */
+  const static char LogLoc[] PROGMEM = "[Core:NTP_getEpochTime]";
+  unsigned long epochTime = timeClient.getEpochTime();
+  Log.verbose(F("%s epochTime: %u" CR), LogLoc, epochTime);
+  return epochTime;
+}
+
+/*
+ * RTC stuff
+ */
+
+void RTC_Init() {
+  const static char LogLoc[] PROGMEM = "[Core:RTC_Init]";
+
+  switch(config.system.rtc) {
+    case RTCs_DS1307:
+      if (! rtc_ds1307.begin()) {
+        Log.warning(F("%s Couldn't find RTC DS1307" CR), LogLoc);
+        rtcError = true;    
+      } else {
+        Log.notice(F("%s RTC DS1307 found" CR), LogLoc);
+        if (rtc_ds1307.isrunning()) {
+          Log.warning(F("%s RTC DS1307 is not running, let's set the time!" CR), LogLoc);
+          rtcError = true;
+        }
+      }
+
+    break;
+    
+    case RTCs_DS3231:
+      if (! rtc_ds3231.begin()) {
+        Log.warning(F("%s Couldn't find RTC DS3231" CR), LogLoc);
+        rtcError = true;
+      } else {
+        Log.notice(F("%s RTC DS3231 found" CR), LogLoc);
+        if (rtc_ds3231.lostPower()) {
+          Log.warning(F("%s RTC DS3231 lost power, let's set the time!" CR), LogLoc);
+          rtcError = true;
+        }
+      }
+
+    break;
+    
+    case RTCs_PCF8563:
+      if (! rtc_pcf8563.begin()) {
+        Log.warning(F("%s Couldn't find RTC PCF8563" CR), LogLoc);
+        rtcError = true;
+        
+      } else {
+        Log.notice(F("%s RTC PCF8563 found" CR), LogLoc);
+        if (rtc_pcf8563.lostPower()) {
+          Log.warning(F("%s RTC PCF8563 lost power, let's set the time!" CR), LogLoc);
+          rtcError = true;
+        }
+      }
+      rtc_pcf8563.start();
+    break;
+    
+    case RTCs_PCF8523:
+      if (! rtc_pcf8523.begin()) {
+        Log.warning(F("%s Couldn't find RTC PCF8523" CR), LogLoc);
+        rtcError = true;
+        
+      } else {
+        Log.notice(F("%s RTC PCF8523 found" CR), LogLoc);
+        if ( ! rtc_pcf8523.initialized() || rtc_pcf8523.lostPower()) {
+          Log.warning(F("%s RTC PCF8523 lost power, let's set the time!" CR), LogLoc);
+          rtcError = true;
+        }
+      }
+      rtc_pcf8523.start();
+    break;
+    
+    default:
+    break;
+  }
+
+}
+
+
+time_t RTC_getEpochTime() {
+  /* convert epoch from RTC (UL) to time_t */
+  const static char LogLoc[] PROGMEM = "[Core:RTC_getEpochTime]";
+  unsigned long epochTime; // = timeClient.getEpochTime();
+  DateTime TimeNow;
+  switch(config.system.rtc) {
+    case RTCs_DS1307:
+      TimeNow = rtc_ds1307.now();
+    break;
+    
+    case RTCs_DS3231:
+      TimeNow = rtc_ds3231.now();
+    break;
+    
+    case RTCs_PCF8523:
+      TimeNow = rtc_pcf8523.now();
+    break;
+    
+    case RTCs_PCF8563:
+      TimeNow = rtc_pcf8563.now();
+    break;
+    
+    default:
+    break;
+  }
+  epochTime = TimeNow.unixtime();
+  Log.verbose(F("%s epochTime: %u" CR), LogLoc, epochTime);
+  return epochTime;
+}
+
+void RTC_SaveTime() {
+  const static char LogLoc[] PROGMEM = "[Core:RTC_SaveTime]";
+  unsigned int TimeNow = now();
+  bool saved = true;
+  
+  switch(config.system.rtc) {
+    case RTCs_DS1307:
+      rtc_ds1307.adjust(DateTime(TimeNow));
+    break;
+    
+    case RTCs_DS3231:
+      rtc_ds3231.adjust(DateTime(TimeNow));
+    break;
+    
+    case RTCs_PCF8523:
+      rtc_pcf8523.adjust(DateTime(TimeNow));
+    break;
+    
+    case RTCs_PCF8563:
+      rtc_pcf8563.adjust(DateTime(TimeNow));
+    break;
+    
+    default:
+      /* only when not in case, we consider not saved */
+      saved = false;
+    break;
+  }
+  #ifdef DEBUG
+  if(saved == true)
+    Log.verbose(F("%s Time (%u) saved to %S" CR), LogLoc, TimeNow, RTCs_descr[config.system.rtc]);
+  #endif
+}
+
+/*
+ * Main Time stuff
+ * 
+ */
+
+String Str_TimeNow() {
+  /* simple helper function to return a String with HH:MM:SS */
+  String str_time;
+  if(hour() < 10)
+    str_time += F("0");
+  str_time += hour();
+  str_time += F(":");
+  if(minute() < 10)
+    str_time += F("0");
+  str_time += minute();
+  str_time += F(":");
+  if(second() < 10)
+    str_time += F("0");
+  str_time += second();
+  return str_time;
+}
+
+String Str_DateNow() {
+  /* simple helper function to return a String with HH:MM:SS */
+  String str_date;
+  if(day() < 10)
+    str_date += F("0");
+  str_date += day();
+  str_date += F(".");
+  if(month() < 10)
+    str_date += F("0");
+  str_date += month();
+  str_date += F(".");
+  str_date += year();
+  return str_date;
+}
+
+String Str_Epoch2Date(unsigned long epochTime) {
+  String dateStr;
+  byte Day = day(epochTime);
+  byte Month = month(epochTime);
+  unsigned int Year = year(epochTime);
+  
+  dateStr = Year;
+  dateStr += "-";
+  
+  if(Month < 10) {
+    dateStr += "0";
+    dateStr += Month;
+  } else {
+    dateStr += Month;
+  }
+  
+  dateStr += "-";
+  
+  if(Day < 10) {
+    dateStr += "0";
+    dateStr += Day;
+  } else {
+    dateStr += Day;
+  }
+  
+  return dateStr;
+}
+
+
+/* Those two functions should be in LittleFS file, but because dependency and lazyness */
+void Time2FS_Save() {
+  const static char LogLoc[] PROGMEM = "[Core:Time2FS_Save]";
+  unsigned long TimeNow;
+  #ifdef ESP8266
+  File file = LittleFS.open(TIME2FS, "w");
+  #endif
+  
+  #ifdef ESP32
+  fs::FS &fs = LittleFS;
+  File file = fs.open(TIME2FS, FILE_WRITE);
+  #endif
+  
+  if (!file) {
+    Log.error(F("%s FAILED to open file for writing: %s" CR), LogLoc, TIME2FS);
+    return;
+  }
+  TimeNow = now();
+  if (!file.print(TimeNow)) {
+    Log.error(F("%s writing time FAILED" CR), LogLoc);
+  } 
+  #ifdef DEBUG
+  else {
+    Log.verbose(F("%s time (%u) written: %s" CR), LogLoc, TimeNow, TIME2FS);
+  }
+  #endif
+  //delay(2000);  // Make sure the CREATE and LASTWRITE times are different
+  file.close();
+
+}
+ 
+void Time2FS_Read() {
+  const static char LogLoc[] PROGMEM = "[Core:Time2FS_Read]";
+  String TimeRead;
+  #ifdef ESP8266
+  File file = LittleFS.open(TIME2FS, "r");
+  #endif
+  
+  #ifdef ESP32
+  fs::FS &fs = LittleFS;
+  File file = fs.open(TIME2FS);
+  #endif
+  
+  if (!file) {
+    Log.error(F("%s FAILED to open time file: %s" CR), LogLoc, TIME2FS);
+    return;
+  }
+
+  //Log.notice(F("%s file content: %s" CR), LogLoc, TIME2FS);
+  //Log.notice(F("%s ----------" CR), LogLoc);
+  //while (file.available()) { Serial.write(file.read()); }
+  //Log.notice(F("%s ----------" CR), LogLoc);
+
+  while (file.available()) { TimeRead = file.readString(); }
+  #ifdef DEBUG
+  Log.verbose(F("%s applying time (%u) to system" CR), LogLoc, TimeRead.toInt());
+  #endif
+  setTime(TimeRead.toInt());
+  file.close();
+}
+
+
+/* Time_Init - Main function for time initialization */
+void Time_Init() {
+  const static char LogLoc[] PROGMEM = "[Core:Time_Init]";
+  
+  /* first check if RTC is configured and init if */
+  if(config.system.rtc > 0)
+    RTC_Init();
+  
+  /* check if ntp is enabled */
+  if(config.system.ntp == true) {
+    Log.notice(F("%s Using NTP" CR), LogLoc);
+    /* initialize NTP and check */
+    if(NTP_Init()) {
+      #ifdef DEBUG
+      Log.verbose(F("%s set NTP as TimeLib SyncProvider" CR), LogLoc);
+      #endif
+      setSyncProvider(NTP_getEpochTime);
+      
+      /* when having a RTC, update it now with new not time */
+      if(config.system.rtc > 0) {
+        RTC_SaveTime();
+      }
+      
+      if(config.system.time2fs == true) {
+        true;
+        //writeFile(TIME2FS, now());
+        Time2FS_Save();
+      }
+      
+    } else if((config.system.rtc > 0) && (rtcError == false)) {
+      #ifdef DEBUG
+      Log.verbose(F("%s set RTC as TimeLib SyncProvider" CR), LogLoc);
+      #endif
+      setSyncProvider(RTC_getEpochTime);
+      //setTime(RTC_getEpochTime());
+      timeSrcStatus = 1;
+    } else {
+      Log.warning(F("%s no TimeLib SyncProvider available. Reading last Timestamp from flash memory" CR), LogLoc);
+      Time2FS_Read();
+      timeSrcStatus = 2;
+    }
+  }
+  /* how often TimeLib should sync with source
+   * 10 minutes is ok i guess
+   */
+  setSyncInterval(600);
+  
+  Log.notice(F("%s Time initialization done. Fallback status %d, %s %s (%u)" CR), LogLoc, timeSrcStatus, Str_DateNow(), Str_TimeNow(), now());
+  
+
+}
+
+
+/*
+ * semi random string generator
+ * https://arduino.stackexchange.com/a/86659
+ */
+const byte RANDOMSTRING_MAX = 16;
+const char * RandomString(){
+  /* Change to allowable characters */
+  const char possible[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?_-=()%&.,;:";
+  static char str[RANDOMSTRING_MAX + 1];
+  for(byte p = 0, i = 0; i < RANDOMSTRING_MAX; i++){
+    byte r = random(0, strlen(possible));
+    str[p++] = possible[r];
+  }
+  str[RANDOMSTRING_MAX] = '\0';
+  return str;
+}
+
+
+/*
+ * Timescale()
+ * returns timescale unit (seconds, minutes, hours,...) in seconds
+ */
+
+unsigned long Timescale(byte unit) {
+  switch(unit) {
+    case TIMESCALE_SECOND:
+      return 1;
+    break;
+    
+    case TIMESCALE_MINUTE:
+      return 60;
+    break;
+    
+    case TIMESCALE_HOUR:
+      //return 60*60;
+      return 3600;
+    break;
+    
+    case TIMESCALE_DAY:
+      //return 60*60*24;
+      return 86400;
+    break;
+    
+    case TIMESCALE_WEEK:
+      //return 60*60*24*7;
+      return 604800;
+    break;
+    
+    case TIMESCALE_MONTH:
+      //return 60*60*24*7*4;
+      return 2419200;
+    break;
+    
+    case TIMESCALE_YEAR:
+      //return 60*60*24*7*4*52;
+      return 125798400;
+    break;
+    
+    default:
+      return 0;
+    break;
+  }
+}
diff --git a/include/CanGrow_LittleFS.h b/include/CanGrow_LittleFS.h
new file mode 100644
index 0000000..01210b1
--- /dev/null
+++ b/include/CanGrow_LittleFS.h
@@ -0,0 +1,855 @@
+/*
+ * 
+ * include/CanGrow_LittleFS.h - LittleFS handling header file
+ * 
+ * 
+ *
+ */
+
+
+// LittleFS auto format
+#define FORMAT_LITTLEFS_IF_FAILED true
+
+void LFS_Init() {
+  const static char LogLoc[] PROGMEM = "[LittleFS:Init]";
+  Log.notice(F("%s" CR), LogLoc);
+  // ESP8266 crashes with first argument set
+  #ifdef ESP8266
+  if(!LittleFS.begin()) {
+  #endif
+  // ESP32 works, do autoformat if mount fails
+  #ifdef ESP32
+  if(!LittleFS.begin(FORMAT_LITTLEFS_IF_FAILED)) {
+  #endif
+    
+    Log.notice(F("%s FAILED initializing. You have to format LittleFS manually. Will now restart." CR), LogLoc);
+    Restart();
+  }
+}
+
+void LFS_Format() {
+  const static char LogLoc[] PROGMEM = "[LittleFS:Format]";
+  Log.notice(F("%s formatting ..." CR), LogLoc);
+  // ESP32 LittleFS needs begin() first, otherwise it would crash
+  // ESP8266 does not need it, so we leave it
+  #ifdef ESP32
+  LittleFS.begin();
+  #endif
+  if(LittleFS.format()) {
+    Log.notice(F("%s done!" CR), LogLoc);
+  } else {
+    Log.error(F("%s FAILED" CR), LogLoc);
+  }
+}
+
+bool existFile(const char *path) {
+  const static char LogLoc[] PROGMEM = "[LittleFS]";
+  #ifdef ESP8266
+  File file = LittleFS.open(path, "r");
+  #endif
+  
+  #ifdef ESP32
+  fs::FS &fs = LittleFS;
+  File file = fs.open(path);
+  #endif
+  
+  if (!file) {
+    Log.notice(F("%s file exists: %s" CR), LogLoc, path);
+    file.close();  
+    return false;
+  } else {
+    Log.warning(F("%s file does not exist: %s" CR), LogLoc, path);
+    file.close();  
+    return true;
+  }
+}
+
+String readFile(const char *path) {
+  const static char LogLoc[] PROGMEM = "[LittleFS]";
+  String fileContent;
+  
+  #ifdef ESP8266
+  File file = LittleFS.open(path, "r");
+  #endif
+  
+  #ifdef ESP32
+  fs::FS &fs = LittleFS;
+  File file = fs.open(path);
+  #endif
+  
+  if (!file) {
+    Log.error(F("%s FAILED to open file for reading: %s" CR), LogLoc, path);
+    return String(F("ERROR CANNOT OPEN"));
+  }
+
+  Log.notice(F("%s file content: %s" CR), LogLoc, path);
+  Log.notice(F("%s ----------" CR), LogLoc);
+  while (file.available()) { Serial.write(file.read()); }
+  Log.notice(F("%s ----------" CR), LogLoc);
+  fileContent = file.readString();
+  file.close();
+  return fileContent;
+}
+
+void writeFile(const char *path, const char *message) {
+  const static char LogLoc[] PROGMEM = "[LittleFS]";
+
+  #ifdef ESP8266
+  File file = LittleFS.open(path, "w");
+  #endif
+  
+  #ifdef ESP32
+  fs::FS &fs = LittleFS;
+  File file = fs.open(path, FILE_WRITE);
+  #endif
+  
+  if (!file) {
+    Log.error(F("%s FAILED to open file for reading: %s" CR), LogLoc, path);
+    return;
+  }
+  if (file.print(message)) {
+    Log.notice(F("%s file written: %s" CR), LogLoc, path);
+  } else {
+    Log.error(F("%s writing file FAILED: %s" CR), LogLoc, path);
+  }
+  //delay(2000);  // Make sure the CREATE and LASTWRITE times are different
+  file.close();
+}
+
+void deleteFile(const char *path) {
+  const static char LogLoc[] PROGMEM = "[LittleFS]";
+  
+  #ifdef ESP32
+  fs::FS &fs = LittleFS;
+  File file = fs.open(path, FILE_WRITE);
+  #endif
+  
+  Log.notice(F("%s deleting file: %s" CR), LogLoc, path);
+  #ifdef ESP8266
+  if (LittleFS.remove(path)) {
+  #endif
+  #ifdef ESP32
+  if (fs.remove(path)) {
+  #endif
+    Log.notice(F("%s deleted file: %s" CR), LogLoc, path);
+  } else {
+    Log.error(F("%s deleting file FAILED: %s" CR), LogLoc, path);
+  }
+}
+
+// https://arduinojson.org/v7/example/config/
+// https://arduinojson.org/v7/assistant/
+bool LoadConfig() {
+  const static char LogLoc[] PROGMEM = "[LittleFS:LoadConfig]";
+  #ifdef ESP8266
+  File file = LittleFS.open(CANGROW_CFG, "r");
+  #endif
+  
+  #ifdef ESP32
+  fs::FS &fs = LittleFS;
+  File file = fs.open(CANGROW_CFG);
+  #endif
+  
+  Log.notice(F("%s loading config from: %s" CR), LogLoc, CANGROW_CFG);
+  
+  JsonDocument doc;
+  // Deserialize the JSON document
+  DeserializationError error = deserializeJson(doc, file);
+  if(error) {
+    Log.error(F("%s FAILED to load config: %s" CR), LogLoc, CANGROW_CFG);
+    if (existFile(CANGROW_CFG)) {
+      readFile(CANGROW_CFG);
+    }
+    return false;
+  }
+  
+  /*
+   * put json values into config structs
+   */
+    
+  // Copy strings from the JsonDocument to the Config struct as char
+  
+  strlcpy(config.test, doc["test"], sizeof(config.test));
+  
+  /* WiFi */
+  JsonObject objWifi = doc["wifi"][0];
+  if(objWifi.containsKey("ssid"))
+    strlcpy(config.wifi.ssid, objWifi["ssid"], sizeof(config.wifi.ssid));
+  if(objWifi.containsKey("password"))
+    strlcpy(config.wifi.password, objWifi["password"], sizeof(config.wifi.password));
+  // Copy bool / int directly into struct
+  if(objWifi.containsKey("dhcp"))
+    config.wifi.dhcp = objWifi["dhcp"];
+  // load the ip addresses as array
+  if(objWifi.containsKey("ip")) {
+    for(byte i=0; i < 4 ; i++) {
+      config.wifi.ip[i] = objWifi["ip"][i];
+      config.wifi.netmask[i] = objWifi["netmask"][i];
+      config.wifi.gateway[i] = objWifi["gateway"][i];
+      config.wifi.dns[i] = objWifi["dns"][i];
+    }
+  }
+
+  
+  /* System */
+  JsonObject objSystem = doc["system"][0];
+  if(objSystem.containsKey("ntpOffset"))
+    config.system.ntpOffset = objSystem["ntpOffset"];
+  if(objSystem.containsKey("maintenanceDuration"))
+    config.system.maintenanceDuration = objSystem["maintenanceDuration"];
+  if(objSystem.containsKey("esp32cam"))
+    strlcpy(config.system.esp32cam, objSystem["esp32cam"], sizeof(config.system.esp32cam));
+  if(objSystem.containsKey("httpUser"))
+    strlcpy(config.system.httpUser, objSystem["httpUser"], sizeof(config.system.httpUser));
+  if(objSystem.containsKey("httpPass"))
+    strlcpy(config.system.httpPass, objSystem["httpPass"], sizeof(config.system.httpPass));
+  if(objSystem.containsKey("httpLogSerial"))
+    config.system.httpLogSerial = objSystem["httpLogSerial"];
+  
+  if(objSystem.containsKey("schedulerInterval"))
+    config.system.schedulerInterval = objSystem["schedulerInterval"];
+  
+  if(objSystem.containsKey("ntp"))
+    config.system.ntp = objSystem["ntp"];
+  if(objSystem.containsKey("rtc"))
+    config.system.rtc = objSystem["rtc"];
+  if(objSystem.containsKey("time2fs"))
+    config.system.time2fs = objSystem["time2fs"];
+  if(objSystem.containsKey("pwmFreq"))
+    config.system.pwmFreq = objSystem["pwmFreq"];
+
+  /* System Outputs */
+  JsonObject objSystemOutput = objSystem["output"][0];
+  for(byte i=0; i < Max_Outputs; i++) {
+    if(objSystemOutput["type"][i] > 0) {
+      
+      if(objSystemOutput.containsKey("type"))
+        config.system.output.type[i] = objSystemOutput["type"][i];
+      if(objSystemOutput.containsKey("device"))
+        config.system.output.device[i] = objSystemOutput["device"][i];
+      if(objSystemOutput.containsKey("name"))
+        strlcpy(config.system.output.name[i], objSystemOutput["name"][i], sizeof(config.system.output.name[i]));
+      if(objSystemOutput.containsKey("enabled"))
+        config.system.output.enabled[i] = objSystemOutput["enabled"][i];
+      // gpio
+      if(objSystemOutput.containsKey("gpio"))
+        config.system.output.gpio[i] = objSystemOutput["gpio"][i];
+      if(objSystemOutput.containsKey("invert"))
+        config.system.output.invert[i] = objSystemOutput["invert"][i];
+      if(objSystemOutput.containsKey("gpio_pwm"))
+        config.system.output.gpio_pwm[i] = objSystemOutput["gpio_pwm"][i];
+      // i2c type
+      if(objSystemOutput.containsKey("i2c_type"))
+        config.system.output.i2c_type[i] = objSystemOutput["i2c_type"][i];
+      // i2c addr
+      if(objSystemOutput.containsKey("i2c_addr"))
+        config.system.output.i2c_addr[i] = objSystemOutput["i2c_addr"][i];
+      // i2c port
+      if(objSystemOutput.containsKey("i2c_port"))
+        config.system.output.i2c_port[i] = objSystemOutput["i2c_port"][i];
+      // web
+      if(objSystemOutput.containsKey("webcall_host"))
+        strlcpy(config.system.output.webcall_host[i], objSystemOutput["webcall_host"][i], sizeof(config.system.output.webcall_host[i]));
+      if(objSystemOutput.containsKey("webcall_path_on"))
+        strlcpy(config.system.output.webcall_path_on[i], objSystemOutput["webcall_path_on"][i], sizeof(config.system.output.webcall_path_on[i]));
+      if(objSystemOutput.containsKey("webcall_path_off"))
+        strlcpy(config.system.output.webcall_path_off[i], objSystemOutput["webcall_path_off"][i], sizeof(config.system.output.webcall_path_off[i]));
+    }
+  }
+  
+  
+  /* System Sensors */
+  JsonObject objSystemSensor = objSystem["sensor"][0];
+  for(byte i=0; i < Max_Sensors; i++) {
+    if(objSystemSensor["type"][i] > 0) {
+      if(objSystemSensor.containsKey("type"))
+        config.system.sensor.type[i] = objSystemSensor["type"][i];
+      if(objSystemSensor.containsKey("name"))
+        strlcpy(config.system.sensor.name[i], objSystemSensor["name"][i], sizeof(config.system.sensor.name[i]));
+      if(objSystemSensor.containsKey("i2c_addr"))
+        //strlcpy(config.system.sensor.i2c_addr[i], objSystemSensor["i2c_addr"][i], sizeof(config.system.sensor.i2c_addr[i]));
+        config.system.sensor.i2c_addr[i] = objSystemSensor["i2c_addr"][i];
+      // gpio
+      if(objSystemSensor.containsKey("gpio")) {
+        for(byte j = 0; j < Max_Sensors_GPIO; j++) {
+          config.system.sensor.gpio[i][j] = objSystemSensor["gpio"][i][j];
+        }
+      }
+      
+      // offset
+      if(objSystemSensor.containsKey("offset")) {
+        for(byte j = 0; j < Max_Sensors_Read; j++) {
+          config.system.sensor.offset[i][j] = objSystemSensor["offset"][i][j];
+        }
+      }
+      
+      // low
+      if(objSystemSensor.containsKey("low")) {
+        for(byte j = 0; j < Max_Sensors_Read; j++) {
+          config.system.sensor.low[i][j] = objSystemSensor["low"][i][j];
+        }
+      }
+      
+      // high
+      if(objSystemSensor.containsKey("high")) {
+        for(byte j = 0; j < Max_Sensors_Read; j++) {
+          config.system.sensor.high[i][j] = objSystemSensor["high"][i][j];
+        }
+      }
+      
+      // rawConvert
+      if(objSystemSensor.containsKey("rawConvert")) {
+        for(byte j = 0; j < Max_Sensors_Read; j++) {
+          config.system.sensor.rawConvert[i][j] = objSystemSensor["rawConvert"][i][j];
+        }
+      }
+      
+    }
+  }
+  
+
+  /* Grow */
+  JsonObject objGrow = doc["grow"][0];
+  if(objGrow.containsKey("name"))
+    strlcpy(config.grow.name, objGrow["name"], sizeof(config.grow.name));
+  if(objGrow.containsKey("start"))
+    config.grow.start = objGrow["start"];
+  if(objGrow.containsKey("daysVeg"))
+    config.grow.daysVeg = objGrow["daysVeg"];
+  if(objGrow.containsKey("daysBloom"))
+    config.grow.daysBloom = objGrow["daysBloom"];
+  
+  /* Grow Light */
+  JsonObject objLight = objGrow["light"][0];
+  for(byte i = 0; i < Max_Outputs; i++) {
+    /* get light.configured */
+    if(objLight.containsKey("configured"))
+      config.grow.light.configured[i] = objLight["configured"][i];
+    /* check if light is configured */  
+    if(config.grow.light.configured[i] == true) {
+      /* get the rest of the config */
+      if(objLight.containsKey("output"))
+        config.grow.light.output[i] = objLight["output"][i];
+      if(objLight.containsKey("sunriseHourVeg"))
+        config.grow.light.sunriseHourVeg[i] = objLight["sunriseHourVeg"][i];
+      if(objLight.containsKey("sunriseMinuteVeg"))
+        config.grow.light.sunriseMinuteVeg[i] = objLight["sunriseMinuteVeg"][i];
+      if(objLight.containsKey("sunsetHourVeg"))
+        config.grow.light.sunsetHourVeg[i] = objLight["sunsetHourVeg"][i];
+      if(objLight.containsKey("sunsetMinuteVeg"))
+        config.grow.light.sunsetMinuteVeg[i] = objLight["sunsetMinuteVeg"][i];
+      if(objLight.containsKey("sunriseHourBloom"))
+        config.grow.light.sunriseHourBloom[i] = objLight["sunriseHourBloom"][i];
+      if(objLight.containsKey("sunriseMinuteBloom"))
+        config.grow.light.sunriseMinuteBloom[i] = objLight["sunriseMinuteBloom"][i];
+      if(objLight.containsKey("sunsetHourBloom"))
+        config.grow.light.sunsetHourBloom[i] = objLight["sunsetHourBloom"][i];
+      if(objLight.containsKey("sunsetMinuteBloom"))
+        config.grow.light.sunsetMinuteBloom[i] = objLight["sunsetMinuteBloom"][i];
+      
+      if(objLight.containsKey("power"))
+        config.grow.light.power[i] = objLight["power"][i];
+      if(objLight.containsKey("fade"))
+        config.grow.light.fade[i] = objLight["fade"][i];
+      if(objLight.containsKey("fadeDuration"))
+        config.grow.light.fadeDuration[i] = objLight["fadeDuration"][i];
+    }
+  }
+  
+  /* Grow Air */
+  JsonObject objAir = objGrow["air"][0];
+  for(byte i = 0; i < Max_Outputs; i++) {
+    /* get air.configured */
+    if(objAir.containsKey("configured"))
+      config.grow.air.configured[i] = objAir["configured"][i];
+    /* check if air is configured */  
+    if(config.grow.air.configured[i] == true) {
+      /* get the rest of the config */
+      if(objAir.containsKey("output"))
+        config.grow.air.output[i] = objAir["output"][i];
+      if(objAir.containsKey("power"))
+        config.grow.air.power[i] = objAir["power"][i];
+      if(objAir.containsKey("controlSensor"))
+        config.grow.air.controlSensor[i] = objAir["controlSensor"][i];
+      if(objAir.containsKey("controlRead"))
+        config.grow.air.controlRead[i] = objAir["controlRead"][i];
+      if(objAir.containsKey("controlMode"))
+        config.grow.air.controlMode[i] = objAir["controlMode"][i];
+      if(objAir.containsKey("min"))
+        config.grow.air.min[i] = objAir["min"][i];
+      if(objAir.containsKey("max"))
+        config.grow.air.max[i] = objAir["max"][i];
+    }
+  }
+  
+  /* Grow Water */
+  JsonObject objWater = objGrow["water"][0];
+  for(byte i = 0; i < Max_Outputs; i++) {
+    /* get air.configured */
+    if(objWater.containsKey("configured"))
+      config.grow.water.configured[i] = objWater["configured"][i];
+    /* check if air is configured */  
+    if(config.grow.water.configured[i] == true) {
+      /* get the rest of the config */
+      if(objWater.containsKey("output"))
+        config.grow.water.output[i] = objWater["output"][i];
+      if(objWater.containsKey("onTime"))
+        config.grow.water.onTime[i] = objWater["onTime"][i];
+      if(objWater.containsKey("controlSensor"))
+        config.grow.water.controlSensor[i] = objWater["controlSensor"][i];
+      if(objWater.containsKey("controlRead"))
+        config.grow.water.controlRead[i] = objWater["controlRead"][i];
+      if(objWater.containsKey("controlMode"))
+        config.grow.water.controlMode[i] = objWater["controlMode"][i];
+      if(objWater.containsKey("min"))
+        config.grow.water.min[i] = objWater["min"][i];
+      if(objWater.containsKey("max"))
+        config.grow.water.max[i] = objWater["max"][i];
+      if(objWater.containsKey("interval"))
+        config.grow.water.interval[i] = objWater["interval"][i];
+      if(objWater.containsKey("intervalUnit"))
+        config.grow.water.intervalUnit[i] = objWater["intervalUnit"][i];
+    }
+  }
+
+  // Close the file (Curiously, File's destructor doesn't close the file)
+  file.close();
+  Log.notice(F("%s config successfully loaded" CR), LogLoc);
+  #ifdef DEBUG
+  Log.verbose(F("%s --- runtime config ---" CR), LogLoc);
+  serializeJsonPretty(doc, Serial);
+  // Json output does not end with NewLine
+  Serial.println("");
+  Log.verbose(F("%s ----------------------" CR), LogLoc);
+  #endif
+  return true;
+}
+
+bool SaveConfig(bool writeToSerial = false) { 
+  const static char LogLoc[] PROGMEM = "[LittleFS:SaveConfig]";
+  /*
+   * Building config.json here 
+   */
+  JsonDocument doc;
+  
+  /* Root */
+  doc["test"] = config.test;
+  
+  /* WiFi */
+  JsonObject objWifi = doc["wifi"].add<JsonObject>();
+  objWifi["ssid"] = config.wifi.ssid;
+  objWifi["password"] = config.wifi.password;
+  // save the ip addressess as array
+  int i;
+  for(i=0; i <4 ; i++) {
+    objWifi["ip"][i] = config.wifi.ip[i];
+    objWifi["netmask"][i] = config.wifi.netmask[i];
+    objWifi["gateway"][i] = config.wifi.gateway[i];
+    objWifi["dns"][i] = config.wifi.dns[i];
+  }
+  objWifi["dhcp"] = config.wifi.dhcp;
+  
+  /* System */
+  JsonObject objSystem = doc["system"].add<JsonObject>();
+  objSystem["ntpOffset"] = config.system.ntpOffset;
+  objSystem["maintenanceDuration"] = config.system.maintenanceDuration;
+  objSystem["esp32cam"] = config.system.esp32cam;
+  objSystem["httpUser"] = config.system.httpUser;
+  objSystem["httpPass"] = config.system.httpPass;
+  objSystem["httpLogSerial"] = config.system.httpLogSerial;
+  objSystem["schedulerInterval"] = config.system.schedulerInterval;
+  objSystem["ntp"] = config.system.ntp;
+  objSystem["rtc"] = config.system.rtc;
+  objSystem["time2fs"] = config.system.time2fs;
+  objSystem["pwmFreq"] = config.system.pwmFreq;
+  
+  /* System Outputs */
+  JsonObject objSystemOutput = objSystem["output"].add<JsonObject>();
+  for(byte i=0; i < Max_Outputs; i++) {
+    if(config.system.output.type[i] > 0) {
+      objSystemOutput["type"][i] = config.system.output.type[i];
+      objSystemOutput["device"][i] = config.system.output.device[i];
+      objSystemOutput["name"][i] = config.system.output.name[i];
+      objSystemOutput["enabled"][i] = config.system.output.enabled[i];
+      // gpio
+      objSystemOutput["gpio"][i] = config.system.output.gpio[i];
+      objSystemOutput["invert"][i] = config.system.output.invert[i];
+      objSystemOutput["gpio_pwm"][i] = config.system.output.gpio_pwm[i];
+      // i2c type
+      objSystemOutput["i2c_type"][i] = config.system.output.i2c_type[i];
+      objSystemOutput["i2c_addr"][i] = config.system.output.i2c_addr[i];
+      objSystemOutput["i2c_port"][i] = config.system.output.i2c_port[i];
+      // web
+      objSystemOutput["webcall_host"][i] = config.system.output.webcall_host[i];
+      objSystemOutput["webcall_path_on"][i] = config.system.output.webcall_path_on[i];
+      objSystemOutput["webcall_path_off"][i] = config.system.output.webcall_path_off[i];
+
+    }
+  }
+  
+  /* System Sensors */
+  JsonObject objSystemSensor = objSystem["sensor"].add<JsonObject>();
+  for(byte i=0; i < Max_Sensors; i++) {
+    if(config.system.sensor.type[i] > 0) {
+      objSystemSensor["type"][i] = config.system.sensor.type[i];      
+      objSystemSensor["name"][i] = config.system.sensor.name[i];
+      objSystemSensor["i2c_addr"][i] = config.system.sensor.i2c_addr[i];
+      for(byte j = 0; j < Max_Sensors_GPIO; j++) {
+        objSystemSensor["gpio"][i][j] = config.system.sensor.gpio[i][j]; 
+      }
+      
+      /* offset reading */
+      for(byte j = 0; j < Max_Sensors_Read; j++) {
+        objSystemSensor["offset"][i][j] = config.system.sensor.offset[i][j]; 
+      }
+      
+      /* low reading */
+      for(byte j = 0; j < Max_Sensors_Read; j++) {
+        objSystemSensor["low"][i][j] = config.system.sensor.low[i][j]; 
+      }
+      
+      /* high reading */
+      for(byte j = 0; j < Max_Sensors_Read; j++) {
+        objSystemSensor["high"][i][j] = config.system.sensor.high[i][j]; 
+      }
+      
+      /* rawConvert reading */
+      for(byte j = 0; j < Max_Sensors_Read; j++) {
+        objSystemSensor["rawConvert"][i][j] = config.system.sensor.rawConvert[i][j]; 
+      }
+    }
+  }
+  
+  
+  /* Grow */
+  JsonObject objGrow = doc["grow"].add<JsonObject>();
+  objGrow["name"] = config.grow.name;
+  objGrow["start"] = config.grow.start;
+  objGrow["daysVeg"] = config.grow.daysVeg;
+  objGrow["daysBloom"] = config.grow.daysBloom;
+  
+  /* Grow Light */
+  JsonObject objLight = objGrow["light"].add<JsonObject>();
+  for(byte i = 0; i < Max_Outputs; i++) {
+    #ifdef DEBUG
+    Log.verbose(F("%s LightId %d, Max_Outputs %d, light.configured %T" CR), LogLoc, i, Max_Outputs, config.grow.light.configured[i]);
+    #endif
+    if(config.grow.light.configured[i] == true) {
+      objLight["configured"][i] = config.grow.light.configured[i];
+      objLight["output"][i] = config.grow.light.output[i];
+      objLight["sunriseHourVeg"][i] = config.grow.light.sunriseHourVeg[i];
+      objLight["sunriseMinuteVeg"][i] = config.grow.light.sunriseMinuteVeg[i];
+      objLight["sunsetHourVeg"][i] = config.grow.light.sunsetHourVeg[i];
+      objLight["sunsetMinuteVeg"][i] = config.grow.light.sunsetMinuteVeg[i];
+      
+      objLight["sunriseHourBloom"][i] = config.grow.light.sunriseHourBloom[i];
+      objLight["sunriseMinuteBloom"][i] = config.grow.light.sunriseMinuteBloom[i];
+      objLight["sunsetHourBloom"][i] = config.grow.light.sunsetHourBloom[i];
+      objLight["sunsetMinuteBloom"][i] = config.grow.light.sunsetMinuteBloom[i];
+      
+      objLight["power"][i] = config.grow.light.power[i];
+      objLight["fade"][i] = config.grow.light.fade[i];
+      objLight["fadeDuration"][i] = config.grow.light.fadeDuration[i];
+    }
+  }
+  
+  /* Grow Air */
+  JsonObject objAir = objGrow["air"].add<JsonObject>();
+  for(byte i = 0; i < Max_Outputs; i++) {
+    //Log.verbose(F("%s LightId %d, Max_Outputs %d, light.configured %T" CR), LogLoc, i, Max_Outputs, config.grow.light.configured[i]);
+    if(config.grow.air.configured[i] == true) {
+      objAir["configured"][i] = config.grow.air.configured[i];
+      objAir["output"][i] = config.grow.air.output[i];
+      objAir["power"][i] = config.grow.air.power[i];
+      objAir["controlSensor"][i] = config.grow.air.controlSensor[i];
+      objAir["controlRead"][i] = config.grow.air.controlRead[i];
+      objAir["controlMode"][i] = config.grow.air.controlMode[i];
+      objAir["min"][i] = config.grow.air.min[i];
+      objAir["max"][i] = config.grow.air.max[i];
+      
+    }
+  }
+  
+  /* Grow Water */
+  JsonObject objWater = objGrow["water"].add<JsonObject>();
+  for(byte i = 0; i < Max_Outputs; i++) {
+    //Log.verbose(F("%s LightId %d, Max_Outputs %d, light.configured %T" CR), LogLoc, i, Max_Outputs, config.grow.light.configured[i]);
+    if(config.grow.water.configured[i] == true) {
+      objWater["configured"][i] = config.grow.water.configured[i];
+      objWater["output"][i] = config.grow.water.output[i];
+      objWater["onTime"][i] = config.grow.water.onTime[i];
+      objWater["controlSensor"][i] = config.grow.water.controlSensor[i];
+      objWater["controlRead"][i] = config.grow.water.controlRead[i];
+      objWater["controlMode"][i] = config.grow.water.controlMode[i];
+      objWater["min"][i] = config.grow.water.min[i];
+      objWater["max"][i] = config.grow.water.max[i];
+      objWater["interval"][i] = config.grow.water.interval[i];
+      objWater["intervalUnit"][i] = config.grow.water.intervalUnit[i];
+      
+    }
+  }
+  
+  
+  /*
+   * END Building config.json here 
+   */
+
+  // if writeToSerial is true, output json to serial, but do not write to LittleFS
+  if(writeToSerial == false) {
+    #ifdef ESP8266
+    File file = LittleFS.open(CANGROW_CFG, "w");
+    #endif
+    
+    #ifdef ESP32
+    fs::FS &fs = LittleFS;
+    File file = fs.open(CANGROW_CFG, FILE_WRITE);
+    #endif
+    
+    if (!file) {
+      //Log.notice(F("%s loading config from: %s" CR), LogLoc, CANGROW_CFG);
+      Log.error(F("%s FAILED to open configfile for writing: %s" CR), LogLoc, CANGROW_CFG);
+      return false;
+    } else {
+      Log.notice(F("%s opened for writing %s" CR), LogLoc, CANGROW_CFG);
+    }
+    // Serialize JSON to file
+    if (serializeJson(doc, file) == 0) {
+      Log.error(F("%s FAILED to write configfile: %s" CR), LogLoc, CANGROW_CFG);
+    } else {
+      Log.notice(F("%s successfully written %s" CR), LogLoc, CANGROW_CFG);
+    }
+    file.close();
+  } else {
+    Log.notice(F("%s --- %s ---" CR), LogLoc, CANGROW_CFG);
+    serializeJsonPretty(doc, Serial);
+    Serial.println("");
+    Log.notice(F("%s ----------------------" CR), LogLoc, CANGROW_CFG);
+  }
+  
+  /* every time config get saved, we save the actual time too 
+   * so when ntp is not available, we hopefully do not lack behind too much
+   * (better then nothing) */
+  Time2FS_Save();
+  return true;
+  
+}
+
+///*
+ //* ESP8266 functions
+ //*/
+
+///*functions from https://github.com/esp8266/Arduino/blob/master/libraries/LittleFS/examples/LittleFS_Timestamp/LittleFS_Timestamp.ino*/
+//#ifdef ESP8266
+//void listDir(const char *dirname) {
+  //Serial.printf("Listing directory: %s\n", dirname);
+
+  //Dir root = LittleFS.openDir(dirname);
+
+  //while (root.next()) {
+    //File file = root.openFile("r");
+    //Serial.print("  FILE: ");
+    //Serial.print(root.fileName());
+    //Serial.print("  SIZE: ");
+    //Serial.print(file.size());
+    //time_t cr = file.getCreationTime();
+    //time_t lw = file.getLastWrite();
+    //file.close();
+    //struct tm *tmstruct = localtime(&cr);
+    //Serial.printf("    CREATION: %d-%02d-%02d %02d:%02d:%02d\n", (tmstruct->tm_year) + 1900, (tmstruct->tm_mon) + 1, tmstruct->tm_mday, tmstruct->tm_hour, tmstruct->tm_min, tmstruct->tm_sec);
+    //tmstruct = localtime(&lw);
+    //Serial.printf("  LAST WRITE: %d-%02d-%02d %02d:%02d:%02d\n", (tmstruct->tm_year) + 1900, (tmstruct->tm_mon) + 1, tmstruct->tm_mday, tmstruct->tm_hour, tmstruct->tm_min, tmstruct->tm_sec);
+  //}
+//}
+
+
+//void readFile(const char *path) {
+  //Serial.printf("Reading file: %s\n", path);
+
+  //File file = LittleFS.open(path, "r");
+  //if (!file) {
+    //Serial.println("Failed to open file for reading");
+    //return;
+  //}
+
+  //Serial.print("Read from file: ");
+  //while (file.available()) { Serial.write(file.read()); }
+  //file.close();
+//}
+
+//void writeFile(const char *path, const char *message) {
+  //Serial.printf("Writing file: %s\n", path);
+
+  //File file = LittleFS.open(path, "w");
+  //if (!file) {
+    //Serial.println("Failed to open file for writing");
+    //return;
+  //}
+  //if (file.print(message)) {
+    //Serial.println("File written");
+  //} else {
+    //Serial.println("Write failed");
+  //}
+  //delay(2000);  // Make sure the CREATE and LASTWRITE times are different
+  //file.close();
+//}
+
+//void appendFile(const char *path, const char *message) {
+  //Serial.printf("Appending to file: %s\n", path);
+
+  //File file = LittleFS.open(path, "a");
+  //if (!file) {
+    //Serial.println("Failed to open file for appending");
+    //return;
+  //}
+  //if (file.print(message)) {
+    //Serial.println("Message appended");
+  //} else {
+    //Serial.println("Append failed");
+  //}
+  //file.close();
+//}
+
+//void renameFile(const char *path1, const char *path2) {
+  //Serial.printf("Renaming file %s to %s\n", path1, path2);
+  //if (LittleFS.rename(path1, path2)) {
+    //Serial.println("File renamed");
+  //} else {
+    //Serial.println("Rename failed");
+  //}
+//}
+
+//void deleteFile(const char *path) {
+  //Serial.printf("Deleting file: %s\n", path);
+  //if (LittleFS.remove(path)) {
+    //Serial.println("File deleted");
+  //} else {
+    //Serial.println("Delete failed");
+  //}
+//}
+//#endif
+
+
+///*
+ //* ESP32 functions
+ //*/
+ 
+///*functions from https://github.com/espressif/arduino-esp32/blob/master/libraries/LittleFS/examples/LITTLEFS_time/LITTLEFS_time.ino*/
+//#ifdef ESP32
+//void listDir(fs::FS &fs, const char *dirname, uint8_t levels) {
+  //Serial.printf("Listing directory: %s\n", dirname);
+
+  //File root = fs.open(dirname);
+  //if (!root) {
+    //Serial.println("Failed to open directory");
+    //return;
+  //}
+  //if (!root.isDirectory()) {
+    //Serial.println("Not a directory");
+    //return;
+  //}
+
+  //File file = root.openNextFile();
+  //while (file) {
+    //if (file.isDirectory()) {
+      //Serial.print("  DIR : ");
+      //Serial.print(file.name());
+      //time_t t = file.getLastWrite();
+      //struct tm *tmstruct = localtime(&t);
+      //Serial.printf(
+        //"  LAST WRITE: %d-%02d-%02d %02d:%02d:%02d\n", (tmstruct->tm_year) + 1900, (tmstruct->tm_mon) + 1, tmstruct->tm_mday, tmstruct->tm_hour,
+        //tmstruct->tm_min, tmstruct->tm_sec
+      //);
+      //if (levels) {
+        //listDir(fs, file.path(), levels - 1);
+      //}
+    //} else {
+      //Serial.print("  FILE: ");
+      //Serial.print(file.name());
+      //Serial.print("  SIZE: ");
+      //Serial.print(file.size());
+      //time_t t = file.getLastWrite();
+      //struct tm *tmstruct = localtime(&t);
+      //Serial.printf(
+        //"  LAST WRITE: %d-%02d-%02d %02d:%02d:%02d\n", (tmstruct->tm_year) + 1900, (tmstruct->tm_mon) + 1, tmstruct->tm_mday, tmstruct->tm_hour,
+        //tmstruct->tm_min, tmstruct->tm_sec
+      //);
+    //}
+    //file = root.openNextFile();
+  //}
+//}
+
+//void removeDir(fs::FS &fs, const char *path) {
+  //Serial.printf("Removing Dir: %s\n", path);
+  //if (fs.rmdir(path)) {
+    //Serial.println("Dir removed");
+  //} else {
+    //Serial.println("rmdir failed");
+  //}
+//}
+
+//void readFile(fs::FS &fs, const char *path) {
+  //Serial.printf("Reading file: %s\n", path);
+
+  //File file = fs.open(path);
+  //if (!file) {
+    //Serial.println("Failed to open file for reading");
+    //return;
+  //}
+
+  //Serial.print("Read from file: ");
+  //while (file.available()) {
+    //Serial.write(file.read());
+  //}
+  //file.close();
+//}
+
+//void writeFile(fs::FS &fs, const char *path, const char *message) {
+  //Serial.printf("Writing file: %s\n", path);
+
+  //File file = fs.open(path, FILE_WRITE);
+  //if (!file) {
+    //Serial.println("Failed to open file for writing");
+    //return;
+  //}
+  //if (file.print(message)) {
+    //Serial.println("File written");
+  //} else {
+    //Serial.println("Write failed");
+  //}
+  //file.close();
+//}
+
+//void appendFile(fs::FS &fs, const char *path, const char *message) {
+  //Serial.printf("Appending to file: %s\n", path);
+
+  //File file = fs.open(path, FILE_APPEND);
+  //if (!file) {
+    //Serial.println("Failed to open file for appending");
+    //return;
+  //}
+  //if (file.print(message)) {
+    //Serial.println("Message appended");
+  //} else {
+    //Serial.println("Append failed");
+  //}
+  //file.close();
+//}
+
+//void renameFile(fs::FS &fs, const char *path1, const char *path2) {
+  //Serial.printf("Renaming file %s to %s\n", path1, path2);
+  //if (fs.rename(path1, path2)) {
+    //Serial.println("File renamed");
+  //} else {
+    //Serial.println("Rename failed");
+  //}
+//}
+
+//void deleteFile(fs::FS &fs, const char *path) {
+  //Serial.printf("Deleting file: %s\n", path);
+  //if (fs.remove(path)) {
+    //Serial.println("File deleted");
+  //} else {
+    //Serial.println("Delete failed");
+  //}
+//}
+//#endif
diff --git a/include/CanGrow_Logo.h b/include/CanGrow_Logo.h
new file mode 100644
index 0000000..b64522e
--- /dev/null
+++ b/include/CanGrow_Logo.h
@@ -0,0 +1,41 @@
+// 'CanGrow_Logo', 128x32px
+const unsigned char bmpCanGrow_Logo [] PROGMEM = {
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+  0x03, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 
+  0x07, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x1f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 
+  0x0e, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x38, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 
+  0x1c, 0x03, 0x00, 0x00, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x70, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 
+  0x18, 0x03, 0x00, 0x00, 0x00, 0x04, 0x07, 0xe0, 0x20, 0x60, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 
+  0x18, 0x03, 0x00, 0x00, 0x00, 0x06, 0x07, 0xe0, 0xe0, 0x60, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 
+  0x18, 0x00, 0x00, 0x00, 0x00, 0x03, 0x87, 0xe1, 0xc0, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+  0x30, 0x00, 0x3f, 0xc3, 0xff, 0x03, 0xc7, 0xe3, 0xc0, 0xcf, 0xf9, 0xff, 0xe3, 0xfc, 0xc1, 0x83, 
+  0x30, 0x00, 0x7f, 0xe3, 0xff, 0x83, 0xe7, 0xe7, 0xc0, 0xcf, 0xfb, 0xff, 0xe7, 0xfe, 0xc3, 0x87, 
+  0x30, 0x00, 0xe0, 0x73, 0x80, 0xc1, 0xf7, 0xef, 0xc0, 0xc0, 0x1b, 0x80, 0x0e, 0x03, 0xc3, 0x86, 
+  0x30, 0x00, 0xc0, 0x33, 0x00, 0xc1, 0xff, 0xff, 0x80, 0xc0, 0x1b, 0x00, 0x0c, 0x03, 0xc7, 0x8e, 
+  0x30, 0x01, 0xc0, 0x37, 0x00, 0xc0, 0xff, 0xff, 0x80, 0xc0, 0x3b, 0x00, 0x1c, 0x03, 0xc7, 0x8c, 
+  0x60, 0x01, 0xc0, 0x37, 0x00, 0xc0, 0xff, 0xff, 0x01, 0x80, 0x3f, 0x00, 0x18, 0x03, 0xcf, 0x9c, 
+  0x60, 0x00, 0x00, 0x37, 0x00, 0xc0, 0x7f, 0xfe, 0x01, 0x80, 0x37, 0x00, 0x18, 0x03, 0xcf, 0x9c, 
+  0x60, 0x00, 0x00, 0x76, 0x01, 0xc0, 0x1f, 0xfc, 0x01, 0x80, 0x36, 0x00, 0x18, 0x06, 0xdf, 0xb8, 
+  0x60, 0x00, 0x7f, 0xe6, 0x01, 0x9f, 0x9f, 0xfc, 0xf9, 0x80, 0x36, 0x00, 0x18, 0x06, 0xdd, 0xb8, 
+  0x60, 0x00, 0xff, 0xe6, 0x01, 0x87, 0xff, 0xff, 0xf1, 0x80, 0x76, 0x00, 0x18, 0x06, 0xdd, 0xb0, 
+  0xc0, 0x01, 0xc0, 0xee, 0x01, 0x83, 0xff, 0xff, 0xc3, 0x00, 0x7e, 0x00, 0x30, 0x06, 0xf9, 0xf0, 
+  0xc0, 0x0b, 0x80, 0x6e, 0x01, 0x81, 0xff, 0xff, 0x83, 0x00, 0x6e, 0x00, 0x30, 0x06, 0xf9, 0xe0, 
+  0xc0, 0x1b, 0x00, 0xec, 0x01, 0x80, 0x1f, 0xf8, 0x03, 0x00, 0x6c, 0x00, 0x30, 0x0e, 0xf1, 0xe0, 
+  0xc0, 0x3b, 0x00, 0xcc, 0x03, 0x80, 0x3f, 0xfc, 0x03, 0x00, 0xec, 0x00, 0x30, 0x0c, 0xf1, 0xc0, 
+  0xc0, 0x7b, 0x01, 0xcc, 0x03, 0x00, 0x7f, 0xfe, 0x03, 0x01, 0xec, 0x00, 0x30, 0x1c, 0xe1, 0xc0, 
+  0x7f, 0xf1, 0xff, 0xdc, 0x03, 0x00, 0xf0, 0x8f, 0x01, 0xff, 0xfc, 0x00, 0x1f, 0xf8, 0xe1, 0xc0, 
+  0x3f, 0xe0, 0xff, 0xcc, 0x03, 0x00, 0x00, 0x80, 0x00, 0xff, 0xcc, 0x00, 0x0f, 0xf0, 0xc1, 0x80, 
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+
+// Array of all bitmaps for convenience. (Total bytes used to store images in PROGMEM = 528)
+const int bmpallArray_LEN = 1;
+const unsigned char* bmpallArray[1] = {
+  bmpCanGrow_Logo
+};
diff --git a/include/CanGrow_Output.h b/include/CanGrow_Output.h
new file mode 100644
index 0000000..53b1112
--- /dev/null
+++ b/include/CanGrow_Output.h
@@ -0,0 +1,540 @@
+/*
+ * 
+ * include/CanGrow_Output.h - Output header file
+ * 
+ * 
+ *
+ */
+
+#include "Output/Output_Common.h"
+#include "Output/Output_I2C_01_MCP4725.h"
+#include "Output/Output_I2C_02_GP8403.h"
+
+/* OutputI2C index struct */
+struct OutputI2C_Index {
+  const char name[16];
+  const byte port[OUTPUT_TYPE_I2C_MAX_PORTS];
+  const byte max;
+};
+
+OutputI2C_Index OutputI2Cindex[] {
+  /* OutputI2C 00 is unset */
+  { "unset", {
+      {},
+  }},
+  /* OutputI2C 01 */
+  {
+    OUTPUT_I2C_01_NAME,
+    {
+      OUTPUT_TYPE_I2C_PORT_BYTE
+    },
+    sizeof(Output_I2C_01_MCP4725_Addr),
+  },
+  
+  /* 02 - DFRobot Gravity (GP8403) */
+  {
+    OUTPUT_I2C_02_NAME,
+    {
+      OUTPUT_TYPE_I2C_PORT_BYTE,
+      OUTPUT_TYPE_I2C_PORT_BYTE
+    },
+    sizeof(Output_I2C_02_GP8403_Addr),
+  }
+  
+};
+
+/* Dont forget to increase the index counter after you added a new output module */
+const byte OutputI2Cindex_length = 2;
+
+
+byte Output_I2C_Addr_Init_Update(const byte OutputI2CindexId, const byte AddrId, const byte PortId, const byte Mode, const bool Invert = false, const byte Value = 0) {
+  const static char LogLoc[] PROGMEM = "[Output:I2C:Addr_Init_Update]";
+  /* Multi purpose function.
+   * 
+   * Modes:
+   *  0 - return the i2c address as byte
+   *  1 - init the output i2c module, returns true (1) if succeeded
+   *  2 - update i2 module data
+   */
+  //byte Dummy_Addr[] = { 0x42, 0x69 };
+  
+  /* invert Value if set, save it to Value_tmp */
+  byte Value_tmp = Value;
+  if(Invert == true)
+    /* Value comes from outputState[] which is only type byte (max 255) */
+    Value_tmp = 255 - Value;
+  
+  switch(OutputI2CindexId) {
+    
+    /* I2C Output module 01 */
+    case 1:
+      switch(Mode) {
+        case OUPUT_I2C_AIU_MODE_ADDR:
+          return Output_I2C_01_MCP4725_Addr[AddrId];
+        break;
+        case OUPUT_I2C_AIU_MODE_INIT:
+          return Output_I2C_01_MCP4725_Init(AddrId, PortId);
+        break;
+        case OUPUT_I2C_AIU_MODE_UPDATE:
+          Output_I2C_01_MCP4725_Update(AddrId, PortId, Value_tmp);
+        break;
+      }
+    break;
+    
+    /* I2C Output module 02 */
+    case 2:
+      switch(Mode) {
+        case OUPUT_I2C_AIU_MODE_ADDR:
+          return Output_I2C_02_GP8403_Addr[AddrId];
+        break;
+        case OUPUT_I2C_AIU_MODE_INIT:
+          return Output_I2C_02_GP8403_Init(AddrId, PortId);
+        break;
+        case OUPUT_I2C_AIU_MODE_UPDATE:
+          Output_I2C_02_GP8403_Update(AddrId, PortId, Value_tmp);
+        break;
+      }
+    break;
+    
+    
+    
+    /* 02 - dummy*/
+    //case 2:
+      
+      //switch(Mode) {
+        //case 0:
+          //return Dummy_Addr[AddrId];
+        //break;
+        //case 1:
+          //return true;
+        //break;
+        //case 2:
+          //true;
+        //break;
+      //}
+    //break;
+    
+    /* unknown i2c output module id */
+    default:
+      Log.error(F("%s OutputI2Cindex ID %d not found" CR), LogLoc, OutputI2CindexId);
+    break;
+  }
+  
+  return 0;
+}
+
+
+byte Output_GPIO_Init_Update(const byte GPIOindexId, const bool PWM, const bool Invert, const byte Mode, const byte Value = 0) {
+  const static char LogLoc[] PROGMEM = "[Output:GPIO:Init_Update]";
+  bool Value_bool = Value;
+  switch(Mode) {
+    
+    case OUTPUT_GPIO_IU_MODE_INIT:
+      pinMode(GPIOindex[GPIOindexId].gpio, OUTPUT);
+      
+      if(Invert == true) {
+          digitalWrite(GPIOindex[GPIOindexId].gpio, 1 - Value_bool);
+        } else {
+          digitalWrite(GPIOindex[GPIOindexId].gpio, Value_bool);
+        }
+      return true;
+    break;
+    
+    case OUTPUT_GPIO_IU_MODE_UPDATE:
+      if((PWM == false) || (GPIOindex[GPIOindexId].note == NO_PWM)) {
+        if(Invert == true) {
+            digitalWrite(GPIOindex[GPIOindexId].gpio, 1 - Value_bool);
+          } else {
+            digitalWrite(GPIOindex[GPIOindexId].gpio, Value);
+          }
+      } else {
+        /* output inverted? */
+        if(Invert == true) {
+            /* when output is set to 0 (off), use digitalWrite LOW to prevent spikes */
+            if(GPIOindex[GPIOindexId].gpio < 255) {
+              analogWrite(GPIOindex[GPIOindexId].gpio, 255 - Value);
+            } else {
+              digitalWrite(GPIOindex[GPIOindexId].gpio, HIGH);
+              #ifdef DEBUG
+              Log.verbose(F("%s digitalWrite HIGH" CR), LogLoc);
+              #endif
+            }
+          /* not inverted */
+          } else {
+            /* when output is set to 0 (off), use digitalWrite LOW to prevent spikes */
+            if(GPIOindex[GPIOindexId].gpio < 255) {
+              analogWrite(GPIOindex[GPIOindexId].gpio, Value);
+            } else {
+              digitalWrite(GPIOindex[GPIOindexId].gpio, LOW);
+              #ifdef DEBUG
+              Log.verbose(F("%s digitalWrite LOW" CR), LogLoc);
+              #endif
+            }
+          }
+      }
+    break;
+  }
+  
+  return 0;
+}
+
+bool Output_Webcall_Init_Update(const byte OutputId, const bool Value = false) {
+  const static char LogLoc[] PROGMEM = "[Output:Webcall:Init_Update]";
+  
+  /* here we invert the value if set. First we hand it to a tmp var, Value_tmp */
+  bool Value_tmp = Value;
+  /* check if output has inverted flagged */
+  if(config.system.output.invert[OutputId] == true)
+    Value_tmp = 1 - Value;
+
+
+  String url;
+  url += F("http://");
+  url += config.system.output.webcall_host[OutputId];
+  url += F("/");
+
+  WiFiClient client;
+  HTTPClient http;
+  
+  ///* set timeout to three seconds */
+  //#ifdef ESP8266 
+  ///* on ESP8266 http.setTimeout accepts miliseconds */
+  //http.setTimeout(3000);
+  //#endif
+  
+  //#ifdef ESP32
+  ///* on ESP32 http.setTimeout() accepts only seconds - https://github.com/espressif/arduino-esp32/issues/3732 */
+  //http.setTimeout(3);
+  //#endif
+  
+  switch(Value_tmp) {
+    /* turn on */
+    case true:
+      url += config.system.output.webcall_path_on[OutputId];
+    break;
+    
+    /* turn off */
+    case false:
+      url += config.system.output.webcall_path_off[OutputId];
+    break;
+  }
+
+  /* build request */
+  http.begin(client, url);
+  
+  /* fire request and check result,  */
+  int httpResponseCode = http.GET();
+  /* if 200 , OK */
+  if(httpResponseCode > 0) {
+    #ifdef DEBUG2
+    Log.verbose(F("%s GET %s (%d)" CR), LogLoc, url.c_str(), httpResponseCode);
+    #endif
+    return true;
+  } else {
+    #ifdef DEBUG2
+    Log.verbose(F("%s FAILED GET %s (%d)" CR), LogLoc, url.c_str(), httpResponseCode);
+    #endif
+    return false;
+  }
+  
+  return 0;
+}
+
+bool Output_Check_PWM(const byte OutputId) {
+  /* when we verify output is GPIO PWM   OR */
+  if(((config.system.output.type[OutputId] == OUTPUT_TYPE_GPIO) && (config.system.output.gpio_pwm[OutputId] == true) && (GPIOindex[config.system.output.type[OutputId]].note != NO_PWM)) ||
+  /* Output is type I2C */ 
+     (config.system.output.type[OutputId] == OUTPUT_TYPE_I2C)
+  ) {
+    /* return true */
+    return true;
+  } else {
+    return false;
+  }
+}
+
+/*
+ * Output_Device
+ * 
+ * add, remove, (modify) config.grow.light[] objects
+ */
+void Output_Device_Grow_AddRemove(const byte OutputId, const byte mode) {
+  /* switch on device type of the output 
+   * 
+   * Modes:
+   *  0 - add - receive OutputId
+   *  1 - remove - receive LightId
+   *  2 - modify - receive LightId
+   * */
+  switch(config.system.output.device[OutputId]) {
+    case OUTPUT_DEVICE_LIGHT:
+      //byte LightId;
+      switch(mode) {
+        /* add */
+        case 0:
+          config.grow.light.configured[OutputId] = true;
+          config.grow.light.output[OutputId] = OutputId;
+          config.grow.light.sunriseHourVeg[OutputId] = 6;
+          config.grow.light.sunriseMinuteVeg[OutputId] = 0;
+          config.grow.light.sunsetHourVeg[OutputId] = 23;
+          config.grow.light.sunsetMinuteVeg[OutputId] = 59;
+          
+          config.grow.light.sunriseHourBloom[OutputId] = 6;
+          config.grow.light.sunriseMinuteBloom[OutputId] = 0;
+          config.grow.light.sunsetHourBloom[OutputId] = 18;
+          config.grow.light.sunsetMinuteBloom[OutputId] = 0;
+          
+          config.grow.light.power[OutputId] = 0;
+          if(Output_Check_PWM(OutputId)) {
+            config.grow.light.fade[OutputId] = true;
+          } else {
+            config.grow.light.fade[OutputId] = false;
+          }
+          
+          config.grow.light.fadeDuration[OutputId] = 30;
+          //SaveConfig()
+        break;
+        
+        /* remove */
+        case 1:
+          /* get LightId for this Output */
+          //for(byte i = 0; i < Max_Outputs; i++) {
+            //if((config.grow.light.configured[i] == true) && (config.grow.light.output[i] == OutputId)) {
+              //LightId = i;
+              //break;
+            //}
+          //}
+          config.grow.light.configured[OutputId] = 0;
+          config.grow.light.output[OutputId] = 0;
+          config.grow.light.sunriseHourVeg[OutputId] = 0;
+          config.grow.light.sunriseMinuteVeg[OutputId] = 0;
+          config.grow.light.sunsetHourVeg[OutputId] = 0;
+          config.grow.light.sunsetMinuteVeg[OutputId] = 0;
+          
+          config.grow.light.sunriseHourBloom[OutputId] = 0;
+          config.grow.light.sunriseMinuteBloom[OutputId] = 0;
+          config.grow.light.sunsetHourBloom[OutputId] = 0;
+          config.grow.light.sunsetMinuteBloom[OutputId] = 0;
+          
+          config.grow.light.power[OutputId] = 0;
+          config.grow.light.fade[OutputId] = 0;
+          
+          config.grow.light.fadeDuration[OutputId] = 0;
+          //SaveConfig();
+        break;
+        
+        //case 2:
+          //true;
+        //break;
+      }
+    break;
+    
+    case OUTPUT_DEVICE_FAN:
+    case OUTPUT_DEVICE_HUMIDIFIER:
+    case OUTPUT_DEVICE_DEHUMIDIFIER:
+    case OUTPUT_DEVICE_HEATING:
+      switch(mode) {
+        case 0:
+            config.grow.air.configured[OutputId] = true;
+            config.grow.air.output[OutputId] = OutputId;
+            config.grow.air.power[OutputId] = 0;
+            config.grow.air.controlSensor[OutputId] = 255; // 255 means unconfigured, because SensorId begins at 0
+            config.grow.air.controlRead[OutputId] = 255; // same here
+            config.grow.air.controlMode[OutputId] = 0;
+            config.grow.air.min[OutputId] = 0;
+            config.grow.air.max[OutputId] = 0;
+        break;
+        
+        case 1:
+            config.grow.air.configured[OutputId] = false;
+            config.grow.air.output[OutputId] = 0;
+            config.grow.air.power[OutputId] = 0;
+            config.grow.air.controlSensor[OutputId] = 0;
+            config.grow.air.controlRead[OutputId] = 0;
+            config.grow.air.controlMode[OutputId] = 0;
+            config.grow.air.min[OutputId] = 0;
+            config.grow.air.max[OutputId] = 0;
+        break;
+      }
+    break;
+    
+    case OUTPUT_DEVICE_PUMP:
+      switch(mode) {
+        case 0:
+            config.grow.water.configured[OutputId] = true;
+            config.grow.water.output[OutputId] = OutputId;
+            config.grow.water.onTime[OutputId] = 0;
+            config.grow.water.controlSensor[OutputId] = 255; // 255 means unconfigured, because SensorId begins at 0
+            config.grow.water.controlRead[OutputId] = 255; // same here
+            config.grow.water.controlMode[OutputId] = 0;
+            config.grow.water.min[OutputId] = 0;
+            config.grow.water.max[OutputId] = 0;
+            config.grow.water.interval[OutputId] = 0;
+            config.grow.water.intervalUnit[OutputId] = 0;
+        break;
+        
+        case 1:
+            config.grow.water.configured[OutputId] = false;
+            config.grow.water.output[OutputId] = 0;
+            config.grow.water.onTime[OutputId] = 0;
+            config.grow.water.controlSensor[OutputId] = 0;
+            config.grow.water.controlRead[OutputId] = 0;
+            config.grow.water.controlMode[OutputId] = 0;
+            config.grow.water.min[OutputId] = 0;
+            config.grow.water.max[OutputId] = 0;
+            config.grow.water.interval[OutputId] = 0;
+            config.grow.water.intervalUnit[OutputId] = 0;
+        break;
+      }
+    break;
+    
+    //case OUTPUT_DEVICE_HUMIDIFIER:
+    //break;
+    
+    //case OUTPUT_DEVICE_DEHUMIDIFIER:
+    //break;
+    
+    //case OUTPUT_DEVICE_HEATING:
+    //break;
+  }
+}
+
+
+/*
+ * Output main functions
+ * 
+ */ 
+void Output_Init() {
+  /* initialize all configured outputs */
+  const static char LogLoc[] PROGMEM = "[Output:Init]";
+  
+  #ifdef DEBUG
+  Log.verbose(F("%s == configured outputs ==" CR), LogLoc);
+  #endif
+
+  /* interate through all available Output IDs */
+  for(byte i = 0; i < Max_Outputs; i++) {
+    /* if configured */
+    if(config.system.output.type[i] > 0) {
+      /* get the configured output type */
+      switch(config.system.output.type[i]) {
+        case OUTPUT_TYPE_GPIO:
+          #ifdef DEBUG
+          Log.verbose(F("%s Output ID %d: %s - Device %s (%d), GPIO %d (%d), Enabled %T, PWM %T, Invert %T" CR), 
+                         LogLoc,
+                         i, config.system.output.name[i],
+                         Output_Device_descr[config.system.output.device[i]], config.system.output.device[i],
+                         GPIOindex[config.system.output.gpio[i]].gpio, config.system.output.gpio[i],
+                         config.system.output.enabled[i], config.system.output.gpio_pwm[i], config.system.output.invert[i]);
+          #endif
+          /* TODO implement gpio init */
+          outputStatus[i] = Output_GPIO_Init_Update(config.system.output.gpio[i], config.system.output.gpio_pwm[i], config.system.output.invert[i], OUTPUT_GPIO_IU_MODE_INIT);
+          
+        break;
+        
+        case OUTPUT_TYPE_I2C:
+          #ifdef DEBUG
+          Log.verbose(F("%s Output ID %d: %s - Device %s (%d), I2C type %s (%d), I2C addr 0x%x (%d), Module port %d, Enabled %T, PWM %T, Invert %T" CR), 
+                         LogLoc,
+                         i, config.system.output.name[i],
+                         Output_Device_descr[config.system.output.device[i]], config.system.output.device[i],
+                         OutputI2Cindex[config.system.output.i2c_type[i]].name, config.system.output.i2c_type[i],
+                         config.system.output.enabled[i], config.system.output.gpio_pwm[i], config.system.output.invert[i]);
+          #endif
+          outputStatus[i] = Output_I2C_Addr_Init_Update(config.system.output.i2c_type[i], config.system.output.i2c_addr[i], config.system.output.i2c_port[i], OUPUT_I2C_AIU_MODE_INIT, config.system.output.invert[i]);
+        break;
+        
+        case OUTPUT_TYPE_WEB:
+          #ifdef DEBUG
+          Log.verbose(F("%s Output ID %d: %s - Device %s (%d), Webcall host %s, Webcall Path ON '%s', Webcall Path OFF '%s', Enabled %T, PWM %T, Invert %T" CR), 
+                         LogLoc,
+                         i, config.system.output.name[i],
+                         Output_Device_descr[config.system.output.device[i]], config.system.output.device[i],
+                         config.system.output.webcall_host[i], config.system.output.webcall_path_on[i], config.system.output.webcall_path_off[i], 
+                         config.system.output.enabled[i], config.system.output.gpio_pwm[i], config.system.output.invert[i]);
+          #endif
+          /* TODO implement webcall init */
+          outputStatus[i] = Output_Webcall_Init_Update(i);
+          
+        break;
+        
+        default:
+          Log.error(F("%s (%d) Output type %d not found" CR), LogLoc, i, config.system.output.type[i]);
+        break;
+      }
+    }
+  }
+
+}
+
+void Output_Update() {
+  const static char LogLoc[] PROGMEM = "[Output:Update]";
+  #ifdef DEBUG2
+  unsigned long mStart = millis();
+  unsigned long mStop;
+  Log.verbose(F("%s Start %u" CR), LogLoc, mStart);
+  #endif
+  /* interate through all available Output IDs */
+  for(byte i = 0; i < Max_Outputs; i++) {
+    /* if configured and enabled */
+    if((config.system.output.type[i] > 0) && (config.system.output.enabled[i] == true)) {
+      /* get the configured output type */
+      switch(config.system.output.type[i]) {
+        
+        /*******
+         * GPIO
+         * *****/
+        case OUTPUT_TYPE_GPIO:
+          /* update GPIO output */
+          Output_GPIO_Init_Update(config.system.output.gpio[i], config.system.output.gpio_pwm[i], config.system.output.invert[i], OUTPUT_GPIO_IU_MODE_UPDATE, outputState[i]);
+          
+        break;
+        
+        /*******
+         * I2C
+         * *****/
+        case OUTPUT_TYPE_I2C:
+          /* perform I2C output update only, when outputStatus is OK (true) */
+          if(outputStatus[i] == true)
+            Output_I2C_Addr_Init_Update(config.system.output.i2c_type[i], config.system.output.i2c_addr[i], config.system.output.i2c_port[i], OUPUT_I2C_AIU_MODE_UPDATE, config.system.output.invert[i], outputState[i]);
+        break;
+        
+        /*******
+         * WEB
+         * ****/
+        case OUTPUT_TYPE_WEB:
+          /* check how often webcall output failed. if limit exceeded, do not update anymore */
+          if((outputStatus[i] == false) && (outputWebcallFailed[i] > 5)) {
+            /* if webcall fail counter has reached limit of 255, reset to 0
+             * so we retry a call. */
+            if(outputWebcallFailed[i] >= 255) {
+              outputWebcallFailed[i] = 0;
+            } else {
+              /* increment webcall failed counter. */
+              outputWebcallFailed[i]++;
+            }
+          } else {
+            /* update webcall output */
+            outputStatus[i] = Output_Webcall_Init_Update(i, outputState[i]);
+            if(outputStatus[i] == false) {
+              /* increment webcall failed counter */
+              outputWebcallFailed[i]++;
+            } else {
+              /* otherwise set to 0 */
+              outputWebcallFailed[i] = 0;
+            }
+          }
+          true;
+        break;
+        
+        default:
+          Log.error(F("%s (%d) Output type %d not found" CR), LogLoc, i, config.system.output.type[i]);
+        break;
+      }
+    }
+  }
+  #ifdef DEBUG2
+  mStop = millis();
+  Log.verbose(F("%s Stop %u (%u)" CR), LogLoc, mStop, mStop - mStart);
+  #endif
+}
diff --git a/include/CanGrow_Sensor.h b/include/CanGrow_Sensor.h
new file mode 100644
index 0000000..55dc7eb
--- /dev/null
+++ b/include/CanGrow_Sensor.h
@@ -0,0 +1,758 @@
+/*
+ * 
+ * include/CanGrow_Sensor.h - sensor header file
+ * 
+ * 
+ * 
+ * ADD A NEW SENSOR
+ * ****************
+ * If you want to add a new sensor, you have to to following things:
+ * 
+ * Check what it's the last used SensorIndex ID. If it's Sensor 08, you have to 
+ * take 09 as next.
+ * 
+ * Copy Sensor/00_Example.h to Sensor/09_YourSensor.h and rename everything in it from
+ * "00_Example" to "09_YourSensor" and edit all the needed functions and variables to your needs.
+ * 
+ * Add a new include line to CanGrow_Sensor.h (this file)
+ *   #include "Sensor/09_YourSensor.h"
+ * 
+ * Add a new Entry to the SensorIndex Array, like: 
+ * ***** SensorIndex[] *****
+ *   ,{
+ *   // 9 - YourSensor
+ *     // Sensor Name
+ *     SENSOR_09_NAME,
+ *     {
+ *       // Sensor Readings
+ *       SENSOR_READ_TYPE_TEMP,
+ *       SENSOR_READ_TYPE_HUMIDITY,
+ *       SENSOR_READ_TYPE_RAW
+ *     },
+ *     // Maximal Sensor Units (most time the Sum of available Addresses)
+ *     sizeof(Sensor_09_YourSensor_Addr),
+ *   }
+ * ************************
+ * 
+ * If you are done with that, you have to add a new Switch case for the new Sensor ID 
+ * to Sensor_Addr_Init_Update() and Sensor_getValue() like:
+ * 
+ * ***** Sensor_Addr_Init_Update() *****
+ *   // Sensor 09
+ *   case 9:
+ *     switch(mode) {
+ *       case 0:
+ *         return Sensor_09_YourSensor_Addr[AddrId];
+ *       break;
+ *       
+ *       case 1:
+ *         return Sensor_09_YourSensor_Init(AddrId);
+ *       break;
+ *       
+ *       case 2:
+ *         Sensor_09_YourSensor_Update(AddrId);
+ *       break;
+ *     }
+ *   break;
+ * *************************************
+ * 
+ * ***** Sensor_getValue() *****
+ *   // Sensor 09
+ *   case 9:
+ *     return Sensor_09_YourSensor[AddrId][ReadValId];
+ *   break;
+ * *****************************
+ */
+
+/* should come as dependency with all adafruit sensor libs. If not, see here: 
+ * https://github.com/adafruit/Adafruit_Sensor */
+#include <Adafruit_Sensor.h>
+
+#include "Sensor/Sensor_Common.h"
+#include "Sensor/01_ADC_builtin.h"
+#include "Sensor/02_BME280.h"
+#include "Sensor/03_BME680.h"
+#include "Sensor/04_SHT3x.h"
+#include "Sensor/05_MLX90614.h"
+#include "Sensor/06_TCS34725.h"
+#include "Sensor/07_ADS1115.h"
+#include "Sensor/08_ADS1015.h"
+#include "Sensor/09_Chirp.h"
+#include "Sensor/10_CCS811.h"
+
+/*
+ * Sensor Todo list:
+ * 
+ * - CCS811 CO2 sensor, will have type SENSOR_TYPE_I2C_WITH_GPIO, it needs signal on pin WAK
+ *   cheap - ~ 8€ on Aliexpress
+ * - HX711 for weight sensor, this sensor needs two GPIOs for communication
+ * - SCD30/40 CO2 sensor, expensive, >70€
+ */
+
+
+struct Sensor_Index {
+  /*
+   * Sensor Index
+   * - name
+   * - readings (array, up to 8 entries)
+   *   - 0 unset
+   *   - 1 Raw
+   *   - 2 Temp
+   *   - 3 Humidity
+   *   - 4 Moisture
+   *   - 5 Pressure 
+   *   - 6 Gas restistance
+   * - max units
+   * 
+   */
+  const char name[16];
+  const byte type;
+  const byte read[Max_Sensors_Read];
+  const byte max;
+  const byte gpioMax;
+};
+
+Sensor_Index SensorIndex[] {
+  /*
+   * Example:
+   * 
+   * // 0 - Example
+   * { SENSOR_00_NAME,
+   *   { 
+   *     SENSOR_READ_TYPE_TEMP,
+   *     SENSOR_READ_TYPE_HUMIDITY,
+   *     SENSOR_READ_TYPE_RAW,
+   *     SENSOR_READ_TYPE_RAW
+   *   },
+   *   // max nr of sensor units by nr of available addresses
+   *   sizeof(Sensor_00_Example_Addr),
+   * },
+   * 
+   */
+   
+  /* 0 is for unset in config */
+  { "unset", 255, {
+    {},
+  }},  
+  
+// 1 - internal ADC
+  { SENSOR_01_NAME,
+    SENSOR_TYPE_INTADC,
+    { 
+      SENSOR_READ_TYPE_RAW
+    },
+    SENSOR_01_MAX,
+  },
+  
+  // 2 - BME280
+  { SENSOR_02_NAME,
+    SENSOR_TYPE_I2C,
+    { 
+      SENSOR_READ_TYPE_TEMP,
+      SENSOR_READ_TYPE_HUMIDITY,
+      SENSOR_READ_TYPE_PRESSURE,
+      SENSOR_READ_TYPE_ALTITUDE
+    },
+    // max nr of sensor units by nr of available addresses
+    sizeof(Sensor_02_BME280_Addr),
+  },
+  
+  // 3 - BME680
+  { SENSOR_03_NAME,
+    SENSOR_TYPE_I2C,
+    { 
+      SENSOR_READ_TYPE_TEMP,
+      SENSOR_READ_TYPE_HUMIDITY,
+      SENSOR_READ_TYPE_PRESSURE,
+      SENSOR_READ_TYPE_ALTITUDE,
+      SENSOR_READ_TYPE_GAS_RESISTANCE
+    },
+    sizeof(Sensor_03_BME680_Addr),
+  },
+  
+  // 4 - SHT3x
+  { SENSOR_04_NAME,
+    SENSOR_TYPE_I2C,
+    { 
+      SENSOR_READ_TYPE_TEMP,
+      SENSOR_READ_TYPE_HUMIDITY
+    },
+    sizeof(Sensor_04_SHT3X_Addr),
+  },
+  
+  // 5 - MLX90614
+  { SENSOR_05_NAME,
+    SENSOR_TYPE_I2C,
+    { 
+      /* Ambient temp */
+      SENSOR_READ_TYPE_TEMP,
+      /* Object temp */
+      SENSOR_READ_TYPE_TEMP
+    },
+    sizeof(Sensor_05_MLX90614_Addr),
+  },
+  
+  // 6 - TCS34725
+  { SENSOR_06_NAME,
+    SENSOR_TYPE_I2C,
+    { 
+      SENSOR_READ_TYPE_COLOR_TEMP,
+      SENSOR_READ_TYPE_LUX,
+      SENSOR_READ_TYPE_COLOR_RED,
+      SENSOR_READ_TYPE_COLOR_GREEN,
+      SENSOR_READ_TYPE_COLOR_BLUE
+    },
+    sizeof(Sensor_06_TCS34725_Addr),
+  },
+  
+  {
+  // 7 - ADS1115
+    SENSOR_07_NAME,
+    SENSOR_TYPE_I2C,
+    {
+      /* A0 */
+      SENSOR_READ_TYPE_RAW,
+      /* A1 */
+      SENSOR_READ_TYPE_RAW,
+      /* A2 */
+      SENSOR_READ_TYPE_RAW,
+      /* A3 */
+      SENSOR_READ_TYPE_RAW
+      
+    },
+    sizeof(Sensor_07_ADS1115_Addr),
+  },
+
+  {
+  // 8 - ADS1015
+    SENSOR_08_NAME,
+    SENSOR_TYPE_I2C,
+    {
+      SENSOR_READ_TYPE_RAW,
+      SENSOR_READ_TYPE_RAW,
+      SENSOR_READ_TYPE_RAW,
+      SENSOR_READ_TYPE_RAW
+      
+    },
+    sizeof(Sensor_08_ADS1015_Addr),
+  },
+  
+  {
+  // 9 - I2C Chirp soilmoisture/temperature sensor
+    SENSOR_09_NAME,
+    SENSOR_TYPE_I2C,
+    {
+      // raw soilmoisture value
+      SENSOR_READ_TYPE_RAW,
+      // temperature
+      SENSOR_READ_TYPE_TEMP,
+      /* raw light value takes 3s to use, so we dont use it. if you need it,
+       * uncomment it here and in Sensor/09_Chirp Sensor_09_Chirp_Update() */
+      // SENSOR_READ_TYPE_RAW
+    },
+    sizeof(Sensor_09_Chirp_Addr),
+  },
+  
+  {
+  /* 10 - CCS811 CO2 I2C sensor */
+    SENSOR_10_NAME,
+    SENSOR_TYPE_I2C,
+    {
+      /* CO2 as parts per million */
+      SENSOR_READ_TYPE_PARTS_PER_MILLION,
+      /* TVOC value (Total Volatile Organic Compouds)*/
+      SENSOR_READ_TYPE_TVOC,
+    },
+    sizeof(Sensor_10_CCS811_Addr),
+  }
+};
+
+/* sum up of number of sensors. Dont forget to increment if you add one :) */
+const byte SensorIndex_length = 10;
+
+byte Sensor_Addr_Init_Update(const byte SensorIndexId, const byte AddrId, const byte mode, const byte Gpio2 = 0) {
+  const static char LogLoc[] PROGMEM = "[Sensor:Addr_Init_Update]";
+  /* Multi purpose function.
+   * 
+   * Modes:
+   *  0 - get the address as byte (i2c_addr index, gpio index)
+   *  1 - init the sensor, returns true (1) if succeeded
+   *  2 - update sensors data
+   * 
+   * When using a sensor which is using bare GPIOs like int ADC, or some 1- or 2-Wire sensors
+   * AddrId is used for the first GPIO and gpio2 for the second.
+   * Maybe i come up later with a better idea, but for now...
+   */
+   #ifdef DEBUG3
+   if(mode > 0)
+     Log.verbose(F("%s Mode: %d, SensorIndexId: %d, AddrId: %d" CR), LogLoc, mode, SensorIndexId, AddrId);
+   #endif
+  switch(SensorIndexId) {
+    /*
+     * Example:
+     * 
+     * case 0:
+     *   if(!onlyReturn)
+     *     Sensor_00_Example_Init(AddrId);
+     *   return Sensor_00_Example_Addr[AddrId];
+     * break;
+     */
+    
+    /* Sensor 01 */
+    /* Internal ADC is an exception. Its clearly not an I2C device, but as I let the ADC
+     * have for both, 8266 and 32, configurable GPIOs, it kinda has.
+     * AddrId with int ADC is GPIOindex[].type */
+    case 1:
+      switch(mode) {
+        case 0:
+          /* internal ADC does not has a address here */
+          return 0;
+        break;
+        
+        case 1:
+          return Sensor_01_ADC_Init(AddrId);
+        break;
+        
+        case 2:
+          Sensor_01_ADC_Update(AddrId);
+        break;
+      }
+    break;
+    
+    /* Sensor 02 */
+    case 2:
+      switch(mode) {
+        case 0:
+          return Sensor_02_BME280_Addr[AddrId];
+        break;
+        
+        case 1:
+          return Sensor_02_BME280_Init(AddrId);
+        break;
+        
+        case 2:
+          Sensor_02_BME280_Update(AddrId);
+        break;
+      }
+    break;
+    
+    /* Sensor 03 */
+    case 3:
+      switch(mode) {
+        case 0:
+          return Sensor_03_BME680_Addr[AddrId];
+        break;
+        
+        case 1:
+          return Sensor_03_BME680_Init(AddrId);
+        break;
+        
+        case 2:
+          Sensor_03_BME680_Update(AddrId);
+        break;
+      }
+    break;
+    
+    /* Sensor 04 */
+    case 4:
+      switch(mode) {
+        case 0:
+          return Sensor_04_SHT3X_Addr[AddrId];
+        break;
+        
+        case 1:
+          return Sensor_04_SHT3X_Init(AddrId);
+        break;
+        
+        case 2:
+          Sensor_04_SHT3X_Update(AddrId);
+        break;
+      }
+    break;
+    
+    /* Sensor 05 */
+    case 5:
+      switch(mode) {
+        case 0:
+          return Sensor_05_MLX90614_Addr[AddrId];
+        break;
+        
+        case 1:
+          return Sensor_05_MLX90614_Init(AddrId);
+        break;
+        
+        case 2:
+          Sensor_05_MLX90614_Update(AddrId);
+        break;
+      }
+    break;
+    
+    /* Sensor 06 */
+    case 6: 
+      switch(mode) {
+        case 0:
+          return Sensor_06_TCS34725_Addr[AddrId];
+        break;
+        
+        case 1:
+          return Sensor_06_TCS34725_Init(AddrId);
+        break;
+        
+        case 2:
+          Sensor_06_TCS34725_Update(AddrId);
+        break;
+      }
+    break;
+    
+    /* Sensor 07 */
+    case 7:
+      switch(mode) {
+        case 0:
+          return Sensor_07_ADS1115_Addr[AddrId];
+        break;
+        
+        case 1:
+          return Sensor_07_ADS1115_Init(AddrId);
+        break;
+        
+        case 2:
+          Sensor_07_ADS1115_Update(AddrId);
+        break;
+      }
+    break;
+
+    /* Sensor 08 */
+    case 8:
+      switch(mode) {
+        case 0:
+          return Sensor_08_ADS1015_Addr[AddrId];
+        break;
+        
+        case 1:
+          return Sensor_08_ADS1015_Init(AddrId);
+        break;
+        
+        case 2:
+          Sensor_08_ADS1015_Update(AddrId);
+        break;
+      }
+    break;
+    
+    /* Sensor 09 */
+    case 9:
+      switch(mode) {
+        case 0:
+          return Sensor_09_Chirp_Addr[AddrId];
+        break;
+        
+        case 1:
+          return Sensor_09_Chirp_Init(AddrId);
+        break;
+        
+        case 2:
+          Sensor_09_Chirp_Update(AddrId);
+        break;
+      }
+    break;
+    
+    /* Sensor 10 */
+    case 10:
+      switch(mode) {
+        case 0:
+          return Sensor_10_CCS811_Addr[AddrId];
+        break;
+        
+        case 1:
+          return Sensor_10_CCS811_Init(AddrId);
+        break;
+        
+        case 2:
+          Sensor_10_CCS811_Update(AddrId);
+        break;
+      }
+    break;
+        
+    /* unknown sensor id */
+    default:
+      Log.error(F("%s SensorIndex ID %d not found" CR), LogLoc, config.system.sensor.type[AddrId]);
+    break;
+  }
+  
+  return 0;
+
+}
+
+float Sensor_getValue(const byte SensorIndexId, const byte AddrId, const byte ReadValId = 0) {
+  const static char LogLoc[] PROGMEM = "[Sensor:getValue]";
+  /* not the best solution, but solution for the moment
+   * i hope i can come up in future with a way, where i do not have to 
+   * maintain three different places with huge switch cases
+   * and return everything as float, even when it could be easy an int
+   * 
+   * here we read the value ReadVal from the given SensorIndexId and its AddrId
+   * In case of RAW readings, like from ADCs , There is an Index ReadValId as well
+   */
+  switch(SensorIndexId) {
+    
+    /* Sensor 01 */
+    case 1:
+      #ifdef ESP8266
+      return Sensor_01_ADC[AddrId];
+      #endif
+      
+      #ifdef ESP32
+      /* */
+      return Sensor_01_ADC[Sensor_01_ADC_ArrId(AddrId)];
+      #endif
+    break;
+    
+    /* Sensor 02 */
+    case 2:
+      return Sensor_02_BME280[AddrId][ReadValId];
+    break;
+    
+    /* Sensor 03 */
+    case 3:
+      return Sensor_03_BME680[AddrId][ReadValId];
+    break;
+    
+    /* Sensor 04 */
+    case 4:
+      return Sensor_04_SHT3X[AddrId][ReadValId];
+    break;
+    
+    /* Sensor 05 */
+    case 5:
+      return Sensor_05_MLX90614[AddrId][ReadValId];
+    break;
+    
+    /* Sensor 06 */
+    case 6: 
+      return Sensor_06_TCS34725[AddrId][ReadValId];
+    break;
+    
+    /* Sensor 07 */
+    case 7:
+      return Sensor_07_ADS1115[AddrId][ReadValId];
+    break;
+    
+    /* Sensor 08 */
+    case 8:
+      return Sensor_08_ADS1015[AddrId][ReadValId];
+    break;
+    
+    /* Sensor 09 */
+    case 9:
+      return Sensor_09_Chirp[AddrId][ReadValId];
+    break;
+    
+    /* Sensor 10 */
+    case 10:
+      return Sensor_10_CCS811[AddrId][ReadValId];
+    break;
+    
+    /* unknown sensor id */
+    default:
+      Log.error(F("%s SensorIndex ID %d not found" CR), LogLoc, SensorIndexId);
+    break;
+  }
+  
+  return 0;
+}
+
+
+
+
+/*
+ * *********************************************************************************************
+ * From here on you do not need to touch any code (hopefully) if you want to add a new sensor!
+ * *********************************************************************************************
+ */
+
+float Sensor_getCalibratedValue(const byte SensorId, const byte ReadId) {
+  float valueRaw;
+  float value;
+  
+  
+  
+  /* if SensorId is configured and there is a reading on ReadingId */
+  if((config.system.sensor.type[SensorId] > 0) && (SensorIndex[config.system.sensor.type[SensorId]].read[ReadId] > 0)) {
+    /* first, get the raw / original value */
+    if(SensorIndex[config.system.sensor.type[SensorId]].type == SENSOR_TYPE_INTADC) {
+      valueRaw = Sensor_getValue(config.system.sensor.type[SensorId], config.system.sensor.gpio[SensorId][0], ReadId);
+    } else if(SensorIndex[config.system.sensor.type[SensorId]].type == SENSOR_TYPE_I2C) {
+      valueRaw = Sensor_getValue(config.system.sensor.type[SensorId], config.system.sensor.i2c_addr[SensorId], ReadId);
+    }
+    
+    /* if reading is RAW, check what to do with it */
+    if(SensorIndex[config.system.sensor.type[SensorId]].read[ReadId] == SENSOR_READ_TYPE_RAW) {
+      /* config.system.sensor.rawConvert 
+       *  0 - unconfigured, return raw value
+       *  1 - soilmoisture, return percentage 
+       *  2 - other TBD */
+      switch(config.system.sensor.rawConvert[SensorId][ReadId]) {
+        /* soilmoisture as percentage */
+        case SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE:
+          /* dont use map when both , low and high, are 0 - this causes a crash */
+          if((config.system.sensor.low[SensorId][ReadId] > 0) || (config.system.sensor.high[SensorId][ReadId] > 0)) {
+            /* use map to calculate percentage value */
+            value = map(valueRaw, config.system.sensor.low[SensorId][ReadId], config.system.sensor.high[SensorId][ReadId], 0, 100);
+          } else {
+            value = 0;
+          }
+          return value;
+        break;
+        
+        default:
+          return valueRaw;
+        break;
+      }
+    } else {
+      /* if not a RAW value, return the value with the offset */
+      return valueRaw + config.system.sensor.offset[SensorId][ReadId];
+    }
+  }
+  return 0;
+}
+
+
+void Sensor_Log_Readings(const byte SensorIndexId , const byte AddrId) {
+  const static char LogLoc[] PROGMEM = "[Sensor:Log_Readings]";
+  Log.verbose(F("%s Sensor %s (%d)" CR), LogLoc, SensorIndex[SensorIndexId].name, AddrId);
+  /* iterate through the SensorIndex readings */
+  for(byte j = 0; j < Max_Sensors_Read; j++) {
+    /* if SensorIndex[].read[] > 0 (means there is a value to read) */
+    if(SensorIndex[SensorIndexId].read[j] > 0 ) {
+      if(SensorIndex[SensorIndexId].type == SENSOR_TYPE_INTADC) {
+        Log.verbose(F("%s  - %s: %F %s" CR), LogLoc, Sensor_Read_descr[SensorIndex[SensorIndexId].read[j]],
+                    Sensor_getValue(SensorIndexId, AddrId),    Sensor_Read_unit[SensorIndex[SensorIndexId].read[j]]);
+      } else {
+        Log.verbose(F("%s  - %s: %F %s" CR), LogLoc, Sensor_Read_descr[SensorIndex[SensorIndexId].read[j]],
+                    Sensor_getValue(SensorIndexId, AddrId, j), Sensor_Read_unit[SensorIndex[SensorIndexId].read[j]]);
+      }
+    }
+  }
+}
+
+void Sensor_Init() {
+  /* main function that does initialize all configured sensors at once. called from setup()*/
+  const static char LogLoc[] PROGMEM = "[Sensor:Init]";
+  /* Go through all configured sensors and initialize them */
+  #ifdef DEBUG
+  Log.verbose(F("%s == Sensor drivers ==" CR), LogLoc);
+  
+  for(byte i = 1; i <= SensorIndex_length; i++) {
+    Log.verbose(F("%s Sensor_Index %d, Name %s, Readings" CR), LogLoc, i, SensorIndex[i].name );
+    
+    for(byte j = 0; j < Max_Sensors_Read; j++) {
+      if(SensorIndex[i].read[j] > 0 ) {
+        Log.verbose(F("%s   %d: %S %S (%d)" CR), LogLoc, j, Sensor_Read_descr[SensorIndex[i].read[j]], Sensor_Read_unit[SensorIndex[i].read[j]], SensorIndex[i].read[j]);
+      }
+    }
+  }
+  
+  Log.verbose(F("%s == configured Sensors ==" CR), LogLoc);
+  #endif
+
+  
+  /* iterate through configured sensors */
+  for(byte i = 0; i < Max_Sensors; i++) {
+    if(config.system.sensor.type[i] > 0) {
+      /* if sensor type is internal ADC */
+      if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_INTADC) {
+        
+        #ifdef DEBUG
+        Log.verbose(F("%s Sensor ID %d: %s - %s (GPIO ID %d, GPIO %d), offering" CR), LogLoc, i,config.system.sensor.name[i],
+                    SensorIndex[config.system.sensor.type[i]].name, config.system.sensor.gpio[i][0], GPIOindex[config.system.sensor.gpio[i][0]].gpio);
+        #endif
+        
+        /* initialize */
+        sensorStatus[i] = Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.gpio[i][0], SENSOR_AIU_MODE_INIT);
+        
+        #ifdef DEBUG
+        /* when init was successful, list the sensor values */
+        if(sensorStatus[i] == true) {
+          Sensor_Log_Readings(config.system.sensor.type[i], config.system.sensor.gpio[i][0]);
+
+        }
+        #endif
+        
+      } else if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_I2C) {
+        /* when SensorIndex[].type is == I2C sensor*/
+        /* get only the I2C Address */
+        byte Addr = Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.i2c_addr[i], SENSOR_AIU_MODE_ADDR);
+        #ifdef DEBUG
+        Log.verbose(F("%s Sensor ID %d: %s - %s (I2C %d, 0x%x), offering" CR), LogLoc, i,config.system.sensor.name[i],
+                    SensorIndex[config.system.sensor.type[i]].name, config.system.sensor.i2c_addr[i], Addr);
+        #endif
+        /* initialize */
+        sensorStatus[i] = Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.i2c_addr[i], SENSOR_AIU_MODE_INIT);
+        #ifdef DEBUG
+        /* when init was successful, list the sensor values */
+        if(sensorStatus[i] == true) {
+          Sensor_Log_Readings(config.system.sensor.type[i], config.system.sensor.i2c_addr[i]);
+        }
+        #endif
+      }     
+    }
+  }
+}
+
+
+void Sensor_Update() {
+  /* Update all configured sensors Values */
+  const static char LogLoc[] PROGMEM = "[Sensor:Update]";
+  
+  #ifdef DEBUG2
+  unsigned long mStart = millis();
+  unsigned long mStop;
+  Log.verbose(F("%s Start %u" CR), LogLoc, mStart);
+  #endif
+  
+  /* go through all possible existing Sensor configurations */
+  for(byte i = 0; i < Max_Sensors; i++) {
+    /* every configured one */
+    if(config.system.sensor.type[i] > 0) {
+      /* if internal ADC */
+      if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_INTADC) {
+        if(sensorStatus[i] == true) {
+          #ifdef DEBUG2
+          Log.verbose(F("%s (%d) %s: %s (%d)" CR), LogLoc, i, config.system.sensor.name[i], SensorIndex[config.system.sensor.type[i]].name, config.system.sensor.gpio[i][0]);
+          #endif
+          /* perform update of sensor values */
+          Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.gpio[i][0], SENSOR_AIU_MODE_UPDATE);
+          #ifdef DEBUG2
+          Sensor_Log_Readings(config.system.sensor.type[i], config.system.sensor.gpio[i][0]);
+          #endif
+        } 
+        #ifdef DEBUG2
+          else {
+          Log.verbose(F("%s Sensor %d (%s, %d) not initialized." CR), LogLoc, i, SensorIndex[config.system.sensor.type[i]].name, Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.gpio[i][0], SENSOR_AIU_MODE_ADDR));
+        }
+        #endif
+      /* Everything above 1 is an I2C sensor */
+      } else if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_I2C) {
+        if(sensorStatus[i] == true) {
+          #ifdef DEBUG2
+          Log.verbose(F("%s (%d) %s: %s (%d)" CR), LogLoc, i, config.system.sensor.name[i], SensorIndex[config.system.sensor.type[i]].name, config.system.sensor.i2c_addr[i]);
+          #endif
+          /* perform update of sensor values */
+          Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.i2c_addr[i], SENSOR_AIU_MODE_UPDATE);
+          #ifdef DEBUG2
+          Sensor_Log_Readings(config.system.sensor.type[i], config.system.sensor.i2c_addr[i]);
+          #endif
+        }
+        #ifdef DEBUG2
+          else {
+          Log.verbose(F("%s Sensor %d (%s, 0x%x) not initialized." CR), LogLoc, i, SensorIndex[config.system.sensor.type[i]].name, Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.i2c_addr[i], SENSOR_AIU_MODE_ADDR));
+        }
+        #endif
+      }
+    }
+  }
+  
+  #ifdef DEBUG2
+  mStop = millis();
+  Log.verbose(F("%s Stop %u (%u)" CR), LogLoc, mStop, mStop - mStart);
+  #endif
+}
diff --git a/include/CanGrow_Timer.h b/include/CanGrow_Timer.h
new file mode 100644
index 0000000..1103657
--- /dev/null
+++ b/include/CanGrow_Timer.h
@@ -0,0 +1,61 @@
+/*
+ * 
+ * include/CanGrow_Timer.h - timer header file
+ * 
+ * 
+ *
+ */
+
+
+/*
+ * Timer stuff
+ */
+
+void Timer_Sensor() {
+  Sensor_Update();
+}
+
+void Timer_Output() {
+  /* Update the outputs (switching GPIOs, sending webcall, etc) */
+  Output_Update();
+}
+
+
+
+void Timer_Control() {
+  const static char LogLoc[] PROGMEM = "[Core:Timer_1s]";
+  #ifdef DEBUG2
+  Log.verbose(F("%s - trigger [Sensor:Update]" CR), LogLoc);
+  #endif
+  
+  /* Updating Light output states in memory */
+  Control_Light();
+  
+  /* Updating Air output states in memory */
+  Control_Air();
+  
+  /* Updating Water output sates in memory */
+  Control_Water();
+}
+
+void Timer_3s() {
+  const static char LogLoc[] PROGMEM = "[Core:Timer_3s]";
+  #ifdef DEBUG2
+  Log.verbose(F("%s" CR), LogLoc);
+  #endif
+  
+}
+
+void Timer_5s() {
+  const static char LogLoc[] PROGMEM = "[Core:Timer_5s]";
+  #ifdef DEBUG2
+  Log.verbose(F("%s" CR), LogLoc);
+  #endif
+}
+
+void TimeR_Init() {
+  timer.setInterval(1000, Timer_Output);
+  timer.setInterval(1000, Timer_Sensor);
+  timer.setInterval(100, Timer_Control);
+  
+}
diff --git a/include/CanGrow_Webserver.h b/include/CanGrow_Webserver.h
new file mode 100644
index 0000000..e69de79
--- /dev/null
+++ b/include/CanGrow_Webserver.h
@@ -0,0 +1,131 @@
+/*
+ * 
+ * include/CanGrow_Webserver.h - webserver header file
+ * 
+ * 
+ *
+ */
+ 
+/*
+ * include static files files
+ */
+#include "Webserver/File_cangrow_CSS.h"
+#include "Webserver/File_cangrow_JS.h"
+#include "Webserver/File_favicon_ico.h"
+
+/*
+ * include webpages header files
+ */
+#include "Webserver/Header.h"
+#include "Webserver/Footer.h"
+#include "Webserver/Webserver_Common.h"
+#include "Webserver/Page_404.h"
+#include "Webserver/Page_root.h"
+#include "Webserver/Page_wifi.h"
+#include "Webserver/Page_system.h"
+#include "Webserver/Page_grow.h"
+
+/*
+ * include Api header files
+ */
+#include "Webserver/Api_sensor.h"
+
+AsyncWebServer webserver(80);
+// load requestLogger middleware
+AsyncLoggingMiddleware requestLogger;
+
+/*
+ * setup all the webhandlers
+ */
+void Webserver_Init() {
+  const static char LogLoc[] PROGMEM = "[Webserver]";
+  Log.notice(F("%s initializing" CR), LogLoc);
+  
+  
+  /* url handler definition */
+  webserver.on("/", HTTP_GET, WebPage_root);
+  webserver.on("/cangrow.css", HTTP_GET, WebFile_cangrow_CSS);
+  webserver.on("/cangrow.js", HTTP_GET, WebFile_cangrow_JS);
+  webserver.on("/favicon.ico", HTTP_GET, WebFile_favicon_ico);
+
+  webserver.on("/wifi/", HTTP_GET, WebPage_wifi);
+  webserver.on("/wifi/", HTTP_POST, WebPage_wifi);
+  webserver.on("/system/", HTTP_GET, WebPage_system);
+  webserver.on("/system/", HTTP_POST, WebPage_system);
+  
+  webserver.on("/system/update", HTTP_GET, WebPage_system_update);
+  webserver.on("/system/update", HTTP_POST, WebPage_system_update, WebPage_system_update_ApplyUpdate);
+  
+  webserver.on("/system/restart", HTTP_GET, WebPage_system_restart);
+  webserver.on("/system/restart", HTTP_POST, WebPage_system_restart);
+  
+  webserver.on("/system/wipe", HTTP_GET, WebPage_system_wipe);
+  webserver.on("/system/wipe", HTTP_POST, WebPage_system_wipe);
+  
+  webserver.on("/system/output/", HTTP_GET, WebPage_system_output);
+  webserver.on("/system/output/", HTTP_POST, WebPage_system_output);
+  
+  webserver.on("/system/output/add", HTTP_GET, WebPage_system_output_add);
+  webserver.on("/system/output/add", HTTP_POST, WebPage_system_output_add);
+  
+  webserver.on("/system/sensor/", HTTP_GET, WebPage_system_sensor);
+  webserver.on("/system/sensor/", HTTP_POST, WebPage_system_sensor);
+  
+  webserver.on("/system/sensor/add", HTTP_GET, WebPage_system_sensor_add);
+  webserver.on("/system/sensor/add", HTTP_POST, WebPage_system_sensor_add);
+
+  webserver.on("/system/sensor/calibrate", HTTP_GET, WebPage_system_sensor_calibrate);
+  webserver.on("/system/sensor/calibrate", HTTP_POST, WebPage_system_sensor_calibrate);
+  
+  
+  /* grow */
+  webserver.on("/grow/", HTTP_GET, WebPage_grow);
+  webserver.on("/grow/", HTTP_POST, WebPage_grow);
+  webserver.on("/grow/light/", HTTP_GET, WebPage_grow_light);
+  webserver.on("/grow/light/", HTTP_POST, WebPage_grow_light);
+  webserver.on("/grow/air/", HTTP_GET, WebPage_grow_air);
+  webserver.on("/grow/air/", HTTP_POST, WebPage_grow_air);
+  webserver.on("/grow/water/", HTTP_GET, WebPage_grow_water);
+  webserver.on("/grow/water/", HTTP_POST, WebPage_grow_water);
+  webserver.on("/grow/dashboard/", HTTP_GET, WebPage_grow_dashboard);
+  webserver.on("/grow/dashboard/", HTTP_POST, WebPage_grow_dashboard);
+  /* api */
+  //webserver.on("/api/sensor", HTTP_GET, Api_sensor_data);
+  webserver.on("/api/sensor/", HTTP_GET, Api_sensor_data);
+  webserver.on("/api/sensor/raw", HTTP_GET, Api_sensor_data_raw);
+  webserver.on("/api/sensor/driver", HTTP_GET, Api_sensor_driver);
+  
+  
+  /* DEBUG only - offer config for direct download */
+  #ifndef DEBUG
+  webserver.serveStatic(CANGROW_CFG, LittleFS, CANGROW_CFG);
+  #endif
+  
+  /* 404 Error page */
+  webserver.onNotFound(WebserverNotFound);
+  
+  
+  // this activates the middleware
+  if(config.system.httpLogSerial == true) {
+    requestLogger.setOutput(Serial);
+    Log.notice(F("%s serial logging: enabled" CR), LogLoc);
+    webserver.addMiddleware(&requestLogger);
+  } else {
+    Log.notice(F("%s serial logging: disabled" CR), LogLoc);
+  }
+  
+  
+  // Workaround, see comment at
+  // https://github.com/mathieucarbou/ESPAsyncWebServer/blob/main/docs/index.md#scanning-for-available-wifi-networks
+  // call the network scan once, so there are some values at the first call
+  // of the wifi settings page. otherwise the first call of the wifi scan would return
+  // an empty list of networks
+  Log.notice(F("%s call [wifi:ScanNetworks] to workaround empty scan results bug" CR), LogLoc);
+  
+  WebPage_wifi_ScanNetworks();
+
+  webserver.begin();
+  Log.notice(F("%s Ready to serve" CR), LogLoc);
+  
+}
+
diff --git a/include/CanGrow_Wifi.h b/include/CanGrow_Wifi.h
new file mode 100644
index 0000000..7aa9ec4
--- /dev/null
+++ b/include/CanGrow_Wifi.h
@@ -0,0 +1,136 @@
+/*
+ * 
+ * include/CanGrow_Wifi.h - Wifi stuff header file
+ * 
+ * 
+ *
+ */
+
+
+void Wifi_AP() {
+  const static char LogLoc[] PROGMEM = "[WiFi:AP]";  
+  char randNr[5];
+  itoa(random(9999), randNr, 10);
+  //WiFi.softAPConfig(config.wifi.ip, config.wifi.gateway, config.wifi.netmask);
+  IPAddress ip(192,168,4,20);
+  IPAddress gateway(0,0,0,0);
+  IPAddress netmask(255,255,255,0);
+  WiFi.softAPConfig(ip, gateway, netmask);
+  
+  /* when no ssid is configured, we assume here cangrow is in a fresh factory reset mode
+   * when a ssid is already configured, we seem not to be able to connect to it. so we protect
+   * our already configured cangrow controller with setting a temporary wifi ap password 
+   * and log it to serial. */  
+  if(strlen(config.wifi.ssid) > 0) {
+    const char * password = RandomString();
+    /* growName[64] + 8 */
+    char ssid[20+5];
+    strcpy(ssid, "CanGrow-FAILED-WIFI-");
+    /* random maximum 4 digit number for ssid
+     * https://arduino.stackexchange.com/a/42987*/
+
+    strcat(ssid, randNr);
+    
+    Log.error(F("%s create access point" CR), LogLoc);
+    
+    Log.error(F("%s  SSID    : %s" CR), LogLoc, ssid);
+    Log.error(F("%s  Password: %s" CR), LogLoc, password);
+    WiFi.softAP(ssid, password);
+  } else {
+    char ssid[21+4];
+    strcpy(ssid, CANGROW_DEFAULT_WIFI_SSID);
+    //strcat(ssid, "-");
+    /* random maximum 4 digit number for ssid
+     * https://arduino.stackexchange.com/a/42987*/
+    //strcat(ssid, randNr);
+    /* start access point default password when being unconfigured */
+    Log.notice(F("%s create access point" CR), LogLoc);
+    Log.notice(F("%s  SSID    : %S" CR), LogLoc, ssid);
+    Log.notice(F("%s  Password: %S" CR), LogLoc, CANGROW_DEFAULT_WIFI_PASSWORD);
+    WiFi.softAP(ssid, CANGROW_DEFAULT_WIFI_PASSWORD);
+    //WiFi.softAP(CANGROW_DEFAULT_WIFI_SSID);
+  }
+  
+  
+  Log.notice(F("%s access point started." CR), LogLoc);
+  Log.notice(F("%s  IP      : %s" CR), LogLoc, IP2Char(ip));
+  Log.notice(F("%s  Netmask : %s" CR), LogLoc, IP2Char(netmask));
+}
+
+
+void Wifi_Connect() {
+  const static char LogLoc[] PROGMEM = "[WiFi:Connect]";
+  Log.notice(F("%s connecting to SSID: %s" CR), LogLoc, config.wifi.ssid);
+  
+  WiFi.begin(config.wifi.ssid, config.wifi.password);
+  if(config.wifi.dhcp == false) {
+    Log.notice(F("%s using static ip configuration:" CR), LogLoc);
+    
+    Log.notice(F("%s   IP     : %s" CR), LogLoc, IP2Char(config.wifi.ip));
+    Log.notice(F("%s   Netmask: %s" CR), LogLoc, IP2Char(config.wifi.netmask));
+    Log.notice(F("%s   Gateway: %s" CR), LogLoc, IP2Char(config.wifi.gateway));
+    Log.notice(F("%s   DNS    : %s" CR), LogLoc, IP2Char(config.wifi.dns));
+    
+    WiFi.config(config.wifi.ip, config.wifi.dns, config.wifi.gateway, config.wifi.netmask);
+  } else {
+    Log.notice(F("%s using DHCP for ip configuration" CR), LogLoc);
+  }
+  
+  Log.notice("%s ", LogLoc);
+  const byte max = 30;
+  byte count = 0;
+  // wait until WiFi connection is established
+  while (count < max) {
+    /* check connection stations */
+    if(WiFi.status() != WL_CONNECTED) {
+      /* if not connected, print dot and increment count */
+      delay(500);
+      Serial.print(".");
+      count++;
+    } else {
+      /* if connected, set count to 10 to exit loop*/
+      count = max+1;
+    }
+  }
+  
+  /* check connection status. */
+  if(WiFi.status() != WL_CONNECTED) {
+    /* if connection failed, create AP */
+    Log.error(F("FAILED! Fallback to AP mode" CR), LogLoc);
+    WiFi.disconnect();
+    /* 
+     * TODO / BUG
+     * 
+     * On ESP32 there are no scan results shown in wifi tab, when connect to
+     * a saved network failed and the esp created then its own network. 
+     * 
+     * without trying to connect it works fine, like when doing a factory reset.
+     * 
+     * switch mode to softAP 
+    /* WiFi.mode(WIFI_AP_STA);
+     */
+
+    Wifi_AP();
+  } else {
+    Serial.println("CONNECTED!");
+    if(config.wifi.dhcp == true) {
+      Log.notice(F("%s DHCP offered ip configuration:" CR), LogLoc);
+      Log.notice(F("%s  IP     : %s" CR), LogLoc, IP2Char(WiFi.localIP()));
+      Log.notice(F("%s  Netmask: %s" CR), LogLoc, IP2Char(WiFi.subnetMask()));
+      Log.notice(F("%s  Gateway: %s" CR), LogLoc, IP2Char(WiFi.gatewayIP()));
+      Log.notice(F("%s  DNS    : %s" CR), LogLoc, IP2Char(WiFi.dnsIP()));
+    }
+  }
+}
+
+void Wifi_Init() {
+  const static char LogLoc[] PROGMEM = "[WiFi:Init]";
+  Log.notice(F("%s" CR), LogLoc);
+  
+  if(strlen(config.wifi.ssid)  == 0) {
+    Log.notice(F("%s config.wifi.ssid is unset" CR), LogLoc);
+    Wifi_AP();
+  } else {
+    Wifi_Connect();
+  }
+}
diff --git a/include/Output/Output_Common.h b/include/Output/Output_Common.h
new file mode 100644
index 0000000..b00b2e9
--- /dev/null
+++ b/include/Output/Output_Common.h
@@ -0,0 +1,84 @@
+/*
+ * 
+ * include/Output/Output_Common.h - Output common header file
+ * 
+ * 
+ *
+ */
+
+/*
+ * Output Type 
+ */
+
+// How many output types exist
+const byte OUTPUT_TYPE__TOTAL = 3;
+ 
+const byte OUTPUT_TYPE_GPIO = 1;
+const byte OUTPUT_TYPE_I2C = 2;
+const byte OUTPUT_TYPE_WEB = 3;
+
+const char OUTPUT_TYPE_GPIO_descr[] PROGMEM = {"GPIO"};
+const char OUTPUT_TYPE_I2C_descr[] PROGMEM = {"I2C"};
+const char OUTPUT_TYPE_WEB_descr[] PROGMEM = {"Webcall"};
+
+const char * Output_Type_descr[] = {
+  NULL, // 0 - no description because 0 means unconfigured
+  OUTPUT_TYPE_GPIO_descr,
+  OUTPUT_TYPE_I2C_descr,
+  OUTPUT_TYPE_WEB_descr,
+};
+
+/* Output_GPIO_Addr_Init_Update() modes */
+const byte OUTPUT_GPIO_IU_MODE_INIT = 0;
+const byte OUTPUT_GPIO_IU_MODE_UPDATE = 1;
+
+/* Output_Webcall_Addr_Init_Update() modes */
+const byte OUTPUT_WEB_IU_MODE_INIT = 0;
+const byte OUTPUT_WEB_IU_MODE_UPDATE = 1;
+
+
+/*
+ * OutputI2C types / modules
+ */
+const byte OUTPUT_TYPE_I2C_MAX_PORTS = 2;
+/* Total number of I2C PORT Types */
+const byte OUTPUT_TYPE_I2C_PORT__TOTAL = 1;
+/* port type for percentage. Those ports receive an int from 0 up to 100 to set their output value */
+const byte OUTPUT_TYPE_I2C_PORT_BYTE = 1;
+
+/* Output_I2C_Addr_Init_Update() modes */
+const byte OUPUT_I2C_AIU_MODE_ADDR = 0;
+const byte OUPUT_I2C_AIU_MODE_INIT = 1;
+const byte OUPUT_I2C_AIU_MODE_UPDATE = 2;
+
+
+/*
+ * Output Device
+ */
+// 0 is unconfigured
+const byte OUTPUT_DEVICE__TOTAL = 6;
+
+const byte OUTPUT_DEVICE_LIGHT = 1;
+const byte OUTPUT_DEVICE_FAN = 2;
+const byte OUTPUT_DEVICE_PUMP = 3;
+const byte OUTPUT_DEVICE_HUMIDIFIER = 4;
+const byte OUTPUT_DEVICE_DEHUMIDIFIER = 5;
+const byte OUTPUT_DEVICE_HEATING = 6;
+
+const char OUTPUT_DEVICE_LIGHT_descr[] PROGMEM = {"&#x1F4A1; Light"};
+const char OUTPUT_DEVICE_FAN_descr[] PROGMEM = {"&#x1F300; Fan"};
+const char OUTPUT_DEVICE_PUMP_descr[] PROGMEM = {"&#x1F4A7; Pump"};
+const char OUTPUT_DEVICE_HUMIDIFIER_descr[] PROGMEM = {"&#x1F300; Humidifier"};
+const char OUTPUT_DEVICE_DEHUMIDIFIER_descr[] PROGMEM = {"&#x1F300; Dehumidifier"};
+const char OUTPUT_DEVICE_HEATING_descr[] PROGMEM = {"&#x1F300; Heating"};
+
+
+const char * Output_Device_descr[] = {
+  NULL, // 0 - no description because 0 means unconfigured
+  OUTPUT_DEVICE_LIGHT_descr,
+  OUTPUT_DEVICE_FAN_descr,
+  OUTPUT_DEVICE_PUMP_descr,
+  OUTPUT_DEVICE_HUMIDIFIER_descr,
+  OUTPUT_DEVICE_DEHUMIDIFIER_descr,
+  OUTPUT_DEVICE_HEATING_descr,
+};
diff --git a/include/Output/Output_I2C_01_MCP4725.h b/include/Output/Output_I2C_01_MCP4725.h
new file mode 100644
index 0000000..6820f38
--- /dev/null
+++ b/include/Output/Output_I2C_01_MCP4725.h
@@ -0,0 +1,45 @@
+/*
+ * 
+ * include/Output/OutputI2C_01_MCP4725.h - sensor header for I2C Output MCP4725 sensor
+ * 
+ * 
+ *
+ */
+
+#include <Adafruit_MCP4725.h>
+
+#define OUTPUT_I2C_01_NAME "MCP4725"
+
+const byte Output_I2C_01_MCP4725_Addr[] = { 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67 };
+
+Adafruit_MCP4725 MCP4725[sizeof(Output_I2C_01_MCP4725_Addr)];
+
+const byte Output_I2C_01_MCP4725_Ports = 1;
+
+void Output_I2C_01_MCP4725_Update(const byte AddrId, const byte PortId, const byte Value) {
+  /* Update Output Port of I2C Module */
+  const static char LogLoc[] PROGMEM = "[Output:I2C:01_MCP4725:Update]";
+  
+  /* 'Value' , which comes from outputState[], is byte, so 0-255. So we need to map this to the MCPs  0-4095 */
+  MCP4725[AddrId].setVoltage(map(Value, 0, 255, 0, 4095), false);
+  
+  #ifdef DEBUG
+  Log.verbose(F("%s 0x%x Port %d, Value %d, MCP4725_Value %d" CR), LogLoc, Output_I2C_01_MCP4725_Addr[AddrId], PortId, Value, map(Value, 0, 255, 0, 4095));
+  #endif
+  
+}
+
+bool Output_I2C_01_MCP4725_Init(const byte AddrId, const byte PortId) {
+  /* Initialize I2C Module, return true when successful */
+  const static char LogLoc[] PROGMEM = "[Output:I2C:01_MCP4725:Init]";
+  bool returnCode;
+  if(MCP4725[AddrId].begin(Output_I2C_01_MCP4725_Addr[AddrId])) {
+    Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Output_I2C_01_MCP4725_Addr[AddrId]);
+    Output_I2C_01_MCP4725_Update(AddrId, PortId, 50);
+    returnCode =  true;
+  } else {
+    Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Output_I2C_01_MCP4725_Addr[AddrId]);
+    returnCode = false;
+  }
+  return returnCode;
+}
diff --git a/include/Output/Output_I2C_02_GP8403.h b/include/Output/Output_I2C_02_GP8403.h
new file mode 100644
index 0000000..ad16776
--- /dev/null
+++ b/include/Output/Output_I2C_02_GP8403.h
@@ -0,0 +1,52 @@
+/*
+ * 
+ * include/Output/OutputI2C_Output_I2C_02_GP8403.h - sensor header for I2C Output GP8403 (DFR Gravity) sensor
+ * 
+ * 
+ *
+ */
+
+#include <DFRobot_GP8XXX.h>
+
+#define OUTPUT_I2C_02_NAME "GP8403"
+
+const byte Output_I2C_02_GP8403_Addr[] = { 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F };
+
+DFRobot_GP8403 GP8403[sizeof(Output_I2C_02_GP8403_Addr)];
+
+const byte Output_I2C_02_GP8403_Ports = 2;
+
+
+void Output_I2C_02_GP8403_Update(const byte AddrId, const byte PortId, const byte Value) {
+  /* Update Output Port of I2C Module */
+  const static char LogLoc[] PROGMEM = "[Output:I2C:02_GP8403:Update]";
+
+  /* 'Value' , which comes from outputState[], is byte, so 0-255. So we need to map this to the GP8403  0-4095 */
+  GP8403[AddrId].setDACOutVoltage(map(Value, 0, 255, 0, 4095), PortId);
+
+  #ifdef DEBUG
+  Log.verbose(F("%s 0x%x Port %d, Value %d, GP8403_Value %d" CR), LogLoc, Output_I2C_02_GP8403_Addr[AddrId], PortId, Value, map(Value, 0, 255, 0, 4095));
+  #endif
+  
+}
+
+bool Output_I2C_02_GP8403_Init(const byte AddrId, const byte PortId) {
+  /* Initialize I2C Module, return true when successful */
+  const static char LogLoc[] PROGMEM = "[Output:I2C:02_GP8403:Init]";
+  bool returnCode;
+  
+  /* Overwrite the default address of the library 0x58 with configured one */
+  GP8403[AddrId] = DFRobot_GP8403(Output_I2C_02_GP8403_Addr[AddrId]);
+
+  if(GP8403[AddrId].begin() == 0) {
+    /* Set output to 0-10V - this is what most grow devices are using as control standard */
+    GP8403[AddrId].setDACOutRange(GP8403[AddrId].eOutputRange10V);
+    Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Output_I2C_02_GP8403_Addr[AddrId]);
+    Output_I2C_02_GP8403_Update(AddrId, PortId, 0);
+    returnCode =  true;
+  } else {
+    Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Output_I2C_02_GP8403_Addr[AddrId]);
+    returnCode = false;
+  }
+  return returnCode;
+}
diff --git a/include/Sensor/00_Example.h b/include/Sensor/00_Example.h
new file mode 100644
index 0000000..4c18eee
--- /dev/null
+++ b/include/Sensor/00_Example.h
@@ -0,0 +1,46 @@
+/*
+ * 
+ * include/Sensor/00_Example.h - example sensor header I2C device
+ * 
+ * 
+ *
+ */
+
+#include <Adafruit_WhateverLib.h>
+
+#define SENSOR_00_NAME "Example sensor"
+
+const byte Sensor_00_Example_Addr[] = { 0x00, 0x01 };
+
+Adafruit_WhateverLib Whatever[sizeof(Sensor_00_Example_Addr)];
+
+/* Create main data array specifying max amount of readings */
+float Sensor_00_Example[sizeof(Sensor_00_Example_Addr)][4];
+
+void Sensor_00_Example_Update(const byte AddrId) {
+  /* keep the same order as in SensorIndex[].read[] !! */
+  Sensor_00_Example[AddrId][0] = Whatever[AddrId].temperature();
+  Sensor_00_Example[AddrId][1] = Whatever[AddrId].humidity();
+  Sensor_00_Example[AddrId][2] = Whatever[AddrId].raw1();
+  Sensor_00_Example[AddrId][3] = Whatever[AddrId].raw2();
+}
+
+bool Sensor_00_Example_Init(const byte AddrId) {
+  /* Sensor Init function
+   * 
+   * returns true (1) when Init was successful
+   * returns false (0) if not.
+   */  
+  const static char LogLoc[] PROGMEM = "[Sensor:00_Example:Init]";
+  bool returnCode;
+  
+  if(Whatever[AddrId].begin(Sensor_00_Example_Addr[AddrId])) {
+    Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Sensor_00_Example_Addr[AddrId]);
+    Sensor_00_Example_Update(AddrId);
+    returnCode = true;
+  } else {
+    Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_00_Example_Addr[AddrId]);
+    returnCode = false;
+  }
+  return returnCode;
+}
diff --git a/include/Sensor/01_ADC_builtin.h b/include/Sensor/01_ADC_builtin.h
new file mode 100644
index 0000000..68e1b43
--- /dev/null
+++ b/include/Sensor/01_ADC_builtin.h
@@ -0,0 +1,112 @@
+/*
+ * 
+ * include/Sensor/01_ADC_builtin.h - sensor header for builtin ADC 
+ * 
+ * 
+ * "Driver" for the internal ADC of the ESP8266 and ESP32
+ * 
+ * Bit dirty hacky workaround to support both boards ADC
+ * 
+ * ESP8266 only has one ADC onboard. For "Multiplexing" I have added
+ * the to "add a GPIO" to it. It simply turns on the given GPIO for 
+ * 100ms, and then turns it off. 
+ * This is kinda a poor (wo)mans multiplexer. Control the supply voltage
+ * of your analog sensor with the GPIO, put a diode to the AOUT and enjoy.
+ * 
+ * You can theoretically use all available pins as "multiplexer", thats why
+ * the Sensor_01_ADC[] array is as large as GPIOindex_length
+ * ************
+ * 
+ * ESP32 has a bunch of ADCs onboard. So it is not needed to go this hacky way,
+ * we can just use all the nice ADCs available.
+ * 
+ * in GPIOindex.note we get the info if the GPIO is an ADC or not 
+ * INT_ADC and INPUT_ONLY tells us this.
+ * To save memory, I build an own "index" for the available
+ * ADCs. Thats wahat Sensor_01_ADC_ArrId() is for.
+ * 
+ * It returns which Index / slot in the array the given GPIO ID
+ * from GPIOindex has.
+ */
+
+#define SENSOR_01_NAME "ADC builtin"
+
+/* Create main data array specifying max amount of readings */
+#ifdef ESP8266
+const byte SENSOR_01_MAX = GPIOindex_length + 1;
+#endif 
+
+#ifdef ESP32
+/* indexing function for our ADC GPIO pins we could theoretically all use */
+byte Sensor_01_ADC_ArrId(const byte GPIOid) {
+  const static char LogLoc[] PROGMEM = "[Sensor:01_ADC:Slot]";
+  byte count = 0;
+  //Log.verbose(F("%s GPIO %d (%d) START" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid);
+  for(byte i = 1; i <= GPIOindex_length; i++) {
+    //Log.verbose(F("%s GPIO %d (%d) NOTE %d - %S" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid, GPIOindex[i].note, GPIO_Index_note_descr[GPIOindex[i].note]);
+    if((GPIOindex[i].note == INPUT_ONLY) || (GPIOindex[i].note == INT_ADC)) {
+      if(i == GPIOid) {
+        //Log.verbose(F("%s ??? GPIO %d (%d) i %d" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid, i);
+        return count;
+      } else {
+        count++;
+        //Log.verbose(F("%s GPIO %d (%d) i %d count" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid, i, count);
+      }
+    }
+  }
+  return 0;
+}
+
+/* this dumb, but yeah - i counted the avail ADC manually (INT_ADC or INPUT_ONLY) */
+const byte SENSOR_01_MAX = 6;
+#endif
+
+int Sensor_01_ADC[SENSOR_01_MAX];
+
+void Sensor_01_ADC_Update(const byte GPIOid) {
+  const static char LogLoc[] PROGMEM = "[Sensor:01_ADC:Update]";
+  #ifdef ESP8266
+  if(GPIOid > 0) {
+    //digitalWrite(GPIOindex[GPIOid].gpio, HIGH);
+    //Log.notice(F("%s GPIO %d (%d) delay ON" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid);
+    digitalWrite(GPIOindex[GPIOid].gpio, HIGH);
+    delay(50);
+    Sensor_01_ADC[GPIOid] = analogRead(A0);
+    digitalWrite(GPIOindex[GPIOid].gpio, LOW);
+    //Log.notice(F("%s GPIO %d (%d) delay OFF" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid);
+  } else {
+    //Log.notice(F("%s GPIO %d (%d) READ" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid);
+    Sensor_01_ADC[GPIOid] = analogRead(A0);
+   }
+  #endif
+  
+  #ifdef ESP32
+  byte slot = Sensor_01_ADC_ArrId(GPIOid);
+  //Log.notice(F("%s GPIO %d (%d) READ - slot %d" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid, slot);
+  Sensor_01_ADC[slot] = analogRead(GPIOindex[GPIOid].gpio);
+  //Log.notice(F("%s GPIO %d (%d) READ - slot %d Val %d" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid, slot, Sensor_01_ADC[slot]);
+  #endif
+  
+}
+
+bool Sensor_01_ADC_Init(const byte GPIOid) {
+  /* Sensor Init function
+   * 
+   * returns true (1) when Init was successful
+   * returns false (0) if not.
+   */
+  const static char LogLoc[] PROGMEM = "[Sensor:01_ADC:Init]";
+  
+  #ifdef ESP8266
+  //Log.notice(F("%s setting GPIO ID %d (%d) as OUTPUT for internal ADC" CR), LogLoc, GPIOid, GPIOindex[GPIOid].gpio);
+  pinMode(GPIOindex[GPIOid].gpio, OUTPUT);
+  #endif
+  
+  #ifdef ESP32
+  //Log.notice(F("%s GPIO ID %d (%d) as INPUT for internal ADC slot %d" CR), LogLoc, GPIOid, GPIOindex[GPIOid].gpio, Sensor_01_ADC_ArrId(GPIOid));
+  //pinMode(GPIOindex[GPIOid].gpio, INPUT);
+  #endif
+  
+  Sensor_01_ADC_Update(GPIOid);
+  return true;
+}
diff --git a/include/Sensor/02_BME280.h b/include/Sensor/02_BME280.h
new file mode 100644
index 0000000..0b72163
--- /dev/null
+++ b/include/Sensor/02_BME280.h
@@ -0,0 +1,54 @@
+/*
+ * 
+ * include/Sensor/00_ADC_builtin.h - sensor header for BME280 I2C sensor
+ * 
+ * 
+ *
+ */
+
+
+#include <Adafruit_BME280.h>
+
+
+#define SENSOR_02_NAME "BME280"
+//#define SENSOR_02_MAXUNITS 2
+    
+/* available addresses in byte array, default is at 0 */ 
+const byte Sensor_02_BME280_Addr[] = { 0x76, 0x77 };
+
+Adafruit_BME280 BME280[sizeof(Sensor_02_BME280_Addr)];
+
+/* creation of BME280 Value Struct, as many as addresses */
+//Sensor_02_BME280 Sensor_02_BME280_Data[sizeof(Sensor_02_BME280_Addr)];
+
+/* main data array */
+float Sensor_02_BME280[sizeof(Sensor_02_BME280_Addr)][4];
+
+void Sensor_02_BME280_Update(const byte AddrId) {
+  /* Temp */
+  Sensor_02_BME280[AddrId][0] = BME280[AddrId].readTemperature();
+  /* Humidity */
+  Sensor_02_BME280[AddrId][1] = BME280[AddrId].readHumidity();
+  /* Pressure */
+  Sensor_02_BME280[AddrId][2] = BME280[AddrId].readPressure() / 1000.00;
+  /* Altitude */
+  Sensor_02_BME280[AddrId][3] = BME280[AddrId].readAltitude(SEALEVELPRESSURE_HPA);
+}
+
+
+bool Sensor_02_BME280_Init(const byte AddrId) {
+  const static char LogLoc[] PROGMEM = "[Sensor:02_BME280:Init]";
+  bool returnCode;
+  //Log.notice(F("%s Init at addr 0x%x (%d)" CR), LogLoc, Sensor_02_BME280_Addr[AddrId], AddrId);
+  if(BME280[AddrId].begin(Sensor_02_BME280_Addr[AddrId])) {
+    Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Sensor_02_BME280_Addr[AddrId]);
+    //Log.notice(F("%s Temp: %F°C Humidity: %F % Pressure: %FhPa, Appr. Altitude %Fm" CR), LogLoc, BME280[AddrId].readTemperature(), BME280[AddrId].readHumidity(), BME280[AddrId].readPressure() / 1000.00, BME280[AddrId].readAltitude(SEALEVELPRESSURE_HPA));
+    Sensor_02_BME280_Update(AddrId);
+    returnCode = true;
+  } else {
+    Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_02_BME280_Addr[AddrId]);
+    returnCode = false;
+  }
+  
+  return returnCode;
+}
diff --git a/include/Sensor/03_BME680.h b/include/Sensor/03_BME680.h
new file mode 100644
index 0000000..eedf416
--- /dev/null
+++ b/include/Sensor/03_BME680.h
@@ -0,0 +1,98 @@
+/*
+ * 
+ * include/Sensor/00_ADC_builtin.h - sensor header for BME680 I2C sensor
+ * 
+ * 
+ *
+ */
+
+
+#include <Adafruit_BME680.h>
+
+#define SENSOR_03_NAME "BME680"
+
+/* available addresses in byte array, default is at 0 */ 
+const byte Sensor_03_BME680_Addr[] = { 0x77, 0x76 };
+
+Adafruit_BME680 BME680[sizeof(Sensor_03_BME680_Addr)];
+
+unsigned long BME680_endtime[sizeof(Sensor_03_BME680_Addr)];
+
+/*struct Sensor_03_BME680 {
+  float humidity;
+  float temperature;
+  float pressure;
+  float altitude;
+  float gas_resistance;
+};
+*/
+/* creation of BME680 Value Struct, as many as addresses */
+/*Sensor_03_BME680 Sensor_03_BME680_Data[sizeof(Sensor_03_BME680_Addr)];*/
+
+float Sensor_03_BME680[sizeof(Sensor_03_BME680_Addr)][5];
+
+/* for async read of BME680 we need to trigger a new reading cycle (as adafruit doc says) */
+void Sensor_03_BME680_BeginReading(const byte AddrId) {
+  const static char LogLoc[] PROGMEM = "[Sensor:03_BME680:BeginReading]";
+  
+  #ifdef DEBUG3
+  Log.warning(F("%s Start reading %u , finishing %u (0x%x)" CR), LogLoc, millis(), BME680_endtime[AddrId], Sensor_03_BME680_Addr[AddrId]);
+  #endif
+  
+  // Tell BME680 to begin measurement.
+  BME680_endtime[AddrId] = BME680[AddrId].beginReading();
+  if(BME680_endtime[AddrId] == 0) {
+    Log.warning(F("%s Failed to begin reading (0x%x)" CR), LogLoc, Sensor_03_BME680_Addr[AddrId]);
+  }
+}
+
+void Sensor_03_BME680_Update(const byte AddrId) {
+  const static char LogLoc[] PROGMEM = "[Sensor:03_BME680:Update]";
+  
+  #ifdef DEBUG3
+  Log.warning(F("%s Start reading %u , finishing %u (0x%x)" CR), LogLoc, millis(), BME680_endtime[AddrId], Sensor_03_BME680_Addr[AddrId]);
+  #endif
+  
+  if(!BME680[AddrId].endReading()) {
+    Log.warning(F("%s Failed to complete reading (0x%x)" CR), LogLoc, Sensor_03_BME680_Addr[AddrId]);
+    return;
+  }
+  
+  Sensor_03_BME680[AddrId][0] = BME680[AddrId].readTemperature();
+  Sensor_03_BME680[AddrId][1] = BME680[AddrId].readHumidity();
+  Sensor_03_BME680[AddrId][2] = BME680[AddrId].readPressure() / 1000;
+  Sensor_03_BME680[AddrId][3] = BME680[AddrId].readAltitude(SEALEVELPRESSURE_HPA);
+  Sensor_03_BME680[AddrId][4] = BME680[AddrId].gas_resistance / 1000.0;
+  
+  /* begin new reading cycle */
+  Sensor_03_BME680_BeginReading(AddrId);
+  
+}
+
+bool Sensor_03_BME680_Init(const byte AddrId) {
+  const static char LogLoc[] PROGMEM = "[Sensor:03_BME680:Init]";
+  bool returnCode;
+  //Log.notice(F("%s Init at addr 0x%x (%d)" CR), LogLoc, Sensor_03_BME680_Addr[AddrId], AddrId);
+  if(BME680[AddrId].begin(Sensor_03_BME680_Addr[AddrId])) {
+    Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Sensor_03_BME680_Addr[AddrId]);
+    
+    // Set up oversampling and filter initialization
+    BME680[AddrId].setTemperatureOversampling(BME680_OS_8X);
+    BME680[AddrId].setHumidityOversampling(BME680_OS_2X);
+    BME680[AddrId].setPressureOversampling(BME680_OS_4X);
+    BME680[AddrId].setIIRFilterSize(BME680_FILTER_SIZE_3);
+    BME680[AddrId].setGasHeater(320, 150); // 320*C for 150 ms
+    
+    /* start to do readings here, like shown in async example
+     * https://github.com/adafruit/Adafruit_BME680/blob/master/examples/bme680async/bme680async.ino */
+    Sensor_03_BME680_BeginReading(AddrId);
+    
+    Sensor_03_BME680_Update(AddrId);
+    returnCode = true;
+  } else {
+    Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_03_BME680_Addr[AddrId]);
+    returnCode = false;
+  } 
+  return returnCode;
+}
+
diff --git a/include/Sensor/04_SHT3x.h b/include/Sensor/04_SHT3x.h
new file mode 100644
index 0000000..c9dea61
--- /dev/null
+++ b/include/Sensor/04_SHT3x.h
@@ -0,0 +1,43 @@
+/*
+ * 
+ * include/Sensor/04_SHT3X.h - SHT3X I2C temp/humidity sensor 
+ * 
+ * 
+ *
+ */
+
+#include <Adafruit_SHT31.h>
+
+#define SENSOR_04_NAME "SHT3x"
+
+const byte Sensor_04_SHT3X_Addr[] = { 0x44, 0x45 };
+
+Adafruit_SHT31 SHT3X[sizeof(Sensor_04_SHT3X_Addr)];
+
+/* Create main data array specifying max amount of readings */
+float Sensor_04_SHT3X[sizeof(Sensor_04_SHT3X_Addr)][2];
+
+void Sensor_04_SHT3X_Update(const byte AddrId) {
+  Sensor_04_SHT3X[AddrId][0] = SHT3X[AddrId].readTemperature();
+  Sensor_04_SHT3X[AddrId][1] = SHT3X[AddrId].readHumidity();
+
+}
+
+bool Sensor_04_SHT3X_Init(const byte AddrId) {
+  /* Sensor Init function
+   * 
+   * returns true (1) when Init was successful
+   * returns false (0) if not.
+   */  
+  const static char LogLoc[] PROGMEM = "[Sensor:04_SHT3X:Init]";
+  bool returnCode;
+  if(SHT3X[AddrId].begin(Sensor_04_SHT3X_Addr[AddrId])) {
+    Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Sensor_04_SHT3X_Addr[AddrId]);
+    Sensor_04_SHT3X_Update(AddrId);
+    returnCode = true;
+  } else {
+    Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_04_SHT3X_Addr[AddrId]);
+    returnCode = false;
+  }
+  return returnCode;
+}
diff --git a/include/Sensor/05_MLX90614.h b/include/Sensor/05_MLX90614.h
new file mode 100644
index 0000000..dac5709
--- /dev/null
+++ b/include/Sensor/05_MLX90614.h
@@ -0,0 +1,44 @@
+/*
+ * 
+ * include/Sensor/05_MLX90614.h - MLX90614 I2C IR temp sensor 
+ * 
+ * 
+ *
+ */
+
+#include <Adafruit_MLX90614.h>
+
+#define SENSOR_05_NAME "MLX90614"
+
+const byte Sensor_05_MLX90614_Addr[] = { 0x5A, 0x5B, 0x5C, 0x5D };
+
+Adafruit_MLX90614 MLX90614[sizeof(Sensor_05_MLX90614_Addr)];
+
+/* Create main data array specifying max amount of readings */
+float Sensor_05_MLX90614[sizeof(Sensor_05_MLX90614_Addr)][2];
+
+void Sensor_05_MLX90614_Update(const byte AddrId) {
+  /* keep the same order as in SensorIndex[].read[] !! */
+  Sensor_05_MLX90614[AddrId][0] = MLX90614[AddrId].readAmbientTempC();
+  Sensor_05_MLX90614[AddrId][1] = MLX90614[AddrId].readObjectTempC();
+}
+
+bool Sensor_05_MLX90614_Init(const byte AddrId) {
+  /* Sensor Init function
+   * 
+   * returns true (1) when Init was successful
+   * returns false (0) if not.
+   */  
+  const static char LogLoc[] PROGMEM = "[Sensor:05_MLX90614:Init]";
+  bool returnCode;
+  
+  if(MLX90614[AddrId].begin(Sensor_05_MLX90614_Addr[AddrId])) {
+    Log.notice(F("%s found at addr 0x%x - emissivity set to %F" CR), LogLoc, Sensor_05_MLX90614_Addr[AddrId], MLX90614[AddrId].readEmissivity());
+    Sensor_05_MLX90614_Update(AddrId);
+    returnCode = true;
+  } else {
+    Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_05_MLX90614_Addr[AddrId]);
+    returnCode = false;
+  }
+  return returnCode;
+}
diff --git a/include/Sensor/06_TCS34725.h b/include/Sensor/06_TCS34725.h
new file mode 100644
index 0000000..10dc0c5
--- /dev/null
+++ b/include/Sensor/06_TCS34725.h
@@ -0,0 +1,72 @@
+/*
+ * 
+ * include/Sensor/06_TCS34725.h - header for I2C color sensor TCS34725
+ * 
+ * 
+ *
+ */
+
+//#include "TCS34725.h"
+
+#include "Adafruit_TCS34725.h"
+
+#define SENSOR_06_NAME "TCS34725"
+
+const byte Sensor_06_TCS34725_Addr[] = { 0x29 };
+
+Adafruit_TCS34725 TCS34725[sizeof(Sensor_06_TCS34725_Addr)];
+/* This library causes a 240ms (or greater if chosen) delay when reading the values from the sensor 
+ * this is not optimal, and there are libs workarounding this behaviour. 
+ * But unfortunatelly the other libs wont connect successful by i2c to the sensor,
+ * which only the adafruit lib does reliably 240MS integration time and 4x gain 
+ * seems to be the sweet spot between delay and value resolutin */
+
+
+/* Create main data array specifying max amount of readings */
+float Sensor_06_TCS34725[sizeof(Sensor_06_TCS34725_Addr)][5];
+
+void Sensor_06_TCS34725_Update(const byte AddrId) {
+  uint16_t r, g, b, c, colorTemp, lux;
+
+  TCS34725[AddrId].getRawData(&r, &g, &b, &c);
+  colorTemp = TCS34725[AddrId].calculateColorTemperature_dn40(r, g, b, c);
+  lux = TCS34725[AddrId].calculateLux(r, g, b);
+  Sensor_06_TCS34725[AddrId][0] = colorTemp;
+  Sensor_06_TCS34725[AddrId][1] = lux;
+  Sensor_06_TCS34725[AddrId][2] = r;
+  Sensor_06_TCS34725[AddrId][3] = g;
+  Sensor_06_TCS34725[AddrId][4] = b;
+
+}
+
+bool Sensor_06_TCS34725_Init(const byte AddrId) {
+  /* Sensor Init function
+   * 
+   * returns true (1) when Init was successful
+   * returns false (0) if not.
+   */  
+  const static char LogLoc[] PROGMEM = "[Sensor:06_TCS34725:Init]";
+  bool returnCode;
+  
+  if(TCS34725[AddrId].begin()) {
+    /* Here I hardcoded here the values for Integration time and Gain. 
+     * For calibration I used my desk lamp and a lux smartphone app. 
+     * I fooled around until the smartphone app reading was kinda the
+     * same as the TCS34725 ones. Yay! 
+     * 
+     * Comes out TCS34725_INTEGRATIONTIME_240MS and TCS34725_GAIN_16X 
+     * seem to be good values. Smartphone reading of my desk lamp is 
+     * 3507lx and on the exakt same spot, height, angle and so on the 
+     * TCS34725 measures 3487lx. I guess this is fine. */
+    
+    Log.notice(F("%s found at addr 0x%x - Integration time: 240ms Gain: 16x" CR), LogLoc, Sensor_06_TCS34725_Addr[AddrId]);
+    TCS34725[AddrId].setIntegrationTime(TCS34725_INTEGRATIONTIME_240MS);
+    TCS34725[AddrId].setGain(TCS34725_GAIN_16X);
+    Sensor_06_TCS34725_Update(AddrId);
+    returnCode = true;
+  } else {
+    Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_06_TCS34725_Addr[AddrId]);
+    returnCode = false;
+  }
+  return returnCode;
+}
diff --git a/include/Sensor/07_ADS1115.h b/include/Sensor/07_ADS1115.h
new file mode 100644
index 0000000..f63f353
--- /dev/null
+++ b/include/Sensor/07_ADS1115.h
@@ -0,0 +1,38 @@
+/*
+ * 
+ * include/Sensor/07_ADS1115.h - ADS1115 16 bit ADC I2C driver  
+ * 
+ * 
+ *
+ */
+
+#include <Adafruit_ADS1X15.h>
+
+#define SENSOR_07_NAME "ADS1115"
+
+
+const byte Sensor_07_ADS1115_Addr[] = { 0x48, 0x49, 0x4A, 0x4B };
+
+Adafruit_ADS1115 ADS1115[sizeof(Sensor_07_ADS1115_Addr)];
+
+int Sensor_07_ADS1115[sizeof(Sensor_07_ADS1115_Addr)][4];
+
+void Sensor_07_ADS1115_Update(const byte AddrId) {
+  for(byte i = 0; i < 4; i++) {
+    Sensor_07_ADS1115[AddrId][i] = ADS1115[AddrId].readADC_SingleEnded(i);
+  }
+}
+
+bool Sensor_07_ADS1115_Init(const byte AddrId) {
+  const static char LogLoc[] PROGMEM = "[Sensor:07_ADS1115:Init]";
+  bool returnCode;
+  if(ADS1115[AddrId].begin(Sensor_07_ADS1115_Addr[AddrId])) {
+    Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Sensor_07_ADS1115_Addr[AddrId]);
+    Sensor_07_ADS1115_Update(AddrId);
+    returnCode = true;
+  } else {
+    Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_07_ADS1115_Addr[AddrId]);
+    returnCode = false;
+  }
+  return returnCode;
+}
diff --git a/include/Sensor/08_ADS1015.h b/include/Sensor/08_ADS1015.h
new file mode 100644
index 0000000..1ed6f03
--- /dev/null
+++ b/include/Sensor/08_ADS1015.h
@@ -0,0 +1,41 @@
+/*
+ * 
+ * include/Sensor/08_ADS1015.h - ADS1115 16 bit ADC I2C driver  
+ * 
+ * 
+ *
+ */
+
+/* 
+ * #include <Adafruit_ADS1X15.h>
+ * This already got included in Sensor_07_ADS1115.h
+ */
+
+#define SENSOR_08_NAME "ADS1015"
+
+
+const byte Sensor_08_ADS1015_Addr[] = { 0x48, 0x49, 0x4A, 0x4B };
+
+Adafruit_ADS1015 ADS1015[sizeof(Sensor_08_ADS1015_Addr)];
+
+int Sensor_08_ADS1015[sizeof(Sensor_08_ADS1015_Addr)][4];
+
+void Sensor_08_ADS1015_Update(const byte AddrId) {
+  for(byte i = 0; i < 4; i++) {
+    Sensor_08_ADS1015[AddrId][i] = ADS1015[AddrId].readADC_SingleEnded(i);
+  }
+}
+
+bool Sensor_08_ADS1015_Init(const byte AddrId) {
+  const static char LogLoc[] PROGMEM = "[Sensor:08_ADS1015:Init]";
+  bool returnCode;
+  if(ADS1015[AddrId].begin(Sensor_08_ADS1015_Addr[AddrId])) {
+    Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Sensor_08_ADS1015_Addr[AddrId]);
+    Sensor_08_ADS1015_Update(AddrId);
+    returnCode = true;
+  } else {
+    Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_08_ADS1015_Addr[AddrId]);
+    returnCode = false;
+  }
+  return returnCode;
+}
diff --git a/include/Sensor/09_Chirp.h b/include/Sensor/09_Chirp.h
new file mode 100644
index 0000000..7499d68
--- /dev/null
+++ b/include/Sensor/09_Chirp.h
@@ -0,0 +1,88 @@
+/*
+ * 
+ * include/Sensor/09_Chirp.h - example sensor header I2C device
+ * 
+ * 
+ *
+ */
+
+#include <I2CSoilMoistureSensor.h>
+
+#define SENSOR_09_NAME "I2C-Chirp"
+
+const byte Sensor_09_Chirp_Addr[] = { 0x20, 0x21, 0x22, 0x23 };
+
+I2CSoilMoistureSensor Chirp[sizeof(Sensor_09_Chirp_Addr)];
+
+/* Create main data array specifying max amount of readings */
+float Sensor_09_Chirp[sizeof(Sensor_09_Chirp_Addr)][3];
+
+void Sensor_09_Chirp_Update(const byte AddrId) {
+  const static char LogLoc[] PROGMEM = "[Sensor:09_Chirp:Update]";
+  
+  #ifdef DEBUG
+  unsigned long mStart = millis();
+  unsigned long mStop;
+  Log.verbose(F("%s Start %u" CR), LogLoc, mStart);
+  #endif
+  
+  /* keep the same order as in SensorIndex[].read[] !! */
+  
+  #ifdef DEBUG
+  Log.verbose(F("%s capacitance (%u)" CR), LogLoc, millis());
+  #endif
+  Sensor_09_Chirp[AddrId][0] = Chirp[AddrId].getCapacitance();
+  
+  #ifdef DEBUG
+  Log.verbose(F("%s temperature (%u)" CR), LogLoc, millis());
+  #endif
+  Sensor_09_Chirp[AddrId][1] = Chirp[AddrId].getTemperature()/(float)10;
+  
+  /* light sensor is disabled, because it takes 3s to read, which is just too much */
+  //#ifndef DEBUG
+  //Log.verbose(F("%s light (%u)" CR), LogLoc, millis());
+  //#endif
+  //Sensor_09_Chirp[AddrId][2] = Chirp[AddrId].getLight(true);
+  
+  Chirp[AddrId].sleep();
+  
+  
+  #ifdef DEBUG
+  mStop = millis();
+  Log.verbose(F("%s Stop %u (%u)" CR), LogLoc, mStop, mStop - mStart);
+  #endif
+}
+
+bool Sensor_09_Chirp_Init(const byte AddrId) {
+  /* Sensor Init function
+   * 
+   * returns true (1) when Init was successful
+   * returns false (0) if not.
+   */  
+  const static char LogLoc[] PROGMEM = "[Sensor:09_Chirp:Init]";
+  bool returnCode;
+  
+  /* manually check if I2C address answers on bus, i2c chirp lib does not return a value */ 
+  Wire.beginTransmission(Sensor_09_Chirp_Addr[AddrId]);
+  short i2cError = Wire.endTransmission();
+  
+  /* when i2c sensor answered to our previous init request */ 
+  if(i2cError == 0) {
+    Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Sensor_09_Chirp_Addr[AddrId]);
+    
+    #ifdef ESP8266
+    /* maybe its not the best idea to place it here, but for the moment.. */
+    Wire.setClockStretchLimit(2500);
+    #endif
+    
+    /* change chirp library I2C address, it will also trigger .begin() afterwards */
+    Chirp[AddrId].changeSensor(Sensor_09_Chirp_Addr[AddrId], false);
+    
+    Sensor_09_Chirp_Update(AddrId);
+    returnCode = true;
+  } else {
+    Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_09_Chirp_Addr[AddrId]);
+    returnCode = false;
+  }
+  return returnCode;
+}
diff --git a/include/Sensor/10_CCS811.h b/include/Sensor/10_CCS811.h
new file mode 100644
index 0000000..11df5d9
--- /dev/null
+++ b/include/Sensor/10_CCS811.h
@@ -0,0 +1,58 @@
+/*
+ * 
+ * include/Sensor/10_CCS811_.h - CCS811 CO2 I2C sensor 
+ * 
+ * 
+ *
+ */
+
+#include "Adafruit_CCS811.h"
+
+#define SENSOR_10_NAME "CCS811"
+
+const byte Sensor_10_CCS811_Addr[] = { 0x5a, 0x5b };
+
+Adafruit_CCS811 CCS811[sizeof(Sensor_10_CCS811_Addr)];
+
+/* Create main data array specifying max amount of readings */
+float Sensor_10_CCS811[sizeof(Sensor_10_CCS811_Addr)][4];
+
+void Sensor_10_CCS811_Update(const byte AddrId) {
+  const static char LogLoc[] PROGMEM = "[Sensor:10_CCS811:Update]";
+  if(CCS811[AddrId].available()){
+    if(!CCS811[AddrId].readData()){
+      /* keep the same order as in SensorIndex[].read[] !! */
+      /* CO2 in ppm */
+      Sensor_10_CCS811[AddrId][0] = CCS811[AddrId].geteCO2();
+      /* TVOC (Total Volatile Organic Compouds) */
+      Sensor_10_CCS811[AddrId][1] = CCS811[AddrId].getTVOC();
+    }
+    #ifndef DEBUG
+    else {
+      Log.error(F("%s 0x%x ERROR getting new data" CR), LogLoc, Sensor_10_CCS811_Addr[AddrId]);
+    }
+    #endif
+  }
+
+  
+}
+
+bool Sensor_10_CCS811_Init(const byte AddrId) {
+  /* Sensor Init function
+   * 
+   * returns true (1) when Init was successful
+   * returns false (0) if not.
+   */  
+  const static char LogLoc[] PROGMEM = "[Sensor:10_CCS811:Init]";
+  bool returnCode;
+  
+  if(CCS811[AddrId].begin(Sensor_10_CCS811_Addr[AddrId])) {
+    Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Sensor_10_CCS811_Addr[AddrId]);
+    Sensor_10_CCS811_Update(AddrId);
+    returnCode = true;
+  } else {
+    Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_10_CCS811_Addr[AddrId]);
+    returnCode = false;
+  }
+  return returnCode;
+}
diff --git a/include/Sensor/Sensor_Common.h b/include/Sensor/Sensor_Common.h
new file mode 100644
index 0000000..7998335
--- /dev/null
+++ b/include/Sensor/Sensor_Common.h
@@ -0,0 +1,152 @@
+/*
+ * 
+ * include/Sensor/Common.h - common sensor header file
+ * 
+ * 
+ *
+ */
+
+
+
+/*
+ * Common used consts and variables, used within the Sensor header for example
+ */
+
+// for bme280 and bme680
+#define SEALEVELPRESSURE_HPA (1013.25)
+
+/* sensor types, int ADC, i2c, one wire , two wire, ...*/
+const byte SENSOR_TYPE__TOTAL = 5;
+
+const byte SENSOR_TYPE_INTADC = 0;
+const byte SENSOR_TYPE_I2C = 1;
+const byte SENSOR_TYPE_ONEWIRE = 2;
+const byte SENSOR_TYPE_TWOWIRE = 3;
+const byte SENSOR_TYPE_I2C_WITH_GPIO = 4;
+
+/* How many different read types exists */
+const byte SENSOR_READ_TYPE__TOTAL = 14;
+
+
+const byte SENSOR_READ_TYPE_RAW = 1;
+const char SENSOR_READ_TYPE_RAW_descr[] PROGMEM = {"Raw value"};
+const char SENSOR_READ_TYPE_RAW_unit[] PROGMEM = {""};
+
+const byte SENSOR_READ_TYPE_TEMP = 2;
+const char SENSOR_READ_TYPE_TEMP_descr[] PROGMEM = {"Temperature"};
+const char SENSOR_READ_TYPE_TEMP_unit[] PROGMEM = {"°C"};
+
+const byte SENSOR_READ_TYPE_HUMIDITY = 3;
+const char SENSOR_READ_TYPE_HUMIDITY_descr[] PROGMEM = {"Humidity"};
+const char SENSOR_READ_TYPE_HUMIDITY_unit[] PROGMEM = {"%"};
+
+const byte SENSOR_READ_TYPE_SOILMOISTURE = 4;
+const char SENSOR_READ_TYPE_SOILMOISTURE_descr[] PROGMEM = {"Moisture"};
+const char SENSOR_READ_TYPE_SOILMOISTURE_unit[] PROGMEM = {"%"};
+
+const byte SENSOR_READ_TYPE_PRESSURE = 5;
+const char SENSOR_READ_TYPE_PRESSURE_descr[] PROGMEM = {"Pressure"};
+const char SENSOR_READ_TYPE_PRESSURE_unit[] PROGMEM = {"Pa"};
+
+const byte SENSOR_READ_TYPE_ALTITUDE = 6;
+const char SENSOR_READ_TYPE_ALTITUDE_descr[] PROGMEM = {"Altitude"};
+const char SENSOR_READ_TYPE_ALTITUDE_unit[] PROGMEM = {"m"};
+
+const byte SENSOR_READ_TYPE_GAS_RESISTANCE = 7;
+const char SENSOR_READ_TYPE_GAS_RESISTANCE_descr[] PROGMEM = {"Gas resistance"};
+const char SENSOR_READ_TYPE_GAS_RESISTANCE_unit[] PROGMEM = {"KOhm"};
+
+const byte SENSOR_READ_TYPE_COLOR_TEMP = 8;
+const char SENSOR_READ_TYPE_COLOR_TEMP_descr[] PROGMEM = {"Color temperature"};
+const char SENSOR_READ_TYPE_COLOR_TEMP_unit[] PROGMEM = {"K"};
+
+const byte SENSOR_READ_TYPE_LUX = 9;
+const char SENSOR_READ_TYPE_LUX_descr[] PROGMEM = {"Lux"};
+const char SENSOR_READ_TYPE_LUX_unit[] PROGMEM = {"lx"};
+
+const byte SENSOR_READ_TYPE_COLOR_RED = 10;
+const char SENSOR_READ_TYPE_COLOR_RED_descr[] PROGMEM = {"Color red"};
+const char SENSOR_READ_TYPE_COLOR_RED_unit[] PROGMEM = {""};
+
+const byte SENSOR_READ_TYPE_COLOR_GREEN = 11;
+const char SENSOR_READ_TYPE_COLOR_GREEN_descr[] PROGMEM = {"Color green"};
+const char SENSOR_READ_TYPE_COLOR_GREEN_unit[] PROGMEM = {""};
+
+const byte SENSOR_READ_TYPE_COLOR_BLUE = 12;
+const char SENSOR_READ_TYPE_COLOR_BLUE_descr[] PROGMEM = {"Color blue"};
+const char SENSOR_READ_TYPE_COLOR_BLUE_unit[] PROGMEM = {""};
+
+const byte SENSOR_READ_TYPE_PARTS_PER_MILLION = 13;
+const char SENSOR_READ_TYPE_PARTS_PER_MILLION_descr[] PROGMEM = {"Part per million"};
+const char SENSOR_READ_TYPE_PARTS_PER_MILLION_unit[] PROGMEM = {"ppm"};
+
+const byte SENSOR_READ_TYPE_TVOC = 14;
+const char SENSOR_READ_TYPE_TVOC_descr[] PROGMEM = {"TVOC"};
+const char SENSOR_READ_TYPE_TVOC_unit[] PROGMEM = {""};
+
+
+const char * Sensor_Read_descr[] = {
+  NULL, // 0 is unset
+  SENSOR_READ_TYPE_RAW_descr,
+  SENSOR_READ_TYPE_TEMP_descr,
+  SENSOR_READ_TYPE_HUMIDITY_descr,
+  SENSOR_READ_TYPE_SOILMOISTURE_descr,
+  SENSOR_READ_TYPE_PRESSURE_descr,
+  SENSOR_READ_TYPE_ALTITUDE_descr,
+  SENSOR_READ_TYPE_GAS_RESISTANCE_descr,
+  SENSOR_READ_TYPE_COLOR_TEMP_descr,
+  SENSOR_READ_TYPE_LUX_descr,
+  SENSOR_READ_TYPE_COLOR_RED_descr,
+  SENSOR_READ_TYPE_COLOR_GREEN_descr,
+  SENSOR_READ_TYPE_COLOR_BLUE_descr,
+  SENSOR_READ_TYPE_PARTS_PER_MILLION_descr,
+  SENSOR_READ_TYPE_TVOC_descr
+};
+
+const char * Sensor_Read_unit[] = {
+  NULL, // 0 is unset
+  SENSOR_READ_TYPE_RAW_unit,
+  SENSOR_READ_TYPE_TEMP_unit,
+  SENSOR_READ_TYPE_HUMIDITY_unit,
+  SENSOR_READ_TYPE_SOILMOISTURE_unit,
+  SENSOR_READ_TYPE_PRESSURE_unit,
+  SENSOR_READ_TYPE_ALTITUDE_unit,
+  SENSOR_READ_TYPE_GAS_RESISTANCE_unit,
+  SENSOR_READ_TYPE_COLOR_TEMP_unit,
+  SENSOR_READ_TYPE_LUX_unit,
+  SENSOR_READ_TYPE_COLOR_RED_unit,
+  SENSOR_READ_TYPE_COLOR_GREEN_unit,
+  SENSOR_READ_TYPE_COLOR_BLUE_unit,
+  SENSOR_READ_TYPE_PARTS_PER_MILLION_unit,
+  SENSOR_READ_TYPE_TVOC_unit
+};
+
+
+/* How many different read convert types exists */
+const byte SENSOR_CONVERT_RAW_TYPE__TOTAL = 1;
+
+const byte SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE = 1;
+const char SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE_descr[] PROGMEM = {"Soilmoisture"};
+const char SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE_unit[] PROGMEM = {"%"};
+
+//const byte SENSOR_CONVERT_RAW_TYPE_OTHER = 2;
+//const char SENSOR_CONVERT_RAW_TYPE_OTHER_descr[] PROGMEM = {"Other"};
+//const char SENSOR_CONVERT_RAW_TYPE_OTHER_unit[] PROGMEM = {"n/a"};
+
+
+const char * Sensor_Convert_Raw_descr[] = {
+  NULL, // 0 is unset
+  SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE_descr,
+  //SENSOR_CONVERT_RAW_TYPE_OTHER_descr
+};
+
+const char * Sensor_Convert_Raw_unit[] = {
+  NULL, // 0 is unset
+  SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE_unit,
+  //SENSOR_CONVERT_RAW_TYPE_OTHER_unit
+};
+
+// Addr_Init_Update modes
+const byte SENSOR_AIU_MODE_ADDR = 0;
+const byte SENSOR_AIU_MODE_INIT = 1;
+const byte SENSOR_AIU_MODE_UPDATE = 2;
diff --git a/include/Webserver/Api_sensor.h b/include/Webserver/Api_sensor.h
new file mode 100644
index 0000000..e7a153b
--- /dev/null
+++ b/include/Webserver/Api_sensor.h
@@ -0,0 +1,124 @@
+/*
+ * 
+ * include/Webserver/Api_Sensor.h - Sensor API header file
+ * 
+ * 
+ *
+ */
+
+void Api_sensor_data(AsyncWebServerRequest* request) {
+  AsyncJsonResponse* response = new AsyncJsonResponse();
+  JsonObject root = response->getRoot().to<JsonObject>();
+  //root["hello"] = "world";
+  for(byte i = 0 ; i < Max_Sensors ; i++) {
+    if(config.system.sensor.type[i] > 0) {
+
+      JsonObject objSensor = root["sensor"].add<JsonObject>();
+      objSensor["id"] = i;
+      objSensor["name"] = config.system.sensor.name[i];
+      objSensor["type"] = SensorIndex[config.system.sensor.type[i]].name;
+      objSensor["status"] = sensorStatus[i];
+      if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_I2C)
+        objSensor["i2c_addr"] = "0x" + String(Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.i2c_addr[i], SENSOR_AIU_MODE_ADDR), HEX);
+      if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_INTADC)
+        objSensor["gpio"] = GPIOindex[config.system.sensor.gpio[i][0]].gpio;
+      
+      for(byte j = 0; j < Max_Sensors_Read; j++) {
+        if(SensorIndex[config.system.sensor.type[i]].read[j] > 0) {
+          
+          JsonObject objReading = objSensor["reading"].add<JsonObject>();
+          
+          /* when for a RAW reading rawConvert is set, return the converted description and unit */
+          if((SensorIndex[config.system.sensor.type[i]].read[j] == SENSOR_READ_TYPE_RAW) && (config.system.sensor.rawConvert[i][j] > 0)) {
+            objReading["descr"] = FPSTR(Sensor_Convert_Raw_descr[config.system.sensor.rawConvert[i][j]]);
+            objReading["unit"] = FPSTR(Sensor_Convert_Raw_unit[config.system.sensor.rawConvert[i][j]]);
+          } else {
+            objReading["descr"] = FPSTR(Sensor_Read_descr[SensorIndex[config.system.sensor.type[i]].read[j]]);
+            objReading["unit"] = FPSTR(Sensor_Read_unit[SensorIndex[config.system.sensor.type[i]].read[j]]);
+          }
+          
+          /* read RAW values
+             when internal ADC */
+          if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_INTADC) {
+            objReading["raw"] = Sensor_getValue( config.system.sensor.type[i], config.system.sensor.gpio[i][0]);
+          } else if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_I2C) {
+            objReading["raw"] = Sensor_getValue( config.system.sensor.type[i], config.system.sensor.i2c_addr[i], j);
+          }
+          
+          objReading["value"] = Sensor_getCalibratedValue(i, j);
+        }
+      }
+    }
+  }
+  response->setLength();
+  request->send(response);
+}
+
+
+void Api_sensor_data_raw(AsyncWebServerRequest* request) {
+  /* Api_sensor_data_raw returns the raw reading value of a specific reading of a sensor 
+   * you can call it with GET http://<IP>/api/sensor/raw?sensor=1&reading=2*/
+  AsyncJsonResponse* response = new AsyncJsonResponse();
+  JsonObject root = response->getRoot().to<JsonObject>();
+  //root["hello"] = "world";
+  
+  if((request->hasParam("sensor")) && (request->hasParam("reading"))) {
+    const AsyncWebParameter* paramSensor = request->getParam("sensor");
+    byte sensorId = paramSensor->value().toInt();
+    
+    const AsyncWebParameter* paramReading = request->getParam("reading");
+    byte readingId = paramReading->value().toInt();
+    
+    root["sensorId"] = sensorId;
+    root["readingId"] = readingId;
+    
+    /* when reading is RAW */
+    if(SensorIndex[config.system.sensor.type[sensorId]].read[readingId] == SENSOR_READ_TYPE_RAW) {
+      /* when internal ADC */
+      if(SensorIndex[config.system.sensor.type[sensorId]].type == SENSOR_TYPE_INTADC) {
+        root["value"] = Sensor_getValue( config.system.sensor.type[sensorId], config.system.sensor.gpio[sensorId][0]);
+      } else if(SensorIndex[config.system.sensor.type[sensorId]].type == SENSOR_TYPE_I2C) {
+        root["value"] = Sensor_getValue( config.system.sensor.type[sensorId], config.system.sensor.i2c_addr[sensorId], readingId);
+      }
+    } else {
+      root["msg"] = String(F("not a RAW reading"));
+    }
+  } else {
+    root["msg"] = String(F("sensor or reading not given"));
+  }
+  
+  response->setLength();
+  request->send(response);
+}
+
+void Api_sensor_driver(AsyncWebServerRequest* request) {
+  /* Api_sensor_data_raw returns the raw reading value of a specific reading of a sensor 
+   * you can call it with GET http://<IP>/api/sensor/raw?sensor=1&reading=2*/
+  AsyncJsonResponse* response = new AsyncJsonResponse();
+  JsonObject root = response->getRoot().to<JsonObject>();
+  //root["hello"] = "world";
+  
+  root["drivers"] = SensorIndex_length;
+  root["maxReadings"] = Max_Sensors_Read;
+  
+  /* empty driver because 0 is unconfigured */
+  JsonObject objSensor = root["sensor"].add<JsonObject>();
+  for(byte i = 1; i <= SensorIndex_length; i++) {
+    //Log.verbose(F("%s Sensor_Index %d, Name %s, Readings" CR), LogLoc, i, SensorIndex[i].name );
+    JsonObject objSensor = root["sensor"].add<JsonObject>();
+    objSensor["index"] = i;
+    objSensor["name"] = FPSTR(SensorIndex[i].name);
+    for(byte j = 0; j < Max_Sensors_Read; j++) {
+      if(SensorIndex[i].read[j] > 0 ) {
+        //Log.verbose(F("%s   %d: %s (%d %d)" CR), LogLoc, j, Sensor_Read_descr[SensorIndex[i].read[j]], SensorIndex[i].read[j], Sensor_Read_unit[SensorIndex[i].read[j]], SensorIndex[i].read[j]);
+        JsonObject objReading = objSensor["reading"].add<JsonObject>();
+        objReading["index"] = j;
+        objReading["descr"] = FPSTR(Sensor_Read_descr[SensorIndex[i].read[j]]);
+        objReading["unit"] = FPSTR(Sensor_Read_unit[SensorIndex[i].read[j]]);
+      }
+    }
+  } 
+  
+  response->setLength();
+  request->send(response);
+}
diff --git a/include/Webserver/File_cangrow_CSS.h b/include/Webserver/File_cangrow_CSS.h
new file mode 100644
index 0000000..34a9a7c
--- /dev/null
+++ b/include/Webserver/File_cangrow_CSS.h
@@ -0,0 +1,257 @@
+/*
+ * 
+ * include/Webserver/File_cangrow_CSS.h - /cangrow.css header file
+ * 
+ * 
+ *
+ */
+
+
+const char File_cangrow_CSS[] PROGMEM = R"(body {
+  color: #cae0d0;
+  background-color: #1d211e;
+  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+  text-align: center;
+}
+
+.footer {
+  color: #343B35;
+}
+
+.center {
+  /*width: 100; */
+  margin: auto;    
+} 
+
+.centered {
+  margin-left: auto;
+  margin-right: auto;
+}
+
+h1 {
+  margin: 15px;
+}
+
+h2 {
+  margin: 10px;
+}
+
+h3 {
+  margin: 5px;
+}
+
+td {
+  text-align: left; 
+  vertical-align: middle;
+  border-bottom: 1px solid #262B27;
+}
+
+hr {
+  height: 1px;
+  border-width: 0;
+  color: #262B27;
+  background-color: #262B27;
+  margin-top: 0.5em;
+  margin-bottom: 0.5em;
+  margin-left: auto;
+  margin-right: auto;
+  border-style: inset;
+  width: 320px;
+}
+
+a:link, a:visited {
+  color: #04AA6D;
+}
+a:hover {
+  color: #64AA6D;
+}
+a:active {
+  color: #04AA6D;
+}
+.infomsg , .warnmsg {
+  color: #fff;
+  border-radius: 3px;
+  padding: 4px;
+  /*width: fit-content; min-width: 200px; max-width: 420px;*/
+  display: inline-block;
+  margin: auto;
+  margin-bottom: 5px;
+  font-weight: bold;
+  /*text-align: center;*/
+  text-decoration: none;
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.5);
+}
+.infomsg {
+  background: #04AA6D;
+}
+.warnmsg {
+  background: #aa4204;
+}
+.inputShort {
+  width: 42px;
+}
+
+.sensorReading {
+  font-style: italic;
+  color: #64AA6D;
+}
+
+.helpbox {
+  font-size: 0.8em;
+  margin-left: 15px;
+  margin-top: 5px;
+  margin-bottom: 5px;
+}
+.nav {
+  background: #333;
+  /*width: 100; */
+  margin: auto;
+  margin-bottom: 10px;
+  padding: 0;
+  position: relative;
+  border-radius: 3px;
+  display: inline-block;
+  text-align: left;
+}
+
+.subnav {
+  /*text-align: center;*/
+  margin: auto;
+  margin-bottom: 10px;
+  padding: 0;
+  position: relative;
+  border-radius: 3px;
+}
+
+.subnavTitle {
+  font-size: 1em;
+  /*font-weight: bold;*/
+  margin-top: -10px;
+  margin-bottom: 10px;
+  /*text-align: center;*/
+}
+.nav li {
+  display: inline-block;
+  list-style: none;
+  border-radius: 3px;
+}
+
+.subnav li {
+  background: #262B27;
+  list-style: none;
+  border-radius: 3px;
+  margin-bottom: 3px;
+  display: inline-block;
+}
+
+.nav li:first-of-type {
+  background: #026b45;
+  border-top-left-radius: 3px;
+  border-bottom-left-radius: 3px;
+}
+.nav li a, .nav span, .subnav li a, .subnav span,  .button, .button:link, input[type=button], input[type=submit], 
+input[type=reset], .linkForm input[type=submit] {
+  color: #ddd;
+  display: block;
+  font-family: 'Lucida Sans Unicode', 'Lucida Grande', sans-serif;
+  font-size:0.8em;
+  padding: 10px 20px;
+  text-decoration: none;
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.5);
+}
+
+.subnav li a, .subnav span {
+  padding: 5px 10px;
+}
+
+.nav li a:hover, .subnav li a:hover, .activeNav, .button:link:hover, .button:visited:hover, input[type=button]:hover, 
+input[type=submit]:hover, input[type=reset]:hover, .linkForm input[type=submit]:hover  {
+  background: #04AA6D;
+  color: #fff;
+  border-radius: 3px;
+}
+
+.nav li a:active, .subnav li a:active {
+  background: #026b45;
+  color: #cae0d0;
+}
+
+.activeNav {
+  background: #444;
+}
+
+.navTime {
+  background: #292929;
+}
+
+.button, .button:link, .button:visited, input[type=button], input[type=submit],input[type=reset],
+.linkForm input[type=submit] {
+  background: #026b45;
+  color: #fff;
+  border-radius: 3px;
+  padding: 6px 12px;
+  /*text-align: center;*/
+  text-decoration: none;
+  display: inline-block;
+  border: none;
+}
+
+.button:link:active, .button:visited:active, input[type=button]:active, input[type=submit]:active,
+input[type=reset]:active, .linkForm input[type=submit]:active {
+  background: #026b45;
+  color: #cae0d0;
+}
+
+input[type=text], input[type=date], input[type=number], input[type=password], select {
+  background: #cae0d0;
+  color: #1d211e;
+  border: 1px solid #026b45;
+  border-radius: 3px;
+}
+
+.linkForm {
+  display: inline-block;
+}
+
+.linkForm input[type=submit] {
+  background: #262B27;
+  padding: 5px;
+  
+}
+
+.hidden {
+  display: none;
+}
+
+.force_hide {
+  display: none !important;
+}
+
+.visible {
+  display: inline;
+  /*justify-content: center!important;*/
+}
+/* a disabled class */
+a.disabled {
+ pointer-events: none;
+}
+ @media only screen and (min-width: 1820px) {
+  /*.center, .nav {
+    width: 60; min-width: 420px;
+  }*/
+  .subnav li {
+    display: '';
+    margin-bottom: 3px;
+   }
+}
+
+/*@media only screen and (min-width: 640px) {
+  
+}*/)";
+
+void WebFile_cangrow_CSS(AsyncWebServerRequest *request) {
+  AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), File_cangrow_CSS);
+  response->addHeader(F("Cache-control"), F("max-age=600"));
+  request->send(response);
+  //request->send_P(200, "text/css", File_cangrow_CSS);
+}
diff --git a/include/Webserver/File_cangrow_JS.h b/include/Webserver/File_cangrow_JS.h
new file mode 100644
index 0000000..36375f7
--- /dev/null
+++ b/include/Webserver/File_cangrow_JS.h
@@ -0,0 +1,241 @@
+/*
+ * 
+ * include/Webserver/File_cangrow_JS.h - /cangrow.js header file
+ * 
+ * 
+ *
+ */
+
+
+const char File_cangrow_JS[] PROGMEM = R"(function toggleDisplay(id) {
+  let el = document.getElementById(id);
+  let el_cs = getComputedStyle(el);
+  
+  if (el_cs.getPropertyValue('display') === 'none') {
+    el.style.display = 'inline';
+  } else {
+    el.style.display = 'none';
+  }
+} 
+
+function hideAllClass(classname) {
+  
+  const el = document.getElementsByClassName(classname);
+  
+  for(let i = 0; i < el.length ; i++) {
+    el[i].style.display = 'none';
+  }
+}
+
+function showSelect(selectId, prefix, hideClass = '') {
+  if(hideClass != '') {
+    hideAllClass(hideClass);
+  }
+  
+  let selVal = document.getElementById(selectId).value;
+  toggleId = prefix + selVal;
+  if(document.getElementById(toggleId) !== null ) {
+    toggleDisplay(toggleId);
+  }
+}
+
+function confirmDelete(name) {
+  return confirm('Delete ' + name + '?');
+}
+
+function SystemOutputAddselectRequired(selectId) {
+  let selVal = document.getElementById(selectId).value;
+  //hideAllClass('hidden');
+  console.log('selectReq Status: ' + selVal);
+  switch(selVal) {
+    case '1':
+      document.getElementById('gpio').required = true;
+      document.getElementById('gpio_pwm').required = true;
+      
+      document.getElementById('i2c_type').required = false;
+      document.getElementById('i2c_addr').required = false;
+      document.getElementById('i2c_port').required = false;
+      document.getElementById('webcall_host').required = false;
+      document.getElementById('webcall_path_on').required = false;
+      document.getElementById('webcall_path_off').required = false;
+    break;
+    
+    case '2':
+      document.getElementById('gpio').required = false;
+      document.getElementById('gpio_pwm').required = false;
+      
+      document.getElementById('i2c_type').required = true;
+      document.getElementById('i2c_addr').required = true;
+      document.getElementById('i2c_port').required = true;
+      document.getElementById('webcall_host').required = false;
+      document.getElementById('webcall_path_on').required = false;
+      document.getElementById('webcall_path_off').required = false;
+    break;
+    
+    case '3':
+      document.getElementById('gpio').required = false;
+      document.getElementById('gpio_pwm').required = false;
+      
+      document.getElementById('i2c_type').required = false;
+      document.getElementById('i2c_addr').required = false;
+      document.getElementById('i2c_port').required = false;
+      document.getElementById('webcall_host').required = true;
+      document.getElementById('webcall_path_on').required = true;
+      document.getElementById('webcall_path_off').required = true;
+    break;
+    
+    default:
+    break;
+  }     
+}
+
+// https://stackoverflow.com/a/67412019
+function SystemOutputAdd_replaceI2cAddr(selectId, replaceId) {
+  let sel = document.querySelector('#' + replaceId);
+  let selVal = document.getElementById(selectId).value;
+  // Remove existing options
+  Array.from(sel).forEach((option) => {
+    sel.removeChild(option)
+  });
+  // get or set your new options here.
+  if(selVal) {
+    addr[selVal].map((optionData) => {
+      let opt = document.createElement('option');
+      let PortsUsed = 0;
+      let label = optionData[0];
+      opt.value = optionData[1];        
+      // iterate through i2c modules available ports
+      for(i = 0; i < optionData[2].length; i++) {
+        if(optionData[2][i] > 0) {
+          PortsUsed++;
+        }
+      }
+      if(PortsUsed  >= optionData[2].length) {
+        opt.disabled = true;
+        label = label + ' (used)';
+      }
+      opt.appendChild(document.createTextNode(label));
+      sel.appendChild(opt);
+    });
+    SystemOutputAdd_replaceI2cPort('i2c_type', 'i2c_addr', 'i2c_port');
+  }
+  
+}
+//////////////////////////////////////
+function SystemOutputAdd_replaceI2cPort(selectTypeId, selectAddrId, replaceId) {
+  let repl = document.querySelector('#' + replaceId);
+  let selValType = document.getElementById(selectTypeId).value;
+  let selValAddr = document.getElementById(selectAddrId).value;
+  // Remove existing options
+  Array.from(repl).forEach((option) => {
+    repl.removeChild(option)
+  });
+  if(selValAddr) {
+    console.log('true');
+    // iterate through i2c modules available ports
+    for(i = 0; i < addr[selValType][selValAddr][2].length; i++) {
+      let opt = document.createElement('option');
+      let label = 'Port ' + i;
+      opt.value = i;
+      if(addr[selValType][selValAddr][2][i] > 0) {
+        label = label + ' (used)';
+        opt.disabled = true;
+      }
+      opt.appendChild(document.createTextNode(label));
+      repl.appendChild(opt);
+      console.log('PortID ' + i + ' Port sum ' + addr[selValType][selValAddr][2].length);
+    }
+  } else {
+    let opt = document.createElement('option');
+    opt.appendChild(document.createTextNode('n/a'));
+    opt.disabled = true;
+    repl.appendChild(opt);
+  }
+}
+//javascript is my passion
+
+
+function SystemSensorAddGpioI2cSel(selectId) {
+  let selVal = document.getElementById(selectId).value;
+  hideAllClass('hidden');
+  if(selVal == 1) {
+     document.getElementById('type_1').style.display = 'inline';
+     document.getElementById('i2c_addr').required = false;
+     if(ESP == '32') {
+      document.getElementById('gpio').required = true;
+     }
+  } else if(selVal > 1) {
+    document.getElementById('type_2').style.display = 'inline';
+    document.getElementById('i2c_addr').required = true;
+    if(ESP == '32') {
+      document.getElementById('gpio').required = false;
+    }
+  }
+}
+
+function convertDateToEpoch(src, dst) {
+  var val = document.getElementById(src).value ;
+  document.getElementById(dst).value = new Date(val).getTime() / 1000;
+}
+
+
+function GrowSelectControlSensorRead(selectId, inputSensor, inputRead) {
+  let selVal = document.getElementById(selectId).value;
+  let sensor = selVal.split(':')[0];
+  let read = selVal.split(':')[1];
+  document.getElementById(inputSensor).value = sensor;
+  document.getElementById(inputRead).value = read;
+}
+
+
+
+
+function GetSensorJson(callback) {
+  let path = '/api/sensor/';
+  //let path = '/api/sensor/raw_' + sensor + '_' + reading;
+  var xobj = new XMLHttpRequest();
+  xobj.overrideMimeType('application/json');
+  xobj.open('GET', path, true);
+  xobj.onreadystatechange = function() {
+    if (xobj.readyState == 4 && xobj.status == "200") {
+        callback(xobj.responseText);
+    }
+  }
+  xobj.send(null);
+}
+
+
+/* propably not the best place, but this as global as it can get i guess */
+var SensorJson; 
+function SensorJsonRefresh() {
+  GetSensorJson(function(response) {
+    /* needs to be a global */
+    SensorJson = JSON.parse(response);
+  });
+  //console.log('Refresh SensorJson');
+}
+
+
+function rawRefresh(sensor, reading, id) {
+  let element = id + sensor + '-' + reading;
+  document.getElementById(element).textContent = SensorJson.sensor[sensor].reading[reading].raw;
+  //console.log(SensorJson.sensor[sensor].reading[reading].raw);
+  //console.log('sensor:' + sensor + ';reading:' + reading + ';id:' + id + ';element:' + element);
+}
+
+
+function sensorRefresh(sensor, reading, id) {
+  let element = id + sensor + '-' + reading;
+  document.getElementById(element).textContent = SensorJson.sensor[sensor].reading[reading].value + ' ' + SensorJson.sensor[sensor].reading[reading].unit;
+  //console.log(SensorJson.sensor[sensor].reading[reading].value + SensorJson.sensor[sensor].reading[reading].unit);
+  //console.log('sensor:' + sensor + ';reading:' + reading + ';id:' + id + ';element:' + element);
+}
+
+)";
+
+void WebFile_cangrow_JS(AsyncWebServerRequest *request) {
+  AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/javascript"), File_cangrow_JS);
+  response->addHeader(F("Cache-control"), F("max-age=600"));
+  request->send(response);
+  //request->send_P(200, "text/javascript", File_cangrow_JS);
+}
diff --git a/include/Webserver/File_favicon_ico.h b/include/Webserver/File_favicon_ico.h
new file mode 100644
index 0000000..2b241cb
--- /dev/null
+++ b/include/Webserver/File_favicon_ico.h
@@ -0,0 +1,38 @@
+unsigned char File_favicon_ico_gz[] = {
+  0x1f, 0x8b, 0x08, 0x08, 0x11, 0x71, 0x19, 0x67, 0x00, 0x03, 0x43, 0x61,
+  0x6e, 0x47, 0x72, 0x6f, 0x77, 0x5f, 0x66, 0x61, 0x76, 0x69, 0x63, 0x6f,
+  0x2e, 0x69, 0x63, 0x6f, 0x00, 0xed, 0x94, 0x49, 0x4b, 0xc3, 0x40, 0x18,
+  0x86, 0xdf, 0xd8, 0xc5, 0xaa, 0xe9, 0x12, 0xa7, 0xcd, 0xd2, 0x26, 0x99,
+  0x7c, 0x89, 0x76, 0x45, 0xb4, 0x2a, 0xb6, 0xa2, 0x42, 0xb1, 0x52, 0x73,
+  0x11, 0xd4, 0x83, 0xdb, 0xc1, 0x8b, 0x08, 0x75, 0xf9, 0xff, 0x67, 0xbf,
+  0x49, 0x3c, 0x58, 0xa4, 0x17, 0xc1, 0x5b, 0x9e, 0xe4, 0x1d, 0xe6, 0xf9,
+  0x86, 0x61, 0x32, 0x03, 0x19, 0x40, 0xe3, 0xa7, 0x56, 0x03, 0xb7, 0x25,
+  0xcc, 0x0b, 0x80, 0x09, 0xa0, 0xcb, 0xe1, 0x12, 0x02, 0xa4, 0xf5, 0x65,
+  0x44, 0xed, 0x08, 0x51, 0x27, 0xc2, 0x56, 0x2f, 0xc2, 0x76, 0x9f, 0x33,
+  0x88, 0xd0, 0xde, 0x09, 0xd1, 0xd9, 0x0b, 0xd1, 0x1d, 0x86, 0xe8, 0x1d,
+  0x86, 0xe8, 0x8f, 0x08, 0x83, 0x31, 0x61, 0x77, 0x12, 0x60, 0x38, 0x0d,
+  0xb0, 0x3f, 0x93, 0x38, 0x88, 0x25, 0x8e, 0xae, 0x7c, 0x8c, 0xae, 0x7d,
+  0x8c, 0x6f, 0x3c, 0x8e, 0x8f, 0xe3, 0x5b, 0x0f, 0x27, 0x77, 0x2e, 0x4e,
+  0xef, 0x39, 0x0f, 0x2e, 0xce, 0x1e, 0x39, 0x4f, 0x2d, 0x4c, 0x9e, 0x9b,
+  0x38, 0x7f, 0x71, 0x30, 0xe5, 0x5c, 0xbc, 0xda, 0x98, 0xcd, 0x2d, 0xcc,
+  0xde, 0x2c, 0x5c, 0xbe, 0x5b, 0x88, 0x3f, 0x4c, 0xc4, 0x9f, 0xe6, 0xd2,
+  0xef, 0xcb, 0xc8, 0xc8, 0xf8, 0x7f, 0x7e, 0xfc, 0x81, 0x45, 0x61, 0xe4,
+  0x57, 0xac, 0xbc, 0x21, 0x8a, 0xa9, 0x6b, 0x82, 0xa4, 0x0c, 0x24, 0x09,
+  0x2d, 0x55, 0x43, 0x78, 0xc4, 0x78, 0xc2, 0x50, 0x85, 0x9c, 0x4d, 0x92,
+  0x12, 0x24, 0xd9, 0x1b, 0x7c, 0xe9, 0x08, 0x51, 0x72, 0x13, 0x5f, 0xaf,
+  0x8a, 0x4d, 0x35, 0x61, 0xd5, 0x4b, 0xc7, 0xfd, 0xef, 0x5b, 0xa8, 0x4e,
+  0x14, 0x24, 0xaf, 0x9f, 0xe8, 0x5a, 0x40, 0x75, 0x87, 0xc8, 0xe1, 0xb2,
+  0xae, 0xbc, 0x2c, 0x85, 0xa6, 0x9c, 0x97, 0x2d, 0x2b, 0x6f, 0x15, 0x34,
+  0x28, 0x87, 0x56, 0x6d, 0x2a, 0xcf, 0x71, 0x2a, 0xca, 0xd3, 0x6e, 0x0a,
+  0x51, 0x65, 0x61, 0x3b, 0x44, 0x8b, 0xdb, 0x6b, 0x34, 0x16, 0x5d, 0xd7,
+  0xff, 0x74, 0x4a, 0xbf, 0xf9, 0x02, 0x31, 0x98, 0x4b, 0x6b, 0x7e, 0x05,
+  0x00, 0x00
+};
+unsigned int File_favicon_ico_gz_len = 326;
+
+void WebFile_favicon_ico(AsyncWebServerRequest *request) {
+  AsyncWebServerResponse *response = request->beginResponse_P(200, F("image/x-icon"), File_favicon_ico_gz, File_favicon_ico_gz_len);
+  response->addHeader(F("Content-Encoding"), F("gzip"));
+  response->addHeader(F("Cache-control"), F("max-age=600"));
+  request->send(response);
+}
diff --git a/include/Webserver/Footer.h b/include/Webserver/Footer.h
new file mode 100644
index 0000000..61ad019
--- /dev/null
+++ b/include/Webserver/Footer.h
@@ -0,0 +1,9 @@
+/*
+ * 
+ * include/Webserver/footer_HTML.h - footer page HTML header file
+ * 
+ * 
+ *
+ */
+
+const char Footer_HTML[] PROGMEM = R"(<div class='footer'><span>Build: %CGBUILD%</span></div></div></body></html>)";
diff --git a/include/Webserver/Header.h b/include/Webserver/Header.h
new file mode 100644
index 0000000..083dd81
--- /dev/null
+++ b/include/Webserver/Header.h
@@ -0,0 +1,28 @@
+/*
+ * 
+ * include/Webserver/header_HTML.h - header page HTML header file
+ * 
+ * 
+ *
+ */
+
+const char Header_HTML[] PROGMEM = R"(<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset='UTF-8'>
+ <meta name='viewport' content='width=device-width, initial-scale=1.0'>
+ <title>%GROWNAME% - CanGrow v%CGVER%</title>
+ <link rel='stylesheet' href='/cangrow.css'>
+ <script type='text/javascript' src='/cangrow.js'></script>
+</head>
+<body>
+ <ul class='nav'><li><a  href='/'>&#x1F331; %GROWNAME%</a></li>
+  <li><a class='%ACTIVE_NAV_GROW%' href='/grow/' >&#128262; Grow settings</a></li>
+  <li><a class='%ACTIVE_NAV_SYSTEM%' href='/system/' >&#9881; System settings</a></li>
+  <li><a class='%ACTIVE_NAV_WIFI%' href='/wifi/' >&#128225; WiFi settings</a></li>
+  <li><a class='%ACTIVE_NAV_HELP%' href='/help' >&#x2753; Help</a></li>
+  <li><span class='navTime'>%TIME%</span></li>
+  <li><a href='https://git.la10cy.net/DeltaLima/CanGrow' target='_blank'>CanGrow v%CGVER%</a></li>
+ </ul>
+ <div class='center'>
+ %NEED_RESTART%)";
diff --git a/include/Webserver/Page_404.h b/include/Webserver/Page_404.h
new file mode 100644
index 0000000..c04c856
--- /dev/null
+++ b/include/Webserver/Page_404.h
@@ -0,0 +1,26 @@
+/*
+ * 404 error page begins
+ */
+
+// 404 page is a good page template btw
+const char Page_404_HTML[] PROGMEM = R"EOF(%HEADER%
+<div class='warnmsg'><h1>&#10071; &#65039; 404 - not found</h1></div>
+%FOOTER% )EOF";
+
+/* processor */
+String Proc_WebPage_404(const String& var) {
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var);
+  } else {
+    return String();
+  }
+}
+
+// https://github.com/mathieucarbou/ESPAsyncWebServer/blob/main/examples/SimpleServer/SimpleServer.ino
+void WebserverNotFound(AsyncWebServerRequest* request) {
+  request->send_P(404, TEXT_HTML, Page_404_HTML, Proc_WebPage_404);
+}
+
+/*
+ * 404 error page ends
+ */
diff --git a/include/Webserver/Page_grow.h b/include/Webserver/Page_grow.h
new file mode 100644
index 0000000..a329c9a
--- /dev/null
+++ b/include/Webserver/Page_grow.h
@@ -0,0 +1,1000 @@
+/*
+ * 
+ * include/Webserver/Page_grow.h - grow page header file
+ * 
+ * 
+ *
+ */
+
+#include "Page_grow_HTML.h"
+
+
+/* subnav processor */
+const byte WEB_GROW_SUBNAV_GENERAL = 1;
+const byte WEB_GROW_SUBNAV_LIGHT = 2;
+const byte WEB_GROW_SUBNAV_AIR = 3;
+const byte WEB_GROW_SUBNAV_WATER = 4;
+const byte WEB_GROW_SUBNAV_DASHBOARD = 5;
+
+bool Test_WebPage_grow_SUBNAV(const String& var) {
+  if(
+    (var == "SUBNAV") ||
+    (var == "ACTIVE_SUBNAV_GENERAL") ||
+    (var == "ACTIVE_SUBNAV_LIGHT") ||
+    (var == "ACTIVE_SUBNAV_AIR") ||
+    (var == "ACTIVE_SUBNAV_WATER") ||
+    (var == "ACTIVE_SUBNAV_DASHBOARD")) {
+    return true;
+  } else {
+    return false;
+  }
+}
+
+/*
+ * Proc_WebPage_grow_SUBNAV - subnav processor for grow
+ * this function works as same as AddHeaderFooter from Common.h
+ * byte activeSubnav:
+ *   1 - Output
+ *   2 - Update
+ *   3 - Restart
+ *   4 - Wipe
+ */
+String Proc_WebPage_grow_SUBNAV(const String& var, byte activeSubnav = 0) {
+  String activeSubnav_ClassName = "activeNav";
+  if(var == "SUBNAV") {
+    return String(Page_grow_HTML_SUBNAV);
+  } else if((var == "ACTIVE_SUBNAV_GENERAL") && (activeSubnav == WEB_GROW_SUBNAV_GENERAL)) {
+    return activeSubnav_ClassName;
+  } else if((var == "ACTIVE_SUBNAV_LIGHT") && (activeSubnav == WEB_GROW_SUBNAV_LIGHT)) {
+    return activeSubnav_ClassName;
+  } else if((var == "ACTIVE_SUBNAV_AIR") && (activeSubnav == WEB_GROW_SUBNAV_AIR)) {
+    return activeSubnav_ClassName;
+  } else if((var == "ACTIVE_SUBNAV_WATER") && (activeSubnav == WEB_GROW_SUBNAV_WATER)) {
+    return activeSubnav_ClassName;
+  } else if((var == "ACTIVE_SUBNAV_DASHBOARD") && (activeSubnav == WEB_GROW_SUBNAV_DASHBOARD)) {
+    return activeSubnav_ClassName;
+  } else {
+    return String();
+  }
+}
+
+/*******************************************************************************
+ * Main grow page
+ */
+// https://techtutorialsx.com/2018/07/23/esp32-arduino-http-server-template-processing-with-multiple-placeholders/
+String Proc_WebPage_grow(const String& var) {
+  const static char LogLoc[] PROGMEM = "[Webserver:grow(Proc)]";
+  /* This is a processor function, which returns a string. 
+   * We check if var contains one of our placeholders from the template.
+   * If we hit a placeholder, we just return the String we want.
+   * 
+   * TestHeaderFooter() Is kinda a processor too, but only checks for 
+   * header specific placeholders.
+   */
+  
+  //Log.verbose(F("%s var: %s" CR), LogLoc, var);
+  
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 1);
+  } else if(Test_WebPage_grow_SUBNAV(var)) {
+    return Proc_WebPage_grow_SUBNAV(var, WEB_GROW_SUBNAV_GENERAL);
+  } else if(var == "GROWNAME") {
+    return String(config.grow.name);
+  } else if(var == "GROWSTART_EPOCH") {
+    /* check if there is an reasonable grow start time */
+    if(config.grow.start > 1711922400) {
+      return String(config.grow.start);
+    } else {
+      return String(); //String(now());
+    }
+  } else if(var == "GROWSTART") {
+    if(config.grow.start > 1711922400) {
+      return Str_Epoch2Date(config.grow.start);
+    } else {
+      return String(); //Str_Epoch2Date(now());
+    }
+  } else if(var == "DAYS_VEG") {
+    return String(config.grow.daysVeg);
+  } else if(var == "DAYS_BLOOM") {
+    return String(config.grow.daysBloom);
+  } else {
+    return String();
+  }
+}
+
+String Proc_WebPage_grow_POST(const String& var) {
+  /* This is the processor for POST
+   * Its exactly the same, just looking for SAVE_MSG string.
+   * If nothing matches, it calles the main Proc_WebPage_grow() 
+   * processor function, so all the other stuff like header and so
+   * on get replaced 
+   */
+  if(var == "SAVE_MSG") {
+    return String(Common_HTML_SAVE_MSG);
+  } else {
+    return Proc_WebPage_grow(var);
+  }
+}
+
+String Proc_WebPage_grow_POST_ERR(const String& var) {
+  if(var == "SAVE_MSG") {
+    return String(Common_HTML_SAVE_MSG_ERR);
+  } else {
+    return Proc_WebPage_grow(var);
+  }
+}
+
+/* WebPage function */
+void WebPage_grow(AsyncWebServerRequest *request) {
+  const static char LogLoc[] PROGMEM = "[Webserver:grow]";
+
+
+  /* Which kind of Request */
+  if(request->method() == HTTP_POST) {
+    
+    if(request->hasParam("name", true)) {
+      const AsyncWebParameter* param = request->getParam("name", true);
+      strlcpy(config.grow.name, param->value().c_str(), sizeof(config.grow.name));
+    }
+    
+    if(request->hasParam("start", true)) {
+      const AsyncWebParameter* param = request->getParam("start", true);
+      config.grow.start = param->value().toInt();
+    }
+    
+    if(request->hasParam("daysVeg", true)) {
+      const AsyncWebParameter* param = request->getParam("daysVeg", true);
+      config.grow.daysVeg = param->value().toInt();
+    }
+    
+    if(request->hasParam("daysBloom", true)) {
+      const AsyncWebParameter* param = request->getParam("daysBloom", true);
+      config.grow.daysBloom = param->value().toInt();
+    }
+    
+    
+    if(SaveConfig()) {
+      // we need a restart to apply the new settings
+      
+      Log.notice(F("%s config saved" CR), LogLoc);
+      
+      request->send_P(200, "text/html", Page_grow_HTML, Proc_WebPage_grow_POST);
+    } else {
+      Log.error(F("%s ERROR while saving config" CR), LogLoc);
+      request->send_P(200, TEXT_HTML, Page_grow_HTML, Proc_WebPage_grow_POST_ERR);
+    }
+  } else {  
+    request->send_P(200, TEXT_HTML, Page_grow_HTML, Proc_WebPage_grow);
+  }
+}
+
+
+
+
+
+
+
+
+/*******************************************************************************
+ * grow light page
+ */
+String Proc_WebPage_grow_light(const String& var) {
+  const static char LogLoc[] PROGMEM = "[Webserver:grow:light(Proc)]";
+  
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 1);
+  } else if(Test_WebPage_grow_SUBNAV(var)) {
+    return Proc_WebPage_grow_SUBNAV(var, WEB_GROW_SUBNAV_LIGHT);
+  } else if(var == "LIGHT") {
+    String html;
+    for(byte i = 0; i < Max_Outputs; i++) {
+      if(config.grow.light.configured[i] == true) {      
+        /* form */
+        html += F("<form method='post' action='/grow/light/'>");
+        /* OutputId */
+        html += F("<input type='hidden' name='output' value='");
+        html += i;
+        html += F("' required>"); 
+        
+        
+        html += F("<h2>");
+        html += config.system.output.name[i];
+        html += F("</h2>");
+        
+        /* Device */
+        html += F("<u>Device:</u><br><b>");
+        html += FPSTR(Output_Device_descr[config.system.output.device[i]]);
+        html += F(" (");
+        html += FPSTR(Output_Type_descr[config.system.output.type[i]]);
+        html += F(")</b><br>");
+        
+        /* Sunrise */
+        html += F("<u>Sunrise</u><br><b>Vegetation </b><input class='inputShort' type='number' name='sunriseHourVeg' min='0' max='23' value='");
+        html += config.grow.light.sunriseHourVeg[i];
+        html += F("' required><b>:</b> <input class='inputShort' type='number' name='sunriseMinuteVeg' min='0' max='59' value='");
+        html += config.grow.light.sunriseMinuteVeg[i];
+        html += F("' required><br>");
+        
+        /* when bloon days is set to 0, disable it */
+        if(config.grow.daysBloom > 0) {
+          html += F("<b>Bloom </b><input class='inputShort' type='number' name='sunriseHourBloom' min='0' max='23' value='");
+          html += config.grow.light.sunriseHourBloom[i];
+          html += F("' required><b>:</b> <input class='inputShort' type='number' name='sunriseMinuteBloom' min='0' max='59' value='");
+          html += config.grow.light.sunriseMinuteBloom[i];
+          html += F("' required><br>");
+        }
+        
+        /* Sunset */
+        html += F("<u>Sunset</u><br><b>Vegetation </b><input class='inputShort' type='number' name='sunsetHourVeg' min='0' max='23' value='");
+        html += config.grow.light.sunsetHourVeg[i];
+        html += F("' required><b>:</b> <input class='inputShort' type='number' name='sunsetMinuteVeg' min='0' max='59' value='");
+        html += config.grow.light.sunsetMinuteVeg[i];
+        html += F("' required><br>");
+        /* when bloon days is set to 0, disable it */
+        if(config.grow.daysBloom > 0) {
+          html += F("<b>Bloom </b><input class='inputShort' type='number' name='sunsetHourBloom' min='0' max='23' value='");
+          html += config.grow.light.sunsetHourBloom[i];
+          html += F("' required><b>:</b> <input class='inputShort' type='number' name='sunsetMinuteBloom' min='0' max='59' value='");
+          html += config.grow.light.sunsetMinuteBloom[i];
+          html += F("' required><br>");
+        }
+        
+        /* power */
+        /* if no pwm, show simple bool select */
+        if(Output_Check_PWM(i) == true) {
+          html += F("<u>Power</u><br><input type='range' name='power' min='0' max='255' value='");
+          html += config.grow.light.power[i];
+          html += F("'/> &#37;<br>");
+          
+          /* fade */
+          html += F("<u>Fade sunset/sunrise</u><br><select name='fade' required>");
+          html += Html_SelectOpt_bool(config.grow.light.fade[i]);
+          html += F("'/></select><br>");
+          
+          /* fade duration */
+          html += F("<u>Fade duration</u><br><input class='inputShort' type='number' name='fadeDuration' min='1' max='255' value='");
+          html += config.grow.light.fadeDuration[i];
+          html += F("' required> Minutes<br>");
+        } else {
+          html += F("<u>Power</u><br><select name='power' required>");
+          html += Html_SelectOpt_bool(config.grow.light.power[i], "On", "Off");
+          html += F("'/></select><br>");
+        }
+        
+        /* submit button */
+        html += F("<input type='submit' value='&#x1F4BE; Save settings' style='margin-top: 8px;'></form>");
+        
+        /* HR HORIZONTAL LINE TO SIGNAL END */
+        html += F("<hr>");
+        
+
+      }
+    }
+    
+    return html;
+  } else {
+    return String();
+  }
+}
+
+String Proc_WebPage_grow_light_POST(const String& var) {
+  /* This is the processor for POST
+   * Its exactly the same, just looking for SAVE_MSG string.
+   * If nothing matches, it calles the main Proc_WebPage_grow() 
+   * processor function, so all the other stuff like header and so
+   * on get replaced 
+   */
+  if(var == "SAVE_MSG") {
+    return String(Common_HTML_SAVE_MSG);
+  } else {
+    return Proc_WebPage_grow_light(var);
+  }
+}
+
+/* WebPage function */
+void WebPage_grow_light(AsyncWebServerRequest *request) {
+  const static char LogLoc[] PROGMEM = "[Webserver:grow:light]";
+
+
+  /* Which kind of Request */
+  if(request->method() == HTTP_POST) {
+    byte OutputId;
+    if(request->hasParam("output", true)) {
+      const AsyncWebParameter* param = request->getParam("output", true);
+      OutputId = param->value().toInt();
+      
+      config.grow.light.output[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("sunriseHourVeg", true)) {
+      const AsyncWebParameter* param = request->getParam("sunriseHourVeg", true);
+      config.grow.light.sunriseHourVeg[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("sunriseMinuteVeg", true)) {
+      const AsyncWebParameter* param = request->getParam("sunriseMinuteVeg", true);
+      config.grow.light.sunriseMinuteVeg[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("sunriseHourBloom", true)) {
+      const AsyncWebParameter* param = request->getParam("sunriseHourBloom", true);
+      config.grow.light.sunriseHourBloom[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("sunriseMinuteBloom", true)) {
+      const AsyncWebParameter* param = request->getParam("sunriseMinuteBloom", true);
+      config.grow.light.sunriseMinuteBloom[OutputId] = param->value().toInt();
+    }
+    
+    
+    if(request->hasParam("sunsetHourVeg", true)) {
+      const AsyncWebParameter* param = request->getParam("sunsetHourVeg", true);
+      config.grow.light.sunsetHourVeg[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("sunsetMinuteVeg", true)) {
+      const AsyncWebParameter* param = request->getParam("sunsetMinuteVeg", true);
+      config.grow.light.sunsetMinuteVeg[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("sunsetHourBloom", true)) {
+      const AsyncWebParameter* param = request->getParam("sunsetHourBloom", true);
+      config.grow.light.sunsetHourBloom[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("sunsetMinuteBloom", true)) {
+      const AsyncWebParameter* param = request->getParam("sunsetMinuteBloom", true);
+      config.grow.light.sunsetMinuteBloom[OutputId] = param->value().toInt();
+    }
+    
+    
+    
+    
+    
+    if(request->hasParam("power", true)) {
+      const AsyncWebParameter* param = request->getParam("power", true);
+      config.grow.light.power[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("fade", true)) {
+      const AsyncWebParameter* param = request->getParam("fade", true);
+      config.grow.light.fade[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("fadeDuration", true)) {
+      const AsyncWebParameter* param = request->getParam("fadeDuration", true);
+      config.grow.light.fadeDuration[OutputId] = param->value().toInt();
+    }
+    
+    SaveConfig();
+      
+    Log.notice(F("%s config saved" CR), LogLoc);
+    
+    request->send_P(200, TEXT_HTML, Page_grow_light_HTML, Proc_WebPage_grow_light_POST);
+
+  } else {  
+    request->send_P(200, TEXT_HTML, Page_grow_light_HTML, Proc_WebPage_grow_light);
+  }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/*******************************************************************************
+ * grow air page
+ */
+String Proc_WebPage_grow_air(const String& var) {
+  const static char LogLoc[] PROGMEM = "[Webserver:grow:air(Proc)]";
+  
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 1);
+  } else if(Test_WebPage_grow_SUBNAV(var)) {
+    return Proc_WebPage_grow_SUBNAV(var, WEB_GROW_SUBNAV_AIR);
+  } else if(var == "AIR") {
+    String html;
+    
+    for(byte i = 0; i < Max_Outputs; i++) {
+      if(config.grow.air.configured[i] == true) {      
+        /* form */
+        html += F("<form method='post' action='/grow/air/'>");
+        /* OutputId */
+        html += F("<input type='hidden' name='output' value='");
+        html += i;
+        html += F("' required>"); 
+        
+        
+        html += F("<h2>");
+        html += config.system.output.name[i];
+        html += F("</h2>");
+        
+        /* Device */
+        html += F("<u>Device:</u><br><b>");
+        html += FPSTR(Output_Device_descr[config.system.output.device[i]]);
+        html += F(" (");
+        html += FPSTR(Output_Type_descr[config.system.output.type[i]]);
+        html += F(")</b><br>");
+        
+        
+        /* speed / power */
+        /* if no pwm, show simple bool select */
+        if(Output_Check_PWM(i) == true) {
+          /* <input type='range' id='PinLEDPWM' name='PinLEDPWM' min='0' max='255' value='255'/> %<br> */
+          if(config.system.output.device[i] == OUTPUT_DEVICE_FAN) {
+            html += F("<u>Speed</u>");
+          } else {
+            html += F("<u>Power</u>");
+          }
+          html += F("<br><input type='range' name='power' min='0' max='255' value='");
+          html += config.grow.air.power[i];
+          html += F("'/> &#37;<br>");
+        } else {
+          html += F("<u>Power</u><br><select name='power' required>");
+          //<input type='range' name='power' min='0' max='100' value='");
+          html += Html_SelectOpt_bool(config.grow.air.power[i], "On", "Off");
+          html += F("'/></select><br>");
+        }
+     
+        
+        /* hidden inputs for SensorId and ReadId*/
+        html += F("<input type='hidden' name='controlSensor' value='");
+        html += config.grow.air.controlSensor[i];
+        html += F("' id='controlSensor");
+        html += i;
+        html += F("'>"); 
+        
+        html += F("<input type='hidden' name='controlRead' value='");
+        html += config.grow.air.controlRead[i];
+        html += F("' id='controlRead");
+        html += i;
+        html += F("'>"); 
+        /* controledBy */
+        html += F("<u>Controled by</u><br><select name='controlBy' id='ctrl");
+        html += i;
+        html += F("'onChange=\"GrowSelectControlSensorRead('ctrl");
+        html += i;
+        html += F("', 'controlSensor");
+        html += i;
+        html += F("', 'controlRead");
+        html += i;
+        html += F("');\"><option value='255:255' selected >---</option>");
+        
+        /* iterate through available sensors and offer useful values */
+        byte count = 0;
+        for(byte j = 0; j < Max_Sensors; j++) {
+          /* if sensor is configured */
+          if(config.system.sensor.type[j] > 0) {
+            /* we want to offer humidity, temperature, gas resistance */
+            for(byte k = 0; k < Max_Sensors_Read; k++) {
+              if(SensorIndex[config.system.sensor.type[j]].read[k] > 0) {
+                if((SensorIndex[config.system.sensor.type[j]].read[k] == SENSOR_READ_TYPE_TEMP) ||
+                   (SensorIndex[config.system.sensor.type[j]].read[k] == SENSOR_READ_TYPE_HUMIDITY) ||
+                   (SensorIndex[config.system.sensor.type[j]].read[k] == SENSOR_READ_TYPE_GAS_RESISTANCE)) {
+                    
+                    html += F("<option value='");
+                    
+                    /* put SensorId and ReadId into one colon sperated string. This we seperate later within javascript */
+                    html += j; // SensorId
+                    html += F(":");
+                    html += k; // ReadId
+                    
+                    html += F("'");
+                    if((config.grow.air.controlSensor[i] == j) && (config.grow.air.controlRead[i] == k))
+                      html += F(" selected");
+                    html += F(">");
+                    html += config.system.sensor.name[j];
+                    html += F(" - ");
+                    html += FPSTR(Sensor_Read_descr[SensorIndex[config.system.sensor.type[j]].read[k]]);
+                    html += F(" (");
+                    html += Sensor_getCalibratedValue(j, k);
+                    html += F(" ");
+                    /* put unit into string */
+                    String unit = FPSTR(Sensor_Read_unit[SensorIndex[config.system.sensor.type[j]].read[k]]);
+                    /* to be able to replace % sign, which is already used by ESPAsyncWebserver's template engine
+                     * with html code for it */
+                    html += F(" ");
+                    unit.replace(F("%"), F("&#37;"));
+                    html += unit;
+                    html += F(")</option>");
+                    count++;
+                }
+              }
+            }
+          }
+        }
+        html += F("</select><br>");
+        
+        /* controlMode */
+        html += F("<u>Control mode</u><br><select name='controlMode' ><option value='' selected>---</option>");
+        //<input type='range' name='power' min='0' max='100' value='");
+        html += Html_SelectOpt_array(CONTROL_AIR_MODE__TOTAL, Control_Air_Mode_descr, config.grow.air.controlMode[i]);
+        html += F("'/> &#37;</select><br>");
+        
+        
+        /* min */
+        html += F("<u>Min</u><br><input type='number' name='min' step='0.1' value='");
+        html += config.grow.air.min[i];
+        html += F("' required><br>");
+        
+        /* max */
+        html += F("<u>Max</u><br><input type='number' name='max' step='0.1' value='");
+        html += config.grow.air.max[i];
+        html += F("' required><br>");
+        
+               
+        /* submit button */
+        html += F("<input type='submit' value='&#x1F4BE; Save settings' style='margin-top: 8px;'></form>");
+        
+        /* HR HORIZONTAL LINE TO SIGNAL END */
+        html += F("<hr>");
+        
+
+      }
+    }
+    
+    return html;
+  } else {
+    return String();
+  }
+}
+
+String Proc_WebPage_grow_air_POST(const String& var) {
+  /* This is the processor for POST
+   * Its exactly the same, just looking for SAVE_MSG string.
+   * If nothing matches, it calles the main Proc_WebPage_grow() 
+   * processor function, so all the other stuff like header and so
+   * on get replaced 
+   */
+  if(var == "SAVE_MSG") {
+    return String(Common_HTML_SAVE_MSG);
+  } else {
+    return Proc_WebPage_grow_air(var);
+  }
+}
+
+/* WebPage function */
+void WebPage_grow_air(AsyncWebServerRequest *request) {
+  const static char LogLoc[] PROGMEM = "[Webserver:grow:air]";
+
+
+  /* Which kind of Request */
+  if(request->method() == HTTP_POST) {
+    
+    byte OutputId;
+    if(request->hasParam("output", true)) {
+      const AsyncWebParameter* param = request->getParam("output", true);
+      OutputId = param->value().toInt();
+      config.grow.air.output[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("power", true)) {
+      const AsyncWebParameter* param = request->getParam("power", true);
+      config.grow.air.power[OutputId] = param->value().toInt();
+    }
+    
+
+    if(request->hasParam("controlSensor", true)) {
+      const AsyncWebParameter* param = request->getParam("controlSensor", true);
+      config.grow.air.controlSensor[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("controlRead", true)) {
+      const AsyncWebParameter* param = request->getParam("controlRead", true);
+      config.grow.air.controlRead[OutputId] = param->value().toInt();
+    }
+    
+    //byte controlBy;
+    //if(request->hasParam("controlBy", true)) {
+      //const AsyncWebParameter* param = request->getParam("controlBy", true);
+      //controlBy = param->value().toInt();
+    //}
+    
+    if(request->hasParam("controlMode", true)) {
+      const AsyncWebParameter* param = request->getParam("controlMode", true);
+      config.grow.air.controlMode[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("min", true)) {
+      const AsyncWebParameter* param = request->getParam("min", true);
+      config.grow.air.min[OutputId] = param->value().toFloat();
+    }
+    
+    if(request->hasParam("max", true)) {
+      const AsyncWebParameter* param = request->getParam("max", true);
+      config.grow.air.max[OutputId] = param->value().toFloat();
+    }
+    
+    
+        
+    SaveConfig();
+      
+    Log.notice(F("%s config saved" CR), LogLoc);
+    
+    request->send_P(200, TEXT_HTML, Page_grow_air_HTML, Proc_WebPage_grow_air_POST);
+
+  } else {  
+    request->send_P(200, TEXT_HTML, Page_grow_air_HTML, Proc_WebPage_grow_air);
+  }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/*******************************************************************************
+ * grow water page
+ */
+String Proc_WebPage_grow_water(const String& var) {
+  const static char LogLoc[] PROGMEM = "[Webserver:grow:water(Proc)]";
+  
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 1);
+  } else if(Test_WebPage_grow_SUBNAV(var)) {
+    return Proc_WebPage_grow_SUBNAV(var, WEB_GROW_SUBNAV_WATER);
+  } else if(var == "WATER") {
+    String html;
+    
+    for(byte i = 0; i < Max_Outputs; i++) {
+      if(config.grow.water.configured[i] == true) {      
+        /* form */
+        html += F("<form method='post' action='/grow/water/'>");
+        /* OutputId */
+        html += F("<input type='hidden' name='output' value='");
+        html += i;
+        html += F("' required>"); 
+        
+        
+        html += F("<h2>");
+        html += config.system.output.name[i];
+        html += F("</h2>");
+        
+        /* Device */
+        html += F("<u>Device:</u><br><b>");
+        html += FPSTR(Output_Device_descr[config.system.output.device[i]]);
+        html += F(" (");
+        html += FPSTR(Output_Type_descr[config.system.output.type[i]]);
+        html += F(")</b><br>");
+        
+        
+        /*
+         * struct Config_Grow_Water {
+            bool configured[Max_Outputs];
+            byte output[Max_Outputs];
+            byte controlSensor[Max_Outputs];
+            byte controlRead[Max_Outputs];
+            byte controlMode[Max_Outputs];
+            byte pumpOn[Max_Sensors];
+            byte min[Max_Sensors];
+            byte max[Max_Sensors];
+            byte interval[Max_Sensors];
+            byte intervalUnit[Max_Sensors];
+          };
+         */
+        
+        /* min */
+        html += F("<u>Pump On</u><br><input class='inputShort' type='number' name='onTime' min='0' max='255' value='");
+        html += config.grow.water.onTime[i];
+        html += F("' required> Seconds<br>");
+                
+        /* hidden inputs for SensorId and ReadId*/
+        html += F("<input type='hidden' name='controlSensor' value='");
+        html += config.grow.water.controlSensor[i];
+        html += F("' id='controlSensor");
+        html += i;
+        html += F("'>"); 
+        
+        html += F("<input type='hidden' name='controlRead' value='");
+        html += config.grow.water.controlRead[i];
+        html += F("' id='controlRead");
+        html += i;
+        html += F("'>"); 
+        
+        
+        /* controlMode */
+        html += F("<u>Control mode</u><br><select name='controlMode'><option value='' selected>---</option>");
+        //<input type='range' name='power' min='0' max='100' value='");
+        html += Html_SelectOpt_array(CONTROL_WATER_MODE__TOTAL, Control_Water_Mode_descr, config.grow.water.controlMode[i]);
+        html += F("</select><br>");
+        
+        /* interval */
+        html += F("<u>Interval</u><br><input class='inputShort' type='number' name='interval' min='0' max='255' value='");
+        html += config.grow.water.interval[i];
+        html += F("' required> ");
+        
+        /* intervalUnit */
+        html += F("<select name='intervalUnit'>");
+        //<input type='range' name='power' min='0' max='100' value='");
+        //html += Html_SelectOpt_bool(config.grow.water.intervalUnit[i]);
+        
+        /* iterate through time scale units */
+        for(byte j = 0; j <= TIMESCALE_WEEK; j++) {
+          html += F("<option value='");
+          html += j;
+          html += F("'");
+          if(config.grow.water.intervalUnit[i] == j)
+            html += F(" selected");
+          html += F(">");
+          html += FPSTR(Timescale_descr[j]);
+          html += F("</option>");
+        }
+        
+        html += F("</select><br>");
+        
+        
+        /* controledBy */
+        html += F("<u>Controled by</u><br><select name='controlBy' id='ctrl");
+        html += i;
+        html += F("'onChange=\"GrowSelectControlSensorRead('ctrl");
+        html += i;
+        html += F("', 'controlSensor");
+        html += i;
+        html += F("', 'controlRead");
+        html += i;
+        html += F("');\"><option value='255:255' selected >---</option>");
+        
+        /* iterate through available sensors and offer useful values */
+        byte count = 0;
+        for(byte j = 0; j < Max_Sensors; j++) {
+          /* if sensor is configured */
+          if(config.system.sensor.type[j] > 0) {
+            /* we want to offer humidity, temperature, gas resistance */
+            for(byte k = 0; k < Max_Sensors_Read; k++) {
+              if(SensorIndex[config.system.sensor.type[j]].read[k] > 0) {
+                if((SensorIndex[config.system.sensor.type[j]].read[k] == SENSOR_READ_TYPE_SOILMOISTURE) || ((SensorIndex[config.system.sensor.type[j]].read[k] == SENSOR_READ_TYPE_RAW) && (config.system.sensor.rawConvert[j][k] == SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE))) {
+                    
+                    html += F("<option value='");
+                    
+                    /* put SensorId and ReadId into one colon sperated string. This we seperate later within javascript */
+                    html += j; // SensorId
+                    html += F(":");
+                    html += k; // ReadId
+                    
+                    html += F("'");
+                    if((config.grow.water.controlSensor[i] == j) && (config.grow.water.controlRead[i] == k))
+                      html += F(" selected");
+                    html += F(">");
+                    html += config.system.sensor.name[j];
+                    html += F(" - (");
+                    html += k;
+                    html += F(") ");
+                    
+                    if((SensorIndex[config.system.sensor.type[j]].read[k] == SENSOR_READ_TYPE_RAW) && (config.system.sensor.rawConvert[j][k] == SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE)) {
+                      html += FPSTR(Sensor_Convert_Raw_descr[config.system.sensor.rawConvert[j][k]]);
+                    } else {
+                      html += FPSTR(Sensor_Read_descr[SensorIndex[config.system.sensor.type[j]].read[k]]);
+                    }
+                    
+                    html += F(" (");
+                    html += Sensor_getCalibratedValue(j, k);
+                    html += F(" ");
+                    /* put unit into string */
+                    String unit;
+                    if((SensorIndex[config.system.sensor.type[j]].read[k] == SENSOR_READ_TYPE_RAW) && (config.system.sensor.rawConvert[j][k] == SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE)) {
+                      unit = FPSTR(Sensor_Convert_Raw_unit[config.system.sensor.rawConvert[j][k]]);
+                    } else {
+                      unit = FPSTR(Sensor_Read_unit[SensorIndex[config.system.sensor.type[j]].read[k]]);
+                    }
+                    /* to be able to replace % sign, which is already used by ESPAsyncWebserver's template engine
+                     * with html code for it */
+                    html += F(" ");
+                    unit.replace(F("%"), F("&#37;"));
+                    html += unit;
+                    html += F(")</option>");
+                    count++;
+                }
+              }
+            }
+          }
+        }
+        html += F("</select><br>");
+        
+        
+        /* min */
+        html += F("<u>Min</u><br><input class='inputShort' type='number' name='min' min='0' max='255' value='");
+        html += config.grow.water.min[i];
+        html += F("' required><br>");
+        
+        /* max */
+        html += F("<u>Max</u><br><input class='inputShort' type='number' name='max' min='0' max='255' value='");
+        html += config.grow.water.max[i];
+        html += F("' required><br>");
+        
+
+               
+        /* submit button */
+        html += F("<input type='submit' value='&#x1F4BE; Save settings' style='margin-top: 8px;'></form>");
+        
+        /* HR HORIZONTAL LINE TO SIGNAL END */
+        html += F("<hr>");
+        
+
+      }
+    }
+    
+    return html;
+  } else {
+    return String();
+  }
+}
+
+String Proc_WebPage_grow_water_POST(const String& var) {
+  /* This is the processor for POST
+   * Its exactly the same, just looking for SAVE_MSG string.
+   * If nothing matches, it calles the main Proc_WebPage_grow() 
+   * processor function, so all the other stuff like header and so
+   * on get replaced 
+   */
+  if(var == "SAVE_MSG") {
+    return String(Common_HTML_SAVE_MSG);
+  } else {
+    return Proc_WebPage_grow_water(var);
+  }
+}
+
+/* WebPage function */
+void WebPage_grow_water(AsyncWebServerRequest *request) {
+  const static char LogLoc[] PROGMEM = "[Webserver:grow:water]";
+
+
+  /* Which kind of Request */
+  if(request->method() == HTTP_POST) {
+    
+    byte OutputId;
+    if(request->hasParam("output", true)) {
+      const AsyncWebParameter* param = request->getParam("output", true);
+      OutputId = param->value().toInt();
+      config.grow.water.output[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("onTime", true)) {
+      const AsyncWebParameter* param = request->getParam("onTime", true);
+      config.grow.water.onTime[OutputId] = param->value().toInt();
+    }
+    
+
+    if(request->hasParam("controlSensor", true)) {
+      const AsyncWebParameter* param = request->getParam("controlSensor", true);
+      config.grow.water.controlSensor[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("controlRead", true)) {
+      const AsyncWebParameter* param = request->getParam("controlRead", true);
+      config.grow.water.controlRead[OutputId] = param->value().toInt();
+    }
+    
+    
+    if(request->hasParam("controlMode", true)) {
+      const AsyncWebParameter* param = request->getParam("controlMode", true);
+      config.grow.water.controlMode[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("min", true)) {
+      const AsyncWebParameter* param = request->getParam("min", true);
+      config.grow.water.min[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("max", true)) {
+      const AsyncWebParameter* param = request->getParam("max", true);
+      config.grow.water.max[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("interval", true)) {
+      const AsyncWebParameter* param = request->getParam("interval", true);
+      config.grow.water.interval[OutputId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("intervalUnit", true)) {
+      const AsyncWebParameter* param = request->getParam("intervalUnit", true);
+      config.grow.water.intervalUnit[OutputId] = param->value().toInt();
+    }
+    
+    SaveConfig();
+      
+    Log.notice(F("%s config saved" CR), LogLoc);
+    
+    request->send_P(200, TEXT_HTML, Page_grow_water_HTML, Proc_WebPage_grow_water_POST);
+
+  } else {  
+    request->send_P(200, TEXT_HTML, Page_grow_water_HTML, Proc_WebPage_grow_water);
+  }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/*******************************************************************************
+ * grow dashboards page
+ */
+String Proc_WebPage_grow_dashboard(const String& var) {
+  const static char LogLoc[] PROGMEM = "[Webserver:grow:dashboard(Proc)]";
+  
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 1);
+  } else if(Test_WebPage_grow_SUBNAV(var)) {
+    return Proc_WebPage_grow_SUBNAV(var, WEB_GROW_SUBNAV_DASHBOARD);
+  } else if(var == "DASHBOARD") {
+    String html;
+    return html;
+  } else {
+    return String();
+  }
+}
+
+String Proc_WebPage_grow_dashboard_POST(const String& var) {
+  /* This is the processor for POST
+   * Its exactly the same, just looking for SAVE_MSG string.
+   * If nothing matches, it calles the main Proc_WebPage_grow() 
+   * processor function, so all the other stuff like header and so
+   * on get replaced 
+   */
+  if(var == "SAVE_MSG") {
+    return String(Common_HTML_SAVE_MSG);
+  } else {
+    return Proc_WebPage_grow_dashboard(var);
+  }
+}
+
+/* WebPage function */
+void WebPage_grow_dashboard(AsyncWebServerRequest *request) {
+  const static char LogLoc[] PROGMEM = "[Webserver:grow:dashboard]";
+
+
+  /* Which kind of Request */
+  if(request->method() == HTTP_POST) {
+    
+    
+    SaveConfig();
+      
+    Log.notice(F("%s config saved" CR), LogLoc);
+    
+    request->send_P(200, TEXT_HTML, Page_grow_dashboard_HTML, Proc_WebPage_grow_dashboard_POST);
+
+  } else {  
+    request->send_P(200, TEXT_HTML, Page_grow_dashboard_HTML, Proc_WebPage_grow_dashboard);
+  }
+}
diff --git a/include/Webserver/Page_grow_HTML.h b/include/Webserver/Page_grow_HTML.h
new file mode 100644
index 0000000..e0c5bf7
--- /dev/null
+++ b/include/Webserver/Page_grow_HTML.h
@@ -0,0 +1,102 @@
+/*
+ * 
+ * include/Webserver/Page_grow_HTML.h - grow page HTML header file
+ * 
+ * 
+ *
+ */
+
+/* submenu SUBNAV */
+const char Page_grow_HTML_SUBNAV[] PROGMEM = R"(<ul class='subnav'>
+  <li><a class='%ACTIVE_SUBNAV_GENERAL%' href='/grow/'>&#x1F6E0;&#xFE0F; General</a></li>
+  <li><a class='%ACTIVE_SUBNAV_LIGHT%' href='/grow/light/'>&#x1F4A1; Light</a></li>
+  <li><a class='%ACTIVE_SUBNAV_AIR%' href='/grow/air/'>&#x1F300; Air</a></li>
+  <li><a class='%ACTIVE_SUBNAV_WATER%' href='/grow/water/'>&#x1F4A7; Water</a></li>
+  <li><a class='%ACTIVE_SUBNAV_DASHBOARD%' href='/grow/dashboard/' >&#x1F5A5;&#xFE0F; Dashboard</a></li>
+</ul>)";
+
+/* /grow/ main page */
+const char Page_grow_HTML[] PROGMEM = R"(%HEADER%
+%SUBNAV%
+%SAVE_MSG%
+<p>here you can set grow stuff<br></p><form method='post' action='/grow/'>
+
+<u>Grow name:</u><br>
+<input type='text' name='name' maxlength='31' value='%GROWNAME%' required><br>
+<input type='hidden' id='start' name='start' value='%GROWSTART_EPOCH%' required>
+<u>Grow start date:</u><br>
+<input type='date' id='GrowStart_sel' onChange='convertDateToEpoch("GrowStart_sel", "start");' value='%GROWSTART%' ><br>
+<u>Vegetation duration:</u><br>
+<input class='inputShort' type='number' name='daysVeg' min='0' max='255' value='%DAYS_VEG%' required> Days<br>
+<u>Bloom duration:</u><br>
+<input class='inputShort' type='number' name='daysBloom' min='0' max='255' value='%DAYS_BLOOM%' required> Days<br>
+
+<br>
+<input type='submit' value='&#x1F4BE; Save settings'>
+</form>
+%FOOTER% )";
+
+/* /grow/light/  page */
+const char Page_grow_light_HTML[] PROGMEM = R"(%HEADER%
+%SUBNAV%
+%SAVE_MSG%
+<p>here you can set light stuff<br></p>
+
+
+%LIGHT%
+
+
+
+
+
+
+%FOOTER% )";
+
+
+/* /grow/air/  page */
+const char Page_grow_air_HTML[] PROGMEM = R"(%HEADER%
+%SUBNAV%
+%SAVE_MSG%
+<p>here you can set air stuff<br></p>
+
+
+%AIR%
+
+
+
+
+
+
+%FOOTER% )";
+
+/* /grow/water/  page */
+const char Page_grow_water_HTML[] PROGMEM = R"(%HEADER%
+%SUBNAV%
+%SAVE_MSG%
+<p>here you can set water stuff<br></p>
+
+
+%WATER%
+
+
+
+
+
+
+%FOOTER% )";
+
+/* /grow/dashboard/  page */
+const char Page_grow_dashboard_HTML[] PROGMEM = R"(%HEADER%
+%SUBNAV%
+%SAVE_MSG%
+<p>here you can set dashboard stuff<br></p>
+
+
+%DASHBOARD%
+
+
+
+
+
+
+%FOOTER% )";
diff --git a/include/Webserver/Page_root.h b/include/Webserver/Page_root.h
new file mode 100644
index 0000000..dbdd5dc
--- /dev/null
+++ b/include/Webserver/Page_root.h
@@ -0,0 +1,28 @@
+/*
+ * 
+ * include/Webserver/Page_root.h - root page header file
+ * 
+ * 
+ *
+ */
+
+#include "Page_root_HTML.h"
+
+
+
+// https://techtutorialsx.com/2018/07/23/esp32-arduino-http-server-template-processing-with-multiple-placeholders/
+String Proc_WebPage_root(const String& var) {
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var);
+  } else if(var == "LOL") {
+    return String("Nice");
+  } else if(var == "LOL") {
+    return String("Jojoojo :)");
+  } else {
+    return String();
+  }
+}
+
+void WebPage_root(AsyncWebServerRequest *request) { 
+  request->send_P(200, TEXT_HTML, Page_root_HTML, Proc_WebPage_root);
+}
diff --git a/include/Webserver/Page_root_HTML.h b/include/Webserver/Page_root_HTML.h
new file mode 100644
index 0000000..8edcaaa
--- /dev/null
+++ b/include/Webserver/Page_root_HTML.h
@@ -0,0 +1,13 @@
+/*
+ * 
+ * include/Webserver/Page_root_HTML.h - root page HTML header file
+ * 
+ * 
+ *
+ */
+
+
+const char Page_root_HTML[] PROGMEM = R"EOF(%HEADER%
+<h2>&#x1F331; Hello world!</h2>
+<a href='/api/sensor/'>Sensor data -> /api/sensor/</a>
+%FOOTER% )EOF";
diff --git a/include/Webserver/Page_system.h b/include/Webserver/Page_system.h
new file mode 100644
index 0000000..578824c
--- /dev/null
+++ b/include/Webserver/Page_system.h
@@ -0,0 +1,1721 @@
+/*
+ * 
+ * include/Webserver/Page_system.h - system settings page header file
+ * 
+ * 
+ *
+ */
+
+
+#include "Page_system_HTML.h"
+
+/* global runtime variables */
+
+/* VERY VERY DIRTY WORKAROUND
+ * I have the problem, that I cannot pass a parameter I receive from a http
+ * request to it's template processor. In my case i want to edit an output,
+ * the user should click an edit button on the system/output overview page.
+ * I am lazy so i want to reuse the output_add page, because it is quite 
+ * kinda exactly the same. so i want to call GET /system/output/add?edit=ID
+ * I have searched and came to the conclusion, that at this point i see no
+ * other way then giving the parameter I need, the outputId, to an global 
+ * variable, so the template processor can read it. 
+ */ 
+byte tmpParam_editOutputId = 255;
+byte tmpParam_editSensorId = 255;
+byte tmpParam_calibrateSensorId = 255;
+
+
+/* subnav processor */
+const byte WEB_SYSTEM_SUBNAV_GENERAL = 1;
+const byte WEB_SYSTEM_SUBNAV_SENSOR = 2;
+const byte WEB_SYSTEM_SUBNAV_OUTPUT = 3;
+const byte WEB_SYSTEM_SUBNAV_UPDATE = 4;
+const byte WEB_SYSTEM_SUBNAV_RESTART = 5;
+const byte WEB_SYSTEM_SUBNAV_WIPE = 6;
+
+bool Test_WebPage_system_SUBNAV(const String& var) {
+  if(
+    (var == "SUBNAV") ||
+    (var == "ACTIVE_SUBNAV_GENERAL") ||
+    (var == "ACTIVE_SUBNAV_SENSOR") ||
+    (var == "ACTIVE_SUBNAV_OUTPUT") ||
+    (var == "ACTIVE_SUBNAV_UPDATE") ||
+    (var == "ACTIVE_SUBNAV_RESTART") ||
+    (var == "ACTIVE_SUBNAV_WIPE")) {
+    return true;
+  } else {
+    return false;
+  }
+}
+
+/*
+ * Proc_WebPage_system_SUBNAV - subnav processor for system
+ * this function works as same as AddHeaderFooter from Common.h
+ * byte activeSubnav:
+ *   1 - Output
+ *   2 - Update
+ *   3 - Restart
+ *   4 - Wipe
+ */
+String Proc_WebPage_system_SUBNAV(const String& var, byte activeSubnav = 0) {
+  String activeSubnav_ClassName = "activeNav";
+  if(var == "SUBNAV") {
+    return String(Page_system_HTML_SUBNAV);
+  } else if((var == "ACTIVE_SUBNAV_GENERAL") && (activeSubnav == WEB_SYSTEM_SUBNAV_GENERAL)) {
+    return activeSubnav_ClassName;
+  } else if((var == "ACTIVE_SUBNAV_SENSOR") && (activeSubnav == WEB_SYSTEM_SUBNAV_SENSOR)) {
+    return activeSubnav_ClassName;
+  } else if((var == "ACTIVE_SUBNAV_OUTPUT") && (activeSubnav == WEB_SYSTEM_SUBNAV_OUTPUT)) {
+    return activeSubnav_ClassName;
+  } else if((var == "ACTIVE_SUBNAV_UPDATE") && (activeSubnav == WEB_SYSTEM_SUBNAV_UPDATE)) {
+    return activeSubnav_ClassName;
+  } else if((var == "ACTIVE_SUBNAV_RESTART") && (activeSubnav == WEB_SYSTEM_SUBNAV_RESTART)) {
+    return activeSubnav_ClassName;
+  } else if((var == "ACTIVE_SUBNAV_WIPE") && (activeSubnav == WEB_SYSTEM_SUBNAV_WIPE)) {
+    return activeSubnav_ClassName;
+  } else {
+    return String();
+  }
+}
+
+/*******************************************************************************
+ * Main system page
+ */
+// https://techtutorialsx.com/2018/07/23/esp32-arduino-http-server-template-processing-with-multiple-placeholders/
+String Proc_WebPage_system(const String& var) {
+  const static char LogLoc[] PROGMEM = "[Webserver:system(Proc)]";
+  /* This is a processor function, which returns a string. 
+   * We check if var contains one of our placeholders from the template.
+   * If we hit a placeholder, we just return the String we want.
+   * 
+   * TestHeaderFooter() Is kinda a processor too, but only checks for 
+   * header specific placeholders.
+   */
+  
+  //Log.verbose(F("%s var: %s" CR), LogLoc, var);
+  
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 2);
+  } else if(Test_WebPage_system_SUBNAV(var)) {
+    return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_GENERAL);
+  } else if(var == "NTPOFFSET") {
+    return String(config.system.ntpOffset);
+    
+  } else if(var == "MAINTDUR") {
+    return String(config.system.maintenanceDuration);
+  } else if(var == "ESP32CAM") {
+    return String(config.system.esp32cam);
+  } else if(var == "HTTPLOGSERIAL") {
+    return Html_SelectOpt_bool(config.system.httpLogSerial);
+  } else if(var == "RTC_STATUS") {
+    /* show warn sign if rtcError  is true (there was an error), otherwise green checkmark */
+    if(config.system.rtc > 0) {
+      if(rtcError == true) {
+        return F(" &#x26A0;&#xFE0F; ");
+      } else {
+        return F(" &#x2705; ");
+      }
+    } else {
+      return String();
+    }
+  } else if(var == "RTC_AVAILABLE") {
+    return Html_SelectOpt_array(RTCs_total, RTCs_descr, config.system.rtc);
+  } else if(var == "TIME2FS") {
+    return Html_SelectOpt_bool(config.system.time2fs);
+  } else if(var == "PWMFREQ") {
+    return String(config.system.pwmFreq);
+  } else {
+    return String();
+  }
+}
+
+String Proc_WebPage_system_POST(const String& var) {
+  /* This is the processor for POST
+   * Its exactly the same, just looking for SAVE_MSG string.
+   * If nothing matches, it calles the main Proc_WebPage_system() 
+   * processor function, so all the other stuff like header and so
+   * on get replaced 
+   */
+  if(var == "SAVE_MSG") {
+    return String(Common_HTML_SAVE_MSG);
+  } else {
+    return Proc_WebPage_system(var);
+  }
+}
+
+String Proc_WebPage_system_POST_ERR(const String& var) {
+  if(var == "SAVE_MSG") {
+    return String(Common_HTML_SAVE_MSG_ERR);
+  } else {
+    return Proc_WebPage_system(var);
+  }
+}
+
+/* WebPage function */
+void WebPage_system(AsyncWebServerRequest *request) {
+  const static char LogLoc[] PROGMEM = "[Webserver:system]";
+  
+  /* when changing httpLogSerial it requires a restart to take effect 
+   * for this we keep the old val to compare it if it got changed
+   * to notice user for a restart */
+  bool old_httpLogSerial = config.system.httpLogSerial;
+  byte old_rtc = config.system.rtc;
+  short old_ntpOffset;
+
+  /* Which kind of Request */
+  if(request->method() == HTTP_POST) {
+    
+    if(request->hasParam("ntp", true)) {
+      const AsyncWebParameter* param = request->getParam("ntp", true);
+      config.system.ntp = param->value().toInt();
+    }
+           
+    if(request->hasParam("ntpOffset", true)) {
+      const AsyncWebParameter* param = request->getParam("ntpOffset", true);
+      //Log.verbose(F("%s POST[%s]: %s" CR), LogLoc, param->value().c_str());
+      old_ntpOffset = config.system.ntpOffset;
+      config.system.ntpOffset = param->value().toInt();
+      if((config.system.ntp == true) && (old_ntpOffset != config.system.ntpOffset)) {
+        // trigger ntp offset update
+        updateNtpOffset = true;
+      }
+
+    }
+    
+    if(request->hasParam("maintenanceDuration", true)) {
+      const AsyncWebParameter* param = request->getParam("maintenanceDuration", true);
+      config.system.maintenanceDuration = param->value().toInt();
+    }
+    
+    if(request->hasParam("esp32cam", true)) {
+      const AsyncWebParameter* param = request->getParam("esp32cam", true);
+      //config.system.esp32cam = param->value().toInt();
+      strlcpy(config.system.esp32cam, param->value().c_str(), sizeof(config.system.esp32cam));
+    }
+    
+    if(request->hasParam("httpLogSerial", true)) {
+      const AsyncWebParameter* param = request->getParam("httpLogSerial", true);
+      config.system.httpLogSerial = param->value().toInt();
+      if( old_httpLogSerial != config.system.httpLogSerial) {
+        needRestart = true;
+      }
+    }
+    
+    
+    if(request->hasParam("rtc", true)) {
+      const AsyncWebParameter* param = request->getParam("rtc", true);
+      config.system.rtc = param->value().toInt();
+      if( old_rtc != config.system.rtc) {
+        needRestart = true;
+        if(config.system.rtc > 0)
+          rtcError = true;
+      }
+    }
+    
+    if(request->hasParam("time2fs", true)) {
+      const AsyncWebParameter* param = request->getParam("time2fs", true);
+      config.system.time2fs = param->value().toInt();
+    }
+    
+    if(request->hasParam("pwmFreq", true)) {
+      const AsyncWebParameter* param = request->getParam("pwmFreq", true);
+      config.system.pwmFreq = param->value().toInt();
+      #ifdef ESP8266
+      /* set pwm frequency global for ESP8266. 
+       * ESP32 pwm frequency setting is done withing CanGrow_Output / Init */
+      analogWriteFreq(config.system.pwmFreq);
+      #endif
+    }
+    
+    if(SaveConfig()) {
+      // we need a restart to apply the new settings
+      
+      Log.notice(F("%s config saved" CR), LogLoc);
+      
+      request->send_P(200, "text/html", Page_system_HTML, Proc_WebPage_system_POST);
+    } else {
+      Log.error(F("%s ERROR while saving config" CR), LogLoc);
+      request->send_P(200, TEXT_HTML, Page_system_HTML, Proc_WebPage_system_POST_ERR);
+    }
+  } else {  
+    request->send_P(200, TEXT_HTML, Page_system_HTML, Proc_WebPage_system);
+  }
+}
+
+
+/*******************************************************************************
+ * Subpage restart
+ */
+String Proc_WebPage_system_restart(const String& var) {
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 2);
+  } else if(Test_WebPage_system_SUBNAV(var)) {
+    return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_RESTART);
+  } else if(var == "RESTART_MSG") {
+    return String(Page_system_restart_HTML_RESTART_MSG);
+  } else {
+    return String();
+  }
+}
+
+String Proc_WebPage_system_restart_POST(const String& var) {
+  if(var == "RESTART_MSG") {
+    return String(Page_system_restart_HTML_RESTART_MSG_POST);
+  } else {
+      return Proc_WebPage_system_restart(var);
+  }  
+}
+
+void WebPage_system_restart(AsyncWebServerRequest *request) {
+  const static char LogLoc[] PROGMEM = "[Webserver:system:restart]";
+  if(request->method() == HTTP_POST) {
+    if(request->hasParam("confirmed", true)) {
+      doRestart = false;
+    }
+    //request->send_P(200, TEXT_HTML, Page_system_restart_HTML, Proc_WebPage_system_restart_POST);
+    
+    /* Add custom header for redirect after timeout 
+     * https://github.com/mathieucarbou/ESPAsyncWebServer?tab=readme-ov-file#send-large-webpage-from-progmem-containing-templates-and-extra-headers */
+    AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", Page_system_restart_HTML, Proc_WebPage_system_restart_POST);
+    /* return Refresh header to redirect to root page after restart */
+    if(config.wifi.dhcp == true) {
+      response->addHeader("Refresh","20; url=http://" + WiFi.localIP().toString());
+    } else {
+      response->addHeader("Refresh","20; url=http://" + String(IP2Char(config.wifi.ip)));
+    }
+        
+    request->send(response);
+    
+    if(request->hasParam("confirmed", true)) {
+      Log.notice(F("%s POST[confirmed]: is set, triggering restart" CR), LogLoc);
+      
+      // set global var doRestart to true causes a restart
+      doRestart = true;
+    }
+    
+  } else {
+    request->send_P(200, TEXT_HTML, Page_system_restart_HTML, Proc_WebPage_system_restart);
+  }
+  
+}
+
+
+
+/*******************************************************************************
+ * Subpage update
+ */
+
+// https://github.com/mathieucarbou/ESPAsyncWebServer/blob/main/docs/index.md#setting-up-the-server
+void WebPage_system_update_ApplyUpdate(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){
+  const static char LogLoc[] PROGMEM = "[Webserver:system:update:ApplyUpdate]";
+    if(!index){
+      Log.notice(F("%s Update Start: %s" CR), LogLoc, filename.c_str());
+      
+      // https://github.com/me-no-dev/ESPAsyncWebServer/issues/455#issuecomment-451728099
+      // workaround for bug with ESP32
+      #ifdef ESP8266
+      Update.runAsync(true);
+      #endif
+      if(!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)){
+        Update.printError(Serial);
+      }
+    }
+    if(!Update.hasError()){
+      if(Update.write(data, len) != len){
+        Update.printError(Serial);
+      }
+    }
+    if(final){
+      if(Update.end(true)){
+        Log.notice(F("%s Update Success: %uB" CR), LogLoc, index+len);
+      } else {
+        Log.error(F("%s FAILED Update:" CR), LogLoc);
+        Update.printError(Serial);
+      }
+    }
+  }
+
+String Proc_WebPage_system_update(const String& var) {
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 2);
+  } else if(Test_WebPage_system_SUBNAV(var)) {
+    return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_UPDATE);
+  } else {
+    return String();
+  }
+}
+
+/* After an update.bin file was uploaded*/
+String Proc_WebPage_system_update_POST(const String& var) {
+  if(var == "CONFIGWIFI_IP") {
+    if(config.wifi.dhcp == true) {
+      return WiFi.localIP().toString();
+    } else {
+      return String(IP2Char(config.wifi.ip));
+    }
+  } else {
+    return String();
+  }
+}
+
+void WebPage_system_update(AsyncWebServerRequest *request) {
+  if(request->method() == HTTP_POST) {
+    doRestart = !Update.hasError();
+    // when doRestart is true, deliver Page_system_update_HTML_POST
+    // otherwise Page_system_update_HTML_POST_FAILED
+    AsyncWebServerResponse *response = request->beginResponse_P(200, TEXT_HTML, doRestart?Page_system_update_HTML_POST:Page_system_update_HTML_POST_FAILED, Proc_WebPage_system_update_POST);
+    response->addHeader(F("Connection"), F("close"));
+    request->send(response);
+  } else {
+    request->send_P(200, TEXT_HTML, Page_system_update_HTML, Proc_WebPage_system_update);
+  }
+}
+
+
+/*******************************************************************************
+ * Subpage wipe
+ */
+String Proc_WebPage_system_wipe(const String& var) {
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 2);
+  } else if(Test_WebPage_system_SUBNAV(var)) {
+    return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_WIPE);
+  } else if(var == "WIPE_MSG") {
+    return String(Page_system_wipe_HTML_WIPE_MSG);
+  } else {
+    return String();
+  }
+}
+
+String Proc_WebPage_system_wipe_POST(const String& var) {
+  if(var == "WIPE_MSG") {
+    return String(Page_system_wipe_HTML_WIPE_MSG_POST);
+  } else {
+      return Proc_WebPage_system_wipe(var);
+  }  
+}
+
+void WebPage_system_wipe(AsyncWebServerRequest *request) {
+  const static char LogLoc[] PROGMEM = "[Webserver:system:wipe]";
+  if(request->method() == HTTP_POST) {
+    request->send_P(200, TEXT_HTML, Page_system_wipe_HTML, Proc_WebPage_system_wipe_POST);
+    
+    if(request->hasParam("confirmed", true)) {
+      Log.notice(F("%s POST[confirmed]: is set, triggering wipe / factory reset" CR), LogLoc);
+      LFS_Format();
+      Log.notice(F("%s triggering restart" CR), LogLoc);
+      doRestart = true;
+    }
+    
+  } else {
+    request->send_P(200, TEXT_HTML, Page_system_wipe_HTML, Proc_WebPage_system_wipe);
+  }
+  
+}
+
+
+/*******************************************************************************
+ * Subpage output
+ */
+String Proc_WebPage_system_output(const String& var) {
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 2);
+  } else if(Test_WebPage_system_SUBNAV(var)) {
+    return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_OUTPUT);
+  } else if(var == "ADD_DISABLED") {
+    /* check if there is a free Output Id. Give_Free_OutputId returns 255 if no id available, otherwise it
+     * gives us the next free id. Here we check if the given ID is greater then Max_Outputs. This will also
+     * reflect a valid result, if there is a free id left or not. */
+    if(Give_Free_OutputId() > Max_Outputs ) {
+      return F("disabled force_hide");
+    } else {
+      return String();
+    }
+    
+  } else if(var == "TR_TD") {
+    // build table body
+    // i dont know a better way at the moment. if you do, please tell me!
+    String html;
+    for(byte i=0; i < Max_Outputs; i++) {
+      if(config.system.output.type[i] > 0) {
+        
+        html += F("<tr><td>");
+        /* show warn sign if outputStatus is false (uninitialized), otherwise green checkmark */
+        if(outputStatus[i] == false) {
+          html += F(" &#x26A0;&#xFE0F;");
+        } else {
+          html += F(" &#x2705;");
+        }
+        /* bit spacing after the status icon */
+        html += F("&nbsp;&nbsp;");
+        html += F("</td><td>");
+        html += i;
+
+        html += F("</td><td>");
+        html += config.system.output.name[i];
+        html += F("</td><td>");
+        html += FPSTR(Output_Type_descr[config.system.output.type[i]]);
+        
+        if((config.system.output.type[i] == OUTPUT_TYPE_GPIO) || ( (config.system.output.type[i] == OUTPUT_TYPE_I2C) && (config.system.output.i2c_type[i] > 0) )) {
+          html += F(" (");
+        
+          switch(config.system.output.type[i]) {
+            case OUTPUT_TYPE_GPIO:
+              html += GPIOindex[config.system.output.gpio[i]].gpio;
+            break;
+            
+            case OUTPUT_TYPE_I2C:
+              html += OutputI2Cindex[config.system.output.i2c_type[i]].name;
+            break;
+            
+            default:
+            break;
+          }
+        
+          html += F(")");
+        }
+        
+        html += F("</td><td>");
+        html += FPSTR(Output_Device_descr[config.system.output.device[i]]);
+        html += F("</td><td>");
+        
+        if(config.system.output.enabled[i] > 0) {
+          html += F("&nbsp;&#x1F7E2;&nbsp;");
+        } else {
+          html += F("&nbsp;&#x1F534;&nbsp;&nbsp;");
+        }
+        
+        html += F("</td><td>");
+
+        // edit button
+        html += F("<form class='linkForm' action='/system/output/add' method='get'>");
+        html += F("<input type='hidden' name='edit' value='");
+        html += i;
+        html += F("'>");
+        html += F("<input type='submit' value='&#x270F;&#xFE0F;' title='Edit'></form> ");
+        
+        
+        // delete button
+        html += F("<form class='linkForm' action='/system/output/' method='post'>");
+        html += F("<input type='hidden' name='delete_output' value='");
+        html += i;
+        html += F("'>");
+        html += F("<input type='submit' value='&#x274C;' onclick=\"return confirmDelete('");
+        html += config.system.output.name[i];;
+        html += F("')\"  title='Delete'></form>");
+       
+        html += F("</td></tr>");
+      }
+    }
+    
+    return html;
+  } else{
+    return String();
+  }
+}
+
+String Proc_WebPage_system_output_POST(const String& var) {
+  if(var == "SAVE_MSG") {
+    return String(Common_HTML_SAVE_MSG);
+  }  else {
+      return Proc_WebPage_system_output(var);
+  }  
+}
+
+void WebPage_system_output(AsyncWebServerRequest *request) {
+  if(request->method() == HTTP_POST) {
+    if(request->hasParam("delete_output", true)) {
+      byte outputId;
+      
+      const AsyncWebParameter* param = request->getParam("delete_output", true);
+      
+      outputId = param->value().toInt();
+      
+      /* remove grow objects */
+      Output_Device_Grow_AddRemove(outputId, 1);
+      
+      // we ensure that every field is empty
+      config.system.output.type[outputId] = 0;
+      config.system.output.device[outputId] = 0;
+      // set every field of char array to 0x00 with memset
+      memset(config.system.output.name[outputId], '\0', sizeof config.system.output.name[outputId]);
+      config.system.output.enabled[outputId] = 0;
+      config.system.output.gpio[outputId] = 0;
+      config.system.output.gpio_pwm[outputId] = 0;
+      config.system.output.invert[outputId] = 0;
+      config.system.output.i2c_type[outputId] = 0;
+      memset(config.system.output.webcall_host[outputId], '\0', sizeof config.system.output.webcall_host[outputId]);
+      memset(config.system.output.webcall_path_on[outputId], '\0', sizeof config.system.output.webcall_path_on[outputId]);
+      memset(config.system.output.webcall_path_off[outputId], '\0', sizeof config.system.output.webcall_path_off[outputId]);
+      
+      #ifdef DEBUG
+      SaveConfig(true);
+      #endif
+      SaveConfig();
+    }
+    
+    request->send_P(200, TEXT_HTML, Page_system_output_HTML, Proc_WebPage_system_output_POST);
+    
+        
+  } else {
+    if(request->hasParam("success")) {
+      // when GET param success is present, we use the _POST processor for the save message
+      request->send_P(200, TEXT_HTML, Page_system_output_HTML, Proc_WebPage_system_output_POST);
+    } else {
+      request->send_P(200, TEXT_HTML, Page_system_output_HTML, Proc_WebPage_system_output);
+    }
+  }
+  
+}
+
+/*******************************************************************************
+ * Subpage output add
+ */
+
+
+/* returns select <option> list of available output types */
+String Html_SelOpt_type_WebPage_system_output_i2c_add(byte selectId = 255) {
+  String html;
+  // go through all available Output I2C modules, skip 0 because it means unconfigured
+  for(byte i = 1; i <= OutputI2Cindex_length; i++) {
+    html += F("<option value='");
+    html += i;
+    html += F("'");
+    if(i == selectId) {
+      html += F(" selected");
+    }
+    html += F(">");
+    html += OutputI2Cindex[i].name;
+    html += F("</option>");
+  }
+  return html;
+}
+
+String Js_I2cAddr_Array_WebPage_system_output_i2c_add() {
+  const static char LogLoc[] PROGMEM = "[Webserver:system:output:add:i2c:Js_I2cAddr_Array]";
+  /* bit hacky, bit dirty, but may work
+   * here we return a 2-dimensional javascript array. the returned stuff
+   * gets directly injected in the template into a js function
+   */
+  String js;
+  
+  /* iterate through all OutputI2Cindex in index*/
+  for(byte i = 1; i <= OutputI2Cindex_length; i++) {
+    // name
+    js += F("[");
+    /* iterate through all available addresses */
+    for(byte j = 0; j < OutputI2Cindex[i].max; j++) {
+      js += F("['0x");
+      js += String(Output_I2C_Addr_Init_Update(i, j, 0, OUPUT_I2C_AIU_MODE_ADDR), HEX);
+      // value
+      js += F("', '");
+      js += j;
+      // used
+      
+      js += F("', [");
+      /* check I2C module ports available */
+      for(byte k = 0; k < OUTPUT_TYPE_I2C_MAX_PORTS; k++) {
+        
+        /* When this Port of the module offers a value */
+        if(OutputI2Cindex[i].port[k] > 0) {
+          js += F("'");
+          /* check if I2C module and port are used in config */
+          for(byte l = 0; l < Max_Outputs; l++) {
+            if((config.system.output.type[l] == OUTPUT_TYPE_I2C) && (config.system.output.i2c_type[l] == i) &&
+               (config.system.output.i2c_addr[l] == j) && (config.system.output.i2c_port[l] == k)) {
+              js += 1;
+              /* exit loop here */
+              l = Max_Outputs + 1;
+            } 
+          }
+          js += F("',");
+        }
+        
+      }
+      
+      js += F("]],");
+    }
+  js += F("],\n");
+  }
+  
+  return js;
+}
+
+
+
+
+String Proc_WebPage_system_output_add(const String& var) {
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 2);
+  } else if(Test_WebPage_system_SUBNAV(var)) {
+    return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_OUTPUT);
+  } else if(var == "ACTION") {
+    return F("&#10133; Add");
+    
+  } else if(var == "OUTPUT_ID") {
+    // we check which id is free. A free ID as type == 0
+    return String(Give_Free_OutputId());
+    
+    
+  } else if(var == "OUTPUT_TYPE") {
+    return Html_SelectOpt_array(OUTPUT_TYPE__TOTAL, Output_Type_descr);
+    
+  } else if(var == "OUTPUT_DEVICE") {
+    return Html_SelectOpt_array(OUTPUT_DEVICE__TOTAL, Output_Device_descr);
+    
+  } else if(var == "OUTPUT_ENABLED") {
+    return Html_SelectOpt_bool();
+    
+  } else if(var == "INVERT") {
+    return Html_SelectOpt_bool();
+  } else if(var == "GPIO_INDEX") {
+    return Html_SelectOpt_GPIOindex();
+    
+  } else if(var == "GPIO_PWM") {
+    return Html_SelectOpt_bool();
+    
+  } else if(var == "I2C_TYPE") {
+    return Html_SelOpt_type_WebPage_system_output_i2c_add();
+  } else if(var == "REPLACE_I2CADDR_JS") {
+ 
+    return Js_I2cAddr_Array_WebPage_system_output_i2c_add();
+    
+  } else {
+    return String();
+  }
+}
+
+String Proc_WebPage_system_output_addEdit(const String& var) {
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 2);
+  } else if(Test_WebPage_system_SUBNAV(var)) {
+    return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_OUTPUT);
+  } else if(var == "ACTION") {
+    return F("&#x270F;&#xFE0F; Edit");
+    
+  } else if(var == "EDIT_MODE") {
+    return F("editmode");
+  } else if(var == "OUTPUT_ID") {
+    // return the outputId we got from GET .../add?edit=ID
+    // dirty workaround to put this in a global variable
+    return String(tmpParam_editOutputId);
+    
+  } else if(var == "OUTPUT_TYPE") {
+    return Html_SelectOpt_array(OUTPUT_TYPE__TOTAL, Output_Type_descr, config.system.output.type[tmpParam_editOutputId]);
+    
+  } else if(var == "OUTPUT_DEVICE") {
+    return Html_SelectOpt_array(OUTPUT_DEVICE__TOTAL, Output_Device_descr, config.system.output.device[tmpParam_editOutputId]);
+    
+  } else if(var == "OUTPUT_NAME") {
+    // "escape" % character, because it would break the template processor.
+    // tasmote webcall for example has percentage char in its path
+    String outputName = config.system.output.name[tmpParam_editOutputId];;
+    outputName.replace(F("%"), F("&#37;"));
+    return outputName;
+    
+  } else if(var == "OUTPUT_ENABLED") {
+    return Html_SelectOpt_bool(config.system.output.enabled[tmpParam_editOutputId]);
+    
+  } else if(var == "INVERT") {
+    return Html_SelectOpt_bool(config.system.output.invert[tmpParam_editOutputId]);
+    
+  } else if(var == "GPIO_INDEX") {
+    return Html_SelectOpt_GPIOindex(config.system.output.gpio[tmpParam_editOutputId]);
+    
+  } else if(var == "GPIO_PWM") {
+    return Html_SelectOpt_bool(config.system.output.gpio_pwm[tmpParam_editOutputId]);
+    
+  } else if(var == "I2C_TYPE") {
+    //return String(config.system.output.i2c_type[tmpParam_editOutputId]);
+    //Html_SelectOpt_array(OutputI2Cindex_length, OutputI2Cindex[].name, config.system.output.i2c_type[tmpParam_editOutputId]);
+    return Html_SelOpt_type_WebPage_system_output_i2c_add(config.system.output.i2c_type[tmpParam_editOutputId]);
+  } else if(var == "REPLACE_I2CADDR_JS") {
+ 
+    return Js_I2cAddr_Array_WebPage_system_output_i2c_add();
+    
+  } else if(var == "I2C_SAVED") {
+    
+    /* Add bit javascript to ensure the saved value is selected */
+    String js;
+    js += F("showSelect('type_sel', 'type_', 'hidden'); SystemOutputAddselectRequired('type_sel');");
+    
+    //js += F("document.getElementById('i2c_type').value='");
+    //js += config.system.output.i2c_type[tmpParam_editOutputId];
+    //js += F("';\n");
+    
+    js += F("SystemOutputAdd_replaceI2cAddr('i2c_type', 'i2c_addr');");
+    
+    js += F("document.getElementById('i2c_addr').value='");
+    js += config.system.output.i2c_addr[tmpParam_editOutputId];
+    js += F("';\n");
+    
+    js += F("SystemOutputAdd_replaceI2cPort('i2c_type', 'i2c_addr', 'i2c_port');\n");
+    
+    js += F("document.getElementById('i2c_port').value='");
+    js += config.system.output.i2c_port[tmpParam_editOutputId];
+    js += F("';\n");
+    
+    
+    //js += "SystemOutputAdd_replaceI2cPort('i2c_type', 'i2c_addr', 'i2c_port');";
+    return js;
+
+    
+  } else if(var == "WEBCALL_HOST") {
+    return String(config.system.output.webcall_host[tmpParam_editOutputId]);
+    
+  } else if(var == "WEBCALL_PATH_ON") {
+    String webcallPathOn = config.system.output.webcall_path_on[tmpParam_editOutputId];
+    webcallPathOn.replace(F("%"), F("&#37;"));
+    return webcallPathOn;
+    
+  } else if(var == "WEBCALL_PATH_OFF") {
+    String webcallPathOff = config.system.output.webcall_path_off[tmpParam_editOutputId];
+    webcallPathOff.replace(F("%"), F("&#37;"));
+    return webcallPathOff;
+    
+  } else if(
+           ((var == "CLASS_TYPE_1") && (config.system.output.type[tmpParam_editOutputId] == 1)) ||
+           ((var == "CLASS_TYPE_2") && (config.system.output.type[tmpParam_editOutputId] == 2)) ||
+           ((var == "CLASS_TYPE_3") && (config.system.output.type[tmpParam_editOutputId] == 3))) {
+    // add class 'visible' which overwrites display with flex!important and justify center
+    return F("visible");
+    
+  } else {
+    return String();
+  }
+}
+
+String Proc_WebPage_system_output_add_POST(const String& var) {
+  if(var == "SAVE_MSG") {
+    return String(Common_HTML_SAVE_MSG);
+  }  else {
+      return Proc_WebPage_system_output_add(var);
+  }  
+}
+
+
+void WebPage_system_output_add(AsyncWebServerRequest *request) {
+  const static char LogLoc[] PROGMEM = "[Webserver:system:output:add]";
+  if(request->method() == HTTP_POST) {
+    
+    byte outputId;
+    byte outputType;
+    //byte outputType_old;
+    
+    byte outputDevice_old;
+    
+    
+    if(request->hasParam("outputId", true)) {
+      const AsyncWebParameter* param = request->getParam("outputId", true);
+      outputId = param->value().toInt();
+    }
+    
+    
+
+
+
+    
+    if(request->hasParam("type", true)) {
+      const AsyncWebParameter* param = request->getParam("type", true);
+      // put info config struct
+      config.system.output.type[outputId] = param->value().toInt();
+      // remember the value in own var to work later with here
+      //outputType = param->value().toInt();
+    }
+    
+    /* save outputDevice_old */
+    outputDevice_old = config.system.output.device[outputId];
+              
+    if(request->hasParam("device", true)) {
+      const AsyncWebParameter* param = request->getParam("device", true);
+      
+      byte outputDevice = param->value().toInt();
+      
+      /* check if output type has changed. if so, delete old Output
+       * Grow Devices and recreate them with new type later */
+      //if((outputType != outputType_old) || (outputDevice != outputDevice_old))
+      if(outputDevice != outputDevice_old) {
+        #ifdef DEBUG
+        Log.verbose(F("%s - device changed, delete old Grow object" CR), LogLoc);
+        #endif
+        Output_Device_Grow_AddRemove(outputId, 1);
+        //config.grow.light.configured[outputId] = false;
+      }
+      /* finally write the new device type into the config */  
+      config.system.output.device[outputId] = param->value().toInt();
+    }
+    
+    
+      
+
+    
+    
+    
+    if(request->hasParam("name", true)) {
+      const AsyncWebParameter* param = request->getParam("name", true);
+      strlcpy(config.system.output.name[outputId], param->value().c_str(), sizeof(config.system.output.name[outputId]));
+    }
+    
+    if(request->hasParam("enabled", true)) {
+      const AsyncWebParameter* param = request->getParam("enabled", true);
+      config.system.output.enabled[outputId] = param->value().toInt();
+    }
+
+        
+    if(request->hasParam("invert", true)) {
+      const AsyncWebParameter* param = request->getParam("invert", true);
+      config.system.output.invert[outputId] = param->value().toInt();
+    }
+    
+    // only fill the type related config vars
+    switch(config.system.output.type[outputId]) {
+      // GPIO
+      case OUTPUT_TYPE_GPIO:
+        if(request->hasParam("gpio", true)) {
+          byte old_gpio = config.system.output.gpio[outputId];
+          const AsyncWebParameter* param = request->getParam("gpio", true);
+          config.system.output.gpio[outputId] = param->value().toInt();
+          if(old_gpio != config.system.output.gpio[outputId])
+            needRestart = true;
+          
+        }
+        
+        if(request->hasParam("gpio_pwm", true)) {
+          const AsyncWebParameter* param = request->getParam("gpio_pwm", true);
+          config.system.output.gpio_pwm[outputId] = param->value().toInt();
+        }
+        break;
+        
+      // I2C
+      case OUTPUT_TYPE_I2C:
+        if(request->hasParam("i2c_type", true)) {
+          byte old_i2c_type = config.system.output.i2c_type[outputId];
+          const AsyncWebParameter* param = request->getParam("i2c_type", true);
+          config.system.output.i2c_type[outputId] = param->value().toInt();
+          if(old_i2c_type != config.system.output.i2c_type[outputId])
+            needRestart = true;
+            outputStatus[outputId] = false;
+        }
+        
+        if(request->hasParam("i2c_addr", true)) {
+          byte old_i2c_addr = config.system.output.i2c_addr[outputId];
+          const AsyncWebParameter* param = request->getParam("i2c_addr", true);
+          config.system.output.i2c_addr[outputId] = param->value().toInt();
+          if(old_i2c_addr != config.system.output.i2c_addr[outputId])
+            needRestart = true;
+            outputStatus[outputId] = false;
+        }
+        
+        if(request->hasParam("i2c_port", true)) {
+          const AsyncWebParameter* param = request->getParam("i2c_port", true);
+          config.system.output.i2c_port[outputId] = param->value().toInt();
+        }
+        break;
+      // Webcall
+      case OUTPUT_TYPE_WEB:
+        if(request->hasParam("webcall_host", true)) {
+          const AsyncWebParameter* param = request->getParam("webcall_host", true);
+          strlcpy(config.system.output.webcall_host[outputId], param->value().c_str(), sizeof(config.system.output.webcall_host[outputId]));
+        }
+        
+        if(request->hasParam("webcall_path_on", true)) {
+          const AsyncWebParameter* param = request->getParam("webcall_path_on", true);
+          strlcpy(config.system.output.webcall_path_on[outputId], param->value().c_str(), sizeof(config.system.output.webcall_path_on[outputId]));
+        }
+        
+        if(request->hasParam("webcall_path_off", true)) {
+          const AsyncWebParameter* param = request->getParam("webcall_path_off", true);
+          strlcpy(config.system.output.webcall_path_off[outputId], param->value().c_str(), sizeof(config.system.output.webcall_path_off[outputId]));
+        }        
+        
+        /* reset in any case the webcall fail counter to trigger update retry */
+        outputWebcallFailed[outputId] = 0;
+        break;
+        default: break;
+    }
+    
+
+    /* create grow objects */
+    if(request->hasParam("editmode")) {
+      //Log.verbose(F("%s - has edit" CR), LogLoc);
+      if(config.system.output.device[outputId] != outputDevice_old) {
+        #ifdef DEBUG
+        Log.verbose(F("%s - device changed, Recreate Grow object" CR), LogLoc);
+        #endif
+        // remove - already done few lines before
+        //Output_Device_Grow_AddRemove(outputId, 1);
+        // add empty
+        Output_Device_Grow_AddRemove(outputId, 0);
+      }
+    } else {
+      #ifdef DEBUG
+      Log.verbose(F("%s - no edit" CR), LogLoc);
+      #endif
+      Output_Device_Grow_AddRemove(outputId, 0);
+    }
+    #ifdef DEBUG
+    SaveConfig(true);
+    #endif
+    SaveConfig();
+    // request->send_P(200, "text/html", Page_system_output_add_HTML, Proc_WebPage_system_output_add_POST);
+    // I like it more when user gets redirected to the output overview after saving
+    request->redirect(F("/system/output/?success"));
+  } else {
+    
+    /* When in edit mode */
+    if(request->hasParam("edit")) {
+        const AsyncWebParameter* param = request->getParam("edit");
+        tmpParam_editOutputId = param->value().toInt();
+        request->send_P(200, TEXT_HTML, Page_system_output_add_HTML, Proc_WebPage_system_output_addEdit);
+      /* when just adding a new output, check if there are free IDs */
+      } else if(Give_Free_OutputId() > Max_Outputs) {
+        /* if not, send error */
+        request->send_P(200, TEXT_HTML, Page_system_output_add_HTML_NO_ID_AVAILABLE, Proc_WebPage_system_output_add);
+      } else {
+        /* otherwise let the user create new output */
+        request->send_P(200, TEXT_HTML, Page_system_output_add_HTML, Proc_WebPage_system_output_add);
+      }
+  }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/*******************************************************************************
+ * Subpage sensor
+ */
+
+String Proc_WebPage_system_sensor(const String& var) {
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 2);
+  } else if(Test_WebPage_system_SUBNAV(var)) {
+    return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_SENSOR);
+  } else if(var == "ADD_DISABLED") {
+      if(Give_Free_SensorId() > Max_Outputs ) {
+        return F("disabled force_hide");
+      } else {
+        return String();
+      }
+  } else if(var == "TR_TD") {
+    // build table body
+    // i dont know a better way at the moment. if you do, please tell me!
+    String html;
+    for(byte i=0; i < Max_Sensors; i++) {
+      if(config.system.sensor.type[i] > 0) {
+
+        html += F("<tr><td>");
+        /* show warn sign if sensorStatus is false (uninitialized), otherwise green checkmark */
+        if(sensorStatus[i] == false) {
+          html += F(" &#x26A0;&#xFE0F;");
+        } else {
+          html += F(" &#x2705;");
+        }
+        /* bit spacing after the status icon */
+        html += F("&nbsp;&nbsp;");
+        html += F("</td><td>");
+        /* sens*/
+        html += i;
+
+        html += F("</td><td>");
+        html += config.system.sensor.name[i];
+        html += F("</td><td>");
+        html += SensorIndex[config.system.sensor.type[i]].name;
+        
+        /* when GPIO pin or I2C sensor is configured (1 is int adc), shot the Pin / addr in overview */
+        if((
+        (SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_INTADC) && config.system.sensor.gpio[i][0] > 0) ||
+        (SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_ONEWIRE) ||
+        (SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_TWOWIRE) ||
+        (SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_I2C)) {
+          html += F(" (");
+          
+          if(config.system.sensor.gpio[i][0] > 0) {
+            html += GPIOindex[config.system.sensor.gpio[i][0]].gpio;
+            
+            if(config.system.sensor.gpio[i][1] > 0) {
+              html += F("/");
+              html += GPIOindex[config.system.sensor.gpio[i][1]].gpio;
+            }
+          } else if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_I2C) {
+            html += F("0x");
+            html += String(Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.i2c_addr[i], SENSOR_AIU_MODE_ADDR), HEX);
+          }
+          html += F(")");
+        }
+                
+        html += F("</td><td>");
+
+        // calibrate button
+        html += F("<form class='linkForm' action='/system/sensor/calibrate' method='get'>");
+        html += F("<input type='hidden' name='calibrate' value='");
+        html += i;
+        html += F("'>");
+        html += F("<input type='submit' value='&#x1F39B;&#xFE0F;' title='Calibrate'></form> ");
+        
+        // edit button
+        html += F("<form class='linkForm' action='/system/sensor/add' method='get'>");
+        html += F("<input type='hidden' name='edit' value='");
+        html += i;
+        html += F("'>");
+        html += F("<input type='submit' value='&#x270F;&#xFE0F;' title='Edit'></form> ");
+        
+        // delete button
+        html += F("<form class='linkForm' action='/system/sensor/' method='post'>");
+        html += F("<input type='hidden' name='delete_sensor' value='");
+        html += i;
+        html += F("'>");
+        html += F("<input type='submit' value='&#x274C;' onclick=\"return confirmDelete('");
+        html += config.system.sensor.name[i];;
+        html += F("')\" title='Delete'></form>");
+       
+        html += F("</td></tr>");
+      }
+    }
+    
+    return html;
+  } else{
+    return String();
+  }
+}
+
+String Proc_WebPage_system_sensor_POST(const String& var) {
+  if(var == "SAVE_MSG") {
+    return String(Common_HTML_SAVE_MSG);
+  }  else {
+      return Proc_WebPage_system_sensor(var);
+  }  
+}
+
+void WebPage_system_sensor(AsyncWebServerRequest *request) {
+  if(request->method() == HTTP_POST) {
+    if(request->hasParam("delete_sensor", true)) {
+      byte sensorId;
+      
+      const AsyncWebParameter* param = request->getParam("delete_sensor", true);      
+      sensorId = param->value().toInt();
+            
+      // we ensure that every field is empty
+      config.system.sensor.type[sensorId] = 0;
+      memset(config.system.sensor.name[sensorId], '\0', sizeof config.system.sensor.name[sensorId]);
+      
+      /* go through all GPIOs */
+      for(byte i = 0; i < Max_Sensors_GPIO; i++) {
+        config.system.sensor.gpio[sensorId][i] = 0;
+      }
+
+      config.system.sensor.i2c_addr[sensorId] = 0;
+      sensorStatus[sensorId];
+      
+      SaveConfig();
+    }
+    request->send_P(200, TEXT_HTML, Page_system_sensor_HTML, Proc_WebPage_system_sensor_POST);
+            
+  } else {
+    if(request->hasParam("success")) {
+      // when GET param success is present, we use the _POST processor for the save message
+      request->send_P(200, TEXT_HTML, Page_system_sensor_HTML, Proc_WebPage_system_sensor_POST);
+    } else {
+      request->send_P(200, TEXT_HTML, Page_system_sensor_HTML, Proc_WebPage_system_sensor);
+    }
+  }
+  
+}
+
+
+/*******************************************************************************
+ * Subpage sensor add
+ */
+ 
+/* returns select <option> list of available output types */
+String Html_SelOpt_type_WebPage_system_sensor_add(byte selectId = 255) {
+  String html;
+  // go through all available Output Devices, skip 0 because it means unconfigured
+  for(byte i = 1; i <= SensorIndex_length; i++) {
+    html += F("<option value='");
+    html += i;
+    html += F("'");
+    if(i == selectId) {
+      html += F(" selected");
+    }
+    html += F(">");
+    html += SensorIndex[i].name;
+    html += F("</option>");
+  }
+  return html;
+}
+
+String Js_I2cAddr_Array_WebPage_system_sensor_add() {
+  const static char LogLoc[] PROGMEM = "[Webserver:system:sensor:add:Js_I2cAddr_Array]";
+  /* bit hacky, bit dirty, but may work
+   * here we return a 2-dimensional javascript array. the returned stuff
+   * gets directly injected in the template into a js function
+   */
+  String js;
+  
+  /* iterate through all SensorIds in index*/
+  for(byte i = 1; i <= SensorIndex_length; i++) {
+    // name
+    js += F("[");
+    /* iterate through all available addresses */
+    for(byte j = 0; j < SensorIndex[i].max; j++) {
+      js += F("['0x");
+      js += String(Sensor_Addr_Init_Update(i, j, SENSOR_AIU_MODE_ADDR), HEX);
+      // value
+      js += F("', '");
+      js += j;
+      // used
+      bool used;
+      js += F("', '");
+      /* check if addr is used */
+      for(byte k = 0; k < Max_Sensors; k++) {
+        if(config.system.sensor.type[k] == i ) {
+          if(config.system.sensor.i2c_addr[k] == j) {
+            js += F("1");
+            // exit loop here
+            k = Max_Sensors + 1;
+          } //else {
+            //js += "0";
+          //}
+          
+        }
+      }
+      js += F("'],");
+    }
+  js += F("],\n");
+  }
+  
+  return js;
+}
+
+String Proc_WebPage_system_sensor_add(const String& var) {
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 2);
+  } else if(Test_WebPage_system_SUBNAV(var)) {
+    return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_SENSOR);
+  } else if(var == "ACTION") {
+    return F("&#10133; Add");
+    
+  } else if(var == "SENSOR_ID") {
+    // we check which id is free. A free ID as type == 0
+    return String(Give_Free_SensorId());
+    
+    
+  } else if(var == "SENSOR_TYPE") {
+    return Html_SelOpt_type_WebPage_system_sensor_add();
+    
+  } else if(var == "GPIO_INDEX") {
+    return Html_SelectOpt_GPIOindex(255, true);
+    
+  } else if(var == "ESP_PLATFORM") {
+    #ifdef ESP8266
+    return F("8266");
+    #endif 
+    
+    #ifdef ESP32
+    return F("32");
+    #endif 
+  } else if(var == "REPLACE_I2CADDR_JS") {
+ 
+    return Js_I2cAddr_Array_WebPage_system_sensor_add();
+    
+  } else {
+    return String();
+  }
+}
+
+String Proc_WebPage_system_sensor_addEdit(const String& var) {
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 2);
+  } else if(Test_WebPage_system_SUBNAV(var)) {
+    return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_SENSOR);
+  } else if(var == "ACTION") {
+    return F("&#x270F;&#xFE0F; Edit");
+    
+  } else if(var == "SENSOR_ID") {
+    // return the sensorId we got from GET .../add?edit=ID
+    // dirty workaround to put this in a global variable
+    return String(tmpParam_editSensorId);
+    
+  } else if(var == "SENSOR_TYPE") {
+    return Html_SelOpt_type_WebPage_system_sensor_add(config.system.sensor.type[tmpParam_editSensorId]);
+    
+  } else if(var == "SENSOR_NAME") {
+    // "escape" % character, because it would break the template processor.
+    // tasmote webcall for example has percentage char in its path
+    String sensorName = config.system.sensor.name[tmpParam_editSensorId];;
+    sensorName.replace(F("%"), F("&#37;"));
+    return sensorName;
+    
+  } else if(var == "GPIO_INDEX") {
+    return Html_SelectOpt_GPIOindex(config.system.sensor.gpio[tmpParam_editSensorId][0], true);
+    
+  } else if(var == "ESP_PLATFORM") {
+    #ifdef ESP8266
+    return F("8266");
+    #endif 
+    
+    #ifdef ESP32
+    return F("32");
+    #endif 
+  } else if(var == "REPLACE_I2CADDR_JS") {
+ 
+    return Js_I2cAddr_Array_WebPage_system_sensor_add();
+    
+  } else if(var == "I2C_SAVED") {
+    
+    /* Add bit javascript to ensure the saved value is selected */
+    String js;
+    js += F("SystemSensorAddGpioI2cSel('type_sel');SystemSensor_replaceAddr('type_sel', 'i2c_addr');");
+    js += F("document.getElementById('i2c_addr').value='");
+    js += config.system.sensor.i2c_addr[tmpParam_editSensorId];
+    js += F("';");
+    return js;
+
+    
+  } else {
+    return String();
+  }
+}
+
+String Proc_WebPage_system_sensor_add_POST(const String& var) {
+  if(var == "SAVE_MSG") {
+    return String(Common_HTML_SAVE_MSG);
+  }  else {
+      return Proc_WebPage_system_sensor_add(var);
+  }  
+}
+
+
+void WebPage_system_sensor_add(AsyncWebServerRequest *request) {
+  if(request->method() == HTTP_POST) {
+    byte sensorId;
+    //byte sensorType;
+    if(request->hasParam("sensorId", true)) {
+      const AsyncWebParameter* param = request->getParam("sensorId", true);
+      sensorId = param->value().toInt();
+    }
+    
+    if(request->hasParam("type", true)) {
+      const AsyncWebParameter* param = request->getParam("type", true);
+      byte old_type = config.system.sensor.type[sensorId];
+      // put info config struct
+      config.system.sensor.type[sensorId] = param->value().toInt();
+      /* when config changed to a different sensor which is not internal ADC, then need restart */ 
+      if((config.system.sensor.type[sensorId] != old_type) && (config.system.sensor.type[sensorId] != 1 ))
+        needRestart = true;
+        
+      } 
+    
+    
+    if(request->hasParam("name", true)) {
+      const AsyncWebParameter* param = request->getParam("name", true);
+      strlcpy(config.system.sensor.name[sensorId], param->value().c_str(), sizeof(config.system.sensor.name[sensorId]));
+    }
+    
+    if(request->hasParam("i2c_addr", true)) {
+      const AsyncWebParameter* param = request->getParam("i2c_addr", true);
+      //strlcpy(config.system.sensor.i2c_addr[sensorId], param->value().c_str(), sizeof(config.system.sensor.i2c_addr[sensorId]));
+      byte old_i2c_addr = config.system.sensor.i2c_addr[sensorId];
+      config.system.sensor.i2c_addr[sensorId] = param->value().toInt();
+      /* when i2c address changes, we need a restart 
+       * TODO or re-initialise the sensor(s) (possible?) */
+      // check if sensor is I2C one and i2c_addr differs from before, otherwise we dont care whats inside here
+      
+      if((SensorIndex[config.system.sensor.type[sensorId]].type == SENSOR_TYPE_I2C) && (config.system.sensor.i2c_addr[sensorId] != old_i2c_addr)) {
+        needRestart = true;
+        /* disable sensor, otherwise ESP will crash */
+        sensorStatus[sensorId] = false;
+      }
+    }
+    
+    if(request->hasParam("gpio", true)) {
+      const AsyncWebParameter* param = request->getParam("gpio", true);
+      byte old_gpio = config.system.sensor.gpio[sensorId][0];
+      config.system.sensor.gpio[sensorId][0] = param->value().toInt();
+      /* when internal ADC we can initialize the sensors here */
+      if((SensorIndex[config.system.sensor.type[sensorId]].type == SENSOR_TYPE_INTADC) && (config.system.sensor.gpio[sensorId][0] != old_gpio)) {
+        
+        /* TODO Crashes on ESP8266 sometimes, idk why, is OK on ESP32 */
+        #ifdef ESP32
+        Sensor_Addr_Init_Update(config.system.sensor.type[sensorId], config.system.sensor.gpio[sensorId][0], SENSOR_AIU_MODE_INIT);
+        sensorStatus[sensorId] = true;
+        #endif
+        
+        #ifdef ESP8266
+        /* because on ESP8266 it crashes sometimes, we force a restart until this get fixed */
+        needRestart = true;
+        sensorStatus[sensorId] = false;
+        #endif
+      }
+    }
+    
+    
+    SaveConfig();
+    
+    // request->send_P(200, "text/html", Page_system_output_add_HTML, Proc_WebPage_system_output_add_POST);
+    // I like it more when user gets redirected to the output overview after saving
+    request->redirect("/system/sensor/?success");
+  } else {
+    
+    /* when in edit mode */
+    if(request->hasParam("edit")) {
+        const AsyncWebParameter* param = request->getParam("edit");
+        tmpParam_editSensorId = param->value().toInt();
+        request->send_P(200, TEXT_HTML, Page_system_sensor_add_HTML, Proc_WebPage_system_sensor_addEdit);
+      /* if we want to add new sensor, check if a sensor id is available. if not send error */
+      } else if(Give_Free_SensorId() > Max_Sensors) {
+        request->send_P(200, TEXT_HTML, Page_system_sensor_add_HTML_NO_ID_AVAILABLE, Proc_WebPage_system_sensor_add);
+      /* Otherwise let the user create new sensor */
+      } else {
+        request->send_P(200, TEXT_HTML, Page_system_sensor_add_HTML, Proc_WebPage_system_sensor_add);
+      }
+  }
+}
+
+
+
+
+
+
+/*******************************************************************************
+ * Subpage sensor calibrate
+ */
+ 
+/*******************************************************************
+String Js_I2cAddr_Array_WebPage_system_sensor_calibrate() {
+  const static char LogLoc[] PROGMEM = "[Webserver:system:sensor:add:Js_I2cAddr_Array]";
+
+  String js;
+  
+
+  for(byte i = 1; i <= SensorIndex_length; i++) {
+    // name
+    js += F("[");
+
+    for(byte j = 0; j < SensorIndex[i].max; j++) {
+      js += F("['0x");
+      js += String(Sensor_Addr_Init_Update(i, j, SENSOR_AIU_MODE_ADDR), HEX);
+      // value
+      js += F("', '");
+      js += j;
+      // used
+      bool used;
+      js += F("', '");
+
+      for(byte k = 0; k < Max_Sensors; k++) {
+        if(config.system.sensor.type[k] == i ) {
+          if(config.system.sensor.i2c_addr[k] == j) {
+            js += F("1");
+            // exit loop here
+            k = Max_Sensors + 1;
+          } //else {
+            //js += "0";
+          //}
+          
+        }
+      }
+      js += F("'],");
+    }
+  js += F("],\n");
+  }
+  
+  return js;
+}
+
+
+String Proc_WebPage_system_sensor_calibrate3(const String& var) {
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 2);
+  } else if(Test_WebPage_system_SUBNAV(var)) {
+    return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_SENSOR);
+  } else if(var == "ACTION") {
+    return F("&#10133; Add");
+    
+  } else if(var == "SENSOR_ID") {
+    // we check which id is free. A free ID as type == 0
+    return String(Give_Free_SensorId());
+    
+    
+  } else if(var == "SENSOR_TYPE") {
+    return Html_SelOpt_type_WebPage_system_sensor_add();
+    
+  } else if(var == "GPIO_INDEX") {
+    return Html_SelectOpt_GPIOindex(255, true);
+    
+  } else if(var == "ESP_PLATFORM") {
+    #ifdef ESP8266
+    return F("8266");
+    #endif 
+    
+    #ifdef ESP32
+    return F("32");
+    #endif 
+  } else if(var == "REPLACE_I2CADDR_JS") {
+ 
+    return Js_I2cAddr_Array_WebPage_system_sensor_add();
+    
+  } else {
+    return String();
+  }
+}
+*******************************************************************/
+
+String Proc_WebPage_system_sensor_calibrate(const String& var) {
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 2);
+  } else if(Test_WebPage_system_SUBNAV(var)) {
+    return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_SENSOR);
+  } else if(var == "SENSOR_ID") {
+    // return the sensorId we got from GET .../add?edit=ID
+    // dirty workaround to put this in a global variable
+    return String(tmpParam_calibrateSensorId);
+    
+  } else if(var == "SENSOR_NAME") {
+    // "escape" % character, because it would break the template processor.
+    // tasmote webcall for example has percentage char in its path
+    String sensorName = config.system.sensor.name[tmpParam_calibrateSensorId];;
+    sensorName.replace(F("%"), F("&#37;"));
+    return sensorName;
+    
+  } else if(var == "SENSOR_READING") {
+    String html;
+    /* TODO the way this page is built is ulgy IMHO 
+     * maybe i will replace everything dynamic at this place with javascript? or i can make 
+     * more / better use of the ESPAsync template engine */
+    html += F("<script>SensorJsonRefresh(); var refreshJson = setInterval(SensorJsonRefresh, 1000);</script>");
+    for(byte i = 0; i < Max_Sensors_Read; i++) {
+      if(SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].read[i] > 0) {
+        html += F("<form method='post' action='/system/sensor/calibrate'>");
+        html += F("<input type='hidden' name='sensorId' value='");
+        html += tmpParam_calibrateSensorId;
+        html += F("'/>");
+        html += F("<input type='hidden' name='readId' value='");
+        html += i;
+        html += F("'/>");
+        
+        html += F("<u>Reading ");
+        html += i;
+        html += F(":</u><br><b>");
+        html += FPSTR(Sensor_Read_descr[SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].read[i]]);
+        html += F("</b> (");
+        html += F("<span class='sensorReading' id='raw-");
+        html += tmpParam_calibrateSensorId;
+        html += "-";
+        html += i;
+        html += F("'>");
+        
+        /* is sensor internal ADC ? */
+        if(SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].type == SENSOR_TYPE_INTADC) {
+          /* reading type RAW ? */
+          if(SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].read[i] == SENSOR_READ_TYPE_RAW) {
+            /* get RAW value */
+            html += (int)Sensor_getValue( config.system.sensor.type[tmpParam_calibrateSensorId], config.system.sensor.gpio[tmpParam_calibrateSensorId][0]);
+          } else {
+            /* print calibrated value if not RAW */
+            html += Sensor_getCalibratedValue(tmpParam_calibrateSensorId, i);
+            //html += Sensor_getValue( config.system.sensor.type[tmpParam_calibrateSensorId], config.system.sensor.gpio[tmpParam_calibrateSensorId][0]);
+          }
+        } else if(SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].type == SENSOR_TYPE_I2C) {
+          /* same stuff for i2c sensor */
+          if(SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].read[i] == SENSOR_READ_TYPE_RAW) {
+            html += (int)Sensor_getValue( config.system.sensor.type[tmpParam_calibrateSensorId], config.system.sensor.i2c_addr[tmpParam_calibrateSensorId], i);
+          } else {
+            html += Sensor_getCalibratedValue(tmpParam_calibrateSensorId, i);
+            html += " ";
+            String unit;
+            unit += FPSTR(Sensor_Read_unit[SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].read[i]]);
+            unit.replace(F("%"), F("&#37;"));
+            html += unit;
+            //html += Sensor_getValue( config.system.sensor.type[tmpParam_calibrateSensorId], config.system.sensor.i2c_addr[tmpParam_calibrateSensorId], i);
+          }
+        }
+        
+        html += F("</span>)");
+        
+        if(SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].read[i] == SENSOR_READ_TYPE_RAW) {
+          html += F("<script>var raw_");
+          html += tmpParam_calibrateSensorId;
+          html += F("_");
+          html += i;
+          html += F(" = setInterval(rawRefresh, 1000, ");
+          html += tmpParam_calibrateSensorId;
+          html += F(", ");
+          html += i;
+          html += F(", 'raw-');</script>");
+        }
+        
+        html += F("<br>");
+        
+        if(SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].read[i] == SENSOR_READ_TYPE_RAW) {
+          html += F("<u>Convert RAW value to:</u><br>");
+          html += F("<select name='rawConvert'><option value='0' >---</option>");
+          for(byte j = 1; j <= SENSOR_CONVERT_RAW_TYPE__TOTAL; j++) {
+            html += F("<option value='");
+            html += j;
+            
+            html += F("'");
+            if(config.system.sensor.rawConvert[tmpParam_calibrateSensorId][i] == j)
+              html += F(" selected");
+            html += F(">");
+            html += FPSTR(Sensor_Convert_Raw_descr[j]);
+            html += F("</option>");
+          }
+          html += F("</select><br>");
+          
+          /* when raw convert is set, display converted reading */
+          if(config.system.sensor.rawConvert[tmpParam_calibrateSensorId][i] > 0) {
+            //html += F("<span class='sensorReading'>");
+            html += F("<span class='sensorReading' id='reading-");
+            html += tmpParam_calibrateSensorId;
+            html += "-";
+            html += i;
+            html += F("'>");
+            
+            html += Sensor_getCalibratedValue(tmpParam_calibrateSensorId, i);
+            html += " ";
+            String unit;
+            unit += FPSTR(Sensor_Convert_Raw_unit[config.system.sensor.rawConvert[tmpParam_calibrateSensorId][i]]);
+            unit.replace(F("%"), F("&#37;"));
+            html += unit;
+            html += F("</span>");
+            
+            html += F("<script>var reading_");
+            html += tmpParam_calibrateSensorId;
+            html += F("_");
+            html += i;
+            html += F(" = setInterval(sensorRefresh, 1000, ");
+            html += tmpParam_calibrateSensorId;
+            html += F(", ");
+            html += i;
+            html += F(", 'reading-');</script>");
+            
+            html += F("<br>");
+          }
+          
+          html += F("<u>Low:</u><br>");
+          html += F("<input type='number' name='low' value='");
+          html += config.system.sensor.low[tmpParam_calibrateSensorId][i];
+          html += F("'/><br>");
+          
+          html += F("<u>High:</u><br>");
+          html += F("<input type='number' name='high' value='");
+          html += config.system.sensor.high[tmpParam_calibrateSensorId][i];
+          html += F("'/><br>");
+          
+        } else {
+          
+          html += F("<u>Offset:</u><br>");
+          html += F("<input type='number' step='0.01' name='offset' value='");
+          html += config.system.sensor.offset[tmpParam_calibrateSensorId][i];
+          html += F("'/><br>");
+          
+        }
+        html += F("<br><input type='submit' value='&#x1F4BE; Save settings'></form>");
+        html += F("<hr>\n\n");
+      }
+    }
+    
+    return html;
+    
+  } else {
+    return String();
+  }
+}
+
+String Proc_WebPage_system_sensor_calibrate_POST(const String& var) {
+  if(var == "SAVE_MSG") {
+    return String(Common_HTML_SAVE_MSG);
+  }  else {
+      return Proc_WebPage_system_sensor_calibrate(var);
+  }  
+}
+
+
+void WebPage_system_sensor_calibrate(AsyncWebServerRequest *request) {
+  if(request->method() == HTTP_POST) {
+    byte sensorId;
+    byte readId;
+    //byte sensorType;
+    if(request->hasParam("sensorId", true)) {
+      const AsyncWebParameter* param = request->getParam("sensorId", true);
+      sensorId = param->value().toInt();
+      tmpParam_calibrateSensorId = sensorId;
+    }
+    
+    if(request->hasParam("readId", true)) {
+      const AsyncWebParameter* param = request->getParam("readId", true);
+      readId = param->value().toInt();
+    }
+    
+    if(request->hasParam("offset", true)) {
+      const AsyncWebParameter* param = request->getParam("offset", true);
+      config.system.sensor.offset[sensorId][readId] = param->value().toFloat();
+    }
+
+    if(request->hasParam("low", true)) {
+      const AsyncWebParameter* param = request->getParam("low", true);
+      config.system.sensor.low[sensorId][readId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("high", true)) {
+      const AsyncWebParameter* param = request->getParam("high", true);
+      config.system.sensor.high[sensorId][readId] = param->value().toInt();
+    }
+    
+    if(request->hasParam("rawConvert", true)) {
+      const AsyncWebParameter* param = request->getParam("rawConvert", true);
+      config.system.sensor.rawConvert[sensorId][readId] = param->value().toInt();
+    }
+    
+    SaveConfig();
+    
+    request->send_P(200, "text/html", Page_system_sensor_calibrate_HTML, Proc_WebPage_system_sensor_calibrate_POST);
+    // I like it more when user gets redirected to the output overview after saving
+    //request->redirect("/system/sensor/?success");
+  } else {
+    
+    /* when in edit mode */
+    if(request->hasParam("calibrate")) {
+        const AsyncWebParameter* param = request->getParam("calibrate");
+        tmpParam_calibrateSensorId = param->value().toInt();
+        request->send_P(200, TEXT_HTML, Page_system_sensor_calibrate_HTML, Proc_WebPage_system_sensor_calibrate);
+      /* if we want to add new sensor, check if a sensor id is available. if not send error */
+      } else {
+        request->send_P(200, TEXT_HTML, Page_system_sensor_calibrate_HTML, Proc_WebPage_system_sensor_calibrate);
+      }
+  }
+}
+
diff --git a/include/Webserver/Page_system_HTML.h b/include/Webserver/Page_system_HTML.h
new file mode 100644
index 0000000..792856c
--- /dev/null
+++ b/include/Webserver/Page_system_HTML.h
@@ -0,0 +1,396 @@
+/*
+ * 
+ * include/Webserver/Page_system_HTML.h - system settings page HTML header file
+ * 
+ * 
+ *
+ */
+
+/* submenu SUBNAV */
+const char Page_system_HTML_SUBNAV[] PROGMEM = R"(<ul class='subnav'>
+  <li><a class='%ACTIVE_SUBNAV_GENERAL%' href='/system/'>&#x1F6E0;&#xFE0F; General</a></li>
+  <li><a class='%ACTIVE_SUBNAV_SENSOR%' href='/system/sensor/'>&#x1F321;&#xFE0F; Sensor</a></li>
+  <li><a class='%ACTIVE_SUBNAV_OUTPUT%' href='/system/output/'>&#9889; Output</a></li>
+  <li><a class='%ACTIVE_SUBNAV_UPDATE%' href='/system/update'>&#x1F504; Firmware update</a></li>
+  <li><a class='%ACTIVE_SUBNAV_RESTART%' href='/system/restart' >&#x1F501; System restart</a></li>
+  <li><a class='%ACTIVE_SUBNAV_WIPE%' href='/system/wipe' >&#x1F4A3; Factory reset</a></li>
+</ul>)";
+
+/* /system main page */
+const char Page_system_HTML[] PROGMEM = R"(%HEADER%
+%SUBNAV%
+%SAVE_MSG%
+<p>here you can set which features and sensors you use<br></p><form method='post' action='/system/'>
+
+<u>NTP offset</u>:<br>
+<input class='inputShort' type='number' name='ntpOffset' min='-12' max='14' value='%NTPOFFSET%' required> Hours<br>
+
+<u>Maintenance duration</u>:<br> <input class='inputShort' type='number' name='maintenanceDuration' min='0' max='900' value='%MAINTDUR%' required> Seconds<br>
+
+<u>ESP32-Cam IP (optional)</u>:<br>
+<input type='text' name='esp32cam' maxlength='16' value='%ESP32CAM%' ><br>
+
+<u>HTTP log to serial</u>:<br>
+<select name='httpLogSerial' required>
+<option disabled value='' selected hidden>---</option>
+%HTTPLOGSERIAL%
+</select><br>
+
+<u>I2C RTC</u>:<br>
+%RTC_STATUS%<select name='rtc' required>
+<option value='0' selected >---</option>
+%RTC_AVAILABLE%
+</select><br>
+
+<u>Save time to LittleFS</u>:<br>
+<select name='time2fs' required>
+<option disabled value='' selected hidden>---</option>
+%TIME2FS%
+</select><br>
+
+<u>PWM Frequency</u>:<br>
+<input type='number' name='pwmFreq' min='0' max='65535' value='%PWMFREQ%'>
+</select><br>
+
+
+<br>
+
+<input type='submit' value='&#x1F4BE; Save settings'>
+</form>
+%FOOTER% )";
+
+
+
+/*******************************************************************************
+ * Subpage update
+ */
+const char Page_system_update_HTML[] PROGMEM = R"(%HEADER%
+
+%SUBNAV%
+Version: %CGVER% <br>
+Build  : %CGBUILD% <br>
+
+<p>You find the latest CanGrow firmware version on the <a href='https://git.la10cy.net/DeltaLima/CanGrow/releases' target='_blank'>release page</a> of the git repository.</p>
+<form method='POST' action='/system/update' enctype='multipart/form-data' onsubmit="document.getElementById('divUploading').style.display = '';">
+   <b>Select .bin file:</b><br>
+   <input type='file' accept='.bin,.bin.gz' name='firmware' required>
+   <input type='submit' value='Update Firmware'>
+</form>
+<div id='divUploading' style='display: none;' class='warnmsg'>&#x1F6DC; Uploading, please wait...</div>
+
+%FOOTER% )";
+
+const char Page_system_update_HTML_POST[] PROGMEM = R"(<html>
+<head><meta http-equiv='refresh' content='20;url=/' /></head>
+<body><h1>Successfully updated!
+<br>Restarting...</h1><br>
+Redirecting to "/" in 20 seconds
+</body></html>)";
+
+const char Page_system_update_HTML_POST_FAILED[] PROGMEM = R"(<html>
+<head></head><body><h1>UPDATE FAILED</h1><br>
+Please see messages on serial monitor for more information and go back to <a href='/system/update'>\"System settings > Firmware update\"</a> and try another file.
+</body></html>)";
+
+/*******************************************************************************
+ * Subpage restart
+ */
+const char Page_system_restart_HTML[] PROGMEM = R"(%HEADER%
+%SUBNAV%
+<div class='warnmsg'>
+%RESTART_MSG%
+</div> 
+%FOOTER% )";
+
+const char Page_system_restart_HTML_RESTART_MSG[] PROGMEM = R"(Do you want to restart CanGrow?<br>Please confirm.
+<form action='/system/restart' method='post'><input type='hidden' name='confirmed' value='true' />
+<input type='submit' value='Confirm restart' />
+</form>)";
+
+const char Page_system_restart_HTML_RESTART_MSG_POST[] PROGMEM = R"(Restarting...<br><span style='helpbox'>Redirecting to root page in 20 seconds.</span>)";
+
+
+
+
+/*******************************************************************************
+ * Subpage wipe
+ */
+const char Page_system_wipe_HTML[] PROGMEM = R"(%HEADER%
+%SUBNAV%
+<div class='warnmsg'>
+%WIPE_MSG%
+</div>
+ 
+%FOOTER% )";
+
+const char Page_system_wipe_HTML_WIPE_MSG[] PROGMEM = R"(All settings will be removed!!<br><br>
+Please confirm wiping LittleFS
+<form action='/system/wipe' method='post'><br>
+Please confirm: <input type='checkbox' id='confirmed' name='confirmed' required /><br>
+<input type='submit' value='Confirm wiping' />
+</form>)";
+
+
+const char Page_system_wipe_HTML_WIPE_MSG_POST[] PROGMEM = R"(Restarting...)";
+
+/*******************************************************************************
+ * Subpage output
+ */
+const char Page_system_output_HTML[] PROGMEM = R"(%HEADER%
+%SUBNAV%
+%SAVE_MSG%
+<a class='button %ADD_DISABLED%' href='/system/output/add'>&#10133; Add output</a>
+<table class='centered'>
+  <tr>
+    <th>&nbsp;</th>
+    <th>ID</th>
+    <th>Name</th>
+    <th>Type</th>
+    <th>Device</th>
+    <th>&nbsp;</th>
+    <th>Action</th>
+  </tr>
+  %TR_TD%
+</table>
+%FOOTER% )";
+
+/*******************************************************************************
+ * Subpage output add
+ */
+const char Page_system_output_add_HTML[] PROGMEM = R"(%HEADER%
+%SUBNAV%
+<h3>%ACTION% output ID %OUTPUT_ID%</h3>
+%SAVE_MSG%
+
+<p>Add/Edit CanGrow output.</p>
+<form method='post' action='/system/output/add?%EDIT_MODE%'>
+<input type='hidden' name='outputId' value='%OUTPUT_ID%' />
+
+<u>Type</u>:<br>
+<select id='type_sel' name='type' onchange="showSelect('type_sel', 'type_', 'hidden'); SystemOutputAddselectRequired('type_sel');" required>
+  <option disabled value='' selected hidden>---</option>
+  %OUTPUT_TYPE%
+</select><br>
+
+<u>Device</u>:<br>
+<select name='device' required>
+  <option disabled value='' selected hidden>---</option>
+  %OUTPUT_DEVICE%
+</select><br>
+
+<u>Name</u>:<br>
+<input type='text' name='name' maxlength='32' value='%OUTPUT_NAME%' required><br>
+
+<u>Enable</u>:<br>
+<select name='enabled' required>
+  <option disabled value='' selected hidden>---</option>
+  %OUTPUT_ENABLED%
+</select><br>
+
+<u>Invert</u>:<br>
+<select name='invert' required>
+  <option disabled value='' selected hidden>---</option>
+  %INVERT%
+</select><br>
+
+
+<div class='hidden %CLASS_TYPE_1%' id='type_1'>
+  <u>GPIO</u>:<br>
+  <select id='gpio' name='gpio'>
+    <option disabled value='' selected hidden>---</option>
+    %GPIO_INDEX%
+  </select><br>
+  
+  <u>GPIO PWM</u>:<br>
+  <select id='gpio_pwm' name='gpio_pwm' >
+    <option disabled value='' selected hidden>---</option>
+    %GPIO_PWM%
+  </select><br>
+  
+</div>
+
+
+<div class='hidden ' id='type_2'>
+  <u>I2C type</u>:<br>
+  <select id='i2c_type' name='i2c_type' onchange="SystemOutputAdd_replaceI2cAddr('i2c_type', 'i2c_addr');">
+    <option disabled value='' selected hidden>---</option>
+%I2C_TYPE%
+  </select><br>
+  <u>I2C address</u>:<br>
+  <select id='i2c_addr' name='i2c_addr' onchange="SystemOutputAdd_replaceI2cPort('i2c_type', 'i2c_addr', 'i2c_port');">
+    <option value='' selected >---</option>
+  </select><br>
+  <u>I2C module port</u>:<br>
+  <select id='i2c_port' name='i2c_port'>
+    <option value='' selected >---</option>
+  </select><br>
+</div>
+
+<div class='hidden %CLASS_TYPE_3%' id='type_3'>
+  <u>Webcall host</u>:<br>
+  <input id='webcall_host' type='text' name='webcall_host' maxlength='32' value='%WEBCALL_HOST%' ><br>
+  
+  <u>Webcall path 'on'</u>:<br>
+  <input id='webcall_path_on' type='text' name='webcall_path_on' maxlength='32' value='%WEBCALL_PATH_ON%' ><br>
+  
+  <u>Webcall path 'off'</u>:<br>
+  <input id='webcall_path_off' type='text' name='webcall_path_off' maxlength='32' value='%WEBCALL_PATH_OFF%' ><br>
+</div>
+
+<br>
+
+<input type='submit' value='&#x1F4BE; Save settings'>
+</form>
+
+<script>
+    var addr = [ [] ,
+%REPLACE_I2CADDR_JS% ];
+
+%I2C_SAVED% 
+</script>
+
+
+%FOOTER% )";
+
+const char Page_system_output_add_HTML_NO_ID_AVAILABLE[] PROGMEM = R"(%HEADER%
+%SUBNAV%
+<h3>You cannot create more outputs, limit reached.</h3>
+%FOOTER% )";
+
+
+
+
+
+
+
+
+
+/*******************************************************************************
+ * Subpage sensor
+ */
+const char Page_system_sensor_HTML[] PROGMEM = R"(%HEADER%
+%SUBNAV%
+%SAVE_MSG%
+<a class='button %ADD_DISABLED%' href='/system/sensor/add'>&#10133; Add sensor</a>
+<table class='centered'>
+  <tr>
+    <th>&nbsp;</th>
+    <th>ID</th>
+    <th>Name</th>
+    <th>Type</th>
+  </tr>
+  %TR_TD%
+</table>
+%FOOTER% )";
+
+/*******************************************************************************
+ * Subpage sensor add
+ */
+const char Page_system_sensor_add_HTML[] PROGMEM = R"(%HEADER%
+%SUBNAV%
+<h3>%ACTION% sensor ID %SENSOR_ID%</h3>
+%SAVE_MSG%
+
+<p>Add/Edit CanGrow sensor.</p>
+<form method='post' action='/system/sensor/add'>
+<input type='hidden' name='sensorId' value='%SENSOR_ID%' />
+<u>Type</u>:<br>
+<select id='type_sel' name='type' onchange="SystemSensorAddGpioI2cSel('type_sel'); SystemSensor_replaceAddr('type_sel', 'i2c_addr');" required>
+  <option disabled value='' selected hidden>---</option>
+  %SENSOR_TYPE%
+</select><br>
+
+
+
+<u>Name</u>:<br>
+<input type='text' name='name' maxlength='32' value='%SENSOR_NAME%' required><br>
+
+
+<div class='hidden %CLASS_TYPE_1%' id='type_1'>
+  <u>GPIO</u>:<br>
+  <select id='gpio' name='gpio'>
+    <option value='' selected >---</option>
+    %GPIO_INDEX%
+  </select><br>
+</div>
+
+
+<div class='hidden %CLASS_TYPE_2%' id='type_2'>
+  <u>I2C address</u>:<br>
+  <select id='i2c_addr' name='i2c_addr' required>
+    <option value='' selected >---</option>
+  </select><br>
+</div>
+
+<div class='hidden %CLASS_TYPE_3% %CLASS_TYPE_1%' id='type_3'>
+  <span>Special 3</span>
+</div>
+
+<!-- Sensor reading calibration -->
+
+
+<br>
+
+<input type='submit' value='&#x1F4BE; Save settings'>
+</form>
+
+<script>
+  var ESP = '%ESP_PLATFORM%';
+  // https://stackoverflow.com/a/67412019
+  function SystemSensor_replaceAddr(selectId, replaceId) {
+    var sel = document.querySelector('#' + replaceId);
+    let selVal = document.getElementById(selectId).value;
+    // Remove existing options
+    Array.from(sel).forEach((option) => {
+      sel.removeChild(option)
+    })
+
+    var addr = [ [] ,
+%REPLACE_I2CADDR_JS%
+               ]
+
+      addr[selVal].map((optionData) => {
+        let opt = document.createElement('option')
+        opt.appendChild(document.createTextNode(optionData[0]));
+        opt.value = optionData[1]
+        if(optionData[2] > 0) {
+          opt.disabled = true
+        }
+        sel.appendChild(opt);
+      })
+  }
+
+%I2C_SAVED%
+</script>
+
+%FOOTER% )";
+
+const char Page_system_sensor_add_HTML_NO_ID_AVAILABLE[] PROGMEM = R"(%HEADER%
+%SUBNAV%
+<h3>You cannot create more sensors, limit reached.</h3>
+%FOOTER% )";
+
+
+
+
+
+/*******************************************************************************
+ * Subpage sensor calibrate
+ */
+const char Page_system_sensor_calibrate_HTML[] PROGMEM = R"(%HEADER%
+%SUBNAV%
+<h3>&#x1F39B;&#xFE0F; Calibrate sensor ID %SENSOR_ID% (%SENSOR_NAME%)</h3>
+%SAVE_MSG%
+
+<p>Calibrate CanGrow sensor.</p>
+
+
+%SENSOR_READING%
+
+
+
+<script>
+
+</script>
+
+%FOOTER% )";
diff --git a/include/Webserver/Page_wifi.h b/include/Webserver/Page_wifi.h
new file mode 100644
index 0000000..088b246
--- /dev/null
+++ b/include/Webserver/Page_wifi.h
@@ -0,0 +1,197 @@
+/*
+ * 
+ * include/Webserver/Page_wifi.h - wifi page header file
+ * 
+ * 
+ *
+ */
+
+
+#include "Page_wifi_HTML.h"
+
+String WebPage_wifi_ScanNetworks() {
+  const static char LogLoc[] PROGMEM= "[Webserver:wifi:ScanNetworks]";
+  String html;
+  #ifdef DEBUG
+  Log.verbose(F("%s scanning for available networks:" CR), LogLoc);
+  #endif
+  // https://github.com/mathieucarbou/ESPAsyncWebServer/blob/main/docs/index.md#scanning-for-available-wifi-networks
+  int n = WiFi.scanComplete();
+  if(n == -2){
+    WiFi.scanNetworks(true);
+  } else if(n){
+    for (int i = 0; i < n; ++i){
+      html += F("<option value='");
+      html += WiFi.SSID(i);
+      html += F("'>");
+      html += WiFi.SSID(i);
+      html += F("</option>");
+      /* dirty hack, arduino-log somehow destroys wifi names in output
+      /* so i have to print them oldschool with Serial.println
+       */ 
+      #ifdef DEBUG
+      Log.verbose(F("%s  - "), LogLoc);
+      Serial.println(WiFi.SSID(i));
+      #endif
+    }
+    WiFi.scanDelete();
+    if(WiFi.scanComplete() == -2){
+      WiFi.scanNetworks(true);
+    }
+  }
+  return html;
+}
+
+// https://techtutorialsx.com/2018/07/23/esp32-arduino-http-server-template-processing-with-multiple-placeholders/
+String Proc_WebPage_wifi(const String& var) {
+  if(TestHeaderFooter(var)) {
+    return AddHeaderFooter(var, 3);
+    //CURRENT_SETTINGS
+  } else if(var == "CURRENT_SETTINGS") {
+    if(strlen(config.wifi.ssid) > 0) {
+      return String(Page_wifi_HTML_CURRENT_SETTINGS);
+    } else {
+      return String();
+    }
+  } else if(var == "CONFIGWIFI_SSID") {
+    return String(config.wifi.ssid);
+  } else if(var == "CONFIGWIFI_DHCP") {
+    return String(config.wifi.dhcp);
+  } else if(var == "CONFIGWIFI_IP") {
+    return String(WiFi.localIP().toString());
+  } else if(var == "CONFIGWIFI_NETMASK") {
+    return String(WiFi.subnetMask().toString());    
+  } else if(var == "CONFIGWIFI_GATEWAY") {
+    return String(WiFi.gatewayIP().toString());
+  } else if(var == "CONFIGWIFI_DNS") {
+    return String(WiFi.dnsIP().toString());
+  } else if(var == "WIFI_LIST") {
+    return String(WebPage_wifi_ScanNetworks());
+  } else {
+    return String();
+  }
+}
+
+
+String Proc_WebPage_wifi_POST(const String& var) {
+  if(var == "SAVE_MSG") {
+    return String(Common_HTML_SAVE_MSG);
+  } else {
+      return Proc_WebPage_wifi(var);
+  }  
+}
+
+String Proc_WebPage_wifi_POST_ERR(const String& var) {
+  if(var == "SAVE_MSG") {
+    return String(Common_HTML_SAVE_MSG_ERR);
+  } else {
+      return Proc_WebPage_wifi(var);
+  }  
+}
+
+void WebPage_wifi(AsyncWebServerRequest *request) {
+  const static char LogLoc[] PROGMEM = "[Webserver:wifi]";
+  
+  if(request->method() == HTTP_POST) {
+    if(request->hasParam("config.wifi.ssid", true)) {
+      const AsyncWebParameter* p_ssid = request->getParam("config.wifi.ssid", true);
+      strlcpy(config.wifi.ssid, p_ssid->value().c_str(), sizeof(config.wifi.ssid));
+      
+    }
+    
+    if(request->hasParam("config.wifi.password", true)) {  
+      const AsyncWebParameter* p_password = request->getParam("config.wifi.password", true);
+      strlcpy(config.wifi.password, p_password->value().c_str(), sizeof(config.wifi.password));
+    }
+    
+    
+    if(
+      (request->hasParam("config.wifi.ip0", true)) &&
+      (request->hasParam("config.wifi.ip1", true)) &&
+      (request->hasParam("config.wifi.ip2", true)) &&
+      (request->hasParam("config.wifi.ip3", true))) {
+        
+      const AsyncWebParameter* p_ip0 = request->getParam("config.wifi.ip0", true);
+      const AsyncWebParameter* p_ip1 = request->getParam("config.wifi.ip1", true);
+      const AsyncWebParameter* p_ip2 = request->getParam("config.wifi.ip2", true);
+      const AsyncWebParameter* p_ip3 = request->getParam("config.wifi.ip3", true);
+
+      config.wifi.ip[0] = p_ip0->value().toInt();
+      config.wifi.ip[1] = p_ip1->value().toInt();
+      config.wifi.ip[2] = p_ip2->value().toInt();
+      config.wifi.ip[3] = p_ip3->value().toInt();
+    }
+    
+    
+    if(
+      (request->hasParam("config.wifi.netmask0", true)) &&
+      (request->hasParam("config.wifi.netmask1", true)) &&
+      (request->hasParam("config.wifi.netmask2", true)) &&
+      (request->hasParam("config.wifi.netmask3", true))) {
+      
+      const AsyncWebParameter* p_netmask0 = request->getParam("config.wifi.netmask0", true);
+      const AsyncWebParameter* p_netmask1 = request->getParam("config.wifi.netmask1", true);
+      const AsyncWebParameter* p_netmask2 = request->getParam("config.wifi.netmask2", true);
+      const AsyncWebParameter* p_netmask3 = request->getParam("config.wifi.netmask3", true);
+
+      config.wifi.netmask[0] = p_netmask0->value().toInt();
+      config.wifi.netmask[1] = p_netmask1->value().toInt();
+      config.wifi.netmask[2] = p_netmask2->value().toInt();
+      config.wifi.netmask[3] = p_netmask3->value().toInt();
+    }
+      
+    if(
+      (request->hasParam("config.wifi.gateway0", true)) &&
+      (request->hasParam("config.wifi.gateway1", true)) &&
+      (request->hasParam("config.wifi.gateway2", true)) &&
+      (request->hasParam("config.wifi.gateway3", true))) {
+      
+      const AsyncWebParameter* p_gateway0 = request->getParam("config.wifi.gateway0", true);
+      const AsyncWebParameter* p_gateway1 = request->getParam("config.wifi.gateway1", true);
+      const AsyncWebParameter* p_gateway2 = request->getParam("config.wifi.gateway2", true);
+      const AsyncWebParameter* p_gateway3 = request->getParam("config.wifi.gateway3", true);
+
+      config.wifi.gateway[0] = p_gateway0->value().toInt();
+      config.wifi.gateway[1] = p_gateway1->value().toInt();
+      config.wifi.gateway[2] = p_gateway2->value().toInt();
+      config.wifi.gateway[3] = p_gateway3->value().toInt();
+    }
+      
+    if(
+      (request->hasParam("config.wifi.dns0", true)) &&
+      (request->hasParam("config.wifi.dns1", true)) &&
+      (request->hasParam("config.wifi.dns2", true)) &&
+      (request->hasParam("config.wifi.dns3", true))) {
+      
+      const AsyncWebParameter* p_dns0 = request->getParam("config.wifi.dns0", true);
+      const AsyncWebParameter* p_dns1 = request->getParam("config.wifi.dns1", true);
+      const AsyncWebParameter* p_dns2 = request->getParam("config.wifi.dns2", true);
+      const AsyncWebParameter* p_dns3 = request->getParam("config.wifi.dns3", true);
+
+      config.wifi.dns[0] = p_dns0->value().toInt();
+      config.wifi.dns[1] = p_dns1->value().toInt();
+      config.wifi.dns[2] = p_dns2->value().toInt();
+      config.wifi.dns[3] = p_dns3->value().toInt();
+    }
+    
+    if(request->hasParam("config.wifi.dhcp", true)) {
+      const AsyncWebParameter* p_dhcp = request->getParam("config.wifi.dhcp", true);
+
+      config.wifi.dhcp = p_dhcp->value().toInt();
+    }
+    
+    if(SaveConfig()) {
+      // we need a restart to apply the new settings
+      needRestart = true;
+      Log.notice(F("%s config saved" CR), LogLoc);
+      request->send_P(200, TEXT_HTML, Page_wifi_HTML, Proc_WebPage_wifi_POST);
+    } else {
+      Log.error(F("%s ERROR while saving config" CR), LogLoc);
+      request->send_P(200, TEXT_HTML, Page_wifi_HTML, Proc_WebPage_wifi_POST_ERR);
+    }
+    
+  } else {
+    request->send_P(200, TEXT_HTML, Page_wifi_HTML, Proc_WebPage_wifi);
+  }
+    
+}
diff --git a/include/Webserver/Page_wifi_HTML.h b/include/Webserver/Page_wifi_HTML.h
new file mode 100644
index 0000000..63bb34a
--- /dev/null
+++ b/include/Webserver/Page_wifi_HTML.h
@@ -0,0 +1,72 @@
+/*
+ * 
+ * include/Webserver/Page_wifi_HTML.h - wifi page HTML header file
+ * 
+ * 
+ *
+ */
+
+const char Page_wifi_HTML[] PROGMEM = R"(%HEADER%
+%SAVE_MSG%
+%CURRENT_SETTINGS%
+
+<p>Select your wifi network from the SSID list.
+<br>Reload the page, if your network is not listed.</p>
+<form method='post' action='/wifi/'>
+
+<u>SSID</u>:<br>
+<select id='config.wifi.ssid' name='config.wifi.ssid' required>
+<option disabled value='' selected hidden>-Select your network-</option>
+
+%WIFI_LIST%
+
+</select><br>
+
+<u>Password</u>:<br>
+<input type='password' name='config.wifi.password'><br>
+
+<u>DHCP</u>:<br>
+<select id='dhcp_sel' name='config.wifi.dhcp' onchange="showSelect('dhcp_sel', 'dhcp_', 'hidden');" required>
+<option disabled value='' selected hidden>---</option>
+<option value='1'>On</option>
+<option value='0'>Off</option>
+</select><br>
+
+<div class='hidden' id='dhcp_0'>
+  <u>IP</u>:<br>
+  <input class='inputShort' type='number' min='0' max='255' name='config.wifi.ip0'> . 
+  <input class='inputShort' type='number' min='0' max='255' name='config.wifi.ip1'> . 
+  <input class='inputShort' type='number' min='0' max='255' name='config.wifi.ip2'> . 
+  <input class='inputShort' type='number' min='0' max='255' name='config.wifi.ip3'><br>
+  
+  <u>Netmask</u>:<br>
+  <input class='inputShort' type='number' min='0' max='255' name='config.wifi.netmask0'> . 
+  <input class='inputShort' type='number' min='0' max='255' name='config.wifi.netmask1'> . 
+  <input class='inputShort' type='number' min='0' max='255' name='config.wifi.netmask2'> . 
+  <input class='inputShort' type='number' min='0' max='255' name='config.wifi.netmask3'><br>
+  
+  <u>Gateway</u>:<br>
+  <input class='inputShort' type='number' min='0' max='255' name='config.wifi.gateway0'> . 
+  <input class='inputShort' type='number' min='0' max='255' name='config.wifi.gateway1'> . 
+  <input class='inputShort' type='number' min='0' max='255' name='config.wifi.gateway2'> . 
+  <input class='inputShort' type='number' min='0' max='255' name='config.wifi.gateway3'><br>
+  
+  <u>DNS</u>:<br>
+  <input class='inputShort' type='number' min='0' max='255' name='config.wifi.dns0'> . 
+  <input class='inputShort' type='number' min='0' max='255' name='config.wifi.dns1'> . 
+  <input class='inputShort' type='number' min='0' max='255' name='config.wifi.dns2'> . 
+  <input class='inputShort' type='number' min='0' max='255' name='config.wifi.dns3'><br>
+</div>
+<br>
+<input type='submit' value='&#x1F4BE; Save settings'>
+</form>
+%FOOTER% )";
+
+const char Page_wifi_HTML_CURRENT_SETTINGS[] PROGMEM = R"(<b><u>Current Settings:</u></b><br>WiFi SSID: <b>%CONFIGWIFI_SSID%</b><br>
+Use DHCP: <b>%CONFIGWIFI_DHCP%</b><br>
+IP address: <b>%CONFIGWIFI_IP%</b><br>
+Subnet mask: <b>%CONFIGWIFI_NETMASK%</b><br>
+Gateway: <b>%CONFIGWIFI_GATEWAY%</b><br>
+DNS: <b>%CONFIGWIFI_DNS%</b><br><br>)";
+
+
diff --git a/include/Webserver/Webserver_Common.h b/include/Webserver/Webserver_Common.h
new file mode 100644
index 0000000..78fe702
--- /dev/null
+++ b/include/Webserver/Webserver_Common.h
@@ -0,0 +1,190 @@
+/*
+ * 
+ * include/Webserver/Common.h - header file with common webserver functions
+ * HTML header or footer to a String()
+ * 
+ * 
+ *
+ */
+
+#include "Webserver_Common_HTML.h"
+
+/*
+ * global char constants for various HTML tags and stuff
+ * 
+ */
+
+/* return type */
+const char TEXT_HTML[] PROGMEM = "text/html";
+
+
+/*
+ * TestHeaderFooter - checks if the given var from the webserver processor
+ * is actual a template variable from header or footer.
+ */
+bool TestHeaderFooter(const String& var) {
+  const static char LogLoc[] PROGMEM = "[Webserver:Common:TestHeaderFooter]";
+  #ifdef DEBUG3
+  Log.verbose(F("%s var: %s" CR), LogLoc, var);
+  #endif
+  if(
+    (var == "HEADER")            ||
+    (var == "FOOTER")            ||
+    (var == "CGVER")             ||
+    (var == "CGBUILD")           ||
+    (var == "GROWNAME")          ||
+    (var == "CANGROW_CSS")       ||
+    (var == "TIME")       ||
+    (var == "NEED_RESTART")      ||
+    (var == "ACTIVE_NAV_GROW")   ||
+    (var == "ACTIVE_NAV_SYSTEM") ||
+    (var == "ACTIVE_NAV_WIFI")   ||
+    (var == "ACTIVE_NAV_HELP")   ||
+    (var == "PLACEHOLDER")) {
+    return true;
+  } else {
+    return false;
+  }
+}
+
+
+/*
+ * AddHeaderFooter - processor for header and footer template variables
+ * 
+ * String& var:
+ *   the string we receive from the processor is the actual
+ *   variable name we replace here.
+ * byte activeNav: 
+ *   contains the number representing which page is active
+ *   1 - grow settings
+ *   2 - system settings 
+ *   3 - wifi settings
+ *   4 - help page
+ */
+String AddHeaderFooter(const String& var, byte activeNav = 0) {
+  String activeNav_ClassName = F("activeNav");
+  if(var == "HEADER") {
+    return String(Header_HTML);
+  } else if(var == "FOOTER") {
+    return String(Footer_HTML);
+  } else if(var == "CGVER") {
+    return String(CANGROW_VER);
+  } else if(var == "CGBUILD") {
+    return String(CANGROW_BUILD);
+  } else if(var == "GROWNAME") {
+    return String(config.grow.name);
+  } else if(var == "CANGROW_CSS") {
+    return String(File_cangrow_CSS);
+  } else if(var == "TIME") {
+    return Str_TimeNow();    
+  } else if((var == "ACTIVE_NAV_GROW") && (activeNav == 1)) {
+    return activeNav_ClassName;
+  } else if((var == "ACTIVE_NAV_SYSTEM") && (activeNav == 2)) {
+    return activeNav_ClassName;
+  } else if((var == "ACTIVE_NAV_WIFI") && (activeNav == 3)) {
+    return activeNav_ClassName;
+  } else if((var == "ACTIVE_NAV_HELP") && (activeNav == 4)) {
+    return activeNav_ClassName;
+  } else if(var == "NEED_RESTART") {
+    if(needRestart == true) {
+      return String(Common_HTML_NEED_RESTART);
+    } else {
+      return String();
+    }
+
+  } else {
+    return String();
+  }
+}
+
+
+/*
+ * Html_SelectOpt_GPIOindex
+ * 
+ * returns <option> list as string with available gpios
+ */ 
+
+String Html_SelectOpt_GPIOindex(byte selectId = 255, bool input = false) {
+  
+  String gpioIndex_html;
+    // iterate through through all available GPIOs in index
+    for(byte i = 1; i <= GPIOindex_length; i++) {
+      bool gpioUsed = Check_GPIOindex_Used(i);
+      
+      gpioIndex_html += F("<option value='");
+      gpioIndex_html += i;
+      gpioIndex_html += F("'");
+      // set disabled option for gpio which are already in use or incompatible // or only inputs when configuring sensor ADC
+      // || ( (input == true) && ((GPIOindex[i].note != INPUT_ONLY) || (GPIOindex[i].note != INT_ADC)) )
+      /* when GPIO is already in use AND not selected OR
+       * input is false AND GPIO is Input only OR
+       * input is true AND GPIO is not INT_ADC AND not INPUT_only*/
+      if( ((gpioUsed == true) && (i != selectId)) || ((input == false) && (GPIOindex[i].note == INPUT_ONLY))
+       #ifdef ESP32
+       /* If we are on ESP32, we check our input GPIOs - we dont need this on ESP8266, as it only has 1 ADC */
+       || ((input == true) && ((GPIOindex[i].note != INT_ADC) && GPIOindex[i].note != INPUT_ONLY)) 
+       #endif
+       ) {
+        //||          ((input == true) && ((GPIOindex[i].note != INPUT_ONLY) || (GPIOindex[i].note != INT_ADC) ))
+        gpioIndex_html += F(" disabled");
+      }
+      
+      if(i == selectId) {
+        gpioIndex_html += F(" selected");
+      }
+      gpioIndex_html += F(">GPIO ");
+      gpioIndex_html += GPIOindex[i].gpio;
+      //add gpio note if there is some
+      //if(GPIOindex[i].note > 0) {
+      gpioIndex_html += F(" ");
+      gpioIndex_html += FPSTR(GPIO_Index_note_descr[GPIOindex[i].note]);
+
+      // disable output incompatible gpio
+      if((GPIOindex[i].note == INPUT_ONLY) && (input == false)) {
+        gpioIndex_html += F(" (N/A)");
+      // add USED if gpio is already in use  
+      } else if((gpioUsed == true) && (i != selectId)) {
+        gpioIndex_html += F(" (used)");
+      }
+      gpioIndex_html += F("</option>");
+    }
+    return gpioIndex_html;
+}
+
+
+String Html_SelectOpt_bool(byte selectVal = 255, String trueStr = "Yes", String falseStr = "No") {
+  String html;
+  html += F("<option value='1'");
+  html += ((selectVal > 0) && (selectVal < 255)) ? F(" selected") : F("");
+  html += F(">");
+  html += trueStr;
+  html += F("</option>");
+  
+  html += F("<option value='0'");
+  html += (selectVal == 0) ? F(" selected") : F(""); 
+  html += F(">");
+  html += falseStr;
+  html += F("</option>");
+  return html;
+}
+
+
+String Html_SelectOpt_array(byte total, const char * descr[] , byte selectVal = 255) {
+  const static char LogLoc[] PROGMEM= "[Webserver:Common:Html_Select_Opt_array]";
+  String html;
+  // go through all available array entries, skip 0 because it means unconfigured
+  for(byte i = 1; i <= total; i++) {
+    //Log.notice(F("%s i: %d selectVal: %d descr: %S" CR), LogLoc, i, selectVal, descr[i]);
+    html += F("<option value='");
+    html += i;
+    html += F("'");
+    if(i == selectVal) {
+      html += F(" selected");
+    }
+    html += F(">");
+    // use FPSTR because our descr is stored in PROGMEM
+    html += FPSTR(descr[i]);
+    html += F("</option>");
+  }
+  return html;
+}
diff --git a/include/Webserver/Webserver_Common_HTML.h b/include/Webserver/Webserver_Common_HTML.h
new file mode 100644
index 0000000..1d263b8
--- /dev/null
+++ b/include/Webserver/Webserver_Common_HTML.h
@@ -0,0 +1,26 @@
+/*
+ * 
+ * include/Webserver/Common_HTML.h - header file with common HTML snippets
+ * HTML header or footer to a String()
+ * 
+ * 
+ *
+ */
+
+
+// double div to force a linebreak. infomsg , warnmsg are inline-block
+const char Common_HTML_SAVE_MSG[] PROGMEM = R"EOF(
+<div><div class='infomsg'>&#x2705; Successfully saved!</div></div>
+)EOF";
+
+const char Common_HTML_SAVE_MSG_ERR[] PROGMEM = R"EOF(
+<div><div class='infomsg'>!! ERROR saving!</div></div>
+)EOF";
+
+const char Common_HTML_NEED_RESTART[] PROGMEM = R"EOF(
+<div><div class='warnmsg'>&#10071; Restart is required to apply new settings!
+<form action='/system/restart' method='post'><input type='hidden' name='confirmed' value='true' />
+ <input type='submit' value='Restart now' />
+</form>
+</div></div>
+)EOF";