diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index 220f63c3c..fa8113e7b 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -137,6 +137,72 @@ public class Controller : ControllerBase .ToList(); } + [HttpGet(nameof(GetCsvTimestampsForInstallation))] + public ActionResult> GetCsvTimestampsForInstallation(Int64 id, Int32 start, Int32 end, Token authToken) + { + var user = Db.GetSession(authToken)?.User; + if (user == null) + return Unauthorized(); + + var installation = Db.GetInstallationById(id); + + if (installation is null || !user.HasAccessTo(installation)) + return Unauthorized(); + + var sampleSize = 100; + var allTimestamps = new List(); + + if (start != 0 && end != 0) + { + allTimestamps = Db.CsvTimestamps + .Where(csvTimestamp => csvTimestamp.InstallationId == id && csvTimestamp.Timestamp > start && csvTimestamp.Timestamp < end) + .OrderBy(csvTimestamp => csvTimestamp.Timestamp).ToList(); + } + else + { + allTimestamps = Db.CsvTimestamps + .Where(csvTimestamp => csvTimestamp.InstallationId == id) + .OrderBy(csvTimestamp => csvTimestamp.Timestamp).ToList(); + + } + + int totalRecords = allTimestamps.Count; + if (totalRecords <= sampleSize) + { + // If the total records are less than or equal to the sample size, return all records + Console.WriteLine("Start timestamp = "+start +" end timestamp = "+end); + Console.WriteLine("SampledTimestamps = " + allTimestamps.Count); + return allTimestamps; + } + + + + int interval = totalRecords / sampleSize; + var sampledTimestamps = new List(); + + for (int i = 0; i < totalRecords; i += interval) + { + sampledTimestamps.Add(allTimestamps[i]); + } + + // If we haven't picked enough records (due to rounding), add the latest record to ensure completeness + if (sampledTimestamps.Count < sampleSize) + { + sampledTimestamps.Add(allTimestamps.Last()); + } + + Console.WriteLine("Start timestamp = "+start +" end timestamp = "+end); + Console.WriteLine("TotalRecords = "+totalRecords + " interval = "+ interval); + Console.WriteLine("SampledTimestamps = " + sampledTimestamps.Count); + + return sampledTimestamps; + + // return Db.CsvTimestamps + // .Where(csvTimestamp => csvTimestamp.InstallationId == id) + // .OrderByDescending(csvTimestamp => csvTimestamp.Timestamp) + // .ToList(); + } + [HttpGet(nameof(GetUserById))] public ActionResult GetUserById(Int64 id, Token authToken) { @@ -555,10 +621,11 @@ public class Controller : ControllerBase { var session = Db.GetSession(authToken); var installationToUpdate = Db.GetInstallationById(installationId); + if (installationToUpdate != null) { - _ = session.RunScriptInBackground(installationToUpdate.VpnIp, batteryNode,version); + _ = session.RunScriptInBackground(installationToUpdate.VpnIp, batteryNode,version,installationToUpdate.Product); } return Ok(); diff --git a/csharp/App/Backend/DataTypes/CsvName.cs b/csharp/App/Backend/DataTypes/CsvName.cs new file mode 100644 index 000000000..b5ebabe06 --- /dev/null +++ b/csharp/App/Backend/DataTypes/CsvName.cs @@ -0,0 +1,11 @@ +using SQLite; + +namespace InnovEnergy.App.Backend.DataTypes; + +public class CsvTimestamp{ + [PrimaryKey, AutoIncrement] + public Int64 Id { get; set; } + public Int64 InstallationId { get; set; } + public Int32 Timestamp { get; set; } + +} \ No newline at end of file diff --git a/csharp/App/Backend/DataTypes/Methods/Session.cs b/csharp/App/Backend/DataTypes/Methods/Session.cs index 5824c9602..cba4852cc 100644 --- a/csharp/App/Backend/DataTypes/Methods/Session.cs +++ b/csharp/App/Backend/DataTypes/Methods/Session.cs @@ -68,10 +68,12 @@ public static class SessionMethods .Apply(Db.Update); } - public static async Task RunScriptInBackground(this Session? session, String vpnIp, Int64 batteryNode,String version) + public static async Task RunScriptInBackground(this Session? session, String vpnIp, Int64 batteryNode,String version,Int64 product) { Console.WriteLine("-----------------------------------Start updating firmware-----------------------------------"); - string scriptPath = "/home/ubuntu/backend/uploadBatteryFw/update_firmware.sh"; + string scriptPath = (product == 0) + ? "/home/ubuntu/backend/uploadBatteryFw/update_firmware_Salimax.sh" + : "/home/ubuntu/backend/uploadBatteryFw/update_firmware_Salidomo.sh"; await Task.Run(() => { diff --git a/csharp/App/Backend/Database/Create.cs b/csharp/App/Backend/Database/Create.cs index 2c8c7aa2e..6a41c8701 100644 --- a/csharp/App/Backend/Database/Create.cs +++ b/csharp/App/Backend/Database/Create.cs @@ -32,6 +32,11 @@ public static partial class Db return Insert(warning); } + public static Boolean Create(CsvTimestamp csvTimestamp) + { + return Insert(csvTimestamp); + } + public static Boolean Create(Folder folder) { return Insert(folder); @@ -144,4 +149,35 @@ public static partial class Db Create(newWarning); } } + + public static void AddCsvTimestamp(CsvTimestamp newCsvTimestamp,int installationId) + { + var maxCSvPerInstallation = 2 * 60 * 24; + //Find the total number of warnings for this installation + var totalCsvNames = CsvTimestamps.Count(csvTimestamp => csvTimestamp.InstallationId == installationId); + + //If there are 100 warnings, remove the one with the oldest timestamp + if (totalCsvNames == maxCSvPerInstallation) + { + var oldestCSvTimestamp = + CsvTimestamps.Where(csvTimestamp => csvTimestamp.InstallationId == installationId) + .OrderBy(csvTimestamp => csvTimestamp.Timestamp) + .FirstOrDefault(); + + //Remove the old error + Delete(oldestCSvTimestamp); + + //Add the new error + Create(newCsvTimestamp); + } + else + { + Console.WriteLine("---------------Added the new Csv Timestamp to the database-----------------"); + Create(newCsvTimestamp); + } + } + + + + } \ No newline at end of file diff --git a/csharp/App/Backend/Database/Db.cs b/csharp/App/Backend/Database/Db.cs index 9ec5ecc6d..6ccd36e49 100644 --- a/csharp/App/Backend/Database/Db.cs +++ b/csharp/App/Backend/Database/Db.cs @@ -37,6 +37,7 @@ public static partial class Db fileConnection.CreateTable(); fileConnection.CreateTable(); fileConnection.CreateTable(); + fileConnection.CreateTable(); return fileConnection; //return CopyDbToMemory(fileConnection); @@ -57,6 +58,7 @@ public static partial class Db memoryConnection.CreateTable(); memoryConnection.CreateTable(); fileConnection.CreateTable(); + fileConnection.CreateTable(); //Copy all the existing tables from the disk to main memory fileConnection.Table().ForEach(memoryConnection.Insert); @@ -68,7 +70,8 @@ public static partial class Db fileConnection.Table().ForEach(memoryConnection.Insert); fileConnection.Table().ForEach(memoryConnection.Insert); fileConnection.Table().ForEach(memoryConnection.Insert); - fileConnection.Table().ForEach(memoryConnection.Insert); + fileConnection.Table().ForEach(memoryConnection.Insert); + fileConnection.Table().ForEach(memoryConnection.Insert); return memoryConnection; } @@ -89,6 +92,7 @@ public static partial class Db public static TableQuery Errors => Connection.Table(); public static TableQuery Warnings => Connection.Table(); public static TableQuery UserActions => Connection.Table(); + public static TableQuery CsvTimestamps => Connection.Table(); public static void Init() { @@ -111,6 +115,7 @@ public static partial class Db Connection.CreateTable(); Connection.CreateTable(); Connection.CreateTable(); + Connection.CreateTable(); }); //UpdateKeys(); diff --git a/csharp/App/Backend/Database/Delete.cs b/csharp/App/Backend/Database/Delete.cs index d9ee41aab..09d361b88 100644 --- a/csharp/App/Backend/Database/Delete.cs +++ b/csharp/App/Backend/Database/Delete.cs @@ -77,6 +77,20 @@ public static partial class Db return Warnings.Delete(error => error.Id == warningToDelete.Id) >0; } } + + public static Boolean Delete(CsvTimestamp csvTimestampToDelete) + { + var deleteSuccess = RunTransaction(DeleteCsvTimestampToDelete); + if (deleteSuccess) + BackupDatabase(); + return deleteSuccess; + + + Boolean DeleteCsvTimestampToDelete() + { + return CsvTimestamps.Delete(csvTimestamp => csvTimestamp.Id == csvTimestampToDelete.Id) >0; + } + } public static Boolean Delete(Installation installation) { diff --git a/csharp/App/Backend/Websockets/RabbitMQManager.cs b/csharp/App/Backend/Websockets/RabbitMQManager.cs index c0cdbe308..3abd0f1c5 100644 --- a/csharp/App/Backend/Websockets/RabbitMQManager.cs +++ b/csharp/App/Backend/Websockets/RabbitMQManager.cs @@ -68,7 +68,18 @@ public static class RabbitMqManager //Every 15 iterations(30 seconds), the installation sends a heartbit message to the queue if (receivedStatusMessage.Type == MessageType.Heartbit) { - Console.WriteLine("This is a heartbit message from installation: " + installationId); + Console.WriteLine("This is a heartbit message from installation: " + installationId + " Name of the file is "+ receivedStatusMessage.Timestamp); + + if (receivedStatusMessage.Timestamp != 0) + { + CsvTimestamp newCsvTimestamp = new CsvTimestamp + { + InstallationId = installationId, + Timestamp = receivedStatusMessage.Timestamp + }; + + Db.AddCsvTimestamp(newCsvTimestamp, installationId); + } } else { @@ -182,52 +193,5 @@ public static class RabbitMqManager }; 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"); - - } } \ No newline at end of file diff --git a/csharp/App/Backend/Websockets/StatusMessage.cs b/csharp/App/Backend/Websockets/StatusMessage.cs index 73009a3a9..0d2571811 100644 --- a/csharp/App/Backend/Websockets/StatusMessage.cs +++ b/csharp/App/Backend/Websockets/StatusMessage.cs @@ -9,6 +9,7 @@ public class StatusMessage public required MessageType Type { get; set; } public List? Warnings { get; set; } public List? Alarms { get; set; } + public Int32 Timestamp { get; set; } } public enum MessageType diff --git a/csharp/App/SaliMax/deploy_all_installations.sh b/csharp/App/SaliMax/deploy_all_installations.sh index 4f5c7fd27..0fdf25c16 100755 --- a/csharp/App/SaliMax/deploy_all_installations.sh +++ b/csharp/App/SaliMax/deploy_all_installations.sh @@ -16,9 +16,9 @@ dotnet publish \ -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") +#ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.29" "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.211") #ip_addresses=("10.2.4.154" "10.2.4.29") -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.29") +ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.29" "10.2.4.33" "10.2.4.32" "10.2.4.36" "10.2.4.35") diff --git a/csharp/App/SaliMax/src/DataTypes/StatusMessage.cs b/csharp/App/SaliMax/src/DataTypes/StatusMessage.cs index 6b24a74cf..42655a543 100644 --- a/csharp/App/SaliMax/src/DataTypes/StatusMessage.cs +++ b/csharp/App/SaliMax/src/DataTypes/StatusMessage.cs @@ -10,6 +10,7 @@ public class StatusMessage public required MessageType Type { get; set; } public List? Warnings { get; set; } public List? Alarms { get; set; } + public Int32 Timestamp { get; set; } } public enum MessageType diff --git a/csharp/App/SaliMax/src/Program.cs b/csharp/App/SaliMax/src/Program.cs index c80e77691..6545ac962 100644 --- a/csharp/App/SaliMax/src/Program.cs +++ b/csharp/App/SaliMax/src/Program.cs @@ -1,4 +1,4 @@ -#undef Amax +#define Amax #undef GridLimit using System.Diagnostics; @@ -58,9 +58,10 @@ internal static class Program private static Boolean _subscribedToQueue = false; private static Boolean _subscribeToQueueForTheFirstTime = false; private static SalimaxAlarmState _prevSalimaxState = SalimaxAlarmState.Green; - private static Int32 _heartBitInterval = 0; + //private static Int32 _heartBitInterval = 0; private const UInt16 NbrOfFileToConcatenate = 15; private static UInt16 _counterOfFile = 0; + private static SalimaxAlarmState _salimaxAlarmState = SalimaxAlarmState.Green; static Program() { @@ -185,7 +186,6 @@ internal static class Program LoadOnAcGrid = gridBusLoad, LoadOnAcIsland = loadOnAcIsland, LoadOnDc = dcLoad, - StateMachine = StateMachine.Default, EssControl = EssControl.Default, Log = new SystemLog { SalimaxAlarmState = SalimaxAlarmState.Green, Message = null, SalimaxAlarms = null, SalimaxWarnings = null}, //TODO: Put real stuff @@ -269,7 +269,7 @@ internal static class Program var subscribedNow = false; //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 if (_subscribeToQueueForTheFirstTime == false) @@ -287,16 +287,16 @@ internal static class Program if (s3Bucket != null) RabbitMqManager.InformMiddleware(currentSalimaxState); } - else if (_subscribedToQueue && _heartBitInterval >= 30) - { - //Send a heartbit to the backend - Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------"); - _heartBitInterval = 0; - currentSalimaxState.Type = MessageType.Heartbit; - - if (s3Bucket != null) - RabbitMqManager.InformMiddleware(currentSalimaxState); - } + // else if (_subscribedToQueue && _heartBitInterval >= 30) + // { + // //Send a heartbit to the backend + // Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------"); + // _heartBitInterval = 0; + // currentSalimaxState.Type = MessageType.Heartbit; + // + // if (s3Bucket != null) + // RabbitMqManager.InformMiddleware(currentSalimaxState); + // } //If there is an available message from the RabbitMQ Broker, apply the configuration file Configuration? config = SetConfigurationFile(); @@ -441,13 +441,13 @@ internal static class Program }); } - var salimaxAlarmsState = warningList.Any() + _salimaxAlarmState = warningList.Any() ? SalimaxAlarmState.Orange : SalimaxAlarmState.Green; // this will be replaced by LedState - salimaxAlarmsState = alarmList.Any() + _salimaxAlarmState = alarmList.Any() ? SalimaxAlarmState.Red - : salimaxAlarmsState; // this will be replaced by LedState + : _salimaxAlarmState; // this will be replaced by LedState int.TryParse(s3Bucket?.Split("-")[0], out var installationId); @@ -455,7 +455,7 @@ internal static class Program { InstallationId = installationId, Product = 0, - Status = salimaxAlarmsState, + Status = _salimaxAlarmState, Type = MessageType.AlarmOrWarning, Alarms = alarmList, Warnings = warningList @@ -683,6 +683,7 @@ internal static class Program private static async Task UploadCsv(StatusRecord status, DateTime timeStamp) { + var csv = status.ToCsv().LogInfo(); await RestApiSavingfile(csv); @@ -730,6 +731,26 @@ internal static class Program Console.WriteLine(error); return false; } + + Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------"); + + var s3Bucket = Config.Load().S3?.Bucket; + int.TryParse(s3Bucket?.Split("-")[0], out var installationId); + int.TryParse(timeStamp.ToUnixTime().ToString(), out var nameOfCsvFile); + + var returnedStatus = new StatusMessage + { + InstallationId = installationId, + Product = 0, + Status = _salimaxAlarmState, + Type = MessageType.Heartbit, + Timestamp = nameOfCsvFile + }; + + + if (s3Bucket != null) + RabbitMqManager.InformMiddleware(returnedStatus); + } _counterOfFile++; diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryView.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryView.tsx index 56378d8d5..f012f281b 100644 --- a/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryView.tsx @@ -170,7 +170,10 @@ function BatteryView(props: BatteryViewProps) { + } /> {props.values.batteryView.map((battery) => ( diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx index 63ca5b839..d9e0228e8 100644 --- a/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx @@ -28,6 +28,7 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack'; interface MainStatsProps { s3Credentials: I_S3Credentials; + id: number; } function MainStats(props: MainStatsProps) { @@ -92,11 +93,7 @@ function MainStats(props: MainStatsProps) { const resultPromise: Promise<{ chartData: BatteryDataInterface; chartOverview: BatteryOverviewInterface; - }> = transformInputToBatteryViewData( - props.s3Credentials, - UnixTime.now().rangeBefore(TimeSpan.fromDays(1)).start, - UnixTime.now() - ); + }> = transformInputToBatteryViewData(props.s3Credentials, props.id); resultPromise .then((result) => { @@ -186,6 +183,7 @@ function MainStats(props: MainStatsProps) { chartOverview: BatteryOverviewInterface; }> = transformInputToBatteryViewData( props.s3Credentials, + props.id, UnixTime.fromTicks(startDate.unix()), UnixTime.fromTicks(endDate.unix()) ); @@ -246,6 +244,7 @@ function MainStats(props: MainStatsProps) { chartOverview: BatteryOverviewInterface; }> = transformInputToBatteryViewData( props.s3Credentials, + props.id, UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)), UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2)) ); diff --git a/typescript/frontend-marios2/src/content/dashboards/History/History.tsx b/typescript/frontend-marios2/src/content/dashboards/History/History.tsx index 5b10d7585..b9518fb9d 100644 --- a/typescript/frontend-marios2/src/content/dashboards/History/History.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/History/History.tsx @@ -136,7 +136,7 @@ function HistoryOfActions(props: HistoryProps) { label="Select Action Date" name="timestamp" value={actionDate} - onChange={(newDate) => handleDateChange(newDate.toDate())} + onChange={(newDate) => handleDateChange(newDate)} sx={{ width: 450, marginTop: 2 diff --git a/typescript/frontend-marios2/src/content/dashboards/Information/InformationSalidomo.tsx b/typescript/frontend-marios2/src/content/dashboards/Information/InformationSalidomo.tsx index b10e749d9..2ca122a05 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Information/InformationSalidomo.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Information/InformationSalidomo.tsx @@ -4,9 +4,13 @@ import { CardContent, CircularProgress, Container, + FormControl, Grid, IconButton, + InputLabel, + MenuItem, Modal, + Select, TextField, Typography, useTheme @@ -42,6 +46,8 @@ function InformationSalidomo(props: InformationSalidomoProps) { useState(false); const navigate = useNavigate(); + const DeviceTypes = ['Cerbo', 'Venus']; + const installationContext = useContext(InstallationsContext); const { updateInstallation, @@ -259,6 +265,61 @@ function InformationSalidomo(props: InformationSalidomoProps) { /> +
+ + } + name="vrmLink" + value={formValues.vrmLink} + onChange={handleChange} + variant="outlined" + fullWidth + /> +
+ +
+ + + + + + +
+
- {/*
*/} - {/* */} - {/* }*/} - {/* name="vrmLink"*/} - {/* value={formValues.vrmLink}*/} - {/* onChange={handleChange}*/} - {/* variant="outlined"*/} - {/* fullWidth*/} - {/* />*/} - {/*
*/} -
(undefined); const [values, setValues] = useState(null); const status = getStatus(props.current_installation.id); - const [ - failedToCommunicateWithInstallation, - setFailedToCommunicateWithInstallation - ] = useState(0); const [connected, setConnected] = useState(true); if (props.current_installation == undefined) { @@ -68,51 +65,122 @@ function Installation(props: singleInstallationProps) { const s3Credentials = { s3Bucket, ...S3data }; - const fetchDataPeriodically = async () => { - const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20)); + function timeout(delay: number) { + return new Promise((res) => setTimeout(res, delay)); + } - try { - const res = await fetchData(now, s3Credentials); + const continueFetching = useRef(false); - if (res != FetchResult.notAvailable && res != FetchResult.tryLater) { - setFailedToCommunicateWithInstallation(0); - setConnected(true); - setValues( - extractValues({ - time: now, - value: res - }) - ); - return true; - } else { - setFailedToCommunicateWithInstallation((prevCount) => { - if (prevCount + 1 >= 3) { - setConnected(false); - } - return prevCount + 1; - }); + const fetchDataForOneTime = async () => { + var timeperiodToSearch = 80; + let res; + let timestampToFetch; + + for (var i = timeperiodToSearch; i > 0; i -= 2) { + timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i)); + try { + res = await fetchData(timestampToFetch, s3Credentials); + if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) { + break; + } + } catch (err) { + console.error('Error fetching data:', err); + return false; } - } catch (err) { + } + + if (i <= 0) { + setConnected(false); return false; } + setConnected(true); + + const timestamp = Object.keys(res)[Object.keys(res).length - 1]; + + setValues( + extractValues({ + time: UnixTime.fromTicks(parseInt(timestamp, 10)), + value: res[timestamp] + }) + ); + return true; }; - const fetchDataOnlyOneTime = async () => { - let success = false; - const max_retransmissions = 3; + const fetchDataPeriodically = async () => { + var timeperiodToSearch = 80; + let res; + let timestampToFetch; - for (let i = 0; i < max_retransmissions; i++) { - success = await fetchDataPeriodically(); - await new Promise((resolve) => setTimeout(resolve, 1000)); - if (success) { - break; + for (var i = timeperiodToSearch; i > 0; i -= 2) { + if (!continueFetching.current) { + return false; + } + timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i)); + + try { + res = await fetchData(timestampToFetch, s3Credentials); + if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) { + break; + } + } catch (err) { + console.error('Error fetching data:', err); + return false; + } + } + + if (i <= 0) { + setConnected(false); + return false; + } + setConnected(true); + + while (continueFetching.current) { + for (const timestamp of Object.keys(res)) { + if (!continueFetching.current) { + setFetchFunctionCalled(false); + return false; + } + console.log(`Timestamp: ${timestamp}`); + console.log(res[timestamp]); + + // Set values asynchronously with delay + setValues( + extractValues({ + time: UnixTime.fromTicks(parseInt(timestamp, 10)), + value: res[timestamp] + }) + ); + // Wait for 2 seconds before processing next timestamp + await timeout(2000); + } + + timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(30)); + console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch); + + for (i = 0; i < 10; i++) { + if (!continueFetching.current) { + return false; + } + + try { + console.log('Trying to fetch timestamp ' + timestampToFetch); + res = await fetchData(timestampToFetch, s3Credentials); + if ( + res !== FetchResult.notAvailable && + res !== FetchResult.tryLater + ) { + break; + } + } catch (err) { + console.error('Error fetching data:', err); + return false; + } + timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(1)); } } }; - useEffect(() => { let path = location.split('/'); - setCurrentTab(path[path.length - 1]); }, [location]); @@ -129,30 +197,28 @@ function Installation(props: singleInstallationProps) { currentTab == 'configuration' || location.includes('batteryview') ) { - let interval; - if ( currentTab == 'live' || (location.includes('batteryview') && !location.includes('mainstats')) || currentTab == 'pvview' ) { - fetchDataPeriodically(); - interval = setInterval(fetchDataPeriodically, 2000); + if (!continueFetching.current) { + continueFetching.current = true; + if (!fetchFunctionCalled) { + setFetchFunctionCalled(true); + fetchDataPeriodically(); + } + } } - if (currentTab == 'configuration' || location.includes('mainstats')) { - fetchDataOnlyOneTime(); + if (currentTab == 'configuration') { + fetchDataForOneTime(); } - // Cleanup function to cancel interval return () => { - if ( - currentTab == 'live' || - currentTab == 'pvview' || - (location.includes('batteryview') && !location.includes('mainstats')) - ) { - clearInterval(interval); - } + continueFetching.current = false; }; + } else { + continueFetching.current = false; } }, [currentTab, location]); @@ -324,7 +390,12 @@ function Installation(props: singleInstallationProps) { } + element={ + + } /> > => { @@ -25,10 +25,7 @@ export const fetchDailyData = ( if (r.status === 404) { return Promise.resolve(FetchResult.notAvailable); } else if (r.status === 200) { - // const text = await r.text(); const csvtext = await r.text(); // Assuming the server returns the Base64 encoded ZIP file as text - - //const response = await fetch(url); // Fetch the resource from the server const contentEncoding = r.headers.get('content-type'); if (contentEncoding != 'application/base64; charset=utf-8') { @@ -43,11 +40,7 @@ export const fetchDailyData = ( const zip = await JSZip.loadAsync(byteArray); // Assuming the CSV file is named "data.csv" inside the ZIP archive const csvContent = await zip.file('data.csv').async('text'); - return parseCsv(csvContent); - - //console.log(parseCsv(text)); - //return parseCsv(text); } else { return Promise.resolve(FetchResult.notAvailable); } @@ -61,7 +54,7 @@ export const fetchDailyData = ( export const fetchData = ( timestamp: UnixTime, s3Credentials?: I_S3Credentials -): Promise> => { +): Promise>> => { const s3Path = `${timestamp.ticks}.csv`; if (s3Credentials && s3Credentials.s3Bucket) { const s3Access = new S3Access( @@ -79,12 +72,10 @@ export const fetchData = ( return Promise.resolve(FetchResult.notAvailable); } else if (r.status === 200) { const csvtext = await r.text(); // Assuming the server returns the Base64 encoded ZIP file as text - - //const response = await fetch(url); // Fetch the resource from the server const contentEncoding = r.headers.get('content-type'); if (contentEncoding != 'application/base64; charset=utf-8') { - return parseCsv(csvtext); + return parseChunk(csvtext); } const byteArray = Uint8Array.from(atob(csvtext), (c) => @@ -96,7 +87,7 @@ export const fetchData = ( // Assuming the CSV file is named "data.csv" inside the ZIP archive const csvContent = await zip.file('data.csv').async('text'); - return parseCsv(csvContent); + return parseChunk(csvContent); } else { return Promise.resolve(FetchResult.notAvailable); } 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 9ecb56639..49035ab9a 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx @@ -23,6 +23,35 @@ export const parseCsv = (text: string): DataRecord => { .reduce((acc, current) => ({ ...acc, ...current }), {} as DataRecord); }; +export const parseChunk = (text: string): Record => { + const lines = text.split(/\r?\n/).filter((line) => line.length > 0); + + let result: Record = {}; + let currentTimestamp = null; + + lines.forEach((line) => { + const fields = line.split(';'); + if (fields[0] === 'Timestamp') { + currentTimestamp = fields[1]; + result[currentTimestamp] = {}; + } else if (currentTimestamp) { + let key = fields[0]; + let value = fields[1]; + let unit = fields[2]; + + if (isNaN(Number(value)) || value === '') { + result[currentTimestamp][key] = { value: value, unit: unit }; + } else { + result[currentTimestamp][key] = { + value: parseFloat(value), + unit: unit + }; + } + } + }); + return result; +}; + export interface I_BoxDataValue { unit: string; value: string | number; diff --git a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx index 1bd5daa80..1433f8e49 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx @@ -13,15 +13,16 @@ import { import Button from '@mui/material/Button'; import { FormattedMessage } from 'react-intl'; import CircularProgress from '@mui/material/CircularProgress'; -import { TimeSpan, UnixTime } from '../../../dataCache/time'; import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import dayjs from 'dayjs'; import { UserContext } from '../../../contexts/userContext'; import { UserType } from '../../../interfaces/UserTypes'; +import { TimeSpan, UnixTime } from '../../../dataCache/time'; interface OverviewProps { s3Credentials: I_S3Credentials; + id: number; } const computeLast7Days = (): string[] => { @@ -43,7 +44,6 @@ const computeLast7Days = (): string[] => { function Overview(props: OverviewProps) { const context = useContext(UserContext); const { currentUser } = context; - const [dailyData, setDailyData] = useState(true); const [aggregatedData, setAggregatedData] = useState(false); const [loading, setLoading] = useState(true); @@ -85,11 +85,7 @@ function Overview(props: OverviewProps) { const resultPromise: Promise<{ chartData: chartDataInterface; chartOverview: overviewInterface; - }> = transformInputToDailyData( - props.s3Credentials, - UnixTime.now().rangeBefore(TimeSpan.fromDays(1)).start, - UnixTime.now() - ); + }> = transformInputToDailyData(props.s3Credentials, props.id); resultPromise .then((result) => { @@ -119,6 +115,7 @@ function Overview(props: OverviewProps) { chartOverview: overviewInterface; }> = transformInputToDailyData( props.s3Credentials, + props.id, UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)), UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2)) ); @@ -174,7 +171,6 @@ function Overview(props: OverviewProps) { chartOverview: overviewInterface; }> = transformInputToAggregatedData( props.s3Credentials, - dayjs().subtract(1, 'week'), dayjs() ); @@ -246,6 +242,7 @@ function Overview(props: OverviewProps) { chartOverview: overviewInterface; }> = transformInputToDailyData( props.s3Credentials, + props.id, UnixTime.fromTicks(startDate.unix()), UnixTime.fromTicks(endDate.unix()) ); diff --git a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx index 6e11a2a10..375b42b69 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect, useRef, useState } from 'react'; import { Card, CircularProgress, Grid, Typography } from '@mui/material'; import { I_Installation } from 'src/interfaces/InstallationTypes'; import { UserContext } from 'src/contexts/userContext'; @@ -56,93 +56,110 @@ function Installation(props: singleInstallationProps) { '-' + 'c0436b6a-d276-4cd8-9c44-1eae86cf5d0e'; + const [fetchFunctionCalled, setFetchFunctionCalled] = useState(false); const s3Credentials = { s3Bucket, ...S3data }; - const fetchDataOnlyOneTime = async () => { - var timeperiodToSearch = 70; + function timeout(delay: number) { + return new Promise((res) => setTimeout(res, delay)); + } - for (var i = timeperiodToSearch; i > 0; i -= 2) { - const now = UnixTime.now().earlier(TimeSpan.fromSeconds(i)); - - const res = await fetchData(now, s3Credentials); - - if (res != FetchResult.notAvailable && res != FetchResult.tryLater) { - setConnected(true); - setFailedToCommunicateWithInstallation(0); - setValues( - extractValues({ - time: now, - value: res - }) - ); - return now; - } - } - }; + const continueFetching = useRef(false); const fetchDataPeriodically = async () => { - const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20)); + var timeperiodToSearch = 80; + let res; + let timestampToFetch; - try { - const res = await fetchData(now, s3Credentials); - - if (res != FetchResult.notAvailable && res != FetchResult.tryLater) { - setConnected(true); - setFailedToCommunicateWithInstallation(0); - setValues( - extractValues({ - time: now, - value: res - }) - ); - return true; - } else { - setFailedToCommunicateWithInstallation((prevCount) => { - if (prevCount + 1 >= 20) { - setConnected(false); - } - return prevCount + 1; - }); + for (var i = timeperiodToSearch; i > 0; i -= 2) { + if (!continueFetching.current) { + return false; } - } catch (err) { + timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i)); + + try { + res = await fetchData(timestampToFetch, s3Credentials); + if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) { + break; + } + } catch (err) { + console.error('Error fetching data:', err); + return false; + } + } + + if (i <= 0) { + setConnected(false); return false; } - }; + setConnected(true); + while (continueFetching.current) { + for (const timestamp of Object.keys(res)) { + if (!continueFetching.current) { + setFetchFunctionCalled(false); + return false; + } + console.log(`Timestamp: ${timestamp}`); + console.log(res[timestamp]); + + // Set values asynchronously with delay + setValues( + extractValues({ + time: UnixTime.fromTicks(parseInt(timestamp, 10)), + value: res[timestamp] + }) + ); + // Wait for 2 seconds before processing next timestamp + await timeout(2000); + } + + timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(30)); + console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch); + + for (i = 0; i < 10; i++) { + if (!continueFetching.current) { + return false; + } + + try { + console.log('Trying to fetch timestamp ' + timestampToFetch); + res = await fetchData(timestampToFetch, s3Credentials); + if ( + res !== FetchResult.notAvailable && + res !== FetchResult.tryLater + ) { + break; + } + } catch (err) { + console.error('Error fetching data:', err); + return false; + } + timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(1)); + } + } + }; useEffect(() => { let path = location.split('/'); - setCurrentTab(path[path.length - 1]); }, [location]); useEffect(() => { - if ( - currentTab == 'live' || - currentTab == 'configuration' || - location.includes('batteryview') - ) { - let interval; - - if ( - currentTab == 'live' || - (location.includes('batteryview') && !location.includes('mainstats')) - ) { - fetchDataOnlyOneTime(); - interval = setInterval(fetchDataPeriodically, 2000); - } - if (currentTab == 'configuration' || location.includes('mainstats')) { - fetchDataOnlyOneTime(); - } - - // Cleanup function to cancel interval and update isMounted when unmounted - return () => { - if ( - currentTab == 'live' || - (location.includes('batteryview') && !location.includes('mainstats')) - ) { - clearInterval(interval); + if (location.includes('batteryview')) { + if (location.includes('batteryview') && !location.includes('mainstats')) { + if (!continueFetching.current) { + continueFetching.current = true; + if (!fetchFunctionCalled) { + setFetchFunctionCalled(true); + fetchDataPeriodically(); + } } + } + + return () => { + continueFetching.current = false; }; + } else { + continueFetching.current = false; } }, [currentTab, location]); diff --git a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/SalidomoInstallationForm.tsx b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/SalidomoInstallationForm.tsx index 254ebf80a..66025c284 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/SalidomoInstallationForm.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/SalidomoInstallationForm.tsx @@ -3,8 +3,12 @@ import { Alert, Box, CircularProgress, + FormControl, IconButton, + InputLabel, + MenuItem, Modal, + Select, TextField, useTheme } from '@mui/material'; @@ -37,6 +41,8 @@ function SalidomonstallationForm(props: SalidomoInstallationFormProps) { 'vpnIp', 'vrmLink' ]; + + const DeviceTypes = ['Cerbo', 'Venus']; const installationContext = useContext(InstallationsContext); const { createInstallation, loading, setLoading, error, setError } = installationContext; @@ -128,6 +134,7 @@ function SalidomonstallationForm(props: SalidomoInstallationFormProps) { value={formValues.region} onChange={handleChange} required + error={formValues.region === ''} />
@@ -180,6 +187,36 @@ function SalidomonstallationForm(props: SalidomoInstallationFormProps) { />
+
+ + + + + + +
+
) => { + timestampArray = res.data; + }) + .catch((err: AxiosError) => { + if (err.response && err.response.status == 401) { + //removeToken(); + //navigate(routes.login); + } + }); + } else { + await axiosConfig + .get(`/GetCsvTimestampsForInstallation?id=${id}&start=${0}&end=${0}`) + .then((res: AxiosResponse) => { + timestampArray = res.data; + }) + .catch((err: AxiosError) => { + if (err.response && err.response.status == 401) { + //removeToken(); + //navigate(routes.login); + } + }); + } - startUnixTime = UnixTime.fromTicks(startUnixTime.ticks + diff / 100); - if (startUnixTime.ticks % 2 !== 0) { - startUnixTime = UnixTime.fromTicks(startUnixTime.ticks + 1); - } - const adjustedTimestamp = new Date(startUnixTime.ticks * 1000); + for (var i = 0; i < timestampArray.length; i++) { + timestampPromises.push( + fetchDataForOneTime( + UnixTime.fromTicks(timestampArray[i].timestamp), + s3Credentials + ) + ); + + const adjustedTimestamp = new Date(timestampArray[i].timestamp * 1000); + //Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset adjustedTimestamp.setHours( adjustedTimestamp.getHours() - adjustedTimestamp.getTimezoneOffset() / 60 ); adjustedTimestampArray.push(adjustedTimestamp); } - //Wait until fetching all the data - const results = await Promise.all(timestampPromises); + const results: Promise>>[] = + await Promise.all(timestampPromises); for (let i = 0; i < results.length; i++) { - const result = results[i]; - if ( - result === FetchResult.notAvailable || - result === FetchResult.tryLater - ) { + if (results[i] == null) { // Handle not available or try later case } else { + const timestamp = Object.keys(results[i])[ + Object.keys(results[i]).length - 1 + ]; + const result = results[i][timestamp]; const battery_nodes = result['/Config/Devices/BatteryNodes'].value .toString() .split(','); @@ -252,14 +283,42 @@ export const transformInputToBatteryViewData = async ( }; }; +const fetchDataForOneTime = async ( + startUnixTime: UnixTime, + s3Credentials: I_S3Credentials +): Promise>> => { + var timeperiodToSearch = 2; + let res; + let timestampToFetch; + + for (var i = 0; i < timeperiodToSearch; i++) { + timestampToFetch = startUnixTime.later(TimeSpan.fromSeconds(i)); + try { + res = await fetchData(timestampToFetch, s3Credentials); + + if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) { + //console.log('Successfully fetched ' + timestampToFetch); + return res; + } + } catch (err) { + console.error('Error fetching data:', err); + } + } + return null; +}; + export const transformInputToDailyData = async ( s3Credentials: I_S3Credentials, - startTimestamp: UnixTime, - endTimestamp: UnixTime + id: number, + start_time?: UnixTime, + end_time?: UnixTime ): Promise<{ chartData: chartDataInterface; chartOverview: overviewInterface; }> => { + //const navigate = useNavigate(); + //const tokencontext = useContext(TokenContext); + //const { removeToken } = tokencontext; const prefixes = ['', 'k', 'M', 'G', 'T']; const MAX_NUMBER = 9999999; const pathsToSearch = [ @@ -309,45 +368,71 @@ export const transformInputToDailyData = async ( }; }); + //console.log(start_time); + //console.log(end_time); + + let timestampArray: CsvTimestamp[] = []; let adjustedTimestampArray = []; - - let startTimestampToNum = Number(startTimestamp); - if (startTimestampToNum % 2 != 0) { - startTimestampToNum += 1; - } - let startUnixTime = UnixTime.fromTicks(startTimestampToNum); - let diff = endTimestamp.ticks - startUnixTime.ticks; - const timestampPromises = []; - while (startUnixTime < endTimestamp) { - timestampPromises.push(fetchData(startUnixTime, s3Credentials)); + if (start_time && end_time) { + await axiosConfig + .get( + `/GetCsvTimestampsForInstallation?id=${id}&start=${start_time.ticks}&end=${end_time.ticks}` + ) + .then((res: AxiosResponse) => { + timestampArray = res.data; + }) + .catch((err: AxiosError) => { + if (err.response && err.response.status == 401) { + //removeToken(); + //navigate(routes.login); + } + }); + } else { + await axiosConfig + .get(`/GetCsvTimestampsForInstallation?id=${id}&start=${0}&end=${0}`) + .then((res: AxiosResponse) => { + timestampArray = res.data; + }) + .catch((err: AxiosError) => { + if (err.response && err.response.status == 401) { + //removeToken(); + //navigate(routes.login); + } + }); + } - startUnixTime = UnixTime.fromTicks(startUnixTime.ticks + diff / 100); - if (startUnixTime.ticks % 2 !== 0) { - startUnixTime = UnixTime.fromTicks(startUnixTime.ticks + 1); - } - const adjustedTimestamp = new Date(startUnixTime.ticks * 1000); + //while (startUnixTime < endTimestamp) { + for (var i = 0; i < timestampArray.length; i++) { + timestampPromises.push( + fetchDataForOneTime( + UnixTime.fromTicks(timestampArray[i].timestamp), + s3Credentials + ) + ); + + const adjustedTimestamp = new Date(timestampArray[i].timestamp * 1000); //Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset adjustedTimestamp.setHours( adjustedTimestamp.getHours() - adjustedTimestamp.getTimezoneOffset() / 60 ); - adjustedTimestampArray.push(adjustedTimestamp); } - const results = await Promise.all(timestampPromises); + const results: Promise>>[] = + await Promise.all(timestampPromises); for (let i = 0; i < results.length; i++) { - const result = results[i]; - if ( - result === FetchResult.notAvailable || - result === FetchResult.tryLater - ) { + if (results[i] == null) { // Handle not available or try later case } else { - // eslint-disable-next-line @typescript-eslint/no-loop-func + const timestamp = Object.keys(results[i])[ + Object.keys(results[i]).length - 1 + ]; + const result = results[i][timestamp]; let category_index = 0; + // eslint-disable-next-line @typescript-eslint/no-loop-func pathsToSearch.forEach((path) => { if (result[path]) { const value = result[path]; @@ -370,6 +455,7 @@ export const transformInputToDailyData = async ( }); } } + categories.forEach((category) => { let value = Math.max( Math.abs(chartOverview[category].max), @@ -500,7 +586,7 @@ export const transformInputToAggregatedData = async ( while (currentDay.isBefore(end_date)) { timestampPromises.push( - fetchDailyData(currentDay.format('YYYY-MM-DD'), s3Credentials) + fetchAggregatedData(currentDay.format('YYYY-MM-DD'), s3Credentials) ); currentDay = currentDay.add(1, 'day'); } diff --git a/typescript/frontend-marios2/src/interfaces/S3Types.tsx b/typescript/frontend-marios2/src/interfaces/S3Types.tsx index 7da146cc4..708ee443d 100644 --- a/typescript/frontend-marios2/src/interfaces/S3Types.tsx +++ b/typescript/frontend-marios2/src/interfaces/S3Types.tsx @@ -17,6 +17,12 @@ export interface ErrorMessage { seen: boolean; } +export interface CsvTimestamp { + id: number; + installationId: number; + timestamp: number; +} + export interface Action { id: number; userName: string;