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.
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 }
}
]
}
}
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}.`);
}
};
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 PythonFaultConditionOne
,FaultConditionTwo
, etc. classes. - Standardized Structure: To create a hook for
FaultConditionTwo
, you would:- Copy
fc01-ahu-low-static.json
tofc02-some-other-fault.json
. - Update the
name
,entryPoint
,points
query, andgroupVariables
to match the requirements of FC2. - Copy
fc01-ahu-low-static.js
tofc02-some-other-fault.js
. - Modify the "SELECT POINTS", "GET PARAMETERS", and "IMPLEMENT FAULT LOGIC" sections to implement the specific equation for FC2.
- Copy
- 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.