From 3812ad90558175dcc5de4303a94fb55ae93c70c1 Mon Sep 17 00:00:00 2001 From: prestoncraw Date: Mon, 3 Aug 2026 14:32:24 -0400 Subject: [PATCH 1/3] Migrate .net9/gemstone libraries from PQBrowser --- .../openHistorian/openHistorian.Core.dll | Bin 0 -> 95232 bytes Directory.Build.props | 5 + Libraries/openHistorian.XDALink/Historian.cs | 203 ++ .../ImportedMeasurement.cs | 222 ++ .../TrendingDataPoint.cs | 92 + .../openHistorian.XDALink.csproj | 48 + .../openXDA.APIAuthentication/APIQuery.cs | 311 +++ .../IAPICredentialRetriever.cs | 65 + .../Properties/AssemblyInfo.cs | 36 + Libraries/openXDA.APIAuthentication/XDAAPI.cs | 232 ++ .../openXDA.APIAuthentication/XDAAPIHelper.cs | 149 ++ .../openXDA.APIAuthentication.csproj | 26 + .../openXDA.Configuration/BreakerSection.cs | 119 + .../openXDA.Configuration/COMTRADESection.cs | 67 + .../DataAnalysisSection.cs | 120 + .../DataPusherSection.cs | 37 + .../openXDA.Configuration/EMAXSection.cs | 66 + .../EPRICapBankAnalyticSection.cs | 89 + .../Edition/EditionChecker.cs | 98 + .../Edition/HttpEditionFilterAttribute.cs | 57 + .../openXDA.Configuration/EmailSection.cs | 111 + .../EventEmailSection.cs | 68 + .../FaultLocationSection.cs | 104 + .../FileEnumeratorSection.cs | 80 + .../FileProcessorSection.cs | 91 + .../FilePrunerSection.cs | 58 + .../FileWatcherSection.cs | 308 +++ .../openXDA.Configuration/GrafanaSection.cs | 72 + .../openXDA.Configuration/LSCVSSection.cs | 68 + .../openXDA.Configuration/OSIPISection.cs | 71 + .../openXDA.Configuration/PQDIFSection.cs | 46 + Libraries/openXDA.Configuration/PQISection.cs | 66 + .../Properties/AssemblyInfo.cs | 36 + .../openXDA.Configuration/RabbitMQSection.cs | 90 + .../openXDA.Configuration/SCADASection.cs | 94 + .../openXDA.Configuration/SSAMSSection.cs | 73 + .../openXDA.Configuration/Subscription.cs | 65 + .../openXDA.Configuration/SystemSection.cs | 109 + .../TaskProcessorSection.cs | 68 + .../TrendingDataSection.cs | 122 + .../openXDA.Configuration.csproj | 48 + Libraries/openXDA.PQI/Address.cs | 81 + Libraries/openXDA.PQI/AuditCurve.cs | 56 + Libraries/openXDA.PQI/Company.cs | 56 + Libraries/openXDA.PQI/Equipment.cs | 74 + Libraries/openXDA.PQI/Facility.cs | 56 + Libraries/openXDA.PQI/FacilityAudit.cs | 53 + Libraries/openXDA.PQI/FacilityInfo.cs | 83 + Libraries/openXDA.PQI/HttpClientExtensions.cs | 42 + Libraries/openXDA.PQI/HttpClientProvider.cs | 37 + Libraries/openXDA.PQI/PQIEquipment.cs | 65 + Libraries/openXDA.PQI/PQIModel.cs | 36 + Libraries/openXDA.PQI/PQIWSClient.cs | 325 +++ Libraries/openXDA.PQI/PQIWSQueryHelper.cs | 282 +++ Libraries/openXDA.PQI/PingClient.cs | 97 + .../openXDA.PQI/Properties/AssemblyInfo.cs | 36 + Libraries/openXDA.PQI/TestCurve.cs | 96 + Libraries/openXDA.PQI/TestCurvePoint.cs | 53 + Libraries/openXDA.PQI/openXDA.PQI.csproj | 32 + .../openXDA.Reports/AllBreakersReport.cs | 411 ++++ Libraries/openXDA.Reports/EmailWriter.cs | 205 ++ .../IndividualBreakerReport.cs | 701 ++++++ Libraries/openXDA.Reports/PQReport.cs | 2034 +++++++++++++++++ .../Properties/AssemblyInfo.cs | 36 + Libraries/openXDA.Reports/README.md | 7 + Libraries/openXDA.Reports/ReportsEngine.cs | 382 ++++ .../openXDA.Reports/openXDA.Reports.csproj | 52 + PQDigest/EventWidgets | 2 +- PQDigest/PQDigest.sln | 111 + 69 files changed, 9390 insertions(+), 1 deletion(-) create mode 100644 Dependencies/openHistorian/openHistorian.Core.dll create mode 100644 Directory.Build.props create mode 100644 Libraries/openHistorian.XDALink/Historian.cs create mode 100644 Libraries/openHistorian.XDALink/ImportedMeasurement.cs create mode 100644 Libraries/openHistorian.XDALink/TrendingDataPoint.cs create mode 100644 Libraries/openHistorian.XDALink/openHistorian.XDALink.csproj create mode 100644 Libraries/openXDA.APIAuthentication/APIQuery.cs create mode 100644 Libraries/openXDA.APIAuthentication/IAPICredentialRetriever.cs create mode 100644 Libraries/openXDA.APIAuthentication/Properties/AssemblyInfo.cs create mode 100644 Libraries/openXDA.APIAuthentication/XDAAPI.cs create mode 100644 Libraries/openXDA.APIAuthentication/XDAAPIHelper.cs create mode 100644 Libraries/openXDA.APIAuthentication/openXDA.APIAuthentication.csproj create mode 100644 Libraries/openXDA.Configuration/BreakerSection.cs create mode 100644 Libraries/openXDA.Configuration/COMTRADESection.cs create mode 100644 Libraries/openXDA.Configuration/DataAnalysisSection.cs create mode 100644 Libraries/openXDA.Configuration/DataPusherSection.cs create mode 100644 Libraries/openXDA.Configuration/EMAXSection.cs create mode 100644 Libraries/openXDA.Configuration/EPRICapBankAnalyticSection.cs create mode 100644 Libraries/openXDA.Configuration/Edition/EditionChecker.cs create mode 100644 Libraries/openXDA.Configuration/Edition/HttpEditionFilterAttribute.cs create mode 100644 Libraries/openXDA.Configuration/EmailSection.cs create mode 100644 Libraries/openXDA.Configuration/EventEmailSection.cs create mode 100644 Libraries/openXDA.Configuration/FaultLocationSection.cs create mode 100644 Libraries/openXDA.Configuration/FileEnumeratorSection.cs create mode 100644 Libraries/openXDA.Configuration/FileProcessorSection.cs create mode 100644 Libraries/openXDA.Configuration/FilePrunerSection.cs create mode 100644 Libraries/openXDA.Configuration/FileWatcherSection.cs create mode 100644 Libraries/openXDA.Configuration/GrafanaSection.cs create mode 100644 Libraries/openXDA.Configuration/LSCVSSection.cs create mode 100644 Libraries/openXDA.Configuration/OSIPISection.cs create mode 100644 Libraries/openXDA.Configuration/PQDIFSection.cs create mode 100644 Libraries/openXDA.Configuration/PQISection.cs create mode 100644 Libraries/openXDA.Configuration/Properties/AssemblyInfo.cs create mode 100644 Libraries/openXDA.Configuration/RabbitMQSection.cs create mode 100644 Libraries/openXDA.Configuration/SCADASection.cs create mode 100644 Libraries/openXDA.Configuration/SSAMSSection.cs create mode 100644 Libraries/openXDA.Configuration/Subscription.cs create mode 100644 Libraries/openXDA.Configuration/SystemSection.cs create mode 100644 Libraries/openXDA.Configuration/TaskProcessorSection.cs create mode 100644 Libraries/openXDA.Configuration/TrendingDataSection.cs create mode 100644 Libraries/openXDA.Configuration/openXDA.Configuration.csproj create mode 100644 Libraries/openXDA.PQI/Address.cs create mode 100644 Libraries/openXDA.PQI/AuditCurve.cs create mode 100644 Libraries/openXDA.PQI/Company.cs create mode 100644 Libraries/openXDA.PQI/Equipment.cs create mode 100644 Libraries/openXDA.PQI/Facility.cs create mode 100644 Libraries/openXDA.PQI/FacilityAudit.cs create mode 100644 Libraries/openXDA.PQI/FacilityInfo.cs create mode 100644 Libraries/openXDA.PQI/HttpClientExtensions.cs create mode 100644 Libraries/openXDA.PQI/HttpClientProvider.cs create mode 100644 Libraries/openXDA.PQI/PQIEquipment.cs create mode 100644 Libraries/openXDA.PQI/PQIModel.cs create mode 100644 Libraries/openXDA.PQI/PQIWSClient.cs create mode 100644 Libraries/openXDA.PQI/PQIWSQueryHelper.cs create mode 100644 Libraries/openXDA.PQI/PingClient.cs create mode 100644 Libraries/openXDA.PQI/Properties/AssemblyInfo.cs create mode 100644 Libraries/openXDA.PQI/TestCurve.cs create mode 100644 Libraries/openXDA.PQI/TestCurvePoint.cs create mode 100644 Libraries/openXDA.PQI/openXDA.PQI.csproj create mode 100644 Libraries/openXDA.Reports/AllBreakersReport.cs create mode 100644 Libraries/openXDA.Reports/EmailWriter.cs create mode 100644 Libraries/openXDA.Reports/IndividualBreakerReport.cs create mode 100644 Libraries/openXDA.Reports/PQReport.cs create mode 100644 Libraries/openXDA.Reports/Properties/AssemblyInfo.cs create mode 100644 Libraries/openXDA.Reports/README.md create mode 100644 Libraries/openXDA.Reports/ReportsEngine.cs create mode 100644 Libraries/openXDA.Reports/openXDA.Reports.csproj diff --git a/Dependencies/openHistorian/openHistorian.Core.dll b/Dependencies/openHistorian/openHistorian.Core.dll new file mode 100644 index 0000000000000000000000000000000000000000..8d36e8c738a95e58f5ec0e7637b2c1040d818fba GIT binary patch literal 95232 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 NnZsf{ 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 V28Ok&hH!Ua68V4s{?F6E{{y?3`e^_F literal 0 HcmV?d00001 diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..9e460a7 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,5 @@ + + + 1.0.179 + + diff --git a/Libraries/openHistorian.XDALink/Historian.cs b/Libraries/openHistorian.XDALink/Historian.cs new file mode 100644 index 0000000..a751701 --- /dev/null +++ b/Libraries/openHistorian.XDALink/Historian.cs @@ -0,0 +1,203 @@ +//****************************************************************************************************** +// Historian.cs - Gbtc +// +// Copyright © 2015, 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: +// ---------------------------------------------------------------------------------------------------- +// 08/23/2015 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone; +using Gemstone.WordExtensions; +using openHistorian.Net; +using openHistorian.Queues; +using openHistorian.Snap; +using SnapDB.Snap; +using SnapDB.Snap.Filters; +using SnapDB.Snap.Services; +using SnapDB.Snap.Services.Reader; + +namespace openHistorian.XDALink +{ + /// + /// Series ID values. + /// + public enum SeriesID + { + Minimum = 0, + Maximum = 1, + Average = 2 + } + + public class Historian : IDisposable + { + #region [ Members ] + + // Constants + + /// + /// Default historian server port number. + /// + public const int DefaultHistorianPort = 38402; + + // Fields + private HistorianClient m_client; + private ClientDatabaseBase m_database; + private Lazy m_queue; + private List> m_treeStreams; + + private bool m_disposed; + + #endregion + + #region [ Constructors ] + + public Historian(string server, string database) + { + string[] split = server.Split(':'); + int port; + + if (split.Length == 1) + { + m_client = new HistorianClient(split[0], DefaultHistorianPort); + } + else + { + if (!int.TryParse(split[1], out port)) + throw new ArgumentException("Invalid format for server[:port]. " + server, "server"); + + m_client = new HistorianClient(split[0], port); + } + + m_database = m_client.GetDatabase(database); + m_queue = new Lazy(() => new HistorianInputQueue(() => m_database)); + m_treeStreams = new List>(); + } + + #endregion + + #region [ Methods ] + + public IEnumerable Read(IEnumerable channels, DateTime startTime, DateTime stopTime) + { + IEnumerable measurementIDs = channels.SelectMany(GetAllMeasurementIDs); + return Read(measurementIDs, startTime, stopTime); + } + + public void Write(int channelID, SeriesID seriesID, DateTime timestamp, double value) + { + HistorianKey historianKey = new HistorianKey(); + HistorianValue historianValue = new HistorianValue(); + + historianKey.PointID = ToPointID(channelID, (int)seriesID); + historianKey.TimestampAsDate = timestamp; + historianValue.AsSingle = (float)value; + + m_queue.Value.Enqueue(historianKey, historianValue); + } + + public void Flush() + { + Flush(1); + } + + public void Flush(int pollingFrequency) + { + if (m_queue.IsValueCreated) + { + while (m_queue.Value.Size > 0) + Thread.Sleep(1000 / pollingFrequency); + } + } + + public void Dispose() + { + if (!m_disposed) + { + try + { + foreach (TreeStream stream in m_treeStreams) + stream.Dispose(); + + if (m_queue.IsValueCreated) + { + Flush(10); + m_queue.Value.Dispose(); + } + + m_database.Dispose(); + m_client.Dispose(); + } + finally + { + m_disposed = true; + } + } + } + + private IEnumerable Read(IEnumerable measurementIDs, DateTime startTime, DateTime stopTime) + { + SeekFilterBase timeFilter = TimestampSeekFilter.CreateFromRange(startTime, stopTime); + MatchFilterBase pointFilter = null; + HistorianKey key = new HistorianKey(); + HistorianValue value = new HistorianValue(); + + if ((object)measurementIDs != null) + pointFilter = PointIDMatchFilter.CreateFromList(measurementIDs); + + // Start stream reader for the provided time window and selected points + using (TreeStream stream = m_database.Read(SortedTreeEngineReaderOptions.Default, timeFilter, pointFilter)) + { + m_treeStreams.Add(stream); + + while (stream.Read(key, value)) + { + yield return new TrendingDataPoint() + { + ChannelID = (int)key.PointID.HighDoubleWord(), + SeriesID = (SeriesID)(int)key.PointID.LowDoubleWord(), + Timestamp = key.TimestampAsDate, + Value = value.AsSingle + }; + } + } + } + + private IEnumerable GetAllMeasurementIDs(int channel) + { + return SeriesIDs.Select(series => ToPointID(channel, (int)series)); + } + + #endregion + + #region [ Static ] + + // Static Fields + private static readonly List SeriesIDs = Enum.GetValues(typeof(SeriesID)) + .Cast() + .ToList(); + + // Static Methods + + public static ulong ToPointID(int channel, int series) + { + return Word.MakeQuadWord((uint)channel, (uint)series); + } + + #endregion + } +} diff --git a/Libraries/openHistorian.XDALink/ImportedMeasurement.cs b/Libraries/openHistorian.XDALink/ImportedMeasurement.cs new file mode 100644 index 0000000..e6721fb --- /dev/null +++ b/Libraries/openHistorian.XDALink/ImportedMeasurement.cs @@ -0,0 +1,222 @@ +//****************************************************************************************************** +// ImportedMeasurement.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: +// ---------------------------------------------------------------------------------------------------- +// 09/27/2017 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +#pragma warning disable 1591 + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using Newtonsoft.Json; + +namespace openHistorian.XDALink +{ + public class ImportedMeasurement + { + public Guid? NodeID { get; set; } + + public Guid? SourceNodeID { get; set; } + + public Guid? SignalID { get; set; } + + [StringLength(200)] + public string Source { get; set; } + + public long PointID { get; set; } + + [StringLength(200)] + public string PointTag { get; set; } + + [StringLength(200)] + public string AlternateTag { get; set; } + + [StringLength(4)] + public string SignalTypeAcronym { get; set; } + + [StringLength(200)] + public string SignalReference { get; set; } + + public int? FramesPerSecond { get; set; } + + [StringLength(200)] + public string ProtocolAcronym { get; set; } + + [StringLength(200)] + [DefaultValue("Frame")] + public string ProtocolType { get; set; } + + public int? PhasorID { get; set; } + + public char? PhasorType { get; set; } + + public char? Phase { get; set; } + + [DefaultValue(0.0D)] + public double Adder { get; set; } + + [DefaultValue(1.0D)] + public double Multiplier { get; set; } + + [StringLength(200)] + public string CompanyAcronym { get; set; } + + public double? Longitude { get; set; } + + public double? Latitude { get; set; } + + public string Description { get; set; } + + [DefaultValue(false)] + public bool Enabled { get; set; } + } + + public class ImportedMeasurementsTable + { + #region [ Members ] + + // Fields + private string m_historianURL; + private string m_authString; + + #endregion + + #region [ Constructors ] + + public ImportedMeasurementsTable(string historianURL, string username, string password) + { + m_historianURL = historianURL; + + string unencodedAuthString = $"{username}:{password}"; + byte[] authBytes = Encoding.UTF8.GetBytes(unencodedAuthString); + m_authString = Convert.ToBase64String(authBytes); + } + + #endregion + + #region [ Methods ] + + public IEnumerable FindAll() + { + string url = GetURL("/api/importedmeasurements/findall"); + return RequestMeasurements(url); + } + + public IEnumerable FindByID(long pointID) + { + string url = GetURL($"/api/importedmeasurements/findbyid/{pointID}"); + return RequestMeasurements(url); + } + + public IEnumerable FindByPointTag(string pointTag) + { + string url = GetURL($"/api/importedmeasurements/findbypointtag/{pointTag}"); + return RequestMeasurements(url); + } + + public IEnumerable FindByAlternateTag(string alternateTag) + { + string url = GetURL($"/api/importedmeasurements/findbyalternatetag/{alternateTag}"); + return RequestMeasurements(url); + } + + public void ImportMeasurements(IEnumerable measurements) + { + string antiForgeryToken = GenerateAntiForgeryToken(); + string url = GetURL("/api/importedmeasurements/importmeasurements"); + + using (MemoryStream stream = new MemoryStream()) + using (StreamWriter writer = new StreamWriter(stream)) + using (JsonTextWriter jsonWriter = new JsonTextWriter(writer)) + { + JsonSerializer serializer = new JsonSerializer(); + serializer.Serialize(jsonWriter, measurements); + jsonWriter.Flush(); + + using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url)) + using (ByteArrayContent content = new ByteArrayContent(stream.ToArray())) + using (HttpClient client = CreateClient()) + { + request.Content = content; + request.Headers.Add("X-GSF-Verify", antiForgeryToken); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + client.SendAsync(request).Result.Dispose(); + } + } + } + + public void DeleteMeasurement(long pointID) + { + string antiForgeryToken = GenerateAntiForgeryToken(); + string url = GetURL($"/api/importedmeasurements/deletemeasurement/{pointID}"); + + using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, url)) + using (HttpClient client = CreateClient()) + { + request.Headers.Add("X-GSF-Verify", antiForgeryToken); + client.SendAsync(request).Result.Dispose(); + } + } + + private IEnumerable RequestMeasurements(string url) + { + using (HttpClient client = CreateClient()) + using (Stream response = client.GetStreamAsync(url).Result) + using (StreamReader streamReader = new StreamReader(response)) + using (JsonTextReader jsonReader = new JsonTextReader(streamReader)) + { + JsonSerializer serializer = new JsonSerializer(); + return serializer.Deserialize>(jsonReader); + } + } + + private string GenerateAntiForgeryToken() + { + string url = GetURL($"/api/importedmeasurements/generaterequestverficationtoken"); + + using (HttpClient client = CreateClient()) + { + return client.GetStringAsync(url).Result; + } + } + + private string GetURL(string relativePath) + { + string baseURL = m_historianURL.TrimEnd('/'); + string subURL = relativePath.TrimStart('/'); + return $"{baseURL}/{subURL}"; + } + + private HttpClient CreateClient() + { + HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", m_authString); + return client; + } + + #endregion + } +} diff --git a/Libraries/openHistorian.XDALink/TrendingDataPoint.cs b/Libraries/openHistorian.XDALink/TrendingDataPoint.cs new file mode 100644 index 0000000..6548742 --- /dev/null +++ b/Libraries/openHistorian.XDALink/TrendingDataPoint.cs @@ -0,0 +1,92 @@ +//****************************************************************************************************** +// TrendingDataPoint.cs - Gbtc +// +// Copyright © 2015, 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: +// ---------------------------------------------------------------------------------------------------- +// 08/23/2015 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; + +namespace openHistorian.XDALink +{ + public class TrendingDataPoint + { + #region [ Members ] + + // Fields + private int m_channelID; + private SeriesID m_seriesID; + private DateTime m_timestamp; + private double m_value; + + #endregion + + #region [ Properties ] + + public int ChannelID + { + get + { + return m_channelID; + } + set + { + m_channelID = value; + } + } + + public SeriesID SeriesID + { + get + { + return m_seriesID; + } + set + { + m_seriesID = value; + } + } + + public DateTime Timestamp + { + get + { + return m_timestamp; + } + set + { + m_timestamp = value; + } + } + + public double Value + { + get + { + return m_value; + } + set + { + m_value = value; + } + } + + #endregion + } +} diff --git a/Libraries/openHistorian.XDALink/openHistorian.XDALink.csproj b/Libraries/openHistorian.XDALink/openHistorian.XDALink.csproj new file mode 100644 index 0000000..5e8ee63 --- /dev/null +++ b/Libraries/openHistorian.XDALink/openHistorian.XDALink.csproj @@ -0,0 +1,48 @@ + + + Debug + AnyCPU + Library + openHistorian.XDALink + openHistorian.XDALink + net9.0 + Debug;Development;Release + enable + true + enable + + + true + full + false + ..\..\..\Build\Output\Debug\Libraries\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + ..\..\..\Build\Output\Release\Libraries\ + TRACE + prompt + 4 + + + + + + + + + + + ..\..\Dependencies\openHistorian\openHistorian.Core.dll + + + + + + + + diff --git a/Libraries/openXDA.APIAuthentication/APIQuery.cs b/Libraries/openXDA.APIAuthentication/APIQuery.cs new file mode 100644 index 0000000..58f8643 --- /dev/null +++ b/Libraries/openXDA.APIAuthentication/APIQuery.cs @@ -0,0 +1,311 @@ +//****************************************************************************************************** +// APIQuery.cs - Gbtc +// +// Copyright © 2022, 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 Eclipse Public License -v 1.0 (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/eclipse-1.0.php +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 07/01/2022 - Christoph Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Sockets; +using System.Security.Principal; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace openXDA.APIAuthentication +{ + /// + /// Exceptions thrown during an API query. + /// + public class APIQueryException : Exception + { + /// + /// Creates a new instance of the class. + /// + /// The URL of the host that issued the failure. + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + public APIQueryException(string hostURL, string message, Exception innerException) + : base(message, innerException) + { + HostURL = hostURL; + } + + /// + /// The URL of the host that issued the failure. + /// + public string HostURL { get; } + } + + /// + /// Issues requests to the API using API authentication. + /// This class handles the GSF token and authentication + /// + public class APIQuery + { + #region [ Members ] + + // Nested Types + private class Host + { + public Host(string url) => + URL = url; + + public string URL { get; } + public string AntiForgeryToken { get; set; } + } + + private class HostUnreachableException : Exception + { + } + + // Fields + private int m_hostIndex; + + #endregion + + #region [ Constructors ] + + /// + /// Creates a new instance of the class. + /// + /// The API key used to identify the user of the API. + /// The token used to authenticate the user of the API. + /// URL that locates the host providing access to the API. + public APIQuery(string apiKey, string apiToken, string hostURL) + : this(apiKey, apiToken, new[] { hostURL }) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The API key used to identify the user of the API. + /// The token used to authenticate the user of the API. + /// List of URLs that locate the hosts providing access to the API. + public APIQuery(string apiKey, string apiToken, IEnumerable hostURLs) + { + APIKey = apiKey; + APIToken = apiToken; + + Hosts = hostURLs + .Select(url => new Host(url)) + .ToList(); + + // Select a random host for the first API call attempt + if (Hosts.Count > 1) + Interlocked.Exchange(ref m_hostIndex, InitialHostIndex); + } + + #endregion + + #region[ Properties ] + + /// + /// The API key identifying the user of the API. + /// + public string APIKey { get; } + + /// + /// The API token used to authenticate the user. + /// + public string APIToken { get; } + + /// + /// The list of URLs that locate the hosts providing access to the API. + /// + public IEnumerable HostsURLs => Hosts + .Select(host => host.URL); + + private List Hosts { get; } + + private int InitialHostIndex => Tuple + .Create(Environment.TickCount, Environment.MachineName) + .GetHashCode() % Hosts.Count; + + #endregion + + #region [ Methods ] + + /// + /// Sends a web request to the host using the credentials for API authentication. + /// + /// Action that configures the HTTP request. + /// Path to the API endpoint locating the resource to be requested. + /// being impersonated if the APIToken supports impersonation. set to NULL for using APIAuthentication only + /// Token used to cancel the request before it has completed. + /// The HTTP response returned by the host that handled the request. + public Task SendWebRequestAsync(Action configure, string path, IPrincipal user = null, CancellationToken cancellationToken = default) => + SendWebRequestAsync(configure, path, HttpCompletionOption.ResponseContentRead, user, cancellationToken); + + /// + /// Sends a web request to the host using the credentials for API authentication. + /// + /// Action that configures the HTTP request. + /// Path to the API endpoint locating the resource to be requested. + /// When the operation should complete (as soon as a response is available or after reading the whole response content). + /// being impersonated if the APIToken supports impersonation. set to NULL for using APIAuthentication only + /// Token used to cancel the request before it has completed. + /// The HTTP response returned by the host that handled the request. + public async Task SendWebRequestAsync(Action configure, string path, HttpCompletionOption httpCompletionOption, IPrincipal user = null, CancellationToken cancellationToken = default) + { + int initialHostIndex = Interlocked.CompareExchange(ref m_hostIndex, 0, 0); + + void UpdateHostIndex(int hostIndex) => + Interlocked.CompareExchange(ref m_hostIndex, hostIndex, initialHostIndex); + + for (int i = 0; i < Hosts.Count; i++) + { + int hostIndex = (initialHostIndex + i) % Hosts.Count; + Host host = Hosts[hostIndex]; + + try + { + HttpResponseMessage response = await SendWebRequestToAsync(host, configure, path, httpCompletionOption, user, cancellationToken).ConfigureAwait(false); + UpdateHostIndex(hostIndex); + return response; + } + catch (HostUnreachableException) + { + continue; + } + catch (Exception ex) + { + UpdateHostIndex(hostIndex); + + string message = $"Failed when sending API query to host {host.URL}: {ex.Message}"; + throw new APIQueryException(host.URL, message, ex); + } + } + + return new HttpResponseMessage(HttpStatusCode.NotFound); + } + + private async Task SendWebRequestToAsync(Host host, Action configure, string path, HttpCompletionOption httpCompletionOption,IPrincipal user, CancellationToken cancellationToken) + { + using (HttpRequestMessage request = BuildRequest(host, path, configure, user)) + { + if (request.Method != HttpMethod.Get) + { + if (host.AntiForgeryToken == null) + host.AntiForgeryToken = await GetAntiForgeryTokenAsync(host, cancellationToken).ConfigureAwait(false); + + request.Headers.Add("X-GSF-Verify", host.AntiForgeryToken); + } + + return await CallAPIAsync(request, httpCompletionOption, cancellationToken).ConfigureAwait(false); + } + } + + private async Task GetAntiForgeryTokenAsync(Host host, CancellationToken cancellationToken) + { + void ConfigureTokenRequest(HttpRequestMessage tokenRequest) + { + MediaTypeWithQualityHeaderValue mediaType = new MediaTypeWithQualityHeaderValue("text/plain"); + tokenRequest.Headers.Accept.Add(mediaType); + tokenRequest.Method = HttpMethod.Get; + } + + using (HttpRequestMessage tokenRequest = BuildRequest(host, "api/rvht", ConfigureTokenRequest, null)) + using (HttpResponseMessage tokenResponse = await CallAPIAsync(tokenRequest, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false)) + { + tokenResponse.EnsureSuccessStatusCode(); + return await tokenResponse.Content.ReadAsStringAsync().ConfigureAwait(false); + } + } + + private async Task CallAPIAsync(HttpRequestMessage request, HttpCompletionOption httpCompletionOption, CancellationToken cancellationToken) + { + try + { + return await HttpClient.SendAsync(request, httpCompletionOption, cancellationToken).ConfigureAwait(false); + } + catch (HttpRequestException ex) + { + // If there are no other hosts to try, + // then we can just throw the exception as-is + if (Hosts.Count == 1) + throw; + + if (IndicatesHostIsUnreachable(ex)) + throw new HostUnreachableException(); + + throw; + } + } + + private HttpRequestMessage BuildRequest(Host host, string path, Action configure, IPrincipal user) + { + HttpRequestMessage request = new HttpRequestMessage(); + + try + { + string cleanHostURL = host.URL.Trim().TrimEnd('/'); + string cleanPath = path.Trim().TrimStart('/'); + string fullurl = $"{cleanHostURL}/{cleanPath}"; + request.RequestUri = new Uri(fullurl); + configure(request); + + string type = "XDA-API"; + string decode = $"{APIKey}:{APIToken}"; + + if (!(user is null)) + { + type= "XDA-API-IMP"; + decode = $"{decode}:{user.Identity.Name}"; + } + + Encoding utf8 = new UTF8Encoding(false); + byte[] credentialData = utf8.GetBytes(decode); + string credentials = Convert.ToBase64String(credentialData); + request.Headers.Authorization = new AuthenticationHeaderValue(type, credentials); + + return request; + } + catch + { + request.Dispose(); + throw; + } + } + + private bool IndicatesHostIsUnreachable(Exception ex) + { + if (ex is SocketException socketException) + return socketException.SocketErrorCode == SocketError.ConnectionRefused || socketException.SocketErrorCode == SocketError.TimedOut; + if (ex.InnerException is null) + return false; + return IndicatesHostIsUnreachable(ex.InnerException); + } + + #endregion + + #region [ Static ] + + private static HttpClient HttpClient { get; } + = new HttpClient(); + + #endregion + } +} diff --git a/Libraries/openXDA.APIAuthentication/IAPICredentialRetriever.cs b/Libraries/openXDA.APIAuthentication/IAPICredentialRetriever.cs new file mode 100644 index 0000000..3b4d8b0 --- /dev/null +++ b/Libraries/openXDA.APIAuthentication/IAPICredentialRetriever.cs @@ -0,0 +1,65 @@ +//****************************************************************************************************** +// XDAAPIHelper.cs - Gbtc +// +// Copyright © 2023, 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 Eclipse Public License -v 1.0 (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/eclipse-1.0.php +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 10/17/2025 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Security.Claims; + +namespace openXDA.APIAuthentication +{ + /// + /// Interface of credential retriever object for . + /// + public interface IAPICredentialRetriever + { + #region [ Properties ] + + /// + /// API Token used to access OpenXDA + /// + string Token { get; } + + /// + /// API Key used to access OpenXDA + /// + string Key { get; } + + /// + /// API Key used to access OpenXDA + /// + string Host { get; } + + /// + /// Refreshes the settings from the original source. + /// + /// A flag indicating if the operation was successful. + bool TryRefreshSettings(); + + /// + /// Retrieves customer key from the claims principle. + /// + /// A value of signifies the current user is authorized to view any object the controller may retrieve. + /// A flag indicating if the operation was successful. + bool TryRetrieveCustomer(ClaimsPrincipal principal, out string customerKey); + + #endregion + } +} diff --git a/Libraries/openXDA.APIAuthentication/Properties/AssemblyInfo.cs b/Libraries/openXDA.APIAuthentication/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..3305774 --- /dev/null +++ b/Libraries/openXDA.APIAuthentication/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("openXDA.APIAuthentication")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("GPA")] +[assembly: AssemblyProduct("openXDA.APIAuthentication")] +[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("93FF1E73-CD8B-4F01-89BC-A4A242FA06A6")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("3.0.5.93")] +[assembly: AssemblyVersion("3.0.5.93")] +[assembly: AssemblyFileVersion("3.0.5.93")] diff --git a/Libraries/openXDA.APIAuthentication/XDAAPI.cs b/Libraries/openXDA.APIAuthentication/XDAAPI.cs new file mode 100644 index 0000000..46315a8 --- /dev/null +++ b/Libraries/openXDA.APIAuthentication/XDAAPI.cs @@ -0,0 +1,232 @@ +//****************************************************************************************************** +// XDAAPI.cs - Gbtc +// +// Copyright © 2023, 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 Eclipse Public License -v 1.0 (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/eclipse-1.0.php +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 10/21/2025 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace openXDA.APIAuthentication +{ + /// + /// Helper class that provides openXDA API Calls. + /// + public class XDAAPI + { + #region [ Properties ] + /// + /// Tells users if the helper has been intialized or not. + /// + public bool IsIntialized { get; private set; } + + /// + /// API Token used to access OpenXDA + /// + public string Token => SettingsRetriever.Token; + + /// + /// API Key used to access OpenXDA + /// + public string Key => SettingsRetriever.Key; + + /// + /// API Key used to access OpenXDA + /// + public string Host => SettingsRetriever.Host; + + /// + /// Object that retireves the token,key, and host of the helper. + /// + private IAPICredentialRetriever SettingsRetriever { get; set; } + + #endregion + + #region [ Methods ] + /// + /// Creates API object and registers retriever. + /// + public XDAAPI(IAPICredentialRetriever retriever) + { + SettingsRetriever = retriever; + if (!TryRefreshSettings()) + throw new ArgumentException("Unable to load settings from retriever."); + } + + /// + /// Recalls the setup settings function from to refetch settings. + /// + public bool TryRefreshSettings() + { + bool success = SettingsRetriever.TryRefreshSettings(); + if (success) + IsIntialized = true; + return success; + } + + /// + /// Retrieves customer key from the claims principle. + /// + /// A value of signifies the current user is authorized to view any object the controller may retrieve. + /// A flag indicating if the operation was successful. + public bool TryRetrieveCustomer(ClaimsPrincipal principal, out string customerKey) => SettingsRetriever.TryRetrieveCustomer(principal, out customerKey); + + /// + /// Gets Response Task from XDA + /// + /// Path to specific API request + /// The of the request + /// response as a + public Task GetResponseTask(string requestURI, HttpContent content = null) + { + if (!IsIntialized) + throw new InvalidOperationException("API helper has not been intialized."); + + APIQuery query = new APIQuery(Key, Token, Host.Split(';')); + + void ConfigureRequest(HttpRequestMessage request) + { + if (content == null) + { + request.Method = HttpMethod.Get; + request.Headers.Accept.Clear(); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + } + else + { + request.Method = HttpMethod.Post; + request.Content = content; + } + } + + return query.SendWebRequestAsync(ConfigureRequest, requestURI); + } + + /// + /// Makes a Get Request to OpenXDA to obtain a string + /// + /// Path to specific API request + /// string + public async Task GetAsync(string requestURI) + { + if (!IsIntialized) + throw new InvalidOperationException("API helper has not been intialized."); + + APIQuery query = new APIQuery(Key, Token, Host.Split(';')); + + void ConfigureRequest(HttpRequestMessage request) + { + request.Method = HttpMethod.Get; + request.Headers.Accept.Clear(); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + } + + HttpResponseMessage responseMessage = await query.SendWebRequestAsync(ConfigureRequest, requestURI).ConfigureAwait(false); + responseMessage.EnsureSuccessStatusCode(); + return await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); + } + + /// + /// Makes a Get Request to OpenXDA to obtain a stream + /// + /// Path to specific API request + /// stream + public async Task GetStreamAsync(string requestURI) + { + if (!IsIntialized) + throw new InvalidOperationException("API helper has not been intialized."); + + APIQuery query = new APIQuery(Key, Token, Host.Split(';')); + + void ConfigureRequest(HttpRequestMessage request) + { + request.Method = HttpMethod.Get; + request.Headers.Accept.Clear(); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + } + + HttpResponseMessage responseMessage = await query.SendWebRequestAsync(ConfigureRequest, requestURI).ConfigureAwait(false); + responseMessage.EnsureSuccessStatusCode(); + return await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false); + } + + /// + /// Makes a Get Request to OpenXDA to obtain an object + /// + /// Path to specific API request + /// a object + public async Task GetAsync(string requestURI) + { + if (!IsIntialized) + throw new InvalidOperationException("API helper has not been intialized."); + + string result = await GetAsync(requestURI).ConfigureAwait(false); + T resultObject = JsonConvert.DeserializeObject(result); + return resultObject; + } + + /// + /// Makes Post request on OpenXDA + /// + /// Path to specific API request + /// The of the request + /// response as a + public async Task PostAsync(string requestURI, HttpContent content) + { + if (!IsIntialized) + throw new InvalidOperationException("API helper has not been intialized."); + + APIQuery query = new APIQuery(Key, Token, Host.Split(';')); + + void ConfigureRequest(HttpRequestMessage request) + { + request.Method = HttpMethod.Post; + request.Content = content; + } + + HttpResponseMessage responseMessage = await query.SendWebRequestAsync(ConfigureRequest, requestURI).ConfigureAwait(false); + return await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); + } + + /// + /// Makes Post request on OpenXDA + /// + /// Path to specific API request + /// The of the request + /// response as a + public async Task PostAllAsync(string endpoint, HttpContent content) => await PostAsync(endpoint, content).ConfigureAwait(false); + + /// + /// Makes a Get Request to OpenXDA to obtain a objects + /// + /// Path to specific API request + /// a object + public async Task> GetAllAsync(string requestURI) => await GetAsync>(requestURI).ConfigureAwait(false); + + #endregion + + } +} diff --git a/Libraries/openXDA.APIAuthentication/XDAAPIHelper.cs b/Libraries/openXDA.APIAuthentication/XDAAPIHelper.cs new file mode 100644 index 0000000..ace01df --- /dev/null +++ b/Libraries/openXDA.APIAuthentication/XDAAPIHelper.cs @@ -0,0 +1,149 @@ +//****************************************************************************************************** +// XDAAPIHelper.cs - Gbtc +// +// Copyright © 2023, 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 Eclipse Public License -v 1.0 (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/eclipse-1.0.php +// +// 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/15/2024 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace openXDA.APIAuthentication +{ + /// + /// Wrapper class that turns into a static for easy use across an application. + /// + /// + /// Must be initialized with the method to fetch settings. + /// + public static class XDAAPIHelper + { + #region [ Properties ] + /// + /// Tells users if the helper has been intialized or not. + /// + public static bool IsIntialized { + get + { + return !(API is null); + } + } + + /// + /// API Token used to access OpenXDA + /// + public static string Token => API.Token; + + /// + /// API Key used to access OpenXDA + /// + public static string Key => API.Key; + + /// + /// API Key used to access OpenXDA + /// + public static string Host => API.Host; + + /// + /// Object that retireves the token, key, and host of the helper. + /// + private static XDAAPI API { get; set; } + + #endregion + + #region [ Methods ] + /// + /// Function for setup of static helper. This must be ran once before using the helper. + /// + public static void InitializeHelper(IAPICredentialRetriever retriever) + { + API = new XDAAPI(retriever); + } + + /// + /// Recalls the setup settings function from to refetch settings. + /// + public static bool TryRefreshSettings() => API.TryRefreshSettings(); + + /// + /// Retrieves customer key from the claims principle. + /// + /// A value of signifies the current user is authorized to view any object the controller may retrieve. + /// A flag indicating if the operation was successful. + public static bool TryRetrieveCustomer(ClaimsPrincipal principal, out string customerKey) => API.TryRetrieveCustomer(principal, out customerKey); + + /// + /// Gets Response Task from XDA + /// + /// Path to specific API request + /// The of the request + /// response as a + public static Task GetResponseTask(string requestURI, HttpContent content = null) => API.GetResponseTask(requestURI, content); + + /// + /// Makes a Get Request to OpenXDA to obtain a string + /// + /// Path to specific API request + /// string + public static async Task GetAsync(string requestURI) => await API.GetAsync(requestURI).ConfigureAwait(false); + + /// + /// Makes a Get Request to OpenXDA to obtain a stream + /// + /// Path to specific API request + /// stream + public static async Task GetStreamAsync(string requestURI) => await API.GetStreamAsync(requestURI).ConfigureAwait(false); + + /// + /// Makes a Get Request to OpenXDA to obtain an object + /// + /// Path to specific API request + /// a object + public static async Task GetAsync(string requestURI) => await API.GetAsync(requestURI).ConfigureAwait(false); + + /// + /// Makes Post request on OpenXDA + /// + /// Path to specific API request + /// The of the request + /// response as a + public static async Task PostAsync(string requestURI, HttpContent content) => await API.PostAsync(requestURI, content).ConfigureAwait(false); + + /// + /// Makes Post request on OpenXDA + /// + /// Path to specific API request + /// The of the request + /// response as a + public static async Task PostAllAsync(string endpoint, HttpContent content) => await API.PostAllAsync(endpoint, content).ConfigureAwait(false); + + /// + /// Makes a Get Request to OpenXDA to obtain a objects + /// + /// Path to specific API request + /// a object + public static async Task> GetAllAsync(string requestURI) => await API.GetAllAsync(requestURI).ConfigureAwait(false); + + #endregion + + } +} diff --git a/Libraries/openXDA.APIAuthentication/openXDA.APIAuthentication.csproj b/Libraries/openXDA.APIAuthentication/openXDA.APIAuthentication.csproj new file mode 100644 index 0000000..facf64d --- /dev/null +++ b/Libraries/openXDA.APIAuthentication/openXDA.APIAuthentication.csproj @@ -0,0 +1,26 @@ + + + net9.0 + Debug;Development;Release + Library + false + openXDA.APIAuthentication + GPA + openXDA.APIAuthentication + Copyright © 2022 + + + ..\..\..\Build\Output\Debug\Libraries\ + ..\..\..\Build\Output\Debug\Libraries\openXDA.APIAuthentication.xml + + + ..\..\..\Build\Output\Release\Libraries\ + ..\..\..\Build\Output\Release\Libraries\openXDA.APIAuthentication.xml + + + + + + + + \ No newline at end of file diff --git a/Libraries/openXDA.Configuration/BreakerSection.cs b/Libraries/openXDA.Configuration/BreakerSection.cs new file mode 100644 index 0000000..d6d07ff --- /dev/null +++ b/Libraries/openXDA.Configuration/BreakerSection.cs @@ -0,0 +1,119 @@ +//****************************************************************************************************** +// BreakerSection.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: +// ---------------------------------------------------------------------------------------------------- +// 08/02/2015 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class BreakerSection + { + #region [ Members ] + + // Constants + public const string CategoryName = "Breakers"; + + #endregion + + #region [ Properties ] + + /// + /// Gets or sets the flag that determines whether to apply additional logic + /// to help obtain more accurate breaker timing results in cases where DC + /// current gradually drains from the line after the breaker is open. + /// + [Setting] + [DefaultValue(false)] + public bool ApplyDCOffsetLogic { get; set; } + + /// + /// Gets or sets the size of the window, in cycles, + /// to use when applying the DC offset logic. + /// + [Setting] + [DefaultValue(9.0D / 8.0D)] + public double DCOffsetWindowSize { get; set; } + + /// + /// Gets or sets the maximum number of cycles that a breaker + /// operation's timing can exceed the configured breaker speed. + /// + [Setting] + [DefaultValue(0.0D)] + public double LateBreakerThreshold { get; set; } + + /// + /// Gets or sets the minimum number of cycles that the breaker is expected + /// to remain closed after receiving the trip coil energized signal. + /// + /// + /// This value helps to prevent phase timing calculations when the current + /// signal is not large enough to detect the point at which the breaker opened. + /// + [Setting] + [DefaultValue(0.0D)] + public double MinCyclesBeforeOpen { get; set; } + + /// + /// Gets or sets the minimum amount of time, in cycles, the system must wait + /// before automatically reclosing after a breaker operation has occurred. + /// + [Setting] + [DefaultValue(15.0D)] + public double MinWaitBeforeReclose { get; set; } + + /// + /// Gets or sets the maximum RMS current, in amps, + /// at which the breaker can be considered open. + /// + [Setting] + [DefaultValue(20.0D)] + public double OpenBreakerThreshold { get; set; } + + /// + /// Gets or sets the minimum duration, in cycles, for which the current must remain + /// at zero in order for a subsequent current spike to be considered a restrike. + /// + [Setting] + [DefaultValue(0.125D)] + public double MinCyclesBeforeRestrike { get; set; } + + /// + /// Gets or sets the maximum duration, in cycles, for which the current must remain + /// at zero in order for a subsequent current spike to be considered a restrike. + /// + [Setting] + [DefaultValue(70.0D)] + public double MaxCyclesBeforeRestrike { get; set; } + + /// + /// Gets or sets the flag that determines whether the system should apply the + /// BreakerOpen event type to events. + /// + [Setting] + [DefaultValue(true)] + public bool BreakerOpenEventTypeEnabled { get; set; } + + #endregion + } +} diff --git a/Libraries/openXDA.Configuration/COMTRADESection.cs b/Libraries/openXDA.Configuration/COMTRADESection.cs new file mode 100644 index 0000000..e457f27 --- /dev/null +++ b/Libraries/openXDA.Configuration/COMTRADESection.cs @@ -0,0 +1,67 @@ +//****************************************************************************************************** +// COMTRADESection.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: +// ---------------------------------------------------------------------------------------------------- +// 06/07/2019 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Configuration; +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class COMTRADESection + { + #region [ Members ] + + // Constants + public const string CategoryName = "COMTRADE"; + + #endregion + + #region [ Properties ] + + public TimeSpan MinWaitTime { get; set; } + + [Setting] + [DefaultValue(false)] + public bool WaitForINF { get; set; } + + [Setting] + [DefaultValue(false)] + public bool UseRelaxedValidation { get; set; } + + [Setting] + [DefaultValue("SELECT ID FROM Meter WHERE Make = 'SEL'")] + public string Root2AdjustmentQuery { get; set; } + + [Setting] + [SettingName(nameof(MinWaitTime))] + [DefaultValue(15.0D)] + [EditorBrowsable(EditorBrowsableState.Never)] + public double _MinWaitTime + { + get => MinWaitTime.TotalSeconds; + set => MinWaitTime = TimeSpan.FromSeconds(value); + } + + #endregion + } +} diff --git a/Libraries/openXDA.Configuration/DataAnalysisSection.cs b/Libraries/openXDA.Configuration/DataAnalysisSection.cs new file mode 100644 index 0000000..d62142e --- /dev/null +++ b/Libraries/openXDA.Configuration/DataAnalysisSection.cs @@ -0,0 +1,120 @@ +//****************************************************************************************************** +// DataAnalysisSection.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/16/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class DataAnalysisSection + { + public const string CategoryName = "DataAnalysis"; + + /// + /// Gets or sets the threshold, in amps, at which the + /// current exceeds engineering reasonableness. + /// + [Setting] + [DefaultValue(0.1D)] + public double InterruptionThreshold { get; set; } + + /// + /// Gets or sets the units of measure to use + /// for lengths (line length and fault distance). + /// + [Setting] + [DefaultValue("miles")] + public string LengthUnits { get; set; } + + /// + /// Gets or sets the threshold, in amps, at which the + /// current exceeds engineering reasonableness. + /// + [Setting] + [DefaultValue(1000000.0D)] + public double MaxCurrent { get; set; } + + /// + /// Gets or sets the maximum duration, in seconds, + /// of the events processed by openXDA. + /// + [Setting] + [DefaultValue(0.0D)] + public double MaxEventDuration { get; set; } + + /// + /// Gets or sets the maximum number of hours beyond the current system time + /// before the time of the record indicates that the data is unreasonable. + /// + [Setting] + [DefaultValue(0.0D)] + public double MaxTimeOffset { get; set; } + + /// + /// Gets or sets the per-unit threshold at which the + /// voltage exceeds engineering reasonableness. + /// + [Setting] + [DefaultValue(2.0D)] + public double MaxVoltage { get; set; } + + /// + /// Gets or sets the maximum number of hours prior to the current system time + /// before the time of the record indicates that the data is unreasonable. + /// + [Setting] + [DefaultValue(0.0D)] + public double MinTimeOffset { get; set; } + + /// + /// Gets or sets the threshold, in amps, at which the + /// current exceeds engineering reasonableness. + /// + [Setting] + [DefaultValue(0.9D)] + public double SagThreshold { get; set; } + + /// + /// Gets or sets the threshold, in amps, at which the + /// current exceeds engineering reasonableness. + /// + [Setting] + [DefaultValue(1.1D)] + public double SwellThreshold { get; set; } + + /// + /// Gets or sets the system frequency. + /// + [Setting] + [DefaultValue(60.0D)] + public double SystemFrequency { get; set; } + + /// + /// Gets or sets the maximum distance, in seconds, + /// between a meter's clock and real time. + /// + [Setting] + [DefaultValue(0.5D)] + public double TimeTolerance { get; set; } + } +} diff --git a/Libraries/openXDA.Configuration/DataPusherSection.cs b/Libraries/openXDA.Configuration/DataPusherSection.cs new file mode 100644 index 0000000..78cd591 --- /dev/null +++ b/Libraries/openXDA.Configuration/DataPusherSection.cs @@ -0,0 +1,37 @@ +//****************************************************************************************************** +// DataPusherSection.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: +// ---------------------------------------------------------------------------------------------------- +// 02/08/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class DataPusherSection + { + public const string CategoryName = "DataPusher"; + + [Setting] + [DefaultValue("systemSettings")] + public string ConnectionString { get; set; } + } +} diff --git a/Libraries/openXDA.Configuration/EMAXSection.cs b/Libraries/openXDA.Configuration/EMAXSection.cs new file mode 100644 index 0000000..22ba7df --- /dev/null +++ b/Libraries/openXDA.Configuration/EMAXSection.cs @@ -0,0 +1,66 @@ +//****************************************************************************************************** +// EMAXSection.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: +// ---------------------------------------------------------------------------------------------------- +// 09/25/2017 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class EMAXSection + { + #region [ Members ] + + // Constants + public const string CategoryName = "EMAX"; + + #endregion + + #region [ Properties ] + + /// + /// Gets or sets the value that indicates whether timestamp + /// correction should be applied when reading native EMAX files. + /// + [Setting] + [DefaultValue(true)] + public bool ApplyTimestampCorrection { get; set; } + + /// + /// Gets or sets the value that indicates whether value + /// correction should be applied when reading native EMAX files. + /// + [Setting] + [DefaultValue(true)] + public bool ApplyValueCorrection { get; set; } + + /// + /// Gets or sets the path to the directory where COMTRADE + /// exports should be located after reading an EMAX file. + /// + [Setting] + [DefaultValue("")] + public string COMTRADEExportDirectory { get; set; } + + #endregion + } +} diff --git a/Libraries/openXDA.Configuration/EPRICapBankAnalyticSection.cs b/Libraries/openXDA.Configuration/EPRICapBankAnalyticSection.cs new file mode 100644 index 0000000..41d0f1d --- /dev/null +++ b/Libraries/openXDA.Configuration/EPRICapBankAnalyticSection.cs @@ -0,0 +1,89 @@ +//****************************************************************************************************** +// EPRICapBankAnalyticSection.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: +// ---------------------------------------------------------------------------------------------------- +// 08/04/2020 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class EPRICapBankAnalyticSection + { + public const string CategoryName = "EPRICapBankAnalytic"; + + [Setting] + [DefaultValue(false)] + public bool Enabled { get; set; } + + [Setting] + [DefaultValue("./CapBankAnalysis/Data/")] + public string DataFileLocation { get; set; } + + [Setting] + [DefaultValue("./CapBankAnalysis/Parameter/")] + public string ParameterFileLocation { get; set; } + + [Setting] + [DefaultValue("./CapBankAnalysis/Results/")] + public string ResultFileLocation { get; set; } + + [Setting] + [DefaultValue(500.0D)] + public double VThreshhold { get; set; } + + [Setting] + [DefaultValue(4.0D)] + public double IThreshhold { get; set; } + + [Setting] + [DefaultValue(10.0D)] + public double THDLimit { get; set; } + + [Setting] + [DefaultValue(1.0D)] + public double Toffset { get; set; } + + [Setting] + [DefaultValue(true)] + public bool EvalPreInsertion { get; set; } + + [Setting] + [DefaultValue("openXDA.CapSwitchAnalysis.dll")] + public string AnalysisRoutineAssembly { get; set; } + + [Setting] + [DefaultValue("openXDA.CapSwitchAnalysis.Analyzer.RunAnalytic")] + public string AnalysisRoutineMethod { get; set; } + + [Setting] + [DefaultValue("")] + public string Analyzer { get; set; } + + [Setting] + [DefaultValue(1200000)] + public int Delay { get; set; } + + [Setting] + [DefaultValue(false)] + public bool KeepFiles { get; set; } + } +} diff --git a/Libraries/openXDA.Configuration/Edition/EditionChecker.cs b/Libraries/openXDA.Configuration/Edition/EditionChecker.cs new file mode 100644 index 0000000..2d4d7d7 --- /dev/null +++ b/Libraries/openXDA.Configuration/Edition/EditionChecker.cs @@ -0,0 +1,98 @@ +//****************************************************************************************************** +// EditionChecker.cs - Gbtc +// +// Copyright © 2023, 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 Eclipse Public License -v 1.0 (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/eclipse-1.0.php +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 10/07/2024 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using Gemstone.Configuration; +using Gemstone.Data; +using Gemstone.Data.Model; +using openXDA.Model; + +namespace openXDA.Configuration +{ + /// + /// Enum to hold different editions of XDA + /// + public enum Edition + { + Base = 0, + Enterprise = 1 + } + + /// + /// Helper class that checks the edition of XDA + /// + public static class EditionChecker + { + /// + /// Current XDA Edition + /// + private static Edition CheckedEdition { get; set; } + // ToDo: Think about replacing this with a hash function instead + private static Guid MagicGuid = new Guid("aa644f0c-a82b-4cf1-ba6e-be3a1b05eb6a"); + + static EditionChecker() + { + UpdateEdition(); + } + + /// + /// Checks to see if current edition is equal to or higher than supplied + /// + /// response as a + public static bool CheckEdition(Edition editionLevel) + { + return CheckedEdition.CompareTo(editionLevel) >= 0; + } + + /// + /// Updates the current edition based on the value in the database + /// + public static void UpdateEdition() + { + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) + { + Guid value; + try + { + value = new Guid(new TableOperations(connection).QueryRecordWhere($"Name = 'System.EditionKey'")?.Value); + } + catch + { + value = new Guid("00000000-0000-0000-0000-000000000000"); + } + if (value == MagicGuid) CheckedEdition = Edition.Enterprise; + else CheckedEdition = Edition.Base; + } + } + + /// + /// Returns the current edition + /// + /// response as a + public static Edition GetEdition() + { + return CheckedEdition; + } + + } +} diff --git a/Libraries/openXDA.Configuration/Edition/HttpEditionFilterAttribute.cs b/Libraries/openXDA.Configuration/Edition/HttpEditionFilterAttribute.cs new file mode 100644 index 0000000..79368e2 --- /dev/null +++ b/Libraries/openXDA.Configuration/Edition/HttpEditionFilterAttribute.cs @@ -0,0 +1,57 @@ +//****************************************************************************************************** +// HttpEditionFilterAttirbute.cs - Gbtc +// +// Copyright © 2023, 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 Eclipse Public License -v 1.0 (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/eclipse-1.0.php +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 10/07/2024 - Gabriel Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace openXDA.Configuration +{ + /// + /// Defines an attribute that will disallow an http method if it does not pass the edition requirement. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] + public class HttpEditionFilterAttribute : ActionFilterAttribute + { + /// + /// Gets the edition needed specified by attribute construction. + /// + public Edition EditionRequred { get; } + public HttpEditionFilterAttribute(Edition edition) + { + EditionRequred = edition; + } + + /// + /// Creates a new . + /// + /// Edition this http method requires. + public override void OnActionExecuting(ActionExecutingContext actionContext) + { + // Wrong edition: skip the method and return forbidden + if (!EditionChecker.CheckEdition(EditionRequred)) + actionContext.Result = new ForbidResult(); + + base.OnActionExecuting(actionContext); + } + } +} diff --git a/Libraries/openXDA.Configuration/EmailSection.cs b/Libraries/openXDA.Configuration/EmailSection.cs new file mode 100644 index 0000000..c092607 --- /dev/null +++ b/Libraries/openXDA.Configuration/EmailSection.cs @@ -0,0 +1,111 @@ +//****************************************************************************************************** +// EmailSection.cs - Gbtc +// +// Copyright © 2015, 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: +// ---------------------------------------------------------------------------------------------------- +// 08/02/2015 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.StringExtensions; +using System.ComponentModel; +using System.Configuration; +using System.Security; + +namespace openXDA.Configuration +{ + public class EmailSection + { + #region [ Members ] + + // Constants + public const string CategoryName = "Email"; + + #endregion + + #region [ Properties ] + + /// + /// Gets or sets the address used by the administrator of the email system. + /// + [Setting] + [DefaultValue("xda-admin@gridprotectionalliance.org")] + public string AdminAddress { get; set; } + + /// + /// Gets or sets the address used in the To line when blind-copying recipients. + /// + [Setting] + [DefaultValue("")] + public string BlindCopyAddress { get; set; } + + /// + /// Gets or sets the hostname or IP address of the SMTP server to + /// use for sending automated email notifications when faults occur. + /// + [Setting] + [DefaultValue("")] + public string SMTPServer { get; set; } + + /// + /// Gets or sets the email address used when sending automated email notifications. + /// + [Setting] + [DefaultValue("openXDA@gridprotectionalliance.org")] + public string FromAddress { get; set; } + + /// + /// Gets or sets the username used to authenticate to the SMTP server. + /// + [Setting] + [DefaultValue("")] + public string Username { get; set; } + + /// + /// Gets or sets the password used to authenticate to the SMTP server. + /// + [Setting] + [DefaultValue(null)] + public string Password + { + get => SecurePassword.ToUnsecureString() ?? ""; + set => SecurePassword = value?.ToSecureString() ?? new SecureString(); + } + + /// + /// Gets or sets the flag that determines whether to enable + /// SSL when establishing communications with the SMTP server. + /// + [Setting] + [DefaultValue(false)] + public bool EnableSSL { get; set; } + + /// + /// Sets the minimum number of samples in an email chart + /// + [Setting] + [DefaultValue(-1)] + public int MinimumChartSamplesPerCycle { get; set; } + + /// + /// Gets the password as a . + /// + public SecureString SecurePassword { get; private set; } + + #endregion + } +} diff --git a/Libraries/openXDA.Configuration/EventEmailSection.cs b/Libraries/openXDA.Configuration/EventEmailSection.cs new file mode 100644 index 0000000..64f4879 --- /dev/null +++ b/Libraries/openXDA.Configuration/EventEmailSection.cs @@ -0,0 +1,68 @@ +//****************************************************************************************************** +// EventEmailSection.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: +// ---------------------------------------------------------------------------------------------------- +// 02/13/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; +using Gemstone.Configuration; + +namespace openXDA.Configuration +{ + public class EventEmailSection + { + public const string CategoryName = "EventEmail"; + + [Setting] + [DefaultValue(false)] + public bool Enabled { get; set; } + + [Setting] + [DefaultValue(0)] + public int MaxEmailCount { get; set; } + + [Setting] + [SettingName(nameof(MaxEmailSpan))] + [DefaultValue(0.0D)] + public double MaxEmailSeconds + { + get => MaxEmailSpan.TotalSeconds; + set => MaxEmailSpan = TimeSpan.FromSeconds(value); + } + + [Setting] + [DefaultValue("http://localhost:8989/RestoreEventEmail.cshtml")] + public string RestorationURL { get; set; } + + public TimeSpan MaxEmailSpan { get; set; } + + [Setting] + [SettingName(nameof(EmailDuplicateThresholdMinutes))] + [DefaultValue(0.0D)] + public double EmailDuplicateThresholdMinutes + { + get => EmailDuplicateThreshold.TotalMinutes; + set => EmailDuplicateThreshold = TimeSpan.FromMinutes(value); + } + + public TimeSpan EmailDuplicateThreshold { get; set; } + } +} diff --git a/Libraries/openXDA.Configuration/FaultLocationSection.cs b/Libraries/openXDA.Configuration/FaultLocationSection.cs new file mode 100644 index 0000000..a9e6e72 --- /dev/null +++ b/Libraries/openXDA.Configuration/FaultLocationSection.cs @@ -0,0 +1,104 @@ +//****************************************************************************************************** +// FaultLocationSection.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: +// ---------------------------------------------------------------------------------------------------- +// 08/02/2015 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public enum FaultCalculationCycleMethod + { + MaxCurrent, + LastFaultedCycle, + LastFaultedCycleExceptAirGapRes + } + + public class FaultLocationSection + { + #region [ Members ] + + // Constants + public const string CategoryName = "FaultLocation"; + + #endregion + + #region [ Properties ] + + [Setting] + [DefaultValue(5.0D)] + public double PrefaultTrigger { get; set; } + + [Setting] + [DefaultValue(50.0D)] + public double PrefaultTriggerAdjustment { get; set; } + + [Setting] + [DefaultValue(1.05D)] + public double MaxFaultDistanceMultiplier { get; set; } + + [Setting] + [DefaultValue(-0.05D)] + public double MinFaultDistanceMultiplier { get; set; } + + [Setting] + [DefaultValue(FaultCalculationCycleMethod.MaxCurrent)] + public FaultCalculationCycleMethod FaultCalculationCycleMethod { get; set; } + + [Setting] + [DefaultValue(1.0D)] + public double MinFaultSegmentCycles { get; set; } + + [Setting] + [DefaultValue(10)] + public int FaultClearingAdjustmentSamples { get; set; } + + [Setting] + [DefaultValue(false)] + public bool WarnMissingDetectionLogic { get; set; } + + /// + /// Indicates whether to use the default fault detection logic + /// when the line-specific fault detection logic fails. + /// + [Setting] + [DefaultValue(true)] + public bool UseDefaultFaultDetectionLogic { get; set; } + + /// + /// Indicates whether to ignore line-specific fault detection logic. + /// + [Setting] + [DefaultValue(false)] + public bool IgnoreFaultDetectionLogic { get; set; } + + [Setting] + [DefaultValue(0.8D)] + public double FaultedVoltageThreshold { get; set; } + + [Setting] + [DefaultValue(0.001D)] + public double GroundedFaultVoltageThreshold { get; set; } + + #endregion + } +} diff --git a/Libraries/openXDA.Configuration/FileEnumeratorSection.cs b/Libraries/openXDA.Configuration/FileEnumeratorSection.cs new file mode 100644 index 0000000..0c403c3 --- /dev/null +++ b/Libraries/openXDA.Configuration/FileEnumeratorSection.cs @@ -0,0 +1,80 @@ +//****************************************************************************************************** +// FileEnumeratorSection.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/16/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; +using Gemstone.IO; + +namespace openXDA.Configuration +{ + //copied from GSF.IO as there was no equivalent in Gemstone.IO + public enum FileEnumerationStrategy + { + // + // Summary: + // Enumerate all files sequentially. + Sequential, + // + // Summary: + // Enumerates the watch directories in parallel, but subdirectories are processed + // sequentially. + ParallelWatchDirectories, + // + // Summary: + // Enumerates every directory, including subdirectories, in parallel. + ParallelSubdirectories, + // + // Summary: + // Does not enumerate directories, relying only on the file watcher to handle file + // processing events. + None + } + + public class FileEnumeratorSection + { + public const string CategoryName = "FileEnumerator"; + + /// + /// Gets or sets the patterns used to determine which + /// folders to skip when enumerating watch directories. + /// + [Setting] + [DefaultValue("")] + public string FolderExclusion { get; set; } + + /// + /// Gets or sets the flag that determines whether the file watcher + /// should raise events for enumerated files in alphabetical order. + /// + [Setting] + [DefaultValue(false)] + public bool OrderedEnumeration { get; set; } + + /// + /// Gets or sets the strategy used for enumeration of files in the file watcher. + /// + [Setting] + [DefaultValue(FileEnumerationStrategy.ParallelSubdirectories)] + public FileEnumerationStrategy Strategy { get; set; } + } +} diff --git a/Libraries/openXDA.Configuration/FileProcessorSection.cs b/Libraries/openXDA.Configuration/FileProcessorSection.cs new file mode 100644 index 0000000..2d89b5a --- /dev/null +++ b/Libraries/openXDA.Configuration/FileProcessorSection.cs @@ -0,0 +1,91 @@ +//****************************************************************************************************** +// FileProcessorSection.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/16/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class FileProcessorSection + { + public const string CategoryName = "FileProcessor"; + + /// + /// Gets or sets the pattern used to parse file paths in + /// order to identify the meter that the file came from. + /// + [Setting] + [DefaultValue(/*lang=regex*/ @"(?[^\\]+)\\[^\\]+$")] + public string FilePattern { get; set; } + + /// + /// Gets or sets the pattern that identifies the part of the file path that + /// + [Setting] + [DefaultValue(/*lang=regex*/ @"^(?.*)\.[^\.]*$")] + public string FileGroupingPattern { get; set; } + + /// + /// Gets or sets the maximum number of hours prior to the current system time + /// before the file creation time indicates that the data should not be processed. + /// + [Setting] + [DefaultValue(0.0D)] + public double MaxFileCreationTimeOffset { get; set; } + + /// + /// Gets or sets the maximum file size, in MB, + /// of the files processed by openXDA. + /// + [Setting] + [DefaultValue(30.0D)] + public double MaxFileSize { get; set; } + + /// + /// Gets or sets the number of threads used + /// for processing file data concurrently. + /// + /// + /// Values less than or equal to zero will be set to the number of logical processors. + /// + [Setting] + [DefaultValue(0)] + public int ProcessingThreadCount + { + get + { + return _ProcessingThreadCount; + } + set + { + _ProcessingThreadCount = value; + + if (_ProcessingThreadCount <= 0) + _ProcessingThreadCount = Environment.ProcessorCount; + } + } + + private int _ProcessingThreadCount { get; set; } + } +} diff --git a/Libraries/openXDA.Configuration/FilePrunerSection.cs b/Libraries/openXDA.Configuration/FilePrunerSection.cs new file mode 100644 index 0000000..2c9a4c2 --- /dev/null +++ b/Libraries/openXDA.Configuration/FilePrunerSection.cs @@ -0,0 +1,58 @@ +//****************************************************************************************************** +// FilePrunerSection.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: +// ---------------------------------------------------------------------------------------------------- +// 02/08/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Configuration; +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class FilePrunerSection + { + public const string CategoryName = "FilePruner"; + + [Setting] + [DefaultValue("* 0 * * *")] + public string Schedule { get; set; } + + /// + /// Gets or sets the amount of time, in days, + /// the file pruner should keep files in openXDA's + /// watch directories. + /// + [Setting] + [SettingName(nameof(RetentionPeriod))] + [DefaultValue(0)] + public int RetentionPeriodDays + { + get => (int)RetentionPeriod.TotalDays; + set => RetentionPeriod = TimeSpan.FromDays(value); + } + + /// + /// Gets or sets the amount of time the file pruner + /// should keep files in openXDA's watch directories. + /// + public TimeSpan RetentionPeriod { get; set; } + } +} diff --git a/Libraries/openXDA.Configuration/FileWatcherSection.cs b/Libraries/openXDA.Configuration/FileWatcherSection.cs new file mode 100644 index 0000000..4b25e6c --- /dev/null +++ b/Libraries/openXDA.Configuration/FileWatcherSection.cs @@ -0,0 +1,308 @@ +//****************************************************************************************************** +// FileWatcherSection.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/16/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Configuration; +using Gemstone.IO; +using Gemstone.StringExtensions; +using System.ComponentModel; +using System.Configuration; +using System.Security; +using FileShare = openXDA.Configuration.FileWatcher.FileShare; + +namespace openXDA.Configuration +{ + namespace FileWatcher + { + /// + /// Represents a file share. + /// + public class FileShare + { + #region [ Members ] + + // Fields + private string m_name; + private string m_domain; + private string m_username; + private SecureString m_password; + + private Exception m_authenticationException; + + #endregion + + #region [ Constructors ] + + /// + /// Creates a new instance of the class. + /// + public FileShare() + { + } + + /// + /// Creates a new instance of the class. + /// + /// A string containing the file share parameters as key-value pairs. + public FileShare(string connectionString) + { + ConnectionStringParser parser = new ConnectionStringParser(); + parser.ParseConnectionString(connectionString, this); + } + + #endregion + + #region [ Properties ] + + /// + /// Gets or sets the name of the file share (\\server\share). + /// + [Setting] + [DefaultValue("")] + public string Name + { + get + { + return m_name; + } + set + { + m_name = value; + } + } + + /// + /// Gets or sets the domain of the user used to authenticate to the file share. + /// + [Setting] + [DefaultValue("")] + public string Domain + { + get + { + return m_domain; + } + set + { + m_domain = value; + } + } + + /// + /// Gets or sets the username of the user used to authenticate to the file share. + /// + [Setting] + [DefaultValue("")] + public string Username + { + get + { + return m_username; + } + set + { + string[] splitValue; + + m_username = value; + + if ((object)value != null) + { + splitValue = value.Split('\\'); + + if (splitValue.Length == 2) + { + m_domain = splitValue[0]; + m_username = splitValue[1]; + } + } + } + } + + /// + /// Gets or sets the password of the user used to authenticate to the file share. + /// + [Setting] + [DefaultValue("")] + public string Password + { + get + { + return m_password.ToUnsecureString() ?? ""; + } + set + { + m_password = value.ToSecureString() ?? new SecureString(); + } + } + + /// + /// Gets the exception encountered during the most recent authentication attempt. + /// + public Exception AuthenticationException + { + get + { + return m_authenticationException; + } + } + + #endregion + + #region [ Methods ] + + /// + /// Attempts to authenticate to the file share. + /// + public void Authenticate() + { + try + { + FilePath.ConnectToNetworkShare(m_name, m_username, Password, m_domain); + m_authenticationException = null; + } + catch (Exception ex) + { + m_authenticationException = ex; + throw; + } + } + + /// + /// Attempts to authenticate to the file share. + /// + /// True if successfully authenticated. False otherwise. + public bool TryAuthenticate() + { + try + { + Authenticate(); + return true; + } + catch + { + return false; + } + } + + #endregion + } + } + + public class FileWatcherSection + { + public const string CategoryName = "FileWatcher"; + + /// + /// Gets or sets the size of the + /// s' internal buffers. + /// + /// + [Setting] + [DefaultValue(65536)] + public int BufferSize { get; set; } + + /// + /// Gets or sets a list of parameters used + /// to authenticate to multiple file shares. + /// + [Setting] + [DefaultValue("")] + public string FileShares + { + get + { + return _FileShares; + } + set + { + _FileShares = value; + + _FileShareList = value.ToNonNullString().ParseKeyValuePairs() + .Select(kvp => kvp.Value) + .Select(fileShareString => new FileShare(fileShareString)) + .ToList(); + } + } + + /// + /// Gets a list of file shares to be authenticated at startup. + /// + public IReadOnlyCollection FileShareList => _FileShareList.AsReadOnly(); + + private string _FileShares { get; set; } + private List _FileShareList { get; set; } + + /// + /// Gets or sets the number of threads used + /// internally to the file processor. + /// + /// + /// Values less than or equal to zero will be set to the number of logical processors. + /// + [Setting] + [DefaultValue(0)] + public int InternalThreadCount + { + get => _InternalThreadCount; + set => _InternalThreadCount = (value > 0) + ? value + : Environment.ProcessorCount; + } + + private int _InternalThreadCount { get; set; } + + /// + /// Gets or sets the list of directories to watch for files. + /// + [Setting] + [DefaultValue("Watch")] + public string WatchDirectories + + { + get + { + return _WatchDirectories; + } + set + { + _WatchDirectories = value; + + if ((object)value != null) + { + _WatchDirectoryList = value + .Split(Path.PathSeparator) + .Select(path => path.Trim()) + .ToList(); + } + } + } + + /// + /// Gets a list of directories to be watched + /// for files containing fault records. + /// + public IReadOnlyCollection WatchDirectoryList => _WatchDirectoryList.AsReadOnly(); + + private string _WatchDirectories { get; set; } + private List _WatchDirectoryList { get; set; } + } +} diff --git a/Libraries/openXDA.Configuration/GrafanaSection.cs b/Libraries/openXDA.Configuration/GrafanaSection.cs new file mode 100644 index 0000000..a6d0bc3 --- /dev/null +++ b/Libraries/openXDA.Configuration/GrafanaSection.cs @@ -0,0 +1,72 @@ +//****************************************************************************************************** +// GrafanaSection.cs - Gbtc +// +// Copyright © 2023, 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: +// ---------------------------------------------------------------------------------------------------- +// 08/18/2023 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class GrafanaSection + { + public const string CategoryName = "Grafana"; + + /// + /// Gets or sets the base path to the Grafana server executable. + /// + [Setting] + [DefaultValue(@"Grafana\bin\grafana-server.exe")] + public string ServerPath { get; set; } + + /// + /// Gets or sets the base path to the + /// folder containing the Grafana process. + /// + [Setting] + [DefaultValue("Grafana")] + public string BasePath { get; set; } + + [Setting] + [DefaultValue("http://localhost:8185")] + public string HostedURL { get; set; } + + [Setting] + [DefaultValue("admin")] + public string AdminUser { get; set; } + + [Setting] + [DefaultValue(1)] + public int OrganizationID { get; set; } + + [Setting] + [DefaultValue("X-WEBAUTH-USER")] + public string AuthProxyHeaderName { get; set; } + + [Setting] + [DefaultValue("x-last-dashboard")] + public string LastDashboardCookieName { get; set; } + + [Setting] + [DefaultValue(30)] + public int InitializationTimeout { get; set; } + } +} diff --git a/Libraries/openXDA.Configuration/LSCVSSection.cs b/Libraries/openXDA.Configuration/LSCVSSection.cs new file mode 100644 index 0000000..3cd00c0 --- /dev/null +++ b/Libraries/openXDA.Configuration/LSCVSSection.cs @@ -0,0 +1,68 @@ +//****************************************************************************************************** +// LSCVSSection.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: +// ---------------------------------------------------------------------------------------------------- +// 04/11/2022 - G. Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class LSCVSSection + { + public const string CategoryName = "LSCVS"; + + /// + /// URL string of the LSCVS report server name. + /// + [Setting] + [DefaultValue("http://localhost/LSCVSReport")] + public string URL { get; set; } + + /// + /// The API Key to use to authenticate to LSCVS + /// + [Setting] + [DefaultValue("")] + public string APIKey { get; set; } + + /// + /// The API Token to use to authenticate to LSCVS + /// + [Setting] + [DefaultValue("")] + public string APIToken { get; set; } + + /// + /// MW ratio threshold to report LSCVS event. + /// + [Setting] + [DefaultValue(0.6D)] + public double ReportingThreshold { get; set; } + + /// + /// Ratio threshold for determining type III sag faults over type II or I. + /// + [Setting] + [DefaultValue(0.95D)] + public double TypeThreshold { get; set; } + } +} diff --git a/Libraries/openXDA.Configuration/OSIPISection.cs b/Libraries/openXDA.Configuration/OSIPISection.cs new file mode 100644 index 0000000..b5d8405 --- /dev/null +++ b/Libraries/openXDA.Configuration/OSIPISection.cs @@ -0,0 +1,71 @@ +//****************************************************************************************************** +// OSIPISection.cs - Gbtc +// +// Copyright © 2025, 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/21/2025 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class OSIPISection + { + #region [ Members ] + + // Constants + public const string CategoryName = "OSIPI"; + + #endregion + + #region [ Properties ] + + /// + /// Gets or sets name of the PI historian server to connect to. + /// + [Setting] + [DefaultValue("")] + public string ServerName { get; set; } + + /// + /// Gets or sets user name for logging into PI historian server. + /// + [Setting] + [DefaultValue("")] + public string UserName { get; set; } + + /// + /// Gets or sets password for logging into PI historian server. + /// + [Setting] + [DefaultValue("")] + public string Password { get; set; } + + /// + /// Gets or sets time to wait when establishing + /// connection to PI server before failing. + /// + [Setting] + [DefaultValue(30000)] + public int ConnectTimeout { get; set; } + + #endregion + } +} diff --git a/Libraries/openXDA.Configuration/PQDIFSection.cs b/Libraries/openXDA.Configuration/PQDIFSection.cs new file mode 100644 index 0000000..670a5fd --- /dev/null +++ b/Libraries/openXDA.Configuration/PQDIFSection.cs @@ -0,0 +1,46 @@ +//****************************************************************************************************** +// PQDIFSection.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: +// ---------------------------------------------------------------------------------------------------- +// 10/08/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class PQDIFSection + { + #region [ Members ] + + // Constants + public const string CategoryName = "PQDIF"; + + #endregion + + #region [ Properties ] + + [Setting] + [DefaultValue(false)] + public bool AllowMultipleDataSource { get; set; } + + #endregion + } +} diff --git a/Libraries/openXDA.Configuration/PQISection.cs b/Libraries/openXDA.Configuration/PQISection.cs new file mode 100644 index 0000000..bbdeb1f --- /dev/null +++ b/Libraries/openXDA.Configuration/PQISection.cs @@ -0,0 +1,66 @@ +//****************************************************************************************************** +// PQISection.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: +// ---------------------------------------------------------------------------------------------------- +// 07/29/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class PQISection + { + #region [ Members ] + + // Constants + public const string CategoryName = "PQI"; + + #endregion + + #region [ Properties ] + + [Setting] + [DefaultValue("https://pqiws.epri.com")] + public string BaseURL { get; set; } + + [Setting] + [DefaultValue("https://go.epri.com")] + public string PingURL { get; set; } + + [Setting] + [DefaultValue(null)] + public string ClientID { get; set; } + + [Setting] + [DefaultValue(null)] + public string ClientSecret { get; set; } + + [Setting] + [DefaultValue(null)] + public string Username { get; set; } + + [Setting] + [DefaultValue(null)] + public string Password { get; set; } + + #endregion + } +} diff --git a/Libraries/openXDA.Configuration/Properties/AssemblyInfo.cs b/Libraries/openXDA.Configuration/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..0666cda --- /dev/null +++ b/Libraries/openXDA.Configuration/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("openXDA.Configuration")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("openXDA.Configuration")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("83d8122c-2bcc-4813-b449-224d9f7e5aee")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("3.0.5.93")] +[assembly: AssemblyVersion("3.0.5.93")] +[assembly: AssemblyFileVersion("3.0.5.93")] diff --git a/Libraries/openXDA.Configuration/RabbitMQSection.cs b/Libraries/openXDA.Configuration/RabbitMQSection.cs new file mode 100644 index 0000000..12e6900 --- /dev/null +++ b/Libraries/openXDA.Configuration/RabbitMQSection.cs @@ -0,0 +1,90 @@ +//****************************************************************************************************** +// RabbitMQSection.cs - Gbtc +// +// Copyright © 2025, 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: +// ---------------------------------------------------------------------------------------------------- +// 11/01/2025 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class RabbitMQSection + { + public const string CategoryName = "RabbitMQ"; + + /// + /// The RabbbitMQ Server Hostname or IP Address. + /// + [Setting] + [DefaultValue("localhost")] + public string Hostname { get; set; } = "localhost"; + + /// + /// Defines the port RabbitMQ is listening on. + /// + [Setting] + [DefaultValue(5672)] + public int Port { get; set; } = 5672; + + /// + /// Defines the name of the exchange to listen to. + /// + [Setting] + [DefaultValue("openxda")] + public string ExchangeName { get; set; } = "openxda"; + + /// + /// Defines the routing key used to listen for messages. + /// + [Setting] + [DefaultValue("openxda")] + public string RoutingKey { get; set; } = "openxda"; + + /// + /// Defines a flag that determines if RabbitMQ integration is enabled. + /// + [Setting] + [DefaultValue(false)] + public bool Enabled { get; set; } = false; + + + /// + /// Defines the routing key used for outbound messages. + /// + [Setting] + [DefaultValue("analytic")] + public string OutboundRoutingKey { get; set; } = "analytic"; + + /// + /// Defines the username used for authenticating to the RabbitMQ service. + /// + [Setting] + [DefaultValue("guest")] + public string UserName { get; set; } = "guest"; + + /// + /// Defines the password used for authenticating to the RabbitMQ service. + /// + [Setting] + [DefaultValue("guest")] + public string Password { get; set; } = "guest"; + } +} diff --git a/Libraries/openXDA.Configuration/SCADASection.cs b/Libraries/openXDA.Configuration/SCADASection.cs new file mode 100644 index 0000000..acb47c9 --- /dev/null +++ b/Libraries/openXDA.Configuration/SCADASection.cs @@ -0,0 +1,94 @@ +//****************************************************************************************************** +// SCADASection.cs - Gbtc +// +// Copyright © 2025, 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/21/2025 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class SCADASection + { + #region [ Members ] + + // Nested Types + public enum SCADAHistorian + { + None, + eDNA, + OSIPI + } + + // Constants + public const string CategoryName = "SCADA"; + + #endregion + + #region [ Properties ] + + /// + /// Gets or sets historian used by the SCADA system. + /// + [Setting] + [DefaultValue(SCADAHistorian.None)] + public SCADAHistorian Historian { get; set; } + + /// + /// Gets or sets the database query to get the list of + /// SCADA points that indicate breaker state for the line. + /// + [Setting] + [DefaultValue("SELECT NULL AS Point WHERE 1 IS NULL")] + public string PointQuery { get; set; } + + /// + /// Gets or sets the tolerance, in seconds, that determines the + /// time range to be queried around the fault clearing point. + /// + [Setting] + [DefaultValue(4.0D)] + public double QueryTolerance { get; set; } + + /// + /// Gets or sets the value of the point representing + /// breaker state when the breaker is open. + /// + [Setting] + [DefaultValue(0.0D)] + public double BreakerOpenValue { get; set; } + + /// + /// Gets the tolerance that determines the time range + /// to be queried around the fault clearing point. + /// + public TimeSpan QueryToleranceSpan + { + get + { + return TimeSpan.FromSeconds(QueryTolerance); + } + } + + #endregion + } +} diff --git a/Libraries/openXDA.Configuration/SSAMSSection.cs b/Libraries/openXDA.Configuration/SSAMSSection.cs new file mode 100644 index 0000000..dff7379 --- /dev/null +++ b/Libraries/openXDA.Configuration/SSAMSSection.cs @@ -0,0 +1,73 @@ +//****************************************************************************************************** +// SSAMSSection.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: +// ---------------------------------------------------------------------------------------------------- +// 02/08/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class SSAMSSection + { + public const string CategoryName = "SSAMS"; + + private const string DefaultDataProviderString = + "AssemblyName={System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089}; " + + "ConnectionType=System.Data.SqlClient.SqlConnection; " + + "AdapterType=System.Data.SqlClient.SqlDataAdapter;"; + + /// + /// Cron string frequency at which the SSAMS process is scheduled. + /// + [Setting] + [DefaultValue("* 0 * * *")] + public string Schedule { get; set; } + + /// + /// Defines the connection string of the SSAMS DB. + /// + [Setting] + [DefaultValue("Data Source=localhost; Initial Catalog=openXDA; Integrated Security=SSPI")] + public string ConnectionString { get; set; } + + /// + /// Defines the connection string of the SSAMS DB. + /// + [Setting] + [DefaultValue(DefaultDataProviderString)] + public string DataProviderString { get; set; } + + /// + /// Command or procedure name that defines the command executed by the external DB. + /// + [Setting] + [DefaultValue("sp_LogSsamEvent")] + public string DatabaseCommand { get; set; } + + /// + /// Parameters for the external DB procedure. + /// + [Setting] + [DefaultValue("1,1,'OpenXDA_HEARTBEAT','','OpenXDA adapter heartbeat at {Timestamp} UTC',''")] + public string CommandParameters { get; set; } + } +} diff --git a/Libraries/openXDA.Configuration/Subscription.cs b/Libraries/openXDA.Configuration/Subscription.cs new file mode 100644 index 0000000..eac8f87 --- /dev/null +++ b/Libraries/openXDA.Configuration/Subscription.cs @@ -0,0 +1,65 @@ +//****************************************************************************************************** +// SubscriptionSection.cs - Gbtc +// +// Copyright © 2015, 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: +// ---------------------------------------------------------------------------------------------------- +// 08/02/2015 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class SubscriptionSection + { + #region [ Members ] + + // Constants + public const string CategoryName = "Subscription"; + + #endregion + + #region [ Properties ] + + /// + /// Gets or sets the Subject Line used when sending the confirmation email + /// + [Setting] + [DefaultValue("OpenXDA Confirm Email")] + public string ConfirmSubject { get; set; } + + /// + /// Gets or sets the Template used when generating the Confirmation Email + /// + [Setting] + [DefaultValue("Please click the following Link to confirm your email address \n http://localhost/SystemCenterNotification/ConfirmEmail")] + public string ConfirmTemplate { get; set; } + + + /// + /// Gets or sets the flag that determines whether a confirmed Email is Required + /// when subscribing to a notification + /// + [Setting] + [DefaultValue(true)] + public bool RequireConfirmation { get; set; } + + #endregion + } +} diff --git a/Libraries/openXDA.Configuration/SystemSection.cs b/Libraries/openXDA.Configuration/SystemSection.cs new file mode 100644 index 0000000..4649edf --- /dev/null +++ b/Libraries/openXDA.Configuration/SystemSection.cs @@ -0,0 +1,109 @@ +//****************************************************************************************************** +// SystemSection.cs - Gbtc +// +// Copyright © 2014, 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 Eclipse Public License -v 1.0 (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/eclipse-1.0.php +// +// 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: +// ---------------------------------------------------------------------------------------------------- +// 09/30/2014 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + /// + /// Represents the system settings for openXDA. + /// + public class SystemSection + { + public const string CategoryName = "System"; + + /// + /// Gets or sets the time zone identifier for the time zone + /// used by meters in the system unless configured otherwise. + /// + [Setting] + [DefaultValue("UTC")] + public string DefaultMeterTimeZone + { + get => DefaultMeterTimeZoneInfo.Id; + set => DefaultMeterTimeZoneInfo = string.IsNullOrEmpty(value) + ? TimeZoneInfo.Local + : TimeZoneInfo.FindSystemTimeZoneById(value); + } + + /// + /// Gets the used by meters + /// in the system unless configured otherwise. + /// + public TimeZoneInfo DefaultMeterTimeZoneInfo { get; private set; } + + /// + /// Gets or sets the amount of time each database + /// query is given to complete, in seconds. + /// + [Setting] + [DefaultValue(120)] + public int DbTimeout { get; set; } + + /// + /// Gets or sets the time zone identifier for the + /// time zone used by openXDA to store data. + /// + /// + /// The default value for this setting (empty string) + /// causes the setting to assume the value of the local + /// time zone of the system openXDA is running on. + /// + [Setting] + [DefaultValue("UTC")] + public string XDATimeZone + { + get + { + return _XDATimeZone; + } + set + { + if (string.IsNullOrEmpty(value)) + _XDATimeZone = TimeZoneInfo.Local.Id; + else + _XDATimeZone = value; + } + } + + /// + /// Gets the for the + /// time zone used by openXDA to store data. + /// + public TimeZoneInfo XDATimeZoneInfo + { + get + { + if ((object)_XDATimeZoneInfo == null) + _XDATimeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(_XDATimeZone); + + return _XDATimeZoneInfo; + } + } + + private string _XDATimeZone { get; set; } + private TimeZoneInfo _XDATimeZoneInfo { get; set; } + } +} diff --git a/Libraries/openXDA.Configuration/TaskProcessorSection.cs b/Libraries/openXDA.Configuration/TaskProcessorSection.cs new file mode 100644 index 0000000..e42b01f --- /dev/null +++ b/Libraries/openXDA.Configuration/TaskProcessorSection.cs @@ -0,0 +1,68 @@ +//****************************************************************************************************** +// TaskProcessorSection.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: +// ---------------------------------------------------------------------------------------------------- +// 02/24/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class TaskProcessorSection + { + public const string CategoryName = "TaskProcessor"; + + /// + /// Gets or sets the query used to filter + /// meters when polling for analysis tasks. + /// + [Setting] + [DefaultValue("SELECT ID FROM Meter")] + public string MeterFilterQuery { get; set; } + + /// + /// Gets or sets the number of threads used + /// for processing meter data concurrently. + /// + /// + /// Values less than or equal to zero will be set to the number of logical processors. + /// + [Setting] + [DefaultValue(0)] + public int ProcessingThreadCount + { + get + { + return _ProcessingThreadCount; + } + set + { + _ProcessingThreadCount = value; + + if (_ProcessingThreadCount <= 0) + _ProcessingThreadCount = Environment.ProcessorCount; + } + } + + private int _ProcessingThreadCount { get; set; } + } +} diff --git a/Libraries/openXDA.Configuration/TrendingDataSection.cs b/Libraries/openXDA.Configuration/TrendingDataSection.cs new file mode 100644 index 0000000..389daad --- /dev/null +++ b/Libraries/openXDA.Configuration/TrendingDataSection.cs @@ -0,0 +1,122 @@ +//****************************************************************************************************** +// TrendingDataSection.cs - Gbtc +// +// Copyright © 2022, 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: +// ---------------------------------------------------------------------------------------------------- +// 08/26/2022 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Configuration +{ + public class TrendingDataSection + { + #region [ Members ] + + // Nested Types + public class RMSSubSection + { + [Setting] + [DefaultValue(@"Trms.dat")] + public string FolderPath { get; set; } + [Setting] + [DefaultValue(null)] + public string DescriptionRegexMatchFilter { get; set; } + } + + public class FlickerSubSection + { + [Setting] + [DefaultValue(@"FkrR[0-9]*\.dat")] + public string FolderPath { get; set; } + [Setting] + [DefaultValue(null)] + public string DescriptionRegexMatchFilter { get; set; } + } + + public class TriggerSubSection + { + [Setting] + [DefaultValue(@"TrR[0-9]*\.dat")] + public string FolderPath { get; set; } + [Setting] + [DefaultValue(@"\s-\sV\S*\sRMS\s*$")] + public string DescriptionTriggerRMSMatch { get; set; } + [Setting] + [DefaultValue(@"\s-\sV\S*\sImpulse\s*$")] + public string DescriptionTriggerImpulseMatch { get; set; } + [Setting] + [DefaultValue(@"\s-\s\S+\sTHD\s*$")] + public string DescriptionTriggerTHDMatch { get; set; } + [Setting] + [DefaultValue(@"\s-\sUnbalance\s*$")] + public string DescriptionTriggerUnbalanceMatch { get; set; } + [Setting] + [DefaultValue(@"\s-\s\S*I\S*\s*$")] + public string DescriptionTriggerCurrentMatch { get; set; } + [Setting] + [DefaultValue(null)] + public string DescriptionRegexMatchFilter { get; set; } + } + + public class FrequencySubSection + { + [Setting] + [DefaultValue(@"Tfrq[0-9]*\.dat")] + public string FolderPath { get; set; } + [Setting] + [DefaultValue(null)] + public string DescriptionRegexMatchFilter { get; set; } + } + + // Constants + public const string CategoryName = "TrendingData"; + + #endregion + + #region [ Properties ] + + /// + /// Gets RMS folder path settings. + /// + [Category] + public RMSSubSection RMS { get; } = new RMSSubSection(); + + /// + /// Gets flicker folder path settings. + /// + [Category] + public FlickerSubSection Flicker { get; } = new FlickerSubSection(); + + /// + /// Gets trigger folder path settings. + /// + [Category] + public TriggerSubSection Trigger { get; } = new TriggerSubSection(); + + /// + /// Gets frequency folder path settings. + /// + [Category] + public FrequencySubSection Frequency { get; } = new FrequencySubSection(); + + #endregion + } +} diff --git a/Libraries/openXDA.Configuration/openXDA.Configuration.csproj b/Libraries/openXDA.Configuration/openXDA.Configuration.csproj new file mode 100644 index 0000000..3d799a9 --- /dev/null +++ b/Libraries/openXDA.Configuration/openXDA.Configuration.csproj @@ -0,0 +1,48 @@ + + + + Debug + AnyCPU + net9.0 + Debug;Development;Release + Library + openXDA.Configuration + openXDA.Configuration + false + enable + enable + true + + + + true + full + false + ..\..\..\Build\Output\Debug\Libraries\ + DEBUG;TRACE + 4 + + + + pdbonly + true + ..\..\..\Build\Output\Release\Libraries\ + TRACE + 4 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Libraries/openXDA.PQI/Address.cs b/Libraries/openXDA.PQI/Address.cs new file mode 100644 index 0000000..37d6e91 --- /dev/null +++ b/Libraries/openXDA.PQI/Address.cs @@ -0,0 +1,81 @@ +//****************************************************************************************************** +// Address.cs - Gbtc +// +// Copyright © 2023, 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: +// ---------------------------------------------------------------------------------------------------- +// 05/19/2023 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +namespace openXDA.PQI +{ + /// + /// Summary of the . + /// + public class Address + { + /// + /// Path to query this address + /// + public string Path { get; set; } + + /// + /// Path to query the company at this address + /// + public string Company { get; set; } + + /// + /// Path to query facilities at this address + /// + public string Facilities { get; set; } + + /// + /// First line of the address (street address) + /// + public string AddressLine1 { get; set; } + + /// + /// Second line of the address (building number, etc.) + /// + public string AddressLine2 { get; set; } + + /// + /// City in which the address is located + /// + public string City { get; set; } + + /// + /// State or province in which the address is located + /// + public string StateOrProvince { get; set; } + + /// + /// Zip code/postal code + /// + public string PostalCode { get; set; } + + /// + /// Country in which the address is located + /// + public string Country { get; set; } + + /// + /// True/false to indicate whether the address is primary + /// + public bool Primary { get; set; } + } +} diff --git a/Libraries/openXDA.PQI/AuditCurve.cs b/Libraries/openXDA.PQI/AuditCurve.cs new file mode 100644 index 0000000..24ba81d --- /dev/null +++ b/Libraries/openXDA.PQI/AuditCurve.cs @@ -0,0 +1,56 @@ +//****************************************************************************************************** +// AuditCurve.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: +// ---------------------------------------------------------------------------------------------------- +// 08/09/2022 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +namespace openXDA.PQI +{ + + public class AuditCurve: PQIModel + { + /// + /// Path to query this audit curve + /// + public string Path { get; set; } + + /// + /// Path to query the equipment this audit curve applies to + /// + public string Equipment { get; set; } + + /// + /// Path to query the areas in which the equipment resides + /// + public string Curve { get; set; } + + /// + /// Path to query the curve itself + /// + public string AuditCurves { get; set; } + + /// + /// The type of the curve (TOLERANCE, PROTECTION, PQDATA) + /// + public string CurveType { get; set; } + + + } +} diff --git a/Libraries/openXDA.PQI/Company.cs b/Libraries/openXDA.PQI/Company.cs new file mode 100644 index 0000000..24d0701 --- /dev/null +++ b/Libraries/openXDA.PQI/Company.cs @@ -0,0 +1,56 @@ +//****************************************************************************************************** +// Company.cs - Gbtc +// +// Copyright © 2023, 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: +// ---------------------------------------------------------------------------------------------------- +// 05/19/2023 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +namespace openXDA.PQI +{ + /// + /// Summary of the . + /// + public class Company + { + /// + /// Path to query this company + /// + public string Path { get; set; } + + /// + /// Path to query the addresses for this company + /// + public string Addresses { get; set; } + + /// + /// Type of the company (EPRI, UTILITY, CONTRACTOR) + /// + public string Type { get; set; } + + /// + /// Name of the company + /// + public string Name { get; set; } + + /// + /// Industry the company is in + /// + public string Industry { get; set; } + } +} diff --git a/Libraries/openXDA.PQI/Equipment.cs b/Libraries/openXDA.PQI/Equipment.cs new file mode 100644 index 0000000..5c250b6 --- /dev/null +++ b/Libraries/openXDA.PQI/Equipment.cs @@ -0,0 +1,74 @@ +//****************************************************************************************************** +// Equipment.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: +// ---------------------------------------------------------------------------------------------------- +// 07/22/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +namespace openXDA.PQI +{ + /// + /// Impacted Equipment + /// + /// + /// This does not correspond to the PQI Equipment model. For the PQI model see + /// + public class Equipment + { + /// + /// Name of the facility + /// + public string Facility { get; set; } + + /// + /// Title of the area + /// + public string Area { get; set; } + + /// + /// Title of the equipment + /// + public string SectionTitle { get; set; } + + /// + /// Rank of the equipment + /// + public int SectionRank { get; set; } + + /// + /// Model of the component + /// + public string ComponentModel { get; set; } + + /// + /// Name of the manufacturer + /// + public string Manufacturer { get; set; } + + /// + /// Name of the series + /// + public string Series { get; set; } + + /// + /// Type of component + /// + public string ComponentType { get; set; } + } +} diff --git a/Libraries/openXDA.PQI/Facility.cs b/Libraries/openXDA.PQI/Facility.cs new file mode 100644 index 0000000..eca4ee2 --- /dev/null +++ b/Libraries/openXDA.PQI/Facility.cs @@ -0,0 +1,56 @@ +//****************************************************************************************************** +// Facility.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: +// ---------------------------------------------------------------------------------------------------- +// 07/18/2022 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +namespace openXDA.PQI +{ + /// + /// Summary of the . For more detailed information use + /// + public class Facility + { + /// + /// Name of the facility + /// + public string Name { get; set; } + + /// + /// Voltage at the facility + /// + public string Voltage { get; set; } + + /// + /// Utility voltage supplied to the facility + /// + public string UtilitySupplyVoltage { get; set; } + + /// + /// Path to query this Facility + /// + public string Path { get; set; } + + /// + /// Path to query the address of the facility + /// + public string Address { get; set; } + } +} diff --git a/Libraries/openXDA.PQI/FacilityAudit.cs b/Libraries/openXDA.PQI/FacilityAudit.cs new file mode 100644 index 0000000..cf65c69 --- /dev/null +++ b/Libraries/openXDA.PQI/FacilityAudit.cs @@ -0,0 +1,53 @@ +//****************************************************************************************************** +// FacilityAudit.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: +// ---------------------------------------------------------------------------------------------------- +// 07/18/2022 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +namespace openXDA.PQI +{ + /// + /// Summary of the . + /// + public class FacilityAudit: PQIModel + { + /// + /// Name of the Audit + /// + public string AuditName { get; set; } + + /// + /// Path to query the equipment in the facility that was audited + /// + public string Equipment { get; set; } + + /// + /// Path to query the facility that was audited. + /// + public string Facility { get; set; } + + /// + /// Path to query this FacilityAudit + /// + public string Path { get; set; } + + + } +} diff --git a/Libraries/openXDA.PQI/FacilityInfo.cs b/Libraries/openXDA.PQI/FacilityInfo.cs new file mode 100644 index 0000000..5c66b54 --- /dev/null +++ b/Libraries/openXDA.PQI/FacilityInfo.cs @@ -0,0 +1,83 @@ +//****************************************************************************************************** +// FacilityInfo.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: +// ---------------------------------------------------------------------------------------------------- +// 07/22/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +namespace openXDA.PQI +{ + public class FacilityInfo + { + /// + /// Name of the facility + /// + public string FacilityName { get; set; } + + /// + /// Voltage at the facility + /// + public string FacilityVoltage { get; set; } + + /// + /// Utility voltage supplied to the facility + /// + public string UtilitySupplyVoltage { get; set; } + + /// + /// First line of the address (street address) + /// + public string AddressLine1 { get; set; } + + /// + /// Second line of the address (building number, etc.) + /// + public string AddressLine2 { get; set; } + + /// + /// City in which the address is located + /// + public string City { get; set; } + + /// + /// State or province in which the address is located + /// + public string StateOrProvince { get; set; } + + /// + /// Zip code/postal code + /// + public string PostalCode { get; set; } + + /// + /// Country in which the address is located. + /// + public string Country { get; set; } + + /// + /// Name of the company + /// + public string CompanyName { get; set; } + + /// + /// Industry the company is in + /// + public string Industry { get; set; } + } +} diff --git a/Libraries/openXDA.PQI/HttpClientExtensions.cs b/Libraries/openXDA.PQI/HttpClientExtensions.cs new file mode 100644 index 0000000..b65b8d6 --- /dev/null +++ b/Libraries/openXDA.PQI/HttpClientExtensions.cs @@ -0,0 +1,42 @@ +//****************************************************************************************************** +// HttpClientExtensions.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: +// ---------------------------------------------------------------------------------------------------- +// 07/27/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace openXDA.PQI +{ + internal static class HttpClientExtensions + { + public static async Task SendRequestAsync(this HttpClient client, Action configure, CancellationToken cancellationToken) + { + using (HttpRequestMessage request = new HttpRequestMessage()) + { + configure(request); + return await client.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + } + } +} diff --git a/Libraries/openXDA.PQI/HttpClientProvider.cs b/Libraries/openXDA.PQI/HttpClientProvider.cs new file mode 100644 index 0000000..a246171 --- /dev/null +++ b/Libraries/openXDA.PQI/HttpClientProvider.cs @@ -0,0 +1,37 @@ +//****************************************************************************************************** +// HttpClientProvider.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: +// ---------------------------------------------------------------------------------------------------- +// 07/26/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Net.Http; + +namespace openXDA.PQI +{ + public static class HttpClientProvider + { + public static HttpClient GetClient() => + LazyInstance.Value; + + private static Lazy LazyInstance { get; } + = new Lazy(() => new HttpClient()); + } +} diff --git a/Libraries/openXDA.PQI/PQIEquipment.cs b/Libraries/openXDA.PQI/PQIEquipment.cs new file mode 100644 index 0000000..a418793 --- /dev/null +++ b/Libraries/openXDA.PQI/PQIEquipment.cs @@ -0,0 +1,65 @@ +//****************************************************************************************************** +// PQIEquipment.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: +// ---------------------------------------------------------------------------------------------------- +// 08/09/2022 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +namespace openXDA.PQI +{ + + public class PQIEquipment: PQIModel + { + /// + /// Path to query this equipment + /// + public string Path { get; set; } + + /// + /// Path to query the facility audit in which the equipment was audited + /// + public string FacilityAudit { get; set; } + + /// + /// Path to query the areas in which the equipment resides + /// + public string Areas { get; set; } + + /// + /// Path to query the audit curves assigned to the equipment + /// + public string AuditCurves { get; set; } + + /// + /// Title of the equipment + /// + public string Title { get; set; } + + /// + /// Rank of the equipment + /// + public int Rank { get; set; } + + /// + /// Content of the equipment + /// + public string Content { get; set; } + + } +} diff --git a/Libraries/openXDA.PQI/PQIModel.cs b/Libraries/openXDA.PQI/PQIModel.cs new file mode 100644 index 0000000..8ca730d --- /dev/null +++ b/Libraries/openXDA.PQI/PQIModel.cs @@ -0,0 +1,36 @@ +//****************************************************************************************************** +// PQIModel.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: +// ---------------------------------------------------------------------------------------------------- +// 08/09/2022 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; + +namespace openXDA.PQI +{ + + public interface PQIModel + { + /// + /// Path to query this Model + /// + string Path { get; set; } + } +} diff --git a/Libraries/openXDA.PQI/PQIWSClient.cs b/Libraries/openXDA.PQI/PQIWSClient.cs new file mode 100644 index 0000000..9ea6593 --- /dev/null +++ b/Libraries/openXDA.PQI/PQIWSClient.cs @@ -0,0 +1,325 @@ +//****************************************************************************************************** +// PQIWSClient.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: +// ---------------------------------------------------------------------------------------------------- +// 07/22/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace openXDA.PQI +{ + public class PQIWSClient + { + private const string BasePath = "PQDashboard"; + + public PQIWSClient(string baseURL, Func tokenProvider) + { + BaseURL = baseURL; + TokenProvider = tokenProvider; + } + + private string BaseURL { get; } + private Func TokenProvider { get; } + + public async Task> GetAllFacilities(CancellationToken cancellationToken = default) + { + string url = BuildURL(BaseURL, "Facility"); + + void ConfigureRequest(HttpRequestMessage request) + { + request.RequestUri = new Uri($"{url}"); + request.Method = HttpMethod.Get; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenProvider()); + + MediaTypeWithQualityHeaderValue acceptHeader = new MediaTypeWithQualityHeaderValue("application/json"); + request.Headers.Accept.Add(acceptHeader); + } + + using (HttpResponseMessage response = await HttpClient.SendRequestAsync(ConfigureRequest, cancellationToken).ConfigureAwait(false)) + return await response.Content.ReadAsAsync>().ConfigureAwait(false); + } + + public async Task> GetAllAddresses(CancellationToken cancellationToken = default) + { + string url = BuildURL(BaseURL, "Address"); + + void ConfigureRequest(HttpRequestMessage request) + { + request.RequestUri = new Uri($"{url}"); + request.Method = HttpMethod.Get; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenProvider()); + + MediaTypeWithQualityHeaderValue acceptHeader = new MediaTypeWithQualityHeaderValue("application/json"); + request.Headers.Accept.Add(acceptHeader); + } + + using (HttpResponseMessage response = await HttpClient.SendRequestAsync(ConfigureRequest, cancellationToken).ConfigureAwait(false)) + return await response.Content.ReadAsAsync>().ConfigureAwait(false); + } + + public async Task> GetAllCompanies(CancellationToken cancellationToken = default) + { + string url = BuildURL(BaseURL, "Company"); + + void ConfigureRequest(HttpRequestMessage request) + { + request.RequestUri = new Uri($"{url}"); + request.Method = HttpMethod.Get; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenProvider()); + + MediaTypeWithQualityHeaderValue acceptHeader = new MediaTypeWithQualityHeaderValue("application/json"); + request.Headers.Accept.Add(acceptHeader); + } + + using (HttpResponseMessage response = await HttpClient.SendRequestAsync(ConfigureRequest, cancellationToken).ConfigureAwait(false)) + return await response.Content.ReadAsAsync>().ConfigureAwait(false); + } + + public async Task GetFacilityInfoAsync(int facilityID, CancellationToken cancellationToken = default) + { + string url = BuildURL(BaseURL, BasePath, "GetFacilityInfo"); + string queryString = $"facilityID={facilityID}"; + + void ConfigureRequest(HttpRequestMessage request) + { + request.RequestUri = new Uri($"{url}?{queryString}"); + request.Method = HttpMethod.Get; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenProvider()); + + MediaTypeWithQualityHeaderValue acceptHeader = new MediaTypeWithQualityHeaderValue("application/json"); + request.Headers.Accept.Add(acceptHeader); + } + + using (HttpResponseMessage response = await HttpClient.SendRequestAsync(ConfigureRequest, cancellationToken).ConfigureAwait(false)) + return await response.Content.ReadAsAsync().ConfigureAwait(false); + } + + public async Task IsImpactedAsync(int facilityID, double magnitude, double duration, CancellationToken cancellationToken = default) + { + string url = BuildURL(BaseURL, BasePath, "IsImpacted"); + + string queryString = + $"facilityID={facilityID}&" + + $"magnitude={magnitude}&" + + $"duration={duration}"; + + void ConfigureRequest(HttpRequestMessage request) + { + request.RequestUri = new Uri($"{url}?{queryString}"); + request.Method = HttpMethod.Get; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenProvider()); + + MediaTypeWithQualityHeaderValue acceptHeader = new MediaTypeWithQualityHeaderValue("application/json"); + request.Headers.Accept.Add(acceptHeader); + } + + using (HttpResponseMessage response = await HttpClient.SendRequestAsync(ConfigureRequest, cancellationToken).ConfigureAwait(false)) + return await response.Content.ReadAsAsync().ConfigureAwait(false); + } + + public async Task> GetImpactedEquipmentAsync(int facilityID, double magnitude, double duration, CancellationToken cancellationToken = default) + { + string url = BuildURL(BaseURL, BasePath, "GetEquipmentImpacted"); + + string queryString = + $"facilityID={facilityID}&" + + $"magnitude={magnitude}&" + + $"duration={duration}"; + + void ConfigureRequest(HttpRequestMessage request) + { + request.RequestUri = new Uri($"{url}?{queryString}"); + request.Method = HttpMethod.Get; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenProvider()); + + MediaTypeWithQualityHeaderValue acceptHeader = new MediaTypeWithQualityHeaderValue("application/json"); + request.Headers.Accept.Add(acceptHeader); + } + + using (HttpResponseMessage response = await HttpClient.SendRequestAsync(ConfigureRequest, cancellationToken).ConfigureAwait(false)) + return await response.Content.ReadAsAsync>().ConfigureAwait(false); + } + + public async Task> GetFacilityAudits(int facilityID, CancellationToken cancellationToken = default) + { + string url = BuildURL(BaseURL, "FacilityAudit"); + string queryString = $"facilityID={facilityID}"; + + void ConfigureRequest(HttpRequestMessage request) + { + request.RequestUri = new Uri($"{url}?{queryString}"); + request.Method = HttpMethod.Get; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenProvider()); + + MediaTypeWithQualityHeaderValue acceptHeader = new MediaTypeWithQualityHeaderValue("application/json"); + request.Headers.Accept.Add(acceptHeader); + } + + using (HttpResponseMessage response = await HttpClient.SendRequestAsync(ConfigureRequest, cancellationToken).ConfigureAwait(false)) + return await response.Content.ReadAsAsync>().ConfigureAwait(false); + } + + public async Task> GetAuditedEquipment(int facilityAuditID, CancellationToken cancellationToken = default) + { + string url = BuildURL(BaseURL, "Equipment"); + string queryString = $"facilityAuditID={facilityAuditID}"; + + void ConfigureRequest(HttpRequestMessage request) + { + request.RequestUri = new Uri($"{url}?{queryString}"); + request.Method = HttpMethod.Get; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenProvider()); + + MediaTypeWithQualityHeaderValue acceptHeader = new MediaTypeWithQualityHeaderValue("application/json"); + request.Headers.Accept.Add(acceptHeader); + } + + using (HttpResponseMessage response = await HttpClient.SendRequestAsync(ConfigureRequest, cancellationToken).ConfigureAwait(false)) + return await response.Content.ReadAsAsync>().ConfigureAwait(false); + } + + public async Task> GetAuditedEquipment(FacilityAudit facilityAudit, CancellationToken cancellationToken = default) + { + string url = BuildURL(BaseURL, facilityAudit.Equipment); + + void ConfigureRequest(HttpRequestMessage request) + { + request.RequestUri = new Uri($"{url}"); + request.Method = HttpMethod.Get; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenProvider()); + + MediaTypeWithQualityHeaderValue acceptHeader = new MediaTypeWithQualityHeaderValue("application/json"); + request.Headers.Accept.Add(acceptHeader); + } + + using (HttpResponseMessage response = await HttpClient.SendRequestAsync(ConfigureRequest, cancellationToken).ConfigureAwait(false)) + return await response.Content.ReadAsAsync>().ConfigureAwait(false); + } + + public async Task> GetAuditCurve(PQIEquipment equipment, CancellationToken cancellationToken = default) + { + string url = BuildURL(BaseURL, equipment.AuditCurves); + + void ConfigureRequest(HttpRequestMessage request) + { + request.RequestUri = new Uri($"{url}"); + request.Method = HttpMethod.Get; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenProvider()); + + MediaTypeWithQualityHeaderValue acceptHeader = new MediaTypeWithQualityHeaderValue("application/json"); + request.Headers.Accept.Add(acceptHeader); + } + + using (HttpResponseMessage response = await HttpClient.SendRequestAsync(ConfigureRequest, cancellationToken).ConfigureAwait(false)) + return await response.Content.ReadAsAsync>().ConfigureAwait(false); + } + + public async Task> GetAuditCurve(int equipmentID, CancellationToken cancellationToken = default) + { + string url = BuildURL(BaseURL, "AuditCurve"); + string queryString = $"equipmentID={equipmentID}"; + + void ConfigureRequest(HttpRequestMessage request) + { + request.RequestUri = new Uri($"{url}?{queryString}"); + request.Method = HttpMethod.Get; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenProvider()); + + MediaTypeWithQualityHeaderValue acceptHeader = new MediaTypeWithQualityHeaderValue("application/json"); + request.Headers.Accept.Add(acceptHeader); + } + + using (HttpResponseMessage response = await HttpClient.SendRequestAsync(ConfigureRequest, cancellationToken).ConfigureAwait(false)) + return await response.Content.ReadAsAsync>().ConfigureAwait(false); + } + + public async Task GetTestCurve(AuditCurve auditCurve, CancellationToken cancellationToken = default) + { + string url = BuildURL(BaseURL, auditCurve.Curve); + + void ConfigureRequest(HttpRequestMessage request) + { + request.RequestUri = new Uri($"{url}"); + request.Method = HttpMethod.Get; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenProvider()); + + MediaTypeWithQualityHeaderValue acceptHeader = new MediaTypeWithQualityHeaderValue("application/json"); + request.Headers.Accept.Add(acceptHeader); + } + + using (HttpResponseMessage response = await HttpClient.SendRequestAsync(ConfigureRequest, cancellationToken).ConfigureAwait(false)) + return await response.Content.ReadAsAsync().ConfigureAwait(false); + } + + public async Task> GetTestCurvePoints(TestCurve testCurve, CancellationToken cancellationToken = default) + { + string url = BuildURL(BaseURL, testCurve.Points); + + void ConfigureRequest(HttpRequestMessage request) + { + request.RequestUri = new Uri($"{url}"); + request.Method = HttpMethod.Get; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenProvider()); + + MediaTypeWithQualityHeaderValue acceptHeader = new MediaTypeWithQualityHeaderValue("application/json"); + request.Headers.Accept.Add(acceptHeader); + } + + using (HttpResponseMessage response = await HttpClient.SendRequestAsync(ConfigureRequest, cancellationToken).ConfigureAwait(false)) + return await response.Content.ReadAsAsync>().ConfigureAwait(false); + } + + private static string BuildURL(params string[] parts) + { + const string Separator = "/"; + string combinedPath = ""; + + foreach (string path in parts) + { + if (path == null) + throw new ArgumentNullException(nameof(parts), "One of the strings in the array is null."); + + if (path.Length == 0) + continue; + + if (combinedPath.Length == 0) + combinedPath = path; + else if (path.StartsWith(Separator) && combinedPath.EndsWith(Separator)) + combinedPath += path.Substring(Separator.Length); + else if (path.StartsWith(Separator)) + combinedPath += path; + else if (combinedPath.EndsWith(Separator)) + combinedPath += path; + else + combinedPath += Separator + path; + } + + return combinedPath; + } + + private static HttpClient HttpClient => + HttpClientProvider.GetClient(); + } +} diff --git a/Libraries/openXDA.PQI/PQIWSQueryHelper.cs b/Libraries/openXDA.PQI/PQIWSQueryHelper.cs new file mode 100644 index 0000000..04b2a11 --- /dev/null +++ b/Libraries/openXDA.PQI/PQIWSQueryHelper.cs @@ -0,0 +1,282 @@ +//****************************************************************************************************** +// PQIWSQueryHelper.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: +// ---------------------------------------------------------------------------------------------------- +// 07/29/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Gemstone.Data; +using Gemstone.Data.DataExtensions; + +namespace openXDA.PQI +{ + public class PQIWSQueryHelper + { + private class EquipmentComparer : IEqualityComparer + { + public bool Equals(Equipment x, Equipment y) + { + if (x is null && y is null) + return true; + + if (x is null || y is null) + return false; + + object xKey = GetKey(x); + object yKey = GetKey(y); + return Equals(xKey, yKey); + } + + public int GetHashCode(Equipment equipment) + { + object key = GetKey(equipment); + return key.GetHashCode(); + } + + private object GetKey(Equipment equipment) + { + return new + { + equipment.Facility, + equipment.Area, + equipment.SectionTitle, + equipment.SectionRank, + equipment.ComponentModel, + equipment.Manufacturer, + equipment.Series, + equipment.ComponentType + }; + } + + public static EquipmentComparer Instance { get; } + = new EquipmentComparer(); + } + + private class PQIModelComparer: IEqualityComparer where T: class, PQIModel + { + public bool Equals(T x, T y) + { + if (x is null && y is null) + return true; + + if (x is null || y is null) + return false; + + object xKey = GetKey(x); + object yKey = GetKey(y); + return Equals(xKey, yKey); + } + + public int GetHashCode(T facilityAudit) + { + object key = GetKey(facilityAudit); + return key.GetHashCode(); + } + + private object GetKey(T facilityAudit) + { + return new + { + facilityAudit.Path + }; + } + + public static PQIModelComparer Instance { get; } + = new PQIModelComparer(); + } + + private const string FacilityDisturbanceQueryFormat = + @"SELECT + Customer.PQIFacilityID, + Disturbance.PerUnitMagnitude, + Disturbance.DurationSeconds + FROM Disturbance LEFT JOIN + Event ON Event.ID = Disturbance.EventID LEFT JOIN + CustomerMeter ON CustomerMeter.MeterID = Event.MeterID LEFT JOIN + Customer ON Customer.ID = CustomerMeter.CustomerID + WHERE PQIFacilityID IS NOT NULL AND EventID = {0} + UNION + SELECT + Customer.PQIFacilityID, + Disturbance.PerUnitMagnitude, + Disturbance.DurationSeconds + FROM Disturbance LEFT JOIN + Event ON Event.ID = Disturbance.EventID LEFT JOIN + CustomerAsset ON CustomerAsset.AssetID = Event.AssetID LEFT JOIN + Customer ON Customer.ID = CustomerAsset.CustomerID + WHERE PQIFacilityID IS NOT NULL AND EventID = {0}"; + + public PQIWSQueryHelper(Func connectionFactory, PQIWSClient pqiwsClient) + { + ConnectionFactory = connectionFactory; + PQIWSClient = pqiwsClient; + } + + private Func ConnectionFactory { get; } + private PQIWSClient PQIWSClient { get; } + + public async Task HasImpactedComponentsAsync(int eventID, CancellationToken cancellationToken = default) + { + async Task IsImpactedAsync(DataRow facilityDisturbance) + { + int facilityID = facilityDisturbance.ConvertField("PQIFacilityID"); + double magnitude = facilityDisturbance.ConvertField("PerUnitMagnitude"); + double duration = facilityDisturbance.ConvertField("DurationSeconds"); + return await PQIWSClient.IsImpactedAsync(facilityID, magnitude, duration, cancellationToken).ConfigureAwait(false); + } + + async Task AnyImpactedAsync(IEnumerable> isImpactedTasks) + { + foreach (Task task in isImpactedTasks) + { + if (await task.ConfigureAwait(false)) + return true; + } + + return false; + } + + using (AdoDataConnection connection = ConnectionFactory()) + using (DataTable table = connection.RetrieveData(FacilityDisturbanceQueryFormat, eventID)) + { + var tasks = table + .AsEnumerable() + .Select(IsImpactedAsync); + + return await AnyImpactedAsync(tasks).ConfigureAwait(false); + } + } + + public async Task> GetAllImpactedEquipmentAsync(int eventID, CancellationToken cancellationToken = default) + { + using (AdoDataConnection connection = ConnectionFactory()) + return await GetAllImpactedEquipmentAsync(connection, eventID, cancellationToken).ConfigureAwait(false); + } + + public async Task> GetAllImpactedEquipmentAsync(IEnumerable eventIDs, CancellationToken cancellationToken = default) + { + using (AdoDataConnection connection = ConnectionFactory()) + { + Task> _GetAllImpactedEquipmentAsync(int eventID) => + GetAllImpactedEquipmentAsync(connection, eventID, cancellationToken); + + var tasks = eventIDs.Select(_GetAllImpactedEquipmentAsync); + List[] equipmentLists = await Task.WhenAll(tasks).ConfigureAwait(false); + return Flatten(equipmentLists); + } + } + + public async Task>>> GetAllTestCurvesAsync (IEnumerable eventIDs, CancellationToken cancellationToken = default) + { + using (AdoDataConnection connection = ConnectionFactory()) + { + Task>>> _GetAllTestCurvesAsync(int eventID) => + GetAllCurvesAsync(connection, eventID, cancellationToken); + + var tasks = eventIDs.Select(_GetAllTestCurvesAsync); + List>>[] curveList = await Task.WhenAll(tasks).ConfigureAwait(false); + return curveList.SelectMany(list => list).ToList(); + } + } + + private async Task>>> GetAllCurvesAsync(AdoDataConnection connection, int eventID, CancellationToken cancellationToken = default) + { + int GetFacilityID(DataRow facilityDisturbance) + { + return facilityDisturbance.ConvertField("PQIFacilityID"); + } + + async Task> GetFacilityAudits(int facilityID) + { + return await PQIWSClient.GetFacilityAudits(facilityID, cancellationToken).ConfigureAwait(false); + } + + async Task> GetAuditedEquipment(FacilityAudit facilityAudit) + { + return await PQIWSClient.GetAuditedEquipment(facilityAudit, cancellationToken).ConfigureAwait(false); + } + + async Task> GetAuditCurves(PQIEquipment equipment) + { + return await PQIWSClient.GetAuditCurve(equipment, cancellationToken).ConfigureAwait(false); + } + + async Task GetTestCurve(AuditCurve auditCurve) + { + return await PQIWSClient.GetTestCurve(auditCurve, cancellationToken).ConfigureAwait(false); + } + + async Task>> GetTestCurvePoints(TestCurve testCurve) + { + return new Tuple>(testCurve, (await PQIWSClient.GetTestCurvePoints(testCurve, cancellationToken).ConfigureAwait(false))); + } + + using (DataTable table = connection.RetrieveData(FacilityDisturbanceQueryFormat, eventID)) + { + int[] facilityIDs = table + .AsEnumerable() + .Select(GetFacilityID) + .ToArray(); + + List[] facilityAudits = await Task.WhenAll(facilityIDs.Distinct().Select(GetFacilityAudits)).ConfigureAwait(false); + List[] auditedEquipment = await Task.WhenAll(Flatten(facilityAudits).Select(GetAuditedEquipment)).ConfigureAwait(false); + List[] auditCurves = await Task.WhenAll(Flatten(auditedEquipment).Select(GetAuditCurves)).ConfigureAwait(false); + TestCurve[] testCurves = await Task.WhenAll(Flatten(auditCurves).Select(GetTestCurve)).ConfigureAwait(false); + + return (await Task.WhenAll(testCurves.Distinct().Select(GetTestCurvePoints)).ConfigureAwait(false)).ToList(); + } + } + private async Task> GetAllImpactedEquipmentAsync(AdoDataConnection connection, int eventID, CancellationToken cancellationToken) + { + async Task> GetImpactedEquipmentAsync(DataRow facilityDisturbance) + { + int facilityID = facilityDisturbance.ConvertField("PQIFacilityID"); + double magnitude = facilityDisturbance.ConvertField("PerUnitMagnitude"); + double duration = facilityDisturbance.ConvertField("DurationSeconds"); + return await PQIWSClient.GetImpactedEquipmentAsync(facilityID, magnitude, duration, cancellationToken).ConfigureAwait(false); + } + + using (DataTable table = connection.RetrieveData(FacilityDisturbanceQueryFormat, eventID)) + { + var tasks = table + .AsEnumerable() + .Select(GetImpactedEquipmentAsync); + + List[] equipmentLists = await Task.WhenAll(tasks).ConfigureAwait(false); + return Flatten(equipmentLists); + } + } + + private List Flatten(IEnumerable> equipmentLists) => equipmentLists + .SelectMany(list => list) + .Distinct(EquipmentComparer.Instance) + .ToList(); + + private List Flatten(IEnumerable> facilityAuditLists) where T: class,PQIModel => facilityAuditLists + .SelectMany(list => list) + .Distinct(PQIModelComparer.Instance) + .ToList(); + } +} diff --git a/Libraries/openXDA.PQI/PingClient.cs b/Libraries/openXDA.PQI/PingClient.cs new file mode 100644 index 0000000..51875a0 --- /dev/null +++ b/Libraries/openXDA.PQI/PingClient.cs @@ -0,0 +1,97 @@ +//****************************************************************************************************** +// PingClient.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: +// ---------------------------------------------------------------------------------------------------- +// 07/26/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace openXDA.PQI +{ + public class PingClient + { + public PingClient(string tokenURL) + { + TokenURL = tokenURL; + IsExpiredFunc = () => true; + } + + private string TokenURL { get; } + private Func IsExpiredFunc { get; set; } + + public string AccessToken { get; private set; } + public bool IsExpired => IsExpiredFunc(); + + /// + /// Exchange credentials for a new access token. + /// + /// The client application's credentials. + /// The user's credentials. + /// Token for cancelling the exchange. + /// The task that represents the exchange of credentials for an access token. + public async Task ExchangeAsync(NetworkCredential clientCredential, NetworkCredential userCredential, CancellationToken cancellationToken = default) + { + void ConfigureRequest(HttpRequestMessage request) + { + request.RequestUri = new Uri(TokenURL); + request.Method = HttpMethod.Post; + + string clientID = clientCredential.UserName; + string clientSecret = clientCredential.Password; + string credentials = $"{clientID}:{clientSecret}"; + byte[] encodedCredentials = Encoding.UTF8.GetBytes(credentials); + string credentials64 = Convert.ToBase64String(encodedCredentials); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials64); + + List> body = new List>(); + body.Add(new KeyValuePair("username", userCredential.UserName)); + body.Add(new KeyValuePair("password", userCredential.Password)); + body.Add(new KeyValuePair("grant_type", "password")); + request.Content = new FormUrlEncodedContent(body); + + MediaTypeWithQualityHeaderValue acceptHeader = new MediaTypeWithQualityHeaderValue("application/json"); + request.Headers.Accept.Add(acceptHeader); + } + + using (HttpResponseMessage response = await HttpClient.SendRequestAsync(ConfigureRequest, cancellationToken).ConfigureAwait(false)) + { + response.EnsureSuccessStatusCode(); + + Stopwatch stopwatch = Stopwatch.StartNew(); + JObject content = await response.Content.ReadAsAsync(cancellationToken).ConfigureAwait(false); + int expiration = content["expires_in"].Value(); + AccessToken = content["access_token"].Value(); + IsExpiredFunc = () => stopwatch.Elapsed.TotalSeconds > expiration; + } + } + + private static HttpClient HttpClient => + HttpClientProvider.GetClient(); + } +} diff --git a/Libraries/openXDA.PQI/Properties/AssemblyInfo.cs b/Libraries/openXDA.PQI/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1d16378 --- /dev/null +++ b/Libraries/openXDA.PQI/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("openXDA.PQI")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("openXDA.PQI")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("3dec81f1-9a02-4bbf-8325-f332ac63393e")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("3.0.5.93")] +[assembly: AssemblyVersion("3.0.5.93")] +[assembly: AssemblyFileVersion("3.0.5.93")] diff --git a/Libraries/openXDA.PQI/TestCurve.cs b/Libraries/openXDA.PQI/TestCurve.cs new file mode 100644 index 0000000..20b900d --- /dev/null +++ b/Libraries/openXDA.PQI/TestCurve.cs @@ -0,0 +1,96 @@ +//****************************************************************************************************** +// TestCurve.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: +// ---------------------------------------------------------------------------------------------------- +// 08/09/2022 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; + +namespace openXDA.PQI +{ + + public class TestCurve + { + /// + /// Path to query this test curve (aka ride-through curve) + /// + public string Path { get; set; } + + /// + /// Path to query the component this curve was assigned to + /// + public string Component { get; set; } + + /// + /// Path to query the audit curves associated with this test curve + /// + public string AuditCurves { get; set; } + + /// + /// Path to query the points in the curve + /// + public string Points { get; set; } + + /// + /// Name of the curve + /// + public string Name { get; set; } + + /// + /// Description of the curve + /// + public string Description { get; set; } + + /// + /// Entity + /// + public string Entity { get; set; } + + /// + /// Location + /// + public string Location { get; set; } + + /// + /// Investigator + /// + public string Investigator { get; set; } + + /// + /// Date + /// + public DateTime? Date { get; set; } + + /// + /// Additional notes about the curve + /// + public string Notes { get; set; } + + /// + /// Frequency (Hz) of the voltage + /// + public double? Frequency { get; set; } + + /// + /// Voltage level (in volts) during normal conditions + /// + public double? NominalVoltage { get; set; } + } +} diff --git a/Libraries/openXDA.PQI/TestCurvePoint.cs b/Libraries/openXDA.PQI/TestCurvePoint.cs new file mode 100644 index 0000000..bad0c12 --- /dev/null +++ b/Libraries/openXDA.PQI/TestCurvePoint.cs @@ -0,0 +1,53 @@ +//****************************************************************************************************** +// TestCurvePoint.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: +// ---------------------------------------------------------------------------------------------------- +// 08/09/2022 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; + +namespace openXDA.PQI +{ + + public class TestCurvePoint + { + /// + /// Path to query this test curve point + /// + public string Path { get; set; } + + /// + /// Path to query the test curve this point belongs to + /// + public string TestCurve { get; set; } + + /// + /// X-value (duration) on a mag/dur chart + /// + public double X { get; set; } + + /// + /// Y-value(magnitude) on a mag/dur chart + /// + public double Y { get; set; } + + + } +} diff --git a/Libraries/openXDA.PQI/openXDA.PQI.csproj b/Libraries/openXDA.PQI/openXDA.PQI.csproj new file mode 100644 index 0000000..f24661b --- /dev/null +++ b/Libraries/openXDA.PQI/openXDA.PQI.csproj @@ -0,0 +1,32 @@ + + + net9.0 + Debug;Development;Release + openXDA.PQI + openXDA.PQI + false + + + + + + + + + true + full + false + ..\..\..\Build\Output\Debug\Libraries\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + ..\..\..\Build\Output\Release\Libraries\ + TRACE + prompt + 4 + + \ No newline at end of file diff --git a/Libraries/openXDA.Reports/AllBreakersReport.cs b/Libraries/openXDA.Reports/AllBreakersReport.cs new file mode 100644 index 0000000..c248f09 --- /dev/null +++ b/Libraries/openXDA.Reports/AllBreakersReport.cs @@ -0,0 +1,411 @@ +//****************************************************************************************************** +// AllBreakersReport.cs - Gbtc +// +// Copyright © 2019, 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: +// ---------------------------------------------------------------------------------------------------- +// 07/05/2019 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using System.Data; +using System.Drawing; +using System.IO; +using System.Linq; +using Gemstone.Configuration; +using Gemstone.Data; +using log4net; +using Root.Reports; + +namespace openXDA.Reports +{ + public class AllBreakersReport : Report + { + #region [ Members ] + + // Nested Types + public class Point + { + public DateTime Time; + public double Value; + } + + // Constants + private const double PageMarginMillimeters = 12.7D; // 1-inch margin + private const double PageHeightMillimeters = 8.5D * 25.4D; // 8.5 inch height (landscape) + private const double PageWidthMillimeters = 11.0D * 25.4D; // 11 inch width (landscape) + private const double FooterHeightMillimeters = (10.0D / 72.0D) * 25.4D; + private const double SpacingMillimeters = 6.0D; + + private const string TitleText = "All Breakers Report"; + + private const string Query = @" + SELECT + MAX(BreakerOperation.TripCoilEnergized) as LastOperationDate, + BreakerOperation.BreakerNumber, + COUNT(BreakerOperation.ID) as Total, + MaximoBreaker.AssetNum, + Asset.AssetName, + -- last timing info + (SELECT Name from Phase WHERE ID = (SELECT TOP 1 PhaseID FROM BreakerOperation as bo WHERE bo.BreakerNumber = BreakerOperation.BreakerNumber ORDER BY TripCoilEnergized)) as LastPhase, + (SELECT TOP 1 BreakerTiming FROM BreakerOperation as bo WHERE bo.BreakerNumber = BreakerOperation.BreakerNumber ORDER BY TripCoilEnergized) as LastWaveformTiming, + (SELECT TOP 1 StatusTiming FROM BreakerOperation as bo WHERE bo.BreakerNumber = BreakerOperation.BreakerNumber ORDER BY TripCoilEnergized) as LastStatusTiming, + MaximoBreaker.BreakerSpeed as MfrSpeed, + (SELECT Name from BreakerOperationType WHERE ID = (SELECT TOP 1 BreakerOperationTypeID FROM BreakerOperation as bo WHERE bo.BreakerNumber = BreakerOperation.BreakerNumber ORDER BY TripCoilEnergized)) as OperationTiming, + (SELECT TOP 1 CASE WHEN StatusTiming < BreakerTiming THEN 'Status' ELSE 'Waveform' END FROM BreakerOperation as bo WHERE bo.BreakerNumber = BreakerOperation.BreakerNumber ORDER BY TripCoilEnergized) as LastMethod, + -- last slow + COUNT(LateBreakerOperation.ID) as TotalLateOperation, + MAX(LateBreakerOperation.TripCoilEnergized) as LastLateOperation, + -- mfr info + MaximoBreaker.Manufacturer, + MaximoBreaker.SerialNum, + MaximoBreaker.MfrYear, + MaximoBreaker.ModelNum, + MaximoBreaker.InterruptCurrentRating, + MaximoBreaker.ContinuousAmpRating + FROM + BreakerOperation JOIN + BreakerOperationType ON BreakerOperation.BreakerOperationTypeID = BreakerOperationType.ID LEFT OUTER JOIN + BreakerOperation LateBreakerOperation ON + BreakerOperation.ID = LateBreakerOperation.ID AND + BreakerOperationType.Name = 'Late' OUTER APPLY + ( + SELECT TOP 1 Asset.* + FROM + Asset JOIN + Channel ON Channel.AssetID = Asset.ID JOIN + BreakerChannel ON BreakerChannel.ChannelID = Channel.ID + WHERE BreakerChannel.BreakerNumber = BreakerOperation.BreakerNumber + ORDER BY Asset.ID + ) Asset LEFT OUTER JOIN + MaximoBreaker ON BreakerOperation.BreakerNumber = SUBSTRING(MaximoBreaker.BreakerNum, PATINDEX('%[^0]%', MaximoBreaker.BreakerNum + '.'), LEN(MaximoBreaker.BreakerNum)) + WHERE CAST(BreakerOperation.TripCoilEnergized AS DATE) BETWEEN {0} AND {1} + GROUP BY + BreakerOperation.BreakerNumber, + MaximoBreaker.AssetNum, + Asset.AssetName, + MaximoBreaker.Manufacturer, + MaximoBreaker.SerialNum, + MaximoBreaker.MfrYear, + MaximoBreaker.ModelNum, + MaximoBreaker.InterruptCurrentRating, + MaximoBreaker.ContinuousAmpRating, + MaximoBreaker.BreakerSpeed + ORDER BY + LastOperationDate + "; + + private const string TestQuery = @" + SELECT + MAX(BreakerOperation.TripCoilEnergized) as LastOperationDate, + BreakerOperation.BreakerNumber, + COUNT(BreakerOperation.ID) as Total, + MaximoBreaker.[Breaker Number] as AssetNum, + MaximoBreaker.[Line Name] as LineName, + -- last timing info + (SELECT Name from Phase WHERE ID = (SELECT TOP 1 PhaseID FROM GTCBreakerOperationsTable as bo WHERE bo.BreakerNumber = BreakerOperation.BreakerNumber ORDER BY TripCoilEnergized)) as LastPhase, + (SELECT TOP 1 BreakerTiming FROM GTCBreakerOperationsTable as bo WHERE bo.BreakerNumber = BreakerOperation.BreakerNumber ORDER BY TripCoilEnergized) as LastWaveformTiming, + (SELECT TOP 1 StatusTiming FROM GTCBreakerOperationsTable as bo WHERE bo.BreakerNumber = BreakerOperation.BreakerNumber ORDER BY TripCoilEnergized) as LastStatusTiming, + MaximoBreaker.[Breaker Mfr Speed] as MfrSpeed, + (SELECT Name from BreakerOperationType WHERE ID = (SELECT TOP 1 BreakerOperationTypeID FROM GTCBreakerOperationsTable as bo WHERE bo.BreakerNumber = BreakerOperation.BreakerNumber ORDER BY TripCoilEnergized)) as OperationTiming, + (SELECT TOP 1 CASE WHEN StatusTiming < BreakerTiming THEN 'Status' ELSE 'Waveform' END FROM GTCBreakerOperationsTable as bo WHERE bo.BreakerNumber = BreakerOperation.BreakerNumber ORDER BY TripCoilEnergized) as LastMethod, + -- last slow + COUNT(BOLate.ID) as TotalLateOperation, + MAX(BOLate.TripCoilEnergized) as LastLateOperation, + -- mfr info + MaximoBreaker.Manufacturer, + MaximoBreaker.[Serial Number] as SerialNum, + MaximoBreaker.[Mfr Year] as MfrYear, + MaximoBreaker.[Model Number] as ModelNum, + MaximoBreaker.[Interrupt current Rating (A)] as InterruptCurrentRating, + MaximoBreaker.[ Continuous Amp Rating (A)] as ContinuousAmpRating + FROM + GTCBreakerOperationsTable as BreakerOperation LEFT JOIN + GTCBreakerOperationsTable as BOLate ON BreakerOperation.ID = BOLate.ID AND BOLate.BreakerOperationTypeID = (SELECT ID FROM BreakerOperationType WHERE Name = 'Late') LEFT JOIN + MaximoBreakerInfo as MaximoBreaker ON BreakerOperation.BreakerNumber = SUBSTRING(MaximoBreaker.[Breaker Number], PATINDEX('%[^0]%', MaximoBreaker.[Breaker Number] + '.'), LEN(MaximoBreaker.[Breaker Number])) + WHERE + Cast(BreakerOperation.TripCoilEnergized as Date) BETWEEN {0} AND {1} + GROUP BY + BreakerOperation.BreakerNumber, + MaximoBreaker.[Breaker Number], + MaximoBreaker.[Line Name], + MaximoBreaker.Manufacturer, + MaximoBreaker.[Serial Number], + MaximoBreaker.[Mfr Year], + MaximoBreaker.[Breaker Mfr Speed], + MaximoBreaker.[Model Number], + MaximoBreaker.[Interrupt current Rating (A)], + MaximoBreaker.[ Continuous Amp Rating (A)] + ORDER BY + LastOperationDate + "; + + #endregion + + #region [ Constructors ] + + public AllBreakersReport(DateTime startTime, DateTime endTime) + { + StartTime = startTime; + EndTime = endTime; + FontDefinition = new FontDef(this, FontDef.StandardFont.Helvetica); + + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) + { +#if DEBUG + DataTable = connection.RetrieveData(TestQuery, startTime, endTime); +#else + DataTable = connection.RetrieveData(Query, startTime, endTime); +#endif + } + } + + #endregion + + #region [ Properties ] + + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public FontDef FontDefinition { get; set; } + public DataTable DataTable { get; set; } + + #endregion + + #region [ Methods ] + + public byte[] CreatePDF() + { + try + { + GenerateReport(); + + using (MemoryStream stream = new MemoryStream()) + { + formatter.Create(this, stream); + return stream.ToArray(); + } + } + catch (Exception ex) { + Log.Error(ex.ToString(), ex); + return null; + } + } + + private void GenerateReport() + { + CreatePage(); + + double verticalMillimeters = InsertHeader(); + CreateTable(verticalMillimeters); + + foreach (Page page in enum_Page) + InsertFooter(page); + } + + private double CreateTable(double verticalMillimeters) + { + if (DataTable.Rows.Count == 0) + { + verticalMillimeters += InsertItalicText(verticalMillimeters, $" No Breaker Operations during {StartTime:MM/dd/yyyy} - {EndTime:MM/dd/yyyy}"); + return verticalMillimeters; + } + + FontProp headerProp = new FontProp(FontDefinition, 0); + headerProp.rSizePoint = 7.0D; + + using (TableLayoutManager tlm = new TableLayoutManager(headerProp)) + { + tlm.tlmCellDef_Header.rAlignV = RepObj.rAlignCenter; // set vertical alignment of all header cells + tlm.tlmCellDef_Default.penProp_LineBottom = new PenProp(this, 0.05, Color.LightGray); // set bottom line for all cells + tlm.tlmHeightMode = TlmHeightMode.AdjustLast; + + tlm.eNewContainer += (oSender, ea) => + { + verticalMillimeters += tlm.rCurY_MM; + verticalMillimeters = NextTablePage(verticalMillimeters, ea); + }; + + List columns = new List() + { + new { Name = "Last Op", Column = "LastOperationDate", Width = (PageWidthMillimeters - 2 * PageMarginMillimeters)*.05 }, + new { Name = "Breaker", Column = "BreakerNumber", Width = (PageWidthMillimeters - 2 * PageMarginMillimeters) * .04 }, + new { Name = "# of Ops", Column = "Total", Width = (PageWidthMillimeters - 2 * PageMarginMillimeters) * .04 }, + new { Name = "Asset", Column = "AssetNum", Width = (PageWidthMillimeters - 2 * PageMarginMillimeters) * .05 }, + new { Name = "Line", Column = "LineName", Width = (PageWidthMillimeters - 2 * PageMarginMillimeters) * .10 }, + new { Name = "Phase", Column = "LastPhase", Width =(PageWidthMillimeters - 2 * PageMarginMillimeters) * .04 }, + new { Name = "Timing (wf)", Column = "LastWaveformTiming", Width = (PageWidthMillimeters - 2 * PageMarginMillimeters) * .05 }, + new { Name = "Timing (sb)", Column = "LastStatusTiming", Width =(PageWidthMillimeters - 2 * PageMarginMillimeters) * .05 }, + new { Name = "MFR Speed", Column = "MfrSpeed", Width =(PageWidthMillimeters - 2 * PageMarginMillimeters) * .04 }, + new { Name = "Operation Timing", Column = "OperationTiming", Width =(PageWidthMillimeters - 2 * PageMarginMillimeters) * .06 }, + new { Name = "Method", Column = "LastMethod", Width =(PageWidthMillimeters - 2 * PageMarginMillimeters) * .05 }, + new { Name = "# Late", Column = "TotalLateOperation", Width = (PageWidthMillimeters - 2 * PageMarginMillimeters) * .03 }, + new { Name = "Last Late", Column = "LastLateOperation", Width =(PageWidthMillimeters - 2 * PageMarginMillimeters) * .05 }, + new { Name = "MFR", Column = "Manufacturer", Width =(PageWidthMillimeters - 2 * PageMarginMillimeters) * .07 }, + new { Name = "Serial #", Column = "SerialNum", Width =(PageWidthMillimeters - 2 * PageMarginMillimeters) * .08 }, + new { Name = "MFR Year", Column = "MfrYear", Width =(PageWidthMillimeters - 2 * PageMarginMillimeters) * .04 }, + new { Name = "Model #", Column = "ModelNum", Width =(PageWidthMillimeters - 2 * PageMarginMillimeters) * .07}, + new { Name = "ICR (A)", Column = "InterruptCurrentRating", Width =(PageWidthMillimeters - 2 * PageMarginMillimeters) * .05}, + new { Name = "CAR (A)", Column = "ContinuousAmpRating", Width =(PageWidthMillimeters - 2 * PageMarginMillimeters) * .05 } + }; + + foreach (var column in columns) + new TlmColumnMM(tlm, column.Name, column.Width); + + List centeredCols = new List() { "Total", "BreakerNumber", "AssetNum", "LastPhase" }; + + foreach (DataRow row in DataTable.Rows) + { + tlm.NewRow(); + int index = 0; + + foreach (var column in columns) + { + if (column.Column == "LastOperationDate" || (column.Column == "LastLateOperation" && row[column.Column].ToString() != string.Empty)) + { + FontProp textProp = new FontProp(FontDefinition, 0); + textProp.rSizePoint = 6.0D; + string date = DateTime.Parse(row[column.Column].ToString()).ToString("MM/dd/yyyy"); + string time = DateTime.Parse(row[column.Column].ToString()).ToString("HH:mm:ss"); + tlm.Add(index, new RepString(textProp, date)); + tlm.NewLine(index); + tlm.Add(index++, new RepString(textProp, time)); + } + else + { + FontProp textProp = new FontProp(FontDefinition, 0); + textProp.rSizePoint = 6.0D; + string value = ""; + + value = row[column.Column].ToString(); + RepString repObj = new RepString(textProp, value); + if (centeredCols.Contains(column.Column)) + repObj.rAlignH = RepObj.rAlignCenter; + + AddDataColumn(tlm, index, value, textProp, column.Width); + index++; + + } + } + } + + tlm.Commit(); + + verticalMillimeters += tlm.rCurY_MM + 10; + } + + return verticalMillimeters; + } + + private void AddDataColumn(TlmBase tlm, int index, string value, FontProp fontProp, double width) + { + double initialWidth = fontProp.rGetTextWidthMM(value); + string remainingString = value.Trim(); + + while (initialWidth > width) + { + string[] tokens = remainingString.Split(' '); + string splitValue = tokens.First(); + + foreach (string token in tokens.Skip(1)) + { + if (fontProp.rGetTextWidthMM(splitValue + token) < width) + splitValue = splitValue + ' ' + token; + else + break; + } + + remainingString = remainingString.Replace(splitValue, ""); + tlm.Add(index, new RepString(fontProp, splitValue)); + tlm.NewLine(index); + initialWidth = fontProp.rGetTextWidthMM(remainingString); + } + + tlm.Add(index, new RepString(fontProp, remainingString)); + } + + // Creates a page and sets width and height to standard 8.5x11 inches. + private Page CreatePage() + { + Page page = new Page(this); + page.rWidthMM = PageWidthMillimeters; + page.rHeightMM = PageHeightMillimeters; + return page; + } + + // Inserts the page footer onto the given page, which includes the time of report generation as well as the page number. + private double InsertFooter(Page page) + { + FontProp font = new FontProp(FontDefinition, 0.0D); + font.rSizePoint = 12.0D; + page.AddMM(PageWidthMillimeters - PageMarginMillimeters - font.rGetTextWidthMM(page.iPageNo.ToString()), PageHeightMillimeters - 5, new RepString(font, page.iPageNo.ToString())); + return font.rSizeMM; + } + + // Inserts the page footer onto the given page, which includes the time of report generation as well as the page number. + private double InsertHeader() + { + string ReportIdentifier = "All Breakers Report - " + StartTime.ToString("MM/dd/yyyy") + " - " + EndTime.ToString("MM/dd/yyyy") + ""; + + FontProp font = new FontProp(FontDefinition, 0.0D); + font.rSizePoint = 12.0D; + + FontProp meterNameFont = new FontProp(FontDefinition, 0.0D); + meterNameFont.rSizePoint = 12.0D; + + int height = 6; + double reportIdentifierHorizontalPosition = PageWidthMillimeters / 2 - font.rGetTextWidthMM(ReportIdentifier) / 2; + page_Cur.AddMM(reportIdentifierHorizontalPosition, height, new RepString(font, ReportIdentifier)); + page_Cur.AddMM(0, 10, new RepRectMM(new BrushProp(this, Color.Black), PageWidthMillimeters, 0.1D)); + + return 20; + } + + // Inserts the given text as a section header (16-pt, bold). + private double InsertItalicText(double verticalMillimeters, string text) + { + FontProp font = new FontProp(FontDefinition, 0.0D); + font.rSizePoint = 14.0D; + font.bBold = false; + font.bItalic = true; + page_Cur.AddMM(PageMarginMillimeters, verticalMillimeters, new RepString(font, text)); + return font.rSizeMM + 5; + } + + private double NextTablePage( double verticalMillimeters, TlmBase.NewContainerEventArgs ea) + { + if (verticalMillimeters > (PageHeightMillimeters * 0.75)) + { + CreatePage(); + verticalMillimeters = InsertHeader(); + } + + ea.container.rHeightMM = PageHeightMillimeters - verticalMillimeters - PageMarginMillimeters; + page_Cur.AddMM(PageMarginMillimeters, verticalMillimeters, ea.container); + + return verticalMillimeters; + } + + #endregion + + #region [ Static ] + + private static readonly ILog Log = LogManager.GetLogger(typeof(AllBreakersReport)); + + #endregion + } +} diff --git a/Libraries/openXDA.Reports/EmailWriter.cs b/Libraries/openXDA.Reports/EmailWriter.cs new file mode 100644 index 0000000..943f9d7 --- /dev/null +++ b/Libraries/openXDA.Reports/EmailWriter.cs @@ -0,0 +1,205 @@ +//****************************************************************************************************** +// EmailWriter.cs - Gbtc +// +// Copyright © 2018, 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: +// ---------------------------------------------------------------------------------------------------- +// 07/13/2018 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Configuration; +using Gemstone.Data; +using Gemstone.Data.DataExtensions; +using Gemstone.StringExtensions; +using log4net; +using openXDA.Configuration; +using openXDA.Model; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Net; +using System.Net.Mail; +using System.Xml.Linq; + +namespace openXDA.Reports +{ + public class EmailWriter + { + #region [ Constructors ] + + public EmailWriter(PQReportsSettings reportsSettings, EmailSection emailSettings) + { + ReportSettings = reportsSettings; + EmailSettings = emailSettings; + } + + #endregion + + #region [ Properties ] + + public EmailSection EmailSettings { get; } + public PQReportsSettings ReportSettings { get; } + + #endregion + + #region [ Methods ] + + public void Execute(int month, int year) + { + GenerateEmail(month, year); + } + + private void GenerateEmail(int month, int year) + { + using(AdoDataConnection connection = new AdoDataConnection(Settings.Default)) + { + string sql = $"SELECT Email FROM UserAccount WHERE Email IS NOT NULL AND Email <> '' AND ID IN (SELECT UserAccountID FROM EmailGroupUserAccount WHERE EmailGroupID IN (SELECT EmailGroupID FROM EmailGroupType WHERE EmailTypeID IN (SELECT ID FROM EmailType WHERE EmailCategoryID = (SELECT ID FROM EmailCategory WHERE Name = 'PQReport'))))"; + DataTable emailTable = connection.RetrieveData(sql); + List recipients = emailTable.Select().Select(row => row.ConvertField("Email")).ToList(); + string template = connection.ExecuteScalar("SELECT Template FROM XSLTemplate WHERE Name = 'PQReport'"); + string data = connection.ExecuteScalar(@" + SELECT + {0} as [Month], + {1} as [Year], + (select value from DashSettings where Name = 'System.XDAInstance') as [XDALink], + (SELECT + Meter.AssetKey as [Meter], + Report.Results as [Result], + Report.ID as [ReportID] + FROM + Report JOIN + Meter ON Report.MeterID = Meter.ID + WHERE + Month = {0} AND + Year = {1} + FOR XML RAW('Report') ,TYPE, ELEMENTS) as [Reports] + FOR XML RAW ('PQReport'),TYPE, ELEMENTS + ", month, year); + + XDocument htmlDocument = XDocument.Parse(data.ApplyXSLTransform(template), LoadOptions.PreserveWhitespace); + + try + { + string subject = (string)htmlDocument.Descendants("title").FirstOrDefault() ?? "Fault detected by openXDA"; + string html = htmlDocument.ToString(SaveOptions.DisableFormatting).Replace("&", "&").Replace("<", "<").Replace(">", ">"); + + + SendEmail(recipients, subject, html); + LoadSentEmail(connection, recipients, subject, html); + + } + catch (Exception ex) + { + Log.Error(ex.ToString()); + } + + } + } + + + private int LoadSentEmail(AdoDataConnection connection, List recipients, string subject, string body) + { + TimeZoneInfo xDATimeZone = TimeZoneInfo.FindSystemTimeZoneById(connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'XDATimeZone'")); + DateTime now = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, xDATimeZone); + string toLine = string.Join("; ", recipients.Select(recipient => recipient.Trim())); + connection.ExecuteNonQuery("INSERT INTO SentEmail VALUES({0}, {1}, {2}, {3})", now, toLine, subject, body); + return connection.ExecuteScalar("SELECT @@IDENTITY"); + } + + private void SendEmail(List recipients, string subject, string body) + { + const int DefaultSMTPPort = 25; + + if (string.IsNullOrEmpty(EmailSettings.SMTPServer)) + return; + + string[] smtpServerParts = EmailSettings.SMTPServer.Split(':'); + string host = smtpServerParts[0]; + int port; + + if (smtpServerParts.Length <= 1 || !int.TryParse(smtpServerParts[1], out port)) + port = DefaultSMTPPort; + + using (SmtpClient smtpClient = new SmtpClient(host, port)) + using (MailMessage emailMessage = new MailMessage()) + { + if (!string.IsNullOrEmpty(EmailSettings.Username) && (object)EmailSettings.SecurePassword != null) + smtpClient.Credentials = new NetworkCredential(EmailSettings.Username, EmailSettings.SecurePassword); + + smtpClient.EnableSsl = EmailSettings.EnableSSL; + + emailMessage.From = new MailAddress(EmailSettings.FromAddress); + emailMessage.Subject = subject; + emailMessage.Body = body; + emailMessage.IsBodyHtml = true; + + // Add the specified To recipients for the email message + foreach (string toRecipient in recipients) + emailMessage.To.Add(toRecipient.Trim()); + + // Send the email + smtpClient.Send(emailMessage); + } + } + + + public void SendEmailWithAttachment(List recipients, string subject, string body, List attachments) + { + const int DefaultSMTPPort = 25; + + if (string.IsNullOrEmpty(EmailSettings.SMTPServer)) + return; + + string[] smtpServerParts = EmailSettings.SMTPServer.Split(':'); + string host = smtpServerParts[0]; + int port; + + if (smtpServerParts.Length <= 1 || !int.TryParse(smtpServerParts[1], out port)) + port = DefaultSMTPPort; + + using (SmtpClient smtpClient = new SmtpClient(host, port)) + using (MailMessage emailMessage = new MailMessage()) + { + if (!string.IsNullOrEmpty(EmailSettings.Username) && (object)EmailSettings.SecurePassword != null) + smtpClient.Credentials = new NetworkCredential(EmailSettings.Username, EmailSettings.SecurePassword); + + smtpClient.EnableSsl = EmailSettings.EnableSSL; + + emailMessage.From = new MailAddress(EmailSettings.FromAddress); + emailMessage.Subject = subject; + emailMessage.Body = body; + emailMessage.IsBodyHtml = true; + attachments.ForEach(x => emailMessage.Attachments.Add(x)); + + // Add the specified To recipients for the email message + foreach (string toRecipient in recipients) + emailMessage.To.Add(toRecipient.Trim()); + + // Send the email + smtpClient.Send(emailMessage); + } + } + + #endregion + + #region [ Static ] + // Static Fields + private static readonly ILog Log = LogManager.GetLogger(typeof(EmailWriter)); + #endregion + } +} diff --git a/Libraries/openXDA.Reports/IndividualBreakerReport.cs b/Libraries/openXDA.Reports/IndividualBreakerReport.cs new file mode 100644 index 0000000..4359c7e --- /dev/null +++ b/Libraries/openXDA.Reports/IndividualBreakerReport.cs @@ -0,0 +1,701 @@ +//****************************************************************************************************** +// IndividualBreakerReport.cs - Gbtc +// +// Copyright © 2019, 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: +// ---------------------------------------------------------------------------------------------------- +// 07/02/2019 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using System.Data; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Windows.Forms.DataVisualization.Charting; +using Gemstone.Data; +using log4net; +using Root.Reports; +using ChartSeries = System.Windows.Forms.DataVisualization.Charting.Series; +using Gemstone.Configuration; + +namespace openXDA.Reports +{ + public class IndividualBreakerReport : Root.Reports.Report + { + #region [ Members ] + public class Point { + public DateTime Time { get; set; } + public double? Value { get; set; } + } + // Constants + private const double PageMarginMillimeters = 25.4D; // 1-inch margin + private const double PageWidthMillimeters = 8.5D * 25.4D; // 8.5 inch width + private const double PageHeightMillimeters = 11.0D * 25.4D; // 11 inch height + + private const double FooterHeightMillimeters = (10.0D / 72.0D) * 25.4D; + private const double SpacingMillimeters = 6.0D; + + private const string TitleText = "Individual Breaker Report"; + + public const string timingQuery = + @" + SELECT + BreakerOperation.TripCoilEnergized as Time, + BreakerOperation.BreakerNumber, + MeterLine.LineName, + Phase.Name as Phase, + BreakerOperation.BreakerTiming, + CASE + WHEN Phase.Name = 'AN' THEN CAST(BreakerOperation.APhaseBreakerTiming as varchar(max)) + WHEN Phase.Name = 'BN' THEN CAST(BreakerOperation.BPhaseBreakerTiming as varchar(max)) + WHEN Phase.Name = 'CN' THEN CAST(BreakerOperation.CPhaseBreakerTiming as varchar(max)) + ELSE 'N/A' + END as WaveformTiming, + BreakerOperation.StatusTiming as StatusTiming, + MaximoBreaker.BreakerSpeed, + MaximoBreaker.BreakerSpeed * 1.12 as SpeedBandwidth, + MaximoBreaker.BreakerSpeed * 0.12 as Bandwidth, + BreakerOperationType.Name as OperationTiming, + MaximoBreaker.Manufacturer, + MaximoBreaker.SerialNum, + MaximoBreaker.MfrYear, + MaximoBreaker.ModelNum, + MaximoBreaker.InterruptCurrentRating, + MaximoBreaker.ContinuousAmpRating, + MIN(ROUND(FaultSummary.PrefaultCurrent, 0)) as PrefaultCurrent, + CASE + WHEN EventStat.IAMax >= EventStat.IBMax AND EventStat.IAMax >= EventStat.ICMax THEN Round(EventStat.IAMax, 0) + WHEN EventStat.IBMax > EventStat.IAMax AND EventStat.IAMax > EventStat.ICMax THEN Round(EventStat.IAMax, 0) + WHEN EventStat.ICMax > EventStat.IAMax AND EventStat.IAMax > EventStat.IBMax THEN Round(EventStat.IAMax, 0) + ELSE NULL + END as MaxCurrent + FROM + BreakerOperation JOIN + BreakerOperationType ON BreakerOperation.BreakerOperationTypeID = BreakerOperationType.ID JOIN + Phase ON Phase.ID = BreakerOperation.PhaseID JOIN + Event ON BreakerOperation.EventID = Event.ID JOIN + MeterLINE ON Event.MeterID = MeterLine.MeterID AND Event.LineID = MeterLine.LineID LEFT JOIN + MaximoBreaker ON BreakerOperation.BreakerNumber = SUBSTRING(MaximoBreaker.BreakerNum, PATINDEX('%[^0]%', MaximoBreaker.BreakerNum + '.'), LEN(MaximoBreaker.BreakerNum)) LEFT JOIN + FaultSummary ON Event.ID = FaultSummary.EventID AND FaultSummary.IsSelectedAlgorithm = 1 LEFT JOIN + EventStat ON Event.ID = EventStat.EventID + WHERE + BreakerOperation.BreakerNumber = {0} AND + CAST(BreakerOperation.TripCoilEnergized as Date) BETWEEN {1} AND {2} + GROUP BY + BreakerOperation.TripCoilEnergized, + BreakerOperation.BreakerNumber, + MeterLine.LineName, + Phase.Name, + BreakerOperation.BreakerTiming, + BreakerOperation.StatusTiming, + MaximoBreaker.BreakerSpeed, + MaximoBreaker.BreakerSpeed * 1.12, + MaximoBreaker.BreakerSpeed * 0.12, + BreakerOperationType.Name, + MaximoBreaker.Manufacturer, + MaximoBreaker.SerialNum, + MaximoBreaker.MfrYear, + MaximoBreaker.ModelNum, + MaximoBreaker.InterruptCurrentRating, + MaximoBreaker.ContinuousAmpRating, + BreakerOperation.APhaseBreakerTiming, + BreakerOperation.BPhaseBreakerTiming, + BreakerOperation.CPhaseBreakerTiming, + EventStat.IAMax, + EventStat.IBMax, + EventStat.ICMax + ORDER BY + Time + "; + + private const string testTimingQuery = @" + SELECT + * + FROM + IndividualBreakerQuery + WHERE + BreakerNumber = {0} AND + Cast(Time as Date) BETWEEN {1} AND {2} + ORDER BY Time + "; + + private const string infoQuery = + @" + SELECT DISTINCT + MaximoBreaker.BreakerNum as [Breaker Number], + MaximoBreaker.BreakerSpeed as [Breaker Mfr Speed], + MaximoBreaker.BreakerSpeed * 1.12 as [Speed Bandwidth], + MaximoBreaker.BreakerSpeed * 0.12 as [12% Speed Bandwidth], + MaximoBreaker.Manufacturer, + MaximoBreaker.SerialNum as [Serial Number], + MaximoBreaker.MfrYear as [Mfr Year], + MaximoBreaker.ModelNum as [Model Number], + MaximoBreaker.InterruptCurrentRating as [Interrupt Current Rating (A)], + MaximoBreaker.ContinuousAmpRating as [Continuous Amp Rating (A)] + FROM + MaximoBreaker + WHERE + BreakerNum = {0} + + "; + + private const string testInfoQuery = @" + SELECT + [Breaker Number], + [Breaker Mfr Speed], + [Speed Bandwidth], + [12% Speed Bandwidth], + Manufacturer, + [Serial Number], + [Mfr Year], + [Model Number], + [Interrupt Current Rating (A)], + [ Continuous Amp Rating (A)] + FROM + MaximoBreakerInfo + WHERE + [Breaker Number] = {0} + "; + + #endregion + + #region [ Properties ] + + public string BreakerID { get; set; } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public FontDef FontDefinition { get; set; } + public DataTable TimingDataTable { get; set; } + public DataTable InfoDataTable { get; set; } + public List TimingPoints { get; set; } + public List MaxCurrentPoints { get; set; } + + public double? SpeedBandwidth { get; set; } + public double? Speed { get; set; } + public double? InterruptCurrentRating { get; set; } + + #endregion + + #region [ Constructors ] + + public IndividualBreakerReport( string breakerId, DateTime startTime, DateTime endTime) + { + StartTime = startTime; + EndTime = endTime; + BreakerID = breakerId; + FontDefinition = new FontDef(this, "Helvetica"); + + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) { +#if DEBUG + TimingDataTable = connection.RetrieveData(testTimingQuery, breakerId, startTime, endTime); +#else + TimingDataTable = connection.RetrieveData(timingQuery, breakerId, startTime, endTime); +#endif + + TimingPoints = TimingDataTable.Select().Select(x => { + double r; + bool s = double.TryParse(x["WaveformTiming"].ToString(), out r); + return new Point { Time = DateTime.Parse(x["Time"].ToString()), Value = (s ? (double?)r : null) }; + }).OrderBy(x => x.Time).ToList(); + + MaxCurrentPoints = TimingDataTable.Select().Select(x => { + double r; + bool s = double.TryParse(x["MaxCurrent"].ToString(), out r); + return new Point { Time = DateTime.Parse(x["Time"].ToString()), Value = (s ? (double?)r : null) }; + }).OrderBy(x => x.Time).ToList(); + + +#if DEBUG + InfoDataTable = connection.RetrieveData(testInfoQuery, breakerId, startTime, endTime); +#else + InfoDataTable = connection.RetrieveData(infoQuery, breakerId, startTime, endTime); +#endif + + double result; + bool success = double.TryParse(InfoDataTable.Select().FirstOrDefault()?["Breaker Mfr Speed"].ToString(), out result); + Speed = (success ? (double?)result : null); + + success = double.TryParse(InfoDataTable.Select().FirstOrDefault()?["Speed Bandwidth"].ToString(), out result); + SpeedBandwidth = (success ? (double?)result : null); + + success = double.TryParse(InfoDataTable.Select().FirstOrDefault()?["Interrupt Current Rating (A)"].ToString(), out result); + InterruptCurrentRating = (success ? (double?)result : null); + + + } + } + + #endregion + + #region [ Methods ] + + public byte[] createPDF() + { + try + { + GenerateReport(); + + using (MemoryStream stream = new MemoryStream()) + { + this.formatter.Create(this, stream); + return stream.ToArray(); + } + } + catch (Exception ex) { + Log.Error(ex.ToString(), ex); + return null; + } + } + + private void GenerateReport() + { + CreatePage(); + double verticalMillimeters = InsertHeader(); + + verticalMillimeters = CreateInfoTable(verticalMillimeters); + verticalMillimeters = CreateTimingTable(verticalMillimeters); + + if (verticalMillimeters > (PageHeightMillimeters * 0.75)) + { + CreatePage(); + verticalMillimeters = InsertHeader(); + } + + Chart chart = GenerateTimingLineChart(); + verticalMillimeters += 75; + + if(chart != null) + page_Cur.AddMM(PageMarginMillimeters, verticalMillimeters, new RepImageMM(ChartToImage(chart), PageWidthMillimeters - 2 * PageMarginMillimeters, 75)); + + if (verticalMillimeters > (PageHeightMillimeters * 0.75)) + { + CreatePage(); + verticalMillimeters = InsertHeader(); + } + + chart = GenerateMaxCurrentLineChart(); + verticalMillimeters += 75; + + if (chart != null) + page_Cur.AddMM(PageMarginMillimeters, verticalMillimeters, new RepImageMM(ChartToImage(chart), PageWidthMillimeters - 2 * PageMarginMillimeters, 75)); + + foreach (Page page in enum_Page) + { + InsertFooter(page); + } + } + + private double CreateInfoTable(double verticalMillimeters) + { + + if (InfoDataTable.Rows.Count == 0) + { + verticalMillimeters += InsertItalicText(verticalMillimeters, $" No Breaker Information available."); + return verticalMillimeters; + } + + FontProp headerProp = new FontProp(FontDefinition, 0); + headerProp.rSizePoint = 10.0D; + using (TableLayoutManager tlm = new TableLayoutManager(headerProp)) + { + FontProp textProp = new FontProp(FontDefinition, 0); + textProp.rSizePoint = 8.0D; + tlm.tlmCellDef_Header.rAlignV = RepObj.rAlignCenter; // set vertical alignment of all header cells + tlm.tlmCellDef_Default.penProp_LineBottom = new PenProp(this, 0.05, Color.LightGray); // set bottom line for all cells + tlm.tlmHeightMode = TlmHeightMode.AdjustLast; + tlm.eNewContainer += (oSender, ea) => + { + verticalMillimeters += tlm.rCurY_MM; + verticalMillimeters = NextTablePage(verticalMillimeters, ea); + }; + + new TlmColumnMM(tlm, "Breaker Info", (PageWidthMillimeters - 2 * PageMarginMillimeters) * .5); + new TlmColumnMM(tlm, "Value", (PageWidthMillimeters - 2 * PageMarginMillimeters) * .5); + + DataRow dataRow = InfoDataTable.Select().First(); + foreach (DataColumn column in InfoDataTable.Columns) + { + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, column.ColumnName)); + tlm.Add(1, new RepString(textProp, dataRow[column.ColumnName].ToString())); + } + + tlm.Commit(); + + verticalMillimeters += tlm.rCurY_MM + 10; + } + + return verticalMillimeters; + } + + private double CreateTimingTable(double verticalMillimeters) + { + + + List columns = new List() + { + new { Name = "Time", Column = "Time", Width = (PageWidthMillimeters - 2 * PageMarginMillimeters)*.1 }, + new { Name = "Line", Column = "LineName", Width = (PageWidthMillimeters - 2 * PageMarginMillimeters)*.2 }, + new { Name = "Phase", Column = "Phase", Width = (PageWidthMillimeters - 2 * PageMarginMillimeters) * .1 }, + new { Name = "Timing (br)", Column = "BreakerTiming", Width = (PageWidthMillimeters - 2 * PageMarginMillimeters) * .1 }, + new { Name = "Timing (wf)", Column = "WaveformTiming", Width = (PageWidthMillimeters - 2 * PageMarginMillimeters) * .1}, + new { Name = "Timing (sp)", Column = "StatusTiming", Width = (PageWidthMillimeters - 2 * PageMarginMillimeters) * .1 }, + new { Name = "Classification", Column = "OperationTiming", Width = (PageWidthMillimeters - 2 * PageMarginMillimeters) * .1 }, + new { Name = "Prefault Current", Column = "PrefaultCurrent", Width =(PageWidthMillimeters - 2 * PageMarginMillimeters) * .1 }, + new { Name = "Max Current", Column = "MaxCurrent", Width =(PageWidthMillimeters - 2 * PageMarginMillimeters) * .1 }, + }; + + if (TimingDataTable.Rows.Count == 0) + { + verticalMillimeters += InsertItalicText(verticalMillimeters, $" No Breaker Operations during {StartTime.ToString("MM/dd/yyyy")} - {EndTime.ToString("MM/dd/yyyy")}"); + return verticalMillimeters; + } + + FontProp headerProp = new FontProp(FontDefinition, 0); + headerProp.rSizePoint = 10.0D; + using (TableLayoutManager tlm = new TableLayoutManager(headerProp)) + { + FontProp textProp = new FontProp(FontDefinition, 0); + textProp.rSizePoint = 8.0D; + tlm.tlmCellDef_Header.rAlignV = RepObj.rAlignCenter; // set vertical alignment of all header cells + tlm.tlmCellDef_Default.penProp_LineBottom = new PenProp(this, 0.05, Color.LightGray); // set bottom line for all cells + tlm.tlmHeightMode = TlmHeightMode.AdjustLast; + tlm.eNewContainer += (oSender, ea) => + { + verticalMillimeters += tlm.rCurY_MM; + verticalMillimeters = NextTablePage(verticalMillimeters, ea); + }; + + + // define columns + foreach (var column in columns) { + new TlmColumnMM(tlm, column.Name, column.Width); + } + + + foreach(DataRow row in TimingDataTable.Rows) + { + tlm.NewRow(); + int index = 0; + foreach (var column in columns) { + if (column.Column == "Time") + { + string date = DateTime.Parse(row[column.Column].ToString()).ToString("MM/dd/yyyy"); + string time = DateTime.Parse(row[column.Column].ToString()).ToString("HH:mm:ss"); + tlm.Add(index, new RepString(textProp, date)); + tlm.NewLine(index); + tlm.Add(index++, new RepString(textProp, time)); + + } + else if (column.Column == "LineName") + AddDataColumn(tlm, index++, row[column.Column].ToString(), textProp, column.Width); + else + tlm.Add(index++, new RepString(textProp, row[column.Column].ToString())); + } + } + + tlm.Commit(); + + verticalMillimeters += tlm.rCurY_MM + 10; + } + + return verticalMillimeters; + } + + private void AddDataColumn(TlmBase tlm, int index, string value, FontProp fontProp, double width) + { + double initialWidth = fontProp.rGetTextWidthMM(value); + string remainingString = value.Trim(); + while (initialWidth > width) + { + string[] tokens = remainingString.Split(' '); + string splitValue = tokens.First(); + + foreach (string token in tokens.Skip(1)) + { + if (fontProp.rGetTextWidthMM(splitValue + token) < width) + splitValue = splitValue + ' ' + token; + else + break; + } + + remainingString = remainingString.Replace(splitValue, ""); + tlm.Add(index, new RepString(fontProp, splitValue)); + tlm.NewLine(index); + initialWidth = fontProp.rGetTextWidthMM(remainingString); + } + tlm.Add(index, new RepString(fontProp, remainingString)); + } + + + // Creates a page and sets width and height to standard 8.5x11 inches. + private Page CreatePage() + { + Page page = new Page(this); + page.rWidthMM = PageWidthMillimeters; + page.rHeightMM = PageHeightMillimeters; + return page; + } + + // Inserts the page footer onto the given page, which includes the time of report generation as well as the page number. + private double InsertFooter(Page page) + { + FontProp font = new FontProp(FontDefinition, 0.0D); + font.rSizePoint = 12.0D; + page.AddMM(PageWidthMillimeters - PageMarginMillimeters - font.rGetTextWidthMM(page.iPageNo.ToString()), PageHeightMillimeters - 15, new RepString(font, page.iPageNo.ToString())); + return font.rSizeMM; + } + + // Inserts the page footer onto the given page, which includes the time of report generation as well as the page number. + private double InsertHeader() + { + string ReportIdentifier = "Individual Breaker Report - " + StartTime.ToString("MM/dd/yyyy") + " - " + EndTime.ToString("MM/dd/yyyy") + ""; + + FontProp font = new FontProp(FontDefinition, 0.0D); + font.rSizePoint = 12.0D; + + FontProp meterNameFont = new FontProp(FontDefinition, 0.0D); + meterNameFont.rSizePoint = 12.0D; + + int height = 6; + + double reportIdentifierHorizontalPosition = PageWidthMillimeters / 2 - font.rGetTextWidthMM(ReportIdentifier) / 2; + + page_Cur.AddMM(reportIdentifierHorizontalPosition, height, new RepString(font, ReportIdentifier)); + page_Cur.AddMM(0, 10, new RepRectMM(new BrushProp(this, Color.Black), PageWidthMillimeters, 0.1D)); + + return 20; + } + + + // Inserts the given text as a section header (16-pt, bold). + private double InsertItalicText(double verticalMillimeters, string text) + { + FontProp font = new FontProp(FontDefinition, 0.0D); + font.rSizePoint = 14.0D; + font.bBold = false; + font.bItalic = true; + page_Cur.AddMM(PageMarginMillimeters, verticalMillimeters, new RepString(font, text)); + return font.rSizeMM + 5; + } + + private double NextTablePage( double verticalMillimeters, TlmBase.NewContainerEventArgs ea) + { + if (verticalMillimeters > (PageHeightMillimeters * 0.75)) + { + CreatePage(); + verticalMillimeters = InsertHeader(); + } + + ea.container.rHeightMM = PageHeightMillimeters - verticalMillimeters - PageMarginMillimeters; + page_Cur.AddMM(PageMarginMillimeters, verticalMillimeters, ea.container); + + return verticalMillimeters; + } + + private Chart GenerateTimingLineChart() + { + if (TimingPoints.Count == 0) + { + return null; + } + + Chart chart; + double timingMax = Math.Ceiling(TimingPoints.Select(x => (x.Value == null ? 0 : (double)x.Value)).Max()); + double bandwidthMax = Math.Ceiling((SpeedBandwidth == null ? 0 : (double)SpeedBandwidth)); + + ChartArea area; + ChartSeries series; + area = new ChartArea(); + + // style x axis + area.AxisX.MajorGrid.Enabled = false; + area.AxisX.IsLabelAutoFit = false; + //area.AxisX.LabelAutoFitMinFontSize = 20; + //area.AxisX.LabelStyle.Format = "MM/dd/yyyy"; + area.AxisX.LabelStyle.Font = new Font(FontFamily.GenericSansSerif, 15); + area.AxisX.IntervalAutoMode = IntervalAutoMode.VariableCount; + area.AxisX.Maximum = TimingPoints.Count + 1; + area.AxisX.Minimum = 0; + //area.AxisX.Interval = 1; + //area.AxisX.MajorTickMark.Interval = 1; + area.AxisX.TextOrientation = TextOrientation.Rotated90; + area.AxisX.LabelStyle.Angle = -45; + //area.AxisX.LabelStyle.IsEndLabelVisible = true; + //area.AxisX.LabelStyle.IsStaggered = true; + + // style y axis + //area.AxisY.MajorGrid.LineColor = Color.LightGray; + area.AxisY.Maximum = (bandwidthMax > timingMax ? bandwidthMax : timingMax); + area.AxisY.Minimum = 0; + area.AxisY.Interval = 1; + area.AxisY.LabelAutoFitMinFontSize = 15; + area.AxisY.LabelStyle.Format = "0"; + + + //create chart + chart = new Chart(); + chart.Width = 1200; + chart.Height = 500; + chart.Name = "Waveform Timing"; + + Legend legend = new Legend("Legend"); + legend.Font = new Font(FontFamily.GenericSansSerif, 15); + chart.Legends.Add(legend); + chart.ChartAreas.Add(area); + + + series = new ChartSeries("Waveform Timing"); + series.ChartType = SeriesChartType.Line; + series.BorderWidth = 2; + series.Color = Color.Black; + series.IsValueShownAsLabel = true; + + int index = 1; + foreach (var point in TimingPoints) + { + area.AxisX.CustomLabels.Add(index, index +.9, point.Time.ToString("MM/dd/yyyy")); + + if (point.Value == null) { + //series.Points.AddY(index++); + } + else { + DataPoint dataPoint = new DataPoint(index++, (double)point.Value); + + series.Points.Add(dataPoint); + } + } + + + chart.Series.Add(series); + + if(SpeedBandwidth != null) + chart.Series.Add(MakeLimitSeries((double)SpeedBandwidth, Color.Red, "12% Bandwidth", 0, TimingPoints.Count+1)); + if (Speed != null) + chart.Series.Add(MakeLimitSeries((double)Speed, Color.Orange, "MFR Speed", 0, TimingPoints.Count+1)); + + return chart; + + } + + private Chart GenerateMaxCurrentLineChart() + { + var nonNullPoints = MaxCurrentPoints.Where(x => x.Value != null); + + if (!nonNullPoints.Any()) + { + return null; + } + + Chart chart; + double max = Math.Ceiling(nonNullPoints.Select(x => (x.Value == null ? 0 : (double)x.Value)).Max()); + double icrMax = Math.Ceiling((InterruptCurrentRating == null ? 0 : (double)InterruptCurrentRating)); + + + ChartArea area; + ChartSeries series; + area = new ChartArea(); + // style x axis + area.AxisX.MajorGrid.Enabled = false; + area.AxisX.IsLabelAutoFit = false; + //area.AxisX.LabelAutoFitMinFontSize = 20; + //area.AxisX.LabelStyle.Format = "MM/dd/yyyy"; + area.AxisX.LabelStyle.Font = new Font(FontFamily.GenericSansSerif, 15); + area.AxisX.IntervalAutoMode = IntervalAutoMode.VariableCount; + area.AxisX.Maximum = nonNullPoints.Count() + 1; + area.AxisX.Minimum = 0; + area.AxisX.Interval = 1; + area.AxisX.MajorTickMark.Interval = 1; + area.AxisX.TextOrientation = TextOrientation.Rotated90; + area.AxisX.LabelStyle.Angle = -45; + area.AxisX.LabelStyle.IsEndLabelVisible = true; + + // style y axis + //area.AxisY.MajorGrid.LineColor = Color.LightGray; + area.AxisY.Maximum = (icrMax > max ? icrMax : max); + area.AxisY.LabelAutoFitMinFontSize = 15; + area.AxisY.LabelStyle.Format = "0"; + + //create chart + chart = new Chart(); + chart.Width = 1200; + chart.Height = 500; + chart.Name = "Max Current"; + + Legend legend = new Legend("Legend"); + legend.Font = new Font(FontFamily.GenericSansSerif, 15); + chart.Legends.Add(legend); + chart.ChartAreas.Add(area); + + + series = new ChartSeries("Max Current"); + series.ChartType = SeriesChartType.Line; + series.BorderWidth = 2; + series.Color = Color.Black; + series.IsValueShownAsLabel = true; + + int index = 1; + foreach (var point in nonNullPoints) + { + area.AxisX.CustomLabels.Add(index, index + .9, point.Time.ToString("MM/dd/yyyy")); + + DataPoint dataPoint = new DataPoint(index++, (double)point.Value); + + series.Points.Add(dataPoint); + } + + chart.Series.Add(series); + + if(InterruptCurrentRating != null) + chart.Series.Add(MakeLimitSeries((double)InterruptCurrentRating, Color.Red, "ICR", 0, nonNullPoints.Count() + 1)); + + return chart; + + } + + + private ChartSeries MakeLimitSeries(double value, Color color, string name, int start, int end) + { + ChartSeries cs = new ChartSeries(name); + cs.ChartType = SeriesChartType.FastLine; + cs.BorderWidth = 1; + + cs.Color = color; + cs.Points.AddXY(start, value); + cs.Points.AddXY(end, value); + return cs; + } + + private Stream ChartToImage(Chart chart) + { + MemoryStream stream = new MemoryStream(); + chart.SaveImage(stream, ChartImageFormat.Jpeg); + stream.Position = 0; + return stream; + } + + #endregion + + #region [ Static ] + + private static readonly ILog Log = LogManager.GetLogger(typeof(PQReport)); + + #endregion + } +} diff --git a/Libraries/openXDA.Reports/PQReport.cs b/Libraries/openXDA.Reports/PQReport.cs new file mode 100644 index 0000000..d7f202e --- /dev/null +++ b/Libraries/openXDA.Reports/PQReport.cs @@ -0,0 +1,2034 @@ +//****************************************************************************************************** +// PQReport.cs - Gbtc +// +// Copyright © 2018, 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: +// ---------------------------------------------------------------------------------------------------- +// 07/12/2018 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data; +using Gemstone.Data.Model; +using log4net; +using openHistorian.XDALink; +using openXDA.Model; +using Root.Reports; +using System; +using System.Collections.Generic; +using System.Data; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Windows.Forms.DataVisualization.Charting; +using ChartSeries = System.Windows.Forms.DataVisualization.Charting.Series; + +namespace openXDA.Reports +{ + public class PQReport : Root.Reports.Report + { + #region [ Members ] + + // Constants + private const double PageMarginMillimeters = 25.4D; // 1-inch margin + private const double PageWidthMillimeters = 8.5D * 25.4D; // 8.5 inch width + private const double PageHeightMillimeters = 11.0D * 25.4D; // 11 inch height + + private const double FooterHeightMillimeters = (10.0D / 72.0D) * 25.4D; + private const double SpacingMillimeters = 6.0D; + + private const int NumBuckets = 45; + + private const string TitleText = "Monthly PQ Compliance Report"; + + private class PointGroup + { + public string Name { get; set; } + public List Data { get; set; } + public Color Color { get; set; } + } + + private class SummaryResults + { + public SummaryResults() + { + Frequency = new FrequencyData(); + VoltageLN = new VoltageData(); + VoltageLL = new VoltageData(); + Flicker = new FlickerData(); + Imbalance = new ImbalanceData(); + THD = new THDData(); + Harmonics = new HarmonicsData(); + Sags = new SagsData(); + Swells = new SwellsData(); + Interruptions = new InterruptionsData(); + Faults = new FaultsData(); + } + + public FrequencyData Frequency { get; set; } + public VoltageData VoltageLN { get; set; } + public VoltageData VoltageLL { get; set; } + public FlickerData Flicker { get; set; } + public ImbalanceData Imbalance { get; set; } + public THDData THD { get; set; } + public HarmonicsData Harmonics { get; set; } + public SagsData Sags { get; set; } + public SwellsData Swells { get; set; } + public InterruptionsData Interruptions { get; set; } + public FaultsData Faults { get; set; } + + public class FrequencyData + { + public double Min { get; set; } + public double Avg { get; set; } + public double Max { get; set; } + public string Compliance { get; set; } + public double Nominal { get; set; } + } + public class VoltageData + { + public double Min { get; set; } + public double Avg { get; set; } + public double Max { get; set; } + public string Compliance { get; set; } + public double Nominal { get; set; } + } + public class FlickerData + { + public double Max { get; set; } + public string Compliance { get; set; } + } + public class ImbalanceData + { + public double Avg { get; set; } + public string Compliance { get; set; } + } + public class THDData + { + public double Min { get; set; } + public double Avg { get; set; } + public double Max { get; set; } + public string Compliance { get; set; } + } + + public class HarmonicsData + { + public string Compliance { get; set; } + } + + public class SagsData + { + public int Count { get; set; } + } + + public class SwellsData + { + public int Count { get; set; } + } + + public class InterruptionsData + { + public int Count { get; set; } + } + + public class FaultsData + { + public int Count { get; set; } + } + } + + private class Curves + { + public static List> IticUpperCurve { get; set; } + public static List> IticLowerCurve { get; set; } + + static Curves() + { + IticUpperCurve = new List>() + { + Tuple.Create(0.0001666667D, 5.0D), + Tuple.Create(0.001D, 2.0D), + Tuple.Create(0.003D, 1.4D), + Tuple.Create(0.003D, 1.2D), + Tuple.Create(0.5D, 1.2D), + Tuple.Create(0.5D, 1.1D), + Tuple.Create(10.0D, 1.1D) + }; + IticLowerCurve = new List>() + { + Tuple.Create(0.02D, 0.0D), + Tuple.Create(0.02D, 0.7D), + Tuple.Create(0.5D, 0.7D), + Tuple.Create(0.5D, 0.8D), + Tuple.Create(10.0D, 0.8D), + Tuple.Create(10.0D, 0.9D) + }; + } + } + + #endregion + + #region [ Properties ] + + public PQReportsSettings ReportsSettings { get; set; } + public Meter Meter { get; set; } + public DateTime FirstOfMonth { get; set; } + public DateTime EndOfMonth { get; set; } + public string Result { get; set; } + public FontDef FontDefinition { get; set; } + private SummaryResults Summary { get; set; } + private AdoDataConnection Connection { get; set; } + #endregion + + #region [ Constructors ] + + public PQReport(PQReportsSettings reportsSettings, Meter meter, DateTime firstOfMonth, DateTime endOfMonth, AdoDataConnection connection) + { + ReportsSettings = reportsSettings; + Meter = meter; + FirstOfMonth = firstOfMonth; + EndOfMonth = endOfMonth; + FontDefinition = new FontDef(this, "Helvetica"); + Summary = new SummaryResults(); + Connection = connection; + } + + #endregion + + #region [ Methods ] + + public byte[] createPDF() + { + try + { + GenerateReport(); + + using (MemoryStream stream = new MemoryStream()) + { + this.formatter.Create(this, stream); + return stream.ToArray(); + } + } + catch (Exception ex) { + Log.Error(ex.ToString(), ex); + return null; + } + } + + private void GenerateReport() + { + DateTime now = DateTime.Now; + + // Build Report + Page coverPage = CreatePage(); + Page pageTwo = CreatePage(); + CreateFrequencyPage(); + CreateVoltageLNPage(); + CreateVoltageLLPage(); + CreateFlickerPage(); + CreateImbalancePage(); + CreateTHDPage(); + //CreateHarmonicsPage(); + CreateMagDurPage(); + double verticalMillimeters = CreateInteruptionsPage(); + verticalMillimeters = CreateSagsPage(verticalMillimeters); + verticalMillimeters = CreateSwellsPage(verticalMillimeters); + verticalMillimeters = CreateFaultsPage(verticalMillimeters); + CreateSummaryPage(pageTwo); + + Result = ( + Summary.Frequency.Compliance == "Pass" && + Summary.VoltageLN.Compliance == "Pass" && + Summary.VoltageLL.Compliance == "Pass" && + Summary.Flicker.Compliance == "Pass" && + Summary.Imbalance.Compliance == "Pass" && + Summary.THD.Compliance == "Pass" && + Summary.Harmonics.Compliance == "Pass" + ? "Pass" : "Fail"); + + CreateCoverPage(coverPage); + + foreach (Page page in enum_Page) + { + if (page.iPageNo != 1) + InsertFooter(page); + } + } + + private void CreateCoverPage(Page page) + { + double verticalMillimeters = PageMarginMillimeters; + verticalMillimeters += InsertTitle(page, verticalMillimeters) + SpacingMillimeters; + + } + + private void CreateFrequencyPage() + { + Page page = CreatePage(); + double verticalMillimeters = InsertHeader(page); + verticalMillimeters += InsertSectionHeader(page, verticalMillimeters, "Section 1: Power Frequency"); + verticalMillimeters += InsertNominal(page, verticalMillimeters, "Frequency", "60.00", "Hz"); + verticalMillimeters += InsertFrequencyPage(page, verticalMillimeters); + } + + private void CreateVoltageLNPage() + { + Page page = CreatePage(); + double verticalMillimeters = InsertHeader(page); + verticalMillimeters += InsertSectionHeader(page, verticalMillimeters, "Section 2a: Supply Voltage (L-N)"); + verticalMillimeters += InsertVoltageLNPage(page, verticalMillimeters); + } + + private void CreateVoltageLLPage() + { + Page page = CreatePage(); + double verticalMillimeters = InsertHeader(page); + verticalMillimeters += InsertSectionHeader(page, verticalMillimeters, "Section 2b: Supply Voltage (L-L)"); + verticalMillimeters += InsertVoltageLLPage(page, verticalMillimeters); + + } + + private void CreateFlickerPage() + { + Page page = CreatePage(); + double verticalMillimeters = InsertHeader(page); + verticalMillimeters += InsertSectionHeader(page, verticalMillimeters, "Section 3: Flicker Severity"); + verticalMillimeters += InsertFlickerPage(page, verticalMillimeters); + } + + private void CreateImbalancePage() + { + Page page = CreatePage(); + double verticalMillimeters = InsertHeader(page); + verticalMillimeters += InsertSectionHeader(page, verticalMillimeters, "Section 4: Voltage Unbalance"); + verticalMillimeters += InsertImbalancePage(page, verticalMillimeters); + } + + private void CreateTHDPage() + { + Page page = CreatePage(); + double verticalMillimeters = InsertHeader(page); + verticalMillimeters += InsertSectionHeader(page, verticalMillimeters, "Section 5: Voltage THD"); + verticalMillimeters += InsertTHDPage(page, verticalMillimeters); + } + + private void CreateHarmonicsPage() + { + Page page = CreatePage(); + double verticalMillimeters = InsertHeader(page); + verticalMillimeters += InsertSectionHeader(page, verticalMillimeters, "Section 6: Harmonics"); + } + + private double CreateInteruptionsPage() + { + Page page = CreatePage(); + double verticalMillimeters = InsertHeader(page); + verticalMillimeters += InsertSectionHeader(page, verticalMillimeters, "Section 8: Interruptions"); + + DataTable dataTable = Connection.RetrieveData(@" + Select + cast(Disturbance.StartTime as Date) as Date, + cast(Disturbance.StartTime as Time) as Time, + Disturbance.PerUnitMagnitude as Depth, + Disturbance.DurationSeconds as Duration + from + Event Join + Disturbance ON Event.ID = Disturbance.EventID + WHERE + Disturbance.PhaseID = (SELECT ID FROM Phase WHERE Name = 'Worst') AND + Event.EventTypeID = (SELECT ID FROM EventType WHERE Name ='Interruption') AND + Disturbance.StartTime BETWEEN {0} AND {1} AND + Event.MeterID = {2} + ", FirstOfMonth, EndOfMonth, Meter.ID); + + Summary.Interruptions.Count = dataTable.Rows.Count; + + if (dataTable.Rows.Count == 0) + { + verticalMillimeters += InsertItalicText( page, verticalMillimeters, $" No Interruptions during {FirstOfMonth.ToString("MM/dd/yyyy")} - {EndOfMonth.ToString("MM/dd/yyyy")}"); + return verticalMillimeters; + } + + FontProp headerProp = new FontProp(FontDefinition, 0); + headerProp.rSizePoint = 10.0D; + using (TableLayoutManager tlm = new TableLayoutManager(headerProp)) + { + FontProp textProp = new FontProp(FontDefinition, 0); + textProp.rSizePoint = 8.0D; + tlm.tlmCellDef_Header.rAlignV = RepObj.rAlignCenter; // set vertical alignment of all header cells + tlm.tlmCellDef_Default.penProp_LineBottom = new PenProp(this, 0.05, Color.LightGray); // set bottom line for all cells + tlm.tlmHeightMode = TlmHeightMode.AdjustLast; + tlm.eNewContainer += (oSender, ea) => verticalMillimeters = NextTablePage(page, verticalMillimeters, ea); + + // define columns + TlmColumn col; + col = new TlmColumnMM(tlm, "Date", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.25); + col = new TlmColumnMM(tlm, "Time", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.25); + col = new TlmColumnMM(tlm, "Depth", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.25); + col = new TlmColumnMM(tlm, "Duration", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.25); + + foreach (DataRow row in dataTable.Rows) + { + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, row["Date"].ToString())); + tlm.Add(1, new RepString(textProp, row["Time"].ToString())); + tlm.Add(2, new RepString(textProp, row["Depth"].ToString())); + tlm.Add(3, new RepString(textProp, row["Duration"].ToString())); + } + + tlm.Commit(); + + verticalMillimeters += tlm.rCurY_MM + 10; + } + + return verticalMillimeters; + } + + private double CreateSagsPage(double verticalMillimeters) + { + Page page = page_Cur; + if (verticalMillimeters > (PageHeightMillimeters * 0.75)) + { + page = CreatePage(); + verticalMillimeters = InsertHeader(page); + } + + verticalMillimeters += InsertSectionHeader(page, verticalMillimeters, "Section 9: Sags"); + + DataTable dataTable = Connection.RetrieveData(@" + Select + cast(Disturbance.StartTime as Date) as Date, + cast(Disturbance.StartTime as Time) as Time, + Disturbance.PerUnitMagnitude as Depth, + Disturbance.DurationSeconds as Duration + from + Event Join + Disturbance ON Event.ID = Disturbance.EventID + WHERE + Disturbance.PhaseID = (SELECT ID FROM Phase WHERE Name = 'Worst') AND + Event.EventTypeID = (SELECT ID FROM EventType WHERE Name ='Sag') AND + Disturbance.StartTime BETWEEN {0} AND {1} AND + Event.MeterID = {2} + ", FirstOfMonth, EndOfMonth, Meter.ID); + + Summary.Sags.Count = dataTable.Rows.Count; + + if (dataTable.Rows.Count == 0) + { + verticalMillimeters += InsertItalicText(page, verticalMillimeters, $" No Faults during {FirstOfMonth.ToString("MM/dd/yyyy")} - {EndOfMonth.ToString("MM/dd/yyyy")}"); + return verticalMillimeters; + } + + FontProp headerProp = new FontProp(FontDefinition, 0); + headerProp.rSizePoint = 10.0D; + using (TableLayoutManager tlm = new TableLayoutManager(headerProp)) + { + FontProp textProp = new FontProp(FontDefinition, 0); + textProp.rSizePoint = 8.0D; + tlm.tlmCellDef_Header.rAlignV = RepObj.rAlignCenter; // set vertical alignment of all header cells + tlm.tlmCellDef_Default.penProp_LineBottom = new PenProp(this, 0.05, Color.LightGray); // set bottom line for all cells + tlm.tlmHeightMode = TlmHeightMode.AdjustLast; + tlm.eNewContainer += (oSender, ea) => verticalMillimeters = NextTablePage(page, verticalMillimeters, ea); + // define columns + TlmColumn col; + col = new TlmColumnMM(tlm, "Date", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.25); + col = new TlmColumnMM(tlm, "Time", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.25); + col = new TlmColumnMM(tlm, "Depth", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.25); + col = new TlmColumnMM(tlm, "Duration (seconds)", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.25); + + foreach (DataRow row in dataTable.Rows) + { + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, ((DateTime)row["Date"]).ToString("MM/dd/yyyy"))); + tlm.Add(1, new RepString(textProp, row["Time"].ToString())); + tlm.Add(2, new RepString(textProp, string.Format("{0:N2}", double.Parse(row["Depth"].ToString()) * 100))); + tlm.Add(3, new RepString(textProp, string.Format("{0:N3}", double.Parse(row["Duration"].ToString())))); + } + + tlm.Commit(); + + verticalMillimeters += tlm.rCurY_MM + 10; + } + + return verticalMillimeters; + + } + + private double CreateSwellsPage(double verticalMillimeters) + { + Page page = page_Cur; + if (verticalMillimeters > (PageHeightMillimeters * 0.75)) + { + page = CreatePage(); + verticalMillimeters = InsertHeader(page); + } + + verticalMillimeters += InsertSectionHeader(page, verticalMillimeters, "Section 10: Swells"); + DataTable dataTable = Connection.RetrieveData(@" + Select + cast(Disturbance.StartTime as Date) as Date, + cast(Disturbance.StartTime as Time) as Time, + Disturbance.PerUnitMagnitude as Depth, + Disturbance.DurationSeconds as Duration + from + Event Join + Disturbance ON Event.ID = Disturbance.EventID + WHERE + Disturbance.PhaseID = (SELECT ID FROM Phase WHERE Name = 'Worst') AND + Event.EventTypeID = (SELECT ID FROM EventType WHERE Name ='Swell') AND + Disturbance.StartTime BETWEEN {0} AND {1} AND + Event.MeterID = {2} + ", FirstOfMonth, EndOfMonth, Meter.ID); + + Summary.Swells.Count = dataTable.Rows.Count; + + + if (dataTable.Rows.Count == 0) + { + verticalMillimeters += InsertItalicText(page, verticalMillimeters, $" No Swells during {FirstOfMonth.ToString("MM/dd/yyyy")} - {EndOfMonth.ToString("MM/dd/yyyy")}"); + return verticalMillimeters; + } + + FontProp headerProp = new FontProp(FontDefinition, 0); + headerProp.rSizePoint = 10.0D; + using (TableLayoutManager tlm = new TableLayoutManager(headerProp)) + { + FontProp textProp = new FontProp(FontDefinition, 0); + textProp.rSizePoint = 8.0D; + tlm.tlmCellDef_Header.rAlignV = RepObj.rAlignCenter; // set vertical alignment of all header cells + tlm.tlmCellDef_Default.penProp_LineBottom = new PenProp(this, 0.05, Color.LightGray); // set bottom line for all cells + tlm.tlmHeightMode = TlmHeightMode.AdjustLast; + tlm.eNewContainer += (oSender, ea) => verticalMillimeters = NextTablePage(page, verticalMillimeters, ea); + // define columns + TlmColumn col; + col = new TlmColumnMM(tlm, "Date", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.25); + col = new TlmColumnMM(tlm, "Time", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.25); + col = new TlmColumnMM(tlm, "Depth", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.25); + col = new TlmColumnMM(tlm, "Duration", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.25); + + foreach (DataRow row in dataTable.Rows) + { + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, row["Date"].ToString())); + tlm.Add(1, new RepString(textProp, row["Time"].ToString())); + tlm.Add(2, new RepString(textProp, string.Format("{0:N2}", double.Parse(row["Depth"].ToString()) * 100))); + tlm.Add(3, new RepString(textProp, string.Format("{0:N3}", double.Parse(row["Duration"].ToString())))); + } + + tlm.Commit(); + + verticalMillimeters += tlm.rCurY_MM + 10; + } + + return verticalMillimeters; + } + + private void CreateMagDurPage() + { + Page page = CreatePage(); + double verticalMillimeters = InsertHeader(page); + verticalMillimeters += InsertSectionHeader( page, verticalMillimeters, "Section 7: Mag-Dur Chart"); + + DataTable dataTable = Connection.RetrieveData(@" + Select + Disturbance.PerUnitMagnitude as Depth, + Disturbance.DurationSeconds as Duration + from + Event Join + Disturbance ON Event.ID = Disturbance.EventID + WHERE + Disturbance.PhaseID = (SELECT ID FROM Phase WHERE Name = 'Worst') AND + ( + Event.EventTypeID = (SELECT ID FROM EventType WHERE Name ='Sag') OR + Event.EventTypeID = (SELECT ID FROM EventType WHERE Name ='Swell') OR + Event.EventTypeID = (SELECT ID FROM EventType WHERE Name ='Interruption') + ) AND + Disturbance.StartTime BETWEEN {0} AND {1} AND + Event.MeterID = {2} + ", FirstOfMonth, EndOfMonth, Meter.ID); + + Chart chart = GenerateMagDurChart(dataTable.Select().Select(row => new Tuple(double.Parse(row["Duration"].ToString()), double.Parse(row["Depth"].ToString()))).ToList(), "0.0000 s", "0.0 %"); + verticalMillimeters += 150; + + page.AddMM(PageMarginMillimeters, verticalMillimeters, new RepImageMM(ChartToImage(chart), PageWidthMillimeters - 2 * PageMarginMillimeters, 150)); + + } + + private double CreateFaultsPage(double verticalMillimeters) + { + Page page = page_Cur; + if (verticalMillimeters > (PageHeightMillimeters * 0.75)) + { + page = CreatePage(); + verticalMillimeters = InsertHeader(page); + } + + verticalMillimeters += InsertSectionHeader(page, verticalMillimeters, "Section 11: Faults"); + DataTable dataTable = Connection.RetrieveData(@" + SELECT + Cast(FaultSummary.Inception as Date) as Date, + Cast(FaultSummary.Inception as Time) as Time, + FaultSummary.Distance, + FaultSummary.DurationSeconds as Duration + FROM + Event JOIN + FaultSummary ON Event.ID = FaultSummary.EventID + WHERE + FaultSummary.IsSelectedAlgorithm = 1 AND + FaultSummary.IsSuppressed = 0 AND + FaultSummary.IsValid = 1 AND + FaultSummary.Inception BETWEEN {0} AND {1} AND + Event.MeterID = {2} + ", FirstOfMonth, EndOfMonth, Meter.ID); + + Summary.Faults.Count = dataTable.Rows.Count; + + if (dataTable.Rows.Count == 0) + { + verticalMillimeters += InsertItalicText(page, verticalMillimeters, $" No Faults during {FirstOfMonth.ToString("MM/dd/yyyy")} - {EndOfMonth.ToString("MM/dd/yyyy")}"); + return verticalMillimeters; + } + + FontProp headerProp = new FontProp(FontDefinition, 0); + headerProp.rSizePoint = 10.0D; + using (TableLayoutManager tlm = new TableLayoutManager(headerProp)) + { + FontProp textProp = new FontProp(FontDefinition, 0); + textProp.rSizePoint = 8.0D; + tlm.tlmCellDef_Header.rAlignV = RepObj.rAlignCenter; // set vertical alignment of all header cells + tlm.tlmCellDef_Default.penProp_LineBottom = new PenProp(this, 0.05, Color.LightGray); // set bottom line for all cells + tlm.tlmHeightMode = TlmHeightMode.AdjustLast; + tlm.eNewContainer += (oSender, ea) => verticalMillimeters = NextTablePage(page, verticalMillimeters, ea); + // define columns + TlmColumn col; + col = new TlmColumnMM(tlm, "Date", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.25); + col = new TlmColumnMM(tlm, "Time", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.25); + col = new TlmColumnMM(tlm, "Distance", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.25); + col = new TlmColumnMM(tlm, "Duration", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.25); + + foreach (DataRow row in dataTable.Rows) + { + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, row["Date"].ToString())); + tlm.Add(1, new RepString(textProp, row["Time"].ToString())); + tlm.Add(2, new RepString(textProp, string.Format("{0:N2}", double.Parse(row["Distance"].ToString()) * 100))); + tlm.Add(3, new RepString(textProp, string.Format("{0:N3}", double.Parse(row["Duration"].ToString())))); + } + + tlm.Commit(); + + verticalMillimeters += tlm.rCurY_MM + 10; + } + + return verticalMillimeters; + } + + private void CreateSummaryPage(Page page) + { + double verticalMillimeters = InsertHeader(page); + verticalMillimeters += InsertSectionHeader(page, verticalMillimeters, "Summary of Results"); + + FontProp headerProp = new FontProp(FontDefinition, 0); + headerProp.rSizePoint = 10.0D; + + using (TableLayoutManager tlm = new TableLayoutManager(headerProp)) + { + FontProp textProp = new FontProp(FontDefinition, 0); + textProp.rSizePoint = 8.0D; + tlm.tlmCellDef_Header.rAlignV = RepObj.rAlignCenter; // set vertical alignment of all header cells + tlm.tlmCellDef_Default.penProp_LineBottom = new PenProp(this, 0.05, Color.LightGray); // set bottom line for all cells + tlm.tlmHeightMode = TlmHeightMode.AdjustLast; + tlm.eNewContainer += (oSender, ea) => verticalMillimeters = NextTablePage(page, verticalMillimeters, ea); + // define columns + TlmColumn col; + col = new TlmColumnMM(tlm, "Measurement", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.35); + col = new TlmColumnMM(tlm, "Min", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.15); + col = new TlmColumnMM(tlm, "Avg", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.15); + col = new TlmColumnMM(tlm, "Max", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.15); + col = new TlmColumnMM(tlm, "Compliance", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.20); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"1. Frequency ({Summary.Frequency.Nominal} Hz)")); + tlm.Add(1, new RepString(textProp, string.Format("{0:N2}", Summary.Frequency.Min))); + tlm.Add(2, new RepString(textProp, string.Format("{0:N2}", Summary.Frequency.Avg))); + tlm.Add(3, new RepString(textProp, string.Format("{0:N2}", Summary.Frequency.Max))); + tlm.Add(4, new RepString(textProp, Summary.Frequency.Compliance)); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"2a. Voltage L-N ({string.Format("{0:N0}", Summary.VoltageLN.Nominal)} V)")); + tlm.Add(1, new RepString(textProp, string.Format("{0:N2}", Summary.VoltageLN.Min))); + tlm.Add(2, new RepString(textProp, string.Format("{0:N2}", Summary.VoltageLN.Avg))); + tlm.Add(3, new RepString(textProp, string.Format("{0:N2}", Summary.VoltageLN.Max))); + tlm.Add(4, new RepString(textProp, Summary.VoltageLN.Compliance)); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"2b. Voltage L-L ({string.Format("{0:N0}", Summary.VoltageLL.Nominal)} V)")); + tlm.Add(1, new RepString(textProp, string.Format("{0:N2}", Summary.VoltageLL.Min))); + tlm.Add(2, new RepString(textProp, string.Format("{0:N2}", Summary.VoltageLL.Avg))); + tlm.Add(3, new RepString(textProp, string.Format("{0:N2}", Summary.VoltageLL.Max))); + tlm.Add(4, new RepString(textProp, Summary.VoltageLL.Compliance)); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"3. Flicker")); + tlm.Add(1, new RepString(textProp, "")); + tlm.Add(2, new RepString(textProp, "")); + tlm.Add(3, new RepString(textProp, string.Format("{0:N2}", Summary.Flicker.Max))); + tlm.Add(4, new RepString(textProp, Summary.Flicker.Compliance)); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"4. Imbalance")); + tlm.Add(1, new RepString(textProp, "")); + tlm.Add(2, new RepString(textProp, string.Format("{0:N2}", Summary.Imbalance.Avg))); + tlm.Add(3, new RepString(textProp, "")); + tlm.Add(4, new RepString(textProp, Summary.Imbalance.Compliance)); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"5. THD")); + tlm.Add(1, new RepString(textProp, string.Format("{0:N2}", Summary.THD.Min))); + tlm.Add(2, new RepString(textProp, string.Format("{0:N2}", Summary.THD.Avg))); + tlm.Add(3, new RepString(textProp, string.Format("{0:N2}", Summary.THD.Max))); + tlm.Add(4, new RepString(textProp, Summary.THD.Compliance)); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"6. Harmonics")); + tlm.Add(1, new RepString(textProp, "")); + tlm.Add(2, new RepString(textProp, "")); + tlm.Add(3, new RepString(textProp, "")); + tlm.Add(4, new RepString(textProp, Summary.Harmonics.Compliance)); + + tlm.Commit(); + + verticalMillimeters += tlm.rCurY_MM + 10; + } + + using (TableLayoutManager tlm = new TableLayoutManager(headerProp)) + { + FontProp textProp = new FontProp(FontDefinition, 0); + textProp.rSizePoint = 8.0D; + tlm.tlmCellDef_Header.rAlignV = RepObj.rAlignCenter; // set vertical alignment of all header cells + tlm.tlmCellDef_Default.penProp_LineBottom = new PenProp(this, 0.05, Color.LightGray); // set bottom line for all cells + tlm.tlmHeightMode = TlmHeightMode.AdjustLast; + tlm.eNewContainer += (oSender, ea) => verticalMillimeters = NextTablePage(page, verticalMillimeters, ea); + // define columns + TlmColumn col; + col = new TlmColumnMM(tlm, "Event Type", (PageWidthMillimeters / 2 - PageMarginMillimeters) * 0.65); + col = new TlmColumnMM(tlm, "Count", (PageWidthMillimeters / 2 - PageMarginMillimeters) * 0.35); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"8. Interruptions")); + tlm.Add(1, new RepString(textProp, string.Format("{0}", Summary.Interruptions.Count))); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"9. Sags")); + tlm.Add(1, new RepString(textProp, string.Format("{0}", Summary.Sags.Count))); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"10. Swells")); + tlm.Add(1, new RepString(textProp, string.Format("{0}", Summary.Swells.Count))); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"11. Faults")); + tlm.Add(1, new RepString(textProp, string.Format("{0}", Summary.Faults.Count))); + + tlm.Commit(); + + verticalMillimeters += tlm.rCurY_MM + 10; + } + } + + // Creates a page and sets width and height to standard 8.5x11 inches. + private Page CreatePage() + { + Page page = new Page(this); + page.rWidthMM = PageWidthMillimeters; + page.rHeightMM = PageHeightMillimeters; + return page; + } + + // Inserts the title and company text on the given page. + private double InsertTitle(Page page, double verticalMillimeters) + { + FontProp titleFont = new FontProp(FontDefinition, 0.0D); + FontProp companyFont = new FontProp(FontDefinition, 0.0D); + + titleFont.rSizePoint = 20.0D; + companyFont.rSizePoint = 14.0D; + titleFont.bBold = true; + companyFont.bBold = true; + + // Title + page.AddCB_MM(verticalMillimeters + titleFont.rSizeMM, new RepString(titleFont, TitleText)); + verticalMillimeters += 1.5D * titleFont.rSizeMM; + verticalMillimeters += 5; + + // Company + page.AddCB_MM(verticalMillimeters + companyFont.rSizeMM, new RepString(companyFont, $"{Connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'CompanyName'")}")); + + verticalMillimeters += 1.5D * titleFont.rSizeMM; + + // openXDA + page.AddCB_MM(verticalMillimeters + companyFont.rSizeMM, new RepString(companyFont, $"openXDA")); + + verticalMillimeters += 1.5D * titleFont.rSizeMM; + + // Date + page.AddCB_MM(verticalMillimeters + companyFont.rSizeMM, new RepString(companyFont, $"{FirstOfMonth.ToString("MMMM yyyy")}")); + + verticalMillimeters += 1.5D * titleFont.rSizeMM; + + // Meter Name + page.AddCB_MM(verticalMillimeters + companyFont.rSizeMM, new RepString(companyFont, $"{Meter.Name} - {Result}")); + + verticalMillimeters += 1.5D * companyFont.rSizeMM; + + + return verticalMillimeters; + } + + // Inserts the page footer onto the given page, which includes the time of report generation as well as the page number. + private double InsertFooter(Page page) + { + FontProp font = new FontProp(FontDefinition, 0.0D); + font.rSizePoint = 12.0D; + page.AddMM(PageWidthMillimeters - PageMarginMillimeters - font.rGetTextWidthMM(page.iPageNo.ToString()), PageHeightMillimeters - 15, new RepString(font, page.iPageNo.ToString())); + return font.rSizeMM; + } + + // Inserts the page footer onto the given page, which includes the time of report generation as well as the page number. + private double InsertHeader(Page page) + { + const string ReportIdentifier = "PQ Report - openXDA"; + + FontProp font = new FontProp(FontDefinition, 0.0D); + font.rSizePoint = 12.0D; + + FontProp meterNameFont = new FontProp(FontDefinition, 0.0D); + meterNameFont.rSizePoint = 12.0D; + + int height = 6; + + double reportIdentifierHorizontalPosition = PageWidthMillimeters / 2 - font.rGetTextWidthMM(ReportIdentifier) / 2; + double meterNameMaxWidth = reportIdentifierHorizontalPosition - PageMarginMillimeters - 4; + string meterName = Meter.Name; + + while (meterNameFont.rGetTextWidthMM(meterName) > meterNameMaxWidth) + meterNameFont.rSizePoint -= 1.0D; + + page.AddMM(PageMarginMillimeters, height, new RepString(meterNameFont, Meter.Name)); + page.AddMM(reportIdentifierHorizontalPosition, height, new RepString(font, ReportIdentifier)); + page.AddMM(PageWidthMillimeters - PageMarginMillimeters - font.rGetTextWidthMM(FirstOfMonth.ToString("MMMM yyyy")), height, new RepString(font, FirstOfMonth.ToString("MMMM yyyy"))); + page.AddMM(0, 10, new RepRectMM(new BrushProp(this, Color.Black), PageWidthMillimeters, 0.1D)); + + return 20; + } + + + // Inserts the given text as a section header (16-pt, bold). + private double InsertSectionHeader(Page page, double verticalMillimeters, string text) + { + FontProp font = new FontProp(FontDefinition, 0.0D); + font.rSizePoint = 14.0D; + font.bBold = true; + page.AddMM(PageMarginMillimeters, verticalMillimeters, new RepString(font, text)); + return font.rSizeMM + 5; + } + + // Inserts the given text as a section header (16-pt, bold). + private double InsertItalicText(Page page, double verticalMillimeters, string text) + { + FontProp font = new FontProp(FontDefinition, 0.0D); + font.rSizePoint = 14.0D; + font.bBold = false; + font.bItalic = true; + page.AddMM(PageMarginMillimeters, verticalMillimeters, new RepString(font, text)); + return font.rSizeMM + 5; + } + + // Inserts the given text as a section header (16-pt, bold). + private double InsertNominal(Page page, double verticalMillimeters, string type, string value, string units) + { + FontProp font = new FontProp(FontDefinition, 0.0D); + font.rSizePoint = 10.0D; + page.AddMM(PageMarginMillimeters, verticalMillimeters, new RepString(font, $"Nominal {type}: {value} {units}")); + return font.rSizeMM + 5; + } + + private double NextTablePage(Page page, double verticalMillimeters, TlmBase.NewContainerEventArgs ea) + { + if (verticalMillimeters > (PageHeightMillimeters * 0.75)) + { + page = CreatePage(); + verticalMillimeters = InsertHeader(page); + verticalMillimeters += InsertSectionHeader(page, verticalMillimeters, "Section 8: Sags (cont.)"); + } + + ea.container.rHeightMM = PageHeightMillimeters - verticalMillimeters - PageMarginMillimeters; + page.AddMM(PageMarginMillimeters, verticalMillimeters, ea.container); + + return verticalMillimeters; + } + + private double InsertFrequencyPage(Page page, double verticalMillimeters) + { + string historianServer = Connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'Historian.Server'") ?? "127.0.0.1"; + string historianInstance = Connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'Historian.Instance'") ?? "XDA"; + IEnumerable channels = new TableOperations(Connection).QueryRecordsWhere("MeterID = {0} AND MeasurementCharacteristicID = (SELECT ID FROM MeasurementCharacteristic WHERE Name = 'Frequency') AND MeasurementTypeID = (SELECT ID FROM MeasurementType WHERE Name = 'Voltage')", Meter.ID); + + if (!channels.Any()) + { + verticalMillimeters += InsertItalicText(page, verticalMillimeters, $" No Frequency channels setup on this meter."); + Summary.Frequency.Compliance = "Pass"; + return verticalMillimeters; + } + + double nominal = Summary.Frequency.Nominal = 60.0D; + + using (Historian historian = new Historian(historianServer, historianInstance)) + { + List points = historian.Read(channels.Select(x => x.ID), FirstOfMonth, EndOfMonth).ToList(); + + if (!points.Any()) + { + verticalMillimeters += InsertItalicText(page, verticalMillimeters, $" No Frequency data during {FirstOfMonth.ToString("MM/dd/yyyy")} - {EndOfMonth.ToString("MM/dd/yyyy")}"); + return verticalMillimeters; + } + + List avg = points + .Where(point => point.SeriesID == SeriesID.Average) + .Select(point => point.Value) + .OrderBy(value => value) + .ToList(); + + int firstPercentileSkipCount = (int)(avg.Count * (100.0D - ReportsSettings.FirstFrequencyPercentile) / 100.0D / 2.0D); + double firstPercentileMinLimit = nominal * (100.0D - ReportsSettings.FirstFrequencyDeviationLimit) / 100.0D; + double firstPercentileMaxLimit = nominal * (100.0D + ReportsSettings.FirstFrequencyDeviationLimit) / 100.0D; + double firstPercentileMin = avg.Skip(firstPercentileSkipCount).FirstOrDefault(); + double firstPercentileMax = avg.AsEnumerable().Reverse().Skip(firstPercentileSkipCount).FirstOrDefault(); + + bool firstTest = + firstPercentileMin >= firstPercentileMinLimit && + firstPercentileMax <= firstPercentileMaxLimit; + + int secondPercentileSkipCount = (int)(avg.Count * (100.0D - ReportsSettings.SecondFrequencyPercentile) / 100.0D / 2.0D); + double secondPercentileMinLimit = nominal * (100.0D - ReportsSettings.SecondFrequencyDeviationLimit) / 100.0D; + double secondPercentileMaxLimit = nominal * (100.0D + ReportsSettings.SecondFrequencyDeviationLimit) / 100.0D; + double secondPercentileMin = avg.Skip(secondPercentileSkipCount).FirstOrDefault(); + double secondPercentileMax = avg.AsEnumerable().Reverse().Skip(secondPercentileSkipCount).FirstOrDefault(); + + bool secondTest = + secondPercentileMin >= secondPercentileMinLimit && + secondPercentileMax <= secondPercentileMaxLimit; + + Summary.Frequency.Compliance = (firstTest && secondTest ? "Pass" : "Fail"); + + FontProp headerProp = new FontProp(FontDefinition, 0); + headerProp.rSizePoint = 10.0D; + + using (TableLayoutManager tlm = new TableLayoutManager(headerProp)) + { + FontProp textProp = new FontProp(FontDefinition, 0); + textProp.rSizePoint = 8.0D; + + tlm.tlmCellDef_Header.rAlignV = RepObj.rAlignCenter; // set vertical alignment of all header cells + tlm.tlmCellDef_Default.penProp_LineBottom = new PenProp(this, 0.05, Color.LightGray); // set bottom line for all cells + tlm.tlmHeightMode = TlmHeightMode.AdjustLast; + tlm.eNewContainer += (oSender, ea) => page.AddMM(PageMarginMillimeters, verticalMillimeters, ea.container); + + // define columns + TlmColumn col; + col = new TlmColumnMM(tlm, "Monthly PQ Report Requirement", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.40); + col = new TlmColumnMM(tlm, "Measured Frequency", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.40); + col = new TlmColumnMM(tlm, "Result", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.20); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"{ReportsSettings.FirstFrequencyPercentile:0.##}% of the time: {firstPercentileMinLimit:N2} Hz - {firstPercentileMaxLimit:N2} Hz")); + tlm.Add(1, new RepString(textProp, (avg.Any() ? $"{firstPercentileMin:N2} - {firstPercentileMax:N2}" : "No Data Provided"))); + tlm.Add(2, new RepString(textProp, (firstTest ? "Pass" : "Fail"))); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"{ReportsSettings.SecondFrequencyPercentile:0.##}% of the time: {secondPercentileMinLimit:N2} Hz - {secondPercentileMaxLimit:N2} Hz")); + tlm.Add(1, new RepString(textProp, (avg.Any() ? $"{secondPercentileMin:N2} - {secondPercentileMax:N2}" : "No Data Provided"))); + tlm.Add(2, new RepString(textProp, (secondTest ? "Pass" : "Fail"))); + + tlm.Commit(); + + verticalMillimeters += tlm.rCurY_MM + 10; + } + + List pointGroups = new List(); + + pointGroups.Add(new PointGroup() + { + Name = "Frequency", + Color = Color.DarkBlue, + Data = points.Where(point => point.SeriesID == SeriesID.Average).ToList() + }); + + pointGroups.Add(new PointGroup() + { + Name = "Frequency Max", + Color = Color.DarkGreen, + Data = points.Where(point => point.SeriesID == SeriesID.Maximum).ToList() + }); + + pointGroups.Add(new PointGroup() + { + Name = "Frequency Min", + Color = Color.Purple, + Data = points.Where(point => point.SeriesID == SeriesID.Minimum).ToList() + }); + + Chart chart = GenerateLineChart("Hz", pointGroups, secondPercentileMaxLimit + (secondPercentileMaxLimit - secondPercentileMinLimit) * .10, secondPercentileMinLimit - (secondPercentileMaxLimit - secondPercentileMinLimit) * .10, "MM/dd", "0.00", "Frequency", secondPercentileMaxLimit, firstPercentileMaxLimit, secondPercentileMinLimit, firstPercentileMinLimit, FirstOfMonth, EndOfMonth); + verticalMillimeters += 75; + + page.AddMM(PageMarginMillimeters, verticalMillimeters, new RepImageMM(ChartToImage(chart), PageWidthMillimeters - 2 * PageMarginMillimeters, 75)); + verticalMillimeters += 75; + + double maxValue = Summary.Frequency.Max = avg.Max(); + double minValue = Summary.Frequency.Min = avg.Min(); + chart = GenerateBarChart("0.00 Hz", avg, maxValue, minValue); + + page.AddMM(PageMarginMillimeters, verticalMillimeters, new RepImageMM(ChartToImage(chart), PageWidthMillimeters - 2 * PageMarginMillimeters, 75)); + + return verticalMillimeters; + } + } + + private double InsertVoltageLNPage( Page page, double verticalMillimeters) + { + string historianServer = Connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'Historian.Server'") ?? "127.0.0.1"; + string historianInstance = Connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'Historian.Instance'") ?? "XDA"; + + Channel line1 = new TableOperations(Connection).QueryRecordWhere("MeterID = {0} AND MeasurementCharacteristicID = (SELECT ID FROM MeasurementCharacteristic WHERE Name = 'RMS') AND MeasurementTypeID = (SELECT ID FROM MeasurementType WHERE Name = 'Voltage') AND PhaseID = (SELECT ID FROM Phase WHERE Name = 'AN')", Meter.ID); + Channel line2 = new TableOperations(Connection).QueryRecordWhere("MeterID = {0} AND MeasurementCharacteristicID = (SELECT ID FROM MeasurementCharacteristic WHERE Name = 'RMS') AND MeasurementTypeID = (SELECT ID FROM MeasurementType WHERE Name = 'Voltage') AND PhaseID = (SELECT ID FROM Phase WHERE Name = 'BN')", Meter.ID); + Channel line3 = new TableOperations(Connection).QueryRecordWhere("MeterID = {0} AND MeasurementCharacteristicID = (SELECT ID FROM MeasurementCharacteristic WHERE Name = 'RMS') AND MeasurementTypeID = (SELECT ID FROM MeasurementType WHERE Name = 'Voltage') AND PhaseID = (SELECT ID FROM Phase WHERE Name = 'CN')", Meter.ID); + + int lineId; + + if (line1 == null && line2 == null && line3 == null) + { + verticalMillimeters += InsertItalicText(page, verticalMillimeters, $" No L-N Voltage channels setup on this meter."); + Summary.VoltageLN.Compliance = "Pass"; + return verticalMillimeters; + } + + if (line1 != null) + lineId = line1.AssetID; + else if (line2 != null) + lineId = line2.AssetID; + else + lineId = line3.AssetID; + + double nominal = Summary.VoltageLN.Nominal = Connection.ExecuteScalar("SELECT VoltageKV FROM Asset WHERE ID = {0}", lineId) * 1000 / Math.Sqrt(3); + verticalMillimeters += InsertNominal(page, verticalMillimeters, "Voltage", string.Format("{0:N0}", nominal), "V L-N"); + + using (Historian historian = new Historian(historianServer, historianInstance)) + { + List points = historian.Read(new List() { line1.ID, line2.ID, line3.ID }, FirstOfMonth, EndOfMonth).ToList(); + + List avgLine1 = points + .Where(point => point.ChannelID == line1.ID) + .Where(point => point.SeriesID == SeriesID.Average) + .Select(point => point.Value) + .OrderBy(value => value) + .ToList(); + + List avgLine2 = points + .Where(point => point.ChannelID == line2.ID) + .Where(point => point.SeriesID == SeriesID.Average) + .Select(point => point.Value) + .OrderBy(value => value) + .ToList(); + + List avgLine3 = points + .Where(point => point.ChannelID == line1.ID) + .Where(point => point.SeriesID == SeriesID.Average) + .Select(point => point.Value) + .OrderBy(value => value) + .ToList(); + + if (!avgLine1.Any() && !avgLine2.Any() && !avgLine3.Any()) + { + verticalMillimeters += InsertItalicText(page, verticalMillimeters, $" No L-N Voltage data during {FirstOfMonth.ToString("MM/dd/yyyy")} - {EndOfMonth.ToString("MM/dd/yyyy")}"); + Summary.VoltageLN.Compliance = "Pass"; + return verticalMillimeters; + } + + Summary.VoltageLN.Min = points.Where(point => point.SeriesID == SeriesID.Minimum).Select(point => point.Value).Min(); + Summary.VoltageLN.Avg = points.Where(point => point.SeriesID == SeriesID.Average).Select(point => point.Value).Average(); + Summary.VoltageLN.Max = points.Where(point => point.SeriesID == SeriesID.Maximum).Select(point => point.Value).Max(); + + double firstPercentileMinLimit = nominal * (100.0D - ReportsSettings.FirstVoltageDeviationLimit) / 100.0D; + double firstPercentileMaxLimit = nominal * (100.0D + ReportsSettings.FirstVoltageDeviationLimit) / 100.0D; + + int firstPercentileSkipCountLine1 = (int)(avgLine1.Count * (100.0D - ReportsSettings.FirstVoltagePercentile) / 100.0D / 2.0D); + int firstPercentileSkipCountLine2 = (int)(avgLine2.Count * (100.0D - ReportsSettings.FirstVoltagePercentile) / 100.0D / 2.0D); + int firstPercentileSkipCountLine3 = (int)(avgLine3.Count * (100.0D - ReportsSettings.FirstVoltagePercentile) / 100.0D / 2.0D); + double firstPercentileMinLine1 = avgLine1.Skip(firstPercentileSkipCountLine1).FirstOrDefault(); + double firstPercentileMinLine2 = avgLine2.Skip(firstPercentileSkipCountLine2).FirstOrDefault(); + double firstPercentileMinLine3 = avgLine3.Skip(firstPercentileSkipCountLine3).FirstOrDefault(); + double firstPercentileMaxLine1 = avgLine1.AsEnumerable().Reverse().Skip(firstPercentileSkipCountLine1).FirstOrDefault(); + double firstPercentileMaxLine2 = avgLine2.AsEnumerable().Reverse().Skip(firstPercentileSkipCountLine2).FirstOrDefault(); + double firstPercentileMaxLine3 = avgLine3.AsEnumerable().Reverse().Skip(firstPercentileSkipCountLine3).FirstOrDefault(); + + bool firstTest = + firstPercentileMinLine1 >= firstPercentileMinLimit && + firstPercentileMaxLine1 <= firstPercentileMaxLimit && + firstPercentileMinLine2 >= firstPercentileMinLimit && + firstPercentileMaxLine2 <= firstPercentileMaxLimit && + firstPercentileMinLine3 >= firstPercentileMinLimit && + firstPercentileMaxLine3 <= firstPercentileMaxLimit; + + double secondPercentileMinLimit = nominal * (100.0D - ReportsSettings.SecondVoltageDeviationLimit) / 100.0D; + double secondPercentileMaxLimit = nominal * (100.0D + ReportsSettings.SecondVoltageDeviationLimit) / 100.0D; + + int secondPercentileSkipCountLine1 = (int)(avgLine1.Count * (100.0D - ReportsSettings.SecondVoltagePercentile) / 100.0D / 2.0D); + int secondPercentileSkipCountLine2 = (int)(avgLine2.Count * (100.0D - ReportsSettings.SecondVoltagePercentile) / 100.0D / 2.0D); + int secondPercentileSkipCountLine3 = (int)(avgLine3.Count * (100.0D - ReportsSettings.SecondVoltagePercentile) / 100.0D / 2.0D); + double secondPercentileMinLine1 = avgLine1.Skip(secondPercentileSkipCountLine1).FirstOrDefault(); + double secondPercentileMinLine2 = avgLine2.Skip(secondPercentileSkipCountLine2).FirstOrDefault(); + double secondPercentileMinLine3 = avgLine3.Skip(secondPercentileSkipCountLine3).FirstOrDefault(); + double secondPercentileMaxLine1 = avgLine1.AsEnumerable().Reverse().Skip(secondPercentileSkipCountLine1).FirstOrDefault(); + double secondPercentileMaxLine2 = avgLine2.AsEnumerable().Reverse().Skip(secondPercentileSkipCountLine2).FirstOrDefault(); + double secondPercentileMaxLine3 = avgLine3.AsEnumerable().Reverse().Skip(secondPercentileSkipCountLine3).FirstOrDefault(); + + bool secondTest = + secondPercentileMinLine1 >= secondPercentileMinLimit && + secondPercentileMaxLine1 <= secondPercentileMaxLimit && + secondPercentileMinLine2 >= secondPercentileMinLimit && + secondPercentileMaxLine2 <= secondPercentileMaxLimit && + secondPercentileMinLine3 >= secondPercentileMinLimit && + secondPercentileMaxLine3 <= secondPercentileMaxLimit; + + Summary.VoltageLN.Compliance = (firstTest && secondTest ? "Pass" : "Fail"); + + FontProp headerProp = new FontProp(FontDefinition, 0); + headerProp.rSizePoint = 10.0D; + + using (TableLayoutManager tlm = new TableLayoutManager(headerProp)) + { + FontProp textProp = new FontProp(FontDefinition, 0); + textProp.rSizePoint = 8.0D; + tlm.tlmCellDef_Header.rAlignV = RepObj.rAlignCenter; // set vertical alignment of all header cells + tlm.tlmCellDef_Default.penProp_LineBottom = new PenProp(this, 0.05, Color.LightGray); // set bottom line for all cells + tlm.tlmHeightMode = TlmHeightMode.AdjustLast; + tlm.eNewContainer += (oSender, ea) => page.AddMM(PageMarginMillimeters, verticalMillimeters, ea.container); + + // define columns + TlmColumn col; + col = new TlmColumnMM(tlm, "Monthly PQ Report Requirement", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.30); + col = new TlmColumnMM(tlm, "Measured L1 Voltage", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.20); + col = new TlmColumnMM(tlm, "Measured L2 Voltage", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.20); + col = new TlmColumnMM(tlm, "Measured L3 Voltage", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.20); + col = new TlmColumnMM(tlm, "Result", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.10); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"{ReportsSettings.FirstVoltagePercentile:0.###}% of the time: {firstPercentileMinLimit:N1} V - {firstPercentileMaxLimit:N1} V")); + tlm.Add(1, new RepString(textProp, (avgLine1.Any() ? $"{firstPercentileMinLine1:N1}V - {firstPercentileMaxLine1:N1}V" : "No data for time period"))); + tlm.Add(2, new RepString(textProp, (avgLine2.Any() ? $"{firstPercentileMinLine2:N1}V - {firstPercentileMaxLine2:N2}V" : "No data for time period"))); + tlm.Add(3, new RepString(textProp, (avgLine3.Any() ? $"{firstPercentileMinLine3:N1}V - {firstPercentileMaxLine3:N2}V" : "No data for time period"))); + tlm.Add(4, new RepString(textProp, (firstTest ? "Pass" : "Fail"))); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"{ReportsSettings.SecondVoltagePercentile:0.###}% of the time: {secondPercentileMinLimit:N1} V - {secondPercentileMaxLine1:N1} V")); + tlm.Add(1, new RepString(textProp, (avgLine1.Any() ? $"{secondPercentileMinLine1:N1}V - {secondPercentileMaxLine1:N1}V" : "No data for time period"))); + tlm.Add(2, new RepString(textProp, (avgLine2.Any() ? $"{secondPercentileMinLine2:N1}V - {secondPercentileMaxLine2:N1}V" : "No data for time period"))); + tlm.Add(3, new RepString(textProp, (avgLine3.Any() ? $"{secondPercentileMinLine3:N1}V - {secondPercentileMaxLine3:N1}V" : "No data for time period"))); + tlm.Add(4, new RepString(textProp, (secondTest ? "Pass" : "Fail"))); + + tlm.Commit(); + + verticalMillimeters += tlm.rCurY_MM + 10; + } + + List pointGroups = new List(); + + pointGroups.Add(new PointGroup() + { + Name = "L1-N", + Color = Color.DarkBlue, + Data = points + .Where(point => point.ChannelID == line1.ID) + .Where(point => point.SeriesID == SeriesID.Average) + .ToList() + }); + + pointGroups.Add(new PointGroup() + { + Name = "L2-N", + Color = Color.DarkGreen, + Data = points + .Where(point => point.ChannelID == line2.ID) + .Where(point => point.SeriesID == SeriesID.Average) + .ToList() + }); + + pointGroups.Add(new PointGroup() + { + Name = "L3-N", + Color = Color.Purple, + Data = points + .Where(point => point.ChannelID == line3.ID) + .Where(point => point.SeriesID == SeriesID.Average) + .ToList() + }); + + List avg = points + .Where(point => point.SeriesID == SeriesID.Average) + .Select(point => point.Value) + .ToList(); + + double maxValue = avg.Max(); + double minValue = avg.Min(); + double chartHigh = (maxValue > secondPercentileMaxLimit ? maxValue : secondPercentileMaxLimit); + double chartLow = (minValue < secondPercentileMinLimit ? minValue : secondPercentileMinLimit); + double chartMax = chartHigh + (chartHigh - chartLow) * .10; + double chartMin = chartLow - (chartHigh - chartLow) * .10; + + Chart chart = GenerateLineChart("V", pointGroups, chartMax, chartMin, "MM/dd", "0.00", "Voltage L-L", secondPercentileMaxLimit, firstPercentileMaxLimit, secondPercentileMinLimit, firstPercentileMinLimit, FirstOfMonth, EndOfMonth); + verticalMillimeters += 75; + + page.AddMM(PageMarginMillimeters, verticalMillimeters, new RepImageMM(ChartToImage(chart), PageWidthMillimeters - 2 * PageMarginMillimeters, 75)); + verticalMillimeters += 75; + + chart = GenerateBarChart("0", avg, maxValue, minValue); + page.AddMM(PageMarginMillimeters, verticalMillimeters, new RepImageMM(ChartToImage(chart), PageWidthMillimeters - 2 * PageMarginMillimeters, 75)); + + return verticalMillimeters; + } + } + + private double InsertVoltageLLPage(Page page, double verticalMillimeters) + { + string historianServer = Connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'Historian.Server'") ?? "127.0.0.1"; + string historianInstance = Connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'Historian.Instance'") ?? "XDA"; + IEnumerable channels = new TableOperations(Connection).QueryRecordsWhere("MeterID = {0} AND MeasurementCharacteristicID = (SELECT ID FROM MeasurementCharacteristic WHERE Name = 'RMS') AND MeasurementTypeID = (SELECT ID FROM MeasurementType WHERE Name = 'Voltage')", Meter.ID); + + Channel line1 = new TableOperations(Connection).QueryRecordWhere("MeterID = {0} AND MeasurementCharacteristicID = (SELECT ID FROM MeasurementCharacteristic WHERE Name = 'RMS') AND MeasurementTypeID = (SELECT ID FROM MeasurementType WHERE Name = 'Voltage') AND PhaseID = (SELECT ID FROM Phase WHERE Name = 'AB')", Meter.ID); + Channel line2 = new TableOperations(Connection).QueryRecordWhere("MeterID = {0} AND MeasurementCharacteristicID = (SELECT ID FROM MeasurementCharacteristic WHERE Name = 'RMS') AND MeasurementTypeID = (SELECT ID FROM MeasurementType WHERE Name = 'Voltage') AND PhaseID = (SELECT ID FROM Phase WHERE Name = 'BC')", Meter.ID); + Channel line3 = new TableOperations(Connection).QueryRecordWhere("MeterID = {0} AND MeasurementCharacteristicID = (SELECT ID FROM MeasurementCharacteristic WHERE Name = 'RMS') AND MeasurementTypeID = (SELECT ID FROM MeasurementType WHERE Name = 'Voltage') AND PhaseID = (SELECT ID FROM Phase WHERE Name = 'CA')", Meter.ID); + + int lineId; + + if (line1 == null && line2 == null && line3 == null) + { + verticalMillimeters += InsertItalicText(page, verticalMillimeters, $" No L-L Voltage channels setup on this meter."); + Summary.VoltageLL.Compliance = "Pass"; + return verticalMillimeters; + } + + if (line1 != null) + lineId = line1.AssetID; + else if (line2 != null) + lineId = line2.AssetID; + else + lineId = line3.AssetID; + + double nominal = Summary.VoltageLL.Nominal = Connection.ExecuteScalar("SELECT VoltageKV FROM Asset WHERE ID = {0}", lineId) * 1000; + verticalMillimeters += InsertNominal(page, verticalMillimeters, "Voltage", string.Format("{0:N0}", nominal), "V L-L"); + + using (Historian historian = new Historian(historianServer, historianInstance)) + { + List points = historian.Read(new List() { line1.ID, line2.ID, line3.ID }, FirstOfMonth, EndOfMonth).ToList(); + + List avgLine1 = points + .Where(point => point.ChannelID == line1.ID) + .Where(point => point.SeriesID == SeriesID.Average) + .Select(point => point.Value) + .OrderBy(value => value) + .ToList(); + + List avgLine2 = points + .Where(point => point.ChannelID == line2.ID) + .Where(point => point.SeriesID == SeriesID.Average) + .Select(point => point.Value) + .OrderBy(value => value) + .ToList(); + + List avgLine3 = points + .Where(point => point.ChannelID == line1.ID) + .Where(point => point.SeriesID == SeriesID.Average) + .Select(point => point.Value) + .OrderBy(value => value) + .ToList(); + + if (!avgLine1.Any() && !avgLine2.Any() && !avgLine3.Any()) + { + verticalMillimeters += InsertItalicText(page, verticalMillimeters, $" No L-L Voltage data during {FirstOfMonth.ToString("MM/dd/yyyy")} - {EndOfMonth.ToString("MM/dd/yyyy")}"); + Summary.VoltageLL.Compliance = "Pass"; + return verticalMillimeters; + } + + Summary.VoltageLL.Min = points.Where(point => point.SeriesID == SeriesID.Minimum).Select(point => point.Value).Min(); + Summary.VoltageLL.Avg = points.Where(point => point.SeriesID == SeriesID.Average).Select(point => point.Value).Average(); + Summary.VoltageLL.Max = points.Where(point => point.SeriesID == SeriesID.Maximum).Select(point => point.Value).Max(); + + double firstPercentileMinLimit = nominal * (100.0D - ReportsSettings.FirstVoltageDeviationLimit) / 100.0D; + double firstPercentileMaxLimit = nominal * (100.0D + ReportsSettings.FirstVoltageDeviationLimit) / 100.0D; + + int firstPercentileSkipCountLine1 = (int)(avgLine1.Count * (100.0D - ReportsSettings.FirstVoltagePercentile) / 100.0D / 2.0D); + int firstPercentileSkipCountLine2 = (int)(avgLine2.Count * (100.0D - ReportsSettings.FirstVoltagePercentile) / 100.0D / 2.0D); + int firstPercentileSkipCountLine3 = (int)(avgLine3.Count * (100.0D - ReportsSettings.FirstVoltagePercentile) / 100.0D / 2.0D); + double firstPercentileMinLine1 = avgLine1.Skip(firstPercentileSkipCountLine1).FirstOrDefault(); + double firstPercentileMinLine2 = avgLine2.Skip(firstPercentileSkipCountLine2).FirstOrDefault(); + double firstPercentileMinLine3 = avgLine3.Skip(firstPercentileSkipCountLine3).FirstOrDefault(); + double firstPercentileMaxLine1 = avgLine1.AsEnumerable().Reverse().Skip(firstPercentileSkipCountLine1).FirstOrDefault(); + double firstPercentileMaxLine2 = avgLine2.AsEnumerable().Reverse().Skip(firstPercentileSkipCountLine2).FirstOrDefault(); + double firstPercentileMaxLine3 = avgLine3.AsEnumerable().Reverse().Skip(firstPercentileSkipCountLine3).FirstOrDefault(); + + bool firstTest = + firstPercentileMinLine1 >= firstPercentileMinLimit && + firstPercentileMaxLine1 <= firstPercentileMaxLimit && + firstPercentileMinLine2 >= firstPercentileMinLimit && + firstPercentileMaxLine2 <= firstPercentileMaxLimit && + firstPercentileMinLine3 >= firstPercentileMinLimit && + firstPercentileMaxLine3 <= firstPercentileMaxLimit; + + double secondPercentileMinLimit = nominal * (100.0D - ReportsSettings.SecondVoltageDeviationLimit) / 100.0D; + double secondPercentileMaxLimit = nominal * (100.0D + ReportsSettings.SecondVoltageDeviationLimit) / 100.0D; + + int secondPercentileSkipCountLine1 = (int)(avgLine1.Count * (100.0D - ReportsSettings.SecondVoltagePercentile) / 100.0D / 2.0D); + int secondPercentileSkipCountLine2 = (int)(avgLine2.Count * (100.0D - ReportsSettings.SecondVoltagePercentile) / 100.0D / 2.0D); + int secondPercentileSkipCountLine3 = (int)(avgLine3.Count * (100.0D - ReportsSettings.SecondVoltagePercentile) / 100.0D / 2.0D); + double secondPercentileMinLine1 = avgLine1.Skip(secondPercentileSkipCountLine1).FirstOrDefault(); + double secondPercentileMinLine2 = avgLine2.Skip(secondPercentileSkipCountLine2).FirstOrDefault(); + double secondPercentileMinLine3 = avgLine3.Skip(secondPercentileSkipCountLine3).FirstOrDefault(); + double secondPercentileMaxLine1 = avgLine1.AsEnumerable().Reverse().Skip(secondPercentileSkipCountLine1).FirstOrDefault(); + double secondPercentileMaxLine2 = avgLine2.AsEnumerable().Reverse().Skip(secondPercentileSkipCountLine2).FirstOrDefault(); + double secondPercentileMaxLine3 = avgLine3.AsEnumerable().Reverse().Skip(secondPercentileSkipCountLine3).FirstOrDefault(); + + bool secondTest = + secondPercentileMinLine1 >= secondPercentileMinLimit && + secondPercentileMaxLine1 <= secondPercentileMaxLimit && + secondPercentileMinLine2 >= secondPercentileMinLimit && + secondPercentileMaxLine2 <= secondPercentileMaxLimit && + secondPercentileMinLine3 >= secondPercentileMinLimit && + secondPercentileMaxLine3 <= secondPercentileMaxLimit; + + Summary.VoltageLL.Compliance = (firstTest && secondTest ? "Pass" : "Fail"); + + FontProp headerProp = new FontProp(FontDefinition, 0); + headerProp.rSizePoint = 10.0D; + + using (TableLayoutManager tlm = new TableLayoutManager(headerProp)) + { + FontProp textProp = new FontProp(FontDefinition, 0); + textProp.rSizePoint = 8.0D; + tlm.tlmCellDef_Header.rAlignV = RepObj.rAlignCenter; // set vertical alignment of all header cells + tlm.tlmCellDef_Default.penProp_LineBottom = new PenProp(this, 0.05, Color.LightGray); // set bottom line for all cells + tlm.tlmHeightMode = TlmHeightMode.AdjustLast; + tlm.eNewContainer += (oSender, ea) => page.AddMM(PageMarginMillimeters, verticalMillimeters, ea.container); + + // define columns + TlmColumn col; + col = new TlmColumnMM(tlm, "Monthly PQ Report Requirement", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.30); + col = new TlmColumnMM(tlm, "Measured L1 Voltage", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.20); + col = new TlmColumnMM(tlm, "Measured L2 Voltage", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.20); + col = new TlmColumnMM(tlm, "Measured L3 Voltage", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.20); + col = new TlmColumnMM(tlm, "Result", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.10); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"{ReportsSettings.FirstVoltagePercentile:0.###}% of the time: {firstPercentileMinLimit:N1} V - {firstPercentileMaxLimit:N1} V")); + tlm.Add(1, new RepString(textProp, (avgLine1.Any() ? $"{firstPercentileMinLine1:N1}V - {firstPercentileMaxLine1:N1}V" : "No data for time period"))); + tlm.Add(2, new RepString(textProp, (avgLine2.Any() ? $"{firstPercentileMinLine2:N1}V - {firstPercentileMaxLine2:N1}V" : "No data for time period"))); + tlm.Add(3, new RepString(textProp, (avgLine3.Any() ? $"{firstPercentileMinLine3:N1}V - {firstPercentileMaxLine3:N1}V" : "No data for time period"))); + tlm.Add(4, new RepString(textProp, (firstTest ? "Pass" : "Fail"))); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"{ReportsSettings.SecondVoltagePercentile:0.###}% of the time: {secondPercentileMinLimit:N1} V - {secondPercentileMaxLimit:N1} V")); + tlm.Add(1, new RepString(textProp, (avgLine1.Any() ? $"{secondPercentileMinLine1:N1}V - {secondPercentileMaxLine1:N1}V" : "No data for time period"))); + tlm.Add(2, new RepString(textProp, (avgLine2.Any() ? $"{secondPercentileMinLine2:N1}V - {secondPercentileMaxLine2:N1}V" : "No data for time period"))); + tlm.Add(3, new RepString(textProp, (avgLine3.Any() ? $"{secondPercentileMinLine3:N1}V - {secondPercentileMaxLine3:N1}V" : "No data for time period"))); + tlm.Add(4, new RepString(textProp, (secondTest ? "Pass" : "Fail"))); + + tlm.Commit(); + + verticalMillimeters += tlm.rCurY_MM + 10; + } + + List pointGroups = new List(); + pointGroups.Add(new PointGroup() + { + Name = "L1-L2", + Color = Color.DarkBlue, + Data = points + .Where(point => point.ChannelID == line1.ID) + .Where(point => point.SeriesID == SeriesID.Average) + .ToList() + }); + + pointGroups.Add(new PointGroup() + { + Name = "L2-L3", + Color = Color.DarkGreen, + Data = points + .Where(point => point.ChannelID == line2.ID) + .Where(point => point.SeriesID == SeriesID.Average) + .ToList() + }); + + pointGroups.Add(new PointGroup() + { + Name = "L3-L1", + Color = Color.Purple, + Data = points + .Where(point => point.ChannelID == line3.ID) + .Where(point => point.SeriesID == SeriesID.Average) + .ToList() + }); + + List avg = points + .Where(point => point.SeriesID == SeriesID.Average) + .Select(point => point.Value) + .ToList(); + + double maxValue = avg.Max(); + double minValue = avg.Min(); + double chartHigh = (maxValue > secondPercentileMaxLimit ? maxValue : secondPercentileMaxLimit); + double chartLow = (minValue < secondPercentileMinLimit ? minValue : secondPercentileMinLimit); + double chartMax = chartHigh + (chartHigh - chartLow) * .10; + double chartMin = chartLow - (chartHigh - chartLow) * .10; + + Chart chart = GenerateLineChart("V", pointGroups, chartMax, chartMin, "MM/dd", "0.00", "Voltage L-L", secondPercentileMaxLimit, firstPercentileMaxLimit, secondPercentileMinLimit, firstPercentileMinLimit, FirstOfMonth, EndOfMonth); + verticalMillimeters += 75; + + page.AddMM(PageMarginMillimeters, verticalMillimeters, new RepImageMM(ChartToImage(chart), PageWidthMillimeters - 2 * PageMarginMillimeters, 75)); + verticalMillimeters += 75; + + chart = GenerateBarChart("0", avg, maxValue, minValue); + page.AddMM(PageMarginMillimeters, verticalMillimeters, new RepImageMM(ChartToImage(chart), PageWidthMillimeters - 2 * PageMarginMillimeters, 75)); + + return verticalMillimeters; + } + } + + private double InsertFlickerPage(Page page, double verticalMillimeters) + { + string historianServer = Connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'Historian.Server'") ?? "127.0.0.1"; + string historianInstance = Connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'Historian.Instance'") ?? "XDA"; + Channel line1 = new TableOperations(Connection).QueryRecordWhere("MeterID = {0} AND MeasurementCharacteristicID = (SELECT ID FROM MeasurementCharacteristic WHERE Name = 'FlkrPST') AND MeasurementTypeID = (SELECT ID FROM MeasurementType WHERE Name = 'Voltage') AND PhaseID = (SELECT ID FROM Phase WHERE Name = 'AN')", Meter.ID); + Channel line2 = new TableOperations(Connection).QueryRecordWhere("MeterID = {0} AND MeasurementCharacteristicID = (SELECT ID FROM MeasurementCharacteristic WHERE Name = 'FlkrPST') AND MeasurementTypeID = (SELECT ID FROM MeasurementType WHERE Name = 'Voltage') AND PhaseID = (SELECT ID FROM Phase WHERE Name = 'BN')", Meter.ID); + Channel line3 = new TableOperations(Connection).QueryRecordWhere("MeterID = {0} AND MeasurementCharacteristicID = (SELECT ID FROM MeasurementCharacteristic WHERE Name = 'FlkrPST') AND MeasurementTypeID = (SELECT ID FROM MeasurementType WHERE Name = 'Voltage') AND PhaseID = (SELECT ID FROM Phase WHERE Name = 'CN')", Meter.ID); + + if (line1 == null && line2 == null && line3 == null) + { + verticalMillimeters += InsertItalicText(page, verticalMillimeters, $" No Flicker channels setup on this meter."); + Summary.Flicker.Compliance = "Pass"; + return verticalMillimeters; + } + + using (Historian historian = new Historian(historianServer, historianInstance)) + { + List points = historian.Read(new List() { line1.ID, line2.ID, line3.ID }, FirstOfMonth, EndOfMonth).Where(x => x.SeriesID == SeriesID.Maximum).ToList(); + + if (!points.Any()) + { + verticalMillimeters += InsertItalicText(page, verticalMillimeters, $" No Flicker data during {FirstOfMonth.ToString("MM/dd/yyyy")} - {EndOfMonth.ToString("MM/dd/yyyy")}"); + return verticalMillimeters; + } + + Summary.Flicker.Max = points.Max(point => point.Value); + + List maxLine1 = points + .Where(point => point.ChannelID == line1.ID) + .Select(point => point.Value) + .OrderByDescending(value => value) + .ToList(); + + List maxLine2 = points + .Where(point => point.ChannelID == line2.ID) + .Select(point => point.Value) + .OrderByDescending(value => value) + .ToList(); + + List maxLine3 = points + .Where(point => point.ChannelID == line3.ID) + .Select(point => point.Value) + .OrderByDescending(value => value) + .ToList(); + + double percentileMaxLimit = ReportsSettings.FlickerHighLimit; + + int percentileSkipCountLine1 = (int)(maxLine1.Count * (100.0D - ReportsSettings.FlickerPercentile) / 100.0D); + int percentileSkipCountLine2 = (int)(maxLine2.Count * (100.0D - ReportsSettings.FlickerPercentile) / 100.0D); + int percentileSkipCountLine3 = (int)(maxLine3.Count * (100.0D - ReportsSettings.FlickerPercentile) / 100.0D); + double percentileMaxLine1 = maxLine1.Skip(percentileSkipCountLine1).FirstOrDefault(); + double percentileMaxLine2 = maxLine2.Skip(percentileSkipCountLine2).FirstOrDefault(); + double percentileMaxLine3 = maxLine3.Skip(percentileSkipCountLine3).FirstOrDefault(); + + bool test = + percentileMaxLine1 <= percentileMaxLimit && + percentileMaxLine2 <= percentileMaxLimit && + percentileMaxLine3 <= percentileMaxLimit; + + Summary.Flicker.Compliance = (test ? "Pass" : "Fail"); + + FontProp headerProp = new FontProp(FontDefinition, 0); + headerProp.rSizePoint = 10.0D; + + using (TableLayoutManager tlm = new TableLayoutManager(headerProp)) + { + FontProp textProp = new FontProp(FontDefinition, 0); + textProp.rSizePoint = 8.0D; + tlm.tlmCellDef_Header.rAlignV = RepObj.rAlignCenter; // set vertical alignment of all header cells + tlm.tlmCellDef_Default.penProp_LineBottom = new PenProp(this, 0.05, Color.LightGray); // set bottom line for all cells + tlm.tlmHeightMode = TlmHeightMode.AdjustLast; + tlm.eNewContainer += (oSender, ea) => page.AddMM(PageMarginMillimeters, verticalMillimeters, ea.container); + + // define columns + TlmColumn col; + col = new TlmColumnMM(tlm, "Monthly PQ Report Requirement", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.30); + col = new TlmColumnMM(tlm, "Measured L1 Plt", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.20); + col = new TlmColumnMM(tlm, "Measured L2 Plt", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.20); + col = new TlmColumnMM(tlm, "Measured L3 Plt", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.20); + col = new TlmColumnMM(tlm, "Result", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.10); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"{ReportsSettings.FlickerPercentile:0.###}% of the time: Plt <= {ReportsSettings.FlickerHighLimit:0.###}")); + tlm.Add(1, new RepString(textProp, $"{percentileMaxLine1:N2}")); + tlm.Add(2, new RepString(textProp, $"{percentileMaxLine2:N2}")); + tlm.Add(3, new RepString(textProp, $"{percentileMaxLine3:N2}")); + tlm.Add(4, new RepString(textProp, (test ? "Pass" : "Fail"))); + + tlm.Commit(); + + verticalMillimeters += tlm.rCurY_MM + 10; + } + + List pointGroups = new List(); + + pointGroups.Add(new PointGroup() + { + Name = "L1Max", + Color = Color.DarkBlue, + Data = points + .Where(point => point.ChannelID == line1.ID) + .ToList() + }); + + pointGroups.Add(new PointGroup() + { + Name = "L2Max", + Color = Color.DarkGreen, + Data = points + .Where(point => point.ChannelID == line2.ID) + .ToList() + }); + + pointGroups.Add(new PointGroup() + { + Name = "L3Max", + Color = Color.Purple, + Data = points + .Where(point => point.ChannelID == line3.ID) + .ToList() + }); + + double maxValue = points.Select(point => point.Value).Max(); + Chart chart = GenerateLineChart("", pointGroups, maxValue * 1.1, 0, "MM/dd", "0.00", "Flicker", ReportsSettings.FlickerHighLimit, null, null, null, FirstOfMonth, EndOfMonth); + verticalMillimeters += 75; + + page.AddMM(PageMarginMillimeters, verticalMillimeters, new RepImageMM(ChartToImage(chart), PageWidthMillimeters - 2 * PageMarginMillimeters, 75)); + verticalMillimeters += 75; + + chart = GenerateBarChart("0.00", points.Select(x => x.Value), maxValue, 0); + page.AddMM(PageMarginMillimeters, verticalMillimeters, new RepImageMM(ChartToImage(chart), PageWidthMillimeters - 2 * PageMarginMillimeters, 75)); + + return verticalMillimeters; + } + } + + private double InsertImbalancePage(Page page, double verticalMillimeters) + { + string historianServer = Connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'Historian.Server'") ?? "127.0.0.1"; + string historianInstance = Connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'Historian.Instance'") ?? "XDA"; + Channel channel = new TableOperations(Connection).QueryRecordWhere("MeterID = {0} AND MeasurementCharacteristicID = (SELECT ID FROM MeasurementCharacteristic WHERE Name = 'AvgImbal') AND MeasurementTypeID = (SELECT ID FROM MeasurementType WHERE Name = 'Voltage')", Meter.ID); + + if (channel == null) + { + verticalMillimeters += InsertItalicText(page, verticalMillimeters, $" No Imbalance channel setup on this meter."); + Summary.Imbalance.Compliance = "Pass"; + return verticalMillimeters; + } + + using (Historian historian = new Historian(historianServer, historianInstance)) + { + List points = historian.Read(new List() { channel.ID }, FirstOfMonth, EndOfMonth).Where(x => x.SeriesID == SeriesID.Average).ToList(); + + if (!points.Any()) + { + verticalMillimeters += InsertItalicText(page, verticalMillimeters, $" No Imbalance data during {FirstOfMonth.ToString("MM/dd/yyyy")} - {EndOfMonth.ToString("MM/dd/yyyy")}"); + return verticalMillimeters; + } + + List avg = points + .Select(point => point.Value) + .OrderBy(value => value) + .ToList(); + + Summary.Imbalance.Avg = points.Select(point => point.Value).Average(); + + int percentileSkipCount = (int)(avg.Count * (100.0D - ReportsSettings.VoltageUnbalancePercentile) / 100.0D / 2.0D); + double percentileMinLimit = ReportsSettings.VoltageUnbalanceLowLimit; + double percentileMaxLimit = ReportsSettings.VoltageUnbalanceHighLimit; + double percentileMin = avg.Skip(percentileSkipCount).FirstOrDefault(); + double percentileMax = avg.AsEnumerable().Reverse().Skip(percentileSkipCount).FirstOrDefault(); + + bool test = + percentileMin >= percentileMinLimit && + percentileMax <= percentileMaxLimit; + + Summary.Imbalance.Compliance = (test ? "Pass" : "Fail"); + + FontProp headerProp = new FontProp(FontDefinition, 0); + headerProp.rSizePoint = 10.0D; + + using (TableLayoutManager tlm = new TableLayoutManager(headerProp)) + { + FontProp textProp = new FontProp(FontDefinition, 0); + textProp.rSizePoint = 8.0D; + tlm.tlmCellDef_Header.rAlignV = RepObj.rAlignCenter; // set vertical alignment of all header cells + tlm.tlmCellDef_Default.penProp_LineBottom = new PenProp(this, 0.05, Color.LightGray); // set bottom line for all cells + tlm.tlmHeightMode = TlmHeightMode.AdjustLast; + tlm.eNewContainer += (oSender, ea) => page.AddMM(PageMarginMillimeters, verticalMillimeters, ea.container); + + // define columns + TlmColumn col; + col = new TlmColumnMM(tlm, "Monthly PQ Report Requirement", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.40); + col = new TlmColumnMM(tlm, "Measured Unbalance u2", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.40); + col = new TlmColumnMM(tlm, "Result", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.20); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"{ReportsSettings.VoltageUnbalancePercentile:0.###}% of the time: {percentileMinLimit:0.###}% ~ {percentileMaxLimit:0.###}% u2")); + tlm.Add(1, new RepString(textProp, $"{percentileMax:N2}")); + tlm.Add(2, new RepString(textProp, (test ? "Pass" : "Fail"))); + + tlm.Commit(); + + verticalMillimeters += tlm.rCurY_MM + 10; + } + + List pointGroups = new List(); + + pointGroups.Add(new PointGroup() + { + Name = "Unbalance", + Color = Color.DarkBlue, + Data = points + }); + + Chart chart = GenerateLineChart("", pointGroups, Math.Max(percentileMax, percentileMaxLimit) * 1.1, Math.Min(percentileMin, percentileMinLimit), "MM/dd", "0.00", "Flicker", percentileMaxLimit, null, percentileMinLimit, null, FirstOfMonth, EndOfMonth); + verticalMillimeters += 75; + + page.AddMM(PageMarginMillimeters, verticalMillimeters, new RepImageMM(ChartToImage(chart), PageWidthMillimeters - 2 * PageMarginMillimeters, 75)); + verticalMillimeters += 75; + + chart = GenerateBarChart("0.00", avg, percentileMax, percentileMin); + page.AddMM(PageMarginMillimeters, verticalMillimeters, new RepImageMM(ChartToImage(chart), PageWidthMillimeters - 2 * PageMarginMillimeters, 75)); + + return verticalMillimeters; + } + } + + private double InsertTHDPage(Page page, double verticalMillimeters) + { + string historianServer = Connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'Historian.Server'") ?? "127.0.0.1"; + string historianInstance = Connection.ExecuteScalar("SELECT Value FROM Setting WHERE Name = 'Historian.Instance'") ?? "XDA"; + + Channel line1 = new TableOperations(Connection).QueryRecordWhere("MeterID = {0} AND MeasurementCharacteristicID = (SELECT ID FROM MeasurementCharacteristic WHERE Name = 'TotalTHD') AND MeasurementTypeID = (SELECT ID FROM MeasurementType WHERE Name = 'Voltage') AND PhaseID = (SELECT ID FROM Phase WHERE Name = 'AN')", Meter.ID); + Channel line2 = new TableOperations(Connection).QueryRecordWhere("MeterID = {0} AND MeasurementCharacteristicID = (SELECT ID FROM MeasurementCharacteristic WHERE Name = 'TotalTHD') AND MeasurementTypeID = (SELECT ID FROM MeasurementType WHERE Name = 'Voltage') AND PhaseID = (SELECT ID FROM Phase WHERE Name = 'BN')", Meter.ID); + Channel line3 = new TableOperations(Connection).QueryRecordWhere("MeterID = {0} AND MeasurementCharacteristicID = (SELECT ID FROM MeasurementCharacteristic WHERE Name = 'TotalTHD') AND MeasurementTypeID = (SELECT ID FROM MeasurementType WHERE Name = 'Voltage') AND PhaseID = (SELECT ID FROM Phase WHERE Name = 'CN')", Meter.ID); + + if (line1 == null && line2 == null && line3 == null) + { + verticalMillimeters += InsertItalicText(page, verticalMillimeters, $" No THD channels setup on this meter."); + Summary.THD.Compliance = "Pass"; + return verticalMillimeters; + } + + using (Historian historian = new Historian(historianServer, historianInstance)) + { + List points = historian.Read(new List() { line1.ID, line2.ID, line3.ID }, FirstOfMonth, EndOfMonth).ToList(); + + if (!points.Any()) + { + verticalMillimeters += InsertItalicText(page, verticalMillimeters, $" No THD data during {FirstOfMonth.ToString("MM/dd/yyyy")} - {EndOfMonth.ToString("MM/dd/yyyy")}"); + return verticalMillimeters; + } + + Summary.THD.Min = points.Where(point => point.SeriesID == SeriesID.Minimum).Select(point => point.Value).Min(); + Summary.THD.Avg = points.Where(point => point.SeriesID == SeriesID.Average).Select(point => point.Value).Average(); + Summary.THD.Max = points.Where(point => point.SeriesID == SeriesID.Maximum).Select(point => point.Value).Max(); + + List maxLine1 = points + .Where(point => point.ChannelID == line1.ID) + .Where(point => point.SeriesID == SeriesID.Maximum) + .Select(point => point.Value) + .OrderByDescending(value => value) + .ToList(); + + List maxLine2 = points + .Where(point => point.ChannelID == line2.ID) + .Where(point => point.SeriesID == SeriesID.Maximum) + .Select(point => point.Value) + .OrderByDescending(value => value) + .ToList(); + + List maxLine3 = points + .Where(point => point.ChannelID == line3.ID) + .Where(point => point.SeriesID == SeriesID.Maximum) + .Select(point => point.Value) + .OrderByDescending(value => value) + .ToList(); + + double percentileMaxLimit = ReportsSettings.VoltageTHDHighLimit; + + int percentileSkipCountLine1 = (int)(maxLine1.Count * (100.0D - ReportsSettings.VoltageTHDPercentile) / 100.0D); + int percentileSkipCountLine2 = (int)(maxLine2.Count * (100.0D - ReportsSettings.VoltageTHDPercentile) / 100.0D); + int percentileSkipCountLine3 = (int)(maxLine3.Count * (100.0D - ReportsSettings.VoltageTHDPercentile) / 100.0D); + double percentileMinLine1 = maxLine1.Last(); + double percentileMaxLine1 = maxLine1.Skip(percentileSkipCountLine1).FirstOrDefault(); + double percentileMinLine2 = maxLine2.Last(); + double percentileMaxLine2 = maxLine2.Skip(percentileSkipCountLine2).FirstOrDefault(); + double percentileMinLine3 = maxLine3.Last(); + double percentileMaxLine3 = maxLine3.Skip(percentileSkipCountLine3).FirstOrDefault(); + + bool test = + percentileMaxLine1 <= percentileMaxLimit && + percentileMaxLine2 <= percentileMaxLimit && + percentileMaxLine3 <= percentileMaxLimit; + + Summary.THD.Compliance = (test ? "Pass" : "Fail"); + + FontProp headerProp = new FontProp(FontDefinition, 0); + headerProp.rSizePoint = 10.0D; + using (TableLayoutManager tlm = new TableLayoutManager(headerProp)) + { + FontProp textProp = new FontProp(FontDefinition, 0); + textProp.rSizePoint = 8.0D; + tlm.tlmCellDef_Header.rAlignV = RepObj.rAlignCenter; // set vertical alignment of all header cells + tlm.tlmCellDef_Default.penProp_LineBottom = new PenProp(this, 0.05, Color.LightGray); // set bottom line for all cells + tlm.tlmHeightMode = TlmHeightMode.AdjustLast; + tlm.eNewContainer += (oSender, ea) => page.AddMM(PageMarginMillimeters, verticalMillimeters, ea.container); + + // define columns + TlmColumn col; + col = new TlmColumnMM(tlm, "Monthly PQ Report Requirement", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.30); + col = new TlmColumnMM(tlm, "Measured L1 THD", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.20); + col = new TlmColumnMM(tlm, "Measured L2 THD", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.20); + col = new TlmColumnMM(tlm, "Measured L3 THD", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.20); + col = new TlmColumnMM(tlm, "Result", (PageWidthMillimeters - 2 * PageMarginMillimeters) * 0.10); + + tlm.NewRow(); + tlm.Add(0, new RepString(textProp, $"{ReportsSettings.VoltageTHDPercentile:0.###}% of the time: THD <= {percentileMaxLimit:0.###}%")); + tlm.Add(1, new RepString(textProp, (maxLine1.Any() ? $"{percentileMinLine1:N2}% - {percentileMaxLine1:N2}%" : "No data for time period"))); + tlm.Add(2, new RepString(textProp, (maxLine2.Any() ? $"{percentileMinLine2:N2}% - {percentileMaxLine2:N2}%" : "No data for time period"))); + tlm.Add(3, new RepString(textProp, (maxLine3.Any() ? $"{percentileMinLine3:N2}% - {percentileMaxLine3:N2}%" : "No data for time period"))); + tlm.Add(4, new RepString(textProp, (test ? "Pass" : "Fail"))); + + tlm.Commit(); + + verticalMillimeters += tlm.rCurY_MM + 10; + } + + List pointGroups = new List(); + + pointGroups.Add(new PointGroup() + { + Name = "L1", + Color = Color.DarkBlue, + Data = points + .Where(point => point.ChannelID == line1.ID) + .Where(point => point.SeriesID == SeriesID.Average) + .ToList() + }); + + pointGroups.Add(new PointGroup() + { + Name = "L2", + Color = Color.DarkGreen, + Data = points + .Where(point => point.ChannelID == line2.ID) + .Where(point => point.SeriesID == SeriesID.Average) + .ToList() + }); + + pointGroups.Add(new PointGroup() + { + Name = "L3", + Color = Color.DarkMagenta, + Data = points + .Where(point => point.ChannelID == line3.ID) + .Where(point => point.SeriesID == SeriesID.Average) + .ToList() + }); + + pointGroups.Add(new PointGroup() + { + Name = "L1-Max", + Color = Color.Blue, + Data = points + .Where(point => point.ChannelID == line1.ID) + .Where(point => point.SeriesID == SeriesID.Maximum) + .ToList() + }); + + pointGroups.Add(new PointGroup() + { + Name = "L2-Max", + Color = Color.Green, + Data = points + .Where(point => point.ChannelID == line2.ID) + .Where(point => point.SeriesID == SeriesID.Maximum) + .ToList() + }); + + pointGroups.Add(new PointGroup() + { + Name = "L3-Max", + Color = Color.Magenta, + Data = points + .Where(point => point.ChannelID == line3.ID) + .Where(point => point.SeriesID == SeriesID.Maximum) + .ToList() + }); + + pointGroups.Add(new PointGroup() + { + Name = "L1-Min", + Color = Color.LightBlue, + Data = points + .Where(point => point.ChannelID == line1.ID) + .Where(point => point.SeriesID == SeriesID.Minimum) + .ToList() + }); + + pointGroups.Add(new PointGroup() + { + Name = "L2-Min", + Color = Color.LightGreen, + Data = points + .Where(point => point.ChannelID == line2.ID) + .Where(point => point.SeriesID == SeriesID.Minimum) + .ToList() + }); + + pointGroups.Add(new PointGroup() + { + Name = "L3-Min", + Color = Color.LightPink, + Data = points + .Where(point => point.ChannelID == line3.ID) + .Where(point => point.SeriesID == SeriesID.Minimum) + .ToList() + }); + + double maxValue = points.Select(point => point.Value).Max(); + double minValue = points.Select(point => point.Value).Min(); + double chartHigh = (maxValue > 8 ? maxValue : 8); + double chartLow = (minValue < 0 ? minValue : 0); + double chartMax = chartHigh + (chartHigh - chartLow) * .10; + double chartMin = chartLow - (chartHigh - chartLow) * .10; + + Chart chart = GenerateLineChart("V", pointGroups, chartMax, chartMin, "MM/dd", "0.00", "Voltage L-L", percentileMaxLimit, null, null, null, FirstOfMonth, EndOfMonth); + verticalMillimeters += 75; + + page.AddMM(PageMarginMillimeters, verticalMillimeters, new RepImageMM(ChartToImage(chart), PageWidthMillimeters - 2 * PageMarginMillimeters, 75)); + verticalMillimeters += 75; + + chart = GenerateBarChart("0.00", points.Select(x => x.Value), maxValue, minValue); + page.AddMM(PageMarginMillimeters, verticalMillimeters, new RepImageMM(ChartToImage(chart), PageWidthMillimeters - 2 * PageMarginMillimeters, 75)); + + return verticalMillimeters; + } + } + + private Chart GenerateLineChart(string units, List pointsGroup, double max, double min, string xFormat, string yFormat, string title, double? highThreshold, double? highWarning, double? lowThreshold, double? lowWarning, DateTime start, DateTime end) + { + + Chart chart; + ChartArea area; + ChartSeries series; + area = new ChartArea(); + area.AxisX.MajorGrid.Enabled = false; + area.AxisX.LabelStyle.Format = xFormat; + area.AxisY.Title = units; + area.AxisY.LabelStyle.Format = yFormat; + area.AxisY.MajorGrid.LineColor = Color.LightGray; + area.AxisY.Maximum = max; + area.AxisY.Minimum = min; + + chart = new Chart(); + chart.Width = 1200; + chart.Height = 500; + chart.Name = title; + + chart.Legends.Add(new Legend()); + chart.ChartAreas.Add(area); + + foreach (var group in pointsGroup) + { + + series = new ChartSeries(group.Name); + series.ChartType = SeriesChartType.FastLine; + series.BorderWidth = 2; + series.Color = group.Color; + + foreach (var point in group.Data) + { + series.Points.AddXY(point.Timestamp, point.Value); + } + + chart.Series.Add(series); + } + + if (highThreshold.HasValue) + chart.Series.Add(MakeLimitSeries((double)highThreshold, Color.Red, "High Threshold", start, end)); + if (lowThreshold.HasValue) + chart.Series.Add(MakeLimitSeries((double)lowThreshold, Color.Red, "Low Threshold", start, end)); + if (highWarning.HasValue) + chart.Series.Add(MakeLimitSeries((double)highWarning, Color.Orange, "High Warning", start, end)); + if (lowWarning.HasValue) + chart.Series.Add(MakeLimitSeries((double)lowWarning, Color.Orange, "Low Warning", start, end)); + + return chart; + + } + + private Chart GenerateBarChart(string units, IEnumerable points, double max, double min) + { + ChartArea area = new ChartArea(); + area.AxisX.MajorGrid.Enabled = false; + area.AxisY.Enabled = AxisEnabled.False; + area.AxisX.IntervalAutoMode = IntervalAutoMode.VariableCount; + area.AxisX.LabelAutoFitStyle = LabelAutoFitStyles.DecreaseFont; + + Chart chart = new Chart(); + chart.Width = 1200; + chart.Height = 500; + chart.ChartAreas.Add(area); + + ChartSeries series = new ChartSeries("bars"); + series.ChartType = SeriesChartType.Column; + series.BorderWidth = 20; + series.Color = Color.DarkBlue; + + double step = (max - min) / NumBuckets; + + Dictionary buckets = Enumerable.Range(0, NumBuckets) + .GroupJoin(points, key => key, point => (int)((point - min) / step), (Key, grouping) => new { Key, Val = grouping.Count() }) + .ToDictionary(obj => (obj.Key * step + min).ToString(units), obj => obj.Val); + + foreach (var kvp in buckets) + series.Points.AddXY(kvp.Key, kvp.Value); + + chart.Series.Add(series); + + return chart; + } + + private Chart GenerateMagDurChart(List> events, string xFormat, string yFormat) + { + + Chart chart; + ChartArea area; + ChartSeries series; + area = new ChartArea(); + area.AxisX.MajorGrid.Enabled = false; + area.AxisX.LabelStyle.Format = xFormat; + area.AxisX.Maximum = 10; + area.AxisX.Minimum = 0.00001; + area.AxisX.IsLogarithmic = true; + area.AxisY.LabelStyle.Format = yFormat; + area.AxisY.MajorGrid.LineColor = Color.LightGray; + area.AxisY.Maximum = 6; + area.AxisY.Minimum = 0; + + chart = new Chart(); + chart.Width = 1000; + chart.Height = 1000; + chart.Name = "ITIC Curve (Interruptions, Sags, Swells)"; + chart.ChartAreas.Add(area); + + series = new ChartSeries("Points"); + series.ChartType = SeriesChartType.Point; + series.BorderWidth = 2; + series.Color = Color.DarkBlue; + + foreach (Tuple evt in events) + { + series.Points.AddXY(evt.Item1, evt.Item2); + } + + chart.Series.Add(series); + + series = new ChartSeries("ITIC Upper"); + series.ChartType = SeriesChartType.Line; + series.BorderWidth = 2; + series.Color = Color.Red; + + foreach (Tuple point in Curves.IticUpperCurve) + { + series.Points.AddXY(point.Item1, point.Item2); + } + + chart.Series.Add(series); + + series = new ChartSeries("ITIC Lower"); + series.ChartType = SeriesChartType.Line; + series.BorderWidth = 2; + series.Color = Color.Red; + + foreach (Tuple point in Curves.IticLowerCurve) + { + series.Points.AddXY(point.Item1, point.Item2); + } + + chart.Series.Add(series); + + return chart; + + } + + private ChartSeries MakeLimitSeries(double value, Color color, string name, DateTime start, DateTime end) + { + ChartSeries cs = new ChartSeries(name); + cs.ChartType = SeriesChartType.FastLine; + cs.BorderWidth = 1; + + cs.Color = color; + cs.Points.AddXY(start, value); + cs.Points.AddXY(end, value); + return cs; + } + + private Stream ChartToImage(Chart chart) + { + MemoryStream stream = new MemoryStream(); + chart.SaveImage(stream, ChartImageFormat.Jpeg); + stream.Position = 0; + return stream; + } + + #endregion + + #region [ Static ] + + private static readonly ILog Log = LogManager.GetLogger(typeof(PQReport)); + + #endregion + } +} diff --git a/Libraries/openXDA.Reports/Properties/AssemblyInfo.cs b/Libraries/openXDA.Reports/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..46f31e0 --- /dev/null +++ b/Libraries/openXDA.Reports/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("openXDA.Reports")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("openXDA.Reports")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("656f97d9-4fb6-4f65-a9a6-1a1dde3af634")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("3.0.5.93")] +[assembly: AssemblyVersion("3.0.5.93")] +[assembly: AssemblyFileVersion("3.0.5.93")] diff --git a/Libraries/openXDA.Reports/README.md b/Libraries/openXDA.Reports/README.md new file mode 100644 index 0000000..0216586 --- /dev/null +++ b/Libraries/openXDA.Reports/README.md @@ -0,0 +1,7 @@ +![Icon](http://www.gridprotectionalliance.org/images/products/icons%2064/openXDA.png)![openXDA](http://www.gridprotectionalliance.org/images/products/productTitles32/PQSiteReport.png) + +**eXtensible Disturbance Analytics PQ Site Report** + +# Overview +The OpenXDA PQ Site Report was developed to provide the OpenXDA service with a monthly reporting feature that will build and distribute a per meter pdf trending data report +each month that shows if there have been any compliance issues with regards to Voltage and THD. diff --git a/Libraries/openXDA.Reports/ReportsEngine.cs b/Libraries/openXDA.Reports/ReportsEngine.cs new file mode 100644 index 0000000..b0d7f83 --- /dev/null +++ b/Libraries/openXDA.Reports/ReportsEngine.cs @@ -0,0 +1,382 @@ +//****************************************************************************************************** +// ReportsEngine.cs - Gbtc +// +// Copyright © 2018, 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: +// ---------------------------------------------------------------------------------------------------- +// 06/14/2018 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Configuration; +using System.Data; +using System.IO; +using System.Linq; +using System.Net.Mail; +using System.Text; +using Gemstone; +using Gemstone.Configuration; +using Gemstone.Data; +using Gemstone.Data.Model; +using Gemstone.Scheduling; +using log4net; +using openXDA.Configuration; +using openXDA.Model; + +namespace openXDA.Reports +{ + public class ReportsEngine : IDisposable + { + #region [ Members ] + private bool m_disposed; + + #endregion + + #region [ Constructors ] + + public ReportsEngine() + { + PQReportsSettings = new PQReportsSettings(); + EmailSettings = new EmailSection(); + BreakerReportsSettings = new BreakerReportsSettings(); + + Scheduler = new ScheduleManager(); + Scheduler.Starting += Scheduler_Starting; + Scheduler.Started += Scheduler_Started; + Scheduler.ScheduleDue += Scheduler_ScheduleDue; + } + + #endregion + + #region [ Properties ] + + private ScheduleManager Scheduler { get; } + + public bool Running { get; private set; } + + [Category] + [SettingName(PQReportsSettings.CategoryName)] + public PQReportsSettings PQReportsSettings { get; } + + [Category] + [SettingName(BreakerReportsSettings.CategoryName)] + public BreakerReportsSettings BreakerReportsSettings { get; } + + [Category] + [SettingName(EmailSection.CategoryName)] + public EmailSection EmailSettings { get; } + + #endregion + + #region [ Methods ] + + public void ProcessMonthlyReport(Meter meter, DateTime month) + { + DateTime firstOfMonth = month.AddDays(1 - month.Day); + DateTime endOfMonth = firstOfMonth.AddMonths(1).AddDays(-1); + + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) + { + ProcessMonthlyReport(meter, firstOfMonth, endOfMonth, connection); + } + } + + public bool Start() + { + try + { + if (!Running) + { + bool scheduled = false; + + if (PQReportsSettings.Enabled) { + Scheduler.AddSchedule("PQReports", PQReportsSettings.Schedule); + scheduled = true; + } + if (BreakerReportsSettings.Enabled) + { + Scheduler.AddSchedule("BreakerReports", PQReportsSettings.Schedule); + scheduled = true; + } + + if (scheduled) + { + Scheduler.Start(); + Running = true; + } + } + + return true; + } + catch (Exception ex) + { + Log.Error(ex.ToString(), ex); + return false; + } + } + + public void Stop() + { + if (Scheduler.IsRunning) + { + bool scheduled1 = false; + bool scheduled2 = false; + + if (!PQReportsSettings.Enabled) + scheduled1 = Scheduler.RemoveSchedule("PQReports"); + if (!BreakerReportsSettings.Enabled) + scheduled2 = Scheduler.RemoveSchedule("BreakerReports"); + + if (scheduled1 && scheduled2 ) + { + Scheduler.Stop(); + Running = false; + } + } + } + + public void Dispose() + { + if (m_disposed) + return; + + Scheduler.Starting -= Scheduler_Starting; + Scheduler.Started -= Scheduler_Started; + Scheduler.ScheduleDue -= Scheduler_ScheduleDue; + Scheduler.Dispose(); + + m_disposed = true; + } + + public void ReloadSystemSettings(string connectionString) + { + ConnectionStringParser.ParseConnectionString(connectionString, this); + + Scheduler.AddSchedule("PQReports", PQReportsSettings.Schedule, true); + Scheduler.AddSchedule("BreakerReports", BreakerReportsSettings.Schedule, true); + + if (PQReportsSettings.Enabled || BreakerReportsSettings.Enabled) + Start(); + else if (!PQReportsSettings.Enabled || !BreakerReportsSettings.Enabled) + Stop(); + + } + + private void Scheduler_Starting(object sender, EventArgs e) + { + } + + private void Scheduler_Started(object sender, EventArgs e) + { + Log.Info("Reports Engine has started successfully..."); + } + + private void Scheduler_Disposed(object sender, EventArgs e) + { + Log.Info("Reports Engine is disposed..."); + } + + private void Scheduler_ScheduleDue(object sender, EventArgs e) + { + Log.Info(string.Format($"Processing {e.Argument.Name}...")); + if (e.Argument.Name == "PQReports") + ProcessPQReports(); + else if (e.Argument.Name == "BreakerReports") + ProcessBreakerReports(); + } + + private void ProcessBreakerReports() { + DateTime today = DateTime.Now; + DateTime firstOfMonth = today.AddDays(1 - today.Day).AddMonths(-1); + DateTime endOfMonth = firstOfMonth.AddMonths(1).AddDays(-1); + + AllBreakersReport report = new AllBreakersReport(firstOfMonth, endOfMonth); + byte[] pdf = report.CreatePDF(); + if (pdf == null) return; + byte[] csv = ExportAllToCSV(report.DataTable, firstOfMonth, endOfMonth); + if (csv == null) return; + + using (MemoryStream pdfStream = new MemoryStream(pdf)) + using (MemoryStream csvStream = new MemoryStream(csv)) + { + string fileName = "AllBreakersReport_" + firstOfMonth.ToString("MM_dd_yyyy") + "_" + endOfMonth.ToString("MM_dd_yyyy"); + string pdfContentType = "application/pdf"; + string csvContentType = "text/csv"; + string pdfName = fileName + ".pdf"; + string csvName = fileName + ".csv"; + + List attachments = new List() { + new Attachment(pdfStream, pdfName, pdfContentType), + new Attachment(csvStream, csvName, csvContentType) + }; + + EmailWriter emailWriter = new EmailWriter(PQReportsSettings, EmailSettings); + emailWriter.SendEmailWithAttachment(BreakerReportsSettings.EmailList.Split(',').ToList(), $"Breaker Report for {fileName}", "", attachments); + + } + + } + + private void ProcessPQReports() + { + DateTime today = DateTime.Now; + DateTime firstOfMonth = today.AddDays(1 - today.Day).AddMonths(-1); + DateTime endOfMonth = firstOfMonth.AddMonths(1).AddDays(-1); + + using (AdoDataConnection connection = new AdoDataConnection(Settings.Default)) + { + // TODO: There is no EmailGroupAssetGroup + IEnumerable meters = new TableOperations(connection).QueryRecordsWhere("ID IN (SELECT MeterID FROM MeterAssetGroup WHERE AssetGroupID IN (SELECT AssetGroupID FROM EmailGroupAssetGroup WHERE EmailGroupID = (SELECT ID FROM EmailGroup WHERE Name = 'PQ Report')))"); + + foreach (Meter meter in meters) + ProcessMonthlyReport(meter, firstOfMonth, endOfMonth, connection); + + EmailWriter emailWriter = new EmailWriter(PQReportsSettings, EmailSettings); + emailWriter.Execute(firstOfMonth.Month, firstOfMonth.Year); + } + } + + private void ProcessMonthlyReport(Meter meter, DateTime firstOfMonth, DateTime endOfMonth, AdoDataConnection connection) + { + Log.Info($"Starting monthly Report for {meter.Name}..."); + PQReport pQReport = new PQReport(PQReportsSettings, meter, firstOfMonth, endOfMonth, connection); + byte[] pdf = pQReport.createPDF(); + Log.Info($"Completed monthly Report for {meter.Name}"); + + try + { + TableOperations to = new TableOperations(connection); + Report report = to.QueryRecordWhere("MeterID = {0} AND Month = {1} AND Year = {2}", meter.ID, firstOfMonth.Month, firstOfMonth.Year); + + if (report != null) + { + report.MeterID = meter.ID; + report.Month = firstOfMonth.Month; + report.Year = firstOfMonth.Year; + report.Results = pQReport.Result; + report.PDF = pdf; + + to.UpdateRecord(report); + } + else + { + to.AddNewRecord(new Report() + { + MeterID = meter.ID, + Month = firstOfMonth.Month, + Year = firstOfMonth.Year, + Results = pQReport.Result, + PDF = pdf + }); + } + + Log.Info($"Loaded monthly Report for {meter.Name}"); + } + catch (Exception ex) + { + Log.Error(ex.ToString(), ex); + } + } + + public string GetHelpMessage(string command) + { + StringBuilder helpMessage = new StringBuilder(); + + helpMessage.Append("Processes the month PQ Reports"); + helpMessage.AppendLine(); + helpMessage.AppendLine(); + helpMessage.Append(" Usage:"); + helpMessage.AppendLine(); + helpMessage.Append(" " + command); + helpMessage.AppendLine(); + helpMessage.Append(" " + command + " -?"); + helpMessage.AppendLine(); + helpMessage.AppendLine(); + helpMessage.Append(" Options:"); + helpMessage.AppendLine(); + helpMessage.Append(" -?".PadRight(25)); + helpMessage.Append("Displays this help message"); + + return helpMessage.ToString(); + } + + private DateTime GetPreviousMatch(Schedule schedule, DateTime now) + { + bool match = false; + + while (!match) + { + now = now.AddMinutes(-1); + + match = schedule.MinutePart.Matches(now); + match &= schedule.HourPart.Matches(now); + match &= schedule.DayPart.Matches(now); + match &= schedule.MonthPart.Matches(now); + match &= schedule.DaysOfWeekPart.Matches(now); + } + + return now; + } + + public void ProcessBreakerReportCommand() { + ProcessBreakerReports(); + } + + public byte[] ExportAllToCSV(DataTable table, DateTime fromDate, DateTime toDate) + { + if (table.Rows.Count == 0) return null; + + using (MemoryStream ms = new MemoryStream()) + using (StreamWriter writer = new StreamWriter(ms)) + { + // Write the CSV header to the file + writer.WriteLine(GetCSVHeader(table)); + + // Write data to the file + foreach (DataRow row in table.Rows) + writer.WriteLine(ToCSV(table, row)); + + return ms.ToArray(); + } + } + + // Converts the data group row of CSV data. + private string ToCSV(DataTable table, DataRow row) + { + IEnumerable columns = table.Columns.Cast().Select(x => "\"" + row[x.ColumnName] + "\""); + return string.Join(",", columns); + } + + // Converts the data group row of CSV data. + private string GetCSVHeader(DataTable table) + { + IEnumerable headers = table.Columns.Cast().Select(x => "\"" + x.ColumnName + "\""); + return string.Join(",", headers); + } + + #endregion + + #region [ Static ] + + private static readonly ILog Log = LogManager.GetLogger(typeof(ReportsEngine)); + private static readonly ConnectionStringParser ConnectionStringParser = new ConnectionStringParser(); + + #endregion + } +} diff --git a/Libraries/openXDA.Reports/openXDA.Reports.csproj b/Libraries/openXDA.Reports/openXDA.Reports.csproj new file mode 100644 index 0000000..239e67e --- /dev/null +++ b/Libraries/openXDA.Reports/openXDA.Reports.csproj @@ -0,0 +1,52 @@ + + + + Debug + AnyCPU + net9.0-windows + Library + true + openXDA.Reports + openXDA.Reports + false + Debug;Development;Release + + + true + full + false + ..\..\..\Build\Output\Debug\Libraries\ + DEBUG;TRACE + prompt + 4 + + + true + full + false + ..\..\..\Build\Output\Debug\Libraries\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + ..\..\..\Build\Output\Release\Libraries\ + TRACE + prompt + 4 + + + + + + + + + + + + + + diff --git a/PQDigest/EventWidgets b/PQDigest/EventWidgets index 645e841..d75968d 160000 --- a/PQDigest/EventWidgets +++ b/PQDigest/EventWidgets @@ -1 +1 @@ -Subproject commit 645e841bf0bfe0c27a2becb23faa7d3dd245dcfd +Subproject commit d75968dd39b3a3070415f1c84f66625b4db6ba3a diff --git a/PQDigest/PQDigest.sln b/PQDigest/PQDigest.sln index c1aed8d..9c5c0ad 100644 --- a/PQDigest/PQDigest.sln +++ b/PQDigest/PQDigest.sln @@ -18,28 +18,134 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FaultData", "..\Libraries\F EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FaultAlgorithms", "..\Libraries\FaultAlgorithms\FaultAlgorithms.csproj", "{94DDEF90-8941-4288-9B8A-CFCA9D64869C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "openHistorian.XDALink", "..\Libraries\openHistorian.XDALink\openHistorian.XDALink.csproj", "{21879DCD-F644-4715-A6FA-87415EA2326E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "openXDA.APIAuthentication", "..\Libraries\openXDA.APIAuthentication\openXDA.APIAuthentication.csproj", "{C5923AAC-7908-42BC-9657-E58C8D721115}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "openXDA.Configuration", "..\Libraries\openXDA.Configuration\openXDA.Configuration.csproj", "{5B4AE5BE-3A11-402E-B37B-47033B32C72D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "openXDA.PQI", "..\Libraries\openXDA.PQI\openXDA.PQI.csproj", "{1BB542A8-7C02-49F9-9C88-FEE45C9B95EC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "openXDA.Reports", "..\Libraries\openXDA.Reports\openXDA.Reports.csproj", "{E5DB7329-9267-4790-9E00-77C0CD03362A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2EF35D93-3B7A-4068-98D4-8646A651DF30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2EF35D93-3B7A-4068-98D4-8646A651DF30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2EF35D93-3B7A-4068-98D4-8646A651DF30}.Debug|x64.ActiveCfg = Debug|Any CPU + {2EF35D93-3B7A-4068-98D4-8646A651DF30}.Debug|x64.Build.0 = Debug|Any CPU + {2EF35D93-3B7A-4068-98D4-8646A651DF30}.Debug|x86.ActiveCfg = Debug|Any CPU + {2EF35D93-3B7A-4068-98D4-8646A651DF30}.Debug|x86.Build.0 = Debug|Any CPU {2EF35D93-3B7A-4068-98D4-8646A651DF30}.Release|Any CPU.ActiveCfg = Release|Any CPU {2EF35D93-3B7A-4068-98D4-8646A651DF30}.Release|Any CPU.Build.0 = Release|Any CPU + {2EF35D93-3B7A-4068-98D4-8646A651DF30}.Release|x64.ActiveCfg = Release|Any CPU + {2EF35D93-3B7A-4068-98D4-8646A651DF30}.Release|x64.Build.0 = Release|Any CPU + {2EF35D93-3B7A-4068-98D4-8646A651DF30}.Release|x86.ActiveCfg = Release|Any CPU + {2EF35D93-3B7A-4068-98D4-8646A651DF30}.Release|x86.Build.0 = Release|Any CPU {093E00E7-2D11-8275-E681-C04C98217BD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {093E00E7-2D11-8275-E681-C04C98217BD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {093E00E7-2D11-8275-E681-C04C98217BD0}.Debug|x64.ActiveCfg = Debug|Any CPU + {093E00E7-2D11-8275-E681-C04C98217BD0}.Debug|x64.Build.0 = Debug|Any CPU + {093E00E7-2D11-8275-E681-C04C98217BD0}.Debug|x86.ActiveCfg = Debug|Any CPU + {093E00E7-2D11-8275-E681-C04C98217BD0}.Debug|x86.Build.0 = Debug|Any CPU {093E00E7-2D11-8275-E681-C04C98217BD0}.Release|Any CPU.ActiveCfg = Release|Any CPU {093E00E7-2D11-8275-E681-C04C98217BD0}.Release|Any CPU.Build.0 = Release|Any CPU + {093E00E7-2D11-8275-E681-C04C98217BD0}.Release|x64.ActiveCfg = Release|Any CPU + {093E00E7-2D11-8275-E681-C04C98217BD0}.Release|x64.Build.0 = Release|Any CPU + {093E00E7-2D11-8275-E681-C04C98217BD0}.Release|x86.ActiveCfg = Release|Any CPU + {093E00E7-2D11-8275-E681-C04C98217BD0}.Release|x86.Build.0 = Release|Any CPU {FE8E8143-09E4-F41C-4800-2CAB679AE674}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FE8E8143-09E4-F41C-4800-2CAB679AE674}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE8E8143-09E4-F41C-4800-2CAB679AE674}.Debug|x64.ActiveCfg = Debug|Any CPU + {FE8E8143-09E4-F41C-4800-2CAB679AE674}.Debug|x64.Build.0 = Debug|Any CPU + {FE8E8143-09E4-F41C-4800-2CAB679AE674}.Debug|x86.ActiveCfg = Debug|Any CPU + {FE8E8143-09E4-F41C-4800-2CAB679AE674}.Debug|x86.Build.0 = Debug|Any CPU {FE8E8143-09E4-F41C-4800-2CAB679AE674}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE8E8143-09E4-F41C-4800-2CAB679AE674}.Release|Any CPU.Build.0 = Release|Any CPU + {FE8E8143-09E4-F41C-4800-2CAB679AE674}.Release|x64.ActiveCfg = Release|Any CPU + {FE8E8143-09E4-F41C-4800-2CAB679AE674}.Release|x64.Build.0 = Release|Any CPU + {FE8E8143-09E4-F41C-4800-2CAB679AE674}.Release|x86.ActiveCfg = Release|Any CPU + {FE8E8143-09E4-F41C-4800-2CAB679AE674}.Release|x86.Build.0 = Release|Any CPU {94DDEF90-8941-4288-9B8A-CFCA9D64869C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {94DDEF90-8941-4288-9B8A-CFCA9D64869C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94DDEF90-8941-4288-9B8A-CFCA9D64869C}.Debug|x64.ActiveCfg = Debug|Any CPU + {94DDEF90-8941-4288-9B8A-CFCA9D64869C}.Debug|x64.Build.0 = Debug|Any CPU + {94DDEF90-8941-4288-9B8A-CFCA9D64869C}.Debug|x86.ActiveCfg = Debug|Any CPU + {94DDEF90-8941-4288-9B8A-CFCA9D64869C}.Debug|x86.Build.0 = Debug|Any CPU {94DDEF90-8941-4288-9B8A-CFCA9D64869C}.Release|Any CPU.ActiveCfg = Release|Any CPU {94DDEF90-8941-4288-9B8A-CFCA9D64869C}.Release|Any CPU.Build.0 = Release|Any CPU + {94DDEF90-8941-4288-9B8A-CFCA9D64869C}.Release|x64.ActiveCfg = Release|Any CPU + {94DDEF90-8941-4288-9B8A-CFCA9D64869C}.Release|x64.Build.0 = Release|Any CPU + {94DDEF90-8941-4288-9B8A-CFCA9D64869C}.Release|x86.ActiveCfg = Release|Any CPU + {94DDEF90-8941-4288-9B8A-CFCA9D64869C}.Release|x86.Build.0 = Release|Any CPU + {21879DCD-F644-4715-A6FA-87415EA2326E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21879DCD-F644-4715-A6FA-87415EA2326E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21879DCD-F644-4715-A6FA-87415EA2326E}.Debug|x64.ActiveCfg = Debug|Any CPU + {21879DCD-F644-4715-A6FA-87415EA2326E}.Debug|x64.Build.0 = Debug|Any CPU + {21879DCD-F644-4715-A6FA-87415EA2326E}.Debug|x86.ActiveCfg = Debug|Any CPU + {21879DCD-F644-4715-A6FA-87415EA2326E}.Debug|x86.Build.0 = Debug|Any CPU + {21879DCD-F644-4715-A6FA-87415EA2326E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21879DCD-F644-4715-A6FA-87415EA2326E}.Release|Any CPU.Build.0 = Release|Any CPU + {21879DCD-F644-4715-A6FA-87415EA2326E}.Release|x64.ActiveCfg = Release|Any CPU + {21879DCD-F644-4715-A6FA-87415EA2326E}.Release|x64.Build.0 = Release|Any CPU + {21879DCD-F644-4715-A6FA-87415EA2326E}.Release|x86.ActiveCfg = Release|Any CPU + {21879DCD-F644-4715-A6FA-87415EA2326E}.Release|x86.Build.0 = Release|Any CPU + {C5923AAC-7908-42BC-9657-E58C8D721115}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5923AAC-7908-42BC-9657-E58C8D721115}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5923AAC-7908-42BC-9657-E58C8D721115}.Debug|x64.ActiveCfg = Debug|Any CPU + {C5923AAC-7908-42BC-9657-E58C8D721115}.Debug|x64.Build.0 = Debug|Any CPU + {C5923AAC-7908-42BC-9657-E58C8D721115}.Debug|x86.ActiveCfg = Debug|Any CPU + {C5923AAC-7908-42BC-9657-E58C8D721115}.Debug|x86.Build.0 = Debug|Any CPU + {C5923AAC-7908-42BC-9657-E58C8D721115}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5923AAC-7908-42BC-9657-E58C8D721115}.Release|Any CPU.Build.0 = Release|Any CPU + {C5923AAC-7908-42BC-9657-E58C8D721115}.Release|x64.ActiveCfg = Release|Any CPU + {C5923AAC-7908-42BC-9657-E58C8D721115}.Release|x64.Build.0 = Release|Any CPU + {C5923AAC-7908-42BC-9657-E58C8D721115}.Release|x86.ActiveCfg = Release|Any CPU + {C5923AAC-7908-42BC-9657-E58C8D721115}.Release|x86.Build.0 = Release|Any CPU + {5B4AE5BE-3A11-402E-B37B-47033B32C72D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B4AE5BE-3A11-402E-B37B-47033B32C72D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B4AE5BE-3A11-402E-B37B-47033B32C72D}.Debug|x64.ActiveCfg = Debug|Any CPU + {5B4AE5BE-3A11-402E-B37B-47033B32C72D}.Debug|x64.Build.0 = Debug|Any CPU + {5B4AE5BE-3A11-402E-B37B-47033B32C72D}.Debug|x86.ActiveCfg = Debug|Any CPU + {5B4AE5BE-3A11-402E-B37B-47033B32C72D}.Debug|x86.Build.0 = Debug|Any CPU + {5B4AE5BE-3A11-402E-B37B-47033B32C72D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B4AE5BE-3A11-402E-B37B-47033B32C72D}.Release|Any CPU.Build.0 = Release|Any CPU + {5B4AE5BE-3A11-402E-B37B-47033B32C72D}.Release|x64.ActiveCfg = Release|Any CPU + {5B4AE5BE-3A11-402E-B37B-47033B32C72D}.Release|x64.Build.0 = Release|Any CPU + {5B4AE5BE-3A11-402E-B37B-47033B32C72D}.Release|x86.ActiveCfg = Release|Any CPU + {5B4AE5BE-3A11-402E-B37B-47033B32C72D}.Release|x86.Build.0 = Release|Any CPU + {1BB542A8-7C02-49F9-9C88-FEE45C9B95EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1BB542A8-7C02-49F9-9C88-FEE45C9B95EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BB542A8-7C02-49F9-9C88-FEE45C9B95EC}.Debug|x64.ActiveCfg = Debug|Any CPU + {1BB542A8-7C02-49F9-9C88-FEE45C9B95EC}.Debug|x64.Build.0 = Debug|Any CPU + {1BB542A8-7C02-49F9-9C88-FEE45C9B95EC}.Debug|x86.ActiveCfg = Debug|Any CPU + {1BB542A8-7C02-49F9-9C88-FEE45C9B95EC}.Debug|x86.Build.0 = Debug|Any CPU + {1BB542A8-7C02-49F9-9C88-FEE45C9B95EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1BB542A8-7C02-49F9-9C88-FEE45C9B95EC}.Release|Any CPU.Build.0 = Release|Any CPU + {1BB542A8-7C02-49F9-9C88-FEE45C9B95EC}.Release|x64.ActiveCfg = Release|Any CPU + {1BB542A8-7C02-49F9-9C88-FEE45C9B95EC}.Release|x64.Build.0 = Release|Any CPU + {1BB542A8-7C02-49F9-9C88-FEE45C9B95EC}.Release|x86.ActiveCfg = Release|Any CPU + {1BB542A8-7C02-49F9-9C88-FEE45C9B95EC}.Release|x86.Build.0 = Release|Any CPU + {E5DB7329-9267-4790-9E00-77C0CD03362A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5DB7329-9267-4790-9E00-77C0CD03362A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5DB7329-9267-4790-9E00-77C0CD03362A}.Debug|x64.ActiveCfg = Debug|Any CPU + {E5DB7329-9267-4790-9E00-77C0CD03362A}.Debug|x64.Build.0 = Debug|Any CPU + {E5DB7329-9267-4790-9E00-77C0CD03362A}.Debug|x86.ActiveCfg = Debug|Any CPU + {E5DB7329-9267-4790-9E00-77C0CD03362A}.Debug|x86.Build.0 = Debug|Any CPU + {E5DB7329-9267-4790-9E00-77C0CD03362A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5DB7329-9267-4790-9E00-77C0CD03362A}.Release|Any CPU.Build.0 = Release|Any CPU + {E5DB7329-9267-4790-9E00-77C0CD03362A}.Release|x64.ActiveCfg = Release|Any CPU + {E5DB7329-9267-4790-9E00-77C0CD03362A}.Release|x64.Build.0 = Release|Any CPU + {E5DB7329-9267-4790-9E00-77C0CD03362A}.Release|x86.ActiveCfg = Release|Any CPU + {E5DB7329-9267-4790-9E00-77C0CD03362A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -48,6 +154,11 @@ Global {093E00E7-2D11-8275-E681-C04C98217BD0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {FE8E8143-09E4-F41C-4800-2CAB679AE674} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {94DDEF90-8941-4288-9B8A-CFCA9D64869C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {21879DCD-F644-4715-A6FA-87415EA2326E} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {C5923AAC-7908-42BC-9657-E58C8D721115} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {5B4AE5BE-3A11-402E-B37B-47033B32C72D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {1BB542A8-7C02-49F9-9C88-FEE45C9B95EC} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {E5DB7329-9267-4790-9E00-77C0CD03362A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8C510F68-7B36-4E81-A5DC-93CD9C1CB91D} From f604cdb555b78b61004a01fe4b405ebe10fed9cc Mon Sep 17 00:00:00 2001 From: prestoncraw Date: Mon, 3 Aug 2026 14:44:25 -0400 Subject: [PATCH 2/3] Migrate PQBrowsers openXDA.Model library --- Libraries/openXDA.Model/Channels/Channel.cs | 57 ++++- .../openXDA.Model/Channels/ChannelGroup.cs | 43 ++++ .../DERAnalytic/DERAnalyticResult.cs | 54 +++++ Libraries/openXDA.Model/Events/EventType.cs | 42 ++++ Libraries/openXDA.Model/Meters/AssetGroup.cs | 60 +++++ Libraries/openXDA.Model/Note.cs | 63 ++++++ Libraries/openXDA.Model/Reports/Report.cs | 46 ++++ .../openXDA.Model/SEBrowser/DetailedSeries.cs | 37 +++ .../openXDA.Model/SEBrowser/TrendChannel.cs | 48 ++++ .../openXDA.Model/SearchRestrictionHelper.cs | 55 +++++ .../Settings/BreakerReportSettings.cs | 64 ++++++ .../Settings/PQReportsSettings.cs | 116 ++++++++++ .../SystemCenter/AdditionalField.cs | 44 ++++ .../SystemCenter/DetailedAsset.cs | 93 ++++++++ .../SystemCenter/DetailedLocation.cs | 104 +++++++++ .../SystemCenter/DetailedMeter.cs | 68 ++++++ .../openXDA.Model/SystemCenter/ValueList.cs | 211 ++++++++++++++++++ .../SystemCenter/ValueListGroup.cs | 205 +++++++++++++++++ .../TransmissionElements/AssetTypes.cs | 57 +++++ .../openXDA.Model/TransmissionElements/DER.cs | 66 ++++++ .../StandardMagDurCurve.cs | 41 ++++ 21 files changed, 1570 insertions(+), 4 deletions(-) create mode 100644 Libraries/openXDA.Model/Channels/ChannelGroup.cs create mode 100644 Libraries/openXDA.Model/DERAnalytic/DERAnalyticResult.cs create mode 100644 Libraries/openXDA.Model/Events/EventType.cs create mode 100644 Libraries/openXDA.Model/Meters/AssetGroup.cs create mode 100644 Libraries/openXDA.Model/Note.cs create mode 100644 Libraries/openXDA.Model/Reports/Report.cs create mode 100644 Libraries/openXDA.Model/SEBrowser/DetailedSeries.cs create mode 100644 Libraries/openXDA.Model/SEBrowser/TrendChannel.cs create mode 100644 Libraries/openXDA.Model/SearchRestrictionHelper.cs create mode 100644 Libraries/openXDA.Model/Settings/BreakerReportSettings.cs create mode 100644 Libraries/openXDA.Model/Settings/PQReportsSettings.cs create mode 100644 Libraries/openXDA.Model/SystemCenter/AdditionalField.cs create mode 100644 Libraries/openXDA.Model/SystemCenter/DetailedAsset.cs create mode 100644 Libraries/openXDA.Model/SystemCenter/DetailedLocation.cs create mode 100644 Libraries/openXDA.Model/SystemCenter/DetailedMeter.cs create mode 100644 Libraries/openXDA.Model/SystemCenter/ValueList.cs create mode 100644 Libraries/openXDA.Model/SystemCenter/ValueListGroup.cs create mode 100644 Libraries/openXDA.Model/TransmissionElements/AssetTypes.cs create mode 100644 Libraries/openXDA.Model/TransmissionElements/DER.cs create mode 100644 Libraries/openXDA.Model/TransmissionElements/StandardMagDurCurve.cs diff --git a/Libraries/openXDA.Model/Channels/Channel.cs b/Libraries/openXDA.Model/Channels/Channel.cs index 491748e..9d65c5b 100644 --- a/Libraries/openXDA.Model/Channels/Channel.cs +++ b/Libraries/openXDA.Model/Channels/Channel.cs @@ -21,13 +21,14 @@ // //****************************************************************************************************** +using Gemstone.Data; +using Gemstone.Data.Model; +using Newtonsoft.Json; + using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Data; using System.Transactions; -using Gemstone.Data; -using Gemstone.Data.Model; -using Newtonsoft.Json; using IsolationLevel = System.Transactions.IsolationLevel; namespace openXDA.Model @@ -476,6 +477,54 @@ public int GetHashCode(Channel obj) } } + [TableName("ChannelDetail")] + public class ChannelDetail : Channel + { + public string MeterName { get; set; } + + public string AssetKey { get; set; } + + public string AssetName { get; set; } + + public new string MeasurementType { get; set; } + + public new string MeasurementCharacteristic { get; set; } + + public new string Phase { get; set; } + + public string Mapping { get; set; } + + public int SeriesTypeID { get; set; } + + public string SeriesType { get; set; } + + [SearchExtension("^VoltageKV$")] + public static RecordRestriction GetVoltageKVRestriction(IRecordFilter filter) + { + return SearchRestrictionHelper.GetSearchRestriction("(SELECT VoltageKV FROM Asset WHERE Asset.ID = FullTbl.AssetID)", filter); + } + + [SearchExtension("^ChannelGroupTypeID$")] + public static RecordRestriction GetChannelGroupTypeRestriction(IRecordFilter filter) + { + RecordRestriction restriction = SearchRestrictionHelper.GetSearchRestriction("ID", filter); + + return new RecordRestriction( + $"EXISTS(SELECT * FROM ChannelGroupType WHERE {restriction.FilterExpression} AND MeasurementTypeID = FullTbl.MeasurementTypeID AND MeasurementCharacteristicID = FullTbl.MeasurementCharacteristicID)", + restriction.Parameters); + } + + [SearchExtension("^SeriesID$")] + public static RecordRestriction GetSeriesRestriction(IRecordFilter filter) + { + RecordRestriction restriction = SearchRestrictionHelper.GetSearchRestriction("Series.SeriesTypeID", filter); + + return new RecordRestriction( + $"(SELECT COUNT(Series.ID) FROM SERIES WHERE Series.ChannelID = FullTbl.ID AND {restriction.FilterExpression}) > 0", + restriction.Parameters); + } + } + public class ChannelInfo { [PrimaryKey(true)] @@ -577,4 +626,4 @@ public static UserDashSettings GetOrAdd(this TableOperations t } -} \ No newline at end of file +} diff --git a/Libraries/openXDA.Model/Channels/ChannelGroup.cs b/Libraries/openXDA.Model/Channels/ChannelGroup.cs new file mode 100644 index 0000000..b5b463c --- /dev/null +++ b/Libraries/openXDA.Model/Channels/ChannelGroup.cs @@ -0,0 +1,43 @@ +//****************************************************************************************************** +// ChannelGroup.cs - Gbtc +// +// Copyright © 2020, 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: +// ---------------------------------------------------------------------------------------------------- +// 10/12/2020 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + + + +using Gemstone.Data.Model; +using System.ComponentModel.DataAnnotations; + +namespace openXDA.Model; + +[PostRoles("Administrator, Transmission SME")] +[PatchRoles("Administrator, Transmission SME")] +[DeleteRoles("Administrator, Transmission SME")] +public class ChannelGroup +{ + [PrimaryKey(true)] + public int ID { get; set; } + + [StringLength(200)] + public string Name { get; set; } + + public string Description { get; set; } +} diff --git a/Libraries/openXDA.Model/DERAnalytic/DERAnalyticResult.cs b/Libraries/openXDA.Model/DERAnalytic/DERAnalyticResult.cs new file mode 100644 index 0000000..c667215 --- /dev/null +++ b/Libraries/openXDA.Model/DERAnalytic/DERAnalyticResult.cs @@ -0,0 +1,54 @@ +//****************************************************************************************************** +// DERAnalyticResult.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: +// ---------------------------------------------------------------------------------------------------- +// 11/09/2021 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data.Model; + +namespace openXDA.Model; + +public class DERAnalyticResult +{ + [PrimaryKey(true)] + public int ID { get; set; } + + public int? EventID { get; set; } + + public int MeterID { get; set; } + + public int AssetID { get; set; } + + public int ChannelID { get; set; } + + public string Regulation { get; set; } + + + public string Parameter { get; set; } + + public double Threshold { get; set; } + + public double Value { get; set; } + + [UseEscapedName] + public DateTime Time { get; set; } + + public string DataType { get; set; } +} \ No newline at end of file diff --git a/Libraries/openXDA.Model/Events/EventType.cs b/Libraries/openXDA.Model/Events/EventType.cs new file mode 100644 index 0000000..3d5a0c1 --- /dev/null +++ b/Libraries/openXDA.Model/Events/EventType.cs @@ -0,0 +1,42 @@ +//****************************************************************************************************** +// EventType.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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// 12/20/2022 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data.Model; + +namespace openXDA.Model; + +public class EventType +{ + [PrimaryKey(true)] + public int ID { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public bool ShowInFilter { get; set; } + + public string Category { get; set; } +} \ No newline at end of file diff --git a/Libraries/openXDA.Model/Meters/AssetGroup.cs b/Libraries/openXDA.Model/Meters/AssetGroup.cs new file mode 100644 index 0000000..9848035 --- /dev/null +++ b/Libraries/openXDA.Model/Meters/AssetGroup.cs @@ -0,0 +1,60 @@ +//****************************************************************************************************** +// AssetGroup.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: +// ---------------------------------------------------------------------------------------------------- +// 08/29/2017 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data.Model; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace openXDA.Model +{ + public class AssetGroup + { + [Required] + [PrimaryKey(true)] + public int ID { get; set; } + + [Required] + [StringLength(100)] + [DefaultSortOrder] + public string Name { get; set; } + + [DefaultValue(true)] + public bool DisplayDashboard { get; set; } + + [DefaultValue(false)] + public bool DisplayEmail { get; set; } + } + + [TableName("AssetGroupView")] + [PostRoles("Administrator, Transmission SME")] + [PatchRoles("Administrator, Transmission SME")] + [DeleteRoles("Administrator, Transmission SME")] + public class AssetGroupView : AssetGroup + { + public int AssetGroups { get; set; } + public int Meters { get; set; } + public int Assets { get; set; } + public int Users { get; set; } + + } +} diff --git a/Libraries/openXDA.Model/Note.cs b/Libraries/openXDA.Model/Note.cs new file mode 100644 index 0000000..dd2407c --- /dev/null +++ b/Libraries/openXDA.Model/Note.cs @@ -0,0 +1,63 @@ +//****************************************************************************************************** +// Note.cs - Gbtc +// +// Copyright © 2020, 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/14/2020 - Billy Ernest +// Generated original version of source code. +// 05/16/2021 - C. Lackner +// Merged notes model from SystemCenter. +// +//****************************************************************************************************** + +using Gemstone.Data; +using Gemstone.Data.Model; +using Gemstone.Web.APIController; +using Microsoft.AspNetCore.Mvc; + +namespace openXDA.Model; + +[TableName("Note"), UseEscapedName] +[PostRoles("Administrator, Transmission SME, PQ Data Viewer")] +[DeleteRoles("Administrator, Transmission SME")] +[PatchRoles("Administrator, Transmission SME")] +public class Notes +{ + [PrimaryKey(true)] + public int ID { get; set; } + public int NoteTypeID { get; set; } + public int NoteApplicationID { get; set; } = 2; + public int NoteTagID { get; set; } = 1; + public int ReferenceTableID { get; set; } + public string Note { get; set; } + public string UserAccount { get; set; } + + [DefaultSortOrder(false)] + public DateTime Timestamp { get; set; } +} + +public class NotesController : ModelController where T : Notes, new() +{ + public override async Task Post([FromBody] T record, CancellationToken cancellationToken) + { + using AdoDataConnection connection = CreateConnection(); + + record.UserAccount = User.Identity.Name; + + int result = new TableOperations(connection).AddNewRecord(record); + return Ok(result); + } +} diff --git a/Libraries/openXDA.Model/Reports/Report.cs b/Libraries/openXDA.Model/Reports/Report.cs new file mode 100644 index 0000000..a4b4dea --- /dev/null +++ b/Libraries/openXDA.Model/Reports/Report.cs @@ -0,0 +1,46 @@ +//****************************************************************************************************** +// Report.cs - Gbtc +// +// Copyright © 2018, 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: +// ---------------------------------------------------------------------------------------------------- +// 07/02/2018 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data.Model; +using System.ComponentModel.DataAnnotations; + +namespace openXDA.Model +{ + public class Report + { + [PrimaryKey(true)] + public int ID { get; set; } + + [Required] + public int MeterID { get; set; } + [Required] + public int Month { get; set; } + [Required] + public int Year { get; set; } + [Required] + [StringLength(4)] + public string Results { get; set; } + + public byte[] PDF { get; set; } + } +} \ No newline at end of file diff --git a/Libraries/openXDA.Model/SEBrowser/DetailedSeries.cs b/Libraries/openXDA.Model/SEBrowser/DetailedSeries.cs new file mode 100644 index 0000000..b328042 --- /dev/null +++ b/Libraries/openXDA.Model/SEBrowser/DetailedSeries.cs @@ -0,0 +1,37 @@ +//****************************************************************************************************** +// DetailedSeries.cs - Gbtc +// +// Copyright © 2020, 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: +// ---------------------------------------------------------------------------------------------------- +// 07/31/2025 - G. Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data.Model; + +namespace SEBrowser.Model +{ + public class DetailedSeries + { + [PrimaryKey(true)] + public int ID { get; set; } + [DefaultSortOrder] + public int ChannelID { get; set; } + public string TypeName { get; set; } + public string TypeDescription { get; set; } + } +} diff --git a/Libraries/openXDA.Model/SEBrowser/TrendChannel.cs b/Libraries/openXDA.Model/SEBrowser/TrendChannel.cs new file mode 100644 index 0000000..6b084f6 --- /dev/null +++ b/Libraries/openXDA.Model/SEBrowser/TrendChannel.cs @@ -0,0 +1,48 @@ +//****************************************************************************************************** +// TrendChannel.cs - Gbtc +// +// Copyright © 2020, 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: +// ---------------------------------------------------------------------------------------------------- +// 07/31/2025 - G. Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data.Model; + +namespace SEBrowser.Model +{ + public class TrendChannel + { + [PrimaryKey(true)] + public string ID { get; set; } + public int ChannelID { get; set; } + [DefaultSortOrder] + public string Name { get; set; } + public string Description { get; set; } + public int AssetID { get; set; } + public string AssetKey { get; set; } + public string AssetName { get; set; } + public int MeterID { get; set; } + public string MeterKey { get; set; } + public string MeterName { get; set; } + public string MeterShortName { get; set; } + public string Phase { get; set; } + public string ChannelGroup { get; set; } + public string ChannelGroupType { get; set; } + public string Unit { get; set; } + } +} diff --git a/Libraries/openXDA.Model/SearchRestrictionHelper.cs b/Libraries/openXDA.Model/SearchRestrictionHelper.cs new file mode 100644 index 0000000..ca740df --- /dev/null +++ b/Libraries/openXDA.Model/SearchRestrictionHelper.cs @@ -0,0 +1,55 @@ +//****************************************************************************************************** +// SearchRestrictionHelper.cs - Gbtc +// +// Copyright © 2026, 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: +// ---------------------------------------------------------------------------------------------------- +// 07/07/2026 - P. Crawford +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + internal static class SearchRestrictionHelper + { + // IN / NOT IN filters carry an array SearchParameter that must expand to one placeholder per element; + // a single placeholder cannot bind an array. + internal static RecordRestriction GetSearchRestriction(string fieldExpression, IRecordFilter filter, int parameterOffset = 0) + { + if (!string.Equals(filter.Operator, "IN", StringComparison.OrdinalIgnoreCase) && + !string.Equals(filter.Operator, "NOT IN", StringComparison.OrdinalIgnoreCase)) + return new RecordRestriction($"{fieldExpression} {filter.Operator} {{{parameterOffset}}}", filter.SearchParameter); + + object[] parameters = GetSearchParameters(filter.SearchParameter).ToArray(); + string placeholders = string.Join(", ", parameters.Select((_, index) => $"{{{index + parameterOffset}}}")); + + return new RecordRestriction($"{fieldExpression} {filter.Operator} ({placeholders})", parameters); + } + + private static IEnumerable GetSearchParameters(object searchParameter) + { + if (searchParameter is string searchText) + return searchText.Trim('(', ')').Split(',').Select(value => (object)value.Trim()); + + if (searchParameter is System.Collections.IEnumerable parameters) + return parameters.Cast(); + + return [searchParameter]; + } + } +} diff --git a/Libraries/openXDA.Model/Settings/BreakerReportSettings.cs b/Libraries/openXDA.Model/Settings/BreakerReportSettings.cs new file mode 100644 index 0000000..2d87837 --- /dev/null +++ b/Libraries/openXDA.Model/Settings/BreakerReportSettings.cs @@ -0,0 +1,64 @@ +//****************************************************************************************************** +// BreakerReportsSettings.cs - Gbtc +// +// Copyright © 2019, 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: +// ---------------------------------------------------------------------------------------------------- +// 07/09/2019 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Model +{ + public class BreakerReportsSettings + { + #region [ Members ] + + // Constants + public const string CategoryName = "BreakerReports"; + + #endregion + + #region [ Properties ] + + /// + /// Indicates whether operation is enabled. + /// + [Setting] + [DefaultValue(false)] + public bool Enabled { get; set; } + + /// + /// Indicates whether operation is enabled. + /// + [Setting] + [DefaultValue("0 0 2 * *")] // Runs on second day of month to ensure all data is in + public string Schedule { get; set; } + + /// + /// Comma separated list of emails to send breaker reports to. + /// + [Setting] + [DefaultValue("")] + public string EmailList { get; set; } + + #endregion + } +} diff --git a/Libraries/openXDA.Model/Settings/PQReportsSettings.cs b/Libraries/openXDA.Model/Settings/PQReportsSettings.cs new file mode 100644 index 0000000..45f77e9 --- /dev/null +++ b/Libraries/openXDA.Model/Settings/PQReportsSettings.cs @@ -0,0 +1,116 @@ +//****************************************************************************************************** +// ReportsSettings.cs - Gbtc +// +// Copyright © 2018, 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: +// ---------------------------------------------------------------------------------------------------- +// 06/14/2018 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel; +using System.Configuration; + +namespace openXDA.Model +{ + public class PQReportsSettings + { + #region [ Members ] + + // Constants + public const string CategoryName = "PQReports"; + + #endregion + + #region [ Properties ] + + /// + /// Indicates whether operation is enabled. + /// + [Setting] + [DefaultValue(false)] + public bool Enabled { get; set; } + + /// + /// Indicates whether operation is enabled. + /// + [Setting] + [DefaultValue("0 0 2 * *")] // Runs on second day of month to ensure all data is in + public string Schedule { get; set; } + + [Setting] + [DefaultValue(99.0D)] + public double FirstFrequencyPercentile { get; set; } + + [Setting] + [DefaultValue(0.3D)] + public double FirstFrequencyDeviationLimit { get; set; } + + [Setting] + [DefaultValue(99.95D)] + public double SecondFrequencyPercentile { get; set; } + + [Setting] + [DefaultValue(0.5D)] + public double SecondFrequencyDeviationLimit { get; set; } + + [Setting] + [DefaultValue(99.0D)] + public double FirstVoltagePercentile { get; set; } + + [Setting] + [DefaultValue(3.0D)] + public double FirstVoltageDeviationLimit { get; set; } + + [Setting] + [DefaultValue(99.95D)] + public double SecondVoltagePercentile { get; set; } + + [Setting] + [DefaultValue(5.0D)] + public double SecondVoltageDeviationLimit { get; set; } + + [Setting] + [DefaultValue(98.5D)] + public double FlickerPercentile { get; set; } + + [Setting] + [DefaultValue(1.0D)] + public double FlickerHighLimit { get; set; } + + [Setting] + [DefaultValue(99.95D)] + public double VoltageUnbalancePercentile { get; set; } + + [Setting] + [DefaultValue(2.0D)] + public double VoltageUnbalanceHighLimit { get; set; } + + [Setting] + [DefaultValue(0.0D)] + public double VoltageUnbalanceLowLimit { get; set; } + + [Setting] + [DefaultValue(99.95D)] + public double VoltageTHDPercentile { get; set; } + + [Setting] + [DefaultValue(8.0D)] + public double VoltageTHDHighLimit { get; set; } + + #endregion + } +} diff --git a/Libraries/openXDA.Model/SystemCenter/AdditionalField.cs b/Libraries/openXDA.Model/SystemCenter/AdditionalField.cs new file mode 100644 index 0000000..adf860e --- /dev/null +++ b/Libraries/openXDA.Model/SystemCenter/AdditionalField.cs @@ -0,0 +1,44 @@ +//****************************************************************************************************** +// AdditionalField.cs - Gbtc +// +// Copyright © 2019, 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: +// ---------------------------------------------------------------------------------------------------- +// 09/20/2019 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Data.Model; + +namespace SystemCenter.Model; + +[UseEscapedName] +[PatchRoles("Administrator, Transmission SME")] +[PostRoles("Administrator, Transmission SME")] +[DeleteRoles("Administrator, Transmission SME")] +public class AdditionalField +{ + [PrimaryKey(true)] + public int ID { get; set; } + public string ParentTable { get; set; } + public string FieldName { get; set; } + public string Type { get; set; } + public int? ExternalDBTableID { get; set; } + public bool IsSecure { get; set; } + public bool IsInfo { get; set; } + public bool IsKey { get; set; } + public bool Searchable { get; set; } +} diff --git a/Libraries/openXDA.Model/SystemCenter/DetailedAsset.cs b/Libraries/openXDA.Model/SystemCenter/DetailedAsset.cs new file mode 100644 index 0000000..1bf65f9 --- /dev/null +++ b/Libraries/openXDA.Model/SystemCenter/DetailedAsset.cs @@ -0,0 +1,93 @@ +//****************************************************************************************************** +// DetailedAsset.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: +// ---------------------------------------------------------------------------------------------------- +// 10/22/2021 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Configuration; +using Gemstone.Data; +using Gemstone.Data.Model; +using openXDA.Model; + +namespace SystemCenter.Model +{ + public class DetailedAsset + { + [PrimaryKey(true)] + public int ID { get; set; } + + [DefaultSortOrder] + public string AssetKey { get; set; } + public string AssetName { get; set; } + public double VoltageKV { get; set; } + public string AssetType { get; set; } + public int Meters { get; set; } + public int Locations { get; set; } + + // The frontend prefixes any user-defined Additional Field filter with "AdditionalField.", so this matches that prefix, strips + // it back to the real field name, and resolves the value through the AdditionalFieldSearch view. + [SearchExtension(@"^AdditionalField\.")] + public static RecordRestriction GetAdditionalFieldRestriction(IRecordFilter filter) + { + string fieldName = filter.FieldName["AdditionalField.".Length..]; + + using AdoDataConnection connection = new(Settings.Default); + TableOperations tableOps = new(connection); + if (RecordFilter.WildCardOperators.Contains(filter.Operator, StringComparer.OrdinalIgnoreCase) && filter.SearchParameter is string stringVal) + filter.SearchParameter = stringVal.Replace("*", tableOps.WildcardChar); + + RecordRestriction valueRestriction = SearchRestrictionHelper.GetSearchRestriction("Value", filter, 1); + + return new RecordRestriction( + $"ID IN (SELECT ParentTableID FROM AdditionalFieldSearch WHERE ParentTable IN ('Line', 'Transformer', 'Breaker', 'CapBank', 'Bus', 'Generation', 'StationAux', 'StationBattery') AND FieldName = {{0}} AND {valueRestriction.FilterExpression})", + new object[] { fieldName }.Concat(valueRestriction.Parameters).ToArray()); + } + + [SearchExtension("^Meter$")] + public static RecordRestriction GetMeterRestriction(IRecordFilter filter) + { + using AdoDataConnection connection = new(Settings.Default); + TableOperations tableOps = new(connection); + if (RecordFilter.WildCardOperators.Contains(filter.Operator, StringComparer.OrdinalIgnoreCase) && filter.SearchParameter is string stringVal) + filter.SearchParameter = stringVal.Replace("*", tableOps.WildcardChar); + + RecordRestriction restriction = SearchRestrictionHelper.GetSearchRestriction("Meter.AssetKey", filter); + + return new RecordRestriction( + $"ID IN (SELECT MeterAsset.AssetID FROM Meter JOIN MeterAsset ON Meter.ID = MeterAsset.MeterID WHERE {restriction.FilterExpression})", + restriction.Parameters); + } + + [SearchExtension("^Location$")] + public static RecordRestriction GetLocationRestriction(IRecordFilter filter) + { + using AdoDataConnection connection = new(Settings.Default); + TableOperations tableOps = new(connection); + if (RecordFilter.WildCardOperators.Contains(filter.Operator, StringComparer.OrdinalIgnoreCase) && filter.SearchParameter is string stringVal) + filter.SearchParameter = stringVal.Replace("*", tableOps.WildcardChar); + + RecordRestriction restriction = SearchRestrictionHelper.GetSearchRestriction("Location.LocationKey", filter); + + return new RecordRestriction( + $"ID IN (SELECT AssetLocation.AssetID FROM Location JOIN AssetLocation ON AssetLocation.LocationID = Location.ID WHERE {restriction.FilterExpression})", + restriction.Parameters); + } + } +} diff --git a/Libraries/openXDA.Model/SystemCenter/DetailedLocation.cs b/Libraries/openXDA.Model/SystemCenter/DetailedLocation.cs new file mode 100644 index 0000000..0a13fbc --- /dev/null +++ b/Libraries/openXDA.Model/SystemCenter/DetailedLocation.cs @@ -0,0 +1,104 @@ +//****************************************************************************************************** +// DetailedLocation.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: +// ---------------------------------------------------------------------------------------------------- +// 10/22/2021 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Configuration; +using Gemstone.Data; +using Gemstone.Data.Model; +using openXDA.Model; + +namespace SystemCenter.Model; + +[PostRoles("Administrator, Transmission SME")] +public class DetailedLocation +{ + [PrimaryKey(true)] + public int ID { get; set; } + + [DefaultSortOrder] + public string LocationKey { get; set; } + + public string Name { get; set; } + + public string Alias { get; set; } + + public string ShortName { get; set; } + + public double Longitude { get; set; } + + public double Latitude { get; set; } + + public string Description { get; set; } + + public int Meters { get; set; } + + public int Assets { get; set; } + + // The frontend prefixes any user-defined Additional Field filter with "AdditionalField.", so this matches that prefix, strips + // it back to the real field name, and resolves the value through the AdditionalFieldSearch view. + [SearchExtension(@"^AdditionalField\.")] + public static RecordRestriction GetAdditionalFieldRestriction(IRecordFilter filter) + { + string fieldName = filter.FieldName["AdditionalField.".Length..]; + + using AdoDataConnection connection = new(Settings.Default); + TableOperations tableOps = new(connection); + if (RecordFilter.WildCardOperators.Contains(filter.Operator, StringComparer.OrdinalIgnoreCase) && filter.SearchParameter is string stringVal) + filter.SearchParameter = stringVal.Replace("*", tableOps.WildcardChar); + + RecordRestriction valueRestriction = SearchRestrictionHelper.GetSearchRestriction("Value", filter, 1); + + return new RecordRestriction( + $"ID IN (SELECT ParentTableID FROM AdditionalFieldSearch WHERE ParentTable = 'Location' AND FieldName = {{0}} AND {valueRestriction.FilterExpression})", + new object[] { fieldName }.Concat(valueRestriction.Parameters).ToArray()); + } + + [SearchExtension("^Meter$")] + public static RecordRestriction GetMeterRestriction(IRecordFilter filter) + { + using AdoDataConnection connection = new(Settings.Default); + TableOperations tableOps = new(connection); + if (RecordFilter.WildCardOperators.Contains(filter.Operator, StringComparer.OrdinalIgnoreCase) && filter.SearchParameter is string stringVal) + filter.SearchParameter = stringVal.Replace("*", tableOps.WildcardChar); + + RecordRestriction restriction = SearchRestrictionHelper.GetSearchRestriction("Meter.AssetKey", filter); + + return new RecordRestriction( + $"ID IN (SELECT Meter.LocationID FROM Meter WHERE {restriction.FilterExpression})", + restriction.Parameters); + } + + [SearchExtension("^Asset$")] + public static RecordRestriction GetAssetRestriction(IRecordFilter filter) + { + using AdoDataConnection connection = new(Settings.Default); + TableOperations tableOps = new(connection); + if (RecordFilter.WildCardOperators.Contains(filter.Operator, StringComparer.OrdinalIgnoreCase) && filter.SearchParameter is string stringVal) + filter.SearchParameter = stringVal.Replace("*", tableOps.WildcardChar); + + RecordRestriction restriction = SearchRestrictionHelper.GetSearchRestriction("Asset.AssetKey", filter); + + return new RecordRestriction( + $"ID IN (SELECT AssetLocation.LocationID FROM Asset JOIN AssetLocation ON AssetLocation.AssetID = Asset.ID WHERE {restriction.FilterExpression})", + restriction.Parameters); + } +} diff --git a/Libraries/openXDA.Model/SystemCenter/DetailedMeter.cs b/Libraries/openXDA.Model/SystemCenter/DetailedMeter.cs new file mode 100644 index 0000000..667d1f4 --- /dev/null +++ b/Libraries/openXDA.Model/SystemCenter/DetailedMeter.cs @@ -0,0 +1,68 @@ +//****************************************************************************************************** +// DetailedMeter.cs - Gbtc +// +// Copyright © 2019, 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: +// ---------------------------------------------------------------------------------------------------- +// 08/01/2020 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.Configuration; +using Gemstone.Data; +using Gemstone.Data.Model; +using openXDA.Model; + +namespace SystemCenter.Model +{ + public class DetailedMeter + { + [PrimaryKey(true)] + public int ID { get; set; } + + [DefaultSortOrder] + public string AssetKey { get; set; } + + public string Name { get; set; } + + public string Location { get; set; } + + public int MappedAssets { get; set; } + + public string Make { get; set; } + + public string Model { get; set; } + + // The frontend prefixes any user-defined Additional Field filter with "AdditionalField.", so this matches that prefix, strips + // it back to the real field name, and resolves the value through the AdditionalFieldSearch view. + [SearchExtension(@"^AdditionalField\.")] + public static RecordRestriction GetAdditionalFieldRestriction(IRecordFilter filter) + { + string fieldName = filter.FieldName["AdditionalField.".Length..]; + + using AdoDataConnection connection = new(Settings.Default); + TableOperations tableOps = new(connection); + if (RecordFilter.WildCardOperators.Contains(filter.Operator, StringComparer.OrdinalIgnoreCase) && filter.SearchParameter is string stringVal) + filter.SearchParameter = stringVal.Replace("*", tableOps.WildcardChar); + + RecordRestriction valueRestriction = SearchRestrictionHelper.GetSearchRestriction("Value", filter, 1); + + return new RecordRestriction( + $"ID IN (SELECT ParentTableID FROM AdditionalFieldSearch WHERE ParentTable = 'Meter' AND FieldName = {{0}} AND {valueRestriction.FilterExpression})", + new object[] { fieldName }.Concat(valueRestriction.Parameters).ToArray()); + } + } +} diff --git a/Libraries/openXDA.Model/SystemCenter/ValueList.cs b/Libraries/openXDA.Model/SystemCenter/ValueList.cs new file mode 100644 index 0000000..fec40b4 --- /dev/null +++ b/Libraries/openXDA.Model/SystemCenter/ValueList.cs @@ -0,0 +1,211 @@ +//****************************************************************************************************** +// ValueList.cs - Gbtc +// +// Copyright © 2018, 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: +// ---------------------------------------------------------------------------------------------------- +// 09/10/2018 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.ComponentModel.DataAnnotations; +using Gemstone.Data; +using Gemstone.Data.Model; +using Gemstone.Web.APIController; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json.Linq; + +namespace SystemCenter.Model; + +[TableName("ValueList"), UseEscapedName, PrimaryLabel("Text")] +public class ValueList +{ + [PrimaryKey(true)] + public int ID { get; set; } + + [ParentKey(typeof(ValueListGroup))] + public int GroupID { get; set; } + public string Value { get; set; } + public string AltValue { get; set; } + public int SortOrder { get; set; } +} + +public class ValueListController : ModelController +{ + [HttpGet, Route("Group/{groupName}")] + public IActionResult GetValueListForGroup(string groupName) + { + using AdoDataConnection connection = CreateConnection(); + return Ok(GetGroup(groupName, connection)); + } + + [HttpGet, Route("Count/{groupName}")] + public IActionResult GetValueListCountDictionary(string groupName) + { + using AdoDataConnection connection = CreateConnection(); + + JObject dictionary = []; + foreach (ValueList item in GetGroup(groupName, connection)) + dictionary.Add(item.ID.ToString(), GetCount(groupName, item.Value, connection).ToString()); + + return Ok(dictionary); + } + + [HttpPatch] + public override Task Patch([FromBody] ValueList newRecord, CancellationToken cancellationToken) + { + // Check if Value changed + bool changeVal = false; + ValueList oldRecord; + + using (AdoDataConnection connection = CreateConnection()) + { + oldRecord = new TableOperations(connection).QueryRecordWhere("ID = {0}", newRecord.ID); + changeVal = !(newRecord.Value == oldRecord.Value); + } + + if (changeVal) + { + ValueListGroup? group; + using AdoDataConnection connection = CreateConnection(); + + group = new TableOperations(connection).QueryRecordWhere("ID = {0}", newRecord.GroupID); + + // Wrapping is needed here, since C# tries to use the wrong method signature otherwise + object[] parameters = { newRecord.Value, oldRecord.Value, group?.Name ?? "" }; + // Update Additional Fields + connection.ExecuteScalar(@"UPDATE + AdditionalFieldValue + SET [Value] = {0} + WHERE + [Value] = {1} AND + ( + SELECT TOP 1 Type + FROM AdditionalField + WHERE AdditionalField.ID = AdditionalFieldValue.AdditionalFieldID + ) = {2}", parameters); + + RestrictedValueList? restriction = RestrictedValueList.List.Find((g) => g.Name == group?.Name); + + if (!(restriction?.UpdateSQL is null)) + { + object[] updateSqlParams = { newRecord.Value, oldRecord.Value }; + connection.ExecuteScalar(restriction.UpdateSQL, updateSqlParams); + } + + } + + return base.Patch(newRecord, cancellationToken); + } + + public override Task Delete(ValueList record, CancellationToken cancellationToken) + { + ValueListGroup? group; + using AdoDataConnection connection = CreateConnection(); + + group = new TableOperations(connection).QueryRecordWhere("ID = {0}", record.GroupID); + RestrictedValueList? restriction = RestrictedValueList.List.Find((g) => g.Name == group.Name); + + if (!(restriction?.CountSQL is null)) + { + int count = connection.ExecuteScalar(restriction.CountSQL, record.Value); + + if (count > 0) + throw new Exception("Cannot delete value that is still in use."); + } + + connection.ExecuteScalar(@"DELETE FROM AdditionalFieldValue + WHERE + [Value] = {0} AND + (SELECT TOP 1 Type FROM AdditionalField AF WHERE AF.ID = AdditionalFieldValue.AdditionalFieldID) = {1}", (object)record.Value, group?.Name); + + return base.Delete(record, cancellationToken); + } + + [HttpGet, Route("Count/{groupName}/{value}")] + public IActionResult GetCount(string groupName, string value) + { + using AdoDataConnection connection = CreateConnection(); + + return Ok(GetCount(groupName, value, connection)); + } + + private IEnumerable GetGroup(string groupName, AdoDataConnection connection) + { + TableOperations groupTable = new(connection); + TableOperations valueTable = new(connection); + List groupIds = groupTable.QueryRecordsWhere("Name = {0}", groupName).Select(group => group?.ID ?? -1).ToList(); + + if (groupIds.Count() == 0) + { + RestrictedValueList restriction = RestrictedValueList.List.Find((g) => g.Name == groupName); + if (!(restriction is null)) + { + groupTable.AddNewRecord( + new ValueListGroup() + { + Description = "", + Name = restriction.Name + }); + groupIds.Add(connection.ExecuteScalar("SELECT @@IDENTITY")); + + int sortOrder = 1; + foreach (object item in restriction.DefaultItems) + { + string value; + string altValue; + if (item.GetType() == typeof(Tuple)) + { + value = ((Tuple)item).Item1; + altValue = ((Tuple)item).Item2; + } + else if (item.GetType() == typeof(string)) + { + value = (string)item; + altValue = (string)item; + } + else + throw new InvalidCastException($"Could not convert object in DefaultItems of value list {restriction.Name} to either tuple or string."); + valueTable.AddNewRecord( + new ValueList() + { + GroupID = groupIds[0], + Value = value, + AltValue = altValue, + SortOrder = sortOrder + }); + sortOrder++; + } + } + else + return new List(); + } + return valueTable.QueryRecordsWhere($"GroupID in ({string.Join(",", groupIds)})").OrderBy(v => v.SortOrder); + } + + private int GetCount(string groupName, string value, AdoDataConnection connection) + { + int nAddlFields = connection.ExecuteScalar(@"SELECT COUNT(AFV.ID) FROM AdditionalFieldValue AFV WHERE + [Value] = {0} AND (SELECT TOP 1 AF.ID FROM AdditionalField AF WHERE Type = {1}) = AFV.AdditionalFieldID + ", value, groupName); + RestrictedValueList restriction = RestrictedValueList.List.Find((g) => g.Name == groupName); + int count = 0; + if (!(restriction?.CountSQL is null)) + count = connection.ExecuteScalar(restriction.CountSQL, value); + + return nAddlFields + count; + } +} diff --git a/Libraries/openXDA.Model/SystemCenter/ValueListGroup.cs b/Libraries/openXDA.Model/SystemCenter/ValueListGroup.cs new file mode 100644 index 0000000..000908e --- /dev/null +++ b/Libraries/openXDA.Model/SystemCenter/ValueListGroup.cs @@ -0,0 +1,205 @@ +//****************************************************************************************************** +// ValueListGroup.cs - Gbtc +// +// Copyright © 2018, 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: +// ---------------------------------------------------------------------------------------------------- +// 09/10/2018 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + +using Gemstone.ComponentModel.DataAnnotations; +using Gemstone.Data; +using Gemstone.Data.Model; +using Gemstone.Web.APIController; +using Microsoft.AspNetCore.Mvc; +using System.ComponentModel.DataAnnotations; + +namespace SystemCenter.Model +{ + [TableName("ValueListGroup"), UseEscapedName, PrimaryLabel("Name")] + public class ValueListGroup + { + [PrimaryKey(true)] + public int ID { get; set; } + + [StringLength(200)] + public string Name { get; set; } + public string Description { get; set; } + } + + public class RestrictedValueList + { + public string Name { get; set; } + public string CountSQL { get; set; } + public string UpdateSQL { get; set; } + + /// + /// Default items in a . + /// Objects may be either or of two , + /// where the represents <, > + /// + public object[] DefaultItems { get; set; } + + public static List List = new List(){ + new RestrictedValueList() { + Name = "TimeZones", + CountSQL = @"SELECT COUNT(ID) FROM + Meter + WHERE [TimeZone] = {0}", + UpdateSQL = @"UPDATE Meter + SET [TimeZone] = {0} + WHERE + [TimeZone] = {1}", + DefaultItems = new string[] {"UTC"} + }, + new RestrictedValueList() { + Name = "Make", + CountSQL = @"SELECT COUNT(ID) FROM + Meter + WHERE [Make] = {0}", + UpdateSQL = @"UPDATE + Meter + SET [Make] = {0} + WHERE + [Make] = {1}", + DefaultItems = new string[] {"GPA"} + }, + new RestrictedValueList() { + Name = "Model", + CountSQL = @"SELECT COUNT(ID) FROM + Meter + WHERE [Model] = {0}", + UpdateSQL = @"UPDATE + Meter + SET [Model] = {0} + WHERE + [Model] = {1}", + DefaultItems = new string[] {"PQMeter"} + }, + new RestrictedValueList() { + Name = "Unit", + CountSQL = @"SELECT COUNT(ID) FROM + ChannelGroupType + WHERE + [Unit] = {0}", + UpdateSQL = @"UPDATE + ChannelGroupType + SET [Unit] = {0} + WHERE + [Unit] = {1}", + DefaultItems = new string[] {"Unknown"} + }, + new RestrictedValueList() { + Name = "Category", + CountSQL = @"SELECT COUNT(ID) FROM + LocationDrawing + WHERE + [Category] = {0}", + UpdateSQL = @"UPDATE + LocationDrawing + SET [Category] = {0} + WHERE + [Category] = {1}", + DefaultItems = new string[] {"Oneline"} + }, + new RestrictedValueList() { + Name = "SpareChannel", + DefaultItems = new string[] {"Spare Channel"} + }, + new RestrictedValueList() { + Name = "TrendLabelDefaults", + DefaultItems = new Tuple[] { + new Tuple("Channel.MeterName", "Meter Name"), + new Tuple("Channel.AssetKey", "Asset Key"), + new Tuple("Channel.Name", "Channel Label"), + new Tuple("Channel.ChannelGroup", "Channel Group Name"), + new Tuple("Series.TypeName", "Channel Series") + } + }, + new RestrictedValueList() { + Name = "TrendLabelOptions", + DefaultItems = new Tuple[] { + new Tuple("Channel.MeterName", "Meter Name"), + new Tuple("Channel.MeterShortName", "Meter Short Name"), + new Tuple("Channel.MeterKey", "Meter Asset Key"), + new Tuple("Channel.AssetName", "Asset Name"), + new Tuple("Channel.AssetKey", "Asset Key"), + new Tuple("Channel.Phase", "Phase Name"), + new Tuple("Channel.Name", "Channel Label"), + new Tuple("Channel.Description", "Channel Description"), + new Tuple("Channel.ChannelGroup", "Channel Group Name"), + new Tuple("Channel.ChannelGroupType", "Channel Group Type"), + new Tuple("Series.TypeName", "Channel Series"), + new Tuple("Channel.Unit", "Unit") + } + } + }; + } + + [Route("api/ValueListGroup")] + public class ValueListGroupController : ModelController + { + public override Task Patch([FromBody] ValueListGroup newRecord, CancellationToken cancellationToken) + { + // Check if Value changed + bool changeVal = false; + ValueListGroup oldRecord; + + using (AdoDataConnection connection = CreateConnection()) + { + oldRecord = new TableOperations(connection).QueryRecordWhere("ID = {0}", newRecord.ID); + changeVal = !(newRecord.Name == oldRecord.Name); + } + + if (changeVal) + { + using AdoDataConnection connection = CreateConnection(); + + // Wrapping is needed here, since C# tries to use the wrong method signature otherwise + object[] parameters = { newRecord.Name, oldRecord.Name }; + + // Update Additional Fields + connection.ExecuteScalar(@"UPDATE + AdditionalField + SET [Type] = {0} + WHERE + [Type] = {1}", parameters); + + } + + return base.Patch(newRecord, cancellationToken); + } + + public override Task Delete(ValueListGroup record, CancellationToken cancellationToken) + { + using (AdoDataConnection connection = CreateConnection()) + { + // Wrapping is needed here, since C# tries to use the wrong method signature otherwise + object[] parameters = { record.Name }; + + // Update Additional Fields + connection.ExecuteScalar(@"UPDATE + AdditionalField + SET [Type] = 'string' + WHERE + [Type] = {0}", parameters); + } + + return base.Delete(record, cancellationToken); + } + } +} \ No newline at end of file diff --git a/Libraries/openXDA.Model/TransmissionElements/AssetTypes.cs b/Libraries/openXDA.Model/TransmissionElements/AssetTypes.cs new file mode 100644 index 0000000..8b16eab --- /dev/null +++ b/Libraries/openXDA.Model/TransmissionElements/AssetTypes.cs @@ -0,0 +1,57 @@ +//****************************************************************************************************** +// AssetTypes.cs - Gbtc +// +// Copyright © 2019, 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: +// ---------------------------------------------------------------------------------------------------- +// 12/24/2019 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.ComponentModel.DataAnnotations; +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + public enum AssetType + { + Line = 1, + Bus = 2, + Breaker = 3, + CapacitorBank = 4, + LineSegement = 5, + Transformer = 6, + CapBankRelay = 7, + DER = 8, + StationAux = 9, + StationBattery = 10, + Generation = 11 + } + + [TableName("AssetType")] + public class AssetTypes + { + [PrimaryKey(true)] + public int ID { get; set; } + + [StringLength(50)] + [DefaultSortOrder] + public string Name { get; set; } + + [StringLength(250)] + public string Description { get; set; } + } +} diff --git a/Libraries/openXDA.Model/TransmissionElements/DER.cs b/Libraries/openXDA.Model/TransmissionElements/DER.cs new file mode 100644 index 0000000..9a0c78f --- /dev/null +++ b/Libraries/openXDA.Model/TransmissionElements/DER.cs @@ -0,0 +1,66 @@ +//****************************************************************************************************** +// DER.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: +// ---------------------------------------------------------------------------------------------------- +// 10/19/2021 - Billy Ernest +// Generated original version of source code. +// +//****************************************************************************************************** + + +using Gemstone.Data; +using Gemstone.Data.Model; +using System.ComponentModel.DataAnnotations; + +namespace openXDA.Model; + +[MetadataType(typeof(Asset))] +public class DER : Asset +{ + + #region [ Properties ] + + public double FullRatedOutputCurrent { get; set; } + + public string VoltageLevel { get; set; } + + #endregion + + #region [ Methods ] + + public static DER DetailedDER(Asset asset, AdoDataConnection connection) + { + if ((object)connection == null) + return null; + + TableOperations table = new TableOperations(connection); + DER record = table.QueryRecordWhere("ID = {0}", asset.ID); + if (record == null) + return null; + + record.LazyContext = asset.LazyContext; + record.ConnectionFactory = asset.ConnectionFactory; + + return record; + } + + public static DER DetailedDER(Asset asset) + { + return DetailedDER(asset, asset.ConnectionFactory.Invoke()); + } + #endregion +} diff --git a/Libraries/openXDA.Model/TransmissionElements/StandardMagDurCurve.cs b/Libraries/openXDA.Model/TransmissionElements/StandardMagDurCurve.cs new file mode 100644 index 0000000..2be02ad --- /dev/null +++ b/Libraries/openXDA.Model/TransmissionElements/StandardMagDurCurve.cs @@ -0,0 +1,41 @@ +//****************************************************************************************************** +// OpenXDAController.cs - Gbtc +// +// Copyright © 2020, 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/04/2020 - Billy Ernest +// Generated original version of source code. +// 04/11/2022 - G. Santos +// Copied portion of source code from SEBrowser. +// +//****************************************************************************************************** + +using Gemstone.Data.Model; + +namespace openXDA.Model +{ + [PostRoles("Administrator")] + [DeleteRoles("Administrator")] + [PatchRoles("Administrator")] + public class StandardMagDurCurve + { + [PrimaryKey] + public int ID { get; set; } + public string Name { get; set; } + public string Area { get; set; } + public string Color { get; set; } + } +} \ No newline at end of file From 692ce0d9a06c99856726efb6966a3534488e45b3 Mon Sep 17 00:00:00 2001 From: prestoncraw Date: Mon, 3 Aug 2026 16:31:25 -0400 Subject: [PATCH 3/3] Finish library migration --- Directory.Build.props | 5 --- .../openHistorian.XDALink.csproj | 33 ++------------- .../openXDA.APIAuthentication.csproj | 19 ++------- .../openXDA.Configuration.csproj | 37 +++-------------- Libraries/openXDA.Model/openXDA.Model.csproj | 1 + Libraries/openXDA.PQI/openXDA.PQI.csproj | 28 +++---------- Libraries/openXDA.Reports/EmailWriter.cs | 19 ++++++++- .../openXDA.Reports/openXDA.Reports.csproj | 40 +++---------------- 8 files changed, 41 insertions(+), 141 deletions(-) delete mode 100644 Directory.Build.props diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index 9e460a7..0000000 --- a/Directory.Build.props +++ /dev/null @@ -1,5 +0,0 @@ - - - 1.0.179 - - diff --git a/Libraries/openHistorian.XDALink/openHistorian.XDALink.csproj b/Libraries/openHistorian.XDALink/openHistorian.XDALink.csproj index 5e8ee63..78a85a2 100644 --- a/Libraries/openHistorian.XDALink/openHistorian.XDALink.csproj +++ b/Libraries/openHistorian.XDALink/openHistorian.XDALink.csproj @@ -1,36 +1,13 @@ + - Debug - AnyCPU - Library - openHistorian.XDALink - openHistorian.XDALink net9.0 - Debug;Development;Release enable - true enable - - true - full - false - ..\..\..\Build\Output\Debug\Libraries\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - ..\..\..\Build\Output\Release\Libraries\ - TRACE - prompt - 4 - + - - + @@ -41,8 +18,4 @@ - - - - diff --git a/Libraries/openXDA.APIAuthentication/openXDA.APIAuthentication.csproj b/Libraries/openXDA.APIAuthentication/openXDA.APIAuthentication.csproj index facf64d..871f63c 100644 --- a/Libraries/openXDA.APIAuthentication/openXDA.APIAuthentication.csproj +++ b/Libraries/openXDA.APIAuthentication/openXDA.APIAuthentication.csproj @@ -1,26 +1,15 @@ + net9.0 - Debug;Development;Release - Library false - openXDA.APIAuthentication - GPA - openXDA.APIAuthentication - Copyright © 2022 - - - ..\..\..\Build\Output\Debug\Libraries\ - ..\..\..\Build\Output\Debug\Libraries\openXDA.APIAuthentication.xml - - - ..\..\..\Build\Output\Release\Libraries\ - ..\..\..\Build\Output\Release\Libraries\openXDA.APIAuthentication.xml + - \ No newline at end of file + + diff --git a/Libraries/openXDA.Configuration/openXDA.Configuration.csproj b/Libraries/openXDA.Configuration/openXDA.Configuration.csproj index 3d799a9..e5c3b0e 100644 --- a/Libraries/openXDA.Configuration/openXDA.Configuration.csproj +++ b/Libraries/openXDA.Configuration/openXDA.Configuration.csproj @@ -1,43 +1,16 @@ - Debug - AnyCPU net9.0 - Debug;Development;Release - Library - openXDA.Configuration - openXDA.Configuration - false enable enable - true - - - - true - full - false - ..\..\..\Build\Output\Debug\Libraries\ - DEBUG;TRACE - 4 - - - - pdbonly - true - ..\..\..\Build\Output\Release\Libraries\ - TRACE - 4 + false - - - - - - + + + @@ -45,4 +18,4 @@ - \ No newline at end of file + diff --git a/Libraries/openXDA.Model/openXDA.Model.csproj b/Libraries/openXDA.Model/openXDA.Model.csproj index 5f169de..7becfe7 100644 --- a/Libraries/openXDA.Model/openXDA.Model.csproj +++ b/Libraries/openXDA.Model/openXDA.Model.csproj @@ -10,6 +10,7 @@ + diff --git a/Libraries/openXDA.PQI/openXDA.PQI.csproj b/Libraries/openXDA.PQI/openXDA.PQI.csproj index f24661b..9cfd97a 100644 --- a/Libraries/openXDA.PQI/openXDA.PQI.csproj +++ b/Libraries/openXDA.PQI/openXDA.PQI.csproj @@ -1,32 +1,14 @@ + net9.0 - Debug;Development;Release - openXDA.PQI - openXDA.PQI false + - - + - - true - full - false - ..\..\..\Build\Output\Debug\Libraries\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - ..\..\..\Build\Output\Release\Libraries\ - TRACE - prompt - 4 - - \ No newline at end of file + + diff --git a/Libraries/openXDA.Reports/EmailWriter.cs b/Libraries/openXDA.Reports/EmailWriter.cs index 943f9d7..1174cf8 100644 --- a/Libraries/openXDA.Reports/EmailWriter.cs +++ b/Libraries/openXDA.Reports/EmailWriter.cs @@ -31,10 +31,13 @@ using System; using System.Collections.Generic; using System.Data; +using System.IO; using System.Linq; using System.Net; using System.Net.Mail; +using System.Xml; using System.Xml.Linq; +using System.Xml.Xsl; namespace openXDA.Reports { @@ -91,7 +94,7 @@ FOR XML RAW('Report') ,TYPE, ELEMENTS) as [Reports] FOR XML RAW ('PQReport'),TYPE, ELEMENTS ", month, year); - XDocument htmlDocument = XDocument.Parse(data.ApplyXSLTransform(template), LoadOptions.PreserveWhitespace); + XDocument htmlDocument = XDocument.Parse(ApplyXSLTransform(data, template), LoadOptions.PreserveWhitespace); try { @@ -111,6 +114,20 @@ FOR XML RAW ('PQReport'),TYPE, ELEMENTS } } + private static string ApplyXSLTransform(string document, string transform) + { + using StringReader documentReader = new StringReader(document); + using StringReader transformReader = new StringReader(transform); + using XmlReader xmlDocumentReader = XmlReader.Create(documentReader); + using XmlReader xmlTransformReader = XmlReader.Create(transformReader); + using StringWriter resultWriter = new StringWriter(); + + XslCompiledTransform compiler = new XslCompiledTransform(); + compiler.Load(xmlTransformReader); + compiler.Transform(xmlDocumentReader, null, resultWriter); + return resultWriter.ToString(); + } + private int LoadSentEmail(AdoDataConnection connection, List recipients, string subject, string body) { diff --git a/Libraries/openXDA.Reports/openXDA.Reports.csproj b/Libraries/openXDA.Reports/openXDA.Reports.csproj index 239e67e..a3fa6bd 100644 --- a/Libraries/openXDA.Reports/openXDA.Reports.csproj +++ b/Libraries/openXDA.Reports/openXDA.Reports.csproj @@ -1,52 +1,22 @@ - + - Debug - AnyCPU net9.0-windows - Library true - openXDA.Reports - openXDA.Reports false - Debug;Development;Release - - - true - full - false - ..\..\..\Build\Output\Debug\Libraries\ - DEBUG;TRACE - prompt - 4 - - - true - full - false - ..\..\..\Build\Output\Debug\Libraries\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - ..\..\..\Build\Output\Release\Libraries\ - TRACE - prompt - 4 + + + - - +