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

PI Developers Club

81 Posts authored by: Marcos Vainer Loeff Employee

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/

Introduction

 

Although PI Vision 4 (2018) is not released yet, the participants of the Virtualization Virtual Hackathon 2018 need to learn the extensibility model of this product in order to raise their chances of creating a valuable custom symbol. As a result, I've decided to write the first blog post of the PI Vision 4 version of my "Developing the Google Maps custom symbol for PI Vision 3" blog post series.

 

The ultimate idea is that when the user drags an element and drops it on the PI Vision display, a Google Map will be created with a marker located according to the values of the latitude and longitude attributes of the dropped element. If another element is dropped on the map, another marker should be created accordingly.

 

This blog post (part 1) will focus on creating the map only, which is not something trivial.

 

 

Disclaimer

 

Again, PI Vision 4 is not released yet. The hackathon participants are working with a preview version. Therefore, this library might not be compatible with the released version of PI Vision 4. I will update this article and library as soon as PI Vision 4 is released though.

PI Vision 4 public preview is planned to start at PI World San Francisco 2018. You will have the opportunity to test yourself this new extensibility model! The PI Vision 4 release is planned for Q4 2018.

 

Setting up your environment

 

I will comment this topic in details after PI Vision 4 is released. For now, the hackathon participants will access their Virtual Machine with the environment already set up. In order to develop your custom symbol, the following products are used:

  • Visual Studio Code
  • Google Chrome
  • Node.js/npm
  • Git

 

If you take a look at the Virtual Machine, you will realize that the PI Vision Extension Library Seed Project was already cloned to the C:\src\pi-vision-extensions folder.

 

Getting started developing the PI Vision symbol

 

Open the command prompt and navigate to the C:\src\pi-vision-extensions folder. Then type "code .". Visual Studio Code with the PI Vision Extensions project will be opened.

 

First rename the \src\example folder and its files. They should start with gmaps instead of example as shown on the screenshot below.

 

 

 

Don't worry, the gmaps-loader.service.ts will be created later. The code to get started for the gmaps.component.ts is below:

 

import { Component, OnChanges, OnInit } from '@angular/core';


@Component({
  selector: 'gmaps',
  templateUrl: 'gmaps.component.html',
  styleUrls: ['gmaps.component.css']
})
export class GoogleMapsComponent implements OnInit, OnChanges {
  constructor()
  {
  }


  ngOnInit() {
  } 
  
  ngOnChanges(changes) {
    if (changes.data) {
     
    }
  }
  
}

 

The custom symbol is actually an Angular component whose decorator (@Component) is its metadata. It describes the selector, templateUrl and styleUrls. You can find more information about Angular in its official web site.

 

The content of HTML template for this component (gmaps.component.html) file is:

 

<div #gmap style="width:100%;height:100%"></div>

 

In order to create a map you just need a div HTML node. The rest is handled by JavaScript.

 

The next step is to write the Google Maps JavaScript code. Since we are using TypeScript, the definition of the Google Maps classes of needs to be downloaded and installed through the command below:

 

npm install --save @types/googlemaps

 

 

 

Getting started with Google Maps JavaScript API

 

Google provides a programming reference and samples for Google Maps JavaScript API, which were really useful to write this blog post. Let's take a look at the most basic example. Their HTML page has the following source code:

 

<!DOCTYPE html>
<html>
  <head>
    <title>Simple Map</title>
    <meta name="viewport" content="initial-scale=1.0">
    <meta charset="utf-8">
    <style>
      html, body {
        height: 100%;
        margin: 0;
        padding: 0;
      }
      #map {
        height: 100%;
      }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <script>

var map;
function initMap() {
  map = new google.maps.Map(document.getElementById('map'), {
    center: {lat: -34.397, lng: 150.644},
    zoom: 8
  });
}

    </script>
    <script src="https://maps.googleapis.com/maps/api/js?&callback=initMap" async defer></script>
  </body>
</html>

 

 

Ok, we have some problems to solve:

 

  1. Editing the index file from the PI Vision 4 web site in order to load the Google Maps JavaScript API is not a recommended practice. The symbol will have to do this task.
  2. The second problem is that url which refers to the GMaps (Google Maps) library has the name of the callback function to be called after this library is loaded. How can we make this work within Angular 5?
  3. Users can add as many instances of this symbol as they want. On the other hand, the GMaps libraries needs to be loaded only once. How to make sure there won't be any conflict?

 

 

