PI ACE is a nice and convenient tool to create programmable analytics with tags and the Module Database. However – the PI System goes asset centric, so how can we exploit PI ACE to work with PI AF?


In the following I would like to show you one way – using the PI AF SDK from PI ACE.


First – we want to use the .NET Framework 4 profile, that requires some additional configuration for the PIACEClassLibraryHost executable. You will need to edit/create the PICAEClassLibraryHost.exe.config file located in PIPC\ACE\Scheduler and PIPC\ACE\Scheduler\x86. Here is what you need:


<?xml version="1.0"?>
supportedRuntime version="v4.0" />


Let’s start thinking what we want to do. In my simple example, I am going to check all elements below a defined root in a given hierarchy and count the elements where a specific attributes (Common_Alarm) value is “Alarm”. In my case I am going to have an output tag that will receive the result – I leave it to you to write the result to an AF Attribute if you prefer .




Now it’s time to open visual studio and start with our PI ACE Module. We use the above MDB module as the context and the AlarmCount alias as output:




To use the AF SDK – we have to add the reference to the AF SDK and we need the PI SDK as well to access our configuration module, the references should look like this:




Some objects will stay at the class level. These are the objects that we don’t want to initialize at every calculation:

    Private myPISystem As OSIsoft.AF.PISystem
    Private myAFDatabase As OSIsoft.AF.AFDatabase
    Private myRootElement As OSIsoft.AF.Asset.AFElement

The configuration stored in the MDB should be initialized only once, and we also want to keep the connection to the AF Server here. So let us look at the ModuleDependentInitialization:

    Protected Overrides Sub ModuleDependentInitialization()

        ' this is the configuration module
        Dim myModule As PISDK.PIModule
        myModule = PIACEBIFunctions.GetPIModuleFromPath(Context)

            ' Connecting to the PI System
            Dim ppAFServer As PISDK.PIProperty
            ppAFServer = myModule.PIProperties.Item("AFServer")
            myPISystem = New OSIsoft.AF.PISystems().Item(ppAFServer.Value)

            ' Connecting to the AF database
            Dim ppAFDatabase As PISDK.PIProperty
            ppAFDatabase = myModule.PIProperties.Item("AFDatabase")
            myAFDatabase = myPISystem.Databases.Item(ppAFDatabase.Value)

            ' Starting from the root element
            Dim ppRootElement4Calc As PISDK.PIProperty
            ppRootElement4Calc = myModule.PIProperties.Item("RootElement4Calc")
            myRootElement = myAFDatabase.Elements(ppRootElement4Calc.Value)
        Catch ex As Exception
            PIACEBIFunctions.LogPIACEMessage(OSIsoft.PI.ACE.MessageLevel.mlErrors, ex.ToString(),MyBase.Name)
        End Try
    End Sub


As mentioned in the beginning, I want to count elements that fulfill a certain condition. Here is my counting function:

    Function myCalc(ByVal myRootElement As OSIsoft.AF.Asset.AFElement, ByRef myCount As Integer) As Integer
        ' The Alarm
        Dim iAlarm As Integer
        iAlarm = 0
        ' Try to find a "Common_Alarm" attribute
            If (myRootElement.Attributes.Item("Common_Alarm").GetValue().Value = "Alarm") Then
                ' Found an alarm
                iAlarm = 1
            End If
            ' Count the number of elements with a Power attribute
            myCount = myCount + 1
        Catch ex As Exception
            ' no "Common_Alarm" attribute, nothing to report
            iAlarm = 0
        End Try
        ' Do the same for every child element and sum up
        For Each myChildElement As OSIsoft.AF.Asset.AFElement In myRootElement.Elements
            iAlarm = iAlarm + myCalc(myChildElement, myCount)
        ' return the Value
        Return iAlarm
    End Function

Please be aware that it will run recursively through the complete hierarchy below the myRootElement – this could be time consuming and you don’t want to run that on a complex hierarchy every second!


So what is left is to call the myCalc function in the ACECalculations. I have included some basic reporting to the message log. This should give you a good indication on how much work your calculation has to do and how long it takes. Finally this should give you some guidance on how often you want to run this :

        ' this is for time measurement only
        Dim lStart, lStop As Long

        ' Calculating the sum of all Alarm conditions
        Dim iAlarm As Integer
        iAlarm = 0
        ' Count the elements with the "Common_Alarm" attribute
        Dim iCount As Integer
        iCount = 0

            ' start of calculation
            lStart = Date.Now.Ticks()
            ' calculate
            iAlarm = myCalc(myRootElement, iCount)
            ' end of calculation
            lStop = Date.Now.Ticks()
            ' report to messagelog
            PIACEBIFunctions.LogPIACEMessage(OSIsoft.PI.ACE.MessageLevel.mlUserMessage, "Alarms:" & iAlarm & "/Count:" & iCount & "/Time:" & ((lStop - lStart) / 1000000.0).ToString("#,###.000"), MyBase.Name)
        Catch ex As Exception
            ' report to messagelog
            PIACEBIFunctions.LogPIACEMessage(OSIsoft.PI.ACE.MessageLevel.mlErrors, ex.ToString(),MyBase.Name)
        End Try

        If (iCount = 0) Then
            ' something wrong, or nothing to report :-(
            SendDataToPI = False
            ' set the value to the output tag
            AlarmCount.Value = iAlarm
        End If