Analytics Web API

·

11 min read

Introduction

Niagara has a lot of capabilities. With an understanding of PX files and property animation, some complex graphics or visualizations are possible. However, it’s still rather limited in visualizing historical data, and all the knowledge in the world can’t make it as customizable as working directly in HTML5. Luckily, a station can work as a web host, and serve up HTML pages. You can even access station data for use in HTML5 pages/widgets via the Web Analytics API!

There is a lot to learn about HTML, Javascript, and CSS. However, unlike Niagara features, there are endless amounts of guides, classes, and forums geared specifically toward web development! Everyone has different preferences, but my own choice is text-based tutorials much like the ones I make. I’ve found the W3 Schools tutorials to be incredibly helpful in particular. If you’re unfamiliar with HTML5 in general, make sure to spend some time learning the basics!

File Structure and Access

As I mentioned, Niagara can serve up HTML/Javascript as if it were a standard web server; All this requires is for the files to be stored in the station’s file directory. You can then just navigate to this file in your station, and it will work like HTML would in any other situation, except with the station tree and menus all still visible.

There are a couple of odd quirks to keep in mind when developing an HTML5 application to run on a station vs a regular web host. The first is to mind your view when trying to access files. The view is specified at the end of the URL, and by default, it will use the “HTML Viewer” view. This doesn’t work for everything, though! There is another view called the “File Download View”. This will just access the regular HTML/Javascript without the station tree/menus. To add this to the link, we just add “%7Cview:web:FileDownloadView” to the end!

Niagara also has historically caused some issues with hosted HTML/Javascript due to “Snooping”. Basically, Niagara will sometimes read through your code, and make some “adjustments” that will break things! I’m not 100% sure if this still happens in current versions. However, we stop it by simply adding a comment to our code. If you’re having issues, simply add “<!--@noSnoop-->” to your HTML file somewhere near the top, or “/* @noSnoop */” to your Javascript.

Analytics Web API

So, great! We can host regular HTML5 applications with no major roadblocks. This is already useful on its own; You could, with this basic knowledge, easily set up some cool stuff, like custom homepages linking to equipment, building layouts, and anything else that doesn’t require live station data. However, we can go further than this with the Analytics Web API. This is an HTTP API that allows you to request station data, that’s bundled with the Analytics module. Using it requires the Analytics service to be installed. However, it doesn’t appear to count against your licensing, and should generally work even without Analytics being licensed on your station.

To set up the web API, all we have to do is add the AnalyticService from the analytics module under our services, then add the “Web API” component under this AnalyticService. Then we just set Enabled to true, and make sure our status is ok. From here, your station is ready!

Basic Data Access

For our first example, we’re just going to access a point’s trend somewhere in our station, and spit out our data to the browser console. To start, we want to create an empty folder to contain our files. Then we want to create a couple of files, index.html and javascript.js. Below is a screenshot of the folder structure, as well as the code we’re going to want to place in each file:

<!--@noSnoop-->
<html>

<head>
    <script src='javascript.js%7Cview:web:FileDownloadView'></script>
</head>

<body>
</body>

</html>
async function NiagaraGetTrend(
    baseNode = 'station:|slot:/Drivers',
    dataArr = ['msi:zoneTemp'],
    timeRange = 'monthToDate',
    interval = 'fiveMinutes',
    aggregation = 'avg',
    baseUrl = 'https://{yourstationaddress}/na'
) {
    requestObject = {
        'requests': [
        ]
    };
    dataArr.forEach(function (dataEntry) {
        singleRequestObject = {
            'message': 'GetTrend',
            'node': baseNode,
            'data': dataEntry,
            'timerange': timeRange,
            'interval': interval,
            'aggregation': aggregation
        };
        requestObject.requests.push(singleRequestObject);
    });
    const response = await fetch(baseUrl, {
        method: 'POST',
        mode: 'cors',
        cache: 'no-cache',
        headers: {
            'Content-Type': 'text/plain',
            'Accept': '*/*',
            'Connection': 'keep-alive',
        },
        redirect: 'follow',
        referrerPolicy: 'no-referrer',
        body: JSON.stringify(requestObject),
    });
    return response.json();
}
NiagaraGetTrend('station:|slot:/Drivers/NiagaraNetwork/KSVenturesInc/KSV_Headquarters/B2925_SSC1/points/RTU_1', ['msi:zoneTemp'], 'yearToDate').then(response => console.log(response));

A lot is happening here, but we’re going to go through it all piece by piece!

In our HTML file, not much is happening here. All this HTML is doing is linking to our javascript, which is doing all the work. In the HTML, there are only two things to point out. The first is to point out that we added that <!--@noSnoop--> comment. The second is that we use the File Download View specification, with:

