Compare commits

...

15 Commits

30 changed files with 771 additions and 584 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.
And in my opinion, get started on React or Testcafe Integration tests ;)
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.
Runner:

View File

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

View File

@ -0,0 +1,15 @@
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,6 +4,7 @@ using InnovEnergy.Lib.S3Utils.DataTypes;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Text;
using System.Text.Json.Nodes;
using InnovEnergy.App.Backend.Database;
@ -165,6 +166,8 @@ public static class ExoCmd
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 method = $"access-key/{installation.S3Key}";
@ -248,7 +251,7 @@ public static class ExoCmd
return await s3Region.PutBucket(installation.BucketName()) != null;
}
public static async Task<Boolean> SendConfig(this Installation installation, String config)
public static async Task<Boolean> SendConfig(this Installation installation, Configuration config)
{
// This looks hacky but here we grab the vpn-Ip of the installation by its installation Name (e.g. Salimax0001)
@ -270,10 +273,48 @@ public static class ExoCmd
// return result.ExitCode == 200;
var maxRetransmissions = 2;
UdpClient udpClient = new UdpClient();
udpClient.Client.ReceiveTimeout = 2000;
int port = 9000;
var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Credentials!);
var url = s3Region.Bucket(installation.BucketName()).Path("config.json");
return await url.PutObject(config);
Console.WriteLine("Trying to reach installation with IP: " + installation.VpnIp);
//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(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);
}
public static async Task<Boolean> SendInstallationConfig(this Session? session, Int64 installationId, String configuration)
public static async Task<Boolean> SendInstallationConfig(this Session? session, Int64 installationId, Configuration configuration)
{
var user = session?.User;
var installation = Db.GetInstallationById(installationId);

View File

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

View File

@ -1,3 +1,4 @@
using System.Net.Http.Headers;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
@ -5,35 +6,28 @@ using System.Threading.Channels;
using Hellang.Middleware.ProblemDetails;
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.Websockets;
using InnovEnergy.Lib.S3Utils.DataTypes;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;
using InnovEnergy.Lib.Utils;
using RabbitMQ.Client;
namespace InnovEnergy.App.Backend;
public static class Program
{
public static void Main(String[] args)
{
//Db.CreateFakeRelations();
Watchdog.NotifyReady();
Db.Init();
var builder = WebApplication.CreateBuilder(args);
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();
RabbitMqManager.InitializeEnvironment();
RabbitMqManager.StartRabbitMqConsumer();
Console.WriteLine("Queue declared");
WebsocketManager.InformInstallationsToSubscribeToRabbitMq();
//WebsocketManager.InformInstallationsToSubscribeToRabbitMq();
WebsocketManager.MonitorInstallationTable();
builder.Services.AddControllers();

View File

@ -0,0 +1,193 @@
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,9 +13,6 @@ namespace InnovEnergy.App.Backend.Websockets;
public static class WebsocketManager
{
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()
{
@ -82,99 +79,7 @@ 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"
public static void InformWebsocketsForInstallation(int installationId)

View File

@ -1,9 +0,0 @@
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

@ -1,18 +0,0 @@
<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

@ -1,66 +0,0 @@
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

@ -1,88 +0,0 @@
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

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

View File

@ -1,129 +0,0 @@
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

@ -1 +0,0 @@
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

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

View File

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

View File

@ -0,0 +1,11 @@
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,6 +4,7 @@ using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Flurl.Http;
using InnovEnergy.App.SaliMax.Devices;
@ -50,7 +51,8 @@ internal static class Program
private static readonly Channel RelaysChannel ;
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 UdpClient _udpListener = null!;
@ -224,7 +226,7 @@ internal static class Program
var currentSalimaxState = GetSalimaxStateAlarm(record);
SendSalimaxStateAlarm(currentSalimaxState);
SendSalimaxStateAlarm(currentSalimaxState,record);
record.ControlConstants();
record.ControlSystemState();
@ -248,6 +250,8 @@ 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.FiError ? "Alert: Fi Error Detected" : "No Fi Error Detected") .WriteLine();
//record.ApplyConfigFile(minSoc:22, gridSetPoint:1);
record.Config.Save();
"===========================================".LogInfo();
@ -258,9 +262,11 @@ internal static class Program
// ReSharper disable once FunctionNeverReturns
}
private static void SendSalimaxStateAlarm(StatusMessage currentSalimaxState)
private static void SendSalimaxStateAlarm(StatusMessage currentSalimaxState, StatusRecord record)
{
var s3Bucket = Config.Load().S3?.Bucket;
//Every 15 iterations(30 seconds), the installation sends a heartbit message to the queue
_heartBitInterval++;
//When the controller boots, it tries to subscribe to the queue
@ -268,30 +274,26 @@ internal static class Program
{
_subscribeToQueueForTheFirstTime = true;
SubscribeToQueue(currentSalimaxState, s3Bucket);
}
//If already subscribed to the queue and the status has been changed, update the queue
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;
if (s3Bucket != null)
InformMiddleware(s3Bucket, currentSalimaxState);
InformMiddleware(currentSalimaxState);
}
else if (_subscribedToQueue && _heartBitInterval>=15)
{
//Send a heartbit to the backend
Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------");
_heartBitInterval = 0;
currentSalimaxState.Type = MessageType.Heartbit;
if (s3Bucket != null)
InformMiddleware(s3Bucket, currentSalimaxState);
InformMiddleware(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, apply the configuration file
if (_udpListener.Available > 0)
{
IPEndPoint? serverEndpoint = null;
@ -302,13 +304,16 @@ internal static class Program
var udpMessage = _udpListener.Receive(ref serverEndpoint);
var message = Encoding.UTF8.GetString(udpMessage);
Console.WriteLine($"Received a message: {message}");
Configuration config = JsonSerializer.Deserialize<Configuration>(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
_udpListener.Send(replyData, replyData.Length, serverEndpoint);
Console.WriteLine($"Replied to {serverEndpoint}: {replyMessage}");
SubscribeToQueue(currentSalimaxState, s3Bucket);
record.ApplyConfigFile(config);
}
}
@ -316,15 +321,24 @@ internal static class Program
{
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();
_channel = _connection.CreateModel();
_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");
if (s3Bucket != null) InformMiddleware(s3Bucket, currentSalimaxState);
if (s3Bucket != null) InformMiddleware(currentSalimaxState);
_subscribedToQueue = true;
}
@ -356,11 +370,8 @@ internal static class Program
return IPAddress.None;
}
private static void InformMiddleware(String? bucket, StatusMessage status)
private static void InformMiddleware(StatusMessage status)
{
//ddddddddd
int.TryParse(bucket[0].ToString(), out var installationId);
var message = JsonSerializer.Serialize(status);
var body = Encoding.UTF8.GetBytes(message);
@ -480,13 +491,13 @@ internal static class Program
? SalimaxAlarmState.Red
: salimaxAlarmsState; // this will be replaced by LedState
int.TryParse(s3Bucket[0].ToString(), out var installationId);
int.TryParse(s3Bucket?.Split("-")[0], out var installationId);
var returnedStatus = new StatusMessage
{
InstallationId = installationId,
Status = salimaxAlarmsState,
Type= MessageType.AlarmOrWarning,
Type = MessageType.AlarmOrWarning,
Alarms = alarmList,
Warnings = warningList
};
@ -712,6 +723,12 @@ internal static class Program
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
static double CalculateAverage( List<Double> data)

View File

@ -0,0 +1,8 @@
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 };
public required Double MinSoc { get; set; }
public required Boolean ForceCalibrationCharge { get; set; }
public required CalibrationChargeType ForceCalibrationCharge { get; set; }
public required Boolean DisplayIndividualBatteries { get; set; }
public required Double PConstant { 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()
{
MinSoc = 20,
ForceCalibrationCharge = false,
ForceCalibrationCharge = CalibrationChargeType.No,
DisplayIndividualBatteries = false,
PConstant = .5,
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()
{
MinSoc = 20,
ForceCalibrationCharge = false,
ForceCalibrationCharge = CalibrationChargeType.No,
DisplayIndividualBatteries = false,
PConstant = .5,
GridSetPoint = 0,

View File

@ -0,0 +1,16 @@
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,10 +1,28 @@
import { TopologyValues } from '../Log/graph.util';
import { Box, CardContent, Container, Grid, TextField } from '@mui/material';
import React from 'react';
import { ConfigurationValues, TopologyValues } from '../Log/graph.util';
import {
Alert,
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 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 {
values: TopologyValues;
id: number;
}
function Configuration(props: ConfigurationProps) {
@ -12,6 +30,110 @@ function Configuration(props: ConfigurationProps) {
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 (
<Container maxWidth="xl">
<Grid
@ -31,80 +153,114 @@ function Configuration(props: ConfigurationProps) {
noValidate
autoComplete="off"
>
<div>
<div style={{ marginBottom: '5px' }}>
<TextField
label={
<FormattedMessage
id="minimum_soc"
defaultMessage="Minimum SoC"
id="minimum_soc "
defaultMessage="Minimum SoC (%)"
/>
}
value={props.values.minimumSoC.values[0].value + ' %'}
name="minimumSoC"
value={formValues.minimumSoC}
onChange={handleChange}
helperText={
errors.minimumSoC ? (
<span style={{ color: 'red' }}>
Value should be between 0-100%
</span>
) : (
''
)
}
fullWidth
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="calibration_charge_forced"
defaultMessage="Calibration Charge forced"
/>
}
value={props.values.calibrationChargeForced.values[0].value}
<FormControl
fullWidth
sx={{ marginLeft: 1, marginBottom: '10px', width: 390 }}
>
<InputLabel
sx={{
fontSize: 14,
backgroundColor: 'transparent'
}}
>
<FormattedMessage
id="forced_calibration_charge"
defaultMessage="Forced Calibration Charge"
/>
</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 style={{ marginBottom: '5px' }}>
<TextField
label={
<FormattedMessage
id="grid_set_point"
defaultMessage="Grid Set Point"
defaultMessage="Grid Set Point (kW)"
/>
}
value={
(
(props.values.gridSetPoint.values[0].value as number) /
1000
).toString() + ' kW'
name="gridSetPoint"
value={formValues.gridSetPoint}
onChange={handleChange}
helperText={
errors.gridSetPoint ? (
<span style={{ color: 'red' }}>
Please provide a valid number
</span>
) : (
''
)
}
fullWidth
/>
</div>
<div>
<div style={{ marginBottom: '5px' }}>
<TextField
label={
<FormattedMessage
id="Installed_Power_DC1010"
defaultMessage="Installed Power DC1010"
defaultMessage="Installed Power DC1010 (kW)"
/>
}
value={
(
(props.values.installedDcDcPower.values[0]
.value as number) * 10
).toString() + ' kW'
}
fullWidth
/>
</div>
<div>
<div style={{ marginBottom: '5px' }}>
<TextField
label={
<FormattedMessage
id="Maximum_Discharge_Power"
defaultMessage="Maximum Discharge Power"
defaultMessage="Maximum Discharge Power (W)"
/>
}
value={
(
(props.values.maximumDischargePower.values[0]
.value as number) *
48 *
(props.values.DcDcNum.values[0].value as number)
).toString() + ' W'
}
fullWidth
/>
@ -121,6 +277,95 @@ function Configuration(props: ConfigurationProps) {
fullWidth
/>
</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>
</CardContent>
</Grid>

View File

@ -129,16 +129,6 @@ function Installation(props: singleInstallationProps) {
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 now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
const date = now.toDate();
@ -146,48 +136,57 @@ function Installation(props: singleInstallationProps) {
try {
const res = await fetchData(now, s3Credentials);
if (!isMounted) {
return;
}
// if (!isMounted) {
// return false;
// }
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);
if (res != FetchResult.notAvailable && res != FetchResult.tryLater) {
setValues(
extractValues({
time: now,
value: res
})
);
return true;
}
} catch (err) {
setErrorLoadingS3Data(true);
return false;
}
};
const interval = setInterval(fetchDataPeriodically, 2000);
const fetchDataOnlyOneTime = async () => {
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
return () => {
isMounted = false;
//isMounted = false;
if (currentTab == 'live') {
clearInterval(interval);
}
};
}
}, [installationId, currentTab]);
@ -662,7 +661,10 @@ function Installation(props: singleInstallationProps) {
<Overview s3Credentials={s3Credentials}></Overview>
)}
{currentTab === 'configuration' && currentUser.hasWriteAccess && (
<Configuration values={values}></Configuration>
<Configuration
values={values}
id={installationId}
></Configuration>
)}
{currentTab === 'manage' && currentUser.hasWriteAccess && (
<AccessContextProvider>

View File

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

View File

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

View File

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

View File

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