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-0018 — Unoccupied override — equipment running during off-hours

Statusverified — engine e2ff2f8, cxf:fnv1a128:964ec962a0c3d75681dcc1c4e0f6c66b, 2026-08-17
Severity3
Methodrule
Phase1
CategoryCRITICAL_WASTE
ConfidenceHIGH
EstimationDIRECT_MEASUREMENT
G36
ClustersCLU-04, CLU-08
Suppresses
Suppressed by
RelatedSYS-0003, SYS-0004, AHU-0026
Playbooksafter-hours-operation
SourceHVAC FDD Reference v1.0 §9, AHU-0018; PNNL-27338 §2; Mazzetto 2025
Operating statesall

Preconditions (host-enforced): Occupancy schedule data available and current; the host evaluates the schedule (time zone, calendar, holidays) into the boolean occ_schedule point. When schedule provenance is unknown or stale, the verdict is NO_EVAL, not healthy.

Points: sf_status, occ_schedule, override_active

Outputs:

  • yFault — True while the supply fan has been running unoccupied, past the grace period, with no active override, for at least alarm_delay

Parameters:

NameDefaultUnitCXF pathDescription
grace_period1800.0sunoccGrace.delayTimeTime after the occupied period ends before unoccupied operation counts against the rule
alarm_delay900.0spersist.delayTimeContinuous fault persistence required before the alarm asserts

Description

The supply fan is operating outside the defined occupancy schedule when no active override or demand condition justifies it. While this fault is active, essentially the entire AHU energy draw — fan power plus any heating or cooling of outdoor air — is waste. Roughly 15% of buildings exhibit it, usually from schedule misconfiguration or a stuck override, and it is the trigger rule for both the After-Hours Operation (CLU-04) and Schedule Dysfunction (CLU-08) clusters. It is also among the fastest faults to pay back: the fix is almost always a $0 schedule or override correction.

Detection Logic

yFault = sf_status
     AND (NOT occ_schedule  sustained for grace_period)
     AND NOT override_active
     sustained continuously for alarm_delay

Block graph (rule.cxf.jsonld):

AHU-0018 block graph

unoccGrace implements the reference’s grace period: the unoccupied state must persist for grace_period before it counts, so normal operation continuing briefly past schedule end never alarms. persist then requires the full justification-free condition (fan on · unoccupied past grace · no override) to hold for alarm_delay. Any re-occupancy, fan stop, or override activation resets the corresponding timer. Worst-case time to alarm after schedule end is grace_period + alarm_delay (default 45 min).

Possible Diagnoses

  1. Schedule misconfiguration or incorrect time zone (DST mismatches are a common culprit)
  2. Stuck override in the BAS (BACnet priority array holding the fan on)
  3. Fan relay or contactor stuck closed
  4. Occupancy sensor triggering unnecessarily
  5. Night setback / morning warmup running too long

Energy Impact

CRITICAL_WASTE, HIGH confidence, DIRECT_MEASUREMENT. While active, the entire AHU draw is waste: waste_kw = ahu_fan_design_kw × (sf_speed/100)³ + active heating/cooling (cubed fan law on speed; thermal penalty typically 1.5–3× the fan energy). Savings range 3–16% of site energy (PNNL-25985 EEM-04 shortened schedules / EEM-16 night setbacks). Climate-neutral: waste scales with operating hours, not weather. Prevalence ~15%.

Emissions Impact

Scope 1 + 2, DIRECT_EMISSIONS, HIGH confidence; typical 3,000–20,000 kg CO₂e/yr (full AHU energy during unoccupied hours). After-hours waste lands in evening/overnight hours when the marginal grid generator is often coal or gas peaking — in solar-heavy regions the nighttime MOER can be 2–3× the midday value, so this fault’s emissions rank can exceed its energy-cost rank. Avoided-emissions basis: MOER.

Deviations

  • The reference’s logic calls in_occupied_schedule(current_time, occ_schedule) — a schedule-evaluation function over a schedule object. Our rule consumes the host-evaluated boolean occ_schedule point instead; schedule interpretation (time zone, calendar, holiday exceptions) is a host concern, consistent with this library’s precondition philosophy and with 223P/Brick having no schedule vocabulary to describe one (see points/ahu.points.json notes).
  • The reference lists grace_period as “minutes after schedule end to allow.” We implement it as a TrueDelay on the unoccupied signal, which grants the same grace after schedule end and equally after any occupied→unoccupied transition — equivalent for schedule-driven hosts, and safer for hosts that drive occ_schedule from occupancy sensing.
  • delayOnInit = true on both timers: a controller restart mid-condition still waits out the full grace + persistence window (same startup-alarm rationale as AHU-0016).

Notes

Remote fix succeeds ~95% of the time at $0 (schedule correction, override release, setback enablement). When this rule fires, check SYS-0003 (lighting) and SYS-0004 (exhaust fans) — they frequently share the same master-schedule root cause, which is why this rule triggers two clusters.

Test Vectors

6 scenarios, clock step 60 s over 3600 s.

ScenarioDescription
normal_occupiedReference vector: fan on during scheduled occupancy
normal_unoccupiedReference vector: fan off while unoccupied
running_after_hoursReference vector: fan on, unoccupied, no override; alarms after grace_period (1800 s) + alarm_delay (900 s)
override_justifies_runReference vector: fan on, unoccupied, but an active override justifies it
fan_stops_within_graceFan shuts down before the grace period elapses; never alarms
reoccupied_resets_persistenceOccupancy resumes after grace elapsed but before alarm_delay completes; timers reset, never alarms
vectors.json
{
  "schema": "cxf-library/vectors/v1",
  "clock": {
    "step_s": 60,
    "horizon_s": 3600
  },
  "scenarios": [
    {
      "name": "normal_occupied",
      "description": "Reference vector: fan on during scheduled occupancy",
      "inputs": {
        "sf_status": true,
        "occ_schedule": true,
        "override_active": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "normal_unoccupied",
      "description": "Reference vector: fan off while unoccupied",
      "inputs": {
        "sf_status": false,
        "occ_schedule": false,
        "override_active": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "running_after_hours",
      "description": "Reference vector: fan on, unoccupied, no override; alarms after grace_period (1800 s) + alarm_delay (900 s)",
      "inputs": {
        "sf_status": true,
        "occ_schedule": false,
        "override_active": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2640,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2760,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "override_justifies_run",
      "description": "Reference vector: fan on, unoccupied, but an active override justifies it",
      "inputs": {
        "sf_status": true,
        "occ_schedule": false,
        "override_active": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "fan_stops_within_grace",
      "description": "Fan shuts down before the grace period elapses; never alarms",
      "inputs": {
        "sf_status": [
          {
            "t": 0,
            "value": true
          },
          {
            "t": 1500,
            "value": false
          }
        ],
        "occ_schedule": false,
        "override_active": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "reoccupied_resets_persistence",
      "description": "Occupancy resumes after grace elapsed but before alarm_delay completes; timers reset, never alarms",
      "inputs": {
        "sf_status": true,
        "occ_schedule": [
          {
            "t": 0,
            "value": false
          },
          {
            "t": 2000,
            "value": true
          }
        ],
        "override_active": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    }
  ]
}