Merge branch 'main' into integrated_AI
This commit is contained in:
commit
2da2ded84c
|
|
@ -195,9 +195,13 @@ public class Controller : ControllerBase
|
||||||
|
|
||||||
while (startTimestamp <= endTimestamp)
|
while (startTimestamp <= endTimestamp)
|
||||||
{
|
{
|
||||||
string bucketPath = installation.Product==(int)ProductType.Salimax || installation.Product==(int)ProductType.SodiStoreMax?
|
string bucketPath;
|
||||||
"s3://"+installation.S3BucketId + "-3e5b3069-214a-43ee-8d85-57d72000c19d/"+startTimestamp :
|
if (installation.Product == (int)ProductType.Salimax || installation.Product == (int)ProductType.SodiStoreMax)
|
||||||
"s3://"+installation.S3BucketId + "-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e/"+startTimestamp;
|
bucketPath = "s3://" + installation.S3BucketId + "-3e5b3069-214a-43ee-8d85-57d72000c19d/" + startTimestamp;
|
||||||
|
else if (installation.Product == (int)ProductType.SodioHome)
|
||||||
|
bucketPath = "s3://" + installation.S3BucketId + "-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa/" + startTimestamp;
|
||||||
|
else
|
||||||
|
bucketPath = "s3://" + installation.S3BucketId + "-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e/" + startTimestamp;
|
||||||
Console.WriteLine("Fetching data for "+startTimestamp);
|
Console.WriteLine("Fetching data for "+startTimestamp);
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -12,13 +12,14 @@ import {
|
||||||
Typography
|
Typography
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { JSONRecordData } from '../Log/graph.util';
|
import { JSONRecordData } from '../Log/graph.util';
|
||||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
import { Link, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
||||||
import routes from '../../../Resources/routes.json';
|
import routes from '../../../Resources/routes.json';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import { I_Installation } from 'src/interfaces/InstallationTypes';
|
import { I_Installation } from 'src/interfaces/InstallationTypes';
|
||||||
|
import MainStatsSodioHome from './MainStatsSodioHome';
|
||||||
|
|
||||||
interface BatteryViewSodioHomeProps {
|
interface BatteryViewSodioHomeProps {
|
||||||
values: JSONRecordData;
|
values: JSONRecordData;
|
||||||
|
|
@ -164,54 +165,20 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/*<Grid container>*/}
|
<Grid container>
|
||||||
{/* <Routes>*/}
|
<Routes>
|
||||||
{/* <Route*/}
|
<Route
|
||||||
{/* path={routes.mainstats + '/*'}*/}
|
path={routes.mainstats + '/*'}
|
||||||
{/* element={*/}
|
element={
|
||||||
{/* <MainStats*/}
|
<MainStatsSodioHome
|
||||||
{/* s3Credentials={props.s3Credentials}*/}
|
s3Credentials={props.s3Credentials}
|
||||||
{/* id={props.installationId}*/}
|
id={props.installationId}
|
||||||
{/* ></MainStats>*/}
|
batteryClusterNumber={props.installation.batteryClusterNumber}
|
||||||
{/* }*/}
|
></MainStatsSodioHome>
|
||||||
{/* />*/}
|
}
|
||||||
{/* {product === 0*/}
|
/>
|
||||||
{/* ? Object.entries(props.values.Battery.Devices).map(*/}
|
</Routes>
|
||||||
{/* ([BatteryId, battery]) => (*/}
|
</Grid>
|
||||||
{/* <Route*/}
|
|
||||||
{/* key={routes.detailed_view + BatteryId}*/}
|
|
||||||
{/* path={routes.detailed_view + BatteryId}*/}
|
|
||||||
{/* element={*/}
|
|
||||||
{/* <DetailedBatteryView*/}
|
|
||||||
{/* batteryId={Number(BatteryId)}*/}
|
|
||||||
{/* s3Credentials={props.s3Credentials}*/}
|
|
||||||
{/* batteryData={battery}*/}
|
|
||||||
{/* installationId={props.installationId}*/}
|
|
||||||
{/* productNum={product}*/}
|
|
||||||
{/* ></DetailedBatteryView>*/}
|
|
||||||
{/* }*/}
|
|
||||||
{/* />*/}
|
|
||||||
{/* )*/}
|
|
||||||
{/* )*/}
|
|
||||||
{/* : Object.entries(props.values.Battery.Devices).map(*/}
|
|
||||||
{/* ([BatteryId, battery]) => (*/}
|
|
||||||
{/* <Route*/}
|
|
||||||
{/* key={routes.detailed_view + BatteryId}*/}
|
|
||||||
{/* path={routes.detailed_view + BatteryId}*/}
|
|
||||||
{/* element={*/}
|
|
||||||
{/* <DetailedBatteryViewSodistore*/}
|
|
||||||
{/* batteryId={Number(BatteryId)}*/}
|
|
||||||
{/* s3Credentials={props.s3Credentials}*/}
|
|
||||||
{/* batteryData={battery}*/}
|
|
||||||
{/* installationId={props.installationId}*/}
|
|
||||||
{/* productNum={product}*/}
|
|
||||||
{/* ></DetailedBatteryViewSodistore>*/}
|
|
||||||
{/* }*/}
|
|
||||||
{/* />*/}
|
|
||||||
{/* )*/}
|
|
||||||
{/* )}*/}
|
|
||||||
{/* </Routes>*/}
|
|
||||||
{/*</Grid>*/}
|
|
||||||
|
|
||||||
<TableContainer
|
<TableContainer
|
||||||
component={Paper}
|
component={Paper}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,818 @@
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
Modal,
|
||||||
|
TextField,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
||||||
|
import ReactApexChart from 'react-apexcharts';
|
||||||
|
import { getChartOptions } from '../Overview/chartOptions';
|
||||||
|
import {
|
||||||
|
BatteryDataInterface,
|
||||||
|
BatteryOverviewInterface,
|
||||||
|
transformInputToBatteryViewDataJson
|
||||||
|
} from '../../../interfaces/Chart';
|
||||||
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
|
import { TimeSpan, UnixTime } from '../../../dataCache/time';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
|
||||||
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
|
|
||||||
|
interface MainStatsSodioHomeProps {
|
||||||
|
s3Credentials: I_S3Credentials;
|
||||||
|
id: number;
|
||||||
|
batteryClusterNumber: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MainStatsSodioHome(props: MainStatsSodioHomeProps) {
|
||||||
|
const [chartState, setChartState] = useState(0);
|
||||||
|
const [batteryViewDataArray, setBatteryViewDataArray] = useState<
|
||||||
|
{
|
||||||
|
chartData: BatteryDataInterface;
|
||||||
|
chartOverview: BatteryOverviewInterface;
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const [isDateModalOpen, setIsDateModalOpen] = useState(false);
|
||||||
|
const [dateOpen, setDateOpen] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [startDate, setStartDate] = useState(dayjs().add(-1, 'day'));
|
||||||
|
const [endDate, setEndDate] = useState(dayjs());
|
||||||
|
const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
|
||||||
|
const [dateSelectionError, setDateSelectionError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const blueColors = [
|
||||||
|
'#99CCFF',
|
||||||
|
'#80BFFF',
|
||||||
|
'#6699CC',
|
||||||
|
'#4D99FF',
|
||||||
|
'#2670E6',
|
||||||
|
'#3366CC',
|
||||||
|
'#1A4D99',
|
||||||
|
'#133366',
|
||||||
|
'#0D274D',
|
||||||
|
'#081A33'
|
||||||
|
];
|
||||||
|
const redColors = [
|
||||||
|
'#ff9090',
|
||||||
|
'#ff7070',
|
||||||
|
'#ff3f3f',
|
||||||
|
'#ff1e1e',
|
||||||
|
'#ff0606',
|
||||||
|
'#fc0000',
|
||||||
|
'#f40000',
|
||||||
|
'#d40000',
|
||||||
|
'#a30000',
|
||||||
|
'#7a0000'
|
||||||
|
];
|
||||||
|
const orangeColors = [
|
||||||
|
'#ffdb99',
|
||||||
|
'#ffc968',
|
||||||
|
'#ffb837',
|
||||||
|
'#ffac16',
|
||||||
|
'#ffa706',
|
||||||
|
'#FF8C00',
|
||||||
|
'#d48900',
|
||||||
|
'#CC7A00',
|
||||||
|
'#a36900',
|
||||||
|
'#993D00'
|
||||||
|
];
|
||||||
|
const greenColors = [
|
||||||
|
'#90EE90',
|
||||||
|
'#77DD77',
|
||||||
|
'#5ECE5E',
|
||||||
|
'#45BF45',
|
||||||
|
'#32CD32',
|
||||||
|
'#28A428',
|
||||||
|
'#1E7B1E',
|
||||||
|
'#145214',
|
||||||
|
'#0A390A',
|
||||||
|
'#052905'
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const resultPromise: Promise<{
|
||||||
|
chartData: BatteryDataInterface;
|
||||||
|
chartOverview: BatteryOverviewInterface;
|
||||||
|
}> = transformInputToBatteryViewDataJson(
|
||||||
|
props.s3Credentials,
|
||||||
|
props.id,
|
||||||
|
2,
|
||||||
|
UnixTime.fromTicks(new Date().getTime() / 1000).earlier(
|
||||||
|
TimeSpan.fromDays(1)
|
||||||
|
),
|
||||||
|
UnixTime.fromTicks(new Date().getTime() / 1000),
|
||||||
|
props.batteryClusterNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
resultPromise
|
||||||
|
.then((result) => {
|
||||||
|
setBatteryViewDataArray((prevData) =>
|
||||||
|
prevData.concat({
|
||||||
|
chartData: result.chartData,
|
||||||
|
chartOverview: result.chartOverview
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [isZooming, setIsZooming] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isZooming) {
|
||||||
|
setLoading(true);
|
||||||
|
} else if (!isZooming && batteryViewDataArray.length > 0) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [isZooming, batteryViewDataArray]);
|
||||||
|
|
||||||
|
function generateSeries(chartData, category, color) {
|
||||||
|
const series = [];
|
||||||
|
const pathsToSearch = [];
|
||||||
|
for (let i = 0; i < props.batteryClusterNumber; i++) {
|
||||||
|
pathsToSearch.push('Node' + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
pathsToSearch.forEach((devicePath) => {
|
||||||
|
if (
|
||||||
|
Object.hasOwnProperty.call(chartData[category].data, devicePath) &&
|
||||||
|
chartData[category].data[devicePath].data.length != 0
|
||||||
|
) {
|
||||||
|
series.push({
|
||||||
|
...chartData[category].data[devicePath],
|
||||||
|
color:
|
||||||
|
color === 'blue'
|
||||||
|
? blueColors[i]
|
||||||
|
: color === 'red'
|
||||||
|
? redColors[i]
|
||||||
|
: color === 'green'
|
||||||
|
? greenColors[i]
|
||||||
|
: orangeColors[i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
});
|
||||||
|
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
|
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('End date must precede start date');
|
||||||
|
setErrorDateModalOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const resultPromise: Promise<{
|
||||||
|
chartData: BatteryDataInterface;
|
||||||
|
chartOverview: BatteryOverviewInterface;
|
||||||
|
}> = transformInputToBatteryViewDataJson(
|
||||||
|
props.s3Credentials,
|
||||||
|
props.id,
|
||||||
|
2,
|
||||||
|
UnixTime.fromTicks(startDate.unix()),
|
||||||
|
UnixTime.fromTicks(endDate.unix()),
|
||||||
|
props.batteryClusterNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
resultPromise
|
||||||
|
.then((result) => {
|
||||||
|
setBatteryViewDataArray((prevData) =>
|
||||||
|
prevData.concat({
|
||||||
|
chartData: result.chartData,
|
||||||
|
chartOverview: result.chartOverview
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
setChartState(batteryViewDataArray.length);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleSetDate = () => {
|
||||||
|
setDateOpen(true);
|
||||||
|
setIsDateModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBatteryViewButton = () => {
|
||||||
|
navigate(
|
||||||
|
location.pathname.split('/').slice(0, -2).join('/') + '/batteryview'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoBack = () => {
|
||||||
|
if (chartState > 0) {
|
||||||
|
setChartState(chartState - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoForward = () => {
|
||||||
|
if (chartState + 1 < batteryViewDataArray.length) {
|
||||||
|
setChartState(chartState + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOkOnErrorDateModal = () => {
|
||||||
|
setErrorDateModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startZoom = () => {
|
||||||
|
setIsZooming(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBeforeZoom = (chartContext, { xaxis }) => {
|
||||||
|
const startX = parseInt(xaxis.min) / 1000;
|
||||||
|
const endX = parseInt(xaxis.max) / 1000;
|
||||||
|
|
||||||
|
const resultPromise: Promise<{
|
||||||
|
chartData: BatteryDataInterface;
|
||||||
|
chartOverview: BatteryOverviewInterface;
|
||||||
|
}> = transformInputToBatteryViewDataJson(
|
||||||
|
props.s3Credentials,
|
||||||
|
props.id,
|
||||||
|
2,
|
||||||
|
UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)),
|
||||||
|
UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2)),
|
||||||
|
props.batteryClusterNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
resultPromise
|
||||||
|
.then((result) => {
|
||||||
|
setBatteryViewDataArray((prevData) =>
|
||||||
|
prevData.concat({
|
||||||
|
chartData: result.chartData,
|
||||||
|
chartOverview: result.chartOverview
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setIsZooming(false);
|
||||||
|
setChartState(batteryViewDataArray.length);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
{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: Dayjs | null) => {
|
||||||
|
if (newDate) {
|
||||||
|
setStartDate(newDate);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
sx={{
|
||||||
|
marginTop: 2,
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateTimePicker
|
||||||
|
label="Select End Date"
|
||||||
|
value={endDate}
|
||||||
|
onChange={(newDate: Dayjs | null) => {
|
||||||
|
if (newDate) {
|
||||||
|
setEndDate(newDate);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
sx={{
|
||||||
|
marginTop: 2,
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && (
|
||||||
|
<>
|
||||||
|
<Grid item xs={6} md={6}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="go back"
|
||||||
|
sx={{
|
||||||
|
marginTop: '20px',
|
||||||
|
backgroundColor: 'grey',
|
||||||
|
color: '#000000',
|
||||||
|
'&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
}}
|
||||||
|
onClick={handleBatteryViewButton}
|
||||||
|
>
|
||||||
|
<ArrowBackIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleSetDate}
|
||||||
|
disabled={loading}
|
||||||
|
sx={{
|
||||||
|
marginTop: '20px',
|
||||||
|
marginLeft: '20px',
|
||||||
|
backgroundColor: dateOpen ? '#808080' : '#ffc04d',
|
||||||
|
color: '#000000',
|
||||||
|
'&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="set_date" defaultMessage="Set Date" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
justifyContent="flex-end"
|
||||||
|
alignItems="center"
|
||||||
|
item
|
||||||
|
xs={6}
|
||||||
|
md={6}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
disabled={!(chartState > 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={!(chartState < batteryViewDataArray.length - 1)}
|
||||||
|
onClick={handleGoForward}
|
||||||
|
sx={{
|
||||||
|
marginTop: '20px',
|
||||||
|
marginLeft: '10px',
|
||||||
|
backgroundColor: '#ffc04d',
|
||||||
|
color: '#000000',
|
||||||
|
'&:hover': { bgcolor: '#f7b34d' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="goback" defaultMessage="Zoom in" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="stretch"
|
||||||
|
spacing={3}
|
||||||
|
>
|
||||||
|
{/* Battery SOC Chart */}
|
||||||
|
<Grid item md={12} 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(
|
||||||
|
batteryViewDataArray[chartState].chartOverview.Soc,
|
||||||
|
'daily',
|
||||||
|
[],
|
||||||
|
true
|
||||||
|
),
|
||||||
|
chart: {
|
||||||
|
events: {
|
||||||
|
beforeZoom: (chartContext, options) => {
|
||||||
|
startZoom();
|
||||||
|
handleBeforeZoom(chartContext, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
series={generateSeries(
|
||||||
|
batteryViewDataArray[chartState].chartData,
|
||||||
|
'Soc',
|
||||||
|
'blue'
|
||||||
|
)}
|
||||||
|
type="line"
|
||||||
|
height={420}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Battery Power Chart */}
|
||||||
|
<Grid item md={12} xs={12}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
overflow: 'visible',
|
||||||
|
marginTop: '10px',
|
||||||
|
marginBottom: '30px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ marginLeft: '20px' }}>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" noWrap>
|
||||||
|
<FormattedMessage
|
||||||
|
id="battery_power"
|
||||||
|
defaultMessage="Battery Power"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
pt: 3
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ReactApexChart
|
||||||
|
options={{
|
||||||
|
...getChartOptions(
|
||||||
|
batteryViewDataArray[chartState].chartOverview.Power,
|
||||||
|
'daily',
|
||||||
|
[],
|
||||||
|
true
|
||||||
|
),
|
||||||
|
chart: {
|
||||||
|
events: {
|
||||||
|
beforeZoom: (chartContext, options) => {
|
||||||
|
startZoom();
|
||||||
|
handleBeforeZoom(chartContext, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
series={generateSeries(
|
||||||
|
batteryViewDataArray[chartState].chartData,
|
||||||
|
'Power',
|
||||||
|
'red'
|
||||||
|
)}
|
||||||
|
type="line"
|
||||||
|
height={420}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Battery Voltage Chart */}
|
||||||
|
<Grid item md={12} xs={12}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
overflow: 'visible',
|
||||||
|
marginTop: '10px',
|
||||||
|
marginBottom: '30px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ marginLeft: '20px' }}>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" noWrap>
|
||||||
|
<FormattedMessage
|
||||||
|
id="battery_voltage"
|
||||||
|
defaultMessage="Battery Voltage"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
pt: 3
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ReactApexChart
|
||||||
|
options={{
|
||||||
|
...getChartOptions(
|
||||||
|
batteryViewDataArray[chartState].chartOverview.Voltage,
|
||||||
|
'daily',
|
||||||
|
[],
|
||||||
|
true
|
||||||
|
),
|
||||||
|
chart: {
|
||||||
|
events: {
|
||||||
|
beforeZoom: (chartContext, options) => {
|
||||||
|
startZoom();
|
||||||
|
handleBeforeZoom(chartContext, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
series={generateSeries(
|
||||||
|
batteryViewDataArray[chartState].chartData,
|
||||||
|
'Voltage',
|
||||||
|
'orange'
|
||||||
|
)}
|
||||||
|
type="line"
|
||||||
|
height={420}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Battery Current Chart */}
|
||||||
|
<Grid item md={12} xs={12}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
overflow: 'visible',
|
||||||
|
marginTop: '10px',
|
||||||
|
marginBottom: '30px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ marginLeft: '20px' }}>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" noWrap>
|
||||||
|
<FormattedMessage
|
||||||
|
id="battery_current"
|
||||||
|
defaultMessage="Battery Current"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
pt: 3
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ReactApexChart
|
||||||
|
options={{
|
||||||
|
...getChartOptions(
|
||||||
|
batteryViewDataArray[chartState].chartOverview.Current,
|
||||||
|
'daily',
|
||||||
|
[],
|
||||||
|
true
|
||||||
|
),
|
||||||
|
chart: {
|
||||||
|
events: {
|
||||||
|
beforeZoom: (chartContext, options) => {
|
||||||
|
startZoom();
|
||||||
|
handleBeforeZoom(chartContext, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
series={generateSeries(
|
||||||
|
batteryViewDataArray[chartState].chartData,
|
||||||
|
'Current',
|
||||||
|
'orange'
|
||||||
|
)}
|
||||||
|
type="line"
|
||||||
|
height={420}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Battery SoH Chart */}
|
||||||
|
<Grid item md={12} xs={12}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
overflow: 'visible',
|
||||||
|
marginTop: '10px',
|
||||||
|
marginBottom: '30px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ marginLeft: '20px' }}>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" noWrap>
|
||||||
|
<FormattedMessage
|
||||||
|
id="battery_soh"
|
||||||
|
defaultMessage="Battery SOH (State Of Health)"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
pt: 3
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ReactApexChart
|
||||||
|
options={{
|
||||||
|
...getChartOptions(
|
||||||
|
batteryViewDataArray[chartState].chartOverview.Soh,
|
||||||
|
'daily',
|
||||||
|
[],
|
||||||
|
true
|
||||||
|
),
|
||||||
|
chart: {
|
||||||
|
events: {
|
||||||
|
beforeZoom: (chartContext, options) => {
|
||||||
|
startZoom();
|
||||||
|
handleBeforeZoom(chartContext, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
series={generateSeries(
|
||||||
|
batteryViewDataArray[chartState].chartData,
|
||||||
|
'Soh',
|
||||||
|
'green'
|
||||||
|
)}
|
||||||
|
type="line"
|
||||||
|
height={420}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MainStatsSodioHome;
|
||||||
|
|
@ -7,7 +7,6 @@ import {
|
||||||
FormControl,
|
FormControl,
|
||||||
Grid,
|
Grid,
|
||||||
IconButton,
|
IconButton,
|
||||||
InputAdornment,
|
|
||||||
InputLabel,
|
InputLabel,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Modal,
|
Modal,
|
||||||
|
|
@ -19,7 +18,7 @@ import {
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { Close as CloseIcon } from '@mui/icons-material';
|
import { Close as CloseIcon } from '@mui/icons-material';
|
||||||
import React, { useContext, useState, useEffect } from 'react';
|
import React, { useContext, useState, useEffect, useRef } from 'react';
|
||||||
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
||||||
import { I_Installation } from '../../../interfaces/InstallationTypes';
|
import { I_Installation } from '../../../interfaces/InstallationTypes';
|
||||||
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
|
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
|
||||||
|
|
@ -57,8 +56,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
||||||
{ id: 4, name: 'Sinexcel' }
|
{ id: 4, name: 'Sinexcel' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const BATTERY_SN_PREFIX = 'PNR020125101';
|
const batterySnRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||||
const BATTERY_SN_SUFFIX_LENGTH = 4;
|
|
||||||
|
|
||||||
// Initialize battery data from props
|
// Initialize battery data from props
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -68,14 +66,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
||||||
if (props.values.batterySerialNumbers) {
|
if (props.values.batterySerialNumbers) {
|
||||||
const serialNumbers = props.values.batterySerialNumbers
|
const serialNumbers = props.values.batterySerialNumbers
|
||||||
.split(',')
|
.split(',')
|
||||||
.filter((sn) => sn.trim() !== '')
|
.filter((sn) => sn.trim() !== '');
|
||||||
.map((sn) => {
|
|
||||||
// If it has the prefix, extract only the suffix
|
|
||||||
if (sn.startsWith(BATTERY_SN_PREFIX)) {
|
|
||||||
return sn.substring(BATTERY_SN_PREFIX.length);
|
|
||||||
}
|
|
||||||
return sn;
|
|
||||||
});
|
|
||||||
setBatterySerialNumbers(serialNumbers);
|
setBatterySerialNumbers(serialNumbers);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -107,41 +98,47 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
||||||
const value = inputValue === '' ? 0 : parseInt(inputValue);
|
const value = inputValue === '' ? 0 : parseInt(inputValue);
|
||||||
setBatteryNumber(value);
|
setBatteryNumber(value);
|
||||||
|
|
||||||
// Preserve existing serial numbers and adjust array size
|
if (value > 0) {
|
||||||
|
// Resize array: preserve existing serial numbers, add empty for new slots
|
||||||
const newSerialNumbers = Array.from({ length: value }, (_, index) => {
|
const newSerialNumbers = Array.from({ length: value }, (_, index) => {
|
||||||
// Keep existing serial number if it exists, otherwise use empty string
|
|
||||||
return batterySerialNumbers[index] || '';
|
return batterySerialNumbers[index] || '';
|
||||||
});
|
});
|
||||||
setBatterySerialNumbers(newSerialNumbers);
|
setBatterySerialNumbers(newSerialNumbers);
|
||||||
|
|
||||||
// Update formValues with preserved serial numbers
|
|
||||||
const fullSerialNumbers = newSerialNumbers
|
|
||||||
.map((suffix) => (suffix ? BATTERY_SN_PREFIX + suffix : ''))
|
|
||||||
.filter((sn) => sn !== '');
|
|
||||||
|
|
||||||
setFormValues({
|
setFormValues({
|
||||||
...formValues,
|
...formValues,
|
||||||
batteryNumber: value,
|
batteryNumber: value,
|
||||||
batterySerialNumbers: fullSerialNumbers.join(',')
|
batterySerialNumbers: newSerialNumbers.filter((sn) => sn !== '').join(',')
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Field is empty (user is mid-edit) — don't clear serial numbers
|
||||||
|
setFormValues({
|
||||||
|
...formValues,
|
||||||
|
batteryNumber: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBatterySerialNumberChange = (index: number, value: string) => {
|
const handleBatterySerialNumberChange = (index: number, value: string) => {
|
||||||
// Only allow digits and limit to 3 characters
|
|
||||||
const sanitizedValue = value.replace(/\D/g, '').substring(0, BATTERY_SN_SUFFIX_LENGTH);
|
|
||||||
const updatedSerialNumbers = [...batterySerialNumbers];
|
const updatedSerialNumbers = [...batterySerialNumbers];
|
||||||
updatedSerialNumbers[index] = sanitizedValue;
|
updatedSerialNumbers[index] = value;
|
||||||
setBatterySerialNumbers(updatedSerialNumbers);
|
setBatterySerialNumbers(updatedSerialNumbers);
|
||||||
// Update formValues for persistence with full serial numbers (prefix + suffix)
|
|
||||||
const fullSerialNumbers = updatedSerialNumbers
|
|
||||||
.map((suffix) => (suffix ? BATTERY_SN_PREFIX + suffix : ''))
|
|
||||||
.filter((sn) => sn !== '');
|
|
||||||
setFormValues({
|
setFormValues({
|
||||||
...formValues,
|
...formValues,
|
||||||
batterySerialNumbers: fullSerialNumbers.join(',')
|
batterySerialNumbers: updatedSerialNumbers.filter((sn) => sn !== '').join(',')
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBatterySnKeyDown = (e: React.KeyboardEvent, index: number) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
const nextIndex = index + 1;
|
||||||
|
if (nextIndex < batteryNumber && batterySnRefs.current[nextIndex]) {
|
||||||
|
batterySnRefs.current[nextIndex].focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(false);
|
setError(false);
|
||||||
|
|
@ -467,20 +464,11 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleBatterySerialNumberChange(index, e.target.value)
|
handleBatterySerialNumberChange(index, e.target.value)
|
||||||
}
|
}
|
||||||
|
onKeyDown={(e) => handleBatterySnKeyDown(e, index)}
|
||||||
|
inputRef={(el) => (batterySnRefs.current[index] = el)}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputProps={{
|
placeholder="Scan or enter serial number"
|
||||||
startAdornment: (
|
|
||||||
<InputAdornment position="start">
|
|
||||||
{BATTERY_SN_PREFIX}
|
|
||||||
</InputAdornment>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
inputProps={{
|
|
||||||
maxLength: BATTERY_SN_SUFFIX_LENGTH,
|
|
||||||
placeholder: '0000'
|
|
||||||
}}
|
|
||||||
helperText={`Enter ${BATTERY_SN_SUFFIX_LENGTH} digits`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,12 @@ function Overview(props: OverviewProps) {
|
||||||
|
|
||||||
resultPromise
|
resultPromise
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
if (result.chartData.soc.data.length === 0) {
|
||||||
|
setDateSelectionError('No data available for the selected date range. Please choose a more recent date.');
|
||||||
|
setErrorDateModalOpen(true);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setDailyDataArray((prevData) =>
|
setDailyDataArray((prevData) =>
|
||||||
prevData.concat({
|
prevData.concat({
|
||||||
chartData: result.chartData,
|
chartData: result.chartData,
|
||||||
|
|
@ -281,6 +287,12 @@ function Overview(props: OverviewProps) {
|
||||||
|
|
||||||
resultPromise
|
resultPromise
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
if (result.chartData.soc.data.length === 0) {
|
||||||
|
setDateSelectionError('No data available for the selected date range. Please choose a more recent date.');
|
||||||
|
setErrorDateModalOpen(true);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setDailyDataArray((prevData) =>
|
setDailyDataArray((prevData) =>
|
||||||
prevData.concat({
|
prevData.concat({
|
||||||
chartData: result.chartData,
|
chartData: result.chartData,
|
||||||
|
|
@ -511,6 +523,7 @@ function Overview(props: OverviewProps) {
|
||||||
>
|
>
|
||||||
<FormattedMessage id="24_hours" defaultMessage="24-hours" />
|
<FormattedMessage id="24_hours" defaultMessage="24-hours" />
|
||||||
</Button>
|
</Button>
|
||||||
|
{product !== 2 && (
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleWeekData}
|
onClick={handleWeekData}
|
||||||
|
|
@ -525,6 +538,7 @@ function Overview(props: OverviewProps) {
|
||||||
>
|
>
|
||||||
<FormattedMessage id="lastweek" defaultMessage="Last week" />
|
<FormattedMessage id="lastweek" defaultMessage="Last week" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/*{aggregatedData && (*/}
|
{/*{aggregatedData && (*/}
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -1013,7 +1027,7 @@ function Overview(props: OverviewProps) {
|
||||||
alignItems="stretch"
|
alignItems="stretch"
|
||||||
spacing={3}
|
spacing={3}
|
||||||
>
|
>
|
||||||
<Grid item md={6} xs={12}>
|
<Grid item md={product === 2 ? 12 : 6} xs={12}>
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{
|
||||||
overflow: 'visible',
|
overflow: 'visible',
|
||||||
|
|
@ -1074,6 +1088,7 @@ function Overview(props: OverviewProps) {
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{product !== 2 && (
|
||||||
<Grid item md={6} xs={12}>
|
<Grid item md={6} xs={12}>
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -1136,6 +1151,7 @@ function Overview(props: OverviewProps) {
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -1336,7 +1352,7 @@ function Overview(props: OverviewProps) {
|
||||||
alignItems="stretch"
|
alignItems="stretch"
|
||||||
spacing={3}
|
spacing={3}
|
||||||
>
|
>
|
||||||
<Grid item md={6} xs={12}>
|
<Grid item md={product === 2 ? 12 : 6} xs={12}>
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{
|
||||||
overflow: 'visible',
|
overflow: 'visible',
|
||||||
|
|
@ -1397,6 +1413,7 @@ function Overview(props: OverviewProps) {
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{product !== 2 && (
|
||||||
<Grid item md={6} xs={12}>
|
<Grid item md={6} xs={12}>
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -1458,6 +1475,7 @@ function Overview(props: OverviewProps) {
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import { FetchResult } from '../../../dataCache/dataCache';
|
||||||
import BatteryViewSodioHome from '../BatteryView/BatteryViewSodioHome';
|
import BatteryViewSodioHome from '../BatteryView/BatteryViewSodioHome';
|
||||||
import SodistoreHomeConfiguration from './SodistoreHomeConfiguration';
|
import SodistoreHomeConfiguration from './SodistoreHomeConfiguration';
|
||||||
import TopologySodistoreHome from '../Topology/TopologySodistoreHome';
|
import TopologySodistoreHome from '../Topology/TopologySodistoreHome';
|
||||||
|
import Overview from '../Overview/overview';
|
||||||
|
|
||||||
interface singleInstallationProps {
|
interface singleInstallationProps {
|
||||||
current_installation?: I_Installation;
|
current_installation?: I_Installation;
|
||||||
|
|
@ -182,11 +183,13 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchDataForOneTime = async () => {
|
const fetchDataForOneTime = async () => {
|
||||||
var timeperiodToSearch = 200;
|
var timeperiodToSearch = 300; // 5 minutes to cover ~4 upload cycles
|
||||||
let res;
|
let res;
|
||||||
let timestampToFetch;
|
let timestampToFetch;
|
||||||
|
|
||||||
for (var i = timeperiodToSearch; i > 0; i -= 2) {
|
// Search from NOW backward to find the most recent data
|
||||||
|
// Step by 10 seconds - balances between finding files quickly and reducing 404s
|
||||||
|
for (var i = 0; i < timeperiodToSearch; i += 10) {
|
||||||
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
|
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
|
||||||
try {
|
try {
|
||||||
res = await fetchDataJson(timestampToFetch, s3Credentials, false);
|
res = await fetchDataJson(timestampToFetch, s3Credentials, false);
|
||||||
|
|
@ -199,7 +202,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i <= 0) {
|
if (i >= timeperiodToSearch) {
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -207,8 +210,10 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
setConnected(true);
|
setConnected(true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
const timestamp = Object.keys(res)[Object.keys(res).length - 1];
|
// Sort timestamps numerically to ensure we get the most recent data point
|
||||||
setValues(res[timestamp]);
|
const timestamps = Object.keys(res).sort((a, b) => Number(b) - Number(a));
|
||||||
|
const latestTimestamp = timestamps[0];
|
||||||
|
setValues(res[latestTimestamp]);
|
||||||
// setValues(
|
// setValues(
|
||||||
// extractValues({
|
// extractValues({
|
||||||
// time: UnixTime.fromTicks(parseInt(timestamp, 10)),
|
// time: UnixTime.fromTicks(parseInt(timestamp, 10)),
|
||||||
|
|
@ -251,9 +256,19 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fetch only one time in configuration tab
|
// Fetch periodically in configuration tab (every 30 seconds to detect S3 updates)
|
||||||
if (currentTab == 'configuration') {
|
if (currentTab == 'configuration') {
|
||||||
|
fetchDataForOneTime(); // Initial fetch
|
||||||
|
|
||||||
|
const configRefreshInterval = setInterval(() => {
|
||||||
|
console.log('Refreshing configuration data from S3...');
|
||||||
fetchDataForOneTime();
|
fetchDataForOneTime();
|
||||||
|
}, 15000); // Refresh every 15 seconds
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
continueFetching.current = false;
|
||||||
|
clearInterval(configRefreshInterval);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -527,6 +542,16 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={routes.overview}
|
||||||
|
element={
|
||||||
|
<Overview
|
||||||
|
s3Credentials={s3Credentials}
|
||||||
|
id={props.current_installation.id}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={'*'}
|
path={'*'}
|
||||||
element={<Navigate to={routes.live}></Navigate>}
|
element={<Navigate to={routes.live}></Navigate>}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import {
|
||||||
useTheme
|
useTheme
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
||||||
import React, { useContext, useState } from 'react';
|
import React, { useContext, useState, useEffect } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { Close as CloseIcon } from '@mui/icons-material';
|
import { Close as CloseIcon } from '@mui/icons-material';
|
||||||
|
|
@ -105,6 +105,78 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
||||||
controlPermission: String(props.values.Config.ControlPermission).toLowerCase() === "true",
|
controlPermission: String(props.values.Config.ControlPermission).toLowerCase() === "true",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Storage key for pending config (optimistic update)
|
||||||
|
const pendingConfigKey = `pendingConfig_${props.id}`;
|
||||||
|
|
||||||
|
// Helper to get current S3 values
|
||||||
|
const getS3Values = () => ({
|
||||||
|
minimumSoC: props.values.Config.MinSoc,
|
||||||
|
maximumDischargingCurrent: props.values.Config.MaximumChargingCurrent,
|
||||||
|
maximumChargingCurrent: props.values.Config.MaximumDischargingCurrent,
|
||||||
|
operatingPriority: OperatingPriorityOptions.indexOf(
|
||||||
|
props.values.Config.OperatingPriority
|
||||||
|
),
|
||||||
|
batteriesCount: props.values.Config.BatteriesCount,
|
||||||
|
clusterNumber: props.values.Config.ClusterNumber ?? 1,
|
||||||
|
PvNumber: props.values.Config.PvNumber ?? 0,
|
||||||
|
timeChargeandDischargePower: props.values.Config?.TimeChargeandDischargePower ?? 0,
|
||||||
|
startTimeChargeandDischargeDayandTime:
|
||||||
|
props.values.Config?.StartTimeChargeandDischargeDayandTime
|
||||||
|
? dayjs(props.values.Config.StartTimeChargeandDischargeDayandTime).toDate()
|
||||||
|
: null,
|
||||||
|
stopTimeChargeandDischargeDayandTime:
|
||||||
|
props.values.Config?.StopTimeChargeandDischargeDayandTime
|
||||||
|
? dayjs(props.values.Config.StopTimeChargeandDischargeDayandTime).toDate()
|
||||||
|
: null,
|
||||||
|
controlPermission: String(props.values.Config.ControlPermission).toLowerCase() === "true",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync form values when props.values changes
|
||||||
|
// Logic: Use localStorage only briefly after submit to prevent flicker, then trust S3
|
||||||
|
useEffect(() => {
|
||||||
|
const s3Values = getS3Values();
|
||||||
|
const pendingConfigStr = localStorage.getItem(pendingConfigKey);
|
||||||
|
|
||||||
|
if (pendingConfigStr) {
|
||||||
|
try {
|
||||||
|
const pendingConfig = JSON.parse(pendingConfigStr);
|
||||||
|
const submittedAt = pendingConfig.submittedAt || 0;
|
||||||
|
const timeSinceSubmit = Date.now() - submittedAt;
|
||||||
|
|
||||||
|
// Within 150 seconds of submit: use localStorage (waiting for S3 sync)
|
||||||
|
// This covers two full S3 upload cycles (75 sec × 2) to ensure new file is available
|
||||||
|
if (timeSinceSubmit < 150000) {
|
||||||
|
// Check if S3 now matches - if so, sync is complete
|
||||||
|
const s3MatchesPending =
|
||||||
|
s3Values.controlPermission === pendingConfig.values.controlPermission &&
|
||||||
|
s3Values.minimumSoC === pendingConfig.values.minimumSoC &&
|
||||||
|
s3Values.operatingPriority === pendingConfig.values.operatingPriority;
|
||||||
|
|
||||||
|
if (s3MatchesPending) {
|
||||||
|
// S3 synced! Clear localStorage and use S3 from now on
|
||||||
|
console.log('S3 synced with submitted config');
|
||||||
|
localStorage.removeItem(pendingConfigKey);
|
||||||
|
setFormValues(s3Values);
|
||||||
|
} else {
|
||||||
|
// Still waiting for sync, keep showing submitted values
|
||||||
|
console.log('Waiting for S3 sync, showing submitted values');
|
||||||
|
setFormValues(pendingConfig.values);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout expired: clear localStorage, trust S3 completely
|
||||||
|
console.log('Timeout expired, trusting S3 data');
|
||||||
|
localStorage.removeItem(pendingConfigKey);
|
||||||
|
} catch (e) {
|
||||||
|
localStorage.removeItem(pendingConfigKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No localStorage or expired: always use S3 (source of truth)
|
||||||
|
setFormValues(s3Values);
|
||||||
|
}, [props.values]);
|
||||||
|
|
||||||
const handleOperatingPriorityChange = (event) => {
|
const handleOperatingPriorityChange = (event) => {
|
||||||
setFormValues({
|
setFormValues({
|
||||||
...formValues,
|
...formValues,
|
||||||
|
|
@ -173,6 +245,13 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
||||||
if (res) {
|
if (res) {
|
||||||
setUpdated(true);
|
setUpdated(true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
|
// Save submitted values to localStorage for optimistic UI update
|
||||||
|
// This ensures the form shows correct values even before S3 syncs (up to 75 sec delay)
|
||||||
|
localStorage.setItem(pendingConfigKey, JSON.stringify({
|
||||||
|
values: formValues,
|
||||||
|
submittedAt: Date.now()
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
||||||
const { currentUser } = context;
|
const { currentUser } = context;
|
||||||
const tabList = [
|
const tabList = [
|
||||||
'live',
|
'live',
|
||||||
|
'overview',
|
||||||
'batteryview',
|
'batteryview',
|
||||||
'information',
|
'information',
|
||||||
'manage',
|
'manage',
|
||||||
'overview',
|
|
||||||
'log',
|
'log',
|
||||||
'history',
|
'history',
|
||||||
'configuration'
|
'configuration'
|
||||||
|
|
@ -100,6 +100,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
||||||
value: 'live',
|
value: 'live',
|
||||||
label: <FormattedMessage id="live" defaultMessage="Live" />
|
label: <FormattedMessage id="live" defaultMessage="Live" />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'overview',
|
||||||
|
label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 'batteryview',
|
value: 'batteryview',
|
||||||
label: (
|
label: (
|
||||||
|
|
@ -109,10 +113,6 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// value: 'overview',
|
|
||||||
// label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
value: 'log',
|
value: 'log',
|
||||||
label: <FormattedMessage id="log" defaultMessage="Log" />
|
label: <FormattedMessage id="log" defaultMessage="Log" />
|
||||||
|
|
@ -159,11 +159,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
||||||
value: 'live',
|
value: 'live',
|
||||||
label: <FormattedMessage id="live" defaultMessage="Live" />
|
label: <FormattedMessage id="live" defaultMessage="Live" />
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// value: 'overview',
|
value: 'overview',
|
||||||
// label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
||||||
// },
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
value: 'information',
|
value: 'information',
|
||||||
label: (
|
label: (
|
||||||
|
|
@ -190,6 +189,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
||||||
value: 'live',
|
value: 'live',
|
||||||
label: <FormattedMessage id="live" defaultMessage="Live" />
|
label: <FormattedMessage id="live" defaultMessage="Live" />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'overview',
|
||||||
|
label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 'batteryview',
|
value: 'batteryview',
|
||||||
label: (
|
label: (
|
||||||
|
|
@ -199,10 +202,6 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// value: 'overview',
|
|
||||||
// label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
value: 'log',
|
value: 'log',
|
||||||
label: <FormattedMessage id="log" defaultMessage="Log" />
|
label: <FormattedMessage id="log" defaultMessage="Log" />
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ export interface BatteryDataInterface {
|
||||||
Power: { name: string; data: [] };
|
Power: { name: string; data: [] };
|
||||||
Voltage: { name: string; data: [] };
|
Voltage: { name: string; data: [] };
|
||||||
Current: { name: string; data: [] };
|
Current: { name: string; data: [] };
|
||||||
|
Soh?: { name: string; data: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BatteryOverviewInterface {
|
export interface BatteryOverviewInterface {
|
||||||
|
|
@ -68,6 +69,7 @@ export interface BatteryOverviewInterface {
|
||||||
Power: chartInfoInterface;
|
Power: chartInfoInterface;
|
||||||
Voltage: chartInfoInterface;
|
Voltage: chartInfoInterface;
|
||||||
Current: chartInfoInterface;
|
Current: chartInfoInterface;
|
||||||
|
Soh?: chartInfoInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const transformInputToBatteryViewDataJson = async (
|
export const transformInputToBatteryViewDataJson = async (
|
||||||
|
|
@ -75,14 +77,18 @@ export const transformInputToBatteryViewDataJson = async (
|
||||||
id: number,
|
id: number,
|
||||||
product: number,
|
product: number,
|
||||||
start_time?: UnixTime,
|
start_time?: UnixTime,
|
||||||
end_time?: UnixTime
|
end_time?: UnixTime,
|
||||||
|
batteryClusterNumber?: number
|
||||||
): Promise<{
|
): Promise<{
|
||||||
chartData: BatteryDataInterface;
|
chartData: BatteryDataInterface;
|
||||||
chartOverview: BatteryOverviewInterface;
|
chartOverview: BatteryOverviewInterface;
|
||||||
}> => {
|
}> => {
|
||||||
const prefixes = ['', 'k', 'M', 'G', 'T'];
|
const prefixes = ['', 'k', 'M', 'G', 'T'];
|
||||||
const MAX_NUMBER = 9999999;
|
const MAX_NUMBER = 9999999;
|
||||||
const categories = ['Soc', 'Temperature', 'Power', 'Voltage', 'Current'];
|
const isSodioHome = product === 2;
|
||||||
|
const categories = isSodioHome
|
||||||
|
? ['Soc', 'Power', 'Voltage', 'Current', 'Soh']
|
||||||
|
: ['Soc', 'Temperature', 'Power', 'Voltage', 'Current'];
|
||||||
const pathCategories =
|
const pathCategories =
|
||||||
product === 3
|
product === 3
|
||||||
? [
|
? [
|
||||||
|
|
@ -120,7 +126,8 @@ export const transformInputToBatteryViewDataJson = async (
|
||||||
Temperature: { name: 'Temperature', data: [] },
|
Temperature: { name: 'Temperature', data: [] },
|
||||||
Power: { name: 'Power', data: [] },
|
Power: { name: 'Power', data: [] },
|
||||||
Voltage: { name: 'Voltage', data: [] },
|
Voltage: { name: 'Voltage', data: [] },
|
||||||
Current: { name: 'Current', data: [] }
|
Current: { name: 'Current', data: [] },
|
||||||
|
...(isSodioHome && { Soh: { name: 'State Of Health', data: [] } })
|
||||||
};
|
};
|
||||||
|
|
||||||
const chartOverview: BatteryOverviewInterface = {
|
const chartOverview: BatteryOverviewInterface = {
|
||||||
|
|
@ -128,7 +135,8 @@ export const transformInputToBatteryViewDataJson = async (
|
||||||
Temperature: { magnitude: 0, unit: '', min: 0, max: 0 },
|
Temperature: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
Power: { magnitude: 0, unit: '', min: 0, max: 0 },
|
Power: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
Voltage: { magnitude: 0, unit: '', min: 0, max: 0 },
|
Voltage: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
Current: { magnitude: 0, unit: '', min: 0, max: 0 }
|
Current: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
...(isSodioHome && { Soh: { magnitude: 0, unit: '', min: 0, max: 0 } })
|
||||||
};
|
};
|
||||||
|
|
||||||
let initialiation = true;
|
let initialiation = true;
|
||||||
|
|
@ -159,7 +167,7 @@ export const transformInputToBatteryViewDataJson = async (
|
||||||
);
|
);
|
||||||
|
|
||||||
const adjustedTimestamp =
|
const adjustedTimestamp =
|
||||||
product == 0 || product == 3
|
product == 0 || product == 2 || product == 3
|
||||||
? new Date(timestampArray[i] * 1000)
|
? new Date(timestampArray[i] * 1000)
|
||||||
: new Date(timestampArray[i] * 100000);
|
: new Date(timestampArray[i] * 100000);
|
||||||
//Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset
|
//Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset
|
||||||
|
|
@ -181,7 +189,78 @@ export const transformInputToBatteryViewDataJson = async (
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = results[i][timestamp];
|
const result = results[i][timestamp];
|
||||||
//console.log(result);
|
|
||||||
|
if (isSodioHome) {
|
||||||
|
// SodistoreHome: extract battery data from InverterRecord
|
||||||
|
const inv = (result as any)?.InverterRecord;
|
||||||
|
if (!inv) continue;
|
||||||
|
|
||||||
|
const numBatteries = batteryClusterNumber || 1;
|
||||||
|
let old_length = pathsToSave.length;
|
||||||
|
|
||||||
|
if (numBatteries > old_length) {
|
||||||
|
for (let b = old_length; b < numBatteries; b++) {
|
||||||
|
const nodeName = 'Node' + b;
|
||||||
|
if (!pathsToSave.includes(nodeName)) {
|
||||||
|
pathsToSave.push(nodeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialiation) {
|
||||||
|
initialiation = false;
|
||||||
|
categories.forEach((category) => {
|
||||||
|
chartData[category].data = [];
|
||||||
|
chartOverview[category] = {
|
||||||
|
magnitude: 0,
|
||||||
|
unit: '',
|
||||||
|
min: MAX_NUMBER,
|
||||||
|
max: -MAX_NUMBER
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numBatteries > old_length) {
|
||||||
|
categories.forEach((category) => {
|
||||||
|
pathsToSave.forEach((path) => {
|
||||||
|
if (pathsToSave.indexOf(path) >= old_length) {
|
||||||
|
chartData[category].data[path] = { name: path, data: [] };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map category names to InverterRecord field suffixes
|
||||||
|
const categoryFieldMap = {
|
||||||
|
Soc: 'Soc',
|
||||||
|
Power: 'Power',
|
||||||
|
Voltage: 'Voltage',
|
||||||
|
Current: 'Current',
|
||||||
|
Soh: 'Soh'
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let j = 0; j < pathsToSave.length; j++) {
|
||||||
|
const batteryIndex = j + 1; // Battery1, Battery2, ...
|
||||||
|
categories.forEach((category) => {
|
||||||
|
const fieldName = `Battery${batteryIndex}${categoryFieldMap[category]}`;
|
||||||
|
const value = inv[fieldName];
|
||||||
|
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
if (value < chartOverview[category].min) {
|
||||||
|
chartOverview[category].min = value;
|
||||||
|
}
|
||||||
|
if (value > chartOverview[category].max) {
|
||||||
|
chartOverview[category].max = value;
|
||||||
|
}
|
||||||
|
chartData[category].data[pathsToSave[j]].data.push([
|
||||||
|
adjustedTimestampArray[i],
|
||||||
|
value
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// SaliMax, Salidomo, SodistoreMax: existing logic
|
||||||
const battery_nodes =
|
const battery_nodes =
|
||||||
result.Config.Devices.BatteryNodes.toString().split(',');
|
result.Config.Devices.BatteryNodes.toString().split(',');
|
||||||
|
|
||||||
|
|
@ -198,8 +277,6 @@ export const transformInputToBatteryViewDataJson = async (
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(pathsToSave);
|
|
||||||
|
|
||||||
if (initialiation) {
|
if (initialiation) {
|
||||||
initialiation = false;
|
initialiation = false;
|
||||||
categories.forEach((category) => {
|
categories.forEach((category) => {
|
||||||
|
|
@ -249,11 +326,7 @@ export const transformInputToBatteryViewDataJson = async (
|
||||||
adjustedTimestampArray[i],
|
adjustedTimestampArray[i],
|
||||||
value
|
value
|
||||||
]);
|
]);
|
||||||
} else {
|
}
|
||||||
// chartData[category].data[pathsToSave[j]].data.push([
|
|
||||||
// adjustedTimestampArray[i],
|
|
||||||
// null
|
|
||||||
// ]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -280,16 +353,20 @@ export const transformInputToBatteryViewDataJson = async (
|
||||||
chartOverview.Soc.unit = '(%)';
|
chartOverview.Soc.unit = '(%)';
|
||||||
chartOverview.Soc.min = 0;
|
chartOverview.Soc.min = 0;
|
||||||
chartOverview.Soc.max = 100;
|
chartOverview.Soc.max = 100;
|
||||||
|
if (!isSodioHome) {
|
||||||
chartOverview.Temperature.unit = '(°C)';
|
chartOverview.Temperature.unit = '(°C)';
|
||||||
|
}
|
||||||
chartOverview.Power.unit =
|
chartOverview.Power.unit =
|
||||||
'(' + prefixes[chartOverview['Power'].magnitude] + 'W' + ')';
|
'(' + prefixes[chartOverview['Power'].magnitude] + 'W' + ')';
|
||||||
chartOverview.Voltage.unit =
|
chartOverview.Voltage.unit =
|
||||||
'(' + prefixes[chartOverview['Voltage'].magnitude] + 'V' + ')';
|
'(' + prefixes[chartOverview['Voltage'].magnitude] + 'V' + ')';
|
||||||
|
|
||||||
chartOverview.Current.unit =
|
chartOverview.Current.unit =
|
||||||
'(' + prefixes[chartOverview['Current'].magnitude] + 'A' + ')';
|
'(' + prefixes[chartOverview['Current'].magnitude] + 'A' + ')';
|
||||||
|
if (isSodioHome) {
|
||||||
// console.log(chartData);
|
chartOverview.Soh.unit = '(%)';
|
||||||
|
chartOverview.Soh.min = 0;
|
||||||
|
chartOverview.Soh.max = 100;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chartData: chartData,
|
chartData: chartData,
|
||||||
|
|
@ -310,15 +387,25 @@ export const transformInputToDailyDataJson = async (
|
||||||
const prefixes = ['', 'k', 'M', 'G', 'T'];
|
const prefixes = ['', 'k', 'M', 'G', 'T'];
|
||||||
|
|
||||||
const MAX_NUMBER = 9999999;
|
const MAX_NUMBER = 9999999;
|
||||||
const pathsToSearch = [
|
// For SodioHome (product=2), paths are placeholders — actual extraction uses
|
||||||
|
// custom fallback logic to handle differences between Growatt and Sinexcel.
|
||||||
|
// Growatt has: Battery1AmbientTemperature, GridPower, PvPower
|
||||||
|
// Sinexcel has: Battery1Temperature, TotalGridPower (meter may be offline), PvPower1-4
|
||||||
|
const pathsToSearch = product == 2
|
||||||
|
? [
|
||||||
|
'SODIOHOME_SOC',
|
||||||
|
'SODIOHOME_TEMPERATURE',
|
||||||
|
'SODIOHOME_BATTERY_POWER',
|
||||||
|
'SODIOHOME_GRID_POWER',
|
||||||
|
'SODIOHOME_PV_POWER',
|
||||||
|
null, // dcBusVoltage not available for SodioHome
|
||||||
|
'SODIOHOME_CONSUMPTION',
|
||||||
|
null // DCLoad not available for SodioHome
|
||||||
|
]
|
||||||
|
: [
|
||||||
'Battery.Soc',
|
'Battery.Soc',
|
||||||
product == 0 ? 'Battery.Temperature' : 'Battery.TemperatureCell1',
|
product == 0 ? 'Battery.Temperature' : 'Battery.TemperatureCell1',
|
||||||
|
|
||||||
//'Battery.Temperature' for salimax,
|
|
||||||
//'Battery.TemperatureCell1',
|
|
||||||
product == 0 ? 'Battery.Dc.Power' : 'Battery.Power',
|
product == 0 ? 'Battery.Dc.Power' : 'Battery.Power',
|
||||||
//'Battery.Dc.Power' for salimax,
|
|
||||||
// 'Battery.Power',
|
|
||||||
'GridMeter.Ac.Power.Active',
|
'GridMeter.Ac.Power.Active',
|
||||||
'PvOnDc',
|
'PvOnDc',
|
||||||
'DcDc.Dc.Link.Voltage',
|
'DcDc.Dc.Link.Voltage',
|
||||||
|
|
@ -419,14 +506,55 @@ export const transformInputToDailyDataJson = async (
|
||||||
let category_index = 0;
|
let category_index = 0;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||||
pathsToSearch.forEach((path) => {
|
pathsToSearch.forEach((path) => {
|
||||||
if (get(result, path) !== undefined) {
|
if (path === null) {
|
||||||
|
// Skip unavailable fields (e.g. dcBusVoltage, DCLoad for SodioHome)
|
||||||
|
category_index++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let value: number | undefined = undefined;
|
let value: number | undefined = undefined;
|
||||||
|
|
||||||
if (category_index === 4) {
|
if (product === 2) {
|
||||||
// Custom logic for 'PvOnDc.Dc.Power'
|
// SodioHome: custom extraction with fallbacks for Growatt/Sinexcel
|
||||||
|
const inv = result?.InverterRecord;
|
||||||
|
if (inv) {
|
||||||
|
switch (category_index) {
|
||||||
|
case 0: // soc
|
||||||
|
value = inv.Battery1Soc;
|
||||||
|
break;
|
||||||
|
case 1: // temperature
|
||||||
|
// Growatt: Battery1AmbientTemperature, Sinexcel: Battery1Temperature
|
||||||
|
value = inv.Battery1AmbientTemperature ?? inv.Battery1Temperature;
|
||||||
|
break;
|
||||||
|
case 2: // battery power
|
||||||
|
value = inv.Battery1Power;
|
||||||
|
break;
|
||||||
|
case 3: // grid power
|
||||||
|
// Growatt: GridPower (always valid), Sinexcel: GridPower may be 0 when
|
||||||
|
// electric meter is offline, TotalGridPower is the reliable fallback
|
||||||
|
value = inv.TotalGridPower ?? inv.GridPower;
|
||||||
|
break;
|
||||||
|
case 4: // pv production
|
||||||
|
// Growatt: PvPower (aggregated), Sinexcel: PvTotalPower or sum PvPower1-4
|
||||||
|
value =
|
||||||
|
inv.PvPower ??
|
||||||
|
inv.PvTotalPower ??
|
||||||
|
['PvPower1', 'PvPower2', 'PvPower3', 'PvPower4']
|
||||||
|
.map((key) => inv[key] ?? 0)
|
||||||
|
.reduce((sum, val) => sum + val, 0);
|
||||||
|
break;
|
||||||
|
case 6: // consumption
|
||||||
|
value = inv.ConsumptionPower;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (category_index === 4) {
|
||||||
|
// Salimax/Salidomo: Custom logic for 'PvOnDc.Dc.Power'
|
||||||
|
if (get(result, path) !== undefined) {
|
||||||
value = Object.values(
|
value = Object.values(
|
||||||
result.PvOnDc as Record<string, { Dc?: { Power?: number } }>
|
result.PvOnDc as Record<string, { Dc?: { Power?: number } }>
|
||||||
).reduce((sum, device) => sum + (device.Dc?.Power || 0), 0);
|
).reduce((sum, device) => sum + (device.Dc?.Power || 0), 0);
|
||||||
|
}
|
||||||
} else if (get(result, path) !== undefined) {
|
} else if (get(result, path) !== undefined) {
|
||||||
// Default path-based extraction
|
// Default path-based extraction
|
||||||
value = path
|
value = path
|
||||||
|
|
@ -449,7 +577,7 @@ export const transformInputToDailyDataJson = async (
|
||||||
value
|
value
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
category_index++;
|
category_index++;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue