Schedules — design philosophy (two tiers, one view)¶
Aqualink-Automate manages schedules at two tiers that appear together in one unified Schedules view. The tiers exist because they make different trade-offs; the UI's job is to present them as one coherent timeline and to move a schedule between tiers when it makes sense.
Tier 1 — Controller schedules (the resilient baseline)¶
The controller's own built-in program timers (Program Groups A/B). These are the "must always work" essentials — the filter-pump loop, heating windows — and they keep running even if the app host, network, or MQTT broker falls over. The app reads them off the wire (IAQ / OneTouch Program pages; see iaq_schedule_protocol.md), and will edit / manage them and help promote app schedules down into them.
They are deliberately limited by the hardware:
| Aspect | Controller capability (owner manual §6.4, §6.11–6.13) |
|---|---|
| Action | on/off span for one equipment item only (no setpoints/%/mode) |
| Days | all / weekdays / weekends / a single day — no arbitrary multi-day |
| Time | 12-hour (AM/PM) on the panel; whole-minute on/off |
| Per device | multiple programs allowed ("PGM 1 of 3") |
| Program Groups | two sets A / B, only one active; per-group custom label |
| Auto-switch | optional: a start date mm/dd for A and for B flips the active group by season |
| VSP pumps | speed sub-programs nested inside the filter-pump cycle (advanced; deferred) |
Only the active group's programs are observable passively; reading the other group would require switching the active group (a write that changes controller behaviour), so a read exposes the active group and reports that the other exists.
Tier 2 — App schedules (rich / exotic)¶
The app's own SchedulerService (--schedules-file, /api/schedules) fires
ICommandDispatcher actions on a wall clock. This is where flexibility lives: one-off "party"
schedules, specific dates, arbitrary day combinations, reactive/conditional triggers, and
actions beyond on/off — setpoints, chlorinator %, circulation mode (already in
Scheduling::ActionType). App schedules are not constrained by the controller's limits;
only promotion is.
Moving between tiers¶
- Promote (app → controller): push an app schedule down so it survives an app outage.
Allowed only when it fits the controller's constraints (single equipment on/off, an
expressible day selection, a free program slot / group). The UI marks which app schedules
are promotable and surfaces exactly what is lost or constrained when they are not (e.g.
"Mon+Wed+Fri isn't representable — the controller allows a single day or weekdays"). The
feasibility predicate is
Scheduling::CheckControllerCandidate/ClassifyDaySelection/IsControllerRepresentableAction(src/core/scheduling/promotion_constraints.h), which returns stable blocker codes the UI localises. It judges the candidate span shape; forming that span from app schedules (an on-edge paired with an off-edge) is the promotion flow's job and lands with the write path. - Adopt (controller → app): pull a controller program up into the app to edit it with the richer app model (then optionally re-promote).
The unified view¶
The existing Schedules view (list + 24-hour timeline) renders both tiers, each row labelled by
source/tier, with conflict flagging across the merged set. Controller rows come from
/api/controller/schedules ({status, schedules[]}); app rows from /api/schedules.
Roadmap¶
- Read controller schedules (IAQ first, then OneTouch) →
ControllerScheduleStoreavailable; model program group (A/B + active) and the day constraint. (in progress) - Surface tier + source + group in the unified view; conflict flagging across tiers.
- Promote app→controller (constraint checker + the write handshake, once the create/edit/ delete button-press flow is fully decoded).
- Adopt controller→app; auto-switch + custom-label read/display; VSP speed sub-programs.