Skip navigation
All Places > PI Developers Club > Blog > Authors Marcos Vainer Loeff
1 2 3 Previous Next

PI Developers Club

83 Posts authored by: Marcos Vainer Loeff Employee

Introduction

 

On the first two parts of this blog post series (part 1 and part 2), you have learned how to create an ASP.NET Core web application using PI AF SDK and .NET Framework. On the second part, you've learned how to secure your web app using ASP.NET Core Identity.

 

On this blog post, we are going to learn one of the best ASP.NET Core features which is Dependency Injection and apply this technique when making calls against our PI System.

 

 

What is Dependency Injection (DI)?

 

According to Wikipedia:

 

"In software engineering, dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object. A dependency is an object that can be used (a service)."

 

Let's try to understand the concept of Dependency Injection through our example from the first part of this blog post series

 

When class A uses some functionality of class B, then it is said that class A has a dependency of class B. In the context of C#, class A needs to create an instance of class B in order to use its methods and properties. 

 

So, transferring the task of creating the object to another entity and directly using the dependency is called dependency injection.

 

In our sample, our HomeController class uses some PI AF SDK methods to render the snapshot values of the found PI Points to the user. We will make a small change in this project by creating a new service of the type IPISystemWrapper, responsible for all PI System communication which will be injected to our HomeController. As a result, the HomeController won't have any PI AF SDK code anymore. It will only inject the IPISystemWrapper service/interface and call its methods to deliver to the Views.

 

 

Why should I use DI when developing apps on top of the PI System?

 

Here are the main advantages of using DI in this context:

 

  • Easier to extend the application. Imagine that many controllers are injecting the IPISystemWrapper service. If you add a new method to this interface, then all the controllers will be able to take advantage of this new feature. As a result, this strategy helps reducing duplicated code.
  • Easier to maintain your code base. The service IPISystemWrapper could have different implementations. The AfSdkWrapper class could be the implementation for PI AF SDK of this service and the PIWebApiWrapper could be the implementation for using PI Web API. This is strategy improves your code base not only from the organization perspective but from the maintainability standpoint.
  • Enable loose coupling. Imagine that you must upgrade your web application from ASP.NET Core 2.2 to ASP.NET Core 3 once it is released. ASP.NET Core 3 cannot be created on top of .NET Framework and therefore, it won't be compatible with the current version of PI AF SDK. I will show you how easy it is to switch from PI AF SDK to another PI Developer Technology.
  • Helps Unit Testing: since all the PI AF SDK code will be written on a unique class. All your unit tests concerning PI AF SDK can be written for this particular class. If you have many controllers using PI AF SDK, it gets more complicated to achieve this goal.

 

Implementing DI on our example

 

This example is simpler than the one from part 1. This is why I am pasting the code from the main files of the VS project here. Nevertheless, please refer to the previous blog post in case something is not clear since I will focus on the DI concepts on this blog post.

 

Disclaimer:

Any of the code in this blog 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.

 

Create a new ASP.NET Core project on top of .NET Framework and add PI AF SDK as a reference.

 

 

Create some Views with the content below in order to display the results to the user:

 

\Views\Home\Query.cshtml

 

<div class="col-sm-8 col-sm-offset-2">
<h1>View Snapshot Values</h1>
<br /><br />
<h4>Search for PI Points and check its snapshots.</h4>
<br /><br />
<div class="container">
<div class="row">
<form asp-controller="Home" asp-action="Result" method="get" class="form-inline">
<div class="form-group">
<label class="sr-only" for="name">Search PI Point: </label>
<input id="pointNameFilter" name="pointNameFilter" placeholder="Point Name" class="form-control" />
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
</div>
</div>

 

\Views\Home\Result.cshtml

@model IEnumerable<PIPointSnapshotModelView>

<div class="col-sm-8 col-sm-offset-2">
<h1>View Snapshot Values</h1>
<br /><br />
<table class="table">
<tr>
<th>PI Point</th>
<th>Value</th>
<th>Timestamp</th>
</tr>
@foreach (var pointSnapshotValue in Model)
{
<tr>
<td>@pointSnapshotValue.PointName</td>
<td>@pointSnapshotValue.Value.ToString()</td>
<td>@pointSnapshotValue.TimeStamp.ToString("dd-MMM-yyyy hh:mm:ss tt")</td>
</tr>
}
</table>
<br />
<center>
<a asp-controller="Home" asp-action="Index" class="btn btn-primary">Make another search</a>
</center>
</div>

 

\Views\Shared\_Layout.cshtml

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<title>My ASP.NET Core MVC with PI AF SDK or PI Web API</title>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<a asp-page="/Index" class="navbar-brand">Applying ASP.NET Core DI with the PI System </a>
</div>
<div class="navbar-collapse collapse">
</div>
</div>
</nav>
<div class="container body-content"style="padding-top: 50px;">
@RenderBody()
</div>
</body>
</html>

 

\Views\_ViewImports.cshtml

@using ASPNETCoreWithPIAFSDK.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

 

\Views\_ViewStart.cshtml

@{
Layout = "_Layout";
}

 

Create a model for the Result.cshtml view:

 

\Models\PIPointSnapshotModelView.cs

using OSIsoft.AF.Asset;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ASPNETCoreWithPIAFSDK.Models
{
public class PIPointSnapshotModelView
{
public string PointName { get; set; }

public object Value { get; set; }
public DateTime TimeStamp { get; set; }


public PIPointSnapshotModelView(string pointName, AFValue afValue)
{
PointName = pointName;
Value = afValue.Value;
TimeStamp = afValue.Timestamp;
}

public PIPointSnapshotModelView(string pointName)
{
PointName = pointName;
}
}
}

 

 

Let's create a folder named Framework on the root with the IPISystemWrapper interface. This interface has only one method called GetSnapshotValuesFromPoints with one string input. This function finds all PI Points filtered using the pointNameFilter input and then finds all of its snapshots.

 

\Framework\IPISystemWrapper.cs

using System.Collections.Generic;
using System.Threading.Tasks;
using ASPNETCoreWithPIAFSDK.Models;

namespace ASPNETCoreWithPIAFSDK.Framework
{
public interface IPISystemWrapper
{
Task<IEnumerable<PIPointSnapshotModelView>> GetSnapshotValuesFromFoundPoints(string pointNameFilter);
}
}

 

The AfSdkWrapper class implements the IPISystemWrapper interface. This is the only class that has PI AF SDK under this VS project. This class implements GetSnapshotValuesFromPoints method using PI AF SDK code in order to show to the user the requested snapshots.

 

\Framework\AfSdkWrapper.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ASPNETCoreWithPIAFSDK.Models;
using OSIsoft.AF;
using OSIsoft.AF.Asset;
using OSIsoft.AF.PI;

namespace ASPNETCoreWithPIAFSDK.Framework
{
public class AfSdkWrapper : IPISystemWrapper
{
private PIServer piServer;

public AfSdkWrapper()
{
//On production, please store the PI Data Archive name on appsettings.json
this.piServer = new PIServers()["MARC-PI2016"];
piServer.Connect();
}
public async Task<IEnumerable<PIPointSnapshotModelView>> GetSnapshotValuesFromFoundPoints(string pointNameFilter)
{
PIPointQuery query = new PIPointQuery(PICommonPointAttributes.Tag, OSIsoft.AF.Search.AFSearchOperator.Equal, pointNameFilter);
IEnumerable<PIPoint> foundPoints = PIPoint.FindPIPoints(piServer, new PIPointQuery[] { query });
PIPointList pointList = new PIPointList(foundPoints.Take(1000));


List<PIPointSnapshotModelView> pointSnapshotValueList = new List<PIPointSnapshotModelView>();
AFListResults<PIPoint, AFValue> values = await pointList.EndOfStreamAsync();
foreach (PIPoint point in pointList)
{
AFValue value = values.Where(v => v.PIPoint == point).Single();
pointSnapshotValueList.Add(new PIPointSnapshotModelView(point.Name.ToString(), value));
}
return pointSnapshotValueList;

}
}
}

 

The HomeController injects the IPISystemWrapper interface which has the unique method needed for the Result action to display the results to the View.

 

\Controllers\HomeController.cs

using ASPNETCoreWithPIAFSDK.Framework;
using ASPNETCoreWithPIAFSDK.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OSIsoft.AF;
using OSIsoft.AF.Asset;
using OSIsoft.AF.PI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ASPNETCoreWithPIAFSDK.Controllers
{
public class HomeController : Controller
{
private IPISystemWrapper piSystemWrapper;

//Injecting the IPISystemWrapper service at runtime
// This interface could be AfsdkWrapper or PIWebApiWrapper, according to what is defined on the Startup.cs
public HomeController(IPISystemWrapper piSystemWrapper)
{
this.piSystemWrapper = piSystemWrapper;
}
public IActionResult Index()
{
return this.RedirectToAction("Query");
}

public IActionResult Query()
{
return View();
}

public async Task<IActionResult> Result(string pointNameFilter)
{
IEnumerable<PIPointSnapshotModelView> values = await piSystemWrapper.GetSnapshotValuesFromFoundPoints(pointNameFilter);
return View(values);
}
}
}

 

Finally, the Startup.cs is the file that adds a service of the type IPISystemWrapper with an implementation of this interface which in our case is AfSdkWrapper. In other words, every time the IPISystemWrapper needs to be injected, the DI will inject an instance of the AfSdkWrapper.

 

\Startup.cs

using ASPNETCoreWithPIAFSDK.Framework;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace ASPNETCoreWithPIAFSDK
{
public class Startup
{
public IConfiguration Configuration { get; set; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IPISystemWrapper, AfSdkWrapper>();
services.AddMvc();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseStatusCodePages();
}

app.UseStaticFiles();
app.UseMvcWithDefaultRoute();


}
}
}

 

Running this application, we can see the snapshot values after making a search:

 

 

Replacing PI AF SDK with PI Web API

Finally, I want to demonstrate how easy it is to switch from PI AF SDK to PI Web API using the DI infrastructure. Let's create the PIWebApiWrapper that implements IPISystemWrapper interface. I won't comment about the PI Web API code because it is out of the scope of this blog post.

 

Disclaimer:

Any of the code in this blog 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.

 

\Framework\PIWebApiWrapper.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using ASPNETCoreWithPIAFSDK.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace ASPNETCoreWithPIAFSDK.Framework
{
public class PIWebApiWrapper : IPISystemWrapper
{
private HttpClient client;
private HttpClientHandler handler;
private string dataServerWebId;

public PIWebApiWrapper()
{
//On production, please store the PI Web API base URL on appsettings.json
this.BaseUrl = "https://marc-web-sql.marc.net/piwebapi";
this.UseKerberos = true;
var handler = new HttpClientHandler();
handler.UseDefaultCredentials = true;
client = new HttpClient(handler);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Add("X-Requested-With", "PIWebApiWrapper");


}

public string BaseUrl { get; private set; }
public string Username { get; private set; }
public string Password { get; private set; }
public bool UseKerberos { get; private set; }

public async Task<IEnumerable<PIPointSnapshotModelView>> GetSnapshotValuesFromFoundPoints(string pointNameFilter)
{
List<PIPointSnapshotModelView> pointSnapshotList = new List<PIPointSnapshotModelView>();

if (string.IsNullOrEmpty(this.dataServerWebId) == true)
{
//On production, please store the PI Data Archive name on appsettings.json
string url = this.BaseUrl + @"/dataservers?path=\\MARC-PI2016";
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, url);
HttpResponseMessage httpResponse = await client.SendAsync(httpRequest);
string response = await httpResponse.Content.ReadAsStringAsync();
JObject jObject = JsonConvert.DeserializeObject<JObject>(response);
this.dataServerWebId = jObject["WebId"].ToString();
}

string url2 = this.BaseUrl + "/dataservers/" + this.dataServerWebId + "/points?nameFilter=" + pointNameFilter;
HttpRequestMessage httpRequest2 = new HttpRequestMessage(HttpMethod.Get, url2);
HttpResponseMessage httpResponse2 = await client.SendAsync(httpRequest2);
string response2 = await httpResponse2.Content.ReadAsStringAsync();


JObject jObject2 = JsonConvert.DeserializeObject<JObject>(response2);

string url3 = this.BaseUrl + "/streamsets/value?";
List<Tuple<string, string>> list = new List<Tuple<string, string>>();
if (jObject2["Items"] != null)
{
foreach (var item in jObject2["Items"])
{
string webId = item["WebId"].ToString();
string name = item["Name"].ToString();
Tuple<string, string> tuple = new Tuple<string, string>(name, webId);
list.Add(tuple);
url3 = url3 + $"webId={webId}&";
}
url3 = url3.Substring(0, url3.Length - 1);
}
else
{
return pointSnapshotList;
}


HttpRequestMessage httpRequest3 = new HttpRequestMessage(HttpMethod.Get, url3);
HttpResponseMessage httpResponse3 = await client.SendAsync(httpRequest3);
string response3 = await httpResponse3.Content.ReadAsStringAsync();

JObject jObject3 = JsonConvert.DeserializeObject<JObject>(response3);
if (jObject3["Items"] != null)
{
foreach (var item in jObject3["Items"])
{
string webId = item["WebId"].ToString();
Tuple<string, string> tuple = list.Where(p => p.Item2 == webId).Single();
PIPointSnapshotModelView piPointSnapshotModelView = new PIPointSnapshotModelView(tuple.Item1);
if (tuple != null)
{
if ((item["Value"]["Value"].Type != JTokenType.Object))
{
piPointSnapshotModelView.Value = item["Value"]["Value"].ToString();
}
else
{
piPointSnapshotModelView.Value = item["Value"]["Value"]["Name"].ToString();
}
piPointSnapshotModelView.TimeStamp = Convert.ToDateTime(item["Value"]["Timestamp"].ToString());
}
pointSnapshotList.Add(piPointSnapshotModelView);
}
}
return pointSnapshotList;
}
}
}

 

If we update the ConfigureServices method, the application will use PI Web API instead of PI AF SDK and present the same results to the final user.

 

\Startup.cs

public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IPISystemWrapper, PIWebApiWrapper>();
services.AddMvc();
}

 

Conclusions

Using Dependency Injenction on your PI applications on top of ASP.NET Core is very interesting approach to help maintain, organize and test your your app for many years. It is not the easiest concept to understand but you will quickly see its value once you start implement it.

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

 

If you are getting started with PI Notifications, setting up your AF tree, creating new notifications rules linked to event frames, you probably want to test them before running on production. The traditional way to test is to set up the SMTP server from your enterprise and send all the e-mails from PI Notifications to your e-mail account. If your notifications are sent to a group of people, you need to check with them if they are receiving the e-mails properly.

 

If you are a developer, you might be interested in this new approach for testing by using a custom SMTP Server. This will help you be more efficient testing your notifications rules before running on production. The idea is that the custom SMTP server will listen to port 25 and it will display on the console information about the e-mails sent by PI Notifications.

 

Getting started

 

You can find the source code of the program here.

 

Open Visual Studio and create a .NET Core Console Application. Then add 4 libraries with the following commands:

 

Install-Package MailKit -Version="2.0.6"
Install-Package MimeKit -Version="2.0.6"
Install-Package SmtpServer" -Version="5.3.0"
Install-Package HtmlAgilityPack" -Version="1.8.9"

 

 

This will install our custom SMTP Server core library, some additional libraries to handle e-mails programmatically and the Html Agility Pack, which it will be described later. Please refer to their GitHub repository for more information.

 

Writing the program

 

Based on the README.MD of their GitHub repository, we have created the application below:

 

using CustomSmtpServerForPINotifications.Models;
using HtmlAgilityPack;
using SmtpServer;
using SmtpServer.Authentication;
using SmtpServer.Mail;
using SmtpServer.Protocol;
using SmtpServer.Storage;
using System;
using System.Threading;
using System.Threading.Tasks;


namespace CustomSmtpServerForPINotifications
{
    class Program
    {
        static void Main(string[] args)
        {
            var options = new SmtpServerOptionsBuilder()
             .ServerName("localhost")
             .Port(25, 587)
             .MessageStore(new SampleMessageStore())
             .MailboxFilter(new SampleMailboxFilter())
             .UserAuthenticator(new SampleUserAuthenticator())
             .Build();


            var smtpServer = new SmtpServer.SmtpServer(options);
            smtpServer.StartAsync(CancellationToken.None).Wait();
        }
    }


    public class SampleMessageStore : MessageStore
    {
        public override Task<SmtpResponse> SaveAsync(ISessionContext context, IMessageTransaction transaction, CancellationToken cancellationToken)
        {
            var textMessage = (ITextMessage)transaction.Message;


            var message = MimeKit.MimeMessage.Load(textMessage.Content);
            Console.WriteLine("\n\nNew e-mail received from {0} to {1}.", message.From.ToString(), message.To.ToString());
            Console.WriteLine("HTML: " + message.HtmlBody);
            return Task.FromResult(SmtpResponse.Ok);
        }
    }




    public class SampleMailboxFilter : IMailboxFilter, IMailboxFilterFactory
    {
        public Task<MailboxFilterResult> CanAcceptFromAsync(ISessionContext context, IMailbox @from, int size = 0, CancellationToken token = default(CancellationToken))
        {
            return Task.FromResult(MailboxFilterResult.Yes);
        }


        public Task<MailboxFilterResult> CanDeliverToAsync(ISessionContext context, IMailbox to, IMailbox @from, CancellationToken token)
        {
            return Task.FromResult(MailboxFilterResult.Yes);
        }


        public IMailboxFilter CreateInstance(ISessionContext context)
        {
            return new SampleMailboxFilter();
        }
    }




    public class SampleUserAuthenticator : IUserAuthenticator, IUserAuthenticatorFactory
    {
        public Task<bool> AuthenticateAsync(ISessionContext context, string user, string password, CancellationToken token)
        {
            return Task.FromResult(true);
        }


        public IUserAuthenticator CreateInstance(ISessionContext context)
        {
            return new SampleUserAuthenticator();
        }
    }
}



 

Running the console application will make the custom SMTP server starts and it will keep monitoring ports 25 and 587. When it receives an e-mail, it will display information with the HTML body of the e-mail message.

 

We can test it by opening the Email Delivery Channel Configuration through PI System Explorer, typing the IP address of the machine running the custom SMTP server and clicking on the "Test..." button.

 

 

The e-mail is shown on the console application as expected. Note that the e-mail information is sent to the custom SMTP server but the SMTP Server never tries to send it to the SMTP server of the test.com domain. This is why it is a very good tool for testing purposes.

 

 

 

Let's validate if the e-mail has all the information that we need when it has the notification content. We are going to use the Html Agility Pack library to extract information from the HTML.

 

 

        public override Task<SmtpResponse> SaveAsync(ISessionContext context, IMessageTransaction transaction, CancellationToken cancellationToken)
        {
            var textMessage = (ITextMessage)transaction.Message;


            var message = MimeKit.MimeMessage.Load(textMessage.Content);
            Console.WriteLine("\n\nNew e-mail received from {0} to {1}.", message.From.ToString(), message.To.ToString());
            Console.WriteLine("HTML: " + message.HtmlBody);
            NotificationRequest request = GenerateRequestFromEmail(message.HtmlBody);
            if (request != null)
            {
                Console.WriteLine("AttributeFullPath: " + request.AttributeFullPath);
                Console.WriteLine("NotificationRuleName: " + request.NotificationRuleName);
                Console.WriteLine("StartTime: " + request.StartTime);
            }
            return Task.FromResult(SmtpResponse.Ok);
        }




        private NotificationRequest GenerateRequestFromEmail(string htmlBody)
        {
            try
            {
                NotificationRequest request = new NotificationRequest();
                var doc = new HtmlDocument();
                doc.LoadHtml(htmlBody);
                var nodes = doc.DocumentNode.SelectNodes("/html[1]/body[1]/div[1]/p");
                foreach (var node in nodes)
                {
                    string[] texts = node.InnerText.Split(':');
                    if (texts[0].ToLower().Trim() == "name")
                    {
                        request.NotificationRuleName = texts[1].Trim();
                    }
                    if (texts[0].ToLower().Trim() == "start time")
                    {
                        request.StartTime = node.InnerText.Replace(texts[0] + ":", string.Empty);
                    }
                    if (texts[0].ToLower().Trim() == "attribute path")
                    {
                        request.AttributeFullPath = texts[1].Trim();
                    }
                }
                return request;
            }
            catch (Exception)
            {
                return null;
            }
        }

 

Recompiling the project on Visual Studio and running the custom SMTP server again, it is possible to see the details of the notification received:

 

 

Conclusion

