From 2895b11efc935a343f16fe619db26742def84899 Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Tue, 10 Feb 2026 14:32:46 +0100 Subject: [PATCH 1/5] made Battery SN automatically filled by Scanner and keep the memory of it when the Battery Number changes --- .../Information/InformationSodistoreHome.tsx | 84 ++++++++----------- 1 file changed, 36 insertions(+), 48 deletions(-) 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" /> ))} From 1b6d5a5916297cf2934f392b91ed2e15732a201f Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Wed, 11 Feb 2026 13:30:34 +0100 Subject: [PATCH 2/5] added Overview Page without last week button for SodistoreHome --- csharp/App/Backend/Controller.cs | 10 +- .../content/dashboards/Overview/overview.tsx | 50 ++++--- .../SodiohomeInstallations/Installation.tsx | 11 ++ .../SodiohomeInstallations/index.tsx | 27 ++-- .../frontend-marios2/src/interfaces/Chart.tsx | 131 ++++++++++++------ 5 files changed, 156 insertions(+), 73 deletions(-) diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index 41c7f70c6..0dcbeb057 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -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 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 && (*/} + + + )} + {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, From 02d2ef054b6e9a20f0752b3b221c5411970a1201 Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Thu, 12 Feb 2026 11:39:39 +0100 Subject: [PATCH 4/5] fix Mode reading on OverView Page from Inverter reading --- .../src/content/dashboards/Log/graph.util.tsx | 2 ++ .../dashboards/SodiohomeInstallations/Installation.tsx | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx b/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx index 928ddcd1d..c3990e84b 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx @@ -452,6 +452,8 @@ export interface JSONRecordData { Battery2Soh:number; PvPower:number; ConsumptionPower:number; + WorkingMode?:string; + OperatingMode?:string; }; AcDcGrowatt: { diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx index e9adb8df0..d8621e97d 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx @@ -339,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} )} From 4d6a2fdd4dc9b4f267060a261346dc3428265735 Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Thu, 12 Feb 2026 11:45:06 +0100 Subject: [PATCH 5/5] Add .env to gitignore to prevent committing secrets Co-Authored-By: Claude Opus 4.6 --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7a1e5ac79..1a9d879a1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ **/obj *.DotSettings.user **/.idea/ - +**/.env