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

ERV-0003 — Frost protection active above release conditions

Statusverified — engine e2ff2f8, cxf:fnv1a128:2d84409ba6461e88a64910d5f847870c, 2026-08-20
Severity3
Methodrule
Phase2
CategoryEXCESS_CONSUMPTION
ConfidenceMEDIUM
EstimationQUALITATIVE_ONLY
G36
Clusters
Suppresses
Suppressed by
RelatedERV-0001, ERV-0002
Playbookserv-effectiveness
SourceLibrary-authored complement to HVAC FDD Reference v1.0 §15 ERV-0002: that rule detects frost protection missing in cold weather; this rule detects the same reported sequence state persisting after warm-weather release should have occurred; Library precedent: ERV-0002 (oat / erv_frost_prot / erv_enabled point contract and strict frost-state watchdog), ERV-0001 (recovery lost intentionally during frost protection), and the repository’s 900 s transient-rejection convention; points/erv.points.json oat, erv_frost_prot, and erv_enabled — the binding distinction between an active sequence state and a permissive/advisory flag; PNNL-19004 p.55 and DOE/NREL Ventilation Integrated Comfort System report pp.28-29 — public examples whose frost controls use exhaust-leaving/core-entering conditions rather than one portable OAT release threshold, supporting site configuration rather than transcription; Greenheck ERV controller IOM 484118, p.4 — manufacturer example combining a device-specific OAT permissive with wheel differential pressure; evidence that technology/controller logic varies, not support for the shipped +5 °C
Operating statesERV enabled and expected to recover energy, with both air streams moving and the installed frost sequence capable of releasing under warm conditions

Preconditions (host-enforced): erv_frost_prot must be the actual active state of the installed frost sequence, not a frost-enable permissive, alarm, or low-temperature advisory. release_oat must be commissioned from that unit’s own release logic and must remain above ERV-0002’s engagement threshold; the shipped +5 °C is only an executable starting point. OAT must be valid and representative of the ERV intake. The host must verify both air streams are moving and exclude manual frost tests, commissioning, smoke/purge modes, maintenance overrides, and technology-specific recovery modes that legitimately retain frost protection above the configured line. A unit with no frost sequence or no observable active state is not deployable for this rule.

Points: oat, erv_frost_prot, erv_enabled

Outputs:

  • yFault — True while the enabled ERV has reported frost protection active above release_oat continuously for sustained_duration

Parameters:

NameDefaultUnitCXF pathDescription
release_oat5.0°CaboveRelease.tADOPTED_TUNABLE: outdoor temperature above which continued frost protection is suspicious. Configure from the installed sequence’s actual release point; +5 °C is not a universal frost boundary and deliberately leaves a neutral band above ERV-0002’s -10 °C engagement default.
sustained_duration900.0spersist.delayTimeADOPTED_TUNABLE: continuous warm-weather frost state required before alarm (15 min). Long enough to reject ordinary release and sensor-filter lag; raise it where the manufacturer’s sequence completes a longer defrost or recovery transition.

Description

Frost protection is supposed to trade recovery efficiency for equipment safety only while icing is credible. A preheat stage, bypass, wheel slowdown, or airflow-unbalance strategy that stays active in mild weather continues paying that trade after its benefit has disappeared. This rule watches the sequence’s reported active state against a commissioned outdoor-air release boundary. It does not judge how the unit protects itself or redesign its frost sequence.

Detection Logic

above_release = oat > release_oat
candidate     = erv_enabled AND erv_frost_prot AND above_release

yFault = candidate sustained continuously for sustained_duration

Block graph (rule.cxf.jsonld):

ERV-0003 block graph

Both the temperature comparison and timer are strict in their own ways: OAT at exactly release_oat is clear, and the alarm appears only after the full continuous duration. delayOnInit = true serves that duration after a restart. Any release of frost mode, OAT return to the boundary, or ERV disable clears the alarm and discards elapsed time immediately.

Possible Diagnoses

  1. Frost-mode software latch, timer, or state machine failed to release
  2. OAT sensor biased high (false alarm), biased low (missed finding), stale, or installed where it does not represent the ERV intake
  3. Preheat valve/relay, bypass damper, or wheel-speed command left overridden
  4. BAS point bound to a frost permissive rather than the sequence’s active state
  5. release_oat configured below the installed sequence’s true release point

Energy Impact

EXCESS_CONSUMPTION, MEDIUM confidence, QUALITATIVE_ONLY. The cost depends on the frost technology: preheat can consume fuel or electricity, bypass/wheel slowdown hands ventilation load back to downstream coils, and airflow imbalance adds fan and envelope load. The rule measures duration but no power or recovered heat.

Emissions Impact

Scope 1 + 2, QUALITATIVE_EMISSIONS. Electric fan/preheat and cooling effects are scope 2; fuel-fired preheat or downstream heat is scope 1. Quantification needs the host’s power, airflow, and temperature measurements rather than this state flag alone.

Deviations

  • Library-authored complement, not a transcribed reference card. The HVAC FDD Reference publishes ERV-0002’s missing-protection direction; this card mirrors its point contract for the opposite operational failure.
  • release_oat = 5 °C is ADOPTED_TUNABLE. It is intentionally distinct from ERV-0002’s -10 °C engagement default, leaving a 15 K neutral band in which neither rule asserts. The installed sequence remains authoritative.
  • Public examples do not establish a generic OAT release line. PNNL-19004 controls an exhaust-leaving temperature and DOE’s VICS prototype tempers core-entering air; Greenheck combines its own permissive with wheel pressure.
  • The roadmap classified 900 s as precedent; this card classifies it as ADOPTED_TUNABLE. The library uses 15-minute rejection windows, but no cited source establishes that duration for every frost technology.
  • The optional release_margin is omitted. With no configured release input, a zero-default margin duplicates release_oat without adding behavior.
  • erv_enabled remains in-graph. ERV-0001/0002 already use this boundary point to avoid nightly raw alarms; fan proof, tests, and overrides remain host preconditions.
  • No suppression or cluster. Excessive frost protection can genuinely cause low effectiveness, so ERV-0001 remains useful; shared remediation is carried by the playbook rather than a new taxonomy entry.
  • No empirical validation claim. Required synthetic vectors ran; the current EnergyPlus harness has no defensible frost-state mapping for this PR.

Notes

Confirm the point meaning before tuning the threshold. A flag that means “frost protection available” rather than “frost protection active” will hold this rule on all year and no temperature adjustment will fix the binding.

Test Vectors

11 scenarios, clock step 30 s over 2400 s.

ScenarioDescription
disabled_unit_holds_the_alarm_downThe raw graph receives a warm OAT and an active frost flag, but erv_enabled is false. This pins the in-graph enable gate; maintenance and test exclusions remain host-side.
frost_protection_below_release_is_healthyProtection is active at 0 C, below the adopted +5 C release line, so the sequence may legitimately remain in frost mode.
oat_exactly_at_release_is_healthyGreaterThreshold is strict: OAT exactly at release_oat does not start persistence.
warm_frost_mode_asserts_after_the_delayOAT is just above the release line from initialization. The assertions leave one 30 s step unbound at the 900 s timer edge, as required by the vector contract.
warm_excursion_just_short_of_delay_stays_clearThe warm interval lasts 870 s on the 30 s grid, one sample short of persistence, then OAT returns to the safe side.
warm_excursion_asserts_after_crossingThe candidate begins at t=300. The true assertion starts one 30 s step after the 900 s timer edge.
frost_flag_clear_restarts_persistenceThe frost flag clears for 300 s during a warm spell, resetting the clock; the true assertion begins one step after the second interval’s timer edge.
repeated_short_warm_excursions_do_not_accumulateThree 300 s warm excursions are separated by boundary crossings. TrueDelay discards rather than accumulates their time.
sequence_release_clears_an_active_alarmA standing warm-weather alarm clears on the same tick the frost sequence releases.
oat_returns_to_release_one_step_before_maturityOAT clears at t=870, one 30 s step before the 900 s timer edge, so the alarm never matures.
frost_releases_two_steps_after_maturityThe violation remains active two steps beyond the 900 s edge. Assertions skip the boundary tick, prove the alarm one step later, and prove immediate recovery at t=960.
vectors.json
{
  "schema": "cxf-library/vectors/v1",
  "clock": {
    "step_s": 30,
    "horizon_s": 2400
  },
  "scenarios": [
    {
      "name": "disabled_unit_holds_the_alarm_down",
      "description": "The raw graph receives a warm OAT and an active frost flag, but erv_enabled is false. This pins the in-graph enable gate; maintenance and test exclusions remain host-side.",
      "inputs": {
        "oat": 10.0,
        "erv_frost_prot": true,
        "erv_enabled": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "frost_protection_below_release_is_healthy",
      "description": "Protection is active at 0 C, below the adopted +5 C release line, so the sequence may legitimately remain in frost mode.",
      "inputs": {
        "oat": 0.0,
        "erv_frost_prot": true,
        "erv_enabled": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "oat_exactly_at_release_is_healthy",
      "description": "GreaterThreshold is strict: OAT exactly at release_oat does not start persistence.",
      "inputs": {
        "oat": 5.0,
        "erv_frost_prot": true,
        "erv_enabled": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "warm_frost_mode_asserts_after_the_delay",
      "description": "OAT is just above the release line from initialization. The assertions leave one 30 s step unbound at the 900 s timer edge, as required by the vector contract.",
      "inputs": {
        "oat": 5.1,
        "erv_frost_prot": true,
        "erv_enabled": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 870,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 930,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "warm_excursion_just_short_of_delay_stays_clear",
      "description": "The warm interval lasts 870 s on the 30 s grid, one sample short of persistence, then OAT returns to the safe side.",
      "inputs": {
        "oat": [
          {
            "t": 0,
            "value": 0.0
          },
          {
            "t": 300,
            "value": 5.1
          },
          {
            "t": 1170,
            "value": 5.0
          }
        ],
        "erv_frost_prot": true,
        "erv_enabled": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "warm_excursion_asserts_after_crossing",
      "description": "The candidate begins at t=300. The true assertion starts one 30 s step after the 900 s timer edge.",
      "inputs": {
        "oat": [
          {
            "t": 0,
            "value": 0.0
          },
          {
            "t": 300,
            "value": 5.1
          }
        ],
        "erv_frost_prot": true,
        "erv_enabled": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1170,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 1230,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "frost_flag_clear_restarts_persistence",
      "description": "The frost flag clears for 300 s during a warm spell, resetting the clock; the true assertion begins one step after the second interval's timer edge.",
      "inputs": {
        "oat": 10.0,
        "erv_frost_prot": [
          {
            "t": 0,
            "value": false
          },
          {
            "t": 300,
            "value": true
          },
          {
            "t": 900,
            "value": false
          },
          {
            "t": 1200,
            "value": true
          }
        ],
        "erv_enabled": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2070,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2130,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "repeated_short_warm_excursions_do_not_accumulate",
      "description": "Three 300 s warm excursions are separated by boundary crossings. TrueDelay discards rather than accumulates their time.",
      "inputs": {
        "oat": [
          {
            "t": 0,
            "value": 0.0
          },
          {
            "t": 300,
            "value": 10.0
          },
          {
            "t": 600,
            "value": 0.0
          },
          {
            "t": 900,
            "value": 10.0
          },
          {
            "t": 1200,
            "value": 0.0
          },
          {
            "t": 1500,
            "value": 10.0
          },
          {
            "t": 1800,
            "value": 0.0
          }
        ],
        "erv_frost_prot": true,
        "erv_enabled": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "sequence_release_clears_an_active_alarm",
      "description": "A standing warm-weather alarm clears on the same tick the frost sequence releases.",
      "inputs": {
        "oat": 10.0,
        "erv_frost_prot": [
          {
            "t": 0,
            "value": true
          },
          {
            "t": 1200,
            "value": false
          }
        ],
        "erv_enabled": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 870,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 930,
          "to_s": 1170,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 1200,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "oat_returns_to_release_one_step_before_maturity",
      "description": "OAT clears at t=870, one 30 s step before the 900 s timer edge, so the alarm never matures.",
      "inputs": {
        "oat": [
          {
            "t": 0,
            "value": 5.1
          },
          {
            "t": 870,
            "value": 5.0
          }
        ],
        "erv_frost_prot": true,
        "erv_enabled": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "frost_releases_two_steps_after_maturity",
      "description": "The violation remains active two steps beyond the 900 s edge. Assertions skip the boundary tick, prove the alarm one step later, and prove immediate recovery at t=960.",
      "inputs": {
        "oat": 5.1,
        "erv_frost_prot": [
          {
            "t": 0,
            "value": true
          },
          {
            "t": 960,
            "value": false
          }
        ],
        "erv_enabled": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 870,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 930,
          "to_s": 930,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 960,
          "to_s": 2400,
          "equals": false
        }
      ]
    }
  ]
}