Solving problem 1: After some research, I found this interesting StackOverflow page, which allows us to dynamically load external JavaScript scripts using Typescript. After making some changes, here is the code that makes the trick:

 

const url = "https://maps.google.com/maps/api/js?key=AIzaSyDUQhTeNplK37EX-mXdAB-zVuYDutE5c2w&callback=gMapsCallback"
let node = document.createElement('script');
node.src = url;
node.type = 'text/javascript';
document.getElementsByTagName('head')[0].appendChild(node);

 

 

Solving problem 2 and 3: If we define a function as property of the window JavaScript object, GMaps will be able to call it. Therefore, we have defined the window['gMapsCallback'] function as:

 

GoogleMapsLoader.promise = new Promise( resolve => {
    
    // Set callback for when google maps is loaded.
    window['gMapsCallback'] = (ev) => {
         resolve();
    };


    let node = document.createElement('script');
    node.src = url;
    node.type = 'text/javascript';
    document.getElementsByTagName('head')[0].appendChild(node);
});

 

We have created a JavaScript Promise that when it is solved it will define the window['gMapsCallback'] and then call the Google Maps JavaScript API which will call the window['gMapsCallback'] method. The beauty of this approach is that the promise is resolved only once which means that the Google Maps will be loaded also once.

 

An Angular service named GoogleMapsLoader is created to load the Google Maps JavaScript library with the following code snippet:

 

 

import { Injectable } from '@angular/core';


const url = "https://maps.google.com/maps/api/js?key=AIzaSyDUQhTeNplK37EX-mXdAB-zVuYDutE5c2w&callback=gMapsCallback"
@Injectable()
export class GoogleMapsLoader {
  private static promise;
  public static load() {
      // First time 'load' is called?
      if (!GoogleMapsLoader.promise) {
          // Make promise to load
          GoogleMapsLoader.promise = new Promise( resolve => {
              // Set callback for when google maps is loaded.
              window['gMapsCallback'] = (ev) => {
                  resolve();
              };


              let node = document.createElement('script');
              node.src = url;
              node.type = 'text/javascript';
              document.getElementsByTagName('head')[0].appendChild(node);
          });
      }
      // Always return promise. When 'load' is called many times, the promise is already resolved.
      return GoogleMapsLoader.promise;
  }
}

 

On the custom symbol (Angular component), this is how you would load the library by calling the GoolgeMapsLoader service:

 

      GoogleMapsLoader.load().then(res => {


      });

 

 

With all these concepts and restrictions in mind, here is the final version of this blog post (part 1).

 

import { Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
import { GoogleMapsLoader } from './gmaps-loader.service'


@Component({
  selector: 'gmaps',
  templateUrl: 'gmaps.component.html',
  styleUrls: ['gmaps.component.css']
})


export class GoogleMapsComponent implements OnInit, OnChanges {
  @ViewChild('gmap') gmapElement: any;
  private map : google.maps.Map


  constructor(private mapLoader : GoogleMapsLoader)
  {


  }


  ngOnInit() {


    GoogleMapsLoader.load().then(res => {
        console.log('GoogleMapsLoader.load.then', res);
        this.initMap();
    });
  } 


  ngOnChanges(changes) {
    if (changes.data) {
     
    }
  }


  private initMap() {
    var mapProp = {
      center: new google.maps.LatLng(18.5793, 73.8143),
      zoom: 15,
      mapTypeId: google.maps.MapTypeId.ROADMAP
    };


    this.map = new google.maps.Map(this.gmapElement.nativeElement, mapProp);
  }
}

 

 

When the custom symbol is created, the ngOnInit() method is called. This function calls initMap() which creates a map with a specific center and zoom. The ngOnChanges() method is called whenever the PI System receives a new value for the associated attribute or element.

The focus of this blog post is just to create a map for each custom symbol added to the display. This method will be used on the following parts of this blog post series.

 

The last step is to update the module.ts on the root folder with the following information:

Rename the ExampleComponent to GoogleMapsComponent which is present on the declaration, exports and entryComponents fields  of the NgModule declarator. Add the GoogleMapsLoader to the providers field which should contain all the services. In order to be successful you need to import those modules properly.

 

Finally, we need to rename the symbol properties on the ExtensionLibrary class. We have copied the google-maps.svg from this GitHub repository and pasted into the \src\assets\images folder. We have deleted all the items from the configProps array of the generalConfig object as at this point we are not interested in setting up the configuration pane of the symbol.

 

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgLibrary, SymbolType, SymbolInputType, ConfigPropType } from './framework';
import { LibModuleNgFactory } from './module.ngfactory';
import {GoogleMapsLoader} from './gmaps/gmaps-loader.service'
import { GoogleMapsComponent} from './gmaps/gmaps.component';


@NgModule({
  declarations: [ GoogleMapsComponent ],
  imports: [ CommonModule ] ,
  providers:  [GoogleMapsLoader],
  exports: [ GoogleMapsComponent],
  entryComponents: [ GoogleMapsComponent ]
})
export class LibModule { }


export class ExtensionLibrary extends NgLibrary {
  module = LibModule;
  moduleFactory = LibModuleNgFactory;
  symbols: SymbolType[] = [
    {
      name: 'gmaps-symbol',
      displayName: 'Google Maps Symbol',
      dataParams: { shape: 'single' },
      thumbnail: '^/assets/images/google-maps.svg',
      compCtor: GoogleMapsComponent,
      inputs: [
        SymbolInputType.Data,
        SymbolInputType.PathPrefix
      ],
      generalConfig: [
        {
          name: 'Google Maps Options',
          isExpanded: true,
          configProps: [ ]
        }
      ],
      layoutWidth: 200,
      layoutHeight: 200
    }
  ];
}

 

Save all files, run your local web pack server using "npm start run" and open PI Vision 4 using Google Chrome. In order to load the custom symbols, you need to go to the PI Vision landing page, select Options on the left pane and turn on the developer mode, according to the screenshot below:

 

 

 

Create a new display. Click on the Google Maps symbol on the left pane. Each click will create a new instance of the Google Maps custom symbol and add it to the PI Vision display. Make sure not only that the Google Maps symbol is added to the top-left pane but also that no exception is thrown when multiple symbols are added on a single display (please check the Google Chrome developer tools).

 

 

Conclusions

If you are a hackathon participant reading this blog post, I hope this material will help you create valueable custom symbols. If this is not the case, I hope you will have a good idea about the new extensibility model of PI Vision 4 and Angular 5.

PI World Innovation Hackathon 2018 

When: April 23th 9am (Day 0) - April 24th 8am (Day 1) 

Where: Embarcadero Room – Parc 55 Hotel

 

Join us at the Innovation Hackathon happening at PI World San Francisco 2018 to learn, network, and compete for prizes! Whether you are a PI System Developer, Architect, Integrator, Administrator, Business Analyst, or Data Scientist, you can enjoy this high-energy and vibrant event. OSIsoft will provide all of the tools, subject matter experts, and a ready-to-start environment. Just show up with your ideas and energy to develop a killer app in 23 hours! 

 

 

The data sponsor for the PI World 2018 SF Innovation Hackathon is DCP Midstream, which is one of the largest producers of natural gas liquids and one of the largest natural gas processing companies in the U.S. They gather and/or process about 12 percent of our nation’s gas supply.

 

The winners of this event will receive the following prizes:

1st place:

  • One-time 100% discount to attend the UC per team member*
  • Free 1-year subscription to PI Developers Club
  • Featured in PI Developers Club Community
  • Echo Dot + Sonos PLAY 5: Ultimate Wireless Smart Speaker for Streaming Music.

2nd place:

  • One-time 50% discount to attend the UC per team member*
  • Free 1-year subscription to PI Developers Club
  • Featured in PI Developers Club Community
  • Bose Quiet Comfort 35 (Series II)

3rd place:

  • Free 1-year subscription to PI Developers Club
  • Featured in PI Developers Club Community
  • Vilros Raspberry Pi 3 Retro Arcade Gaming Kit + 5 USB Classic Controllers

 

*Note: Discounts are for any OSIsoft PI World events in 2018 or 2019. TechCon labs and other training sessions are not included.

 

 

Register Today

 

Disclaimer: “You understand that OSIsoft strives to conduct business according to the highest ethical standards, and it may determine in its sole discretion that it is not appropriate to provide a prize to a winner under certain circumstances.”

Filter Blog

By date: By tag: