//****************************************************************************************************** // Program.cs - Gbtc // // Copyright © 2017, 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: // ---------------------------------------------------------------------------------------------------- // 03/28/2017 - J. Ritchie Carroll // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.IO; using System.Linq; using System.Xml.Linq; using GSF; using GSF.Console; using GSF.Data; using GSF.Data.Model; using GSF.IO; using LoadImpedanceCalcs.Model; using Phasor = LoadImpedanceCalcs.Model.Phasor; // ReSharper disable AssignNullToNotNullAttribute namespace LoadImpedanceCalcs { class Program { private const string DefaultSourceApp = "SIEGate"; private const string DefaultSourceDevice = "IMPEDANCE"; private const double DefaultLagTime = 3.0D; private const double DefaultLeadTime = 5.0D; private const int DefaultFramesPerSecond = 30; private const string AssemblyName = "PowerCalculations.dll"; private const string TypeName = "PowerCalculations.ImpedanceCalculator"; private const int RequiredArgumentCount = 1; private static Guid nodeID; static int Main() { int row = 0; try { Arguments args = new Arguments(Environment.CommandLine, true); string sourceApp, sourceDevice, setting; double lagTime, leadTime; int framesPerSecond, successes = 0, failures = 0, simpleFormattedRecords = 0, explicitFormattedRecords = 0; bool skipFirstRow; if (args.Count != RequiredArgumentCount) throw new ArgumentException($"Expected {RequiredArgumentCount} argument, received {args.Count}."); if (!args.TryGetValue("sourceApp", out sourceApp)) sourceApp = DefaultSourceApp; if (!args.TryGetValue("sourceDevice", out sourceDevice)) sourceDevice = DefaultSourceDevice; if (!args.TryGetValue("lagTime", out setting) || !double.TryParse(setting, out lagTime)) lagTime = DefaultLagTime; if (!args.TryGetValue("leadTime", out setting) || !double.TryParse(setting, out leadTime)) leadTime = DefaultLeadTime; if (!args.TryGetValue("framesPerSecond", out setting) || !int.TryParse(setting, out framesPerSecond)) framesPerSecond = DefaultFramesPerSecond; if (args.TryGetValue("skipFirstRow", out setting)) skipFirstRow = setting.ParseBoolean(); else skipFirstRow = true; string sourceFileName = FilePath.GetAbsolutePath(args["OrderedArg1"]); string configFile = FilePath.GetAbsolutePath($"{sourceApp}.exe.config"); if (!File.Exists(configFile)) throw new FileNotFoundException($"Config file for {sourceApp} application \"{configFile}\" was not found."); XDocument serviceConfig = XDocument.Load(configFile); nodeID = Guid.Parse(serviceConfig .Descendants("systemSettings") .SelectMany(systemSettings => systemSettings.Elements("add")) .Where(element => "NodeID".Equals((string)element.Attribute("name"), StringComparison.OrdinalIgnoreCase)) .Select(element => (string)element.Attribute("value")) .FirstOrDefault()); string connectionString = serviceConfig .Descendants("systemSettings") .SelectMany(systemSettings => systemSettings.Elements("add")) .Where(element => "ConnectionString".Equals((string)element.Attribute("name"), StringComparison.OrdinalIgnoreCase)) .Select(element => (string)element.Attribute("value")) .FirstOrDefault(); string dataProviderString = serviceConfig .Descendants("systemSettings") .SelectMany(systemSettings => systemSettings.Elements("add")) .Where(element => "DataProviderString".Equals((string)element.Attribute("name"), StringComparison.OrdinalIgnoreCase)) .Select(element => (string)element.Attribute("value")) .FirstOrDefault(); using (AdoDataConnection connection = new AdoDataConnection(connectionString, dataProviderString)) using (StreamReader reader = File.OpenText(sourceFileName)) { TableOperations deviceTable = new TableOperations(connection); TableOperations measurementTable = new TableOperations(connection); TableOperations phasorTable = new TableOperations(connection); TableOperations actionAdapterTable = new TableOperations(connection); string line; while ((object)(line = reader.ReadLine()) != null) { row++; if (skipFirstRow) { skipFirstRow = false; continue; } // Simple mode: -- simple mode only works when devices on both end of the line are measuring a single voltage and current // 0 1 2 // TieLine, SendingDevice, ReceivingDevice // Explicit mode: // 0 1 2 3 4 5 6 7 8 9 10 // TieLine, SendingSubstation, ReceivingSubstation, SV1Angle, SV1Magnitude, SI1Angle, SI1Magnitude, RV1Angle, RV1Magnitude, RI1Angle, RI1Magnitude string[] columns = line.Split(','); if (columns.Length < 3) { Console.WriteLine($"Not enough columns in CSV file at row {row} - expected 3 for simple mode, encountered {columns.Length}, skipped row."); continue; } if (columns.Length > 3 && columns.Length < 11) { Console.WriteLine($"Not enough columns in CSV file at row {row} - expected 11 for explicit mode, encountered {columns.Length}, skipped row."); continue; } string tieLineID = columns[0].ToUpperInvariant().Trim(); string sendDevice = columns[1].ToUpperInvariant().Trim(); string receiveDevice = columns[2].ToUpperInvariant().Trim(); string sv1Angle = null, sv1Magnitude = null, si1Angle = null, si1Magnitude = null, rv1Angle = null, rv1Magnitude = null, ri1Angle = null, ri1Magnitude = null; string[] measurements; bool simpleMode = columns.Length == 3; List inputMeasurements = new List(); Dictionary adapterConnectionString = new Dictionary(StringComparer.OrdinalIgnoreCase); if (simpleMode) { simpleFormattedRecords++; } else { explicitFormattedRecords++; sv1Angle = columns[3].ToUpperInvariant().Trim(); sv1Magnitude = columns[4].ToUpperInvariant().Trim(); si1Angle = columns[5].ToUpperInvariant().Trim(); si1Magnitude = columns[6].ToUpperInvariant().Trim(); rv1Angle = columns[7].ToUpperInvariant().Trim(); rv1Magnitude = columns[8].ToUpperInvariant().Trim(); ri1Angle = columns[9].ToUpperInvariant().Trim(); ri1Magnitude = columns[10].ToUpperInvariant().Trim(); } // Add initial connection string settings adapterConnectionString["FramesPerSecond"] = framesPerSecond.ToString(); adapterConnectionString["LagTime"] = lagTime.ToString(CultureInfo.InvariantCulture); adapterConnectionString["LeadTime"] = leadTime.ToString(CultureInfo.InvariantCulture); // Get or add virtual device to associate new output measurements with Device device = GetOrAddDevice(deviceTable, sourceDevice); if (simpleMode) { // Get input measurement IDs for sending device measurements = GetInputMeasurements(deviceTable, measurementTable, phasorTable, sendDevice); if ((object)measurements == null) { failures++; continue; } inputMeasurements.AddRange(measurements); // Get input measurement IDs for receiving device measurements = GetInputMeasurements(deviceTable, measurementTable, phasorTable, receiveDevice); if ((object)measurements == null) { failures++; continue; } inputMeasurements.AddRange(measurements); } else { if (GetInputMeasurement(measurementTable, sv1Angle, inputMeasurements) || GetInputMeasurement(measurementTable, sv1Magnitude, inputMeasurements) || GetInputMeasurement(measurementTable, si1Angle, inputMeasurements) || GetInputMeasurement(measurementTable, si1Magnitude, inputMeasurements) || GetInputMeasurement(measurementTable, rv1Angle, inputMeasurements) || GetInputMeasurement(measurementTable, rv1Magnitude, inputMeasurements) || GetInputMeasurement(measurementTable, ri1Angle, inputMeasurements) || GetInputMeasurement(measurementTable, ri1Magnitude, inputMeasurements)) { failures++; continue; } } // Define input measurement keys connection string parameter adapterConnectionString["InputMeasurementKeys"] = string.Join("; ", inputMeasurements); bool newAdd; CustomActionAdapter actionAdapter = GetOrAddActionAdapter(actionAdapterTable, tieLineID, out newAdd); Console.WriteLine($"{(newAdd ? "Adding" : "Augmenting")} impedance calculation for \"{tieLineID}\"..."); // Get output measurement IDs for calculator adapter, creating them if needed OrderedDictionary outputTypes = new OrderedDictionary(StringComparer.OrdinalIgnoreCase) { ["RESISTANCE"] = "Resistance", ["REACTANCE"] = "Reactance", ["CONDUCTANCE"] = "Conductance", ["SUSCEPTANCE"] = "Susceptance", ["LINEIMPEDANCE"] = "Line Impedance", ["LINEIMPEDANCEANGLE"] = "Line Impedance Angle", ["LINEADMITTANCE"] = "Line Admittance", ["LINEADMITTANCEANGLE"] = "Line Admittance Angle" }; measurements = GetOutputMeasurements(measurementTable, device.ID, tieLineID, sendDevice, receiveDevice, outputTypes); // Define input measurement keys connection string parameter adapterConnectionString["OutputMeasurements"] = string.Join("; ", measurements); // Save updates to action adapter actionAdapter.Enabled = true; actionAdapter.ConnectionString = adapterConnectionString.JoinKeyValuePairs(); actionAdapterTable.UpdateRecord(actionAdapter); successes++; } } Console.WriteLine(); Console.WriteLine($"Found {simpleFormattedRecords:N0} simple formatted records."); Console.WriteLine($"Found {explicitFormattedRecords:N0} explicit formatted records."); Console.WriteLine(); Console.WriteLine($"{successes:N0} successful imports."); Console.WriteLine($"{failures:N0} failed imports."); #if DEBUG Console.ReadKey(); #endif return 0; } catch (Exception ex) { Console.WriteLine(); Console.WriteLine($"Import halted at row {row}!"); Console.Error.WriteLine($"Load Exception: {ex.Message}"); return 1; } } private static Device GetOrAddDevice(TableOperations deviceTable, string acronym) { Device device = deviceTable.QueryRecordWhere("NodeID = {0} AND Acronym = {1}", nodeID, acronym); if ((object)device == null) { device = deviceTable.NewRecord(); device.NodeID = nodeID; device.Acronym = acronym; device.Name = acronym; deviceTable.AddNewRecord(device); device = deviceTable.QueryRecordWhere("NodeID = {0} AND Acronym = {1}", nodeID, acronym); if ((object)device == null) throw new InvalidOperationException($"Failed to lookup Device record with Acronym of \"{acronym}\"."); } return device; } private static Device GetDevice(TableOperations deviceTable, string acronym) { Device device = deviceTable.QueryRecordWhere("NodeID = {0} AND Acronym = {1}", nodeID, acronym); if ((object)device == null) Console.WriteLine($"Failed to lookup Device record with Acronym of \"{acronym}\""); return device; } private static Phasor[] GetPostiveSequencePhasors(TableOperations phasorTable, int deviceID) { List phasors = phasorTable.QueryRecordsWhere("Phase = '+' AND DeviceID = {0}", deviceID).OrderBy(phasor => phasor.SourceIndex).ToList(); return new[] { phasors.FirstOrDefault(phasor => phasor.Type == "V"), phasors.FirstOrDefault(phasor => phasor.Type == "I") }; } private static Measurement GetPhasorMeasurement(TableOperations measurementTable, string deviceAcronym, char phasorElement, int phasorIndex) { string signalReference = $"{deviceAcronym}-P{phasorElement}{phasorIndex}"; Measurement measurement = measurementTable.QueryRecordWhere("SignalReference = {0}", signalReference); if ((object)measurement == null) Console.WriteLine($"Failed to lookup Measurement record with SignalReference of \"{signalReference}\""); return measurement; } private static bool GetInputMeasurement(TableOperations measurementTable, string pointTag, List inputMeasurements) { Measurement measurement = measurementTable.QueryRecordWhere("PointTag = {0}", pointTag); if ((object)measurement == null) { Console.WriteLine($"Failed to lookup Measurement record with PointTag of \"{pointTag}\""); return true; } inputMeasurements.Add(measurement.SignalID.ToString()); return false; } private static string[] GetInputMeasurements(TableOperations deviceTable, TableOperations measurementTable, TableOperations phasorTable, string deviceAcronym) { Device device = GetDevice(deviceTable, deviceAcronym); if ((object)device == null) return null; Phasor[] phasors = GetPostiveSequencePhasors(phasorTable, device.ID); if (phasors.Length != 2 || phasors.Any(phasor => (object)phasor == null)) { Console.WriteLine($"Failed to find a positive sequence voltage and current phasor for device \"{device.Acronym}\""); return null; } List measurements = new List(); Measurement measurement; for (int i = 0; i < phasors.Length; i++) { // Get angle measurement measurement = GetPhasorMeasurement(measurementTable, deviceAcronym, 'A', phasors[i].SourceIndex); if ((object)measurement == null) return null; measurements.Add(measurement); // Get magnitude measurement measurement = GetPhasorMeasurement(measurementTable, deviceAcronym, 'M', phasors[i].SourceIndex); if ((object)measurement == null) return null; measurements.Add(measurement); } return measurements.Select(m => m.SignalID.ToString()).ToArray(); } private static CustomActionAdapter GetOrAddActionAdapter(TableOperations actionAdapterTable, string tielineID, out bool newAdd) { string adapterName = $"IMPEDANCE_{tielineID}_CALC"; CustomActionAdapter actionAdapter = actionAdapterTable.QueryRecordWhere("NodeID = {0} AND AdapterName = {1}", nodeID, adapterName); if ((object)actionAdapter == null) { actionAdapter = actionAdapterTable.NewRecord(); actionAdapter.NodeID = nodeID; actionAdapter.AdapterName = adapterName; actionAdapter.AssemblyName = AssemblyName; actionAdapter.TypeName = TypeName; actionAdapterTable.AddNewRecord(actionAdapter); // Re-query newly added record to get auto-increment ID actionAdapter = actionAdapterTable.QueryRecordWhere("NodeID = {0} AND AdapterName = {1}", nodeID, adapterName); newAdd = true; if ((object)actionAdapter == null) throw new InvalidOperationException($"Failed to lookup CustomActionAdapter record with AdapterName of \"{adapterName}\"."); } else { newAdd = false; } return actionAdapter; } private static string[] GetOutputMeasurements(TableOperations measurementTable, int deviceID, string tieLineID, string sender, string receiver, OrderedDictionary outputTypes) { List measurements = new List(); foreach (string outputType in outputTypes.Keys) { string pointTag = $"IMPEDANCE_{tieLineID}-{outputType}:CV"; string description = $"{tieLineID} [{sender} => {receiver}] Calculated {outputTypes[outputType]} Value"; Measurement measurement = measurementTable.QueryRecordWhere("PointTag = {0}", pointTag); if ((object)measurement == null) { measurement = measurementTable.NewRecord(); measurement.DeviceID = deviceID; measurement.PointTag = pointTag; measurement.SignalReference = pointTag; measurement.Description = description; measurement.Enabled = true; measurementTable.AddNewRecord(measurement); measurement = measurementTable.QueryRecordWhere("PointTag = {0}", pointTag); if ((object)measurement == null) throw new InvalidOperationException($"Failed to lookup Measurement record with PointTag of \"{pointTag}\"."); } else { measurement.Description = description; measurementTable.UpdateRecord(measurement); } measurements.Add(measurement); } return measurements.Select(m => m.SignalID.ToString()).ToArray(); } } }