From a10762fefe5bdb5af22115ac6d4e343187196656 Mon Sep 17 00:00:00 2001 From: Noe Date: Tue, 7 May 2024 17:19:02 +0200 Subject: [PATCH 1/2] Inserted Set date button in overview aggregated data Compressed csv files when pushing aggregated data to S3 Front en can now parse both compressed and not-compressed data in parallel --- .../App/SaliMax/deploy_all_installations.sh | 2 + .../src/AggregationService/HourlyData.cs | 34 +- .../Installations/FlatInstallationView.tsx | 17 +- .../dashboards/Installations/fetchData.tsx | 15 +- .../content/dashboards/Overview/overview.tsx | 656 +++++++----------- .../frontend-marios2/src/interfaces/Chart.tsx | 17 +- 6 files changed, 329 insertions(+), 412 deletions(-) diff --git a/csharp/App/SaliMax/deploy_all_installations.sh b/csharp/App/SaliMax/deploy_all_installations.sh index 9368fde0a..8b3c12c57 100755 --- a/csharp/App/SaliMax/deploy_all_installations.sh +++ b/csharp/App/SaliMax/deploy_all_installations.sh @@ -17,6 +17,8 @@ dotnet publish \ 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.4.154" "10.2.4.29") + for ip_address in "${ip_addresses[@]}"; do rsync -v \ diff --git a/csharp/App/SaliMax/src/AggregationService/HourlyData.cs b/csharp/App/SaliMax/src/AggregationService/HourlyData.cs index ca8d088a5..07e29161f 100644 --- a/csharp/App/SaliMax/src/AggregationService/HourlyData.cs +++ b/csharp/App/SaliMax/src/AggregationService/HourlyData.cs @@ -1,3 +1,5 @@ +using System.IO.Compression; +using System.Text; using System.Text.Json; using Flurl.Http; using InnovEnergy.App.SaliMax.Devices; @@ -68,7 +70,37 @@ public class AggregatedData var s3Path = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd") + ".csv"; var request = _S3Config.CreatePutRequest(s3Path); - var response = await request.PutAsync(new StringContent(csv)); + + // Compress CSV data to a byte array + byte[] compressedBytes; + using (var memoryStream = new MemoryStream()) + { + //Create a zip directory and put the compressed file inside + using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) + { + var entry = archive.CreateEntry("data.csv", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive + using (var entryStream = entry.Open()) + using (var writer = new StreamWriter(entryStream)) + { + writer.Write(csv); + } + } + + compressedBytes = memoryStream.ToArray(); + } + + // Encode the compressed byte array as a Base64 string + string base64String = Convert.ToBase64String(compressedBytes); + + // Create StringContent from Base64 string + var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64"); + + // Upload the compressed data (ZIP archive) to S3 + var response = await request.PutAsync(stringContent); + + // + // var request = _S3Config.CreatePutRequest(s3Path); + // var response = await request.PutAsync(new StringContent(csv)); if (response.StatusCode != 200) { diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx index ccf926149..4feb271c4 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx @@ -31,6 +31,21 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { const [selectedInstallation, setSelectedInstallation] = useState(-1); const currentLocation = useLocation(); + const sortedInstallations = [...props.installations].sort((a, b) => { + // Compare the status field of each installation and sort them based on the status. + //Installations with alarms go first + let a_status = getStatus(a.id); + let b_status = getStatus(b.id); + + if (a_status > b_status) { + return -1; + } + if (a_status < b_status) { + return 1; + } + return 0; + }); + const handleSelectOneInstallation = (installationID: number): void => { if (selectedInstallation != installationID) { setSelectedInstallation(installationID); @@ -100,7 +115,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { - {props.installations.map((installation) => { + {sortedInstallations.map((installation) => { const isInstallationSelected = installation.s3BucketId === selectedInstallation; diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/fetchData.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/fetchData.tsx index 01d0a02fd..a141f0558 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/fetchData.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/fetchData.tsx @@ -58,15 +58,24 @@ export const fetchData = ( if (r.status === 404) { return Promise.resolve(FetchResult.notAvailable); } else if (r.status === 200) { - const base64String = await r.text(); // Assuming the server returns the Base64 encoded ZIP file as text - const byteArray = Uint8Array.from(atob(base64String), (c) => + 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); + } + + const byteArray = Uint8Array.from(atob(csvtext), (c) => c.charCodeAt(0) ); - // Decompress the byte array using JSZip + //Decompress the byte array using JSZip 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); } else { return Promise.resolve(FetchResult.notAvailable); diff --git a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx index b21f0df56..4b96a1343 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx @@ -45,14 +45,10 @@ function Overview(props: OverviewProps) { const { currentUser } = context; const [dailyData, setDailyData] = useState(true); - const [weeklyData, setWeeklyData] = useState(false); - const [weeklybalance, setWeeklyBalance] = useState([]); - const [monthlybalance, setMonthlyBalance] = useState([]); - const [monthlyData, setMonthlyData] = useState(false); + const [aggregatedData, setAggregatedData] = useState(false); const [loading, setLoading] = useState(true); const [chartState, setChartState] = useState(0); - const [monthlyDateList, setMonthlyDateList] = useState([]); - const [weeklyDateList, setWeeklyDateList] = useState([]); + const [aggregatedChartState, setAggregatedChartState] = useState(0); const [isDateModalOpen, setIsDateModalOpen] = useState(false); const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false); const [dateSelectionError, setDateSelectionError] = useState(''); @@ -64,19 +60,18 @@ function Overview(props: OverviewProps) { }[] >([]); + const [aggregatedDataArray, setAggregatedDataArray] = useState< + { + chartData: chartAggregatedDataInterface; + chartOverview: overviewInterface; + datelist: any[]; + netbalance: any[]; + }[] + >([]); + const [startDate, setStartDate] = useState(dayjs().add(-1, 'day')); const [endDate, setEndDate] = useState(dayjs()); - const [weeklyDataArray, setWeeklyDataArray] = useState<{ - chartData: chartAggregatedDataInterface; - chartOverview: overviewInterface; - }>({ chartData: null, chartOverview: null }); - - const [monthlyDataArray, setMonthlyDataArray] = useState<{ - chartData: chartAggregatedDataInterface; - chartOverview: overviewInterface; - }>({ chartData: null, chartOverview: null }); - useEffect(() => { const resultPromise: Promise<{ chartData: chartDataInterface; @@ -135,17 +130,19 @@ function Overview(props: OverviewProps) { const handle24HourData = () => { setDailyData(true); - setWeeklyData(false); - setMonthlyData(false); + setAggregatedData(false); setChartState(0); }; const handleWeekData = () => { setDailyData(false); - setWeeklyData(true); - setMonthlyData(false); + setAggregatedData(true); + setAggregatedChartState(0); - if (weeklyDataArray.chartData != null) { + if ( + aggregatedDataArray[aggregatedChartState] && + aggregatedDataArray[aggregatedChartState].chartData != null + ) { return; } setLoading(true); @@ -153,15 +150,15 @@ function Overview(props: OverviewProps) { const resultPromise: Promise<{ chartAggregatedData: chartAggregatedDataInterface; chartOverview: overviewInterface; - }> = transformInputToAggregatedData(props.s3Credentials, 'weekly'); + }> = transformInputToAggregatedData( + props.s3Credentials, + + dayjs().subtract(1, 'week'), + dayjs() + ); resultPromise .then((result) => { - setWeeklyDataArray({ - chartData: result.chartAggregatedData, - chartOverview: result.chartOverview - }); - const powerDifference = []; for ( let i = 0; @@ -174,8 +171,16 @@ function Overview(props: OverviewProps) { ); } - setWeeklyBalance(powerDifference); - setWeeklyDateList(computeLast7Days()); + setAggregatedDataArray((prevData) => + prevData.concat({ + chartData: result.chartAggregatedData, + chartOverview: result.chartOverview, + datelist: computeLast7Days(), + netbalance: powerDifference + }) + ); + + setAggregatedChartState(aggregatedDataArray.length); setLoading(false); }) .catch((error) => { @@ -212,88 +217,100 @@ function Overview(props: OverviewProps) { } setLoading(true); - const resultPromise: Promise<{ - chartData: chartDataInterface; - chartOverview: overviewInterface; - }> = transformInputToDailyData( - props.s3Credentials, - UnixTime.fromTicks(startDate.unix()), - UnixTime.fromTicks(endDate.unix()) - ); - resultPromise - .then((result) => { - setDailyDataArray((prevData) => - prevData.concat({ - chartData: result.chartData, - chartOverview: result.chartOverview - }) - ); + if (dailyData) { + const resultPromise: Promise<{ + chartData: chartDataInterface; + chartOverview: overviewInterface; + }> = transformInputToDailyData( + props.s3Credentials, + UnixTime.fromTicks(startDate.unix()), + UnixTime.fromTicks(endDate.unix()) + ); - setLoading(false); - setChartState(dailyDataArray.length); - }) - .catch((error) => { - console.error('Error:', error); - }); + resultPromise + .then((result) => { + setDailyDataArray((prevData) => + prevData.concat({ + chartData: result.chartData, + chartOverview: result.chartOverview + }) + ); + + setLoading(false); + setChartState(dailyDataArray.length); + }) + .catch((error) => { + console.error('Error:', error); + }); + } else { + setDailyData(false); + setAggregatedData(true); + + setLoading(true); + + const resultPromise: Promise<{ + chartAggregatedData: chartAggregatedDataInterface; + chartOverview: overviewInterface; + dateList: string[]; + }> = transformInputToAggregatedData( + props.s3Credentials, + startDate, + endDate + ); + + resultPromise + .then((result) => { + const powerDifference = []; + + for ( + let i = 0; + i < result.chartAggregatedData.gridImportPower.data.length; + i++ + ) { + powerDifference.push( + result.chartAggregatedData.gridImportPower.data[i] - + Math.abs(result.chartAggregatedData.gridExportPower.data[i]) + ); + } + + setAggregatedDataArray((prevData) => + prevData.concat({ + chartData: result.chartAggregatedData, + chartOverview: result.chartOverview, + datelist: result.dateList, + netbalance: powerDifference + }) + ); + + setAggregatedChartState(aggregatedDataArray.length); + setLoading(false); + }) + .catch((error) => { + console.error('Error:', error); + }); + } }; const handleGoBack = () => { - if (chartState > 0) { + if (dailyData && chartState > 0) { setChartState(chartState - 1); } + if (aggregatedData && aggregatedChartState > 0) { + setAggregatedChartState(aggregatedChartState - 1); + } }; const handleGoForward = () => { - if (chartState + 1 < dailyDataArray.length) { + if (dailyData && chartState + 1 < dailyDataArray.length) { setChartState(chartState + 1); } - }; - - const handleMonthData = () => { - setDailyData(false); - setWeeklyData(false); - setMonthlyData(true); - - if (monthlyDataArray.chartData != null) { - return; + if ( + aggregatedData && + aggregatedChartState + 1 < aggregatedDataArray.length + ) { + setAggregatedChartState(aggregatedChartState + 1); } - setLoading(true); - - const resultPromise: Promise<{ - chartAggregatedData: chartAggregatedDataInterface; - chartOverview: overviewInterface; - dateList: string[]; - }> = transformInputToAggregatedData(props.s3Credentials, 'monthly'); - - resultPromise - .then((result) => { - setMonthlyDataArray({ - chartData: result.chartAggregatedData, - chartOverview: result.chartOverview - }); - - const powerDifference = []; - - for ( - let i = 0; - i < result.chartAggregatedData.gridImportPower.data.length; - i++ - ) { - powerDifference.push( - result.chartAggregatedData.gridImportPower.data[i] - - Math.abs(result.chartAggregatedData.gridExportPower.data[i]) - ); - } - - setMonthlyBalance(powerDifference); - - setMonthlyDateList(result.dateList); - setLoading(false); - }) - .catch((error) => { - console.error('Error:', error); - }); }; const renderGraphs = () => { @@ -438,86 +455,74 @@ function Overview(props: OverviewProps) { sx={{ marginTop: '20px', marginLeft: '10px', - backgroundColor: weeklyData ? '#808080' : '#ffc04d', + backgroundColor: aggregatedData ? '#808080' : '#ffc04d', color: '#000000', '&:hover': { bgcolor: '#f7b34d' } }} > - {/**/} - {/* */} - {/**/} - {dailyData && ( - <> - - - )} - - {dailyData && ( - - - - - )} + + + + + + + + {loading && ( )} - {(weeklyData || monthlyData) && ( + {aggregatedData && ( - {weeklyData && ( - - )} - - {monthlyData && ( - - )} + @@ -806,48 +776,26 @@ function Overview(props: OverviewProps) { /> )} - {weeklyData && ( + {aggregatedData && ( - )} - - {monthlyData && ( - )} - {weeklyData && currentUser.userType == UserType.admin && ( + {aggregatedData && currentUser.userType == UserType.admin && ( )} - {weeklyData && currentUser.userType == UserType.client && ( + {aggregatedData && currentUser.userType == UserType.client && ( - )} - - {monthlyData && currentUser.userType == UserType.admin && ( - - )} - - {monthlyData && currentUser.userType == UserType.client && ( - )} - {weeklyData && ( + {aggregatedData && ( - )} - - {monthlyData && ( - )} - {weeklyData && ( + {aggregatedData && ( - )} - - {monthlyData && ( - Date: Fri, 10 May 2024 19:16:16 +0200 Subject: [PATCH 2/2] Updated RabbitMq to receive messages from Salidomo Fixed bug in front-end when changing role from partner to client Sorted installation view (alarms first) --- .../App/Backend/Websockets/RabbitMQManager.cs | 14 ++--- .../App/SaliMax/deploy_all_installations.sh | 4 +- csharp/App/SaliMax/src/Program.cs | 3 +- .../dashboards/Installations/fetchData.tsx | 24 +++++++- .../dashboards/Installations/index.tsx | 61 ++++++++++++++++++- .../dashboards/Tree/InstallationTree.tsx | 21 ++++++- 6 files changed, 114 insertions(+), 13 deletions(-) diff --git a/csharp/App/Backend/Websockets/RabbitMQManager.cs b/csharp/App/Backend/Websockets/RabbitMQManager.cs index fb300b18f..0ca03cdb5 100644 --- a/csharp/App/Backend/Websockets/RabbitMQManager.cs +++ b/csharp/App/Backend/Websockets/RabbitMQManager.cs @@ -57,9 +57,9 @@ public static class RabbitMqManager if (receivedStatusMessage != null) { Console.WriteLine("----------------------------------------------"); - Console.WriteLine("Received a message from installation: " + receivedStatusMessage.InstallationId + " and status is: " + receivedStatusMessage.Status); - int installationId = (int)Db.Installations.Where(f => f.Product == 0 && f.S3BucketId == receivedStatusMessage.InstallationId).Select(f => f.Id).FirstOrDefault(); + int installationId = (int)Db.Installations.Where(f => f.Product == receivedStatusMessage.Product && f.S3BucketId == receivedStatusMessage.InstallationId).Select(f => f.Id).FirstOrDefault(); + Console.WriteLine("Received a message from installation: " + installationId + " , product is: "+receivedStatusMessage.Product+ " and status is: " + receivedStatusMessage.Status); //This is a heartbit message, just update the timestamp for this installation. //There is no need to notify the corresponding front-ends. @@ -77,7 +77,7 @@ public static class RabbitMqManager { Warning newWarning = new Warning { - InstallationId = receivedStatusMessage.InstallationId, + InstallationId = installationId, Description = warning.Description, Date = warning.Date, Time = warning.Time, @@ -85,7 +85,7 @@ public static class RabbitMqManager Seen = false }; //Create a new warning and add it to the database - Db.HandleWarning(newWarning, receivedStatusMessage.InstallationId); + Db.HandleWarning(newWarning, installationId); } } @@ -93,12 +93,12 @@ public static class RabbitMqManager //Traverse the Alarm list, and store each of them to the database if (receivedStatusMessage.Alarms != null) { - Console.WriteLine("Add an alarm for installation "+receivedStatusMessage.InstallationId); + Console.WriteLine("Add an alarm for installation "+installationId); foreach (var alarm in receivedStatusMessage.Alarms) { Error newError = new Error { - InstallationId = receivedStatusMessage.InstallationId, + InstallationId = installationId, Description = alarm.Description, Date = alarm.Date, Time = alarm.Time, @@ -106,7 +106,7 @@ public static class RabbitMqManager Seen = false }; //Create a new error and add it to the database - Db.HandleError(newError, receivedStatusMessage.InstallationId); + Db.HandleError(newError, installationId); } } } diff --git a/csharp/App/SaliMax/deploy_all_installations.sh b/csharp/App/SaliMax/deploy_all_installations.sh index 8b3c12c57..4f5c7fd27 100755 --- a/csharp/App/SaliMax/deploy_all_installations.sh +++ b/csharp/App/SaliMax/deploy_all_installations.sh @@ -16,8 +16,10 @@ 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.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.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") + for ip_address in "${ip_addresses[@]}"; do diff --git a/csharp/App/SaliMax/src/Program.cs b/csharp/App/SaliMax/src/Program.cs index 50b5049d4..0db6ecbf1 100644 --- a/csharp/App/SaliMax/src/Program.cs +++ b/csharp/App/SaliMax/src/Program.cs @@ -1,4 +1,4 @@ -#define Amax +#undef Amax #undef GridLimit using System.IO.Compression; @@ -486,6 +486,7 @@ internal static class Program var returnedStatus = new StatusMessage { InstallationId = installationId, + Product = 0, Status = salimaxAlarmsState, Type = MessageType.AlarmOrWarning, Alarms = alarmList, diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/fetchData.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/fetchData.tsx index a141f0558..1f14194f2 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/fetchData.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/fetchData.tsx @@ -25,9 +25,29 @@ export const fetchDailyData = ( if (r.status === 404) { return Promise.resolve(FetchResult.notAvailable); } else if (r.status === 200) { - const text = await r.text(); + // 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') { + return parseCsv(csvtext); + } + + const byteArray = Uint8Array.from(atob(csvtext), (c) => + c.charCodeAt(0) + ); + + //Decompress the byte array using JSZip + 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); + //return parseCsv(text); } else { return Promise.resolve(FetchResult.notAvailable); } diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx index 197a141dc..4b12b2c00 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx @@ -135,6 +135,33 @@ function InstallationTabs() { ) } ] + : currentUser.userType == UserType.partner + ? [ + { + value: 'live', + label: + }, + { + value: 'overview', + label: + }, + { + value: 'batteryview', + label: ( + + ) + }, + + { + value: 'information', + label: ( + + ) + } + ] : [ { value: 'live', @@ -219,7 +246,8 @@ function InstallationTabs() { ) } ] - : [ + : currentUser.userType == UserType.partner + ? [ { value: 'list', icon: @@ -249,6 +277,37 @@ function InstallationTabs() { ) }, + { + value: 'information', + label: ( + + ) + } + ] + : [ + { + value: 'list', + icon: + }, + { + value: 'tree', + icon: + }, + + { + value: 'live', + label: + }, + { + value: 'overview', + label: ( + + ) + }, + { value: 'information', label: ( diff --git a/typescript/frontend-marios2/src/content/dashboards/Tree/InstallationTree.tsx b/typescript/frontend-marios2/src/content/dashboards/Tree/InstallationTree.tsx index afb84ab99..3f842fd4c 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Tree/InstallationTree.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Tree/InstallationTree.tsx @@ -9,11 +9,30 @@ import { InstallationsContext } from 'src/contexts/InstallationsContextProvider' import { Route, Routes } from 'react-router-dom'; import routes from '../../../Resources/routes.json'; import Folder from './Folder'; +import { WebSocketContext } from '../../../contexts/WebSocketContextProvider'; function InstallationTree() { const { foldersAndInstallations, fetchAllFoldersAndInstallations } = useContext(InstallationsContext); + const webSocketContext = useContext(WebSocketContext); + const { getStatus } = webSocketContext; + + const sortedInstallations = [...foldersAndInstallations].sort((a, b) => { + // Compare the status field of each installation and sort them based on the status. + //Installations with alarms go first + let a_status = getStatus(a.id); + let b_status = getStatus(b.id); + + if (a_status > b_status) { + return -1; + } + if (a_status < b_status) { + return 1; + } + return 0; + }); + useEffect(() => { fetchAllFoldersAndInstallations(); }, []); @@ -23,7 +42,7 @@ function InstallationTree() { return ( node.parentId == parent_id && ( - {foldersAndInstallations.map((subnode) => { + {sortedInstallations.map((subnode) => { return ( subnode != node && subnode.parentId == node.id && (