Skip to content

Updating from 3.17 or earlier? Read the upgrade guide first.

Crop Steering
Browse the documents

Timer programs and irrigation modes

Applies to release 3.18.1 · View source at the tag · Markdown

On this page

Bottom line: every zone has a Watering by setting: steering (the engine waters by its probes, as before), timer (the room's timer programs water it) or off (no shots at all, rescue shots included). A zone on timer still gets the engine's safety shots: the P3 emergency, the lights-on watchdog, the minimum-daily floor and a blind zone's copy of a sibling's rescue. Timer shots go through every safety check a steering shot does: the kill switch, the zone switch, faults, source water, the daily budget and the pump queue. One planned shot can be skipped from the timeline. Every program save is checked, previewed, read back and written to the journal.

Getting there

  • Dashboard menu: Irrigation › Timer programs (route #/plan/timers). This page holds the room's programs, the program editor and the Irrigation mode panel (who waters each zone).
  • Dashboard menu: Irrigation › Timeline (route #/plan/watering). This shows the planned timer shots and has the Skip this shot button.
  • With an integration that has no timer programs, the Timer programs page says so and shows no programs. The Timeline still works; only Skip this shot answers "Skipping a shot needs Crop Steering 3.18 or newer."

Irrigation modes (Watering by)

Irrigation › Timer programs: the programs, each zone's irrigation mode and the next 24 hours

Each zone has one select:

select.crop_steering_<prefix>zone_<n>_irrigation_mode, named Zone N Watering by, with options steering, timer and off. A new zone starts on steering.

ModeWhat the zone gets
steeringThe engine's shots, as always. Timer programs do not water it, even if it is listed in one.
timerThe slots of the one active timer program that lists it, plus the engine's safety shots: p3_emergency, watchdog, min_daily and blind_copy_rescue. Every other engine shot is dropped (reason TIMER: the engine's <kind> shot is not given).
offNothing: no engine shots, no timer shots and no rescue shots (reason OFF: this zone is set to no watering).

Safety-critical points:

  • off stops rescue shots too. A zone on off will not get a P3 emergency or watchdog shot. The page warns: "Off: no shots at all, rescue shots included."
  • A zone on timer that no active program lists gets only the safety shots. The page warns: "No running program covers this zone: it will not be watered."
  • Steering needs a probe. On the Timer programs page the Steering button is greyed out for a zone with no probe ("Needs a probe in this zone").
  • A test shot still runs on any mode, off included. It is the operator's own action. The kill switch and the other checks still apply to it.
  • The blind fallback shot (blind_fallback, a steering estimate for a zone with no readable probe) is not kept in timer mode.

Changing a zone's mode

On Irrigation › Timer programs › Irrigation mode, pick the mode, read the sentence that says what will change, then press Switch to … (Steering, Timer or Off) or Keep …. The button uses the zone's display name. The sentences are:

  • To timer: "Zone N stops steering and waters only by its timers." If no running program covers the zone, it adds "No running program covers it, so it will not be watered."
  • To off: "Zone N gets no shots at all, rescue shots included."
  • To steering: "Zone N steers by its probe again; its timers stop watering it."

You can also set the select directly in Home Assistant (select.select_option).

How the controller reads the mode

  • The controller reads each zone's select on every pass and remembers the last mode it read in its state.json.
  • If the select reads unknown, unavailable or is missing, the controller uses the last mode it read. A zone stays off through a Home Assistant restart.
  • If no mode was ever read for a zone (an older integration with no select), the zone runs on steering, as it always did.
  • If the integration restores a saved mode it does not recognise, the select reads unknown until someone picks a mode. It does not fall back to steering.

Mode off compared with the zone switch

Watering by = offZone switch switch.crop_steering_<prefix>zone_<n>_enabled off
Where it actsOn the decision, before anything opens. The engine's shot and any timer slot are dropped.A safety check (zone_paused) that holds every shot for the zone.
Test shotStill runs.Held.
A shot already runningNot affected. Only new decisions are dropped.Stopped early. Alert CS-305, "this zone was set to off".
If it cannot be readThe last mode read is used.Held: an unreadable zone switch counts as off.

Use the zone switch to pause a zone for work on it. Use mode off when a zone should not be watered by the engine or a timer, but you still want to be able to give it a test shot.

Mode timer compared with the kill switch

The kill switch (the room's engine switch) holds every shot in the room, timer shots and safety shots included. A timer program never runs while the kill switch is off. Turning the kill switch off during a timer shot stops that shot (CS-305, "the kill switch was set to off").

Timer programs

A room holds any number of timer programs. Each program waters a list of zones for a fixed number of seconds at times set by its type and window.

Program fields

FieldRequiredMeaning
idNo (new) / Yes (change)Leave it out to add a program. Give it to change an existing one.
nameNoShown on the page. Blank becomes Timer.
typeYesrecycle, simple or one_off.
zonesYesAt least one zone number in this room. Duplicates are removed.
duration_sYesSeconds each zone runs per shot. Must be greater than 0.
activeNotrue (default) or false. An inactive program never runs.
windowNoWhen the program may water. Default {"kind": "all"}. See below.
interval_minrecycleMinutes between shots. At least 1.
sync_timeNo (recycle)HH:MM. The first shot lines up on this time. It must be inside the window.
frequency_per_daysimpleShots per day. At least 1. A fraction is cut to a whole number (2.5 becomes 2), not refused.
start_atone_offISO date and time, for example 2026-10-24T09:00:00+13:00.

Anything else sent with a program (for example next_at from a list) is not stored. The integration adds created_at, updated_at and, on delete, deleted_at.

Windows

kindWindow
allThe whole day, from local midnight.
lights_onFrom the room's lights-on hour to its lights-off hour.
lights_offFrom lights-off to lights-on.
customFrom from to to, both HH:MM. If to is earlier than from, the window crosses midnight. from and to cannot be the same.

The lights hours come from the room's number.crop_steering_<prefix>lights_on_hour and lights_off_hour, or from what setup saved if those cannot be read.

When each type waters

  • recycle: every interval_min minutes from the start of the window, until the window ends. With sync_time, the first shot is at sync_time and the next ones follow every interval_min minutes until the window ends; no shots fall between the window start and sync_time. If the lights hours later move the window so that sync_time falls outside it, the shots start at the start of the window.
  • simple: frequency_per_day shots spread evenly across the window, the first at its start. For example, frequency_per_day: 4 with {"kind": "all"} waters at 00:00, 06:00, 12:00 and 18:00.
  • one_off: one shot for each zone at start_at.

A wall-clock time that does not exist because of a daylight-saving change is left out.

Example

Two minutes every two hours during lights-on for zones 1 and 2:

{
  "name": "Mother room",
  "type": "recycle",
  "zones": [1, 2],
  "duration_s": 120,
  "window": {"kind": "lights_on"},
  "interval_min": 120,
  "active": true
}

Rules the integration checks

  • A zone can belong to one active program only. A save that would put a zone in two active programs is refused, for example "Zone 3 is already in timer 'Mother room'".
  • Every zone must exist in the room ("Zone 9 is not in this room").
  • Errors are in plain words, for example "interval must be at least 1 minute", "sync time must be inside the window", "End must be after start".

How a timer shot is sized and checked

  • A timer shot asks for duration_s seconds. The room's maximum shot length still caps it. A capped shot runs, cut, and raises CS-306.
  • The zone needs a known flow (dripper flow and drippers per plant) and substrate volume. Without them the shot does not run and CS-204 says why.
  • The daily budget applies. A shot that would go past the zone's daily maximum is cut to what is left of the budget and runs (logged as "shot clipped Xs -> Ys"). It is held only when less than the minimum shot length is left, and then alert CS-205 (daily cap) is raised. The page words this as "Shots past it are skipped and logged."
  • A plan hold does not stop timer shots. Plan holds stop routine steering only.
  • Zones wait for the pump only when running them together would go past the pump's capacity (max_flow_lph, max_concurrent_zones) or an exclusive group. The preview shows "Waiting for pump" only when the pump runs one zone at a time and the program has more than one zone.
  • Water given by a timer shot is never used as input by the Auto setpoints learner.

Missed slots

  • If a safety shot fires in the same pass as a timer slot, the safety shot goes first. The slot is still owed and is given on a later pass.
  • An owed slot is dropped 15 minutes after its time, or at the end of its program window if that is sooner. It is journaled as intent.refused with guard expired.
  • After a controller restart, the first pass only starts the clock. Slots missed while the controller was down are not given late.
  • If a program cannot be worked out, the controller logs it once ("timer program '…' not run: …") and runs the room's other programs.

Using the Timer programs page

  1. Pick a program in the list, or start a new one. With no programs the page says "No timer yet. Most mother rooms use 2 min every 2 h during lights-on."
  2. Set the zones. The list holds zones set to Timer plus any zone already in the program, whatever its mode (the page's own hint says only Timer zones are listed). If no zone is set to Timer, the page says "No zone is set to Timer yet. Set one under Irrigation mode."
  3. Set the amount. It reads as a sentence, for example "Run for 2 min every 120 min" (the interval is always in minutes, 10 to 120). A one-off reads "Run for 3 min at 20:00 on Fri 23 Oct". The zones and the window are chosen beside it.
  4. Check Next 24 hours. It draws the shots this program would give. Shots past a zone's daily maximum are drawn as "Blocked by daily max".
  5. Check Daily max per zone and Pump capacity. The pump card shows the flow needed against the pump's max_flow_lph, or "The pump cannot feed every zone it may run at once."
  6. Press Review & apply. The page shows the differences, saves and reads the program back from the list.
  7. Delete program removes it. It can be put back with Undo.

The Same safety as steering card is a reminder that Stop all, the source-water pH/EC check and the feed-tank level guard hold timer shots as they hold steering shots.

Skipping one planned shot

On Irrigation › Timeline, a planned shot has a Skip this shot button.

A skip covers one zone at one time:

  • A timer slot at exactly that time is not given.
  • An engine shot that would fire within 15 minutes from that time is not given, once. This works on a steering zone too.
  • These shots are never skipped: p3_emergency, watchdog, min_daily and blind_copy_rescue.
  • The time may be up to 48 hours ahead and no more than 24 hours ago. A skip older than 24 hours is no longer published in timers_config; it is pruned from the store at the next save. Skipping the same zone and time again replaces the earlier skip.
  • Each skipped shot is journaled as intent.refused with guard skipped and the reason "skipped by ", where the id is the Home Assistant user id, else the calling actor, else timer_skip.
  • Once skipped, the shot on the timeline is marked skipped and has no Skip button.

Projection: what the timeline shows as planned

At the end of each pass the controller works out sensor.crop_steering_<prefix>projection_app and writes it when its state or attributes changed. Its state is the number of planned shots. Its intents attribute lists the next 24 hours:

  • Timer zones: each program slot with source: "timer", program_id, duration_s and expected: false.
  • Steering zones: the engine's next expected shot with source: "engine", duration_s: null and expected: true. This is an estimate based on what the engine is waiting for, not a promise.
  • Off zones: nothing.
  • Each intent has a stable id and phase, the phase the engine expects the zone to be in then (null where moisture decides).

The controller writes this sensor only once the integration publishes timer programs. If the projection fails, the error is logged and watering continues. The timeline (timeline_get) draws planned blocks from this sensor only, so nothing is drawn twice. It shows only intents after now and marks skipped ones.

Entities

EntityStateAttributes
select.crop_steering_<prefix>zone_<n>_irrigation_modesteering, timer or offoptions
sensor.crop_steering_<prefix>timers_configThe store revision, or unavailableroom_id, revision, programs (active, not deleted), skips (zone, at, until, by), or error
sensor.crop_steering_<prefix>projection_appNumber of planned shotsroom_id, intents

If the stored programs cannot be read, timers_config reads unavailable, the store is never saved over and every save is refused. While it is unavailable the controller has no programs to run, so timer zones get only their safety shots.

Services

room_id takes the form room:<prefix>, for example room:f2_. It is optional for timer_program_list and required for the other four. The writes take the expected_revision from timer_program_list. If the store changed since, the write is refused with stale_revision ("Timer programs changed elsewhere. Reload before saving."). With api: 2 the answer comes in the API v2 envelope. The writes also take client (mcp or aigrow) and proposal_token.

ServiceAdminWhat it does
crop_steering.timer_program_listNoThe room's programs that are not deleted, each with room_id, next_at (next shot within 8 days), error and zones_in_timer_mode, plus revision. Without room_id: every room's programs, with revisions and errors per room.
crop_steering.timer_program_previewNoChecks the full program sent with it (there is no lookup by id, so send a saved program in full) and returns preview (zone, at, duration_s for each shot), shots and total_s. hours from 1 to 48, default 24. Saves nothing.
crop_steering.timer_program_setYesAdds a program, or changes one when program.id is given. Returns program, the new revision and preview for the next 24 hours.
crop_steering.timer_program_deleteYesSoft delete by id: sets active: false and deleted_at. The program stays in the store for its history and is no longer listed.
crop_steering.timer_skipYesSkips zone at at (ISO date and time; a time with no offset is taken in Home Assistant's time zone). Returns skip (zone, at, until) and revision.

"Admin" means a signed-in user calling it must be a Home Assistant administrator. Every save journals config.changed with store: "timers", the section (programs or skip), the old and new values and the diff.

Example:

service: crop_steering.timer_program_set
data:
  room_id: "room:f2_"
  expected_revision: 3
  program:
    name: Mother room
    type: recycle
    zones: [1, 2]
    duration_s: 120
    window: {kind: lights_on}
    interval_min: 120

Timer programs are also part of presets and copy settings (section timers). The MCP server can preview and apply a program (preview_timer_program, then apply_proposal) but cannot delete one or skip a shot.

Code

  • Integration store and services: custom_components/crop_steering/timers_api.py
  • Program checks and projection: crop-steering-engine/src/crop_steering_engine/timers.py (copied to custom_components/crop_steering/generated/timers.py and the controller's crop_steering_engine/)
  • The select: custom_components/crop_steering/select.py, options in const.py IRRIGATION_MODES
  • Controller: addons/f2_control/f2_control/controller.py IRRIGATION_MODES, TIMER_KEEPS, _irrigation_modes, _apply_modes, _publish_projection, _skipped_by
  • Timeline planned blocks and skips: custom_components/crop_steering/timeline.py
  • Pages: frontend/src/pages/timers.tsx, frontend/src/lib/timers.ts, frontend/src/pages/watering.tsx