12 Deploy and Monitor
12 Deploy and Monitor
Calibration done; the script knows when to water. The last step is handing it off to cron so it runs on a schedule without you babysitting the terminal β and then optionally bringing it up under a small Flask web UI for at-a-glance monitoring from your phone. Cron is the source of truth for scheduling; the Flask UI is just a window onto the running system. Both are LAN-only; both are optional past the cron schedule. Most people stop after cron + SSH. Thatβs a complete, working build.
The cron schedule
Page 09βs setup-pi.sh already installed two cron entries β a 7am check and a 6pm check. Open the crontab with crontab -e to see them, or run crontab -l to print them without opening the editor. The two lines look like this:
0 7 * * * ~/irrigation/run.sh >> ~/irrigation/logs/cron.log 2>&1
0 18 * * * ~/irrigation/run.sh >> ~/irrigation/logs/cron.log 2>&1
Each scheduled run executes ~/irrigation/run.sh (the venv-activator wrapper from page 09 that calls python3 irrigate.py), redirecting both stdout and stderr to the cron log at ~/irrigation/logs/cron.log. The scriptβs per-zone JSON log lives separately at ~/irrigation/logs/irrigation.json β thatβs the structured one status.py reads.
What does 0 7 * * * mean? Cronβs five fields are minute, hour, day-of-month, month, day-of-week. 0 7 * * * reads as βminute 0, hour 7, every day, every month, every weekdayβ β 7am sharp every day. 0 18 * * * is the same thing at 6pm. The * means βany value.β If you want the full reference, man 5 crontab on the Pi has it.
Optional: midday check during Seattleβs dry season
In July and August, Seattle gets enough sun + heat that twice-a-day watering can fall behind. Add a third cron line for a midday check:
# Uncomment in July/August for a midday check
# 0 12 * * * ~/irrigation/run.sh >> ~/irrigation/logs/cron.log 2>&1
To edit the crontab, run crontab -e from the Piβs SSH session, paste the line in, save, and exit. Comment it back out in September when temperatures drop. The script is moisture-driven, not time-driven β adding a midday slot doesnβt water more often if soil is fine; it just gives the script another chance to notice if a zone went dry between 7am and 6pm.
The status script
status.py lives at ~/irrigation/status.py (page 09βs setup-pi.sh installed it alongside the irrigation script). Run it anytime you want a quick snapshot of what the system has been doing and what the sensors are reading right now:
cd ~/irrigation
source venv/bin/activate
python3 status.pyThe output prints two blocks. The first is recent activity β the last 10 entries from irrigation.json, each line showing timestamp, zone, action (watered or skip), moisture reading, and how many cycles ran. The second block is current sensor readings β a fresh ADC read per zone, averaged over 5 samples, with a DRY / OK / WET label based on the same thresholds the irrigation script uses. If a zone shows ERROR, the sensor isnβt responding (loose wire most likely; the Stuck? sidebar below points at the diagnostic).
Remote check from your laptop
You donβt have to SSH into the Pi and re-activate the venv every time you want a status check. Run the whole thing as a single SSH command from your laptop:
ssh [email protected] 'cd ~/irrigation && source venv/bin/activate && python3 status.py'That gives you the same two-block output, printed straight back to your laptop terminal. If irrigator.local doesnβt resolve (your phone or laptop doesnβt speak mDNS), fall back to the Piβs IP address β page 09βs mDNS callout walks the lookup. macOS, modern Linux, and most home routers handle .local natively; older Windows machines sometimes donβt. If youβre on iOS/Android, an SSH client like Termux or Termius will resolve irrigator.local correctly when on the same WiFi.
Skip or stay? If βSSH from another device whenever I want a status checkβ is enough for you, youβre done with this page. The cron schedule keeps the irrigation running; status.py over SSH gives you visibility on demand. Novice readers: skip ahead to page 13 β. The Flask UI section below is for readers who want a phone-friendly status page on the LAN β itβs nicer, but not required for a complete build.
Optional: the Flask web UI
For at-a-glance status from any phone or tablet on your home WiFi, run a small Flask app on the Pi that exposes a status page at http://irrigator.local:8080/. It reads the moisture sensors + the log tail every page load and renders a single HTML page that meta-refreshes every 30 seconds. No JavaScript required. Thereβs a manual-water override behind a query-string key, but itβs LAN-only β never port-forward this to the public internet (the security caveat below covers why). The screenshots arenβt ready yet (Phase 7 ships the actual app); the ASCII mockups below show the layout the implementation is targeting.
The home page (GET /)
When you visit http://irrigator.local:8080/ from your phone, the home page shows current moisture per zone with a DRY / OK / WET label, the last few log entries, and the next scheduled cron run. In the shipped UI each zone is a coloured panel β DRY is pink, OK is green, WET is blue β so the status is legible at armβs length without reading the label. The ASCII bars in the mockup below are an aspirational visual only; the actual rendering uses the coloured-panel treatment.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Irrigator [auto-refresh] β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Zones β
β βββββ β
β lettuce 14820 ββββββββββββ OK β
β tomatoes 21340 βββββββββββββ DRY β
β raised_bed 11200 ββββββββββββ WET β
β β
β Recent activity β
β βββββββββββββββ β
β 2026-04-26 18:00 tomatoes watered β
β 2026-04-26 18:00 lettuce skip β
β 2026-04-26 07:00 raised_bed watered β
β 2026-04-26 07:00 tomatoes watered β
β β
β Next run: 2026-04-27 07:00 β
β β
β [Manual water form below β when ?key= is valid] β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The page meta-refreshes every 30s β leave it open on a phone propped on the kitchen counter and youβve got a live moisture display. No JS, no app to install; just a bookmark.
The manual-water override (POST /water)
When you arrive with a valid ?key= in the URL, the home page renders an inline form below the recent-activity log that lets you trigger a single zone for N seconds without waiting for the next cron run. Useful for βI just transplanted the lettuce and want to give it a drink right nowβ or βIβm bench-testing a new sensor and want to confirm the valve still fires.β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Manual water β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Zone: [ tomatoes βΎ ] β
β Seconds: [ 30 ] (1β120) β
β β
β [Run zone] β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The formβs own action URL embeds the ?key= you arrived with β thereβs no typed Key field on the form itself; the key travels as a query-string parameter on every submit. The form posts zone + seconds to /water?key=$IRRIGATOR_KEY. The Flask app validates the key, caps seconds at 120 (both client- and server-side β the formβs max attribute and a server-side check), and shells out to the existing manual_water.py to open the valve and run the pump for the requested duration. Without a valid key, the request is rejected with a 403; with one, the relevant zone fires immediately. Without ?key= in the URL the form simply doesnβt render β the home page shows a one-liner reminder to append the key to the URL. The 120s cap is the same safety limit manual_water.py already enforces from the command line β if you want longer, youβd have to edit the script and accept the risk of leaving a pump running.
Code listing + service unit
Phase 7 ships irrigator-web.py (the Flask app) and irrigator-web.service (a systemd unit that binds it to 0.0.0.0:8080 and restarts on failure). The service sources IRRIGATOR_KEY from /etc/default/irrigator-web so each install can have a unique key without baking it into source. The four existing scripts (irrigate.py, calibrate.py, status.py, manual_water.py) plus irrigator-web.py will all live at static/projects/balcony-irrigation/scripts/ for direct download β single-page checkout, no git clone needed.
Security caveat: LAN only. Do NOT port-forward port 8080 to the public internet. The shared ?key= is enough deterrent for casual nosy neighbors on your home WiFi; it is NOT enough to expose this anywhere a port-scanner can find it. The Flask app runs as a regular user with GPIO access, which means an attacker with the key can fire your pump on demand β annoying at worst on a balcony, catastrophic if the same pattern shows up on a system controlling a sprinkler valve indoors. If you want off-LAN access, see Tailscale below β never punch a hole in your router for this app.
Off-LAN access (advanced)
Tailscale is the right answer for off-LAN access without exposing port 8080 to the internet. Install Tailscale on the Pi (curl -fsSL https://tailscale.com/install.sh | sh, then sudo tailscale up), install Tailscale on your laptop or phone, and the Piβs MagicDNS hostname (e.g., irrigator-pi.your-tailnet.ts.net) becomes reachable from anywhere youβve authed Tailscale. http://irrigator-pi.your-tailnet.ts.net:8080/ works from any of those devices. WireGuard mesh-VPN under the hood; no port-forward, no public exposure, no NAT punch-through to debug.
Home Assistant integration via MQTT publishing is a v2 idea (REQUIREMENTS.md WEB-UI-V2-01); not in this build. The hook would be a small MQTT publish call from the irrigation loop that emits per-zone moisture + last-water-time so HA can plot it alongside the rest of your home sensors.
Build complete. Page 13βs diagnostic flowchart is your reference for when something stops working β bookmark it.