diff --git a/docs/plans/2026-05-18-whiletrue-trigger-mode-design.md b/docs/plans/2026-05-18-whiletrue-trigger-mode-design.md new file mode 100644 index 0000000..0e53370 --- /dev/null +++ b/docs/plans/2026-05-18-whiletrue-trigger-mode-design.md @@ -0,0 +1,134 @@ +# WhileTrue Trigger Mode for Conditional & Expression Script Triggers — Design + +**Date:** 2026-05-18 +**Status:** Approved (brainstorming) — implementation to follow + +## Context + +Template script triggers of type `Conditional` and `Expression` currently fire +*once* when their condition first holds: + +- **Expression** is edge-triggered — the script runs once per `false→true` + transition of the boolean expression. +- **Conditional** runs the script on each change of the monitored attribute for + which the `{operator, threshold}` comparison is true. + +There is no way to keep a script running *while* a condition remains true. This +design adds a second mode, **WhileTrue**, alongside the existing behavior +(referred to as **OnTrue**). + +### Decisions taken during brainstorming + +- **WhileTrue is a timer re-fire cadence.** On the `false→true` edge the script + fires immediately; while the condition stays true it re-fires on a periodic + timer; on the `true→false` edge the timer stops. It re-fires even if no + attribute updates arrive (e.g. a static condition). +- **The re-fire interval is the script's existing `MinTimeBetweenRuns`** — no + new configuration field. One value serves as both the WhileTrue cadence and + the general per-script throttle. +- **OnTrue is unchanged.** A trigger config with no `mode` field parses as + `OnTrue`, so every existing deployed template behaves identically. +- **Alarms are out of scope** — alarm triggers are already level-based ("active + while true"). + +## Design + +### 1. Trigger model & storage + +`Conditional` and `Expression` trigger `TriggerConfiguration` JSON gains an +optional `mode` field: + +| Trigger | JSON shape | +|-------------|---------------------------------------------------------| +| Conditional | `{ attributeName, operator, threshold, mode? }` | +| Expression | `{ expression, mode? }` | + +`mode` is `"OnTrue"` (default; absent ⇒ `OnTrue`) or `"WhileTrue"`. Entity +types are unchanged — `TemplateScript.TriggerConfiguration` stays `string?`. + +### 2. Runtime evaluation (`ScriptActor`) + +The internal trigger-config records gain a `Mode`: + +```csharp +internal enum TriggerMode { OnTrue, WhileTrue } +internal record ConditionalTriggerConfig( + string AttributeName, string Operator, double Threshold, TriggerMode Mode) : ScriptTriggerConfig; +internal record ExpressionTriggerConfig(string Expression, TriggerMode Mode) : ScriptTriggerConfig; +``` + +**OnTrue** — current behavior, unchanged: +- Conditional: fire on each monitored-attribute change for which the comparison + is true. +- Expression: fire once per `false→true` transition. + +**WhileTrue** — the actor tracks the condition's current truth state: +- **`false→true` edge:** fire once immediately (via `TrySpawnExecution`, which + still respects `MinTimeBetweenRuns` against any prior run), then start a + periodic Akka timer with both initial delay and interval set to + `MinTimeBetweenRuns`. +- **Each timer tick:** spawn an execution **directly** — the timer interval is + itself the cadence, so ticks bypass the `MinTimeBetweenRuns` skip-check (which + could otherwise drop a tick to sub-millisecond timing jitter). +- **`true→false` edge:** cancel the timer. +- The state transitions naturally re-arm: a later `true` after a `false` starts + a fresh cycle. + +Truth-state sources: +- Conditional depends only on the monitored attribute, so it is re-evaluated on + each change of that attribute (unchanged gate). +- Expression is re-evaluated on every attribute change (unchanged). + +A throwing/non-bool expression is treated as `false` (existing behavior); for +WhileTrue that cleanly stops the timer. + +**Edge case — WhileTrue with no `MinTimeBetweenRuns`:** a periodic timer has no +interval to use, so the trigger degrades to a single edge fire and logs a +warning. Deployment-time validation of this combination is a deliberate +non-goal of this change. + +New surface in `ScriptActor`: a `TriggerMode` on the two config records, one +`WhileTrueTick` internal message, and start/stop timer helpers (the actor +already implements `IWithTimers` for Interval triggers). Timer key +`"whiletrue-trigger"`. + +### 3. Editors & codec + +- **`ScriptTriggerConfigCodec`:** `ScriptTriggerModel` gains a `Mode` + (`ScriptTriggerMode` enum, default `OnTrue`). `Parse` reads `mode` for the + Conditional and Expression kinds; `Serialize` writes it for those kinds. + Absent/unrecognized `mode` ⇒ `OnTrue`. +- **`ScriptTriggerEditor.razor`:** the Conditional and Expression panels gain a + mode `