FCU-0006 — FCU fan proof-of-operation failure
| Status | verified — engine e2ff2f8, cxf:fnv1a128:8c4af7be56126b213921f77982c3f3e7, 2026-08-20 |
| Severity | 2 |
| Method | rule |
| Phase | 2 |
| Category | PROTECTIVE |
| Confidence | HIGH |
| Estimation | PROXY_ESTIMATION |
| G36 | — |
| Clusters | — |
| Suppresses | — |
| Suppressed by | — |
| Related | FCU-0001, FCU-0002, FCU-0003, FCU-0004, FCU-0005 |
| Playbooks | proof-of-operation, fcu-faults |
| Source | Library proof-of-operation precedents AHU-0039, TOWER-0004, PMP-0003, and HW-0009 — final command versus independent status, separate direction timers, and initialization-safe persistence; ASHRAE Guideline 36-2021 section 5.1.6 — equipment is proven when its digital status matches the state set by its digital command; semantic grounding only, not a source for these shipped timer values; Library-authored FCU fan timing adaptation; no cited source publishes 60 s start and 60 s stop proof limits as portable values |
| Operating states | all states in which this FCU has an actively controlled fan and its final individual fan command and independent run proof are authoritative |
Preconditions (host-enforced): The FCU must have an actively commanded fan; passive/convection units are not applicable. Bind fan_cmd to the final fan output after occupancy, mode, condensate, freeze, and local interlock logic, not to unit enable or thermostat demand. Bind fan_status to independent motor-current, airflow, speed, rotation, or auxiliary-contact proof for the same fan, never to a command echo. Exclude fan coast-down, exercise, maintenance, condensate-alarm shutdown, freeze protection, and local hand operation unless represented in the final command. For ECM and multi-speed fans, Boolean normalization proves only operation and must not hide a failed requested speed stage. Inputs must be fresh and aligned; unmet obligations are NO_EVAL, not healthy.
Points: fan_cmd, fan_status
Outputs:
yFault— True while either final command/status mismatch has matured through its own proof timeryFailToStart— Diagnostic direction flag; true after a final start command remains without independent run proof for start_proof_time. False never means NO_EVALyUnexpectedRun— Diagnostic direction flag; true after independent operation continues without a final run command for stop_proof_time. False never means NO_EVAL
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
start_proof_time | 60.0 | s | startProof.delayTime | ADOPTED_TUNABLE final-command-to-proof allowance. Commission above normal device response, acceleration, proof pickup, and telemetry latency. |
stop_proof_time | 60.0 | s | stopProof.delayTime | ADOPTED_TUNABLE command-off-to-proof-dropout allowance. Commission above normal deceleration, coast-down, proof dropout, and telemetry latency; intentional sequence operation must remain represented in the final command. |
Description
This rule checks whether the FCU fan did what its final Boolean command requested. Commanded on without independent proof is a fail-to-start; proven on without command is unexpected operation. The direction identifies the mismatch, not its cause, and neither diagnostic output is an evaluability gate.
Detection Logic
fail_to_start = fan_cmd AND NOT fan_status
unexpected_run = NOT fan_cmd AND fan_status
yFailToStart = fail_to_start sustained for start_proof_time
yUnexpectedRun = unexpected_run sustained for stop_proof_time
yFault = yFailToStart OR yUnexpectedRun
Each direction has its own TrueDelay(delayOnInit=true). Agreement clears both
lanes immediately. A direct mismatch reversal clears the old diagnostic and
starts the other timer from zero; elapsed time never transfers between lanes.
Possible Diagnoses
- Failed motor, ECM, controller, breaker, contactor, or output wiring.
- Seized fan wheel, failed belt, coupling, or bearing.
- Condensate, freeze, or local safety interlock omitted from command.
- Bad current, airflow, speed, rotation, or auxiliary-contact proof.
- Local thermostat, hand switch, or independent controller owns the fan.
Energy Impact
The effect is direction-dependent. Unexpected operation can waste measured electrical energy during the mismatch. Fail-to-start is primarily availability, comfort, and diagnostic-coverage loss; these two booleans cannot price it.
Emissions Impact
Scope 2 is proxy-only for unexpected operation: multiply independently measured device kW by mismatch hours and an appropriate operating emissions factor. Do not claim avoided energy or emissions for fail-to-start without another model.
Deviations
- Both timers are adopted commissioning values. No cited source establishes universal FCU fan proof windows. Configure them independently around the actual sequence, proof device, sampling, and network latency.
- The command is final and device-scoped. An upstream enable, demand, or fleet request can disagree with status while downstream logic works correctly.
- Status is independent proof. Command echo makes the graph tautological; proof type determines whether electrical operation, rotation, or delivery was actually demonstrated.
- No whole-rule suppression is encoded. Fail-to-start can invalidate another rule’s running premise, but unexpected operation may leave that rule physically meaningful; current metadata cannot suppress by direction.
delayOnInit=trueis explicit on both lanes. Evaluator restart into an existing mismatch must serve the full configured proof time.- No empirical FPR or TPR is claimed. Current simulation telemetry cannot provide both an independent final command and field-like proof for this device.
- Passive fan coils are explicitly not applicable. A host must not synthesize a command/status pair for a unit that moves air only by convection.
- The Boolean pair proves fan operation, not the requested ECM or multi-speed stage. A wrong-stage failure needs speed or stage feedback and is outside this graph.
Notes
Read the direction before interpreting FCU-0002 through FCU-0005: fail-to-start removes their airflow premise, while unexpected operation can leave their temperature signatures physically meaningful.
Test Vectors
23 scenarios, clock step 10 s over 2400 s.
| Scenario | Description |
|---|---|
agreement_stopped | Command and independent proof agree off; every direction remains clear. |
agreement_running | Command and independent proof agree on; every direction remains clear. |
fail_to_start_at_initialization | A standing commanded-on/unproved mismatch is clear one tick before 60 s, asserts exactly at 60 s, and remains true after the initialization-safe start timer. |
unexpected_run_at_initialization | A standing command-off/proven-on mismatch is clear one tick before 60 s, asserts exactly at 60 s, and remains true after the initialization-safe stop timer. |
normal_delayed_start_and_stop | Status proves one tick inside both configured allowances, so ordinary start and stop latency never alarms. |
start_mismatch_ends_one_tick_before_proof | A start mismatch beginning at 300 s clears at 350 s, one 10 s evaluator step before maturity, and never alarms. |
start_mismatch_ends_at_proof_boundary | The start mismatch clears exactly 60 s after it begins. Agreement on the maturity tick wins and no alarm is asserted. |
stop_mismatch_ends_one_tick_before_proof | A stop mismatch beginning at 300 s clears at 350 s, one 10 s evaluator step before maturity, and never alarms. |
stop_mismatch_ends_at_proof_boundary | The stop mismatch clears exactly 60 s after it begins. Agreement on the maturity tick wins and no alarm is asserted. |
mature_start_alarm_recovers | A fail-to-start persists past its proof time, then clears immediately when independent proof arrives. |
mature_stop_alarm_recovers | An unexpected run persists past its proof time, then clears immediately when proof drops. |
pending_direction_reversal_starts_fresh_lane | Command and status swap while fail-to-start is pending. The old lane resets and the unexpected-run lane must serve its complete independent timer. |
mature_direction_reversal_hands_off | A mature fail-to-start reverses directly into unexpected-run. The old flag clears on the reversal tick, yFault has a real gap, and the opposite lane serves a fresh complete stop timer. |
two_short_start_mismatches_do_not_accumulate | Two commanded-on/unproved episodes are separated by agreement; elapsed time cannot transfer across the reset. |
two_short_stop_mismatches_do_not_accumulate | Two proven-on/uncommanded episodes are separated by agreement; elapsed time cannot transfer across the reset. |
command_chatter_never_matures | The final command chatters faster than either proof window while status is off; no continuous mismatch matures. |
status_chatter_never_matures | Independent proof chatters against a held-on command; every unproved interval clears before the start timer. |
unexpected_run_chatter_never_matures | Independent proof chatters on against a held-off command; every unexpected-run interval clears before the stop timer. |
upstream_enable_raw_false_positive | An upstream enable held true while downstream logic correctly withholds operation produces a raw fail-to-start alarm; this pins why only the final command is a valid binding. |
command_echo_raw_blind_spot | A status echo follows the command while the device is mechanically stopped. The pair agrees and the graph stays silent, pinning why proof must be independent. |
manual_local_run_is_host_excluded | A locally commanded device is proven on while the BAS final command is off. The raw unexpected-run alarm is correct; approved service or hand operation is host NO_EVAL. |
wrong_speed_stage_is_boolean_blind_spot | An ECM is commanded to high speed but independent Boolean proof reports only that it runs at some speed. Both booleans agree, so a failed requested stage is outside this operation-only rule. |
passive_unit_is_not_applicable | A passive fan coil has no honest final fan command or proof. Synthetic disagreement would alarm raw, demonstrating why the host must not instantiate this rule on convection-only units. |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 10,
"horizon_s": 2400
},
"scenarios": [
{
"name": "agreement_stopped",
"description": "Command and independent proof agree off; every direction remains clear.",
"inputs": {
"fan_cmd": false,
"fan_status": false
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "agreement_running",
"description": "Command and independent proof agree on; every direction remains clear.",
"inputs": {
"fan_cmd": true,
"fan_status": true
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "fail_to_start_at_initialization",
"description": "A standing commanded-on/unproved mismatch is clear one tick before 60 s, asserts exactly at 60 s, and remains true after the initialization-safe start timer.",
"inputs": {
"fan_cmd": true,
"fan_status": false
},
"expect": [
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 50,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 60,
"to_s": 2400,
"equals": true
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 50,
"equals": false
},
{
"output": "yFault",
"from_s": 60,
"to_s": 2400,
"equals": true
}
]
},
{
"name": "unexpected_run_at_initialization",
"description": "A standing command-off/proven-on mismatch is clear one tick before 60 s, asserts exactly at 60 s, and remains true after the initialization-safe stop timer.",
"inputs": {
"fan_cmd": false,
"fan_status": true
},
"expect": [
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 50,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 60,
"to_s": 2400,
"equals": true
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 50,
"equals": false
},
{
"output": "yFault",
"from_s": 60,
"to_s": 2400,
"equals": true
}
]
},
{
"name": "normal_delayed_start_and_stop",
"description": "Status proves one tick inside both configured allowances, so ordinary start and stop latency never alarms.",
"inputs": {
"fan_cmd": [
{
"t": 0,
"value": false
},
{
"t": 300,
"value": true
},
{
"t": 1200,
"value": false
}
],
"fan_status": [
{
"t": 0,
"value": false
},
{
"t": 350,
"value": true
},
{
"t": 1250,
"value": false
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "start_mismatch_ends_one_tick_before_proof",
"description": "A start mismatch beginning at 300 s clears at 350 s, one 10 s evaluator step before maturity, and never alarms.",
"inputs": {
"fan_cmd": [
{
"t": 0,
"value": false
},
{
"t": 300,
"value": true
}
],
"fan_status": [
{
"t": 0,
"value": false
},
{
"t": 350,
"value": true
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "start_mismatch_ends_at_proof_boundary",
"description": "The start mismatch clears exactly 60 s after it begins. Agreement on the maturity tick wins and no alarm is asserted.",
"inputs": {
"fan_cmd": [
{
"t": 0,
"value": false
},
{
"t": 300,
"value": true
}
],
"fan_status": [
{
"t": 0,
"value": false
},
{
"t": 360,
"value": true
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "stop_mismatch_ends_one_tick_before_proof",
"description": "A stop mismatch beginning at 300 s clears at 350 s, one 10 s evaluator step before maturity, and never alarms.",
"inputs": {
"fan_cmd": [
{
"t": 0,
"value": true
},
{
"t": 300,
"value": false
}
],
"fan_status": [
{
"t": 0,
"value": true
},
{
"t": 350,
"value": false
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "stop_mismatch_ends_at_proof_boundary",
"description": "The stop mismatch clears exactly 60 s after it begins. Agreement on the maturity tick wins and no alarm is asserted.",
"inputs": {
"fan_cmd": [
{
"t": 0,
"value": true
},
{
"t": 300,
"value": false
}
],
"fan_status": [
{
"t": 0,
"value": true
},
{
"t": 360,
"value": false
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "mature_start_alarm_recovers",
"description": "A fail-to-start persists past its proof time, then clears immediately when independent proof arrives.",
"inputs": {
"fan_cmd": [
{
"t": 0,
"value": false
},
{
"t": 300,
"value": true
}
],
"fan_status": [
{
"t": 0,
"value": false
},
{
"t": 460,
"value": true
}
]
},
"expect": [
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 350,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 360,
"to_s": 450,
"equals": true
},
{
"output": "yFailToStart",
"from_s": 460,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 350,
"equals": false
},
{
"output": "yFault",
"from_s": 360,
"to_s": 450,
"equals": true
},
{
"output": "yFault",
"from_s": 460,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "mature_stop_alarm_recovers",
"description": "An unexpected run persists past its proof time, then clears immediately when proof drops.",
"inputs": {
"fan_cmd": [
{
"t": 0,
"value": true
},
{
"t": 300,
"value": false
}
],
"fan_status": [
{
"t": 0,
"value": true
},
{
"t": 460,
"value": false
}
]
},
"expect": [
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 350,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 360,
"to_s": 450,
"equals": true
},
{
"output": "yUnexpectedRun",
"from_s": 460,
"to_s": 2400,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 350,
"equals": false
},
{
"output": "yFault",
"from_s": 360,
"to_s": 450,
"equals": true
},
{
"output": "yFault",
"from_s": 460,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "pending_direction_reversal_starts_fresh_lane",
"description": "Command and status swap while fail-to-start is pending. The old lane resets and the unexpected-run lane must serve its complete independent timer.",
"inputs": {
"fan_cmd": [
{
"t": 0,
"value": true
},
{
"t": 40,
"value": false
}
],
"fan_status": [
{
"t": 0,
"value": false
},
{
"t": 40,
"value": true
}
]
},
"expect": [
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 90,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 100,
"to_s": 2400,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 90,
"equals": false
},
{
"output": "yFault",
"from_s": 100,
"to_s": 2400,
"equals": true
}
]
},
{
"name": "mature_direction_reversal_hands_off",
"description": "A mature fail-to-start reverses directly into unexpected-run. The old flag clears on the reversal tick, yFault has a real gap, and the opposite lane serves a fresh complete stop timer.",
"inputs": {
"fan_cmd": [
{
"t": 0,
"value": true
},
{
"t": 400,
"value": false
}
],
"fan_status": [
{
"t": 0,
"value": false
},
{
"t": 400,
"value": true
}
]
},
"expect": [
{
"output": "yFailToStart",
"from_s": 60,
"to_s": 390,
"equals": true
},
{
"output": "yFailToStart",
"from_s": 400,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 450,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 460,
"to_s": 2400,
"equals": true
},
{
"output": "yFault",
"from_s": 60,
"to_s": 390,
"equals": true
},
{
"output": "yFault",
"from_s": 400,
"to_s": 450,
"equals": false
},
{
"output": "yFault",
"from_s": 460,
"to_s": 2400,
"equals": true
}
]
},
{
"name": "two_short_start_mismatches_do_not_accumulate",
"description": "Two commanded-on/unproved episodes are separated by agreement; elapsed time cannot transfer across the reset.",
"inputs": {
"fan_cmd": [
{
"t": 0,
"value": false
},
{
"t": 300,
"value": true
},
{
"t": 340,
"value": false
},
{
"t": 900,
"value": true
},
{
"t": 940,
"value": false
}
],
"fan_status": false
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "two_short_stop_mismatches_do_not_accumulate",
"description": "Two proven-on/uncommanded episodes are separated by agreement; elapsed time cannot transfer across the reset.",
"inputs": {
"fan_cmd": false,
"fan_status": [
{
"t": 0,
"value": false
},
{
"t": 300,
"value": true
},
{
"t": 340,
"value": false
},
{
"t": 900,
"value": true
},
{
"t": 940,
"value": false
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "command_chatter_never_matures",
"description": "The final command chatters faster than either proof window while status is off; no continuous mismatch matures.",
"inputs": {
"fan_cmd": [
{
"t": 0,
"value": false
},
{
"t": 300,
"value": true
},
{
"t": 340,
"value": false
},
{
"t": 600,
"value": true
},
{
"t": 640,
"value": false
},
{
"t": 900,
"value": true
},
{
"t": 940,
"value": false
}
],
"fan_status": false
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "status_chatter_never_matures",
"description": "Independent proof chatters against a held-on command; every unproved interval clears before the start timer.",
"inputs": {
"fan_cmd": true,
"fan_status": [
{
"t": 0,
"value": true
},
{
"t": 300,
"value": false
},
{
"t": 340,
"value": true
},
{
"t": 600,
"value": false
},
{
"t": 640,
"value": true
},
{
"t": 900,
"value": false
},
{
"t": 940,
"value": true
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "unexpected_run_chatter_never_matures",
"description": "Independent proof chatters on against a held-off command; every unexpected-run interval clears before the stop timer.",
"inputs": {
"fan_cmd": false,
"fan_status": [
{
"t": 0,
"value": false
},
{
"t": 300,
"value": true
},
{
"t": 340,
"value": false
},
{
"t": 600,
"value": true
},
{
"t": 640,
"value": false
},
{
"t": 900,
"value": true
},
{
"t": 940,
"value": false
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "upstream_enable_raw_false_positive",
"description": "An upstream enable held true while downstream logic correctly withholds operation produces a raw fail-to-start alarm; this pins why only the final command is a valid binding.",
"inputs": {
"fan_cmd": true,
"fan_status": false
},
"expect": [
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 50,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 60,
"to_s": 2400,
"equals": true
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 50,
"equals": false
},
{
"output": "yFault",
"from_s": 60,
"to_s": 2400,
"equals": true
}
]
},
{
"name": "command_echo_raw_blind_spot",
"description": "A status echo follows the command while the device is mechanically stopped. The pair agrees and the graph stays silent, pinning why proof must be independent.",
"inputs": {
"fan_cmd": [
{
"t": 0,
"value": false
},
{
"t": 300,
"value": true
}
],
"fan_status": [
{
"t": 0,
"value": false
},
{
"t": 300,
"value": true
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "manual_local_run_is_host_excluded",
"description": "A locally commanded device is proven on while the BAS final command is off. The raw unexpected-run alarm is correct; approved service or hand operation is host NO_EVAL.",
"inputs": {
"fan_cmd": false,
"fan_status": true
},
"expect": [
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 50,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 60,
"to_s": 2400,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 50,
"equals": false
},
{
"output": "yFault",
"from_s": 60,
"to_s": 2400,
"equals": true
}
]
},
{
"name": "wrong_speed_stage_is_boolean_blind_spot",
"description": "An ECM is commanded to high speed but independent Boolean proof reports only that it runs at some speed. Both booleans agree, so a failed requested stage is outside this operation-only rule.",
"inputs": {
"fan_cmd": true,
"fan_status": true
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "passive_unit_is_not_applicable",
"description": "A passive fan coil has no honest final fan command or proof. Synthetic disagreement would alarm raw, demonstrating why the host must not instantiate this rule on convection-only units.",
"inputs": {
"fan_cmd": true,
"fan_status": false
},
"expect": [
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 50,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 60,
"to_s": 2400,
"equals": true
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 50,
"equals": false
},
{
"output": "yFault",
"from_s": 60,
"to_s": 2400,
"equals": true
}
]
}
]
}