//******************************************************************************************************
// OutputStream.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:
// ----------------------------------------------------------------------------------------------------
// 07/26/2011 - Magdiel Lorenzo
// Generated original version of source code.
// 09/08/2011 - Mehulbhai Thakkar
// Modified code to use SQL queries directly instead from script file resource.
// Added comments to all properties and static methods.
// 09/14/2012 - Aniket Salver
// Added paging and sorting technique.
// 03/08/2017 - J. Ritchie Carroll
// Modified UI code to auto-set assign default auto publish config frame based on selections.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Data;
using GSF.ComponentModel.DataAnnotations;
using GSF.Data;
using GSF.Diagnostics;
using GSF.PhasorProtocols.IEEEC37_118;
using GSF.TimeSeries.UI;
using GSF.TimeSeries.UI.DataModels;
using Measurement = GSF.TimeSeries.UI.DataModels.Measurement;
using C37Concentrator = PhasorProtocolAdapters.IeeeC37_118.Concentrator;
// ReSharper disable ConstantConditionalAccessQualifier
// ReSharper disable AccessToDisposedClosure
// ReSharper disable ConstantConditionalAccessQualifier
namespace GSF.PhasorProtocols.UI.DataModels
{
///
/// Supported Synchrophasor Output Protocols.
///
public enum OutputProtocol
{
///
/// IEEE C37.118-2011 Output.
///
IEEE_C37_118_2011 = 3,
///
/// IEEE C37.118-2005 Output.
///
IEEE_C37_118_2005 = 0,
///
/// BPA PDCstream Output.
///
BPA_PDCSTREAM = 1,
///
/// IEC 61850-90-5 Output.
///
IEC_61850_90_5 = 2
}
///
/// Defines a value converter for the enumeration.
///
public class OutputProtocolValueConverter : IValueConverter
{
/// Converts a value.
/// The value produced by the binding source.
/// The type of the binding target property.
/// The converter parameter to use.
/// The culture to use in the converter.
/// A converted value. If the method returns , the valid null value is used.
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is null)
return null;
if (parameter is null)
return null;
if (parameter is not CollectionViewSource viewSource)
return null;
if (viewSource.Source is not Dictionary typeList)
return null;
string name = value.ToString();
if (Enum.TryParse(name, out OutputProtocol type) && typeList.TryGetValue(type, out string typeName))
return new KeyValuePair(type, typeName);
return null;
}
/// Converts a value.
/// The value that is produced by the binding target.
/// The type to convert to.
/// The converter parameter to use.
/// The culture to use in the converter.
/// A converted value. If the method returns , the valid null value is used.
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is KeyValuePair pair ? (object)pair.Key : null;
}
///
/// Gets default instance of the .
///
public static readonly OutputProtocolValueConverter Default = new();
}
///
/// Represents a record of as defined in the database.
///
public class OutputStream : DataModelBase
{
#region [ Members ]
private Guid m_nodeID;
private int m_ID;
private string m_acronym;
private string m_name;
private OutputProtocol m_type = OutputProtocol.IEEE_C37_118_2005;
private string m_connectionString;
private int m_idCode;
private string m_commandChannel;
private string m_dataChannel;
private bool m_autoPublishConfigFrame;
private bool m_autoStartDataChannel;
private int m_nominalFrequency;
private int m_framesPerSecond;
private double m_lagTime;
private double m_leadTime;
private bool m_useLocalClockAsRealTime;
private bool m_allowSortsByArrival;
private int m_loadOrder;
private bool m_enabled;
private bool m_ignoreBadTimeStamps;
private int m_timeResolution;
private bool m_allowPreemptivePublishing;
private string m_downsamplingMethod;
private string m_dataFormat;
private string m_coordinateFormat;
private int m_currentScalingValue;
private int m_voltageScalingValue;
private int m_analogScalingValue;
private int m_digitalMaskValue;
private bool m_performTimestampReasonabilityCheck;
private bool m_roundToNearestTimestamp;
private string m_mirroringSourceDevice = "";
// ReSharper disable once MemberInitializerValueIgnored
// Assign initial changed state to true before calling base class constructor call to prevent unintended updates during default value initialization
private bool m_autoPublishConfigFrameChangedByUser = true; // Called 1st
#endregion
#region [ Constructors ]
///
/// Creates a new .
///
public OutputStream() : base(false) // Called 2nd
{
// Post base class constructor call, reset changed state value back to false
m_autoPublishConfigFrameChangedByUser = false; // Called 3rd
try
{
// Set default lead/lag time settings to those used by manager screens
object lagTimeValue = IsolatedStorageManager.ReadFromIsolatedStorage("LagTime");
if (lagTimeValue is not null && double.TryParse(lagTimeValue.ToString(), out double lagTime) && lagTime > 0.0D)
m_lagTime = lagTime;
object leadTimeValue = IsolatedStorageManager.ReadFromIsolatedStorage("LeadTime");
if (leadTimeValue is not null && double.TryParse(leadTimeValue.ToString(), out double leadTime) && leadTime > 0.0D)
m_leadTime = leadTime;
}
catch (Exception ex)
{
Logger.SwallowException(ex);
}
}
#endregion
#region [ Properties ]
///
/// Gets or sets function to call when config frame size calculation should be updated.
///
public Action UpdateConfigFrameSize { get; set; }
///
/// Gets or sets 's NodeID.
///
public Guid NodeID
{
get => m_nodeID;
set
{
m_nodeID = value;
OnPropertyChanged("NodeID");
}
}
///
/// Gets or sets 's ID.
///
// Field is populated by database via auto-increment and has no screen interaction, so no validation attributes are applied.
public int ID
{
get => m_ID;
set
{
m_ID = value;
OnPropertyChanged("ID");
}
}
///
/// Gets or sets 's Acronym.
///
[Required(ErrorMessage = "Output stream acronym is a required field, please provide value.")]
[StringLength(200, ErrorMessage = "Output stream acronym cannot exceed 200 characters.")]
[AcronymValidation]
public string Acronym
{
get => m_acronym;
set
{
m_acronym = value;
OnPropertyChanged("Acronym");
}
}
///
/// Gets or sets 's Name.
///
[StringLength(200, ErrorMessage = "Output stream name cannot exceed 200 characters.")]
public string Name
{
get => m_name;
set
{
m_name = value;
OnPropertyChanged("Name");
}
}
///
/// Gets or sets 's output protocol type.
///
[Required(ErrorMessage = "Output stream type is a required, please provide a value.")]
public OutputProtocol Type
{
get => m_type;
set
{
const string ConfigTypeKey = nameof(C37Concentrator.TargetConfigurationType);
m_type = value;
Dictionary settings = ConnectionString?.ParseKeyValuePairs() ?? new Dictionary(StringComparer.OrdinalIgnoreCase);
switch (m_type)
{
case OutputProtocol.IEEE_C37_118_2011:
settings[ConfigTypeKey] = DraftRevision.Std2011.ToString();
break;
case OutputProtocol.IEEE_C37_118_2005:
settings[ConfigTypeKey] = DraftRevision.Std2005.ToString();
break;
default:
settings.Remove(ConfigTypeKey);
break;
}
ConnectionString = settings.JoinKeyValuePairs();
OnPropertyChanged("Type");
UpdateConfigFrameSize?.Invoke();
}
}
///
/// Gets or sets 's ConnectionString.
///
[DefaultValue("addPhaseLabelSuffix=false; replaceWithSpaceChar=_")]
public string ConnectionString
{
get => m_connectionString;
set
{
Dictionary settings = value.ParseKeyValuePairs();
if (settings.TryGetValue(nameof(RoundToNearestTimestamp), out string setting))
{
RoundToNearestTimestamp = setting.ParseBoolean();
settings.Remove(nameof(RoundToNearestTimestamp));
m_connectionString = settings.JoinKeyValuePairs();
}
else
{
m_connectionString = value;
}
OnPropertyChanged("ConnectionString");
}
}
///
/// Gets or sets 's IDCode.
///
[Required(ErrorMessage = "Output stream IDCode is a required field, please provide a value.")]
public int IDCode
{
get => m_idCode;
set
{
m_idCode = value;
OnPropertyChanged("IDCode");
}
}
///
/// Gets or sets 's CommandChannel.
///
public string CommandChannel
{
get => m_commandChannel;
set
{
m_commandChannel = value;
SetDefaultAutoPublishConfigFrameState();
OnPropertyChanged("CommandChannel");
}
}
///
/// Gets or sets 's DataChannel.
///
public string DataChannel
{
get => m_dataChannel;
set
{
m_dataChannel = value;
SetDefaultAutoPublishConfigFrameState();
OnPropertyChanged("DataChannel");
}
}
///
/// Gets or sets 's AutoPublishConfigFrame flag.
///
[Required(ErrorMessage = "Output stream auto publish config frame is a required field, please provide a value.")]
[DefaultValue(false)]
public bool AutoPublishConfigFrame
{
get => m_autoPublishConfigFrame;
set
{
m_autoPublishConfigFrame = value;
m_autoPublishConfigFrameChangedByUser = true;
OnPropertyChanged("AutoPublishConfigFrame");
}
}
///
/// Gets or sets 's AutoStartDataChannel flag.
///
[Required(ErrorMessage = "Output stream auto start data channel is a required field, please provide a value.")]
[DefaultValue(true)]
public bool AutoStartDataChannel
{
get => m_autoStartDataChannel;
set
{
m_autoStartDataChannel = value;
OnPropertyChanged("AutoStartDataChannel");
}
}
///
/// Gets or sets 's Nominal Frequency.
///
[Required(ErrorMessage = "Output stream nominal frequency is a required field, please provide a value.")]
[DefaultValue(60)]
public int NominalFrequency
{
get => m_nominalFrequency;
set
{
m_nominalFrequency = value;
OnPropertyChanged("NominalFrequency");
}
}
///
/// Gets or sets 's Frames Per Second.
///
[Required(ErrorMessage = "Output stream frames per second is a required field, please provide a value.")]
[DefaultValue(30)]
public int FramesPerSecond
{
get => m_framesPerSecond;
set
{
m_framesPerSecond = value;
OnPropertyChanged("FramesPerSecond");
}
}
///
/// Gets or sets 's LagTime.
///
[Required(ErrorMessage = "Output stream lag time is a required field, please provide a value.")]
[DefaultValue(3.0)]
public double LagTime
{
get => m_lagTime;
set
{
m_lagTime = value;
OnPropertyChanged("LagTime");
}
}
///
/// Gets or sets 's LeadTime.
///
[Required(ErrorMessage = "Output stream lead time is a required field, please provide a value.")]
[DefaultValue(1.0)]
public double LeadTime
{
get => m_leadTime;
set
{
m_leadTime = value;
OnPropertyChanged("LeadTime");
}
}
///
/// Gets or sets 's UseLocalClockAsRealTime flag.
///
[Required(ErrorMessage = "Output stream use local clock as real-time is a required field, please provide a value.")]
[DefaultValue(false)]
public bool UseLocalClockAsRealTime
{
get => m_useLocalClockAsRealTime;
set
{
m_useLocalClockAsRealTime = value;
OnPropertyChanged("UseLocalClockAsRealTime");
}
}
///
/// Gets or sets 's AllowSortsByArrival flag.
///
[Required(ErrorMessage = "Output stream allow sorts by arrival is a required field, please provide a value.")]
[DefaultValue(true)]
public bool AllowSortsByArrival
{
get => m_allowSortsByArrival;
set
{
m_allowSortsByArrival = value;
OnPropertyChanged("AllowSortsByArrival");
}
}
///
/// Gets or sets 's LoadOrder.
///
[Required(ErrorMessage = "Output stream load order is a required field, please provide a value.")]
[DefaultValue(0)]
public int LoadOrder
{
get => m_loadOrder;
set
{
m_loadOrder = value;
OnPropertyChanged("LoadOrder");
}
}
///
/// Gets or sets 's Enabled flag.
///
[Required(ErrorMessage = "Output stream enabled is a required field, please provide a value.")]
[DefaultValue(false)]
public bool Enabled
{
get => m_enabled;
set
{
m_enabled = value;
OnPropertyChanged("Enabled");
}
}
///
/// Gets or sets 's IgnoreBadTimeStamps flag.
///
[Required(ErrorMessage = "Output stream ignore bad timestamps is a required field, please provide a value.")]
[DefaultValue(false)]
public bool IgnoreBadTimeStamps
{
get => m_ignoreBadTimeStamps;
set
{
m_ignoreBadTimeStamps = value;
OnPropertyChanged("IgnoreBadTimeStamps");
}
}
///
/// Gets or sets 's TimeResolution.
///
[Required(ErrorMessage = "Output stream time resolution is a required field, please provide a value.")]
[DefaultValue(330000)]
public int TimeResolution
{
get => m_timeResolution;
set
{
m_timeResolution = value;
OnPropertyChanged("TimeResolution");
}
}
///
/// Gets or sets 's AllowPreemptivePublishing flag.
///
[Required(ErrorMessage = "Output stream allow preemptive publishing flag is a required field, please provide a value.")]
[DefaultValue(true)]
public bool AllowPreemptivePublishing
{
get => m_allowPreemptivePublishing;
set
{
m_allowPreemptivePublishing = value;
OnPropertyChanged("AllowPreemptivePublishing");
}
}
///
/// Gets or sets 's DownSamplingMethod.
///
[Required(ErrorMessage = "Output stream down sampling method is a required field, please provide a value.")]
[DefaultValue("LastReceived")]
public string DownSamplingMethod
{
get => m_downsamplingMethod;
set
{
m_downsamplingMethod = value;
OnPropertyChanged("DownSamplingMethod");
}
}
///
/// Gets or sets 's DataFormat.
///
[Required(ErrorMessage = "Output stream data format is a required field, please provide a value.")]
[DefaultValue("FloatingPoint")]
public string DataFormat
{
get => m_dataFormat;
set
{
m_dataFormat = value;
OnPropertyChanged("DataFormat");
}
}
///
/// Gets or sets 's CoordinateFormat.
///
[Required(ErrorMessage = "Output stream coordinate format is a required field, please provide a value.")]
[DefaultValue("Polar")]
public string CoordinateFormat
{
get => m_coordinateFormat;
set
{
m_coordinateFormat = value;
OnPropertyChanged("CoordinateFormat");
}
}
///
/// Gets or sets 's CurrentScalingValue.
///
[Required(ErrorMessage = "Output stream current scaling value is a required field, please provide a value.")]
[DefaultValue(2423)]
public int CurrentScalingValue
{
get => m_currentScalingValue;
set
{
m_currentScalingValue = value;
OnPropertyChanged("CurrentScalingValue");
}
}
///
/// Gets or sets 's VoltageScalingValue.
///
[Required(ErrorMessage = "Output stream voltage scaling value is a required field, please provide a value.")]
[DefaultValue(2725785)]
public int VoltageScalingValue
{
get => m_voltageScalingValue;
set
{
m_voltageScalingValue = value;
OnPropertyChanged("VoltageScalingValue");
}
}
///
/// Gets or sets 's AnalogScalingValue.
///
[Required(ErrorMessage = "Output stream analog scaling value is a required field, please provide a value.")]
[DefaultValue(1373291)]
public int AnalogScalingValue
{
get => m_analogScalingValue;
set
{
m_analogScalingValue = value;
OnPropertyChanged("AnalogScalingValue");
}
}
///
/// Gets or sets 's DigitalMaskValue.
///
[Required(ErrorMessage = "Output stream digital mask value is a required field, please provide a value.")]
[DefaultValue(-65536)]
public int DigitalMaskValue
{
get => m_digitalMaskValue;
set
{
m_digitalMaskValue = value;
OnPropertyChanged("DigitalMaskValue");
}
}
///
/// Gets 's Node name.
///
public string NodeName { get; private set; }
///
/// Gets 's TypeName.
///
public string OutputProtocolName => OutputProtocolNames[Type];
///
/// Gets or sets 's PerformTimestampReasonabilityCheck flag.
///
[Required(ErrorMessage = "Output stream perform timestamp reasonability check is a required field, please provide a value.")]
[DefaultValue(true)]
public bool PerformTimestampReasonabilityCheck
{
get => m_performTimestampReasonabilityCheck;
set
{
m_performTimestampReasonabilityCheck = value;
OnPropertyChanged("PerformTimestampReasonabilityCheck");
}
}
///
/// Gets or sets 's PerformTimestampReasonabilityCheck flag.
///
[Required(ErrorMessage = "Output stream perform timestamp reasonability check is a required field, please provide a value.")]
[DefaultValue(false)]
public bool RoundToNearestTimestamp
{
get => m_roundToNearestTimestamp;
set
{
m_roundToNearestTimestamp = value;
OnPropertyChanged("RoundToNearestTimestamp");
}
}
///
/// Gets or sets CreatedOn.
///
// Field is populated by database via trigger and has no screen interaction, so no validation attributes are applied
public DateTime CreatedOn { get; set; }
///
/// Gets or sets CreatedBy.
///
// Field is populated by database via trigger and has no screen interaction, so no validation attributes are applied
public string CreatedBy { get; set; }
///
/// Gets or sets UpdatedOn.
///
// Field is populated by database via trigger and has no screen interaction, so no validation attributes are applied
public DateTime UpdatedOn { get; set; }
///
/// Gets or sets UpdatedBy.
///
// Field is populated by database via trigger and has no screen interaction, so no validation attributes are applied
public string UpdatedBy { get; set; }
///
/// Gets or sets id of the device used for mirroring output stream.
///
public string MirroringSourceDevice
{
get => m_mirroringSourceDevice;
set
{
if (m_mirroringSourceDevice == value)
return;
if (MessageBox.Show("WARNING: This will replace all existing devices and measurements associated with this output stream. Do you want to continue?", "IEEE C37.118 Mirroring", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
m_mirroringSourceDevice = value;
OnPropertyChanged("MirroringSourceDevice");
}
}
}
#endregion
#region [ Methods ]
private void SetDefaultAutoPublishConfigFrameState()
{
if (m_autoPublishConfigFrameChangedByUser)
return;
m_autoPublishConfigFrame = string.IsNullOrWhiteSpace(m_commandChannel) && !string.IsNullOrWhiteSpace(m_dataChannel);
OnPropertyChanged("AutoPublishConfigFrame");
}
#endregion
#region [ Static ]
// Static Properties
///
/// Gets an output stream type lookup list, maps enum value to its display name.
///
public static ReadOnlyDictionary OutputProtocolNames { get; } = new ReadOnlyDictionary
(
new Dictionary
{
{ OutputProtocol.IEEE_C37_118_2011, "IEEE C37.118-2011" },
{ OutputProtocol.IEEE_C37_118_2005, "IEEE C37.118-2005" },
{ OutputProtocol.BPA_PDCSTREAM, "BPA PDCstream" },
{ OutputProtocol.IEC_61850_90_5, "IEC 61850-90-5" }
}
);
// Static Methods
///
/// Loads information as an style list.
///
/// to connection to database.
/// Boolean flag indicating if only enabled s needed.
/// The field to sort by.
/// ASC or DESC for ascending or descending respectively.
/// Collection of .
public static IList LoadKeys(AdoDataConnection database, bool enabledOnly, string sortMember, string sortDirection)
{
bool createdConnection = false;
try
{
createdConnection = CreateConnection(ref database);
IList outputStreamList = new List();
DataTable outputStreamTable;
string query;
string sortClause = string.Empty;
if (!string.IsNullOrEmpty(sortMember))
sortClause = $"ORDER BY {sortMember} {sortDirection}";
if (enabledOnly)
{
query = database.ParameterizedQueryString($"SELECT ID FROM OutputStreamDetail WHERE NodeID = {{0}} AND Enabled = {{1}} {sortClause}", "nodeID", "enabled");
outputStreamTable = database.Connection.RetrieveData(database.AdapterType, query, database.CurrentNodeID(), database.Bool(true));
}
else
{
query = database.ParameterizedQueryString($"SELECT * FROM OutputStreamDetail WHERE NodeID = {{0}} {sortClause}", "nodeID");
outputStreamTable = database.Connection.RetrieveData(database.AdapterType, query, database.CurrentNodeID());
}
foreach (DataRow row in outputStreamTable.Rows)
{
outputStreamList.Add(row.ConvertField("ID"));
}
return outputStreamList;
}
finally
{
if (createdConnection)
database?.Dispose();
}
}
///
/// Loads information as an style list.
///
/// to connection to database.
/// Keys of the measurements to be loaded from the database
/// Collection of .
public static ObservableCollection Load(AdoDataConnection database, IList keys)
{
bool createdConnection = false;
try
{
createdConnection = CreateConnection(ref database);
ObservableCollection outputStreamList = new();
if (keys is not null && keys.Count > 0)
{
string commaSeparatedKeys = keys.Select(key => "" + key.ToString() + "").Aggregate((str1, str2) => str1 + "," + str2);
string query = $"SELECT * FROM OutputStreamDetail WHERE ID IN ({commaSeparatedKeys})";
DataTable outputStreamTable = database.Connection.RetrieveData(database.AdapterType, query, DefaultTimeout);
outputStreamList = new ObservableCollection(
from item in outputStreamTable.AsEnumerable()
let id = item.ConvertField("ID")
let type = (OutputProtocol)item.ConvertField("Type")
orderby keys.IndexOf(id)
select new OutputStream
{
NodeID = database.Guid(item, "NodeID"),
ID = id,
Acronym = item.Field("Acronym"),
Name = item.Field("Name"),
Type = type,
ConnectionString = item.Field("ConnectionString"),
IDCode = item.ConvertField("IDCode"),
CommandChannel = item.Field("CommandChannel"),
DataChannel = item.Field("DataChannel"),
AutoPublishConfigFrame = Convert.ToBoolean(item.Field