//******************************************************************************************************
// AnalogChannel.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:
// ----------------------------------------------------------------------------------------------------
// 05/19/2011 - J. Ritchie Carroll
// Generated original version of source code.
// 12/13/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Globalization;
using GSF.PhasorProtocols;
using GSF.Units;
using GSF.Units.EE;
using Newtonsoft.Json;
namespace GSF.COMTRADE
{
///
/// Represents an analog channel definition of the .
///
public class AnalogChannel
{
#region [ Members ]
// Constants
///
/// Default multiplier for current magnitude values.
///
public const double DefaultCurrentMagnitudeMultiplier = 0.05D;
///
/// Default multiplier for voltage magnitude values.
///
public const double DefaultVoltageMagnitudeMultiplier = 5.77362D;
///
/// Default multiplier for phase angle values.
///
public const double DefaultPhaseAngleMultiplier = 1.0E-4D;
///
/// Default multiplier for frequency values.
///
public const double DefaultFrequencyMultiplier = 0.001D;
///
/// Default multiplier for dF/dt values.
///
public const double DefaultDfDtMultiplier = 0.01D;
///
/// Default multiplier for an analog value.
///
public const double DefaultAnalogMultipler = 0.04D;
// Fields
private readonly bool m_targetFloatingPoint;
private string m_stationName;
private string m_channelName;
private char m_phaseDesignation;
private SignalKind m_signalKind;
private double m_nominalFrequency;
private string m_circuitComponent;
private string m_units;
private AngleUnit? m_angleUnit;
private char m_scalingIdentifier;
#endregion
#region [ Constructors ]
///
/// Creates a new instance of the .
///
/// Target schema version.
/// Determines if file type is targeting floating point.
public AnalogChannel(int version = 1999, bool targetFloatingPoint = false)
{
Version = version;
m_targetFloatingPoint = targetFloatingPoint;
m_phaseDesignation = char.MinValue;
m_signalKind = SignalKind.Analog;
CoordinateFormat = CoordinateFormat.Polar;
Multiplier = targetFloatingPoint ? 1.0D : DefaultAnalogMultipler;
Adder = 0.0D;
m_nominalFrequency = 60.0D;
MinValue = targetFloatingPoint ? float.MinValue : -99999;
MaxValue = targetFloatingPoint ? float.MaxValue : 99998;
PrimaryRatio = 1.0D;
SecondaryRatio = 1.0D;
m_scalingIdentifier = 'P';
}
///
/// Creates a new instance of the from an existing line image.
///
/// Line image to parse.
/// Target schema version.
/// Determines if file type is targeting floating point.
/// Indicates whether to relax validation on the number of line image elements.
public AnalogChannel(string lineImage, int version = 1999, bool targetFloatingPoint = false, bool useRelaxedValidation = false)
{
// An,ch_id,ph,ccbm,uu,a,b,skew,min,max,primary,secondary,PS
string[] parts = lineImage.Split(',');
Version = version;
m_targetFloatingPoint = targetFloatingPoint;
if (parts.Length < 10 || (!useRelaxedValidation && parts.Length != 10 && parts.Length != 13))
throw new InvalidOperationException($"Unexpected number of line image elements for analog channel definition: {parts.Length} - expected 10 or 13{Environment.NewLine}Image = {lineImage}");
Index = int.Parse(parts[0].Trim());
Name = parts[1];
Units = parts[4]; // Assign Units before PhaseID
PhaseID = parts[2];
CircuitComponent = parts[3];
Multiplier = double.Parse(parts[5].Trim());
Adder = double.Parse(parts[6].Trim());
Skew = double.Parse(parts[7].Trim());
MinValue = double.Parse(parts[8].Trim());
MaxValue = double.Parse(parts[9].Trim());
if (parts.Length >= 13)
{
PrimaryRatio = double.Parse(parts[10].Trim());
SecondaryRatio = double.Parse(parts[11].Trim());
ScalingIdentifier = parts[12].Trim()[0];
}
}
#endregion
#region [ Properties ]
///
/// Gets or sets index of this .
///
public int Index { get; set; }
///
/// Gets or sets name of this formatted as station_name:channel_name.
///
/// Name must be formatted as station_name:channel_name.
public string Name
{
get
{
if (string.IsNullOrEmpty(m_stationName))
return null;
if (!string.IsNullOrEmpty(m_channelName))
return $"{m_stationName}:{m_channelName}";
return m_stationName;
}
set
{
string[] parts = value.Split(':');
if (parts.Length == 2)
{
m_stationName = parts[0].Trim();
m_channelName = parts[1].Trim();
}
else
{
m_stationName = value;
}
}
}
///
/// Gets or sets station name component of this .
///
public string StationName
{
get => m_stationName;
set => m_stationName = value.Replace(":", "_").Trim();
}
///
/// Gets or sets channel name component of this .
///
public string ChannelName
{
get => m_channelName;
set => m_channelName = value.Replace(":", "_").Trim();
}
///
/// Gets or sets the 2-character phase identifier for this .
///
public string PhaseID
{
get
{
switch (m_signalKind)
{
case SignalKind.Magnitude:
if (m_phaseDesignation != char.MinValue)
{
if (CoordinateFormat == CoordinateFormat.Rectangular)
return m_phaseDesignation + "r";
return m_phaseDesignation + "m";
}
break;
case SignalKind.Angle:
if (m_phaseDesignation != char.MinValue)
{
if (CoordinateFormat == CoordinateFormat.Rectangular)
return m_phaseDesignation + "i";
return m_phaseDesignation + "a";
}
break;
case SignalKind.Frequency:
return "F";
case SignalKind.DfDt:
return "df";
}
return PhaseDesignation;
}
set
{
value = value.Trim();
if (string.IsNullOrEmpty(value))
{
m_phaseDesignation = char.MinValue;
SignalKind = SignalKind.Analog;
CoordinateFormat = CoordinateFormat.Polar;
}
else
{
if (string.Compare(value, "F", StringComparison.OrdinalIgnoreCase) == 0)
{
m_phaseDesignation = char.MinValue;
SignalKind = SignalKind.Frequency;
CoordinateFormat = CoordinateFormat.Polar;
}
else if (string.Compare(value, "df", StringComparison.OrdinalIgnoreCase) == 0)
{
m_phaseDesignation = char.MinValue;
SignalKind = SignalKind.DfDt;
CoordinateFormat = CoordinateFormat.Polar;
}
else if (value.Length > 1)
{
PhaseDesignation = value[0].ToString();
char component = char.ToLowerInvariant(value[1]);
switch (component)
{
case 'r':
SignalKind = SignalKind.Magnitude;
CoordinateFormat = CoordinateFormat.Rectangular;
break;
case 'i':
SignalKind = SignalKind.Angle;
CoordinateFormat = CoordinateFormat.Rectangular;
break;
case 'm':
SignalKind = SignalKind.Magnitude;
CoordinateFormat = CoordinateFormat.Polar;
break;
case 'a':
SignalKind = SignalKind.Angle;
CoordinateFormat = CoordinateFormat.Polar;
break;
}
}
else
{
PhaseDesignation = value[0].ToString();
SignalKind = SignalKind.Analog;
CoordinateFormat = CoordinateFormat.Polar;
}
}
}
}
///
/// Gets or sets phase designation of this .
///
/// Value is not a valid phase designation.
public string PhaseDesignation
{
get => m_phaseDesignation.ToString().RemoveNull();
set
{
value = value.Trim();
if (string.IsNullOrEmpty(value))
{
m_phaseDesignation = char.MinValue;
}
else
{
char phaseDesignation = char.ToUpper(value[0]);
switch (phaseDesignation)
{
case 'A':
case 'R':
case '1':
m_phaseDesignation = 'A';
break;
case 'B':
case 'S':
case '2':
m_phaseDesignation = 'B';
break;
case 'C':
case 'T':
case '3':
m_phaseDesignation = 'C';
break;
case 'P':
case '+':
m_phaseDesignation = 'P';
break;
case 'N':
case '-':
m_phaseDesignation = '-';
break;
case 'Z':
case '0':
m_phaseDesignation = '0';
break;
default:
m_phaseDesignation = char.MinValue;
break;
}
}
}
}
///
/// Gets or sets phasor type of this , if applicable.
///
public PhasorType PhasorType { get; set; }
///
/// Gets or sets nominal frequency of this .
///
public double NominalFrequency
{
get => m_nominalFrequency;
set
{
m_nominalFrequency = value;
if (m_signalKind == SignalKind.Frequency)
Adder = (double)m_nominalFrequency;
}
}
///
/// Gets or sets signal kind of this .
///
/// Value is not a valid analog signal kind.
public SignalKind SignalKind
{
get => m_signalKind;
set
{
if (s_validAnalogSignalKinds.BinarySearch(value) < 0)
throw new ArgumentException(value + " is not a valid analog signal kind.");
m_signalKind = value;
if (m_targetFloatingPoint)
{
switch (m_signalKind)
{
case SignalKind.Angle:
Angle minAngle = new Angle(-Math.PI);
Angle maxAngle = new Angle(Math.PI);
MinValue = minAngle.ConvertTo(AngleUnit);
MaxValue = maxAngle.ConvertTo(AngleUnit);
break;
case SignalKind.Frequency:
MinValue = m_nominalFrequency - 4.0D;
MaxValue = m_nominalFrequency + 4.0D;
break;
}
return;
}
switch (m_signalKind)
{
case SignalKind.Angle:
Multiplier = DefaultPhaseAngleMultiplier;
Adder = 0.0D;
break;
case SignalKind.Magnitude:
if (PhasorType == PhasorType.Current)
Multiplier = DefaultCurrentMagnitudeMultiplier;
else
Multiplier = DefaultVoltageMagnitudeMultiplier;
Adder = 0.0D;
break;
case SignalKind.Frequency:
Multiplier = DefaultFrequencyMultiplier;
Adder = m_nominalFrequency;
break;
case SignalKind.DfDt:
Multiplier = DefaultDfDtMultiplier;
Adder = 0.0D;
break;
case SignalKind.Status:
case SignalKind.Digital:
Multiplier = 1.0D;
Adder = 0.0D;
break;
default:
Multiplier = DefaultAnalogMultipler;
Adder = 0.0D;
break;
}
}
}
///
/// Gets or sets coordinate format of this .
///
public CoordinateFormat CoordinateFormat { get; set; }
///
/// Gets or sets circuit component of this .
///
public string CircuitComponent
{
get => m_circuitComponent;
set
{
if (!string.IsNullOrWhiteSpace(value))
m_circuitComponent = value.Trim();
else
m_circuitComponent = "";
if (m_circuitComponent.Length > 64)
m_circuitComponent = m_circuitComponent.Substring(0, 64);
}
}
///
/// Gets or sets units of this .
///
public string Units
{
get => m_units;
set
{
m_units = string.IsNullOrWhiteSpace(value) ? "" : value.Trim();
if (m_units.Length > 32)
m_units = m_units.Substring(0, 32);
m_angleUnit = null;
}
}
///
/// Gets or sets value multiplier of this .
///
public double Multiplier { get; set; }
///
/// Gets or sets adder of this .
///
public double Adder { get; set; }
///
/// Gets or sets time skew between channels of this .
///
public double Skew { get; set; }
///
/// Gets or sets minimum unscaled value of this .
///
public double MinValue { get; set; }
///
/// Gets or sets maximum unscaled value of this .
///
public double MaxValue { get; set; }
///
/// Gets or sets the channel voltage or current transformer ratio primary factor of this .
///
public double PrimaryRatio { get; set; }
///
/// Gets or sets the channel voltage or current transformer ratio secondary factor of this .
///
public double SecondaryRatio { get; set; }
///
/// Gets or sets the primary or secondary data scaling identifier of this .
///
public char ScalingIdentifier
{
get => m_scalingIdentifier;
set
{
value = char.ToUpper(value);
if (value != 'P' && value != 'S')
throw new ArgumentException(value + " is not a valid primary or secondary data scaling identifier - must be either \'P\' or \'S\'.");
m_scalingIdentifier = value;
}
}
///
/// Gets derived from , if applicable to channel type.
///
public AngleUnit AngleUnit => m_angleUnit ?? (m_angleUnit = GetAngleUnit(Units)).Value;
///
/// Gets target schema version.
///
[JsonIgnore]
public int Version { get; }
#endregion
#region [ Methods ]
///
/// Converts to its string format.
///
public override string ToString()
{
// An,ch_id,ph,ccbm,uu,a,b,skew,min,max
List values = new List
{
Index.ToString(),
Name,
PhaseID,
CircuitComponent,
Units,
Multiplier.ToString(CultureInfo.InvariantCulture),
Adder.ToString(CultureInfo.InvariantCulture),
Skew.ToString(CultureInfo.InvariantCulture),
MinValue.ToString(CultureInfo.InvariantCulture),
MaxValue.ToString(CultureInfo.InvariantCulture)
};
// ...,primary,secondary,PS
if (Version >= 1999)
{
values.Add(PrimaryRatio.ToString(CultureInfo.InvariantCulture));
values.Add(SecondaryRatio.ToString(CultureInfo.InvariantCulture));
values.Add(ScalingIdentifier.ToString());
}
return string.Join(",", values);
}
#endregion
#region [ Static ]
// Static Fields
private static readonly List s_validAnalogSignalKinds;
// Static Constructor
static AnalogChannel()
{
s_validAnalogSignalKinds = new List(new[] { SignalKind.Analog, SignalKind.Angle, SignalKind.Calculation, SignalKind.DfDt, SignalKind.Frequency, SignalKind.Magnitude, SignalKind.Statistic });
s_validAnalogSignalKinds.Sort();
}
// Static Methods
// Attempt to parse units as an AngleUnit enum value
private static AngleUnit GetAngleUnit(string units)
{
if (!Enum.TryParse(units, true, out AngleUnit angleUnit))
{
// Fall back on other common names for angle units
if (units.StartsWith("deg", StringComparison.OrdinalIgnoreCase))
angleUnit = AngleUnit.Degrees;
else if (units.StartsWith("grad", StringComparison.OrdinalIgnoreCase) || units.StartsWith("gon", StringComparison.OrdinalIgnoreCase))
angleUnit = AngleUnit.Grads;
else if (units.StartsWith("arcm", StringComparison.OrdinalIgnoreCase) || units.StartsWith("min", StringComparison.OrdinalIgnoreCase) || units.StartsWith("moa", StringComparison.OrdinalIgnoreCase))
angleUnit = AngleUnit.ArcMinutes;
else if (units.StartsWith("arcs", StringComparison.OrdinalIgnoreCase) || units.StartsWith("sec", StringComparison.OrdinalIgnoreCase))
angleUnit = AngleUnit.ArcSeconds;
else if (units.StartsWith("ang", StringComparison.OrdinalIgnoreCase) || units.StartsWith("mil", StringComparison.OrdinalIgnoreCase))
angleUnit = AngleUnit.AngularMil;
else // rad
angleUnit = AngleUnit.Radians;
}
return angleUnit;
}
#endregion
}
}