Skip navigation
All Places > PI Developers Club > Blog > 2015 > June
2015
mjarvis

PI Cloud Connect

Posted by mjarvis Employee Jun 30, 2015

picc.png

It was recently brought to my attention that not many PI Dev Club members were aware of the opportunity to use OSIsoft’s first cloud-based service. PI Cloud Connect offers a free trial, and the Development PI System will work with it. PI Cloud Connect enables people to share data across enterprises in a simple and low-maintenance way. Today there are many customers in many different industries actively using the product, including Power Generation, Transmission & Distribution, Oil & Gas, and Pharmaceutical.

Problems Solved

  • The sharing of data does not require a VPN
  • Deliver asset context along with the equipment data
  • Automatically receive new features

dataflow.jpg

MythBusters

Is PI Cloud Connect replacing PI to PI?

No.

 

  • PI Cloud Connect is best for:
    • Sharing data across enterprises.
    • Sharing subsets of PI System data, which includes selected AF objects and their associated PI Data Archive data.
  • PI to PI is best for:
    • Intra-company data aggregation.
    • PI Data Archive data only.

Resources

  • Website for signing up and more information
  • YouTube video series
  • ARC View customer testimonial on PI Cloud Connect in the Oil and Gas Business

PI AF 2.x allows the creation of custom data references, which extend the capability of PI AF by defining the information and mechanism necessary for an AF Attribute to read, calculate, and/or write a value. There are many reasons to implement a custom data reference (e.g. displaying data in AF from an external data source, custom calculations – though we encourage checking out asset analytics first!). In the background, data references are simply .NET classes that derive from the OSIsoft.AF.Asset.AFDataReference base class that are registered and stored in the AF Server. If you haven’t worked with custom data references before, please check out the white paper “Implementing AF 2.x Data References” available for download from the Tech Support Download Center.

 

The purpose of this blog post is to discuss a less-documented feature, which is to implement AF Data Pipes for custom data references. An AF Data Pipe is simply a collection of AF Attribute objects that are signed up for data change events on the server. It is often used by clients to get all data change events for AF Attributes. This question was first brought up in a thread: Custom Data Reference - How To Implement Data Pipe? After realizing that there is limited information about this topic, I have decided to test out implementing an AF Data Pipe.

 

 

Background

There are two types of data source for the AF Data Pipe (AFDataPipe): calculation or system of record. AFDataPipe considers a data reference a calculation data source if it has one or more inputs. For a calculation data reference to support data pipe, it must have at least one input supporting DataPipe.

 

In general, for a data reference plugin to support AFDataPipe:

  • If the data reference is not system of record and depends on AFSDK to get the inputs data to the data reference, AFSDK can handle getting the inputs and call AFDataReference.GetValue to compute the output for the data pipe event.
  • If the data reference is system of record, then it can implement a derived class of AFEventSource and expose a static CreateDataPipe method in the plugin to return this derived AFEventSource.

 

 

My Custom Data Reference

In this blog, I will be implementing a simple data reference based on a system of record to illustrate how to support AFDataPipe. This data reference, called SQLDR, simply gets the timestamps and values from the specified SQL table for display in PI AF.

 

Please feel free to skip this section if you are familiar with implementing a custom data reference. I’m using AF SDK 2.6.2 in this example.

 

Creating the Visual Studio Project

  1. Create a new Class Library using the .NET Framework 4.5.
  2. Add a reference to the OSIsoft.AFSDK.dll located at %pihome%\AF\PublicAssemblies\4.0 folder.
  3. Rename the class name (I renamed mine to SQLDR.cs).

 

Preparing our new class library to implement AFDataReference

Add the using directive:

using OSIsoft.AF.Asset;

 

Add inheritance for SQLDR to AFDataReference:

public class SQLDR : AFDataReference

 

Note that each custom AFDataReference class implementation requires a unique System.Runtime.InteropServices.GuidAttribute attribute to be specified to uniquely identify the data reference in AF. A System.SerializableAttribute and System.ComponentModel.DescriptionAttribute are also required on the class before an AFDataReference can be used in AF.

 

using System.ComponentModel;
using System.Runtime.InteropServices;

 

[Serializable]
[Guid("A1AC3A39-9E55-4700-BBC0-68299E67C4A1")]
[Description("SQLDR; Get values from a SQL table")]
public class SQLDR : AFDataReference

 

 

Implementing AFDataReference

ConfigString

There are 3 pieces of information we need for our custom data reference: the name of the SQL server, the database, as well as the name of the table containing the data. We will implement a simple configuration where these information are stored in a string separated by semicolons.

 

// Private fields storing configuration of data reference
private string _tableName = String.Empty;
private string _dbName = String.Empty;
private string _sqlName = String.Empty;

// Public property for name of the SQL table
public string TableName
{
    get
    {
        return _tableName;
    }
    set
    {
        if (_tableName != value)
        {
            _tableName = value;
            SaveConfigChanges();
        }
    }
}

// Public property for name of the SQL database
public string DBName
{
    get
    {
        return _dbName;
    }
    set
    {
        if (_dbName != value)
        {
            _dbName = value;
            SaveConfigChanges();
        }
    }
}

// Public property for name of the SQL instance
public string SQLName
{
    get
    {
        return _sqlName;
    }
    set
    {
        if (_sqlName != value)
        {
            _sqlName = value;
            SaveConfigChanges();
        }
    }
}

// Get or set the config string for the SQL data reference
public override string ConfigString
{
    get
    {
        return String.Format("{0};{1};{2}", SQLName, DBName, TableName);
    }
    set
    {
        if (value != null)
        {
            string[] configSplit = value.Split(';');
            SQLName = configSplit[0];
            DBName = configSplit[1];
            TableName = configSplit[2];
            SaveConfigChanges();
        }
    }
}

 

 

GetValue/GetValues

Next, we need to tell AF what values to retrieve and display when the client requests for values for the data reference. Since we will be getting data from a SQL server, let’s write a separate SQLHelper class.

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data;
using System.Data.SqlClient;

namespace AFSDK_CustomDR_SQL
{
    /*****************************************************************************************
     * SQL Helper class to get data from the specified SQL Server, SQL Database and SQL Table. 
     * Given a start and end time, the static GetSQLData method will return a SQLDataReader
     * object that contains all values between the time interval in ascending order.
     * Given the end time only, only the most current value will be returned. 
     *****************************************************************************************/
    class SQLHelper
    {
        public static SqlDataReader GetSQLData(string sqlServer, string sqlDb, string sqlTable, DateTime startTime, DateTime endTime)
        {
            // Construct connection string to SQL Server based on input parameters for SQL server name and database name.
            string connectString = String.Format("server={0}; database={1}; Integrated Security=SSPI; Connection Timeout=10", sqlServer, sqlDb);
            SqlConnection sqlConnection = new SqlConnection(connectString);

            // Construct SQL query
            string query;
            using (SqlCommand cmd = new SqlCommand())
            {
                cmd.Connection = sqlConnection;

                // SQL query for the most recent values before the end time
                if (startTime == DateTime.MinValue)
                {
                    query = String.Format("SELECT TOP 1 pi_time, pi_value FROM {0}.{1} WHERE pi_time <= @time ORDER BY pi_time DESC", sqlDb, sqlTable);
                    SqlParameter sqlTime = cmd.Parameters.Add(new SqlParameter("time", System.Data.SqlDbType.DateTime2));
                    sqlTime.Value = endTime;
                    cmd.CommandText = query;
                }

                // SQL query for all values over a specified time range
                else
                {
                    query = String.Format("SELECT pi_time, pi_value FROM {0}.{1} WHERE pi_time >= @startTime AND pi_time <= @endTime ORDER BY pi_time ASC", sqlDb, sqlTable);
                    SqlParameter sqlStartTime = cmd.Parameters.Add(new SqlParameter("startTime", System.Data.SqlDbType.DateTime2));
                    SqlParameter sqlEndTime = cmd.Parameters.Add(new SqlParameter("endTime", System.Data.SqlDbType.DateTime2));
                    sqlStartTime.Value = startTime;
                    sqlEndTime.Value = endTime;
                    cmd.CommandText = query;
                }

                /* Open SQL connection and return the SqlDataReader object. Use CommandBehavior.CloseConnection to ensure that the 
                 * SQL connection is closed when the SqlDataReader object is closed. */
                sqlConnection.Open();
                SqlDataReader sqlReader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
                return sqlReader;
            }
        }
    }
}

 

 

Now, we are ready to override the GetValue and GetValues methods in our main SQLDR class.

 

Add the using directive:

using System.Data;
using System.Data.SqlClient;
using OSIsoft.AF.Time;

 

Override GetValue and GetValues:

// Return latest value if timeContext is null, otherwise return latest value before a specific time
public override AFValue GetValue(object context, object timeContext, AFAttributeList inputAttributes, AFValues inputValues)
{
    AFValue currentVal = new AFValue();
    DateTime time;
    if (timeContext != null)
    {
        time = ((AFTime)timeContext).LocalTime;
    }
    else
    {
        time = DateTime.Now;
    }
    using (SqlDataReader reader = SQLHelper.GetSQLData(SQLName, DBName, TableName, DateTime.MinValue, time))
    {
        if (reader.Read())
        {
            currentVal.Timestamp = AFTime.Parse(reader["pi_time"].ToString());
            currentVal.Value = reader["pi_value"];
        }
    }

    return currentVal;
}

// Return all values (converted to AFValues) over a specific time interval
public override AFValues GetValues(object context, AFTimeRange timeRange, int numberOfValues, AFAttributeList inputAttributes, AFValues[] inputValues)
{
    AFValues values = new AFValues();
    DateTime startTime = timeRange.StartTime.LocalTime;
    DateTime endTime = timeRange.EndTime.LocalTime;
    using (SqlDataReader reader = SQLHelper.GetSQLData(SQLName, DBName, TableName, startTime, endTime))
    {
        while (reader.Read())
        {
            AFValue newVal = new AFValue();
            newVal.Timestamp = AFTime.Parse(reader["pi_time"].ToString());
            newVal.Value = reader["pi_value"];
            values.Add(newVal);
        }
    }
    return values;
}

 

 

Note that we could also be implementing Rich Data Access (RDA) methods such as RecordedValue, InterpolatedValue, etc. These were omitted in this short example. However, I encourage you to check out his PI Developers Club discussion about the topic: When would one need to implement AFDataMethods on a custom DR?

 

SupportedMethods, SupportedDataMethods and SupportedContexts

Finally, we will specify the minimal list of supported methods for our simple custom data reference.

 

Add the using directive:

using OSIsoft.AF.Data;

 

Override SupportedMethods, SupportedDataMethods, and SupportedContexts:

public override AFDataReferenceMethod SupportedMethods
{
    get
    {
        return AFDataReferenceMethod.GetValue | AFDataReferenceMethod.GetValues;
    }
}

public override AFDataMethods SupportedDataMethods
{
    get
    {
        return AFDataMethods.None;
}

public override AFDataReferenceContext SupportedContexts
{
    get
    {
        return AFDataReferenceContext.Time;
    }
}

 

 

Testing

To test our SQLDR data reference so far, let’s register it using the RegPlugIn.exe utility. Navigate to %pihome%\AF directory and run the following command:

Regplugin /PISystem:your-af-server “C:\path\to\dll”

 

Verify that the plug-in is registered by running:

Regplugin /PISystem:your-af-server /List

 

regplugin-list.png

 

Finally, open up PI System Explorer and create a new Attribute using our custom data reference SQLDR!

 

CDRTest.png                    CDRTest2.png

 

 

Implementing the Data Pipe

Alright! We have a working data reference. We are now ready to implement AF Data Pipe.

 

If you try to sign up for updates, you will not receive any updates because the data reference does not support AFDataPipe currently.

 

There are a few things we will need to do:

  1. Implement AFEventSource with AddSignup, RemoveSignup, GetEvents() and Dispose(bool). AFEventSource is a public abstract class providing a basis for data reference developers to implement the Data Pipe feature. The base class will handle all the interaction with AF SDK pipe modules, and implementer does not have to worry about interfacing with different data sinks.
  2. Add a static method, CreateDataPipe, in your implementation of AFDataReference to return an AFEventSource object that you have implemented.
  3. Explicitly state support for data pipe by changing the SupportedDataMethods to include the AFDataMethod.DataPipe.

 

1. Inheriting AFEventSource

The AFEventSource base class has methods to publish data pipe events and report errors, as well as maintains a dictionary of the AFAttributes being monitored by the eventsource. We will create a new class (EventSource.cs) which inherits from the AFEventSource base class.

 

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

namespace AFSDK_CustomDR_SQL
{
    /*****************************************************************************************
     * The EventSource class implements AFEventSource to specify how to get data pipe events 
     * from the system of record. 
     *****************************************************************************************/
    class EventSource : AFEventSource
     {

 

 

Storing Timestamps

First, we would like to store the start time when the data pipe was initiated, as well as the last timestamp where data was retrieved for each AF Attribute. Since there can be a list of AFAttributes signed up for the data pipe, we will keep a dictionary of <AFAttribute, AFTime> to record the last timestamps for each AF Attribute.

 

// Last timestamps for each AF Attribute
Dictionary<AFAttribute, AFTime> _lastTimes = new Dictionary<AFAttribute, AFTime>();

// Start time when the pipe is initiated
AFTime _startTime;

// Initialize the start time for the event source
public EventSource()
{
    _startTime = new AFTime("*");
}

 

 

The GetEvents method

Next, we will implement the GetEvents method, which is designed to get data pipe events from the system of record. The base class maintains a dictionary of the AFAttributes being monitored by the eventsource. The idea here is to publish AFDataPipeEvents between the last timestamps till current time, for each AF Attribute that is signed up for the data pipe.

 

// Get new events for the pipe from the last timestamps till current time of evaluation
protected override bool GetEvents()
{
    // Set evaluation time to current time
    AFTime evalTime = AFTime.Now;

    // Get the list of AF Attributes signed up on the data pipe
    IEnumerable<AFAttribute> signupList = base.Signups;

    // Get values for each AF Attribute, one at a time
    foreach (AFAttribute att in signupList)
    {
        if (!ReferenceEquals(att, null))
        {
            // Add AF Attribute if it hasn't been added to the _lastTimes dictionary yet
            if (!_lastTimes.ContainsKey(att))
            {
                _lastTimes.Add(att, this._startTime);
            }

            // Set time range to get all values between last timestamps to current evaluation time
            AFTimeRange timeRange = new AFTimeRange(_lastTimes[att], evalTime);

            /* Note: Use RecordedValues if supported. GetValues call return interpolated values at the start and end time,
             * which can be problematic in a data pipe implementation. GetValues is used here for this simple example because
             * the implementation of GetValues in my custom DR does not return interpolated values at the start and end time. */
            AFValues vals = att.GetValues(timeRange, 0, att.DefaultUOM);

            // Store old last time for the AF Attribute
            AFTime lastTime = _lastTimes[att];

            // Publish each value to the data pipe
            foreach (AFValue val in vals)
            {
                // Record latest timestamp
                if (val.Timestamp > lastTime)
                {
                    lastTime = val.Timestamp;
                }
                AFDataPipeEvent ev = new AFDataPipeEvent(AFDataPipeAction.Add, val);
                base.PublishEvent(att, ev);
            }

            // Add a tick to the latest time stamp to prevent the next GetValues call from returning value at the same time
            _lastTimes[att] = lastTime + TimeSpan.FromTicks(1);
        }
    }
    return false;
}

 

 

The Dispose method

Finally, we will override the dispose method to clean up objects.

 

// Dispose resources
protected override void Dispose(bool disposing)
{
    _lastTimes = null;
}

 

 

2. Add the CreateDataPipe method

At the main SQLDR class, we will add a static method CreateDataPipe that returns an AFEventSource object.

 

// Return an AFEventSource object for this custom data reference
public static object CreateDataPipe()
{
    EventSource pipe = new EventSource();
    return pipe;
}

 

 

3. Include Data Pipe in Supported Data Methods

Last but not least, we need to tell clients that data pipe is supported for our custom data reference. We will therefore change the SupportedDataMethods to include DataPipe.

 

public override AFDataMethods SupportedDataMethods
{
     get
    {
        return AFDataMethods.DataPipe;
    }
}

 

 

Testing Data Pipe Functionality

To test, let’s rebuild our solution and re-register the data reference to the AF server. Remember RegPlugIn? We will be using it to first un-register the data reference:

RegPlugIn /PISystem:your-af-server name-of-dll.dll /unregister

 

(You can find the name of the dll by inspecting the output of RegPlugIn /PISystem:your-AF-server /List)

 

Then, we will reregister using the same command as before:

Regplugin /PISystem:your-af-server “C:\path\to\dll”

 

Make sure the SQLDR attribute still looks good in PSE! (Reopen PSE to make sure that the latest data reference version is downloaded.)

 

This time, we will be testing the event pipe functionality in a custom AF SDK application. Before testing, I have created 3 attributes: one using PI Point data reference mapping to the PI Tag CDT158 (cdt158), and two attributes using our custom data reference SQLDR (SQLDRTest and SQLDRTest2). The code for the test is taken from Marco’s blog post: Using data pipes with future data in PI AF SDK 2.7. I will not go into the detailed implementation here; nonetheless, here are the results:

datapipe_results.png

 

Notice our SQLData Reference gets AFDataPipe updates, just as a PI Point data reference! (You might be wondering why the values of the custom data reference is identical to CDT158. The truth is, I am using the PI Interface for RDBMS to write the value of CDT158 to a SQL table, then using the custom data reference to read from the same SQL table. So the fact that we see the same value updates for CDT158 shows that my data pipe is working!)

 

 

Limitations in this Example

Note that in the above example, we are looping through each AFAttribute and making a single RecordedValues call. If you have implemented RDA methods and your data source supports bulk calls, you can do a bulk call on AFAttributeList in the GetEvents method. The downside is that you will be using the same time range for your bulk call, which may not apply for your data reference. If you want to keep track of the list of AFAttributes signed up for the data pipe, you can implement AddSignUp and RemoveSignUp in AFEventSource as well.

 

 

Things to Note

When implementing data pipes, be careful not to retrieve the same event twice. You will notice that we are adding a tick to lastTime in order to get around this issue. In addition, we are using SQLDbType.DateTime2 (instead of DateTime) to add higher precision when doing value retrieval.

 

 

Debugging

If you need to debug your custom data reference at any time during development, check out the following resources:

(Note that the second reference is written specifically for debugging custom delivery channel, but the general principals can be applied to debugging custom data references.)

 

 

Conclusion

I hope this blog post is useful to you. The full VS 2013 solution (including the test project) is in the GitHub repository afsdk-customdr-sql. Please feel free to give me any feedbacks and comments!

PI ProcessBook 2014 introduces a new AF Display Builder add-in that provides visualization to the AF hierarchy (see KB01122 for more information). The AF Display Builder allows users to easily explore PI AF elements and attributes as well as create symbols in PI ProcessBook. Recently, I was assisting a customer in building a PI ProcessBook docking window that displays the AF hierarchy. When users double-click on any element, the .pdi file associated with the element will be opened. I figure I will share this application here in case someone is looking to build a docking window for other purposes.

 

Part I of the blog post will show how to add some simple AF UI controls to a docking window in PI ProcessBook, while Part II will show how to modify the controls to open a .pdi file when an element is double-clicked.

 

 

Part I: Building a PI ProcessBook Docking Window with some AF UI Controls

 

Using the PI ProcessBook Add-In Templates

 

We provide VB.NET and C# templates for creating PI ProcessBook 3.x add-ins. These templates are available for download in the Tech Support Download Center. Since we are developing a docking window in C#, after downloading the templates, unzip and place the AddInPBDockWin_CS file in %USERPROFILE%\Documents\Visual Studio 2013\Templates\ProjectTemplates\Visual C# folder.

 

In Visual Studio 2013, create a new project and select the AddInPBDockWin_CS template:

addtemplate.png

 

Investigating the Docking Window Add-In Template

 

Let’s briefly investigate the docking window add-in project that we have just created from the template. For detailed information, please refer to the document “PI ProcesBook AddIn Templates for VS2008.doc” that comes with the template download.

 

Connect.cs

The Connect class contains IDTExtensibility2 methods that all PI ProcessBook add-ins must implement. The two main methods that can be changed are:

  1. OnConnection
    • Specifies the load behavior when PI ProcessBook initializes the add-in.
  2. OnDisconnection
    • Specifies any cleanup needed when PI ProcessBook terminates the add-in.

 

AddinReg.reg

This file contains the registry keys that PI ProcessBook uses to locate and initialize add-ins. While we are here, let’s edit the “FriendlyName” and “Description” field. Also, make sure that the registry path is correct (see the "extra" section below for more information).

addinreg.png

 

Adding References to PI AF SDK and AF UI

 

Let’s add some references so we can access objects from the AF Database. In addition, we will make use of some ready-made controls available from the AF UI so we don’t have to reinvent the wheel.

  • AF SDK (from %pihome%\AF\PublicAssemblies\4.0\OSIsoft.AFSDK.dll)
  • AF UI (from %pihome%\AF\PublicAssemblies\4.0\OSIsoft.AF.UI.dll)

Since we are adding the .NET 4 version of the PI AF SDK and AF UI to the project, please ensure that the project is targeting .NET framework 4.5.

 

 

Adding the AF UI to a .NET User Control

 

We will next add a .NET user control and put some AF UI components in it. (Thanks to Steve Pilon’s post which gave many useful guidelines!)

 

1.     Right-click the PB_DockWin_AFTreeView project and add a user control. We’ll name it AFTreeControl.cs:

     usercontrol.png

 

2.     In the design mode, resize the user control. We will then add 3 AF UI controls:

    • PISystemPicker: to allow user select and connect to an AF server
    • AFDatabasePicker: to allow user select and connect to a database in the AF server selected
    • AFTreeView: to display the AF element hierarchy

 

     For more information about AF UI components, refer to the AF User Interface Library Reference located at %pihome%\help\AFUIRef.chm.

 

     Modify the PISystem picker and AFDatabasePicker to remove unneeded controls. E.g.:

     afPickerProp.png

 

     The resulting user control will look something like this:

     usercontrol2.png

 

3.     Open the code behind the user control and make the user control visible to COM:

 

using System.Runtime.InteropServices;

 

namespace PB_DockWin_AFTreeView
{
    [ComVisible(true)]
    public partial class AFTreeControl : UserControl
    {

 

 

Adding functionality to the AF UI Controls

 

Let’s add code to our user control (AFTreeContol.cs) to allow us to populate the AFTreeView with the element hierarchy when an AF Server and AF Database is selected.

 

To begin, add using directives to the referenced AF SDK and AF UI:

 

using OSIsoft.AF;
using OSIsoft.AF.UI;

 

We would like the AFDatabasePicker (named afDbPicker) to populate the list of databases whenever an AF server is selected from the PISystemPicker (named afPicker) control.

 

// Initialize the AF UI Controls
public AFTreeControl()
{
    InitializeComponent();
    afDbPicker.SystemPicker = afPicker;
}

 

Next, we need populate the AFTreeView when users select/change the AF Database using AFDatabasePicker. We will add code to react to the SelectionChange event:

  • Click on the AFDatabasePickerControl and select Events under the Properties Window.
  • Double-click on SelectionChange:

     eventchange.png

 

Visual Studio will automatically populates code needed to handle this event. Back in the User Controls code, we see that afDbPicker_SectionChange has been added. Add the following code to get the elements to display in the AFTreeView (named afTreeView):

 

// Change the elements in the TreeView to the selected AF system and AF Database
private void afDbPicker_SelectionChange(object sender, SelectionChangeEventArgs e)
{
    afTreeView.AFRoot = null;
    AFDatabase myDatabase = afDbPicker.AFDatabase;
    if (myDatabase != null && myDatabase.Elements != null)
        afTreeView.AFRoot = myDatabase.Elements;
}

 

Now, our control should display the AF element hierarchy after an AF server and database has been selected.

 

 

Displaying the User Control in PI ProcessBook

 

The last step would be to ask the docking window add-in to load our user control.

 

Let’s clean up the default label added as an example in the docking window template:

  • Remove the label control and the associated variable m_strAddInName
  • Change the name of the docking window to “AFTreeView Docking Window”
  • Add our user control (AFTreeControl) to the view in the docking window

 

The edited OnConnection method looks like the following:

 

public void OnConnection(object Application, Extensibility.ext_ConnectMode ConnectMode, object AddInInst, ref System.Array custom)
{
    try
    {
        m_theApp = (PBObjLib.Application)Application;

        #region docking window

        m_dockWindow = m_theApp.DockWindows.Add(PBObjLib.pbDockWindowType.pbTypeSingleViewWindow, "AFTreeView Docking Window", PBObjLib.pbDockPosition.pbDockLeft, false);

        if (m_dockWindow != null)
        {
            m_dockWindow.Visible = true;

            //ensure the tree view is fully visible
            m_dockWindow.Width = 250;

            //create a view in the docking window to contain the control
            PBObjLib.PBControlView ctrlView = (PBObjLib.PBControlView)m_dockWindow.Views.Add(PBObjLib.pbViewType.pbControlViewType);

            AFTreeControl treeControl = (AFTreeControl)ctrlView.CreateControl("PB_DockWin_AFTreeView.AFTreeControl");
        }

        #endregion

    }
    catch (Exception ex)
    {
        MessageBox.Show("Exception in OnConnection=" + ex.Message);
    }
}

 

 

Finally, we are ready to test! Note that the Debug configuration specify the start action as opening PI ProcessBook. Make sure to change the hard-coded path to reflect the actual location of the PI ProcessBook executable on your machine.

debugprop.png

 

Clicking Debug will register the add-in in the registry (remember AddReg.reg?) and launch PI ProcessBook. The docking window should now be loaded automatically!

dockingwin.png

 

 

Part II: Additional Functionality

 

We have successfully added a AFTreeView in a PI ProcessBook docking window! We are ready to add some custom functionality!

 

As I mentioned in the beginning of this blog post, the goal of this application is to open up a .pdi file when user double-click on any element. To do that, we will parse the path of the element by accessing the node that has been double-clicked. If a .pdi file with the same name already exists, the file will be opened; if not, a new file will be opened.

 

To open up a display at a double-click event, the user control needs to know about the PI ProcessBook application.

 

// The processBook application
public PBObjLib.Application app; 

 

We can then set the ProcessBook Application in the OnConnection method during initialization:

 

AFTreeControl treeControl = (AFTreeControl)ctrlView.CreateControl("PB_DockWin_AFTreeView.AFTreeControl");
treeControl.app = m_theApp;

 

(By the way, this is by no means perfect. Please feel free to comment/improve upon this!)

 

 

Let’s add code to react to a NodeMouseDoubleClick event: (make sure to first create an event handler for the event as described above for the SectionChange event for the AFDatabasePicker)

 

using System.IO;

 

// Open a new display when the node is double-clicked
private void afTreeView_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
{
     // Parse element path to get the name of the element
    string[] path = e.Node.FullPath.Split('\\');
    string filePath = String.Format("C:\\Dev Support\\VBA in PB\\{0}.pdi", path[path.Length - 1]);

     // If the pdi exists, open the file
    if (File.Exists(filePath))
    {
        app.Displays.Open(filePath, false);
    }
     // If pdi doesn't exist, add a dislpay with the name of the element
    else
    {
        app.Displays.Add(path[path.Length - 1]);
    }
}

 

 

Rebuild the project and test the new functionality in PI ProcessBook!

 

 

Extra: A Few Tips about Deployment

 

Note that PI ProcessBook reads the list of add-ins from the registry at

  • HKLM\SOFTWARE\Wow6432Node\PISystem\PI – ProcessBook\Addins, or
  • HKLM\SOFTWARE\Software\PISystem\PI – ProcessBook\Addins

 

depending on the bitness. It is important to modify the AddinReg.reg file so that the registry keys are added in the correct location.

 

To modify the load/unload behavior, you can open up Add-In Manager in PI ProcessBook (Tools > Add-In Manager) and modify the default loading properties for your add-in:

addinmanager.png

 

Finally, if you have finished development and would like to deploy your add-in on a client machine, please refer to KB00592.

 

 

Conclusion

 

The full project is in the GitHub repository pb-dockwin-aftreeview. Feedback and comments are always welcomed!

When Asset-Based Analytics was first released with PI Asset Framework 2014 last year, Marcos wrote an excellent blog post about how to programmatically interact with expression analyses. Over the course of the year, we have seen some interests in accessing, editing or creating analyses programmatically.

 

Before we start, I would like to stress that the creation and maintenance of AF analyses is best accomplished by using the Analyses Management plug-in in PI System Explorer. You can find detailed information in our user guides for Asset-Based Analytics. In addition, we strongly encourage the use of analysis templates, which take advantage of the AF hierarchy and is a much more efficient way to create analyses. Every element derived from an element template automatically acquires analyses from its analysis templates.

 

In some scenario, you might want to make bulk edits to attribute names, time rules, or exporting analysis configuration into another application. In this blog post, I will add a few code snippets on:

  • Accessing and creating rollup analyses
  • Changing natural and periodic time rules
  • Viewing, editing and creating event frame generation analyses

 

The following is tested with PI AF SDK 2.6.2.

 

 

Rollup Analyses

 

Unlike expression analyses which store the equation/expression in the config string of the analysis rule:

 

   Figure 1: Expression analysis

 

AnalysisRule.ConfigString gives Average := (‘Temperature’ + ‘Temperature2’)/2

 

Rollup analyses store the configurations in variable mappings:

     Figure 2: Rollup analysis

 

AnalysisRule.VariableMapping gives SearchCriteria||Elements[@Name=*]|Attributes[@Name=Level];Sum||Level_Rollup;

 

In fact, the ConfigString is configuration specific to the AnalysisRule, while the VariableMapping is PI AF SDK infrastructure that is common to all AnalysisRules. The reason Rollup doesn't have any config string is that the analysis it performs is fixed, it's just a matter of "wiring up" the inputs/outputs to attributes.

 

 

To programmatically create a roll-up analysis (one that is identical to Figure 2), we can create a new analysis with the analysis rule plug in type of "RollUp" and set the variable mapping according to the format observed above. E.g.

 

//Add rollup analysis with name "Rollup_Calc"
AFAnalysis rollupAnalysis = tanks.Analyses.Add("Rollup_Calc");

// Define analysis rule plug-in
rollupAnalysis.AnalysisRulePlugIn = myAF.AnalysisRulePlugIns["Rollup"];

// Define the variable mappings
AFAttribute rollupLevel = tanks.Attributes["Level_Rollup"];
string rollupSearch = "Elements[@Name=*]|Attributes[@Name=Level]";
rollupAnalysis.AnalysisRule.MapVariable("SearchCriteria",rollupSearch);
rollupAnalysis.AnalysisRule.MapVariable("Sum", rollupLevel);

/* An alternative way to define the variable mappings using the AFVariableMap object. It provides a richer object model for examining and updating the variable mapping configuration.
AFVariableMap rollupVarMap = rollupAnalysis.AnalysisRule.VariableMap;
AFVariableMappingData rollupSearchMap = new AFVariableMappingData(rollupSearch);
AFVariableMappingData rollupLevelMap = new AFVariableMappingData(rollupLevel);
rollupVarMap.SetMapping("SearchCriteria", rollupSearchMap);
rollupVarMap.SetMapping("Sum", rollupLevelMap);
*/

// Define periodic time rule with frequency of 5 minutes
rollupAnalysis.TimeRulePlugIn = myAF.TimeRulePlugIns["Periodic"];
rollupAnalysis.TimeRule.ConfigString = "Frequency=300";

// Enable and check in newly created analysis
rollupAnalysis.SetStatus(AFStatus.Enabled);
rollupAnalysis.CheckIn();

 

 

Natural and Periodic Time Rules

 

In the above example, we have created a periodic time rule such that the analysis runs every 5 minutes. Let's examine how we can change it to a natural time rule.

 

The structures for periodic and natural time rule differ slightly:

PeriodicNatural
Time rule plug inPeriodicNaturalNatural
Time rule config stringFrequency=300;Offset=10(null)"attribute1";"attribute2"
CommentsFrequency and offset are represented in seconds (offset is optional)Analysis triggered on any inputTriggered on one or more input attribute

 

For the above rollup analysis (Figure 2), if we would like to change from periodic to natural time rule (and trigger on any input):

 

AFAnalysis rollupAnalysis = tanks.Analyses["Rollup_Calc"];
rollupAnalysis.TimeRulePlugIn = myAF.TimeRulePlugIns["Natural"];
rollupAnalysis.SetStatus(AFStatus.Enabled);
rollupAnalysis.CheckIn();

 

 

Event Frame Generation Analyses

 

Before attempting to programmatically construct an Event Frame Generation analysis, let’s first inspect the analysis rule structure by looking at an existing event frame analysis.

   Figure 3: Event Frame Generation analysis

 

To inspect the analysis, the easiest way would be to look at the exported XML file for the element where the analysis rule belongs to:

 

<AFAnalysis>
    <Name>EF_Calc</Name>
    <Status>Enabled</Status>
    <Target>
        <AFElementRef>.</AFElementRef>
    </Target>
    <AFAnalysisRule>
        <AFPlugIn>EventFrame</AFPlugIn>
        <ConfigString>EFTGUID=49b65f5d-17a9-4ddf-8fe2-68d0b55a92a8;EFTNAME=EFTemplate</ConfigString>
        <AFAnalysisRule>
            <AFPlugIn>PerformanceEquation</AFPlugIn>
            <ConfigString>StartTrigger:= 'Temperature' &gt; 200;</ConfigString>
        </AFAnalysisRule>
        <AFAnalysisRule>
            <AFPlugIn>TimeTrue</AFPlugIn>
            <ConfigString>TimeTrueValue=0</ConfigString>
        </AFAnalysisRule>
    </AFAnalysisRule>
    <AFTimeRule>
        <AFPlugIn>Natural</AFPlugIn>
    </AFTimeRule>
</AFAnalysis>

 

 

Alternatively, we can look at the Analysis.AnalysisRule in Visual Studio:

ef3.png

 

From both, we can see the following:

  • The analysis rule plug in type is "EventFrame"
  • The config string for the analysis rule is of the format:
    • EFTGUID=<GUID of event frame template>;EFTNAME=<name of event frame template>
  • The start trigger (end trigger) and time true are stored as child analysis rules within the event frame analysis rule
    • Start trigger:
      • Plug in type: PerformanceEquation
      • Config string format: StartTrigger:= <equation>;
    • Time true:
      • Plug in type: TimeTrue
      • Config string format: TimeTrueValue=<value>

 

With these information, we can create our Event Frame Generation Analysis programmatically. The configuration of this analysis is identical to the one shown in Figure 3.

 

// Obtaining the event frame template
AFElementTemplate efTemplate = myDB.ElementTemplates["EFTemplate"];

// Add event frame generation analysis with the name "EF_Calc"
AFAnalysis efAnalysis = tanks.Analyses.Add("EF_Calc");

// Define analysis rule plug in and build config string
efAnalysis.AnalysisRulePlugIn = myAF.AnalysisRulePlugIns["EventFrame"];
efAnalysis.AnalysisRule.ConfigString = String.Format("EFTGUID={0};EFTNAME={1}", efTemplate.UniqueID, efTemplate.Name);

// Define plug in type and config string for child analysis rules
AFAnalysisRule triggerRule = efAnalysis.AnalysisRule.AnalysisRules.Add(myAF.AnalysisRulePlugIns["PerformanceEquation"]);
triggerRule.ConfigString = "StartTrigger:= 'Temperature' > 200;";
AFAnalysisRule timeTrueRule = efAnalysis.AnalysisRule.AnalysisRules.Add(myAF.AnalysisRulePlugIns["TimeTrue"]);
timeTrueRule.ConfigString = "TimeTrueValue=0";
            
// Define natural time rule to trigger at any input
efAnalysis.TimeRulePlugIn = myAF.TimeRulePlugIns["Natural"];
efAnalysis.SetStatus(AFStatus.Enabled);
efAnalysis.CheckIn();

 

 

Conclusion

 

At the very least, I hope this blog post can give you a glimpse into the configuration of rollup and event frame generation analyses. Feedback and comments are always welcomed!

 

 

EDIT (6/11/2015): Courtesy of David Moler and Mike Zboray, I have edited the blog post to incorporate their comments. Please keep the comments coming!

bshang

Using PI Web API with Python

Posted by bshang Employee Jun 4, 2015

Motivation

 

Traditionally, accessing the PI System from the Python programming language can be challenging. See for example these threads.

Working with Pi Data from Python - Python version of Pi SDK?

AF SDK and Python (not IronPython)

Re: What is the simplest way to write to the snapshot table in python, without providing the timestamp.

Python & PI Code

Accessing the REST service from Nodejs or Python with authentication

Re: Using PISDK in Python: How do I access PISDK constants?

 

The primary Python language implementation, CPython, is based on C. The PI SDK, an older PI Developer Technology, is based on COM, while AF SDK, a widely-used PI Developer Technology, is based on .NET. These differences in platform require interfaces or other wrappers that must be used as a bridge, but they can be cumbersome to use.

 

For example, to use PI SDK within CPython, the pywin32 library can be used to allow Python to communicate with COM classes. For AF SDK, the adodbapi library can be used to access AF via PI OLEDB Enterprise. Additionally, Python for .NET may be an option but this has not been well explored. Regardless, PI SDK will be deprecated and PI OLEDB may not be suitable in some cases. PI ODBC and pyodbc looks interesting though and may be worth exploring more...they almost sound like they were made for each other...

 

An alternative approach is to use a .NET implementation of the Python language, such as IronPython. However, one of the strengths of Python is its wide variety of analytics libraries, such as numpy, scipy, pandas, and scikit-learn. These analytics libraries call into highly optimized C and Fortran numerical routines, but integration of these Python libraries into the .NET runtime via IronPython has been historically challenging.

 

PI Web API

 

The addition of PI Web API to the suite of PI Developer Technologies vastly increased the accessible surface area of PI from various programming platforms. One of the benefits of exposing a RESTful API is that much of the web has adopted this architectural style, and hence, the API communicates with programmers via a common "language" (i.e. HTTP). Programmers really need to just understand the basics of formulating an HTTP request and handing a response, and do not need to download custom SDK's or deeply interact with and understand that SDK's object model. Once the basics of HTTP are understood, then the rest is just implementation in the desired programming language (Of course, it is not "just" so simple. I'm looking at you, OAuth... )

 

In addition, RESTful API documentation all look fairly similar, so once PI Web API is understood, for example, it is easier to pick up API's from Facebook, Google, Esri, etc., enabling apps that can connect to multiple services and provide greater contextual experience for the user.

 

YAWAPP

 

This blog post will present YAWAPP (Yet Another Way to Access PI from Python). But hopefully, it will motivate some compelling use cases. As mentioned, RESTful API's can provide a standardized approach for data integration. Bringing PI System data into Python, particularly CPython, allows this data to be consumed by Python's extensive, optimized numerical libraries. Using PI Web API in Python can also be used to expose PI data in web and mobile applications via Python's popular web application framework, Django.

 

The complete source code examples for this post are hosted in the Github repo:

bzshang/piwebapi-python-examples · GitHub

 

The GitHub repo also contains the technical documentation of the code. This rest of this post will provide a higher level overview of the steps that could be followed to use PI Web API within Python.

 

Disclaimer: I am not a Python developer. I've used Python once 3 years ago and then only recently. But hopefully, I'll demonstrate how easy it is to get started when working with RESTful API's, even with little prior experience.

 

Which Python?

 

There are many distributions of Python language implementations. For example, here is a small list:

 

Here, I will be using the Python distribution from python.org (CPython), namely version 2.7.10 for Windows.

 

Which IDE?

 

There are many IDE's to choose from. As mentioned, I'm not a Python developer and previously, I hacked away with Notepad++... I decided to use a more extensive IDE this time and chose PyCharm (Community Edition). Why? It was the first one that came up in the google search , so you are free to explore your own.

 

I found PyCharm offers many features familiar to Visual Studio users (intellisense, code completion, static analysis, debugging, project file/package management, etc.), but the Community edition did not have the "streamlined" feel of Visual Studio, which can be expected. I will say it does seem to be more enjoyable to program in Python than C#...

 

Which Python packages?

 

I imported three packages into my example project.

 

requests is used as the HTTP client library and seems to be the most popular within the Python community. Amazon, Google, and even POTUS use it. json as the name suggests helps me deserialize JSON text into Python dictionaries and vice versa. bunch is a package that provides a wrapper class around Python dictionaries so I can browse the dictionary using dot notation (e.g. dict.key instead of dict["key"]), evocative of the C# anonymous type.

 

Overview of Examples

 

I assume the reader is somewhat familiar with the PI Web API already and its basic operations. Below are some introductory resources to get started:

KB01128 - Introduction to PI Web API

PI Web API Online Reference

Getting started with PI Web API 

Learn the Basics of PI Programming - Simple PI Web API Calls

 

I show how to browse AF databases, elements, and attributes using PI Web API along with the requests library. I also show how to retrieve the current value of an attribute, write to that attribute, and modify its properties. What is really shown, however, are how to perform the following using requests:

  • How to use different HTTP verbs (GET, POST, PATCH in my examples) using requests
  • How to pass in query string parameters in the URL
  • How to set request headers (i.e. Content-Type: application/json)
  • How to send JSON request payloads (body)
  • How to parse the incoming JSON using json and bunch

 

All of these are well-documented by the requests authors. It could be said that, in my examples, I just substituted their URL strings for ones relevant to PI Web API

 

Authentication

 

Something that is not at all trivial though is authentication via requests. Out of the box, it supports Basic authentication but not Kerberos. If PI Web API can only be authenticated via Kerberos, then the requests-kerberos package must also be installed.

 

Examples on GitHub

 

The GitHub repo bzshang/piwebapi-python-examples contains the examples and more technical details. One of the benefits of GitHub is that now, I can fix my bugs behind the reader's eyes

 

Please post your questions and comments in this PI Square blog post!

Filter Blog

By date: By tag: