diff --git a/Dependencies/openHistorian/openHistorian.Core.dll b/Dependencies/openHistorian/openHistorian.Core.dll new file mode 100644 index 0000000..8d36e8c Binary files /dev/null and b/Dependencies/openHistorian/openHistorian.Core.dll differ 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..78a85a2 --- /dev/null +++ b/Libraries/openHistorian.XDALink/openHistorian.XDALink.csproj @@ -0,0 +1,21 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + ..\..\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..871f63c --- /dev/null +++ b/Libraries/openXDA.APIAuthentication/openXDA.APIAuthentication.csproj @@ -0,0 +1,15 @@ + + + + net9.0 + false + + + + + + + + + + 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..e5c3b0e --- /dev/null +++ b/Libraries/openXDA.Configuration/openXDA.Configuration.csproj @@ -0,0 +1,21 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + 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 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/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..9cfd97a --- /dev/null +++ b/Libraries/openXDA.PQI/openXDA.PQI.csproj @@ -0,0 +1,14 @@ + + + + net9.0 + false + + + + + + + + + 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..1174cf8 --- /dev/null +++ b/Libraries/openXDA.Reports/EmailWriter.cs @@ -0,0 +1,222 @@ +//****************************************************************************************************** +// 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.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 +{ + 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(ApplyXSLTransform(data, 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 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) + { + 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..a3fa6bd --- /dev/null +++ b/Libraries/openXDA.Reports/openXDA.Reports.csproj @@ -0,0 +1,22 @@ + + + + net9.0-windows + true + false + + + + + + + + + + + + + + + + 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}