Open source

PlantPulse

github.com/juanmmm21/PlantPulse

DIY plant monitoring: ESP32 sensor nodes, a free Supabase backend and Telegram alerts.

Lines
2,281
Files
18
Test files
0
Commits
24
Started
12 Aug 2026
Last commit
21 Oct 2026
  • TypeScript 65.9%
  • C++ 16%
  • SQL 14.5%
  • C 3.6%
  1. 01

    A €15–25 node: capacitive soil probe, optional DHT22 and BH1750, all on an ESP32.

  2. 02

    Deep sleep between readings every 30 minutes; the wireless variant runs for months on a LiPo.

  3. 03

    Each plant carries its own thresholds — 'does it need water?' is a query, not a guess.

  4. 04

    A scheduled function sends a Telegram alert when a plant crosses a line, plus a daily summary.

Built withC++ESP32SupabasePostgreSQLTypeScriptTelegram

DIY plant monitoring: ESP32 sensor nodes that measure soil moisture, ambient conditions and light, publish every reading to a free Supabase backend, and message your phone over Telegram when a plant needs water or a node goes quiet.

What it is

PlantPulse turns any potted plant into something you can check remotely. A small, cheap (~15–25 €) sensor node sits in the pot: a capacitive soil moisture probe in the dirt, plus optional air temperature/humidity (DHT22) and light (BH1750) sensors, all wired to an ESP32. Every 30 minutes the node wakes from deep sleep, takes a reading, pushes it over HTTPS to a PostgreSQL database hosted on Supabase's free tier, and goes back to sleep.

Pots without a socket nearby get a fully wireless variant from the same firmware: a low-power ESP32 board, the soil probe and a small LiPo cell, running for months between charges and reporting its own battery voltage.

Because the data lives behind a hosted REST API rather than on your LAN, you can check on your plants from anywhere — no port forwarding, no VPN, no server of your own to maintain. Each plant carries its own thresholds in the database, so "does it need water?" is a query, not a guess.

You don't have to remember to ask, either. A scheduled function inside Supabase compares every plant against its own thresholds and sends a Telegram message when one crosses a line — too dry, too cold, too hot, too dark, a battery running low, or a node that stopped reporting — plus a daily summary of everything. There is no mobile app to install or publish: Telegram already knows how to put a notification on your phone.

How it works

 ┌─────────────────────────┐
 │  Plant pot              │
 │  ┌───────────────────┐  │      HTTPS POST             ┌──────────────────┐
 │  │ ESP32 node        │  │   (every 30 min)            │  Supabase        │
 │  │  · soil moisture  │──┼────────────────────────────▶│  PostgreSQL      │
 │  │  · DHT22 (opt.)   │  │   /rest/v1/readings         │  + PostgREST API │
 │  │  · BH1750 (opt.)  │  │                             │  + RLS policies  │
 │  └───────────────────┘  │                             └────────┬─────────┘
 └─────────────────────────┘                                      │
                                                                  │ REST
                                       pg_cron ────┐              │
                                    (every 15 min) │              │
                                                   ▼              ▼
                                        Edge Function      any client, anywhere
                                           `notify`
                                                   │
                                                   ▼  Telegram Bot API
                                             your phone

The node is fire-and-forget: all waits are bounded by explicit timeouts, and any failure (WiFi down, HTTP error, sensor glitch) is logged over serial and simply skipped until the next wake-up. Optional sensors that fail or aren't installed are reported as null, never as fake values.

Engineering notes

  • Capacitive over resistive soil sensing — resistive probes corrode within weeks in wet soil; the capacitive probe has no exposed metal and stays accurate for years. Raw ADC values are averaged over 16 samples (the sensor output has significant ripple) and mapped through a per-unit two-point calibration (dry air / glass of water), because individual sensor units differ substantially.
  • Deep sleep architecture — the whole duty cycle lives in setup(); loop() is intentionally empty since deep sleep resets the chip. This keeps power draw low and the firmware trivially restartable: every cycle starts from a clean state.
  • Battery nodes are a board choice, then a firmware budget — a stock ESP32 DevKit draws 4–15 mA in deep sleep (AMS1117 regulator, USB-UART bridge, power LED), which empties a LiPo in days no matter what the firmware does; battery nodes therefore use boards that sleep at ~15–25 µA (XIAO ESP32-C6, FireBeetle 2 ESP32-E). With the board fixed, awake time dominates, so the firmware powers the ~5 mA soil probe from a GPIO only while sampling, caches the access point's channel and BSSID in RTC memory to skip the WiFi scan on every wake, optionally skips DHCP with a static IP, and prints its awake time each cycle so the energy budget is measurable. Battery voltage is sampled at rest, before the radio sags it; below a cutoff (or after a brownout reset) the node stops using the radio and checks back every 12 h, recovering on its own once recharged.
  • Serverless backend — Supabase provides PostgreSQL, an auto-generated REST API (PostgREST) and row-level security on the free tier. The schema ships as one idempotent SQL file; RLS limits the shipped anon key to inserting readings and reading data, nothing else.
  • Alerts are a state machine, not a cron that shouts — each condition opens exactly one row in alerts and closes only once the value clears the threshold with margin (hysteresis), so a plant hovering at 30 % moisture doesn't message you twice an hour. Plants that like to dry out completely (snake plants, succulents) can also require the soil to stay dry for a set time — say two days at 0 % — before the watering alert fires. Sensor-based alerts freeze while a node is offline: a stale reading says nothing about the plant right now, so a dead node sends one notification instead of four.
  • needs_water is computed in the database — the latest_readings view joins each plant's latest reading with its own thresholds, so any client resolves the home-screen question ("which plants need attention?") with a single request.

Repository layout

PlantPulse/
├── firmware/plantpulse/
│   ├── plantpulse.ino       # ESP32 sketch: wake → measure → publish → deep sleep
│   └── config.example.h     # config template (copy to config.h, which is gitignored)
├── supabase/
│   ├── schema.sql           # full schema: tables, views, alerts, RLS, cron jobs
│   └── functions/notify/    # Edge Function: evaluates alerts, sends Telegram
│       ├── logic.ts         # pure alert evaluation (open / hold / resolve)
│       ├── messages.ts      # pure message formatting
│       ├── db.ts            # PostgREST access, no SDK
│       ├── telegram.ts      # Bot API client
│       └── *_test.ts        # unit tests for the pure modules
└── docs/                    # step-by-step build guides (in Spanish)
    ├── 01-lista-de-compra.md          # shopping list (~15–25 € per node)
    ├── 02-montaje-y-cableado.md       # wiring, placement, calibration
    ├── 03-puesta-en-marcha.md         # Supabase setup, flashing, verification
    ├── 04-notificaciones-telegram.md  # bot, function deploy, thresholds
    └── 05-nodos-a-bateria.md          # wireless battery nodes: boards, wiring, battery life

Hardware

PartRoleRequired
ESP32 DevKit (30- or 38-pin)MCU + WiFiyes
Capacitive soil moisture sensor v1.2/v2.0soil moisture (analog, GPIO 34)yes
DHT22 / AM2302 moduleair temperature + humidity (GPIO 27)optional
BH1750 modulelight level in lux (I2C, GPIO 25/26)optional

No soldering required. The 38-pin DevKit is exactly one inch wide, so on a breadboard it covers one whole header row — the default pins all sit on the reachable side. Full wiring tables, cable types and an ASCII schematic are in docs/02-montaje-y-cableado.md.

Wireless battery node (soil moisture only):

PartRole
Seeed Studio XIAO ESP32-C6 (or DFRobot FireBeetle 2 ESP32-E)low-power MCU + WiFi, onboard LiPo charger
1S LiPo, 500–1000 mAh, with protection circuitpower, recharged over USB-C in place
Capacitive soil moisture sensor, VCC on a GPIOswitched on only while sampling
2 × 220 kΩ resistors (XIAO only)voltage divider for battery monitoring

Wiring, config.h presets per board, battery-life estimates and calibration are in docs/05-nodos-a-bateria.md.

Getting started

The detailed walkthrough (with expected outputs and a troubleshooting table) is docs/03-puesta-en-marcha.md. In short:

  1. Backend: create a free Supabase project, run supabase/schema.sql in the SQL Editor, insert a row into plants and note its UUID.
  2. Toolchain: Arduino IDE 2 with the Espressif ESP32 board package, plus the DHT sensor library (Adafruit) and BH1750 (Christopher Laws) libraries.
  3. Firmware: copy firmware/plantpulse/config.example.h to config.h, fill in WiFi credentials (2.4 GHz only), Supabase URL + anon key, the plant UUID and your calibration values, then flash with board ESP32 Dev Module.
  4. Verify: watch the serial monitor at 115200 baud for [http] 201: lectura registrada, then check the readings table in Supabase.
  5. Notifications: create a Telegram bot with @BotFather, deploy the notify function with the Supabase CLI and store its URL and shared token in Vault. Full walkthrough in docs/04-notificaciones-telegram.md.

Data contract

readings insert payload (sent by the firmware):

{
  "plant_id": "8f8e6c1a-…",
  "soil_moisture_pct": 41.7,
  "soil_moisture_raw": 2451,
  "air_temp_c": 24.1,
  "air_humidity_pct": 51.2,
  "light_lux": 342.5
}

Battery-powered nodes add "battery_v": 3.96; mains-powered nodes omit the key entirely, so their firmware keeps working against a schema that predates the column.

latest_readings view (one row per plant — what any dashboard or script should read):

columnmeaning
plant_id, name, species, locationplant identity
soil_moisture_pct, air_temp_c, air_humidity_pct, light_luxlatest measurement (null = sensor absent/failed)
moisture_min_pct, moisture_max_pctper-plant comfort band
needs_watertrue when the latest moisture is below the plant's minimum
created_attimestamp of the latest reading

Example query with curl:

curl "https://<project>.supabase.co/rest/v1/latest_readings?select=name,soil_moisture_pct,needs_water" \
  -H "apikey: <anon-key>" -H "Authorization: Bearer <anon-key>"

Troubleshooting

  • [wifi] timeout — wrong credentials or a 5 GHz-only network; the ESP32 only joins 2.4 GHz.
  • HTTP 401 — anon key mistyped or truncated (it is very long).
  • Moisture stuck at 0% or 100% — calibration values in config.h don't match your sensor unit; redo the two-point calibration.
  • Upload stuck at Connecting.... — hold the board's BOOT button until flashing starts.

More cases in docs/03-puesta-en-marcha.md.

License

MIT — Juan Martos Cuevas (juanmmm21).

esc
↑↓ navigate↵ open