Zone terminals, FCU, and unit ventilators
The shipped FCU / zone_other rule family covers coil delivery, passing valves, ventilation feedback, sensor loss, deadband, and heat/cool mode cycling for these controllers. Per-rule SQL and pandas live in the DataFusion cookbook and the Pandas cookbook. Haystack tags for each rule are on the SQL rules → Haystack map.
Open-FDD ZONE is a control definition, not “VAV boxes only.”
Haystack multi-tag, then the Open-FDD stamp
Project Haystack puts several markers on one record. A fan coil is one equip that also carries fanCoilUnit and zone (and usually hvac). Each point on that equip is its own record with several markers. Open-FDD does not select the controller by reading the equipment id. It consumes two things from that multi-tag model:
- One equipment stamp —
equipTypeorequipment_type. Markersequip+fanCoilUnit+zonecollapse to a stampcanonical_kindmaps tozone_other(fanCoil,fanCoilUnit,fcu,zone_other,zone, or a standalone-DDC alias). - Mapped point roles — each point’s markers flatten to one package key (
zone+air+temp+sensor→zone-air-temp). Ingest sends that key throughhaystack_point_to_roleto a SQL column. The pandas oracle uses the Haystack key as the column name.
The equipment id is only the historian key. AC_1, a BAS guid, or any other opaque id is valid when the stamp and the point map are present. A missing stamp is not repaired by putting FCU or ZONE in the id.
| Haystack markers on one record | Open-FDD input |
|---|---|
Equip: equip, fanCoilUnit, zone |
Stamp equipType: fanCoil → kind zone_other (master) |
| Equip: follower with no zone sensor | Resolved kind general (stamp-based; see below) |
Point: zone, air, temp, sensor |
zone-air-temp → zone_t |
Point: discharge, air, temp, sensor |
discharge-air-temp → sat |
Point: heating, valve, cmd |
heating-valve → htg_valve_pct |
Point: cooling, valve, cmd |
cooling-valve → clg_valve_pct |
Point: damper, cmd and damper feedback |
damper-cmd → damper_cmd, damper → damper_pct |
Point: zone, co2, sensor |
zone-co2 → zone_co2 |
Point: fan, status and fan, cmd |
fan-status → fan_status, fan-cmd → fan_cmd |
Point: cooling, sp and heating, sp |
cooling-sp → cooling_sp, heating-sp → heating_sp (site °C) |
Point: zone, air, temp, sp |
zone-air-temp-sp → zone_air_temp_sp |
Compact package map (the shipped ingest shape). The id AC_1 is opaque. The stamp and the points keys are the model:
{
"equipType": "fanCoil",
"equip": "AC_1",
"points": {
"zone-air-temp": "zn_t",
"zone-air-temp-sp": "zn_sp",
"discharge-air-temp": "da_t",
"heating-valve": "htg_cmd",
"cooling-valve": "clg_cmd",
"damper-cmd": "oa_cmd",
"damper": "oa_pos",
"zone-co2": "co2",
"fan-status": "fan_s",
"fan-cmd": "fan_c",
"cooling-sp": "clg_sp",
"heating-sp": "htg_sp"
}
}
Master vs follower stays a stamp, plus which roles are mapped.
| Master | Follower | |
|---|---|---|
| Haystack equip markers | equip + fanCoilUnit (or zone DDC) + zone |
equip without a zone-sensor point |
| Open-FDD stamp | token that canonical_kind maps to zone_other |
resolved kind general |
| Point roles | includes zone-air-temp |
omits zone-air-temp; may still map zone-air-temp-sp |
canonical_kind does not map the token general. Product kind_for returns zone_other from the master stamps above. The pandas oracle maps a resolved GENERAL type to kind general, which is on the FCU equipment_kinds list. FCU-SENSOR-NULL reports zero hours when the zone_t column is absent, so a setpoint-only follower is not a dead-sensor fault.
A fan coil’s stamp stays zone_other. An air handler that also carries these point roles uses an ahu stamp. Unit ventilators stay CV AHU (unitVentilator), not fanCoilUnit.
ZONE control (comfort + sensor FDD)
Stamp as ZONE (equipType: zone_other, zone, fcu, fanCoil, or standalone-DDC aliases). Applies when the asset is a zone-level controller or monitor:
| Shape | What it is | Typical roles / FDD |
|---|---|---|
| Fan-coil (FCU) | Zone terminal with control-valve PID (hunting, stuck valve, valve vs zone_t) | zone_t, setpoints, valve cmd/feedback, occupancy when present |
| Standalone DDC monitor | Generic DDC watching a standalone local control system (sensors, maybe setpoints; not a full AHU plant) | Zone / space sensors + validity / flatline / out-of-range equations |
Both shapes:
- Get Overview Building schedule & zone comfort (FDD starting point) when the zone-temp comfort gate is ON (default).
- Run the zone sensor-value fault equation set (temperature performance, sensor health, related ZONE category rules) – not AHU economizer / SAT-reset plant rules.
Do not stamp FCU or standalone zone DDC as ahu.
VAV boxes remain equipType: vav (zone terminals with airflow/damper). They share the same comfort gate for zone-temp performance rules.
Family Zones RCx (comfort ranking, space-temp series, airflow series, zone health) uses that same set: VAV, FCU, zone_other / standalone DDC, baseboard, and a heat pump that has zone-air-temp.
Haystack puts more than one marker on the same asset. The package path keeps those tags together: equip, a zone-terminal marker (fanCoilUnit, zone, or vav), and the point roles (zone-air-temp, plus airflow or valve roles when the terminal has them). Compact ingest is equipType plus points (fanCoilUnit / zone / vav with zone-air-temp). Family Zones membership reads that stamp and the mapped roles. A heat-pump stamp joins only when zone-air-temp is mapped, because plant heat pumps share that stamp. A non-zone stamp such as ahu stays out. Membership never uses equipment_id LIKE or a prefix (RTU_01 stays distinct from RTU_010). Other RCx presets use an exact equipment-id list from the package stamp.
VAV-1 scores the comfort band only while occupied when occ_mode or the Overview calendar is set (require_occupied, default on). Unoccupied setback stays on VAV-2.
FCU / zone_other rule roles
Nine registry rules (FCU-SENSOR-NULL, FCU-HTG-COIL, FCU-CLG-COIL, FCU-VALVE-PASS-HTG, FCU-VALVE-PASS-CLG, FCU-DAMPER-POS, FCU-CO2-DAMPER, FCU-DEADBAND, FCU-MODE-CYCLE) select equipment from the stamp and from registry equipment_kinds: zone_other, general, and ahu. A missing or unrecognized stamp is unclassified. Do not infer kind from the folder name or equipment_id (#1043, #1045, #1046, #1047).
| Role | How it is selected | Why |
|---|---|---|
| Master (owns a zone sensor) | equipType that canonical_kind maps to zone_other: zone_other, zone, fcu, fanCoil, fanCoilUnit, or a standalone-DDC alias |
Product kind_for returns zone_other, so the family applies |
Follower (setpoint only, no zone_t column) |
registry kind general |
The kind is on the FCU list. FCU-SENSOR-NULL forces zero hours when zone_t is absent, so the follower is not a dead-sensor fault. canonical_kind does not map the token general; the pandas oracle maps a resolved GENERAL type to general |
| Air handler that carries the same roles | stamp that canonicalizes to ahu |
The kind is on the registry list. A fan coil stays on the zone_other stamp |
Haystack tags the nine rules read
Preferred tags are the haystack_point_to_role arms in crates/fdd_core/src/columns.rs. Map these in the package. Empty FCU results mean the map is missing a required role.
| Need | Haystack tag | SQL role | Rules |
|---|---|---|---|
| Zone temperature | zone-air-temp |
zone_t |
sensor-null (optional), both coils, both passing valves |
| Own zone setpoint | zone-air-temp-sp |
zone_air_temp_sp |
FCU-SENSOR-NULL (required) |
| Discharge / supply air | discharge-air-temp |
sat |
both coils, both passing valves |
| Heating valve | heating-valve |
htg_valve_pct |
heating coil, cooling coil, both passing valves, mode cycle |
| Cooling valve | cooling-valve |
clg_valve_pct |
cooling coil, both passing valves, mode cycle |
| Damper command | damper-cmd |
damper_cmd |
FCU-DAMPER-POS, FCU-CO2-DAMPER |
| Damper feedback | damper |
damper_pct |
FCU-DAMPER-POS |
| Zone CO₂ | zone-co2 |
zone_co2 |
FCU-CO2-DAMPER |
| Cooling setpoint | cooling-sp or zone-cooling-sp |
cooling_sp |
FCU-DEADBAND |
| Heating setpoint | heating-sp or zone-heating-sp |
heating_sp |
FCU-DEADBAND |
| Fan proof | fan-status |
fan_status |
duration rules (optional; preferred) |
| Fan command fallback | fan-cmd |
fan_cmd |
duration rules when status is null |
Commands may be 0–1 or 0–100. Fan proof prefers fan_status. fan_cmd above 10% is the fallback. CO₂ below 300 ppm is invalid. Valve-shut tests use 0.05.
Per-rule roles both engines need
Use this table to map a package before tip stress. The Haystack tag is the pandas column. The SQL role is the DataFusion column. Required roles are required on both engines. Fan proof is optional on both duration rules. The last column is registry optional_roles that the shared SQL CTE selects and the pandas predicate does not read; DataFusion injects NULL when they are absent.
Parity stays sql_screening until tip and field stress on issue-mapped gates. A green CI run does not close Soft-OPEN for this family.
| Rule | Confirm | Required Haystack tag (pandas) | SQL role | Optional both engines | SQL registry optional only |
|---|---|---|---|---|---|
FCU-SENSOR-NULL |
0 s | zone-air-temp-sp |
zone_air_temp_sp |
zone-air-temp → zone_t |
— |
FCU-HTG-COIL |
900 s | discharge-air-temp, zone-air-temp, heating-valve |
sat, zone_t, htg_valve_pct |
fan-status → fan_status, fan-cmd → fan_cmd |
clg_valve_pct, damper_cmd, damper_pct, zone_co2 |
FCU-CLG-COIL |
900 s | discharge-air-temp, zone-air-temp, cooling-valve, heating-valve |
sat, zone_t, clg_valve_pct, htg_valve_pct |
fan-status → fan_status, fan-cmd → fan_cmd |
damper_cmd, damper_pct, zone_co2 |
FCU-VALVE-PASS-HTG |
900 s | discharge-air-temp, zone-air-temp, heating-valve, cooling-valve |
sat, zone_t, htg_valve_pct, clg_valve_pct |
fan-status → fan_status, fan-cmd → fan_cmd |
damper_cmd, damper_pct, zone_co2 |
FCU-VALVE-PASS-CLG |
900 s | discharge-air-temp, zone-air-temp, heating-valve, cooling-valve |
sat, zone_t, htg_valve_pct, clg_valve_pct |
fan-status → fan_status, fan-cmd → fan_cmd |
damper_cmd, damper_pct, zone_co2 |
FCU-DAMPER-POS |
900 s | damper-cmd, damper |
damper_cmd, damper_pct |
fan-status → fan_status, fan-cmd → fan_cmd |
sat, zone_t, htg_valve_pct, clg_valve_pct, zone_co2 |
FCU-CO2-DAMPER |
900 s | zone-co2, damper-cmd |
zone_co2, damper_cmd |
fan-status → fan_status, fan-cmd → fan_cmd |
sat, zone_t, htg_valve_pct, clg_valve_pct, damper_pct |
FCU-DEADBAND |
0 s | cooling-sp, heating-sp |
cooling_sp, heating_sp |
— | — |
FCU-MODE-CYCLE |
0 s | heating-valve, cooling-valve |
htg_valve_pct, clg_valve_pct |
— | — |
FCU-SENSOR-NULL forces zero hours when the zone_t / zone-air-temp column is absent, so a setpoint-only follower is not a dead-sensor fault. A present column that is null still runs the 90% coverage test.
°F canonical vs °C pass-through
sat and zone_t are canonical temperature roles. Coil and passing-valve thresholds are °F (default 5.4°F, which is 3°C). On a metric session those two columns are converted to °F through history_si.
cooling_sp and heating_sp are not temperature roles. FCU-DEADBAND subtracts them in site-native °C (default minimum 1°C). Map Celsius setpoints into those two roles. A Fahrenheit number in cooling_sp or heating_sp will not be rewritten to °C, and the deadband test will not mean what the slider says.
zone_air_temp_sp is also pass-through. FCU-SENSOR-NULL only tests that it is present, so its unit does not enter a temperature comparison. The pandas quality pass skips zone-air-temp-sp, cooling-sp, and heating-sp on FCU-* rules so Fahrenheit sensor ranges do not null them first.
Unit ventilator = CV AHU
A unit ventilator (UV) is an air-handling unit, constant volume – same family as CV AHU.
| Stamp | Canonical kind | Display |
|---|---|---|
unitVentilator / uv / unit_ventilator |
ahu |
CV AHU |
cv_ahu / cvahu |
ahu |
CV AHU |
ahu / rtu / mau / doas |
ahu |
AHU (or subtype label) |
UV is not ZONE. Do not put UV on the ZONE comfort path just because it serves one room – use AHU constant-volume / air-handler rules and roles (sat, fans, OA/RA/MA when present).
Stamp cheat sheet
| Asset | equipType |
Kind |
|---|---|---|
| VAV box | vav |
vav |
| FCU (valve PID zone) | fcu / fanCoil |
zone_other |
| Standalone zone DDC | zone_other / zone |
zone_other |
| Unit ventilator | unitVentilator / cv_ahu |
ahu (CV) |
| VAV AHU | vav_ahu / ahu |
ahu |
Authoritative product map: edge/src/equipment_types.rs. Agent contract: openfdd_agent_spec/DATA_CONTRACT.md + skills/openfdd-package-mapping/SKILL.md.
Opaque ids (DM-04)
The stamp plus the mapped point roles select the equipment. AC_1 with equipType: fanCoil is a fan coil (equipment_type_source: package, kind zone_other) even though the id does not say FCU. The same id with equipType: ahu exports as AHU. A missing or unrecognized stamp is unclassified (equipment_type_source: unclassified) and matches nothing; equipment-id text is not a kind. A VAV→AHU parent is present only when the package map sets parentAhu / parent_ahu to an exact sibling id (parent_ahu_source: package). That link is omitted from Turtle ofdd:parentAhu when the source is anything else. Id tokens (VAV, ZONE, AHU) and a building with a single AHU do not create a parent. A parent link is not cohort membership and is not a plot filter.