Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
8c2a360ba3
|
|
@ -3,4 +3,4 @@
|
|||
**/obj
|
||||
*.DotSettings.user
|
||||
**/.idea/
|
||||
|
||||
**/.env
|
||||
|
|
|
|||
|
|
@ -194,9 +194,13 @@ public class Controller : ControllerBase
|
|||
|
||||
while (startTimestamp <= endTimestamp)
|
||||
{
|
||||
string bucketPath = installation.Product==(int)ProductType.Salimax || installation.Product==(int)ProductType.SodiStoreMax?
|
||||
"s3://"+installation.S3BucketId + "-3e5b3069-214a-43ee-8d85-57d72000c19d/"+startTimestamp :
|
||||
"s3://"+installation.S3BucketId + "-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e/"+startTimestamp;
|
||||
string bucketPath;
|
||||
if (installation.Product == (int)ProductType.Salimax || installation.Product == (int)ProductType.SodiStoreMax)
|
||||
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);
|
||||
|
||||
try
|
||||
|
|
|
|||
|
|
@ -12,13 +12,14 @@ import {
|
|||
Typography
|
||||
} from '@mui/material';
|
||||
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 { FormattedMessage } from 'react-intl';
|
||||
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
||||
import routes from '../../../Resources/routes.json';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { I_Installation } from 'src/interfaces/InstallationTypes';
|
||||
import MainStatsSodioHome from './MainStatsSodioHome';
|
||||
|
||||
interface BatteryViewSodioHomeProps {
|
||||
values: JSONRecordData;
|
||||
|
|
@ -164,54 +165,20 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
|
|||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/*<Grid container>*/}
|
||||
{/* <Routes>*/}
|
||||
{/* <Route*/}
|
||||
{/* path={routes.mainstats + '/*'}*/}
|
||||
{/* element={*/}
|
||||
{/* <MainStats*/}
|
||||
{/* s3Credentials={props.s3Credentials}*/}
|
||||
{/* id={props.installationId}*/}
|
||||
{/* ></MainStats>*/}
|
||||
{/* }*/}
|
||||
{/* />*/}
|
||||
{/* {product === 0*/}
|
||||
{/* ? Object.entries(props.values.Battery.Devices).map(*/}
|
||||
{/* ([BatteryId, battery]) => (*/}
|
||||
{/* <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>*/}
|
||||
<Grid container>
|
||||
<Routes>
|
||||
<Route
|
||||
path={routes.mainstats + '/*'}
|
||||
element={
|
||||
<MainStatsSodioHome
|
||||
s3Credentials={props.s3Credentials}
|
||||
id={props.installationId}
|
||||
batteryClusterNumber={props.installation.batteryClusterNumber}
|
||||
></MainStatsSodioHome>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</Grid>
|
||||
|
||||
<TableContainer
|
||||
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,
|
||||
Grid,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Modal,
|
||||
|
|
@ -19,7 +18,7 @@ import {
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
import Button from '@mui/material/Button';
|
||||
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_Installation } from '../../../interfaces/InstallationTypes';
|
||||
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
|
||||
|
|
@ -57,8 +56,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
{ id: 4, name: 'Sinexcel' }
|
||||
];
|
||||
|
||||
const BATTERY_SN_PREFIX = 'PNR020125101';
|
||||
const BATTERY_SN_SUFFIX_LENGTH = 4;
|
||||
const batterySnRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
// Initialize battery data from props
|
||||
useEffect(() => {
|
||||
|
|
@ -68,14 +66,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
if (props.values.batterySerialNumbers) {
|
||||
const serialNumbers = props.values.batterySerialNumbers
|
||||
.split(',')
|
||||
.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;
|
||||
});
|
||||
.filter((sn) => sn.trim() !== '');
|
||||
setBatterySerialNumbers(serialNumbers);
|
||||
}
|
||||
}, []);
|
||||
|
|
@ -107,41 +98,47 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
const value = inputValue === '' ? 0 : parseInt(inputValue);
|
||||
setBatteryNumber(value);
|
||||
|
||||
// Preserve existing serial numbers and adjust array size
|
||||
const newSerialNumbers = Array.from({ length: value }, (_, index) => {
|
||||
// Keep existing serial number if it exists, otherwise use empty string
|
||||
return batterySerialNumbers[index] || '';
|
||||
});
|
||||
setBatterySerialNumbers(newSerialNumbers);
|
||||
if (value > 0) {
|
||||
// Resize array: preserve existing serial numbers, add empty for new slots
|
||||
const newSerialNumbers = Array.from({ length: value }, (_, index) => {
|
||||
return batterySerialNumbers[index] || '';
|
||||
});
|
||||
setBatterySerialNumbers(newSerialNumbers);
|
||||
|
||||
// Update formValues with preserved serial numbers
|
||||
const fullSerialNumbers = newSerialNumbers
|
||||
.map((suffix) => (suffix ? BATTERY_SN_PREFIX + suffix : ''))
|
||||
.filter((sn) => sn !== '');
|
||||
|
||||
setFormValues({
|
||||
...formValues,
|
||||
batteryNumber: value,
|
||||
batterySerialNumbers: fullSerialNumbers.join(',')
|
||||
});
|
||||
setFormValues({
|
||||
...formValues,
|
||||
batteryNumber: value,
|
||||
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) => {
|
||||
// Only allow digits and limit to 3 characters
|
||||
const sanitizedValue = value.replace(/\D/g, '').substring(0, BATTERY_SN_SUFFIX_LENGTH);
|
||||
const updatedSerialNumbers = [...batterySerialNumbers];
|
||||
updatedSerialNumbers[index] = sanitizedValue;
|
||||
updatedSerialNumbers[index] = value;
|
||||
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({
|
||||
...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 = () => {
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
|
|
@ -467,20 +464,11 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
onChange={(e) =>
|
||||
handleBatterySerialNumberChange(index, e.target.value)
|
||||
}
|
||||
onKeyDown={(e) => handleBatterySnKeyDown(e, index)}
|
||||
inputRef={(el) => (batterySnRefs.current[index] = el)}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
{BATTERY_SN_PREFIX}
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
inputProps={{
|
||||
maxLength: BATTERY_SN_SUFFIX_LENGTH,
|
||||
placeholder: '0000'
|
||||
}}
|
||||
helperText={`Enter ${BATTERY_SN_SUFFIX_LENGTH} digits`}
|
||||
placeholder="Scan or enter serial number"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -452,6 +452,8 @@ export interface JSONRecordData {
|
|||
Battery2Soh:number;
|
||||
PvPower:number;
|
||||
ConsumptionPower:number;
|
||||
WorkingMode?:string;
|
||||
OperatingMode?:string;
|
||||
};
|
||||
|
||||
AcDcGrowatt: {
|
||||
|
|
|
|||
|
|
@ -118,6 +118,12 @@ function Overview(props: OverviewProps) {
|
|||
|
||||
resultPromise
|
||||
.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) =>
|
||||
prevData.concat({
|
||||
chartData: result.chartData,
|
||||
|
|
@ -281,6 +287,12 @@ function Overview(props: OverviewProps) {
|
|||
|
||||
resultPromise
|
||||
.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) =>
|
||||
prevData.concat({
|
||||
chartData: result.chartData,
|
||||
|
|
@ -511,20 +523,22 @@ function Overview(props: OverviewProps) {
|
|||
>
|
||||
<FormattedMessage id="24_hours" defaultMessage="24-hours" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleWeekData}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
marginTop: '20px',
|
||||
marginLeft: '10px',
|
||||
backgroundColor: aggregatedData ? '#808080' : '#ffc04d',
|
||||
color: '#000000',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="lastweek" defaultMessage="Last week" />
|
||||
</Button>
|
||||
{product !== 2 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleWeekData}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
marginTop: '20px',
|
||||
marginLeft: '10px',
|
||||
backgroundColor: aggregatedData ? '#808080' : '#ffc04d',
|
||||
color: '#000000',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="lastweek" defaultMessage="Last week" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/*{aggregatedData && (*/}
|
||||
<Button
|
||||
|
|
@ -1013,7 +1027,7 @@ function Overview(props: OverviewProps) {
|
|||
alignItems="stretch"
|
||||
spacing={3}
|
||||
>
|
||||
<Grid item md={6} xs={12}>
|
||||
<Grid item md={product === 2 ? 12 : 6} xs={12}>
|
||||
<Card
|
||||
sx={{
|
||||
overflow: 'visible',
|
||||
|
|
@ -1074,6 +1088,7 @@ function Overview(props: OverviewProps) {
|
|||
/>
|
||||
</Card>
|
||||
</Grid>
|
||||
{product !== 2 && (
|
||||
<Grid item md={6} xs={12}>
|
||||
<Card
|
||||
sx={{
|
||||
|
|
@ -1136,6 +1151,7 @@ function Overview(props: OverviewProps) {
|
|||
/>
|
||||
</Card>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
|
|
@ -1336,7 +1352,7 @@ function Overview(props: OverviewProps) {
|
|||
alignItems="stretch"
|
||||
spacing={3}
|
||||
>
|
||||
<Grid item md={6} xs={12}>
|
||||
<Grid item md={product === 2 ? 12 : 6} xs={12}>
|
||||
<Card
|
||||
sx={{
|
||||
overflow: 'visible',
|
||||
|
|
@ -1397,6 +1413,7 @@ function Overview(props: OverviewProps) {
|
|||
/>
|
||||
</Card>
|
||||
</Grid>
|
||||
{product !== 2 && (
|
||||
<Grid item md={6} xs={12}>
|
||||
<Card
|
||||
sx={{
|
||||
|
|
@ -1458,6 +1475,7 @@ function Overview(props: OverviewProps) {
|
|||
/>
|
||||
</Card>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { FetchResult } from '../../../dataCache/dataCache';
|
|||
import BatteryViewSodioHome from '../BatteryView/BatteryViewSodioHome';
|
||||
import SodistoreHomeConfiguration from './SodistoreHomeConfiguration';
|
||||
import TopologySodistoreHome from '../Topology/TopologySodistoreHome';
|
||||
import Overview from '../Overview/overview';
|
||||
|
||||
interface singleInstallationProps {
|
||||
current_installation?: I_Installation;
|
||||
|
|
@ -338,7 +339,11 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
{values.Config.OperatingPriority}
|
||||
{props.current_installation.device === 4
|
||||
? values.InverterRecord?.WorkingMode
|
||||
: props.current_installation.device === 3
|
||||
? values.InverterRecord?.OperatingMode
|
||||
: values.Config.OperatingPriority}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -541,6 +546,16 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
/>
|
||||
)}
|
||||
|
||||
<Route
|
||||
path={routes.overview}
|
||||
element={
|
||||
<Overview
|
||||
s3Credentials={s3Credentials}
|
||||
id={props.current_installation.id}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={'*'}
|
||||
element={<Navigate to={routes.live}></Navigate>}
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
|||
const { currentUser } = context;
|
||||
const tabList = [
|
||||
'live',
|
||||
'overview',
|
||||
'batteryview',
|
||||
'information',
|
||||
'manage',
|
||||
'overview',
|
||||
'log',
|
||||
'history',
|
||||
'configuration'
|
||||
|
|
@ -100,6 +100,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
|||
value: 'live',
|
||||
label: <FormattedMessage id="live" defaultMessage="Live" />
|
||||
},
|
||||
{
|
||||
value: 'overview',
|
||||
label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
||||
},
|
||||
{
|
||||
value: 'batteryview',
|
||||
label: (
|
||||
|
|
@ -109,10 +113,6 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
|||
/>
|
||||
)
|
||||
},
|
||||
// {
|
||||
// value: 'overview',
|
||||
// label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
||||
// },
|
||||
{
|
||||
value: 'log',
|
||||
label: <FormattedMessage id="log" defaultMessage="Log" />
|
||||
|
|
@ -159,11 +159,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
|||
value: 'live',
|
||||
label: <FormattedMessage id="live" defaultMessage="Live" />
|
||||
},
|
||||
// {
|
||||
// value: 'overview',
|
||||
// label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
||||
// },
|
||||
|
||||
{
|
||||
value: 'overview',
|
||||
label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
||||
},
|
||||
{
|
||||
value: 'information',
|
||||
label: (
|
||||
|
|
@ -190,6 +189,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
|||
value: 'live',
|
||||
label: <FormattedMessage id="live" defaultMessage="Live" />
|
||||
},
|
||||
{
|
||||
value: 'overview',
|
||||
label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
||||
},
|
||||
{
|
||||
value: 'batteryview',
|
||||
label: (
|
||||
|
|
@ -199,10 +202,6 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
|||
/>
|
||||
)
|
||||
},
|
||||
// {
|
||||
// value: 'overview',
|
||||
// label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
||||
// },
|
||||
{
|
||||
value: 'log',
|
||||
label: <FormattedMessage id="log" defaultMessage="Log" />
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ export interface BatteryDataInterface {
|
|||
Power: { name: string; data: [] };
|
||||
Voltage: { name: string; data: [] };
|
||||
Current: { name: string; data: [] };
|
||||
Soh?: { name: string; data: [] };
|
||||
}
|
||||
|
||||
export interface BatteryOverviewInterface {
|
||||
|
|
@ -68,6 +69,7 @@ export interface BatteryOverviewInterface {
|
|||
Power: chartInfoInterface;
|
||||
Voltage: chartInfoInterface;
|
||||
Current: chartInfoInterface;
|
||||
Soh?: chartInfoInterface;
|
||||
}
|
||||
|
||||
export const transformInputToBatteryViewDataJson = async (
|
||||
|
|
@ -75,14 +77,18 @@ export const transformInputToBatteryViewDataJson = async (
|
|||
id: number,
|
||||
product: number,
|
||||
start_time?: UnixTime,
|
||||
end_time?: UnixTime
|
||||
end_time?: UnixTime,
|
||||
batteryClusterNumber?: number
|
||||
): Promise<{
|
||||
chartData: BatteryDataInterface;
|
||||
chartOverview: BatteryOverviewInterface;
|
||||
}> => {
|
||||
const prefixes = ['', 'k', 'M', 'G', 'T'];
|
||||
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 =
|
||||
product === 3
|
||||
? [
|
||||
|
|
@ -120,7 +126,8 @@ export const transformInputToBatteryViewDataJson = async (
|
|||
Temperature: { name: 'Temperature', data: [] },
|
||||
Power: { name: 'Power', data: [] },
|
||||
Voltage: { name: 'Voltage', data: [] },
|
||||
Current: { name: 'Current', data: [] }
|
||||
Current: { name: 'Current', data: [] },
|
||||
...(isSodioHome && { Soh: { name: 'State Of Health', data: [] } })
|
||||
};
|
||||
|
||||
const chartOverview: BatteryOverviewInterface = {
|
||||
|
|
@ -128,7 +135,8 @@ export const transformInputToBatteryViewDataJson = async (
|
|||
Temperature: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||
Power: { 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;
|
||||
|
|
@ -159,7 +167,7 @@ export const transformInputToBatteryViewDataJson = async (
|
|||
);
|
||||
|
||||
const adjustedTimestamp =
|
||||
product == 0 || product == 3
|
||||
product == 0 || product == 2 || product == 3
|
||||
? new Date(timestampArray[i] * 1000)
|
||||
: new Date(timestampArray[i] * 100000);
|
||||
//Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset
|
||||
|
|
@ -181,79 +189,144 @@ export const transformInputToBatteryViewDataJson = async (
|
|||
];
|
||||
|
||||
const result = results[i][timestamp];
|
||||
//console.log(result);
|
||||
const battery_nodes =
|
||||
result.Config.Devices.BatteryNodes.toString().split(',');
|
||||
|
||||
//Initialize the chartData structure based on the node names extracted from the first result
|
||||
let old_length = pathsToSave.length;
|
||||
if (isSodioHome) {
|
||||
// SodistoreHome: extract battery data from InverterRecord
|
||||
const inv = (result as any)?.InverterRecord;
|
||||
if (!inv) continue;
|
||||
|
||||
if (battery_nodes.length > old_length) {
|
||||
battery_nodes.forEach((node) => {
|
||||
const node_number =
|
||||
product == 3 ? Number(node) + 1 : Number(node) - 1;
|
||||
if (!pathsToSave.includes('Node' + node_number)) {
|
||||
pathsToSave.push('Node' + node_number);
|
||||
}
|
||||
});
|
||||
}
|
||||
const numBatteries = batteryClusterNumber || 1;
|
||||
let old_length = pathsToSave.length;
|
||||
|
||||
// console.log(pathsToSave);
|
||||
|
||||
if (initialiation) {
|
||||
initialiation = false;
|
||||
categories.forEach((category) => {
|
||||
chartData[category].data = [];
|
||||
chartOverview[category] = {
|
||||
magnitude: 0,
|
||||
unit: '',
|
||||
min: MAX_NUMBER,
|
||||
max: -MAX_NUMBER
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (battery_nodes.length > old_length) {
|
||||
categories.forEach((category) => {
|
||||
pathsToSave.forEach((path) => {
|
||||
if (pathsToSave.indexOf(path) >= old_length) {
|
||||
chartData[category].data[path] = { name: path, data: [] };
|
||||
if (numBatteries > old_length) {
|
||||
for (let b = old_length; b < numBatteries; b++) {
|
||||
const nodeName = 'Node' + b;
|
||||
if (!pathsToSave.includes(nodeName)) {
|
||||
pathsToSave.push(nodeName);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (
|
||||
let category_index = 0;
|
||||
category_index < pathCategories.length;
|
||||
category_index++
|
||||
) {
|
||||
let category = categories[category_index];
|
||||
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++) {
|
||||
let path = pathsToSearch[j] + pathCategories[category_index];
|
||||
const batteryIndex = j + 1; // Battery1, Battery2, ...
|
||||
categories.forEach((category) => {
|
||||
const fieldName = `Battery${batteryIndex}${categoryFieldMap[category]}`;
|
||||
const value = inv[fieldName];
|
||||
|
||||
if (get(result, path) !== undefined) {
|
||||
const value = path
|
||||
.split('.')
|
||||
.reduce((o, key) => (o ? o[key] : undefined), result);
|
||||
|
||||
if (value < chartOverview[category].min) {
|
||||
chartOverview[category].min = value;
|
||||
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 =
|
||||
result.Config.Devices.BatteryNodes.toString().split(',');
|
||||
|
||||
if (value > chartOverview[category].max) {
|
||||
chartOverview[category].max = value;
|
||||
//Initialize the chartData structure based on the node names extracted from the first result
|
||||
let old_length = pathsToSave.length;
|
||||
|
||||
if (battery_nodes.length > old_length) {
|
||||
battery_nodes.forEach((node) => {
|
||||
const node_number =
|
||||
product == 3 ? Number(node) + 1 : Number(node) - 1;
|
||||
if (!pathsToSave.includes('Node' + node_number)) {
|
||||
pathsToSave.push('Node' + node_number);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (initialiation) {
|
||||
initialiation = false;
|
||||
categories.forEach((category) => {
|
||||
chartData[category].data = [];
|
||||
chartOverview[category] = {
|
||||
magnitude: 0,
|
||||
unit: '',
|
||||
min: MAX_NUMBER,
|
||||
max: -MAX_NUMBER
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (battery_nodes.length > old_length) {
|
||||
categories.forEach((category) => {
|
||||
pathsToSave.forEach((path) => {
|
||||
if (pathsToSave.indexOf(path) >= old_length) {
|
||||
chartData[category].data[path] = { name: path, data: [] };
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
for (
|
||||
let category_index = 0;
|
||||
category_index < pathCategories.length;
|
||||
category_index++
|
||||
) {
|
||||
let category = categories[category_index];
|
||||
|
||||
for (let j = 0; j < pathsToSave.length; j++) {
|
||||
let path = pathsToSearch[j] + pathCategories[category_index];
|
||||
|
||||
if (get(result, path) !== undefined) {
|
||||
const value = path
|
||||
.split('.')
|
||||
.reduce((o, key) => (o ? o[key] : undefined), result);
|
||||
|
||||
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
|
||||
]);
|
||||
}
|
||||
chartData[category].data[pathsToSave[j]].data.push([
|
||||
adjustedTimestampArray[i],
|
||||
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.min = 0;
|
||||
chartOverview.Soc.max = 100;
|
||||
chartOverview.Temperature.unit = '(°C)';
|
||||
if (!isSodioHome) {
|
||||
chartOverview.Temperature.unit = '(°C)';
|
||||
}
|
||||
chartOverview.Power.unit =
|
||||
'(' + prefixes[chartOverview['Power'].magnitude] + 'W' + ')';
|
||||
chartOverview.Voltage.unit =
|
||||
'(' + prefixes[chartOverview['Voltage'].magnitude] + 'V' + ')';
|
||||
|
||||
chartOverview.Current.unit =
|
||||
'(' + prefixes[chartOverview['Current'].magnitude] + 'A' + ')';
|
||||
|
||||
// console.log(chartData);
|
||||
if (isSodioHome) {
|
||||
chartOverview.Soh.unit = '(%)';
|
||||
chartOverview.Soh.min = 0;
|
||||
chartOverview.Soh.max = 100;
|
||||
}
|
||||
|
||||
return {
|
||||
chartData: chartData,
|
||||
|
|
@ -310,21 +387,31 @@ export const transformInputToDailyDataJson = async (
|
|||
const prefixes = ['', 'k', 'M', 'G', 'T'];
|
||||
|
||||
const MAX_NUMBER = 9999999;
|
||||
const pathsToSearch = [
|
||||
'Battery.Soc',
|
||||
product == 0 ? 'Battery.Temperature' : 'Battery.TemperatureCell1',
|
||||
|
||||
//'Battery.Temperature' for salimax,
|
||||
//'Battery.TemperatureCell1',
|
||||
product == 0 ? 'Battery.Dc.Power' : 'Battery.Power',
|
||||
//'Battery.Dc.Power' for salimax,
|
||||
// 'Battery.Power',
|
||||
'GridMeter.Ac.Power.Active',
|
||||
'PvOnDc',
|
||||
'DcDc.Dc.Link.Voltage',
|
||||
'LoadOnAcGrid.Power.Active',
|
||||
'LoadOnDc.Power'
|
||||
];
|
||||
// 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',
|
||||
product == 0 ? 'Battery.Temperature' : 'Battery.TemperatureCell1',
|
||||
product == 0 ? 'Battery.Dc.Power' : 'Battery.Power',
|
||||
'GridMeter.Ac.Power.Active',
|
||||
'PvOnDc',
|
||||
'DcDc.Dc.Link.Voltage',
|
||||
'LoadOnAcGrid.Power.Active',
|
||||
'LoadOnDc.Power'
|
||||
];
|
||||
const categories = [
|
||||
'soc',
|
||||
'temperature',
|
||||
|
|
@ -419,37 +506,78 @@ export const transformInputToDailyDataJson = async (
|
|||
let category_index = 0;
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
pathsToSearch.forEach((path) => {
|
||||
if (get(result, path) !== undefined) {
|
||||
let value: number | undefined = undefined;
|
||||
if (path === null) {
|
||||
// Skip unavailable fields (e.g. dcBusVoltage, DCLoad for SodioHome)
|
||||
category_index++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (category_index === 4) {
|
||||
// Custom logic for 'PvOnDc.Dc.Power'
|
||||
let value: number | undefined = undefined;
|
||||
|
||||
if (product === 2) {
|
||||
// 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(
|
||||
result.PvOnDc as Record<string, { Dc?: { Power?: number } }>
|
||||
).reduce((sum, device) => sum + (device.Dc?.Power || 0), 0);
|
||||
} else if (get(result, path) !== undefined) {
|
||||
// Default path-based extraction
|
||||
value = path
|
||||
.split('.')
|
||||
.reduce((o, key) => (o ? o[key] : undefined), result);
|
||||
}
|
||||
|
||||
// Only push value if defined
|
||||
if (value !== undefined) {
|
||||
if (value < chartOverview[categories[category_index]].min) {
|
||||
chartOverview[categories[category_index]].min = value;
|
||||
}
|
||||
|
||||
if (value > chartOverview[categories[category_index]].max) {
|
||||
chartOverview[categories[category_index]].max = value;
|
||||
}
|
||||
|
||||
chartData[categories[category_index]].data.push([
|
||||
adjustedTimestampArray[i],
|
||||
value
|
||||
]);
|
||||
}
|
||||
} else if (get(result, path) !== undefined) {
|
||||
// Default path-based extraction
|
||||
value = path
|
||||
.split('.')
|
||||
.reduce((o, key) => (o ? o[key] : undefined), result);
|
||||
}
|
||||
|
||||
// Only push value if defined
|
||||
if (value !== undefined) {
|
||||
if (value < chartOverview[categories[category_index]].min) {
|
||||
chartOverview[categories[category_index]].min = value;
|
||||
}
|
||||
|
||||
if (value > chartOverview[categories[category_index]].max) {
|
||||
chartOverview[categories[category_index]].max = value;
|
||||
}
|
||||
|
||||
chartData[categories[category_index]].data.push([
|
||||
adjustedTimestampArray[i],
|
||||
value
|
||||
]);
|
||||
}
|
||||
|
||||
category_index++;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue