Added configuration control from front-end
This commit is contained in:
parent
3a0c96fe23
commit
5d14b61d9c
|
@ -518,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);
|
||||
|
||||
|
|
|
@ -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.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using InnovEnergy.App.Backend.Database;
|
||||
|
@ -248,7 +249,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 +271,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);
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -27,45 +27,6 @@ public static class RabbitMqManager
|
|||
//string vpnServerIp = "194.182.190.208";
|
||||
string vpnServerIp = "10.2.0.11";
|
||||
|
||||
// ConnectionFactory factory = new ConnectionFactory();
|
||||
// factory.HostName = vpnServerIp;
|
||||
// factory.AutomaticRecoveryEnabled = true;
|
||||
// //factory.UserName = "";
|
||||
// //factory.Password = "";
|
||||
// factory.VirtualHost = "/";
|
||||
// factory.Port = 5672;
|
||||
//
|
||||
// //factory.AuthMechanisms = new IAuthMechanismFactory[] { new ExternalMechanismFactory() };
|
||||
//
|
||||
// System.Diagnostics.Debug.WriteLine("2 ");
|
||||
//
|
||||
// X509Certificate2Collection certCollection = new X509Certificate2Collection();
|
||||
// X509Certificate2 certificate = new X509Certificate2("/etc/rabbitmq/testca/ca_certificate.pem");
|
||||
// certCollection.Add(certificate);
|
||||
//
|
||||
// factory.Ssl.Certs = certCollection;
|
||||
// factory.Ssl.Enabled = true;
|
||||
// factory.Ssl.ServerName = "Webserver-FrontAndBack";
|
||||
// factory.Ssl.Version = SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13;
|
||||
// factory.Ssl.AcceptablePolicyErrors = SslPolicyErrors.RemoteCertificateChainErrors;
|
||||
// factory.Ssl.CertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
|
||||
// {
|
||||
// if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors)
|
||||
// {
|
||||
// // Log or debug information about the chain
|
||||
// foreach (var chainElement in chain.ChainElements)
|
||||
// {
|
||||
// Console.WriteLine($"Element Subject: {chainElement.Certificate.Subject}");
|
||||
// Console.WriteLine($"Element Issuer: {chainElement.Certificate.Issuer}");
|
||||
// // Add more details as needed
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Your custom validation logic
|
||||
// return sslPolicyErrors == SslPolicyErrors.None;
|
||||
// };
|
||||
|
||||
|
||||
Factory = new ConnectionFactory
|
||||
{
|
||||
HostName = vpnServerIp,
|
||||
|
@ -73,81 +34,9 @@ public static class RabbitMqManager
|
|||
VirtualHost = "/",
|
||||
UserName = "consumer",
|
||||
Password = "faceaddb5005815199f8366d3d15ff8a",
|
||||
//AuthMechanisms = new IAuthMechanismFactory[] { new ExternalMechanismFactory() },
|
||||
|
||||
// Ssl = new SslOption
|
||||
// {
|
||||
// Enabled = true,
|
||||
// ServerName = "Webserver-FrontAndBack",
|
||||
// //Roots = new X509Certificate2Collection { caCertificate },
|
||||
// //CertPath = "/etc/rabbitmq/testca/ca_certificate.pem",
|
||||
// CertPath = "/etc/rabbitmq/client/client_certificate.pem",
|
||||
//
|
||||
//
|
||||
// // CertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
|
||||
// // {
|
||||
// // //X509Certificate2 clientCertificate = new X509Certificate2("/etc/rabbitmq/client/client_certificate.pem");
|
||||
// //
|
||||
// // //X509Certificate2 caCertificate = new X509Certificate2("/etc/openvpn/client/ca-certificate");
|
||||
// // X509Certificate2 caCertificate = new X509Certificate2("/etc/rabbitmq/testca/ca_certificate.pem");
|
||||
// //
|
||||
// //
|
||||
// //
|
||||
// // Console.WriteLine(certificate.Subject);
|
||||
// // Console.WriteLine("---------------------------------");
|
||||
// // //Console.WriteLine(certificate.GetPublicKey());
|
||||
// // // Your custom validation logic using the CA certificate
|
||||
// // // Return true if the certificate is valid, false otherwise
|
||||
// // return certificate.Issuer == caCertificate.Subject;
|
||||
// // }
|
||||
// CertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
|
||||
// {
|
||||
// X509Certificate2 caCertificate = new X509Certificate2("/etc/rabbitmq/testca/ca_certificate.pem");
|
||||
//
|
||||
// // Add the CA certificate to the chain policy's extra store
|
||||
// chain.ChainPolicy.ExtraStore.Add(caCertificate);
|
||||
//
|
||||
// // Check if the chain builds successfully
|
||||
// bool chainIsValid = chain.Build((X509Certificate2)certificate);
|
||||
//
|
||||
// if (!chainIsValid)
|
||||
// {
|
||||
// Console.WriteLine("Certificate chain validation failed:");
|
||||
//
|
||||
// // Print details of each chain status
|
||||
// foreach (var chainStatus in chain.ChainStatus)
|
||||
// {
|
||||
// Console.WriteLine($"Chain Status: {chainStatus.Status}");
|
||||
// Console.WriteLine($"Chain Status Information: {chainStatus.StatusInformation}");
|
||||
// // Add more details as needed
|
||||
// // Check if the failure is due to UntrustedRoot
|
||||
// if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot)
|
||||
// {
|
||||
// // Manually check if the root certificate is the expected one
|
||||
// if (certificate.Issuer == caCertificate.Subject)
|
||||
// {
|
||||
// Console.WriteLine("Manually trusting the root certificate.");
|
||||
// chainIsValid = true;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// }
|
||||
//
|
||||
// // Additional validation logic if needed
|
||||
// Console.WriteLine($"Certificate Subject: {certificate.Subject}"+chainIsValid);
|
||||
//
|
||||
// // Return true if the certificate is valid
|
||||
// return chainIsValid;
|
||||
// }
|
||||
|
||||
|
||||
//}
|
||||
};
|
||||
|
||||
|
||||
|
||||
Connection = Factory.CreateConnection();
|
||||
Channel = Connection.CreateModel();
|
||||
Console.WriteLine("Middleware subscribed to RabbitMQ queue, ready for receiving messages");
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -226,7 +226,7 @@ internal static class Program
|
|||
|
||||
var currentSalimaxState = GetSalimaxStateAlarm(record);
|
||||
|
||||
SendSalimaxStateAlarm(currentSalimaxState);
|
||||
SendSalimaxStateAlarm(currentSalimaxState,record);
|
||||
|
||||
record.ControlConstants();
|
||||
record.ControlSystemState();
|
||||
|
@ -250,7 +250,7 @@ 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.ApplyConfigFile(minSoc:22, gridSetPoint:1);
|
||||
|
||||
record.Config.Save();
|
||||
|
||||
|
@ -262,7 +262,7 @@ 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;
|
||||
|
||||
|
@ -293,7 +293,7 @@ internal static class Program
|
|||
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;
|
||||
|
@ -304,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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -327,23 +330,7 @@ internal static class Program
|
|||
VirtualHost = "/",
|
||||
UserName = "producer",
|
||||
Password = "b187ceaddb54d5485063ddc1d41af66f",
|
||||
// Ssl = new SslOption
|
||||
// {
|
||||
// Enabled = true,
|
||||
// ServerName = VpnServerIp, // Disable hostname validation
|
||||
// CertPath = "/etc/openvpn/client/client-certificate",
|
||||
//
|
||||
// CertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
|
||||
// {
|
||||
// X509Certificate2 caCertificate = new X509Certificate2("/etc/openvpn/client/ca-certificate");
|
||||
// //Console.WriteLine(caCertificate);
|
||||
// //Console.WriteLine("---------------------------------");
|
||||
// //Console.WriteLine(certificate.GetPublicKey());
|
||||
// // Your custom validation logic using the CA certificate
|
||||
// // Return true if the certificate is valid, false otherwise
|
||||
// return certificate.Issuer == caCertificate.Subject;
|
||||
// }
|
||||
// }
|
||||
|
||||
};
|
||||
|
||||
_connection = _factory.CreateConnection();
|
||||
|
@ -736,10 +723,11 @@ internal static class Program
|
|||
return value == "/Battery/Dc/Power";
|
||||
}
|
||||
|
||||
private static void ApplyConfigFile(this StatusRecord status, Double minSoc, Double gridSetPoint)
|
||||
private static void ApplyConfigFile(this StatusRecord status, Configuration config)
|
||||
{
|
||||
status.Config.MinSoc = minSoc;
|
||||
status.Config.GridSetPoint = gridSetPoint;
|
||||
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
|
||||
|
|
|
@ -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={
|
||||
<FormControl
|
||||
fullWidth
|
||||
sx={{ marginLeft: 1, marginBottom: '10px', width: 390 }}
|
||||
>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
backgroundColor: 'transparent'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="forced_calibration_charge"
|
||||
defaultMessage="Forced Calibration Charge"
|
||||
/>
|
||||
}
|
||||
value={props.values.calibrationChargeForced.values[0].value}
|
||||
fullWidth
|
||||
/>
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={selectedForcedCalibrationChargeOption}
|
||||
onChange={handleSelectedCalibrationChargeChange}
|
||||
open={openForcedCalibrationCharge}
|
||||
onClose={handleCloseForcedCalibrationCharge}
|
||||
onOpen={handleOpenForcedCalibrationCharge}
|
||||
//renderValue={selectedForcedCalibrationChargeOption}
|
||||
>
|
||||
{forcedCalibrationChargeOptions.map((option) => (
|
||||
<MenuItem key={option} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<div 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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue