new design with dynamic pricing

This commit is contained in:
Yinyin Liu 2026-04-15 19:13:27 +02:00
parent 0169576620
commit e0d6d04409
7 changed files with 196 additions and 26 deletions

View File

@ -22,6 +22,12 @@ public class Configuration
public DateTime? StartTimeChargeandDischargeDayandTime { get; set; }
public DateTime? StopTimeChargeandDischargeDayandTime { get; set; }
// Sinexcel Dynamic Pricing (under GridPriority) — strings for demo; engine will parse later.
public string? DynamicPricingMode { get; set; }
public string? CurrentPrice { get; set; }
public string? PriceToSell { get; set; }
public string? PriceToBuy { get; set; }
public String GetConfigurationString()
{
return $"MinimumSoC: {MinimumSoC}, GridSetPoint: {GridSetPoint}, CalibrationChargeState: {CalibrationChargeState}, CalibrationChargeDate: {CalibrationChargeDate}, " +
@ -48,7 +54,8 @@ public class Configuration
{
return $"MinimumSoC: {MinimumSoC}, MaximumDischargingCurrent: {MaximumDischargingCurrent}, MaximumChargingCurrent: {MaximumChargingCurrent}, OperatingPriority: {OperatingPriority}, " +
$"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}, " +
$"DynamicPricingMode: {DynamicPricingMode}, CurrentPrice: {CurrentPrice}, PriceToSell: {PriceToSell}, PriceToBuy: {PriceToBuy}";
}
// TODO: SodistoreGrid — update configuration fields when defined

View File

@ -709,6 +709,12 @@ export type ConfigurationValues = {
timeChargeandDischargePower?: number;
startTimeChargeandDischargeDayandTime?: Date | null;
stopTimeChargeandDischargeDayandTime?: Date | null;
// For sodistoreHome-Sinexcel: Dynamic Pricing (under GridPriority)
dynamicPricingMode?: string;
currentPrice?: string;
priceToSell?: string;
priceToBuy?: string;
};
//
// export interface Pv {

View File

@ -9,6 +9,7 @@ import {
FormControl,
Grid,
IconButton,
InputAdornment,
InputLabel,
Modal,
Select,
@ -26,7 +27,12 @@ import axiosConfig from '../../../Resources/axiosConfig';
import { UserContext } from '../../../contexts/userContext';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
import { I_Installation } from 'src/interfaces/InstallationTypes';
import { INSTALLATION_PRESETS } from '../Information/installationSetupUtils';
import {
INSTALLATION_PRESETS,
buildSodistoreProPreset,
getPresetsForDevice,
PresetConfig
} from '../Information/installationSetupUtils';
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import {DateTimePicker } from '@mui/x-date-pickers';
@ -60,6 +66,14 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
'PvPriorityCharging': 'GridPriority',
};
// Dynamic Pricing Mode — backend enum values with UI labels
const DynamicPricingOptions = ['Disabled', 'SpotPrice', 'Tou'] as const;
const dynamicPricingLabelKey: Record<string, string> = {
Disabled: 'dynamicPricingOff',
SpotPrice: 'dynamicPricingOn',
Tou: 'dynamicPricingTou',
};
const [errors, setErrors] = useState({
minimumSoC: false,
gridSetPoint: false
@ -122,6 +136,10 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
return parsed && parsed.year() >= 2020 ? parsed.toDate() : new Date();
})(),
controlPermission: String(props.values.Config.ControlPermission).toLowerCase() === "true",
dynamicPricingMode: (props.values.Config as any).DynamicPricingMode ?? 'Disabled',
currentPrice: (props.values.Config as any).CurrentPrice?.toString() ?? '',
priceToSell: (props.values.Config as any).PriceToSell?.toString() ?? '',
priceToBuy: (props.values.Config as any).PriceToBuy?.toString() ?? '',
};
};
@ -263,7 +281,11 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
stopTimeChargeandDischargeDayandTime: formValues.stopTimeChargeandDischargeDayandTime
? new Date(formValues.stopTimeChargeandDischargeDayandTime.getTime() - formValues.stopTimeChargeandDischargeDayandTime.getTimezoneOffset() * 60000)
: null,
controlPermission:formValues.controlPermission
controlPermission:formValues.controlPermission,
dynamicPricingMode: formValues.dynamicPricingMode,
currentPrice: formValues.currentPrice,
priceToSell: formValues.priceToSell,
priceToBuy: formValues.priceToBuy,
};
setLoading(true);
@ -441,12 +463,15 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
)}
{device === 4 && (() => {
const preset = INSTALLATION_PRESETS[props.installation.installationModel];
const inverterCount = preset ? preset.length : 1;
const pvStrings = (props.installation.pvStringsPerInverter || '')
.split(',')
.map((s) => s.trim())
.filter((s) => s !== '');
// Mirror Information tab's preset derivation so both views always agree.
const isSodistorePro = product === 5;
const model = props.installation.installationModel;
const presetConfig: PresetConfig | null = isSodistorePro
? (model && parseInt(model, 10) > 0
? buildSodistoreProPreset(parseInt(model, 10))
: null)
: (getPresetsForDevice(device)[model] || null);
const inverterCount = presetConfig?.length ?? 1;
return (
<>
@ -459,8 +484,10 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
/>
</div>
{preset && preset.map((clusters, i) => {
const batteriesInInverter = clusters.reduce((a, b) => a + b, 0);
{Array.from({ length: inverterCount }, (_, i) => {
const batteriesInInverter = presetConfig
? (presetConfig[i] ?? []).reduce((a, b) => a + b, 0)
: 0;
return (
<div key={`battCount_${i}`} style={{ marginBottom: '5px' }}>
<TextField
@ -475,20 +502,6 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
</div>
);
})}
{Array.from({ length: inverterCount }, (_, i) => (
<div key={`pvCount_${i}`} style={{ marginBottom: '5px' }}>
<TextField
label={intl.formatMessage(
{ id: 'pvInInverter' },
{ number: i + 1 }
)}
value={pvStrings[i] ?? 0}
InputProps={{ readOnly: true }}
fullWidth
/>
</div>
))}
</>
);
})()}
@ -679,6 +692,114 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
</>
)}
{/* --- Sinexcel + GridPriority: Dynamic Pricing --- */}
{device === 4 &&
OperatingPriorityOptions[formValues.operatingPriority] === 'GridPriority' && (
<>
<Typography variant="h6" sx={{ mt: 3, mb: 1 }}>
<FormattedMessage id="dynamicPricing" defaultMessage="Dynamic Pricing" />
</Typography>
<Divider sx={{ mb: 2 }} />
<div style={{ marginBottom: '5px', marginTop: '10px' }}>
<FormControl fullWidth sx={{ marginLeft: 1, width: 390 }}>
<InputLabel sx={{ fontSize: 14, backgroundColor: 'white' }}>
<FormattedMessage id="dynamicPricingMode" defaultMessage="Dynamic Pricing Mode" />
</InputLabel>
<Select
value={formValues.dynamicPricingMode ?? 'Disabled'}
onChange={(e) => {
setFormDirty(true);
setFormValues((prev) => ({
...prev,
dynamicPricingMode: e.target.value as string,
}));
}}
>
{DynamicPricingOptions.map((opt) => (
<MenuItem key={opt} value={opt}>
{intl.formatMessage({ id: dynamicPricingLabelKey[opt] })}
</MenuItem>
))}
</Select>
</FormControl>
</div>
{formValues.dynamicPricingMode === 'SpotPrice' && (
<>
<div style={{ marginBottom: '5px' }}>
<TextField
label={intl.formatMessage({ id: 'networkProvider' })}
value={
props.installation.networkProvider ||
intl.formatMessage({ id: 'networkProviderSetOnInformationTab' })
}
InputProps={{ readOnly: true }}
fullWidth
/>
</div>
<div style={{ marginBottom: '5px' }}>
<TextField
label={intl.formatMessage({ id: 'currentPrice' })}
name="currentPrice"
value={formValues.currentPrice ?? ''}
onChange={(e) => {
const v = e.target.value;
if (v === '' || /^\d*\.?\d*$/.test(v)) {
setFormDirty(true);
setFormValues((prev) => ({ ...prev, currentPrice: v }));
}
}}
InputProps={{
endAdornment: <InputAdornment position="end">CHF/kWh</InputAdornment>,
}}
fullWidth
/>
</div>
<div style={{ marginBottom: '5px' }}>
<TextField
label={intl.formatMessage({ id: 'priceToSell' })}
name="priceToSell"
value={formValues.priceToSell ?? ''}
onChange={(e) => {
const v = e.target.value;
if (v === '' || /^\d*\.?\d*$/.test(v)) {
setFormDirty(true);
setFormValues((prev) => ({ ...prev, priceToSell: v }));
}
}}
InputProps={{
endAdornment: <InputAdornment position="end">CHF/kWh</InputAdornment>,
}}
fullWidth
/>
</div>
<div style={{ marginBottom: '5px' }}>
<TextField
label={intl.formatMessage({ id: 'priceToBuy' })}
name="priceToBuy"
value={formValues.priceToBuy ?? ''}
onChange={(e) => {
const v = e.target.value;
if (v === '' || /^\d*\.?\d*$/.test(v)) {
setFormDirty(true);
setFormValues((prev) => ({ ...prev, priceToBuy: v }));
}
}}
InputProps={{
endAdornment: <InputAdornment position="end">CHF/kWh</InputAdornment>,
}}
fullWidth
/>
</div>
</>
)}
</>
)}
<div
style={{
display: 'flex',

View File

@ -529,6 +529,15 @@
"systemSettings": "Systemeinstellungen",
"pvPerInverter": "PV pro Wechselrichter",
"pvInInverter": "PV in Wechselrichter {number}",
"dynamicPricing": "Dynamische Preisgestaltung",
"dynamicPricingMode": "Modus der dynamischen Preisgestaltung",
"dynamicPricingOff": "Aus",
"dynamicPricingOn": "Ein",
"dynamicPricingTou": "TOU",
"currentPrice": "Aktueller Preis",
"priceToSell": "Verkaufspreis",
"priceToBuy": "Kaufpreis",
"networkProviderSetOnInformationTab": "Im Informations-Tab festlegen",
"tourLanguageTitle": "Sprache",
"tourLanguageContent": "Wählen Sie Ihre bevorzugte Sprache. Die Oberfläche unterstützt Englisch, Deutsch, Französisch und Italienisch.",
"tourExploreTitle": "Installation erkunden",

View File

@ -277,6 +277,15 @@
"systemSettings": "System Settings",
"pvPerInverter": "PV per Inverter",
"pvInInverter": "PV in Inverter {number}",
"dynamicPricing": "Dynamic Pricing",
"dynamicPricingMode": "Dynamic Pricing Mode",
"dynamicPricingOff": "Off",
"dynamicPricingOn": "On",
"dynamicPricingTou": "TOU",
"currentPrice": "Current Price",
"priceToSell": "Price to Sell",
"priceToBuy": "Price to Buy",
"networkProviderSetOnInformationTab": "Set on Information tab",
"tourLanguageTitle": "Language",
"tourLanguageContent": "Choose your preferred language. The interface supports English, German, French, and Italian.",
"tourExploreTitle": "Explore an Installation",

View File

@ -529,6 +529,15 @@
"systemSettings": "Paramètres système",
"pvPerInverter": "PV par onduleur",
"pvInInverter": "PV dans l'onduleur {number}",
"dynamicPricing": "Tarification dynamique",
"dynamicPricingMode": "Mode de tarification dynamique",
"dynamicPricingOff": "Désactivé",
"dynamicPricingOn": "Activé",
"dynamicPricingTou": "TOU",
"currentPrice": "Prix actuel",
"priceToSell": "Prix de vente",
"priceToBuy": "Prix d'achat",
"networkProviderSetOnInformationTab": "À définir dans l'onglet Informations",
"tourLanguageTitle": "Langue",
"tourLanguageContent": "Choisissez votre langue préférée. L'interface est disponible en anglais, allemand, français et italien.",
"tourExploreTitle": "Explorer une installation",

View File

@ -529,6 +529,15 @@
"systemSettings": "Impostazioni di sistema",
"pvPerInverter": "PV per inverter",
"pvInInverter": "PV nell'inverter {number}",
"dynamicPricing": "Prezzi dinamici",
"dynamicPricingMode": "Modalità prezzi dinamici",
"dynamicPricingOff": "Off",
"dynamicPricingOn": "On",
"dynamicPricingTou": "TOU",
"currentPrice": "Prezzo attuale",
"priceToSell": "Prezzo di vendita",
"priceToBuy": "Prezzo di acquisto",
"networkProviderSetOnInformationTab": "Imposta nella scheda Informazioni",
"tourLanguageTitle": "Lingua",
"tourLanguageContent": "Scegli la tua lingua preferita. L'interfaccia supporta inglese, tedesco, francese e italiano.",
"tourExploreTitle": "Esplora un'installazione",