Although this custom SMTP server shouldn't be used for production, I am sure that it can be a good friend for developers and PI admins when they need to test their notifications.

 

Please share your thoughts with me and the community!!

Introduction

 

When I started as a vCampus Support Engineer back in 2012, one of my favorites materials to learn about our PI Developer Technologies was the material written for our vCampus Live! events. Recently, I've taken a look at the vCampus Live! 2012 workbooks and I found the "Develop Custom Delivery Channels for Use with PI Notifications" hands-on lab. In this lab, it was shown how to develop a custom delivery channel that would write notification to the Windows Event Logs using PI Notification 2012 and Visual Studio 2012 and .NET Framework 3.5.

 

The new generation of PI Notification (which starts in 2016) uses Event Frames instead of PI Points to store their historical data and does not support custom delivery channels. Nevertheless, it has integration with custom web services.

 

On this blog post, I will show you how to develop a custom web service that not only integrates with the newer releases of PI Notification and but also writes notifications events to the Windows Event Log. This time we are going to use Visual Studio 2017 and ASP.NET Core 2.1.

 

Creating a delivery endpoint

 

Please make sure that you have PI AF 2018 and PI Notifications 2018 installed on your system. If you are interested in setting up new Notification Rules, please refer to this video.

 

Before starting to code, we need to create a new delivery endpoint as shown on the screenshot below. Note that you should select WebService as the delivery channel. REST is the standard nowadays and this is what we are going to use.  We are going to create an action that accepts HTTP POST requests. For this demo, we won't be using any type of authentication although PI Notifications does support Basic and Windows.

 

 

 

 

 

 

 

Creating the ASP.NET Core project with .NET Framework

 

You can download the source code package of this blog post by accessing this GitHub repository.

 

As explained in this blog post, ASP.NET Core could be created with .NET Framework and .NET Core. We are going to create an ASP.NET Core application using .NET Framework because it is easier to integrate it with Windows Event Log. Let's create the project by referring the screenshot below:

 

 

On the second screen, make sure to select ".NET Framework" and "ASP.NET Core 2.1". This code probably does not work with ASP.NET Core 2.0. The solution would be to update Visual Studio as it will show newer versions of that platform.

 

 

 

 

 

Adding the ASP.NET Core MVC library

 

Open Package Manager Console and type:

 

Install-Package Microsoft.AspNetCore.Mvc

 

This will install the MVC component of the ASP.NET Core.

 

 

Editing the Startup.cs

 

Now that we have all libraries added, we need to enable it by editing the Startup.cs.

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;


namespace CustomWebServiceForPINotifications
{
    public class Startup
    {
        public IConfiguration IConfiguration { get; }


        public Startup(IConfiguration configuration)
        {
            IConfiguration = configuration;
        }


        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }


        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }


            app.UseMvc();
        }
    }
}

 

 

I have published some blog post and videos about using ASP.NET Core with PI AF SDK and Angular. Please refer to the Developing Web Apps on top of the PI System Hub.

 

 

Creating the model

 

Create a new folder on the project root named Models and create a new class called NotificationRequest.cs. This .NET class will be used to process the JSON created by PI Notification according to the WebService configured. In this case, the JSON will have 3 properties: NotificationRuleName, AttributeFullPath and StartTime. Please refer to this example in order to learn how to create the Notification Rule programmatically.

 

 

Now that the properties were defined, we can write our NotificationRequest class as:

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;


namespace CustomWebServiceForPINotifications.Models
{
    public class NotificationRequest
    {
        public string NotificationRuleName { get; set; }
        public string AttributeFullPath { get; set; }
        public DateTime StartTime { get; set; }
    }
}

 

 

Creating the controller

 

Create a new folder on the project root named Controllers and create a new class called NotificationReceiverController.cs

 

This controller has two actions: Get and Post. The first action is used to make sure that the web site is up and running properly. The second action has the same code of the hands-on lab used to write a text to the Windows Event Log. For the second action, if the operation is successful, it will return a 201 status code. If not, it will return a bad request error.

 

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using CustomWebServiceForPINotifications.Models;


namespace CustomWebServiceForPINotifications.Controllers
{
    [Route("api/notifications-receiver")]
    [ApiController]
    public class NotificationReceiverController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get()
        {
            return Ok(new List<int> { 1, 2, 3, 4 });
        }


        [HttpPost]
        public IActionResult Post([FromBody]NotificationRequest request)
        {


            try
            {
                string sSource = "PI Notifications Delivery Channel Log";
                string sLog = "Application";
                if (!System.Diagnostics.EventLog.SourceExists(sSource))
                    System.Diagnostics.EventLog.CreateEventSource(sSource, sLog);


                string message = String.Format("Notification: {0} triggered for asset {1} at {2}", request.NotificationRuleName,
                    request.AttributeFullPath.ToString(), request.StartTime.ToString());
                System.Diagnostics.EventLog.WriteEntry(sSource, message, System.Diagnostics.EventLogEntryType.Information);
                return StatusCode(201);


            }
            catch (Exception ex)
            {
                return BadRequest(ex);
            }
        }


    }
}



 

Publishing to IIS

 

Now that the code is ready, we should publish the application to IIS. Please refer to the video ASP.NET Core 2 (Web API) and Angular with PI AF SDK: Part 5 - Publishing the application to IIS  for more information. You will have to download the run time version of ASP.NET Core and make sure the service running the web site has enough privileges in the folder with the web site files.

 

We can easily see if our web service is running as expected by testing it with our browser:

 

 

If there is something wrong, you will receive an error message.

 

 

Testing the integration between PI Notifications and the Custom Web Service

 

Ok, it is time to test our Notification. Let's go back to the Web Service Configuration window and click on the "Test Send" button. The result of the operation will be shown on the bottom of the Window.

 

 

We can confirm that the operation was successful by opening the Windows Event Viewer on the machine hosting our web service.

 

 

 

 

Conclusion

 

PI Notification 2016 does not allow developing custom delivery channels but it does allow you to integrate it with custom web services. I hope that this blog post will help you develop great integrations with the new generation of PI Notifications!!

We are excited to present the PI World Innovation Hackathon EMEA 2018 winners!

 

DEME kindly provided a sample of their data with sensors, jack-up vessels and soil models information. Participants were encouraged to create killer applications for DEME by leveraging the PI System infrastructure.

 

OSI_Barcelona18_Day1_120.jpg

 

The participants had 23 hours to create an app using any of the following technologies:

  • PI Server 2018
  • PI Web API 2018
  • PI Vision 2017 R2

 

Our judges evaluated each app based on their creativity, technical content, potential business impact, data analysis and insight and UI/UX. Although it is a tough challenge to create an app in 23 hours, 4 groups were able to finish their app and present to the judges!

 

Prizes:

1st place: Intel NUC Barebone (Core 3-7100U, 120 GB SSD, 8 GB RAM), one year free subscription to PI Developers Club, one time free registration at OSIsoft PI World over the next 1 year

2nd place: Bose SoundLink Around-Ear Wireless Headphones II (black), one year free subscription to PI Developers Club, 50% discount for registration at OSIsoft PI World over the next 1 year

3rd place: Raspberry Pi 3 Model B+ Retro Arcade Gaming Kit incl. 2 classic controllers and one year free subscription to PI Developers Club

 

 

Without further do, here are the winners!

 

1st place - AG Solution

 

The team members were: Sergio Hernandez, Juri Krivoruchko and Marc Torralba

 

 

OSI_Barcelona18_Day4_386.jpg

 

Team AG Solution has developed an application on top of PI AF SDK and PI Vision. They've developed a custom data reference that uses Stochastic Dual Coordinate classifier from ML.NET to detect the current state of vessel by the direction of changing the state.

 

The team used the following technologies:

  • PI AF SDK
  • ML.NET
  • PI Vision

 

Here are some screenshots presented by the AG Solution team!

 

 

 

 

 

 

 

 

2nd place - M.E.S.S

 

The team members were:  David Rodriguez, Leandro Hideo, Alexander Hosefelder and Alexander Dixon

 

OSI_Barcelona18_Day4_384.jpg

 

Team M.E.S.S developed an algorithm in R in order to detect sensors anomalies using Machine Learning.

 

The team used the following technologies:

  • R
  • PI Web API
  • PI Web API package for R

 

Here are some screenshots presented by M.E.S.S!

 

 

 

 

 

 

3rd place - Werusys Cologne

 

The team members were: Kai Weber, Ansgar Backhaus and Julian Weber

 

 

OSI_Barcelona18_Day4_383.jpg

 

Team Werusys Cologne developed an application to analyze wind mill installation case data based on hidden markov model.

 

The team used the following technologies:

  • PI Web API
  • Seeq

 

Here are some screenshots presented by the Werusys Cologne!

 

Introduction

 

After almost 4 years, it is time to rewrite/update my old ASP.NET MVC 5 blog post since the recommended Microsoft technology nowadays for web development is ASP.NET Core MVC 2.

 

But what is ASP.NET Core MVC?

 

According to this web page, the ASP.NET Core MVC framework is a lightweight, open source, highly testable presentation framework optimized for use with ASP.NET Core.

ASP.NET Core MVC provides a patterns-based way to build dynamic websites that enables a clean separation of concerns. It gives you full control over markup, supports TDD-friendly development and uses the latest web standards.

 

Can ASP.NET Core MVC be used together with PI AF SDK?

 

Since PI AF SDK is compiled against .NET Framework, one could argue that PI AF SDK is not compatible with ASP.NET Core since it was not compiled against .NET Core. Nevertheless, you can create an ASP.NET Core MVC project against .NET Core or .NET Framework. If you choose the second option, you can add PI AF SDK as a reference to your project.

 

Requirements

 

If you want to follow this blog post along, please make sure the following software are installed on your computer:

  • Visual Studio 2017 Update 3
  • PI AF SDK 2017 R2+

 

 

Source code package

You can have access to the source code package related to this blog post in this GitHub repository.

 

Creating the project

 

Let's see how this works. Open Visual Studio and create a new project from scratch. Select the Web item on the left pane and then "ASP.NET Core Web Application". Then write "ASPNETCoreWithAFSDKPar1"as the name of the project and click on the "Ok" button.

 

 

Figure 1 – Creating a new  ASP.NET Core MVC project in Visual Studio.

 

 

