Compare commits
15 Commits
05d7f91ec5
...
a04804077c
Author | SHA1 | Date |
---|---|---|
Noe | a04804077c | |
Noe | 5d14b61d9c | |
atef | 3a0c96fe23 | |
atef | 3b02b46b78 | |
Noe | 980089d7e0 | |
Noe | 30e4991032 | |
atef | 5bd881ea8f | |
atef | 9840e0c9f8 | |
Noe | e143a4be6e | |
Noe | da7f75c55c | |
atef | 0f9a4ddc4e | |
atef | 9cd85f1d31 | |
atef | 825a6c87bd | |
atef | 732274933b | |
atef | fce689d551 |
|
@ -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.
|
||||||
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.
|
You can add them into the Gitea Actions Pipeline, read the documentation on Github-actions and integration tests.
|
||||||
|
|
||||||
Runner:
|
Runner:
|
||||||
|
|
|
@ -326,11 +326,20 @@ 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)
|
return Unauthorized() ;
|
||||||
? newUser.HidePassword()
|
|
||||||
: Unauthorized() ;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost(nameof(CreateInstallation))]
|
[HttpPost(nameof(CreateInstallation))]
|
||||||
|
@ -509,9 +518,10 @@ public class Controller : ControllerBase
|
||||||
|
|
||||||
|
|
||||||
[HttpPost(nameof(EditInstallationConfig))]
|
[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);
|
var session = Db.GetSession(authToken);
|
||||||
|
//Console.WriteLine(config.GridSetPoint);
|
||||||
|
|
||||||
//var installationToUpdate = Db.GetInstallationById(installationId);
|
//var installationToUpdate = Db.GetInstallationById(installationId);
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ 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;
|
||||||
|
@ -165,6 +166,8 @@ 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}";
|
||||||
|
|
||||||
|
@ -248,7 +251,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, 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)
|
// 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;
|
// 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!);
|
Console.WriteLine("Trying to reach installation with IP: " + installation.VpnIp);
|
||||||
var url = s3Region.Bucket(installation.BucketName()).Path("config.json");
|
//Try at most MAX_RETRANSMISSIONS times to reach an installation.
|
||||||
return await url.PutObject(config);
|
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);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, String configuration)
|
public static async Task<Boolean> SendInstallationConfig(this Session? session, Int64 installationId, Configuration configuration)
|
||||||
{
|
{
|
||||||
var user = session?.User;
|
var user = session?.User;
|
||||||
var installation = Db.GetInstallationById(installationId);
|
var installation = Db.GetInstallationById(installationId);
|
||||||
|
|
|
@ -181,6 +181,7 @@ 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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
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;
|
||||||
|
@ -5,35 +6,28 @@ 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();
|
||||||
string vpnServerIp = "194.182.190.208";
|
RabbitMqManager.StartRabbitMqConsumer();
|
||||||
//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();
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -13,10 +13,7 @@ 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()
|
||||||
{
|
{
|
||||||
var installationIps = Db.Installations.Select(inst => inst.VpnIp).ToList();
|
var installationIps = Db.Installations.Select(inst => inst.VpnIp).ToList();
|
||||||
|
@ -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"
|
//Inform all the connected websockets regarding installation "installationId"
|
||||||
public static void InformWebsocketsForInstallation(int installationId)
|
public static void InformWebsocketsForInstallation(int installationId)
|
||||||
|
|
|
@ -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>();
|
|
||||||
}
|
|
|
@ -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>
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
|
|
||||||
namespace InnovEnergy.App.Middleware;
|
|
||||||
|
|
||||||
public class StatusMessage
|
|
||||||
{
|
|
||||||
public required int InstallationId { get; init; }
|
|
||||||
public required int Status { get; init; }
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
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;
|
||||||
|
@ -187,18 +188,24 @@ 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;
|
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;
|
return mustDoCalibrationCharge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static Double ControlGridPower(this StatusRecord status, Double targetPower)
|
private static Double ControlGridPower(this StatusRecord status, Double targetPower)
|
||||||
{
|
{
|
||||||
return ControlPower
|
return ControlPower
|
||||||
(
|
(
|
||||||
|
|
|
@ -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; init; }
|
public required Config Config { get; set; }
|
||||||
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
|
||||||
|
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ 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;
|
||||||
|
@ -50,8 +51,9 @@ 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!;
|
||||||
private static ConnectionFactory? _factory ;
|
private static ConnectionFactory? _factory ;
|
||||||
|
@ -59,7 +61,7 @@ internal static class Program
|
||||||
private static IModel? _channel;
|
private static IModel? _channel;
|
||||||
private static Boolean _subscribedToQueue = false;
|
private static Boolean _subscribedToQueue = false;
|
||||||
private static Boolean _subscribeToQueueForTheFirstTime = false;
|
private static Boolean _subscribeToQueueForTheFirstTime = false;
|
||||||
private static SalimaxAlarmState _prevSalimaxState = SalimaxAlarmState.Green;
|
private static SalimaxAlarmState _prevSalimaxState = SalimaxAlarmState.Green;
|
||||||
private static Int32 _heartBitInterval = 0;
|
private static Int32 _heartBitInterval = 0;
|
||||||
|
|
||||||
static Program()
|
static Program()
|
||||||
|
@ -224,7 +226,7 @@ internal static class Program
|
||||||
|
|
||||||
var currentSalimaxState = GetSalimaxStateAlarm(record);
|
var currentSalimaxState = GetSalimaxStateAlarm(record);
|
||||||
|
|
||||||
SendSalimaxStateAlarm(currentSalimaxState);
|
SendSalimaxStateAlarm(currentSalimaxState,record);
|
||||||
|
|
||||||
record.ControlConstants();
|
record.ControlConstants();
|
||||||
record.ControlSystemState();
|
record.ControlSystemState();
|
||||||
|
@ -247,7 +249,9 @@ 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();
|
||||||
|
@ -258,9 +262,11 @@ internal static class Program
|
||||||
// ReSharper disable once FunctionNeverReturns
|
// ReSharper disable once FunctionNeverReturns
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SendSalimaxStateAlarm(StatusMessage currentSalimaxState)
|
private static void SendSalimaxStateAlarm(StatusMessage currentSalimaxState, StatusRecord record)
|
||||||
{
|
{
|
||||||
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
|
||||||
|
@ -268,30 +274,26 @@ internal static class Program
|
||||||
{
|
{
|
||||||
_subscribeToQueueForTheFirstTime = true;
|
_subscribeToQueueForTheFirstTime = true;
|
||||||
SubscribeToQueue(currentSalimaxState, s3Bucket);
|
SubscribeToQueue(currentSalimaxState, s3Bucket);
|
||||||
|
|
||||||
if (_subscribedToQueue && currentSalimaxState.Status != _prevSalimaxState)
|
|
||||||
{
|
|
||||||
_prevSalimaxState = currentSalimaxState.Status;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//If already subscribed to the queue and the status has been changed, update the queue
|
//If already subscribed to the queue and the status has been changed, update the queue
|
||||||
else if (_subscribedToQueue && currentSalimaxState.Status != _prevSalimaxState)
|
if (_subscribedToQueue && currentSalimaxState.Status != _prevSalimaxState)
|
||||||
{
|
{
|
||||||
_prevSalimaxState = currentSalimaxState.Status;
|
_prevSalimaxState = currentSalimaxState.Status;
|
||||||
if (s3Bucket != null)
|
if (s3Bucket != null)
|
||||||
InformMiddleware(s3Bucket, currentSalimaxState);
|
InformMiddleware(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(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)
|
if (_udpListener.Available > 0)
|
||||||
{
|
{
|
||||||
IPEndPoint? serverEndpoint = null;
|
IPEndPoint? serverEndpoint = null;
|
||||||
|
@ -301,14 +303,17 @@ 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}");
|
||||||
|
|
||||||
SubscribeToQueue(currentSalimaxState, s3Bucket);
|
record.ApplyConfigFile(config);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,15 +321,24 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -356,11 +370,8 @@ internal static class Program
|
||||||
return IPAddress.None;
|
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 message = JsonSerializer.Serialize(status);
|
||||||
var body = Encoding.UTF8.GetBytes(message);
|
var body = Encoding.UTF8.GetBytes(message);
|
||||||
|
|
||||||
|
@ -480,15 +491,15 @@ 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[0].ToString(), out var installationId);
|
int.TryParse(s3Bucket?.Split("-")[0], 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
|
||||||
};
|
};
|
||||||
|
|
||||||
return returnedStatus;
|
return returnedStatus;
|
||||||
|
@ -712,7 +723,13 @@ 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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace InnovEnergy.App.SaliMax.SystemConfig;
|
||||||
|
|
||||||
|
public enum CalibrationChargeType
|
||||||
|
{
|
||||||
|
No,
|
||||||
|
UntilEoc,
|
||||||
|
Yes
|
||||||
|
}
|
|
@ -14,25 +14,25 @@ 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 Boolean ForceCalibrationCharge { get; set; }
|
public required CalibrationChargeType 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; }
|
||||||
public required Double BatterySelfDischargePower { get; set; }
|
public required Double BatterySelfDischargePower { get; set; }
|
||||||
public required Double HoldSocZone { get; set; }
|
public required Double HoldSocZone { get; set; }
|
||||||
public required DevicesConfig IslandMode { get; set; }
|
public required DevicesConfig IslandMode { get; set; }
|
||||||
public required DevicesConfig GridTie { get; set; }
|
public required DevicesConfig GridTie { get; set; }
|
||||||
|
|
||||||
public required Double MaxBatteryChargingCurrent { get; set; }
|
public required Double MaxBatteryChargingCurrent { get; set; }
|
||||||
public required Double MaxBatteryDischargingCurrent { get; set; }
|
public required Double MaxBatteryDischargingCurrent { get; set; }
|
||||||
public required Double MaxDcPower { get; set; }
|
public required Double MaxDcPower { get; set; }
|
||||||
|
|
||||||
public required Double MaxChargeBatteryVoltage { get; set; }
|
public required Double MaxChargeBatteryVoltage { get; set; }
|
||||||
public required Double MinDischargeBatteryVoltage { get; set; }
|
public required Double MinDischargeBatteryVoltage { get; set; }
|
||||||
|
|
||||||
public required DeviceConfig Devices { get; set; }
|
public required DeviceConfig Devices { get; set; }
|
||||||
public required S3Config? S3 { get; set; }
|
public required S3Config? S3 { get; set; }
|
||||||
|
|
||||||
private static String? LastSavedData { get; set; }
|
private static String? LastSavedData { 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 = false,
|
ForceCalibrationCharge = CalibrationChargeType.No,
|
||||||
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 = false,
|
ForceCalibrationCharge = CalibrationChargeType.No,
|
||||||
DisplayIndividualBatteries = false,
|
DisplayIndividualBatteries = false,
|
||||||
PConstant = .5,
|
PConstant = .5,
|
||||||
GridSetPoint = 0,
|
GridSetPoint = 0,
|
||||||
|
|
|
@ -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; }
|
||||||
|
}
|
|
@ -1,10 +1,28 @@
|
||||||
import { TopologyValues } from '../Log/graph.util';
|
import { ConfigurationValues, TopologyValues } from '../Log/graph.util';
|
||||||
import { Box, CardContent, Container, Grid, TextField } from '@mui/material';
|
import {
|
||||||
import React from 'react';
|
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 { 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) {
|
||||||
|
@ -12,6 +30,110 @@ 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
|
||||||
|
@ -31,80 +153,114 @@ function Configuration(props: ConfigurationProps) {
|
||||||
noValidate
|
noValidate
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
>
|
>
|
||||||
<div>
|
<div style={{ marginBottom: '5px' }}>
|
||||||
<TextField
|
<TextField
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="minimum_soc"
|
id="minimum_soc "
|
||||||
defaultMessage="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
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<TextField
|
<FormControl
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id="calibration_charge_forced"
|
|
||||||
defaultMessage="Calibration Charge forced"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value={props.values.calibrationChargeForced.values[0].value}
|
|
||||||
fullWidth
|
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>
|
|
||||||
|
<div style={{ marginBottom: '5px' }}>
|
||||||
<TextField
|
<TextField
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="grid_set_point"
|
id="grid_set_point"
|
||||||
defaultMessage="Grid Set Point"
|
defaultMessage="Grid Set Point (kW)"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
value={
|
name="gridSetPoint"
|
||||||
(
|
value={formValues.gridSetPoint}
|
||||||
(props.values.gridSetPoint.values[0].value as number) /
|
onChange={handleChange}
|
||||||
1000
|
helperText={
|
||||||
).toString() + ' kW'
|
errors.gridSetPoint ? (
|
||||||
|
<span style={{ color: 'red' }}>
|
||||||
|
Please provide a valid number
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
}
|
}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div style={{ marginBottom: '5px' }}>
|
||||||
<TextField
|
<TextField
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="Installed_Power_DC1010"
|
id="Installed_Power_DC1010"
|
||||||
defaultMessage="Installed Power DC1010"
|
defaultMessage="Installed Power DC1010 (kW)"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
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>
|
<div style={{ marginBottom: '5px' }}>
|
||||||
<TextField
|
<TextField
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="Maximum_Discharge_Power"
|
id="Maximum_Discharge_Power"
|
||||||
defaultMessage="Maximum Discharge Power"
|
defaultMessage="Maximum Discharge Power (W)"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
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
|
||||||
/>
|
/>
|
||||||
|
@ -121,6 +277,95 @@ 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>
|
||||||
|
|
|
@ -129,65 +129,64 @@ function Installation(props: singleInstallationProps) {
|
||||||
|
|
||||||
const s3Credentials = { s3Bucket, ...S3data };
|
const s3Credentials = { s3Bucket, ...S3data };
|
||||||
|
|
||||||
|
const fetchDataPeriodically = async () => {
|
||||||
|
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
|
||||||
|
const date = now.toDate();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetchData(now, s3Credentials);
|
||||||
|
|
||||||
|
// if (!isMounted) {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (res != FetchResult.notAvailable && res != FetchResult.tryLater) {
|
||||||
|
setValues(
|
||||||
|
extractValues({
|
||||||
|
time: now,
|
||||||
|
value: res
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDataOnlyOneTime = async () => {
|
||||||
|
let success = false;
|
||||||
|
while (true) {
|
||||||
|
success = await fetchDataPeriodically();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
if (success) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
installationId == props.current_installation.id &&
|
installationId == props.current_installation.id &&
|
||||||
(currentTab == 'live' || currentTab == 'configuration')
|
(currentTab == 'live' || currentTab == 'configuration')
|
||||||
) {
|
) {
|
||||||
let isMounted = true;
|
//let isMounted = true;
|
||||||
setFormValues(props.current_installation);
|
setFormValues(props.current_installation);
|
||||||
setErrorLoadingS3Data(false);
|
var interval;
|
||||||
let disconnectedStatusResult = [];
|
|
||||||
|
|
||||||
const fetchDataPeriodically = async () => {
|
if (currentTab == 'live') {
|
||||||
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
|
interval = setInterval(fetchDataPeriodically, 2000);
|
||||||
const date = now.toDate();
|
}
|
||||||
|
if (currentTab == 'configuration') {
|
||||||
try {
|
fetchDataOnlyOneTime();
|
||||||
const res = await fetchData(now, s3Credentials);
|
}
|
||||||
|
|
||||||
if (!isMounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
extractValues({
|
|
||||||
time: now,
|
|
||||||
value: res
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setErrorLoadingS3Data(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const interval = setInterval(fetchDataPeriodically, 2000);
|
|
||||||
|
|
||||||
// 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;
|
||||||
clearInterval(interval);
|
if (currentTab == 'live') {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [installationId, currentTab]);
|
}, [installationId, currentTab]);
|
||||||
|
@ -662,7 +661,10 @@ function Installation(props: singleInstallationProps) {
|
||||||
<Overview s3Credentials={s3Credentials}></Overview>
|
<Overview s3Credentials={s3Credentials}></Overview>
|
||||||
)}
|
)}
|
||||||
{currentTab === 'configuration' && currentUser.hasWriteAccess && (
|
{currentTab === 'configuration' && currentUser.hasWriteAccess && (
|
||||||
<Configuration values={values}></Configuration>
|
<Configuration
|
||||||
|
values={values}
|
||||||
|
id={installationId}
|
||||||
|
></Configuration>
|
||||||
)}
|
)}
|
||||||
{currentTab === 'manage' && currentUser.hasWriteAccess && (
|
{currentTab === 'manage' && currentUser.hasWriteAccess && (
|
||||||
<AccessContextProvider>
|
<AccessContextProvider>
|
||||||
|
|
|
@ -80,7 +80,7 @@ function installationForm(props: installationFormProps) {
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: isMobile ? '50%' : '30%',
|
top: isMobile ? '50%' : '40%',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
width: 500,
|
width: 500,
|
||||||
|
@ -152,10 +152,11 @@ 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
|
||||||
|
|
|
@ -34,6 +34,12 @@ 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;
|
||||||
|
|
|
@ -95,7 +95,7 @@ function UsersSearch() {
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="successfullyCreatedUser"
|
id="successfullyCreatedUser"
|
||||||
defaultMessage="Successfully Updated User"
|
defaultMessage="Successfully Created User"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
|
@ -215,7 +215,7 @@ function userForm(props: userFormProps) {
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: isMobile ? '50%' : '30%',
|
top: isMobile ? '50%' : '40%',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
width: 500,
|
width: 500,
|
||||||
|
|
Loading…
Reference in New Issue