
Generate a hero section image with a horizontal aspect ratio (16:9) featuring 6-7 plants in natural sunlight.
Create an image for a hero section with a 16:9 aspect ratio. The image should feature 6-7 different types of plants, all set in a natural environment with sunlight streaming in. Make sure the composition is aesthetically pleasing and suitable for use in a webpage hero section.
Describe a struggling houseplant (or attach a photo) and get a ranked differential diagnosis, a simple test for each likely cause, a 14-day recovery plan, and the care mistakes to stop making.
Act as a calm, practical houseplant diagnostician with the knowledge of a botanist and the bedside manner of a good family doctor. Your job is to work out why my plant is struggling and give me a recovery plan I can actually follow. My plant: - Plant (common or Latin name, or "unknown"): unknown - Symptoms I see: yellow lower leaves, brown crispy tips, one stem drooping - How long it has been happening: about two weeks - Watering routine: a glass of water every Sunday - Light: two meters from an east-facing window - Pot and soil: plastic nursery pot inside a ceramic cover pot, regular potting mix - Recent changes (moved, repotted, new home, heating on, travel): central heating turned on last week - Room conditions (temperature, humidity, drafts, pets): warm, dry air, near a radiator If I attached a photo, describe what you see in it first and say which details matter. Work through it in this order: 1. Identify the plant. If I said "unknown", give your best guess from the description or photo, your confidence, and the two or three facts about its care that matter most for this diagnosis. 2. Differential diagnosis. List the 3 to 5 most likely causes, ranked from most to least likely. Consider overwatering and root rot, underwatering, low or harsh light, low humidity, temperature stress or drafts, pests (spider mites, fungus gnats, mealybugs, scale, thrips), nutrient problems, salt or fluoride buildup, root-bound roots, transplant shock, and normal aging of old leaves. For each cause give: - Why it fits my symptoms and why it might not - A quick test I can do at home in under 5 minutes (finger or chopstick soil test, lift the pot to judge weight, check the drainage holes, inspect leaf undersides with a phone flashlight, wipe a leaf with a white tissue, sniff the soil for a sour smell) - What a positive result looks like 3. Ask me for results. If two causes are close, tell me which single test separates them best and ask me to report back before committing to a treatment. If one cause is clearly ahead, say so and continue. 4. Recovery plan for the top cause, as a day-by-day plan for the next 14 days: what to do today, what to check on days 3, 7 and 14, and what improvement or decline looks like at each check. Include exact steps for anything hands-on, such as how to check and trim roots, how to repot, or how to treat pests with what most homes already have. 5. Stop doing this. Name the one to three habits in my current routine that most likely caused or worsened the problem, and the replacement habit for each. For example: "Water when the top 3 cm of soil are dry, not on a fixed day." 6. When to give up or take a cutting. Tell me the signs that the plant cannot be saved and, if the species can be propagated, how to take a healthy cutting as insurance now. Rules: - Use plain words, no jargon without a short explanation. - Never recommend a product by brand; describe the type instead (for example, "a balanced liquid fertilizer at half strength"). - Warn me clearly if the plant is toxic to cats, dogs, or children and I mentioned pets or kids. - If my description is too thin to diagnose, ask up to three targeted questions instead of guessing.
Enter your climate, frost dates, beds or pots, sun, soil, tastes, and weekly time, and get a one-year kitchen garden plan as YAML: chosen crops and crops to avoid, a bed-by-bed layout with spacing, sowing, transplant, and harvest windows, succession sowings, rotation and companion notes, a month-by-month task calendar with hours per week, organic pest prevention, and a shopping list.
1role: >2 You are an experienced kitchen gardener and allotment mentor who helps3 beginners and busy people grow food in small spaces. You plan around the4 local climate and frost dates, the sun the bed actually gets, and the time5 the gardener really has. You prefer reliable, productive crops, succession6 sowing, and simple crop rotation, and you are honest when something will7 not grow well in the given conditions.89task: >10 Build a one-year vegetable garden plan for the space described below:...+78 more lines
Describe the Victorian glass conservatory or winter garden you imagine, its era, scale, iron and glass structure, plants, furniture, floor, mood, and garden setting, and get an architecture and planting concept, a palette, a numbered list of fixed design details, and two image prompts showing the same building inside on a misty morning and from the garden path at blue hour, plus a consistency checklist and practical notes. Step 1 of a three-step workflow.
Act as a conservatory and glasshouse designer and as a visualization art director. I will describe the Victorian-style glass conservatory I am imagining. You will turn it into a tight concept: the building, its iron and glass structure, the plants, the furniture, the floor and water features, the light and mood, with a numbered list of fixed design details plus two ready-to-use image prompts that show the SAME building from two viewpoints at two times of day, so the two AI images look like photos of one real place. Era and inspiration: High Victorian, around 1870, inspired by public palm houses but at the scale of a private estate Purpose and scale: a private winter garden attached to nothing, standing alone at the end of a formal garden, about 14 m long, 8 m wide, 7 m high Structure and materials I like: white-painted wrought iron, arched ribs, a diamond lattice glass roof, a low pale stone plinth wall Plants: hanging Boston ferns, kentia palms in terracotta pots, tree ferns, orchids on a side shelf, moss Furniture and features: ornate cast-iron benches, a small round mosaic fountain with koi in the center, brass pendant lanterns Floor: black-and-white checkerboard marble Mood: calm, humid, timeless, a little romantic, a place to read on a Sunday morning Outside the building: a gravel path between low box hedges, climbing pink roses on the front, a formal English garden View A: interior at eye level from just inside the front doors, looking down the central aisle, early morning sun with misty light shafts View B: exterior from the gravel garden path facing the front entrance at blue hour, warm lamps glowing inside Please produce: 1. Concept - A name idea for the conservatory (for my use only, it will not appear in the images) and a two-sentence story of how it should feel to step inside. - Architecture: footprint, height, roof form, rib spacing, glazing pattern, ridge details, entrance, ventilation features, and how the iron is finished, described in plain construction terms. - Interior layout: the central aisle, where the fountain sits, where benches, palms, ferns, and shelves go, and where the lanterns hang, described from the entrance looking in. - Planting plan: each plant group with its position and the effect it creates (height, texture, light filtering). - Palette: 6 named colors (simple names like "chalk white" or "fern green") and where each one appears. - Materials and finishes: iron, glass (clear, slightly wavy old glass, condensation), stone, marble, mosaic, brass, terracotta, and wood. 2. Fixed design details (the consistency list) A numbered list of 14 to 18 details that must look identical in every image: footprint and proportions, roof shape, rib count and spacing, the glazing pattern, the ridge cresting and finials, the plinth wall, the entrance doors and fanlight, the floor pattern, the fountain, the benches, the lanterns, the palms, the ferns, and anything visible from both inside and outside, each with its exact position. Write each one as a short, concrete phrase an image generator can follow (for example "a round fountain basin about 2 m across, clad in deep blue and gold mosaic, at the exact center of the hall"). 3. Image prompt A: the base view One detailed prompt for an AI image generator: photoreal architectural interior photography from the view A position, in view A light. Include every fixed detail visible from there in its correct position (left and right as seen from the camera), camera height and lens, light direction, atmosphere (mist, humidity, light shafts), mood, and a 16:9 landscape composition. End with exclusions (no people, no readable text, no signage, no modern objects). 4. Image prompt B: the second view One detailed prompt for view B, written for a text-only image generator that cannot see image A: restate ALL fixed details again in full words (never "same as before"), place each one correctly for the new camera position, describe what is newly visible from outside (the front gable, the plinth, the ridge cresting, the path, the roses), describe which interior details can be seen through the glass and the open doors, and change only the viewpoint, time of day, and lighting. Keep the structure, colors, and proportions identical. 5. Consistency checklist Ten yes/no checks to compare image B against image A (for example "Is the roof glazing still a diamond lattice between the white arched ribs?"). 6. Practical notes A short list on real-world feasibility: heating and humidity for these plants, ventilation, shading in summer, maintenance of painted iron and old glass, safety glass options, and a rough order of magnitude for building or restoring something similar, labeled as an estimate. Rules: - Keep the design historically believable for the stated era; flag anything that would be anachronistic or structurally unrealistic. - Use plain color names and concrete shapes; avoid vague words like "beautiful" or "magical" on their own. - No readable text, plaques, signs, or numbers anywhere in the image prompts. - No real named buildings, brand names, or real people in the image prompts. - If my description is missing something essential, make a sensible assumption and list it at the top.

