Skip to content

Instantly share code, notes, and snippets.

@bbartling
Last active April 16, 2025 16:11
Show Gist options
  • Save bbartling/8bce2e167f0712c97dd2dd2808731e3d to your computer and use it in GitHub Desktop.
Save bbartling/8bce2e167f0712c97dd2dd2808731e3d to your computer and use it in GitHub Desktop.
GL36 Java Script Gists

❄️ Supply Air Temperature Reset – ASHRAE G36 Trim & Respond

This logic implements a modular supply air temperature reset strategy based on ASHRAE Guideline 36, section 5.16.2. VAVs issue SAT requests based on zone-level temperature demand, which are summed and used by the AHU to reset the discharge air temperature via trim and respond logic.


1️⃣ VAV Cooling Request – sat-calculate-requests.hook.js

Each VAV compares zone temperature against the occupied cooling setpoint and determines how many SAT reset requests to send.

const limit = (value, min, max) => {
    if (value > max) return max;
    if (value < min) return min;
    return value;
};

module.exports = async ({ points, sdk, groupVariables }) => {
    const CoolingBackOff = groupVariables.byLabel("CoolingLoopBackOff");
    const isInCoolingBackOffLoop = Boolean(CoolingBackOff.latestValue?.value);

    const setIsInCoolingBackOffLoop = async (status) => {
        await CoolingBackOff.write(status ? 1 : 0);
    };

    const VavEquip = points.byLabel("vav").first();
    const ZoneTemperature = points.byLabel("zone-air-temp-sensor").first();
    const ZoneTemperatureSetpoint = points.byLabel("zone-air-temp-occ-cooling-sp").first();
    const CoolingLoop = points.byLabel("cool-cmd").first();

    if (!ZoneTemperature || !ZoneTemperatureSetpoint || !CoolingLoop) {
        return { result: "success", message: "Missing required points." };
    }

    async function sendRequest(count) {
        let importanceMultiplier = Number(VavEquip?.attrs.importanceMultiplier) || 1;
        const adjustedRequests = importanceMultiplier * count;
        await groupVariables.byLabel("Cooling_SAT_Requests").write({ real: adjustedRequests });
    }

    if (isInCoolingBackOffLoop) {
        if (CoolingLoop.latestValue?.value < 85) {
            await setIsInCoolingBackOffLoop(false);
        } else {
            await sendRequest(1);
            return { result: "success", message: "Sent 1 request. Staying in loop." };
        }
    }

    const getSuppressedUntilTime = () => {
        const supressionMinutes = limit(
            Math.abs(ZoneTemperatureSetpoint.latestValue.value - ZoneTemperatureSetpoint.latestValue.value) * 5,
            0, 30
        );
        return new Date().getTime() + supressionMinutes * 60000;
    };

    const suppressedUntil = getSuppressedUntilTime() ?? 0;

    if (ZoneTemperatureSetpoint.isChanged()) {
        await groupVariables.byLabel("SuppressedUntilTime").write(suppressedUntil);
        return;
    } else if (suppressedUntil > new Date().getTime()) {
        return;
    }

    if (
        await ZoneTemperature.trueFor("2m", v => v.value > ZoneTemperatureSetpoint.latestValue.value + 5)
    ) {
        await sendRequest(3);
        return { result: "success", message: "Sent 3 requests" };
    } else if (
        await ZoneTemperature.trueFor("2m", v => v.value > ZoneTemperatureSetpoint.latestValue.value + 3)
    ) {
        await sendRequest(2);
        return { result: "success", message: "Sent 2 requests" };
    } else if (CoolingLoop.latestValue?.value > 95) {
        await sendRequest(1);
        return { result: "success", message: "Sent 1 request." };
    } else {
        await sendRequest(0);
        return { result: "success", message: "Sent 0 requests." };
    }
};

This code intelligently monitors each zone’s thermal demand by comparing the actual zone temperature to its occupied cooling setpoint, and assigns 0 to 3 cooling requests based on how far the zone is above that setpoint. The greater the deviation, the higher the number of requests sent, reflecting the urgency of cooling needed in that space. If the zone temperature exceeds the setpoint by more than 5°F for at least two minutes, it generates the maximum of three requests, signaling significant discomfort. Lesser deviations trigger fewer requests accordingly. The logic also incorporates a damper-based loop that ensures at least one request persists when the cooling command is active and the damper is fully open, even if temperature thresholds aren’t exceeded—indicating the VAV is trying to cool but can't meet the load. To avoid unnecessary oscillations and request spikes, a suppression mechanism is employed when the setpoint changes, temporarily pausing request generation to give the system time to stabilize. Each request is scaled using an optional importance multiplier, giving more weight to high-priority or critical zones. The net result is a responsive, zone-driven signal that feeds into the AHU-level SAT reset logic, ensuring that supply air temperature is dynamically adjusted based on actual, time-weighted comfort needs throughout the building.