On the next window, make sure that ".NET Framework" and "ASP.NET Core 2.0" are selected. Next, select the Empty template and then click on “OK”. Figure 2 shows the screenshot for selecting the template. We have chosen the empty template instead of the MVC template because the last option not only comes with the MVC binaries but also with some Controllers, Views, Layouts and the ASP.NET Identity responsible for the authentication and authorization from your web site. As we are not going to use many of those default files, the Empty seemed to be a better option for me.

 

 

 

 

Figure 2 – Selecting the ASP.NET Core template.

 

If you don't see those options on this window, make sure you have an updated version of Visual Studio 2017.

 

Right click on Dependencies and then "Add Reference..". Browse to the PI AF SDK binary and add it to your project.

 

Figure 3 – Adding PI AF SDK to your project

 

Before we start coding, make sure the target framework is .NET Framework and not .NET Core. Right click on the project and select properties to verify this information.

 

Figure 4 – VS project properties

 

Add NuGet libraries

 

Open the ASPNETCoreWithAFSDKPart1.csproj file and update it with the following content.

 

<Project Sdk="Microsoft.NET.Sdk.Web">


  <PropertyGroup>
    <TargetFramework>net472</TargetFramework>
  </PropertyGroup>


  <ItemGroup>
    <Content Remove="bower.json" />
  </ItemGroup>


  <ItemGroup>
    <Folder Include="wwwroot\" />
  </ItemGroup>


  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore" Version="2.0.3" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.3" />
    <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.2" />
    <PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="2.0.2" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.3" />
  </ItemGroup>


  <ItemGroup>
    <Reference Include="OSIsoft.AFSDK">
      <HintPath>..\..\..\..\..\Program Files (x86)\PIPC\AF\PublicAssemblies\4.0\OSIsoft.AFSDK.dll</HintPath>
    </Reference>
  </ItemGroup>


  <ItemGroup>
    <Content Update="Views\Shared\_Layout.cshtml">
      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
    </Content>
  </ItemGroup>


</Project>

 

Save and recompile this project. The missing libraries will be retrieved from the internet and will be added to your project automatically.

 

Configure the HTTP requests pipeline

 

Open the Startup.cs file and edit it according to the content below:

 

 public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }


        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }

 

This will make sure that:

  • MVC is enabled.
  • Static files are enabled.
  • Developer exception pages are shown if an exception is thrown (Don't use it on production).

 

Setting up Bower and adding Bootstrap

 

We are going to use bootstrap 3 to style our web site. Although there are better options nowaways, we are going to use bower to download bootstrap and jQuery. On the VS project root create two file: .bowerrc and bower.json.

 

.bowerrc

{
  "directory": "wwwroot/lib"
}

 

bower.json

 

{
  "name": "ASPNETCOreWithAFSDKPart1",
  "private": true,
  "dependencies": {
    "bootstrap": "3.3.7"
  }
}

 

The first file tells the system to paste the retrieved libraries on the wwwroot//lib folder. The second file has all the dependencies.

 

Open the command prompt and change directory to the VS project folder. Then run"bower install" to install Bootstrap 3.3.7 and jQuery (both are JavaScript libraries), according to the dependencies listed on the bower.json file.

 

Creating the Home Controller

 

Let's create a new folder called Controllers and our home controller (new file homeController.cs) with folowing code:

 

using System.Collections.Generic;
using System.Linq;
using ASPNETCoreWithAFSDKPart1.Models;
using Microsoft.AspNetCore.Mvc;
using OSIsoft.AF.PI;


namespace ASPNETCoreWithAFSDKPart1.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return this.RedirectToAction("Query");
        }


        public IActionResult Query()
        {
            return View();
        }


        public IActionResult Result(string pointNameQuery, string pointSourceQuery)
        {
            PIServer piServer = new PIServers()["MARC-PI2016"];
            piServer.Connect();


            PIPointQuery query1 = new PIPointQuery(PICommonPointAttributes.Tag, OSIsoft.AF.Search.AFSearchOperator.Equal, pointNameQuery);
            PIPointQuery query2 = new PIPointQuery(PICommonPointAttributes.PointSource, OSIsoft.AF.Search.AFSearchOperator.Equal, pointSourceQuery);
            IEnumerable<PIPoint> foundPoints = PIPoint.FindPIPoints(piServer, new PIPointQuery[] { query1, query2 });
            PIPointList pointList = new PIPointList(foundPoints.Take(1000));


            List<PIPointSnapshotModelView> pointSnapshotValueList = new List<PIPointSnapshotModelView>();
            foreach (PIPoint point in pointList)
            {
                pointSnapshotValueList.Add(new PIPointSnapshotModelView(point.Name.ToString(), point.CurrentValue()));
            }


            return View(pointSnapshotValueList.AsEnumerable());
        }
    }
}

 

When the web site starts, it will load the Index() action, which will be redirected to the Query() action. This action will display a page with a form. When the user submits the form, the Result action will be called. PI AF SDK will search for PI Points matching certain conditions with the help of PIPointQuery class. The results are converted to an IEnumerable<PIPointSnapshotModelView>.

 

 

Creating the Model

 

The model all has the information needed to display to the use (PI Point name, value and attribute). Please create a new file PIPointSnapshotModelView.cs in the Models folder. Create the Models folder if it doesn't exist.

 

using OSIsoft.AF.Asset;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;


namespace ASPNETCoreWithAFSDKPart1.Models
{
    public class PIPointSnapshotModelView
    {
        public string PointName { get; set; }
        public object Value { get; set; }
        public DateTime TimeStamp { get; set; }


        public PIPointSnapshotModelView(string pointName, AFValue afValue)
        {
            PointName = pointName;
            Value = afValue.Value;
            TimeStamp = afValue.Timestamp;
        }
    }
}

 

 

Creating the Shared View

 

Please create a new folder on the root called Views. Then create 3 files with the content below:

 

_ViewStart.cshtml

@{
    Layout = "_Layout";
}

 

 

_ViewImports.cshtml

@using  ASPNETCoreWithAFSDKPart1.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

 

Shared\_Layout.cshtml (don't forget to create the Views\Shared folder first!)

@model IEnumerable<ASPNETCoreWithAFSDKPart1.Models.PIPointSnapshotModelView>
@{
    Layout = null;
}


<!DOCTYPE html>


<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Results</title>
    <link href="~/lib/tether/dist/css/tether.css" rel="stylesheet" />
    <link href="~/lib/bootstrap/dist/css/bootstrap.css" rel="stylesheet" />
    <script src="~/lib/tether/dist/js/tether.js"></script>
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
</head>
<body>
    @RenderBody()
</body>
</html>

 

Notes:

  • _ViewStart file can be used to define common view code that you want to execute at the start of each View's rendering.
  • _ViewImports.cshtml serves one major purpose: to provide namespaces which can be used by all other views.
  • _Layout: is the master layout that will be used by Query.cshtml and Result.html.

 

Creating the Views

 

Now, we just need to create Query.cshtml and Result.cshtml to finish our application!

 

Query.cshtml


<div class="col-sm-8 col-sm-offset-2">
    <h1>My first ASP.NET Core MVC with PI AF SDK</h1>
    <br /><br />
    <h4>Search for PI Points and check its snapshots.</h4>
    <br /><br />
    <div class="container">
        <div class="row">
            <form asp-controller="Home" asp-action="Result" method="get" class="form-inline">
                <div class="form-group">
                    <label class="sr-only" for="name">Search PI Point: </label>
                    <input id="pointNameQuery" name="pointNameQuery"  placeholder="Point Name" class="form-control" />
                </div>
                <div class="form-group">
                    <label class="sr-only" for="inputfile">With this Point Source: </label>
                    <input id="pointSourceQuery" name="pointSourceQuery" placeholder="Point Source" class="form-control" />
                </div>
                <button type="submit" class="btn btn-default">Submit</button>
            </form>
        </div>
    </div>
</div>

 

 

Result.cshtml

 

@model IEnumerable<PIPointSnapshotModelView>






<div class="col-sm-8 col-sm-offset-2">
    <h1>My first ASP.NET Core MVC with PI AF SDK</h1>
    <br /><br />
    <table class="table">
        <tr>
            <th>PI Point</th>
            <th>Value</th>
            <th>Timestamp</th>
        </tr>
        @foreach (var pointSnapshotValue in Model)
            {
        <tr>
            <td>@pointSnapshotValue.PointName</td>
            <td>@pointSnapshotValue.Value.ToString()</td>
            <td>@pointSnapshotValue.TimeStamp.ToString()</td>
        </tr>
            }
    </table>
    <br />
    <center>
        <a asp-controller="Home" asp-action="Index" class="btn btn-primary">Make another search</a>
    </center>
</div>



 

 

Testing the web app

 

We have finished writing our ASP.NET Core MVC web application. It is time to test. When you run this web application on Visual Studio, you should see the Query page below.

 

 

Figure 5 – Screenshot of the Query page from our web application

 

Figure 6 shows a screenshot in case we want to find all PI Points which match the “sin*d” from any PI Point Source. After clicking on submit, it will show the table with the PI Points found on the Result page.

 

 

Figure 6 – Screenshot of the Result page from our web application

 

Note that the design of the page has improved using bootstrap adding only a few words to the View. The query string defines the input parameters so you can edit them directly on the uri. If you click on “Make another search” it should go back to the previous page.

 

Conclusions

If you are a PI Developer that needs to develop a web application on top of the PI System., ASP.NET Core MVC with PI AF SDK is a great option. Nevertheless, there are other options as well, which we are going to see on my upcoming blog posts. Stay tuned!

I am happy to announce that the client libraries below were updated using the new PI Web API 2018 Swagger specification. All the new methods from PI Web API 2018 are available for you to use on the client side! The StreamUpdate related methods (still in CTP) were also added to those projects.

 

 

If you have questions, please don't hesitate to ask!

Introduction

 

PI Web API 2018 was released in late June with some interesting features. In this blog post, I will show you some of those features using the PI Web API client library for .NET Standard which was already upgraded to 2018.

 

According to the Changelog of the PI Web API 2018 help, the new features of PI Web API 2018 are listed below:

 

  • Read for notification contact templates, notification rules, notification rule subscribers and notification rule templates
  • Read, create, update and delete for annotation attachments on event frames
  • Retrieve relative paths of an element
  • Retrieve full inheritance branch of an element template
  • Allow reading annotations of a stream using the "associations" parameter
  • Allow showing all child attribute templates of an element template
  • Add parameter support for tables
  • Filter attributes by trait and trait category
  • Support health traits
  • Incrementally receive updates from streams with Stream Updates (CTP)

Data model changes:

  • Expose 'ServerTime' property on objects of asset server, data server and system
  • Expose 'DisplayDigits', 'Span' and 'Zero' properties on objects of attribute and point
  • Expose 'DefaultUnitsNameAbbreviation' property on objects of attribute and attribute template

 

You can have an idea of the new features by looking at the list above but showing some code snippets will help you understand better.

 

Please visit the GitHub repository to download the source code package used in this blog post.

 

Demo

 

Before we start, please create a .NET Framework or .NET Core console application and add the OSIsoft.PIDevClub.PIWebApiClient according to the README.MD file of the client library repository if you want to try yourself.

 

 

Read for notification contact templates, notification rules, notification rule subscribers and notification rule templates

 

PI Web API 2018 comes with 4 new controllers (NotificationContactTemplate, NotificationRule, NotificationRuleSubscriber and NotificationRuleTemplate) with read access to the notification objects.

 

For this demonstration, I have created a Notification Rule to make sure that it is accessible through client.NotificationRule.GetNotificationRuleQuery() action.

 

 

            PIAssetDatabase db = client.AssetDatabase.GetByPath(@"\\MARC-PI2016\WhatsNewInPIWebAPI2018");
            PIItemsNotificationRule notificationRules = client.NotificationRule.GetNotificationRulesQuery(db.WebId, query: "Name:=Not*");
            Console.WriteLine($"Found {notificationRules.Items.Count}");

 

HTTP Request: GET - /piwebapi/notificationrules/search?databaseWebId=F1RDbvBs-758HkKnOzuxolJRlgBW-drMj-Q0eZlG9e0V3JigTUFSQy1QSTIwMTZcV0hBVFNORVdJTlBJV0VCQVBJMjAxOA&query=Name%3a%3dNot*

 

Running this application, the result is:

 

 

Read, create, update and delete for annotation attachments on event frames

 

I have manually created a new Event Frame (EF) using PI System Explorer. I want to retrieve the same EF programmatically and add an annotation with a value of "EF Test" through the CreateAnnotiationWithHttpInfo() method available of the client library. I've reviewed the status code to make sure that the request was made successfully.

 

 

            PIAssetDatabase db = client.AssetDatabase.GetByPath(@"\\MARC-PI2016\WhatsNewInPIWebAPI2018");
            PIItemsEventFrame efs = client.AssetDatabase.GetEventFrames(db.WebId);
            PIEventFrame ef = efs.Items.First();
            PIAnnotation piAnnotation = new PIAnnotation();
            piAnnotation.Value = "EF Test";
            ApiResponse<object> result = client.EventFrame.CreateAnnotationWithHttpInfo(ef.WebId, piAnnotation);
            if (result.StatusCode < 300)
            {
                Console.WriteLine("Annotation in EF was created successfully!");
            }


            PIItemsAnnotation piAnnotations = client.EventFrame.GetAnnotations(ef.WebId);
            foreach (PIAnnotation annotation in piAnnotations.Items)
            {
                Console.WriteLine($"Annotation from Event Frame: {annotation.Value}");
            }

 

HTTP Request: POST - /piwebapi/eventframes/F1FmbvBs-758HkKnOzuxolJRlgbFJEBGSE6BGbywAVXQAeEATUFSQy1QSTIwMTZcV0hBVFNORVdJTlBJV0VCQVBJMjAxOFxFVkVOVEZSQU1FU1tFRjIwMTgwNzEwLTAwMV0/annotations

 

I can see my created annotations in PI System Explorer. Just right click on the EF and then click on "Annotate..." You will be able to see all EFs generated programmatically.

 

 

Retrieve relative paths of an element

 

PI Web API 2018 allows you to get a list of the full or relative paths to this display. Let's see how this works. Please refer to the AF Tree below.

 

 

 

Using the code snippet below.

 

            PIElement element = client.Element.GetByPath(@"\\MARC-PI2016\AFPartnerCourseWeather\Cities\Chicago");
            PIItemsstring relativePath = client.Element.GetPaths(element.WebId, @"\\MARC-PI2016\AFPartnerCourseWeather");
            Console.WriteLine($"The Relative Path is {relativePath.Items.First()}.");

 

The second line calls GetPaths with the second input being "\\MARC-PI2016\AFPartnerCourseWeather" as the full path of one of their parents' AF object, which in this case is the AF database.

 

HTTP Request: GET -  /piwebapi/elements/F1EmbvBs-758HkKnOzuxolJRlg-Jnwgsge5hGAwwAVXX0eAQTUFSQy1QSTIwMTZcQUZQQVJUTkVSQ09VUlNFV0VBVEhFUlxDSVRJRVNcQ0hJQ0FHTw/paths?relativePath=%5c%5cMARC-PI2016%5cAFPartnerCourseWeather

 

The result will be the element relative path which is the path relative to the element path of one of its parents.

 

 

Retrieve full inheritance branch of an element template

 

In this example I have create 3 element templates and 1 element:

  • TemplateA
  • TemplateAA derived from TemplateA
  • TemplateAAA derived from TemplateAA.

 

 

PI Web API provides two interesting methods: GetBaseElementTemplates() and GetDerivedElementTemplates() to get the base and derived element templates from element templates. Let's see how this work:

 

            PIElementTemplate templateA = client.ElementTemplate.GetByPath("\\\\MARC-PI2016\\WhatsNewInPIWebAPI2018\\ElementTemplates[TemplateA]");
            PIElementTemplate templateAA = client.ElementTemplate.GetByPath("\\\\MARC-PI2016\\WhatsNewInPIWebAPI2018\\ElementTemplates[TemplateAA]");
            PIElementTemplate templateAAA = client.ElementTemplate.GetByPath("\\\\MARC-PI2016\\WhatsNewInPIWebAPI2018\\ElementTemplates[TemplateAAA]");


            PIItemsElementTemplate baseTemplatesFromTemplateA = client.ElementTemplate.GetBaseElementTemplates(templateA.WebId);           
            PIItemsElementTemplate baseTemplatesFromTemplateAA = client.ElementTemplate.GetBaseElementTemplates(templateAA.WebId);
            PIItemsElementTemplate baseTemplatesFromTemplateAAA = client.ElementTemplate.GetBaseElementTemplates(templateAAA.WebId);


            Console.WriteLine($"There are {baseTemplatesFromTemplateA.Items.Count} base templates in Element Template A");
            Console.WriteLine($"There are {baseTemplatesFromTemplateAA.Items.Count} base templates in Element Template AA");
            Console.WriteLine($"There are {baseTemplatesFromTemplateAAA.Items.Count} base templates in Element Template AAA");




            PIItemsElementTemplate derivedTemplatesFromTemplateA = client.ElementTemplate.GetDerivedElementTemplates(templateA.WebId);
            PIItemsElementTemplate derivedTemplatesFromTemplateAA = client.ElementTemplate.GetDerivedElementTemplates(templateAA.WebId);
            PIItemsElementTemplate derivedTemplatesFromTemplateAAA = client.ElementTemplate.GetDerivedElementTemplates(templateAAA.WebId);


            Console.WriteLine($"There are {derivedTemplatesFromTemplateA.Items.Count} derived templates in Element Template A");
            Console.WriteLine($"There are {derivedTemplatesFromTemplateAA.Items.Count} derived templates in Element Template AA");
            Console.WriteLine($"There are {derivedTemplatesFromTemplateAAA.Items.Count} derived templates in Element Template AAA");

 

HTTP Request: GET - /piwebapi/elementtemplates/F1ETbvBs-758HkKnOzuxolJRlg-av52dhufE6iHqLJJk5faATUFSQy1QSTIwMTZcV0hBVFNORVdJTlBJV0VCQVBJMjAxOFxFTEVNRU5UVEVNUExBVEVTW1RFTVBMQVRFQV0/baseelementtemplates

HTTP Request GET - /piwebapi/elementtemplates/F1ETbvBs-758HkKnOzuxolJRlg-av52dhufE6iHqLJJk5faATUFSQy1QSTIwMTZcV0hBVFNORVdJTlBJV0VCQVBJMjAxOFxFTEVNRU5UVEVNUExBVEVTW1RFTVBMQVRFQV0/derivedelementtemplates

 

Running the application, the results are:

 

 

Allow reading annotations of a stream using the "associations" parameter

 

Some of the actions from the Stream and StreamSets controllers have the associations parameter added as an optional input. If this input is null, the values will be retrieved without their annotations. If the associations input is equal to "Annotations", then the annotations will be retrieved as well. Let's take a look at the example using the Stream.GetRecorded() method.

 

 

            PIPoint point1 = client.Point.GetByPath($"\\\\{piDataArchive.Name}\\SINUSOID");
            PIExtendedTimedValues valuesWithAnnotation = client.Stream.GetRecorded(point1.WebId, "Annotations");
            IEnumerable<List<PIStreamAnnotation>> annotationsList = valuesWithAnnotation.Items.Where(v => v.Annotated == true).Select(v => v.Annotations);
            foreach (List<PIStreamAnnotation> annotations in annotationsList)
            {
                foreach (PIStreamAnnotation annotation in annotations)
                {
                    Console.WriteLine($"Showing annotation: {annotation.Value}, {annotation.ModifyDate}");
                }
            }

 

HTTP Request: GET  /piwebapi/streams/F1DPQuorgJ0MskeiLb6TmEmH5gAQAAAATUFSQy1QSTIwMTZcU0lOVVNPSUQ/recorded?associations=Annotations

 

The result is shown below:

 

 

 

Allow showing all child attribute templates of an element template

 

On this demonstration, I have created a new attribute template called ParentAttribute on the TemplateAAA element template which has 2 child attributes templates named ChildAttribute1 and ChildAttribute2.

 

 

On previous versions of PI Web API it was not possible to access the child attribute templates of an attribute template. But this is possible in 2018 version:

 

            PIElementTemplate templateAAA = client.ElementTemplate.GetByPath("\\\\MARC-PI2016\\WhatsNewInPIWebAPI2018\\ElementTemplates[TemplateAAA]");
            PIItemsAttributeTemplate attributesWithChild = client.ElementTemplate.GetAttributeTemplates(templateAAA.WebId, showDescendants: true);
            foreach (PIAttributeTemplate attributeTemplate in attributesWithChild.Items)
            {
                Console.WriteLine($"Showing attribute template -  Path: {attributeTemplate.Path}, HasChildren:{attributeTemplate.HasChildren}");
            }

 

HTTP Request:  /piwebapi/elementtemplates/F1ETbvBs-758HkKnOzuxolJRlgoPGTVEM9d0S3M9GObE-kagTUFSQy1QSTIwMTZcV0hBVFNORVdJTlBJV0VCQVBJMjAxOFxFTEVNRU5UVEVNUExBVEVTW1RFTVBMQVRFQUFBXQ/attributetemplates?showDescendants=True

 

The results are shown below:

 

 

Filter attributes by trait and trait category

 

PI Web API allows you to get the attribute traits of an attribute by adding two new inputs to the GetAttributes() method: trait and traitCategory. I have created the attribute traits for the MainAttribute.

 

Please refer to the code below.

 

            PIAttribute mainAttribute = client.Attribute.GetByPath(@"\\MARC-PI2016\WhatsNewInPIWebAPI2018\AttributeTraits|MainAttribute");
            PIItemsAttribute attributeTraits = client.Attribute.GetAttributes(mainAttribute.WebId, trait: new List<string> { "Minimum", "Target" });

 

 

HTTP Request: GET -  /piwebapi/attributes/F1AbEbvBs-758HkKnOzuxolJRlgBG1jiF-E6BGbywAVXQAeEAUkg-GMAek0-aGWOEUJKR9QTUFSQy1QSTIwMTZcV0hBVFNORVdJTlBJV0VCQVBJMjAxOFxBVFRSSUJVVEVUUkFJVFN8TUFJTkFUVFJJQlVURQ/attributes?trait=Minimum&trait=Target

 

Please refer to the Attribute Trait page of the PI Web API help for more information.

 

Support health traits

 

PI Web API also allows you to get the health score and health status of an element by adding the same inputs of the previous example.

 

 

            PIElement mainElement = client.Element.GetByPath(@"\\MARC-PI2016\WhatsNewInPIWebAPI2018\AttributeTraits");
            PIItemsAttribute healthAttributeTraits = client.Element.GetAttributes(mainElement.WebId, trait: new List<string> { "HealthScore" });

 

The variable healthAttributeTraits will store the Health Score attribute of the element.

 

Please refer to the Attribute Trait page of the PI Web API help for more information.

 

Incrementally receive updates from streams with Stream Updates (CTP)

 

PI Web API comes with Stream Updates whose behavior is similar to the PI Web API channels but it uses the default HTTP requests instead of WebSockets. Here is the description of this feature on the PI Web API help.

 

"Stream Updates is a way in PI Web API to stream incremental and most recent data updates for PIPoints/Attributes on streams and streamsets without opening a websocket. It uses markers to mark the specific event in a stream where the client got the last updates and uses those to get the updates since that point in the stream."

 

In the example below, we will get the WebIDs of 3 PI Points and retrieve their new real time values every 30 seconds.

 

 

            PIPoint point1 = client.Point.GetByPath("\\\\marc-pi2016\\sinusoid");
            PIPoint point2 = client.Point.GetByPath("\\\\marc-pi2016\\sinusoidu");
            PIPoint point3 = client.Point.GetByPath("\\\\marc-pi2016\\cdt158");
            List<string> webIds = new List<string>() { point1.WebId, point2.WebId, point3.WebId };


            PIItemsStreamUpdatesRegister piItemsStreamUpdatesRegister = client.StreamSet.RegisterStreamSetUpdates(webIds);
            List<string> markers = piItemsStreamUpdatesRegister.Items.Select(i => i.LatestMarker).ToList();
            int k = 3;
            while (k > 0)
            {
                PIItemsStreamUpdatesRetrieve piItemsStreamUpdatesRetrieve = client.StreamSet.RetrieveStreamSetUpdates(markers);
                markers = piItemsStreamUpdatesRetrieve.Items.Select(i => i.LatestMarker).ToList();
                foreach (PIStreamUpdatesRetrieve item in piItemsStreamUpdatesRetrieve.Items)
                {
                    foreach (PIDataPipeEvent piEvent in item.Events)
                    {
                        Console.WriteLine("Action={0}, Value={1}, SourcePath={2}", piEvent.Action, piEvent.Value, item.SourcePath);
                    }
                }
                System.Threading.Thread.Sleep(30000);
                k--;
            }

 

 

HTTP Request: POST - /piwebapi/streamsets/updates?webId=F1DPQuorgJ0MskeiLb6TmEmH5gAQAAAATUFSQy1QSTIwMTZcU0lOVVNPSUQ&webId=F1DPQuorgJ0MskeiLb6TmEmH5gAgAAAATUFSQy1QSTIwMTZcU0lOVVNPSURV&webId=F1DPQuorgJ0MskeiLb6TmEmH5g9AQAAATUFSQy1QSTIwMTZcQ0RUMTU4 H

 

HTTP Request: GET /piwebapi/streamsets/updates?marker=796081654a5648a3bf0d322fb58df518_0&marker=e2604da295584f498ce65976437b0c0f_0&marker=6f402964b31e4635aac2b87a68b79e60_0

 

The results are shown below:

 

 

Exposing properties of objects

 

There was some data model changes:

  • Expose 'ServerTime' property on objects of asset server, data server and system
  • Expose 'DisplayDigits', 'Span' and 'Zero' properties on objects of attribute and point
  • Expose 'DefaultUnitsNameAbbreviation' property on objects of attribute and attribute template

 

We can easily demonstrate that through the following code:

 

            PIPoint point = client.Point.GetByPath($"\\\\{piDataArchive.Name}\\SINUSOID");
            //Expose 'ServerTime' property on objects of asset server, data server and system
            PIItemsAssetDatabase dbs = client.AssetServer.GetDatabases(afServer.WebId);
            afServer = client.AssetServer.Get(afServer.WebId);
            Console.WriteLine($"PI AF Server ServerTime is {afServer.ServerTime}");




            //Expose 'DisplayDigits', 'Span' and 'Zero' properties on objects of attribute and point
            Console.WriteLine($"Sinusoid PIPoint: DisplayDigits={point.DisplayDigits}, Span={point.Span}, Zero={point.Zero}");


            //Expose 'DefaultUnitsNameAbbreviation' property on objects of attribute and attribute template
            PIAttribute attribute = client.Attribute.GetByPath(@"\\MARC-PI2016\AFPartnerCourseWeather\Cities\Chicago|Temperature");
            Console.WriteLine($"DefaultUnitsNameAbbreviation of the attribute is {attribute.DefaultUnitsNameAbbreviation}");

 

The results are shown below:

 

 

Conclusion

 

I hope you've found this blog post useful to learn the new features of PI Web API 2018. Please provide your feedback so we can repeat when future PI Web API releases are published.

Introduction

 

A few years ago, we've announced the public PI Web API endpoint in order to:

 

  • Provide developers access to a PI System who may not be able to access PI otherwise
  • Create code samples against the public endpoint and to host them under OSIsoft organization on GitHub
  • Offer developers a playground to exercise with PI Web API
  • Create a streamlined way to offer datasets in a PI System

 

Although we were able to achieve the goals above, I felt that visualizing the data only through PI Web API is a challenge since common tools like PI System Explorer and PI Vision are not available to work with PI data stored on a remote PI Web API endpoint. Given this scenario, I've developed the PI Web API Data Reference which allows local attributes to access data from remote attributes through PI Web API endpoints. As a result, now I can see PI data from the public PI Web API endpoint within the PI System Explorer.

 

This custom data reference (CDR) was developed on top of the PI Web API client library for .NET Framework and PI AF SDK. The basic idea is to make HTTP requests against PI Web API and convert the responses into PI AF SDK objects which PSE will be able to process.

 

Requirements

 

  • PI Web API 2017 R2+ installed within your domain using Kerberos or Basic Authentication. If you are using an older version, some methods might not work.
  • PI AF 2017 R2+
  • .NET Framework 4.6.2

 

GitHub repository

Please visit the GitHub repository for this project where you can find its source code, download the CDR under the GitHub release section and read about the new features added. There are two folders: src and dist. The src folder has the source code package. If you compile the VS Solution in the Release mode, the binaries will be created on the dist folder.  Due to the settings of the .gitgnore file, this folder is empty.

 

Video to get started:

 

 

 

 

Installation

  • Copy all files from the dist folder to %PIHOME%\PIWebApiDR
  • Register the CDR using the following command (you can also run register_plugin.bat):

 

"%pihome%\AF\regplugin" "OSIsoft.PIDevClub.PIWebApiDR.dll" "OSIsoft.PIDevClub.PIWebApiClient.dll" "Newtonsoft.Json.dll" /own:OSIsoft.PIDevClub.PIWebApiDR.dll /PISystem:PISYSTEMNAME 

 

 

Uninstallation

  • Unregister the CDR using the following command (you can also run unregister_plugin.bat):

 

"%pihome%\AF\regplugin" -u "OSIsoft.PIDevClub.PIWebApiDR.dll" /PISystem:PISYSTEMNAME 

 

  • Delete all files from the %PIHOME%\PIWebApiDR folder.

 

ConfigString and ConfigStringEditor

 

The config string of this CDR has the following structure:RemoteDatabaseId={RemoteDatabaseId};WebId={WebId}

  • RemoteDatabaseId is the ID of the RemoteDatabase which is an AF Element with many attributes with the configuration to connect to a remote PI Web API.
  • WebId is the Web ID 2.0 of the remote AF Attribute.

 

The ConfigStringEditor (the window that is opened when you click on the "Settings..." button on PI System Explorer generally used to edit the attribute config string) does not allow the user to change the ConfigString of the attribute. It is used only to visualize the information. Please use the Remote Database Manager to delete and create databases mapping remote ones.

 

 

 

The remote databases are stored under the "OSIsoft Technology" root element of the Configuration AF database.

 

 

 

Remote Database Manager

The Remote Database Manager should be used to manage the remote database on the system as well as to create local AF databases mapped to remote AF databases through PI Web API. Below you can find a screenshot of this application that comes with the PI Web API Data Reference.

 

 

Visualizing PI data from remote AF databases locally in PSE

On the screenshot below, you can see a PI System Explorer accessing data from the "Universities Mapped" database. This database was created by the Remote Database Manager, mapping the "Universities" AF database from the public PI Web API.

 

 

Visualizing PI data from remote AF databases in PI Vision 3

 

This custom data reference works with PI Vision 3 as long as the following procedure is followed:

  1. On the PI Vision 3 machine, edit the file C:\ProgramData\OSIsoft\Coresight\PIDS_Settings.config  by adding the following key:   <add key="AFDRSetting:PI Web API" value="DisableInputs"/>
  2. Edit the PI Vision 3 web config (C:\Program Files\PIPC\PIVision) by making sure the file attribute of pidsSetting node is defined as:  <pidsSettings file="C:\ProgramData\OSIsoft\Coresight\PIDS_Settings.config" />
  3. Save and restart IIS

 

The procedure above will make sure that PI Vision will read data using the default PI AF SDK methods.

 

Features of the CDR in PI AF SDK

 

When writing our UnitTests, we have create a sample AF database with attributes using the PI Point DR and then created mapped AF database with PI Web API DR using the Remote Database Manager. Each test will get data from the PI Point DR and PI Web API DR and compare the results to make sure they are the same. Below you can find the features implemented on the PI Web API data reference:

  • Asynchronous
  • DataPipe (please refer to PIWebApiEventSource.cs)
  • InterpolatedValue
  • InterpolatedValues
  • InterpolatedValuesAtTimes
  • PlotValues
  • RecordedValue
  • RecordedValues
  • RecordedValuesAtTimes
  • Summary
  • Summaries
  • Bulk calls

 

If you use AFDataPIpes with Attributes with PI Web API Data Reference, the CDR will use PI Web API Channels to get new real-time values under the hood.

 

Troubleshooting

If you are having issues make sure:

  • .NET Framework 4.6.2 is installed on the computer registering the plugin.
  • Run the Remote Database Manager with administrative privileges.
  • Unblock the files after extracting them from the compressed file.

 

 

Disclaimer

 

In case you are using Basic authentication, the username and password will be stored in fixed attributes. Although the password will be encoded, it won't be safe. Every user with read access to the Configuration database will be able to get the password of the remote PI Web API endpoint. For Kerberos authentication, the credentials won't be stored on the AF database.

 

This is not an official product. As described, there are security risks involved. Use it carefully.

 

Conclusion

I hope that the PI Community will benefit a lot from using the library as another tool to share data. Please test yourself and let me know if it works fine. I plan to record a video with some tips to use this great feature.

Introduction

 

This is a MATLAB toolbox that integrates the PI System with MATLAB through PI Web API. With this toolbox you can retrieve PI data without having to generate the URL for each request. This version was developed on top of the PI Web API 2018 Swagger specification.

 

In the new upcoming 2018 release, PI Asset Analytics will introduce native connectivity to MATLAB enabling users to schedule and run their MATLAB functions fully integrated into their analyses. In other words, you will be able to integrate the PI System with MATLAB on production using a model that you have already built. This tool was developed for you to create new models with PI System data before using it on production.

 

Finally, this client library is read-only since only the HTTP GET request methods were added to the library.

 

Requirements

 

  • PI Web API 2018 installed within your domain using Kerberos or Basic Authentication. If you are using an older version, some methods might not work.
  • MATLAB 2018a+

 

GitHub repository

 

Please visit the GitHub repository for this project where you can find its source code, download the toolbox and read about the new features added.

 

Installation

 

This MATLAB toolbox is not available on MATLAB central servers. You should download it directly from this GitHub repository on the release section.

 

Please use the command below to install the toolbox:

 

matlab.addons.toolbox.installToolbox('piwebapi.mltbx')

 

If the installation is successful, you should see this toolbox inside matlab.addons.toolbox.installedToolboxes:

 

toolboxes = matlab.addons.toolbox.installedToolboxes;

 

If you want to uninstall this toolbox, use the command below:

 

matlab.addons.toolbox.uninstallToolbox(toolboxes(i))

 

 

Documentation

All the methods and classes from this MATLAB Toolbox are described on its documentation, which can be opened by typing on the console:

 

demo toolbox 'PI Web API client library for Matlab'

 

Notes

 

It is highly recommended to turn debug mode on in case you are using PI Web API 2017 R2+ in order to receive more detailed exception errors. This can be achieved by creating or editing the DebugMode attribute's value to TRUE from the System Configuration element.

 

Examples

 

Please refer to the following examples to understand how to use this library:

 

Create an instance of the piwebapi top level object using Basic authentication.

 

username = 'webapiuser';
useKerberos = false;
password = 'password'
baseUrl = 'https://devdata.osisoft.com/piwebapi';
verifySsl = false;
client = piwebapi(baseUrl, useKerberos, username, password, verifySsl);

 

Only the Basic authentication is available on this initial version. Please make sure to set up PI Web API properly to make it compatible with this authentication method.

If you are having issues with your SSL certificate and you want to ignore this error, set verifySsl to false.

 

Retrieve data from the home PI Web API endpoint

 

pilanding = client.home.get();

 

Get the PI Data Archive object

 

dataServer = client.dataServer.getByName(serverName);

 

Get the PI Point, AF Element and AF Attribute objects

 

point = client.point.getByPath("\\PISRV1\sinusoid");
attribute = client.attribute.getByPath("\\PISRV1\Universities\UC Davis\Buildings\Academic Surge Building\Electricity|Demand");
element = client.element.getByPath("\\PISRV1\Universities\UC Davis\Buildings\Academic Surge Building\Electricity"); 

 

Get recorded, interpolated and plot values from a stream

 

webId = point1.WebId;
startTime = "*-10d";
endTime = "*";
interval = "1h";
intervals = 30;
maxCount = 100;
desiredUnits = '';
selectedFields = '';
associations = '';
boundaryType = '';
filterExpression = '';
includeFilteredValues = '';


recordedValues = client.stream.getRecorded(webId, associations, boundaryType, desiredUnits, endTime, filterExpression, includeFilteredValues, maxCount, selectedFields, startTime);
interpolatedValues = client.stream.getInterpolated(webId, desiredUnits, endTime, filterExpression, includeFilteredValues, interval, selectedFields, startTime);
plotValues = client.stream.getPlot(webId, desiredUnits, endTime, intervals, selectedFields, startTime);

 

Get recorded, interpolated and plot values from a streamset in bulk

 

sortField = '';
sortOrder= '';

webIds = { point1.WebId, point2.WebId, point3.WebId, attribute.WebId};

recordedValuesInBulk = client.streamSet.getRecordedAdHoc(webIds, boundaryType, endTime, filterExpression, includeFilteredValues, maxCount, selectedFields, sortField, sortOrder, startTime);
interpolatedValuesInBulk = client.streamSet.getInterpolatedAdHoc(webIds, endTime, filterExpression, includeFilteredValues, interval, selectedFields, sortField, sortOrder, startTime);
plotValuesInBulk = client.streamSet.getPlotAdHoc(webIds, endTime, intervals, selectedFields, sortField, sortOrder, startTime);

 

 

Conclusion

 

If you want to create models to be used in production, please use this library and let me know if it works fine. You can send me an e-mail directly (mloeff@osisoft.com) or leave a comment below.

Introduction

 

After publishing the PI Web API client libraries on GitHub, I have received several enhancement requests (for .NET Standard, Java and Python) from our customers and partners. Some of them were added to the libraries!

 

Enhancements for the client library for .NET Standard

 

Migrated from RestSharp to HttpClient

 

Although there is no change for the end user,  RestSharp was replaced by the native HttpClient. The main reason is to use the CancellationTokenSource which will be commented on the next item. Also, HttpClient is available natively on .NET Standard so there is no need to download an extra NuGet package.

 

CancellationToken added for Async requests

 

Using the CancellationTokenSource allows you to cancel HTTP requests during a running operation. Below you can find an example:

 

Stopwatch watch = Stopwatch.StartNew();
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
PIItemsStreamValues bulkValues = null;
try
{
     Task t = Task.Run(async () =>
     {
          bulkValues = await client.StreamSet.GetRecordedAdHocAsync(webId: webIds, startTime: "*-1800d", endTime: "*", maxCount: 50000, cancellationToken: cancellationTokenSource.Token);
     });
     //Cancel the request after 1s
     System.Threading.Thread.Sleep(1000);
     cancellationTokenSource.Cancel();
     t.Wait();
     Console.WriteLine("Completed task: Time elapsed: {0}s", 0.001 * watch.ElapsedMilliseconds);
}
catch (Exception)
{
     Console.WriteLine("Cancelled task: Time elapsed: {0}s", 0.001 * watch.ElapsedMilliseconds);
};

 

Fixed known issue on the Calculation controller

 

There was a known issue reported on GitHub when calling Calculation.GetAtTimes() method using expressions with comma. This was fixed so the code below works successfully!

 

string expression = "'sinusoid'*2 + 'cdt158'";
PITimedValues values = client.Calculation.GetAtTimes(webId: dataServer.WebId, expression: expression , time: new List<string>() { "*-1d" });

string expression2 = "'cdt158'+tagval('sinusoid','*-1d')";
PITimedValues values2 = client.Calculation.GetAtTimes(webId: dataServer.WebId, expression: expression2, time: new List<string>() { "*-1d" });

 

Enhancements for the client library for Java

 

PI Web API Batch was added in order to make more complex requests with better performance. You can find more information about PI Web API Batch here.

 

Added PI Web API Batch

 

Map<String, PIRequest> batch = new HashMap<String, PIRequest>();
PIRequest req1 = new PIRequest();
PIRequest req2 = new PIRequest();
PIRequest req3 = new PIRequest();
req1.setMethod("GET");
req1.setResource("https://marc-web-sql.marc.net/piwebapi/points?path=\\\\MARC-PI2016\\sinusoid");
req2.setMethod("GET");
req2.setResource("https://marc-web-sql.marc.net/piwebapi/points?path=\\\\MARC-PI2016\\cdt158");
req3.setMethod("GET");
req3.setResource("https://marc-web-sql.marc.net/piwebapi/streamsets/value?webid={0}&webid={1}");

List<String> parameters = new ArrayList<>();
parameters.add("$.1.Content.WebId");
parameters.add("$.2.Content.WebId" );
req3.setParameters(parameters);


List<String> parentIds = new ArrayList<>();
parentIds.add("1");
parentIds.add("2");
req3.setParentIds(parentIds);

batch.put("1", req1);
batch.put("2", req2);
batch.put("3", req3);
Map<String, PIResponse> batchResponse = client.getBatch().execute(batch);

Object content1 = batchResponse.get("1").getContent();
Object content2 = batchResponse.get("2").getContent();
Object content3 = batchResponse.get("3").getContent();

JSON json = new JSON(client.getApiClient());
PIPoint pointBatch1 = json.deserialize(json.serialize(content1), new TypeToken<PIPoint>(){}.getType());
PIPoint pointBatch2 = json.deserialize(json.serialize(content2), new TypeToken<PIPoint>(){}.getType());
PIItemsStreamValue batchStreamValues = json.deserialize(json.serialize(content3), new TypeToken<PIItemsStreamValue>(){}.getType());

 

 

Added Web ID 2.0 client generation

 

Now, it is also possible to generate Web ID 2.0 without having to make an HTTP request against PI Web API. The library also provides a way to get information for a particular Web ID. Remember that this only works with PI Web API 2017 R2+.

 

PIDataServer dataServer = client.getDataServer().getByPath("\\\\MARC-PI2016", null, null);
PIPoint point = client.getPoint().getByPath("\\\\marc-pi2016\\sinusoid",null, null);
PIElement element = client.getElement().getByPath("\\\\MARC-PI2016\\CrossPlatformLab\\marc.adm",null, null);
PIAttribute attribute = client.getAttribute().getByPath( "\\\\MARC-PI2016\\CrossPlatformLab\\marc.adm|Heading",null,null);

WebIdInfo webIdInfo2 = client.getWebIdHelper().getWebIdInfo(attribute.getWebId());
WebIdInfo webIdInfo = client.getWebIdHelper().getWebIdInfo(element.getWebId());
WebIdInfo webIdInfo4 = client.getWebIdHelper().getWebIdInfo(point.getWebId());
WebIdInfo webIdInfo3 = client.getWebIdHelper().getWebIdInfo(dataServer.getWebId());

String web_id1 = client.getWebIdHelper().generateWebIdByPath("\\\\PISRV1\\CDF144_Repeated24h_forward", PIPoint.class, null);
String web_id2 = client.getWebIdHelper().generateWebIdByPath("\\\\PISRV1\\Universities\\UC Davis\\Buildings\\Academic Surge Building|Electricity Totalizer", PIAttribute.class, PIElement.class);

 

Available for downloading through JitPack

 

I've received a request on GitHub to publish the library on Maven Central. Since it is not an easy process, especially if you are not familiar, I've decided to publish it though JitPack.

 

If you want to use the Java library, please read the instructions here about how to retrieve the library without having to compile it locally.

 

 

Enhancements for the client library for Python

 

 

Added Kerberos as an authentication method

 

Robert Raesemann asked me in this blog post to make the client library for Python compatible with Kerberos authentication. Now it is possible to instantiate the PI Web API top level object as:

 

from osisoft.pidevclub.piwebapi.pi_web_api_client import PIWebApiClient
  client = PIWebApiClient("https://test.osisoft.com/piwebapi", useKerberos=True, verifySsl=False)

 

 

Added PI Web API Batch

 

PI Web API Batch was also added to Python.

 

  req1 = PIRequest()
  req2 = PIRequest()
  req3 = PIRequest()
  req1.method = "GET"
  req1.resource = "https://localhost/piwebapi/points?path=\\\\MARC-PI2016\\sinusoid"
  req2.method = "GET"
  req2.resource = "https://localhost/piwebapi/points?path=\\\\MARC-PI2016\\cdt158"
  req3.method = "GET"
  req3.resource = "https://localhost/piwebapi/streamsets/value?webid={0}&webid={1}"
  req3.parameters = ["$.1.Content.WebId", "$.2.Content.WebId"]
  req3.parent_ids = ["1", "2"]

  batch = {
"1": req1,
"2": req2,
"3": req3
  }

  batchResponse = client.batch.execute(batch)
  point1 = client.api_client.deserialize_object(batchResponse["1"].content, 'PIPoint')
  point2 = client.api_client.deserialize_object(batchResponse["2"].content, 'PIPoint')
  itemsStreamValue = client.api_client.deserialize_object(batchResponse["3"].content, 'PIItemsStreamValue')

 

Thanks Rafael Borges for helping me with this task!

 

 

Optional parameters with default values

 

In this new version, you don't need to define all parameters of each method. Optional parameters have default values which are going to be used if they are not defined. Let's see an example:

 

piItemsStreamValues = client.streamSet.get_recorded_ad_hoc(webIds, start_time="*-3d", end_time="*",
                                                                   include_filtered_values=True, max_count=1000)

 

 

Added Web ID 2.0 client generation

 

Web ID 2.0 client generation was also added to the library. Here is an example:

 

pi_data_server_web_id = client.webIdHelper.generate_web_id_by_path("\\\\PISRV1", type(PIDataServer()), None)
  point1_web_id = client.webIdHelper.generate_web_id_by_path("\\\\PISRV1\\SINUSOID", type(PIPoint()))
  point2_web_id = client.webIdHelper.generate_web_id_by_path("\\\\PISRV1\\CDT158", type(PIPoint()))
  point3_web_id = client.webIdHelper.generate_web_id_by_path("\\\\PISRV1\\SINUSOIDU", type(PIPoint()))
  pi_attribute_web_id = client.webIdHelper.generate_web_id_by_path(
"\\\\PISRV1\\Universities\\UC Davis\\Buildings\\Academic Surge Building|Electricity Totalizer",
type(PIAttribute()), type(PIElement()))

  pi_element_web_id= client.webIdHelper.generate_web_id_by_path(
"\\\\PISRV1\\Universities\\UC Davis\\Buildings\\Academic Surge Building", type(PIElement()), None)

 

Available for downloading through PyPI (Python Package Index)

 

Just run the code below to download it:

 

pip install osisoft.pidevclub.piwebapi

 

You can find more information on the PyPI library page.

 

Conclusion

 

I hope you find value in those improvements. If you have an enhancement request concerning one of the client libraries, please let me know!

 

It is almost time to update to 2018!

 

Stay tuned for new updates and releases!

We are excited to present the PI World Innovation Hackathon 2018 winners!

 

DCP Midstream kindly provided a sample of their data with boosters and compressors information. Participants were encouraged to create killer applications for DCP Midstream by leveraging the PI System infrastructure.

 

The participants had 23 hours to create an app using any of the following technologies:

  • PI Server 2017 R2
  • PI Web API 2017 R2
  • PI Vision 2017  R2

 

Our judges evaluated each app based on their creativity, technical content, potential business impact, data analysis and insight and UI/UX. Although it is a tough challenge to create an app in 23 hours, six groups were able to finish their app and present to the judges!

 

Prizes:

1st place: Sonos PLAY 5 + Echo Dot, one year free subscription to PI Developers Club, one time free registration at OSIsoft PI World over the next 1 year

2nd place: Bose Quiet Comfort 35 (Series II), one year free subscription to PI Developers Club, 50% discount for registration at OSIsoft PI World over the next 1 year

3rd place: Vilros Raspberry Pi 3 Retro Arcade Gaming Kit + 5 USB Classic Controllers and one year free subscription to PI Developers Club

 

 

Without further do, here are the winners!

 

1st place - Oogway

 

The team members were: Kshitiz Jain, Matthew Wallace, Paurav Joshi and Diego Eduardo Mercadal

 

 

1st place.jpg

 

1st place winners interview.jpg

 

Team Oogway built a dashboard that enables the plants to make a more informed decision on the route boosters should take. This is done through providing the optimal plant for each booster and telling the current target of each booster. While giving the ability to drill down into each of the boosters to understand efficiency and anomalies.

 

The team used the following technologies:

  • Google Maps API embedded into PI Vision
  • PI AF SDK

Here are some screenshots presented by the Oogway team!

 

Compressor Page:

 

 

 

 

 

2nd place - <Insert Obligatory Gas Joke Here>

 

The team members were:  Rob Raesemann, Greg Busch, Lonnie Bowling and David Rodriguez

 

2nd place.jpg

 

They developed an app which utilizes PI AF event frames to generate leading data sets for further analysis. Utilizes PI Web API client library in Python to analyze and visualize data. Uses the PI AF SDK and Angular to provide visualization and interaction with event frames by end users. Explores novel voice interaction with end users.

 

The team used the following technologies:

  • Python
  • PI AF SDK
  • PI Web API and its client library for Python
  • Angular

 

Here are some screenshots presented by <Insert Obligatory Gas Joke Here>!

 

 

 

 

3rd place - Fantastic Four

 

The team members were: Xihui Zhang, Michael Baldauff, Jonathan Mejeur  and Syed Rehanrawos

 

3rd place.jpg

 

Team Fantastic Four developed an app which leverages features of PI Vision 4 in order to display IIOT results from a high level overview to multistate reporting.

 

The team used the following technologies:

  • PI Vision 4

 

Here is a screenshot presented by the Fantastic Four!

 

This is the material for the talk "Build PI Applications Faster with PI Web API Client Libraries" held during the PI World SF 2018 Developer Track.

 

GitHub - osimloeff/Building-Apps-Faster-Client-Libs-TechCon2018: Samples used on the "Building applications faster using…

 

It includes:

1. Visual Studio Solution

2. PowerPoint Presentation

 

Click "Download ZIP" on the right-side of the toolbar or fork and clone to your local repository.

 

This talk and source code have 10 examples:

 

  • Example 1 - Authentication
  • Example 2 - Retrieve basic PI Objects: PIElement, PIAttributes
  • Example 3 - Handling exceptions
  • Example 4 - Updating PI Point description
  • Example 5 - Retrieve data in bulk
  • Example 6 - Update data in bulk
  • Example 7 - PI Web API Batch
  • Examples 8 and 9 - Web ID 2.0 client generation
  • Example 10 - PI Web API Channels

 

This is the material for the "Developing Modern Web Apps Using Angular 5 and PI Web API " hands-on-lab held during the PI World SF 2018 Developer Track.

 

GitHub - osimloeff/Modern-Web-Technologies-TechCon2018

 

It includes:

1. Two Visual Studio Code Projects

2. Workbook

3. XML to be imported in PI AF

4. PowerPoint Presentation

 

Click "Download ZIP" on the right-side of the toolbar or fork and clone to your local repository.

 

The screenshot below shows the sample app used in this lab.

 

 

This lab has 5 exercises.

  • Exercises 1 and 2 are about retrieving the cities geolocation.
  • Exercises 3 is about getting live and performing calculations.
  • Exercise 4 will explain how integrate your app with PI Vision 3.
  • Exercise 5 is about PI Web API Batch

 

The Virtual Machine for this lab is available on OSIsoft Virtual Learning Environment whose link is below:

https://learning.osisoft.com/Labs/LabInformation/

Filter Blog

By date: By tag: