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? MaximumDischargingCurrent { get; set; }
public double? MaximumChargingCurrent { get; set; } public double? MaximumChargingCurrent { get; set; }
public double? OperatingPriority { get; set; } public double? OperatingPriority { get; set; }
public int? InverterNumber { get; set; }
public double? BatteriesCount { get; set; } public double? BatteriesCount { get; set; }
public List<int>? BatteriesCountPerInverter { get; set; }
public double? ClusterNumber { get; set; } public double? ClusterNumber { get; set; }
public double? PvNumber { get; set; } public double? PvNumber { get; set; }
public bool ControlPermission { get; set; } public bool ControlPermission { get; set; }
@ -25,7 +27,7 @@ public class Configuration
return $"MinimumSoC: {MinimumSoC}, GridSetPoint: {GridSetPoint}, CalibrationChargeState: {CalibrationChargeState}, CalibrationChargeDate: {CalibrationChargeDate}, " + return $"MinimumSoC: {MinimumSoC}, GridSetPoint: {GridSetPoint}, CalibrationChargeState: {CalibrationChargeState}, CalibrationChargeDate: {CalibrationChargeDate}, " +
$"CalibrationDischargeState: {CalibrationDischargeState}, CalibrationDischargeDate: {CalibrationDischargeDate}, " + $"CalibrationDischargeState: {CalibrationDischargeState}, CalibrationDischargeDate: {CalibrationDischargeDate}, " +
$"MaximumDischargingCurrent: {MaximumDischargingCurrent}, MaximumChargingCurrent: {MaximumChargingCurrent}, OperatingPriority: {OperatingPriority}, " + $"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}"; $"SinexcelTimeChargeandDischargePower: {TimeChargeandDischargePower}, SinexcelStartTimeChargeandDischargeDayandTime: {StartTimeChargeandDischargeDayandTime}, SinexcelStopTimeChargeandDischargeDayandTime: {StopTimeChargeandDischargeDayandTime}";
} }
@ -45,7 +47,7 @@ public class Configuration
public string GetConfigurationSodistoreHome() public string GetConfigurationSodistoreHome()
{ {
return $"MinimumSoC: {MinimumSoC}, MaximumDischargingCurrent: {MaximumDischargingCurrent}, MaximumChargingCurrent: {MaximumChargingCurrent}, OperatingPriority: {OperatingPriority}, " + 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}"; $"SinexcelTimeChargeandDischargePower: {TimeChargeandDischargePower}, SinexcelStartTimeChargeandDischargeDayandTime: {StartTimeChargeandDischargeDayandTime}, SinexcelStopTimeChargeandDischargeDayandTime: {StopTimeChargeandDischargeDayandTime}";
} }

View File

@ -329,6 +329,8 @@ export interface JSONRecordData {
MaximumDischargingCurrent: number; MaximumDischargingCurrent: number;
OperatingPriority: string; OperatingPriority: string;
BatteriesCount: number; BatteriesCount: number;
InverterNumber?: number;
BatteriesCountPerInverter?: number[];
ClusterNumber: number; ClusterNumber: number;
PvNumber: number; PvNumber: number;
ControlPermission:boolean; ControlPermission:boolean;
@ -696,6 +698,8 @@ export type ConfigurationValues = {
maximumChargingCurrent: number; maximumChargingCurrent: number;
operatingPriority: number; operatingPriority: number;
batteriesCount: number; batteriesCount: number;
inverterNumber: number;
batteriesCountPerInverter: number[];
clusterNumber: number; clusterNumber: number;
PvNumber: number; PvNumber: number;
controlPermission:boolean; controlPermission:boolean;

View File

@ -70,6 +70,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
})); }));
}; };
const theme = useTheme(); const theme = useTheme();
const [formDirty, setFormDirty] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [updated, setUpdated] = useState(false); const [updated, setUpdated] = useState(false);
@ -89,15 +90,21 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
const pendingConfigKey = `pendingConfig_${props.id}`; const pendingConfigKey = `pendingConfig_${props.id}`;
// Helper to build form values from S3 data // 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, minimumSoC: props.values.Config.MinSoc,
maximumDischargingCurrent: props.values.Config.MaximumChargingCurrent, maximumDischargingCurrent: props.values.Config.MaximumChargingCurrent,
maximumChargingCurrent: props.values.Config.MaximumDischargingCurrent, maximumChargingCurrent: props.values.Config.MaximumDischargingCurrent,
operatingPriority: resolveOperatingPriorityIndex( operatingPriority: resolveOperatingPriorityIndex(
props.values.Config.OperatingPriority props.values.Config.OperatingPriority
), ),
inverterNumber: inverterNum,
batteriesCountPerInverter: batteriesPerInverter,
batteriesCount: props.values.Config.BatteriesCount, batteriesCount: props.values.Config.BatteriesCount,
clusterNumber: props.values.Config.ClusterNumber ?? 1, clusterNumber: props.values.Config.ClusterNumber || 1,
PvNumber: props.values.Config.PvNumber ?? 0, PvNumber: props.values.Config.PvNumber ?? 0,
timeChargeandDischargePower: props.values.Config?.TimeChargeandDischargePower ?? 0, timeChargeandDischargePower: props.values.Config?.TimeChargeandDischargePower ?? 0,
startTimeChargeandDischargeDayandTime: startTimeChargeandDischargeDayandTime:
@ -109,7 +116,8 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
? dayjs(props.values.Config.StopTimeChargeandDischargeDayandTime).toDate() ? dayjs(props.values.Config.StopTimeChargeandDischargeDayandTime).toDate()
: null, : null,
controlPermission: String(props.values.Config.ControlPermission).toLowerCase() === "true", controlPermission: String(props.values.Config.ControlPermission).toLowerCase() === "true",
}); };
};
// Restore pending config from localStorage, converting date strings back to Date objects. // Restore pending config from localStorage, converting date strings back to Date objects.
// Returns { values, s3ConfigSnapshot } or null if no pending config. // 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. // 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 // 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. // submit time, the device has uploaded new data — trust S3 regardless of values.
// Skip reset if the user is actively editing (formDirty).
useEffect(() => { useEffect(() => {
if (formDirty) return;
const s3Values = getS3Values(); const s3Values = getS3Values();
const pending = restorePendingConfig(); const pending = restorePendingConfig();
@ -192,6 +203,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
}, [props.values]); }, [props.values]);
const handleOperatingPriorityChange = (event) => { const handleOperatingPriorityChange = (event) => {
setFormDirty(true);
setFormValues({ setFormValues({
...formValues, ...formValues,
['operatingPriority']: OperatingPriorityOptions.indexOf( ['operatingPriority']: OperatingPriorityOptions.indexOf(
@ -230,7 +242,9 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
maximumDischargingCurrent: formValues.maximumDischargingCurrent, maximumDischargingCurrent: formValues.maximumDischargingCurrent,
maximumChargingCurrent: formValues.maximumChargingCurrent, maximumChargingCurrent: formValues.maximumChargingCurrent,
operatingPriority: formValues.operatingPriority, operatingPriority: formValues.operatingPriority,
batteriesCount:formValues.batteriesCount, inverterNumber: formValues.inverterNumber,
batteriesCountPerInverter: formValues.batteriesCountPerInverter,
batteriesCount: formValues.batteriesCountPerInverter?.[0] ?? formValues.batteriesCount,
clusterNumber:formValues.clusterNumber, clusterNumber:formValues.clusterNumber,
PvNumber:formValues.PvNumber, PvNumber:formValues.PvNumber,
timeChargeandDischargePower: formValues.timeChargeandDischargePower, timeChargeandDischargePower: formValues.timeChargeandDischargePower,
@ -259,6 +273,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
if (res) { if (res) {
setUpdated(true); setUpdated(true);
setLoading(false); setLoading(false);
setFormDirty(false);
// Save submitted values + S3 snapshot to localStorage for optimistic UI update. // Save submitted values + S3 snapshot to localStorage for optimistic UI update.
// s3ConfigSnapshot = fingerprint of S3 Config at submit time. // s3ConfigSnapshot = fingerprint of S3 Config at submit time.
@ -273,6 +288,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
}; };
const handleChange = (e) => { const handleChange = (e) => {
setFormDirty(true);
const { name, value } = e.target; const { name, value } = e.target;
if (name === 'minimumSoC') { if (name === 'minimumSoC') {
@ -305,6 +321,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
}; };
const handleTimeChargeDischargeChange = (name: string, value: any) => { const handleTimeChargeDischargeChange = (name: string, value: any) => {
setFormDirty(true);
setFormValues((prev) => ({ setFormValues((prev) => ({
...prev, ...prev,
[name]: value [name]: value
@ -384,11 +401,13 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
<Switch <Switch
name="controlPermission" name="controlPermission"
checked={Boolean(formValues.controlPermission)} checked={Boolean(formValues.controlPermission)}
onChange={(e) => onChange={(e) => {
setFormDirty(true);
setFormValues((prev) => ({ setFormValues((prev) => ({
...prev, ...prev,
controlPermission: e.target.checked, controlPermission: e.target.checked,
})) }));
}
} }
sx={{ transform: "scale(1.4)", marginLeft: "15px" }} sx={{ transform: "scale(1.4)", marginLeft: "15px" }}
/> />
@ -405,19 +424,63 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
<div style={{ marginBottom: '5px' }}> <div style={{ marginBottom: '5px' }}>
<TextField <TextField
label={ label={intl.formatMessage({ id: 'inverterNumber' })}
<FormattedMessage name="inverterNumber"
id="batteriesCount " value={formValues.inverterNumber ?? ''}
defaultMessage="Batteries Count" onChange={(e) => {
/> setFormDirty(true);
const raw = e.target.value;
if (raw === '') {
setFormValues((prev) => ({ ...prev, inverterNumber: '' as any }));
return;
} }
name="batteriesCount" const parsed = parseInt(raw);
value={formValues.batteriesCount} if (isNaN(parsed) || parsed < 1) return;
onChange={handleChange} const currentArr = formValues.batteriesCountPerInverter || [1];
const newArr = Array.from({ length: parsed }, (_, i) => currentArr[i] ?? 1);
setFormValues((prev) => ({
...prev,
inverterNumber: parsed,
batteriesCountPerInverter: newArr,
}));
}}
fullWidth fullWidth
/> />
</div> </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 && ( {device === 4 && (
<> <>
<div style={{ marginBottom: '5px' }}> <div style={{ marginBottom: '5px' }}>
@ -489,12 +552,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
<div style={{ marginBottom: '5px' }}> <div style={{ marginBottom: '5px' }}>
<TextField <TextField
label={ label={intl.formatMessage({ id: 'maximumChargingCurrentPerBattery' })}
<FormattedMessage
id="maximumChargingCurrent "
defaultMessage="Maximum Charging Current"
/>
}
name="maximumChargingCurrent" name="maximumChargingCurrent"
value={formValues.maximumChargingCurrent} value={formValues.maximumChargingCurrent}
onChange={handleChange} onChange={handleChange}
@ -504,12 +562,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
<div style={{ marginBottom: '5px' }}> <div style={{ marginBottom: '5px' }}>
<TextField <TextField
label={ label={intl.formatMessage({ id: 'maximumDischargingCurrentPerBattery' })}
<FormattedMessage
id="maximumDischargingCurrent "
defaultMessage="Maximum Discharging Current"
/>
}
name="maximumDischargingCurrent" name="maximumDischargingCurrent"
value={formValues.maximumDischargingCurrent} value={formValues.maximumDischargingCurrent}
onChange={handleChange} onChange={handleChange}
@ -554,13 +607,13 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
{/* Power input*/} {/* Power input*/}
<div style={{ marginBottom: '5px' }}> <div style={{ marginBottom: '5px' }}>
<TextField <TextField
label={intl.formatMessage({ id: 'powerW' })} label={intl.formatMessage({ id: 'powerPerInverterKW' })}
name="timeChargeandDischargePower" name="timeChargeandDischargePower"
value={formValues.timeChargeandDischargePower} value={formValues.timeChargeandDischargePower}
onChange={(e) => onChange={(e) =>
handleTimeChargeDischargeChange(e.target.name, e.target.value) handleTimeChargeDischargeChange(e.target.name, e.target.value)
} }
helperText={intl.formatMessage({ id: 'enterPowerValue' })} helperText={intl.formatMessage({ id: 'perInverter' })}
fullWidth fullWidth
/> />
</div> </div>

View File

@ -486,6 +486,11 @@
"minimumSocPercent": "Minimaler Ladezustand (%)", "minimumSocPercent": "Minimaler Ladezustand (%)",
"powerW": "Leistung (W)", "powerW": "Leistung (W)",
"enterPowerValue": "Positiven oder negativen Leistungswert eingeben", "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)", "startDateTime": "Startdatum und -zeit (Startzeit < Stoppzeit)",
"stopDateTime": "Stoppdatum und -zeit (Startzeit < Stoppzeit)", "stopDateTime": "Stoppdatum und -zeit (Startzeit < Stoppzeit)",
"tourLanguageTitle": "Sprache", "tourLanguageTitle": "Sprache",

View File

@ -234,6 +234,11 @@
"minimumSocPercent": "Minimum SoC (%)", "minimumSocPercent": "Minimum SoC (%)",
"powerW": "Power (W)", "powerW": "Power (W)",
"enterPowerValue": "Enter a positive or negative power value", "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)", "startDateTime": "Start Date and Time (Start Time < Stop Time)",
"stopDateTime": "Stop Date and Time (Start Time < Stop Time)", "stopDateTime": "Stop Date and Time (Start Time < Stop Time)",
"tourLanguageTitle": "Language", "tourLanguageTitle": "Language",

View File

@ -486,6 +486,11 @@
"minimumSocPercent": "SoC minimum (%)", "minimumSocPercent": "SoC minimum (%)",
"powerW": "Puissance (W)", "powerW": "Puissance (W)",
"enterPowerValue": "Entrez une valeur de puissance positive ou négative", "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)", "startDateTime": "Date et heure de début (Début < Fin)",
"stopDateTime": "Date et heure de fin (Début < Fin)", "stopDateTime": "Date et heure de fin (Début < Fin)",
"tourLanguageTitle": "Langue", "tourLanguageTitle": "Langue",

View File

@ -486,6 +486,11 @@
"minimumSocPercent": "SoC minimo (%)", "minimumSocPercent": "SoC minimo (%)",
"powerW": "Potenza (W)", "powerW": "Potenza (W)",
"enterPowerValue": "Inserire un valore di potenza positivo o negativo", "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)", "startDateTime": "Data e ora di inizio (Inizio < Fine)",
"stopDateTime": "Data e ora di fine (Inizio < Fine)", "stopDateTime": "Data e ora di fine (Inizio < Fine)",
"tourLanguageTitle": "Lingua", "tourLanguageTitle": "Lingua",