<script src='javascript.js%7Cview:web:FileDownloadView'></script>

This ensures that the file we’re linking to is our actual JS file, instead of the default, which would be an HTML page displaying our JS file.

Now, in our JS file, we’re doing a few things. First, we’re setting up an async function we can reuse in the future to access historical data. This function is the majority of our code, and we’re going to split it up a bit to explain each piece.

async function NiagaraGetTrend(
    baseNode = 'station:|slot:/Drivers',
    dataArr = ['msi:zoneTemp'],
    timeRange = 'monthToDate',
    interval = 'fiveMinutes',
    aggregation = 'avg',
    baseUrl = 'https://{yourstationaddress}/na'
)

Here we’re just defining the arguments of our method, and the default values of each. This saves us a bunch of time since it will use these values if we don’t specify. These just go in order; If we call `NiagaraGetTrend(‘station:|slot:/Drivers/BacnetNetwork/RTU_1‘, [‘msi:zoneTemp‘])`, it will grab the Zone Temp’s trend under RTU1, and automatically grab monthToDate data at 5-minute intervals, and average results if there are multiple. Ideally, our Base URL won’t even change; This will likely be set once per station.

requestObject = {
    'requests': [
    ]
};
dataArr.forEach(function (dataEntry) {
    singleRequestObject = {
        'message': 'GetTrend',
        'node': baseNode,
        'data': dataEntry,
        'timerange': timeRange,
        'interval': interval,
        'aggregation': aggregation
    };
    requestObject.requests.push(singleRequestObject);
});

