improve multi-inverter configurtaion page - add inverter number and fix unit issue

This commit is contained in:
Yinyin Liu 2026-03-19 11:32:18 +01:00
parent 35938e9597
commit ce7a9d3cf2
7 changed files with 130 additions and 51 deletions

View File

@ -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<int>? BatteriesCountPerInverter { get; set; }
public double? ClusterNumber { get; set; }
public double? PvNumber { get; set; }
public bool ControlPermission { get; set; }
@ -25,7 +27,7 @@ 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}";
}
@ -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}";
}

View File

@ -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;

View File

@ -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,15 +90,21 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
const pendingConfigKey = `pendingConfig_${props.id}`;
// Helper to build form values from S3 data
const getS3Values = (): Partial<ConfigurationValues> => ({
const getS3Values = (): Partial<ConfigurationValues> => {
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,
clusterNumber: props.values.Config.ClusterNumber || 1,
PvNumber: props.values.Config.PvNumber ?? 0,
timeChargeandDischargePower: props.values.Config?.TimeChargeandDischargePower ?? 0,
startTimeChargeandDischargeDayandTime:
@ -109,7 +116,8 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
? 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) {
<Switch
name="controlPermission"
checked={Boolean(formValues.controlPermission)}
onChange={(e) =>
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) {
<div style={{ marginBottom: '5px' }}>
<TextField
label={
<FormattedMessage
id="batteriesCount "
defaultMessage="Batteries Count"
/>
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;
}
name="batteriesCount"
value={formValues.batteriesCount}
onChange={handleChange}
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
/>
</div>
{Array.from({ length: formValues.inverterNumber ?? 1 }, (_, i) => (
<div key={`battCount_${i}`} style={{ marginBottom: '5px' }}>
<TextField
label={intl.formatMessage(
{ id: 'batteriesCountInInverter' },
{ number: i + 1 }
)}
name={`batteriesCountPerInverter_${i}`}
value={formValues.batteriesCountPerInverter?.[i] ?? ''}
onChange={(e) => {
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
/>
</div>
))}
{device === 4 && (
<>
<div style={{ marginBottom: '5px' }}>
@ -489,12 +552,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
<div style={{ marginBottom: '5px' }}>
<TextField
label={
<FormattedMessage
id="maximumChargingCurrent "
defaultMessage="Maximum Charging Current"
/>
}
label={intl.formatMessage({ id: 'maximumChargingCurrentPerBattery' })}
name="maximumChargingCurrent"
value={formValues.maximumChargingCurrent}
onChange={handleChange}
@ -504,12 +562,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
<div style={{ marginBottom: '5px' }}>
<TextField
label={
<FormattedMessage
id="maximumDischargingCurrent "
defaultMessage="Maximum Discharging Current"
/>
}
label={intl.formatMessage({ id: 'maximumDischargingCurrentPerBattery' })}
name="maximumDischargingCurrent"
value={formValues.maximumDischargingCurrent}
onChange={handleChange}
@ -554,13 +607,13 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
{/* Power input*/}
<div style={{ marginBottom: '5px' }}>
<TextField
label={intl.formatMessage({ id: 'powerW' })}
label={intl.formatMessage({ id: 'powerPerInverterKW' })}
name="timeChargeandDischargePower"
value={formValues.timeChargeandDischargePower}
onChange={(e) =>
handleTimeChargeDischargeChange(e.target.name, e.target.value)
}
helperText={intl.formatMessage({ id: 'enterPowerValue' })}
helperText={intl.formatMessage({ id: 'perInverter' })}
fullWidth
/>
</div>

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",