new updates again
This commit is contained in:
parent
2889d4c281
commit
be8c7d69b2
|
|
@ -11,6 +11,9 @@ public class Configuration
|
||||||
|
|
||||||
public double? MaximumDischargingCurrent { get; set; }
|
public double? MaximumDischargingCurrent { get; set; }
|
||||||
public double? MaximumChargingCurrent { get; set; }
|
public double? MaximumChargingCurrent { get; set; }
|
||||||
|
// Nested per-inverter / per-cluster topology + limits (Sinexcel).
|
||||||
|
// Keys: "Inverter1".."InverterN" → { Clusters: { "Cluster1".. }, PvCount }
|
||||||
|
public Dictionary<string, InverterConfig>? Inverters { get; set; }
|
||||||
public double? OperatingPriority { get; set; }
|
public double? OperatingPriority { get; set; }
|
||||||
public int? InverterNumber { get; set; }
|
public int? InverterNumber { get; set; }
|
||||||
public double? BatteriesCount { get; set; }
|
public double? BatteriesCount { get; set; }
|
||||||
|
|
@ -78,3 +81,16 @@ public enum CalibrationChargeType
|
||||||
AdditionallyOnce,
|
AdditionallyOnce,
|
||||||
ChargePermanently
|
ChargePermanently
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class InverterConfig
|
||||||
|
{
|
||||||
|
public Dictionary<string, ClusterConfig> Clusters { get; set; } = new();
|
||||||
|
public int PvCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ClusterConfig
|
||||||
|
{
|
||||||
|
public int BatteryCount { get; set; }
|
||||||
|
public double MaxChargingCurrent { get; set; }
|
||||||
|
public double MaxDischargingCurrent { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -685,6 +685,17 @@ export interface I_BoxDataValue {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ClusterConfig = {
|
||||||
|
BatteryCount: number;
|
||||||
|
MaxChargingCurrent: number;
|
||||||
|
MaxDischargingCurrent: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InverterConfig = {
|
||||||
|
Clusters: { [clusterKey: string]: ClusterConfig };
|
||||||
|
PvCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ConfigurationValues = {
|
export type ConfigurationValues = {
|
||||||
minimumSoC: string | number;
|
minimumSoC: string | number;
|
||||||
gridSetPoint: number;
|
gridSetPoint: number;
|
||||||
|
|
@ -696,6 +707,9 @@ export type ConfigurationValues = {
|
||||||
//For sodistoreHome
|
//For sodistoreHome
|
||||||
maximumDischargingCurrent: number;
|
maximumDischargingCurrent: number;
|
||||||
maximumChargingCurrent: number;
|
maximumChargingCurrent: number;
|
||||||
|
// Nested per-inverter / per-cluster topology + limits (Sinexcel).
|
||||||
|
// Keys: "Inverter1".."InverterN" → { Clusters: { "Cluster1".. }, PvCount }
|
||||||
|
inverters?: { [inverterKey: string]: InverterConfig };
|
||||||
operatingPriority: number;
|
operatingPriority: number;
|
||||||
batteriesCount: number;
|
batteriesCount: number;
|
||||||
inverterNumber: number;
|
inverterNumber: number;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
import { ConfigurationValues, JSONRecordData } from '../Log/graph.util';
|
import { ConfigurationValues, InverterConfig, JSONRecordData } from '../Log/graph.util';
|
||||||
import {
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
Alert,
|
Alert,
|
||||||
Box,
|
Box,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
Chip,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Container,
|
Container,
|
||||||
Divider,
|
Divider,
|
||||||
|
|
@ -17,6 +21,7 @@ import {
|
||||||
Typography,
|
Typography,
|
||||||
useTheme
|
useTheme
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
|
||||||
import React, { useContext, useState, useEffect } from 'react';
|
import React, { useContext, useState, useEffect } from 'react';
|
||||||
import { FormattedMessage, useIntl } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
@ -30,7 +35,9 @@ import { I_Installation } from 'src/interfaces/InstallationTypes';
|
||||||
import {
|
import {
|
||||||
buildSodistoreProPreset,
|
buildSodistoreProPreset,
|
||||||
getPresetsForDevice,
|
getPresetsForDevice,
|
||||||
PresetConfig
|
parseBatterySnTree,
|
||||||
|
PresetConfig,
|
||||||
|
BatterySnTree
|
||||||
} from '../Information/installationSetupUtils';
|
} from '../Information/installationSetupUtils';
|
||||||
import { LocalizationProvider } from '@mui/x-date-pickers';
|
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
|
|
@ -104,15 +111,67 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
||||||
// Storage key for pending config (optimistic update)
|
// Storage key for pending config (optimistic update)
|
||||||
const pendingConfigKey = `pendingConfig_${props.id}`;
|
const pendingConfigKey = `pendingConfig_${props.id}`;
|
||||||
|
|
||||||
|
// Hardware topology — derived from Information tab (single source of truth).
|
||||||
|
const isSodistorePro = product === 5;
|
||||||
|
const installationModel = props.installation.installationModel;
|
||||||
|
const presetConfig: PresetConfig | null = isSodistorePro
|
||||||
|
? (installationModel && parseInt(installationModel, 10) > 0
|
||||||
|
? buildSodistoreProPreset(parseInt(installationModel, 10))
|
||||||
|
: null)
|
||||||
|
: (getPresetsForDevice(device)[installationModel] || null);
|
||||||
|
const inverterCount = presetConfig?.length ?? 1;
|
||||||
|
|
||||||
|
// Build the nested Inverters config from topology (presetConfig).
|
||||||
|
// Used as fallback when the device hasn't yet written the structured Inverters object.
|
||||||
|
const buildInvertersFromPreset = (
|
||||||
|
chargeScalar: number | undefined,
|
||||||
|
dischargeScalar: number | undefined,
|
||||||
|
): { [k: string]: InverterConfig } => {
|
||||||
|
if (!presetConfig) return {};
|
||||||
|
const out: { [k: string]: InverterConfig } = {};
|
||||||
|
presetConfig.forEach((clusters, invIdx) => {
|
||||||
|
const clObj: { [k: string]: any } = {};
|
||||||
|
clusters.forEach((batteryCount, clIdx) => {
|
||||||
|
clObj[`Cluster${clIdx + 1}`] = {
|
||||||
|
BatteryCount: batteryCount,
|
||||||
|
MaxChargingCurrent: chargeScalar ?? 0,
|
||||||
|
MaxDischargingCurrent: dischargeScalar ?? 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
out[`Inverter${invIdx + 1}`] = { Clusters: clObj, PvCount: 0 };
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
// 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 inverterNum = props.values.Config.InverterNumber ?? 1;
|
||||||
const batteriesPerInverter: number[] = props.values.Config.BatteriesCountPerInverter
|
const batteriesPerInverter: number[] = props.values.Config.BatteriesCountPerInverter
|
||||||
?? Array(inverterNum).fill(props.values.Config.BatteriesCount || 1);
|
?? Array(inverterNum).fill(props.values.Config.BatteriesCount || 1);
|
||||||
|
// Read per-inverter Clusters/PvCount from each Devices.InverterN entry on disk.
|
||||||
|
const cfgDevices = (props.values.Config as any).Devices as { [k: string]: any } | undefined;
|
||||||
|
const cfgInverters: { [k: string]: InverterConfig } | undefined = cfgDevices
|
||||||
|
? Object.fromEntries(
|
||||||
|
Object.entries(cfgDevices)
|
||||||
|
.filter(([k, v]: [string, any]) => k.startsWith('Inverter') && (v?.Clusters || v?.PvCount != null))
|
||||||
|
.map(([k, v]: [string, any]) => [k, { Clusters: v.Clusters ?? {}, PvCount: v.PvCount ?? 0 }])
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
const hasInverterData = cfgInverters && Object.keys(cfgInverters).length > 0;
|
||||||
return {
|
return {
|
||||||
minimumSoC: props.values.Config.MinSoc,
|
minimumSoC: props.values.Config.MinSoc,
|
||||||
maximumDischargingCurrent: props.values.Config.MaximumDischargingCurrent,
|
maximumDischargingCurrent: props.values.Config.MaximumDischargingCurrent,
|
||||||
maximumChargingCurrent: props.values.Config.MaximumChargingCurrent,
|
maximumChargingCurrent: props.values.Config.MaximumChargingCurrent,
|
||||||
|
// Always overlay Information-tab topology so battery counts and PV count
|
||||||
|
// reflect what's actually installed (Information tab is source of truth).
|
||||||
|
inverters: overlayTopology(
|
||||||
|
hasInverterData
|
||||||
|
? cfgInverters
|
||||||
|
: buildInvertersFromPreset(
|
||||||
|
props.values.Config.MaximumChargingCurrent,
|
||||||
|
props.values.Config.MaximumDischargingCurrent,
|
||||||
|
)
|
||||||
|
),
|
||||||
operatingPriority: resolveOperatingPriorityIndex(
|
operatingPriority: resolveOperatingPriorityIndex(
|
||||||
props.values.Config.OperatingPriority
|
props.values.Config.OperatingPriority
|
||||||
),
|
),
|
||||||
|
|
@ -179,6 +238,40 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
||||||
// Fingerprint S3 Config for change detection (not value comparison)
|
// Fingerprint S3 Config for change detection (not value comparison)
|
||||||
const getS3ConfigFingerprint = () => JSON.stringify(props.values.Config);
|
const getS3ConfigFingerprint = () => JSON.stringify(props.values.Config);
|
||||||
|
|
||||||
|
// Overlay Information-tab-derived topology onto a `inverters` config object.
|
||||||
|
// Battery counts come from the SN tree (filled SNs); PvCount comes from pvStringsPerInverter.
|
||||||
|
// Existing per-cluster current limits are preserved.
|
||||||
|
const overlayTopology = (
|
||||||
|
inverters: { [k: string]: InverterConfig } | undefined
|
||||||
|
): { [k: string]: InverterConfig } | undefined => {
|
||||||
|
if (!presetConfig) return inverters;
|
||||||
|
const tree = parseBatterySnTree(props.installation.batterySerialNumbers || '', presetConfig);
|
||||||
|
const pvStrings = (props.installation.pvStringsPerInverter || '')
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim());
|
||||||
|
const out: { [k: string]: InverterConfig } = {};
|
||||||
|
presetConfig.forEach((clusters, invIdx) => {
|
||||||
|
const invKey = `Inverter${invIdx + 1}`;
|
||||||
|
const existingInv = inverters?.[invKey];
|
||||||
|
const cls: { [k: string]: any } = {};
|
||||||
|
clusters.forEach((_slotCount, clIdx) => {
|
||||||
|
const clKey = `Cluster${clIdx + 1}`;
|
||||||
|
const filled = (tree[invIdx]?.[clIdx] ?? []).filter((s) => s !== '').length;
|
||||||
|
const existingCl = existingInv?.Clusters?.[clKey];
|
||||||
|
cls[clKey] = {
|
||||||
|
BatteryCount: filled,
|
||||||
|
MaxChargingCurrent: existingCl?.MaxChargingCurrent ?? 0,
|
||||||
|
MaxDischargingCurrent: existingCl?.MaxDischargingCurrent ?? 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
out[invKey] = {
|
||||||
|
Clusters: cls,
|
||||||
|
PvCount: parseInt(pvStrings[invIdx] || '0', 10) || 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize form from localStorage (if pending submit exists) or from S3
|
// Initialize form from localStorage (if pending submit exists) or from S3
|
||||||
// This runs in the useState initializer so the component never renders stale values
|
// This runs in the useState initializer so the component never renders stale values
|
||||||
const [formValues, setFormValues] = useState<Partial<ConfigurationValues>>(() => {
|
const [formValues, setFormValues] = useState<Partial<ConfigurationValues>>(() => {
|
||||||
|
|
@ -266,17 +359,23 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
||||||
if (!validateTimeOnly()) {
|
if (!validateTimeOnly()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Re-overlay Information-tab topology at submit time, so battery counts and PV count
|
||||||
|
// are always the latest from the Information tab (it's the source of truth).
|
||||||
|
const inverters = overlayTopology(formValues.inverters);
|
||||||
|
// Pull the first cluster's value as a legacy single-scalar fallback for older firmware.
|
||||||
|
const firstInvKey = inverters ? Object.keys(inverters)[0] : undefined;
|
||||||
|
const firstClusterKey = firstInvKey && inverters
|
||||||
|
? Object.keys(inverters[firstInvKey].Clusters)[0]
|
||||||
|
: undefined;
|
||||||
|
const firstCluster = firstInvKey && firstClusterKey && inverters
|
||||||
|
? inverters[firstInvKey].Clusters[firstClusterKey]
|
||||||
|
: undefined;
|
||||||
const configurationToSend: Partial<ConfigurationValues> = {
|
const configurationToSend: Partial<ConfigurationValues> = {
|
||||||
minimumSoC: formValues.minimumSoC,
|
minimumSoC: formValues.minimumSoC,
|
||||||
maximumDischargingCurrent: formValues.maximumDischargingCurrent,
|
maximumDischargingCurrent: firstCluster?.MaxDischargingCurrent ?? formValues.maximumDischargingCurrent,
|
||||||
maximumChargingCurrent: formValues.maximumChargingCurrent,
|
maximumChargingCurrent: firstCluster?.MaxChargingCurrent ?? formValues.maximumChargingCurrent,
|
||||||
|
inverters,
|
||||||
operatingPriority: formValues.operatingPriority,
|
operatingPriority: formValues.operatingPriority,
|
||||||
inverterNumber: formValues.inverterNumber,
|
|
||||||
batteriesCountPerInverter: formValues.batteriesCountPerInverter,
|
|
||||||
batteriesCount: formValues.batteriesCountPerInverter?.[0] ?? formValues.batteriesCount,
|
|
||||||
clusterNumber:formValues.clusterNumber,
|
|
||||||
PvNumber:formValues.PvNumber,
|
|
||||||
pvCountPerInverter: formValues.pvCountPerInverter,
|
|
||||||
timeChargeandDischargePower: formValues.timeChargeandDischargePower,
|
timeChargeandDischargePower: formValues.timeChargeandDischargePower,
|
||||||
startTimeChargeandDischargeDayandTime: formValues.startTimeChargeandDischargeDayandTime
|
startTimeChargeandDischargeDayandTime: formValues.startTimeChargeandDischargeDayandTime
|
||||||
? new Date(formValues.startTimeChargeandDischargeDayandTime.getTime() - formValues.startTimeChargeandDischargeDayandTime.getTimezoneOffset() * 60000)
|
? new Date(formValues.startTimeChargeandDischargeDayandTime.getTime() - formValues.startTimeChargeandDischargeDayandTime.getTimezoneOffset() * 60000)
|
||||||
|
|
@ -470,15 +569,15 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{device === 4 && (() => {
|
{device === 4 && (() => {
|
||||||
// Mirror Information tab's preset derivation so both views always agree.
|
// Read the SN tree from the Information tab data (single source of truth).
|
||||||
const isSodistorePro = product === 5;
|
// Filled batteries per cluster = entries with a non-empty serial number.
|
||||||
const model = props.installation.installationModel;
|
const tree: BatterySnTree | null = presetConfig
|
||||||
const presetConfig: PresetConfig | null = isSodistorePro
|
? parseBatterySnTree(props.installation.batterySerialNumbers || '', presetConfig)
|
||||||
? (model && parseInt(model, 10) > 0
|
: null;
|
||||||
? buildSodistoreProPreset(parseInt(model, 10))
|
// PV strings per inverter — comma-separated string from Information tab.
|
||||||
: null)
|
const pvStrings = (props.installation.pvStringsPerInverter || '')
|
||||||
: (getPresetsForDevice(device)[model] || null);
|
.split(',')
|
||||||
const inverterCount = presetConfig?.length ?? 1;
|
.map((s) => s.trim());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -492,21 +591,70 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{Array.from({ length: inverterCount }, (_, i) => {
|
{Array.from({ length: inverterCount }, (_, i) => {
|
||||||
const batteriesInInverter = presetConfig
|
const clusters = presetConfig?.[i] ?? [];
|
||||||
? (presetConfig[i] ?? []).reduce((a, b) => a + b, 0)
|
const treeForInverter = tree?.[i] ?? [];
|
||||||
: 0;
|
const filledBat = treeForInverter.flat().filter((s) => s !== '').length;
|
||||||
|
const totalBat = clusters.reduce((a, b) => a + b, 0);
|
||||||
return (
|
return (
|
||||||
<div key={`battCount_${i}`} style={{ marginBottom: '5px' }}>
|
<Accordion
|
||||||
<TextField
|
key={`inv-${i}`}
|
||||||
label={intl.formatMessage(
|
defaultExpanded={false}
|
||||||
{ id: 'batteriesCountInInverter' },
|
sx={{ ml: 1, mr: 1, mt: 1 }}
|
||||||
{ number: i + 1 }
|
>
|
||||||
)}
|
<AccordionSummary
|
||||||
value={batteriesInInverter}
|
expandIcon={<ExpandMoreIcon />}
|
||||||
InputProps={{ readOnly: true }}
|
sx={{
|
||||||
fullWidth
|
'& .MuiAccordionSummary-content': { flexGrow: 0, justifyContent: 'flex-start' },
|
||||||
/>
|
justifyContent: 'flex-start',
|
||||||
</div>
|
'& .MuiAccordionSummary-expandIconWrapper': { ml: 1 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ fontWeight: 'bold' }}>
|
||||||
|
<FormattedMessage id="inverterN" defaultMessage="Inverter {n}" values={{ n: i + 1 }} />
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
label={`${filledBat} ${intl.formatMessage({ id: 'batteries', defaultMessage: 'batteries' })}`}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
/>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
{clusters.map((_slotCount, clIdx) => {
|
||||||
|
const filledInCluster = (treeForInverter[clIdx] ?? [])
|
||||||
|
.filter((s) => s !== '').length;
|
||||||
|
return (
|
||||||
|
<div key={`cl-${i}-${clIdx}`} style={{ marginBottom: '5px' }}>
|
||||||
|
<TextField
|
||||||
|
label={intl.formatMessage(
|
||||||
|
{ id: 'batteryNumberInClusterN', defaultMessage: 'Battery Number in Cluster {n}' },
|
||||||
|
{ n: clIdx + 1 }
|
||||||
|
)}
|
||||||
|
value={filledInCluster}
|
||||||
|
InputProps={{ readOnly: true }}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{(() => {
|
||||||
|
const pvCount = parseInt(pvStrings[i] || '0', 10) || 0;
|
||||||
|
return (
|
||||||
|
<div key={`pv-${i}`} style={{ marginBottom: '5px' }}>
|
||||||
|
<TextField
|
||||||
|
label={intl.formatMessage(
|
||||||
|
{ id: 'pvStringsNumberInInverterN', defaultMessage: 'PV Strings Number in Inverter {n}' },
|
||||||
|
{ n: i + 1 }
|
||||||
|
)}
|
||||||
|
value={pvCount}
|
||||||
|
InputProps={{ readOnly: true }}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
|
|
@ -556,25 +704,106 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: '5px' }}>
|
{device === 4 ? (
|
||||||
<TextField
|
// Per-cluster, per-inverter charging/discharging current limits — nested config.
|
||||||
label={intl.formatMessage({ id: 'maximumChargingCurrentPerBattery' })}
|
Array.from({ length: inverterCount }, (_, invIdx) => {
|
||||||
name="maximumChargingCurrent"
|
const invKey = `Inverter${invIdx + 1}`;
|
||||||
value={formValues.maximumChargingCurrent}
|
const clusters = presetConfig?.[invIdx] ?? [0];
|
||||||
onChange={handleChange}
|
return (
|
||||||
fullWidth
|
<Accordion
|
||||||
/>
|
key={`limits-${invKey}`}
|
||||||
</div>
|
defaultExpanded={false}
|
||||||
|
sx={{ ml: 1, mr: 1, mt: 1 }}
|
||||||
|
>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon />}
|
||||||
|
sx={{
|
||||||
|
'& .MuiAccordionSummary-content': { flexGrow: 0, justifyContent: 'flex-start' },
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
'& .MuiAccordionSummary-expandIconWrapper': { ml: 1 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ fontWeight: 'bold' }}>
|
||||||
|
<FormattedMessage id="inverterN" defaultMessage="Inverter {n}" values={{ n: invIdx + 1 }} />
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
{clusters.map((_slotCount, clIdx) => {
|
||||||
|
const clKey = `Cluster${clIdx + 1}`;
|
||||||
|
const cluster = formValues.inverters?.[invKey]?.Clusters?.[clKey];
|
||||||
|
const charge = cluster?.MaxChargingCurrent ?? '';
|
||||||
|
const discharge = cluster?.MaxDischargingCurrent ?? '';
|
||||||
|
const setClusterField = (
|
||||||
|
field: 'MaxChargingCurrent' | 'MaxDischargingCurrent',
|
||||||
|
v: string
|
||||||
|
) => {
|
||||||
|
if (v !== '' && !/^\d*\.?\d*$/.test(v)) return;
|
||||||
|
setFormDirty(true);
|
||||||
|
setFormValues((prev) => {
|
||||||
|
const inverters = { ...(prev.inverters ?? {}) };
|
||||||
|
const inv = inverters[invKey]
|
||||||
|
?? { Clusters: {}, PvCount: 0 };
|
||||||
|
const cls = { ...(inv.Clusters ?? {}) };
|
||||||
|
const existing = cls[clKey] ?? {
|
||||||
|
BatteryCount: presetConfig?.[invIdx]?.[clIdx] ?? 0,
|
||||||
|
MaxChargingCurrent: 0,
|
||||||
|
MaxDischargingCurrent: 0,
|
||||||
|
};
|
||||||
|
cls[clKey] = {
|
||||||
|
...existing,
|
||||||
|
[field]: v === '' ? 0 : parseFloat(v),
|
||||||
|
};
|
||||||
|
inverters[invKey] = { ...inv, Clusters: cls };
|
||||||
|
return { ...prev, inverters };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div key={`cl-limits-${invKey}-${clKey}`} style={{ marginBottom: '5px' }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ mt: 1, ml: 1 }}>
|
||||||
|
<FormattedMessage id="clusterN" defaultMessage="Cluster {n}" values={{ n: clIdx + 1 }} />
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
label={intl.formatMessage({ id: 'maximumChargingCurrentPerClusterLabel' })}
|
||||||
|
value={charge}
|
||||||
|
onChange={(e) => setClusterField('MaxChargingCurrent', e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label={intl.formatMessage({ id: 'maximumDischargingCurrentPerClusterLabel' })}
|
||||||
|
value={discharge}
|
||||||
|
onChange={(e) => setClusterField('MaxDischargingCurrent', e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style={{ marginBottom: '5px' }}>
|
||||||
|
<TextField
|
||||||
|
label={intl.formatMessage({ id: 'maximumChargingCurrentPerBattery' })}
|
||||||
|
name="maximumChargingCurrent"
|
||||||
|
value={formValues.maximumChargingCurrent}
|
||||||
|
onChange={handleChange}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: '5px' }}>
|
<div style={{ marginBottom: '5px' }}>
|
||||||
<TextField
|
<TextField
|
||||||
label={intl.formatMessage({ id: 'maximumDischargingCurrentPerBattery' })}
|
label={intl.formatMessage({ id: 'maximumDischargingCurrentPerBattery' })}
|
||||||
name="maximumDischargingCurrent"
|
name="maximumDischargingCurrent"
|
||||||
value={formValues.maximumDischargingCurrent}
|
value={formValues.maximumDischargingCurrent}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{device === 4 && (
|
{device === 4 && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -802,16 +1031,9 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
||||||
<div style={{ marginBottom: '5px' }}>
|
<div style={{ marginBottom: '5px' }}>
|
||||||
<TextField
|
<TextField
|
||||||
label={intl.formatMessage({ id: 'currentPrice' })}
|
label={intl.formatMessage({ id: 'currentPrice' })}
|
||||||
name="currentPrice"
|
value={(props.values.Config as any).CurrentPrice?.toString() ?? ''}
|
||||||
value={formValues.currentPrice ?? ''}
|
|
||||||
onChange={(e) => {
|
|
||||||
const v = e.target.value;
|
|
||||||
if (v === '' || /^\d*\.?\d*$/.test(v)) {
|
|
||||||
setFormDirty(true);
|
|
||||||
setFormValues((prev) => ({ ...prev, currentPrice: v }));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
InputProps={{
|
InputProps={{
|
||||||
|
readOnly: true,
|
||||||
endAdornment: <InputAdornment position="end">CHF/kWh</InputAdornment>,
|
endAdornment: <InputAdornment position="end">CHF/kWh</InputAdornment>,
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|
|
||||||
|
|
@ -521,7 +521,12 @@
|
||||||
"enterPowerValue": "Positiven oder negativen Leistungswert eingeben",
|
"enterPowerValue": "Positiven oder negativen Leistungswert eingeben",
|
||||||
"inverterNumber": "Anzahl Wechselrichter",
|
"inverterNumber": "Anzahl Wechselrichter",
|
||||||
"batteriesCountInInverter": "Batterieanzahl in Wechselrichter {number}",
|
"batteriesCountInInverter": "Batterieanzahl in Wechselrichter {number}",
|
||||||
|
"batteryNumberInClusterN": "Batterieanzahl in Cluster {n}",
|
||||||
|
"pvStringsNumberInInverterN": "Anzahl PV-Strings in Wechselrichter {n}",
|
||||||
|
"batteries": "Batterien",
|
||||||
"maximumChargingCurrentPerBattery": "Maximaler Ladestrom pro Batterie (A)",
|
"maximumChargingCurrentPerBattery": "Maximaler Ladestrom pro Batterie (A)",
|
||||||
|
"maximumChargingCurrentPerClusterLabel": "Maximaler Ladestrom pro Cluster (A)",
|
||||||
|
"maximumDischargingCurrentPerClusterLabel": "Maximaler Entladestrom pro Cluster (A)",
|
||||||
"maximumDischargingCurrentPerBattery": "Maximaler Entladestrom pro Batterie (A)",
|
"maximumDischargingCurrentPerBattery": "Maximaler Entladestrom pro Batterie (A)",
|
||||||
"powerPerInverterKW": "Leistung pro Wechselrichter (kW)",
|
"powerPerInverterKW": "Leistung pro Wechselrichter (kW)",
|
||||||
"startDateTime": "Startdatum und -zeit (Startzeit < Stoppzeit)",
|
"startDateTime": "Startdatum und -zeit (Startzeit < Stoppzeit)",
|
||||||
|
|
|
||||||
|
|
@ -269,8 +269,13 @@
|
||||||
"enterPowerValue": "Enter a positive or negative power value",
|
"enterPowerValue": "Enter a positive or negative power value",
|
||||||
"inverterNumber": "Inverter Number",
|
"inverterNumber": "Inverter Number",
|
||||||
"batteriesCountInInverter": "Batteries Count in Inverter {number}",
|
"batteriesCountInInverter": "Batteries Count in Inverter {number}",
|
||||||
|
"batteryNumberInClusterN": "Battery Number in Cluster {n}",
|
||||||
|
"pvStringsNumberInInverterN": "PV Strings Number in Inverter {n}",
|
||||||
|
"batteries": "batteries",
|
||||||
"maximumChargingCurrentPerBattery": "Maximum Charging Current per Battery (A)",
|
"maximumChargingCurrentPerBattery": "Maximum Charging Current per Battery (A)",
|
||||||
"maximumDischargingCurrentPerBattery": "Maximum Discharging Current per Battery (A)",
|
"maximumDischargingCurrentPerBattery": "Maximum Discharging Current per Battery (A)",
|
||||||
|
"maximumChargingCurrentPerClusterLabel": "Maximum Charging Current per Cluster (A)",
|
||||||
|
"maximumDischargingCurrentPerClusterLabel": "Maximum Discharging Current per Cluster (A)",
|
||||||
"powerPerInverterKW": "Power per Inverter (kW)",
|
"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)",
|
||||||
|
|
|
||||||
|
|
@ -521,7 +521,12 @@
|
||||||
"enterPowerValue": "Entrez une valeur de puissance positive ou négative",
|
"enterPowerValue": "Entrez une valeur de puissance positive ou négative",
|
||||||
"inverterNumber": "Nombre d'onduleurs",
|
"inverterNumber": "Nombre d'onduleurs",
|
||||||
"batteriesCountInInverter": "Nombre de batteries dans l'onduleur {number}",
|
"batteriesCountInInverter": "Nombre de batteries dans l'onduleur {number}",
|
||||||
|
"batteryNumberInClusterN": "Nombre de batteries dans le cluster {n}",
|
||||||
|
"pvStringsNumberInInverterN": "Nombre de chaînes PV dans l'onduleur {n}",
|
||||||
|
"batteries": "batteries",
|
||||||
"maximumChargingCurrentPerBattery": "Courant de charge maximum par batterie (A)",
|
"maximumChargingCurrentPerBattery": "Courant de charge maximum par batterie (A)",
|
||||||
|
"maximumChargingCurrentPerClusterLabel": "Courant de charge maximum par cluster (A)",
|
||||||
|
"maximumDischargingCurrentPerClusterLabel": "Courant de décharge maximum par cluster (A)",
|
||||||
"maximumDischargingCurrentPerBattery": "Courant de décharge maximum par batterie (A)",
|
"maximumDischargingCurrentPerBattery": "Courant de décharge maximum par batterie (A)",
|
||||||
"powerPerInverterKW": "Puissance par onduleur (kW)",
|
"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)",
|
||||||
|
|
|
||||||
|
|
@ -521,7 +521,12 @@
|
||||||
"enterPowerValue": "Inserire un valore di potenza positivo o negativo",
|
"enterPowerValue": "Inserire un valore di potenza positivo o negativo",
|
||||||
"inverterNumber": "Numero di inverter",
|
"inverterNumber": "Numero di inverter",
|
||||||
"batteriesCountInInverter": "Numero di batterie nell'inverter {number}",
|
"batteriesCountInInverter": "Numero di batterie nell'inverter {number}",
|
||||||
|
"batteryNumberInClusterN": "Numero di batterie nel cluster {n}",
|
||||||
|
"pvStringsNumberInInverterN": "Numero di stringhe PV nell'inverter {n}",
|
||||||
|
"batteries": "batterie",
|
||||||
"maximumChargingCurrentPerBattery": "Corrente massima di carica per batteria (A)",
|
"maximumChargingCurrentPerBattery": "Corrente massima di carica per batteria (A)",
|
||||||
|
"maximumChargingCurrentPerClusterLabel": "Corrente massima di carica per cluster (A)",
|
||||||
|
"maximumDischargingCurrentPerClusterLabel": "Corrente massima di scarica per cluster (A)",
|
||||||
"maximumDischargingCurrentPerBattery": "Corrente massima di scarica per batteria (A)",
|
"maximumDischargingCurrentPerBattery": "Corrente massima di scarica per batteria (A)",
|
||||||
"powerPerInverterKW": "Potenza per inverter (kW)",
|
"powerPerInverterKW": "Potenza per inverter (kW)",
|
||||||
"startDateTime": "Data e ora di inizio (Inizio < Fine)",
|
"startDateTime": "Data e ora di inizio (Inizio < Fine)",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue