Updated backend to provide support for the overview tab

Updated frontend to parse chunks
This commit is contained in:
Noe 2024-06-27 12:51:24 +02:00
parent 7f5ea79d16
commit 5d3b3b4cb2
10 changed files with 631 additions and 47 deletions

View File

@ -11,8 +11,8 @@ public static class WebsocketManager
{ {
public static Dictionary<int, InstallationInfo> InstallationConnections = new Dictionary<int, InstallationInfo>(); public static Dictionary<int, InstallationInfo> InstallationConnections = new Dictionary<int, InstallationInfo>();
//Every 1 minute, check the timestamp of the latest received message for every installation. //Every 2 minutes, check the timestamp of the latest received message for every installation.
//If the difference between the two timestamps is more than one minute, we consider this installation unavailable. //If the difference between the two timestamps is more than two minutes, we consider this installation unavailable.
public static async Task MonitorInstallationTable() public static async Task MonitorInstallationTable()
{ {
while (true){ while (true){
@ -25,7 +25,7 @@ public static class WebsocketManager
} }
} }
} }
await Task.Delay(TimeSpan.FromMinutes(1)); await Task.Delay(TimeSpan.FromMinutes(2));
} }
} }

View File

@ -18,7 +18,7 @@ dotnet publish \
echo -e "\n============================ Deploy ============================\n" echo -e "\n============================ Deploy ============================\n"
#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.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.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") 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" )

View File

@ -59,7 +59,7 @@ internal static class Program
private static Boolean _subscribeToQueueForTheFirstTime = false; private static Boolean _subscribeToQueueForTheFirstTime = false;
private static SalimaxAlarmState _prevSalimaxState = SalimaxAlarmState.Green; private static SalimaxAlarmState _prevSalimaxState = SalimaxAlarmState.Green;
//private static Int32 _heartBitInterval = 0; //private static Int32 _heartBitInterval = 0;
private const UInt16 NbrOfFileToConcatenate = 15; private const UInt16 NbrOfFileToConcatenate = 30;
private static UInt16 _counterOfFile = 0; private static UInt16 _counterOfFile = 0;
private static SalimaxAlarmState _salimaxAlarmState = SalimaxAlarmState.Green; private static SalimaxAlarmState _salimaxAlarmState = SalimaxAlarmState.Green;

View File

@ -269,7 +269,6 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
<IconButton <IconButton
aria-label="go back" aria-label="go back"
sx={{ sx={{
marginTop: props.productNum != 0 ? '20px' : '0px',
backgroundColor: 'grey', backgroundColor: 'grey',
color: '#000000', color: '#000000',
'&:hover': { bgcolor: '#f7b34d' } '&:hover': { bgcolor: '#f7b34d' }
@ -279,40 +278,36 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButton>
{props.productNum === 0 && ( <Select
<> value={selectedVersion}
<Select onChange={(event) => setSelectedVersion(event.target.value)}
value={selectedVersion} displayEmpty
onChange={(event) => setSelectedVersion(event.target.value)} variant="outlined"
displayEmpty sx={{
variant="outlined" marginTop: '30px',
sx={{ marginLeft: '20px',
marginTop: '30px', height: '40px'
marginLeft: '20px', }}
height: '40px' >
}} <MenuItem disabled value="">
> Select Firmware Version
<MenuItem disabled value=""> </MenuItem>
Select Firmware Version <MenuItem value="AF11">AF11</MenuItem>
</MenuItem> <MenuItem value="AF0A">AF0A</MenuItem>
<MenuItem value="AF11">AF11</MenuItem> </Select>
<MenuItem value="AF0A">AF0A</MenuItem> <Button
</Select> variant="contained"
<Button onClick={handleUpdateFirmware}
variant="contained" disabled={!selectedVersion} // Disable button if no version selected
onClick={handleUpdateFirmware} sx={{
disabled={!selectedVersion} // Disable button if no version selected marginLeft: '20px',
sx={{ backgroundColor: '#ffc04d',
marginLeft: '20px', color: '#000000',
backgroundColor: '#ffc04d', '&:hover': { bgcolor: '#f7b34d' }
color: '#000000', }}
'&:hover': { bgcolor: '#f7b34d' } >
}} Update Firmware
> </Button>
Update Firmware
</Button>
</>
)}
</Grid> </Grid>
</Grid> </Grid>
<Grid container> <Grid container>

View File

@ -361,7 +361,7 @@ function InformationSalidomo(props: InformationSalidomoProps) {
<div> <div>
<TextField <TextField
label="S3 Write Key" label="S3 Write Secret Key"
name="s3writesecretkey" name="s3writesecretkey"
value={formValues.s3WriteSecret} value={formValues.s3WriteSecret}
variant="outlined" variant="outlined"

View File

@ -72,7 +72,7 @@ function Installation(props: singleInstallationProps) {
const continueFetching = useRef(false); const continueFetching = useRef(false);
const fetchDataForOneTime = async () => { const fetchDataForOneTime = async () => {
var timeperiodToSearch = 80; var timeperiodToSearch = 70;
let res; let res;
let timestampToFetch; let timestampToFetch;
@ -107,7 +107,7 @@ function Installation(props: singleInstallationProps) {
}; };
const fetchDataPeriodically = async () => { const fetchDataPeriodically = async () => {
var timeperiodToSearch = 80; var timeperiodToSearch = 70;
let res; let res;
let timestampToFetch; let timestampToFetch;
@ -133,6 +133,7 @@ function Installation(props: singleInstallationProps) {
return false; return false;
} }
setConnected(true); setConnected(true);
console.log('NUMBER OF FILES=' + Object.keys(res).length);
while (continueFetching.current) { while (continueFetching.current) {
for (const timestamp of Object.keys(res)) { for (const timestamp of Object.keys(res)) {
@ -154,7 +155,7 @@ function Installation(props: singleInstallationProps) {
await timeout(2000); await timeout(2000);
} }
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(30)); timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(60));
console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch); console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch);
for (i = 0; i < 10; i++) { for (i = 0; i < 10; i++) {

View File

@ -0,0 +1,574 @@
import { Box, Card, Container, Grid, Modal, Typography } from '@mui/material';
import ReactApexChart from 'react-apexcharts';
import React, { useContext, useEffect, useState } from 'react';
import { I_S3Credentials } from 'src/interfaces/S3Types';
import { getChartOptions } from './chartOptions';
import {
chartAggregatedDataInterface,
overviewInterface,
transformInputToAggregatedData
} from 'src/interfaces/Chart';
import Button from '@mui/material/Button';
import { FormattedMessage } from 'react-intl';
import CircularProgress from '@mui/material/CircularProgress';
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';
interface salidomoOverviewProps {
s3Credentials: I_S3Credentials;
id: number;
}
const computeLast7Days = (): string[] => {
const currentDate = new Date();
const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
let currentDayIndex = currentDate.getDay();
const last7Days = [];
for (let i = 0; i < 7; i++) {
last7Days.push(daysOfWeek[currentDayIndex]);
currentDayIndex++;
if (currentDayIndex > 6) {
currentDayIndex = 0;
}
}
return last7Days;
};
function SalidomoOverview(props: salidomoOverviewProps) {
const context = useContext(UserContext);
const { currentUser } = context;
const [loading, setLoading] = useState(true);
const [aggregatedChartState, setAggregatedChartState] = useState(0);
const [isDateModalOpen, setIsDateModalOpen] = useState(false);
const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
const [dateSelectionError, setDateSelectionError] = useState('');
const [dateOpen, setDateOpen] = useState(false);
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());
useEffect(() => {
handleWeekData();
}, []);
const handleWeekData = () => {
setAggregatedChartState(0);
if (
aggregatedDataArray[aggregatedChartState] &&
aggregatedDataArray[aggregatedChartState].chartData != null
) {
return;
}
setLoading(true);
const resultPromise: Promise<{
chartAggregatedData: chartAggregatedDataInterface;
chartOverview: overviewInterface;
}> = transformInputToAggregatedData(
props.s3Credentials,
dayjs().subtract(1, 'week'),
dayjs()
);
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: computeLast7Days(),
netbalance: powerDifference
})
);
setAggregatedChartState(aggregatedDataArray.length);
setLoading(false);
})
.catch((error) => {
console.error('Error:', error);
});
};
const handleSetDate = () => {
setDateOpen(true);
setIsDateModalOpen(true);
};
const handleOkOnErrorDateModal = () => {
setErrorDateModalOpen(false);
};
const handleCancel = () => {
setIsDateModalOpen(false);
setDateOpen(false);
};
const handleConfirm = () => {
setIsDateModalOpen(false);
setDateOpen(false);
if (endDate.isAfter(dayjs())) {
setDateSelectionError('You cannot ask for future data');
setErrorDateModalOpen(true);
return;
} else if (startDate.isAfter(endDate)) {
setDateSelectionError('Εnd date must precede start date');
setErrorDateModalOpen(true);
return;
}
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 (aggregatedChartState > 0) {
setAggregatedChartState(aggregatedChartState - 1);
}
};
const handleGoForward = () => {
if (aggregatedChartState + 1 < aggregatedDataArray.length) {
setAggregatedChartState(aggregatedChartState + 1);
}
};
const renderGraphs = () => {
return (
<Container maxWidth="xl">
{isErrorDateModalOpen && (
<Modal open={isErrorDateModalOpen} onClose={() => {}}>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 450,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<Typography
variant="body1"
gutterBottom
sx={{ fontWeight: 'bold' }}
>
{dateSelectionError}
</Typography>
<Button
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={handleOkOnErrorDateModal}
>
Ok
</Button>
</Box>
</Modal>
)}
{isDateModalOpen && (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<Modal open={isDateModalOpen} onClose={() => {}}>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 450,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<DateTimePicker
label="Select Start Date"
value={startDate}
onChange={(newDate) => setStartDate(newDate)}
sx={{
marginTop: 2
}}
/>
<DateTimePicker
label="Select End Date"
value={endDate}
onChange={(newDate) => setEndDate(newDate)}
sx={{
marginTop: 2
}}
/>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
<Button
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={handleConfirm}
>
Confirm
</Button>
<Button
sx={{
marginTop: 2,
marginLeft: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={handleCancel}
>
Cancel
</Button>
</div>
</Box>
</Modal>
</LocalizationProvider>
)}
<Grid container>
<Grid item xs={6} md={6}>
<Button
variant="contained"
onClick={handleSetDate}
disabled={loading}
sx={{
marginTop: '20px',
marginLeft: '10px',
backgroundColor: dateOpen ? '#808080' : '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="lastmonth" defaultMessage="Set Date" />
</Button>
</Grid>
<Grid
container
justifyContent="flex-end"
alignItems="center"
item
xs={6}
md={6}
>
<Button
variant="contained"
disabled={!(aggregatedChartState > 0)}
onClick={handleGoBack}
sx={{
marginTop: '20px',
marginLeft: '10px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="goback" defaultMessage="Zoom out" />
</Button>
<Button
variant="contained"
disabled={
!(aggregatedChartState < aggregatedDataArray.length - 1)
}
onClick={handleGoForward}
sx={{
marginTop: '20px',
marginLeft: '10px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="goback" defaultMessage="Zoom in" />
</Button>
</Grid>
{loading && (
<Container
maxWidth="xl"
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100vh'
}}
>
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
<Typography variant="body2" style={{ color: 'black' }} mt={2}>
Fetching data...
</Typography>
</Container>
)}
{!loading && (
<Grid item xs={12} md={12}>
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item md={6} xs={12}>
<Card
sx={{
overflow: 'visible',
marginTop: '30px',
marginBottom: '30px'
}}
>
<Box
sx={{
marginLeft: '20px'
}}
>
<Box display="flex" alignItems="center">
<Box>
<Typography variant="subtitle1" noWrap>
<FormattedMessage
id="battery_soc"
defaultMessage="Battery SOC (State Of Charge)"
/>
</Typography>
</Box>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
pt: 3
}}
></Box>
</Box>
<ReactApexChart
options={{
...getChartOptions(
aggregatedDataArray[aggregatedChartState]
.chartOverview.soc,
'weekly',
aggregatedDataArray[aggregatedChartState].datelist,
true
)
}}
series={[
{
...aggregatedDataArray[aggregatedChartState].chartData
.minsoc,
color: '#69d2e7'
},
{
...aggregatedDataArray[aggregatedChartState].chartData
.maxsoc,
color: '#008FFB'
}
]}
type="bar"
height={400}
/>
</Card>
</Grid>
<Grid item md={6} xs={12}>
<Card
sx={{
overflow: 'visible',
marginTop: '30px',
marginBottom: '30px'
}}
>
<Box
sx={{
marginLeft: '20px'
}}
>
<Box display="flex" alignItems="center">
<Box>
<Typography variant="subtitle1" noWrap>
<FormattedMessage
id="battery_power"
defaultMessage={'Battery Energy'}
/>
</Typography>
</Box>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
pt: 3
}}
></Box>
</Box>
{currentUser.userType == UserType.admin && (
<ReactApexChart
options={{
...getChartOptions(
aggregatedDataArray[aggregatedChartState]
.chartOverview.dcPower,
'weekly',
aggregatedDataArray[aggregatedChartState].datelist,
false
)
}}
series={[
{
...aggregatedDataArray[aggregatedChartState]
.chartData.dcChargingPower,
color: '#008FFB'
},
{
...aggregatedDataArray[aggregatedChartState]
.chartData.heatingPower,
color: '#ff9900'
},
{
...aggregatedDataArray[aggregatedChartState]
.chartData.dcDischargingPower,
color: '#69d2e7'
}
]}
type="bar"
height={400}
/>
)}
{currentUser.userType == UserType.client && (
<ReactApexChart
options={{
...getChartOptions(
aggregatedDataArray[aggregatedChartState]
.chartOverview.dcPowerWithoutHeating,
'weekly',
aggregatedDataArray[aggregatedChartState].datelist,
true
)
}}
series={[
{
...aggregatedDataArray[aggregatedChartState]
.chartData.dcChargingPower,
color: '#008FFB'
},
{
...aggregatedDataArray[aggregatedChartState]
.chartData.dcDischargingPower,
color: '#69d2e7'
}
]}
type="bar"
height={400}
/>
)}
</Card>
</Grid>
</Grid>
</Grid>
)}
</Grid>
</Container>
);
};
return <>{renderGraphs()}</>;
}
export default SalidomoOverview;

View File

@ -17,6 +17,7 @@ import InformationSalidomo from '../Information/InformationSalidomo';
import BatteryView from '../BatteryView/BatteryView'; import BatteryView from '../BatteryView/BatteryView';
import Log from '../Log/Log'; import Log from '../Log/Log';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import SalidomoOverview from '../Overview/salidomoOverview';
interface singleInstallationProps { interface singleInstallationProps {
current_installation?: I_Installation; current_installation?: I_Installation;
@ -66,7 +67,7 @@ function Installation(props: singleInstallationProps) {
const continueFetching = useRef(false); const continueFetching = useRef(false);
const fetchDataPeriodically = async () => { const fetchDataPeriodically = async () => {
var timeperiodToSearch = 80; var timeperiodToSearch = 70;
let res; let res;
let timestampToFetch; let timestampToFetch;
@ -113,7 +114,7 @@ function Installation(props: singleInstallationProps) {
await timeout(2000); await timeout(2000);
} }
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(30)); timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(60));
console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch); console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch);
for (i = 0; i < 10; i++) { for (i = 0; i < 10; i++) {
@ -296,6 +297,16 @@ function Installation(props: singleInstallationProps) {
} }
/> />
<Route
path={routes.overview}
element={
<SalidomoOverview
s3Credentials={s3Credentials}
id={props.current_installation.id}
></SalidomoOverview>
}
/>
<Route <Route
path={routes.batteryview + '*'} path={routes.batteryview + '*'}
element={ element={

View File

@ -92,6 +92,10 @@ function SalidomoInstallationTabs() {
/> />
) )
}, },
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
},
{ {
value: 'log', value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" /> label: <FormattedMessage id="log" defaultMessage="Log" />

View File

@ -403,7 +403,6 @@ export const transformInputToDailyData = async (
}); });
} }
//while (startUnixTime < endTimestamp) {
for (var i = 0; i < timestampArray.length; i++) { for (var i = 0; i < timestampArray.length; i++) {
timestampPromises.push( timestampPromises.push(
fetchDataForOneTime( fetchDataForOneTime(