FCU-0005 — Inactive heating coil temperature rise (leak)
| Status | verified — engine e2ff2f8, cxf:fnv1a128:77f1afc9046249364d915e9e86936853, 2026-08-17 |
| Severity | 3 |
| Method | rule |
| Phase | 1 |
| Category | CRITICAL_WASTE |
| Confidence | HIGH |
| Estimation | DIRECT_MEASUREMENT |
| G36 | §5.22.6 FC#5 |
| Clusters | — |
| Suppresses | — |
| Suppressed by | — |
| Related | FCU-0004, FCU-0003, FCU-0006, FCU-0007, FPB-0003 |
| Playbooks | fcu-faults |
| Source | HVAC FDD Reference v1.0 §12, FCU-0005; G36 §5.22.6 FC#5 (the chapter’s cited source; clause text not available — see Deviations); PNNL EEM-03 (fix leaking valves) |
| Operating states | OS#2 (no active coils) — host-gated |
Preconditions (host-enforced): The fan must be running. sat is only a coil leaving temperature while air is moving over the coil; on a cycling-fan FCU the discharge sensor sits in stagnant duct air between cycles and reads whatever the coil above it is doing, which is this fault’s exact signature and none of its meaning. This graph does not consume the dictionary’s canonical
fan_cmdorfan_status, so the host must gate it on trusted fan proof; an FCU-0006 fail-to-start makes this verdict NO_EVAL (see Deviations). Suspend evaluation for a settling window after the heating valve closes — a coil giving up the hot water standing in it shows the same rise for several minutes, and alarm_delay is sized to ride out the usual case rather than to replace the gate. rat and sat must both be trustworthy and must both be in the airstream: this binding uses them as the coil entering and leaving temperatures, so a return sensor mounted on the wall as a space sensor, or a discharge sensor in a supply plenum shared with another unit, breaks the premise with no other symptom. Nothing in this rule cross-checks either sensor. When any gate is unmet the verdict is NO_EVAL, not healthy — as it is whenever the in-rule output yCmdOk reads false.
Points: htg_vlv_cmd, sat, rat
Outputs:
yFault— True while the valve has been commanded shut and sat has stayed more than inactive_coil_threshold above rat, continuously for at least alarm_delayyCmdOk— True while htg_vlv_cmd is below cmd_closed_threshold — the coil is commanded shut and the rise across it is therefore interpretable. False means the coil is allowed to be heating and this rule has no verdict; the host reports NO_EVAL, not healthy
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
inactive_coil_threshold | 3.0 | °C | riseBig.t | Rise from entering to leaving air that stops being fan heat and sensor error and starts being a leak. ADOPTED — the reference names the parameter in the equation and publishes no value for it (see Deviations). 3.0 °C is the rounded G36-style composition for FCU-grade instrumentation: sqrt(e_ret² + e_sup²) + dTSF with a ±1.4 °C-class sensor pair and 1 °C of fan heat. Because the fan sits inside the measurement, the shipped value fires at about 2 °C of true coil rise; a site that measures its own fan rise substitutes it in the sum directly |
cmd_closed_threshold | 1.0 | % | vlvShut.t | Command below which the heating valve counts as commanded shut. ADOPTED — the reference writes the test as htg_vlv_cmd = 0%, which is not a comparison a real-valued signal supports (see Deviations). 1.0% is deliberately tighter than AHU-0016’s 5% open threshold: this rule needs the valve to be at rest, not merely nearly closed |
alarm_delay | 1800.0 | s | persist.delayTime | Continuous violation required before the alarm asserts (30 min). ADOPTED — the reference publishes no tunables line for this card (see Deviations); the value is the AHU twin’s G36 AlarmDelay |
Description
A fan coil’s heating valve is a small two-way valve on a small coil, and when its seat wears the leak is correspondingly small: a few degrees of rise across a coil the sequence believes is shut. Nothing about the unit looks broken — the fan runs, the valve reports 0%, the zone holds setpoint, because the cooling coil or the neighbouring units quietly take the extra heat back out. Nobody complains, so nobody looks, which is why this is worth a rule rather than a walkthrough: FCUs are deployed by the hundred in hotels and apartments, each wasting an amount too small to notice on a bill and too tedious to find by hand. The chapter puts one leaking valve at 3–10% of zone heating energy and 100–800 kg CO₂e a year. Reading temperatures rather than flows, the rule cannot separate a worn seat, a valve that never quite closes, and hot water thermosiphoning through a vertically piped coil with the valve shut and blameless; all three are on the diagnosis list.
Detection Logic
yCmdOk = htg_vlv_cmd < cmd_closed_threshold (false ⇒ host reports NO_EVAL)
rise = sat − rat
yFault = (rise > inactive_coil_threshold) AND yCmdOk,
sustained continuously for alarm_delay
Block graph (rule.cxf.jsonld):
rise subtracts in the reference’s own order (leaving_temp − entering_temp),
which is what makes the sign of this rule the opposite of FCU-0004’s.
vlvShut feeds gate and also leaves the block as yCmdOk, so a host can tell
the two silences apart: quiet with yCmdOk true is a coil shut and behaving,
quiet with it false is a coil being asked to heat, about which this rule has
nothing to say. The allowance is where the physics lives. On a healthy FCU with
both valves shut, sat − rat is not zero — the fan sits between the two sensors
and puts its shaft work into the air — so with a pair of zone-grade sensors each
allowed to be off by more than a degree, a healthy unit can read nearly 3 °C
with nothing running. With the fan inside the measurement the shipped 3.0 °C
fires when the coil’s own contribution passes roughly 2 °C, and a bigger fan
raises the floor it has to clear; sites that measure their fan rise should put
their number in the sum. Both comparisons are strict. persist requires 30
continuous minutes, separating a leaking seat from a coil surrendering the hot
water standing in it after a call ends; recovery is immediate on the tick the
rise falls back inside the allowance or the valve opens, and delayOnInit = true
holds the window across a restart.
Possible Diagnoses
Transcribed from the reference’s FCU-0005 card:
- Heating coil valve leaking through — the worn or eroded seat; the common case, priced by the playbook at $150–$600 to replace
- Valve not fully closing (mechanical) — the actuator has lost its close position or binds short of the seat, distinguishable on site by stroking it against feedback
- Gravity circulation through the coil — hot water thermosiphoning up through a vertically piped coil with the valve genuinely shut. Nothing is broken and a new valve fixes nothing; the fix is a check valve or a piping change, and the playbook flags this specifically for multi-story buildings
A fourth belongs in the operator’s head though the reference does not list it: either sensor being wrong produces this trace with a perfectly good valve. A return sensor reading low and a discharge sensor reading high are indistinguishable here and both are cheap to check against a portable reference, which is why G36 §5.16.14 puts the two sensor errors ahead of the valve in its own diagnosis order.
Energy Impact
CRITICAL_WASTE, HIGH confidence, DIRECT_MEASUREMENT, 3–10% of zone heating
energy, mapped by the reference to PNNL EEM-03 (fix leaking valves).
DIRECT_MEASUREMENT is honest here: the two temperatures the rule reads are the
measurement, and the runtime formula converts them to thermal power with one
substitution — design airflow for measured, since an FCU has no flow station.
HIGH confidence because a sustained rise across a coil commanded shut has no
benign explanation other than a sensor. Cooling-dominant per the chapter, and the
reason also explains when the rule can see anything: heat leaking into a zone in
cooling season is paid for twice, and that is the season the heating valve is
commanded shut for weeks at a time — precisely the state yCmdOk requires. In
deep heating weather the same leak hides behind legitimate calls for heat.
Emissions Impact
Scope 1, DIRECT_EMISSIONS, HIGH confidence; typically 100–800 kg CO₂e/yr per unit of parasitic heating, MOER basis. Scope 1 is the reference’s assignment and it assumes a fuel-fired hot water plant; on a site whose hot water comes from an electric boiler or a heat pump the same leaked heat is Scope 2, and the cooling that removes it is Scope 2 either way, so an all-electric building reads the whole exchange as Scope 2. Where the cause turns out to be a sensor there is nothing to attribute at all.
Deviations
inactive_coil_thresholdis adopted, not transcribed. The reference names the parameter and publishes no tunables line for this card. 3.0 °C uses G36 §5.16.14’s composition — root-sum-square of the two sensor errors plus a fan-heat term — which at the ±1.4 °C-class sensors an FCU typically carries and 1 °C of fan rise gives 2.98. The AHU twin AHU-0015 ships 4.1623 for the same composition because it reads the coil through a mixed-air sensor G36 allows to be off by 3 °C, so the FCU threshold is legitimately tighter. A site with matched ±0.5 °C sensors and a measured 0.4 °C fan rise gets ≈ 1.1.- Fan heat is inside the measurement, and it is where this pair stops being symmetric. Neither the chapter nor the playbook mentions fan heat; the term comes from G36’s ΔTSF via AHU-0015. Here the measured rise is the true coil rise plus the fan’s, so 3.0 °C trips at about 2 °C of coil rise, while on the cooling-side twin the fan’s rise hides part of the drop and the same threshold needs about 4 °C — so the one number the reference asks for makes the heating rule roughly twice as sensitive. A host wanting matched sensitivity raises this threshold or lowers FCU-0004’s by one fan-heat term.
htg_vlv_cmd = 0%becomes< 1.0%. CDLRealshas no equality block, and equality on a float from a BAS would be the wrong test anyway — controllers write 0.0001% and round-tripped analog values land near but not on zero. 1.0% is tighter than AHU-0016’s 5% “open” threshold because that rule needs the valve to be doing something and this one needs it to be doing nothing.alarm_delayis adopted at 30 minutes. The reference publishes no delay here; FCU-0001’s 60 min belongs to a transition counter and says nothing about a coil. 1800 s is the AHU twin’s G36 AlarmDelay and it does identifiable work — riding out the residual heat a just-closed coil gives up. A site whose coils purge in five minutes can cut it and detect leaks sooner.- Strict comparisons at both boundaries. A rise sitting exactly on 3.0 °C is
not a fault and a command sitting exactly on 1.0% is not shut. The reference
writes
>for the temperature test; the command side is the adopted test above. Both err toward silence, and a host binding coarsely quantized temperatures should retune down. - The G36 clause is the chapter’s citation, carried forward unverified. The
library’s G36 material covers §5.16.14 (the AHU set) and not the FCU set, so
everything claimed as transcribed comes from the reference’s ch.12 card and the
g36field is provenance the reference asserts. If §5.22.6 states averaging windows, epsilons or an alarm delay, they will correct the adopted values above. ratandsatstand in for the coil entering and leaving temperatures, as the FCU point dictionary directs, so the rule sees the whole air path: the fan is inside the measurement (handled by the fan-heat term) and so is any duct after the coil (not handled, and it biases this rule slightly louder). Two configurations break the binding outright — aratbound to a wall-mounted space sensor, and a four-pipe unit taking ducted outdoor air upstream of the coils.- The fan-running gate remains host-side. The dictionary now defines
fan_cmdandfan_statusfor FCU-0006, but this graph does not consume them. A cycling-fan FCU evaluated between cycles is the realistic way to get a false alarm out of this rule. Gate on trusted fan proof and treat FCU-0006 fail-to-start as NO_EVAL here. yCmdOkis the library’s, not the reference’s. Exposing the command conjunct as a boundary output adds no logic and changes no verdict; it lets the host distinguish “the coil is shut and quiet” from “the coil is heating, ask me later”, which are the sameyFault = falseand mean opposite things. Same wiring as RTU-0002’syStageOkand AHU-0021’syTempDeltaOk.- Instantaneous samples instead of rolling averages. G36’s AHU set computes every signal as a 5-minute rolling average; whether §5.22.6 does is unknown here. Persistence is not equivalent — averaging tolerates a signal whose mean sits outside the bound while it keeps crossing back, so a valve hunting around its seat can hide indefinitely. A steady leak reads the same either way.
- A leak is only visible between calls for heat. The rule is silent whenever the valve is open, so a valve that leaks all winter is detected in spring and one on a unit in continuous heating is never detected at all. That is inherent in the reference’s equation, and it is the structural reason the chapter calls the fault cooling-dominant.
- Severity 3 is the reference’s, and it disagrees with the AHU twin. AHU-0014/AHU-0015 carry severity 2, assigned by this library because the AHU reference has no card to state one. The difference is defensible on scale and is recorded rather than smoothed, because a host ranking a mixed fleet by severity will see the same physics at two levels.
- Operating state OS#2 is host-enforced. The graph’s command test covers the
heating half of “no active coils”; the cooling half is not tested and does not
need to be, since an active cooling coil drives
satbelowratand can only silence this rule, never trip it. - The runtime formula is extended. To the chapter’s
(leaving_temp − entering_temp) × fcu_airflow × cp_airthis card adds air density (the product needs mass flow) and subtracts the fan’s rise, which is in the measured difference and is none of the coil’s doing — a third of the answer at the threshold. clustersis empty. The chapter README calls FCU-0004/FCU-0005 the zone-scale members of the simultaneous-conditioning family, butclusters/clusters.jsonlists only AHU rules under CLU-01 and this card does not edit the cluster set. The relationship is carried byrelatedand the playbook.persist.delayOnInit = true(Modelica/CDL default isfalse), the library’s standing choice: a leak already present when the controller restarts waits out the full 30 minutes instead of alarming on the first tick.- The reference publishes no vectors for this card, so
vectors.jsonis authored from the equation.
Notes
Read yCmdOk before reading yFault. On a unit in heating season it will be
false most of the day, and every yFault = false under it means “not
evaluated”, not “no leak”.
This card is one half of a pair the reference states symmetrically, and the
places where the pair is not symmetric are worth carrying into FCU-0004: the
sign of the subtraction (sat − rat here, rat − sat there), the direction fan
heat pushes the measurement (into this rule’s threshold, against that one’s),
and the emissions scope (Scope 1 here for a fuel-fired plant, Scope 2 there for
the chiller). Everything else is identical, because nothing in the chapter
distinguishes them.
The fcu-faults playbook orders the service.
Step 1.3 is the manual version of this rule — command the valve to 0% and
measure across the coil, where any measurable change confirms the leak — and
it is where the gravity-circulation check lives, the one diagnosis a new valve
will not fix. Step 2.3 is the remote workaround: lock the heating valve out for
the season, which on a fleet firing a dozen units at once is the difference
between a summer of parasitic heating and none. Step 3.4 ranks that fleet by
|temp_change| × airflow × cp_air, this card’s runtime estimator. Expect
FCU-0003 on the same unit in cooling weather — if both are active, this one is
the cause and that one the consequence.
Test Vectors
14 scenarios, clock step 60 s over 5400 s.
| Scenario | Description |
|---|---|
healthy_unit_shows_fan_heat_only | The pin this rule exists to keep clear. Valve commanded shut, and the air still leaves the unit 1.0 °C warmer than it entered because the fan sits in the airstream between the two sensors. Every healthy FCU in the fleet reads like this in OS#2; inactive_coil_threshold carries the fan-heat term precisely so it stays quiet. |
rise_just_below_threshold | Threshold edge from inside: sat − rat = 2.9 °C, one tenth under the allowance. Fan heat plus a pair of zone-grade sensors leaning the wrong way lands here, which is exactly what the allowance is sized to absorb. |
rise_exactly_at_threshold | Threshold edge, on the line: sat − rat = 3.0 °C exactly. riseBig is a strict > because CDL Reals has no GreaterEqual, so a rise sitting precisely on inactive_coil_threshold reads healthy. The reference writes > here too, so this side agrees with it (see Deviations). |
rise_just_over_threshold | Threshold edge from outside: 3.1 °C clears the strict comparison on the first tick, and with delayOnInit the alarm lands exactly one alarm_delay later, at t = 1800 s. Both sides of the delay edge are pinned at adjacent ticks. |
cmd_just_below_closed_threshold | Command edge from inside: 0.9% is below cmd_closed_threshold, so the valve counts as commanded shut and a 6 °C rise is a leak. This is the case a host sees when its BAS writes a small non-zero null value rather than a clean 0%. |
cmd_exactly_at_closed_threshold | Command edge, on the line: htg_vlv_cmd = 1.0% exactly. vlvShut is a strict <, so the valve does not count as shut and the same 6 °C rise raises nothing. yCmdOk goes false, which is the rule telling the host this is NO_EVAL rather than a clean bill of health. |
cmd_just_above_closed_threshold | Command edge from outside, pinning the third side: 1.1% with the same 6 °C rise. A valve cracked open at 1.1% is modulating, not shut, and the coil is allowed to be doing something. |
valve_open_and_heating_normally | The unit doing its job: valve at 60% and a 13 °C rise across the coil. This is the largest rise in the vector set and it is not a fault — the command is what separates parasitic heat from heat somebody asked for. |
gravity_circulation_overnight | Diagnosis 3, and the playbook’s multi-story warning: with the pump running and the valve shut, hot water thermosiphons up through a vertically piped coil and the discharge sits 5 °C above the return all night. Nothing about the valve is broken — a check valve is the fix — and this rule cannot tell that apart from a worn seat, which is why both are on the diagnosis list. |
leak_starts_mid_run | The rising edge is mid-run rather than at load: the unit runs on fan heat alone until t = 600 s, then the seat lets go and the rise jumps to 6 °C. TrueDelay measures from that edge, so the alarm lands at 600 + 1800 = 2400 s exactly. |
transient_clears_before_alarm_delay | Residual heat, not a leak: hot water standing in a just-closed coil gives up a 6 °C rise for 1200 s and then the unit settles back to fan heat. Anything shorter than alarm_delay never reaches the operator, which is most of what the 30 minutes buys. |
fault_clears_on_recovery | A sustained 6 °C rise alarms at 1800 s; the coil is isolated at t = 3000 s and the rise falls back to fan heat. TrueDelay has no off-delay, so yFault drops on that same tick. |
zone_calls_for_heat_before_alarm | Recovery through the command term instead of the temperature term: the rise never changes, but at t = 1200 s the zone calls and the valve is driven to 55%. The conjunction releases, the timer is discarded, and yCmdOk marks the rest of the run NO_EVAL. A leak that only ever shows itself between calls for heat is invisible to this rule — see Deviations. |
oscillating_rise_never_alarms | The known miss, pinned so it cannot change silently: the rise swings between 6 °C and 1 °C on a 20-minute period, so its mean is well outside the allowance while no single episode reaches alarm_delay. A rolling average would report this; persistence restarts on every compliant tick and stays silent (see Deviations). |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 60,
"horizon_s": 5400
},
"scenarios": [
{
"name": "healthy_unit_shows_fan_heat_only",
"description": "The pin this rule exists to keep clear. Valve commanded shut, and the air still leaves the unit 1.0 \u00b0C warmer than it entered because the fan sits in the airstream between the two sensors. Every healthy FCU in the fleet reads like this in OS#2; inactive_coil_threshold carries the fan-heat term precisely so it stays quiet.",
"inputs": {
"htg_vlv_cmd": 0.0,
"rat": 22.0,
"sat": 23.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "rise_just_below_threshold",
"description": "Threshold edge from inside: sat \u2212 rat = 2.9 \u00b0C, one tenth under the allowance. Fan heat plus a pair of zone-grade sensors leaning the wrong way lands here, which is exactly what the allowance is sized to absorb.",
"inputs": {
"htg_vlv_cmd": 0.0,
"rat": 22.0,
"sat": 24.9
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "rise_exactly_at_threshold",
"description": "Threshold edge, on the line: sat \u2212 rat = 3.0 \u00b0C exactly. `riseBig` is a strict `>` because CDL Reals has no GreaterEqual, so a rise sitting precisely on inactive_coil_threshold reads healthy. The reference writes `>` here too, so this side agrees with it (see Deviations).",
"inputs": {
"htg_vlv_cmd": 0.0,
"rat": 22.0,
"sat": 25.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "rise_just_over_threshold",
"description": "Threshold edge from outside: 3.1 \u00b0C clears the strict comparison on the first tick, and with delayOnInit the alarm lands exactly one alarm_delay later, at t = 1800 s. Both sides of the delay edge are pinned at adjacent ticks.",
"inputs": {
"htg_vlv_cmd": 0.0,
"rat": 22.0,
"sat": 25.1
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1740,
"equals": false
},
{
"output": "yFault",
"from_s": 1800,
"to_s": 5400,
"equals": true
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "cmd_just_below_closed_threshold",
"description": "Command edge from inside: 0.9% is below cmd_closed_threshold, so the valve counts as commanded shut and a 6 \u00b0C rise is a leak. This is the case a host sees when its BAS writes a small non-zero null value rather than a clean 0%.",
"inputs": {
"htg_vlv_cmd": 0.9,
"rat": 22.0,
"sat": 28.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1740,
"equals": false
},
{
"output": "yFault",
"from_s": 1800,
"to_s": 5400,
"equals": true
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "cmd_exactly_at_closed_threshold",
"description": "Command edge, on the line: htg_vlv_cmd = 1.0% exactly. `vlvShut` is a strict `<`, so the valve does not count as shut and the same 6 \u00b0C rise raises nothing. yCmdOk goes false, which is the rule telling the host this is NO_EVAL rather than a clean bill of health.",
"inputs": {
"htg_vlv_cmd": 1.0,
"rat": 22.0,
"sat": 28.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": false
}
]
},
{
"name": "cmd_just_above_closed_threshold",
"description": "Command edge from outside, pinning the third side: 1.1% with the same 6 \u00b0C rise. A valve cracked open at 1.1% is modulating, not shut, and the coil is allowed to be doing something.",
"inputs": {
"htg_vlv_cmd": 1.1,
"rat": 22.0,
"sat": 28.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": false
}
]
},
{
"name": "valve_open_and_heating_normally",
"description": "The unit doing its job: valve at 60% and a 13 \u00b0C rise across the coil. This is the largest rise in the vector set and it is not a fault \u2014 the command is what separates parasitic heat from heat somebody asked for.",
"inputs": {
"htg_vlv_cmd": 60.0,
"rat": 22.0,
"sat": 35.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": false
}
]
},
{
"name": "gravity_circulation_overnight",
"description": "Diagnosis 3, and the playbook's multi-story warning: with the pump running and the valve shut, hot water thermosiphons up through a vertically piped coil and the discharge sits 5 \u00b0C above the return all night. Nothing about the valve is broken \u2014 a check valve is the fix \u2014 and this rule cannot tell that apart from a worn seat, which is why both are on the diagnosis list.",
"inputs": {
"htg_vlv_cmd": 0.0,
"rat": 21.0,
"sat": 26.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1740,
"equals": false
},
{
"output": "yFault",
"from_s": 1800,
"to_s": 5400,
"equals": true
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "leak_starts_mid_run",
"description": "The rising edge is mid-run rather than at load: the unit runs on fan heat alone until t = 600 s, then the seat lets go and the rise jumps to 6 \u00b0C. TrueDelay measures from that edge, so the alarm lands at 600 + 1800 = 2400 s exactly.",
"inputs": {
"htg_vlv_cmd": 0.0,
"rat": 22.0,
"sat": [
{
"t": 0,
"value": 23.0
},
{
"t": 600,
"value": 28.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2340,
"equals": false
},
{
"output": "yFault",
"from_s": 2400,
"to_s": 5400,
"equals": true
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "transient_clears_before_alarm_delay",
"description": "Residual heat, not a leak: hot water standing in a just-closed coil gives up a 6 \u00b0C rise for 1200 s and then the unit settles back to fan heat. Anything shorter than alarm_delay never reaches the operator, which is most of what the 30 minutes buys.",
"inputs": {
"htg_vlv_cmd": 0.0,
"rat": 22.0,
"sat": [
{
"t": 0,
"value": 28.0
},
{
"t": 1200,
"value": 23.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "fault_clears_on_recovery",
"description": "A sustained 6 \u00b0C rise alarms at 1800 s; the coil is isolated at t = 3000 s and the rise falls back to fan heat. TrueDelay has no off-delay, so yFault drops on that same tick.",
"inputs": {
"htg_vlv_cmd": 0.0,
"rat": 22.0,
"sat": [
{
"t": 0,
"value": 28.0
},
{
"t": 3000,
"value": 23.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1740,
"equals": false
},
{
"output": "yFault",
"from_s": 1800,
"to_s": 2940,
"equals": true
},
{
"output": "yFault",
"from_s": 3000,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "zone_calls_for_heat_before_alarm",
"description": "Recovery through the command term instead of the temperature term: the rise never changes, but at t = 1200 s the zone calls and the valve is driven to 55%. The conjunction releases, the timer is discarded, and yCmdOk marks the rest of the run NO_EVAL. A leak that only ever shows itself between calls for heat is invisible to this rule \u2014 see Deviations.",
"inputs": {
"rat": 22.0,
"sat": 28.0,
"htg_vlv_cmd": [
{
"t": 0,
"value": 0.0
},
{
"t": 1200,
"value": 55.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 1140,
"equals": true
},
{
"output": "yCmdOk",
"from_s": 1200,
"to_s": 5400,
"equals": false
}
]
},
{
"name": "oscillating_rise_never_alarms",
"description": "The known miss, pinned so it cannot change silently: the rise swings between 6 \u00b0C and 1 \u00b0C on a 20-minute period, so its mean is well outside the allowance while no single episode reaches alarm_delay. A rolling average would report this; persistence restarts on every compliant tick and stays silent (see Deviations).",
"inputs": {
"htg_vlv_cmd": 0.0,
"rat": 22.0,
"sat": [
{
"t": 0,
"value": 28.0
},
{
"t": 600,
"value": 23.0
},
{
"t": 1200,
"value": 28.0
},
{
"t": 1800,
"value": 23.0
},
{
"t": 2400,
"value": 28.0
},
{
"t": 3000,
"value": 23.0
},
{
"t": 3600,
"value": 28.0
},
{
"t": 4200,
"value": 23.0
},
{
"t": 4800,
"value": 28.0
},
{
"t": 5400,
"value": 23.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
}
]
}