From e0d6d04409689c9c7ebe73b4094f924e5ad40a18 Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Wed, 15 Apr 2026 19:13:27 +0200 Subject: [PATCH] new design with dynamic pricing --- csharp/App/Backend/DataTypes/Configuration.cs | 11 +- .../src/content/dashboards/Log/graph.util.tsx | 6 + .../SodistoreHomeConfiguration.tsx | 169 +++++++++++++++--- typescript/frontend-marios2/src/lang/de.json | 9 + typescript/frontend-marios2/src/lang/en.json | 9 + typescript/frontend-marios2/src/lang/fr.json | 9 + typescript/frontend-marios2/src/lang/it.json | 9 + 7 files changed, 196 insertions(+), 26 deletions(-) diff --git a/csharp/App/Backend/DataTypes/Configuration.cs b/csharp/App/Backend/DataTypes/Configuration.cs index 8ba1d7a43..a0e747a00 100644 --- a/csharp/App/Backend/DataTypes/Configuration.cs +++ b/csharp/App/Backend/DataTypes/Configuration.cs @@ -21,7 +21,13 @@ public class Configuration public double? TimeChargeandDischargePower { get; set; } 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 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 006cdd59a..f29b37388 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx @@ -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 { diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistoreHomeConfiguration.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistoreHomeConfiguration.tsx index b6576b083..14b88be79 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistoreHomeConfiguration.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistoreHomeConfiguration.tsx @@ -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 = { + 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) { /> - {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 (
); })} - - {Array.from({ length: inverterCount }, (_, i) => ( -
- -
- ))} ); })()} @@ -679,6 +692,114 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) { )} + {/* --- Sinexcel + GridPriority: Dynamic Pricing --- */} + {device === 4 && + OperatingPriorityOptions[formValues.operatingPriority] === 'GridPriority' && ( + <> + + + + + +
+ + + + + + +
+ + {formValues.dynamicPricingMode === 'SpotPrice' && ( + <> +
+ +
+ +
+ { + const v = e.target.value; + if (v === '' || /^\d*\.?\d*$/.test(v)) { + setFormDirty(true); + setFormValues((prev) => ({ ...prev, currentPrice: v })); + } + }} + InputProps={{ + endAdornment: CHF/kWh, + }} + fullWidth + /> +
+ +
+ { + const v = e.target.value; + if (v === '' || /^\d*\.?\d*$/.test(v)) { + setFormDirty(true); + setFormValues((prev) => ({ ...prev, priceToSell: v })); + } + }} + InputProps={{ + endAdornment: CHF/kWh, + }} + fullWidth + /> +
+ +
+ { + const v = e.target.value; + if (v === '' || /^\d*\.?\d*$/.test(v)) { + setFormDirty(true); + setFormValues((prev) => ({ ...prev, priceToBuy: v })); + } + }} + InputProps={{ + endAdornment: CHF/kWh, + }} + fullWidth + /> +
+ + )} + + )} +