ESP8266-based IoT monitoring and control system for a 3-tank rainwater harvesting setup. Monitors water levels via 4–20 mA pressure transmitters, automates Tank 3 filling through a motorized ball valve, and integrates with Home Assistant via MQTT Discovery.
| Component | Qty | Purpose |
|---|---|---|
| WeMos D1 Mini Lite (ESP8266) | 1 | Microcontroller |
| Adafruit INA219 breakout | 3 | 4–20 mA current measurement |
| 4–20 mA liquid level transmitter (0–20 kPa) | 3 | Pressure head sensing |
| 3-wire 12V motorized ball valve | 1 | Tank 3 fill control |
| 2-channel opto-isolated relay module | 1 | Valve power and direction |
| 12V power supply | 1 | Valve motor supply |
| Pin | GPIO | Function | Notes |
|---|---|---|---|
| D1 | 5 | I2C SCL | Shared bus for 3× INA219 |
| D2 | 4 | I2C SDA | Shared bus for 3× INA219 |
| D5 | 14 | Keep Awake | Pull LOW to prevent deep sleep |
| D6 | 12 | Valve Power Relay | Active-LOW — energises 12V supply |
| D7 | 13 | Valve Direction Relay | Active-LOW — LOW = open, HIGH = close |
| A0 | ADC0 | Battery Monitor | Currently disabled (USE_BAT=0) |
| Sensor | Address | Jumper Config |
|---|---|---|
| Tank 1 | 0x40 | A0 = GND, A1 = GND |
| Tank 2 | 0x41 | A0 = Vcc, A1 = GND |
| Tank 3 | 0x44 | A0 = GND, A1 = Vcc |
All three tanks: 1820 mm diameter × 1920 mm height (cylindrical).
src/
├── main.h # Global defines, WiFi/MQTT config, pin assignments
├── main.cpp # Application entry: setup, loop, MQTT handlers, HA Discovery
├── tank.h / tank.cpp # Tank class — INA219 driver, level calculation, sensor health
├── fill_controller.h/.cpp # FillController — state machine, valve control, EEPROM config
└── secrets.h # Credentials (gitignored — see Setup section)
- Boot — Relays driven HIGH (safe) before
pinMode; serial, I2C, and tanks initialised. - Connect — WiFi → mDNS (
TankCommander) → OTA handler → MQTT broker. - HA Discovery — Publishes auto-discovery configs for all entities.
- Loop — OTA handle → MQTT loop → decode commands → immediately acknowledge accepted control changes over MQTT → read sensors (every interval seconds) → publish telemetry/state → run fill controller.
- Power Mode — The default build is always-online for reliable Home Assistant control. Optional deep sleep support remains available behind
TC_ALLOW_DEEP_SLEEP=1for deployments that intentionally trade responsiveness for power saving.
DISABLED ──► IDLE ──► VALVE_OPENING ──► FILLING ──► VALVE_CLOSING ──► COOLDOWN ──► IDLE
│ ▲
└── FAULT_SENSOR / FAULT_TIMEOUT
Valve actuation is non-blocking (millis-based): 50 ms relay settle → 8.5 s valve travel.
| Parameter | Default | Description |
|---|---|---|
fillEnabled |
false |
Master enable/disable |
targetLevel_pc |
80% | Stop filling at this level |
lowThreshold_pc |
60% | Auto-fill trigger |
maxDuration_min |
30 min | Abort fill safety timeout |
| Hard max | 60 min | Absolute non-configurable limit |
| Cooldown | 30 s | Prevents rapid valve cycling |
Configuration is persisted to EEPROM (magic 0xFC03, version 3).
The Tank class continuously checks each 4–20 mA loop:
| Condition | Threshold | Result |
|---|---|---|
| Wire break | < 3.5 mA | FAULT_SENSOR |
| Short circuit | > 21.0 mA | FAULT_SENSOR |
| Stuck reading | ±0.01 mA for 5 consecutive reads | FAULT_SENSOR |
Each sensor converts milliamps to pressure head using a linear equation:
head_mm = mA × m + b
Default calibration: m = 128.5657757, b = -516.7934059
Calibration can be updated at runtime via MQTT (see below). Changes are not persisted across reboots.
This file is gitignored. Create it manually:
#pragma once
#define WIFI_SSID "YourSSID"
#define WIFI_PASS "YourPassword"
#define MQTT_SERVER "your-broker.example.com"
#define MQTT_PORT 1883
#define OTA_PASS "YourOTAPassword"Install PlatformIO IDE (VS Code extension) or the CLI.
Serial upload (first time):
pio run -t upload --upload-port COMx # Windows
pio run -t upload --upload-port /dev/ttyUSBx # LinuxOTA upload (subsequent):
# Set the OTA password as an environment variable first
export OTA_PASS="YourOTAPassword"
pio run -t uploadOTA is preconfigured in platformio.ini to target 10.0.0.68. Change upload_port if your device has a different IP.
The default firmware build keeps the controller online continuously so Home Assistant commands are confirmed immediately.
If you explicitly want the older low-power behavior, add -D TC_ALLOW_DEEP_SLEEP=1 to the target environment's build_flags in platformio.ini.
pio device monitor -b 115200- Host: Defined in
secrets.h(MQTT_SERVER) - Port: 1883
- Client ID:
TankCommander - Keep-alive: Matches the sensor read interval (default 60 s)
- Availability topic:
tanks/statuswith payloadsonline/offline
| Topic | Payload | Retained | Description |
|---|---|---|---|
tanks |
JSON {interval, interval_s, keepawake_sw, keepawake_hw} |
No | Device status and controller settings |
tanks/status |
online / offline |
Yes | MQTT availability for all discovered entities |
tanks/tank1 |
JSON {mm, pc, L, bus_V} |
No | Tank 1 readings |
tanks/tank2 |
JSON {mm, pc, L, bus_V} |
No | Tank 2 readings |
tanks/tank3 |
JSON {mm, pc, L, bus_V} |
No | Tank 3 readings |
tanks/fill/state |
JSON (see below) | Yes | Fill controller state and persisted config |
Fill state payload:
{
"state": "idle",
"filling": false,
"enabled": false,
"target_pc": 80.0,
"low_threshold_pc": 60.0,
"max_duration_min": 30,
"tank3_pc": 75.5,
"tank3_L": 12340.0,
"sensor_ok": true,
"fault": "none",
"fill_elapsed_min": 0,
"fill_reason": "none",
"valve": "closed"
}| Topic | Payload | Description |
|---|---|---|
tanks/commands/interval |
Seconds (integer) | Sensor read interval |
tanks/commands/keepawake |
True / False |
Software keep-awake |
tanks/commands/cal1 |
"m,b" |
Tank 1 calibration |
tanks/commands/cal2 |
"m,b" |
Tank 2 calibration |
tanks/commands/cal3 |
"m,b" |
Tank 3 calibration |
tanks/fill/config/enabled |
true / false |
Enable/disable fill system |
tanks/fill/config/target |
10–100 |
Fill target percentage |
tanks/fill/config/low_threshold |
5–95 |
Auto-fill trigger percentage |
tanks/fill/config/max_duration |
1–60 |
Max fill duration (minutes) |
tanks/fill/command |
start / stop |
Manual fill start/stop |
tanks/fill/command/override |
clear_fault |
Clear fault state |
Accepted control commands are acknowledged immediately by republishing the relevant state topic. Home Assistant therefore tracks confirmed device state instead of waiting for the next telemetry interval.
mosquitto_pub -h your-broker.example.com -t "tanks/commands/cal1" -m "128.5657757,-516.7934059"Tank Commander publishes MQTT Discovery messages automatically on boot. No manual HA configuration is needed — entities appear under the Tank Commander device.
All discovered entities now share the tanks/status availability topic, and controllable entities are designed to reflect confirmed device state rather than optimistic UI state.
| Entity | Type | Description |
|---|---|---|
| Valve Mode | Select | closed / auto / open control |
| Fill Target Level | Number | Target % (10–100) |
| Fill Low Threshold | Number | Auto-fill trigger % (5–95) |
| Fill Max Duration | Number | Timeout in minutes (1–60) |
| Fill State | Sensor | Current state machine state (enum) |
| Valve State | Sensor | Valve position/activity (enum) |
| Fill Fault | Sensor | Fault classification |
| Fill Elapsed | Sensor | Minutes elapsed in current fill |
| Fill Reason | Sensor | Why the current or last fill was started |
| Clear Fault | Button | Reset fault state |
| Keep Awake | Switch | Sleep override when deep sleep is enabled |
| Keep Awake (HW) | Binary Sensor | Hardware keep-awake input state |
| Transmission Interval | Number | Sensor read interval in seconds |
| Tank 1/2/3 Level (mm) | Sensor | Water height in mm |
| Tank 1/2/3 Level (%) | Sensor | Water height percentage |
| Tank 1/2/3 Volume (L) | Sensor | Calculated volume |
automation:
- alias: "Alert when Tank 3 sensor fails"
trigger:
- platform: state
entity_id: binary_sensor.tankcommander_sensor_health
to: "on"
action:
- service: notify.mobile_app
data:
title: "Tank Commander"
message: "Tank 3 sensor fault detected!"Managed by PlatformIO (platformio.ini):
| Library | Purpose |
|---|---|
| Adafruit INA219 (fork) | 4–20 mA current measurement |
| Adafruit BusIO | I2C abstraction |
| ArduinoJson ~6.16.0 | JSON serialisation |
| PubSubClient | MQTT client |
| RunningAverage | Sensor smoothing |
| ArduinoOTA (built-in) | Over-the-air updates |
- Relays are driven HIGH (de-energised) at the very start of
setup()beforepinModeis called — the valve cannot actuate during boot. - Both relays are de-energised before deep sleep as a defence-in-depth measure when
TC_ALLOW_DEEP_SLEEP=1is enabled. - Hard maximum fill duration of 60 minutes cannot be overridden via MQTT.
- Sensor fault detection automatically halts any active fill.
- Non-blocking valve control ensures the main loop continues running (OTA, MQTT, watchdog) while the valve is moving.
Private project — not licensed for redistribution.