ESP32 Greenhouse Controller (ESP32-4R-A2, Multi-file, LittleFS, Wi-Fi Config, Auth + AP Captive Portal)
This project implements a greenhouse controller based on the ESP32-4R-A2 relay board.
It controls:
- 2 lights
- 1 fan (12 V)
- 1 pump (12 V)
It reads:
- Temperature + humidity from an SHT40 (I²C)
- Soil moisture from 2× HD38 analog sensors
It displays status on a small 0.91" WE-DA-361 I²C OLED and exposes a web UI (via Wi-Fi) with:
- Dashboard (live sensors, relay states, modes)
- Configuration (thresholds, timings, light schedules, grow profiles, timezone, web-auth config)
- Config tabs grouped into Environment, Lights, Water & Air, Grow profile, Wi‑Fi, System, and Security for clearer navigation, with compact preset previews and automation pills.
- Timezone dropdown includes UTC, Europe/Berlin, Europe/London, US/Eastern, US/Central, US/Mountain, and US/Pacific options.
- History charts (temperature, humidity, soil moisture) with configurable axis ranges (defaults: 10–40 °C and 0–100 %RH)
- History chart labels use the device timezone when supported by the browser.
- Live sparklines for sensors on the dashboard
- Grow profile tab with preset previews, chamber-targeted apply (Ch1→Light1, Ch2→Light2), plus system tab showing current device time
- Inline preset editor on the Config → Grow profile tab to rename/update grow profile defaults (soil thresholds, light schedules, automation, and fan/pump/preset automation toggles) with values saved to NVS
- Light schedules on the dashboard, grow profile previews, and preset tables show timezone-aware on/off countdowns based on the device clock (including midnight-crossing schedules)
- Relay controls that disable while requests are processing, with toast feedback for mode/toggle actions
- Per-chamber dashboard labeling for soil tiles and light controls, with
/api/statusexposing chamber metadata (includingid1/2 andidx0/1) for UI and integrations - Dashboard surfaces the applied grow profile per chamber (using
/api/statusprofile_id/profile_labelfields) with graceful fallbacks for custom/manual configs - Refreshed dashboard branding with an EZgrow logo and favicon for quick device identification, using a natural logo aspect ratio in the top bar
- Wi-Fi configuration (scan SSIDs, select, store SSID/password in NVS) via the
/wifionboarding page (captive portal entry point) or the Config Wi-Fi tab - HTTP Basic Authentication (credentials stored in NVS, configurable in UI)
- Captive portal for Wi-Fi onboarding in AP mode (auto-redirects to
/wifi)
All charts work offline, using LittleFS to serve Chart.js from the ESP32.
The device supports:
- Station (STA) mode: connects to your existing Wi-Fi network.
- Access Point (AP) fallback + Captive Portal: starts
EZgrow-SetupAP and captive portal if STA connection fails or no SSID is configured.
-
Create the project folder
controller/ controller.ino Greenhouse.h Greenhouse.cpp WebUI.h WebUI.cpp data/ chart.umd.min.js -
Install Arduino support
- Install the ESP32 board package via the Boards Manager (Espressif Systems).
- In the Arduino IDE, select:
- Board:
ESP32 Dev Module(or the closest ESP32-4R-A2 equivalent) - Correct COM port.
- Board:
-
Install required libraries (Library Manager)
Adafruit SHT4XAdafruit Unified SensorU8g2- (Core libraries
WiFi.h,WebServer.h,DNSServer.h,Preferences.hare part of the ESP32 core.)
-
Download Chart.js and place it in
data/-
Download a recent Chart.js 4 UMD build (e.g. from JSDelivr).
-
Save it as:
controller/data/chart.umd.min.js
-
-
Optional: Set compile-time default Wi-Fi (bootstrap only)
In
Greenhouse.cpp, you can set initial default credentials:static const char* DEFAULT_WIFI_SSID = "YOUR_SSID"; static const char* DEFAULT_WIFI_PASS = "YOUR_PASSWORD";
These are only used if no Wi-Fi credentials are stored in NVS.
Once you save SSID/password via the/wifipage, those NVS values override defaults. -
Upload LittleFS data
- Install the ESP32 LittleFS Data Upload plugin for Arduino IDE (if not already installed).
- In Arduino IDE, open the
controllersketch. - Use the “ESP32 LittleFS Data Upload” menu to upload the
data/folder to the ESP32.
-
Compile and upload the firmware
- Click Verify to compile.
- Click Upload to flash the ESP32-4R-A2.
-
First-time Wi-Fi setup / Captive portal
After boot, one of two things will happen:
- The ESP32 connects to your network.
- The IP address is:
- Printed to the Serial Monitor (115200 baud).
- Shown briefly on the OLED.
Open the dashboard:
http://<esp32-ip-address>/- Default web credentials (when first booting):
- User:
admin - Password:
admin
- User:
Endpoints:
/: dashboard and charts/config: control thresholds, timings, schedules, auth config/wifi: Wi-Fi configuration (scan, select SSID, save); also exposed inside/configas the Wi-Fi tab, and used as the captive portal entry point
-
The ESP32 starts an access point (AP):
- SSID:
EZgrow-Setup - Password: empty (open AP, set a password in
initHardware()if desired)
- SSID:
-
The controller enables a captive portal:
- A DNS server resolves all hostnames to the ESP32 AP IP.
- Any request to an unknown path is redirected to
/wifi.
-
Connect your phone or laptop to
EZgrow-Setup. -
Most devices will automatically pop up a Wi-Fi sign-in page.
If not, open:http://192.168.4.1/wifi -
On the Wi-Fi page:
- Click a network in the table to populate the SSID.
- Enter your Wi-Fi password.
- Save.
-
The device will reboot and attempt to connect to the selected Wi-Fi network in STA mode.
- Controller board: ESP32-4R-A2 (4-relay ESP32 module)
- Actuators (via onboard relays + external 12 V supply):
- Light 1 (Relay 1)
- Light 2 (Relay 2)
- 12 V fan (Relay 3)
- 12 V pump (Relay 4)
- Sensors:
- 1× SHT40 (I²C) – temperature and humidity
- 2× HD38 soil moisture sensors (analog output, powered at 3.3 V)
- Display:
- WE-DA-361 – 0.91" 128×32 I²C OLED (SSD1306 compatible)
-
Fan control (automatic):
- Controlled by temperature OR humidity with configurable hysteresis:
fanOnTemp,fanOffTemp(°C).fanHumOn,fanHumOff(% RH).
- Fan turns ON if temperature ≥
fanOnTempor humidity ≥fanHumOn. - Fan turns OFF when both are back in safe range:
- temperature ≤
fanOffTempand humidity ≤fanHumOff.
- temperature ≤
- Fan activation waits for hot/humid conditions to persist for ~2 minutes before engaging (OFF hysteresis remains unchanged).
- Controlled by temperature OR humidity with configurable hysteresis:
-
Pump control (automatic):
- Soil moisture-based control using 2 sensors and configurable:
- Per-chamber configs (
ChamberConfig): name (1–24 chars, HTML stripped), dry/wet thresholds, and optional profile IDs. - Minimum OFF time (
pumpMinOffSec). - Maximum ON time (
pumpMaxOnSec).
- Per-chamber configs (
- When the pump starts, it records which chambers were below their dry thresholds and only requires those chambers to reach their wet thresholds before shutting off (or when
pumpMaxOnSecelapses), respecting the minimum OFF interval between cycles. - Pump activation requires ~2 minutes of continuous dryness (in addition to the minimum OFF timer) before starting.
- Soil moisture-based control using 2 sensors and configurable:
-
Lights:
- Each light can run:
- In AUTO mode using daily schedules (ON/OFF times).
- In MANUAL mode with direct toggling via web UI.
- Schedules allow intervals that cross midnight (e.g. 20:00–06:00).
- Each light can run:
-
STA (station) mode:
- Connects to an existing Wi-Fi network using SSID/password stored in NVS.
- Shows assigned IP on Serial and OLED.
- Web pages are protected by HTTP Basic Auth (unless disabled).
-
AP fallback + Captive Portal:
- If STA connection fails, or no SSID is configured:
- Starts AP
EZgrow-Setup(open by default). - AP IP is typically
192.168.4.1.
- Starts AP
- If STA connection fails, or no SSID is configured:
- Timezone change is applied only when updated
- Open
/configand note the current timezone display. - Submit the form without changing the timezone and confirm that NTP/timezone updates are not triggered.
- Change the timezone selection, save, and verify the device applies the new timezone once after the configuration is stored.
- Captive portal redirects all requests to
/wifi. - No auth is required in AP-only mode to simplify onboarding.
- Open
-
Dashboard (
/) (Basic Auth protected in STA mode):- Current time (from NTP).
- Temperature (°C), humidity (%).
- Soil moisture 1/2 (%).
- States and modes of Light 1, Light 2, Fan, Pump (ON/OFF + AUTO/MAN).
- Light schedule summary.
- History charts:
- Temperature and humidity (line chart).
- Light 1 and Light 2 states (step chart).
- Links to
/configand/wifi.
-
Configuration (
/config) (Basic Auth protected in STA mode):- Environment thresholds:
- Fan ON/OFF temperature.
- Fan ON/OFF humidity.
- Chamber names (1–24 characters; HTML is stripped on save).
- Per-chamber soil DRY/WET thresholds (Ch1 uses soil sensor 1, Ch2 uses soil sensor 2; pump is shared across both chambers).
- Pump minimum OFF time.
- Pump maximum ON time.
- Light 1/2 schedules (ON/OFF time + “use schedule” flags).
- AUTO/MANUAL for fan and pump.
- Grow profiles:
- Per-chamber preset selectors with dedicated “Apply” buttons that call the chamber-specific apply endpoint to update soil thresholds and light schedules.
- A combined preset selector with “Apply to both + env” to update both chambers plus environment thresholds and automation defaults.
- Applied profile banner updates when presets are applied (per-chamber or both).
- Web UI authentication section:
- Username:
- If empty, HTTP authentication is disabled.
- Password:
- If blank on submit, the existing password is kept.
- If non-empty, updates the stored password.
- Username:
- Environment thresholds:
-
Wi-Fi configuration (
/wifi):- Lists available SSIDs (scanned with
WiFi.scanNetworks()). - Shows current connection (if any).
- Form:
- SSID (clicking a row in the table fills this).
- Password.
- On submit:
- Credentials are stored in NVS (
gh_wifi). - Device responds with a “saved” page.
- Device restarts and attempts connection with new credentials.
- Credentials are stored in NVS (
- Lists available SSIDs (scanned with
-
History API (
/api/history):- JSON feed of logged samples (10-minute cadence) retained for 7 days.
- Defaults to the last 24 hours; pass
?days=1..7to request a specific range. - Each point is the average of all readings captured during its 10-minute window and includes timestamp, temperature, humidity, soil1/soil2 moisture, and Light 1/2 states.
- Fan/pump automation uses 1-minute averaged sensor readings (independent of the chart cadence) to avoid reacting to brief spikes.
-
Static asset from LittleFS:
/chart.umd.min.js– Chart.js UMD bundle served from LittleFS for offline charts.
-
Credentials storage:
- Username/password stored in NVS (
gh_authnamespace). - Initial default (when nothing stored):
- user:
admin, pass:admin.
- user:
- Username/password stored in NVS (
-
When auth is enforced:
- In STA mode (ESP32 connected to a Wi-Fi network), all pages except Chart.js:
/,/config,/wifi,/api/history,/toggle,/moderequire Basic Auth.
- If username is empty in
/config, auth is considered disabled:- No Basic Auth challenge, all pages are open (not recommended on shared networks).
- In STA mode (ESP32 connected to a Wi-Fi network), all pages except Chart.js:
-
AP + captive portal mode:
- When only AP mode is active (STA not connected), auth is disabled regardless of stored credentials:
- The goal is to make onboarding easy.
- After STA connection is established, auth applies again (if username is non-empty).
- When only AP mode is active (STA not connected), auth is disabled regardless of stored credentials:
controller/
controller.ino # Main entry point (setup/loop)
Greenhouse.h # Config/state structs, function declarations
Greenhouse.cpp # Hardware init, sensors, control logic, Wi-Fi/AP, NTP, history, NVS helpers
WebUI.h # Web server API declarations
WebUI.cpp # HTTP routes, HTML, config UI, Wi-Fi UI, history, auth, captive portal
data/
chart.umd.min.js # Chart.js UMD bundle (served via LittleFS)
The
data/directory is uploaded to the ESP32’s LittleFS partition using the ESP32 LittleFS Data Upload tool.
The ESP32-4R-A2 board maps its relays to the following GPIOs:
| Function | Relay | ESP32 GPIO |
|---|---|---|
| Light 1 | RLY1 | 25 |
| Light 2 | RLY2 | 26 |
| Fan | RLY3 | 32 |
| Pump | RLY4 | 33 |
Typical wiring:
- 12 V+ → relay COM
- Relay NO → load + (light/fan/pump +)
- Load − → 12 V GND
- 12 V GND → ESP32 GND (common ground)
Relays are configured as active LOW in the code:
LOW= relay ON (energized).HIGH= relay OFF.
If your board uses active HIGH relays, you can invert RELAY_ACTIVE_LEVEL in Greenhouse.cpp.
The SHT40 and OLED share the ESP32 I²C bus:
| Signal | ESP32 pin | Connected devices |
|---|---|---|
| SDA | 21 | SHT40 SDA, WE-DA-361 SDA |
| SCL | 22 | SHT40 SCL, WE-DA-361 SCL |
Power:
- SHT40:
- VCC → 3.3 V
- GND → GND
- WE-DA-361 OLED:
- VCC → 3.3 V (module supports 3.3–5 V)
- GND → GND
If the modules do not have pull-up resistors on SDA/SCL:
- Add ~4.7 kΩ from SDA → 3.3 V.
- Add ~4.7 kΩ from SCL → 3.3 V.
Use ADC1 pins (Wi-Fi-safe ADC):
| Sensor | ESP32 pin | Notes |
|---|---|---|
| Soil sensor 1 | 34 | ADC1_CH6 (input only) |
| Soil sensor 2 | 35 | ADC1_CH7 (input only) |
Power for each HD38:
- VCC → 3.3 V
- GND → GND
- AO (analog out) → respective ADC pin (34 or 35)
Use 3.3 V for HD38 to keep the analog output within ESP32 ADC limits.
Main interface (Basic Auth protected in STA mode):
- Time (if NTP synced, otherwise “syncing...”).
- Temperature, humidity.
- Soil 1/2 moisture.
- Relay states and modes:
- Light 1, Light 2, Fan, Pump (ON/OFF + AUTO/MAN).
- Light schedules (L1 and L2).
- Buttons:
- Toggle for each device (in MANUAL mode).
- Switch to AUTO/MANUAL for each device.
- History charts:
- Temperature & Humidity (line chart, dual axis).
- Light 1 & Light 2 (step-like 0/1 chart).
- Direct links to:
/config(greenhouse logic + auth settings)/wifi(network config)
Protected by Basic Auth in STA mode. The page uses tabs:
- Environment
- Fan ON/OFF temperature thresholds (°C).
- Fan ON/OFF humidity thresholds (%RH).
- Soil DRY/WET thresholds (%).
- Pump minimum OFF time and maximum ON time (seconds).
- Lights
- Use schedule (AUTO) vs MANUAL toggle for each light.
- ON/OFF times for Light 1 and Light 2 (
<input type="time">).
- Automation
- AUTO/MANUAL toggles for the fan and pump.
- Grow profile
- Select Seedling/Vegetative/Flowering presets and apply them per chamber (Ch1 → Light 1, Ch2 → Light 2) or across both chambers + environment.
- Per-chamber apply updates that chamber's soil thresholds and mapped light schedule/auto flag, and applies preset fan/pump automation defaults when provided (other chamber thresholds stay untouched).
- Preview table shows preset values before applying.
- Wi-Fi
- Mirrors the
/wifionboarding page inside/config, showing connection status, the SSID/password form, and the scanned network table while keeping/wifias the captive portal entry point.
- Mirrors the
- System
- Displays the current device time.
- Timezone dropdown (UTC, Europe/Berlin, Europe/London, US/Eastern, US/Central, US/Mountain, US/Pacific). Changes apply immediately to NTP/time display.
- Reboot control with confirmation, calling the
/api/rebootendpoint, logging the requester, and responding before restarting so the UI can show feedback.
- Security
- HTTP Basic Auth credentials: username (empty disables auth) and password (blank keeps existing password).
On submit:
- Values are validated (basic sanity).
- Configuration is saved to NVS (via
Preferences). - New values are applied immediately (no reboot required).
- Auth changes take effect on the next request.
Protected by Basic Auth in STA mode, open in AP-only mode (onboarding). The same UI is available inside /config via the Wi-Fi tab for quick adjustments while /wifi remains the captive portal entry path:
-
Current connection:
- Shows whether the ESP32 is connected and to which SSID.
- Shows current IP and RSSI.
-
SSID/password form:
- SSID (
<input type="text">). - Password (
<input type="password">). - Credentials persist in NVS (
gh_wifinamespace).
- SSID (
-
Network scan:
- Table of nearby SSIDs, RSSI, and encryption type (open / secured).
- Clicking a row populates the SSID field.
On submit:
- SSID/password are stored in NVS.
- Device responds with a simple “saved” page.
- After a short delay, the device restarts.
- On next boot, the device attempts STA connection using the saved credentials.
Connection behaviour:
- STA connection attempts time out after ~15 seconds to keep the control loop responsive.
- While disconnected, the controller retries the STA connection roughly once per minute with backoff instead of rapid-fire association attempts.
- If the device stays offline for a couple of minutes, it automatically re-enables the
EZgrow-SetupAP + captive portal for onboarding. - When STA connectivity is restored, the AP is torn down again and captive-portal-only auth is disabled so Basic Auth is enforced on the LAN.
-
GET /toggle?id=light1|light2|fan|pump
Toggles the specified relay only if that device is in MANUAL mode.
Protected by Basic Auth in STA mode. -
GET /mode?id=light1|light2|fan|pump&auto=0|1
Switches the specified device between AUTO and MANUAL:- For lights: toggles use of schedule (AUTO) vs manual relay control.
- For fan/pump: toggles automatic control logic vs manual relay control.
Protected by Basic Auth in STA mode.
-
GET /api/grow/apply?chamber=0|1|2&profile=0-3(orchamber_id=1|2) Applies a grow profile to a single chamber (soil thresholds + linked light schedule/auto + preset fan/pump automation defaults). Accepts legacy zero-based indexes (0/1) or chamber IDs (1/2, with2also accepted viachamber=2) and responds with bothchamber_idxandchamber_idalongside the applied label and chamber metadata.
Protected by Basic Auth in STA mode. -
POST /api/reboot
Authenticated reboot endpoint that logs the requester, acknowledges the request with JSON, and then restarts the controller after a short delay so the response can reach the UI.
- Returns a JSON payload containing an array of historical points for the last 24 hours, one per minute.
- Used by the dashboard’s JavaScript to render charts.
- Protected by Basic Auth in STA mode.
If autoFan is enabled:
- Let
T= measured temperature,H= measured relative humidity. - Fan turns ON when:
T ≥ fanOnTempORH ≥ fanHumOnand the hot/humid condition persists for at least ~120 seconds.
- Fan turns OFF when:
T ≤ fanOffTempANDH ≤ fanHumOff
(or the respective values are unavailable, in which case they are ignored).
Hysteresis ensures stable behaviour (fanOnTemp > fanOffTemp and fanHumOn > fanHumOff).
If autoPump is enabled:
- “Too dry” if soil sensor 1
< chamber1.soilDryThresholdor soil sensor 2< chamber2.soilDryThreshold. - “Wet enough” if soil sensor 1
> chamber1.soilWetThresholdand soil sensor 2> chamber2.soilWetThreshold. - Chamber names (max 24 chars, HTML stripped) and optional profile IDs are stored per chamber; thresholds are clamped to 0–100 with wet > dry enforced.
- Pump turns ON when:
- Not currently running, AND
- “Too dry”, AND
- At least
pumpMinOffSecseconds elapsed since the last stop, AND - Dryness persists for ~120 seconds before activation.
- Pump turns OFF when:
- “Wet enough”, OR
- Pump has been ON for more than
pumpMaxOnSecseconds.
Each light has:
onMinutesandoffMinutes(minutes since midnight).enabledflag:true: AUTO (schedule active).false: MANUAL.
Schedules support intervals that cross midnight:
- Example 1: 08:00–20:00 (day).
- Example 2: 20:00–06:00 (night).
When a light is in MANUAL mode, its relay is controlled solely by the web UI Toggle button.
The WE-DA-361 0.91" OLED (128×32) shows:
- Line 1:
T:xx.xC H:yy% - Line 2:
S1:aa% S2:bb% - Line 3:
L1xA/M L2xA/M FxA/M PxA/M
Where:
x=1(ON) or0(OFF).A= AUTO /M= MANUAL.
On boot:
- Initially:
"Greenhouse boot...". - Then:
- If STA connected: IP address of the ESP32.
- If AP fallback: AP SSID (
EZgrow-Setup) and AP IP (e.g.192.168.4.1).
Default mapping:
soilPercent = map(raw, 0, 4095, 100, 0);
soilPercent = constrain(soilPercent, 0, 100);To calibrate:
-
Record
raw_dryin dry medium andraw_wetin fully wet medium. -
Use:
soilPercent = map(raw, raw_wet, raw_dry, 100, 0); soilPercent = constrain(soilPercent, 0, 100);
Then adjust per-chamber dry/wet thresholds (and optional chamber names) via /config.
Example configuration:
fanOnTemp= 28 °CfanOffTemp= 26 °CfanHumOn= 80 %fanHumOff= 70 %
These can be tuned in the config UI to better fit your greenhouse.
-
Web UI Auth:
- HTTP Basic Auth; credentials stored in ESP32 NVS (plain text).
- Strongly recommended to set a reasonably strong password.
- You can disable auth by clearing the username field in
/config.
-
AP / captive portal:
- AP is open by default (
EZgrow-Setupwith no password). - For more security, set a password in
initHardware()when callingWiFi.softAP(...).
- AP is open by default (
-
Transport security:
- The controller uses plain HTTP (no TLS).
- Recommended precautions:
- Keep it on a trusted local network, not exposed directly to the internet.
- Optionally place it behind a reverse proxy that terminates HTTPS and enforces additional auth.
- Use HTTPS (with reverse proxy or ESP32 TLS if resources allow).
- Add multi-user roles or API tokens for automation.
- MQTT integration (for Home Assistant, etc.).
- Long-term data logging to SD card or an external database.
- Additional sensors:
- CO₂
- Light intensity
- ...
- BLE / Bluetooth-based control UI.
MIT License
Copyright (c) 2025 Marco Horstmann
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.