Smart Garden Watering with Node-RED and Home Assistant
After spending one too many summers manually turning on sprinklers during a heatwave (or worse, forgetting to), I decided it was time to automate my garden watering. The result is a Node-RED flow that evaluates weather conditions daily at sunrise and decides whether to water and for how long.
What the System Does
The core idea is simple: water the garden based on three factors:
- Maximum temperature since the last watering
- Days elapsed since the last watering
- Total rainfall (actual + expected) since the last watering
Every morning at sunrise, the system evaluates these conditions and either starts a watering cycle or skips the day. When it waters, it sequences through four zones one at a time to maintain adequate water pressure.
Hardware Setup
My setup uses two types of Gardena smart irrigation products:
┌─────────────────┐ │ Water Main │ └────────┬────────┘ │ ┌───────────────────┼───────────────────┐ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ Gardena Smart │ │ Outdoor Tap │ │ Gateway │ └────────┬────────┘ └────────┬────────┘ │ │ ▼ ┌────────▼────────┐ ┌─────────────────┐ │ 3-Valve Smart │ │ Bluetooth Valve │ │ Distribution Box│ │ (Gardena) │ └────────┬────────┘ └────────┬────────┘ │ │ ┌───────┼───────┐ ▼ │ │ │ ┌─────────────────┐ ▼ ▼ ▼ │ Gardena Water │ Lawn Planter Planter │ Stop + Spray │ Left Right └─────────────────┘ (Drip) (Drip) Front YardBackyard (via Smart Gateway):
- Lawn - Standard sprinkler coverage
- Left Planter - Drip irrigation system
- Right Planter - Drip irrigation system
Front yard (via Bluetooth valve):
- Front Yard - Connected to a Gardena water stop with spray nozzle
The zones are watered sequentially rather than simultaneously because running multiple zones would drop the water pressure too much for effective coverage.
The Decision Logic
Every day at sunrise, the system runs through this decision tree:
┌─────────────┐ │ Sunrise │ └──────┬──────┘ │ ▼ ┌────────────────────────┐ │ Days since watering >4?│ └────────────┬───────────┘ │ │ Yes No │ │ ▼ ▼ ┌──────────┐ ┌─────────────────────┐ │Reset all │ │Check max temperature│ │counters │ └──────────┬──────────┘ └──────────┘ │ ▼ ┌───────────────────────┐ │ Match temperature tier │ │ & check days threshold │ └───────────┬───────────┘ │ ▼ ┌───────────────────────┐ │ Rain < threshold for │ │ this tier? │ └───────────┬───────────┘ │ │ Yes No │ │ ▼ ▼ ┌────────────┐ ┌──────────┐ │Start cycle │ │Skip today│ └────────────┘ └──────────┘Temperature Tiers
The watering duration and conditions depend on how hot it’s been:
| Max Temp | Days Since Last | Rain Threshold | Duration |
|---|---|---|---|
| > 31°C | any | < 20.1mm | 90 min |
| 26-31°C | ≥ 2 days | < 15.1mm | 75 min |
| 21-26°C | ≥ 3 days | < 10.1mm | 60 min |
| 15-21°C | ≥ 3 days | < 10.1mm | 45 min |
| Any | > 4 days | - (safety) | reset |
Visual representation:
Duration (min) │ 90 ├────────────────────────────────────────────── > 31°C (extreme heat) │ 75 ├──────────────────────────────────── 26-31°C (hot) │ 60 ├────────────────────────── 21-26°C (warm) │ 45 ├──────────────── 15-21°C (mild) │ 0 ├──── < 15°C (skip watering) └────┬─────┬─────┬─────┬─────┬────► Temperature (°C) 15 21 26 31 35The logic is intentionally conservative: during extreme heat, water every day regardless of how recently you watered. During milder temperatures, wait a few days between cycles.
Rain Tracking
Rain data comes from OpenWeatherMap integration in Home Assistant. The system tracks two values:
- Actual rainfall - How much rain has fallen since the last watering
- Expected rainfall - Today’s forecast:
precipitation amount × probability
For example, if the forecast shows 5mm with 60% probability, the expected rain is 5 × 0.6 = 3mm.
Both values are accumulated between watering sessions and summed to determine if we’ve had “enough” rain. This approach isn’t perfect—sometimes forecasts are wrong—but it’s been reliable enough in practice.
// Simplified rain calculationconst actualRain = msg.payload; // from weather sensorconst currentTotal = parseFloat(global.get('rain_since_watering')) || 0;const newTotal = currentTotal + actualRain;global.set('rain_since_watering', newTotal);Temperature Tracking
Temperature tracking follows a similar pattern, but we only care about the maximum temperature since the last watering:
// Temperature tracking logicconst currentTemp = msg.payload;const storedMax = parseFloat(global.get('max_temp_since_watering')) || 0;
if (currentTemp > storedMax) { global.set('max_temp_since_watering', currentTemp);}This runs every time the temperature sensor updates. After watering (or after 4+ days), the maximum is reset to start fresh.
The Watering Sequence
When watering is triggered, each zone runs sequentially. Here’s what a 90-minute cycle looks like:
Time ────────────────────────────────────────────────────────────────────►
Sunrise ~6h later │ │ ▼ ▼ ┌─────────┐ │Evaluate │ │Conditions│ └────┬────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ Notification: "Watering started for 90 minutes" │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ Front Yard │ │ ████████████████████████████████████████████████████████████████ │ │ 0 ──────────────────────────────────────────────────────────► 90min │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ Lawn │ │ ████████████████████████████████████████████████████████████████ │ │ 0 ──────────────────────────────────────────────────────────► 90min │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ Left Planter (drip) │ │ ████████████████████████████████████████████████████████████████ │ │ 0 ──────────────────────────────────────────────────────────► 90min │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ Right Planter (drip) │ │ ████████████████████████████████████████████████████████████████ │ │ 0 ──────────────────────────────────────────────────────────► 90min │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ Notification: "Watering stopped" │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ Reset: days counter, max temp, rain accumulator │ └─────────────────────────────────────────────────────────────────────┘A full 90-minute cycle across all four zones takes 6 hours to complete. The morning start time ensures everything is done by early afternoon.
Node-RED Flow Overview
The flow uses these key node types:
Triggers:
state-changenodes watchingsun.sunfor sunrisestate-changenodes watching temperature and rain sensors
Logic:
functionnodes for calculations and threshold comparisonsswitchnodes for routing based on conditionscurrent-statenodes to check sensor values
Actions:
call-servicenodes to turn valves on/offdelaynodes for watering durationapi-call-servicefor mobile notifications
Flow control:
link-inandlink-outnodes for the reset sequence
Here’s a simplified version of the main evaluation function:
const maxTemp = parseFloat(global.get('max_temp_since_watering')) || 0;const daysSince = parseInt(global.get('days_since_watering')) || 0;const totalRain = parseFloat(global.get('rain_since_watering')) || 0;
// Safety reset after 4+ daysif (daysSince > 4) { return [null, null, null, null, { payload: 'reset' }];}
// Extreme heat: >31°C, any days, <20.1mm rainif (maxTemp > 31 && totalRain < 20.1) { return [{ payload: 90 }, null, null, null, null]; // 90 min}
// Hot: 26-31°C, 2+ days, <15.1mm rainif (maxTemp > 26 && maxTemp <= 31 && daysSince >= 2 && totalRain < 15.1) { return [null, { payload: 75 }, null, null, null]; // 75 min}
// Warm: 21-26°C, 3+ days, <10.1mm rainif (maxTemp > 21 && maxTemp <= 26 && daysSince >= 3 && totalRain < 10.1) { return [null, null, { payload: 60 }, null, null]; // 60 min}
// Mild: 15-21°C, 3+ days, <10.1mm rainif (maxTemp > 15 && maxTemp <= 21 && daysSince >= 3 && totalRain < 10.1) { return [null, null, null, { payload: 45 }, null]; // 45 min}
// Skip wateringreturn null;Each output connects to a different watering duration flow.
Lessons Learned After One Season
After running this system through a full summer, I’ve made several adjustments:
Temperature thresholds needed tweaking. My initial thresholds were too conservative. During a 35°C+ heatwave, even 90 minutes wasn’t enough for some plants. I added a manual boost option for extreme conditions.
Drip systems need longer. The planters with drip irrigation needed roughly 50% longer run times compared to the lawn sprinklers to achieve equivalent saturation. I adjusted the per-zone durations accordingly.
Rain prediction is imperfect. Occasionally the system skips a day expecting rain that never comes, or waters just before an unexpected shower. It’s still better than manual management, but there’s room for improvement.
Notifications are essential. Getting a mobile notification when watering starts lets me catch any issues (forgotten hose still connected, etc.) before 6 hours of watering begins.
Known Limitations
The current system has a few gaps I’m aware of:
No mid-cycle abort. If it starts raining heavily while watering is in progress, the system will continue until the cycle completes. Adding a rain sensor trigger to stop early is on my to-do list.
No soil moisture feedback. The system uses weather data as a proxy for soil conditions, but actual soil moisture sensors would provide ground truth. The Gardena ecosystem doesn’t include these, so it would require additional hardware.
Manual intervention during heatwaves. Extended heat periods sometimes need manual watering boosts beyond what the automation provides. A “heatwave mode” with more aggressive settings could help.
Future Improvements
Some enhancements I’m considering:
- Rain sensor integration - Abort watering if significant rain is detected during a cycle
- Soil moisture sensors - Replace weather-based guessing with actual measurements
- Per-zone adjustments - Different plants have different needs; the planters could use separate logic from the lawn
- Seasonal presets - Automatically adjust thresholds based on time of year
Conclusion
Automating garden watering with Node-RED and Home Assistant has been one of my more satisfying home automation projects. The system handles 90% of watering decisions correctly, which is enough to keep the garden healthy while freeing me from the daily chore of manual irrigation.
If you’re considering a similar setup, start simple. Begin with one zone and basic temperature-based logic, then add complexity as you learn what your garden actually needs. The beauty of Node-RED is that you can iterate quickly—just drag, connect, and deploy.
For questions or suggestions, feel free to reach out on GitHub.