Update detailed battery view on front-end
This commit is contained in:
parent
67343ad842
commit
242969306b
|
@ -242,7 +242,7 @@ function BatteryView(props: BatteryViewProps) {
|
|||
>
|
||||
{battery.Warnings.value === '' ? (
|
||||
'None'
|
||||
) : battery.Warnings.value.toString().split(';').length >
|
||||
) : battery.Warnings.value.toString().split('-').length >
|
||||
1 ? (
|
||||
<Link
|
||||
style={{ color: 'black' }}
|
||||
|
@ -274,7 +274,7 @@ function BatteryView(props: BatteryViewProps) {
|
|||
>
|
||||
{battery.Alarms.value === '' ? (
|
||||
'None'
|
||||
) : battery.Alarms.value.toString().split(';').length >
|
||||
) : battery.Alarms.value.toString().split('-').length >
|
||||
1 ? (
|
||||
<Link
|
||||
style={{ color: 'black' }}
|
||||
|
|
|
@ -84,8 +84,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
|||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '150px',
|
||||
marginTop: '30px'
|
||||
height: '150px'
|
||||
};
|
||||
|
||||
const batteryStringStyle = {
|
||||
|
@ -132,6 +131,17 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
|
|||
<Grid container>
|
||||
<Grid item md={4.9} xs={4.9}></Grid>
|
||||
<Grid item md={2.2} xs={2.2}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="div"
|
||||
sx={{
|
||||
marginLeft: '120px',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{'Node ' + props.batteryData.BatteryId}
|
||||
</Typography>
|
||||
|
||||
<div style={batteryStyle}>
|
||||
<div
|
||||
style={{
|
||||
|
|
|
@ -108,20 +108,19 @@ function MainStats(props: MainStatsProps) {
|
|||
function generateSeries(chartData, category, color) {
|
||||
const series = [];
|
||||
const pathsToSearch = [
|
||||
'Battery1',
|
||||
'Battery2',
|
||||
'Battery3',
|
||||
'Battery4',
|
||||
'Battery5',
|
||||
'Battery6',
|
||||
'Battery7',
|
||||
'Battery8',
|
||||
'Battery9',
|
||||
'Battery10'
|
||||
'Node2',
|
||||
'Node3',
|
||||
'Node4',
|
||||
'Node5',
|
||||
'Node6',
|
||||
'Node7',
|
||||
'Node8',
|
||||
'Node9',
|
||||
'Node10',
|
||||
'Node11'
|
||||
];
|
||||
|
||||
let i = 0;
|
||||
// Assuming the chartData.Soc.data structure
|
||||
pathsToSearch.forEach((devicePath) => {
|
||||
if (
|
||||
Object.hasOwnProperty.call(chartData[category].data, devicePath) &&
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Button from '@mui/material/Button';
|
||||
import { DateField } from '@mui/x-date-pickers/DateField';
|
||||
import axiosConfig from '../../../Resources/axiosConfig';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
|
@ -57,6 +56,8 @@ function Configuration(props: ConfigurationProps) {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [updated, setUpdated] = useState(false);
|
||||
const [dateSelectionError, setDateSelectionError] = useState('');
|
||||
const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
|
||||
const [openForcedCalibrationCharge, setOpenForcedCalibrationCharge] =
|
||||
useState(false);
|
||||
const [
|
||||
|
@ -65,19 +66,13 @@ function Configuration(props: ConfigurationProps) {
|
|||
] = useState<string>(
|
||||
props.values.calibrationChargeForced[0].value.toString()
|
||||
);
|
||||
const [isDateModalOpen, setIsDateModalOpen] = useState(false);
|
||||
const [calibrationChargeDate, setCalibrationChargeDate] = useState();
|
||||
const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
|
||||
const [dateSelectionError, setDateSelectionError] = useState('');
|
||||
const [dateFieldOpen, setDateFieldOpen] = useState(false);
|
||||
|
||||
const [formValues, setFormValues] = useState<ConfigurationValues>({
|
||||
minimumSoC: props.values.minimumSoC[0].value,
|
||||
gridSetPoint: (props.values.gridSetPoint[0].value as number) / 1000,
|
||||
forceCalibrationCharge: forcedCalibrationChargeOptions.indexOf(
|
||||
props.values.calibrationChargeForced[0].value.toString()
|
||||
),
|
||||
calibrationChargeDate: Date(props.values.calibrationChargeDate[0].value)
|
||||
calibrationChargeDate: null
|
||||
});
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
|
@ -97,30 +92,24 @@ function Configuration(props: ConfigurationProps) {
|
|||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsDateModalOpen(false);
|
||||
};
|
||||
const handleOkOnErrorDateModal = () => {
|
||||
setErrorDateModalOpen(false);
|
||||
setIsDateModalOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
setIsDateModalOpen(false);
|
||||
|
||||
if (calibrationChargeDate.isBefore(dayjs())) {
|
||||
setDateSelectionError('You must use a future date');
|
||||
const handleConfirm = (newDate) => {
|
||||
if (newDate.isBefore(dayjs())) {
|
||||
setDateSelectionError('You must specify a future date');
|
||||
setErrorDateModalOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setDateFieldOpen(true);
|
||||
setFormValues({
|
||||
...formValues,
|
||||
['calibrationChargeDate']: newDate.toDate()
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectedCalibrationChargeChange = (event) => {
|
||||
if (event.target.value != 'ChargePermanently') {
|
||||
setIsDateModalOpen(true);
|
||||
}
|
||||
setSelectedForcedCalibrationChargeOption(event.target.value);
|
||||
|
||||
setFormValues({
|
||||
|
@ -181,6 +170,47 @@ function Configuration(props: ConfigurationProps) {
|
|||
alignItems="stretch"
|
||||
spacing={3}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
<Grid item xs={12} md={12}>
|
||||
<CardContent>
|
||||
<Box
|
||||
|
@ -246,126 +276,29 @@ function Configuration(props: ConfigurationProps) {
|
|||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
{(dateFieldOpen || formValues.forceCalibrationCharge != 2) && (
|
||||
{formValues.forceCalibrationCharge != 2 && (
|
||||
<div>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<DateField
|
||||
label="Calibration Charge Date"
|
||||
value={calibrationChargeDate}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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 Calibration Charge Date"
|
||||
value={calibrationChargeDate}
|
||||
onChange={(newDate) =>
|
||||
setCalibrationChargeDate(newDate)
|
||||
label="Select Next Calibration Charge Date"
|
||||
value={
|
||||
formValues.forceCalibrationCharge == 0
|
||||
? dayjs(
|
||||
props.values.repetitiveCalibrationChargeDate[0]
|
||||
.value
|
||||
)
|
||||
: dayjs(
|
||||
props.values.additionalCalibrationChargeDate[0]
|
||||
.value
|
||||
)
|
||||
}
|
||||
onChange={handleConfirm}
|
||||
sx={{
|
||||
marginTop: 2
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
color: '#111111',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
}}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
marginLeft: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
color: '#111111',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
}}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
</Modal>
|
||||
</LocalizationProvider>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: '5px' }}>
|
||||
|
@ -461,26 +394,7 @@ function Configuration(props: ConfigurationProps) {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{
|
||||
ml: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
An error has occurred
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => setError(false)} // Set error state to false on click
|
||||
sx={{ marginLeft: '4px' }}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{updated && (
|
||||
<Alert
|
||||
severity="success"
|
||||
|
|
|
@ -159,7 +159,8 @@ export type TopologyValues = {
|
|||
DcDcNum: I_BoxDataValue[];
|
||||
calibrationChargeForced: I_BoxDataValue[];
|
||||
mode: I_BoxDataValue[];
|
||||
calibrationChargeDate: I_BoxDataValue[];
|
||||
repetitiveCalibrationChargeDate: I_BoxDataValue[];
|
||||
additionalCalibrationChargeDate: I_BoxDataValue[];
|
||||
|
||||
batteryView: Battery[];
|
||||
};
|
||||
|
@ -173,6 +174,7 @@ const batteryPaths = [
|
|||
'/Battery/Devices/%id%/Dc/Voltage',
|
||||
'/Battery/Devices/%id%/Soc',
|
||||
'/Battery/Devices/%id%/Temperatures/Cells/Average',
|
||||
//'/Log/SalimaxWarnings/Battery/%id%',
|
||||
'/Battery/Devices/%id%/Warnings',
|
||||
'/Battery/Devices/%id%/Alarms',
|
||||
|
||||
|
@ -294,23 +296,37 @@ export const topologyPaths: TopologyPaths = {
|
|||
gridSetPoint: ['/Config/GridSetPoint'],
|
||||
maximumDischargePower: ['/Config/MaxBatteryDischargingCurrent'],
|
||||
DcDcNum: ['/DcDc/SystemControl/NumberOfConnectedSlaves'],
|
||||
calibrationChargeForced: ['/Config/ForceCalibrationCharge'],
|
||||
calibrationChargeForced: ['/Config/ForceCalibrationChargeState'],
|
||||
mode: ['/EssControl/Mode'],
|
||||
calibrationChargeDate: ['/EssControl/Date']
|
||||
repetitiveCalibrationChargeDate: [
|
||||
'/Config/DayAndTimeForRepetitiveCalibration'
|
||||
],
|
||||
additionalCalibrationChargeDate: [
|
||||
'/Config/DayAndTimeForAdditionalCalibration'
|
||||
]
|
||||
};
|
||||
|
||||
//We are using the function every time we fetch the data from S3 (every 2 seconds).
|
||||
//The data is of the following form: TopologyValues
|
||||
//key: I_BoxDataValue[] ==> key: [{unit:'',value:''},{unit:'',value:''},...]
|
||||
//battery_view: [ {"Battery_id": 2,'FwVersion': {'unit':,'value':}},
|
||||
// {"Battery_id": 4,'FwVersion': {'unit':,'value':}}
|
||||
//]
|
||||
//For batteries, we follow a different approach. We define a key battery_view that is of type Battery[]
|
||||
|
||||
export const extractValues = (
|
||||
timeSeriesData: DataPoint
|
||||
): TopologyValues | null => {
|
||||
const extractedValues: TopologyValues = {} as TopologyValues;
|
||||
|
||||
for (const topologyKey of Object.keys(topologyPaths)) {
|
||||
//Each topologykey may have more than one paths (for example inverter)
|
||||
const paths = topologyPaths[topologyKey];
|
||||
let topologyValues: { unit: string; value: string | number }[] = [];
|
||||
|
||||
if (topologyKey === 'batteryView') {
|
||||
extractedValues[topologyKey] = [];
|
||||
const battery_ids_from_csv = timeSeriesData.value[
|
||||
const node_ids_from_csv = timeSeriesData.value[
|
||||
'/Config/Devices/BatteryNodes'
|
||||
].value
|
||||
.toString()
|
||||
|
@ -322,7 +338,11 @@ export const extractValues = (
|
|||
while (pathIndex < paths.length) {
|
||||
let battery = {};
|
||||
let existingKeys = 0;
|
||||
battery[BatteryKeys[0]] = battery_ids_from_csv[battery_index];
|
||||
|
||||
//We prepare a battery object for each node. We extract the nodes from the '/Config/Devices/BatteryNodes' path. For example, nodes can be [2,4,5,6] (one is missing)
|
||||
//BatteryKeys[0] is the battery id. We set the battery id to the corresponding node id.
|
||||
battery[BatteryKeys[0]] = node_ids_from_csv[battery_index];
|
||||
//Then, search all the remaining battery keys
|
||||
for (let i = 1; i < BatteryKeys.length; i++) {
|
||||
const path = paths[pathIndex];
|
||||
if (timeSeriesData.value.hasOwnProperty(path)) {
|
||||
|
|
|
@ -61,6 +61,17 @@ export interface BatteryOverviewInterface {
|
|||
Current: chartInfoInterface;
|
||||
}
|
||||
|
||||
// We use this function in order to retrieve data for main stats.
|
||||
//The data is of the following form:
|
||||
//'Soc' : {name:'Soc',data:[
|
||||
// 'Node1': {name:'Node1', data: [[timestamp,value],[timestamp,value]]},
|
||||
// 'Node2': {name:'Node2', data: [[timestamp,value],[timestamp,value]]},
|
||||
// ]},
|
||||
//'Temperature' : {name:'Temperature',data:[
|
||||
// 'Node1': {name:'Node1', data: [[timestamp,value],[timestamp,value]]},
|
||||
// 'Node2': {name:'Node2', data: [[timestamp,value],[timestamp,value]]},
|
||||
// ]}
|
||||
|
||||
export const transformInputToBatteryViewData = async (
|
||||
s3Credentials: I_S3Credentials,
|
||||
startTimestamp: UnixTime,
|
||||
|
@ -94,18 +105,7 @@ export const transformInputToBatteryViewData = async (
|
|||
'/Battery/Devices/10/'
|
||||
];
|
||||
|
||||
const pathsToSave = [
|
||||
'Battery1',
|
||||
'Battery2',
|
||||
'Battery3',
|
||||
'Battery4',
|
||||
'Battery5',
|
||||
'Battery6',
|
||||
'Battery7',
|
||||
'Battery8',
|
||||
'Battery9',
|
||||
'Battery10'
|
||||
];
|
||||
const pathsToSave = [];
|
||||
|
||||
const chartData: BatteryDataInterface = {
|
||||
Soc: { name: 'State Of Charge', data: [] },
|
||||
|
@ -123,20 +123,7 @@ export const transformInputToBatteryViewData = async (
|
|||
Current: { magnitude: 0, unit: '', min: 0, max: 0 }
|
||||
};
|
||||
|
||||
categories.forEach((category) => {
|
||||
chartData[category].data = [];
|
||||
pathsToSave.forEach((path) => {
|
||||
chartData[category].data[path] = { name: path, data: [] };
|
||||
});
|
||||
|
||||
chartOverview[category] = {
|
||||
magnitude: 0,
|
||||
unit: '',
|
||||
min: MAX_NUMBER,
|
||||
max: -MAX_NUMBER
|
||||
};
|
||||
});
|
||||
|
||||
let initialiation = true;
|
||||
let adjustedTimestampArray = [];
|
||||
|
||||
let startTimestampToNum = Number(startTimestamp);
|
||||
|
@ -160,6 +147,7 @@ export const transformInputToBatteryViewData = async (
|
|||
adjustedTimestampArray.push(adjustedTimestamp);
|
||||
}
|
||||
|
||||
//Wait until fetching all the data
|
||||
const results = await Promise.all(timestampPromises);
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
|
@ -170,6 +158,32 @@ export const transformInputToBatteryViewData = async (
|
|||
) {
|
||||
// Handle not available or try later case
|
||||
} else {
|
||||
const battery_nodes = result['/Config/Devices/BatteryNodes'].value
|
||||
.toString()
|
||||
.split(',');
|
||||
|
||||
//Initialize the chartData structure based on the node names extracted from the first result
|
||||
if (initialiation) {
|
||||
initialiation = false;
|
||||
|
||||
battery_nodes.forEach((node) => {
|
||||
pathsToSave.push('Node' + node);
|
||||
});
|
||||
categories.forEach((category) => {
|
||||
chartData[category].data = [];
|
||||
pathsToSave.forEach((path) => {
|
||||
chartData[category].data[path] = { name: path, data: [] };
|
||||
});
|
||||
|
||||
chartOverview[category] = {
|
||||
magnitude: 0,
|
||||
unit: '',
|
||||
min: MAX_NUMBER,
|
||||
max: -MAX_NUMBER
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
for (
|
||||
let category_index = 0;
|
||||
category_index < pathCategories.length;
|
||||
|
|
Loading…
Reference in New Issue