diff --git a/csharp/App/Backend/DataTypes/Configuration.cs b/csharp/App/Backend/DataTypes/Configuration.cs index 5c0726ca5..8ba1d7a43 100644 --- a/csharp/App/Backend/DataTypes/Configuration.cs +++ b/csharp/App/Backend/DataTypes/Configuration.cs @@ -12,7 +12,9 @@ public class Configuration public double? MaximumDischargingCurrent { get; set; } public double? MaximumChargingCurrent { get; set; } public double? OperatingPriority { get; set; } + public int? InverterNumber { get; set; } public double? BatteriesCount { get; set; } + public List? BatteriesCountPerInverter { get; set; } public double? ClusterNumber { get; set; } public double? PvNumber { get; set; } public bool ControlPermission { get; set; } @@ -25,11 +27,11 @@ public class Configuration return $"MinimumSoC: {MinimumSoC}, GridSetPoint: {GridSetPoint}, CalibrationChargeState: {CalibrationChargeState}, CalibrationChargeDate: {CalibrationChargeDate}, " + $"CalibrationDischargeState: {CalibrationDischargeState}, CalibrationDischargeDate: {CalibrationDischargeDate}, " + $"MaximumDischargingCurrent: {MaximumDischargingCurrent}, MaximumChargingCurrent: {MaximumChargingCurrent}, OperatingPriority: {OperatingPriority}, " + - $"BatteriesCount: {BatteriesCount}, ClusterNumber: {ClusterNumber}, PvNumber: {PvNumber}, ControlPermission:{ControlPermission}, "+ + $"InverterNumber: {InverterNumber}, BatteriesCount: {BatteriesCount}, BatteriesCountPerInverter: [{(BatteriesCountPerInverter != null ? string.Join(", ", BatteriesCountPerInverter) : "")}], ClusterNumber: {ClusterNumber}, PvNumber: {PvNumber}, ControlPermission:{ControlPermission}, "+ $"SinexcelTimeChargeandDischargePower: {TimeChargeandDischargePower}, SinexcelStartTimeChargeandDischargeDayandTime: {StartTimeChargeandDischargeDayandTime}, SinexcelStopTimeChargeandDischargeDayandTime: {StopTimeChargeandDischargeDayandTime}"; - + } - + public string GetConfigurationSalimax() { return @@ -45,7 +47,7 @@ public class Configuration public string GetConfigurationSodistoreHome() { return $"MinimumSoC: {MinimumSoC}, MaximumDischargingCurrent: {MaximumDischargingCurrent}, MaximumChargingCurrent: {MaximumChargingCurrent}, OperatingPriority: {OperatingPriority}, " + - $"BatteriesCount: {BatteriesCount}, ClusterNumber: {ClusterNumber}, PvNumber: {PvNumber}, ControlPermission:{ControlPermission}, "+ + $"InverterNumber: {InverterNumber}, BatteriesCount: {BatteriesCount}, BatteriesCountPerInverter: [{(BatteriesCountPerInverter != null ? string.Join(", ", BatteriesCountPerInverter) : "")}], ClusterNumber: {ClusterNumber}, PvNumber: {PvNumber}, ControlPermission:{ControlPermission}, "+ $"SinexcelTimeChargeandDischargePower: {TimeChargeandDischargePower}, SinexcelStartTimeChargeandDischargeDayandTime: {StartTimeChargeandDischargeDayandTime}, SinexcelStopTimeChargeandDischargeDayandTime: {StopTimeChargeandDischargeDayandTime}"; } 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 0f8124d56..e74ca9df3 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx @@ -329,6 +329,8 @@ export interface JSONRecordData { MaximumDischargingCurrent: number; OperatingPriority: string; BatteriesCount: number; + InverterNumber?: number; + BatteriesCountPerInverter?: number[]; ClusterNumber: number; PvNumber: number; ControlPermission:boolean; @@ -696,6 +698,8 @@ export type ConfigurationValues = { maximumChargingCurrent: number; operatingPriority: number; batteriesCount: number; + inverterNumber: number; + batteriesCountPerInverter: number[]; clusterNumber: number; PvNumber: number; controlPermission:boolean; diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistoreHomeConfiguration.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistoreHomeConfiguration.tsx index 620a8f388..1a0351107 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistoreHomeConfiguration.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistoreHomeConfiguration.tsx @@ -70,6 +70,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) { })); }; const theme = useTheme(); + const [formDirty, setFormDirty] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); const [updated, setUpdated] = useState(false); @@ -89,27 +90,34 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) { const pendingConfigKey = `pendingConfig_${props.id}`; // Helper to build form values from S3 data - const getS3Values = (): Partial => ({ - minimumSoC: props.values.Config.MinSoc, - maximumDischargingCurrent: props.values.Config.MaximumChargingCurrent, - maximumChargingCurrent: props.values.Config.MaximumDischargingCurrent, - operatingPriority: resolveOperatingPriorityIndex( - props.values.Config.OperatingPriority - ), - batteriesCount: props.values.Config.BatteriesCount, - clusterNumber: props.values.Config.ClusterNumber ?? 1, - PvNumber: props.values.Config.PvNumber ?? 0, - timeChargeandDischargePower: props.values.Config?.TimeChargeandDischargePower ?? 0, - startTimeChargeandDischargeDayandTime: - props.values.Config?.StartTimeChargeandDischargeDayandTime - ? dayjs(props.values.Config.StartTimeChargeandDischargeDayandTime).toDate() - : null, - stopTimeChargeandDischargeDayandTime: - props.values.Config?.StopTimeChargeandDischargeDayandTime - ? dayjs(props.values.Config.StopTimeChargeandDischargeDayandTime).toDate() - : null, - controlPermission: String(props.values.Config.ControlPermission).toLowerCase() === "true", - }); + const getS3Values = (): Partial => { + const inverterNum = props.values.Config.InverterNumber ?? 1; + const batteriesPerInverter: number[] = props.values.Config.BatteriesCountPerInverter + ?? Array(inverterNum).fill(props.values.Config.BatteriesCount || 1); + return { + minimumSoC: props.values.Config.MinSoc, + maximumDischargingCurrent: props.values.Config.MaximumChargingCurrent, + maximumChargingCurrent: props.values.Config.MaximumDischargingCurrent, + operatingPriority: resolveOperatingPriorityIndex( + props.values.Config.OperatingPriority + ), + inverterNumber: inverterNum, + batteriesCountPerInverter: batteriesPerInverter, + batteriesCount: props.values.Config.BatteriesCount, + clusterNumber: props.values.Config.ClusterNumber || 1, + PvNumber: props.values.Config.PvNumber ?? 0, + timeChargeandDischargePower: props.values.Config?.TimeChargeandDischargePower ?? 0, + startTimeChargeandDischargeDayandTime: + props.values.Config?.StartTimeChargeandDischargeDayandTime + ? dayjs(props.values.Config.StartTimeChargeandDischargeDayandTime).toDate() + : null, + stopTimeChargeandDischargeDayandTime: + props.values.Config?.StopTimeChargeandDischargeDayandTime + ? dayjs(props.values.Config.StopTimeChargeandDischargeDayandTime).toDate() + : null, + controlPermission: String(props.values.Config.ControlPermission).toLowerCase() === "true", + }; + }; // Restore pending config from localStorage, converting date strings back to Date objects. // Returns { values, s3ConfigSnapshot } or null if no pending config. @@ -169,7 +177,10 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) { // When S3 data updates (polled every 60s), reconcile with any pending localStorage. // Strategy: device is the authority. Once S3 Config changes from the snapshot taken at // submit time, the device has uploaded new data — trust S3 regardless of values. + // Skip reset if the user is actively editing (formDirty). useEffect(() => { + if (formDirty) return; + const s3Values = getS3Values(); const pending = restorePendingConfig(); @@ -192,6 +203,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) { }, [props.values]); const handleOperatingPriorityChange = (event) => { + setFormDirty(true); setFormValues({ ...formValues, ['operatingPriority']: OperatingPriorityOptions.indexOf( @@ -230,7 +242,9 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) { maximumDischargingCurrent: formValues.maximumDischargingCurrent, maximumChargingCurrent: formValues.maximumChargingCurrent, operatingPriority: formValues.operatingPriority, - batteriesCount:formValues.batteriesCount, + inverterNumber: formValues.inverterNumber, + batteriesCountPerInverter: formValues.batteriesCountPerInverter, + batteriesCount: formValues.batteriesCountPerInverter?.[0] ?? formValues.batteriesCount, clusterNumber:formValues.clusterNumber, PvNumber:formValues.PvNumber, timeChargeandDischargePower: formValues.timeChargeandDischargePower, @@ -259,6 +273,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) { if (res) { setUpdated(true); setLoading(false); + setFormDirty(false); // Save submitted values + S3 snapshot to localStorage for optimistic UI update. // s3ConfigSnapshot = fingerprint of S3 Config at submit time. @@ -273,6 +288,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) { }; const handleChange = (e) => { + setFormDirty(true); const { name, value } = e.target; if (name === 'minimumSoC') { @@ -305,6 +321,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) { }; const handleTimeChargeDischargeChange = (name: string, value: any) => { + setFormDirty(true); setFormValues((prev) => ({ ...prev, [name]: value @@ -384,11 +401,13 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) { + onChange={(e) => { + setFormDirty(true); setFormValues((prev) => ({ ...prev, controlPermission: e.target.checked, - })) + })); + } } sx={{ transform: "scale(1.4)", marginLeft: "15px" }} /> @@ -405,19 +424,63 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
- } - name="batteriesCount" - value={formValues.batteriesCount} - onChange={handleChange} + label={intl.formatMessage({ id: 'inverterNumber' })} + name="inverterNumber" + value={formValues.inverterNumber ?? ''} + onChange={(e) => { + setFormDirty(true); + const raw = e.target.value; + if (raw === '') { + setFormValues((prev) => ({ ...prev, inverterNumber: '' as any })); + return; + } + const parsed = parseInt(raw); + if (isNaN(parsed) || parsed < 1) return; + const currentArr = formValues.batteriesCountPerInverter || [1]; + const newArr = Array.from({ length: parsed }, (_, i) => currentArr[i] ?? 1); + setFormValues((prev) => ({ + ...prev, + inverterNumber: parsed, + batteriesCountPerInverter: newArr, + })); + }} fullWidth />
+ {Array.from({ length: formValues.inverterNumber ?? 1 }, (_, i) => ( +
+ { + setFormDirty(true); + const raw = e.target.value; + if (raw === '') { + setFormValues((prev) => { + const arr = [...(prev.batteriesCountPerInverter || [1])]; + arr[i] = '' as any; + return { ...prev, batteriesCountPerInverter: arr }; + }); + return; + } + const parsed = parseInt(raw); + if (isNaN(parsed) || parsed < 1) return; + setFormValues((prev) => { + const arr = [...(prev.batteriesCountPerInverter || [1])]; + arr[i] = parsed; + return { ...prev, batteriesCountPerInverter: arr }; + }); + }} + fullWidth + /> +
+ ))} + {device === 4 && ( <>
@@ -489,12 +552,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
- } + label={intl.formatMessage({ id: 'maximumChargingCurrentPerBattery' })} name="maximumChargingCurrent" value={formValues.maximumChargingCurrent} onChange={handleChange} @@ -504,12 +562,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
- } + label={intl.formatMessage({ id: 'maximumDischargingCurrentPerBattery' })} name="maximumDischargingCurrent" value={formValues.maximumDischargingCurrent} onChange={handleChange} @@ -554,13 +607,13 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) { {/* Power input*/}
handleTimeChargeDischargeChange(e.target.name, e.target.value) } - helperText={intl.formatMessage({ id: 'enterPowerValue' })} + helperText={intl.formatMessage({ id: 'perInverter' })} fullWidth />
diff --git a/typescript/frontend-marios2/src/lang/de.json b/typescript/frontend-marios2/src/lang/de.json index 1bab475d6..fe64612e4 100644 --- a/typescript/frontend-marios2/src/lang/de.json +++ b/typescript/frontend-marios2/src/lang/de.json @@ -486,6 +486,11 @@ "minimumSocPercent": "Minimaler Ladezustand (%)", "powerW": "Leistung (W)", "enterPowerValue": "Positiven oder negativen Leistungswert eingeben", + "inverterNumber": "Anzahl Wechselrichter", + "batteriesCountInInverter": "Batterieanzahl in Wechselrichter {number}", + "maximumChargingCurrentPerBattery": "Maximaler Ladestrom pro Batterie (A)", + "maximumDischargingCurrentPerBattery": "Maximaler Entladestrom pro Batterie (A)", + "powerPerInverterKW": "Leistung pro Wechselrichter (kW)", "startDateTime": "Startdatum und -zeit (Startzeit < Stoppzeit)", "stopDateTime": "Stoppdatum und -zeit (Startzeit < Stoppzeit)", "tourLanguageTitle": "Sprache", diff --git a/typescript/frontend-marios2/src/lang/en.json b/typescript/frontend-marios2/src/lang/en.json index 2a312466e..7e065d17d 100644 --- a/typescript/frontend-marios2/src/lang/en.json +++ b/typescript/frontend-marios2/src/lang/en.json @@ -234,6 +234,11 @@ "minimumSocPercent": "Minimum SoC (%)", "powerW": "Power (W)", "enterPowerValue": "Enter a positive or negative power value", + "inverterNumber": "Inverter Number", + "batteriesCountInInverter": "Batteries Count in Inverter {number}", + "maximumChargingCurrentPerBattery": "Maximum Charging Current per Battery (A)", + "maximumDischargingCurrentPerBattery": "Maximum Discharging Current per Battery (A)", + "powerPerInverterKW": "Power per Inverter (kW)", "startDateTime": "Start Date and Time (Start Time < Stop Time)", "stopDateTime": "Stop Date and Time (Start Time < Stop Time)", "tourLanguageTitle": "Language", diff --git a/typescript/frontend-marios2/src/lang/fr.json b/typescript/frontend-marios2/src/lang/fr.json index 3614a2847..7433cd7ed 100644 --- a/typescript/frontend-marios2/src/lang/fr.json +++ b/typescript/frontend-marios2/src/lang/fr.json @@ -486,6 +486,11 @@ "minimumSocPercent": "SoC minimum (%)", "powerW": "Puissance (W)", "enterPowerValue": "Entrez une valeur de puissance positive ou négative", + "inverterNumber": "Nombre d'onduleurs", + "batteriesCountInInverter": "Nombre de batteries dans l'onduleur {number}", + "maximumChargingCurrentPerBattery": "Courant de charge maximum par batterie (A)", + "maximumDischargingCurrentPerBattery": "Courant de décharge maximum par batterie (A)", + "powerPerInverterKW": "Puissance par onduleur (kW)", "startDateTime": "Date et heure de début (Début < Fin)", "stopDateTime": "Date et heure de fin (Début < Fin)", "tourLanguageTitle": "Langue", diff --git a/typescript/frontend-marios2/src/lang/it.json b/typescript/frontend-marios2/src/lang/it.json index c1bc2f11a..5606a3f44 100644 --- a/typescript/frontend-marios2/src/lang/it.json +++ b/typescript/frontend-marios2/src/lang/it.json @@ -486,6 +486,11 @@ "minimumSocPercent": "SoC minimo (%)", "powerW": "Potenza (W)", "enterPowerValue": "Inserire un valore di potenza positivo o negativo", + "inverterNumber": "Numero di inverter", + "batteriesCountInInverter": "Numero di batterie nell'inverter {number}", + "maximumChargingCurrentPerBattery": "Corrente massima di carica per batteria (A)", + "maximumDischargingCurrentPerBattery": "Corrente massima di scarica per batteria (A)", + "powerPerInverterKW": "Potenza per inverter (kW)", "startDateTime": "Data e ora di inizio (Inizio < Fine)", "stopDateTime": "Data e ora di fine (Inizio < Fine)", "tourLanguageTitle": "Lingua",