Here, we’re starting to structure our request “message”. Essentially, when we use the Analytics Web API, we’re packing up our request parameters in a JSON object and sending that as the body of the request. This is just being populated from the async function parameters. This request object is an array of request properties, so if we add multiple tags in our dataArr parameter (IE, [‘msi:zoneTemp’, 'msi:dischargeAirTemp’], it creates multiple messages and gives us back multiple responses.

const response = await fetch(baseUrl, {
    method: 'POST',
    mode: 'cors',
    cache: 'no-cache',
    headers: {
        'Content-Type': 'text/plain',
        'Accept': '*/*',
        'Connection': 'keep-alive',
    },
    redirect: 'follow',
    referrerPolicy: 'no-referrer',
    body: JSON.stringify(requestObject),
});
return response.json();

Finally, to close out our async function creation, we make the HTTP call itself. This is where the magic happens, so to speak! The specifics of how these HTTP requests work are pretty generally applicable, and usually easy to find information on elsewhere. We’re using the fetch API, officially documented by Mozilla here!

The Analytics Web API expects a POST request, where our body is the request object we made above. You may also notice that there are no credentials specified; While most official documentation in Niagara assumes you’re using HTTP Basic authorization, this exposes the credentials in clear text and is insecure unless these calls are being made from a server, rather than an individual client application. Since we’re serving up this HTML directly to the user, we have to hide these credentials. Luckily, the way it’s structured above, it will just use the current station user’s permissions!

NiagaraGetTrend('station:|slot:/Drivers/NiagaraNetwork/KSVenturesInc/KSV_Headquarters/B2925_SSC1/points/RTU_1', ['msi:zoneTemp'], 'yearToDate').then(response => console.log(response));

With this method all set and ready to go, we can make our API call and log it out to our console! In this example, I’m just grabbing the Zone Temp from an RTU in my office. Since our function is an async method, we have to use JS promises. This can make our code rather complex if we’re doing a lot of different calls, but for simpler applications, this essentially just means we’ll run an anonymous function in the “.then()” portion of the code that will do all of our real JS. This just waits until our data is available, passes it through, then does whatever is required, whether it’s as simple as logging out to our console, or as complex as crunching some numbers to transform data and then spitting it into an HTML5 chart.

Below is what you should see if you access your browser console while running this code; Note that you’ll have to change your base URL, base node, and data tags to match the data you want to pull from your station.

As you can see, we get an array of responses; Since we only requested one point’s trend, there is only a single response object in this array. Structured like this, our actual data is accessible via responses[0].rows, as an array. Each entry in this array has timestamp, value, and status, as [0], [1], and [2], so to access our earliest value, we would reference responses[0].rows[0][1]!

Charting Our Data

Spitting out values to the console is cool and all, but it doesn’t exactly sell a job or provide useful functionality; Now that we know how to access data, let’s use it for something a bit more helpful! There are a lot of different libraries out there for data visualization, from the easy-to-use plotly to the infinitely customizable D3.js. For our example, we’re going to take the above information and put it in a basic plotly line chart. As before, I’m going to post the full code below, then explain the important parts in detail.

<!--@noSnoop-->
<html>

<head>
    <script src="plotly.js%7Cview:web:FileDownloadView"></script>
    <script src='javascript.js%7Cview:web:FileDownloadView'></script>
</head>

<body>
    <div id="chart"></div>
</body>

</html>
async function NiagaraGetTrend(
    baseNode = 'station:|slot:/Drivers',
    dataArr = ['msi:zoneTemp'],
    timeRange = 'monthToDate',
    interval = 'fiveMinutes',
    aggregation = 'avg',
    baseUrl = 'https://{yourstationaddress}/na'
) {
    requestObject = {
        'requests': [
        ]
    };
    dataArr.forEach(function (dataEntry) {
        singleRequestObject = {
            'message': 'GetTrend',
            'node': baseNode,
            'data': dataEntry,
            'timerange': timeRange,
            'interval': interval,
            'aggregation': aggregation
        };
        requestObject.requests.push(singleRequestObject);
    });
    const response = await fetch(baseUrl, {
        method: 'POST',
        mode: 'cors',
        cache: 'no-cache',
        headers: {
            'Content-Type': 'text/plain',
            'Accept': '*/*',
            'Connection': 'keep-alive',
        },
        redirect: 'follow',
        referrerPolicy: 'no-referrer',
        body: JSON.stringify(requestObject),
    });
    return response.json();
}
NiagaraGetTrend('station:|slot:/Drivers/NiagaraNetwork/KSVenturesInc/KSV_Headquarters/B2925_SSC1/points/RTU_1', ['msi:zoneTemp'], 'yearToDate').then(response => {
    const values = response.responses[0].rows.map(y => y[1]);
    const timestamps = response.responses[0].rows.map(x => x[0]);
    var trace1 = {
        x: timestamps,
        y: values,
        mode: 'lines'
    };
    var data = [trace1];
    var layout = {
        title: 'RTU1 Zone Temp'
    };
    Plotly.newPlot('chart', data, layout);
});

As you can see, most of this code was entirely re-used; Our only changes were an additional imported file in our index.html and a difference in the usage of our NiagaraGetTrend async function. Below is a look at what we get in the browser:

So, for our additional import in the HTML file, we simply go download the .js file for this library, slap it in our station, and refer to it the same way we do our own .js file. It’s generally best to store libraries on the station rather than reach out to CDNs like many tutorials say to do; Not every station will have internet access, and requesting external resources is often restricted for security reasons.

Our entire NiagaraGetTrend function is being re-used from before, so it’s only where we call this function that things are different.

NiagaraGetTrend('station:|slot:/Drivers/NiagaraNetwork/KSVenturesInc/KSV_Headquarters/B2925_SSC1/points/RTU_1', ['msi:zoneTemp'], 'yearToDate').then(response => {
    const values = response.responses[0].rows.map(y => y[1]);
    const timestamps = response.responses[0].rows.map(x => x[0]);
    var trace1 = {
        x: timestamps,
        y: values,
        mode: 'lines'
    };
    var data = [trace1];
    var layout = {
        title: 'RTU1 Zone Temp'
    };
    Plotly.newPlot('chart', data, layout);
});

We are grabbing the same data as before, but then we’re doing a few things to “fix” our data. Plotly expects a basic array for our data and timestamp, but Niagara gives us a 2d array as our response. The values and timestamps constants being defined here are using the js “map” function, which just makes a new array by running a function on each entry in another array. Here, all we have to do is specify that instead of the timestamp, value, and status, we want an array with only the value and another array with only the timestamp.

From here, it’s all a fairly basic plotly implementation, just using the basic code defined in one of their examples!

Further Reading

Hopefully, this gives you the basic tools to use the Analytics Web API for data access in HTML5. There is a ton to learn here in general; As I mentioned above, W3 Schools is a fantastic resource for learning basic HTML/Javascript/CSS. We also used plotly js; They have many very good examples here!

In our examples, we were using the GetTrend message type in the Analytics Web API. This is what I’ve found to be the most useful overall since more basic graphics unrelated to historical data are generally far faster and easier to build using PX files. However, this API has many message types, allowing you to grab live data, invoke actions, and more! These are all reasonably well-documented if a bit hard to track down. We were also focused entirely on self-hosted HTML, living on a station. This greatly simplifies authentication. However, to access this data from another server, you will need to read up a bit on HTTP basic authentication. Just remember to be mindful of your credentials’ security. Anything coded in HTML/JS/CSS is visible in cleartext to anyone using the application!

Web API Protocol: module://docAnalytics/doc/WebAPIProtocol.html

Message Types and Parameters: module://docAnalytics/doc/MessagesAnalytics..

HTTP Authentication: https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication