Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

AHU-0027 — Manual override detected on critical control point

Statusverified — engine e2ff2f8, cxf:fnv1a128:3104e499f03189fbd6a10f3c3a9e1243, 2026-08-17
Severity4
Methodrule
Phase2
CategoryEXCESS_CONSUMPTION
ConfidenceLOW
EstimationQUALITATIVE_ONLY
G36
Clusters
Suppresses
Suppressed by
RelatedAHU-0018
Playbooksafter-hours-operation
SourceHVAC FDD Reference v1.0 §9, AHU-0027; PNNL RetuningOpps A30; PNNL-27338
Operating statesall

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:

NameDefaultUnitCXF pathDescription
max_override_duration86400.0sstaleOvr.delayTimeContinuous override duration beyond which the override counts as stale (24 h)
alarm_delay3600.0spersist.delayTimeFurther 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):

AHU-0027 block graph

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

  1. Temporary override forgotten by the operator after troubleshooting
  2. Override set during commissioning and never removed — often predating the current operations staff
  3. 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_duration point is dropped: the duration is measured in-rule with a TrueDelay chain, 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 = true an 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 AlarmDelay is separate from max_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_active from 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.

ScenarioDescription
no_overrideReference vector: no override anywhere in the 48 h window
override_two_hoursReference vector: a 2 h override — normal troubleshooting, well inside max_override_duration
override_forty_eight_hoursReference vector: override held continuously; alarms at max_override_duration (86400 s) + alarm_delay (3600 s) = 90000 s
release_just_before_alarmTiming edge: override released at 88800 s — past max_override_duration but 1200 s short of the alarm; never asserts
release_resets_durationTransient: 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_durationTransient: 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
        }
      ]
    }
  ]
}