//******************************************************************************************************
// 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
}
}