//****************************************************************************************************** // PowerCalculationConfigurationValidation.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: // ---------------------------------------------------------------------------------------------------- // 11/2/2015 - Ryan McCoy // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.Data; using System.Diagnostics.CodeAnalysis; using GSF.Configuration; using GSF.Data; using PhasorProtocolAdapters; namespace PowerCalculations.PowerMultiCalculator { #region [ Static ] /// /// Implements validations for power calculations in the power calculation adapter /// public static class PowerCalculationConfigurationValidation { /// /// Validates that data operation and adapter instance exist within database. /// public static void ValidateDatabaseDefinitions() { using (AdoDataConnection database = new AdoDataConnection("systemSettings")) { if (!DataOperationExists(database)) CreateDataOperation(database); if (!AdapterInstanceExists(database)) CreateAdapterInstance(database); } } /// /// Returns true if a data operation exists to run this class. Returns false otherwise. /// /// Database connection to use for checking the data operation /// True or false indicating whether the operation exists private static bool DataOperationExists(AdoDataConnection database) { return Convert.ToInt32(database.ExecuteScalar($"SELECT COUNT(*) FROM DataOperation WHERE TypeName='{typeof(PowerCalculationConfigurationValidation).FullName}' AND MethodName='ValidatePowerCalculationConfigurations'")) > 0; } /// /// Creates a data operation to run the validations in this class. /// /// Database connection to use for creating the data operation private static void CreateDataOperation(AdoDataConnection database) { database.ExecuteNonQuery($"INSERT INTO DataOperation(Description, AssemblyName, TypeName, MethodName, Enabled) VALUES ('Power Calculation Validations', 'PowerCalculations.dll', '{typeof(PowerCalculationConfigurationValidation).FullName}', 'ValidatePowerCalculationConfigurations', 1)"); } /// /// Returns true if a data operation exists to run this class. Returns false otherwise. /// /// Database connection to use for checking the data operation /// True or false indicating whether the operation exists private static bool AdapterInstanceExists(AdoDataConnection database) { return Convert.ToInt32(database.ExecuteScalar($"SELECT COUNT(*) FROM CustomActionAdapter WHERE TypeName='{typeof(PowerMultiCalculatorAdapter).FullName}'")) > 0; } /// /// Creates a data operation to run the validations in this class. /// /// Database connection to use for creating the data operation private static void CreateAdapterInstance(AdoDataConnection database) { database.ExecuteNonQuery($"INSERT INTO CustomActionAdapter(NodeID, AdapterName, AssemblyName, TypeName, ConnectionString, Enabled) VALUES ('{ConfigurationFile.Current.Settings["systemSettings"]["NodeID"].ValueAs()}', 'PHASOR!POWERCALC', 'PowerCalculations.dll', '{typeof(PowerMultiCalculatorAdapter).FullName}', 'FramesPerSecond=30; LagTime=5.0; LeadTime=3.0', 1)"); } /// /// Data operation to validate power calculation configuration. This method checks that input measurements and non-null output measurements exist, are enabled, and have the correct signal type. /// If null output measurements are found in the configuration, this method will create the output measurements and update the configuration. /// /// Database connection for configuration information /// Node ID for database queries /// Not used /// Not used /// Delegate for method to communicate status updates /// Exception handling delegate public static void ValidatePowerCalculationConfigurations(AdoDataConnection database, string nodeIDQueryString, ulong trackingVersion, string arguments, Action statusMessage, Action processException) { try { CheckInputMeasurementsExist(database, nodeIDQueryString, statusMessage); CheckInputMeasurementsAreEnabled(database, nodeIDQueryString, statusMessage); CheckInputMeasurementsSignalType(database, nodeIDQueryString, statusMessage); CheckOutputMeasurementsExist(database, nodeIDQueryString, statusMessage); CheckOutputMeasurementsAreEnabled(database, nodeIDQueryString, statusMessage); CheckOutputMeasurementsSignalType(database, nodeIDQueryString, statusMessage); CreateOutputMeasurementsWhereNull(database, nodeIDQueryString, statusMessage); } catch (Exception e) { processException(e); } } /// /// Checks power calculation configuration for null output measurements. When found, a new measurement is created, and the configuration is updated to use the new measurement. /// /// Configuration database connection /// Node ID formatted for query /// Delegate for communicating status updates [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities", Justification = "Code has been evaluated, SQL text used by function is not based on user input.")] private static void CreateOutputMeasurementsWhereNull(AdoDataConnection database, string nodeIDQueryString, Action statusMessage) { statusMessage("Checking for calculations with null output measurements..."); string query = $"SELECT " + $" pc.ID, " + $" pc.CircuitDescription, " + $" pc.ActivePowerOutputSignalID, " + $" pc.ApparentPowerOutputSignalID, " + $" pc.ReactivePowerOutputSignalID, " + $" v.Acronym AS VendorAcronym, " + $" d.Acronym AS DeviceAcronym, " + $" c.Acronym AS CompanyAcronym, " + $" d.id AS DeviceID, " + $" vm.HistorianID AS HistorianID, " + $" p.Label AS CurrentLabel " + $"FROM " + $" PowerCalculation pc JOIN " + $" Measurement vm ON vm.SignalID = pc.VoltageAngleSignalID JOIN " + $" Measurement im ON im.SignalID = pc.CurrentAngleSignalID LEFT OUTER JOIN " + $" Phasor p ON im.DeviceID = p.DeviceID AND im.PhasorSourceIndex = p.SourceIndex LEFT OUTER JOIN " + $" Device d ON vm.DeviceID = d.ID LEFT OUTER JOIN " + $" VendorDevice vd ON vd.ID = d.VendorDeviceID LEFT OUTER JOIN " + $" Vendor v ON vd.VendorID = v.ID LEFT OUTER JOIN " + $" Company c ON d.CompanyID = c.ID " + $"WHERE " + $" pc.Enabled <> 0 AND " + $" pc.NodeID = {nodeIDQueryString} AND " + $" ( " + $" pc.ActivePowerOutputSignalID IS NULL OR " + $" pc.ReactivePowerOutputSignalID IS NULL OR " + $" pc.ApparentPowerOutputSignalID IS NULL " + $" )"; Dictionary activePowerUpdates = new Dictionary(); Dictionary reactivePowerUpdates = new Dictionary(); Dictionary apparentPowerUpdates = new Dictionary(); using (IDbCommand cmd = database.Connection.CreateCommand()) { cmd.CommandText = query; using (IDataReader rdr = cmd.ExecuteReader()) { while (rdr.Read()) { int powerCalculationID = rdr.GetInt32(0); string companyAcronym = rdr.IsDBNull(7) ? "" : rdr.GetString(7); string vendorAcronym = rdr.IsDBNull(5) ? "" : rdr.GetString(5); string signalTypeAcronym = "CALC"; string circuitDescription = rdr.IsDBNull(1) ? "" : rdr.GetString(1); int deviceID = rdr.GetInt32(8); int? historianID = rdr.IsDBNull(9) ? null : (int?)rdr.GetInt32(9); // Remove any settings defined in circuit description int semicolonIndex = circuitDescription.IndexOf(';'); if (semicolonIndex > -1) circuitDescription = circuitDescription.Substring(0, semicolonIndex); if (rdr.IsDBNull(2)) // Real - MW { // create active power output measurement PowerMeasurement measurement = CreateMeasurement(companyAcronym, circuitDescription + "-MW", vendorAcronym, signalTypeAcronym, circuitDescription, deviceID, historianID, "Active Power Calculation"); activePowerUpdates.Add(powerCalculationID, measurement); } if (rdr.IsDBNull(3)) // Apparent - MVA { // create apparent power output measurement PowerMeasurement measurement = CreateMeasurement(companyAcronym, circuitDescription + "-MVA", vendorAcronym, signalTypeAcronym, circuitDescription, deviceID, historianID, "Apparent Power Calculation"); apparentPowerUpdates.Add(powerCalculationID, measurement); } if (rdr.IsDBNull(4)) // Reactive - MVAR { //create reactive power output measurement PowerMeasurement measurement = CreateMeasurement(companyAcronym, circuitDescription + "-MVAR", vendorAcronym, signalTypeAcronym, circuitDescription, deviceID, historianID, "Reactive Power Calculation"); reactivePowerUpdates.Add(powerCalculationID, measurement); } } } } int newMeasurementsCount = activePowerUpdates.Count + reactivePowerUpdates.Count + apparentPowerUpdates.Count; if (newMeasurementsCount > 0) { MeasurementRepository repo = new MeasurementRepository(); statusMessage($"Creating {newMeasurementsCount} new output measurements for power calculation..."); foreach (KeyValuePair update in activePowerUpdates) { repo.Save(database, update.Value); UpdatePowerCalculation(database, update.Key, activePowerOutputSignalID: update.Value.SignalID); } statusMessage("Successfully created new active power calculations."); foreach (KeyValuePair update in reactivePowerUpdates) { repo.Save(database, update.Value); UpdatePowerCalculation(database, update.Key, reactivePowerOutputSignalID: update.Value.SignalID); } statusMessage("Successfully created new reactive power calculations."); foreach (KeyValuePair update in apparentPowerUpdates) { repo.Save(database, update.Value); UpdatePowerCalculation(database, update.Key, apparentPowerOutputSignalID: update.Value.SignalID); } statusMessage("Successfully created new apparent power calculations."); statusMessage("Completed creation of new measurements for null output measurements on power calculations."); } } /// /// Creates a new measurement object for power calculation output measurements /// private static PowerMeasurement CreateMeasurement(string companyAcronym, string deviceAcronym, string vendorAcronym, string signalTypeAcronym, string circuitDescription, int deviceID, int? historianID, string descriptionSuffix) { PowerMeasurement measurement = new PowerMeasurement { PointTag = CommonPhasorServices.CreatePointTag(companyAcronym, deviceAcronym, vendorAcronym, signalTypeAcronym), Adder = 0, Multiplier = 1, Description = $"{circuitDescription} {descriptionSuffix}", DeviceID = deviceID, HistorianID = historianID, SignalTypeID = 10, Enabled = true, SignalID = Guid.Empty }; if (measurement.PointTag != null) { int beginIndex = measurement.PointTag.IndexOf('_'); measurement.SignalReference = measurement.PointTag.Substring(beginIndex + 1); } return measurement; } /// /// Updated power calculation to use new Signal IDs for output measurement /// /// Configuration database connection /// ID of the power calculation to be updated /// Active power output signal ID, if needed for update /// Reactive power output signal ID, if needed for update /// Apparent power output signal ID, if needed for update [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities", Justification = "Code has been evaluated, SQL generated by function is based non-text based numeric input.")] private static void UpdatePowerCalculation(AdoDataConnection database, int powerCalculationID, Guid? activePowerOutputSignalID = null, Guid? reactivePowerOutputSignalID = null, Guid? apparentPowerOutputSignalID = null) { if (activePowerOutputSignalID == null && reactivePowerOutputSignalID == null && apparentPowerOutputSignalID == null) return; List updates = new List(); if (activePowerOutputSignalID != null) updates.Add($"ActivePowerOutputSignalID='{activePowerOutputSignalID.Value}'"); if (reactivePowerOutputSignalID != null) updates.Add($"ReactivePowerOutputSignalID='{reactivePowerOutputSignalID.Value}'"); if (apparentPowerOutputSignalID != null) updates.Add($"ApparentPowerOutputSignalID='{apparentPowerOutputSignalID.Value}'"); string query = "UPDATE PowerCalculation SET "; query += string.Join(", ", updates) + " "; query += $"WHERE ID={powerCalculationID} "; using (IDbCommand cmd = database.Connection.CreateCommand()) { cmd.CommandText = query; cmd.ExecuteNonQuery(); } } /// /// Verifies that output measurements are set to calculated signal type. Calculations are disabled if output measurements are not configured correctly. /// /// Configuration database connection /// Node ID formatted for query /// Delegate to communicate status updates private static void CheckOutputMeasurementsSignalType(AdoDataConnection database, string nodeIDQueryString, Action statusMessage) { statusMessage("Validating if non-null output measurements have correct signal type..."); string query = $"UPDATE PowerCalculation " + $"SET Enabled = 0 " + $"WHERE " + $" Enabled = 1 AND " + $" NodeID = {nodeIDQueryString} AND " + $" ( " + $" (SELECT Acronym FROM Measurement JOIN SignalType ON Measurement.SignalTypeID = SignalType.ID WHERE SignalID = ActivePowerOutputSignalID) <> 'CALC' OR " + $" (SELECT Acronym FROM Measurement JOIN SignalType ON Measurement.SignalTypeID = SignalType.ID WHERE SignalID = ActivePowerOutputSignalID) <> 'CALC' OR " + $" (SELECT Acronym FROM Measurement JOIN SignalType ON Measurement.SignalTypeID = SignalType.ID WHERE SignalID = ActivePowerOutputSignalID) <> 'CALC' " + $" )"; const string failureMessage = "One or more power calculations are defined with non-null output measurements having the wrong signal type. Power calculations will be disabled."; RunDatabaseValidation(database, statusMessage, query, failureMessage); } /// /// Verifies that output measurements are enabled. Power calculations will be disabled where outputs are disabled. /// /// /// /// private static void CheckOutputMeasurementsAreEnabled(AdoDataConnection database, string nodeIDQueryString, Action statusMessage) { statusMessage("Validating if non-null output measurements are enabled..."); string query = $"UPDATE PowerCalculation " + $"SET Enabled = 0 " + $"WHERE " + $" Enabled = 1 AND " + $" NodeID = {nodeIDQueryString} AND " + $" (SELECT COUNT(*) FROM Measurement WHERE Enabled = 0 AND SignalID IN (ActivePowerOutputSignalID, ReactivePowerOutputSignalID, ApparentPowerOutputSignalID)) > 0"; const string failureMessage = "One or more power calculations are defined with output measurements that are disabled. Power calculations will be disabled."; RunDatabaseValidation(database, statusMessage, query, failureMessage); } /// /// Verifies that output measurements exist in the measurement table. Power calculations are disabled where the configured outputs do not exist. /// /// Configuration database connection /// Node ID formatted for query /// Delegate to communicate status updates private static void CheckOutputMeasurementsExist(AdoDataConnection database, string nodeIDQueryString, Action statusMessage) { statusMessage("Validating if non-null output measurements exist..."); string query = $"UPDATE PowerCalculation SET Enabled=0 WHERE Enabled=1 AND NodeID={nodeIDQueryString} " + "AND ((ActivePowerOutputSignalID IS NOT NULL AND ActivePowerOutputSignalID NOT IN (SELECT SignalID from Measurement)) " + "OR (ReactivePowerOutputSignalID IS NOT NULL AND ReactivePowerOutputSignalID NOT IN (SELECT SignalID from Measurement)) " + "OR (ApparentPowerOutputSignalID IS NOT NULL AND ApparentPowerOutputSignalID NOT IN (SELECT SignalID from Measurement)))"; const string failureMessage = "One or more power calculations are defined with non-null output measurements that do not exist. Power calculations will be disabled."; RunDatabaseValidation(database, statusMessage, query, failureMessage); } /// /// Validates that input measurements have the correct signal type. Power calculations are disabled where inputs have the wrong signal types. /// /// Configuration database connection /// Node ID formatted for database query /// Delegate for status updates private static void CheckInputMeasurementsSignalType(AdoDataConnection database, string nodeIDQueryString, Action statusMessage) { statusMessage("Validating if input measurements have correct signal type..."); string query = $"UPDATE PowerCalculation " + $"SET Enabled = 0 " + $"WHERE " + $" Enabled = 1 AND " + $" NodeID = {nodeIDQueryString} AND " + $" ( " + $" (SELECT Acronym FROM Measurement JOIN SignalType ON Measurement.SignalTypeID = SignalType.ID WHERE SignalID = VoltageAngleSignalID) <> 'VPHA' OR " + $" (SELECT Acronym FROM Measurement JOIN SignalType ON Measurement.SignalTypeID = SignalType.ID WHERE SignalID = VoltageMagSignalID) <> 'VPHM' OR " + $" (SELECT Acronym FROM Measurement JOIN SignalType ON Measurement.SignalTypeID = SignalType.ID WHERE SignalID = CurrentAngleSignalID) <> 'IPHA' OR " + $" (SELECT Acronym FROM Measurement JOIN SignalType ON Measurement.SignalTypeID = SignalType.ID WHERE SignalID = CurrentMagSignalID) <> 'IPHM' " + $" )"; const string failureMessage = "One or more power calculations are defined with input measurements having the wrong signal type. Power calculations will be disabled."; RunDatabaseValidation(database, statusMessage, query, failureMessage); } /// /// Verifies that input measurements are enabled. Power calculations are disabled where input measurements are disabled. /// /// Configuration database connection /// Node ID formatted for query /// Delegate for communicating status updates private static void CheckInputMeasurementsAreEnabled(AdoDataConnection database, string nodeIDQueryString, Action statusMessage) { statusMessage("Validating if input measurements are enabled..."); string query = $"UPDATE PowerCalculation " + $"SET Enabled = 0 " + $"WHERE " + $" Enabled = 1 AND " + $" NodeID = {nodeIDQueryString} AND " + $" (SELECT COUNT(*) FROM Measurement WHERE Enabled = 0 AND SignalID IN (VoltageAngleSignalID, VoltageMagSignalID, CurrentAngleSignalID, CurrentMagSignalID)) > 0"; const string failureMessage = "One or more power calculations are defined with input measurements that are disabled. Power calculations will be disabled."; RunDatabaseValidation(database, statusMessage, query, failureMessage); } /// /// Verifies that input measurements exist. Power calculations are disabled where input measurements do not exist. /// /// Configuration database connection /// Node ID formatted for query /// Delegate for communicating status updates private static void CheckInputMeasurementsExist(AdoDataConnection database, string nodeIDQueryString, Action statusMessage) { statusMessage("Validating if input measurements exist..."); string query = $"UPDATE PowerCalculation SET Enabled=0 WHERE Enabled=1 AND NodeID={nodeIDQueryString}" + "AND (VoltageMagSignalID NOT IN (SELECT SignalID from Measurement) " + "OR VoltageAngleSignalID NOT IN (SELECT SignalID from Measurement) " + "OR CurrentMagSignalID NOT IN (SELECT SignalID from Measurement) " + "OR CurrentAngleSignalID NOT IN (SELECT SignalID from Measurement))"; const string failureMessage = "One or more power calculations are defined with input measurements that do not exist. Power calculations will be disabled."; RunDatabaseValidation(database, statusMessage, query, failureMessage); } [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities", Justification = "Code has been evaluated, functions calling this method create SQL text that is not based on user input.")] private static void RunDatabaseValidation(AdoDataConnection database, Action statusMessage, string query, string failureMessage) { using (IDbCommand cmd = database.Connection.CreateCommand()) { cmd.CommandText = query; int affectedRows = cmd.ExecuteNonQuery(); if (affectedRows > 0) statusMessage(failureMessage); } } #endregion } }