229 lines
11 KiB
C#
229 lines
11 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Net.WebSockets;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using InnovEnergy.App.Backend.Database;
|
|
using InnovEnergy.App.Backend.DataTypes;
|
|
using InnovEnergy.Lib.Utils;
|
|
|
|
namespace InnovEnergy.App.Backend.Websockets;
|
|
|
|
public static class WebsocketManager
|
|
{
|
|
public static Dictionary<Int64, InstallationInfo> InstallationConnections = new Dictionary<Int64, InstallationInfo>();
|
|
|
|
public static async Task MonitorInstallationTable()
|
|
{
|
|
while (true)
|
|
{
|
|
var idsToInform = new List<Int64>();
|
|
|
|
lock (InstallationConnections)
|
|
{
|
|
Console.WriteLine("Monitoring installation table...");
|
|
foreach (var installationConnection in InstallationConnections)
|
|
{
|
|
Console.WriteLine("installationConnection ID is " + installationConnection.Key + ", latest timestamp is" +installationConnection.Value.Timestamp + ", product is "+ installationConnection.Value.Product
|
|
+ ", and time diff is "+ (DateTime.Now - installationConnection.Value.Timestamp));
|
|
|
|
if ((installationConnection.Value.Product == (int)ProductType.Salimax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) ||
|
|
(installationConnection.Value.Product == (int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(60)) ||
|
|
(installationConnection.Value.Product == (int)ProductType.SodioHome && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(4)) ||
|
|
(installationConnection.Value.Product == (int)ProductType.SodiStoreMax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2))
|
|
)
|
|
{
|
|
Console.WriteLine("Installation ID is " + installationConnection.Key);
|
|
Console.WriteLine("installationConnection.Value.Timestamp is " + installationConnection.Value.Timestamp);
|
|
|
|
installationConnection.Value.Status = (int)StatusType.Offline;
|
|
Installation installation = Db.Installations.FirstOrDefault(f => f.Product == installationConnection.Value.Product && f.Id == installationConnection.Key);
|
|
installation.Status = (int)StatusType.Offline;
|
|
installation.Apply(Db.Update);
|
|
if (installationConnection.Value.Connections.Count > 0)
|
|
{
|
|
idsToInform.Add(installationConnection.Key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send notifications outside the lock so we can await the async SendAsync calls
|
|
foreach (var id in idsToInform)
|
|
await InformWebsocketsForInstallation(id);
|
|
|
|
await Task.Delay(TimeSpan.FromMinutes(1));
|
|
}
|
|
}
|
|
|
|
//Inform all the connected websockets regarding installation "installationId"
|
|
public static async Task InformWebsocketsForInstallation(Int64 installationId)
|
|
{
|
|
var installation = Db.GetInstallationById(installationId);
|
|
byte[] dataToSend;
|
|
List<WebSocket> connections;
|
|
|
|
lock (InstallationConnections)
|
|
{
|
|
var installationConnection = InstallationConnections[installationId];
|
|
Console.WriteLine("Update all the connected websockets for installation " + installation.Name);
|
|
|
|
var jsonObject = new
|
|
{
|
|
id = installationId,
|
|
status = installationConnection.Status,
|
|
testingMode = installation.TestingMode
|
|
};
|
|
|
|
dataToSend = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(jsonObject));
|
|
connections = installationConnection.Connections.ToList(); // snapshot before releasing lock
|
|
}
|
|
|
|
// Send to all connections concurrently (preserves original fire-and-forget intent),
|
|
// but isolate failures so one closed socket doesn't affect others or crash the caller.
|
|
await Task.WhenAll(connections
|
|
.Where(c => c.State == WebSocketState.Open)
|
|
.Select(async c =>
|
|
{
|
|
try
|
|
{
|
|
await c.SendAsync(new ArraySegment<byte>(dataToSend, 0, dataToSend.Length),
|
|
WebSocketMessageType.Text, true, CancellationToken.None);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"WebSocket send failed for installation {installationId}: {ex.Message}");
|
|
}
|
|
}));
|
|
}
|
|
|
|
|
|
public static async Task HandleWebSocketConnection(WebSocket currentWebSocket)
|
|
{
|
|
var buffer = new byte[4096];
|
|
try
|
|
{
|
|
while (currentWebSocket.State == WebSocketState.Open)
|
|
{
|
|
//Listen for incoming messages on this WebSocket
|
|
var result = await currentWebSocket.ReceiveAsync(buffer, CancellationToken.None);
|
|
|
|
if (result.MessageType != WebSocketMessageType.Text)
|
|
continue;
|
|
|
|
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
|
var installationIds = JsonSerializer.Deserialize<int[]>(message);
|
|
Console.WriteLine("Received Websocket message: " + message);
|
|
|
|
//This is a ping message to keep the connection alive, reply with a pong
|
|
if (installationIds[0] == -1)
|
|
{
|
|
var jsonObject = new
|
|
{
|
|
id = -1,
|
|
status = -1
|
|
};
|
|
|
|
var jsonString = JsonSerializer.Serialize(jsonObject);
|
|
var dataToSend = Encoding.UTF8.GetBytes(jsonString);
|
|
await currentWebSocket.SendAsync(dataToSend,
|
|
WebSocketMessageType.Text,
|
|
true,
|
|
CancellationToken.None
|
|
);
|
|
|
|
continue;
|
|
}
|
|
|
|
//Received a new message from this websocket.
|
|
//We have a HandleWebSocketConnection per connected frontend
|
|
byte[] encodedDataToSend;
|
|
lock (InstallationConnections)
|
|
{
|
|
List<WebsocketMessage> dataToSend = new List<WebsocketMessage>();
|
|
|
|
//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
|
|
//Then, report the status of each requested installation to the front-end that created the websocket connection
|
|
foreach (var installationId in installationIds)
|
|
{
|
|
var installation = Db.GetInstallationById(installationId);
|
|
if (!InstallationConnections.ContainsKey(installationId))
|
|
{
|
|
//Since we keep all the changes to the database, in case that the backend reboots, we need to update the in-memory data structure.
|
|
//Thus, if the status is -1, we put an old timestamp, otherwise, we put the most recent timestamp.
|
|
//We store everything to the database, because when the backend reboots, we do not want to wait until all the installations send the heartbit messages.
|
|
//We want the in memory data structure to be up to date immediately.
|
|
InstallationConnections[installationId] = new InstallationInfo
|
|
{
|
|
Status = installation.Status,
|
|
Timestamp = installation.Status==(int)StatusType.Offline ? DateTime.Now.AddDays(-1) : DateTime.Now,
|
|
Product = installation.Product
|
|
};
|
|
}
|
|
|
|
InstallationConnections[installationId].Connections.Add(currentWebSocket);
|
|
|
|
var jsonObject = new WebsocketMessage
|
|
{
|
|
id = installationId,
|
|
status = InstallationConnections[installationId].Status,
|
|
testingMode = installation.TestingMode
|
|
};
|
|
|
|
dataToSend.Add(jsonObject);
|
|
|
|
}
|
|
var jsonString = JsonSerializer.Serialize(dataToSend);
|
|
encodedDataToSend = Encoding.UTF8.GetBytes(jsonString);
|
|
|
|
// Console.WriteLine("Printing installation connection list");
|
|
// Console.WriteLine("----------------------------------------------");
|
|
// foreach (var installationConnection in InstallationConnections)
|
|
// {
|
|
// Console.WriteLine("Installation ID: " + installationConnection.Key + " Number of Connections: " + installationConnection.Value.Connections.Count);
|
|
// }
|
|
// Console.WriteLine("----------------------------------------------");
|
|
}
|
|
|
|
await currentWebSocket.SendAsync(encodedDataToSend,
|
|
WebSocketMessageType.Text,
|
|
true,
|
|
CancellationToken.None
|
|
);
|
|
}
|
|
|
|
lock (InstallationConnections)
|
|
{
|
|
//When the front-end terminates the connection, the following code will be executed
|
|
//Console.WriteLine("The connection has been terminated");
|
|
foreach (var installationConnection in InstallationConnections)
|
|
{
|
|
if (installationConnection.Value.Connections.Contains(currentWebSocket))
|
|
{
|
|
installationConnection.Value.Connections.Remove(currentWebSocket);
|
|
}
|
|
}
|
|
}
|
|
|
|
await currentWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Connection closed by server", CancellationToken.None);
|
|
// lock (InstallationConnections)
|
|
// {
|
|
// //Print the installationConnections dictionary after deleting a websocket
|
|
// Console.WriteLine("Print the installation connections list after deleting a websocket");
|
|
// Console.WriteLine("----------------------------------------------");
|
|
// foreach (var installationConnection in InstallationConnections)
|
|
// {
|
|
// Console.WriteLine("Installation ID: " + installationConnection.Key + " Number of Connections: " + installationConnection.Value.Connections.Count);
|
|
// }
|
|
//
|
|
// Console.WriteLine("----------------------------------------------");
|
|
// }
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("WebSocket error: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
} |