Added configuration control from front-end

This commit is contained in:
Noe 2023-12-05 13:14:01 +01:00
parent 3a0c96fe23
commit 5d14b61d9c
10 changed files with 423 additions and 227 deletions

View File

@ -518,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);

View File

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

View File

@ -4,6 +4,7 @@ using InnovEnergy.Lib.S3Utils.DataTypes;
using System.Diagnostics.CodeAnalysis; using System.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;
@ -248,7 +249,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 +271,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);
} }

View File

@ -66,7 +66,7 @@ public static class SessionMethods
.Apply(Db.Update); .Apply(Db.Update);
} }
public static async Task<Boolean> SendInstallationConfig(this Session? session, Int64 installationId, 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);

View File

@ -27,45 +27,6 @@ public static class RabbitMqManager
//string vpnServerIp = "194.182.190.208"; //string vpnServerIp = "194.182.190.208";
string vpnServerIp = "10.2.0.11"; 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 Factory = new ConnectionFactory
{ {
HostName = vpnServerIp, HostName = vpnServerIp,
@ -73,81 +34,9 @@ public static class RabbitMqManager
VirtualHost = "/", VirtualHost = "/",
UserName = "consumer", UserName = "consumer",
Password = "faceaddb5005815199f8366d3d15ff8a", 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(); Connection = Factory.CreateConnection();
Channel = Connection.CreateModel(); Channel = Connection.CreateModel();
Console.WriteLine("Middleware subscribed to RabbitMQ queue, ready for receiving messages"); Console.WriteLine("Middleware subscribed to RabbitMQ queue, ready for receiving messages");

View File

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

View File

@ -226,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();
@ -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.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.ApplyConfigFile(minSoc:22, gridSetPoint:1);
record.Config.Save(); record.Config.Save();
@ -262,7 +262,7 @@ 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;
@ -293,7 +293,7 @@ internal static class Program
InformMiddleware(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;
@ -304,13 +304,16 @@ 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);
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 // 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);
} }
} }
@ -327,23 +330,7 @@ internal static class Program
VirtualHost = "/", VirtualHost = "/",
UserName = "producer", UserName = "producer",
Password = "b187ceaddb54d5485063ddc1d41af66f", 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(); _connection = _factory.CreateConnection();
@ -736,10 +723,11 @@ internal static class Program
return value == "/Battery/Dc/Power"; 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.MinSoc = config.MinimumSoC;
status.Config.GridSetPoint = gridSetPoint; 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

View File

@ -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={ fullWidth
sx={{ marginLeft: 1, marginBottom: '10px', width: 390 }}
>
<InputLabel
sx={{
fontSize: 14,
backgroundColor: 'transparent'
}}
>
<FormattedMessage <FormattedMessage
id="forced_calibration_charge" id="forced_calibration_charge"
defaultMessage="Forced Calibration Charge" defaultMessage="Forced Calibration Charge"
/> />
} </InputLabel>
value={props.values.calibrationChargeForced.values[0].value} <Select
fullWidth 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>

View File

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

View File

@ -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;