diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSodioHome.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSodioHome.tsx index db6b47d6c..fb3e221d3 100644 --- a/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSodioHome.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSodioHome.tsx @@ -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) { - {/**/} - {/* */} - {/* */} - {/* }*/} - {/* />*/} - {/* {product === 0*/} - {/* ? Object.entries(props.values.Battery.Devices).map(*/} - {/* ([BatteryId, battery]) => (*/} - {/* */} - {/* }*/} - {/* />*/} - {/* )*/} - {/* )*/} - {/* : Object.entries(props.values.Battery.Devices).map(*/} - {/* ([BatteryId, battery]) => (*/} - {/* */} - {/* }*/} - {/* />*/} - {/* )*/} - {/* )}*/} - {/* */} - {/**/} + + + + } + /> + + ([]); + + 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 && ( + + + + Fetching data... + + + )} + {isErrorDateModalOpen && ( + {}}> + + + {dateSelectionError} + + + + + + )} + {isDateModalOpen && ( + + {}}> + + { + if (newDate) { + setStartDate(newDate); + } + }} + renderInput={(params) => ( + + )} + /> + + { + if (newDate) { + setEndDate(newDate); + } + }} + renderInput={(params) => ( + + )} + /> + +
+ + + +
+
+
+
+ )} + + {!loading && ( + <> + + + + + + + + + + + + + + {/* Battery SOC Chart */} + + + + + + + + + + + + + + { + startZoom(); + handleBeforeZoom(chartContext, options); + } + } + } + }} + series={generateSeries( + batteryViewDataArray[chartState].chartData, + 'Soc', + 'blue' + )} + type="line" + height={420} + /> + + + + {/* Battery Power Chart */} + + + + + + + + + + + + + + { + startZoom(); + handleBeforeZoom(chartContext, options); + } + } + } + }} + series={generateSeries( + batteryViewDataArray[chartState].chartData, + 'Power', + 'red' + )} + type="line" + height={420} + /> + + + + {/* Battery Voltage Chart */} + + + + + + + + + + + + + + { + startZoom(); + handleBeforeZoom(chartContext, options); + } + } + } + }} + series={generateSeries( + batteryViewDataArray[chartState].chartData, + 'Voltage', + 'orange' + )} + type="line" + height={420} + /> + + + + {/* Battery Current Chart */} + + + + + + + + + + + + + + { + startZoom(); + handleBeforeZoom(chartContext, options); + } + } + } + }} + series={generateSeries( + batteryViewDataArray[chartState].chartData, + 'Current', + 'orange' + )} + type="line" + height={420} + /> + + + + {/* Battery SoH Chart */} + + + + + + + + + + + + + + { + startZoom(); + handleBeforeZoom(chartContext, options); + } + } + } + }} + series={generateSeries( + batteryViewDataArray[chartState].chartData, + 'Soh', + 'green' + )} + type="line" + height={420} + /> + + + + + )} + + ); +} + +export default MainStatsSodioHome; diff --git a/typescript/frontend-marios2/src/interfaces/Chart.tsx b/typescript/frontend-marios2/src/interfaces/Chart.tsx index a173888c1..caffc231e 100644 --- a/typescript/frontend-marios2/src/interfaces/Chart.tsx +++ b/typescript/frontend-marios2/src/interfaces/Chart.tsx @@ -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,