Marcos Vainer Loeff

PI Web API 2015 R2  - New features demo - JavaScript

Blog Post created by Marcos Vainer Loeff Employee on Jul 30, 2015

Introduction

 

As you know, PI Web API 2015 R2 is already released! That is why I have decided to write a blog post to show its main new features through a sample application using HTML5 and jQuery. You can download the source code package here.

 

Objetives

 

The enhancements of this new version taken from the release notes are:

  • Support for PI Server (Data Archive and AF) 2015
    • Support for reading and writing Future Data
    • Support for creating PI Points with the Future attribute
    • Support for retrieving end-of-stream values from Streams and StreamSets
    • Support for hidden and excluded AF Attributes
    • Support for the PI AF Server 2015 security model
  • Bulk writes of time series data (single and multiple value)
  • Ad-hoc bulk reads and writes of time series data
  • The ability to capture values on PI Event Frames
  • Indexed Search contains improvements to reduce crawl times for highly referenced AF Databases.

 

In order to make this work more scalable, a PI Web API module was developed. Although the structure of this module was taken from my other blog post Using PI Web API on HTML5 with jQuery, its methods are completely different in order to demonstrate properly the new features of the product.

 

This sample application has 7 examples which call methods from the module described. The idea was to make a module that you can easily use in your own HTML5 web application.

 

 

Preparing the environment

 

This sample application was developed using the following settings for PI Web API:

 

  • AuthenticationMethods: only Basic
  • CorsHeaders: *
  • CorsMethods: GET, POST, PATCH, OPTIONS
  • CorsOrigins: http://localhost:54622
  • CorsSupportCredentials: True

 

The domain of the CorsOrigins is localhost with a uncommon port because this is the domain which Visual Studio chooses by default for development when I run my web application.

The options above are not mandatory, but you might need to overcome some issues in case you have a different environment.  If the domain where your application runs is the same from the PI Web API (including the port), then you won’t need to care with CORS attributes.

 

You can check your PI Web API settings using PI System Explorer. Please refer to the PI Web API User Guide for more information.

 

 

PI Web API module

 

The JavaScript module has 8 public methods available:

 

  • SetBaseServiceUrl(baseUrl) --> This method define the BaseServiceUrl so that all the other methods could connect to the correct PI Web API endpoint on later requests This method should be called before any other method otherwise you will receive an error.
  • CreateFuturePIPoint(piPointName, piDataArchiveName, updateDOM) --> Creates a new future PI Point whose name is piPointName on the PI Data Archive whose name is stored on the piDataArchiveName variable.  Finally, updateDOM is a function which is executed in case  the last ajax request run successfully. This function will have the same purpose for all the other methods.
  • UpdateValuesForSingleStream(piPointName, piDataArchiveName, updateDOM) --> sends 100 future random values for a given future PI Point.
  • UpdateValuesForMultipleStreams(piPointNames, piDataArchiveName, updateDOM) --> sends 100 future random values for each future PI Point within the piPointNames array.
  • GetValuesFromSingleStream(piPointName, piDataArchiveName, startTime, endTime, updateDOM) --> gets the compressed values from a particular PI Point on a given time range, including future values.
  • GetValuesFromMultipleStreams(attributePaths, startTime, endTime, updateDOM) --> gets the compressed values for a given time range from all attributes whose path is included within the attributePaths.
  • GetEndOfTheStream(piPointName, piDataArchiveName, updateDOM) --> get the end of the stream from the sinusoid and from the piPointName. End of the stream is a new concept introduced with PI AF SDK 2.7 and the PI Server 2015. Please refer to those product documents for more details.
  • MakeAttributeHidden(attributePath, updateDOM) --> change the value from the isHidden and description property for a given AFAttribute specified by its path.

 

The methods above can connect to the PI Data Archive and also to the PI AF Sever. No matter which one of the two options you choose, the way you do it are quite similar. The complete code of this module is on the file piwebapi_wrapper.js within the Scripts folder of source code package or below:

 

 

var piwebapi = (function () {
    var base_service_url = 'NotDefined';
    //Specify the domain user name
    var username = 'username';


    //Specify the domain user password
    var password = 'password';


    //Generate 100 future random values (one value per day)
    function generateRandomFutureValues(n) {
        var values = [];
        for (i = 0; i < 100; i++) {
            var newValue = new Object();
            newValue.Timestamp = "* + " + i.toString() + "d";
            newValue.Value = Math.random();;
            newValue.Good = true;
            newValue.Questionable = false;
            values.push(newValue);
        }
        return values;
    }


    //Get PI Point Web Id
    function getPIPointWebId(piDataArchiveName, piPointName, successCallBack, errorCallBack) {
        baseUrlCheck();
        var url = base_service_url + 'points?path=\\\\' + piDataArchiveName + '\\' + piPointName;
        return processJsonContent('GET', null, url, successCallBack, errorCallBack);
    }


    //Get PI Data Archive Web Id
    function getPIDataArchiveWebId(piDataArchiveName, successCallBack, errorCallBack) {
        baseUrlCheck();
        var url = base_service_url + "dataservers?name=" + piDataArchiveName;
        return processJsonContent('GET', null, url, successCallBack, errorCallBack);
    }


    //Get AF Attribute Web Id
    function getAttributeWebId(attributePath, successCallBack, errorCallBack) {
        baseUrlCheck();
        var url = base_service_url + "attributes?path=" + attributePath;
        return processJsonContent('GET', null, url, successCallBack, errorCallBack);
    }




    //Returns the ajax function that will be responsbile for communicating with PI Web API. The request starts after the method $.when method is called.
    function processJsonContent(type, data, url, successCallBack, errorCallBack) {
        return $.ajax({
            type: type,
            headers: {
                "Content-Type": "application/json; charset=utf-8",
            },
            url: url,
            cache: false,
            data: data,
            async: true,
            username: username,
            password: password,
            
            crossDomain: true,
            xhrFields: {
                withCredentials: true
            },
            success: successCallBack,
            error: errorCallBack
        });
        //It seems that contentType option doesn't work pretty well. Using the headers option seems to work better.
        //crossDomain and xhrFields with  withCredentials: true needed to be used because the Anonymous authentication is disabled
        //and that my web application and PI Web API are running in different domains.
    }




    function createFuturePIPoint(piPointName, piDataArchiveName, updateDOM) {
        
        baseUrlCheck();
        var errorCallBack = function (error) {
            alert(error.responseJSON.Errors[0]);
        }


        var ajax = getPIDataArchiveWebId(piDataArchiveName, null, errorCallBack);
        $.when(ajax).then(function (dataServerJson, textStatus, jqXHR) {
            //alert(jqXHR.status); // Alerts 200
            var url = base_service_url + 'dataservers/' + dataServerJson.WebId + '/points';
            var data = new Object();
            data.Name = piPointName;
            data.PointClass = "classic";
            data.PointType = "Float32";
            data.Future = true;
            var jsonString = JSON.stringify(data);
            var ajax2 = processJsonContent('POST', jsonString, url, updateDOM, errorCallBack);
            $.when(ajax2).then(function (data, textStatus, jqXHR) {
                alert('Future PI Point was created successfully!');
            });


        });


    }


    function writeFutureValuesForStream(piPointName, piDataArchiveName, updateDOM) {
        baseUrlCheck();
        var errorCallBack = function (error) {
            alert(error.responseJSON.Errors[0]);
        }


        var ajax = getPIPointWebId(piDataArchiveName, piPointName, null, errorCallBack);
        $.when(ajax).then(function (piPointJson, textStatus, jqXHR) {
            //alert(jqXHR.status); // Alerts 200
            var url = base_service_url + 'streams/' + piPointJson.WebId + '/recorded';
            var futureValues = generateRandomFutureValues(100);
            var jsonString = JSON.stringify(futureValues);
            var ajax2 = processJsonContent('POST', jsonString, url, updateDOM, errorCallBack);
            $.when(ajax2).then(function (data, textStatus, jqXHR) {
                alert('Future values were sent successfully!');
            });


        });
    }




    function readFutureValues(piPointName, piDataArchiveName, startTime, endTime, updateDOM) {
        baseUrlCheck();


        var errorCallBack = function (error) {
            alert(error.responseJSON.Errors[0]);
        }


        var ajax = getPIPointWebId(piDataArchiveName, piPointName, null, errorCallBack);
        $.when(ajax).then(function (piPointJson, textStatus, jqXHR) {
            //alert(jqXHR.status); // Alerts 200
            var url = base_service_url + 'streams/' + piPointJson.WebId + '/recorded?startTime=' + startTime + '&endTime=' + endTime;
            var ajax2 = processJsonContent('GET', null, url, updateDOM, errorCallBack);
            $.when(ajax2).then(function (data, textStatus, jqXHR) {


            });
        });
    }






    function getEndOfTheStream(piPointName, piDataArchiveName, updateDOM) {
        baseUrlCheck();
        var errorCallBack = function (error) {
            alert(error.responseJSON.Errors[0]);
        }


        var ajax = getPIPointWebId(piDataArchiveName, piPointName, null, errorCallBack);
        $.when(ajax).then(function (piPointJson, textStatus, jqXHR) {
            //alert(jqXHR.status); // Alerts 200
            var url = base_service_url + 'streams/' + piPointJson.WebId + '/end';
            var ajax2 = processJsonContent('GET', null, url, updateDOM, errorCallBack);
            $.when(ajax2).then(function (data, textStatus, jqXHR) {


            });
        });
    }






    function makeAttributeHidden(attributePath, updateDOM) {
        baseUrlCheck();
        var errorCallBack = function (error) {
            alert(error.responseJSON.Errors[0]);
        }


        var ajax = getAttributeWebId(attributePath, null, errorCallBack);
        $.when(ajax).then(function (attributeJson, textStatus, jqXHR) {
            //alert(jqXHR.status); // Alerts 200
            var url = base_service_url + 'attributes/' + attributeJson.WebId;
            var data = new Object();
            data.isHidden = true;
            data.Description = "Test description 124";
            var jsonString = JSON.stringify(data);
            var ajax2 = processJsonContent('PATCH', jsonString, url, updateDOM, errorCallBack);


            $.when(ajax2).then(function (data, textStatus, jqXHR) {
                alert('Hidden and description properties were changed successfully!');
            });
        });
    }


    function writeFutureValuesForStreams(piPointNames, piDataArchiveName, updateDOM) {
        baseUrlCheck();
        var errorCallBack = function (error) {
            alert(error.responseJSON.Errors[0]);
        }


        var ajaxPt1 = getPIPointWebId(piDataArchiveName, piPointNames[0], null, errorCallBack);
        var ajaxPt2 = getPIPointWebId(piDataArchiveName, piPointNames[1], null, errorCallBack);
        var ajaxPt3 = getPIPointWebId(piDataArchiveName, piPointNames[2], null, errorCallBack);
        var data = [];
        $.when(ajaxPt1, ajaxPt2, ajaxPt3).done(function (r1, r2, r3) {
            var results = new Array(3);
            results[0] = r1[0];
            results[1] = r2[0];
            results[2] = r3[0];


            for (var i = 0; i < results.length; i++) {
                var obj = new Object();
                obj.WebId = results[i].WebId;
                obj.Items = generateRandomFutureValues(100);
                data.push(obj);
            }


            var jsonString = JSON.stringify(data);
            var url = base_service_url + 'streamsets/recorded';
            var ajax = processJsonContent('POST', jsonString, url, updateDOM, errorCallBack);


            $.when(ajax).done(function (a1) {
                alert('Future values were sent successfully to the 3 pi tags!');
            });
        });
    }




    function readValuesFromMultipleStreams(attributePaths, startTime, endTime, updateDOM) {
        baseUrlCheck();
        var errorCallBack = function (error) {
            alert(error.responseJSON.Errors[0]);
        }


        var ajaxPt1 = getAttributeWebId(attributePaths[0], null, errorCallBack);
        var ajaxPt2 = getAttributeWebId(attributePaths[1], null, errorCallBack);
        var ajaxPt3 = getAttributeWebId(attributePaths[2], null, errorCallBack);
        var data = [];
        $.when(ajaxPt1, ajaxPt2, ajaxPt3).done(function (r1, r2, r3) {
            var results = new Array(3);
            results[0] = r1[0];
            results[1] = r2[0];
            results[2] = r3[0];
            var webIdSection = '';
            for (var i = 0; i < results.length; i++) {
                webIdSection = webIdSection + '&webid=' + results[i].WebId;
            }
            webIdSection = webIdSection.substr(1);
            var url = base_service_url + 'streamsets/recorded?' + webIdSection + '&startTime=' + startTime + '&endTime=' + endTime;
            var ajax = processJsonContent('GET', null, url, updateDOM, errorCallBack);


            $.when(ajax).done(function (a1) {
                alert('Values received successfully from the 3 different attributes!');
            });
        });
    }


    function baseUrlCheck() {
        if (base_service_url == "NotDefined") {
            alert("Service base url was not defined");
        }
    }




    return {
        CreateFuturePIPoint: function (piPointName, piDataArchiveName, updateDOM) {
            createFuturePIPoint(piPointName, piDataArchiveName, updateDOM)
        },
        UpdateValuesForSingleStream: function (piPointName, piDataArchiveName, updateDOM) {
            writeFutureValuesForStream(piPointName, piDataArchiveName, updateDOM);
        },
        UpdateValuesForMultipleStreams: function (piPointNames, piDataArchiveName, updateDOM) {
            writeFutureValuesForStreams(piPointNames, piDataArchiveName, updateDOM);
        },
        GetValuesFromMultipleStreams: function (attributePaths, startTime, endTime, updateDOM) {
            readValuesFromMultipleStreams(attributePaths, startTime, endTime, updateDOM);
        },
        GetValuesFromSingleStream: function (piPointName, piDataArchiveName, startTime, endTime, updateDOM) {
            readFutureValues(piPointName, piDataArchiveName, startTime, endTime, updateDOM);
        },
        GetEndOfTheStream: function (piPointName, piDataArchiveName, updateDOM) {
            getEndOfTheStream(piPointName, piDataArchiveName, updateDOM);
        },
        MakeAttributeHidden: function (attributePath, updateDOM) {
            makeAttributeHidden(attributePath, updateDOM);
        },
        SetBaseServiceUrl: function (baseUrl) {
            base_service_url = baseUrl;
            if (base_service_url.slice(-1) != "/") {
                base_service_url = base_service_url + "/";
            }
        }
    }
}());

 

Web Sample Application

 

This web sample application has two NuGet JavaScript libraries:

 

jQuery 2.1.4 -->  This JavaScript library makes life easier when making HTTP request through Ajax.

Twitter Bootstrap 3.3.5 --> This JavaScript library enhances the design of the web application. It also makes the application responsive, which is a very important feature as web sites are being more accessed by smartphones. I have taken some code snippets from the default ASP.NET MVC template to have a good design in a short amount of time.

 

Before you start testing this sample application, you need to open the app.js file under the Scripts folder and change the first lines according to the following guidelines:

 

serviceBaseUrl --> Defines the PI Web API endpoint.

afServerName and piServerName --> Defines the PI AF Server and PI Data Archves names that PI Web API will connect to.

attributePathForHiddenTest --> Path of the attribute which will have its isHidden and description properties changed.

attributePaths --> array of AF Attribute paths used by the GetValuesFromMultipleStreams() method.

piPointNames --> array of future PI Point names that are going to be created on the first example.

 

Once you start the application, make sure to run Exercise 1 first. It will create 3 PI Points which are going to be used by some of the other exercises as well.

Understanding the method readValuesFromMultipleStreams

 

 

I will explain what happens with the readValuesFromMultipleStreams under the hood. If you are able to understand it, the same logic applies for other methods as well. Here is what happens under the hood:

 

  • baseUrlCheck() --> Checks to see if the serviceBaseUrl, which is the PI Web API endpoint, was defined.
  • errorCallBack --> Defines the function which will be called in case there is an error with any ajax request to PI Web API.
  • getAttributeWebId(attributePaths[0], null, errorCallBack); --> This method returns the $.ajax function which is responsable for the communication to PI Web API. The function is returned but it only runs the HTTP request on the next step.
  • $.when(ajaxPt1, ajaxPt2, ajaxPt3).done(r1,r2,r3) --> This method will start the 3 HTTP requests. When all of three requests have their execution finished, their respective responses will be available on r1, r2 and r3, which contains the webIds for each attribute that are going to be used to generate the new URL for the next ajax request.
  • var ajax = processJsonContent('GET', null, url, updateDOM, errorCallBack); --> After the new URL is generated using the 3 webIds, startTime and endTime, the processJsonContent will return an ajax function ready to get the requested compressed values.
  • $.when(ajax).done(a1) --> Run the new ajax request and it provides the result on the variable a1.

 

 function readValuesFromMultipleStreams(attributePaths, startTime, endTime, updateDOM) {
        baseUrlCheck();
        var errorCallBack = function (error) {
            alert(error.responseJSON.Errors[0]);
        }


        var ajaxPt1 = getAttributeWebId(attributePaths[0], null, errorCallBack);
        var ajaxPt2 = getAttributeWebId(attributePaths[1], null, errorCallBack);
        var ajaxPt3 = getAttributeWebId(attributePaths[2], null, errorCallBack);
        var data = [];
        $.when(ajaxPt1, ajaxPt2, ajaxPt3).done(function (r1, r2, r3) {
            var results = new Array(3);
            results[0] = r1[0];
            results[1] = r2[0];
            results[2] = r3[0];
            var webIdSection = '';
            for (var i = 0; i < results.length; i++) {
                webIdSection = webIdSection + '&webid=' + results[i].WebId;
            }
            webIdSection = webIdSection.substr(1);
            var url = base_service_url + 'streamsets/recorded?' + webIdSection + '&startTime=' + startTime + '&endTime=' + endTime;
            var ajax = processJsonContent('GET', null, url, updateDOM, errorCallBack);


            $.when(ajax).done(function (a1) {
                alert('Values received successfully from the 3 different attributes!');
            });
        });
    }

 

Concerning the logic of the PI Web API module, for most of the methods they can be divided in two parts. The first one gets the WebId or WebIds from attributes, PI Points or PI Data Archive.The second part completes the process using the webId provided by the previous ajax request results.

 

Conclusions

 

PI Web API 2015 R2 supports future data and also introduces bulk calls support for writing and reading data not only for a single stream but also for multiple streams in a single HTTP request. We expected an increase of the performance from your applications in case you take advantages of the bulk actions available on this release properly.

 

 

Stay tuned for the upcoming blog posts!!

Attachments

Outcomes