From a8db23cadfc8c72837e26018a0bd8df503bb05dd Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Wed, 8 Apr 2026 10:06:40 +0200 Subject: [PATCH] adapt sodistore home setup for growatt and sinexcel, add battery voltage to OverView page --- .../Information/InformationSodistoreHome.tsx | 60 ++++++++++-- .../Information/installationSetupUtils.ts | 21 +++- .../dashboards/Overview/chartOptions.tsx | 26 ++++- .../content/dashboards/Overview/overview.tsx | 10 +- .../SodistorehomeInstallationForm.tsx | 95 ++++++++++--------- .../frontend-marios2/src/interfaces/Chart.tsx | 24 +++-- 6 files changed, 169 insertions(+), 67 deletions(-) diff --git a/typescript/frontend-marios2/src/content/dashboards/Information/InformationSodistoreHome.tsx b/typescript/frontend-marios2/src/content/dashboards/Information/InformationSodistoreHome.tsx index d29a32c47..1c3d18bd8 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Information/InformationSodistoreHome.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Information/InformationSodistoreHome.tsx @@ -34,7 +34,7 @@ import { useNavigate } from 'react-router-dom'; import { UserType } from '../../../interfaces/UserTypes'; import axiosConfig from '../../../Resources/axiosConfig'; import { - INSTALLATION_PRESETS, + getPresetsForDevice, PresetConfig, BatterySnTree, parseBatterySnTree, @@ -113,7 +113,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) { ? (inverterCount && parseInt(inverterCount, 10) > 0 ? buildSodistoreProPreset(parseInt(inverterCount, 10)) : null) - : (INSTALLATION_PRESETS[selectedPreset] || null); + : (getPresetsForDevice(formValues.device)[selectedPreset] || null); const [batterySnTree, setBatterySnTree] = useState(() => { if (presetConfig) { @@ -141,8 +141,32 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) { return Array.from({ length: invCount }, () => '1'); }); + // When presetConfig is available, ensure flat values (batteryClusterNumber, batteryNumber) + // stay in sync with the current preset structure. This handles: + // - Legacy installations with device=0 → user sets device type + // - Preset structure changed (e.g., Growatt home 9 was [[1,1]] → now [[2]]) + useEffect(() => { + if (!presetConfig) return; + + // Re-parse battery tree if empty but serial numbers exist + let tree = batterySnTree; + if (tree.length === 0 && props.values.batterySerialNumbers) { + tree = parseBatterySnTree(props.values.batterySerialNumbers, presetConfig); + setBatterySnTree(tree); + } + + // Always recalculate flat values from current preset to keep DB in sync + const flat = computeFlatValues(presetConfig, tree); + if ( + flat.batteryClusterNumber !== formValues.batteryClusterNumber || + flat.batteryNumber !== formValues.batteryNumber + ) { + setFormValues((prev) => ({ ...prev, ...flat })); + } + }, [presetConfig]); + const handlePresetChange = (newPreset: string) => { - const newConfig = INSTALLATION_PRESETS[newPreset]; + const newConfig = getPresetsForDevice(formValues.device)[newPreset]; if (!newConfig) return; // Check for data loss — either from existing tree or legacy flat data @@ -171,7 +195,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) { }; const applyPreset = (newPreset: string) => { - const newConfig = INSTALLATION_PRESETS[newPreset]; + const newConfig = getPresetsForDevice(formValues.device)[newPreset]; if (!newConfig) return; setSelectedPreset(newPreset); @@ -315,10 +339,28 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) { const handleChange = (e) => { const { name, value } = e.target; - setFormValues({ - ...formValues, - [name]: value - }); + const updated = { ...formValues, [name]: value }; + + // When device type changes, reset preset if it's not available for the new device + if (name === 'device' && !isSodistorePro) { + const newDevicePresets = getPresetsForDevice(Number(value)); + if (selectedPreset && !newDevicePresets[selectedPreset]) { + setSelectedPreset(''); + setBatterySnTree([]); + setInverterSerialNumbers([]); + setDataloggerSerialNumbers([]); + setPvStringsPerInverter([]); + updated.installationModel = ''; + updated.batteryNumber = 0; + updated.batteryClusterNumber = 0; + updated.batterySerialNumbers = ''; + updated.inverterSN = ''; + updated.dataloggerSN = ''; + updated.pvStringsPerInverter = ''; + } + } + + setFormValues(updated); }; const handleSubmit = () => { @@ -849,7 +891,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) { - {Object.keys(INSTALLATION_PRESETS).map((name) => ( + {Object.keys(getPresetsForDevice(formValues.device)).map((name) => ( {name} diff --git a/typescript/frontend-marios2/src/content/dashboards/Information/installationSetupUtils.ts b/typescript/frontend-marios2/src/content/dashboards/Information/installationSetupUtils.ts index 6c4f4beb1..37fc70d4a 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Information/installationSetupUtils.ts +++ b/typescript/frontend-marios2/src/content/dashboards/Information/installationSetupUtils.ts @@ -12,13 +12,24 @@ export type PresetConfig = number[][]; // 3D array: [inverter][cluster][batteryIndex] = serialNumber export type BatterySnTree = string[][][]; -export const INSTALLATION_PRESETS: Record = { - 'sodistore home 9': [[1, 1]], - 'sodistore home 18': [[2, 2]], - 'sodistore home 27': [[2, 2], [1, 1]], - 'sodistore home 36': [[2, 2], [2, 2]], +// Device-aware presets: keyed by device ID, then model name +// Device 3 = Growatt, Device 4 = inesco 12K +export const INSTALLATION_PRESETS: Record> = { + 3: { + 'sodistore home 9': [[2]], + 'sodistore home 18': [[4]], + }, + 4: { + 'sodistore home 9': [[1, 1]], + 'sodistore home 18': [[2, 2]], + 'sodistore home 27': [[2, 2], [1, 1]], + 'sodistore home 36': [[2, 2], [2, 2]], + }, }; +export const getPresetsForDevice = (deviceId: number): Record => + INSTALLATION_PRESETS[deviceId] ?? {}; + export const buildSodistoreProPreset = (inverterCount: number): PresetConfig => Array.from({ length: inverterCount }, () => [2, 2]); diff --git a/typescript/frontend-marios2/src/content/dashboards/Overview/chartOptions.tsx b/typescript/frontend-marios2/src/content/dashboards/Overview/chartOptions.tsx index 1b202f803..6e4df3c25 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Overview/chartOptions.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Overview/chartOptions.tsx @@ -6,7 +6,8 @@ export const getChartOptions = ( chartInfo: chartInfoInterface, type: string, dateList: string[], - stacked: Boolean + stacked: Boolean, + voltageInfo?: chartInfoInterface ): ApexOptions => { return type.includes('daily') ? { @@ -165,7 +166,28 @@ export const getChartOptions = ( return Math.round(value).toString(); } } - } + }, + ...(voltageInfo ? [{ + seriesName: 'Battery Voltage', + opposite: true, + tickAmount: 5, + min: voltageInfo.min > 0 ? Math.floor(voltageInfo.min / 5) * 5 : 0, + max: Math.ceil(voltageInfo.max / 5) * 5, + title: { + text: '(V)', + style: { + fontSize: '12px' + }, + offsetY: -190, + offsetX: -45, + rotate: 0 + }, + labels: { + formatter: function (value: number) { + return Math.round(value).toString(); + } + } + }] : []) ] : { tickAmount: chartInfo.unit === '(%)' ? 5 : 6, diff --git a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx index 0d0fa4a4f..1854b361e 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx @@ -712,7 +712,8 @@ function Overview(props: OverviewProps) { dailyDataArray[chartState].chartOverview.overview, 'dailyoverview', [], - true + true, + (product === 2 || product === 5) ? dailyDataArray[chartState].chartOverview.batteryVoltage : undefined ), chart: { events: { @@ -744,7 +745,12 @@ function Overview(props: OverviewProps) { ...dailyDataArray[chartState].chartData.soc, type: 'line', color: '#008FFB' - } + }, + ...((product === 2 || product === 5) ? [{ + ...dailyDataArray[chartState].chartData.batteryVoltage, + type: 'line' as const, + color: '#9b59b6' + }] : []) ]} height={420} /> diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistorehomeInstallationForm.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistorehomeInstallationForm.tsx index 88e2814e3..c35c740a5 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistorehomeInstallationForm.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistorehomeInstallationForm.tsx @@ -17,7 +17,7 @@ import { Close as CloseIcon } from '@mui/icons-material'; import { I_Installation } from 'src/interfaces/InstallationTypes'; import { InstallationsContext } from 'src/contexts/InstallationsContextProvider'; import { FormattedMessage } from 'react-intl'; -import { INSTALLATION_PRESETS, SODIOHOME_DEVICE_TYPES } from '../Information/installationSetupUtils'; +import { getPresetsForDevice, SODIOHOME_DEVICE_TYPES } from '../Information/installationSetupUtils'; interface SodistorehomeInstallationFormPros { cancel: () => void; @@ -38,7 +38,7 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros) ...(isSodistorePro ? { device: 4 } : {}), }); const [inverterCount, setInverterCount] = useState(''); - const requiredFields = ['name', 'vpnIp', ...(isSodistorePro ? [] : ['installationModel'])]; + const requiredFields = ['name', 'vpnIp', ...(isSodistorePro ? [] : ['device', 'installationModel'])]; const DeviceTypes = isSodistorePro ? [{ id: 4, name: 'inesco 12K - WR Hybrid' }] @@ -49,11 +49,17 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros) const handleChange = (e) => { const { name, value } = e.target; + const updated = { ...formValues, [name]: value }; - setFormValues({ - ...formValues, - [name]: value - }); + // Reset preset when device type changes if current preset is invalid + if (name === 'device' && !isSodistorePro) { + const newDevicePresets = getPresetsForDevice(Number(value)); + if (formValues.installationModel && !newDevicePresets[formValues.installationModel]) { + updated.installationModel = ''; + } + } + + setFormValues(updated); }; const handleSubmit = async (e) => { setLoading(true); @@ -167,10 +173,48 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros) /> ) : ( + <> + {/* Device type must be selected before model — it determines available presets */}
+ + + + + +
+ +
+ - {Object.keys(INSTALLATION_PRESETS).map((name) => ( + {Object.keys(getPresetsForDevice(formValues.device as number)).map((name) => ( {name} @@ -202,42 +246,7 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
- )} - - {!isSodistorePro && ( -
- - - - - - -
+ )} diff --git a/typescript/frontend-marios2/src/interfaces/Chart.tsx b/typescript/frontend-marios2/src/interfaces/Chart.tsx index 791374c64..3b9a8cd2a 100644 --- a/typescript/frontend-marios2/src/interfaces/Chart.tsx +++ b/typescript/frontend-marios2/src/interfaces/Chart.tsx @@ -30,6 +30,7 @@ export interface overviewInterface { overview: chartInfoInterface; ACLoad: chartInfoInterface; DCLoad: chartInfoInterface; + batteryVoltage: chartInfoInterface; } export interface chartAggregatedDataInterface { @@ -53,6 +54,7 @@ export interface chartDataInterface { dcBusVoltage: { name: string; data: number[] }; ACLoad: { name: string; data: number[] }; DCLoad: { name: string; data: number[] }; + batteryVoltage: { name: string; data: number[] }; } export interface BatteryDataInterface { @@ -428,7 +430,8 @@ export const transformInputToDailyDataJson = async ( 'SODIOHOME_PV_POWER', null, // dcBusVoltage not available for SodioHome 'SODIOHOME_CONSUMPTION', - null // DCLoad not available for SodioHome + null, // DCLoad not available for SodioHome + 'SODIOHOME_BATTERY_VOLTAGE' ] : [ 'Battery.Soc', @@ -438,7 +441,8 @@ export const transformInputToDailyDataJson = async ( 'PvOnDc', 'DcDc.Dc.Link.Voltage', 'LoadOnAcGrid.Power.Active', - 'LoadOnDc.Power' + 'LoadOnDc.Power', + null // batteryVoltage not available for Salimax ]; const categories = [ 'soc', @@ -448,7 +452,8 @@ export const transformInputToDailyDataJson = async ( 'pvProduction', 'dcBusVoltage', 'ACLoad', - 'DCLoad' + 'DCLoad', + 'batteryVoltage' ]; const chartData: chartDataInterface = { @@ -459,7 +464,8 @@ export const transformInputToDailyDataJson = async ( pvProduction: { name: 'PV Power', data: [] }, dcBusVoltage: { name: 'DC Bus Voltage', data: [] }, ACLoad: { name: 'AC Load', data: [] }, - DCLoad: { name: 'DC Load', data: [] } + DCLoad: { name: 'DC Load', data: [] }, + batteryVoltage: { name: 'Battery Voltage', data: [] } }; const chartOverview: overviewInterface = { @@ -472,7 +478,8 @@ export const transformInputToDailyDataJson = async ( dcBusVoltage: { magnitude: 0, unit: '', min: 0, max: 0 }, overview: { magnitude: 0, unit: '', min: 0, max: 0 }, ACLoad: { magnitude: 0, unit: '', min: 0, max: 0 }, - DCLoad: { magnitude: 0, unit: '', min: 0, max: 0 } + DCLoad: { magnitude: 0, unit: '', min: 0, max: 0 }, + batteryVoltage: { magnitude: 0, unit: '', min: 0, max: 0 } }; categories.forEach((category) => { @@ -566,6 +573,9 @@ export const transformInputToDailyDataJson = async ( case 6: // consumption value = inv.TotalLoadPower ?? inv.ConsumptionPower; break; + case 8: // battery voltage + value = inv.AvgBatteryVoltage ?? inv.Battery1Voltage; + break; } } } else if (category_index === 4) { @@ -636,6 +646,7 @@ export const transformInputToDailyDataJson = async ( '(' + prefixes[chartOverview['ACLoad'].magnitude] + 'W' + ')'; chartOverview.DCLoad.unit = '(' + prefixes[chartOverview['DCLoad'].magnitude] + 'W' + ')'; + chartOverview.batteryVoltage.unit = '(V)'; chartOverview.overview = { magnitude: Math.max( @@ -750,7 +761,8 @@ export const transformInputToAggregatedDataJson = async ( dcBusVoltage: { magnitude: 0, unit: '', min: 0, max: 0 }, overview: { magnitude: 0, unit: '', min: 0, max: 0 }, ACLoad: { magnitude: 0, unit: '', min: 0, max: 0 }, - DCLoad: { magnitude: 0, unit: '', min: 0, max: 0 } + DCLoad: { magnitude: 0, unit: '', min: 0, max: 0 }, + batteryVoltage: { magnitude: 0, unit: '', min: 0, max: 0 } }; pathsToSearch.forEach((path) => {