//****************************************************************************************************** // APPPDCConfig.cs - Gbtc // // Copyright © 2021, 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://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/01/2021 - J. Ritchie Carroll // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; using GSF.Communication; using GSF.PhasorProtocols; using GSF.Units.EE; using ShowMessageFunc = System.Func; namespace APPPDCImporter { // By replacing this one class, a converter from any other PDC configuration can be created. The result // should always be a ConfigurationFrame that has all needed information to create an input configuration // in the host GPA synchrophasor application, e.g., openPDC. public static class APPPDCConfig { public const string IPAddressToken = "[[ipAddress]]"; public const string DefaultLineFrequency = "60"; public const string DefaultServerName = "APPPDC"; public const ushort DefaultIDCode = 1; public const ushort DefaultFrameRate = 30; public const string DefaultPhasorType = "Voltage"; public const char DefaultPhase = '+'; public const string DefaultTransportProtocol = "TCP"; public const ushort DefaultPort = 4712; public const ushort DefaultDataPort = 4713; public static ConfigurationFrame Parse(string configFile, ShowMessageFunc showMessage) { string[] rows = File.ReadAllLines(configFile); Dictionary configSettings = rows .Select(row => row.Split('=')) .ToDictionary(key => key[0], value => value[1], StringComparer.OrdinalIgnoreCase); // Fixed for now LineFrequency nominalFrequency = LineFrequency.Hz60; // Load server name setting string serverName = configSettings["IPAddr"]; // Load ID code setting if (!ushort.TryParse(configSettings["IdCode"], out ushort idCode)) idCode = DefaultIDCode; // Load frame rate setting if (!ushort.TryParse(configSettings["DataRate"], out ushort frameRate)) frameRate = DefaultFrameRate; // With an ID code, frame rate and server name, there's enough info to create a config frame ConfigurationFrame configFrame = new(idCode, frameRate, serverName); // Load all output PMUs int index = -1; while (configSettings.TryGetValue($"PmuId{++index}", out string setting) && ushort.TryParse(setting, out idCode)) { string name = configSettings[$"PmuStn{index}"]; if (string.IsNullOrWhiteSpace(name)) continue; // With a name and an ID code, there's enough info to create a config cell representing the PMU ConfigurationCell configCell = new(configFrame, name, idCode) { NominalFrequency = nominalFrequency, }; name = "VaChan"; PhasorType phasorType = PhasorType.Voltage; char phase = 'A'; string description = $"{configCell.StationName} phase {phase} {phasorType.ToString().ToLowerInvariant()} phasor"; configCell.PhasorDefinitions.Add(new PhasorDefinition(configCell, name, description, phasorType, phase)); name = "VbChan"; phasorType = PhasorType.Voltage; phase = 'B'; description = $"{configCell.StationName} phase {phase} {phasorType.ToString().ToLowerInvariant()} phasor"; configCell.PhasorDefinitions.Add(new PhasorDefinition(configCell, name, description, phasorType, phase)); name = "VcChan"; phasorType = PhasorType.Voltage; phase = 'C'; description = $"{configCell.StationName} phase {phase} {phasorType.ToString().ToLowerInvariant()} phasor"; configCell.PhasorDefinitions.Add(new PhasorDefinition(configCell, name, description, phasorType, phase)); name = "IaChan"; phasorType = PhasorType.Current; phase = 'A'; description = $"{configCell.StationName} phase {phase} {phasorType.ToString().ToLowerInvariant()} phasor"; configCell.PhasorDefinitions.Add(new PhasorDefinition(configCell, name, description, phasorType, phase)); name = "IbChan"; phasorType = PhasorType.Current; phase = 'B'; description = $"{configCell.StationName} phase {phase} {phasorType.ToString().ToLowerInvariant()} phasor"; configCell.PhasorDefinitions.Add(new PhasorDefinition(configCell, name, description, phasorType, phase)); name = "IcChan"; phasorType = PhasorType.Current; phase = 'C'; description = $"{configCell.StationName} phase {phase} {phasorType.ToString().ToLowerInvariant()} phasor"; configCell.PhasorDefinitions.Add(new PhasorDefinition(configCell, name, description, phasorType, phase)); name = "InChan"; phasorType = PhasorType.Current; phase = 'N'; description = $"{configCell.StationName} phase {phase} {phasorType.ToString().ToLowerInvariant()} phasor"; configCell.PhasorDefinitions.Add(new PhasorDefinition(configCell, name, description, phasorType, phase)); configFrame.Cells.Add(configCell); } // Get connection details configured for APP PDC configFrame.TransportProtocol = DefaultTransportProtocol; if (!ushort.TryParse(configSettings["PortTCP"], out ushort port)) port = DefaultPort; //if (!ushort.TryParse(configSettings["PortUDP"], out ushort dataPort)) // dataPort = DefaultDataPort; // Setup MultiProtocolFrameParser style connection string Dictionary getTcpSettings(bool commandChannel = false) { // Use of simple "protocol" key in commandChannel and separation of // server/port keys are required to create a proper "PmuConnection" // file that can be successfully parsed by the PMU Connection Tester return new Dictionary(StringComparer.OrdinalIgnoreCase) { [commandChannel ? "protocol" : nameof(TransportProtocol)] = nameof(TransportProtocol.Tcp), ["server"] = IPAddressToken, ["port"] = $"{port}", ["isListener"] = "false", ["interface"] = "0.0.0.0" }; } Dictionary settings = configFrame.Settings; settings[nameof(PhasorProtocol)] = nameof(PhasorProtocol.IEEEC37_118V1); foreach (KeyValuePair pair in getTcpSettings()) settings[pair.Key] = pair.Value; // Save device IP configuration Dictionary deviceIPs = configFrame.DeviceIPs; deviceIPs[serverName] = serverName; configFrame.TargetDeviceIP = serverName; return configFrame; } private static string GetValue(this IEnumerable target, string defaultValue) { string value = target .Descendants("Value") .FirstOrDefault()?.Value ?? $"{defaultValue}"; if (string.IsNullOrWhiteSpace(value)) value = defaultValue ?? ""; return value.Trim(); } private static T GetValue(this IEnumerable target, T defaultValue) where T : IConvertible { Type type = typeof(T); string result = target.GetValue($"{defaultValue}"); if (type == typeof(ushort)) { if (!ushort.TryParse(result, out ushort value)) value = defaultValue.ToUInt16(null); return (T)Convert.ChangeType(value, type); } if (type == typeof(bool)) { bool.TryParse(result, out bool value); return (T)Convert.ChangeType(value, type); } return (T)Convert.ChangeType(result, type); } private static string GuessPhase(string name) => string.IsNullOrWhiteSpace(name) || name.Length < 2 ? null : name[1].ToString(); private static char TranslatePhase(char phase) => phase switch { '1' => '+', '2' => '-', _ => phase }; } }