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:
Noe 2023-11-22 16:49:47 +01:00
parent 6301cf38a0
commit bdad83995d
10 changed files with 196 additions and 134 deletions

View File

@ -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();
} }

View File

@ -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 =>
@ -94,12 +95,4 @@ public static class Program
Version = "v1" Version = "v1"
}; };
} }
// var x = new CorsPolicy
// {
// Headers = { "*" },
// Origins = { "*" },
// Methods = { "*" }
// };

View File

@ -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; }
}

View File

@ -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>();
} }

View File

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

View File

@ -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;
@ -23,7 +22,6 @@ public static class WebsocketManager
var installationIps = Db.Installations.Select(inst => inst.VpnIp).ToList(); var installationIps = Db.Installations.Select(inst => inst.VpnIp).ToList();
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;
@ -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);
@ -90,90 +99,107 @@ public static class WebsocketManager
Console.WriteLine("----------------------------------------------"); Console.WriteLine("----------------------------------------------");
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
if (receivedStatusMessage.Status==2)
{
Console.WriteLine("-----------------------New error-----------------------");
Error newError = new Error //This is a heartbit message, just update the timestamp for this installation.
{ //There is no need to notify the corresponding front-ends.
InstallationId = receivedStatusMessage.InstallationId, if (receivedStatusMessage.Type == MessageType.Heartbit)
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)
{ {
Console.WriteLine("-----------------------New warning-----------------------"); InstallationConnections[installationId].Timestamp = DateTime.Now;
Warning newWarning = new Warning
{
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.HandleWarning(newWarning,receivedStatusMessage.InstallationId);
}
if (!InstallationConnections.ContainsKey(installationId))
{
Console.WriteLine("Create new empty list for installation: " + installationId);
InstallationConnections[installationId] = new InstallationInfo
{
Status = receivedStatusMessage.Status
};
} }
else else
{ {
InstallationConnections[installationId].Status = receivedStatusMessage.Status; //Traverse the Warnings list, and store each of them to the database
} if (receivedStatusMessage.Warnings != null)
Console.WriteLine("----------------------------------------------");
foreach (var installationConnection in InstallationConnections)
{
if (installationConnection.Key == installationId && installationConnection.Value.Connections.Count > 0)
{ {
Console.WriteLine("Update all the connected websockets for installation " + installationId); foreach (var warning in receivedStatusMessage.Warnings)
installationConnection.Value.Status = receivedStatusMessage.Status;
var jsonObject = new
{ {
id = installationId, Warning newWarning = new Warning
status = receivedStatusMessage.Status {
}; InstallationId = receivedStatusMessage.InstallationId,
Description = warning.Description,
string jsonString = JsonSerializer.Serialize(jsonObject); Date = warning.Date,
byte[] dataToSend = Encoding.UTF8.GetBytes(jsonString); Time = warning.Time,
DeviceCreatedTheMessage = warning.CreatedBy,
foreach (var connection in installationConnection.Value.Connections) Seen = false
{ };
connection.SendAsync( //Create a new warning and add it to the database
new ArraySegment<byte>(dataToSend, 0, dataToSend.Length), Db.HandleWarning(newWarning, receivedStatusMessage.InstallationId);
WebSocketMessageType.Text,
true, // Indicates that this is the end of the message
CancellationToken.None
);
} }
} }
//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
};
//Create a new error and add it to the database
Db.HandleError(newError, receivedStatusMessage.InstallationId);
}
}
//This installation id does not exist in our data structure, add it.
if (!InstallationConnections.ContainsKey(installationId))
{
Console.WriteLine("Create new empty list for installation: " + installationId);
InstallationConnections[installationId] = new InstallationInfo
{
Status = receivedStatusMessage.Status,
Timestamp = DateTime.Now
};
}
else
{
InstallationConnections[installationId].Status = receivedStatusMessage.Status;
InstallationConnections[installationId].Timestamp = DateTime.Now;
}
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); Channel.BasicConsume(queue: "statusQueue", autoAck: true, consumer: consumer);
} }
//Inform all the connected websockets regarding installation "installationId"
public static void InformWebsocketsForInstallation(int installationId)
{
var installationConnection = InstallationConnections[installationId];
Console.WriteLine("Update all the connected websockets for installation " + installationId);
var jsonObject = new
{
id = installationId,
status = installationConnection.Status
};
string jsonString = JsonSerializer.Serialize(jsonObject);
byte[] dataToSend = Encoding.UTF8.GetBytes(jsonString);
foreach (var connection in installationConnection.Connections)
{
connection.SendAsync(
new ArraySegment<byte>(dataToSend, 0, dataToSend.Length),
WebSocketMessageType.Text,
true, // Indicates that this is the end of the message
CancellationToken.None
);
}
}
public static async Task HandleWebSocketConnection(WebSocket currentWebSocket) public static async Task HandleWebSocketConnection(WebSocket currentWebSocket)
{ {
@ -217,37 +243,36 @@ 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)) Console.WriteLine("Create new empty list for installation id " + installationId);
InstallationConnections[installationId] = new InstallationInfo
{ {
Console.WriteLine("Create new empty list for installation id " + installationId); Status = -1
InstallationConnections[installationId] = new InstallationInfo
{
Status = -1
};
}
InstallationConnections[installationId].Connections.Add(currentWebSocket);
var jsonObject = new
{
id = installationId,
status = InstallationConnections[installationId].Status
}; };
var jsonString = JsonSerializer.Serialize(jsonObject);
var dataToSend = Encoding.UTF8.GetBytes(jsonString);
currentWebSocket.SendAsync(dataToSend,
WebSocketMessageType.Text,
true, // Indicates that this is the end of the message
CancellationToken.None
);
} }
InstallationConnections[installationId].Connections.Add(currentWebSocket);
var jsonObject = new
{
id = installationId,
status = InstallationConnections[installationId].Status
};
var jsonString = JsonSerializer.Serialize(jsonObject);
var dataToSend = Encoding.UTF8.GetBytes(jsonString);
currentWebSocket.SendAsync(dataToSend,
WebSocketMessageType.Text,
true, // Indicates that this is the end of the message
CancellationToken.None
);
}
Console.WriteLine("Printing installation connection list"); Console.WriteLine("Printing installation connection list");
Console.WriteLine("----------------------------------------------"); Console.WriteLine("----------------------------------------------");
foreach (var installationConnection in InstallationConnections) foreach (var installationConnection in InstallationConnections)

View File

@ -1,5 +1,5 @@
Prototype ie-entwicklung@10.2.3.115 Prototype Prototype ie-entwicklung@10.2.3.115 Prototype
Salimax0001 ie-entwicklung@10.2.3.104 Marti Technik (Bern) Salimax0001 ie-entwicklung@10.2.3.104 Marti Technik (Bern)
Salimax0002 ie-entwicklung@10.2.4.29 Weidmann Oberwil (ZG) Salimax0002 ie-entwicklung@10.2.4.29 Weidmann Oberwil (ZG)
Salimax0003 ie-entwicklung@10.2.4.33 Elektrotechnik Stefan GmbH Salimax0003 ie-entwicklung@10.2.4.33 Elektrotechnik Stefan GmbH

View File

@ -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 List<AlarmOrWarning>? Warnings { get; set; } public required MessageType Type { get; set; }
public List<AlarmOrWarning>? Alarms { get; set; } public List<AlarmOrWarning>? Warnings { get; set; }
public List<AlarmOrWarning>? Alarms { get; set; }
}
public enum MessageType
{
AlarmOrWarning,
Heartbit
} }

View File

@ -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
}; };

View File

@ -18,9 +18,9 @@ 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();