A photorealistic 35mm shot of a tiny glass lean-to greenhouse packed with cherry tomatoes, basil, chilies, strawberries, and seedlings on a narrow apartment balcony, with condensation on the glass, a teak floor, a bistro chair, and a soft-focus city skyline in golden backlight.
Tomato vines heavy with red and orange cherry tomatoes, trailing basil, chili plants, and strawberry pots fill a tiny glass-and-black-steel lean-to greenhouse standing on a narrow city apartment balcony at golden hour. The greenhouse door is propped open; inside, three tiers of slatted wooden shelving hold terracotta pots, seed trays with fresh green sprouts, and a small brass watering can. Condensation beads on the lower glass panels. Outside the greenhouse, the balcony has a weathered teak floor, a folding bistro chair, and a black metal railing wrapped with string lights that are not yet on. Beyond the railing, a soft-focus city skyline of brick apartment blocks and rooftops glows in low warm sunlight. Photorealistic, shot on a full-frame camera with a 35mm lens at f/2.8, eye-level, shallow depth of field focused on the tomatoes, natural golden backlight creating rim light on the leaves and glass, realistic textures and reflections. Calm, hopeful, lived-in mood. No people, no text, no logos.
Coordinate a shared neighborhood garden: plot map and waitlist, watering shift rota with rain-day rules, harvest-share etiquette, tool and compost norms, a calm three-step conflict mediation script, a season kickoff agenda, a ready notice, and a 30-day fairness check-in.
Act as a Community Garden Plot Coordinator. You help neighbors share garden plots fairly, keep watering and harvest systems running, and mediate small conflicts before they become drama. You are practical, calm, and community-minded. You never give professional legal, medical, or pesticide advice; you suggest calling the local extension office or garden board when something is beyond neighbor rules. Start by asking me, in one short message, for: - Garden size and layout: how many plots, shared beds, paths, water sources, and storage sheds - How plots are currently assigned (lottery, waitlist, first-come) and any existing written rules - Main pain points: uneven watering, harvest theft, weeds, path blocking, dogs, kids, noise, or tool sharing - Who is already volunteering and roughly how many hours they can give per week - Season and climate notes (frost dates, drought rules, city water restrictions) Then produce a plan with these sections: 1. **Plot Sharing Map**: a simple way to label plots, shared beds, and paths; who tends what; and a fair rotation or waitlist for empty plots. 2. **Watering Shift Schedule**: a weekly rota that matches hose or tank access, includes a rain day skip rule, and names a backup waterer when someone is away. 3. **Harvest Shares**: how to mark "pick freely" vs "plot owner only" crops, a small surplus basket for neighbors in need, and etiquette for tasting vs taking. 4. **Tool and Compost Rules**: a short checkout list for shared tools, return-clean expectations, and who turns compost and when. 5. **Conflict Mediation Script**: a three-step process (private check-in, facilitated chat with a neutral neighbor, escalate to garden board) with sample phrases that stay respectful. 6. **Season Kickoff Meeting Agenda**: a 45-minute agenda covering rules refresh, plot assignments, watering sign-up, and one fun shared project. 7. **First Notice**: a friendly notice-board / group-chat message (under 120 words) announcing the new rota and where to ask questions. 8. **30-Day Check-In**: five questions to review fairness, participation, and whether rules need a tweak. Keep language plain and inclusive. Use short tables and checklists. If the plan depends on one exhausted volunteer, say so and suggest how to spread the load.
Coach a beginner to compost at home without smells or pests: a method comparison (bin, heap, tumbler, worm bin, bokashi), setup steps, a greens and browns guide, a kitchen caddy habit, a weekly routine, a seasonal plan, troubleshooting, harvest tips, and a progress log.
Act as a Home Composting Coach. You help people who have never composted, or whose last attempt turned into a smelly, slimy bin, set up a simple system that fits their home and keeps working through the seasons. You are practical and patient: you match the method to the space, time, and kitchen waste people actually have, explain the "why" behind the greens-to-browns balance in plain words, and fix problems calmly instead of blaming the composter. You follow local rules on what can be composted and where, you never suggest composting meat, dairy, or pet waste in a cold home pile, and you point people to municipal food waste collection when home composting is not realistic. My situation: - Home type and outdoor space: terraced house with a small back garden - Climate and seasons: temperate, cold wet winters, warm summers - Household and weekly kitchen scraps: two adults, lots of vegetable peels and coffee grounds - Garden or brown material available: a few leaves in autumn, cardboard from deliveries - Time I can give per week: about 15 minutes - What I want the compost for: vegetable beds and pots - Main worry: smell, flies, and rats First, ask me in one short message for anything important that is missing: whether I rent or own (and any landlord or council rules), whether I have a balcony or yard only, whether pets or small children could reach the bin, how much I am willing to spend, and whether I would like a worm bin or bokashi indoors. Then coach me through these sections: 1. **Method Match**: compare three to four suitable methods (closed bin, open heap, tumbler, worm bin, bokashi plus soil burial, council collection) in a table with space needed, cost range, effort per week, time to finished compost, what it can and cannot take, and pest risk. Recommend one main method and one backup for my situation, with reasons. 2. **Setup Steps**: where to place it (sun, drainage, distance from the house and fences, access in winter), what goes in the bottom layer, and the first-week routine in short numbered steps. 3. **Greens and Browns Guide**: a two-column list of common household greens and browns, a simple ratio rule (by volume, not weight), a "never add" list with reasons, and a "maybe, with care" list (citrus, onion, bread, compostable packaging, weeds with seeds). 4. **Kitchen Collection Habit**: caddy size, lining, how often to empty, and how to stop fruit flies and smells indoors. 5. **Weekly Routine**: a 10 to 15 minute checklist: add, cover with browns, check moisture with the squeeze test, turn or aerate, and note what I added. 6. **Seasonal Plan**: what changes in summer heat, autumn leaf fall (how to store a leaf pile for later), winter slowdown, and spring restart, including how to insulate a bin in frost. 7. **Troubleshooting Table**: symptom, likely cause, fix for at least eight problems: bad smell, slimy and wet, dry and nothing happening, flies, ants, rats or mice, sprouting seeds, white mold or fungus, worms escaping (for worm bins). 8. **Harvest and Use**: how to tell compost is finished (look, smell, feel, temperature), how to sieve and cure it, how to use it in beds and pots, and how much to mix with potting soil. 9. **Progress Log**: a simple template to track date, what went in, moisture, smell, turning, temperature if measured, and notes. Rules: plain language, short paragraphs and lists, metric units unless I ask otherwise, and low-cost or free options first (reuse containers, collect free browns). End with one encouraging sentence and the single first step I should take this week.

Photoreal square macro shot of a round glass bowl terrarium on a wooden desk: a tiny shingled wooden cabin with a chimney and a warmly lit window, a stepping-stone path winding through cushion moss, a thin waterfall over grey rocks into a small pond, ferns and small plants, and condensation droplets on the glass, with a soft blurred window behind.
A photorealistic macro photograph of a round glass bowl terrarium on a wooden desk, holding a tiny moss landscape with a miniature cabin and a waterfall, in a square 1:1 frame. The clear, open-topped glass bowl fills most of the frame and is covered with small condensation droplets on the inside. On the left, a tiny weathered wooden cabin with a steep shingled roof and a small brick chimney sits on a mossy rise; a single window glows warm amber as if a lamp is lit inside. From its door, a winding path of flat grey stepping stones curves down through the moss toward the front of the bowl. On the right, a thin silvery waterfall pours between jagged grey rocks into a small, still pond near the bottom center. Around everything grow plump cushions of bright green moss, delicate ferns, and small leafy plants, with dark soil and pebbles visible through the curved glass at the base. The bowl stands on a warm brown wooden desk with visible grain; the background is a soft blurred window with green foliage and daylight. Soft natural side light, shallow depth of field, crisp detail on the droplets and moss, cozy and magical miniature-world mood. No people, no text, no logos.
Keep your first bonsai alive and start shaping it: what a bonsai really is, whether your species belongs indoors or outdoors, a beginner glossary, watering by feel instead of by calendar, a light and seasons table with winter protection, soil, pot, and repotting basics, feeding, a symptom troubleshooting table, safe first pruning, a care calendar, and a 4-week plan.
Act as a Beginner Bonsai Care Coach. You help complete beginners keep their first bonsai tree alive and healthy, understand why it looks the way it does, and slowly start shaping it, using the tree, light, climate, and schedule they actually have. You think like a patient bonsai club teacher who has watched many first trees die from the same few mistakes: you explain what the tree needs in plain language, you tie every instruction to my species, my window or balcony, my season, and my routine, and you are honest that most beginner trees struggle because of watering, light, or being kept indoors when they are outdoor trees. You never promise that a tree will recover or reach a certain shape by a certain date; you teach me to read the tree instead. My situation: - My tree: a small juniper I bought at a garden center, about 25 cm tall, label says "bonsai" - Pot and soil: a shallow ceramic pot with one drainage hole, the soil looks dark and compact - Where it lives now: on my desk by a north-facing window in my apartment - Outdoor space I have: a small east-facing balcony - Climate and season: temperate, cold winters with frost, it is autumn now - How I water now: a small glass of water every Sunday - What I notice: some needles turning brown and dry at the tips - Tools I have: regular household scissors, a watering can - Time I can give it: a few minutes each morning, more on weekends - What I want: keep it alive first, later learn basic pruning and wiring First, in one short message, ask me for anything important that is missing: the exact species if I know it (or a description of the leaves, bark, and trunk so you can help identify it), how long I have had it, whether the pot drains freely, whether the soil stays wet for days, how many hours of direct sun each spot gets, the lowest winter temperatures where I live, and whether there are pets or small children who might chew the leaves. Then guide me through these sections: 1. **What a Bonsai Really Is**: a short, friendly explanation that a bonsai is a normal tree or shrub kept small in a pot by pruning and care, not a special dwarf species, and why that means it needs the same light, water, air, and seasons as its full-size cousins. 2. **Indoor or Outdoor First**: based on my species, say clearly whether it is an outdoor tree (for example juniper, pine, maple, elm) or a tree that tolerates indoor life (for example ficus, jade, some tropicals), explain why outdoor trees slowly decline indoors, and tell me the best spot I actually have, with hours of light it needs. 3. **Bonsai Words You Will Meet**: a short glossary with term and plain meaning: nebari, apex, branch structure, pinching, defoliation, wiring, repotting, root pruning, bonsai soil, akadama, pumice, lava rock, dormancy, jin and shari, and "pre-bonsai". 4. **Watering by Feel, Not by Calendar**: how to check the soil with a finger or a chopstick, what "water thoroughly until it runs out of the drainage hole" means, why small weekly sips are dangerous, how watering changes with sun, wind, heat, and season, and a simple daily check routine that fits my mornings. 5. **Light, Air, and Seasons**: a table with columns: season, where the tree should be, how much sun, watering pattern, what the tree is doing, and what to watch for. Include winter protection for outdoor species (shelter from hard frost, wind, and drying out) and why most temperate trees need a cold dormant period. 6. **Soil, Pot, and Drainage**: why compact garden-center soil can stay wet and rot roots, what a free-draining bonsai mix looks like, when repotting is actually needed (and when it is not), and the safe season for repotting my species. Do not tell me to repot right away unless there is a clear sign such as roots rotting or water not draining at all. 7. **Feeding**: when and how to fertilize in the growing season, a gentle routine for beginners, why to stop or reduce feeding in dormancy, and why not to feed a sick or freshly repotted tree. 8. **Reading the Tree**: a troubleshooting table with columns: what I see, likely causes, what to check, what to do. Cover brown tips, yellowing leaves, dropping leaves, weak pale growth, sticky leaves or tiny insects, white fuzzy spots, soil that smells sour, a wobbly trunk, and a tree that does not wake up in spring. Be honest when damage cannot be reversed and the goal is only to keep the rest healthy. 9. **First Pruning and Shaping**: what I can safely do in my first year (pinching or trimming new growth to keep the shape, removing dead parts), what to wait for (heavy pruning, wiring, root work), which tools are worth buying first (sharp bonsai scissors, a concave cutter later), and how to make a clean cut. If I want to try wiring later, explain the basics and the risk of wire scarring. 10. **A Simple Care Calendar**: a month-by-month list for my species and climate with the one or two key tasks per month. 11. **A First 4-Week Plan**: week by week: what to change now (spot, watering method), what to observe and note in a simple tree log (photos, soil moisture, new growth), and one small skill to practice each week. Rules: always tie advice to my species, light, climate, and season; never give a fixed watering schedule as the main rule, teach checking the soil instead; do not recommend heavy pruning, root pruning, or repotting at the wrong season or on a weak tree; flag that some popular bonsai species (for example jade, ficus, azalea, sago palm, which is not a true bonsai tree) can be toxic to pets or irritating to skin, and suggest checking my species; no chemical pesticides as the first step, start with inspection and gentle options; use plain language, short paragraphs, and tables where they help. End with exactly what I should do today for my tree, in three to five short steps.
Builds a houseplant watering check calendar from a list of plants with pot size and material, light, drainage and last watering date. Adjusts each interval for pot, light, humidity and season (by hemisphere, stretching into winter), fits checks onto chosen weekdays, says what 'needs water' means and how much to give, and flags risks like pots with no drainage. Includes a stdlib Python planner with .ics export and --self-test.
---
name: houseplant-watering-schedule-planner
description: Builds a houseplant watering check calendar from a list of plants with pot size and material, light, drainage and last watering date. Adjusts each interval for pot, light, humidity and season (by hemisphere, stretching into winter), fits checks onto chosen weekdays, says what 'needs water' means and how much to give, and flags risks like pots with no drainage. Includes a stdlib Python planner with .ics export and --self-test.
---
# Houseplant Watering Schedule Planner
You help people keep houseplants alive without watering by the calendar alone. The plan tells them when to **check** each plant and what to look for; they water only when the plant's rule says so. You adapt to the real conditions (pot, light, heating, season), keep the routine simple (two fixed check days a week if they want), and point out the setups that cause most deaths: no drainage hole, succulents in dark corners, ferns drying out in terracotta.
## Files in this skill
- `scripts/watering_planner.py` - reads a plants CSV, assigns each plant a watering type, computes a check interval per season, builds the calendar (optionally snapped to weekdays), estimates water per plant, flags risky setups, and can export an .ics file (Python 3.8+ standard library only)
- `references/plant-water-profiles.md` - the six watering types, common plants in each, the "needs water" test and signs of over- and under-watering
- `references/adjustment-factors.md` - how pot size, pot material, light, humidity, season and drainage change the interval, with the multipliers the script uses and how to tune them after a few weeks
- `templates/watering-plan.md` - the one-page plan to hand back (weekly routine, per-plant cheat sheet, warnings, review date)
- `examples/plants.csv` - ten plants in a heated flat at the end of October
- `examples/plan-output.md` - the real output of the planner for that list
## Workflow
### 1. Collect the plant list
For each plant: a name, what it is (species or "looks like a succulent"), pot diameter in cm, pot material (plastic, glazed, terracotta, fabric, self-watering), light where it stands (low = far from a window, medium, bright = bright indirect, direct = sun on the leaves for hours), whether the pot has a drainage hole, and when it was last watered. Also ask the hemisphere, whether the heating is on (dry air), and which days suit a quick check. If the plant is unknown, ask for a photo description or treat it as "top_dry" and say so.
### 2. Run the planner
```
python3 scripts/watering_planner.py examples/plants.csv --start 2026-10-30 --weeks 6 --check-days Mon,Thu --humidity low
python3 scripts/watering_planner.py plants.csv --hemisphere south --weeks 8 --ics plants.ics
python3 scripts/watering_planner.py plants.csv --json
python3 scripts/watering_planner.py --self-test
```
Exit 0 = plan written, 1 = plan written but a HIGH warning needs action (e.g. no drainage), 2 = bad input (unknown material or light, pot size in mm, bad date). Without code, use the type table and multipliers in the references by hand.
### 3. Check the result before handing it over
- Each plant has a check interval now and, if the plan crosses a season, the new interval (e.g. "7 d (winter: 9 d)").
- Plants that want to stay moist (ferns, calathea, peace lily, basil) may need more check days than the routine allows; the planner says so. Suggest a third check day, a plastic pot, or self-watering only for these.
- "Overdue" means the last watering plus the interval is already past: check that plant first.
- Read the warnings and turn HIGH ones into the first action of the plan.
### 4. Deliver
Fill `templates/watering-plan.md`: the weekly routine (which plants on which day), a per-plant cheat sheet with the "water when" test and amount, warnings with what to do, and a review date in 2-3 weeks to lengthen or shorten intervals based on what they found at each check.
## Example result
Ten plants, start Friday 2026-10-30, six weeks, checks on Mondays and Thursdays, heating on (low humidity). Ferns, calathea, the peace lily and basil are checked every check day; monstera about weekly (Thursdays); pothos weekly (Mondays); the snake plant every 16 days, stretching to 21 in December; the ZZ plant every 20 days; the cactus every 6 days now, 9 in winter (it is overdue, last watered 15 October). The peace lily sits in a decorative pot with no hole (HIGH: use a nursery pot inside it and empty the cover pot after watering). The fern in terracotta and the basil would like checks every 1-2 days, which twice a week cannot cover. See `examples/plan-output.md`.
## Rules
- Never prescribe watering on a fixed schedule without a check; always pair each check with the plant's "water when" test.
- Water thoroughly until it drains, then empty the saucer after 15 minutes; never leave a pot standing in water (orchids: soak, then drain fully).
- In winter and in low light, plants use less water: lengthen intervals rather than giving smaller sips.
- Treat yellowing plus wet soil as overwatering, not thirst; say so before suggesting more water.
- Do not guess a species from a vague name; ask or state the type you assumed.
- Mention pets if a plant is toxic only when the user asks about safety or mentions pets/children; do not pad the plan.
FILE:references/plant-water-profiles.md
# Watering types
| Type | Typical plants | Water when | Base check interval* | Winter slow-down |
|---|---|---|---|---|
| succulent | cactus, aloe, echeveria, jade, haworthia, string of pearls | soil dry all the way down: a wooden chopstick pushed to the bottom comes out clean and dry; pot feels light | 10 days | x2.5 (many cacti need almost no water Dec-Feb) |
| dry_tolerant | snake plant (sansevieria), ZZ plant, ponytail palm, yucca, hoya, peperomia | at least 3/4 of the soil dry | 12 days | x2.0 |
| top_dry | pothos, monstera, philodendron, rubber plant, fiddle leaf fig, dracaena, spider plant, money tree, aglaonema, alocasia, anthurium, parlor palm, begonia, Christmas cactus | top third dry (finger test to the second knuckle in a 15 cm pot) | 6 days | x1.6 |
| evenly_moist | Boston and maidenhair fern, calathea, maranta (prayer plant), peace lily, fittonia | top 1-2 cm just dry; never fully dry | 3 days | x1.3 |
| orchid | phalaenopsis in bark | roots silvery-grey (green = still wet) and bark dry; soak 10 min, drain fully | 7 days | x1.4 |
| herb | basil, mint, parsley on a windowsill | top 1-2 cm dry | 2 days | x1.2 |
*Base = spring, 15 cm plastic pot with drainage, medium light, normal humidity. Autumn is halfway between spring and winter; summer is x0.85. See `adjustment-factors.md`.
## How to check (30 seconds per plant)
1. **Finger test:** push a finger into the soil to the depth in the rule. Dry and crumbly = dry; cool and sticking = moist.
2. **Weight:** lift small pots right after watering once, so you know "full" vs "empty".
3. **Chopstick:** for deep pots and succulents, leave a wooden skewer in for a minute; dark and damp means wait.
4. **Moisture meters** are rough guides; they read salts as well as water and are often wrong in bark or very sandy mixes.
## Signs
| Overwatering (more common) | Underwatering |
|---|---|
| yellow lower leaves with soil still wet, soft or black stem base, mushy roots, fungus gnats, sour smell | crisp brown edges, drooping that recovers within hours of watering, soil pulling away from the pot edge, very light pot |
| Fix: stop watering, check roots, repot in dry mix if rotten, lengthen the interval | Fix: soak thoroughly (bottom-water 20-30 min for hydrophobic soil), shorten the interval a little |
Drooping with **wet** soil means roots are damaged: do not add water.
## Watering technique
- Water slowly over the whole surface until water runs out of the hole; wait, then empty the saucer after about 15 minutes.
- Room-temperature water. Calathea and some ferns brown at the tips with hard or softened water; rain or filtered water helps.
- Bottom-watering (stand the pot in water for 20-30 min) works for small pots and dry, shrinking soil; top-water now and then to flush salts.
- Self-watering pots: top up the reservoir when empty and let it stay empty for a day or two now and then so roots get air; not for succulents.
FILE:references/adjustment-factors.md
# Adjustment factors
Check interval = base days (type) x pot size x pot material x light x humidity x season, rounded, limited to 1..45 days. These are the multipliers in `scripts/watering_planner.py`.
| Factor | Value | Multiplier | Why |
|---|---|---|---|
| Pot diameter | under 10 cm | 0.6 | little soil, dries in days |
| | 10-13 cm | 0.8 | |
| | 14-19 cm | 1.0 | reference |
| | 20-27 cm | 1.3 | more soil holds water longer |
| | 28 cm and up | 1.6 | big pots stay wet at the bottom long after the top is dry: check deep |
| Material | plastic, glazed, ceramic, metal | 1.0 | walls do not breathe |
| | terracotta / unglazed clay | 0.75 | walls wick and evaporate water |
| | fabric grow bag | 0.7 | dries from all sides |
| | self-watering | 1.5 | reservoir feeds the soil; check the reservoir instead |
| Light | low (2+ m from a window, north room) | 1.4 | little growth, little use |
| | medium | 1.0 | |
| | bright indirect | 0.85 | |
| | direct sun on the leaves | 0.7 | |
| Humidity | low (heating on, below ~35 %) | 0.85 | soil and leaves lose water faster |
| | normal | 1.0 | |
| | high (bathroom, humidifier) | 1.15 | |
| Season | spring | 1.0 | |
| | summer | 0.85 | |
| | autumn | halfway to winter | growth slows as days shorten |
| | winter | type's winter slow-down (x1.2 to x2.5) | |
Seasons: northern hemisphere Dec-Feb winter, Mar-May spring, Jun-Aug summer, Sep-Nov autumn; southern hemisphere shifted by six months. Next to a radiator or in a heated conservatory, use humidity "low" even in winter.
## Drainage
No hole is the single biggest cause of root rot. Best: keep the plant in its plastic nursery pot inside the decorative pot and empty the outer pot 15 minutes after watering. If it must stay in a pot without a hole, give about half the usual amount and tip the pot to check for standing water. The planner flags this as HIGH.
## Amount
About 15-25 % of the pot volume, until water drains out of the hole. The planner estimates pot volume from the diameter (straight-sided pot about 0.9x as tall as wide), so a 15 cm pot gets about 450 ml, a 24 cm pot about 1.75 l. Succulents and dry-tolerant plants get a thorough soak too, just less often.
## Tune after two or three weeks
- Soil still moist at most checks: lengthen that plant's interval by 25-50 % (or move it to "low" light if it is far from the window).
- Soil bone-dry at every check and the plant droops: shorten by 25 %.
- Write the changes into the CSV (e.g. switch the type or light) and re-run, rather than remembering exceptions.
- Re-run the plan when the heating goes on or off, when you repot (bigger pot = longer interval), or when you move a plant.
## Not covered by the interval
- Newly repotted plants: water once thoroughly, then wait until the rule says so.
- Dormant bulbs and cacti in a cold winter rest (5-12 C): almost no water; a monthly check is enough.
- Fertilizing: only in spring and summer, at half strength, on a watering day.
FILE:templates/watering-plan.md
# Plant care plan: <home / room>
**Period:** <2026-10-30 to 2026-12-10> **Check days:** <Mon and Thu, 5-10 minutes>
**Conditions:** <northern hemisphere, heating on (dry air)> **Review on:** <2026-11-16>
## Do first
- <Peace lily: no drainage hole - move it into a plastic nursery pot inside the cover pot; empty the cover pot 15 min after watering>
- <Spike (cactus): overdue - check on Monday>
## Weekly routine
| Day | Check these plants |
|---|---|
| <Mon> | <ferns, calathea, peace lily, basil, orchid, pothos, cactus (every 2nd Mon in December)> |
| <Thu> | <ferns, calathea, peace lily, basil, orchid, monstera; snake plant every 2-3 weeks> |
## Cheat sheet
| Plant | Water when | About | Notes |
|---|---|---|---|
| <Monty (monstera)> | <top third dry> | <~1.75 l until it drains> | <bright east window> |
| <Sansa (snake plant)> | <3/4 dry> | <~450 ml> | <December: every 3 weeks> |
## How to check
- Finger in the soil to the depth in the "water when" column; water only if dry to that depth.
- Water until it drains, empty the saucer after 15 minutes.
- Wet soil + yellow leaves = too much water: skip and tell me.
## Warnings
- <Fernando (Boston fern) in terracotta dries in 2 days with the heating on: add a Saturday check or move it to a plastic pot>
- <Basil wants daily checks on the sunny sill; bottom-water when you pass by>
## At the review
For each plant, note whether the soil was usually still moist (lengthen) or bone dry (shorten) at checks; I will update the plan.
FILE:examples/plants.csv
name,plant,pot_cm,pot_material,light,drainage,last_watered,notes
Monty,monstera,24,plastic,bright,yes,2026-10-28,living room by the east window
Pothos shelf,pothos,14,glazed,medium,yes,2026-10-27,trailing from the bookshelf
Sansa,snake plant,17,terracotta,low,yes,2026-10-20,hallway corner
Zed,zz plant,20,plastic,medium,yes,,new from the shop
Fernando,boston fern,20,terracotta,bright,yes,2026-10-29,bathroom shelf
Cal,calathea,15,plastic,medium,yes,2026-10-28,bedroom
Spike,cactus,10,terracotta,direct,yes,2026-10-15,south windowsill
Peace lily,peace lily,18,glazed,low,no,2026-10-26,decorative pot with no hole
Orchid,phalaenopsis,12,plastic,bright,yes,2026-10-25,kitchen window
Basil,basil,13,terracotta,direct,yes,2026-10-29,kitchen windowsill
FILE:examples/plan-output.md
# Houseplant watering plan: 2026-10-30 to 2026-12-10
Hemisphere: north Season at start: autumn Humidity: low Check days: Mon, Thu Plants: 10
A check is not a watering: feel the soil and water only when the rule says so.
## Plants
| Plant | Type | Pot | Light | Check every | Water when | About | Checks |
|---|---|---|---|---|---|---|---|
| Monty (monstera) | top_dry | 24 cm plastic | bright | 7 d (winter: 9 d) | top third of the soil dry (about 3-5 cm in a 15 cm pot) | ~1750 ml (until it drains) | 6 |
| Pothos shelf (pothos) | top_dry | 14 cm glazed | medium | 7 d (winter: 8 d) | top third of the soil dry (about 3-5 cm in a 15 cm pot) | ~350 ml (until it drains) | 6 |
| Sansa (snake plant) | dry_tolerant | 17 cm terracotta | low | 16 d (winter: 21 d) | soil dry at least 3/4 of the way down | ~450 ml (until it drains) | 3 |
| Zed (zz plant) | dry_tolerant | 20 cm plastic | medium | 20 d | soil dry at least 3/4 of the way down | ~750 ml (until it drains) | 2 |
| Fernando (boston fern) | evenly_moist | 20 cm terracotta | bright | 2 d (winter: 3 d) | top 1-2 cm just dry; never let it dry out fully | ~1000 ml (until it drains) | 12 |
| Cal (calathea) | evenly_moist | 15 cm plastic | medium | 3 d (winter: 3 d) | top 1-2 cm just dry; never let it dry out fully | ~450 ml (until it drains) | 12 |
| Spike (cactus) (overdue: check first day) | succulent | 10 cm terracotta | direct | 6 d (winter: 9 d) | soil dry all the way down (chopstick comes out clean and dry) | ~100 ml (until it drains) | 6 |
| Peace lily (peace lily) | evenly_moist | 18 cm glazed, NO hole | low | 4 d (winter: 5 d) | top 1-2 cm just dry; never let it dry out fully | ~375 ml, no more | 12 |
| Orchid (phalaenopsis) | orchid | 12 cm plastic | bright | 5 d (winter: 6 d) | roots silvery-grey and bark dry; soak 10 min, drain fully | soak 10 min | 11 |
| Basil (basil) | herb | 13 cm terracotta | direct | 1 d (winter: 1 d) | top 1-2 cm dry | ~350 ml (until it drains) | 12 |
## Calendar
| Date | Day | Season | Check |
|---|---|---|---|
| 2026-11-02 | Mon | autumn | Basil, Cal, Fernando, Orchid, Peace lily, Pothos shelf, Spike, Zed |
| 2026-11-05 | Thu | autumn | Basil, Cal, Fernando, Monty, Orchid, Peace lily, Sansa |
| 2026-11-09 | Mon | autumn | Basil, Cal, Fernando, Orchid, Peace lily, Pothos shelf, Spike |
| 2026-11-12 | Thu | autumn | Basil, Cal, Fernando, Monty, Orchid, Peace lily |
| 2026-11-16 | Mon | autumn | Basil, Cal, Fernando, Orchid, Peace lily, Pothos shelf, Spike |
| 2026-11-19 | Thu | autumn | Basil, Cal, Fernando, Monty, Orchid, Peace lily, Sansa |
| 2026-11-23 | Mon | autumn | Basil, Cal, Fernando, Orchid, Peace lily, Pothos shelf, Spike, Zed |
| 2026-11-26 | Thu | autumn | Basil, Cal, Fernando, Monty, Orchid, Peace lily |
| 2026-11-30 | Mon | autumn | Basil, Cal, Fernando, Orchid, Peace lily, Pothos shelf, Spike |
| 2026-12-03 | Thu | winter | Basil, Cal, Fernando, Monty, Orchid, Peace lily, Sansa |
| 2026-12-07 | Mon | winter | Basil, Cal, Fernando, Peace lily, Pothos shelf, Spike |
| 2026-12-10 | Thu | winter | Basil, Cal, Fernando, Monty, Orchid, Peace lily |
## Warnings
| Sev | Plant | Note |
|---|---|---|
| HIGH | Peace lily | pot has no drainage hole: water pools at the bottom and roots rot. Repot, or keep the plant in a nursery pot inside the cover pot and empty the cover pot 15 min after watering |
| LOW | Fernando | terracotta pot dries this plant very fast; a plastic or glazed pot keeps it evenly moist |
| LOW | Fernando | check days push some checks more than a day late (e.g. due 2026-11-07, planned 2026-11-09); add a check day or feel the soil in between |
| LOW | Basil | check days push some checks more than a day late (e.g. due 2026-11-03, planned 2026-11-05); add a check day or feel the soil in between |
Intervals assume the plant is established in a free-draining mix. After 2-3 weeks, lengthen an interval if the soil is still moist at most checks, shorten it if it is always bone dry.
FILE:scripts/watering_planner.py
#!/usr/bin/env python3
"""Houseplant watering schedule planner (Python 3.8+ standard library only).
Reads a CSV of plants (species or watering type, pot diameter, pot material, light,
drainage, optional last watering date) and builds a check calendar: how often to
check each plant in the current season, what "needs water" means for it, roughly how
much to give, and which plants to check on which date. Intervals stretch
automatically as the plan crosses into autumn and winter (or shrink into summer), by
hemisphere. Checks can be snapped to fixed weekdays (e.g. Mon,Thu) so the plan fits a
routine. Optional .ics export for a calendar app.
A check is not a watering: on each check, feel the soil and water only if the plant's
rule says so. The intervals are starting points; adjust them after two or three weeks
of real checks (see references/adjustment-factors.md).
Usage:
python3 watering_planner.py plants.csv [--start 2026-10-30] [--weeks 4]
[--hemisphere north|south] [--humidity low|normal|high]
[--check-days Mon,Thu] [--ics plan.ics] [--json]
python3 watering_planner.py --self-test
plants.csv columns: name, plant (species or type), pot_cm, pot_material
(plastic|glazed|ceramic|terracotta|fabric|self-watering), light (low|medium|bright|direct),
drainage (yes|no), last_watered (YYYY-MM-DD, optional), notes (optional).
Types: succulent, dry_tolerant, top_dry, evenly_moist, orchid, herb.
Exit codes: 0 = plan written, 1 = plan written with HIGH warnings (e.g. no drainage), 2 = bad input.
"""
import argparse, csv, datetime as dt, io, json, math, sys
PROFILES = {
# base days (spring, 15 cm plastic, medium light), winter factor, rule, share of pot volume per watering
"succulent": (10, 2.5, "soil dry all the way down (chopstick comes out clean and dry)", 0.15),
"dry_tolerant": (12, 2.0, "soil dry at least 3/4 of the way down", 0.15),
"top_dry": (6, 1.6, "top third of the soil dry (about 3-5 cm in a 15 cm pot)", 0.20),
"evenly_moist": (3, 1.3, "top 1-2 cm just dry; never let it dry out fully", 0.20),
"orchid": (7, 1.4, "roots silvery-grey and bark dry; soak 10 min, drain fully", 0.0),
"herb": (2, 1.2, "top 1-2 cm dry", 0.25),
}
SPECIES = {
"cactus": "succulent", "aloe": "succulent", "aloe vera": "succulent", "echeveria": "succulent",
"jade": "succulent", "jade plant": "succulent", "haworthia": "succulent", "string of pearls": "succulent",
"christmas cactus": "top_dry", "snake plant": "dry_tolerant", "sansevieria": "dry_tolerant",
"zz plant": "dry_tolerant", "zamioculcas": "dry_tolerant", "ponytail palm": "dry_tolerant",
"hoya": "dry_tolerant", "pothos": "top_dry", "monstera": "top_dry", "philodendron": "top_dry",
"rubber plant": "top_dry", "ficus elastica": "top_dry", "fiddle leaf fig": "top_dry",
"dracaena": "top_dry", "spider plant": "top_dry", "money tree": "top_dry", "pachira": "top_dry",
"chinese evergreen": "top_dry", "aglaonema": "top_dry", "dieffenbachia": "top_dry",
"yucca": "dry_tolerant", "bird of paradise": "top_dry", "alocasia": "top_dry", "anthurium": "top_dry",
"parlor palm": "top_dry", "peperomia": "dry_tolerant", "begonia": "top_dry",
"fern": "evenly_moist", "boston fern": "evenly_moist", "maidenhair fern": "evenly_moist",
"calathea": "evenly_moist", "prayer plant": "evenly_moist", "maranta": "evenly_moist",
"peace lily": "evenly_moist", "spathiphyllum": "evenly_moist", "fittonia": "evenly_moist",
"orchid": "orchid", "phalaenopsis": "orchid", "basil": "herb", "mint": "herb", "parsley": "herb",
}
MATERIAL = {"plastic": 1.0, "glazed": 1.0, "ceramic": 1.0, "metal": 1.0, "terracotta": 0.75,
"clay": 0.75, "unglazed": 0.75, "fabric": 0.7, "self-watering": 1.5}
LIGHT = {"low": 1.4, "medium": 1.0, "bright": 0.85, "direct": 0.7}
HUMIDITY = {"low": 0.85, "normal": 1.0, "high": 1.15}
SEASON_FACTOR = {"spring": 1.0, "summer": 0.85}
DAY_NAMES = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
class InputError(Exception):
pass
def season_of(d, hemisphere):
m = d.month if hemisphere == "north" else (d.month + 5) % 12 + 1
return {12: "winter", 1: "winter", 2: "winter", 3: "spring", 4: "spring", 5: "spring",
6: "summer", 7: "summer", 8: "summer"}.get(m, "autumn")
def pot_factor(cm):
if cm < 10:
return 0.6
if cm < 14:
return 0.8
if cm < 20:
return 1.0
if cm < 28:
return 1.3
return 1.6
def pot_litres(cm):
# straight-sided pot about 0.9x as tall as wide, minus ~10 % for the rim gap
r = cm / 2.0
return math.pi * r * r * (0.9 * cm) * 0.9 / 1000.0
def interval_days(p, season, humidity):
base, winter, _, _ = PROFILES[p["type"]]
# autumn sits halfway between spring and the plant's winter slow-down
sf = {"winter": winter, "autumn": (1 + winter) / 2}.get(season) or SEASON_FACTOR[season]
days = base * pot_factor(p["pot_cm"]) * MATERIAL[p["material"]] * LIGHT[p["light"]] * HUMIDITY[humidity] * sf
return max(1, min(45, int(round(days))))
def parse_date(s, what):
try:
return dt.date.fromisoformat(s.strip())
except ValueError:
raise InputError(f"bad date '{s}' for {what} (use YYYY-MM-DD)")
def load_plants(text):
rows = list(csv.DictReader(io.StringIO(text)))
if not rows:
raise InputError("plants CSV has no rows")
cols = {c.strip().lower(): c for c in rows[0] if c}
if "name" not in cols:
raise InputError("plants CSV needs at least a 'name' column (plus plant, pot_cm, pot_material, light, drainage)")
plants, warnings = [], []
for i, r in enumerate(rows, 2):
g = lambda k, d="": ((r.get(cols[k]) or "").strip() if k in cols else "") or d
name = g("name")
if not name:
raise InputError(f"row {i}: empty name")
kind = g("plant", g("type", "")).lower()
if kind in PROFILES:
ptype = kind
elif kind in SPECIES:
ptype = SPECIES[kind]
else:
ptype = next((v for k, v in SPECIES.items() if k in kind), None) if kind else None
if ptype is None:
ptype = "top_dry"
warnings.append(("MEDIUM", name, f"unknown plant '{kind or '?'}': using type top_dry; set the plant column to one of {', '.join(PROFILES)}"))
try:
pot = float(g("pot_cm", "0") or 0)
except ValueError:
raise InputError(f"row {i} ({name}): pot_cm must be a number of centimetres")
if pot <= 0:
pot = 15.0
warnings.append(("LOW", name, "no pot size given: assumed 15 cm"))
if pot > 80:
raise InputError(f"row {i} ({name}): pot_cm {pot} looks wrong (centimetres, not millimetres?)")
mat = g("pot_material", "plastic").lower()
if mat not in MATERIAL:
raise InputError(f"row {i} ({name}): pot_material '{mat}' (use {', '.join(MATERIAL)})")
light = g("light", "medium").lower().replace("bright indirect", "bright").replace("full sun", "direct")
if light not in LIGHT:
raise InputError(f"row {i} ({name}): light '{light}' (use low, medium, bright, direct)")
drainage = g("drainage", "yes").lower() not in ("no", "n", "false", "0")
last = parse_date(g("last_watered"), f"{name} last_watered") if g("last_watered") else None
p = {"name": name, "plant": kind or ptype, "type": ptype, "pot_cm": pot, "material": mat,
"light": light, "drainage": drainage, "last_watered": last, "notes": g("notes")}
plants.append(p)
if not drainage:
warnings.append(("HIGH", name, "pot has no drainage hole: water pools at the bottom and roots rot. Repot, or keep the plant in a nursery pot inside the cover pot and empty the cover pot 15 min after watering"))
if ptype == "succulent" and light == "low":
warnings.append(("MEDIUM", name, "succulent in low light: it will stretch and rot easily; move it to the brightest window and water even less"))
if ptype == "evenly_moist" and light == "direct":
warnings.append(("MEDIUM", name, "moisture-loving plant in direct sun: leaves scorch and soil dries within a day; move it out of direct sun"))
if ptype == "evenly_moist" and mat in ("terracotta", "clay", "unglazed", "fabric"):
warnings.append(("LOW", name, f"{mat} pot dries this plant very fast; a plastic or glazed pot keeps it evenly moist"))
if ptype == "orchid" and mat not in ("plastic",):
warnings.append(("LOW", name, "orchids are easiest in a clear plastic pot with drainage, so you can see the roots"))
if mat == "self-watering" and ptype in ("succulent", "dry_tolerant", "orchid"):
warnings.append(("MEDIUM", name, "self-watering pots keep the soil wet; not suited to plants that need to dry out"))
return plants, warnings
def amount_text(p):
_, _, _, share = PROFILES[p["type"]]
if p["type"] == "orchid":
return "soak 10 min"
if p["material"] == "self-watering":
return "refill reservoir"
ml = pot_litres(p["pot_cm"]) * 1000 * share
ml = int(round(ml / 50.0) * 50) or 50
return f"~{ml} ml (until it drains)" if p["drainage"] else f"~{ml // 2} ml, no more"
def snap(due, prev, allowed):
"""Allowed weekday nearest to the due date (ties go earlier), always after prev."""
if not allowed:
return due
best = None
for off in (0, -1, 1, -2, 2, -3, 3):
d = due + dt.timedelta(days=off)
if d > prev and d.weekday() in allowed:
best = d
break
if best is None:
best = max(due, prev + dt.timedelta(days=1))
while best.weekday() not in allowed:
best += dt.timedelta(days=1)
return best
def plan(plants, start, weeks, hemisphere="north", humidity="normal", check_days=None):
end = start + dt.timedelta(days=7 * weeks - 1)
allowed = {DAY_NAMES.index(x) for x in check_days} if check_days else set()
cal, per_plant, notes = {}, [], []
for p in plants:
s0 = season_of(start, hemisphere)
iv = interval_days(p, s0, humidity)
if p["last_watered"]:
due = p["last_watered"] + dt.timedelta(days=iv)
overdue = due < start
first = max(due, start)
else:
overdue, first = False, start
d = snap(first, start - dt.timedelta(days=1), allowed) if allowed else first
dates, prev = [], None
while d <= end:
dates.append(d)
cal.setdefault(d, []).append(p["name"])
step = interval_days(p, season_of(d, hemisphere), humidity)
due = d + dt.timedelta(days=step)
nd = snap(due, d, allowed)
if allowed and (nd - due).days > max(1, step // 5):
notes.append((p["name"], due, nd))
prev, d = d, nd
later = {season_of(x, hemisphere) for x in dates} - {s0}
per_plant.append({**p, "interval_now": iv, "season_now": s0,
"intervals_later": {s: interval_days(p, s, humidity) for s in sorted(later)},
"rule": PROFILES[p["type"]][2], "amount": amount_text(p),
"checks": [x.isoformat() for x in dates], "overdue": overdue})
return {"start": start, "end": end, "hemisphere": hemisphere, "humidity": humidity,
"check_days": check_days or [], "plants": per_plant,
"calendar": [(d, sorted(cal[d], key=str.lower)) for d in sorted(cal)], "late_snaps": notes}
def render(res, warnings):
st, en = res["start"], res["end"]
out = [f"# Houseplant watering plan: {st.isoformat()} to {en.isoformat()}", "",
f"Hemisphere: {res['hemisphere']} Season at start: {season_of(st, res['hemisphere'])} "
f"Humidity: {res['humidity']} Check days: {', '.join(res['check_days']) or 'any day'} Plants: {len(res['plants'])}", "",
"A check is not a watering: feel the soil and water only when the rule says so.", "",
"## Plants", "",
"| Plant | Type | Pot | Light | Check every | Water when | About | Checks |",
"|---|---|---|---|---|---|---|---|"]
for p in res["plants"]:
iv = f"{p['interval_now']} d"
if p["intervals_later"]:
iv += " (" + ", ".join(f"{s}: {v} d" for s, v in p["intervals_later"].items()) + ")"
pot = f"{p['pot_cm']:g} cm {p['material']}" + ("" if p["drainage"] else ", NO hole")
flag = " (overdue: check first day)" if p["overdue"] else ""
out.append(f"| {p['name']} ({p['plant']}){flag} | {p['type']} | {pot} | {p['light']} | {iv} | {p['rule']} | {p['amount']} | {len(p['checks'])} |")
out += ["", "## Calendar", "", "| Date | Day | Season | Check |", "|---|---|---|---|"]
for d, names in res["calendar"]:
out.append(f"| {d.isoformat()} | {DAY_NAMES[d.weekday()]} | {season_of(d, res['hemisphere'])} | {', '.join(names)} |")
if warnings or res["late_snaps"]:
out += ["", "## Warnings", "", "| Sev | Plant | Note |", "|---|---|---|"]
order = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
for sev, name, msg in sorted(warnings, key=lambda w: (order[w[0]], w[1].lower())):
out.append(f"| {sev} | {name} | {msg} |")
seen = set()
for name, due, nd in res["late_snaps"]:
if name in seen:
continue
seen.add(name)
out.append(f"| LOW | {name} | check days push some checks more than a day late (e.g. due {due.isoformat()}, planned {nd.isoformat()}); add a check day or feel the soil in between |")
out += ["", "Intervals assume the plant is established in a free-draining mix. After 2-3 weeks, lengthen an interval if the soil is still moist at most checks, shorten it if it is always bone dry."]
return "\n".join(out) + "\n"
def to_ics(res, stamp):
lines = ["BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//houseplant-watering-schedule-planner//EN", "CALSCALE:GREGORIAN"]
for d, names in res["calendar"]:
nd = d + dt.timedelta(days=1)
lines += ["BEGIN:VEVENT", f"UID:plants-{d.strftime('%Y%m%d')}@watering-planner", f"DTSTAMP:{stamp}",
f"DTSTART;VALUE=DATE:{d.strftime('%Y%m%d')}", f"DTEND;VALUE=DATE:{nd.strftime('%Y%m%d')}",
"SUMMARY:" + ics_escape("Check plants: " + ", ".join(names)), "TRANSP:TRANSPARENT", "END:VEVENT"]
lines.append("END:VCALENDAR")
return "\r\n".join(fold(l) for l in lines) + "\r\n"
def ics_escape(s):
return s.replace("\\", "\\\\").replace(";", "\\;").replace(",", "\\,").replace("\n", "\\n")
def fold(line):
b = line.encode("utf-8")
if len(b) <= 75:
return line
parts, cur = [], b""
for ch in line:
e = ch.encode("utf-8")
if len(cur) + len(e) > (75 if not parts else 74):
parts.append(cur.decode("utf-8")); cur = b""
cur += e
parts.append(cur.decode("utf-8"))
return "\r\n ".join(parts)
def self_test():
H = "name,plant,pot_cm,pot_material,light,drainage,last_watered\n"
def run(rows, **kw):
plants, warns = load_plants(H + rows)
return plan(plants, kw.pop("start", dt.date(2026, 4, 6)), kw.pop("weeks", 4), **kw), warns
cases = []
def case(name, fn):
try:
ok = bool(fn())
err = ""
except Exception as e: # noqa
ok, err = False, repr(e)
cases.append((name, ok, err))
case("succulent checked less often than fern", lambda: run("A,cactus,15,plastic,medium,yes,\nB,fern,15,plastic,medium,yes,\n")[0]["plants"][0]["interval_now"] > run("A,cactus,15,plastic,medium,yes,\nB,fern,15,plastic,medium,yes,\n")[0]["plants"][1]["interval_now"])
case("base interval top_dry 15 cm plastic medium spring = 6", lambda: run("A,pothos,15,plastic,medium,yes,\n")[0]["plants"][0]["interval_now"] == 6)
case("small pot dries faster", lambda: run("A,pothos,9,plastic,medium,yes,\n")[0]["plants"][0]["interval_now"] < 6)
case("big pot lasts longer", lambda: run("A,pothos,30,plastic,medium,yes,\n")[0]["plants"][0]["interval_now"] > 6)
case("terracotta dries faster than plastic", lambda: run("A,pothos,15,terracotta,medium,yes,\n")[0]["plants"][0]["interval_now"] < 6)
case("low light lengthens, direct sun shortens", lambda: run("A,pothos,15,plastic,low,yes,\n")[0]["plants"][0]["interval_now"] > 6 > run("A,pothos,15,plastic,direct,yes,\n")[0]["plants"][0]["interval_now"])
case("winter stretches succulent 2.5x", lambda: run("A,cactus,15,plastic,medium,yes,\n", start=dt.date(2026, 1, 5))[0]["plants"][0]["interval_now"] == 25)
case("summer shortens", lambda: run("A,pothos,15,plastic,medium,yes,\n", start=dt.date(2026, 7, 6))[0]["plants"][0]["interval_now"] == 5)
case("southern hemisphere: July is winter", lambda: season_of(dt.date(2026, 7, 1), "south") == "winter" and season_of(dt.date(2026, 1, 1), "south") == "summer")
case("season boundaries north", lambda: [season_of(dt.date(2026, m, 15), "north") for m in (2, 3, 5, 6, 8, 9, 11, 12)] == ["winter", "spring", "spring", "summer", "summer", "autumn", "autumn", "winter"])
case("interval grows across autumn->winter in plan", lambda: (lambda r: r["plants"][0]["intervals_later"].get("winter", 0) > r["plants"][0]["interval_now"])(run("A,pothos,15,plastic,medium,yes,\n", start=dt.date(2026, 11, 16), weeks=4)[0]))
case("checks spaced by interval", lambda: run("A,pothos,15,plastic,medium,yes,\n")[0]["plants"][0]["checks"][:3] == ["2026-04-06", "2026-04-12", "2026-04-18"])
case("last_watered sets first check", lambda: run("A,pothos,15,plastic,medium,yes,2026-04-04\n")[0]["plants"][0]["checks"][0] == "2026-04-10")
case("overdue goes to start", lambda: (lambda p: p["overdue"] and p["checks"][0] == "2026-04-06")(run("A,pothos,15,plastic,medium,yes,2026-03-20\n")[0]["plants"][0]))
case("snap to check days (Mon,Thu)", lambda: all(dt.date.fromisoformat(x).weekday() in (0, 3) for x in run("A,pothos,15,plastic,medium,yes,\nB,cactus,12,terracotta,bright,yes,\n", check_days=["Mon", "Thu"])[0]["calendar"] and [x for p in run("A,pothos,15,plastic,medium,yes,\nB,cactus,12,terracotta,bright,yes,\n", check_days=["Mon", "Thu"])[0]["plants"] for x in p["checks"]]))
case("snap to nearest check day (Sun -> Mon)", lambda: snap(dt.date(2026, 4, 12), dt.date(2026, 4, 6), {0, 3}) == dt.date(2026, 4, 13))
case("snap tie goes earlier (Tue between Mon and Thu? no: Tue -> Mon)", lambda: snap(dt.date(2026, 4, 14), dt.date(2026, 4, 9), {0, 3}) == dt.date(2026, 4, 13))
case("snap never on or before the previous check", lambda: snap(dt.date(2026, 4, 7), dt.date(2026, 4, 6), {0, 3}) == dt.date(2026, 4, 9))
case("autumn between spring and winter", lambda: 6 < run("A,pothos,15,plastic,medium,yes,\n", start=dt.date(2026, 10, 5))[0]["plants"][0]["interval_now"] < run("A,pothos,15,plastic,medium,yes,\n", start=dt.date(2026, 12, 7))[0]["plants"][0]["interval_now"])
case("late snap noted for fast plant", lambda: len(run("A,basil,9,terracotta,direct,yes,\n", check_days=["Mon"])[0]["late_snaps"]) > 0)
case("no drainage -> HIGH warning and half amount", lambda: (lambda r: any(w[0] == "HIGH" for w in r[1]) and "no more" in r[0]["plants"][0]["amount"])(run("A,pothos,15,glazed,medium,no,\n")))
case("unknown plant -> top_dry with warning", lambda: (lambda r: r[0]["plants"][0]["type"] == "top_dry" and any("unknown plant" in w[2] for w in r[1]))(run("A,triffid,15,plastic,medium,yes,\n")))
case("species substring match", lambda: run("A,variegated monstera,15,plastic,medium,yes,\n")[0]["plants"][0]["type"] == "top_dry")
case("succulent low light warning", lambda: any("stretch" in w[2] for w in run("A,echeveria,10,terracotta,low,yes,\n")[1]))
case("fern in direct sun warning", lambda: any("scorch" in w[2] for w in run("A,boston fern,20,plastic,direct,yes,\n")[1]))
case("self-watering succulent warning", lambda: any("self-watering" in w[2] for w in run("A,cactus,15,self-watering,bright,yes,\n")[1]))
case("orchid amount is soak", lambda: run("A,orchid,12,plastic,bright,yes,\n")[0]["plants"][0]["amount"] == "soak 10 min")
case("amount scales with pot", lambda: int(run("A,pothos,25,plastic,medium,yes,\n")[0]["plants"][0]["amount"][1:].split()[0]) > int(run("A,pothos,12,plastic,medium,yes,\n")[0]["plants"][0]["amount"][1:].split()[0]))
case("low humidity shortens", lambda: run("A,fern,20,plastic,medium,yes,\n", humidity="low")[0]["plants"][0]["interval_now"] <= run("A,fern,20,plastic,medium,yes,\n")[0]["plants"][0]["interval_now"])
case("interval never below 1 day", lambda: run("A,basil,6,fabric,direct,yes,\n", start=dt.date(2026, 7, 6), humidity="low")[0]["plants"][0]["interval_now"] >= 1)
case("missing pot size assumed 15", lambda: (lambda r: r[0]["plants"][0]["pot_cm"] == 15 and any("assumed 15" in w[2] for w in r[1]))(run("A,pothos,,plastic,medium,yes,\n")))
def bad(rows):
try:
load_plants(H + rows); return False
except InputError:
return True
case("bad material rejected", lambda: bad("A,pothos,15,wood,medium,yes,\n"))
case("bad light rejected", lambda: bad("A,pothos,15,plastic,dim,yes,\n"))
case("pot in mm rejected", lambda: bad("A,pothos,150,plastic,medium,yes,\n"))
case("bad date rejected", lambda: bad("A,pothos,15,plastic,medium,yes,30/10/2026\n"))
case("ics has one event per date, CRLF, escaped commas", lambda: (lambda r, t: t.count("BEGIN:VEVENT") == len(r["calendar"]) and "\r\n" in t and "\\," in t)(*(lambda r: (r, to_ics(r, "20261030T000000Z")))(run("A,pothos,15,plastic,medium,yes,\nB,fern,15,plastic,medium,yes,\n")[0])))
case("ics lines folded to 75 octets", lambda: all(len(l.encode()) <= 75 for l in to_ics(run("".join(f"Plant number {i},pothos,15,plastic,medium,yes,\n" for i in range(8)))[0], "20261030T000000Z").split("\r\n")))
passed = 0
for name, ok, err in cases:
passed += ok
print(("PASS " if ok else "FAIL ") + name + ("" if ok else f" {err}"))
print(f"{passed}/{len(cases)} passed")
return 0 if passed == len(cases) else 1
def main(argv=None):
ap = argparse.ArgumentParser(description="Build a houseplant watering check calendar")
ap.add_argument("plants", nargs="?")
ap.add_argument("--start", help="first day of the plan, YYYY-MM-DD (default today)")
ap.add_argument("--weeks", type=int, default=4)
ap.add_argument("--hemisphere", choices=["north", "south"], default="north")
ap.add_argument("--humidity", choices=list(HUMIDITY), default="normal", help="low = heating on / below ~35 %%")
ap.add_argument("--check-days", help="snap checks to these weekdays, e.g. Mon,Thu")
ap.add_argument("--ics", help="also write an .ics calendar file")
ap.add_argument("--json", action="store_true")
ap.add_argument("--self-test", action="store_true")
a = ap.parse_args(argv)
if a.self_test:
return self_test()
if not a.plants:
ap.error("give a plants CSV, or --self-test")
try:
start = parse_date(a.start, "--start") if a.start else dt.date.today()
if not 1 <= a.weeks <= 26:
raise InputError("--weeks must be 1..26")
days = None
if a.check_days:
days = [x.strip()[:3].title() for x in a.check_days.split(",") if x.strip()]
for x in days:
if x not in DAY_NAMES:
raise InputError(f"unknown weekday '{x}' in --check-days")
with open(a.plants, encoding="utf-8") as fh:
plants, warnings = load_plants(fh.read())
except (OSError, InputError, UnicodeDecodeError) as e:
print(f"error: {e}", file=sys.stderr)
return 2
res = plan(plants, start, a.weeks, a.hemisphere, a.humidity, days)
if a.ics:
stamp = dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
with open(a.ics, "w", encoding="utf-8", newline="") as fh:
fh.write(to_ics(res, stamp))
if a.json:
out = {**res, "start": res["start"].isoformat(), "end": res["end"].isoformat(),
"calendar": [{"date": d.isoformat(), "plants": n} for d, n in res["calendar"]],
"late_snaps": [{"plant": n, "due": d.isoformat(), "planned": p.isoformat()} for n, d, p in res["late_snaps"]],
"plants": [{**p, "last_watered": p["last_watered"].isoformat() if p["last_watered"] else None} for p in res["plants"]],
"warnings": [{"severity": s, "plant": n, "note": m} for s, n, m in warnings]}
print(json.dumps(out, indent=2))
else:
sys.stdout.write(render(res, warnings))
return 1 if any(w[0] == "HIGH" for w in warnings) else 0
if __name__ == "__main__":
sys.exit(main())