AHU-0027 — Manual override detected on critical control point
| Status | verified — engine e2ff2f8, cxf:fnv1a128:3104e499f03189fbd6a10f3c3a9e1243, 2026-08-17 |
| Severity | 4 |
| Method | rule |
| Phase | 2 |
| Category | EXCESS_CONSUMPTION |
| Confidence | LOW |
| Estimation | QUALITATIVE_ONLY |
| G36 | — |
| Clusters | — |
| Suppresses | — |
| Suppressed by | — |
| Related | AHU-0018 |
| Playbooks | after-hours-operation |
| Source | HVAC FDD Reference v1.0 §9, AHU-0027; PNNL RetuningOpps A30; PNNL-27338 |
| Operating states | all |
Preconditions (host-enforced): override_active is mapped from the BAS — a BACnet priority-array occupancy at priorities 1-8, or the vendor’s operator-override flag — on a control point that matters. The host must ensure the flag reflects operator or manual overrides only; automation modes (optimal start, demand response, smoke control) writing at their own priorities are not overrides and must not raise it.
Points: override_active
Outputs:
yFault— True once override_active has been held continuously for max_override_duration plus alarm_delay
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
max_override_duration | 86400.0 | s | staleOvr.delayTime | Continuous override duration beyond which the override counts as stale (24 h) |
alarm_delay | 3600.0 | s | persist.delayTime | Further persistence required after the duration limit before the alarm asserts |
Description
An override has been active on a control point for longer than any legitimate troubleshooting session. Overrides are a normal service tool — hold the valve open, watch the response, release it — but the release is the step that gets skipped. What was a ten-minute diagnostic becomes the building’s permanent control strategy, invisible to anyone reading the sequence, and it stays that way until someone audits the priority arrays. Around 10% of buildings carry at least one; they are a standard retro-commissioning finding (PNNL RetuningOpps A30, PNNL-27338). The cost is entirely a function of what was overridden — a heating valve held at 100% is expensive, a nuisance alarm limit is not — hence info severity. The value of the rule is that it turns an invisible condition into a work order.
Detection Logic
yFault = override_active
sustained continuously for max_override_duration
and then held a further alarm_delay
Block graph (rule.cxf.jsonld):
Two delays in series measure the duration rather than reading it: staleOvr
turns true only after override_active has been continuously true for
max_override_duration (24 h), and persist requires a further alarm_delay
(1 h) before yFault asserts — 25 h total for an override that is never
released. Any release, however brief, drops both timers to false and discards
the accumulated duration, so the alarm only ever describes one continuous
override. An override released and re-applied restarts the full 25 h.
Possible Diagnoses
- Temporary override forgotten by the operator after troubleshooting
- Override set during commissioning and never removed — often predating the current operations staff
- Override set to mask an underlying equipment problem (a failed sensor, a stuck actuator), in which case releasing it will expose the real fault
Energy Impact
EXCESS_CONSUMPTION, LOW confidence, QUALITATIVE_ONLY. There is no per-fault energy model: the waste depends on which point is overridden and to what value, spanning from zero (an override matching what the sequence would command) to the full cost of a disabled control loop. Quantify it after identifying the point, using the Energy Impact Reference §4.4 framework. Prevalence ~10% of buildings (PNNL RetuningOpps A30); no PNNL EEM maps to this fault.
Emissions Impact
Scope 1 or 2, QUALITATIVE_EMISSIONS, LOW confidence, basis N/A. Which scope applies follows the overridden point: a hot-water valve, boiler enable, or gas-fired stage drives on-site combustion (scope 1); a fan, chiller, pump, or electric-heat command drives purchased electricity (scope 2); a setpoint or damper can drive both. No magnitude range until the point is known.
Deviations
- The reference’s host-computed
override_durationpoint is dropped: the duration is measured in-rule with aTrueDelaychain, so the rule deploys against a raw BAS flag with no host-side state machine and no derived point whose reset semantics would have to be specified and trusted. What is implemented — continuous override, any release resets — matches the reference’s intent for a stale override. - The trade-off is that the duration does not survive a controller restart:
with
delayOnInit = truean override already active for a week re-times from the restart and alarms 25 h later. That is the conservative direction; a host keeping its own override history can raise the fault sooner. - Two delays instead of one: the reference’s
AlarmDelayis separate frommax_override_duration, so we keep both parameters independently tunable even though a single 90000 s delay would behave identically at the default values. A site that wants a 4 h override limit changes one parameter. - The reference tags this fault for AHU, RTU, VAV, CHW, and HW. This is the
AHU-family instance; the block graph is equipment-agnostic and each sibling
family binds
override_activefrom its own point dictionary.
Notes
Step 2 of the after-hours-operation playbook — release stuck overrides in the BACnet priority array — is this fault’s remote fix verbatim, which is why it shares that playbook. The two also co-occur: AHU-0018 treats an active override as justification for after-hours fan operation, so a forgotten override silences FC-052 while this rule flags the reason it is silent.
Take diagnosis 3 seriously before releasing anything. If the override was masking a failed sensor or a stuck actuator, releasing it puts the equipment back under a control loop that cannot work — trend the affected point for a shift after release.
Test Vectors
6 scenarios, clock step 300 s over 172800 s.
| Scenario | Description |
|---|---|
no_override | Reference vector: no override anywhere in the 48 h window |
override_two_hours | Reference vector: a 2 h override — normal troubleshooting, well inside max_override_duration |
override_forty_eight_hours | Reference vector: override held continuously; alarms at max_override_duration (86400 s) + alarm_delay (3600 s) = 90000 s |
release_just_before_alarm | Timing edge: override released at 88800 s — past max_override_duration but 1200 s short of the alarm; never asserts |
release_resets_duration | Transient: a 20 h override released at 72000 s; the elapsed duration is discarded, so the remaining 28 h of the window stay clear |
reoverride_restarts_duration | Transient: released at 72000 s then re-applied at 75600 s; the clock restarts from the re-apply, alarming at 165600 s rather than 90000 s |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 300,
"horizon_s": 172800
},
"scenarios": [
{
"name": "no_override",
"description": "Reference vector: no override anywhere in the 48 h window",
"inputs": {
"override_active": false
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 172800,
"equals": false
}
]
},
{
"name": "override_two_hours",
"description": "Reference vector: a 2 h override \u2014 normal troubleshooting, well inside max_override_duration",
"inputs": {
"override_active": [
{
"t": 0,
"value": true
},
{
"t": 7200,
"value": false
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 172800,
"equals": false
}
]
},
{
"name": "override_forty_eight_hours",
"description": "Reference vector: override held continuously; alarms at max_override_duration (86400 s) + alarm_delay (3600 s) = 90000 s",
"inputs": {
"override_active": true
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 89700,
"equals": false
},
{
"output": "yFault",
"from_s": 90300,
"to_s": 172800,
"equals": true
}
]
},
{
"name": "release_just_before_alarm",
"description": "Timing edge: override released at 88800 s \u2014 past max_override_duration but 1200 s short of the alarm; never asserts",
"inputs": {
"override_active": [
{
"t": 0,
"value": true
},
{
"t": 88800,
"value": false
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 172800,
"equals": false
}
]
},
{
"name": "release_resets_duration",
"description": "Transient: a 20 h override released at 72000 s; the elapsed duration is discarded, so the remaining 28 h of the window stay clear",
"inputs": {
"override_active": [
{
"t": 0,
"value": true
},
{
"t": 72000,
"value": false
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 172800,
"equals": false
}
]
},
{
"name": "reoverride_restarts_duration",
"description": "Transient: released at 72000 s then re-applied at 75600 s; the clock restarts from the re-apply, alarming at 165600 s rather than 90000 s",
"inputs": {
"override_active": [
{
"t": 0,
"value": true
},
{
"t": 72000,
"value": false
},
{
"t": 75600,
"value": true
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 165300,
"equals": false
},
{
"output": "yFault",
"from_s": 165900,
"to_s": 172800,
"equals": true
}
]
}
]
}