Plant Care: Watering Schedules, Moisture Sensors and Sitter Handoffs
EasyAI-assisted coding · AI-Assisted Debugging
Plant Care: Watering Schedules, Moisture Sensors and Sitter Handoffs
You've joined the small team behind a houseplant-care app. Customers add their plants, log when they water and fertilize them, and open a "Today" screen that tells them what needs care. Three support tickets are open against the scheduling core, and two features are queued behind them. The last one, plant sitters, changes some assumptions the first two were built on. All dates are YYYY-MM-DD strings and all intervals are whole days.
Codebase
| File | What it does |
|---|---|
plants/api.py | run(operations) entry point and the Dispatcher that routes each operation |
plants/garden.py | Garden: the plant registry and every operation on it |
plants/schedule.py | Due-date arithmetic: when a plant is next due for water or fertilizer |
plants/seasons.py | Season rules: which months are summer, which are growing season |
plants/reminders.py | Builds, orders and renders the "Today" (due) list |
plants/catalog.py | Species lookup on top of the care table |
plants/models.py | Plant and SpeciesProfile dataclasses |
plants/dates.py | Strict date parsing and day arithmetic |
plants/config.py | The care table, season months and the postponement limit |
plants/errors.py | PlantError and every error code |
plants/sitters.py | SitterRota stub for Part 3 |
plants/handoff.py | Sitter handoff stub for Part 3 |
tests/ | Existing tests (some currently fail) |
Read config.py and seasons.py before changing any schedule rule: the season months and the species care table live there, and they are the single source of truth.
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh garden and returns one result per operation. Every operation is [name, date, ...args]. date is the day the request is made; it never decreases from one operation to the next. Errors are returned as strings ("error: <code>"), never raised.
| Operation | Arguments after date | Returns | Errors (checked in this order) |
|---|---|---|---|
add_plant | plant_id, name, species | true | invalid arguments, unknown species, plant exists |
water | plant_id | true | plant not found |
fertilize | plant_id | true | plant not found |
next_watering | plant_id | date string | plant not found |
next_fertilizing | plant_id | date string or null | plant not found |
due | none | list of [plant_id, task, overdue_days] | none |
- Operation-level errors come first: an operation that is not a list with at least a name and a date, or a wrong number of arguments, is
error: invalid arguments; an unknown name iserror: unknown operation(checked before the argument count); adatethat is not a real calendar day written exactly asYYYY-MM-DDiserror: invalid date. Only then do the operation's own errors apply. plant_idandnamemust be non-blank strings (invalid argumentsotherwise). Plant ids are case-sensitive.speciesmust be a key of the care table inconfig.py.- A new plant counts as watered and fertilized on the day it is added.
waterandfertilizerestart the plant's watering / fertilizing clock at the request date. They are independent of each other.
Part 1: Fix the scheduling core (about 15 minutes)
Three tickets are open: *a fern watered in late September shows as overdue too early in October*; *customers get fertilizer reminders in September and none in March*; and *the Today list order looks random when two plants are equally overdue*. Find the root causes and fix them; this Part is about reading the existing code before editing it.
- Seasons. Summer is May through September. Every other month is winter. Each species has a summer and a winter watering interval (
config.CARE_TABLE). - Next watering.
next_wateringon dateDislast_watered + interval(D)whereinterval(D)is the summer or winter interval depending on the month ofD, the date the next watering is being computed, not the month of the last watering. Theduelist uses the same rule with its own date. - Fertilizing. The growing season is March through August (note: not the same months as summer).
next_fertilizingonDisnullwhenDis outside the growing season; otherwise it islast_fertilized + fertilize_days. The fertilizing interval does not change with seasons. The due date itself may fall outside the growing season, only the question date matters. - Due list.
dueon dateDlists a[plant_id, task, overdue_days]entry for each plant whose next watering date is on or beforeD, with task"water", and for each plant whose next fertilizing date exists and is on or beforeD, with task"fertilize".overdue_daysisD - due datein days, so0means due today. A plant can appear twice. - Order. Most overdue first. Ties are broken by plant name (case-insensitive), then plant id, then task name (
"fertilize"before"water").
Worked example
[["add_plant", "2026-07-01", "p1", "Fernie", "fern"],
["add_plant", "2026-07-01", "p2", "Monty", "monstera"],
["water", "2026-07-03", "p1"],
["next_watering", "2026-07-03", "p1"],
["due", "2026-07-06"]]returns [true, true, true, "2026-07-05", [["p1", "water", 1], ["p2", "water", 0]]]: a fern waters every 2 days in July, so Fernie is due on the 5th and is one day overdue on the 6th; Monty (5 days) is due that day exactly. Fertilizing is not due yet (14 and 30 days).
A season-crossing example: a monstera added on 2026-09-28 answers next_watering with 2026-10-03 when asked on 2026-09-30 (summer interval, 5 days) but 2026-10-08 when asked on 2026-10-02 (winter interval, 10 days).
Part 2: Moisture sensors (about 12 minutes)
Some customers have a soil-moisture sensor. A wet reading means the plant can wait a bit longer. New operation:
| Operation | Arguments after date | Returns | Errors (checked in this order) |
|---|---|---|---|
moisture | plant_id, percent | the plant's next watering date, as next_watering would answer on this date | plant not found, invalid reading |
percentmust be an integer from 0 to 100 inclusive (true/false, strings and floats are not readings), otherwiseinvalid reading. A rejected reading changes nothing.- A reading is wet when it is strictly above the species'
wet_abovevalue (CARE_TABLE). - A wet reading postpones the plant's next watering by one day, unless two postponing readings have already been counted in a row, in which case it changes nothing. A reading that is not wet ends the "row": the next wet reading may postpone again. Postponements already granted are kept. So wet, wet, wet, dry, wet postpones three times in total.
- Watering the plant (
water) removes all its postponements and ends the row. - Postponements add days after the interval:
next_wateringonDbecomeslast_watered + interval(D) + postponements. This applies everywhere a watering due date is used, including theduelist. Readings never affect fertilizing or other plants.
Worked example (all with the fern added on 2026-07-01, wet above 70)
[["add_plant", "2026-07-01", "f", "Fernie", "fern"],
["moisture", "2026-07-02", "f", 80],
["moisture", "2026-07-02", "f", 71],
["moisture", "2026-07-02", "f", 90],
["moisture", "2026-07-02", "f", 70],
["moisture", "2026-07-03", "f", 71],
["next_watering", "2026-07-03", "f"],
["water", "2026-07-04", "f"],
["moisture", "2026-07-04", "f", 100]]returns [true, "2026-07-04", "2026-07-05", "2026-07-05", "2026-07-05", "2026-07-06", "2026-07-06", true, "2026-07-07"]. The third reading is wet but the row is already two long; 70 is not wet but restarts the row, so the reading on the 3rd postpones a third time. Watering on the 4th clears all three, and the new reading postpones that watering by one day (07-04 + 2 + 1).
Part 3: Plant sitters (about 15 minutes)
Product wants vacation mode: before a trip the owner registers a plant sitter, hands them a sheet of everything due while away, and the owner's app should treat waterings the sitter logs exactly like their own. This changes what a watering is: the clock may now be set by an event that happened earlier than the day it was logged, and the schedule has to be projected over a range instead of asked about for one day. All Part 1 and Part 2 behavior stays unchanged.
New operation sitter, and water gets two optional trailing arguments:
| Operation | Arguments after date | Returns | Errors (checked in this order) |
|---|---|---|---|
sitter | name, start, end | true | invalid arguments, invalid date, invalid range |
water | plant_id[, by[, on]] | true | plant not found, invalid arguments, invalid date, sitter not on duty |
handoff | start, end | list of [plant_id, water_dates, fertilize_dates] | invalid date, invalid range |
sitterrecords thatname(a non-blank string, case-sensitive) covers the whole household fromstarttoend, both days included.start/endmust be valid dates;endbeforestartisinvalid range. The same sitter may be registered for several windows.waterwithbyandon.byis a sitter name ornull(the owner);onis the day the watering actually happened (a date string), ornull/absent for "the request date". Checks, in order: the plant exists;byisnullor a non-blank string (invalid arguments);onis a valid date no later than the request date (invalid date); a non-nullbyhas a window containingon(sitter not on duty).by: nullnever needs a window, so the owner can backdate too.- A sitter's watering counts for the owner. It restarts the clock and clears postponements exactly like an owner's, as if it happened on
on. But a watering dated before the plant's currentlast_wateredis only a late log of an older event: it is accepted (true) and changes nothing. A watering dated the same day aslast_watereddoes restart the clock (clearing postponements) even if the date does not change. handoffreturns, for the rangestart..end(both days included), when each plant needs care.start/endmust be valid dates,endbeforestartisinvalid range. It changes no state. Plants are ordered by name (case-insensitive), then id; a plant with no dates at all in the range is left out.- Watering dates. The first date is the plant's
next_wateringas ofstart(the season ofstartpicks the interval, existing postponements count). If that is beforestart, the plant is handed over onstartitself. Assume the sitter waters on every listed date; the next date is that date plus the interval in force on that date (no postponements). List dates up to and includingend. - Fertilizing dates. The first date is
last_fertilized + fertilize_days, moved tostartif earlier. Any date that falls outside the growing season is moved to the next March 1. Then repeat: the next date is the previous one plusfertilize_days, again moved to the next March 1 when outside the growing season. List dates up to and includingend.
Worked example
[["add_plant", "2026-07-01", "p1", "Fernie", "fern"],
["add_plant", "2026-07-01", "p2", "Monty", "monstera"],
["handoff", "2026-07-08", "2026-07-10", "2026-07-16"]]returns [true, true, [["p1", ["2026-07-10", "2026-07-12", "2026-07-14", "2026-07-16"], ["2026-07-15"]], ["p2", ["2026-07-10", "2026-07-15"], []]]]. Both plants are already overdue on the 10th, so both start there. The fern repeats every 2 days; its fertilizer is due on the 15th.
And for sitter logs (fern added 2026-07-01):
[["sitter", "2026-07-01", "jo", "2026-07-05", "2026-07-12"],
["water", "2026-07-13", "p1", "jo", "2026-07-11"],
["next_watering", "2026-07-13", "p1"],
["water", "2026-07-13", "p1", "jo", "2026-07-13"]]gives true, true, "2026-07-13" (the 11th plus 2 days), then "error: sitter not on duty" (Jo's window ended on the 12th).
Constraints
- Python 3 standard library only; no wall clock and no randomness.
- Every operation returns exactly one value; errors are returned, never raised.
- Keep existing behavior of earlier Parts intact. Hidden tests re-check Parts 1 and 2 after Part 3.
Example 1
- Input:
- [["add_plant","2026-07-01","p1","Fernie","fern"],["add_plant","2026-07-01","p2","Monty","monstera"],["water","2026-07-03","p1"],["next_watering","2026-07-03","p1"],["due","2026-07-06"]]
- Output:
- [true,true,true,"2026-07-05",[["p1","water",1],["p2","water",0]]]
- Explanation
- A fern waters every 2 days in July, so Fernie is due on the 5th and is a day overdue on the 6th; Monty (5 days) is due on the 6th.
Example 2
- Input:
- [["add_plant","2026-07-01","f","Fernie","fern"],["moisture","2026-07-02","f",80],["moisture","2026-07-02","f",71],["moisture","2026-07-02","f",90]]
- Output:
- [true,"2026-07-04","2026-07-05","2026-07-05"]
- Explanation
- Each wet reading postpones watering one day, but only two in a row count.
Example 3
- Input:
- [...add p1 fern and p2 monstera on 2026-07-01..., ["handoff","2026-07-08","2026-07-10","2026-07-16"]]
- Output:
- [..., [["p1",["2026-07-10","2026-07-12","2026-07-14","2026-07-16"],["2026-07-15"]],["p2",["2026-07-10","2026-07-15"],[]]]]
- Explanation
- Both plants are overdue at the start of the range, so each starts on the 10th; the sitter waters on every listed date.
Guidelines
- Python 3 standard library only
- Dates are YYYY-MM-DD strings; operation dates never decrease; intervals are whole days
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised
- Earlier Parts' behavior must not change when later Parts are added