diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSodioHome.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSodioHome.tsx index 4283a8bd6..9d6d74cd9 100644 --- a/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSodioHome.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSodioHome.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { Container, Grid, @@ -14,12 +14,16 @@ import { import { JSONRecordData } from '../Log/graph.util'; import { Link, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import Button from '@mui/material/Button'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, useIntl } 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'; +import { + ActiveCluster, + getActiveClusters +} from '../Information/installationSetupUtils'; interface BatteryViewSodioHomeProps { values: JSONRecordData; @@ -36,42 +40,78 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) { const currentLocation = useLocation(); const navigate = useNavigate(); + const intl = useIntl(); const inverter = (props.values as any)?.InverterRecord; const batteryClusterNumber = props.installation.batteryClusterNumber; + const batterySerialNumbers = props.installation.batterySerialNumbers; const hasDevices = !!inverter?.Devices; - const sortedBatteryView = inverter - ? Array.from({ length: batteryClusterNumber }, (_, i) => { - if (hasDevices) { - // Sinexcel: map across devices — 0→D1/B1, 1→D1/B2, 2→D2/B1, 3→D2/B2 - const deviceId = String(Math.floor(i / 2) + 1); - const batteryIndex = (i % 2) + 1; - const device = inverter.Devices[deviceId]; + const activeClusters: ActiveCluster[] = useMemo(() => { + const parsed = getActiveClusters(batterySerialNumbers || ''); + if (parsed.length > 0) return parsed; + // Legacy/empty fallback: assume 2 clusters per inverter (all current Sinexcel + // presets), which matches the previous floor(i/2)+1 / (i%2)+1 mapping. + // For Growatt (batteryClusterNumber = 1) this collapses to a single row. + return Array.from({ length: batteryClusterNumber }, (_, i) => ({ + invIdx: Math.floor(i / 2), + clIdx: i % 2, + flatIdx: i + })); + }, [batterySerialNumbers, batteryClusterNumber]); + const inverterCount = activeClusters.reduce( + (max, c) => Math.max(max, c.invIdx + 1), + 0 + ); + const showInverterLabel = hasDevices && inverterCount > 1; + + const sortedBatteryView = inverter + ? activeClusters.map(({ invIdx, clIdx, flatIdx }) => { + const label = showInverterLabel + ? intl.formatMessage( + { + id: 'batteryClusterInInverter', + defaultMessage: 'Battery Cluster {cl} in Inverter {inv}' + }, + { cl: clIdx + 1, inv: invIdx + 1 } + ) + : intl.formatMessage( + { id: 'batteryClusterN', defaultMessage: 'Battery Cluster {n}' }, + { n: clIdx + 1 } + ); + + if (hasDevices) { + // Sinexcel: Devices keyed by "1","2",... (1-based dict keys) + const device = inverter.Devices[String(invIdx + 1)]; + const bi = clIdx + 1; + return { + BatteryId: String(flatIdx + 1), + label, + battery: { + Voltage: device?.[`Battery${bi}PackTotalVoltage`] ?? 0, + Current: device?.[`Battery${bi}PackTotalCurrent`] ?? 0, + Power: device?.[`Battery${bi}Power`] ?? 0, + Soc: + device?.[`Battery${bi}Soc`] ?? + device?.[`Battery${bi}SocSecondvalue`] ?? + 0 + } + }; + } + // Growatt: flat Battery1, Battery2, ... on InverterRecord + const index = clIdx + 1; return { - BatteryId: String(i + 1), - battery: { - Voltage: device?.[`Battery${batteryIndex}PackTotalVoltage`] ?? 0, - Current: device?.[`Battery${batteryIndex}PackTotalCurrent`] ?? 0, - Power: device?.[`Battery${batteryIndex}Power`] ?? 0, - Soc: device?.[`Battery${batteryIndex}Soc`] ?? device?.[`Battery${batteryIndex}SocSecondvalue`] ?? 0, - } - }; - } else { - // Growatt: flat Battery1, Battery2, ... - const index = i + 1; - return { - BatteryId: String(index), + BatteryId: String(flatIdx + 1), + label, battery: { Voltage: inverter[`Battery${index}Voltage`] ?? 0, Current: inverter[`Battery${index}Current`] ?? 0, Power: inverter[`Battery${index}Power`] ?? 0, - Soc: inverter[`Battery${index}Soc`] ?? 0, + Soc: inverter[`Battery${index}Soc`] ?? 0 } }; - } - }) + }) : []; const [loading, setLoading] = useState(sortedBatteryView.length == 0); @@ -193,6 +233,8 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) { s3Credentials={props.s3Credentials} id={props.installationId} batteryClusterNumber={props.installation.batteryClusterNumber} + activeClusters={activeClusters} + showInverterLabel={showInverterLabel} > } /> @@ -225,7 +267,7 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) { - {sortedBatteryView.map(({ BatteryId, battery }) => ( + {sortedBatteryView.map(({ BatteryId, label, battery }) => ( */} - {'Battery Cluster ' + BatteryId} + {label} {/**/} 0) { + props.activeClusters.forEach((c) => { + pathsToSearch.push('Node' + c.flatIdx); + }); + } else { + for (let i = 0; i < props.batteryClusterNumber; i++) { + pathsToSearch.push('Node' + i); + } } const total = pathsToSearch.length; @@ -207,7 +217,8 @@ function MainStatsSodioHome(props: MainStatsSodioHomeProps) { 2, UnixTime.fromTicks(startDate.unix()), UnixTime.fromTicks(endDate.unix()), - props.batteryClusterNumber + props.batteryClusterNumber, + props.activeClusters ); resultPromise @@ -270,7 +281,8 @@ function MainStatsSodioHome(props: MainStatsSodioHomeProps) { 2, UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)), UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2)), - props.batteryClusterNumber + props.batteryClusterNumber, + props.activeClusters ); resultPromise diff --git a/typescript/frontend-marios2/src/content/dashboards/Information/installationSetupUtils.ts b/typescript/frontend-marios2/src/content/dashboards/Information/installationSetupUtils.ts index 2756f3d97..e5e65e0ec 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Information/installationSetupUtils.ts +++ b/typescript/frontend-marios2/src/content/dashboards/Information/installationSetupUtils.ts @@ -115,6 +115,32 @@ export const computeFlatValues = ( }; }; +export interface ActiveCluster { + invIdx: number; + clIdx: number; + flatIdx: number; +} + +export const getActiveClusters = (raw: string): ActiveCluster[] => { + if (!raw || raw.trim() === '') return []; + if (!raw.includes('/') && !raw.includes('|')) return []; + + const result: ActiveCluster[] = []; + let flatIdx = 0; + raw.split('/').forEach((invStr, invIdx) => { + invStr.split('|').forEach((clStr, clIdx) => { + const hasSn = clStr + .split(',') + .some((s) => s.trim() !== ''); + if (hasSn) { + result.push({ invIdx, clIdx, flatIdx }); + } + flatIdx += 1; + }); + }); + return result; +}; + export const wouldLoseData = ( oldTree: BatterySnTree, newPreset: PresetConfig diff --git a/typescript/frontend-marios2/src/interfaces/Chart.tsx b/typescript/frontend-marios2/src/interfaces/Chart.tsx index 3b9a8cd2a..012d45d0d 100644 --- a/typescript/frontend-marios2/src/interfaces/Chart.tsx +++ b/typescript/frontend-marios2/src/interfaces/Chart.tsx @@ -81,11 +81,13 @@ export const transformInputToBatteryViewDataJson = async ( product: number, start_time?: UnixTime, end_time?: UnixTime, - batteryClusterNumber?: number + batteryClusterNumber?: number, + activeClusters?: Array<{ invIdx: number; clIdx: number; flatIdx: number }> ): Promise<{ chartData: BatteryDataInterface; chartOverview: BatteryOverviewInterface; }> => { + const useActive = !!activeClusters && activeClusters.length > 0; const prefixes = ['', 'k', 'M', 'G', 'T']; const MAX_NUMBER = 9999999; const isSodioHome = product === 2 || product === 5; @@ -199,17 +201,34 @@ export const transformInputToBatteryViewDataJson = async ( const inv = (result as any)?.InverterRecord; if (!inv) continue; - const numBatteries = batteryClusterNumber || 1; + // Iteration order: either active-cluster list (skips empty slots, + // preserves flat hardware indices) or a contiguous 0..N-1 fallback. + const iter = useActive + ? activeClusters!.map((c) => ({ + flatIdx: c.flatIdx, + invIdx: c.invIdx, + clIdx: c.clIdx + })) + : Array.from({ length: batteryClusterNumber || 1 }, (_, k) => ({ + flatIdx: k, + invIdx: Math.floor(k / 2), + clIdx: k % 2 + })); + + const inverterCount = iter.reduce( + (max, c) => Math.max(max, c.invIdx + 1), + 0 + ); + const showInverterLabel = !!inv?.Devices && inverterCount > 1; + let old_length = pathsToSave.length; - if (numBatteries > old_length) { - for (let b = old_length; b < numBatteries; b++) { - const nodeName = 'Node' + b; - if (!pathsToSave.includes(nodeName)) { - pathsToSave.push(nodeName); - } + iter.forEach((c) => { + const nodeName = 'Node' + c.flatIdx; + if (!pathsToSave.includes(nodeName)) { + pathsToSave.push(nodeName); } - } + }); if (initialiation) { initialiation = false; @@ -224,12 +243,15 @@ export const transformInputToBatteryViewDataJson = async ( }); } - if (numBatteries > old_length) { + if (pathsToSave.length > old_length) { categories.forEach((category) => { - pathsToSave.forEach((path) => { + iter.forEach((c) => { + const path = 'Node' + c.flatIdx; if (pathsToSave.indexOf(path) >= old_length) { - const displayIndex = pathsToSave.indexOf(path); - chartData[category].data[path] = { name: 'Battery Cluster ' + (displayIndex + 1), data: [] }; + const name = showInverterLabel + ? `Battery Cluster ${c.clIdx + 1} in Inverter ${c.invIdx + 1}` + : `Battery Cluster ${c.clIdx + 1}`; + chartData[category].data[path] = { name, data: [] }; } }); }); @@ -253,24 +275,23 @@ export const transformInputToBatteryViewDataJson = async ( Soh: 'Soh' }; - for (let j = 0; j < pathsToSave.length; j++) { + iter.forEach((c) => { + const path = 'Node' + c.flatIdx; categories.forEach((category) => { let value: number | undefined; if (hasDevices) { - // Sinexcel: nested under Devices — 0→D1/B1, 1→D1/B2, 2→D2/B1, ... - const deviceId = String(Math.floor(j / 2) + 1); - const bi = (j % 2) + 1; - const device = inv.Devices[deviceId]; + // Sinexcel: Devices keyed by "1","2",... (1-based dict keys) + const device = inv.Devices[String(c.invIdx + 1)]; + const bi = c.clIdx + 1; const fieldName = `Battery${bi}${categoryFieldMapSinexcel[category]}`; value = device?.[fieldName]; - // Fallback for Soc if ((value === undefined || value === null) && category === 'Soc') { value = device?.[`Battery${bi}SocSecondvalue`]; } } else { - // Growatt: flat Battery1Soc, Battery2Voltage, ... - const batteryIndex = j + 1; + // Growatt: flat Battery1Soc, Battery2Voltage, ... on InverterRecord + const batteryIndex = c.clIdx + 1; const fieldName = `Battery${batteryIndex}${categoryFieldMapGrowatt[category]}`; value = inv[fieldName]; } @@ -282,13 +303,13 @@ export const transformInputToBatteryViewDataJson = async ( if (value > chartOverview[category].max) { chartOverview[category].max = value; } - chartData[category].data[pathsToSave[j]].data.push([ + chartData[category].data[path].data.push([ adjustedTimestampArray[i], value ]); } }); - } + }); } else { // SaliMax, Salidomo, SodistoreMax: existing logic const battery_nodes = diff --git a/typescript/frontend-marios2/src/lang/de.json b/typescript/frontend-marios2/src/lang/de.json index ecb97852c..a22ed6d0c 100644 --- a/typescript/frontend-marios2/src/lang/de.json +++ b/typescript/frontend-marios2/src/lang/de.json @@ -97,6 +97,8 @@ "selectModel": "Modell auswählen...", "inverterN": "Wechselrichter {n}", "clusterN": "Cluster {n}", + "batteryClusterN": "Batterie-Cluster {n}", + "batteryClusterInInverter": "Batterie-Cluster {cl} an Wechselrichter {inv}", "clustersBatteriesSummary": "{filledClusters}/{totalClusters} Cluster, {filledBat}/{totalBat} Batterien", "batteriesSummary": "{filled}/{total} Batterien", "inverterNSerialNumber": "Wechselrichter {n} Seriennummer", diff --git a/typescript/frontend-marios2/src/lang/en.json b/typescript/frontend-marios2/src/lang/en.json index 5b5e67b91..afe7c51ad 100644 --- a/typescript/frontend-marios2/src/lang/en.json +++ b/typescript/frontend-marios2/src/lang/en.json @@ -79,6 +79,8 @@ "selectModel": "Select model...", "inverterN": "Inverter {n}", "clusterN": "Cluster {n}", + "batteryClusterN": "Battery Cluster {n}", + "batteryClusterInInverter": "Battery Cluster {cl} in Inverter {inv}", "clustersBatteriesSummary": "{filledClusters}/{totalClusters} clusters, {filledBat}/{totalBat} batteries", "batteriesSummary": "{filled}/{total} batteries", "inverterNSerialNumber": "Inverter {n} Serial Number", diff --git a/typescript/frontend-marios2/src/lang/fr.json b/typescript/frontend-marios2/src/lang/fr.json index d6cdc6f62..bd0c344ae 100644 --- a/typescript/frontend-marios2/src/lang/fr.json +++ b/typescript/frontend-marios2/src/lang/fr.json @@ -91,6 +91,8 @@ "selectModel": "Sélectionner le modèle...", "inverterN": "Onduleur {n}", "clusterN": "Cluster {n}", + "batteryClusterN": "Cluster de batteries {n}", + "batteryClusterInInverter": "Cluster de batteries {cl} sur onduleur {inv}", "clustersBatteriesSummary": "{filledClusters}/{totalClusters} clusters, {filledBat}/{totalBat} batteries", "batteriesSummary": "{filled}/{total} batteries", "inverterNSerialNumber": "Numéro de série onduleur {n}", diff --git a/typescript/frontend-marios2/src/lang/it.json b/typescript/frontend-marios2/src/lang/it.json index 59a8c11ce..a1510706b 100644 --- a/typescript/frontend-marios2/src/lang/it.json +++ b/typescript/frontend-marios2/src/lang/it.json @@ -79,6 +79,8 @@ "selectModel": "Seleziona modello...", "inverterN": "Inverter {n}", "clusterN": "Cluster {n}", + "batteryClusterN": "Cluster batteria {n}", + "batteryClusterInInverter": "Cluster batteria {cl} su inverter {inv}", "clustersBatteriesSummary": "{filledClusters}/{totalClusters} cluster, {filledBat}/{totalBat} batterie", "batteriesSummary": "{filled}/{total} batterie", "inverterNSerialNumber": "Numero di serie inverter {n}",