2️⃣ AHU Request Summation – sat-sum-requests.hook.js

The AHU aggregates all SAT requests from zones into a single total value.

module.exports = async ({ points, sdk, groupVariables }) => {
  points.forEach(p => {
    if (p.latestValue.value < 0) {
      sdk.logEvent("Negative value detected");
    }
  });

  const values = points.map(x => x.latestValue.value ?? 0);
  const requests = values.reduce((a, b) => a + b, 0);
  const RequestVariable = groupVariables.byLabel("Total SAT Requests");
  await RequestVariable.write(requests);
  return { result: "success", message: `Request Total: ${requests}` };
};

3️⃣ AHU Trim & Respond – sat-trim-and-respond.hook.js

This AHU control logic adjusts the discharge air temperature setpoint based on total cooling requests and outside air temperature.

const { limit, interpolate } = require('@intellimation-optimization/lib');

module.exports = async ({ points, groupVariables, sdk }) => {
  if (sdk.groupKey === '') return { result: 'success', message: 'Dropping empty group.' };

  const minClgSAT = 55;
  const maxClgSAT = groupVariables.byLabel('maxClgSAT')?.latestValue?.value ?? 70;
  const OATMin = 60;
  const OATMax = 70;

  const totalRequests = points.where(p => p.attrs.label === 'Total SAT Requests').first();
  const SATSetpoint = points.byLabel('discharge-air-temp-sp').first();
  const outsideAirTemp = points.byLabel('air-temp-sensor').first()?.latestValue.value;

  const SP0 = maxClgSAT;
  const SPmin = minClgSAT;
  const SPmax = maxClgSAT;
  const I = 2;
  const SPtrim = 0.2;
  const SPres = -0.3;
  const SPResMax = -1.0;

  const systemStatus = points.byLabel('fan-run-cmd').first();
  const resetToInitial = systemStatus?.latestValue.value === 1 &&
                         systemStatus.latestValue.ts.getTime() === systemStatus.changeTime.getTime();

  if (resetToInitial) {
    await SATSetpoint.write({ real: SP0 });
    return;
  }

  const runTandRLoop = await systemStatus.trueFor('10m', v => v.value === 1);

  const getProportionalSetpoint = (tMax) => {
    return interpolate(
      outsideAirTemp,
      [
        { x: OATMin, y: tMax },
        { x: OATMax, y: minClgSAT }
      ],
      { min: minClgSAT, max: maxClgSAT }
    );
  };

  if (runTandRLoop) {
    if (totalRequests.latestValue.value <= I) {
      const currentSetpoint = SATSetpoint?.latestValue?.value || 0;
      const tMax = limit(currentSetpoint + SPtrim, SPmin, SPmax);
      await groupVariables.byLabel('tMax')?.write(tMax);
      const newSetpoint = getProportionalSetpoint(tMax);
      return { result: 'success', message: `Trimming to ${newSetpoint}` };
    } else {
      const currentSetpoint = SATSetpoint?.latestValue?.value || 0;
      const respondAmount = Math.max(SPres * (totalRequests.latestValue.value - I), SPResMax);
      const tMax = limit(currentSetpoint + respondAmount, SPmin, SPmax);
      await groupVariables.byLabel('tMax')?.write(tMax);
      const newSetpoint = getProportionalSetpoint(tMax);
      return { result: 'success', message: `Responding to ${newSetpoint}` };
    }
  }
};

This logic implements an advanced, weather-aware Trim and Respond strategy for dynamically resetting the AHU’s supply air temperature (SAT) in response to real-time zone-level cooling demand. It continuously monitors the total number of SAT requests generated by VAV zones and uses that count to determine how aggressively the AHU should cool. When demand is low—indicated by request counts below a defined threshold—the system trims the SAT setpoint upward incrementally, reducing cooling energy while still maintaining comfort. When demand exceeds the threshold, it responds by lowering the SAT proportionally, allowing the system to rapidly meet increasing cooling needs. To prevent overcorrection and excessive SAT swings, the logic enforces upper and lower bounds on setpoint changes and uses a rate-limited response curve to modulate reset intensity. Crucially, this logic is further refined by interpolating against the outdoor air temperature (OAT): reset actions are softened when the weather is mild and intensified when the OAT rises, allowing for seasonally adaptive cooling strategies. Additionally, the logic includes a high-OAT lockout that forces the SAT to the minimum allowable value (typically 55°F) when outdoor air exceeds 70°F, ensuring maximum cooling capacity during extreme heat. A startup safeguard is also embedded: if the AHU fan is newly energized, the SAT resets to its default value to stabilize initial system operation. Altogether, this control strategy ensures precise, demand-driven SAT management that maximizes occupant comfort, reduces energy consumption, and adapts intelligently to both indoor and outdoor conditions.


✅ Summary

  • VAVs detect unmet cooling and issue requests to the AHU.
  • The AHU sums zone requests and adjusts the SAT using trim/respond logic.
  • OAT-based interpolation ensures SAT reset is responsive to outdoor conditions.
  • Suppression periods and fan-on state detection prevent false triggers.

🌬️ Duct Static Pressure Reset – ASHRAE G36 Trim & Respond

This logic implements an automated duct static pressure reset strategy aligned with ASHRAE Guideline 36, sections 5.6.8.2 and 5.16.1.2. It uses distributed VAV-level requests with centralized AHU-level trim and respond logic to reduce fan energy while maintaining airflow delivery.


1️⃣ VAV Pressure Request – static-pressure-calculate-requests.hook.js

Each VAV evaluates its damper position and airflow delivery. If it's starved for air, it submits 1 to 3 pressure "requests" based on severity.

const { selectPoints } = require('@intellimation-optimization/lib');

const getPercentage = (numerator, denominator) => {
  return (numerator / denominator) * 100;
};

const targetAirRefs = ['Diggs_RTU3', 'Diggs_RTU4', 'Diggs_RTU7', 'Diggs_RTU8', 'Diggs_RTU9'];

module.exports = async ({ groupVariables, points, sdk }) => {
  const [VavEquip, AirFlowSetpoint, MeasuredAirflow, DamperPosition] = selectPoints(points, [
    'vav',
    'discharge-air-flow-sp',
    'discharge-air-flow-sensor',
    'damper-cmd',
  ]);

  if (!targetAirRefs.includes(AirFlowSetpoint.attrs.airRef)) {
    return { result: 'success', message: `Skipping airRef: ${AirFlowSetpoint.attrs.airRef}` };
  }

  const DamperLoop = groupVariables.find((v) => v.attrs.label === 'inDamperLoop');
  if (!DamperLoop) throw new Error('DamperLoop not found');

  const Cooling_SP_Requests = groupVariables.byLabel('Cooling_SP_Requests');
  const isInDamperLoop = Boolean(DamperLoop.latestValue?.value);

  const setIsInDamperLoop = async (status) => {
    await DamperLoop.write(status ? 1 : 0);
  };

  async function sendRequest(count) {
    let importanceMultiplier = Number(VavEquip?.attrs.importanceMultiplier) || 1;
    const adjustedRequests = importanceMultiplier * count;
    await Cooling_SP_Requests.write({ real: adjustedRequests });
  }

  if (isInDamperLoop && DamperPosition.latestValue.value > 85) {
    await sendRequest(1);
    return;
  } else {
    await setIsInDamperLoop(false);
  }

  if (
    (await MeasuredAirflow.trueFor(
      '1m',
      (v) =>
        v.value != null &&
        AirFlowSetpoint.latestValue.value > 0 &&
        getPercentage(v.value, AirFlowSetpoint.latestValue.value) < 50
    )) &&
    (await DamperPosition.trueFor('1m', (v) => v.value > 95))
  ) {
    await sendRequest(3);
  } else if (
    (await MeasuredAirflow.trueFor(
      '1m',
      (v) =>
        v.value != null &&
        AirFlowSetpoint.latestValue.value > 0 &&
        getPercentage(v.value, AirFlowSetpoint.latestValue.value) < 70
    )) &&
    (await DamperPosition.trueFor('1m', (v) => v.value > 95))
  ) {
    await sendRequest(2);
  } else if (DamperPosition.latestValue.value > 95) {
    await setIsInDamperLoop(true);
    await sendRequest(1);
  } else {
    await sendRequest(0);
  }
};

This logic evaluates the performance of each VAV box to determine whether it is adequately receiving airflow and, if not, signals the AHU to increase duct static pressure. It does this by comparing the VAV’s measured airflow against its airflow setpoint while simultaneously checking if the damper is nearly fully open—an indication that the terminal unit is attempting to compensate for insufficient pressure. When this condition is met, the code assigns a pressure request value from 1 to 3 based on how severely the airflow falls short of the setpoint, with the highest request level triggered when airflow drops below 50% of the target. A persistence mechanism—referred to as the "damper loop"—ensures that once a request is initiated due to sustained high damper position, it remains active until airflow conditions improve. Additionally, the logic filters out zones that are not part of the target control group using their airRef attributes, effectively ignoring data from rogue or non-participating zones. Each request is scaled by a configurable importance multiplier, allowing high-priority spaces to exert greater influence on the system. The final request value is written to a group variable, where it is later aggregated and used by the AHU’s trim and respond algorithm to dynamically adjust the system’s duct pressure setpoint. This ensures responsive, zone-driven control that balances comfort and efficiency.---

2️⃣ AHU Request Summation – static-pressure-sum-requests.hook.js

The AHU sums the individual VAV pressure requests into a total.

module.exports = async ({ points, groupVariables, sdk }) => {
  const requests = points.map((x) => x.latestValue.value).reduce((a, b) => a + (b || 0), 0);
  const RequestVariable = groupVariables.byLabel('Total SP Requests');
  await RequestVariable.write(requests);
  return { result: 'success', message: `sending ${requests} requests for ${sdk.groupKey}` };
};

3️⃣ AHU Trim & Respond – static-pressure-trim-and-respond.hook.js

This AHU-level logic trims or increases the duct pressure setpoint based on total requests. If the fan starts, it resets the setpoint to default.

const { limit } = require('@intellimation-optimization/lib');

module.exports = async ({ points, sdk, groupVariables }) => {
  if (sdk.groupKey === '') return { result: 'success', message: 'Ignoring empty group' };

  const totalRequests = points.where((p) => p.attrs.label === 'Total SP Requests').first();
  if (!totalRequests) {
    return { result: 'error', message: 'Missing Total Requests points in query.' };
  }

  const R = totalRequests.latestValue?.value;
  const DischargeAirPressureSp = groupVariables.byLabel('SP_Setpoint');
  const dischargeAirPressureSpValue = DischargeAirPressureSp.latestValue?.value ?? 0;

  const bounds = {
    Diggs_RTU4: { min: 0.4, max: 1.75, initial: 1.25 },
  };

  const bound = bounds[sdk.groupKey];
  if (!bound) return { result: 'error', message: 'Cannot get bounds for group.' };

  const SP0 = bound.initial;
  const SPmin = bound.min;
  const SPmax = bound.max;
  const I = 6;
  const SPtrim = -0.02;
  const SPres = 0.04;
  const SPResMax = 0.08;

  const systemStatus = points.byLabel('run-cmd').first();
  const resetToInitial = systemStatus?.latestValue.value === 1 && systemStatus.isChanged();

  if (resetToInitial) {
    await DischargeAirPressureSp.write({ real: SP0 });
    return { result: 'success', message: `Reset to initial ${SP0}` };
  }

  const runTandRLoop = await systemStatus.trueFor('10m', (v) => v.value === 1);

  if (runTandRLoop) {
    if (R <= I) {
      const newSetpoint = limit(dischargeAirPressureSpValue + SPtrim, SPmin, SPmax);
      await DischargeAirPressureSp.write({ real: newSetpoint });
      return { result: 'success', message: `Reset to ${newSetpoint}` };
    } else {
      const respondAmount = Math.min(SPres * (R - I), SPResMax);
      const newSetpoint = limit(dischargeAirPressureSpValue + respondAmount, SPmin, SPmax);
      await DischargeAirPressureSp.write({ real: newSetpoint });
      return { result: 'success', message: `Reset to ${newSetpoint}` };
    }
  }
};

This AHU-level logic implements a Trim and Respond static pressure reset strategy to dynamically adjust the duct static pressure setpoint based on zone-level demand. It receives the total number of pressure requests issued by the VAV boxes and uses this value to determine whether to trim the setpoint downward to save fan energy, or respond by increasing the setpoint to meet rising airflow needs. If the total number of requests is at or below a defined threshold, the logic trims the static pressure in small, controlled steps—ensuring that pressure isn’t unnecessarily high during periods of low demand. If requests exceed the threshold, it increases the pressure setpoint proportionally, with a cap on the maximum rate of change to prevent overshoot. Each AHU uses preconfigured static pressure bounds to ensure the setpoint remains within safe operational limits. The logic also includes a startup safeguard: if the AHU fan has just been turned on, the pressure is reset to a known initial value to ensure consistent system behavior on startup. The entire sequence runs only when the fan has been proven on for a minimum runtime duration, reducing the risk of reacting to transient states. Altogether, this logic ensures that duct static pressure is continually optimized based on real-time zone demand, improving energy efficiency while maintaining airflow performance.


✅ Summary

  • Zone-level logic ensures VAVs request pressure only when truly needed.
  • AHU summation aggregates distributed demand into one variable.
  • Trim & Respond control balances energy savings with airflow reliability.
  • Loop persistence and initial state resets stabilize long-term operation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment