Duct Static Pressure Setpoint Controller Full Example

In this section we will do a code walk-through of the application file dsp-sp-cntl_B.js.  This application starts with the file dsp-sp-cntl_A.js that was described in detail on the page Internal Application Self Instantiation.  The walk-through here will discuss the changes made to complete the progression of this example.

This internal application leverages the IAP/MQ API to accomplish area controller functions that typically require system level access.  More precisely, the application will monitor a key performance indication variable based on the type SNVT_hvac_status on any number of devices connected to the system, and use this monitored data to establish a setpoint reset.  So far, our example application can create itself in the system, and monitor updates to its own inputs and configuration properties.  Next we will set up the application monitor 1-n VAV controllers managed by the SmartServer, and fill out the sequence with a rudimentary algorithm that makes no claims of completeness.  Follow along in VS Code by viewing the file dsp-sp-cntl_B.js.

  1. The first modification to the first version of this application is at line 46.  The monitorSpec object array defines target devices and a datapoint of interest.  Notice how the applications supports both EVB versions of the VAV simulator application.

  2. Lines 68-70 define additional Map objects for key/value access to zone demands and device state keyed by IAP/MQ device handle.

    dsp-sp-cntl_B.js
    // This application works with VAV controller SNVT_hvac_status values to drive the control sequence
    // This object defines the targeted device type based on the PID.  In this example, the PID is for the
    // VAVsim6000 application that should be running on one or more FT 6050 EVBs or FT 5000 EVBs 
    // attached to your SmartServer.  Note that event:true, and rate:0 apply in the DMM managed system.
    // Use rate: 60, event false in IMM defices
    let monitorSpecs = [
        {
            pid: "9000015600040461",    // EVB 6050 target: VAVsim6000
            nvList: [
                // TODO: Chose only one of the following.  Event false and non-zero polling for IMM mode.  
                // event:true is used in DMM systems
                {ptPath: "device/0/nvoVAVstatus", ms:{rate: 0, report: "any", threshold:0, event:true}}, 
            ]
        },
        {
            pid: "9000015600040451",    // EVB 5000 target: VAV5000
            nvList: [
                // TODO: Chose only one of the following.  Event false and non-zero polling for IMM mode.  
                // event:true is used in DMM systems
                {ptPath: "device/0/nvoVAVstatus", ms:{rate: 0, report: "any", threshold:0, event:true}}, 
            ]
        }
    ];
    // Map objects are used for keyed (device handle used for key) access to various aspects related to 
    // monitored devices, and this application logic
    
    const myInputs = new Map();
    const devStates = new Map();
    const zoneDemand = new Map();
    const rougeZones = new Map();
    
    let glpPrefix="glp/0";  // this will include the sid once determined
    let subscribtionsActive = false;
    let myAppTimer;
  3. In this version of the application, the handleSid() function is modified to subscribe to ../fb/dev/lon/+/sts (line 185).  The device status has two important roles:
    1. At startup, devices that are targeted in the monitorSpecs array that are state:'provisioned' and health:'normal' will have the targeted datapoint monitoring object parameters set.
    2. If a device is state:'deleted' or health:'down', the device's cooling demand value is dropped from consideration.

  4. The application subscribes to the topic for ../ev/data to monitor the nvoVAVstatus data events variable (line 187)

    dsp-sp-cntl_B.js
    function handleSid (sidMsg) {
        // Assuming the SID topic is a string sidMsg
        let nowTs = new Date(); // Seconds TS good enough
        if (typeof(sidMsg) === typeof("xyz")) {
            if (sidMsg.length > 0) {
                glpPrefix += `/${sidMsg}`;
                console.log(`${nowTs.toISOString()}- SmartServer SID: ${sidMsg}`);
                // Note how the "+" wild card is used to monitor device status for 
                // any lon device.  Note, once we know the SID, we need to avoid
                // adding multiple listeners.  Subscbribe once and only once              
                if (!subscribtionsActive) { 
                    // The following fb topic will capture the fact that the internal lon device has been created/provisioned
                    client.subscribe (`${glpPrefix}/fb/dev/lon/${myAppIf.myDeviceHandle}/if/#`);
                    // NVs and CP updates appear on this event channel
                    client.subscribe (`${glpPrefix}/ev/updated/dev/lon/type/${myAppIf.myAppPID}`);
                    // This IAP/MQ topic reports the status of all devices on the lon channel                      
                    client.subscribe (`${glpPrefix}/fb/dev/lon/+/sts`);
                    // Subscribe to ALL data events
                    client.subscribe (`${glpPrefix}/ev/data`,{qos : 0})               
                    client.unsubscribe (sidTopic);
                    subscribtionsActive = true;
                } else {
                    console.log(`${nowTs.toISOString()} - Redundant SID topic message`);
                }
            } else {
                // We are not provisioned.
                    sid = undefined;
                    cosole.log(`${nowTs.toISOString()} - [${s}] Problem with SID payload.`);
            }
        } else {
            console.error('The sid topic returned an unexpected payload type.')
        }
    }
  5. The monitorSpecs object array defines the program ID and the datapoint name that is the focus of this application.  The IAP/MQ topic ../fb/dev/lon/+/sts  is a broad topic that reports the status of all LON devices on the SmartServer.  The function qualifidDevice() (line 218) will return the index of the monitorSpecs object if the device type is defined in monitorSpecs.  A value of -1 returned by this function signals that the device in that sts message is not a device of interest.

  6. The function handleDeviceSts() (line 229) is only called if the sts message passed the qualifiedDevice() test.  If the device is state:'provisioned' and health.'normal', the monitor parameters for the targeted datapoint is set.  See the comments in the code regarding the different monitor object parameters to use when you are in IMM mode.  This example is using DMM and uses event driven updates to monitor the devices rather than polling.  See the comments in the code if you plan to run this application on a SmartServer using IMM mode so the monitor parameters can be changed to support polling.

    dsp-sp-cntl_B.js
    function handleMyPointUpdates (updateEvent){
        // The values published in calculateDspSP() will generate updated events
        if (updateEvent.datapoint.search("nvo") !== -1)
            return;
        // Record value in the Interface map
        myInputs.set(updateEvent.datapoint, updateEvent.value);
        // If required, at check for nvi and CPs values that require generate a event driven response
        if (updateEvent.datapoint === "nviEnable")
            myAppIf.nvoDspSP = updateEvent.value.state == 1 ? myInputs.get("cpDefaultDspSP") : myInputs.get("cpMinDspSP");
        if (updateEvent.datapoint === "cpLoopInterval") {
            clearInterval(myAppTimer);
            myAppTimer = setInterval(calculateDspSP, updateEvent.value * 1000);
        }
        console.log(`${updateEvent.datapoint}: ${JSON.stringify(updateEvent.value)}`);
                
    }
    function qualifiedDevice (stsMsg) {
        // qualify the device as a targeted for monitoring by returning the monitorSpecs Index            
        let i;
        for (i = 0; i < monitorSpecs.length; i++)
            if (stsMsg.type === monitorSpecs[i].pid)
                return i;
        if (i === monitorSpecs.length)
            return -1;  
    }
    // Function enables point monitoring for provisioned targeet devices that are healthy
    // returning true if monitoring is setup
    function handleDeviceSts (devHandle, stsMsg, monSpecIndex) {
        let dpTopic;
        let nowTs = new Date(); // Seconds TS good enough
    
        if (stsMsg.state === "provisioned") {
            // define the monitorObj assuming provisioned and healthy devices
            let monitorObj = {}; 
       
            // To track the health of monitored devices the devState map is used to determine if the monitoring has be set
            let setMonitorParams = false;
            if (stsMsg.health === "normal") {
                if (!devStates.has(devHandle)) { // First time through, set up monitoring
                    devStates.set(devHandle,"normal");
                    setMonitorParams = true;
                } else { // Transistion from down or unknown to normal, set monitoring
                    if(devStates.get(devHandle) !== "normal") {
                        devStates.set(devHandle, "normal");
                        setMonitorParams = true;                            
                    }
                } 
                console.log(`${nowTs.toISOString()} - Device: ${devHandle} (S/N: ${stsMsg.addr.domain[0].subnet}/${stsMsg.addr.domain[0].nodeId}) is Up.`);
            } else { 
                devStates.set(devHandle, stsMsg.health);
                console.log(`${nowTs.toISOString()} - Device: ${devHandle} (S/N: ${stsMsg.addr.domain[0].subnet}/${stsMsg.addr.domain[0].nodeId}) is ${stsMsg.health}.`);
                return;
            }
            // Setup monitoring of the nvoVAVstatus variable
            monitorSpecs[monSpecIndex].nvList.forEach(function(dp) {
                // setMonitorRate is true at the health transitions and at startup.  
                if (setMonitorParams) {             
                    monitorObj = dp.ms;
                    dpTopic = `${glpPrefix}/rq/dev/lon/${devHandle}/if/${dp.ptPath}/monitor`;
                    console.log (`${nowTs.toISOString()} - Set Monitor: ${dpTopic}, Interval: ${monitorObj.rate} Event: ${monitorObj.event}`);
                    client.publish (
                        dpTopic, 
                        JSON.stringify(monitorObj), 
                        {qos:2,retain: false}, 
                        (err) => {
                            if (err != null)
                                console.error(`${nowTs.toISOString()} - Failed to update Nv`);
                        }     
                    );
                }                   
            });
            return;
        } else {
            devStates.delete(devHandle);  // If not provisioned, drop all consideration
            zoneDemand.delete(devHandle);
            console.log(`${nowTs.toISOString()} - Device: ${devHandle} (S/N: ${payload.addr.domain[0].subnet}/${payload.addr.domain[0].node}) is ${d1.state}`);
        }   
        return;
    }
  7. The mqtt object client.on() message handler has additional IAP/MQ topic handlers to service additional IAP/MQ traffic.  Take a moment to review the changes below.  Take note of how the updates to the zoneDemand Map object record the latest value of the cool_output field of the nvoVAVstatus data event.

    dsp-sp-cntl_B.js
    // IAP/MQ. MQTT message handler. 
    client.on(
        "message", 
        (topic, message) => {
        try {
            const payload = JSON.parse(message);
            let devHandle;  
            let nowTs = new Date(); // Seconds TS good enough
            
            if (topic === sidTopic) {
                // Assuming the SID topic is a string payload
                handleSid(payload);
            }  
    
            // This topic applies to only one device in the system, this internal device
            if (topic.endsWith(`${myAppIf.myDeviceHandle}/if/${myAppIf.myFbName}/0`)) {
                // Note - The payload is null when the device is in the deleted state
                if (payload == null)
                    return;
                //     
                if (!myAppIf.initialized) {  
                    client.unsubscribe(`${glpPrefix}/fb/dev/lon/${myAppIf.myDeviceHandle}/if/#`); 
                    initializeInputs(payload);
                    myAppIf.initialized = true;
                    console.log("myDev.1 internal device interface is ready!");
                }
            }
    
            // The IAP channel ../ev/updated will carry updates for NVs/CPs for internal Lon Devices.
            // note that more than one instance could exist.  The Updated event has different
            // structure than a data event.
            if (topic.endsWith (`/ev/updated/dev/lon/type/${myAppIf.myAppPID}`)) {
                if (payload.handle == myAppIf.myDeviceHandle) {
                    handleMyPointUpdates(payload);
                }
            }
            // Process glp/0/{sid}/fb/dev/lon/+/sts messages.  Device state and health are used to determine if the 
            // data events are to be considered valid
            if (topic.endsWith ("/sts")) {  
                let monSpecIndex;
                monSpecIndex = qualifiedDevice(payload);
                if (monSpecIndex == -1)
                    return;
                devHandle = topic.split("/")[6];
                handleDeviceSts (devHandle, payload, monSpecIndex);    
            }
            // Data events will arrive on this topic.  In this example, the monitoring for the VAV 
            // SNVT_hvac_status is set to events:true (DMM mode).  The LTE engine will create a bound connection
            // to the network variable.
            if (topic.endsWith ("/ev/data")) {
                // Payload is a DP update, but the /ev/data topic could include many other data
                // events that are used by this application.
                let logRecord;
                let dpState;
                let pointPath;
    
                // <TS>,"<PointPath>","","","PointState,"","<value>"
                // Build up the to the <value>.  Making a log record that matches
                // SmartServer 2 log format
                devHandle =  payload.topic.split("/")[6];
                // Only looking for data events from targed devices
                if (devStates.has(devHandle)) {
                    if (devStates.get(devHandle) == "normal") {
                        zoneDemand.set(devHandle, payload.data.cool_output);
                    } else // Prevent stale data from being used in the control
                        zoneDemand.delete (devHandle);  
                    dpState = devStates.get(devHandle) == "normal" ? "ONLINE" : "OFFLINE";
                    pointPath = `${payload.message.split("/")[0]}.cool_output:`;
                    logRecord = `${devHandle}/../${pointPath} ${payload.data.cool_output} ${dpState}`; 
                    console.log(logRecord);     
                }
            }
            // The IAP channel ../ev/updated will carry updates for NVs/CPs for internal Lon Devices.
            // Note that more than one instance could exist.  The Updated event has different
            // structure than a data event.
            if (topic.endsWith (`/ev/updated/dev/lon/type/${myAppIf.myAppPID}`)) {
                if (payload.handle == myAppIf.myDeviceHandle) {
                    // The values published in calculateDspSP() will generate updated events
                    if (payload.datapoint.search("nvo"))
                        return;
                    // If required, checks for nvi values that must generate a event driven response
                    if (payload.datapoint === "nviEnable") {
                        myAppIf.nvoDspSP = payload.datapoint.value.state == 1 ? myInputs.get("cpDefaultDspSP") : myInputs.get("cpMinDspSP");    
                        client.publish(
                            `${glpPrefix}/rq/dev/lon/${myAppIf.myDeviceHandle}/if/${myAppIf.myFbName}/0/nvoDspSP/value`,
                            JSON.stringify(myAppIf.nvoDspSP)
                        )   
                    }
                    console.log(`${payload.datapoint}: ${JSON.stringify(payload.value)}`);
                }
            }
        } catch(error) {
            console.error("MQTT Message: " + error);
        }
    }   // onMessage handler
    );  // onMessage registration
    
    
  8. In this version of the application, the calculateDspSP() function is filled out to implement the control sequence.  You are welcome to not be impressed by this control sequence, which implements a simple proportional control to increase or decrease the duct static pressure setpoint based on the maximum cooling demand reported by the monitored simulated VAVs.  Many improvements are possible, particularly with some of the algorithm parameters in the object myAppIf that really should by configuration properties, such as the gain and deadband.  What you should note is how the network variable outputs of the internal application are written by publishing to the IAP/MQ topic ../rq/dev/lon/if/SpControl/[nv name]/value.  Also notice how the nviEnable input network variable is used to control whether the nvoDspSp value is published.      

    dsp-sp-cntl_B.js
    function calculateDspSP() {
        let nowTs = new Date(); // Seconds TS good enough
        if (myAppIf.startup) {
            clearInterval (myAppTimer);
            // Setup regular interval processing of algrithm
            myAppTimer = setInterval(calculateDspSP, myInputs.get("cpLoopInterval") * 1000);
            myAppIf.startup = false;
        }
        if (zoneDemand.size > 0) {
            let minVal = 100.0;
            let maxVal = 0.0;
            let sumDemand = 0.0;
            zoneDemand.forEach((value)=> {
                sumDemand += value;
                if (value < minVal)
                    minVal = value;
                if (value > maxVal)
                    maxVal = value;    
            });
            myAppIf.nvoMaxDemand = maxVal;
            myAppIf.nvoMinDemand = minVal;
            myAppIf.nvoAvgDemand = sumDemand/zoneDemand.size;
        }
    
        // Seek to keep the maximum zoned demand to 90%.  When a zone exceeds 95%, increase Dsp
        // Stop the adustment until when the pressure dips below 90%.  When the maximum box demand
        // drops below 85%, adjust Dsp downward by not a the 
        if (myInputs.get("nviEnable").state) {
            if (myAppIf.nvoMaxDemand == undefined)
                return;
            if ((myAppIf.nvoMaxDemand  >= 90 - myAppIf.deadband) && (myAppIf.nvoMaxDemand  <= 90 + myAppIf.deadband))
                return; // No adjustment need
            
            let error = myAppIf.nvoMaxDemand  - 90;
            myAppIf.nvoDspSP += parseInt(myAppIf.gain * error); 
            // Clip the DspSP 
            if (myAppIf.nvoDspSP  < myInputs.get("cpMinDspSP")) 
                myAppIf.nvoDspSP  = myInputs.get("cpMinDspSP");
            if (myAppIf.nvoDspSP  > myInputs.get("cpMaxDspSP"))    
                myAppIf.nvoDspSP  = myInputs.get("cpMaxDspSP");
        }
        client.publish(
            `${glpPrefix}/rq/dev/lon/${myAppIf.myDeviceHandle}/if/SpController/0/nvoDspSP/value`,
            JSON.stringify(myAppIf.nvoDspSP)
        );
        client.publish(
            `${glpPrefix}/rq/dev/lon/${myAppIf.myDeviceHandle}/if/SpController/0/nvoMinDemand/value`,
            JSON.stringify(myAppIf.nvoMinDemand)
        );
        client.publish(
            `${glpPrefix}/rq/dev/lon/${myAppIf.myDeviceHandle}/if/SpController/0/nvoMaxDemand/value`,
            JSON.stringify(myAppIf.nvoMaxDemand)
        );
        client.publish(
            `${glpPrefix}/rq/dev/lon/${myAppIf.myDeviceHandle}/if/SpController/0/nvoAvgDemand/value`,
            JSON.stringify(myAppIf.nvoAvgDemand)
        );
    }
  9. The final task is to connect this application to the simulated Discharge Air Controller.  Confirm the device name for this device is DAC in the Device widget, and use the Device widget action. Import device types to apply the connection file Connect DAC.csv.  This will establish the binding form the internal device to the Discharge Air Controller simulation.  In release 2.51.001, it was necessary to reboot the SmartServer for SmartServer internal device to take the correct binding information.

    This completes our code walk-through.  You can test this code by running from the VS code debugger after editing your launch.json file to point to the application dsp-sp-cntl_B.js.

Running on the SmartServer

Follow these steps to install and run this application to the SmartServer:

  1. Connect to the SmartServer using winscp as user apollo.

  2. Create the directory /var/apollo/data/apps/dsp-sp-cntl .  

  3. Copy the files dsp-sp-cntl_B.js, package.json, package-lock.json from the ..\dspSPController folder to the directory create in step 2.

  4. Connect by SSH as user apollo and make the directory created in step 2 your current working directory by typing:  cd  /var/apollo/data/apps/dsp-sp-cntl   

  5. You can now run the application directly by typing  node dsp-sp-cntl_b.js

Applications run from the console are terminated by the operating system when the console session is ended or disconnected.  To run this application as a service, the file dspSPcntl.conf should be copied to the folder /etc/supervirsor/conf.d on the SmartServer.  This file contains the path that step 2 above required you to enter.  If you used a different path (remember character case matters in Linux), you would need to edit the contents of this file to match the path used at lines 3, 11, and 12.

When you reboot the SmartServer, this application will run as a service. 

  • You can use sudo supervisorctl in the SSH console to start and stop the service. 
  • You can monitor the application console output by typing: tail -f /var/log/supervisor/stdout-dspcntl.log
  • To prevent the application from running, you can change the extension of /etc/supervisor/conf.d/dspSPcntl.conf to something else like .bak. or delete the file and reboot. 
  • From sudo supervisorctl, you can stop and restart the application, but these commands do not persist between reboots.

Testing the Application

If you reset you network of simulated VAVs, they will start at 50% cooling demand and ramp to 90%, which will take about 80s.  While the devices report low demand (below 85%) the discharge air duct static pressures setpoint will ramp down.  If a real system were in place, the decreasing duct pressure would result in increased cooling demand.  If you use the right button on the EVB to increase the demand of one device to above 95%, you will observe an increasing static pressure.  If you return to 90% demand (+/-2%) for all the devices, the change in setpoint will stop.  If you take all simulated VAVs to a demand below 85%, the dsp-sp-cnt_B.js application will lower the duct static pressure setpoint until all VAVs are back in the optimal range of 90% +/-2%.  To the BAS experts that commission commercial VAV systems, EnOcean acknowledges this control sequence is overly simple and the timing for reset calculations is likely too aggressive. The intention is about being able to bring some dynamic to this example. 

Next Step

Return to Developing Internal Apps and Custom Drivers