diff --git a/Source/Applications/openPDC/openPDC/Startup.cs b/Source/Applications/openPDC/openPDC/Startup.cs index 79f1c06b0a..7ff707a3b8 100644 --- a/Source/Applications/openPDC/openPDC/Startup.cs +++ b/Source/Applications/openPDC/openPDC/Startup.cs @@ -21,11 +21,6 @@ // //****************************************************************************************************** -using System; -using System.Security; -using System.Web.Http; -using System.Web.Http.Cors; -using System.Web.Http.ExceptionHandling; using GSF.IO; using GSF.Web; using GSF.Web.Hosting; @@ -36,9 +31,14 @@ using ModbusAdapters; using Newtonsoft.Json; using openPDC.Adapters; -using Owin; using openPDC.Model; +using Owin; using PhasorWebUI; +using System; +using System.Security; +using System.Web.Http; +using System.Web.Http.Cors; +using System.Web.Http.ExceptionHandling; namespace openPDC { @@ -53,6 +53,11 @@ public override void Handle(ExceptionHandlerContext context) public class Startup { + /// + /// Gets the authentication options used for the hosted web server. + /// + public static AuthenticationOptions AuthenticationOptions { get; } = new AuthenticationOptions(); + public void Configuration(IAppBuilder app) { // Add Content-Security Headers @@ -77,15 +82,16 @@ public void Configuration(IAppBuilder app) } }); - // Modify the JSON serializer to serialize dates as UTC - otherwise, timezone will not be appended - // to date strings and browsers will select whatever timezone suits them + // Modify the JSON serializer to serialize dates as UTC - otherwise, timezone will not + // be appended to date strings and browsers will select whatever timezone suits them JsonSerializerSettings settings = JsonUtility.CreateDefaultSerializerSettings(); settings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; JsonSerializer serializer = JsonSerializer.Create(settings); GlobalHost.DependencyResolver.Register(typeof(JsonSerializer), () => serializer); AppModel model = Program.Host.Model; - // Load security hub into application domain before establishing SignalR hub configuration, initializing default status and exception handlers + // Load security hub into application domain before establishing SignalR hub + // configuration, initializing default status and exception handlers try { using (new SecurityHub( @@ -119,7 +125,7 @@ public void Configuration(IAppBuilder app) Program.Host.LogException )) { - WebExtensions.AddEmbeddedResourceAssembly(hub.GetType().Assembly); + WebExtensions.AddEmbeddedResourceAssembly(hub.GetType().Assembly); } } catch (Exception ex) @@ -145,9 +151,8 @@ public void Configuration(IAppBuilder app) // Enable GSF role-based security authentication app.UseAuthentication(AuthenticationOptions); - // Enable cross-domain scripting default policy - controllers can manually - // apply "EnableCors" attribute to class or an action to override default - // policy configured here + // Enable cross-domain scripting default policy - controllers can manually apply + // "EnableCors" attribute to class or an action to override default policy configured here try { if (!string.IsNullOrWhiteSpace(model.Global.DefaultCorsOrigins)) @@ -180,6 +185,12 @@ public void Configuration(IAppBuilder app) { using (new GrafanaController()) { } + using (new DeviceController()) { } + + using (new PhasorController()) { } + + using (new DevicePhasorController()) { } + httpConfig.Routes.MapHttpRoute( name: "CustomAPIs", routeTemplate: "api/{controller}/{action}/{id}", @@ -208,8 +219,8 @@ private void Load_ModbusAssembly() { try { - // Wrap class reference in lambda function to force - // assembly load errors to occur within the try-catch + // Wrap class reference in lambda function to force assembly load errors to occur + // within the try-catch new Action(() => { // Make embedded resources of Modbus poller available to web server @@ -224,12 +235,7 @@ private void Load_ModbusAssembly() Program.Host.LogException(new InvalidOperationException($"Failed to load Modbus assembly: {ex.Message}", ex)); } } - - // Static Properties - /// - /// Gets the authentication options used for the hosted web server. - /// - public static AuthenticationOptions AuthenticationOptions { get; } = new AuthenticationOptions(); + // Static Properties } -} +} \ No newline at end of file diff --git a/Source/Libraries/openPDC.Adapters/Constants/StringConstant.cs b/Source/Libraries/openPDC.Adapters/Constants/StringConstant.cs new file mode 100644 index 0000000000..05c0b95992 --- /dev/null +++ b/Source/Libraries/openPDC.Adapters/Constants/StringConstant.cs @@ -0,0 +1,14 @@ +namespace openPDC.Adapters.Constants +{ + internal static class StringConstant + { + #region [ Constants ] + + internal const string Acronym = "Acronym"; + internal const string DeviceID = "DeviceID"; + internal const string SourceIndex = "SourceIndex"; + internal const string SystemSettings = "systemSettings"; + + #endregion [ Constants ] + } +} \ No newline at end of file diff --git a/Source/Libraries/openPDC.Adapters/DeviceController.cs b/Source/Libraries/openPDC.Adapters/DeviceController.cs new file mode 100644 index 0000000000..f337bb9c33 --- /dev/null +++ b/Source/Libraries/openPDC.Adapters/DeviceController.cs @@ -0,0 +1,230 @@ +using GSF.Data; +using GSF.Data.Model; +using GSF.Diagnostics; +using openPDC.Adapters.Constants; +using openPDC.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http; +using System.Web.Http.Description; + +namespace openPDC.Adapters +{ + /// + /// Controller for Device (PMU) operations in openPDC. Provides endpoints to query data from + /// devices registered in the system. + /// + public class DeviceController : ApiController + { + #region [ Members ] + + private static readonly LogPublisher Log = Logger.CreatePublisher(typeof(DeviceController), MessageClass.Application); + + #endregion [ Members ] + + #region [ Properties ] + + /// + /// Gets the DataContext for database operations. + /// + private static AdoDataConnection DataContext + { + get + { + return new AdoDataConnection(StringConstant.SystemSettings); + } + } + + #endregion [ Properties ] + + #region [ Methods ] + + /// + /// Gets all devices (PMUs) in the system. + /// + /// List of all registered devices. + /// Returns the list of devices + /// Internal error processing the request + [HttpGet] + [ResponseType(typeof(IEnumerable))] + public IHttpActionResult GetAllDevices() + { + try + { + Log.Publish(MessageLevel.Info, nameof(GetAllDevices), "Querying all devices"); + + using AdoDataConnection context = DataContext; + TableOperations deviceTable = new(context); + var devices = deviceTable.QueryRecords(StringConstant.Acronym); + + Log.Publish(MessageLevel.Info, nameof(GetAllDevices), $"Returned {devices.Count()} devices"); + return Ok(devices); + } + catch (Exception ex) + { + Log.Publish(MessageLevel.Error, nameof(GetAllDevices), "Error querying devices", exception: ex); + return InternalServerError(ex); + } + } + + /// + /// Gets a specific device by Acronym. + /// + /// Device (PMU) acronym. + /// Specified device. + /// Returns the device + /// Device not found + /// Internal error processing the request + [HttpGet] + [ResponseType(typeof(DeviceDetail))] + public IHttpActionResult GetDeviceByAcronym(string acronym) + { + try + { + Log.Publish(MessageLevel.Info, nameof(GetDeviceByAcronym), $"Querying device with acronym: {acronym}"); + + using AdoDataConnection context = DataContext; + TableOperations deviceTable = new(context); + RecordRestriction restriction = new("Acronym = {0}", acronym); + var device = deviceTable.QueryRecords(restriction: restriction).FirstOrDefault(); + + if (device == null) + { + Log.Publish(MessageLevel.Warning, nameof(GetDeviceByAcronym), $"Device not found: {acronym}"); + return NotFound(); + } + + Log.Publish(MessageLevel.Info, nameof(GetDeviceByAcronym), $"Device found: {acronym}"); + return Ok(device); + } + catch (Exception ex) + { + Log.Publish(MessageLevel.Error, nameof(GetDeviceByAcronym), $"Error querying device {acronym}", exception: ex); + return InternalServerError(ex); + } + } + + /// + /// Gets devices by company. + /// + /// Company acronym. + /// List of devices from the specified company. + /// Returns the list of devices + /// No devices found for the company + /// Internal error processing the request + [HttpGet] + [ResponseType(typeof(IEnumerable))] + public IHttpActionResult GetDevicesByCompany(string companyAcronym) + { + try + { + Log.Publish(MessageLevel.Info, nameof(GetDevicesByCompany), $"Querying devices for company: {companyAcronym}"); + + using AdoDataConnection context = DataContext; + TableOperations deviceTable = new(context); + RecordRestriction restriction = new("CompanyAcronym = {0}", companyAcronym); + var devices = deviceTable.QueryRecords(StringConstant.Acronym, restriction: restriction).ToList(); + + if (!devices.Any()) + { + Log.Publish(MessageLevel.Warning, nameof(GetDevicesByCompany), $"No devices found for company: {companyAcronym}"); + return NotFound(); + } + + Log.Publish(MessageLevel.Info, nameof(GetDevicesByCompany), $"Returned {devices.Count} devices from company {companyAcronym}"); + return Ok(devices); + } + catch (Exception ex) + { + Log.Publish(MessageLevel.Error, nameof(GetDevicesByCompany), $"Error querying devices for company {companyAcronym}", exception: ex); + return InternalServerError(ex); + } + } + + /// + /// Gets devices by protocol. + /// + /// Protocol name (e.g.: IeeeC37_118V1, SEL Fast Message). + /// List of devices using the specified protocol. + /// Returns the list of devices + /// No devices found for the protocol + /// Internal error processing the request + [HttpGet] + [ResponseType(typeof(IEnumerable))] + public IHttpActionResult GetDevicesByProtocol(string protocolName) + { + try + { + Log.Publish(MessageLevel.Info, nameof(GetDevicesByProtocol), $"Querying devices for protocol: {protocolName}"); + + using AdoDataConnection context = DataContext; + TableOperations deviceTable = new(context); + RecordRestriction restriction = new("ProtocolName = {0}", protocolName); + var devices = deviceTable.QueryRecords(StringConstant.Acronym, restriction: restriction).ToList(); + + if (!devices.Any()) + { + Log.Publish(MessageLevel.Warning, nameof(GetDevicesByProtocol), $"No devices found for protocol: {protocolName}"); + return NotFound(); + } + + Log.Publish(MessageLevel.Info, nameof(GetDevicesByProtocol), $"Returned {devices.Count} devices for protocol {protocolName}"); + return Ok(devices); + } + catch (Exception ex) + { + Log.Publish(MessageLevel.Error, nameof(GetDevicesByProtocol), $"Error querying devices for protocol {protocolName}", exception: ex); + return InternalServerError(ex); + } + } + + /// + /// Gets enabled or disabled devices. + /// + /// true for enabled, false for disabled. + /// List of devices filtered by status. + /// Returns the list of devices + /// No devices found with the specified status + /// Internal error processing the request + [HttpGet] + [ResponseType(typeof(IEnumerable))] + public IHttpActionResult GetDevicesByStatus(bool enabled) + { + try + { + string status = enabled ? "enabled" : "disabled"; + Log.Publish(MessageLevel.Info, nameof(GetDevicesByStatus), $"Querying {status} devices"); + + using AdoDataConnection context = DataContext; + TableOperations deviceTable = new(context); + RecordRestriction restriction = new("Enabled = {0}", enabled ? 1 : 0); + var devices = deviceTable.QueryRecords(StringConstant.Acronym, restriction: restriction).ToList(); + + if (!devices.Any()) + { + Log.Publish(MessageLevel.Warning, nameof(GetDevicesByStatus), $"No {status} devices found"); + return NotFound(); + } + + Log.Publish(MessageLevel.Info, nameof(GetDevicesByStatus), $"Returned {devices.Count} {status} devices"); + return Ok(devices); + } + catch (Exception ex) + { + Log.Publish(MessageLevel.Error, nameof(GetDevicesByStatus), $"Error querying devices by status", exception: ex); + return InternalServerError(ex); + } + } + + [HttpGet] + public HttpResponseMessage Index() + { + return new HttpResponseMessage(HttpStatusCode.OK); + } + + #endregion [ Methods ] + } +} \ No newline at end of file diff --git a/Source/Libraries/openPDC.Adapters/DevicePhasorController.cs b/Source/Libraries/openPDC.Adapters/DevicePhasorController.cs new file mode 100644 index 0000000000..f599207c91 --- /dev/null +++ b/Source/Libraries/openPDC.Adapters/DevicePhasorController.cs @@ -0,0 +1,377 @@ +using GSF.Data; +using GSF.Data.Model; +using GSF.Diagnostics; +using openPDC.Adapters.Constants; +using openPDC.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Web.Http; +using System.Web.Http.Description; + +namespace openPDC.Adapters +{ + /// + /// Controller for combined Device and Phasor operations. Provides endpoints to query devices + /// (PMUs) along with their phasors in a single request. + /// + public class DevicePhasorController : ApiController + { + #region [ Members ] + + private static readonly LogPublisher Log = Logger.CreatePublisher(typeof(DevicePhasorController), MessageClass.Application); + + #endregion [ Members ] + + #region [ Properties ] + + /// + /// Gets the DataContext for database operations. + /// + private static AdoDataConnection DataContext + { + get + { + return new AdoDataConnection(StringConstant.SystemSettings); + } + } + + #endregion [ Properties ] + + #region [ Methods ] + + /// + /// Gets all devices with their respective phasors. + /// + /// List of devices with their phasors. + /// Returns the list of devices with phasors + /// Internal error processing the request + [HttpGet] + [ResponseType(typeof(IEnumerable))] + public IHttpActionResult GetAllDevicesWithPhasors() + { + try + { + Log.Publish(MessageLevel.Info, nameof(GetAllDevicesWithPhasors), "Querying all devices with phasors"); + + using AdoDataConnection context = DataContext; + TableOperations deviceTable = new(context); + TableOperations phasorTable = new(context); + + var devices = deviceTable.QueryRecords(StringConstant.Acronym).ToList(); + var allPhasors = phasorTable.QueryRecords("DeviceID, SourceIndex").ToList(); + + var result = devices.Select(device => new DeviceWithPhasors + { + Device = device, + Phasors = [.. allPhasors.Where(p => p.DeviceAcronym == device.Acronym).OrderBy(p => p.SourceIndex)] + }).ToList(); + + Log.Publish(MessageLevel.Info, nameof(GetAllDevicesWithPhasors), $"Returned {result.Count} devices with phasors"); + return Ok(result); + } + catch (Exception ex) + { + Log.Publish(MessageLevel.Error, nameof(GetAllDevicesWithPhasors), "Error querying devices with phasors", exception: ex); + return InternalServerError(ex); + } + } + + /// + /// Gets all devices with their respective phasors in CSV format. + /// + /// List of devices with their phasors in CSV format. + /// Returns the list of devices with phasors in CSV format + /// Internal error processing the request + [HttpGet] + public HttpResponseMessage GetAllDevicesWithPhasorsAsCsv() + { + try + { + Log.Publish(MessageLevel.Info, nameof(GetAllDevicesWithPhasorsAsCsv), "Generating CSV with all devices and phasors"); + + using AdoDataConnection context = DataContext; + TableOperations deviceTable = new(context); + TableOperations phasorTable = new(context); + + var devices = deviceTable.QueryRecords(StringConstant.Acronym).ToList(); + var allPhasors = phasorTable.QueryRecords("DeviceID, SourceIndex").ToList(); + + var csv = new StringBuilder(); + + // Cabeçalho + csv.AppendLine("DeviceAcronym,DeviceName,CompanyAcronym,VendorAcronym,ProtocolName,IsConcentrator,FramesPerSecond,DeviceEnabled,Latitude,Longitude,PhasorID,PhasorLabel,PhasorType,PhasorPhase,SourceIndex,BaseKV"); + + // Dados + foreach (var device in devices) + { + var devicePhasors = allPhasors.Where(p => p.DeviceAcronym == device.Acronym).OrderBy(p => p.SourceIndex).ToList(); + + if (devicePhasors.Any()) + { + foreach (var phasor in devicePhasors) + { + csv.AppendLine($"{EscapeCsvField(device.Acronym)},{EscapeCsvField(device.Name)},{EscapeCsvField(device.CompanyAcronym)},{EscapeCsvField(device.VendorAcronym)},{EscapeCsvField(device.ProtocolName)},{EscapeCsvField(device.IsConcentrator.ToString())},{device.FramesPerSecond},{device.Enabled},{device.Latitude},{device.Longitude},{phasor.ID},{EscapeCsvField(phasor.Label)},{EscapeCsvField(phasor.Type)},{EscapeCsvField(phasor.Phase)},{phasor.SourceIndex},{phasor.BaseKV}"); + } + } + else + { + // Device without phasors - add line with device information only + csv.AppendLine($"{EscapeCsvField(device.Acronym)},{EscapeCsvField(device.Name)},{EscapeCsvField(device.CompanyAcronym)},{EscapeCsvField(device.VendorAcronym)},{EscapeCsvField(device.ProtocolName)},{EscapeCsvField(device.IsConcentrator.ToString())},{device.FramesPerSecond},{device.Enabled},{device.Latitude},{device.Longitude},,,,,0,0"); + } + } + + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(csv.ToString(), Encoding.UTF8, "text/csv"); + response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") + { + FileName = $"all_devices_phasors_{DateTime.UtcNow:yyyyMMdd_HHmmss}.csv" + }; + + Log.Publish(MessageLevel.Info, nameof(GetAllDevicesWithPhasorsAsCsv), $"CSV generated with {devices.Count} devices"); + return response; + } + catch (Exception ex) + { + Log.Publish(MessageLevel.Error, nameof(GetAllDevicesWithPhasorsAsCsv), "Error generating CSV", exception: ex); + return Request.CreateResponse(HttpStatusCode.InternalServerError, new { message = ex.Message }); + } + } + + /// + /// Gets devices from a company with their phasors. + /// + /// Company acronym. + /// List of devices from the company with their phasors. + /// Returns the list of devices with phasors + /// No devices found for the company + /// Internal error processing the request + [HttpGet] + [ResponseType(typeof(IEnumerable))] + public IHttpActionResult GetDevicesWithPhasorsByCompany(string companyAcronym) + { + try + { + Log.Publish(MessageLevel.Info, nameof(GetDevicesWithPhasorsByCompany), $"Querying devices with phasors for company: {companyAcronym}"); + + using AdoDataConnection context = DataContext; + TableOperations deviceTable = new(context); + TableOperations phasorTable = new(context); + + RecordRestriction deviceRestriction = new("CompanyAcronym = {0}", companyAcronym); + var devices = deviceTable.QueryRecords(StringConstant.Acronym, restriction: deviceRestriction).ToList(); + + if (!devices.Any()) + { + Log.Publish(MessageLevel.Warning, nameof(GetDevicesWithPhasorsByCompany), $"No devices found for company: {companyAcronym}"); + return NotFound(); + } + + var allPhasors = phasorTable.QueryRecords("DeviceID, SourceIndex").ToList(); + + var result = devices.Select(device => new DeviceWithPhasors + { + Device = device, + Phasors = [.. allPhasors.Where(p => p.DeviceAcronym == device.Acronym).OrderBy(p => p.SourceIndex)] + }).ToList(); + + Log.Publish(MessageLevel.Info, nameof(GetDevicesWithPhasorsByCompany), $"Returned {result.Count} devices from company {companyAcronym}"); + return Ok(result); + } + catch (Exception ex) + { + Log.Publish(MessageLevel.Error, nameof(GetDevicesWithPhasorsByCompany), $"Error querying devices for company {companyAcronym}", exception: ex); + return InternalServerError(ex); + } + } + + /// + /// Gets enabled devices with their phasors. + /// + /// true for enabled, false for disabled. + /// List of devices filtered by status with their phasors. + /// Returns the list of devices with phasors + /// No devices found with the specified status + /// Internal error processing the request + [HttpGet] + [ResponseType(typeof(IEnumerable))] + public IHttpActionResult GetDevicesWithPhasorsByStatus(bool enabled) + { + try + { + string status = enabled ? "enabled" : "disabled"; + Log.Publish(MessageLevel.Info, nameof(GetDevicesWithPhasorsByStatus), $"Querying {status} devices with phasors"); + + using AdoDataConnection context = DataContext; + TableOperations deviceTable = new(context); + TableOperations phasorTable = new(context); + + RecordRestriction deviceRestriction = new("Enabled = {0}", enabled ? 1 : 0); + var devices = deviceTable.QueryRecords(StringConstant.Acronym, restriction: deviceRestriction).ToList(); + + if (!devices.Any()) + { + Log.Publish(MessageLevel.Warning, nameof(GetDevicesWithPhasorsByStatus), $"No {status} devices found"); + return NotFound(); + } + + var allPhasors = phasorTable.QueryRecords("DeviceID, SourceIndex").ToList(); + + var result = devices.Select(device => new DeviceWithPhasors + { + Device = device, + Phasors = [.. allPhasors.Where(p => p.DeviceAcronym == device.Acronym).OrderBy(p => p.SourceIndex)] + }).ToList(); + + Log.Publish(MessageLevel.Info, nameof(GetDevicesWithPhasorsByStatus), $"Returned {result.Count} {status} devices"); + return Ok(result); + } + catch (Exception ex) + { + Log.Publish(MessageLevel.Error, nameof(GetDevicesWithPhasorsByStatus), $"Error querying devices by status", exception: ex); + return InternalServerError(ex); + } + } + + /// + /// Gets a specific device with its phasors by Acronym. + /// + /// Device (PMU) acronym. + /// Device with its phasors. + /// Returns the device with its phasors + /// Device not found + /// Internal error processing the request + [HttpGet] + [ResponseType(typeof(DeviceWithPhasors))] + public IHttpActionResult GetDeviceWithPhasorsByAcronym(string acronym) + { + try + { + Log.Publish(MessageLevel.Info, nameof(GetDeviceWithPhasorsByAcronym), $"Querying device {acronym} with phasors"); + + using AdoDataConnection context = DataContext; + TableOperations deviceTable = new(context); + TableOperations phasorTable = new(context); + + RecordRestriction deviceRestriction = new("Acronym = {0}", acronym); + var device = deviceTable.QueryRecords(restriction: deviceRestriction).FirstOrDefault(); + + if (device == null) + { + Log.Publish(MessageLevel.Warning, nameof(GetDeviceWithPhasorsByAcronym), $"Device not found: {acronym}"); + return NotFound(); + } + + RecordRestriction phasorRestriction = new("DeviceAcronym = {0}", acronym); + var phasors = phasorTable.QueryRecords(StringConstant.SourceIndex, phasorRestriction).ToList(); + + var result = new DeviceWithPhasors + { + Device = device, + Phasors = phasors + }; + + Log.Publish(MessageLevel.Info, nameof(GetDeviceWithPhasorsByAcronym), $"Returned device {acronym} with {phasors.Count} phasors"); + + return Ok(result); + } + catch (Exception ex) + { + Log.Publish(MessageLevel.Error, nameof(GetDeviceWithPhasorsByAcronym), $"Error querying device {acronym}", exception: ex); + return InternalServerError(ex); + } + } + + /// + /// Gets a specific device with its phasors by Acronym in CSV format. + /// + /// Device (PMU) acronym. + /// Device with its phasors in CSV format. + /// Returns the device with its phasors in CSV format + /// Device not found + /// Internal error processing the request + [HttpGet] + public HttpResponseMessage GetDeviceWithPhasorsByAcronymAsCsv(string acronym) + { + try + { + Log.Publish(MessageLevel.Info, nameof(GetDeviceWithPhasorsByAcronymAsCsv), $"Generating CSV for device {acronym} with phasors"); + + using AdoDataConnection context = DataContext; + TableOperations deviceTable = new(context); + TableOperations phasorTable = new(context); + + RecordRestriction deviceRestriction = new("Acronym = {0}", acronym); + var device = deviceTable.QueryRecords(restriction: deviceRestriction).FirstOrDefault(); + + if (device == null) + { + Log.Publish(MessageLevel.Warning, nameof(GetDeviceWithPhasorsByAcronymAsCsv), $"Device not found: {acronym}"); + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + RecordRestriction phasorRestriction = new("DeviceAcronym = {0}", acronym); + var phasors = phasorTable.QueryRecords(StringConstant.SourceIndex, true, int.MaxValue, 0, phasorRestriction).ToList(); + + var csv = new StringBuilder(); + + // Cabeçalho do Device + csv.AppendLine("# Device Information"); + csv.AppendLine("Acronym,Name,CompanyAcronym,VendorAcronym,ProtocolName,FramesPerSecond,Enabled,Latitude,Longitude"); + csv.AppendLine($"{EscapeCsvField(device.Acronym)},{EscapeCsvField(device.Name)},{EscapeCsvField(device.CompanyAcronym)},{EscapeCsvField(device.VendorAcronym)},{EscapeCsvField(device.ProtocolName)},{device.FramesPerSecond},{device.Enabled},{device.Latitude},{device.Longitude}"); + + // Linha em branco + csv.AppendLine(); + + // Cabeçalho dos Phasors + csv.AppendLine("# Phasors"); + csv.AppendLine("ID,DeviceAcronym,Label,Type,Phase,SourceIndex,BaseKV"); + + // Dados dos Phasors + foreach (var phasor in phasors) + { + csv.AppendLine($"{phasor.ID},{EscapeCsvField(phasor.DeviceAcronym)},{EscapeCsvField(phasor.Label)},{EscapeCsvField(phasor.Type)},{EscapeCsvField(phasor.Phase)},{phasor.SourceIndex},{phasor.BaseKV}"); + } + + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(csv.ToString(), Encoding.UTF8, "text/csv"); + response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") + { + FileName = $"device_{acronym}_phasors.csv" + }; + + Log.Publish(MessageLevel.Info, nameof(GetDeviceWithPhasorsByAcronymAsCsv), $"CSV generated for device {acronym} with {phasors.Count} phasors"); + return response; + } + catch (Exception ex) + { + Log.Publish(MessageLevel.Error, nameof(GetDeviceWithPhasorsByAcronymAsCsv), $"Error generating CSV for device {acronym}", exception: ex); + return Request.CreateResponse(HttpStatusCode.InternalServerError, new { message = ex.Message }); + } + } + + /// + /// Escapes CSV fields to handle commas, quotes and line breaks. + /// + /// Field to be escaped. + /// Escaped field. + private static string EscapeCsvField(string field) + { + if (string.IsNullOrEmpty(field)) + return string.Empty; + + if (field.Contains(",") || field.Contains("\"") || field.Contains("\n") || field.Contains("\r")) + { + return $"\"{field.Replace("\"", "\"\"")}\""; + } + + return field; + } + + #endregion [ Methods ] + } +} \ No newline at end of file diff --git a/Source/Libraries/openPDC.Adapters/PhasorController.cs b/Source/Libraries/openPDC.Adapters/PhasorController.cs new file mode 100644 index 0000000000..b81602f77d --- /dev/null +++ b/Source/Libraries/openPDC.Adapters/PhasorController.cs @@ -0,0 +1,147 @@ +using GSF.Data; +using GSF.Data.Model; +using GSF.Diagnostics; +using openPDC.Adapters.Constants; +using openPDC.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Http; +using System.Web.Http.Description; + +namespace openPDC.Adapters +{ + /// + /// Controller for Phasor operations in openPDC. Provides endpoints to query phasor data + /// from PMUs. + /// + public class PhasorController : ApiController + { + #region [ Members ] + + private static readonly LogPublisher Log = Logger.CreatePublisher(typeof(PhasorController), MessageClass.Application); + + #endregion [ Members ] + + #region [ Properties ] + + /// + /// Gets the DataContext for database operations. + /// + private static AdoDataConnection DataContext + { + get + { + return new AdoDataConnection(StringConstant.SystemSettings); + } + } + + #endregion [ Properties ] + + #region [ Methods ] + + /// + /// Gets all phasors in the system. + /// + /// List of all registered phasors. + /// Returns the list of phasors + /// Internal error processing the request + [HttpGet] + [ResponseType(typeof(IEnumerable))] + public IHttpActionResult GetAllPhasors() + { + try + { + Log.Publish(MessageLevel.Info, nameof(GetAllPhasors), "Querying all phasors"); + + using AdoDataConnection context = DataContext; + TableOperations phasorTable = new(context); + var phasors = phasorTable.QueryRecords(StringConstant.DeviceID); + + Log.Publish(MessageLevel.Info, nameof(GetAllPhasors), $"Returned {phasors.Count()} phasors"); + return Ok(phasors); + } + catch (Exception ex) + { + Log.Publish(MessageLevel.Error, nameof(GetAllPhasors), "Error querying phasors", exception: ex); + return InternalServerError(ex); + } + } + + /// + /// Gets the phasors of a specific device by ID. + /// + /// Device (PMU) ID. + /// List of phasors from the specified device. + /// Returns the list of device phasors + /// Device not found or has no phasors + /// Internal error processing the request + [HttpGet] + [ResponseType(typeof(IEnumerable))] + public IHttpActionResult GetPhasorsByDevice(int deviceId) + { + try + { + Log.Publish(MessageLevel.Info, nameof(GetPhasorsByDevice), $"Querying phasors for device ID: {deviceId}"); + + using AdoDataConnection context = DataContext; + TableOperations phasorTable = new(context); + RecordRestriction restriction = new("DeviceID = {0}", deviceId); + var phasors = phasorTable.QueryRecords(StringConstant.SourceIndex, restriction).ToList(); + + if (!phasors.Any()) + { + Log.Publish(MessageLevel.Warning, nameof(GetPhasorsByDevice), $"No phasors found for device ID: {deviceId}"); + return NotFound(); + } + + Log.Publish(MessageLevel.Info, nameof(GetPhasorsByDevice), $"Returned {phasors.Count} phasors for device ID {deviceId}"); + return Ok(phasors); + } + catch (Exception ex) + { + Log.Publish(MessageLevel.Error, nameof(GetPhasorsByDevice), $"Error querying phasors for device ID {deviceId}", exception: ex); + return InternalServerError(ex); + } + } + + /// + /// Gets the phasors of a specific device by Acronym. + /// + /// Device (PMU) acronym. + /// List of phasors from the specified device. + /// Returns the list of device phasors + /// Device not found or has no phasors + /// Internal error processing the request + [HttpGet] + [ResponseType(typeof(IEnumerable))] + public IHttpActionResult GetPhasorsByDeviceAcronym(string deviceAcronym) + { + try + { + Log.Publish(MessageLevel.Info, nameof(GetPhasorsByDeviceAcronym), $"Querying phasors for device: {deviceAcronym}"); + + using AdoDataConnection context = DataContext; + TableOperations phasorTable = new(context); + RecordRestriction restriction = new("DeviceAcronym = {0}", deviceAcronym); + var phasors = phasorTable.QueryRecords(StringConstant.SourceIndex, restriction).ToList(); + + if (!phasors.Any()) + { + Log.Publish(MessageLevel.Warning, nameof(GetPhasorsByDeviceAcronym), $"No phasors found for device: {deviceAcronym}"); + return NotFound(); + } + + Log.Publish(MessageLevel.Info, nameof(GetPhasorsByDeviceAcronym), $"Returned {phasors.Count} phasors for device {deviceAcronym}"); + return Ok(phasors); + } + catch (Exception ex) + { + Log.Publish(MessageLevel.Error, nameof(GetPhasorsByDeviceAcronym), $"Error querying phasors for device {deviceAcronym}", exception: ex); + return InternalServerError(ex); + } + } + + #endregion [ Methods ] + } +} \ No newline at end of file diff --git a/Source/Libraries/openPDC.Adapters/openPDC.Adapters.csproj b/Source/Libraries/openPDC.Adapters/openPDC.Adapters.csproj index aad33dee05..07c8a20b74 100644 --- a/Source/Libraries/openPDC.Adapters/openPDC.Adapters.csproj +++ b/Source/Libraries/openPDC.Adapters/openPDC.Adapters.csproj @@ -96,7 +96,11 @@ + + + + diff --git a/Source/Libraries/openPDC.Model/DeviceWithPhasors.cs b/Source/Libraries/openPDC.Model/DeviceWithPhasors.cs new file mode 100644 index 0000000000..b08019bdb4 --- /dev/null +++ b/Source/Libraries/openPDC.Model/DeviceWithPhasors.cs @@ -0,0 +1,31 @@ +// ReSharper disable CheckNamespace +#pragma warning disable 1591 + +using System.Collections.Generic; + +namespace openPDC.Model +{ + /// + /// DTO that represents a Device with its associated Phasors. + /// + public class DeviceWithPhasors + { + /// + /// Default constructor. + /// + public DeviceWithPhasors() + { + Phasors = new List(); + } + + /// + /// Device Information(PMU). + /// + public DeviceDetail Device { get; set; } + + /// + /// List of Phasors associated with the Device. + /// + public List Phasors { get; set; } + } +} \ No newline at end of file diff --git a/Source/Libraries/openPDC.Model/openPDC.Model.csproj b/Source/Libraries/openPDC.Model/openPDC.Model.csproj index db5f656d80..8f432cf74c 100644 --- a/Source/Libraries/openPDC.Model/openPDC.Model.csproj +++ b/Source/Libraries/openPDC.Model/openPDC.Model.csproj @@ -53,6 +53,7 @@ +