//******************************************************************************************************
// PIRTInputAdapter.cs - Gbtc
//
// Copyright © 2010, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://www.opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 08/19/2012 - Ryan McCoy
// Generated original version of source code.
// 06/18/2014 - J. Ritchie Carroll
// Updated code to use PIConnection instance.
// 12/17/2014 - J. Ritchie Carroll
// Updated to use AF-SDK
//
//******************************************************************************************************
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using GSF;
using GSF.Collections;
using GSF.Diagnostics;
using GSF.Threading;
using GSF.TimeSeries;
using GSF.TimeSeries.Adapters;
using OSIsoft.AF;
using OSIsoft.AF.Asset;
using OSIsoft.AF.Data;
using OSIsoft.AF.PI;
namespace PIAdapters
{
///
/// Uses PI event pipes to deliver real-time PI data to GSF host
///
[Description("OSI-PI: Reads real-time measurements from an OSI-PI server using AF-SDK.")]
public class PIRTInputAdapter : InputAdapterBase
{
#region [ Members ]
// Nested Types
private class DataUpdateObserver : IObserver
{
public event EventHandler> DataUpdated;
public event EventHandler Completed;
private readonly Action m_exceptionHandler;
public DataUpdateObserver(Action exceptionHandler)
{
m_exceptionHandler = exceptionHandler;
}
public void OnCompleted() => Completed?.Invoke(this, EventArgs.Empty);
public void OnError(Exception error) => m_exceptionHandler?.Invoke(error);
public void OnNext(AFDataPipeEvent value)
{
if (value.Action != AFDataPipeAction.Delete)
DataUpdated?.Invoke(this, new EventArgs(value.Value));
}
}
// Fields
private readonly ConcurrentDictionary m_tagKeyMap; // Map PI tag ID to GSFSchema measurement keys
private readonly ShortSynchronizedOperation m_restartConnection; // Restart connection operation
private readonly ShortSynchronizedOperation m_readEvents; // Reads PI events
private readonly System.Timers.Timer m_eventTimer; // Timer to trigger event reader
private PIConnection m_connection; // PI server connection
private PIDataPipe m_dataPipe; // PI data pipe
private List m_dataPoints; // Last list of subscribed data points
private readonly DataUpdateObserver m_dataUpdateObserver; // Custom observer class for handling point updates
private Ticks m_lastReceivedTimestamp; // Last received timestamp from PI event pipe
private double m_lastReceivedValue;
private bool m_disposed;
#endregion
#region [ Constructors ]
///
/// Receives real-time updates from PI
///
public PIRTInputAdapter()
{
m_tagKeyMap = new ConcurrentDictionary();
m_restartConnection = new ShortSynchronizedOperation(Start);
m_readEvents = new ShortSynchronizedOperation(ReadEvents);
m_dataUpdateObserver = new DataUpdateObserver(ex => OnProcessException(MessageLevel.Error, ex));
m_dataUpdateObserver.DataUpdated += m_dataUpdateObserver_DataUpdated;
m_eventTimer = new System.Timers.Timer(1000.0D);
m_eventTimer.Elapsed += m_eventTimer_Elapsed;
m_eventTimer.AutoReset = true;
m_eventTimer.Enabled = false;
}
#endregion
#region [ Properties ]
///
/// Returns false to indicate that this does NOT connect asynchronously
///
protected override bool UseAsyncConnect => false;
///
/// Returns false to indicate that this does NOT support temporal processing.
/// Temporal processing is supported in a separate adapter that is not driven by event pipes.
///
public override bool SupportsTemporalProcessing => false;
///
/// Gets or sets the measurements that this has been requested to provide
///
public override MeasurementKey[] RequestedOutputMeasurementKeys
{
get => base.RequestedOutputMeasurementKeys;
set
{
base.RequestedOutputMeasurementKeys = value;
if (value is not null && value.Any())
SubscribeToPointUpdates(value);
}
}
///
/// Gets or sets the name of the PI server for the adapter's PI connection.
///
[ConnectionStringParameter, Description("Defines the name of the PI server for the adapter's PI connection.")]
public string ServerName { get; set; }
///
/// Gets or sets the name of the PI user ID for the adapter's PI connection.
///
[ConnectionStringParameter, Description("Defines the name of the PI user ID for the adapter's PI connection."), DefaultValue("")]
public string UserName { get; set; }
///
/// Gets or sets the password used for the adapter's PI connection.
///
[ConnectionStringParameter, Description("Defines the password used for the adapter's PI connection."), DefaultValue("")]
public string Password { get; set; }
///
/// Gets or sets the timeout interval (in milliseconds) for the adapter's connection
///
[ConnectionStringParameter, Description("Defines the timeout interval (in milliseconds) for the adapter's connection"), DefaultValue(PIConnection.DefaultConnectTimeout)]
public int ConnectTimeout { get; set; }
///
/// Last timestamp received from PI
///
public DateTime LastReceivedTimestamp => m_lastReceivedTimestamp;
///
/// Returns the status of the adapter
///
public override string Status
{
get
{
StringBuilder status = new();
status.Append(base.Status);
status.AppendFormat(" Last Received Timestamp: {0}", m_lastReceivedTimestamp.ToString("MM/dd/yyyy HH:mm:ss.fff"));
status.AppendLine();
status.AppendFormat(" Last Received Value: {0:N3}", m_lastReceivedValue);
status.AppendLine();
status.AppendFormat(" Latency of Last Received: {0}", (DateTime.UtcNow.Ticks - m_lastReceivedTimestamp).ToElapsedTimeString(3));
status.AppendLine();
if (m_dataPoints is not null)
{
status.AppendFormat(" PI Data Point Count: {0:N0}: {1}...", m_dataPoints.Count, m_dataPoints.Select(p => p.ID.ToString()).Take(5).ToDelimitedString(", "));
status.AppendLine();
}
return status.ToString();
}
}
#endregion
#region [ Methods ]
///
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected override void Dispose(bool disposing)
{
if (m_disposed)
return;
try
{
if (!disposing)
return;
if (m_connection is not null)
{
m_connection.Disconnected -= m_connection_Disconnected;
m_connection.Dispose();
m_connection = null;
}
m_dataPipe?.Dispose();
if (m_eventTimer is not null)
{
m_eventTimer.Elapsed -= m_eventTimer_Elapsed;
m_eventTimer.Dispose();
}
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
base.Dispose(disposing); // Call base class Dispose().
}
}
///
/// Reads values from the connection string and prepares this for connecting to PI
///
public override void Initialize()
{
base.Initialize();
//m_measurements = new List();
Dictionary settings = Settings;
if (!settings.TryGetValue(nameof(ServerName), out string setting))
throw new InvalidOperationException("Server name is a required setting for PI connections. Please add a server in the format servername=myservername to the connection string.");
ServerName = setting;
if (settings.TryGetValue(nameof(UserName), out setting))
UserName = setting;
else
UserName = null;
if (settings.TryGetValue(nameof(Password), out setting))
Password = setting;
else
Password = null;
if (settings.TryGetValue(nameof(ConnectTimeout), out setting) && int.TryParse(setting, out int value))
ConnectTimeout = value;
else
ConnectTimeout = PIConnection.DefaultConnectTimeout;
}
///
/// Connects to the configured PI server.
///
protected override void AttemptConnection()
{
m_connection = new PIConnection
{
ServerName = ServerName,
UserName = UserName,
Password = Password,
ConnectTimeout = ConnectTimeout
};
m_connection.Disconnected += m_connection_Disconnected;
m_connection.Open();
m_dataPipe = new PIDataPipe(AFDataPipeType.Snapshot);
m_dataPipe.Subscribe(m_dataUpdateObserver);
if (AutoStart && OutputMeasurements is not null && OutputMeasurements.Any())
SubscribeToPointUpdates(this.OutputMeasurementKeys());
}
///
/// Disconnects from the configured PI server if a connection is open
///
protected override void AttemptDisconnection()
{
m_eventTimer.Enabled = false;
if (m_dataPoints is not null)
{
m_dataPoints.Clear();
m_dataPoints = null;
}
if (m_dataPipe is not null)
{
m_dataPipe.Dispose();
m_dataPipe = null;
}
if (m_connection is not null)
{
m_connection.Dispose();
m_connection = null;
}
}
///
/// Returns brief status with measurements processed
///
///
///
public override string GetShortStatus(int maxLength) => $"Received {ProcessedMeasurements} measurements from PI...".CenterText(maxLength);
private void SubscribeToPointUpdates(MeasurementKey[] keys)
{
OnStatusMessage(MessageLevel.Info, $"Subscribing to updates for {keys.Length} measurements...");
var query = from row in DataSource.Tables["ActiveMeasurements"].AsEnumerable()
from key in keys
where row["ID"].ToString() == key.ToString()
select new
{
Key = key,
AlternateTag = row["AlternateTag"].ToString(),
PointTag = row["PointTag"].ToString()
};
List dataPoints = new();
foreach (var row in query)
{
string tagName = row.PointTag;
if (!string.IsNullOrWhiteSpace(row.AlternateTag))
tagName = row.AlternateTag;
#if DEBUG
OnStatusMessage(MessageLevel.Debug, $"DEBUG: Looking up point tag '{tagName}'...");
#endif
PIPoint point = GetPIPoint(m_connection.Server, tagName);
if (point is not null)
{
#if DEBUG
OnStatusMessage(MessageLevel.Debug, $"DEBUG: Found point tag '{tagName}'...");
#endif
dataPoints.Add(point);
m_tagKeyMap[point.ID] = row.Key;
}
#if DEBUG
else
{
OnStatusMessage(MessageLevel.Debug, $"DEBUG: Failed to find point tag '{tagName}'...");
}
#endif
}
// Remove sign-ups for any existing point list
if (m_dataPoints is not null)
m_dataPipe.RemoveSignups(m_dataPoints);
// Sign up for updates on selected points
AFListResults initialEvents = m_dataPipe.AddSignupsWithInitEvents(dataPoints);
#if DEBUG
OnStatusMessage(MessageLevel.Debug, $"DEBUG: Initial event count = {initialEvents.Results.Count}...");
#endif
foreach (AFDataPipeEvent item in initialEvents.Results)
{
#if DEBUG
OnStatusMessage(MessageLevel.Debug, "DEBUG: Found initial event for action...");
#endif
if (item.Action != AFDataPipeAction.Delete)
m_dataUpdateObserver_DataUpdated(this, new EventArgs(item.Value));
}
m_dataPoints = dataPoints;
m_eventTimer.Enabled = true;
}
private PIPoint GetPIPoint(PIServer server, string tagName)
{
PIPoint.TryFindPIPoint(server, tagName, out PIPoint point);
return point;
}
private void m_eventTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
#if DEBUG
OnStatusMessage(MessageLevel.Debug, "DEBUG: Timer elapsed...");
#endif
m_readEvents.TryRunOnceAsync();
}
private void ReadEvents()
{
if (m_dataPipe is null)
return;
#if DEBUG
OnStatusMessage(MessageLevel.Debug, "DEBUG: Data pipe called for next 100 GetUpdateEvents...");
#endif
AFListResults updateEvents = m_dataPipe.GetUpdateEvents(100);
#if DEBUG
OnStatusMessage(MessageLevel.Debug, $"DEBUG: Update event count = {updateEvents.Count}...");
#endif
foreach (AFDataPipeEvent item in updateEvents.Results)
{
#if DEBUG
OnStatusMessage(MessageLevel.Debug, "DEBUG: Found update event for action...");
#endif
if (item.Action != AFDataPipeAction.Delete)
m_dataUpdateObserver_DataUpdated(this, new EventArgs(item.Value));
}
}
// PI data updated handler
private void m_dataUpdateObserver_DataUpdated(object sender, EventArgs e)
{
#if DEBUG
OnStatusMessage(MessageLevel.Debug, $"DEBUG: Data observer event handler called with a new value: {Convert.ToDouble(e.Argument.Value):N3}...");
#endif
AFValue value = e.Argument;
#if DEBUG
OnStatusMessage(MessageLevel.Debug, $"DEBUG: Data observer event handler looking up point ID {value?.PIPoint.ID:N0} in table...");
#endif
if (value is not null && m_tagKeyMap.TryGetValue(value.PIPoint.ID, out MeasurementKey key))
{
#if DEBUG
OnStatusMessage(MessageLevel.Debug, $"DEBUG: Data observer event handler found point ID {value.PIPoint.ID:N0} in table: {key}...");
#endif
Measurement measurement = new();
measurement.Metadata = key.Metadata;
measurement.Value = Convert.ToDouble(value.Value);
measurement.Timestamp = value.Timestamp.UtcTime;
OnNewMeasurements(new[] { measurement });
m_lastReceivedTimestamp = measurement.Timestamp;
m_lastReceivedValue = measurement.Value;
}
#if DEBUG
else
{
OnStatusMessage(MessageLevel.Debug, $"DEBUG: Data observer event handler did not find point ID {value.PIPoint.ID:N0} in table...");
}
#endif
}
// Since we may get a plethora of these requests, we use a synchronized operation to restart once
private void m_connection_Disconnected(object sender, EventArgs e) =>
m_restartConnection.RunOnceAsync();
#endregion
}
}