//****************************************************************************************************** // FileExporter.cs - Gbtc // // Copyright © 2012, 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: // ---------------------------------------------------------------------------------------------------- // 01/10/2007 - J. Ritchie Carroll // Initial version of source generated. // 12/29/2009 - Jian R. Zuo // Converted code to C# and corrected angle wrapping algorithm. // 01/15/2010 - J. Ritchie Carroll // Abstracted code for general purpose use. // 01/29/2010 - Jian R. Zuo // Add default value to m_exportInterval avoid "Attempted to divide by zero" exception // 02/01/2010 - Jian R. Zuo // Change "return Status.ToString();" to "return status.ToString();" // 04/27/2010 - J. Ritchie Carroll // Performed full code review, optimization and issue fixes for ICCP data export. // 04/26/2011 - J. Ritchie Carroll // Modified code to optimize export quality. // 12/27/2011 - J. Ritchie Carroll // Updated export code to be more resilient. // 12/13/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Linq; using System.Text; using GSF; using GSF.Collections; using GSF.Diagnostics; using GSF.IO; using GSF.TimeSeries; using GSF.TimeSeries.Adapters; using GSF.Units.EE; using PhasorProtocolAdapters; namespace ICCPExport { /// /// Represents an action adapter that exports measurements on an interval to a file that can be picked up by other systems such as ICCP. /// [Description("File Based ICCP: Exports measurements to a file that can be picked up by other systems")] public class FileExporter : CalculatedMeasurementBase { #region [ Members ] // Nested Types private enum DataQuality { Good = 0, Suspect = 20, Bad = 32 }; // Constants private const double SqrtOf3 = 1.7320508075688772935274463415059D; // Fields private MultipleDestinationExporter m_dataExporter; private ConcurrentDictionary m_measurementTags; private MeasurementKey m_referenceAngleKey; private bool m_useReferenceAngle; private bool m_useNumericQuality; private int m_exportInterval; private Ticks m_lastPublicationTime; private string m_companyTagPrefix; private bool m_statusDisplayed; private long m_skippedExports; #endregion #region [ Properties ] /// /// Gets or sets the interval, in seconds, at which data will be queued for concentration and then exported. /// [ConnectionStringParameter, Description("Define the interval, in seconds, at which data will be queued for concentration and then exported.")] public double ExportInterval { get { return m_exportInterval / 1000.0D; } set { m_exportInterval = (int)(value * 1000.0D); } } /// /// Gets or sets a value that determines whether a reference angle is used to adjust the value of phase angles. /// [ConnectionStringParameter, Description("Indicates whether a reference angle is used to adjust the value of phase angles. IMPORTANT: If this is true, ReferenceAngleMeasurement is a required parameter.")] public bool UseReferenceAngle { get { return m_useReferenceAngle; } set { m_useReferenceAngle = value; } } /// /// Gets or sets the key of the measurement used to adjust the value of phase angles. /// [ConnectionStringParameter, Description("Define the key of the measurement used to adjust the value of phase angles."), DefaultValue(""), CustomConfigurationEditor("GSF.TimeSeries.UI.WPF.dll", "GSF.TimeSeries.UI.Editors.MeasurementEditor", "filterExpression={SignalAcronym = 'IPHA' OR SignalAcronym = 'VPHA'}; selectable=false")] public string ReferenceAngleMeasurement { get { return m_referenceAngleKey.ToString(); } set { m_referenceAngleKey = MeasurementKey.Parse(value); } } /// /// Gets or sets the prefix attached to the beginning of the measurements' point tags, excluding the underscore. /// [ConnectionStringParameter, Description("Define the prefix attached to the beginning of the measurements' point tags, excluding the underscore."), DefaultValue(null)] public string CompanyTagPrefix { get { return m_companyTagPrefix; } set { m_companyTagPrefix = value.ToUpper().Trim(); } } /// /// Gets or sets the value that determines whether the measurement quality is represented as a numeric value rather than a symbolic value. /// [ConnectionStringParameter, Description("Indicate whether the measurement quality is represented as a numeric value rather than a symbolic value. QUALITIES: Good = 0; Suspect = 20; Bad = 32"), DefaultValue(false)] public bool UseNumericQuality { get { return m_useNumericQuality; } set { m_useNumericQuality = value; } } /// /// Returns the detailed status of the . /// public override string Status { get { StringBuilder status = new StringBuilder(); status.AppendFormat(" Using numeric quality: {0}", m_useNumericQuality); status.AppendLine(); status.AppendFormat(" Using reference angle: {0}", m_useReferenceAngle); status.AppendLine(); status.AppendFormat(" Skipped exports: {0}", m_skippedExports); status.AppendLine(); if (m_useReferenceAngle) { status.AppendFormat(" Reference angle point: {0}", m_referenceAngleKey); status.AppendLine(); } if (!string.IsNullOrWhiteSpace(m_companyTagPrefix)) { status.AppendFormat(" Company tag prefix: {0}", m_companyTagPrefix); status.AppendLine(); } if (m_dataExporter != null) status.Append(m_dataExporter.Status); status.Append(base.Status); return status.ToString(); } } #endregion #region [ Methods ] private bool m_disposed; /// /// 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) { try { if (disposing) { if (m_dataExporter != null) { m_dataExporter.SaveSettings(); m_dataExporter.StatusMessage -= m_dataExporter_StatusMessage; m_dataExporter.ProcessException -= m_dataExporter_ProcessException; m_dataExporter.Dispose(); } m_dataExporter = null; } } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } } /// /// Initializes . /// public override void Initialize() { base.Initialize(); Dictionary settings = Settings; const string errorMessage = "{0} is missing from Settings - Example: exportInterval=5; useReferenceAngle=True; referenceAngleMeasurement=DEVARCHIVE:6; companyTagPrefix=TVA; useNumericQuality=True; inputMeasurementKeys={{FILTER ActiveMeasurements WHERE Device='SHELBY' AND SignalType='FREQ'}}"; string setting; double seconds; // Load required parameters if (!settings.TryGetValue("exportInterval", out setting) || !double.TryParse(setting, out seconds)) throw new ArgumentException(string.Format(errorMessage, "exportInterval")); m_exportInterval = (int)(seconds * 1000.0D); m_lastPublicationTime = 0; if (m_exportInterval <= 0) throw new ArgumentException("exportInterval should not be 0 - Example: exportInterval=5.5"); if (InputMeasurementKeys == null || InputMeasurementKeys.Length == 0) throw new InvalidOperationException("There are no input measurements defined. You must define \"inputMeasurementKeys\" to define which measurements to export."); if (!settings.TryGetValue("useReferenceAngle", out setting)) throw new ArgumentException(string.Format(errorMessage, "useReferenceAngle")); m_useReferenceAngle = setting.ParseBoolean(); if (m_useReferenceAngle) { // Reference angle measurement has to be defined if using reference angle if (!settings.TryGetValue("referenceAngleMeasurement", out setting)) throw new ArgumentException(string.Format(errorMessage, "referenceAngleMeasurement")); m_referenceAngleKey = MeasurementKey.Parse(setting); // Make sure reference angle is part of input measurement keys collection if (!InputMeasurementKeys.Contains(m_referenceAngleKey)) InputMeasurementKeys = InputMeasurementKeys.Concat(new[] { m_referenceAngleKey }).ToArray(); // Make sure reference angle key is actually an angle measurement SignalType signalType = InputMeasurementKeyTypes[InputMeasurementKeys.IndexOf(key => key == m_referenceAngleKey)]; if (signalType != SignalType.IPHA && signalType != SignalType.VPHA) throw new InvalidOperationException($"Specified reference angle measurement key is a {signalType.GetFormattedName()} signal, not a phase angle."); } // Load optional parameters if (settings.TryGetValue("companyTagPrefix", out setting)) m_companyTagPrefix = setting.ToUpper().Trim(); else m_companyTagPrefix = null; if (settings.TryGetValue("useNumericQuality", out setting)) m_useNumericQuality = setting.ParseBoolean(); else m_useNumericQuality = false; // Suffix company tag prefix with an underscore if defined if (!string.IsNullOrWhiteSpace(m_companyTagPrefix)) m_companyTagPrefix = m_companyTagPrefix.EnsureEnd('_'); // Define a default export location - user can override and add multiple locations in config later... m_dataExporter = new MultipleDestinationExporter(ConfigurationSection, m_exportInterval); m_dataExporter.StatusMessage += m_dataExporter_StatusMessage; m_dataExporter.ProcessException += m_dataExporter_ProcessException; m_dataExporter.Initialize(new[] { new ExportDestination(FilePath.GetAbsolutePath(ConfigurationSection + ".txt"), false) }); // Create new measurement tag name dictionary m_measurementTags = new ConcurrentDictionary(); string pointID = "undefined"; // Lookup point tag name for input measurement in the ActiveMeasurements table foreach (MeasurementKey key in InputMeasurementKeys) { try { // Get measurement key as a string pointID = key.ToString(); // Lookup measurement key in active measurements table DataRow row = DataSource.Tables["ActiveMeasurements"].Select($"ID='{pointID}'")[0]; // Remove invalid symbols that may be in tag name string pointTag = row["PointTag"].ToNonNullString(pointID).Replace('-', '_').Replace(':', '_').ToUpper(); // Prefix point tag with company prefix if defined if (!string.IsNullOrWhiteSpace(m_companyTagPrefix) && !pointTag.StartsWith(m_companyTagPrefix)) pointTag = m_companyTagPrefix + pointTag; m_measurementTags.TryAdd(key, pointTag); } catch (Exception ex) { OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed to lookup point tag for measurement [{pointID}] due to exception: {ex.Message}")); } } // We enable tracking of latest measurements so we can use these values if points are missing - since we are using // latest measurement tracking, we sort all incoming points even though most of them will be thrown out... TrackLatestMeasurements = true; } /// /// Process frame of time-aligned measurements that arrived within the defined lag time. /// /// of measurements that arrived within lag time and are ready for processing. /// Index of within one second of data ranging from zero to frames per second - 1. protected override void PublishFrame(IFrame frame, int index) { Ticks timestamp = frame.Timestamp; // Only publish when the export interval time has passed if ((timestamp - m_lastPublicationTime).ToMilliseconds() > m_exportInterval) { ConcurrentDictionary measurements = frame.Measurements; m_lastPublicationTime = timestamp; if (measurements.Count > 0) { StringBuilder fileData = new StringBuilder(); IMeasurement measurement, referenceAngle; MeasurementKey inputMeasurementKey; SignalType signalType; DataQuality measurementQuality; double measurementValue, referenceAngleValue; string measurementTag; bool displayedWarning = false; // We need to get calculated reference angle value in order to export relative phase angles // If the value is not here, we don't export referenceAngle = null; // Make sure reference made it in this frame... if (m_useReferenceAngle && !measurements.TryGetValue(m_referenceAngleKey, out referenceAngle)) { OnProcessException(MessageLevel.Warning, new InvalidOperationException("Calculated reference angle was not found in this frame, possible reasons: system is initializing, receiving no data or lag time is too small. File creation was skipped.")); } else { // Export all defined input measurements for (int i = 0; i < InputMeasurementKeys.Length; i++) { inputMeasurementKey = InputMeasurementKeys[i]; signalType = InputMeasurementKeyTypes[i]; // Look up measurement's tag name if (m_measurementTags.TryGetValue(inputMeasurementKey, out measurementTag)) { // See if measurement exists in this frame if (measurements.TryGetValue(inputMeasurementKey, out measurement)) { // Get measurement's adjusted value (takes into account any adder and or multipler) measurementValue = measurement.AdjustedValue; // Interpret data quality flags measurementQuality = (measurement.ValueQualityIsGood() ? (measurement.TimestampQualityIsGood() ? DataQuality.Good : DataQuality.Suspect) : DataQuality.Bad); } else { // Didn't find measurement in this frame, try using a recent value measurementValue = LatestMeasurements[inputMeasurementKey]; // Interpret data quality flags - if no recent measurement is available, we mark it as bad measurementQuality = (Double.IsNaN(measurementValue) ? DataQuality.Bad : DataQuality.Good); // We'll export zero instead of NaN for bad data if (measurementQuality == DataQuality.Bad) measurementValue = 0.0D; } // Export tag name field fileData.Append(measurementTag); fileData.Append(","); // Export measurement value making any needed adjustments based on signal type if (signalType == SignalType.VPHA || signalType == SignalType.IPHA) { // This is a phase angle measurement, export the value relative to the reference angle (if available) if (referenceAngle == null) { // No reference angle defined, export raw angle fileData.Append(measurementValue); } else { // Get reference angle's adjusted value (takes into account any adder and or multipler) referenceAngleValue = referenceAngle.AdjustedValue; // Handle relative angle wrapping double dis0 = Math.Abs(measurementValue - referenceAngleValue); double dis1 = Math.Abs(measurementValue - referenceAngleValue + 360); double dis2 = Math.Abs(measurementValue - referenceAngleValue - 360); if ((dis0 < dis1) && (dis0 < dis2)) measurementValue = measurementValue - referenceAngleValue; else if (dis1 < dis2) measurementValue = measurementValue - referenceAngleValue + 360; else measurementValue = measurementValue - referenceAngleValue - 360; fileData.Append(measurementValue); } } else if (signalType == SignalType.VPHM) { // Typical voltages from PMU's are line-to-neutral volts so we convert them to line-to-line kilovolts fileData.Append(measurementValue * SqrtOf3 / 1000.0D); } else { // Export all other types of measurements as their raw value fileData.Append(measurementValue); } // Export interpreted measurement quality fileData.Append(","); if (m_useNumericQuality) fileData.Append((int)measurementQuality); else fileData.Append(measurementQuality); // Terminate line (ICCP file link expects these two terminating commas, weird...) fileData.AppendLine(",,"); } else { // We were unable to find measurement tag for this key - this is unexpected OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed to find measurement tag for measurement {inputMeasurementKey}")); } } } // Queue up measurement export to data exporter - this will only allow one export at a time try { m_dataExporter.ExportData(fileData.ToString()); } catch (Exception ex) { m_skippedExports++; OnStatusMessage(MessageLevel.Warning, $"WARNING: Skipped export due to exception: {ex.Message}"); displayedWarning = true; } // We display export status every other minute if (new DateTime(timestamp).Minute % 2 == 0 && !displayedWarning) { //Make sure message is only displayed once during the minute if (!m_statusDisplayed) { OnStatusMessage(MessageLevel.Info, $"{m_dataExporter.TotalExports} successful file based measurement exports..."); m_statusDisplayed = true; } } else { m_statusDisplayed = false; } } else { // No data was available in the frame, lag time set too tight? OnProcessException(MessageLevel.Warning, new InvalidOperationException("No measurements were available for file based data export, possible reasons: system is initializing , receiving no data or lag time is too small. File creation was skipped.")); } } } private void m_dataExporter_StatusMessage(object sender, EventArgs e) { OnStatusMessage(MessageLevel.Info, e.Argument); } private void m_dataExporter_ProcessException(object sender, EventArgs e) { OnProcessException(MessageLevel.Warning, e.Argument); } #endregion } }