Skip to content

Feature Request: Native cycle-and-soak with minimum soak time #234

Description

@JB09

Problem / Use Case

I’m running a residential lawn irrigation system on Home Assistant with Irrigation Unlimited driving 6 zones split across 2 hose bibs (3 zones each). Each bib can only run one zone at a time (flow/pressure limit), but both bibs can run concurrently — so I use two sequences that run in parallel, which IU handles well.

My soil is heavy clay. Clay absorbs water slowly, so applying a zone’s full runtime in one shot causes runoff and pooling long before the water reaches the root zone. The standard fix is cycle-and-soak: split each zone’s runtime into shorter cycles with a soak period between them so the water can infiltrate. For clay, that soak needs to be a meaningful, guaranteed minimum (commonly ~60 minutes).

Why the current options don’t fully solve it

IU can approximate cycle-and-soak today with repeat + delay, but there are real gaps for this use case:

  • delay sits between every zone, not between a zone’s own cycles. To guarantee a per-zone soak I have to set a large delay, which inserts that delay after every zone and creates hours of idle dead time. There’s no concept of a per-zone minimum soak that gets intelligently filled by running the other zones in the meantime — the way commercial controllers (RainMachine, Rain Bird) do it.
  • No minimum-soak guarantee. If I rely on other zones to fill the soak, the soak is just however long those zones happen to take. With only 3 zones per bib, that falls well short of the ~60 minutes clay needs, and there’s no way to enforce a floor.
  • repeat multiplies the adjust_time value. I drive durations from Smart Irrigation via adjust_time, which pushes a total runtime per zone. With repeat: N, that total is multiplied by N, so the zone runs N× too long (or hits the max-duration cap). This makes the Smart Irrigation integration and cycle-and-soak mutually difficult — see related issues Smart Irrigation integration with Irrigation Unlimited not fully functional #203 and Despite different configuration the watering time is limited to one hour ("magically") #211.
  • No even distribution / minimum cycle length. Splitting purely by a max cycle length leaves a tiny trailing cycle (e.g. 15 + 15 + 5 min). I’d want the total distributed evenly across cycles, with a configurable minimum cycle length so no cycle runs for an impractically short time.
  • No “skip when budget met.” Zones with small needs get cycled the same number of times as zones with large needs, wasting passes and extending the run window.

Desired outcome

A native cycle-and-soak mode (per sequence or per zone) that:

  • Splits each zone’s total runtime into evenly distributed cycles, capped at a configurable max cycle length (the runoff threshold) and floored at a configurable min cycle length
  • Enforces a configurable minimum soak between a zone’s own cycles, filling that soak by interleaving the other zones and inserting idle time only when needed to reach the minimum
  • Lets zones with smaller totals finish early and drop out of later cycles
  • Re-derives the per-cycle split from a total set via adjust_time, so Smart Irrigation (and similar) integrations work without the caller having to pre-divide by a repeat count

This would make IU a clean fit for clay-soil lawns and, importantly, resolve the long-standing friction between repeat and adjust_time / Smart Irrigation that several users have hit (#203, #211).

Proposed approach

A new opt-in cycle-and-soak block, configurable at the sequence (and optionally zone) level. Sketch of the config:

sequences:
  - name: "Bib 1 - Front/Side"
    cycle:
      max_duration: "00:15"   # runoff threshold; cap per cycle
      min_duration: "00:10"   # floor; never run a cycle shorter than this
      min_soak: "01:00"       # guaranteed soak between a zone's own cycles
    zones:
      - zone_id: 1
        duration: "00:45"     # total; cycle logic derives the per-cycle split
      ...

Behavior:

  1. For each zone, derive the cycle count from its total duration:
    num_cycles = ceil(total / max_duration)
  2. Distribute the total evenly across those cycles: per_cycle = total / num_cycles. This avoids a tiny trailing cycle (e.g. 45 min → 3 × 15, never 15 + 15 + 15 + 0, and 35 min → 3 × ~11.7, never 15 + 15 + 5).
  3. Apply the minimum cycle floor: cap num_cycles at floor(total / min_duration) so per_cycle never drops below min_duration. If a zone’s total is itself below min_duration (e.g. a rained-on day), run it once for whatever it needs.
  4. Between a zone’s own cycles, guarantee min_soak. Fill that soak by interleaving the other zones in the sequence; insert idle delay only for the remainder needed to reach min_soak.
  5. Zones with fewer cycles complete early and drop out of later passes, rather than being padded to match the longest zone.

Key integration point: when a zone’s total is set via adjust_time, the cycle logic re-derives the per-cycle split from that new total. This is what makes the feature compose cleanly with Smart Irrigation (which pushes a daily total per zone) without the caller having to divide by a repeat count — and is the part that today’s repeat breaks.

This mirrors how commercial controllers (RainMachine, Rain Bird) implement cycle-and-soak: even distribution, a minimum cycle length, a minimum soak floor, and other zones filling the soak window.

Notes from testing (current behavior, v2025.10.0)

For reference, behavior I confirmed empirically while exploring workarounds:

  • repeat multiplies the adjust_time actual value (push 9 min with repeat: 3 → zone runs 27 min total).
  • delay is applied between every zone in a sequence, not between a single zone’s cycles.
  • In adjust_time, the zones parameter targets the position within the sequence (1-based), not the global zone id.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions