Using WebSockets with Custom Web Pages

WebSockets are supported with SmartServer 2.8 and higher.

This page shows how to create a SmartServer hosted web page that uses WebSockets to get datapoint updates and consists of the following:

WebSockets Overview

The SmartServer uses WebSockets in two ways: a general purpose WebSocket which is used to get datapoint updates and Data Log WebSockets used to get log data.  This section discusses how to develop a Web page using the SmartServer general purpose WebSocket. For data log WebSockets, see Data Log WebSocket Queries and Parameters.

The general purpose WebSocket is a persistent connection between your web page and the SmartServer and allows the web page to receive live datapoint updates for subscribed datapoints. For example, the CMS web pages use WebSockets.

If you issue an on-demand GET request using a max_age parameter specifying the maximum age time, then the GET response returns the cached value. Sometime later, you will see a WebSocket update for this datapoint. If you were not using a WebSocket, then you would have to issue a second GET request to see a datapoint update. Therefore, a WebSocket speeds up the process for getting datapoint updates. WebSocket updates typically contain live datapoint values, not cached values. If the last datapoint value is older than the time specified by max_seconds, then the SmartServer goes on the wire to read the datapoint value.

WebSockets also eliminate the problem of datapoint feedback flickering for some applications when you use two datapoints for a single web page HTML element, like image swappers or textfields (one input datapoint for writing and one output datapoint for feedback). When you are not using WebSockets, after changing the value for a multi-datapoint HTML element on the web page, the value can seem to flicker between new value, the old value, and then the new value again. This is known as datapoint feedback flickering and is caused by using the GET response cached output datapoint value. WebSockets eliminate this issue since the HTML elements values are only updated on live WebSocket data and not the cached GET response data.

To use WebSockets, you first need to open a WebSocket connection to the SmartServer. Next, subscribe to a list of datapoints (using dpQualifiers) that are needed for the web page. WebSocket updates will occur if you do a periodic on-demand GET request, or if datapoint values are updated by some other process (another web page, periodic monitoring, or other processes doing on-demand GETs for that datapoint). Perform an initial GET request for all datapoints used on the web page so that the datapoint values on the web page can be populated, and then do a periodic GET request for only those datapoints that will change in value.

The WebSocket update format is different from the GET response format, so you need to design your code to support both.

Please read the Prerequisites - Important Design Considerations section below before starting your own development.

Prerequisites – Important Design Considerations 

Before you continue, please review each of the important design considerations described in this section.

Single User Login

For a single user login, only one web page that uses WebSockets (CMS or custom web pages) can be opened at a time, otherwise only one web page may see datapoint updates.

A subscribe GET request is used to receive WebSocket datapoint updates for a specific list of datapoints. If more than one WebSocket web page is open for a give user login, then all of these web pages will see the same list of datpoint updates. Since there is only one subscribe list per user login, the last web page that sent the subscribe request will control the datapoint that updates are sent to. This issue also applies to CMS webpages. For example, if you have two CMS web pages open and the Datapoint Browser widget on both web pages show different datapoints, then the last web page that was opened or the last Datapoint Browser Widget that had its datapoint list change will control the datapoints that have WebSocket updates.

CPU Bandwidth

Verify that your web page does not use too much CPU bandwidth. A reasonable update rate for quad core hardware is a poll every second with 5 datapoints per poll using a max_age of 1 second. For dual core hardware, use a maximum of a poll every every 2 seconds, with 5 datapoints per poll request using a max_age of 2 seconds.

This corresponds to an event rate of 5 EPS for quad core hardware and 2.5 EPS for dual core hardware. Subtract this number from 40 EPS for quad core hardware and 20 EPS for dual core hardware.

For example, for quad core hardware subtract the 5 EPS from 40 EPS, leaving 35 EPS for background monitoring. If a web page has more than 5 datapoints, cycle through multiple GET requests with a different list of datapoints.

Periodic Datapoint GETs

Only do periodic datapoint GET requests for devices that are running (deviceHealth = normal).

You should design your web page to take into account that some devices may go down before and while accessing your web page. When a device is down, you should eliminate all of its datapoints in your datapoint GET request. Neglecting to do so will cause a significant impact on the CPU. When a down device comes back up, you should add it back to the list. Be sure to perform a periodic device status check (every 5 to 10 minutes) using a PUT request listing device IDs.

Limit the Datapoints in Your Polling

You should reduce the number of datapoints in your periodic polling.

Some UI graphics (clickable image swappers, SVG and some HTML inputs) use an output datapoint for current value and an input datapoint for changing the value. To reduce the number of datapoints in a WebSocket GET request, try to minimize the number of datapoints that are polled. The initial GET request typically includes a list of all datapoints. After that, periodic requests should use a limited list (input datapoints for multi-datapoint HTML elements, or datapoints never updated should be taken out of the periodic GET request).

Datapoint Format

Custom web pages define datapoints using the following datapoint path format:

        <deviceName>/<blockName>/<blockIndex>/<datapointName>

For example:

Sensor1/Lamp/0/nvoValueFb

To use WebSockets, you need to convert the datapoint path to a dpQualifier pathname used by the web socket subscribe and the Web socket GET request. The dpQualifer is the same one returned when getting the datapoint value.

dpQualifer format:

<SmartServerSID>/<Protocol>/<Device DID>/<BlockName>/<BlockIndex>/<Datapoint XIF Name>

For example:

17q3awh/lon/5/Lamp/0/nvoValueFb

Ondemand GET request with a list of datapoints:

/iap/devs/*/if/*/*/*+qualifier=-<dpQualifier list>/value?max_age=5&noxs=true    (dpQualifiers in a GET URL must have the "/" replaced with a "%2F%")

For example, to perform an ondemand GET request for two datapoints:

https://10.1.128.82/iap/devs/*/if/*/*/*+qualifier=-17q3awh/lon/F5/Lamp/0/nviValue,17q3awh/lon/5/Lamp/F1/nvoValueFb/value?max_age=0&noxs=true


Sent on the wire: 

In the example below, notice how the trailing /value?max_age still has the /.


https://10.1.128.82/iap/devs/*/if/*/*/*+qualifier=-17q3awh%2Flon%2F5%2FLamp%2F0%2FnviValue,17q3awh%2Flon%2F5%2FLamp%2F1%2FnvoValueFb/value?max_age=0&noxs=true

There are many types of WebSocket updates. Datapoint updates have an action of UPD:DATAPOINT.

WebSocket Instructions

  1. Make a WebSocket connection.

           a. SmartServer release 2.8 or higher:
                  let socketRequest = "wss://"+ window.location.hostname + "/iap/ws";

           b. SmartServer release 2.75.020:
                  let socketRequest = "wss://"+ window.location.hostname + ":8443/iap/ws";

  2. Get a device list so you can convert the web page datapoint path to the dpQualifer.

    URL
    :
    https://10.1.128.82/iap/devs?short=true

  3. Subscribe to all datapoints.

    URL:
    https://10.1.128.82/iap/dp/updates/subscribe

    Payload:
    ["17q3awh/lon/5/Lamp/0/nviValue","17q3awh/lon/5/Lamp/0/nvoValueFb","17q3awh/lon/5/LightSensor/0/nvoLuxLevel"]

  4. Do an initial WebSocket GET request for all datapoints using max_age 0.

    Use one of the following formats:

    1. GET request format:  get all properties for a datapoint (needed for datapoint SNVT type)
      /iap/devs/*/if/*/*/*+qualifier=-<dpQualifier list>/*?max_age=0&noxs=true

    2. GET request format:  Get only datapoint value (typically you would use this one)
      /iap/devs/*/if/*/*/*+qualifier=-<dpQualifier list>/value?max_age=0&noxs=true

      URL: (Before replacing the qualifier path / with %2F)
      https://10.1.128.82/iap/devs/*/if/*/*/*+qualifier=-17q3awh/lon/5/Lamp/0/nviValue,17q3awh/lon/5/Lamp/1/nvoValueFb,17q3awh/lon/5/LightSensor/0/nvoLuxLevel/value?max_age=0&noxs=true

      URL: (What is sent on the wire – replace the qualifier path / with %2F)
      https://10.1.128.82/iap/devs/*/if/*/*/*+qualifier=-17q3awh%2Flon%2F5%2FLamp%2F0%2FnviValue,17q3awh%2Flon%2F5%2FLamp%2F1%2FnvoValueFb,17q3awh%2Flon%2F5%2FLightSensor%2F0%2FnvoLuxLevel/value?max_age=0&noxs=true

      Use the GET response (cached values to populate the web page).

  5. Periodically issue a limited web socket GET request using a non-zero max_age for devices that are UP.

    /iap/devs/*/if/*/*/*+qualifier=-<dpQualifier list>/value?max_age=5&noxs=true
    https://10.1.128.82/iap/devs/*/if/*/*/*+qualifier=-17q3awh%2Flon%2F5%2FLamp%2F1%2FnvoValueFb,17q3awh%2Flon%2F5%2FLightSensor%2F0%2FnvoLuxLevel/value?max_age=5&noxs=true

    Note:   Do not use GET response (cached values) to update the web page. Only use WebSocket updates.
  6. Response data structure is different between the GET response and the WebSocket data.

    WebSocket update:
    Look for action":"UPD:DATAPOINT in the WebSocket update.

    For each datapoint:
    {\"datapointQualifier\":\"17q4bn7/lon/1/device/0/nviLamp1\",\"value\":{\"value\":0,\"state\":0},\"blockName\":\"device\",\"blockIndex\":0,\"datapointName\":\"nviLamp1\"}

    WebSocket event.data for two datapoints:
    "{\"action\":\"UPD:DATAPOINT\",\"payload\":[{\"datapointQualifier\":\"17q4bn7/lon/1/device/0/nviLamp1\",\"value\":{\"value\":0,\"state\":0},\"blockName\":\"device\",\"blockIndex\":0,\"datapointName\":\"nviLamp1\"},{\"datapointQualifier\":\"17q4bn7/lon/1/device/0/nvoPulseOut\",\"value\":50,\"blockName\":\"device\",\"blockIndex\":0,\"datapointName\":\"nvoPulseOut\"}]}"

  7. Check the device state of all devices every 5 to 10 minutes. Use the PUT listByIDs to get the status for only devices used on the web page. Change the limited WebSocket GET request if devices go up or down.

    Note:  Devices might be reported down if a single datapoint poll does not work.


    Put URL:  /iap/devs/listByIDs
    Payload:  [3,4]

WebSocket Driver Example

/*************************************************************************
 * 
 *   ivProcessReadDeviceDpData - Used to send Datapoint WebSocket update
 *      - Create function in your code
 * **********************************************************************/
    let ivSocketRequest = "wss://"+ window.location.hostname + ":8443/iap/ws";
    let ivSocket;
    let ivWsShowConsoleLog = true; // set to false to reduce memory usage in Web Browser
    var ivWsMessage, ivWsLastDatapointUpdate;
    var ivWsSocketConnected = false;
    
    
    function ivWsLogout() {
        ivSocket.close();
    }
    function ivWebSocketInit() {
        ivSocket =  new WebSocket(ivSocketRequest);
        ivSocket.onopen = function(event) {
            if(ivWsShowConsoleLog)
                console.log("Web Socket Open");
            ivWsSocketConnected = true;
       }
        ivSocket.onmessage = function(event) {
            if(bivNeedToSubscribe)
                return;
            ivWsMessage = JSON.parse(event.data);
            var i, index = -1;
            var ivWsLastDatapointUpdate;
            try
            {
                if(ivWsMessage.action === "UPD:CPU") {
                }
                else if(ivWsMessage.action === "UPD:EPS") {
                }
                else if(ivWsMessage.action === "UPD:DATAPOINT"){
                    
                    ivWsLastMessage = ivWsMessage;
                    if(ivWsShowConsoleLog)
                        console.log(ivWsMessage);
                    json = [];
                    if(typeof ivWsMessage.payload !== 'undefined'){
                        for(i=0; i < ivWsMessage.payload.length; i ++)
                        {
                            index ++;
                            ivWsLastDatapointUpdate = {};
                            ivWsLastDatapointUpdate.datapointQualifier = ivWsMessage.payload[i].datapointQualifier;
                            ivWsLastDatapointUpdate.value = ivWsMessage.payload[i].value;
                            json[index] = ivWsLastDatapointUpdate;
                        }
                        if(json.length > 0) {
                            ivProcessReadDeviceDpData(0, 1, "", json)
                        }
                    }
                }
            }
            catch {}
        }
        ivSocket.onclose = function(event) {
            ivWsSocketConnected = false;
            if(event.wasClean) {
                if(ivWsShowConsoleLog)
                    console.log("web Socket close cleanly");
           
            }
            else {
                if(ivWsShowConsoleLog)
                    console.log('Web Socket died, code = ' + event.code + 'reason = ' + event.reason);
            }
        }
        ivSocket.onerror = function(error) {
            if(ivWsShowConsoleLog)
                console.log("error: " + error.message);
        }
    }