Skip navigation
All Places > PI Developers Club > Blog > 2019 > May
2019
rborges

PI Web API and Node-RED

Posted by rborges Employee May 21, 2019

1. Introduction

1.1 What is Node-RED?


Before going to the juicy bits of this blog post, let me start by explaining what Node-RED is for those who are not familiar with this tool. Quoting their own website, Node-RED is a flow-based programming tool for the internet of things. What this means, in simple English, is that you can drag and drop functional boxes for wiring together devices and online services.

 

This is what a Node-RED flow looks like:

A group of nodes is called a flow. Each node is responsible for manipulating an object called message. In this flow, Node-RED is listening for UDP packets and, based on the destination port, it can either pull high GPIO 15 or ground GPIO 13 while sending an HTTP request to a given API (and keep trying while not succeed).

 

Another selling point is the community support. Being an open source project (Apache 2.0 if you are curious), community engagement has skyrocketed and you can find thousands of different nodes: from OPC UA and Modbus nodes to Instagram and other social media platforms. From FFT to sentiment analysis. So most likely you won't even need to write a single line of code.

 

1.2 Why should I use it?

 

Rafa, usually graphical programming is just a dumbed-down version of actual programming. Why should I use it if I can write my own application? Because it's easy and fast. As a software engineer, I agree that a tailor-made firmware in C is more reliable and efficient. But can you create one in seconds? Can you easily include new features or make changes to it? This is an old debate where, on one hand, we have practicality and, on the other hand, robustness.

 

Let's just remember that we are talking about IoT devices, frequently located outside the boundaries of the main network and sending data through a non-wired channel. Unpredictability and unreliability are a given and your logic can go easily go sideways with an unforeseen condition. So would you prefer to update your code through a web interface or going on the field with a computer and a USB UART dongle to reflash your firmware?

 

1.3 Caveats


Everything sounds great, but it's not all roses. Although it has a very small footprint, you can't deploy Node-RED on small microchips (ESP8266, if you are wondering) or limited hardware like Arduino. Right now, the only "IoT hardware" capable of running it is Raspberry PIs and Beagle Bone Black. This poses a challenge if you are considering deploying the platform on the field, but there are alternatives if you need to use minimalist hardware.

 

Another caveat is the engine itself. Its capabilities are as big as the capabilities of the device hosting it. If you deploy it on a small Raspberry PI, don't expect the performance of deployment on a full-blown server. It may sound like an obvious observation, but because IoT devices can scale up easily on a mesh network, sometimes we forget that the host doesn't scale up that easily. So, your mileage may vary when it comes to performance.

 

1.4 Architectural Considerations

 

In today's example, we will have multiple sensors sending data directly through an access point (a Raspberry PI 3 B+ in our case). A rough representation is this:

 

For the geeks out there who are curious about the setup I'm using, here's more info: I'm using BME280 for temperature, humidity and pressure and a DS1307 for real-time clock. All of them are using the I2C bus to send data through an ESP-01 breakout. The ESP8266 is running a custom firmware that creates a mesh network. The Raspberry PI host is connected to the main corporate network through its own wi-fi module, but it's also connected to the mesh network through its own ESP-01 breakout. Each sensor is sending its data to specific MQTT topics on a Mosquitto broker running on the same Raspberry PI. I have three sets of sensors: one is sitting on my desk, the second one is at the office's kitchen, and the last one is in a meeting room, about 20m (65 ft) from my desk.

 

 

Although the hardware side is not related to PI, leave a comment below if you would like to hear a little bit more about what I'm doing. By the way, this is a preparation for an ongoing project, where a more professional looking version this will eventually be deployed around our office here in London.

 

Keep in mind that this is not a suitable production-grade architecture as there is no data buffer, no redundancy, no scalability, and no fail-safe mechanism. If you need any of these items (and you need if data is critical to your operation!), you should consider giving a look at EDS (Edge Data Store), our answer to data management on the edge (watch this presentation about EDS, you won't regret it).

 

2. Sample Flows

 

2.1 Reading Values from the Sensors

 

This is not a Node-RED tutorial, but let me just show how I'm capturing the data. After all, this is how everything starts. As I mentioned before I'm running an MQTT broker and each sensor data has it's own topic. Because I like to sort my data by data domains, my topics are the following:

 

DeviceMeasurementTopic
Desk SensorTemperatureoffice/temperature/desk
Desk SensorHumidityoffice/humidity/desk
Desk SensorPressureoffice/pressure/desk
Kitchen SensorTemperatureoffice/temperature/kitchen
Kitchen SensorHumidityoffice/humidity/kitchen
Kitchen SensorPressureoffice/pressure/kitchen
Meeting Room SensorTemperatureoffice/temperature/meetingroom
Meeting Room SensorHumidityoffice/humidity/meetingroom
Meeting Room SensorPressureoffice/pressure/meetingroom

 

This allows me to easily get to all temperatures by subscribing to office/temperature/# or get all office data by subscribing to office/#. I can also get only my desk data by subscribing to office/+/desk. Here's an example of how to get the data using an MQTT node:

Dead simple, right? The green node is a debug node that outputs all the content of the payload. Here's the output for a single topic. Keep in mind that we receive several messages like this (one for each topic we are subscribed to).

 

 

Because I'm trying to keep things as streamlined as possible, the sensors are already streaming an output that is pretty much what we need to send to PI, including the AFPath where we are sending data to. On my custom ESP8266 firmware I have this hardcoded:

 

const String baseAFPath = "\\\\RBORGES-AF\\IoTDemo\\Rafael Desk Environmental Data|";
const String tempAFPath = baseAFPath + "temperature";
const String humiAFPath = baseAFPath + "humidity";
const String pressAFPath = baseAFPath + "pressure";
const String dpointAFPath = baseAFPath + "dewpoint";

 

In a more real-life scenario, instead of a hardcoded string, you should do something like this:

 

const String chipID = ESP.getChipId();
const String tempTopic = "office/temperature_"+ chipID + "/desk";
const String humiTopic = "office/humidity_"+ chipID + "/desk";
const String presTopic = "office/pressure_"+ chipID + "/desk";
const String vccTopic = "office/vcc_"+ chipID + "/desk";
const String ipTopic = "office/ip_"+ chipID + "/desk";

 

Then, on your Node-RED server, you would correlate the chip ID with the attributes you are sending data to. That has the extra benefit of easy maintenance, in the case of you moving the sensor to a different location. There's no need for you to rewrite the firmware, just update the reference table.

 

By the way, talking about real-life scenarios, if you are using the PI System infrastructure, you should consider OMF as your format standard, in the near future, you will be able to send OMF data right away to PI Web API. This will free you from the transformations we have to do in the next section.

 

2.2 Sending Values to PI


Now that we have the data, we need to send it to PI. As the blog title suggests, we will use PI Web API as our data entry point. In order to do this, we have to make some transformations on our data. First, we have to add the WebID for the attribute we want to write data to and then we have to execute an HTTP POST with a JSON body containing the data itself.

 

The first thing is to get the WebID. Using Web ID 2.0, it's actually pretty easy to encode the AF path into a valid Web ID. I strongly suggest you Christopher Sawyer's excellent post on how to encode, decode, and some basic concepts behind it.

 

Going back to Node-RED, in order to encode the path into a valid Web ID, we have to execute custom code. This is easily done with the function node, where you can run any arbitrary javascript code. It exposes the whole message as a plain JavaScript object and allows you to manipulate it in a programmatic way.

 

path = msg.path
if (path.substring(0, 2) == "\\\\") {
path = path.substring(2);
}
var encoded_path = new Buffer(path.toUpperCase()).toString('base64');
var count = 0;
encoded_path = encoded_path.replace('+', '-').replace('/', '_');
for (var i = (encoded_path.length - 1); i > 0; i--) {
if (encoded_path[i] == "=") {
count++;
}
else {
break;
}
}
if (count > 0) {
encoded_path = encoded_path.slice(0, -count);
}
msg.webid = "P1AbE" + encoded_path
return msg;

Do you get the idea? I'm just getting the path variable from our message, encoding in base64 and concatenating with "P1Abe". The P1Abe WebID header means it encodes the path and refers to an AFAttribute that is a child of an AFElement. Once again, if you have not checked Chris' blog post, stop reading this article now and go read it! A final note for those JavaScript nerds wondering why I'm not using btoa(). Node-RED, as the name suggests, run on Node.js and it doesn't expose btoa() / atob().

 

At this point, this is our message object:

 

Our payload is ready and we have the Web ID encoded, so we are pretty much ready to send the data to PI. To do this, we now need the HTTP Request node.

 

It works by getting the payload content and sending it as a JSON body to a given URL. Here's the configuration for our example:

We define it as a POST to the Stream Controller Update Value Method, we set the URL, I enable SSL to properly handle the SSL certificate and finally, I select basic authentication. Let me just call your attention to the URL I'm using https://rborges-af/piwebapi/streams/{{webid}}/value. See the {{webid}}? It's a template system called Mustache Notation. It allows us to get a value from the message object and use it to feed the template.

 

Here's the full flow:

A recap: we subscribe to an MQTT topic, we convert the string into a valid JSON, we do some housekeeping by moving the path information out of the payload, we use the path to encode our WebID and we finally post the data. I added the debug node as an output of our request, so we can get the HTTP Response object and see if it's everything working properly. Here's the output:

 

 

The 202 status code on our response means that the data was sent. We can now see it on AF:

 

 

2.3 A Periodic Calculation Engine

 

Another cool thing that we can do with Node-RED, is to deploy flows that are triggered periodically. The Inject node allows us to not only inject an arbitrary JSON but also do it in a regular fashion. Here's an example. We first inject a JSON with two important tags that will be used for a given analysis:

 

 

Then we configure it to do it periodically. You can define a frequency (e.g., every 10 minutes), an interval between times (e.g., every 10 minutes between 8 AM and 3 PM), or at specific times (every noon on weekdays). For our example, every minute.

 

Now we can use it as the start trigger for our flow.

 

In this example, we are injecting the JSON every minute, we split it to get data from the PI Web API for each PI tag, we join the messages into a single message, we pass it through a generic splitter where we prepare it for FFT and the result we send it to our maintenance database. I'm using our beloved CDT158 and SINUSOID, but it could easily be vibrational data so we can log vibration information from a maintenance perspective.

 

Keep in mind that the inject node is just on way to do it. There are plenty of other ways for you to trigger a flow. Another possibility is to listen to multiple tags and start it only when a condition is met. I helped a customer a couple of years ago to wire up some electronics and trigger a flow when a door was opened and closed. This was used to log on PI when the lab door was open.

 

3. Conclusion

 

3.1 The PI System and IoT data

 

Today we saw how easy it is for you to wire up sensor data with PI, using nothing else than PI Web API requests. It's simple, cost-effective and fun to execute. I actually use this same architecture for my home automation system with PI (let me know in the comments if you would like to know why I use PI at my home).

 

But let me stress once again that this is just a proof-of-concept. An enterprise-grade project would never ever send data directly like that as it's a security breach and the lack of buffer makes it very unreliable. Once again give a look on EDS if you need to send edge data to your PI System.

 

3.2 Reference Material

In this blog we will discuss AF SDK's PIPoint.FindPIPoints method when passing in an enumerable collection of tag names.  As far as immediate usage of the method, there is very little to add that isn't already in the help.  A general weakness of many help examples is that the example is intentionally short, simple, and typically has perfect data.  This blog is being written with the specific thought that some of the tag names passed into FindPIPoints intentionally will not find a tag.  We offer a couple of ideas on what you can do to discover which ones were not found, though any good developer should not feel limited to just the ideas presented here.

 

My inspiration for this blog comes from 2 different partner code reviews hosted months apart.  Both applications used the PIDataPipe where a sizeable list of tag names were being read from a text file.  Despite both developers being quite skillful, I was taken aback that both would try to find each tag one-at-a-time.  Overall, their logic was similar:

 

  • Open the text file and read a tag name for each line.
  • Issue a PIPoint.TryFindPIPoint to find the current tag by its name.
  • If the tag was not found, log it for later diagnostics.
  • If the tag was found, issue a AddSignups for that tag, even if it is the only tag in a list.

 

As I mentioned, these developers were quite skilled and knew about bulk calls.  This was apparent in each application where they would later issue a RemoveSignups in bulk.  When I asked one of them why they would read 5000 tags one-at-a-time instead of in bulk, the answer was all about the logging.  It was critical to their application that it logs whenever a tag is not found.

 

The thing about FindPIPoints is that it only returns what is found (doh!).  If you pass in 1000 tag names and 100 of them are not found, then what you get back is 900 PIPoints.  What we will explore here are ways to quickly, easily, and efficiently discover which 100 were not found.  That is the missing piece as to why these developers were not using a bulk call.  This is not a hard task to code up, and the possible solutions covered here rely more upon simple .NET Framework objects, and very little to do with AF SDK (that is, beyond the FindPIPoints call).

 

Case Insensitive Dictionary

The coding challenge is to reconcile the list of tag names you pass into FindPIPoints and quickly determine which ones didn't make the cut.  As an application developer, my first choice to solve this would be a dictionary keyed by the tag name (a string) with a lookup value of a PIPoint.  I would also want this dictionary to be case insensitive just in case your exact casing of the tag name in the text file does not exactly match the casing of the tag name on the PI Data Archive.

 

The logic would be that every tag name you pass in will be a key in the dictionary.  We then will make a one bulk call to FindPIPoints.  We enumerate over the returned list of PIPoint to populate the dictionary with the found PIPoints.  Afterwards, any remaining dictionary entry that does not have a valid PIPoint would indicate a tag name that was not found.  The biggest takeaway is not the mechanics of the dictionary, but rather that we issued only one efficient bulk call to the PI Data Archive.

 

Since my example method calls FindPIPoints and FindPIPoints has an optional 3rd parameter of attributeNamesthen I too want my custom method to mirror the signature of FindPIPoints. 

 

LEGAL DISCLAIMER

Since we will share some sample .NET code, we must provide the obligatory legal notice and disclaimers.  The sample code being shared herein is subject to the following disclaimer:

 

Copyright 2019 OSIsoft, LLC.

 

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

 

http://www.apache.org/licenses/LICENSE-2.0

 

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

 

To put this another way, if you have problems with these sample methods, do NOT call Tech Support, nor create a case in my.OSIsoft.com. The only outlet for discussing issues with this code is here within the PI Developers Club forum.

 

The code being shared below:

  • Has not been put through any regression testing.
  • Has a very limited test profile (only me on my local PI Data Archive)
  • Has not been endorsed by any OSIsoft Product team
  • Is in no way an official offering by OSIsoft
  • Is offered only as a learning example

 

With that said, we strongly advise that you do not use this in your production code.  Keep in mind that we cannot stop you from doing so, but if you do, then it's no longer our code but rather is becomes YOUR code to support, maintain, validate, debug, and assume the full responsibilities as owner of said code.  Such is the nature of an "AS IS" license.

 

C# Sample Implementation Returning a Dictionary

public static IDictionary<string, PIPoint> GetExampleDictionary(PIServer piServer, IEnumerable<string> names, IEnumerable<string> attributeNames = null)
{
    if (piServer == null)
        throw new ArgumentNullException(nameof(piServer));
    if (names == null)
        throw new ArgumentNullException(nameof(names));

    // We will use a case-insensitive dictionary keyed by tag name with a Value of PIPoint.
    // A null PIPoint would indicate a tag name that was not found.
    var dict = new Dictionary<string, PIPoint>(StringComparer.OrdinalIgnoreCase);

    // We want every tag name to have an entry in our dictionary.
    // Initially the PIPoint will be null, which we try to populate shortly below.
    foreach (var tagname in names)
    {
        if (tagname != null)
            dict[tagname] = null;
    }

    if (dict.Count == 0)
        return dict;

    // Live Library: https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/M_OSIsoft_AF_PI_PIPoint_FindPIPoints_2.htm
    // If a tag name is not found, an exception is not thrown nor is a null PIPoint returned.
    // FindPIPoints will only return PIPoints that were actually found.
    var tags = PIPoint.FindPIPoints(piServer, names, attributeNames);
           
    // Finally we will assign the found PIPoints to its proper entry in the dictionary.
    // Note that some dictionary entries may still have null PIPoints for those tag names that were not found.
    foreach (var tag in tags)
    {
        dict[tag.Name] = tag;
    }

    return dict;
}

 

Great.  We issued a bulk call and thanks to a tiny bit of effort on our part, we now have a handy little dictionary at our fingertips.  Let's see an example on how we could consume this dictionary depending upon a given context of working with tags that have been found & validated, or working with tag names needing to log a "Not Found" message.  For setting up this example, we want to have a few good tag names along with some bad tag names.  For the bad ones, let's include some crazy edge cases of wild card patterns being used, duplicate names with different casing, or even a tag name containing illegal characters!

 

string wicked = "* ? ; { } [ ] | \\ ` ' \" ,"; // Every character not allowed in a tag name
string[] tagnames = new[] { "CDM158", "SINUSOID", "sinusoid", "sinus*", "This tag does not exist", "", "     ", wicked };

PIServer pida = new PIServers().DefaultPIServer;

var dict = LearningExample.GetExampleDictionary(pida, tagnames);
var points = dict.Values.Where(x => x != null).ToList();
var badNames = dict.Where(x => x.Value == null).Select(x => x.Key).ToList();

Console.WriteLine($"PIPoints Found: {points.Count}");
foreach (var point in points)
    Console.WriteLine($"   {point.Name}");

Console.WriteLine($"PIPoints NOT Found: {badNames.Count}");
foreach (var badName in badNames)
    Console.WriteLine($"   '{badName}'");

 

And that sample code might produce the following:

 

Sample Console Output
PIPoints Found: 2
   CDM158
   SINUSOID
PIPoints NOT Found: 5
   'sinus*'
   'This tag does not exist'
   ''
   '    '
   '* ? ; { } [ ] | \ ` ' " ,'

 

The help warns us that we cannot use wildcard patterns, so we understand why 'sinus*' is not found.  Observe that I even threw in some blank tag names, one with zero length and one with many blanks (which is why I quote the ones that are missing).  And the last line tells me that an exception will not be thrown even if you use illegal characters in the tag name being searched.

 

All my consuming application needed to do is take a little handling of whether a PIPoint was found or not.  Simple concept, simple code, but big performance difference when dealing with thousands of tags.

 

VB.NET Sample Implementation Returning a Dictionary

Public Function GetExampleDictionary(ByVal piServer As PIServer, ByVal names As IEnumerable(Of String), Optional ByVal attributeNames As IEnumerable(Of String) = Nothing) As IDictionary(Of String, PIPoint)

    If piServer Is Nothing Then Throw New ArgumentNullException(NameOf(piServer))
    If names Is Nothing Then Throw New ArgumentNullException(NameOf(names))

    ' We will use a case-insensitive dictionary keyed by tag name with a Value of PIPoint.
    ' A Nothing PIPoint would indicate a tag name that was not found.
    Dim dict = New Dictionary(Of String, PIPoint)(StringComparer.OrdinalIgnoreCase)

    ' We want every tag name to have an entry in our dictionary.
    ' Initially the PIPoint will be Nothing, which we try to populate shortly below.
    For Each tagname In names
        If tagname IsNot Nothing Then dict(tagname) = Nothing
    Next

    If dict.Count = 0 Then Return dict

    ' Live Library: https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/M_OSIsoft_AF_PI_PIPoint_FindPIPoints_2.htm
    ' If a tag name is not found, an exception is not thrown nor is a null PIPoint returned.
    ' FindPIPoints will only return PIPoints that were actually found.
    Dim tags = PIPoint.FindPIPoints(piServer, names, attributeNames)

    ' Finally we will assign the found PIPoints to its proper entry in the dictionary.
    ' Note that some dictionary entries may still have Nothing PIPoints for those tag names that were not found.
    For Each tag In tags
        dict(tag.Name) = tag
    Next

    Return dict
End Function

 

And here's an example of how VB.NET could consume that dictionary:

 

Dim wicked As String = "* ? ; { } [ ] | \ ` ' "" ,"  ' Every character not allowed in a tag name
Dim tagnames As String() = {"CDM158", "SINUSOID", "sinusoid", "sinus*", "This tag does not exist", "", "     ", wicked}

Dim pida As PIServer = New PIServers().DefaultPIServer

Dim dict = LearningExample.GetExampleDictionary(pida, tagnames, Nothing)
Dim points = dict.Values.Where(Function(x) x IsNot Nothing).ToList()
Dim badNames = dict.Where(Function(x) x.Value Is Nothing).Select(Function(x) x.Key).ToList()

Console.WriteLine($"PIPoints Found: {points.Count}")
For Each point In points
    Console.WriteLine($"   {point.Name}")
Next

Console.WriteLine($"PIPoints NOT Found: {badNames.Count}")
For Each badName In badNames
    Console.WriteLine($"   '{badName}'")
Next

 

Alternatively Return a ValueTuple

There are more than one way to do things and the above was my first train of thought.  I am the type of person that ponders other ways to try things to see which might be faster or easier to work with.  The problem I have with a dictionary being returned is that extra care and handling that must be done.  Other than the fact that you send in one list of tag names, what I really want coming back are 2 mostly unrelated things: I want a list of the PIPoints that were found and I want a separate list of the tag names where a PIPoint was not found.

 

Wanting to avoid the cumbersomeness of using out modifiers on the signature of a void method, I next considered tuples.  In particular, the ValueTuple class.

 

If you are using Visual Studio 2017 with .NET 4.6.2, you will need to install the NuGet package for ValueTuple.  If you are using Visual Studio 2019 with .NET 4.7.2, ValueTuple is now a part of mscorlib.  Tip: if you are converting from VS 2017 to VS 2019 and also upgraded the target framework being used, you will need to uninstall the NuGet ValueTuple package.

 

Internally, the methods will still use a case insensitive dictionary.  The only difference is what type of object we will return.

 

C# Sample Implementation Returning a ValueTuple

public static (IList<PIPoint> FoundList, IList<string> MissingList) GetExampleValueTuple(this PIServer piServer, IEnumerable<string> names, IEnumerable<string> attributeNames = null)
{
    if (piServer == null)
        throw new ArgumentNullException(nameof(piServer));
    if (names == null)
        throw new ArgumentNullException(nameof(names));

    // We will use a case-insensitive dictionary keyed by tag name with a Value of PIPoint.
    // A null PIPoint would indicate a tag name that was not found.
    var dict = new Dictionary<string, PIPoint>(StringComparer.OrdinalIgnoreCase);

    // We want every tag name to have an entry in our dictionary.
    // Initially the PIPoint will be null, which we try to populate shortly below.
    foreach (var tagname in names)
    {
        if (tagname != null)
            dict[tagname] = null;
    }

    if (dict.Count == 0)
        return (new PIPointList(), new List<string>());

    // Live Library: https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/M_OSIsoft_AF_PI_PIPoint_FindPIPoints_2.htm
    // If a tag name is not found, an exception is not thrown nor is a null PIPoint returned.
    // FindPIPoints will only return PIPoints that were actually found.
    var tags = PIPoint.FindPIPoints(piServer, names, attributeNames);

    // Finally we will assign the found PIPoints to its proper entry in the dictionary.
    // Note that some dictionary entries may still have null PIPoints for those tag names that were not found.
    foreach (var tag in tags)
    {
        dict[tag.Name] = tag;
    }

    // The variable 'tags' has the found PIPoints.
    // Let's create a list of tag names of any PIPoints that were not found, if any.
    var missing = dict.Where(x => x.Value == null)?.Select(x => x.Key);

    // Return a tuple of the found tags and the missing tag names.
    // The consumer can pass tags directly into a PIDataPipe.AddSignups,
    // and log the missing tag names.
    return (tags, missing.ToList());
}

 

C# allows different ways we can consume this object.  Both C# and VB.NET allow you to grab the returned ValueTuple as an singular object and then use its different fields:

 

var tagLookup = pida.GetExampleValueTuple(tagnames);

Console.WriteLine($"PIPoints Found: {tagLookup.FoundList.Count}");
foreach (var point in tagLookup.FoundList)
    Console.WriteLine($"   {point.Name}");

Console.WriteLine($"PIPoints NOT Found: {tagLookup.MissingList.Count}");
foreach (var badName in tagLookup.MissingList)
    Console.WriteLine($"   '{badName}'");

 

In the first line above, the tagLookup.FoundList will be a IList<PIPoint> and the tagLookup.MissingList will be an IList<string>.

 

The other nice way (which VB.NET lacks BTW) is to decompose the fields into individual variables as they are declared:

 

var (points, badNames) = pida.GetExampleValueTuple(tagnames, null);

Console.WriteLine($"PIPoints Found: {points.Count}");
foreach (var point in points)
    Console.WriteLine($"   {point.Name}");

Console.WriteLine($"PIPoints NOT Found: {badNames.Count}");
foreach (var badName in badNames)
    Console.WriteLine($"   '{badName}'");

 

In the first line above, the variable points will be a IList<PIPoint> and the variable badNames will be an IList<string>.

 

VB.NET Sample Implementation Returning a ValueTuple

<Extension()> Public Function GetExampleValueTuple(ByVal piServer As PIServer, ByVal names As IEnumerable(Of String), Optional ByVal attributeNames As IEnumerable(Of String) = Nothing) As (TagsFound As IList(Of PIPoint), MissingTagNames As IList(Of String))

    If piServer Is Nothing Then Throw New ArgumentNullException(NameOf(piServer))
    If names Is Nothing Then Throw New ArgumentNullException(NameOf(names))

    ' We will use a case-insensitive dictionary keyed by tag name with a Value of PIPoint.
    ' A Nothing PIPoint would indicate a tag name that was not found.
    Dim dict = New Dictionary(Of String, PIPoint)(StringComparer.OrdinalIgnoreCase)

    ' We want every tag name to have an entry in our dictionary.
    ' Initially the PIPoint will be Nothing, which we try to populate shortly below.
    For Each tagname In names
        If tagname IsNot Nothing Then dict(tagname) = Nothing
    Next

    If dict.Count = 0 Then Return (New List(Of PIPoint)(), New List(Of String)())

    ' Live Library: https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/M_OSIsoft_AF_PI_PIPoint_FindPIPoints_2.htm
    ' If a tag name is not found, an exception is not thrown nor is a null PIPoint returned.
    ' FindPIPoints will only return PIPoints that were actually found.
    Dim tags = PIPoint.FindPIPoints(piServer, names, attributeNames)

    ' Finally we will assign the found PIPoints to its proper entry in the dictionary.
    ' Note that some dictionary entries may still have Nothing PIPoints for those tag names that were not found.
    For Each tag In tags
        dict(tag.Name) = tag
    Next

    ' The variable 'tags' has the found PIPoints.
    ' Let's create a list of tag names of any PIPoints that were not found, if any.
    Dim missing = dict.Where(Function(x) x.Value Is Nothing).Select(Function(x) x.Key)

    ' Return a tuple of the found tags and the missing tag names.
    ' The consumer can pass tags directly into a PIDataPipe.AddSignups,
    ' and log the missing tag names.
    Return (tags, missing.ToList())

End Function

 

As mentioned above, VB.NET is unable to decompose the fields on one single line. Thus you must use the entire ValueTuple object and work with its fields.  Here is a simple example:

 

Dim tagLookup = pida.GetExampleValueTuple(tagnames)
Dim points = tagLookup.TagsFound
Dim badNames = tagLookup.MissingTagNames

Console.WriteLine($"PIPoints Found: {points.Count}")
For Each point In points
    Console.WriteLine($"   {point.Name}")
Next

Console.WriteLine($"PIPoints NOT Found: {badNames.Count}")
For Each badName In badNames
    Console.WriteLine($"   '{badName}'")
Next

 

Conclusion

The code above has not done anything special with the FindPIPoints method.  In fact, we are using it exactly for the purposes of which it was intended, and also exactly in the manner in which it was intended.  All we really did was considered various ways to take the results returned by FindPIPoints and transform them into another object more conducive to certain applications.  We then threw both good and bad cases at it to see if that transformation behaves as we expected.  These sample methods just ever so slightly touched upon AF SDK, and the subsequent transformation relies on very common, very routine .NET objects.

 

We showed 2 possible ways for you to transform the results.  And I am sure these are not the only ways.  I encourage you to contemplate these and other techniques to find what works best for you and your company based on your coding experience, preferences, style, and company policies.

Introduction

 

PI Web API 2017 R2 is the first version that implements Web ID 2.0, which is great to improve the performance of your applications. Using this feature, your app doesn't need to make an HTTP request in order to get the Web ID of a PI System object since now you can generate it directly on the client side. If you want to learn more about Web ID 2.0 please refer to this material.

 

The purpose of this blog post is to show a simple way to generate Web IDs 2.0 in Python and Java.

 

Explaining the logic

 

Using the code below, you can generate Web IDs 2.0 Path only type with the following structure:

  • The first character will always be 'P'.
  • The second character will always be '1', which means that the Web ID version is 2.0.
  • Then we have the Marker with two characters which refers to the object's type.
  • Then we have the Owner Marker with 1 character (optional).
  • Finally, we have the Name Payload which encodes the following string PIObject.GetPath().Substring(2).ToUpperInvariant().

 

If you take a look at the Web ID 2.0 Specification Tables, you will realize that the Owner Marker needs to be defined only for some object types. The reason is that some object types could belong to different owners.

 

Let's take an example. We know that a PI Point will always belong to a PI Data Archive. Nevertheless, an AF Attribute could belong to an AF Element or to an AF Event Frame. This is why you don't need to define an Owner Marker for a PI Point but you do need to do it for an AF Attribute.

 

The beauty of the code below is that you can generate Web IDs for all objects using just a single method GenerateWebIdByPath method with 3 inputs:

  • Path of the PI System object
  • Class of the PI System object
  • Class of the owner of the PI System object (optional)

 

Here is a summary of what the code actually does:

  • Convert the object type to a Marker
  • Convert the owner object type to an Owner Marker
  • Validate if the Marker and Owner Marker are valid
  • Encode the Name Payload
  • Generate the Web ID 2.0

 

And finally the code...

 

Disclaimer:

This code could contain bugs and shouldn’t be used in production without extensive testing.

You agree that if you use any of the provided code in your own production code that you accept all ownership, risks, liabilities, and responsibilities associated with the performance, support, and maintenance of the code.

 


Licensing

 

Copyright 2019 OSIsoft, LLC.

 

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

 

http://www.apache.org/licenses/LICENSE-2.0

 

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

 

To put this another way, if you have problems with this method, do NOT call Tech Support, nor create a case in my.OSIsoft.com. The only outlet for discussing issues with this code is here within the PI Developers Club forum.


Sample Python code

 

import base64



class WebIdHelper(object):
def __init__(self):
self.marker_owner = None
pass




def generate_web_id_by_path(self, path,  objct_type, owner_type=None):
self.validate_type_and_owner_type(objct_type, owner_type)
marker = self.get_marker(objct_type)
owner_marker = self.get_owner_marker(owner_type)
if path[0:2] == "\\\\":
path = path[2:]
encoded_path = self.encode_string(path.upper())
return "P1{}{}{}".format(marker, owner_marker, encoded_path)


def validate_type_and_owner_type(self, object_type, owner_type):
if isinstance(PIAttribute(), object_type):
if isinstance(PIElement(), owner_type) and isinstance(PIEventFrame(), owner_type):
raise WebIdException("PIAttribute owner type must be a PIElement or a PIEventFrame.")
elif isinstance(PIAttributeTemplate(), object_type):
if isinstance(PIElementTemplate(), owner_type):
raise WebIdException("PIElementTemplate owner type must be a PIElementTemplate.")
elif isinstance(PIEnumerationSet(), object_type) or isinstance(PIEnumerationValue(), object_type):
if isinstance(PIDataServer(), owner_type) == False and isinstance(PIAssetServer(), owner_type) == False:
raise  WebIdException("PIEnumerationSet and  PIEnumerationValue owner type must be a PIDataServer or PIAssetServer.")
elif isinstance(PITimeRule(), object_type):
if isinstance(PIAnalysis(), owner_type) and isinstance(PIAnalysisTemplate(), owner_type):
raise WebIdException("PITimeRule owner type must be a PIAnalysis and PIAnalysisTemplate.")


def get_owner_marker(self, owner_type):
if owner_type == None:
return ""
if isinstance(PIAssetServer(),owner_type):
self.marker_owner = "R"
elif isinstance(PIDataServer(), owner_type):
self.marker_owner = "D"
elif isinstance(PIAnalysis(), owner_type):
self.marker_owner = "X"
elif isinstance(PIAnalysisTemplate(), owner_type):
self.marker_owner = "T"
elif isinstance(PIElement(), owner_type):
self.marker_owner = "E"
if isinstance(PIElementTemplate(), owner_type):
self.marker_owner = "E"
elif isinstance(PIEventFrame(), owner_type):
self.marker_owner = "F"
return self.marker_owner


def get_marker(self, object_type):
marker = None


if isinstance(PIAnalysis(), object_type):
marker = "Xs"
elif isinstance(PIAnalysisCategory(), object_type):
marker = "XC"
elif isinstance(PIAnalysisTemplate(), object_type):
marker = "XT"
elif isinstance(PIAnalysisRule(), object_type):
marker = "XR"
elif isinstance(PIAnalysisRulePlugIn(), object_type):
marker = "XP"
elif isinstance(PIAttribute(), object_type):
marker = "Ab"
elif isinstance(PIAttributeCategory(), object_type):
marker = "AC"
elif isinstance(PIAttributeTemplate(), object_type):
marker = "AT"
elif isinstance(PIAssetDatabase(), object_type):
marker = "RD"
elif isinstance(PIAssetServer(), object_type):
marker = "RS"
elif isinstance(PIElement(), object_type):
marker = "Em"
elif isinstance(PIElementCategory(), object_type):
marker = "EC"
elif isinstance(PIElementTemplate(), object_type):
marker = "ET"
elif isinstance(PIEnumerationSet(), object_type):
marker = "MS"
elif isinstance(PIEnumerationValue(), object_type):
marker = "MV"
elif isinstance(PIEventFrame(), object_type):
marker = "Fm"
elif isinstance(PITimeRule(), object_type):
marker = "TR"
elif isinstance(PITimeRulePlugIn(), object_type):
marker = "TP"
elif isinstance(PISecurityIdentity(), object_type):
marker = "SI"
elif isinstance(PISecurityMapping(), object_type):
marker = "SM"
elif isinstance(PITable(), object_type):
marker = "Bl"
elif isinstance(PITableCategory(), object_type):
marker = "BC"
elif isinstance(PIPoint(), object_type):
marker = "DP"
elif isinstance(PIDataServer(), object_type):
marker = "DS"
elif isinstance(PIUnit(), object_type):
marker = "Ut"
elif isinstance(PIUnitClass(), object_type):
marker = "UC"
if (marker == None):
raise WebIdException("Invalid object type.")
return marker


def encode_string(self, value):
bytes = value.upper().encode('utf-8')
return self.encode(bytes)


def encode(self, value):
encoded = base64.b64encode(value).decode()
return encoded.strip('=').replace('+', '-').replace('/', '_')

 

Sample Java code

 

 

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.UUID;

public class WebIdHelper
{
    public WebIdInfo getWebIdInfo(String webId) throws WebIdException {
        return new WebIdInfo(webId);
    }

    public String generateWebIdByPath(String path, Class type, Class ownerType) throws WebIdException {
        validateTypeAndOwnerType(type, ownerType);
        String marker = getMarker(type);
        String ownerMarker = getOwnerMarker(ownerType);

        if (path.substring(0,2).equals("\\\\"))
        {
            path = path.substring(2, path.length());
        }
        String encodedPath = encode(path.toUpperCase());
        return ("P1" + marker + ownerMarker + encodedPath);
    }

    private void validateTypeAndOwnerType(Class type, Class ownerType) throws WebIdException {
        if (type == PIAttribute.class)
        {
            if ((ownerType != PIElement.class) && (ownerType != PIEventFrame.class))
            {
                throw new WebIdException("PIAttribte owner type must be a PIElement or a PIEventFrame.");
            }
        }
        else if (type == PIAttributeTemplate.class)
        {
            if ((ownerType != PIElementTemplate.class))
            {
                throw new WebIdException("PIElementTemplate owner type must be a PIElementTemplate.");
            }
        }
        else if ((type == PIEnumerationSet.class) || (type == PIEnumerationValue.class))
        {
            if ((ownerType != PIDataServer.class) && (ownerType != PIAssetServer.class))
            {
                throw new WebIdException("PIEnumerationSet and  PIEnumerationValue owner type must be a PIDataServer or PIAssetServer.");
            }
        }
        else if (type == PITimeRule.class)
        {
            if ((ownerType != PIAnalysis.class) && (ownerType != PIAnalysisTemplate.class))
            {
                throw new WebIdException("PITimeRule owner type must be a PIAnalysis and PIAnalysisTemplate.");
            }
        }
    }

    private String getOwnerMarker(Class ownerType)
    {
        String markerOwner = "";
        if (ownerType == null)
        {
            return markerOwner;
        }

        if (ownerType == PIAssetServer.class)
        {
            markerOwner = "R";
        }
        else if (ownerType == PIDataServer.class)
        {
            markerOwner = "D";
        }
        else if (ownerType == PIAnalysis.class)
        {
            markerOwner = "X";
        }
        else if (ownerType == PIAnalysisTemplate.class)
        {
            markerOwner = "T";
        }
        else if (ownerType == PIElement.class)
        {
            markerOwner = "E";
        }
        if (ownerType == PIElementTemplate.class)
        {
            markerOwner = "E";
        }
        else if (ownerType == PIEventFrame.class)
        {
            markerOwner = "F";
        }
        return markerOwner;
    }

    private String getMarker(Class type) throws WebIdException {
        String marker = null;
        if (type == PIAnalysis.class)
        {
            marker = "Xs";
        }
        else if (type == PIAnalysisCategory.class)
        {
            marker = "XC";
        }
        else if (type == PIAnalysisTemplate.class)
        {
            marker = "XT";
        }
        else if (type == PIAnalysisRule.class)
        {
            marker = "XR";
        }
        else if (type == PIAnalysisRulePlugIn.class)
        {
            marker = "XP";
        }
        else if (type == PIAttribute.class)
        {
            marker = "Ab";
        }
        else if (type == PIAttributeCategory.class)
        {
            marker = "AC";
        }
        else if (type == PIAttributeTemplate.class)
        {
            marker = "AT";
        }
        else if (type == PIAssetDatabase.class)
        {
            marker = "RD";
        }
        else if (type == PIAssetServer.class)
        {
            marker = "RS";
        }
        else if (type == PIElement.class)
        {
            marker = "Em";
        }
        else if (type == PIElementCategory.class)
        {
            marker = "EC";
        }
        else if (type == PIElementTemplate.class)
        {
            marker = "ET";
        }
        else if (type == PIEnumerationSet.class)
        {
            marker = "MS";
        }
        else if (type == PIEnumerationValue.class)
        {
            marker = "MV";
        }
        else if (type == PIEventFrame.class)
        {
            marker = "Fm";
        }
        else if (type == PITimeRule.class)
        {
            marker = "TR";
        }
        else if (type == PITimeRulePlugIn.class)
        {
            marker = "TP";
        }
        else if (type == PISecurityIdentity.class)
        {
            marker = "SI";
        }
        else if (type == PISecurityMapping.class)
        {
            marker = "SM";
        }
        else if (type == PITable.class)
        {
            marker = "Bl";
        }
        else if (type == PITableCategory.class)
        {
            marker = "BC";
        }
        else if (type == PIPoint.class)
        {
            marker = "DP";
        }
        else if (type == PIDataServer.class)
        {
            marker = "DS";
        }
        else if (type == PIUnit.class)
        {
            marker = "Ut";
        }
        else if (type == PIUnitClass.class)
        {
            marker = "UC";
        }
        if (marker == null)
        {
            throw new WebIdException("Invalid object type.");
        }

        return marker;
    }

    public static String encode(String value)
    {
        byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
        return encode(bytes);
    }

    public static String encode(byte[] bytes)
    {
        String value =  Base64.getEncoder().encodeToString(bytes);
        value = trimString(value, '=');
        return value.replace('+', '-').replace('/', '_');
    }

    public static String encode(UUID value)
    {
        byte[] bytes = value.toString().getBytes();
        return encode(bytes);
    }

    public static String trimString(String text, char trimBy) {
        int beginIndex = 0;
        int length = text.length();
        char[] textChar = text.toCharArray();

        while ((beginIndex < length) && (textChar[beginIndex] == trimBy)) {
            beginIndex++;
        }

        while ((beginIndex < length) && (textChar[length-1] == trimBy)) {
            length--;
        }

        if ((beginIndex > 0) || (length < text.length()))
        {
            return text.substring(beginIndex, length);
        }
        else
        {
            return  text;
        }
    }
}

 

Final Remarks and Conclusion

 

We hope that your Java and Python application will have a better performance by taking advantage of the Web ID 2.0 client-side generation!

 

Remember that PI Web API versions prior to 2017 R2 won't understand the Web IDs generated using this blog post!

Introduction

Normally, when we search for PI Points in PI Web API, we come equipped with its path. In this case, we can simply use the GetByPath action of the Point Controller to achieve this.

Point Controller GetByPath

 

But what if one day, your custom web application has a requirement where a list of Point IDs is given to you and there is a need for you to find the name of the PI Point corresponding to these IDs? If you are developing your application using the .NET Framework, then you have the option to use the PIPoint.FindPIPoint method available in AF SDK.

PIPoint.FindPIPoint

But since you are developing a web application, you do not have that luxury.

 

Today, in this blog post, I will be addressing how to search for a PI Point using its Point ID via PI Web API and WebID 2.0. An example of such a request can be found here.

https://pisquare.osisoft.com/thread/39989-find-pi-point-by-id

 

Concepts

If you don't know already, WebID version 2.0, introduced in PI Web API 2017 R2, provides different types of WebIDs (see: WebID Type). Specifying the WebID type gives you options for reducing WebID sizes (for URL length limitations), for identifying ambiguous paths/names of AF Event Frames and AF Notifications, and for accommodating path and name changes.

 

I will be using the IDOnly type today to achieve my goal of searching via the Point ID. The language of choice will be JavaScript.

 

Let's take a look at the composition of the IDOnly type of a PI Point WebID.

 

I chose a sample that is available from our public PI Web API endpoint found here.

I1DPW6Wlk0_Utku9vWTvxg45oA0egAAA

 

Let's try to break it down.

 

NameValue
IWeb ID type indicator. “IDOnly” in this case
1Web ID version number
DPWeb ID marker for PI Point objects
W6Wlk0_Utku9vWTvxg45oAURL Safe Base64 encoded string of PI Data Archive Id
0egAAAURL Safe Base64 encoded string of PI Point Id

 

With this knowledge, we can see that we are able to use the Point ID directly by encoding it in the WebID and using it in the Point Controller Get action.

Point Controller Get

 

Function to generate IDOnly type of WebID

Disclaimer:

This code could contain bugs and shouldn’t be used in production without extensive testing.

You agree that if you use any of the provided code in your own production code that you accept all ownership, risks, liabilities, and responsibilities associated with the performance, support, and maintenance of the code.

 

Please see this post here if you would like to see this same block of code with nicer indents.

function NewIDOnlyWebID(dataType, guid, oid, ownerType, ownerguid) {

//get the marker for the datatype
var marker = getOwnerMarker(dataType, ownerType)

//encode the server id to a base64 string
var serverwebid = encodeguid(guid)

//encode the owner id if datatype has a owner
var typeswithowner = ["AFAnalysisRule", "AFAttribute", 'AFAttributeTemplate', "AFEnumerationValue", "AFTimeRule"];
if (typeswithowner.includes(dataType)) {
if (ownerguid) {
serverwebid += encodeguid(ownerguid)
}
else {
throw 'please provide a valid owner guid'
}
}

//return webid if datatype is a server
if (dataType == "PIServer" || dataType == "PISystem") {
return 'I1' + marker + (serverwebid).replace(/=/g, '').replace(/\//g, '_').replace(/\+/g, '-')
}

//return webid if datatype is a pi point
if (dataType == "PIPoint") {
if (!oid) throw 'provide a valid PI Point ID'
var arr = new Uint8Array(new Uint32Array([oid]).buffer);
var pointwebid = btoa(String.fromCharCode.apply(null, arr))
return 'I1' + marker + (serverwebid + pointwebid).replace(/=/g, '').replace(/\//g, '_').replace(/\+/g, '-')
}

//return webid for af objects
var afwebid = encodeguid(oid)
return 'I1' + marker + (serverwebid + afwebid).replace(/=/g, '').replace(/\//g, '_').replace(/\+/g, '-')
}

function encodeguid(guid) {
var s = guid.replace(/[^0-9a-f]/ig, '').toLowerCase();

//check for invalid guid
if (s.length != 32) throw 'invalid guid';

//arrange bytes as PI Web API uses Microsoft style GUID
s = s.slice(6, 8) + s.slice(4, 6) + s.slice(2, 4) + s.slice(0, 2) +
s.slice(10, 12) + s.slice(8, 10) +
s.slice(14, 16) + s.slice(12, 14) +
s.slice(16);

//base64 encode the byte array
var t = '';
for (var n = 0; n < s.length; n += 2) {
t += String.fromCharCode(parseInt(s.substr(n, 2), 16));
}
return btoa(t)
}

function getOwnerMarker(dataType, ownerType) {
var marker
switch (dataType) {
case 'AFAnalysis':
marker = 'XS'
break;
case 'AFAnalysisCategory':
marker = 'XC'
break;
case 'AFAnalysisTemplate':
marker = 'XT'
break;
case 'AFAnalysisRule':
marker = 'XR'
switch (ownerType) {
case 'AFAnalysis':
marker += "X"
break
case 'AFAnalysisTemplate':
marker += "T"
break
default:
throw "please provide owner type"
}
break;
case 'AFAnalysisRulePlugin':
marker = 'XP'
break;
case 'AFAttribute':
marker = 'Ab'
switch (ownerType) {
case 'AFElement':
marker += "E"
break
case 'AFEventFrame':
marker += "F"
break
case 'AFNotification':
marker += "N"
break
default:
throw "please provide owner type"
}
break;
case 'AFAttributeCategory':
marker = 'AC'
break;
case 'AFAttributeTemplate':
marker = 'ATE'
break;
case 'AFDatabase':
marker = 'RD'
break;
case 'AFElement':
marker = 'Em'
break;
case 'AFElementCategory':
marker = 'EC'
break;
case 'AFElementTemplate':
marker = 'ET'
break;
case 'AFEnumerationSet':
marker = 'MS'
switch (ownerType) {
case 'PISystem':
marker += "R"
break
case 'PIServer':
marker += "D"
break
default:
throw "please provide owner type"
}
break;
case 'AFEnumerationValue':
marker = 'MV'
switch (ownerType) {
case 'PISystem':
marker += "R"
break
case 'PIServer':
marker += "D"
break
default:
throw "please provide owner type"
}
break;
case 'AFEventFrame':
marker = 'Fm'
break;
case 'AFNotification':
marker = 'Nf'
break;
case 'AFNotificationTemplate':
marker = 'NT'
break;
case 'AFNotificationContactTemplate':
marker = 'NC'
break;
case 'AFTimeRule':
marker = 'TR'
switch (ownerType) {
case 'AFAnalysis':
marker += "X"
break
case 'AFAnalysisTemplate':
marker += "T"
break
default:
throw "please provide owner type"
}
break;
case 'AFTimeRulePlugin':
marker = 'TP'
break;
case 'AFSecurityIdentity':
marker = 'SI'
break;
case 'AFSecurityMapping':
marker = 'SM'
break;
case 'AFTable':
marker = 'Bl'
break;
case 'AFTableCategory':
marker = 'BC'
break;
case 'PIPoint':
marker = 'DP'
break;
case 'PIServer':
marker = 'DS'
break;
case 'PISystem':
marker = 'RS'
break;
case 'UOM':
marker = 'Ut'
break;
case 'UOMClass':
marker = 'UC'
break;
default:
throw "please provide a suitable datatype"

}
return marker
}

The code above provides a generalized function that can help you to generate a PI Point WebID. The function takes 5 parameters.

 

namevalue
dataTypethe type of object
guidthe guid of the server that the object belongs to
oidthe id of the object itself
ownerTypethe type of the owner object (only required for objects with owner types)
ownerguidthe guid of the owner object (only required for objects with owner types)

 

 

Example usage for PI Point objects, 

console.log(NewIDOnlyWebID('PIPoint', '93a5a55b-d44f-4bb6-bdbd-64efc60e39a0', 59601))

Result will be the PI Point WebID

I1DPW6Wlk0_Utku9vWTvxg45oA0egAAA

 

Example usage for AF Attribute objects belong to an AF Element,

console.log(NewIDOnlyWebID('AFAttribute', '0b101021-e3bc-433d-9f06-a6a2db5f0803', '4f46d670-487e-5aa1-38b9-cd626ea43bc6', 'AFElement', 'cd24b9af-68d5-11e8-80db-000d3a10c7ce'))

Result will be the AF Attribute WebID

I1AbEIRAQC7zjPUOfBqai218IAwr7kkzdVo6BGA2wANOhDHzgcNZGT35IoVo4uc1ibqQ7xg

 

 

How to get the object guids?

For PI Data Archive, we can make a call to the DataServer Controller and utilize its GetByPath action and only select the Id field. An example is shown below.

 

 

Similarly for AF Server, we can make a call to the AssetServer Controller and utilize its GetByPath action and only select the Id field. I will leave this as an exercise for you to try out.

 

For AF objects, we can find the guid at the lower right corner of PI System Explorer.

 

Solve the initial problem

At the start of this blog, our challenge was to find the name of the PI Point corresponding to a Point ID that was given. Now with the PI Point WebID that was generated, we can easily do so with the Get action of the Point Controller.

 

 

 

Conclusion

We can see that WebID 2.0 has enhanced the flexibility of PI Web API and it allows us to do things that were previously thought impossible. The possibilities are endless and only limited by your imagination. I hoped you have enjoyed reading this blog and learnt something useful from it.

 

Please drop any comments below!

 

 

 

See also

pi-web-api-web-id-20-specification-tables

using-web-id-20-to-optimize-your-applications

Filter Blog

By date: By tag: