Skip to content

Instantly share code, notes, and snippets.

@bbartling
Last active July 21, 2025 15:50
Show Gist options
  • Save bbartling/a5ab33d9961e5e8d217845e2d94e02ea to your computer and use it in GitHub Desktop.
Save bbartling/a5ab33d9961e5e8d217845e2d94e02ea to your computer and use it in GitHub Desktop.
Python converted HVAC Fault Equation to JavaScript with AI like magic

Of course. Here is a conceptual model and implementation for an AHU Fault Condition 1 hook based on the Normal Framework (NF) architecture, designed for reusability across your other fault detection rules.

This approach translates the logic from your Python script into a serverless JavaScript function (a "hook") that runs within the NF environment. It operates on real-time data points, using configurable variables for thresholds, which is ideal for a scalable Fault Detection and Diagnostics (FDD) application.


Hook Definition (fc01-ahu-low-static.json)

First, you define the hook's configuration. This JSON file tells the Normal runtime what data to select, how to group it, when to run the code, and what settings (variables) to make available. Each fault condition will have its own definition file.

This configuration creates a hook that:

  • Triggers whenever new data arrives for the specified points (MODE_ON_DATA).
  • Runs separately for each piece of equipment that has an equipRef attribute (e.g., once per AHU).
  • Creates tunable parameters (groupVariables) for each AHU, complete with default values.
  • Creates a new output point (fc01_fault_flag) to store the fault status.
{
  "hook": {
    "name": "fc01-ahu-low-static",
    "entryPoint": "/hooks/fc01-ahu-low-static.js",
    "mode": "MODE_ON_DATA",
    "points": {
      "query": {
        "or": [
          { "label": "duct-static-pressure" },
          { "label": "duct-static-pressure-setpoint" },
          { "label": "supply-fan-speed" }
        ]
      }
    },
    "groups": ["equipRef"],
    "groupVariables": [
      {
        "label": "fc01_fault_duration",
        "attrs": { "equipRef": "{{ Points.First.Attrs.equipRef }}" },
        "default_value": { "string": "5m" }
      },
      {
        "label": "duct_static_inches_err_thres",
        "attrs": { "equipRef": "{{ Points.First.Attrs.equipRef }}" },
        "default_value": { "real": 0.1 }
      },
      {
        "label": "vfd_speed_percent_max",
        "attrs": { "equipRef": "{{ Points.First.Attrs.equipRef }}" },
        "default_value": { "real": 99.0 }
      },
      {
        "label": "vfd_speed_percent_err_thres",
        "attrs": { "equipRef": "{{ Points.First.Attrs.equipRef }}" },
        "default_value": { "real": 5.0 }
      },
      {
        "label": "fc01_fault_flag",
        "attrs": {
          "equipRef": "{{ Points.First.Attrs.equipRef }}",
          "marker": "",
          "point": ""
        },
        "default_value": { "real": 0 }
      }
    ]
  }
}

Hook Logic (/hooks/fc01-ahu-low-static.js)

This JavaScript file contains the business logic for Fault Condition 1. It directly implements the fault equation from your Python script using the data and parameters provided by the NF runtime.

The logic checks if the duct pressure is too low while the fan is at maximum speed. To prevent nuisance alarms, it uses the point.trueFor() method to ensure the condition has been continuously met for a configurable duration (e.g., 5 minutes) before setting the fault flag.

/**
 * Fault Condition 1: AHU low duct static pressure fan fault.
 * Detects if duct static pressure is below its setpoint minus a threshold,
 * while the supply fan is running at or near its maximum speed.
 *
 * Fault Equation: fc1_flag = 1 if (DP < DPSP - εDP) and (VFDSPD >= VFDSPD_max - εVFDSPD) for N consecutive values else 0
 */
module.exports = async ({ points, sdk, groupVariables }) => {
  // 1. SELECT POINTS: Get the required data points for this AHU group using their labels.
  const ductStatic = points.byLabel('duct-static-pressure').first();
  const ductStaticSp = points.byLabel('duct-static-pressure-setpoint').first();
  const supplyVfdSpeed = points.byLabel('supply-fan-speed').first();

  // 2. GET PARAMETERS: Get the tunable parameters and the output point from group variables.
  const faultDuration = groupVariables.byLabel('fc01_fault_duration').first();
  const staticErrThres = groupVariables.byLabel('duct_static_inches_err_thres').first();
  const vfdMax = groupVariables.byLabel('vfd_speed_percent_max').first();
  const vfdErrThres = groupVariables.byLabel('vfd_speed_percent_err_thres').first();
  const faultFlagPoint = groupVariables.byLabel('fc01_fault_flag').first();

  // Verify all necessary points and variables exist before running logic.
  if (!ductStatic || !ductStaticSp || !supplyVfdSpeed || !faultDuration || !staticErrThres || !vfdMax || !vfdErrThres || !faultFlagPoint) {
    sdk.logEvent(`FC01: Missing required points or variables for group: ${sdk.groupKey}. Skipping run.`);
    return;
  }

  // 3. IMPLEMENT FAULT LOGIC: Translate the fault equation into code.

  // Normalize VFD speed from percentage (0-100) to fraction (0-1) for consistent calculation.
  // This matches the behavior in the original Python script.
  const normalize = (value) => (value > 1.0 ? value / 100.0 : value);

  const vfdSpeedVal = normalize(supplyVfdSpeed.latestValue.real);
  const vfdMaxVal = normalize(vfdMax.latestValue.real);
  const vfdErrThresVal = normalize(vfdErrThres.latestValue.real);

  // Check if the fault condition is currently true.
  const isStaticLow = ductStatic.latestValue.real < (ductStaticSp.latestValue.real - staticErrThres.latestValue.real);
  const isFanMax = vfdSpeedVal >= (vfdMaxVal - vfdErrThresVal);
  const isFaultActive = isStaticLow && isFanMax;

  // 4. CHECK DURATION & WRITE OUTPUT: Use trueFor to confirm the condition is stable.
  // This avoids nuisance alarms by ensuring the combined condition of all points has been met for the required duration.
  // We check this by creating a synthetic history check on one of the primary trigger points.
  const durationStr = faultDuration.latestValue.string;
  const faultSustained = await ductStatic.trueFor(durationStr, () => isFaultActive);

  // Set the fault flag based on whether the condition has been sustained.
  const newFaultValue = faultSustained ? 1 : 0;

  // Only write a new value if the fault status has changed to minimize database writes.
  if (faultFlagPoint.latestValue.real !== newFaultValue) {
    await faultFlagPoint.write({ real: newFaultValue });
    sdk.logEvent(`FC01: Fault status for group ${sdk.groupKey} changed to ${newFaultValue}.`);
  }
};

Reusability for Other Faults

This structure is highly reusable and provides a template for your next 14 fault conditions.

  • Modular Design: Each fault is an independent hook (.json + .js file). This isolates logic, making it easy to develop, test, and debug one fault at a time without affecting others. This mirrors the modularity of your Python FaultConditionOne, FaultConditionTwo, etc. classes.
  • Standardized Structure: To create a hook for FaultConditionTwo, you would:
    1. Copy fc01-ahu-low-static.json to fc02-some-other-fault.json.
    2. Update the name, entryPoint, points query, and groupVariables to match the requirements of FC2.
    3. Copy fc01-ahu-low-static.js to fc02-some-other-fault.js.
    4. Modify the "SELECT POINTS", "GET PARAMETERS", and "IMPLEMENT FAULT LOGIC" sections to implement the specific equation for FC2.
  • Centralized Configuration: All thresholds and time delays are stored as groupVariables. This allows building operators or technicians to tune the FDD logic for specific AHUs directly through the Normal UI without ever touching the JavaScript code, significantly improving maintainability.

Excellent question. Converting your fault detection logic to ASHRAE's Control Description Language (CDL) represents a shift from a platform-specific implementation to a standardized, vendor-neutral definition.

Here’s a breakdown of what CDL is and how you would approach converting your Fault Condition 1 logic.

What is ASHRAE's Control Description Language (CDL)?

ASHRAE CDL, being developed under the proposed standard ASHRAE 231P, is a formal language designed to standardize the way building automation control sequences are written.

