Included heart-bit functionality, replaced static variable assignments with enums, implemented monitoring functionality for the backend to check for disconnected installations
This commit is contained in:
parent
6301cf38a0
commit
bdad83995d
|
@ -98,6 +98,8 @@ public class Controller : ControllerBase
|
||||||
|
|
||||||
return Db.Errors
|
return Db.Errors
|
||||||
.Where(error => error.InstallationId == id)
|
.Where(error => error.InstallationId == id)
|
||||||
|
.OrderByDescending(error => error.Date)
|
||||||
|
.ThenByDescending(error => error.Time)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,6 +117,8 @@ public class Controller : ControllerBase
|
||||||
|
|
||||||
return Db.Warnings
|
return Db.Warnings
|
||||||
.Where(error => error.InstallationId == id)
|
.Where(error => error.InstallationId == id)
|
||||||
|
.OrderByDescending(error => error.Date)
|
||||||
|
.ThenByDescending(error => error.Time)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ public static class Program
|
||||||
WebsocketManager.StartRabbitMqConsumer();
|
WebsocketManager.StartRabbitMqConsumer();
|
||||||
Console.WriteLine("Queue declared");
|
Console.WriteLine("Queue declared");
|
||||||
WebsocketManager.InformInstallationsToSubscribeToRabbitMq();
|
WebsocketManager.InformInstallationsToSubscribeToRabbitMq();
|
||||||
|
WebsocketManager.MonitorInstallationTable();
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddProblemDetails(setup =>
|
builder.Services.AddProblemDetails(setup =>
|
||||||
|
@ -95,11 +96,3 @@ public static class Program
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// var x = new CorsPolicy
|
|
||||||
// {
|
|
||||||
// Headers = { "*" },
|
|
||||||
// Origins = { "*" },
|
|
||||||
// Methods = { "*" }
|
|
||||||
// };
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.Backend.Websockets;
|
||||||
|
|
||||||
|
public class AlarmOrWarning
|
||||||
|
{
|
||||||
|
public required String Date { get; set; }
|
||||||
|
public required String Time { get; set; }
|
||||||
|
public required String Description { get; set; }
|
||||||
|
public required String CreatedBy { get; set; }
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
|
namespace InnovEnergy.App.Backend.Websockets;
|
||||||
|
|
||||||
public class InstallationInfo
|
public class InstallationInfo
|
||||||
{
|
{
|
||||||
public int Status { get; set; }
|
public int Status { get; set; }
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
public List<WebSocket> Connections { get; } = new List<WebSocket>();
|
public List<WebSocket> Connections { get; } = new List<WebSocket>();
|
||||||
}
|
}
|
|
@ -1,10 +1,17 @@
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.Backend.Websockets;
|
||||||
|
|
||||||
public class StatusMessage
|
public class StatusMessage
|
||||||
{
|
{
|
||||||
public required int InstallationId { get; init; }
|
public required Int32 InstallationId { get; set; }
|
||||||
public required int Status { get; init; }
|
public required int Status { get; set; }
|
||||||
public String Date { get; set; } = null!;
|
public required MessageType Type { get; set; }
|
||||||
public String Time { get; set; } = null!;
|
public List<AlarmOrWarning>? Warnings { get; set; }
|
||||||
public String Description { get; init; } = null!;
|
public List<AlarmOrWarning>? Alarms { get; set; }
|
||||||
public String CreatedBy { get; init; } = null!;
|
}
|
||||||
|
|
||||||
|
public enum MessageType
|
||||||
|
{
|
||||||
|
AlarmOrWarning,
|
||||||
|
Heartbit
|
||||||
}
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
|
@ -24,7 +23,6 @@ public static class WebsocketManager
|
||||||
Console.WriteLine("Count is "+installationIps.Count);
|
Console.WriteLine("Count is "+installationIps.Count);
|
||||||
var maxRetransmissions = 2;
|
var maxRetransmissions = 2;
|
||||||
|
|
||||||
|
|
||||||
UdpClient udpClient = new UdpClient();
|
UdpClient udpClient = new UdpClient();
|
||||||
udpClient.Client.ReceiveTimeout = 2000;
|
udpClient.Client.ReceiveTimeout = 2000;
|
||||||
int port = 9000;
|
int port = 9000;
|
||||||
|
@ -55,14 +53,8 @@ public static class WebsocketManager
|
||||||
}
|
}
|
||||||
catch (SocketException ex)
|
catch (SocketException ex)
|
||||||
{
|
{
|
||||||
if (ex.SocketErrorCode == SocketError.TimedOut)
|
if (ex.SocketErrorCode == SocketError.TimedOut){Console.WriteLine("Timed out waiting for a response. Retry...");}
|
||||||
{
|
else{Console.WriteLine("Error: " + ex.Message);}
|
||||||
Console.WriteLine("Timed out waiting for a response. Retry...");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine("Error: " + ex.Message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,6 +64,23 @@ public static class WebsocketManager
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Every 1 minute, check the timestamp of the latest received message for every installation.
|
||||||
|
//If the difference between the two timestamps is more than one minute, we consider this installation unavailable.
|
||||||
|
public static async Task MonitorInstallationTable()
|
||||||
|
{
|
||||||
|
while (true){
|
||||||
|
lock (InstallationConnections){
|
||||||
|
foreach (var installationConnection in InstallationConnections){
|
||||||
|
if ((DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(1)){
|
||||||
|
installationConnection.Value.Status = -1;
|
||||||
|
if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task StartRabbitMqConsumer()
|
public static async Task StartRabbitMqConsumer()
|
||||||
{
|
{
|
||||||
var consumer = new EventingBasicConsumer(Channel);
|
var consumer = new EventingBasicConsumer(Channel);
|
||||||
|
@ -91,73 +100,97 @@ public static class WebsocketManager
|
||||||
Console.WriteLine("Update installation connection table");
|
Console.WriteLine("Update installation connection table");
|
||||||
var installationId = receivedStatusMessage.InstallationId;
|
var installationId = receivedStatusMessage.InstallationId;
|
||||||
|
|
||||||
//This is an error message
|
//This is a heartbit message, just update the timestamp for this installation.
|
||||||
if (receivedStatusMessage.Status==2)
|
//There is no need to notify the corresponding front-ends.
|
||||||
|
if (receivedStatusMessage.Type == MessageType.Heartbit)
|
||||||
{
|
{
|
||||||
Console.WriteLine("-----------------------New error-----------------------");
|
InstallationConnections[installationId].Timestamp = DateTime.Now;
|
||||||
|
|
||||||
Error newError = new Error
|
|
||||||
{
|
|
||||||
InstallationId = receivedStatusMessage.InstallationId,
|
|
||||||
Description = receivedStatusMessage.Description,
|
|
||||||
Date = receivedStatusMessage.Date,
|
|
||||||
Time = receivedStatusMessage.Time,
|
|
||||||
DeviceCreatedTheMessage = receivedStatusMessage.CreatedBy,
|
|
||||||
Seen = false
|
|
||||||
};
|
|
||||||
//Create a new error and add it to the database
|
|
||||||
Db.HandleError(newError,receivedStatusMessage.InstallationId);
|
|
||||||
}
|
}
|
||||||
else if (receivedStatusMessage.Status==1)
|
else
|
||||||
|
{
|
||||||
|
//Traverse the Warnings list, and store each of them to the database
|
||||||
|
if (receivedStatusMessage.Warnings != null)
|
||||||
|
{
|
||||||
|
foreach (var warning in receivedStatusMessage.Warnings)
|
||||||
{
|
{
|
||||||
Console.WriteLine("-----------------------New warning-----------------------");
|
|
||||||
|
|
||||||
Warning newWarning = new Warning
|
Warning newWarning = new Warning
|
||||||
{
|
{
|
||||||
InstallationId = receivedStatusMessage.InstallationId,
|
InstallationId = receivedStatusMessage.InstallationId,
|
||||||
Description = receivedStatusMessage.Description,
|
Description = warning.Description,
|
||||||
Date = receivedStatusMessage.Date,
|
Date = warning.Date,
|
||||||
Time = receivedStatusMessage.Time,
|
Time = warning.Time,
|
||||||
DeviceCreatedTheMessage = receivedStatusMessage.CreatedBy,
|
DeviceCreatedTheMessage = warning.CreatedBy,
|
||||||
|
Seen = false
|
||||||
|
};
|
||||||
|
//Create a new warning and add it to the database
|
||||||
|
Db.HandleWarning(newWarning, receivedStatusMessage.InstallationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Traverse the Alarm list, and store each of them to the database
|
||||||
|
if (receivedStatusMessage.Alarms != null)
|
||||||
|
{
|
||||||
|
foreach (var alarm in receivedStatusMessage.Alarms)
|
||||||
|
{
|
||||||
|
Error newError = new Error
|
||||||
|
{
|
||||||
|
InstallationId = receivedStatusMessage.InstallationId,
|
||||||
|
Description = alarm.Description,
|
||||||
|
Date = alarm.Date,
|
||||||
|
Time = alarm.Time,
|
||||||
|
DeviceCreatedTheMessage = alarm.CreatedBy,
|
||||||
Seen = false
|
Seen = false
|
||||||
};
|
};
|
||||||
//Create a new error and add it to the database
|
//Create a new error and add it to the database
|
||||||
Db.HandleWarning(newWarning,receivedStatusMessage.InstallationId);
|
Db.HandleError(newError, receivedStatusMessage.InstallationId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//This installation id does not exist in our data structure, add it.
|
||||||
if (!InstallationConnections.ContainsKey(installationId))
|
if (!InstallationConnections.ContainsKey(installationId))
|
||||||
{
|
{
|
||||||
Console.WriteLine("Create new empty list for installation: " + installationId);
|
Console.WriteLine("Create new empty list for installation: " + installationId);
|
||||||
InstallationConnections[installationId] = new InstallationInfo
|
InstallationConnections[installationId] = new InstallationInfo
|
||||||
{
|
{
|
||||||
Status = receivedStatusMessage.Status
|
Status = receivedStatusMessage.Status,
|
||||||
|
Timestamp = DateTime.Now
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
InstallationConnections[installationId].Status = receivedStatusMessage.Status;
|
InstallationConnections[installationId].Status = receivedStatusMessage.Status;
|
||||||
|
InstallationConnections[installationId].Timestamp = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine("----------------------------------------------");
|
Console.WriteLine("----------------------------------------------");
|
||||||
|
//Update all the connected front-ends regarding this installation
|
||||||
|
if(InstallationConnections[installationId].Connections.Count > 0)
|
||||||
|
{
|
||||||
|
InformWebsocketsForInstallation(installationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Channel.BasicConsume(queue: "statusQueue", autoAck: true, consumer: consumer);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var installationConnection in InstallationConnections)
|
//Inform all the connected websockets regarding installation "installationId"
|
||||||
{
|
public static void InformWebsocketsForInstallation(int installationId)
|
||||||
if (installationConnection.Key == installationId && installationConnection.Value.Connections.Count > 0)
|
|
||||||
{
|
{
|
||||||
|
var installationConnection = InstallationConnections[installationId];
|
||||||
Console.WriteLine("Update all the connected websockets for installation " + installationId);
|
Console.WriteLine("Update all the connected websockets for installation " + installationId);
|
||||||
installationConnection.Value.Status = receivedStatusMessage.Status;
|
|
||||||
|
|
||||||
var jsonObject = new
|
var jsonObject = new
|
||||||
{
|
{
|
||||||
id = installationId,
|
id = installationId,
|
||||||
status = receivedStatusMessage.Status
|
status = installationConnection.Status
|
||||||
};
|
};
|
||||||
|
|
||||||
string jsonString = JsonSerializer.Serialize(jsonObject);
|
string jsonString = JsonSerializer.Serialize(jsonObject);
|
||||||
byte[] dataToSend = Encoding.UTF8.GetBytes(jsonString);
|
byte[] dataToSend = Encoding.UTF8.GetBytes(jsonString);
|
||||||
|
|
||||||
foreach (var connection in installationConnection.Value.Connections)
|
foreach (var connection in installationConnection.Connections)
|
||||||
{
|
{
|
||||||
connection.SendAsync(
|
connection.SendAsync(
|
||||||
new ArraySegment<byte>(dataToSend, 0, dataToSend.Length),
|
new ArraySegment<byte>(dataToSend, 0, dataToSend.Length),
|
||||||
|
@ -167,13 +200,6 @@ public static class WebsocketManager
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Channel.BasicConsume(queue: "statusQueue", autoAck: true, consumer: consumer);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static async Task HandleWebSocketConnection(WebSocket currentWebSocket)
|
public static async Task HandleWebSocketConnection(WebSocket currentWebSocket)
|
||||||
{
|
{
|
||||||
|
@ -217,7 +243,6 @@ public static class WebsocketManager
|
||||||
//Each front-end will send the list of the installations it wants to access
|
//Each front-end will send the list of the installations it wants to access
|
||||||
//If this is a new key (installation id), initialize the list for this key and then add the websocket object for this client
|
//If this is a new key (installation id), initialize the list for this key and then add the websocket object for this client
|
||||||
//Then, report the status of each requested installation to the front-end that created the websocket connection
|
//Then, report the status of each requested installation to the front-end that created the websocket connection
|
||||||
if (installationIds != null)
|
|
||||||
foreach (var installationId in installationIds)
|
foreach (var installationId in installationIds)
|
||||||
{
|
{
|
||||||
if (!InstallationConnections.ContainsKey(installationId))
|
if (!InstallationConnections.ContainsKey(installationId))
|
||||||
|
|
|
@ -1,10 +1,19 @@
|
||||||
|
|
||||||
|
using InnovEnergy.App.SaliMax.Ess;
|
||||||
|
|
||||||
namespace InnovEnergy.App.SaliMax.MiddlewareClasses;
|
namespace InnovEnergy.App.SaliMax.MiddlewareClasses;
|
||||||
|
|
||||||
public class StatusMessage
|
public class StatusMessage
|
||||||
{
|
{
|
||||||
public required Int32 InstallationId { get; set; }
|
public required Int32 InstallationId { get; set; }
|
||||||
public required Int32 Status { get; set; }
|
public required SalimaxAlarmState Status { get; set; }
|
||||||
|
public required MessageType Type { get; set; }
|
||||||
public List<AlarmOrWarning>? Warnings { get; set; }
|
public List<AlarmOrWarning>? Warnings { get; set; }
|
||||||
public List<AlarmOrWarning>? Alarms { get; set; }
|
public List<AlarmOrWarning>? Alarms { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum MessageType
|
||||||
|
{
|
||||||
|
AlarmOrWarning,
|
||||||
|
Heartbit
|
||||||
|
}
|
|
@ -59,7 +59,8 @@ internal static class Program
|
||||||
private static IModel? _channel;
|
private static IModel? _channel;
|
||||||
private static Boolean _subscribedToQueue = false;
|
private static Boolean _subscribedToQueue = false;
|
||||||
private static Boolean _subscribeToQueueForTheFirstTime = false;
|
private static Boolean _subscribeToQueueForTheFirstTime = false;
|
||||||
private static Int32 _prevSalimaxState = 0; // 0 is a green status
|
private static SalimaxAlarmState _prevSalimaxState = SalimaxAlarmState.Green;
|
||||||
|
private static Int32 _heartBitInterval = 0;
|
||||||
|
|
||||||
static Program()
|
static Program()
|
||||||
{
|
{
|
||||||
|
@ -260,6 +261,7 @@ internal static class Program
|
||||||
private static void SendSalimaxStateAlarm(StatusMessage currentSalimaxState)
|
private static void SendSalimaxStateAlarm(StatusMessage currentSalimaxState)
|
||||||
{
|
{
|
||||||
var s3Bucket = Config.Load().S3?.Bucket;
|
var s3Bucket = Config.Load().S3?.Bucket;
|
||||||
|
_heartBitInterval++;
|
||||||
|
|
||||||
//When the controller boots, it tries to subscribe to the queue
|
//When the controller boots, it tries to subscribe to the queue
|
||||||
if (_subscribeToQueueForTheFirstTime==false)
|
if (_subscribeToQueueForTheFirstTime==false)
|
||||||
|
@ -279,6 +281,14 @@ internal static class Program
|
||||||
if (s3Bucket != null)
|
if (s3Bucket != null)
|
||||||
InformMiddleware(s3Bucket, currentSalimaxState);
|
InformMiddleware(s3Bucket, currentSalimaxState);
|
||||||
}
|
}
|
||||||
|
else if (_subscribedToQueue && _heartBitInterval==15)
|
||||||
|
{
|
||||||
|
_heartBitInterval = 0;
|
||||||
|
currentSalimaxState.Type = MessageType.Heartbit;
|
||||||
|
|
||||||
|
if (s3Bucket != null)
|
||||||
|
InformMiddleware(s3Bucket, currentSalimaxState);
|
||||||
|
}
|
||||||
|
|
||||||
//If there is an available message from the RabbitMQ Broker, subscribe to the queue
|
//If there is an available message from the RabbitMQ Broker, subscribe to the queue
|
||||||
if (_udpListener.Available > 0)
|
if (_udpListener.Available > 0)
|
||||||
|
@ -417,7 +427,7 @@ internal static class Program
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var warning in record.AcDc.Alarms)
|
foreach (var warning in record.AcDc.Warnings)
|
||||||
{
|
{
|
||||||
warningList.Add(new AlarmOrWarning
|
warningList.Add(new AlarmOrWarning
|
||||||
{
|
{
|
||||||
|
@ -428,7 +438,7 @@ internal static class Program
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var warning in record.DcDc.Alarms)
|
foreach (var warning in record.DcDc.Warnings)
|
||||||
{
|
{
|
||||||
warningList.Add(new AlarmOrWarning
|
warningList.Add(new AlarmOrWarning
|
||||||
{
|
{
|
||||||
|
@ -441,7 +451,7 @@ internal static class Program
|
||||||
|
|
||||||
if (record.Battery != null)
|
if (record.Battery != null)
|
||||||
{
|
{
|
||||||
foreach (var warning in record.Battery.Alarms)
|
foreach (var warning in record.Battery.Warnings)
|
||||||
{
|
{
|
||||||
warningList.Add(new AlarmOrWarning
|
warningList.Add(new AlarmOrWarning
|
||||||
{
|
{
|
||||||
|
@ -473,7 +483,8 @@ internal static class Program
|
||||||
var returnedStatus = new StatusMessage
|
var returnedStatus = new StatusMessage
|
||||||
{
|
{
|
||||||
InstallationId = installationId,
|
InstallationId = installationId,
|
||||||
Status = (Int16)salimaxAlarmsState,
|
Status = salimaxAlarmsState,
|
||||||
|
Type= MessageType.AlarmOrWarning,
|
||||||
Alarms = alarmList,
|
Alarms = alarmList,
|
||||||
Warnings = warningList
|
Warnings = warningList
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,8 +19,8 @@ public class AcDcDevicesRecord
|
||||||
public SystemControlRegisters? SystemControl { get; }
|
public SystemControlRegisters? SystemControl { get; }
|
||||||
public IReadOnlyList<AcDcRecord> Devices { get; init; }
|
public IReadOnlyList<AcDcRecord> Devices { get; init; }
|
||||||
|
|
||||||
//public IEnumerable<AlarmMessage> Alarms => new []{AlarmMessage.BatteryOvervoltage}; //Devices.SelectMany(d => d.Status.Alarms).Distinct();
|
//public IEnumerable<AlarmMessage> Alarms => new []{AlarmMessage.OvercurrentBalancer}; //Devices.SelectMany(d => d.Status.Alarms).Distinct();
|
||||||
//public IEnumerable<WarningMessage> Warnings => new []{WarningMessage.TempDerating}; //Devices.SelectMany(d => d.Status.Warnings).Distinct();
|
//public IEnumerable<WarningMessage> Warnings => new []{WarningMessage.DcOffset}; //Devices.SelectMany(d => d.Status.Warnings).Distinct();
|
||||||
|
|
||||||
public IEnumerable<AlarmMessage> Alarms => Devices.SelectMany(d => d.Status.Alarms).Distinct();
|
public IEnumerable<AlarmMessage> Alarms => Devices.SelectMany(d => d.Status.Alarms).Distinct();
|
||||||
public IEnumerable<WarningMessage> Warnings => Devices.SelectMany(d => d.Status.Warnings).Distinct();
|
public IEnumerable<WarningMessage> Warnings => Devices.SelectMany(d => d.Status.Warnings).Distinct();
|
||||||
|
|
Loading…
Reference in New Issue