Compare commits

..

No commits in common. "a04804077c35748edd915577ed2071c92d475907" and "05d7f91ec53306d692fd087a375904d57cd14f34" have entirely different histories.

30 changed files with 583 additions and 770 deletions

View File

@ -14,7 +14,7 @@ I'll reroute my emails to one of you for software updates.
MARIOS: Please make sure to patch out the vulnerable npm packages in the frontend. MARIOS: Please make sure to patch out the vulnerable npm packages in the frontend.
Get started on React or Testcafe Integration tests ;) And in my opinion, get started on React or Testcafe Integration tests ;)
You can add them into the Gitea Actions Pipeline, read the documentation on Github-actions and integration tests. You can add them into the Gitea Actions Pipeline, read the documentation on Github-actions and integration tests.
Runner: Runner:

View File

@ -326,20 +326,11 @@ public class Controller : ControllerBase
[HttpPost(nameof(CreateUser))] [HttpPost(nameof(CreateUser))]
public async Task<ActionResult<User>> CreateUser([FromBody] User newUser, Token authToken) public async Task<ActionResult<User>> CreateUser([FromBody] User newUser, Token authToken)
{ {
var create = Db.GetSession(authToken).Create(newUser); var create = Db.GetSession(authToken).Create(newUser);
if (create)
{
var mail_success= await Db.SendNewUserEmail(newUser);
if (!mail_success)
{
Db.GetSession(authToken).Delete(newUser);
}
return mail_success ? newUser.HidePassword():Unauthorized(); return create && await Db.SendNewUserEmail(newUser)
} ? newUser.HidePassword()
: Unauthorized() ;
return Unauthorized() ;
} }
[HttpPost(nameof(CreateInstallation))] [HttpPost(nameof(CreateInstallation))]
@ -518,10 +509,9 @@ public class Controller : ControllerBase
[HttpPost(nameof(EditInstallationConfig))] [HttpPost(nameof(EditInstallationConfig))]
public async Task<ActionResult<IEnumerable<Object>>> EditInstallationConfig([FromBody] Configuration config, Int64 installationId, Token authToken) public async Task<ActionResult<IEnumerable<Object>>> EditInstallationConfig([FromBody] String config, Int64 installationId, Token authToken)
{ {
var session = Db.GetSession(authToken); var session = Db.GetSession(authToken);
//Console.WriteLine(config.GridSetPoint);
//var installationToUpdate = Db.GetInstallationById(installationId); //var installationToUpdate = Db.GetInstallationById(installationId);

View File

@ -1,15 +0,0 @@
namespace InnovEnergy.App.Backend.DataTypes;
public class Configuration
{
public Double MinimumSoC { get; set; }
public Double GridSetPoint { get; set; }
public CalibrationChargeType ForceCalibrationCharge { get; set; }
}
public enum CalibrationChargeType
{
No,
UntilEoc,
Yes
}

View File

@ -4,7 +4,6 @@ using InnovEnergy.Lib.S3Utils.DataTypes;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Net; using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Text; using System.Text;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using InnovEnergy.App.Backend.Database; using InnovEnergy.App.Backend.Database;
@ -166,8 +165,6 @@ public static class ExoCmd
public static async Task<Boolean> RevokeReadKey(this Installation installation) public static async Task<Boolean> RevokeReadKey(this Installation installation)
{ {
//Check exoscale documentation https://openapi-v2.exoscale.com/topic/topic-api-request-signature
var url = $"https://api-ch-dk-2.exoscale.com/v2/access-key/{installation.S3Key}"; var url = $"https://api-ch-dk-2.exoscale.com/v2/access-key/{installation.S3Key}";
var method = $"access-key/{installation.S3Key}"; var method = $"access-key/{installation.S3Key}";
@ -251,7 +248,7 @@ public static class ExoCmd
return await s3Region.PutBucket(installation.BucketName()) != null; return await s3Region.PutBucket(installation.BucketName()) != null;
} }
public static async Task<Boolean> SendConfig(this Installation installation, Configuration config) public static async Task<Boolean> SendConfig(this Installation installation, String config)
{ {
// This looks hacky but here we grab the vpn-Ip of the installation by its installation Name (e.g. Salimax0001) // This looks hacky but here we grab the vpn-Ip of the installation by its installation Name (e.g. Salimax0001)
@ -273,48 +270,10 @@ public static class ExoCmd
// return result.ExitCode == 200; // return result.ExitCode == 200;
var maxRetransmissions = 2;
UdpClient udpClient = new UdpClient();
udpClient.Client.ReceiveTimeout = 2000;
int port = 9000;
Console.WriteLine("Trying to reach installation with IP: " + installation.VpnIp); var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Credentials!);
//Try at most MAX_RETRANSMISSIONS times to reach an installation. var url = s3Region.Bucket(installation.BucketName()).Path("config.json");
for (int j = 0; j < maxRetransmissions; j++) return await url.PutObject(config);
{
//string message = "This is a message from RabbitMQ server, you can subscribe to the RabbitMQ queue";
byte[] data = Encoding.UTF8.GetBytes(JsonSerializer.Serialize<Configuration>(config));
udpClient.Send(data, data.Length, installation.VpnIp, port);
//Console.WriteLine($"Sent UDP message to {installation.VpnIp}:{port}: {config}");
Console.WriteLine($"Sent UDP message to {installation.VpnIp}:{port}"+" GridSetPoint is "+config.GridSetPoint +" and MinimumSoC is "+config.MinimumSoC);
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Parse(installation.VpnIp), port);
try
{
byte[] replyData = udpClient.Receive(ref remoteEndPoint);
string replyMessage = Encoding.UTF8.GetString(replyData);
Console.WriteLine("Received " + replyMessage + " from installation " + installation.VpnIp);
break;
}
catch (SocketException ex)
{
if (ex.SocketErrorCode == SocketError.TimedOut){Console.WriteLine("Timed out waiting for a response. Retry...");}
else
{
Console.WriteLine("Error: " + ex.Message);
return false;
}
}
}
return true;
//var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Credentials!);
//var url = s3Region.Bucket(installation.BucketName()).Path("config.json");
//return await url.PutObject(config);
} }

View File