Think of it this way:

  • Currently: Control logic is often described in plain English on engineering documents. This text is ambiguous and must be manually translated into a proprietary programming language by each control vendor, often leading to errors and inconsistencies.
  • With CDL: The logic is defined in a precise, machine-readable format. This formal definition acts as the unambiguous "source of truth." It can be automatically validated, simulated, and even used to help generate the vendor-specific code.

For your project, CDL provides a way to define your fifteen fault conditions in a format that is independent of the Normal Framework, Python, or any other platform.

Converting Fault Condition 1 to CDL

The process involves abstracting the logic from your Python code and JavaScript hook into CDL's formal structure.

Your FaultConditionOne class already provides a perfect blueprint with its clear separation of inputs, parameters, and logic.

Here is a conceptual example of what your FC-1 logic would look like in a simplified, CDL-like format.

//---------------------------------------------------------
// FAULT DEFINITION: FC-01 - AHU Low Duct Static Pressure
// DESCRIPTION: Duct static is too low when fan is at full speed.
//---------------------------------------------------------

// Define the data points required from the building automation system.
// This corresponds to the INPUT_COLS in your Python script.
INPUTS
  Duct_Static_Pressure          REAL  (unit="inches_of_water");
  Duct_Static_Pressure_Setpoint REAL  (unit="inches_of_water");
  Supply_Fan_Speed              REAL  (unit="%");
END_INPUTS

// Define the tunable parameters for the fault.
// This corresponds to the FAULT_PARAMS in your Python script.
PARAMETERS
  Static_Pressure_Err_Thres     REAL  (default=0.1, unit="inches_of_water");
  VFD_Speed_Max                 REAL  (default=99.0, unit="%");
  VFD_Speed_Err_Thres           REAL  (default=5.0, unit="%");
  Fault_Duration                DURATION (default=MINUTES(5));
END_PARAMETERS

// Define the output point for the fault flag.
OUTPUTS
  FC01_Fault_Flag               BOOLEAN;
END_OUTPUTS

// Define the core fault detection logic.
// This implements the equation from your Python script.
LOGIC
  // Define the two conditions for the fault.
  Condition_Static_Low := (Duct_Static_Pressure < (Duct_Static_Pressure_Setpoint - Static_Pressure_Err_Thres));
  Condition_Fan_Max    := (Supply_Fan_Speed >= (VFD_Speed_Max - VFD_Speed_Err_Thres));

  // The final fault is true only if both conditions have been met continuously.
  // This replaces the stateful logic or the .trueFor() method from the hook.
  IF (Condition_Static_Low AND Condition_Fan_Max) FOR DURATION >= Fault_Duration THEN
    SET FC01_Fault_Flag TO TRUE;
  ELSE
    SET FC01_Fault_Flag TO FALSE;
  END_IF
END_LOGIC

Key Differences from Your Current Approach

Aspect Normal Framework Hook ASHRAE CDL
Nature Procedural & Platform-Specific: It's JavaScript code written to execute on a specific runtime (NF). Declarative & Standardized: It's a formal description of logic, independent of any platform.
Execution The NF runtime invokes the JS file when data changes or on a schedule. The CDL file is not executed directly. It is used as a reference or input for tools that generate executable code.
Purpose To implement the fault detection logic. To define and verify the fault detection logic in an unambiguous way.
Verification Logic is verified by testing the running code against live or simulated data. Logic can be formally verified and simulated by CDL-aware tools before deployment.

Benefits of Converting to CDL

  1. Portability: The same CDL file could be used to generate hooks for the Normal Framework, a Python script for a different system, or proprietary code for a hardware-based controller. This drastically reduces vendor lock-in.
  2. Clarity and Source of Truth: It eliminates ambiguity. The CDL file becomes the definitive source for how a fault is defined, making it easier for engineers, programmers, and building operators to be on the same page.
  3. Automated Validation: CDL allows for automated tools to check your logic for correctness (e.g., are you using the right data types? Is the logic sound?) before it ever gets deployed to a building, saving significant commissioning time.
  4. Improved Documentation: The CDL file itself serves as precise, machine-readable documentation for your FDD library.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment