Ventilation verification — ASHRAE 62.1 VRP & DCV
camber.ventilation does the explicit code-rate ventilation check that complements the CO₂
proxy in camber.iaq and the OA-fraction diagnostic in camber.oafraction.
- Ventilation Rate Procedure (VRP) — is the delivered outdoor air at least the ASHRAE 62.1 requirement for the zone?
- Demand-Controlled Ventilation (DCV) — is outdoor air actually raised when CO₂ (or occupancy) rises, or is it static (DCV not working)?
flowchart TD
frame["role-frame (OA airflow, CO2, occupancy)"] --> vrp["assess_62_1 (VRP)"]
frame --> gate["occupied · fan on · economizer off"]
gate --> dcv["assess_dcv (DCV)"]
rates["OA_RATES_62_1 (Table 6.1)"] --> req["required_oa_cfm (Voz)"]
req --> vrp
vrp --> vstatus["status: under / adequate / over"]
dcv --> dstatus["status: functioning / static / uncorrelated"]
vstatus --> rule1["ventilation_rate_62_1 rule"]
dstatus --> rule2["dcv_verification / dcv_system_verification rules"]
rule1 --> finding["Finding (warn / fault)"]
rule2 --> finding
Two independent checks: is enough OA delivered (VRP), and does OA modulate with demand (DCV).
The VRP requirement
ASHRAE 62.1 sets the zone outdoor-air requirement as
Vbz = Rp·Pz + Ra·Az (people term + area term)
Voz = Vbz / Ez (corrected for air-distribution effectiveness)
required_oa_cfm(area_sqft, population, *, rp, ra, ez=1.0) computes Voz. The Rp/Ra rates
come from OA_RATES_62_1 (62.1 Table 6.1, a public-standard subset) via oa_rates_for(space_type),
or you pass them explicitly.
from camber.ventilation import assess_62_1
# a 2,000 ft² office for 10 people, metering ~120 cfm OA over occupied hours
r = assess_62_1(
oa_cfm_series,
area_sqft=2000,
population=10,
space_type="office",
occupied_mask=occ,
aggregate="median",
)
r.required_cfm # 170.0 (5·10 + 0.06·2000)
r.status # "under" -> deficit 50 cfm
measured_oa_cfm may be a scalar or a time series; a series is filtered by occupied_mask and
reduced by aggregate. Status is under (ratio < under_tol), over (ratio > over_factor,
an energy penalty), or adequate.
Option flags — assess_62_1
| flag | default | effect |
|---|---|---|
space_type |
— | look up Rp/Ra from the 62.1 table |
rp, ra |
from table | override the people / area rates |
ez |
1.0 |
zone air-distribution effectiveness (raises the requirement when < 1) |
aggregate |
"median" |
reduce a series: median/mean/min/p05/p95 |
occupied_mask |
None |
restrict a series to occupied hours |
under_tol |
0.9 |
flag under below this fraction of required |
over_factor |
1.5 |
flag over above this multiple of required |
System-level VRP (multiple-zone systems)
assess_62_1 compares an air handler's outdoor air with one zone's Voz. An air handler that
serves several zones must bring in the system intake instead (0.92, #17):
Vou = D·Σ(Rp·Pz) + Σ(Ra·Az) D = Ps / ΣPz (uncorrected OA intake, occupant diversity)
Vot = Vou / Ev (multiple-zone recirculating system)
Judged against one zone, a correctly ventilated four-zone unit reads "over-ventilated 5×", and a unit 60 % short of its system requirement can read "adequate".
Sources. The maintainer has no licensed 62.1-2022 copy, so the procedure follows the free ASHRAE 62.1-2016 Addendum f (Vou, D and the simplified Ev; numbered §6.2.5 there and §6.2.4 in 62.1-2019) and public secondary sources (design guides and ASHRAE Journal columns on the addendum); the standard's text is not quoted. The 2019/2022 section and table numbers, the default occupant densities and the Ez rows are unverified against those editions.
Which Ev. system_outdoor_air(zones, method=...) offers both:
simplified(the default):Ev = 0.88·D + 0.22forD < 0.60, else0.75. It needs only each zone's area, population and rates — what a site can usually state — and it is the path the addendum added for exactly that reason. The addendum limits it on VAV systems to zones whose minimum primary airflow is at least1.5 × Voz(per the secondary sources; wording unverified): givevpz_min_cfmand zones below it are listed (vpz_min_short) with a caveat to use the appendix method. In the simplified procedure Ez does not change Vot.appendix: the multiple-zone calculation of the standard's normative appendix for a single supply system without secondary recirculation —Zpz = Voz / Vpz,Xs = Vou / Vps,Evz = 1 + Xs − Zpz,Ev = min Evz. It needs every zone's primary airflow (vpz_cfm, elsevpz_min_cfm, the critical condition of a VAV zone) and the system'svps_cfm(default ΣVpz, which over-states Vps and the requirement when zone peaks do not coincide; caveated).
system_type="single_zone" gives Vot = Voz, and "100pct_oa" gives Vot = ΣVoz. Voz uses the
zone air-distribution effectiveness by mode: DEFAULT_EZ_COOLING = 1.0 (ceiling supply of cool
air) and DEFAULT_EZ_HEATING = 0.8 (ceiling supply of warm air with ceiling return, the
maintainer-approved heating default). Floor supply and displacement have other values; set
ez_cooling / ez_heating per zone. Without a system population (ps, or d), D = 1: the
largest Vou and, simplified, Ev = 0.75 — a conservative requirement that can over-state it, said
in a caveat.
Measured OA. assess_system_62_1(oa, requirement, heating_mask=, judged_mask=) judges each
sample against its mode's Vot and takes the median ratio: under below 0.9, over above 1.5,
else adequate. With no flow station, estimate_oa_cfm estimates OA from
(MAT − RAT)/(OAT − RAT) × supply airflow on samples with |OAT − RAT| ≥ 10 °F, with ±2 °F per
sensor and ±10 % on supply flow propagated to a band; a verdict must then hold across the band, or
it is uncertain. The rule refuses the estimate when one of the temperatures is a copy of
another point or MAT leaves the OA/RA band (the sensor-health checks).
The rule. VentilationSystemVRP (ventilation_system_62_1, a fleet rule, not
auto-registered) runs from the config's ventilation section:
"ventilation": {
"zones": "zones.csv",
"systems": {"AHU-1": {"ps": 40}, "AHU-2": {"method": "appendix", "vps_cfm": 9000}}
}
zones.csv has one row per zone: zone, area_sqft, population, space_type (or rp, ra),
and optionally system, ez_cooling, ez_heating, vpz_min_cfm, vpz_cfm, area_assumed,
population_assumed. A zone without system joins the air handler that serves it through the
config topology (the zone's nearest ancestor with an OA signal; ids match ignoring case and
separators). Occupied (a trended OCCUPANCY point, else weekdays 07–18), fan-on samples are judged;
heating-valve samples (above 5 %) against the heating Vot.
| Outcome | Severity |
|---|---|
| no zones configured, a zone missing area / population / rates, no OA flow and no usable estimate | info, declined, with the reason |
| under-ventilated | fault |
| over-ventilated (a conditioning-energy penalty) | warn |
| adequate | ok |
| uncertain (estimate band straddles a threshold) or too few samples | info |
Severity is capped at warn when any zone's area or population is marked assumed (a caveat
lists them), when OA is estimated from temperatures, or when membership was not declared — read
from a semantic (Brick) model or the naming heuristic, or all zones given to the only OA source.
When the topology lists more terminals under the unit than the zones table does, the finding says
Vot covers the listed zones only.
On open data (catalog examples; not gated benchmarks):
lbnl-b59publishes no per-zone areas or populations, only two office floors of 2,325 m². With a stated assumption — that area split equally over the 51 underfloor terminals (981 ft² each) at the office default of 5 people per 1,000 ft² (unverified) — and the Brick model's terminal → RTU assignment (disputed at the 0.89 intake, so capped atwarn), the four RTUs need Vot 1,110–2,000 cfm (D = 1) and measured 5,570–7,490 cfm of OA over occupied hours from 2020-04-11 (median): over-ventilated 3.4–5.6×, every RTU, and still 3.5–5.8× at D = 0.5. The temperature estimate on the same hours reads 0.4–0.7 of the flow stations (0.16 on RTU04, whose return air copies its supply air); with the stations removed the rule saysuncertainon RTU01–03 and declines RTU04. Neither the stations' nor the MAT sensors' accuracy is known (the MAT sensors fail the flow balance; see SENSOR-HEALTH.md).ornl-frp-vav(one RTU and ten VAV boxes, no OA flow station, no published room areas): with an assumed 300 ft² per room and the office default density, the 31 one-day runs readoveron 7 (OA estimated at 930–1,850 cfm against Vot 340 cfm),uncertainon 4 and too few samples on 20 (15-minute data; mild days with|OAT − RAT| < 10 °Fleave nothing to estimate from). There is no ground truth for OA, so this exercises the plumbing only.
Verdict thresholds are validated on synthetic systems only; no licence-clean dataset has design zone areas and populations with measured OA.
DCV verification
from camber.ventilation import assess_dcv, economizer_active_mask
econ, basis = economizer_active_mask(idx, econ_cmd=econ_cmd) # or oat= + heat_valve=
res = assess_dcv(
oa_signal, co2_series, occupied_mask=occ, economizer_mask=econ, co2_setpoint=1000, oa_floor=720
)
res.status # "functioning" | "static" | "uncorrelated" | "insufficient"
res.reason # why "insufficient" (see the table below)
res.demand_lift # CO₂ when OA was raised minus CO₂ when OA sat at its floor
oa_signal can be OA flow, OA fraction, or OA-damper position. demand_signal is one of three
kinds, detected automatically (demand_kind="auto") or stated:
| kind | what it is | lift threshold |
|---|---|---|
co2 |
zone CO₂, ppm | min_lift_ppm (50 ppm) |
presence |
anything within 0..1 — an occupancy point, or its hourly mean | min_lift_occupancy (0.2) |
count |
occupant count: non-negative, below any plausible CO₂ | min_lift_people (1 person) |
Until 0.82.0 anything not strictly 0/1 was read as CO₂, so an occupant count, or a presence point
averaged to 10-minute means, was filtered out as implausible CO₂ and the verdict came back
too_few_samples with no hint why.
Which samples are judged
DCV is only responsible for OA some of the time, so the rest is excluded before anything is judged:
- Unoccupied — outside
occupied_mask. The rules build it from the schedule AND-ed with a trendedOCCUPANCYpoint, minusWARMUP/COOLDOWN. - Fan off or in transition —
SUPPLY_FAN_STATUSbelow 0.95 (an hourly mean below 1 is a partial hour whose OA mean is diluted). - Economizing — an air handler's OA damper follows
max(economizer, DCV minimum), so while it economizes OA tracks outdoor temperature, not CO₂.economizer_active_maskfinds those samples from the best evidence available: a trendedECON_CMD(any nonzero hourly mean counts); elseOAT+HEAT_VALVE(a sequenced SAT loop holds OA at minimum while heating, and the high limit locks the economizer out above 75 °F — the top of the ASHRAE 90.1 fixed dry-bulb range, so it excludes more hours when unsure); elseOATalone; else nothing, which the rule caveats. - OA closed — at or below 2% of its 95th percentile. DCV never shuts OA fully while a space is
occupied (62.1 keeps an area-based floor), so a closed damper inside the occupied window is
warm-up, a fan transition or a fault. Left in, an un-flagged morning warm-up closure made a
stuck DCV look like it responded. Reported as
closed_pct.
The verdict
A DCV controller maps demand to OA: a proportional reset raises OA as CO₂ climbs past its engage
level; an integral loop raises OA only once CO₂ reaches setpoint and holds it there. Either way,
the samples where OA is raised are the samples where demand is high. So assess_dcv compares
CO₂ when OA is raised (above 25% of its robust p5–p95 range) with CO₂ when OA sits at its floor
(within 5%) — demand_lift.
Conditioning on OA rather than on CO₂ matters for integral control: a PI loop holds CO₂ just under
setpoint with OA at its floor much of the time, so "OA when CO₂ is high" is mostly the floor, while
"CO₂ when OA was raised" stays high. The comparison is median-based, so a few outliers do not move
it, and it reads in ppm. The Pearson correlation the check used before is still reported as
correlation, as a diagnostic only. An integral loop weakens it (0.48–0.66 in simulation, against
0.92–0.99 for a proportional reset) without breaking it; what actually broke the old check was the
economizer, closed-damper samples and outliers, described above.
| status | when |
|---|---|
| insufficient | too few samples; demand never varied; CO₂ never reached the engage level (co2_setpoint − 200, else 800 ppm) where DCV should respond; or OA was raised too rarely to compare. Not evidence either way — a lightly occupied building is not a broken DCV |
| static | demand reached the DCV range and OA did not move (robust modulation < min_modulation) |
| functioning | demand_lift ≥ min_lift_ppm (occupancy: ≥ min_lift_occupancy) |
| uncorrelated | OA modulates, but not upward with demand |
Occupancy demand is judged within each hour of day (weekdays and weekends apart). Occupancy
follows the clock, and so does a valve on a time clock or a damper doing thermal duty, so a plain
comparison credits a schedule with "responding to occupancy". Only outdoor air that is higher on
busier days at the same hour counts. Outdoor air that is never both raised and at its floor
within one hour reads insufficient with reason="schedule_confounded". raised_when_vacant_pct
reports how often OA was raised with the space empty. An occupancy-based verdict is still weaker
than a CO₂ one: a supply that rises with occupants' heat gain in a 100% outdoor-air system
genuinely ventilates more per person, and occupancy data alone cannot say whether a thermostat
or a DCV loop did it.
CO₂ demand is judged within the hour of day too (0.93, #37; stratify_hour=True). A valve
that opens on a morning clock and on CO₂ mixes the two in a pooled comparison: its clock-driven
opening at low morning CO₂ drags the raised-valve median down, and a working CO₂ response reads
"uncorrelated" -- or reads "functioning" on a second sensor, depending on which one you trust
(an office room in the B4B data did both). Within each hour of day (weekdays and weekends apart)
the clock cancels, so the lift is the per-hour median difference, averaged over the hours that
hold at least three raised and three at-floor samples. Where fewer than min_bin such pairs
exist the pooled lift decides, as before; lift_basis says which was used and
demand_lift_pooled is always reported. A pooled verdict carries a caveat, because a valve on a
pure time clock never has both states within one hour and so cannot be told apart from one that
follows CO₂.
Sub-checks, each None when it cannot be evaluated:
co2_breach_at_min_pct(needsco2_setpoint) — share of judged samples with CO₂ above setpoint while OA is at its floor: under-ventilation the DCV is not answering.below_floor_pct(needsoa_floor, same units as the OA signal) — OA below the floor, over every occupied sample: it is measured before the economizer and closed-damper exclusions, because an economizer only raises OA and a damper shut while occupied is the deepest below-floor case there is. (Until 0.82.0 it ran after them, so OA below the floor while the economizer might be active vanished into "not judged".) For 62.1 dynamic reset the floor is the area componentRa·Az(§6.2.7; numbering varies by edition). For a multiple-zone system the true intake floor is higher thanΣRa·Az, so this under-flags — the safe direction.excess_at_low_demand_pct(needsoa_floor) — share of low-CO₂ samples with OA held above the floor: DCV installed but not saving energy, the reason it exists (ASHRAE 90.1 §6.4.3.8).
Option flags — assess_dcv
| flag | default | effect |
|---|---|---|
occupied_mask |
None |
restrict to occupied samples |
economizer_mask |
None |
exclude (possibly) economizing samples — see economizer_active_mask |
co2_setpoint |
None |
sets the engage level and enables the breach sub-check |
dcv_engage_ppm |
setpoint − 200, else 800 | CO₂ by which DCV should already be raising OA |
min_modulation |
0.1 |
min robust OA range (p95−p5)/p95; below ⇒ "static" |
min_lift_ppm |
50 |
min demand_lift (CO₂) for "functioning" |
min_lift_occupancy |
0.2 |
min demand_lift (presence fraction) for "functioning" |
min_lift_people |
1.0 |
min demand_lift (occupant count) for "functioning" |
demand_kind |
"auto" |
co2 / presence / count, or detect |
min_demand_span |
150 |
min CO₂ p90−p10 to judge at all |
oa_floor, floor_tol |
None, 0.10 |
floor for the floor sub-checks |
min_samples, min_bin |
24, 6 |
sample gates for the verdict and each bin |
closed_frac |
0.02 |
OA at or below this share of its p95 is "closed" |
stratify_hour |
True |
take the CO₂ lift within the hour of day (0.93); False = pooled |
min_corr |
— | deprecated (0.82), ignored; warns |
Rules
dcv_verification(DemandControlledVentilation) — config-free, auto-registered. Runs on equipment carrying both an OA signal (OA_AIRFLOW, elseOA_DAMPER) and CO₂ — an air handler with a return-air CO₂ sensor, or a single-zone unit. A VAV zone's frame (CO₂, no OA) returns nothing; the fleet rule covers it. Severity:okfunctioning;warnstatic, or uncorrelated with the economizer excluded;infoinsufficient, or uncorrelated when the economizer could not be excluded (OA may be following outdoor temperature);warnwhen OA sits above the floor on ≥excess_warn_pctof low-demand samples;faultwhen the breach or below-floor share reachesbreach_fault_pct/below_floor_fault_pct. Flags:co2_setpoint,dcv_engage_ppm,min_modulation,min_lift_ppm,econ_high_limit_f,oa_floor_cfm(a number, or{equip: cfm}), the three thresholds,occupied_only.
Occupied hours: a trended OCCUPANCY point replaces the schedule (so a 24/7 space isn't
cut to a weekday window); otherwise start_hour / end_hour / occupied_days (default
Mon–Fri 07–18). Unventilated while occupied: samples inside the occupied hours with CO₂ ≥
unventilated_co2_ppm (default co2_setpoint, else 1100 ppm) and the fan off or the OA shut
are reported as unventilated_high_co2_hours / _pct, and are a fault once they add up to
unventilated_fault_hours (4 h). The verdict itself judges modulation, not outages, so without
this a lecture theatre at its CO₂ sensor's full scale with the fan off read "not judged".
CAMBER temperatures are °F: when the economizer is inferred from OAT and nearly every sample is
excluded, the finding says an OAT in °C would do that.
Below the floor with the fan off (0.98, #93). An occupied hour below oa_floor_cfm because
the supply fan was off is a schedule the fan did not keep, not a minimum-OA problem. The fan is
read from SUPPLY_FAN_STATUS, else from SUPPLY_FAN_SPEED at or below fan_off_speed_pct
(5 %). Those hours are named in the summary and a caveat ("supply fan off while scheduled
occupied") and counted as fan_off_occupied_pct / fan_off_occupied_hours, and
below_floor_pct is the shortfall with the fan running. The share test still reads every
below-floor sample (below_floor_total_pct), so the severity does not move. With no fan
signal nothing changes. Duration (opt-in): below_floor_fault_hours (default None)
makes a contiguous below-floor episode of that many occupied hours a fault whatever its share,
because a short, concentrated event otherwise vanishes in a long record. The longest episode is
reported either way (below_floor_longest_h, _start, _fan_off_h). It counts consecutive
occupied samples, so it continues across the night between two occupied days, and the summary
says when the fan was off for most of it.
Which OA signal (0.93, #37). The best signal judges every sample it covers, a lesser one only
the samples the better ones miss: OA_AIRFLOW, then OA_DAMPER. An air handler whose flow
station is masked for a period is judged there on its damper position and on its flow
elsewhere; metrics["oa_segments"] lists each signal's verdict and date span, the first
segment with a verdict leads the finding, and the worst severity wins. With
full_outdoor_air=True (a 100 % outdoor-air unit: no return air, no economizer damper, every
cfm the fan moves is outdoor air) the supply AIRFLOW and then the SUPPLY_FAN_SPEED follow
as proxies; no economizer exclusion applies (economizer_basis="full_outdoor_air"), and the
finding says the fan may also speed up for cooling.
- dcv_system_verification (DcvSystemVerification) — the fleet twin, auto-registered.
Most buildings put CO₂ on the zones and OA on the air handler, so no single frame has both. This
groups zones to their serving air handler through the served-by topology — each zone's
nearest ancestor that carries an OA signal, so a Brick chain air handler → VAV → zone works
(semantic, or the naming heuristic run_fleet builds — which caps severity at warn), drops zone CO₂ that is implausible,
stuck flat, or stays more than 300 ppm above outdoor when unoccupied, takes the per-timestamp
maximum (the critical zone should drive the reset; agg="mean" is available) and judges each
air handler as above. With no usable grouping and exactly one OA source, all zones join it, with
a caveat; with several, unattributed zones are declined rather than guessed. One finding, with a
per_ahu breakdown.
- co2_ventilation (CO2Ventilation) — CO₂ adequacy: under-ventilated when CO₂ rises more
than 700 ppm above outdoor, over-ventilated when it stays within 150 ppm (warn at 60 % of
occupied hours). Since 0.93 (#38) an economizer's outdoor air is not over-ventilation: the
economizer-mode hours (iaq.economizer_mode_mask — a trended ECON_CMD; else the OAT below
the 75 °F high limit and the OA damper more than 5 points above its minimum, or the unit on
~100 % outside air by freecooling.integrated_economizer_mask) are left out of
over_vent_pct and reported apart (econ_hours_pct, over_vent_econ_pct,
over_vent_all_pct); with fewer than 10 hours left, over-ventilation is not judged. An OAT alone
excludes nothing (a 100 % outdoor-air unit has no economizer). Under-ventilation is judged on
every occupied hour. Occupied hours (0.107, #200) are a trended OCCUPANCY point's, which
replaces the assumed weekday 07-18 schedule; a finding judged on the schedule says so in a
caveat, and occupancy_basis names the source. Flags: exclude_economizer,
oa_damper_min_pct (default: the damper's 5th percentile while open), econ_high_limit_f.
- co2_ventilation_system (CO2VentilationSystem, 0.93) — the fleet twin: a zone frame
carries CO₂ but not the damper, so each CO₂ zone is joined to its serving air handler (served-by
topology, else the naming heuristic; one economizing unit and no grouping: every zone joins it)
and judged with that unit's economizer-mode hours excluded. A zone no unit can be attributed to
is judged without the exclusion, with a caveat. One finding with a per_zone breakdown.
- ventilation_system_62_1 (VentilationSystemVRP, 0.92) — the system-level VRP above; runs
from the config's ventilation section.
- ventilation_rate_62_1 (VentilationRateProcedure) — needs the zone's design inputs, so
it's instantiated explicitly (not auto-registered). It judges one zone; for an air handler
serving several zones use ventilation_system_62_1:
from camber.rules.ventilation_rule import VentilationRateProcedure
rule = VentilationRateProcedure(area_sqft=2000, population=10, space_type="office")
finding = rule.analyze("AHU-1", role_frame) # fault on under-ventilation
Validation and limits
The verdict logic is tested against camber.faultlab.dcv_sim, a well-mixed zone CO₂ mass balance
(V·dC/dt = N·G − Q·(C − C_out)) under proportional, integral and static DCV with an economizer,
warm-up closures and a fan schedule — and, since 0.82.0, against open real-building data
(licence-clean, not vendored; datasets cited by DOI):
| Data | What it establishes |
|---|---|
| Lab room running a known DCV law, 3 + 6 L/s per person (Zenodo 10.5281/zenodo.18299691, CC BY) | functioning on CO₂ (lift 392 ppm), on the occupant count and on the controller's own occupancy estimate. The CO₂ mass balance closes at 5.3–5.8 mL/s per person, close to the 5.0 mL/s (0.0105 cfm) the simulator assumes for a sedentary adult |
| Three office rooms, camera counts + VAV damper (data descriptor doi:10.1038/s41597-019-0274-4, CC0) | functioning on counts in all three (lift 2–20 people within the hour); on CO₂ one room, the other two never reached the engage level |
| Office building with no DCV, 4 rooftop units, 11 CO₂ zones, Brick model (Dryad 10.7941/D1N33Q) | insufficient on every unit — never a false functioning (specificity); all 11 zones attributed through the Brick air handler → VAV → zone chain. Below the 750 cfm floor (the config's assumed Ra·Az per unit) in 2.0–4.2 % of occupied hours over the OA-flow record (April–December 2020): info, under the 10 % below_floor_fault_pct. Those hours are days with the supply fans off while the schedule says occupied (October and December 2020; fan speed feedback 1.2–2.6 %, and −24.9 on 2020-12-17), not the wildfire smoke mode. Since 0.98 the finding says so: "supply fan off while scheduled occupied" for 41, 82, 40 and 40 h, and below_floor_pct (the shortfall with the fan running) is 0.1, 0.0, 0.1 and 0.0 %; the longest episode is 22 h (RTU02: 64 h, 2020-10-08 to 10-15), so below_floor_fault_hours: 4 would flag every unit (a fault, shown as warn because this run's zone grouping is heuristic). The smoke mode stays unflagged: from 2020-08-24 to 09-06 the dampers were held at their 10 % minimum and the units still took in 1,130–3,625 cfm (median by unit), so the smoke-mode weeks alone read 0–0.9 % below the floor (re-measured in 0.98; earlier versions of this page called the smoke mode a below-floor fault on two units). Since 0.93 the months before the OA-flow record (Aug 2019 – Mar 2020) are judged on the damper position: insufficient too, zone CO₂ never varying 150 ppm. co2_ventilation_system: 57–71 % of occupied hours are economizer mode and set apart; the zones stay within 150 ppm of outdoor in 99–100 % of the remaining minimum-damper hours — over-ventilated at the minimum, not only while economizing |
| Lecture theatre, fan speed + two CO₂ sensors (Zenodo 10.5281/zenodo.3406555, CDLA-Permissive) | fault: 110 occupied hours with the fan off and CO₂ at the sensor's 2000 ppm full scale. Since 0.93 the unit (100 % outdoor air) is judged on its fan speed: insufficient on the fan-on hours (CO₂ never varies enough), and the fan-off hours at full-scale CO₂ remain a fault |
| Rooms with a schedule-driven ventilation valve (github energietransitie/b4b-windesheim, CC BY) | not functioning on presence once the lift is taken within the hour — a time clock is not DCV. On CO₂ (0.93, within the hour): one room reads functioning on both its sensors, where the pooled lift read "uncorrelated" on one; the other stays sensor-dependent (functioning on its desk sensor, a 12 ppm same-hour lift on the BMS sensor) |
- Without
ECON_CMDthe economizer state is inferred, and the OAT-only fallback discards all mild weather — expectinsufficientmore often. That is the honest answer, not a defect. - Damper position is not OA flow; the floor sub-checks need
OA_AIRFLOWin cfm. - Return-air CO₂ averages the zones and dilutes the critical one.
dcv_verificationattaches this caveat to air handlers and to equipment of unknown class, not to terminals or fan coils, whose CO₂ is the room's own (0.98). - A fixed OA damper on a variable-speed supply fan moves OA flow with fan speed, and fan speed tends to follow occupancy through cooling load — that can mimic DCV. Not detected.
- The fleet rule's
maxaggregation follows the worst trusted zone sensor; the plausibility, stuck and unoccupied-offset guards remove the obvious bad ones, not all.
Scope
This verifies the rate from trends; it is not a substitute for a stamped 62.1 ventilation calculation. The Table 6.1 defaults are a convenience — confirm the zone's category and design population against the project's mechanical schedule.
Further reading
PNNL's AHU Minimum Outdoor-Air Operation guide (PNNL-SA-88958) backs the DCV, CO₂ and 62.1 rules, and training chapter 5 covers the air handler (links only; see References). When a DCV finding fires, its advisory action follows the cause: a working DCV whose outdoor air stays above its floor at low demand is told to lower that floor, not to "enable DCV" (see ASO).