@ -66,7 +66,7 @@ public static class SessionMethods
.Apply(Db.Update); .Apply(Db.Update);
} }
public static async Task<Boolean> SendInstallationConfig(this Session? session, Int64 installationId, Configuration configuration) public static async Task<Boolean> SendInstallationConfig(this Session? session, Int64 installationId, String configuration)
{ {
var user = session?.User; var user = session?.User;
var installation = Db.GetInstallationById(installationId); var installation = Db.GetInstallationById(installationId);

View File

@ -181,7 +181,6 @@ public static partial class Db
.ToList(); .ToList();
const String provider = "exo.io"; const String provider = "exo.io";
Console.WriteLine("-----------------------UPDATED READ KEYS-------------------------------------------------------------------");
foreach (var region in regions) foreach (var region in regions)
{ {

View File

@ -1,4 +1,3 @@
using System.Net.Http.Headers;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@ -6,28 +5,35 @@ using System.Threading.Channels;
using Hellang.Middleware.ProblemDetails; using Hellang.Middleware.ProblemDetails;
using InnovEnergy.App.Backend.Database; using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.Websockets; using InnovEnergy.App.Backend.Websockets;
using InnovEnergy.Lib.S3Utils.DataTypes;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using InnovEnergy.Lib.Utils; using InnovEnergy.Lib.Utils;
using RabbitMQ.Client; using RabbitMQ.Client;
namespace InnovEnergy.App.Backend; namespace InnovEnergy.App.Backend;
public static class Program public static class Program
{ {
public static void Main(String[] args) public static void Main(String[] args)
{ {
//Db.CreateFakeRelations();
Watchdog.NotifyReady(); Watchdog.NotifyReady();
Db.Init(); Db.Init();
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
RabbitMqManager.InitializeEnvironment();
RabbitMqManager.StartRabbitMqConsumer(); string vpnServerIp = "194.182.190.208";
//string vpnServerIp = "127.0.0.1";
WebsocketManager.Factory = new ConnectionFactory { HostName = vpnServerIp};
WebsocketManager.Connection = WebsocketManager.Factory.CreateConnection();
WebsocketManager.Channel = WebsocketManager.Connection.CreateModel();
Console.WriteLine("Middleware subscribed to RabbitMQ queue, ready for receiving messages");
WebsocketManager.Channel.QueueDeclare(queue: "statusQueue", durable: true, exclusive: false, autoDelete: false, arguments: null);
WebsocketManager.StartRabbitMqConsumer();
Console.WriteLine("Queue declared"); Console.WriteLine("Queue declared");
//WebsocketManager.InformInstallationsToSubscribeToRabbitMq(); WebsocketManager.InformInstallationsToSubscribeToRabbitMq();
WebsocketManager.MonitorInstallationTable(); WebsocketManager.MonitorInstallationTable();
builder.Services.AddControllers(); builder.Services.AddControllers();

View File

@ -1,193 +0,0 @@
using System.Drawing.Printing;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.DataTypes;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
namespace InnovEnergy.App.Backend.Websockets;
public static class RabbitMqManager
{
public static ConnectionFactory Factory = null!;
public static IConnection Connection = null!;
public static IModel Channel = null!;
public static void InitializeEnvironment()
{
//string vpnServerIp = "194.182.190.208";
string vpnServerIp = "10.2.0.11";
Factory = new ConnectionFactory
{
HostName = vpnServerIp,
Port = 5672,
VirtualHost = "/",
UserName = "consumer",
Password = "faceaddb5005815199f8366d3d15ff8a",
};
Connection = Factory.CreateConnection();
Channel = Connection.CreateModel();
Console.WriteLine("Middleware subscribed to RabbitMQ queue, ready for receiving messages");
Channel.QueueDeclare(queue: "statusQueue", durable: true, exclusive: false, autoDelete: false, arguments: null);
}
public static async Task StartRabbitMqConsumer()
{
var consumer = new EventingBasicConsumer(Channel);
consumer.Received += (_, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
StatusMessage? receivedStatusMessage = JsonSerializer.Deserialize<StatusMessage>(message);
lock (WebsocketManager.InstallationConnections)
{
//Consumer received a message
if (receivedStatusMessage != null)
{
Console.WriteLine("----------------------------------------------");
Console.WriteLine("Received a message from installation: " + receivedStatusMessage.InstallationId + " and status is: " + receivedStatusMessage.Status);
var installationId = receivedStatusMessage.InstallationId;
//This is a heartbit message, just update the timestamp for this installation.
//There is no need to notify the corresponding front-ends.
if (receivedStatusMessage.Type == MessageType.Heartbit)
{
Console.WriteLine("This is a heartbit message from installation: " + installationId);
}
else
{
//Traverse the Warnings list, and store each of them to the database
if (receivedStatusMessage.Warnings != null)
{
foreach (var warning in receivedStatusMessage.Warnings)
{
Warning newWarning = new Warning
{
InstallationId = receivedStatusMessage.InstallationId,
Description = warning.Description,
Date = warning.Date,
Time = warning.Time,
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)
{
Console.WriteLine("Add an alarm for installation "+receivedStatusMessage.InstallationId);
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);
}
}
}
var prevStatus = 0;
//This installation id does not exist in our data structure, add it.
if (!WebsocketManager.InstallationConnections.ContainsKey(installationId))
{
prevStatus = -2;
Console.WriteLine("Create new empty list for installation: " + installationId);
WebsocketManager.InstallationConnections[installationId] = new InstallationInfo
{
Status = receivedStatusMessage.Status,
Timestamp = DateTime.Now
};
}
else
{
prevStatus = WebsocketManager.InstallationConnections[installationId].Status;
WebsocketManager.InstallationConnections[installationId].Status = receivedStatusMessage.Status;
WebsocketManager.InstallationConnections[installationId].Timestamp = DateTime.Now;
}
//Console.WriteLine("----------------------------------------------");
//Update all the connected front-ends regarding this installation
if(prevStatus != receivedStatusMessage.Status && WebsocketManager.InstallationConnections[installationId].Connections.Count > 0)
{
WebsocketManager.InformWebsocketsForInstallation(installationId);
}
}
}
};
Channel.BasicConsume(queue: "statusQueue", autoAck: true, consumer: consumer);
}
public static void InformInstallationsToSubscribeToRabbitMq()
{
var installationIps = Db.Installations.Select(inst => inst.VpnIp).ToList();
Console.WriteLine("Count is "+installationIps.Count);
var maxRetransmissions = 2;
UdpClient udpClient = new UdpClient();
udpClient.Client.ReceiveTimeout = 2000;
int port = 9000;
//Send a message to each installation and tell it to subscribe to the queue
using (udpClient)
{
for (int i = 0; i < installationIps.Count; i++)
{
if(installationIps[i]==""){continue;}
Console.WriteLine("-----------------------------------------------------------");
Console.WriteLine("Trying to reach installation with IP: " + installationIps[i]);
//Try at most MAX_RETRANSMISSIONS times to reach an installation.
for (int j = 0; j < maxRetransmissions; j++)
{
string message = "This is a message from RabbitMQ server, you can subscribe to the RabbitMQ queue";
byte[] data = Encoding.UTF8.GetBytes(message);
udpClient.Send(data, data.Length, installationIps[i], port);
Console.WriteLine($"Sent UDP message to {installationIps[i]}:{port}: {message}");
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Parse(installationIps[i]), port);
try
{
byte[] replyData = udpClient.Receive(ref remoteEndPoint);
string replyMessage = Encoding.UTF8.GetString(replyData);
Console.WriteLine("Received " + replyMessage + " from installation " + installationIps[i]);
break;
}
catch (SocketException ex)
{
if (ex.SocketErrorCode == SocketError.TimedOut){Console.WriteLine("Timed out waiting for a response. Retry...");}
else{Console.WriteLine("Error: " + ex.Message);}
}
}
}
}
Console.WriteLine("Start RabbitMQ Consumer");
}
}

View File

@ -13,6 +13,9 @@ namespace InnovEnergy.App.Backend.Websockets;
public static class WebsocketManager public static class WebsocketManager
{ {
public static Dictionary<int, InstallationInfo> InstallationConnections = new Dictionary<int, InstallationInfo>(); public static Dictionary<int, InstallationInfo> InstallationConnections = new Dictionary<int, InstallationInfo>();
public static ConnectionFactory Factory = null!;
public static IConnection Connection = null!;
public static IModel Channel = null!;
public static void InformInstallationsToSubscribeToRabbitMq() public static void InformInstallationsToSubscribeToRabbitMq()
{ {
@ -79,7 +82,99 @@ public static class WebsocketManager
} }
} }
public static async Task StartRabbitMqConsumer()
{
var consumer = new EventingBasicConsumer(Channel);
consumer.Received += (_, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
StatusMessage? receivedStatusMessage = JsonSerializer.Deserialize<StatusMessage>(message);
lock (InstallationConnections)
{
//Consumer received a message
if (receivedStatusMessage != null)
{
Console.WriteLine("Received a message from installation: " + receivedStatusMessage.InstallationId + " and status is: " + receivedStatusMessage.Status);
Console.WriteLine("----------------------------------------------");
Console.WriteLine("Update installation connection table");
var installationId = receivedStatusMessage.InstallationId;
//This is a heartbit message, just update the timestamp for this installation.
//There is no need to notify the corresponding front-ends.
if (receivedStatusMessage.Type == MessageType.Heartbit)
{
InstallationConnections[installationId].Timestamp = DateTime.Now;
}
else
{
//Traverse the Warnings list, and store each of them to the database
if (receivedStatusMessage.Warnings != null)
{
foreach (var warning in receivedStatusMessage.Warnings)
{
Warning newWarning = new Warning
{
InstallationId = receivedStatusMessage.InstallationId,
Description = warning.Description,
Date = warning.Date,
Time = warning.Time,
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
};
//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);
}
//Inform all the connected websockets regarding installation "installationId" //Inform all the connected websockets regarding installation "installationId"
public static void InformWebsocketsForInstallation(int installationId) public static void InformWebsocketsForInstallation(int installationId)

View File

@ -0,0 +1,9 @@
using System.Net.WebSockets;
namespace InnovEnergy.App.Middleware;
public class InstallationInfo
{
public int Status { get; set; }
public List<WebSocket> Connections { get; } = new List<WebSocket>();
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../InnovEnergy.App.props" />
<PropertyGroup>
<RootNamespace>InnovEnergy.App.Middleware</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RabbitMQ.Client" Version="6.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Lib\Utils\Utils.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,66 @@
using InnovEnergy.App.Middleware;
using System;
using System.Net;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Text;
using InnovEnergy.Lib.Utils;
internal class Program
{
public static readonly object SharedDataLock = new object();
public static async Task Main(string[] args)
{
//For each installation id, we maintain a list of the connected clients
var installationConnections = new Dictionary<int, InstallationInfo>();
var installationsIds = new List<int> {1};
var installationIps = new List<string> {"10.2.3.115"};
var MAX_RETRANSMISSIONS = 2;
RabbitMqConsumer.StartRabbitMqConsumer(installationConnections,SharedDataLock);
UdpClient udpClient = new UdpClient();
udpClient.Client.ReceiveTimeout = 2000;
int port = 9000;
//Send a message to each installation and tell it to subscribe to the queue
for (int i = 0; i < installationsIds.Count; i++)
{
using (udpClient)
{
//Try at most MAX_RETRANSMISSIONS times to reach an installation.
for (int j = 0; j < MAX_RETRANSMISSIONS; j++)
{
string message = "This is a message from RabbitMQ server, you can subscribe to the RabbitMQ queue";
byte[] data = Encoding.UTF8.GetBytes(message);
udpClient.Send(data, data.Length, installationIps[i], port);
Console.WriteLine($"Sent UDP message to {installationIps[i]}:{port}: {message}");
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Parse(installationIps[i]), port);
try
{
byte[] replyData = udpClient.Receive(ref remoteEndPoint);
string replyMessage = Encoding.UTF8.GetString(replyData);
Console.WriteLine("Received message from installation " + installationsIds[i]);
break;
}
catch (SocketException ex)
{
if (ex.SocketErrorCode == SocketError.TimedOut)
{
Console.WriteLine("Timed out waiting for a response. Retry...");
}
else
{
Console.WriteLine("Error: " + ex.Message);
}
}
}
}
}
Console.WriteLine("WebSocket server is running. Press Enter to exit.");
await WebSocketListener.StartServerAsync(installationConnections,SharedDataLock);
}
}

View File

@ -0,0 +1,88 @@
using System.Net.WebSockets;
using System.Text.Json;
namespace InnovEnergy.App.Middleware;
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
public static class RabbitMqConsumer
{
private static ConnectionFactory _factory = null!;
private static IConnection _connection = null!;
private static IModel _channel= null!;
public static void StartRabbitMqConsumer(Dictionary<Int32, InstallationInfo> installationConnections, Object sharedDataLock)
{
string vpnServerIp = "194.182.190.208";
_factory = new ConnectionFactory { HostName = "localhost" };
_connection = _factory.CreateConnection();
_channel = _connection.CreateModel();
Console.WriteLine("Middleware subscribed to RabbitMQ queue, ready for receiving messages");
_channel.QueueDeclare(queue: "statusQueue", durable: false, exclusive: false, autoDelete: false, arguments: null);
var consumer = new EventingBasicConsumer(_channel);
consumer.Received += (_, ea) => Callback(installationConnections, sharedDataLock, ea);
_channel.BasicConsume(queue: "statusQueue", autoAck: true, consumer: consumer);
}
private static void Callback(Dictionary<Int32, InstallationInfo> installationConnections, Object sharedDataLock, BasicDeliverEventArgs ea)
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
StatusMessage? receivedStatusMessage = JsonSerializer.Deserialize<StatusMessage>(message);
lock (sharedDataLock)
{
// Process the received message
if (receivedStatusMessage != null)
{
Console.WriteLine("Received a message from installation: " + receivedStatusMessage.InstallationId + " and status is: " + receivedStatusMessage.Status);
Console.WriteLine("----------------------------------------------");
Console.WriteLine("Update installation connection table");
var installationId = receivedStatusMessage.InstallationId;
if (!installationConnections.ContainsKey(installationId))
{
Console.WriteLine("Create new empty list for installation: " + installationId);
installationConnections[installationId] = new InstallationInfo
{
Status = receivedStatusMessage.Status
};
}
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);
installationConnection.Value.Status = receivedStatusMessage.Status;
var jsonObject = new
{
id = installationId,
status = receivedStatusMessage.Status
};
string jsonString = JsonSerializer.Serialize(jsonObject);
byte[] dataToSend = Encoding.UTF8.GetBytes(jsonString);
foreach (var connection in installationConnection.Value.Connections)
{
connection.SendAsync(
new ArraySegment<byte>(dataToSend, 0, dataToSend.Length),
WebSocketMessageType.Text,
true, // Indicates that this is the end of the message
CancellationToken.None
);
}
}
}
}
}
}
}

View File

@ -0,0 +1,8 @@
namespace InnovEnergy.App.Middleware;
public class StatusMessage
{
public required int InstallationId { get; init; }
public required int Status { get; init; }
}

View File

@ -0,0 +1,129 @@
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
namespace InnovEnergy.App.Middleware;
public static class WebSocketListener
{
public static async Task StartServerAsync(Dictionary<Int32, InstallationInfo> installationConnections, Object sharedDataLock)
{
var listener = new HttpListener();
listener.Prefixes.Add("http://127.0.0.1:8080/");
listener.Start();
//Http listener listens for connections. When it accepts a new connection, it creates a new Task to handle this connection
while (true)
{
var context = await listener.GetContextAsync();
if (context.Request.IsWebSocketRequest)
{
var webSocketContext = await context.AcceptWebSocketAsync(null);
var webSocket = webSocketContext.WebSocket;
// Add the connected WebSocket to the collection
Console.WriteLine("Accepted a new websocket connection");
HandleWebSocketConnection(webSocket, installationConnections);
}
else
{
context.Response.StatusCode = 400;
context.Response.Close();
}
}
//We have a task per websocket connection
async Task HandleWebSocketConnection(WebSocket currentWebSocket, Dictionary<Int32, InstallationInfo> installationConnections)
{
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);
Console.WriteLine("Received a new message from websocket");
if (result.MessageType != WebSocketMessageType.Text)
continue;
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
var installationIds = JsonSerializer.Deserialize<int[]>(message);
lock (sharedDataLock)
{
//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)
{
if (!installationConnections.ContainsKey(installationId))
{
Console.WriteLine("Create new empty list for installation id "+installationId);
installationConnections[installationId] = new InstallationInfo
{
Status = -2
};
}
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("----------------------------------------------");
foreach (var installationConnection in installationConnections)
{
Console.WriteLine("Installation ID: " + installationConnection.Key + " Number of Connections: " + installationConnection.Value.Connections.Count);
}
Console.WriteLine("----------------------------------------------");
}
}
//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);
//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);
}
}
}
}

View File

@ -0,0 +1 @@
dotnet publish Middleware.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@194.182.190.208:~/middleware

View File

@ -1,34 +0,0 @@
#!/bin/bash
dotnet_version='net6.0'
salimax_ip="$1"
username='ie-entwicklung'
root_password='Salimax4x25'
set -e
echo -e "\n============================ Build ============================\n"
dotnet publish \
./SaliMax.csproj \
-p:PublishTrimmed=false \
-c Release \
-r linux-x64
echo -e "\n============================ Deploy ============================\n"
ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.33" "10.2.4.32" "10.2.4.36" "10.2.4.35" "10.2.4.154" "10.2.4.113" "10.2.4.29")
for ip_address in "${ip_addresses[@]}"; do
rsync -v \
--exclude '*.pdb' \
./bin/Release/$dotnet_version/linux-x64/publish/* \
$username@"$ip_address":~/salimax
ssh "$username"@"$ip_address" "cd salimax && echo '$root_password' | sudo -S ./restart"
echo "Deployed and ran commands on $ip_address"
done

View File

@ -1,4 +1,3 @@
using InnovEnergy.App.SaliMax.SystemConfig;
using InnovEnergy.Lib.Devices.Battery48TL; using InnovEnergy.Lib.Devices.Battery48TL;
using InnovEnergy.Lib.Devices.Battery48TL.DataTypes; using InnovEnergy.Lib.Devices.Battery48TL.DataTypes;
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc.DataTypes; using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc.DataTypes;
@ -190,22 +189,16 @@ public static class Controller
private static Boolean MustDoCalibrationCharge(this StatusRecord statusRecord) private static Boolean MustDoCalibrationCharge(this StatusRecord statusRecord)
{ {
var calibrationChargeForced = statusRecord.Config.ForceCalibrationCharge; var calibrationChargeForced = statusRecord.Config.ForceCalibrationCharge;
var batteryCalibrationChargeRequested = statusRecord.Battery?.CalibrationChargeRequested?? false ; var batteryCalibrationChargeRequested = statusRecord.Battery?.CalibrationChargeRequested?? false ;
var mustDoCalibrationCharge = batteryCalibrationChargeRequested || calibrationChargeForced == CalibrationChargeType.Yes || calibrationChargeForced == CalibrationChargeType.UntilEoc ; var mustDoCalibrationCharge = batteryCalibrationChargeRequested || calibrationChargeForced;
if (statusRecord.Battery is not null)
{
if (calibrationChargeForced == CalibrationChargeType.UntilEoc && statusRecord.Battery.Eoc )
{
statusRecord.Config.ForceCalibrationCharge = CalibrationChargeType.No;
}
}
return mustDoCalibrationCharge; return mustDoCalibrationCharge;
} }
private static Double ControlGridPower(this StatusRecord status, Double targetPower) public static Double ControlGridPower(this StatusRecord status, Double targetPower)
{ {
return ControlPower return ControlPower
( (

View File

@ -25,7 +25,7 @@ public record StatusRecord
public required DcPowerDevice? LoadOnDc { get; init; } public required DcPowerDevice? LoadOnDc { get; init; }
public required RelaysRecord? Relays { get; init; } public required RelaysRecord? Relays { get; init; }
public required AmptStatus? PvOnDc { get; init; } public required AmptStatus? PvOnDc { get; init; }
public required Config Config { get; set; } public required Config Config { get; init; }
public required SystemLog Log { get; set; } // TODO: init only public required SystemLog Log { get; set; } // TODO: init only
public required EssControl EssControl { get; set; } // TODO: init only public required EssControl EssControl { get; set; } // TODO: init only

View File

@ -1,11 +0,0 @@
using InnovEnergy.App.SaliMax.SystemConfig;
namespace InnovEnergy.App.SaliMax.MiddlewareClasses;
public class Configuration
{
public Double MinimumSoC { get; set; }
public Double GridSetPoint { get; set; }
public CalibrationChargeType ForceCalibrationCharge { get; set; }
}

View File

@ -4,7 +4,6 @@ using System.Net.NetworkInformation;
using System.Net.Sockets; using System.Net.Sockets;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Threading.Tasks; using System.Reactive.Threading.Tasks;
using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using Flurl.Http; using Flurl.Http;
using InnovEnergy.App.SaliMax.Devices; using InnovEnergy.App.SaliMax.Devices;
@ -51,8 +50,7 @@ internal static class Program
private static readonly Channel RelaysChannel ; private static readonly Channel RelaysChannel ;
private static readonly Channel BatteriesChannel ; private static readonly Channel BatteriesChannel ;
//private const String VpnServerIp = "194.182.190.208"; private const String VpnServerIp = "194.182.190.208";
private const String VpnServerIp = "10.2.0.11";
private static IPAddress? _controllerIpAddress; private static IPAddress? _controllerIpAddress;
private static UdpClient _udpListener = null!; private static UdpClient _udpListener = null!;
@ -226,7 +224,7 @@ internal static class Program
var currentSalimaxState = GetSalimaxStateAlarm(record); var currentSalimaxState = GetSalimaxStateAlarm(record);
SendSalimaxStateAlarm(currentSalimaxState,record); SendSalimaxStateAlarm(currentSalimaxState);
record.ControlConstants(); record.ControlConstants();
record.ControlSystemState(); record.ControlSystemState();
@ -250,8 +248,6 @@ internal static class Program
(record.Relays is null ? "No relay Data available" : record.Relays.FiWarning ? "Alert: Fi Warning Detected" : "No Fi Warning Detected").WriteLine(); (record.Relays is null ? "No relay Data available" : record.Relays.FiWarning ? "Alert: Fi Warning Detected" : "No Fi Warning Detected").WriteLine();
(record.Relays is null ? "No relay Data available" : record.Relays.FiError ? "Alert: Fi Error Detected" : "No Fi Error Detected") .WriteLine(); (record.Relays is null ? "No relay Data available" : record.Relays.FiError ? "Alert: Fi Error Detected" : "No Fi Error Detected") .WriteLine();
//record.ApplyConfigFile(minSoc:22, gridSetPoint:1);
record.Config.Save(); record.Config.Save();
"===========================================".LogInfo(); "===========================================".LogInfo();
@ -262,11 +258,9 @@ internal static class Program
// ReSharper disable once FunctionNeverReturns // ReSharper disable once FunctionNeverReturns
} }
private static void SendSalimaxStateAlarm(StatusMessage currentSalimaxState, StatusRecord record) private static void SendSalimaxStateAlarm(StatusMessage currentSalimaxState)
{ {
var s3Bucket = Config.Load().S3?.Bucket; var s3Bucket = Config.Load().S3?.Bucket;
//Every 15 iterations(30 seconds), the installation sends a heartbit message to the queue
_heartBitInterval++; _heartBitInterval++;
//When the controller boots, it tries to subscribe to the queue //When the controller boots, it tries to subscribe to the queue
@ -274,26 +268,30 @@ internal static class Program
{ {
_subscribeToQueueForTheFirstTime = true; _subscribeToQueueForTheFirstTime = true;
SubscribeToQueue(currentSalimaxState, s3Bucket); SubscribeToQueue(currentSalimaxState, s3Bucket);
}
//If already subscribed to the queue and the status has been changed, update the queue
if (_subscribedToQueue && currentSalimaxState.Status != _prevSalimaxState) if (_subscribedToQueue && currentSalimaxState.Status != _prevSalimaxState)
{
_prevSalimaxState = currentSalimaxState.Status;
}
}
//If already subscribed to the queue and the status has been changed, update the queue
else if (_subscribedToQueue && currentSalimaxState.Status != _prevSalimaxState)
{ {
_prevSalimaxState = currentSalimaxState.Status; _prevSalimaxState = currentSalimaxState.Status;
if (s3Bucket != null) if (s3Bucket != null)
InformMiddleware(currentSalimaxState); InformMiddleware(s3Bucket, currentSalimaxState);
} }
else if (_subscribedToQueue && _heartBitInterval>=15) else if (_subscribedToQueue && _heartBitInterval>=15)
{ {
//Send a heartbit to the backend
Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------"); Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------");
_heartBitInterval = 0; _heartBitInterval = 0;
currentSalimaxState.Type = MessageType.Heartbit; currentSalimaxState.Type = MessageType.Heartbit;
if (s3Bucket != null) if (s3Bucket != null)
InformMiddleware(currentSalimaxState); InformMiddleware(s3Bucket, currentSalimaxState);
} }
//If there is an available message from the RabbitMQ Broker, apply the configuration file //If there is an available message from the RabbitMQ Broker, subscribe to the queue
if (_udpListener.Available > 0) if (_udpListener.Available > 0)
{ {
IPEndPoint? serverEndpoint = null; IPEndPoint? serverEndpoint = null;
@ -304,16 +302,13 @@ internal static class Program
var udpMessage = _udpListener.Receive(ref serverEndpoint); var udpMessage = _udpListener.Receive(ref serverEndpoint);
var message = Encoding.UTF8.GetString(udpMessage); var message = Encoding.UTF8.GetString(udpMessage);
Configuration config = JsonSerializer.Deserialize<Configuration>(message); Console.WriteLine($"Received a message: {message}");
Console.WriteLine($"Received a configuration message: GridSetPoint is "+config.GridSetPoint +", MinimumSoC is "+config.MinimumSoC+ " and ForceCalibrationCharge is "+config.ForceCalibrationCharge);
// Send the reply to the sender's endpoint // Send the reply to the sender's endpoint
_udpListener.Send(replyData, replyData.Length, serverEndpoint); _udpListener.Send(replyData, replyData.Length, serverEndpoint);
Console.WriteLine($"Replied to {serverEndpoint}: {replyMessage}"); Console.WriteLine($"Replied to {serverEndpoint}: {replyMessage}");
record.ApplyConfigFile(config); SubscribeToQueue(currentSalimaxState, s3Bucket);
} }
} }
@ -321,24 +316,15 @@ internal static class Program
{ {
try try
{ {
//_factory = new ConnectionFactory { HostName = VpnServerIp }; _factory = new ConnectionFactory { HostName = VpnServerIp };
_factory = new ConnectionFactory
{
HostName = VpnServerIp,
Port = 5672,
VirtualHost = "/",
UserName = "producer",
Password = "b187ceaddb54d5485063ddc1d41af66f",
};
_connection = _factory.CreateConnection(); _connection = _factory.CreateConnection();
_channel = _connection.CreateModel(); _channel = _connection.CreateModel();
_channel.QueueDeclare(queue: "statusQueue", durable: true, exclusive: false, autoDelete: false, arguments: null); _channel.QueueDeclare(queue: "statusQueue", durable: true, exclusive: false, autoDelete: false, arguments: null);
Console.WriteLine("The controller sends its status to the middleware for the first time"); Console.WriteLine("The controller sends its status to the middleware for the first time");
if (s3Bucket != null) InformMiddleware(currentSalimaxState);
if (s3Bucket != null) InformMiddleware(s3Bucket, currentSalimaxState);
_subscribedToQueue = true; _subscribedToQueue = true;
} }
@ -370,8 +356,11 @@ internal static class Program
return IPAddress.None; return IPAddress.None;
} }
private static void InformMiddleware(StatusMessage status) private static void InformMiddleware(String? bucket, StatusMessage status)
{ {
//ddddddddd
int.TryParse(bucket[0].ToString(), out var installationId);
var message = JsonSerializer.Serialize(status); var message = JsonSerializer.Serialize(status);
var body = Encoding.UTF8.GetBytes(message); var body = Encoding.UTF8.GetBytes(message);
@ -491,13 +480,13 @@ internal static class Program
? SalimaxAlarmState.Red ? SalimaxAlarmState.Red
: salimaxAlarmsState; // this will be replaced by LedState : salimaxAlarmsState; // this will be replaced by LedState
int.TryParse(s3Bucket?.Split("-")[0], out var installationId); int.TryParse(s3Bucket[0].ToString(), out var installationId);
var returnedStatus = new StatusMessage var returnedStatus = new StatusMessage
{ {
InstallationId = installationId, InstallationId = installationId,
Status = salimaxAlarmsState, Status = salimaxAlarmsState,
Type = MessageType.AlarmOrWarning, Type= MessageType.AlarmOrWarning,
Alarms = alarmList, Alarms = alarmList,
Warnings = warningList Warnings = warningList
}; };
@ -723,12 +712,6 @@ internal static class Program
return value == "/Battery/Dc/Power"; return value == "/Battery/Dc/Power";
} }
private static void ApplyConfigFile(this StatusRecord status, Configuration config)
{
status.Config.MinSoc = config.MinimumSoC;
status.Config.GridSetPoint = config.GridSetPoint*1000;
status.Config.ForceCalibrationCharge = config.ForceCalibrationCharge;
}
// Method to calculate average for a variableValue in a dictionary // Method to calculate average for a variableValue in a dictionary
static double CalculateAverage( List<Double> data) static double CalculateAverage( List<Double> data)

View File

@ -1,8 +0,0 @@
namespace InnovEnergy.App.SaliMax.SystemConfig;
public enum CalibrationChargeType
{
No,
UntilEoc,
Yes
}

View File

@ -15,7 +15,7 @@ public class Config //TODO: let IE choose from config files (Json) and connect t
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
public required Double MinSoc { get; set; } public required Double MinSoc { get; set; }
public required CalibrationChargeType ForceCalibrationCharge { get; set; } public required Boolean ForceCalibrationCharge { get; set; }
public required Boolean DisplayIndividualBatteries { get; set; } public required Boolean DisplayIndividualBatteries { get; set; }
public required Double PConstant { get; set; } public required Double PConstant { get; set; }
public required Double GridSetPoint { get; set; } public required Double GridSetPoint { get; set; }
@ -40,7 +40,7 @@ public class Config //TODO: let IE choose from config files (Json) and connect t
public static Config Default => new() public static Config Default => new()
{ {
MinSoc = 20, MinSoc = 20,
ForceCalibrationCharge = CalibrationChargeType.No, ForceCalibrationCharge = false,
DisplayIndividualBatteries = false, DisplayIndividualBatteries = false,
PConstant = .5, PConstant = .5,
GridSetPoint = 0, GridSetPoint = 0,
@ -117,7 +117,7 @@ public class Config //TODO: let IE choose from config files (Json) and connect t
public static Config Default => new() public static Config Default => new()
{ {
MinSoc = 20, MinSoc = 20,
ForceCalibrationCharge = CalibrationChargeType.No, ForceCalibrationCharge = false,
DisplayIndividualBatteries = false, DisplayIndividualBatteries = false,
PConstant = .5, PConstant = .5,
GridSetPoint = 0, GridSetPoint = 0,

View File

@ -1,16 +0,0 @@
namespace InnovEnergy.App.SaliMax.SystemConfig;
public class DeviceTopology
{
public required Boolean GridMeter { get; set; }
public required Boolean PvOnAcGrid { get; set; }
public required Boolean LoadOnAcGrid { get; set; }
public required Boolean AcGridToAcIsland { get; set; }
public required Boolean PvOnAcIsland { get; set; }
public required Boolean LoadOnAcIsland { get; set; }
public required Boolean AcDc { get; set; }
public required Boolean PvOnDc { get; set; }
public required Boolean LoadOnDc { get; set; }
public required Boolean DcDc { get; set; }
public required Boolean Battery { get; set; }
}

View File

@ -1,28 +1,10 @@
import { ConfigurationValues, TopologyValues } from '../Log/graph.util'; import { TopologyValues } from '../Log/graph.util';
import { import { Box, CardContent, Container, Grid, TextField } from '@mui/material';
Alert, import React from 'react';
Box,
CardContent,
CircularProgress,
Container,
FormControl,
Grid,
IconButton,
InputLabel,
Select,
TextField,
useTheme
} from '@mui/material';
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Button from '@mui/material/Button';
import axiosConfig from '../../../Resources/axiosConfig';
import { Close as CloseIcon } from '@mui/icons-material';
import MenuItem from '@mui/material/MenuItem';
interface ConfigurationProps { interface ConfigurationProps {
values: TopologyValues; values: TopologyValues;
id: number;
} }
function Configuration(props: ConfigurationProps) { function Configuration(props: ConfigurationProps) {
@ -30,110 +12,6 @@ function Configuration(props: ConfigurationProps) {
return null; return null;
} }
const forcedCalibrationChargeOptions = ['No', 'UntilEoc', 'Yes'];
const [formValues, setFormValues] = useState<ConfigurationValues>({
minimumSoC: props.values.minimumSoC.values[0].value,
gridSetPoint: (props.values.gridSetPoint.values[0].value as number) / 1000,
forceCalibrationCharge: forcedCalibrationChargeOptions.indexOf(
props.values.calibrationChargeForced.values[0].value.toString()
)
});
const handleSubmit = async (e) => {
setLoading(true);
const res = await axiosConfig
.post(`/EditInstallationConfig?installationId=${props.id}`, formValues)
.catch((err) => {
if (err.response) {
setError(true);
setLoading(false);
}
});
if (res) {
setUpdated(true);
setLoading(false);
}
};
const [errors, setErrors] = useState({
minimumSoC: false,
gridSetPoint: false
});
const SetErrorForField = (field_name, state) => {
setErrors((prevErrors) => ({
...prevErrors,
[field_name]: state
}));
};
const theme = useTheme();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [updated, setUpdated] = useState(false);
const [openForcedCalibrationCharge, setOpenForcedCalibrationCharge] =
useState(false);
const [
selectedForcedCalibrationChargeOption,
setSelectedForcedCalibrationChargeOption
] = useState<string>(
props.values.calibrationChargeForced.values[0].value.toString()
);
//const forcedCalibrationChargeOptions = ['No', 'UntilEoc', 'Yes'];
const handleSelectedCalibrationChargeChange = (event) => {
setSelectedForcedCalibrationChargeOption(event.target.value);
setFormValues({
...formValues,
['forceCalibrationCharge']: forcedCalibrationChargeOptions.indexOf(
event.target.value
)
});
};
const handleOpenForcedCalibrationCharge = () => {
setOpenForcedCalibrationCharge(true);
};
const handleCloseForcedCalibrationCharge = () => {
setOpenForcedCalibrationCharge(false);
};
const handleChange = (e) => {
const { name, value } = e.target;
switch (name) {
case 'minimumSoC':
if (
/[^0-9.]/.test(value) ||
isNaN(parseFloat(value)) ||
parseFloat(value) > 100
) {
SetErrorForField(name, true);
} else {
SetErrorForField(name, false);
}
break;
case 'gridSetPoint':
if (/[^0-9.]/.test(value) || isNaN(parseFloat(value))) {
SetErrorForField(name, true);
} else {
SetErrorForField(name, false);
}
break;
default:
return true;
}
setFormValues({
...formValues,
[name]: value
});
};
return ( return (
<Container maxWidth="xl"> <Container maxWidth="xl">
<Grid <Grid
@ -153,114 +31,80 @@ function Configuration(props: ConfigurationProps) {
noValidate noValidate
autoComplete="off" autoComplete="off"
> >
<div style={{ marginBottom: '5px' }}> <div>
<TextField <TextField
label={ label={
<FormattedMessage <FormattedMessage
id="minimum_soc " id="minimum_soc"
defaultMessage="Minimum SoC (%)" defaultMessage="Minimum SoC"
/> />
} }
name="minimumSoC" value={props.values.minimumSoC.values[0].value + ' %'}
value={formValues.minimumSoC}
onChange={handleChange}
helperText={
errors.minimumSoC ? (
<span style={{ color: 'red' }}>
Value should be between 0-100%
</span>
) : (
''
)
}
fullWidth fullWidth
/> />
</div> </div>
<div> <div>
<FormControl <TextField
fullWidth label={
sx={{ marginLeft: 1, marginBottom: '10px', width: 390 }}
>
<InputLabel
sx={{
fontSize: 14,
backgroundColor: 'transparent'
}}
>
<FormattedMessage <FormattedMessage
id="forced_calibration_charge" id="calibration_charge_forced"
defaultMessage="Forced Calibration Charge" defaultMessage="Calibration Charge forced"
/>
}
value={props.values.calibrationChargeForced.values[0].value}
fullWidth
/> />
</InputLabel>
<Select
value={selectedForcedCalibrationChargeOption}
onChange={handleSelectedCalibrationChargeChange}
open={openForcedCalibrationCharge}
onClose={handleCloseForcedCalibrationCharge}
onOpen={handleOpenForcedCalibrationCharge}
//renderValue={selectedForcedCalibrationChargeOption}
>
{forcedCalibrationChargeOptions.map((option) => (
<MenuItem key={option} value={option}>
{option}
</MenuItem>
))}
</Select>
</FormControl>
</div> </div>
<div>
<div style={{ marginBottom: '5px' }}>
<TextField <TextField
label={ label={
<FormattedMessage <FormattedMessage
id="grid_set_point" id="grid_set_point"
defaultMessage="Grid Set Point (kW)" defaultMessage="Grid Set Point"
/> />
} }
name="gridSetPoint" value={
value={formValues.gridSetPoint} (
onChange={handleChange} (props.values.gridSetPoint.values[0].value as number) /
helperText={ 1000
errors.gridSetPoint ? ( ).toString() + ' kW'
<span style={{ color: 'red' }}>
Please provide a valid number
</span>
) : (
''
)
} }
fullWidth fullWidth
/> />
</div> </div>
<div style={{ marginBottom: '5px' }}> <div>
<TextField <TextField
label={ label={
<FormattedMessage <FormattedMessage
id="Installed_Power_DC1010" id="Installed_Power_DC1010"
defaultMessage="Installed Power DC1010 (kW)" defaultMessage="Installed Power DC1010"
/> />
} }
value={ value={
(
(props.values.installedDcDcPower.values[0] (props.values.installedDcDcPower.values[0]
.value as number) * 10 .value as number) * 10
).toString() + ' kW'
} }
fullWidth fullWidth
/> />
</div> </div>
<div style={{ marginBottom: '5px' }}> <div>
<TextField <TextField
label={ label={
<FormattedMessage <FormattedMessage
id="Maximum_Discharge_Power" id="Maximum_Discharge_Power"
defaultMessage="Maximum Discharge Power (W)" defaultMessage="Maximum Discharge Power"
/> />
} }
value={ value={
(
(props.values.maximumDischargePower.values[0] (props.values.maximumDischargePower.values[0]
.value as number) * .value as number) *
48 * 48 *
(props.values.DcDcNum.values[0].value as number) (props.values.DcDcNum.values[0].value as number)
).toString() + ' W'
} }
fullWidth fullWidth
/> />
@ -277,95 +121,6 @@ function Configuration(props: ConfigurationProps) {
fullWidth fullWidth
/> />
</div> </div>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
<Button
variant="contained"
onClick={handleSubmit}
sx={{
marginLeft: '10px'
}}
>
<FormattedMessage
id="applychanges"
defaultMessage="Apply Changes"
/>
</Button>
{loading && (
<CircularProgress
sx={{
color: theme.palette.primary.main,
marginLeft: '20px'
}}
/>
)}
{error && (
<Alert
severity="error"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center'
}}
>
An error has occurred
<IconButton
color="inherit"
size="small"
onClick={() => setError(false)} // Set error state to false on click
sx={{ marginLeft: '4px' }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
{updated && (
<Alert
severity="success"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center'
}}
>
Successfully applied configuration file
<IconButton
color="inherit"
size="small"
onClick={() => setUpdated(false)}
sx={{ marginLeft: '4px' }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
{error && (
<Alert
severity="error"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center'
}}
>
An error has occurred
<IconButton
color="inherit"
size="small"
onClick={() => setError(false)}
sx={{ marginLeft: '4px' }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
</div>
</Box> </Box>
</CardContent> </CardContent>
</Grid> </Grid>

View File

@ -129,6 +129,16 @@ function Installation(props: singleInstallationProps) {
const s3Credentials = { s3Bucket, ...S3data }; const s3Credentials = { s3Bucket, ...S3data };
useEffect(() => {
if (
installationId == props.current_installation.id &&
(currentTab == 'live' || currentTab == 'configuration')
) {
let isMounted = true;
setFormValues(props.current_installation);
setErrorLoadingS3Data(false);
let disconnectedStatusResult = [];
const fetchDataPeriodically = async () => { const fetchDataPeriodically = async () => {
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20)); const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
const date = now.toDate(); const date = now.toDate();
@ -136,57 +146,48 @@ function Installation(props: singleInstallationProps) {
try { try {
const res = await fetchData(now, s3Credentials); const res = await fetchData(now, s3Credentials);
// if (!isMounted) { if (!isMounted) {
// return false; return;
// } }
if (res != FetchResult.notAvailable && res != FetchResult.tryLater) { if (
res === FetchResult.notAvailable ||
res === FetchResult.tryLater
) {
disconnectedStatusResult.unshift(-1);
disconnectedStatusResult = disconnectedStatusResult.slice(0, 5);
let i = 0;
//If at least one status value shows an error, then show error
for (i; i < disconnectedStatusResult.length; i++) {
if (disconnectedStatusResult[i] != -1) {
break;
}
}
if (i === disconnectedStatusResult.length) {
setErrorLoadingS3Data(true);
}
} else {
setErrorLoadingS3Data(false);
setValues( setValues(
extractValues({ extractValues({
time: now, time: now,
value: res value: res
}) })
); );
return true;
} }
} catch (err) { } catch (err) {
return false; setErrorLoadingS3Data(true);
} }
}; };
const fetchDataOnlyOneTime = async () => { const interval = setInterval(fetchDataPeriodically, 2000);
let success = false;
while (true) {
success = await fetchDataPeriodically();
await new Promise((resolve) => setTimeout(resolve, 1000));
if (success) {
break;
}
}
};
useEffect(() => {
if (
installationId == props.current_installation.id &&
(currentTab == 'live' || currentTab == 'configuration')
) {
//let isMounted = true;
setFormValues(props.current_installation);
var interval;
if (currentTab == 'live') {
interval = setInterval(fetchDataPeriodically, 2000);
}
if (currentTab == 'configuration') {
fetchDataOnlyOneTime();
}
// Cleanup function to cancel interval and update isMounted when unmounted // Cleanup function to cancel interval and update isMounted when unmounted
return () => { return () => {
//isMounted = false; isMounted = false;
if (currentTab == 'live') {
clearInterval(interval); clearInterval(interval);
}
}; };
} }
}, [installationId, currentTab]); }, [installationId, currentTab]);
@ -661,10 +662,7 @@ function Installation(props: singleInstallationProps) {
<Overview s3Credentials={s3Credentials}></Overview> <Overview s3Credentials={s3Credentials}></Overview>
)} )}
{currentTab === 'configuration' && currentUser.hasWriteAccess && ( {currentTab === 'configuration' && currentUser.hasWriteAccess && (
<Configuration <Configuration values={values}></Configuration>
values={values}
id={installationId}
></Configuration>
)} )}
{currentTab === 'manage' && currentUser.hasWriteAccess && ( {currentTab === 'manage' && currentUser.hasWriteAccess && (
<AccessContextProvider> <AccessContextProvider>

View File

@ -80,7 +80,7 @@ function installationForm(props: installationFormProps) {
<Box <Box
sx={{ sx={{
position: 'absolute', position: 'absolute',
top: isMobile ? '50%' : '40%', top: isMobile ? '50%' : '30%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
width: 500, width: 500,
@ -152,11 +152,10 @@ function installationForm(props: installationFormProps) {
error={formValues.country === ''} error={formValues.country === ''}
/> />
</div> </div>
<div> <div>
<TextField <TextField
label={<FormattedMessage id="VpnIp" defaultMessage="VpnIp" />} label={<FormattedMessage id="VpnIp" defaultMessage="VpnIp" />}
name="vpnIp" name="VpnIp"
value={formValues.vpnIp} value={formValues.vpnIp}
onChange={handleChange} onChange={handleChange}
fullWidth fullWidth

View File

@ -34,12 +34,6 @@ export type BoxData = {
values: I_BoxDataValue[]; values: I_BoxDataValue[];
}; };
export type ConfigurationValues = {
minimumSoC: string | number;
gridSetPoint: number;
forceCalibrationCharge: number;
};
export type TopologyValues = { export type TopologyValues = {
gridBox: BoxData; gridBox: BoxData;
pvOnAcGridBox: BoxData; pvOnAcGridBox: BoxData;

View File

@ -95,7 +95,7 @@ function UsersSearch() {
> >
<FormattedMessage <FormattedMessage
id="successfullyCreatedUser" id="successfullyCreatedUser"
defaultMessage="Successfully Created User" defaultMessage="Successfully Updated User"
/> />
<IconButton <IconButton

View File

@ -215,7 +215,7 @@ function userForm(props: userFormProps) {
<Box <Box
sx={{ sx={{
position: 'absolute', position: 'absolute',
top: isMobile ? '50%' : '40%', top: isMobile ? '50%' : '30%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
width: 500, width: 500,