diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index bf0fb6383..e8f508340 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -195,9 +195,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 diff --git a/csharp/App/Backend/db-1733849565.sqlite b/csharp/App/Backend/db-1733849565.sqlite deleted file mode 100644 index f3b3e33a3..000000000 Binary files a/csharp/App/Backend/db-1733849565.sqlite and /dev/null differ 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/content/dashboards/Information/InformationSodistoreHome.tsx b/typescript/frontend-marios2/src/content/dashboards/Information/InformationSodistoreHome.tsx index 4b1cf5184..cebd9afbb 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Information/InformationSodistoreHome.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Information/InformationSodistoreHome.tsx @@ -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: ( - - {BATTERY_SN_PREFIX} - - ) - }} - inputProps={{ - maxLength: BATTERY_SN_SUFFIX_LENGTH, - placeholder: '0000' - }} - helperText={`Enter ${BATTERY_SN_SUFFIX_LENGTH} digits`} + placeholder="Scan or enter serial number" /> ))} diff --git a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx index b7c699e0e..8e0a4a249 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx @@ -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) { > - + {product !== 2 && ( + + )} {/*{aggregatedData && (*/}