[NOTE: one product release after this blog was original published, AF offered a cleaner, more direct way. See this link for a short, easy example. - Rick]
Welcome to Part 1 of a 3-part series on creating a Duration attribute on an event frame.
Part 1 - How To using StringBuilder DR for Start and End Time attributes
Part 2 - Performance Testing, Methodology, and Results
Part 3 - Variations on a Theme
One of the earliest posts by my former boss Randy Esposito was way back in 2011 when Event Frames were about to be released to production:
Event Frame "Duration" attribute
The answer given by Chris Manhard has become the de facto standard (and why wouldn't it be?). Not that I am trying to reject a standard, but I do want to re-introduce an alternative way to produce a Duration attribute.
In order to provide some context to what was available in May 2011, let's step in the Wayback Machine. There was no PISquare but there was vCampus. The current production release of AF was 2.3 released in December 2010. Event Frames were in beta testing for release of AF 2.4 in December of 2011. The StringBuilder DR was many years away from being released with AF 2.6 in April 2014. And though the StringConcat DR was available, it was an unsupported product and therefore was not considered as part of a solution for production code. So at that time, Chris's answer wasn't just the best choice, it was practically the only viable choice short of writing your own custom DR.
If you read all the replies in Randy's thread, you'll see there was some issues with point permissions. But to get the duration in the "standard" way does require a call to the PI Server. The Sinusoid tag may be coarsely archived, which might affect the precision of the duration, and if you use one of your own equipment tags it could be very dense which could affect the call. And it still bugs me that I have to make a call.
How about skipping the call to the PI Server and getting the best precision answer at the same time? All it will cost you is 2 tiny attributes. To do this requires use of the StringBuilder DR, which we will use to generate a time string, which is then converted to a DateTime value. This was interesting news to me: that you can use StringBuilder to output values to non-string attributes.
For my new attributes, I call them "Start Time" and "End Time". I like seeing the blank before "Time" plus it reiterates to me that I am referencing the attributes and not the properties named StartTime and EndTime. You are free to choose the names of your own liking. You may consider using "Event End" and "Event Start" to keep them grouped together, in which case I would also use "Event Duration". Use whatever makes sense for you.
First let's define the Start Time and End Time attributes:
It was interesting to see that the string output could be converted to a DateTime. Anytime you store a DateTime you should always use UTC. That's okay but PSE like many of the OSIsoft client tools will display the times in LocalTime. In short, we store the UTC time but what you see is Local.
If you'd like to copy-and-paste directly, without typing what appears in an image, that would be:
Start Time: "%UtcStartTime:O%";
End Time: "%UtcEndTime:O%";
Of interesting note is the ":O" format specifier at the end. This enables the result to have sub-second precision, is preferred for round trips, and works in any culture since it uses the ISO 8601 format. To see more regarding sub-second precision, see my earlier blog A Detailed Exploration of AFTime Precision. Using the ":O" specifier will generate a string looking something like "2016-08-10T22:45:00.000Z", which is converted to a DateTime value using DateTime.Parse("2016-08-10T22:45:00.000Z").
Tip : StringBuilder's config editor can sometimes be finicky and may want to switch the Value Type to be String . You will need to watch out for this and make sure that the Value Type is DateTime anytime you edit the attribute.
This makes the Duration amazingly easy:
You are free to choose a Default UOM to your application needs as long as it belongs to the Time class. In my example above, I want the duration to be in hours. But most importantly the formula result must have a UOM=s -- so do not change the formula because the difference of 2 times will always be measured in seconds. Another thing to keep in mind is the Duration type is not a .NET TimeSpan object. Rather it is a scalar Double, although Single would work as well depending upon your precision. If you are dealing with sub-seconds, I suggest using Double.
Let's look at a Closed event frame, that is one that has an EndTime property defined:
Here's what the attributes look like in PSE:
That's clean and easy enough. Note if I open this event frame 5 minutes later or 5 days later, the values will be the same.
Now let's consider an Open event frame, i.e. one without and EndTime property defined.
The reply by Chris back in 2011 still applies: the EndTime property is treated as Now, in which case our End Time attribute is treated as Now(). Unlike a Closed event frame, with an Open event frame the values will differ 5 minutes or 5 days from now because Now() is always changing! The one caveat I would give with regards to working with Open event frames is that you may need to issue a Refresh immediately before doing something with them.
Here's what the attributes looked like just a few minutes apart:
Some may object to having to create attributes that mirror properties even if those attributes are lightweight. I admit I had that initial feeling when I tried doing it this way, but the notion of skipping a call to the PI Server was a major driving factor. If you'd rather not see these time attributes, you can marked them as Hidden if you are using AF 2.7 or later. Once I started using these attributes, I preferred seeing them on the event frame alongside the Duration without having to jump back-and-forth between the General and Attributes tabs.
Like many things in AF, there's more than one way to skin a cat. It shouldn't surprise the reader that there are many ways to achieve the same thing. The big take away to me is that a technique that is in your standard bag-of-tricks one year can fall to the wayside 5 years, 3 years, or even 1 year down the road as new features come out with major releases. That's something to consider in light of AF 2.8 with attribute traits and many new async methods. Granted, it sure is comforting to know that code I wrote years ago is probably safe with new releases. But its also encouraging to know that I can improve my code with a handful of simple changes that utilize new features.
Performance Cost?
I mentioned the new techniques have the tiny cost of 2 more attributes. The begs the question: how is this cost reflected in performance? One would think not making an RPC call should be faster. While that thinking might be based on experience, its still conjecture and not a definitive answer. Next up in this series I will discuss the tests performed. If you can't wait and you'd like a hint: yes, avoiding a call to the Data Archive is indeed faster.
Other related links
Event Frame duration - easy way in AF2.6 or higher
Here Roger Palmen introduces the same concept in 2014. While my blog has more a more detailed walked through and explanation, Roger's post is an Idea which has a voting button. This blog goes a tiny bit further by (1) using the ":O" format specifier to internally produce a culture-neutral ISO 8601 time string and (2) properly assigns the UOM=s inside the duration's formula.
Can you get event frame duration as attribute for calculations in AF
This recent post by Blake Trahan shows that he's not the first, nor will he be the last, to ask for something like this.
Cheers,
Rick Davin
PS - no cats were harmed in the making of this blog.
Hi Rick,
I think a custom data reference might be an alternative. Here is a snippet that I tested:
public override AFValue GetValue(object context, object timeContext, AFAttributeList inputAttributes, AFValues inputValues)
{
var element = Attribute.Element;
if (element != null) // data reference on element
{
var database = Element.Database;
var frames = AFEventFrame.FindEventFrames(database, null,
new AFTime(DateTime.Now), 0, 1,
AFEventFrameSearchMode.BackwardFromEndTime,
"", element.Name, null, element.Template, true);
if (frames.Count != 1) return new AFValue(-1);
return new AFValue(frames[0].TimeRange.Span.TotalSeconds);
}
if (!(timeContext is AFTimeRange)) return new AFValue(-1);
// data reference on event frame
var afTimeRange = (AFTimeRange)timeContext ;
return new AFValue(afTimeRange.Span.TotalSeconds);
}