DEV Community

Zhao Yang
Zhao Yang

Posted on

Monsoon: a garden planner for places that never see frost (local LLM + open weather data)

Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass Submission 🌿

This is a submission for the Touch Grass challenge: build something with open-source AI that gets people off the screen and into the world.

Monsoon is a garden planner for tropical and subtropical gardeners. Once a week it looks at your location, the 7-day forecast, and a local crop calendar. Then it hands you one short card: what to sow, transplant, or protect this week, and why.

It runs on an open-weight model on your own machine. Your garden, your location, and your questions never go to someone else's server. The only network call is a weekly weather fetch from an open-source weather API.

The whole interaction is designed to take less time than watering a single pot.


The problem: every garden app assumes you have a winter

Search "what should I plant this week" and you'll find a lot of excellent tools. Almost all of them are built around one number: your last frost date. Count back six weeks, start your tomatoes indoors, transplant after the frost passes.

That works well in Ohio. It makes no sense in Hanoi, Bangkok, Manila, Lagos, or Recife.

I garden on a balcony in Hanoi. Our year isn't split by frost. It's split by heat, rain, and the cool dry window between them:

  • Summer (roughly May–September): hot, humid, and heavy downpours. This is the season for water spinach, amaranth, gourds, and okra. Most cool-season greens bolt or rot.
  • Autumn into winter (roughly September–December): the start of the winter crop season. Farmers in the Red River Delta plant kohlrabi, cabbage, mustard greens, tomatoes, and potatoes now. It's October as I write this, so this window is open right now.
  • Late winter (January–February): cool and drizzly, with occasional cold snaps below ~10 Β°C that can stunt heat-loving plants.
  • Spring (March–April): warming up again, and time to switch back to summer crops.

Then there's the far north of the country: up in the mountains around Sapa, frost really does happen in some winters. So a planner for Vietnam can't just drop frost. It has to treat frost as one risk among several: heavy rain, heat stress, cold snaps, and frost where it applies.

Farmers here have handled this for centuries with seasonal crop calendars, which agricultural extension services now publish by province. But those calendars are written for fields, in PDFs. They don't help someone with six pots on a balcony who needs to know: should I sow the mustard seeds today, or will Thursday's storm wash them out?

That gap is what I built Monsoon to fill.


What I built

Monsoon does three things:

1. The weekly card (the main feature)

Every Sunday evening, Monsoon generates one card:

🌱 MONSOON β€” Week of 12 Oct Β· Hanoi

DO THIS WEEK
β†’ Sow mustard greens β€” Mon or Tue
  Cool season has started and the soil is still warm.
  Get them in before Thursday: ~35 mm of rain is forecast,
  which can wash out fresh seed.

ALSO GOOD
β†’ Transplant tomato seedlings in the evening (highs still ~31 Β°C)

SKIP
β†’ Water spinach β€” its season is ending; new sowings will be weak.

Next check: Sunday 19 Oct
Enter fullscreen mode Exit fullscreen mode

That's the whole interface. You read it in about 15 seconds, then you go outside and do it.

The card can go to a Telegram message, a printable page, or plain text you stick on the fridge. I deliberately didn't build a dashboard with charts and streaks. Engagement is not the goal. Dirt under your fingernails is the goal.

2. Ask a question (optional)

Sometimes you need more than the card: "My kohlrabi leaves have holes in them, what now?" or "Can I grow basil through December?" You can ask the local model directly. It answers using your location, the season, and the same crop knowledge base the card uses.

3. Your garden profile

A small local file describes what you've got: balcony or ground, sun hours, what's currently planted, and roughly when you planted it. It never leaves your machine.

location:
  name: Hanoi
  lat: 21.03
  lon: 105.85
space: balcony
sun_hours: 5
planted:
  - crop: tomato
    stage: seedling
    date: 2026-09-20
  - crop: basil
    stage: established
    date: 2026-06-01
Enter fullscreen mode Exit fullscreen mode

How it works

The most important design decision: the LLM is not in charge of agronomy.

Small models are good at language and reasoning over facts you hand them. They are not reliable sources of planting windows for a specific province. If you ask a small model cold, "when do I plant potatoes in Hanoi?", you might get a confident answer copied from temperate-climate advice.

So Monsoon splits the work in two:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Crop calendar   β”‚   β”‚ Weather forecast β”‚   β”‚  Garden profile   β”‚
β”‚  (curated JSON)  β”‚   β”‚  (Open-Meteo,    β”‚   β”‚  (local YAML)     β”‚
β”‚                  β”‚   β”‚   cached weekly) β”‚   β”‚                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚                      β”‚                       β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                        β–Ό
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
            β”‚  Rules engine (Python)β”‚   ← deterministic: what's *allowed*
            β”‚  candidate tasks +    β”‚
            β”‚  weather flags        β”‚
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                        β–Ό
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
            β”‚  Local LLM (Ollama)   β”‚   ← language: what *matters most*,
            β”‚  rank + explain,      β”‚     and how to say it
            β”‚  JSON-schema output   β”‚
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                        β–Ό
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
            β”‚  Validator            β”‚   ← rejects anything not in the
            β”‚                       β”‚     candidate list
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                        β–Ό
                  🌱 Weekly card
Enter fullscreen mode Exit fullscreen mode

The rules engine decides what is possible. The model decides what matters most this week and how to say it. A validator makes sure the model can't invent a crop or a task that the rules didn't allow.

Step 1: A crop calendar as data, not vibes

Each crop is a small JSON record. Planting windows are stored per climate zone, so northern lowlands, the northern mountains, and the south each get their own windows.

{
  "id": "mustard_greens",
  "name": "Mustard greens",
  "zones": {
    "north_lowland": {
      "sow": [{ "start": [9, 1], "end": [2, 28] }]
    },
    "north_mountain": {
      "sow": [{ "start": [8, 15], "end": [10, 31] }],
      "frost_sensitive": false
    },
    "south": {
      "sow": [{ "start": [11, 1], "end": [3, 31] }]
    }
  },
  "seed_washout_risk": true,
  "max_temp_for_sowing": 32,
  "days_to_harvest": [30, 45],
  "containers_ok": true
}
Enter fullscreen mode Exit fullscreen mode

Windows like September β†’ February wrap around New Year, so the window check has to handle that:

from datetime import date

def in_window(today: date, window: dict) -> bool:
    start = date(today.year, *window["start"])
    end = date(today.year, *window["end"])
    if end < start:  # window wraps over New Year, e.g. Sep -> Feb
        return today >= start or today <= end
    return start <= today <= end
Enter fullscreen mode Exit fullscreen mode

I seeded the calendar from published seasonal crop guidance and general horticulture references, then simplified it for home and container growing. This is the part of the project I'd most like other people to contribute to. It's a plain JSON file, so a gardener in Chiang Mai or Kerala can add their crops without touching any code.

Step 2: Weather from an open API, fetched once a week

Open-Meteo is an open-source weather API. It needs no API key, and you can self-host it if you want. Monsoon fetches a 7-day forecast once a week and caches it to disk:

import json
import pathlib
import requests

CACHE = pathlib.Path("cache/forecast.json")

def fetch_forecast(lat: float, lon: float) -> dict:
    try:
        r = requests.get(
            "https://api.open-meteo.com/v1/forecast",
            params={
                "latitude": lat,
                "longitude": lon,
                "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum",
                "timezone": "auto",
                "forecast_days": 7,
            },
            timeout=10,
        )
        r.raise_for_status()
        daily = r.json()["daily"]
        CACHE.parent.mkdir(exist_ok=True)
        CACHE.write_text(json.dumps(daily))
        return daily
    except requests.RequestException:
        # No signal? Use last week's forecast and say so on the card.
        if CACHE.exists():
            return json.loads(CACHE.read_text()) | {"stale": True}
        return {"stale": True, "time": []}
Enter fullscreen mode Exit fullscreen mode

If there's no connection, Monsoon still works. It falls back to the cached forecast (or to the calendar alone) and tells you so on the card.

Step 3: The rules engine turns weather into flags

A few simple heuristics cover most of what matters in a tropical garden:

def weather_flags(daily: dict) -> list[dict]:
    flags = []
    for day, rain, tmax, tmin in zip(
        daily.get("time", []),
        daily.get("precipitation_sum", []),
        daily.get("temperature_2m_max", []),
        daily.get("temperature_2m_min", []),
    ):
        if rain is not None and rain >= 20:
            flags.append({"day": day, "type": "heavy_rain", "mm": rain})
        if tmax is not None and tmax >= 32:
            flags.append({"day": day, "type": "heat", "c": tmax})
        if tmin is not None and tmin <= 10:
            flags.append({"day": day, "type": "cold_snap", "c": tmin})
        if tmin is not None and tmin <= 2:
            flags.append({"day": day, "type": "frost_risk", "c": tmin})
    return flags
Enter fullscreen mode Exit fullscreen mode

The thresholds are rules of thumb, not science. Heavy rain means don't sow small seeds right before it. Heat means transplant in the evening and shade seedlings. A cold snap means cover basil and peppers. Frost risk only really fires in the mountains, which is exactly the point.

The engine then crosses three things: crops that are in their window, crops already in your garden, and these flags. The result is a short list of candidate tasks, each with an ID and the facts behind it.

Step 4: The local model ranks and explains

The model gets the candidate list, the weather flags, and your garden profile. Its job is to pick the single most important task, add one or two extras, flag anything to skip, and explain each choice in plain language.

I run it with Ollama and use structured output: the response must match a JSON schema, so I never have to parse free text.

import ollama
from pydantic import BaseModel

class Task(BaseModel):
    task_id: str
    when: str
    why: str

class WeekCard(BaseModel):
    do_this: Task
    also_good: list[Task]
    skip: list[Task]

SYSTEM = """You are a calm, practical gardening helper for a home gardener.
You may ONLY choose tasks from the provided candidate list, using their exact task_id.
Use the weather flags to decide timing. Keep every 'why' under 30 words.
Never invent crops, dates, or numbers that are not in the input."""

def make_card(candidates: list[dict], flags: list[dict], profile: dict) -> WeekCard:
    resp = ollama.chat(
        model="qwen3:4b",
        messages=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": json.dumps({
                "candidates": candidates,
                "weather_flags": flags,
                "garden": profile,
            })},
        ],
        format=WeekCard.model_json_schema(),
        options={"temperature": 0.2},
    )
    return WeekCard.model_validate_json(resp.message.content)
Enter fullscreen mode Exit fullscreen mode

Step 5: Trust, but verify

Even with a schema, the model could return a task_id that isn't in the candidate list. So a validator checks every task. Anything unknown gets dropped, and if the main task is invalid, Monsoon falls back to the top-ranked candidate from the rules engine:

def validate(card: WeekCard, candidates: list[dict]) -> WeekCard:
    allowed = {c["task_id"] for c in candidates}
    card.also_good = [t for t in card.also_good if t.task_id in allowed]
    card.skip = [t for t in card.skip if t.task_id in allowed]
    if card.do_this.task_id not in allowed:
        top = candidates[0]
        card.do_this = Task(task_id=top["task_id"], when="this week", why=top["reason"])
    return card
Enter fullscreen mode Exit fullscreen mode

This is the piece that made me comfortable letting a small model write advice. It can phrase things badly, but it can't tell you to plant something that's out of season.

Choosing the model

I wanted three things: it runs on an ordinary laptop, it handles multiple languages well, and it follows a JSON schema reliably. A small Qwen3 model fits all three, and its open weights mean I can fine-tune it later.

A card is generated once a week, so even a slow machine is fine. Speed simply doesn't matter much for this use case.

Because the model is behind a single function call, swapping it is a one-line change. If a better small multilingual model comes out next month, I change the model name and keep going.


Why open innovation matters for this project

The challenge asks where the open approach worked better than a closed one. For Monsoon, the honest answer is that almost every key property of the project depends on it being open.

1. My garden is my data

A garden profile looks harmless, but it contains your precise location, when you're home, what you grow, and over time, a log of your habits. A planner that's useful every week for years builds up exactly the kind of long-term personal data that I don't want sitting on a server I don't control.

With a local model, the only thing that ever leaves my machine is a latitude/longitude pair sent to a weather API. And because Open-Meteo is open source, even that can be self-hosted.

2. It costs nothing to run, forever

A weekly card for one gardener is tiny. But a free tool for a whole community garden, a school, or a farmers' cooperative shouldn't depend on someone paying for API calls every month. Run on a spare laptop or a small single-board computer, Monsoon's running cost is effectively the electricity. Nobody has to keep paying for it to keep working.

3. It works when the internet doesn't

Gardens are often outside the strongest Wi-Fi, and rural connectivity can be patchy. Monsoon needs the network for exactly one request a week, and degrades gracefully without it. The model, the calendar, and your history are all local.

4. I can make it local in the cultural sense too

This is the part I care about most. Being open doesn't just mean "runs locally." It means I can adapt the system to a place:

  • The knowledge base is a plain, editable file. A gardener in Chiang Mai can add Thai crops and zones. A grower in Kerala can add monsoon timings for their region. None of that needs anyone's permission.
  • The model can be changed or fine-tuned. The next step I'm exploring is a small LoRA adapter trained on regional agricultural extension material, so the model uses the same terms and crop variety names local farmers use instead of generic, temperate-climate gardening language.
  • The prompts and rules are visible. If the advice is wrong, you can see why. Was it the calendar, the weather rule, or the model's phrasing? Then you can fix the right layer.

A closed, hosted model treats "Hanoi" as an edge case of a system built mostly around temperate assumptions. An open system lets the people who actually live in a place define what normal looks like.

Where closed would have been easier (to be fair)

Closed frontier models are better at answering open-ended plant questions from general knowledge. For the "ask a question" feature, a bigger hosted model would give richer answers.

But Monsoon's core loop doesn't need a genius. It needs a model that follows a schema, reads a short list of facts, and writes three clear sentences. Small open models are well suited to that. And splitting the system into rules + model + validator means the model's weaknesses are contained.


Making the screen the shortest part

The theme asks for builds where the screen is the shortest part of the experience. Here's how I approached that:

Design choice Why
One card per week, not a feed Nothing to scroll, nothing to check daily
One main task on each card Easy to remember once you're outside
Timing built in ("Mon or Tue, before the rain") You don't need to open a weather app separately
Printable / message-based output The card can live on the fridge, not in an app
No streaks, badges, or notifications beyond the weekly card No pressure to come back to the screen

About 15 seconds of reading per week. Everything else happens in the garden.


Limitations

  • The crop calendar is the bottleneck. The output is only as good as the windows in the JSON, and coverage is still limited to common vegetables and herbs.
  • Microclimates are hard. A balcony on the 12th floor facing west is not the same as a ground-level garden in the same district. The sun_hours and space fields help, but only a bit.
  • Weather thresholds are generic. 20 mm of rain is a rough cutoff. Different crops and soils react differently.
  • Small models still drift. Without the validator, a small model can suggest tasks that aren't in the list or get timing wrong. The validator catches those mistakes, but the explanations can still be bland.
  • This is not expert agronomic advice. It's a nudge for home gardeners. For pests and diseases, the card points you to local extension services or experienced growers.

What's next

  • [ ] More crops and zones, especially Southeast Asia and South Asia
  • [ ] A LoRA fine-tune on regional agricultural extension material
  • [ ] A simple "what I actually did" log, so next year's cards learn from this year
  • [ ] A community-garden mode: one card for a shared plot, sent to everyone in a group chat
  • [ ] Running it on a cheap single-board computer that prints the card on a small receipt printer every Sunday

Try it

pip install requests ollama pydantic pyyaml
ollama pull qwen3:4b      # or any model you prefer
cp garden.example.yaml garden.yaml   # edit with your location and plants
python monsoon.py card
Enter fullscreen mode Exit fullscreen mode

If you garden somewhere without frost, I'd love your help with the crop calendar. Share your region's crops and planting windows in the comments.

Now close the laptop and go check on your seedlings. 🌱


Built with: Ollama, Qwen3 (open weights), Open-Meteo, Python, Pydantic.

Top comments (0)