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 @@
+