diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index d74099345..cd8091d79 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -518,9 +518,10 @@ public class Controller : ControllerBase [HttpPost(nameof(EditInstallationConfig))] - public async Task>> EditInstallationConfig([FromBody] String config, Int64 installationId, Token authToken) + public async Task>> EditInstallationConfig([FromBody] Configuration config, Int64 installationId, Token authToken) { var session = Db.GetSession(authToken); + //Console.WriteLine(config.GridSetPoint); //var installationToUpdate = Db.GetInstallationById(installationId); diff --git a/csharp/App/Backend/DataTypes/Configuration.cs b/csharp/App/Backend/DataTypes/Configuration.cs new file mode 100644 index 000000000..f9e4eeceb --- /dev/null +++ b/csharp/App/Backend/DataTypes/Configuration.cs @@ -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 +} \ No newline at end of file diff --git a/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs b/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs index 9ab2ecf54..c03010eed 100644 --- a/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs +++ b/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs @@ -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 SendConfig(this Installation installation, String config) + public static async Task 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(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); } diff --git a/csharp/App/Backend/DataTypes/Methods/Session.cs b/csharp/App/Backend/DataTypes/Methods/Session.cs index 38cadb339..4b01788ba 100644 --- a/csharp/App/Backend/DataTypes/Methods/Session.cs +++ b/csharp/App/Backend/DataTypes/Methods/Session.cs @@ -66,7 +66,7 @@ public static class SessionMethods .Apply(Db.Update); } - public static async Task SendInstallationConfig(this Session? session, Int64 installationId, String configuration) + public static async Task SendInstallationConfig(this Session? session, Int64 installationId, Configuration configuration) { var user = session?.User; var installation = Db.GetInstallationById(installationId); diff --git a/csharp/App/Backend/Websockets/RabbitMQManager.cs b/csharp/App/Backend/Websockets/RabbitMQManager.cs index 3c74a7d6c..66ccd1e39 100644 --- a/csharp/App/Backend/Websockets/RabbitMQManager.cs +++ b/csharp/App/Backend/Websockets/RabbitMQManager.cs @@ -26,45 +26,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 { @@ -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"); diff --git a/csharp/App/SaliMax/src/MiddlewareClasses/Configuration.cs b/csharp/App/SaliMax/src/MiddlewareClasses/Configuration.cs new file mode 100644 index 000000000..0a3fd986c --- /dev/null +++ b/csharp/App/SaliMax/src/MiddlewareClasses/Configuration.cs @@ -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; } +} + diff --git a/csharp/App/SaliMax/src/Program.cs b/csharp/App/SaliMax/src/Program.cs index fa6499224..dc805a914 100644 --- a/csharp/App/SaliMax/src/Program.cs +++ b/csharp/App/SaliMax/src/Program.cs @@ -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; @@ -303,14 +303,17 @@ internal static class Program var udpMessage = _udpListener.Receive(ref serverEndpoint); var message = Encoding.UTF8.GetString(udpMessage); + + Configuration config = JsonSerializer.Deserialize(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 _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 diff --git a/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx b/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx index e93a7aa98..9cd33a976 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx @@ -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({ + 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( + 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 ( -
+
} - value={props.values.minimumSoC.values[0].value + ' %'} + name="minimumSoC" + value={formValues.minimumSoC} + onChange={handleChange} + helperText={ + errors.minimumSoC ? ( + + Value should be between 0-100% + + ) : ( + '' + ) + } fullWidth />
- + - } - value={props.values.calibrationChargeForced.values[0].value} - fullWidth - /> + + +
-
+ +
} - value={ - ( - (props.values.gridSetPoint.values[0].value as number) / - 1000 - ).toString() + ' kW' + name="gridSetPoint" + value={formValues.gridSetPoint} + onChange={handleChange} + helperText={ + errors.gridSetPoint ? ( + + Please provide a valid number + + ) : ( + '' + ) } fullWidth />
-
+
} value={ - ( - (props.values.installedDcDcPower.values[0] - .value as number) * 10 - ).toString() + ' kW' + (props.values.installedDcDcPower.values[0] + .value as number) * 10 } fullWidth />
-
+
} value={ - ( - (props.values.maximumDischargePower.values[0] - .value as number) * - 48 * - (props.values.DcDcNum.values[0].value as number) - ).toString() + ' W' + (props.values.maximumDischargePower.values[0] + .value as number) * + 48 * + (props.values.DcDcNum.values[0].value as number) } fullWidth /> @@ -121,6 +277,95 @@ function Configuration(props: ConfigurationProps) { fullWidth />
+ +
+ + {loading && ( + + )} + {error && ( + + An error has occurred + setError(false)} // Set error state to false on click + sx={{ marginLeft: '4px' }} + > + + + + )} + {updated && ( + + Successfully applied configuration file + setUpdated(false)} + sx={{ marginLeft: '4px' }} + > + + + + )} + {error && ( + + An error has occurred + setError(false)} + sx={{ marginLeft: '4px' }} + > + + + + )} +
diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx index 7c0e454ce..32379ef0d 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx @@ -129,65 +129,64 @@ function Installation(props: singleInstallationProps) { 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(() => { if ( installationId == props.current_installation.id && (currentTab == 'live' || currentTab == 'configuration') ) { - let isMounted = true; + //let isMounted = true; setFormValues(props.current_installation); - setErrorLoadingS3Data(false); - let disconnectedStatusResult = []; + var interval; - 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; - } - - 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); + if (currentTab == 'live') { + interval = setInterval(fetchDataPeriodically, 2000); + } + if (currentTab == 'configuration') { + fetchDataOnlyOneTime(); + } // Cleanup function to cancel interval and update isMounted when unmounted return () => { - isMounted = false; - clearInterval(interval); + //isMounted = false; + if (currentTab == 'live') { + clearInterval(interval); + } }; } }, [installationId, currentTab]); @@ -662,7 +661,10 @@ function Installation(props: singleInstallationProps) { )} {currentTab === 'configuration' && currentUser.hasWriteAccess && ( - + )} {currentTab === 'manage' && currentUser.hasWriteAccess && ( diff --git a/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx b/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx index 00a54f159..f1c5f5207 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx @@ -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;