This commit is contained in:
Yinyin Liu 2026-04-16 10:12:48 +02:00
parent e0d6d04409
commit 372ab2203d
11 changed files with 143 additions and 16 deletions

View File

@ -1895,6 +1895,14 @@ public class Controller : ControllerBase
{ {
var session = Db.GetSession(authToken); var session = Db.GetSession(authToken);
// Dynamic Pricing in Spot Price mode: forward the provider chosen on the Information tab
// so the device knows which operator's API to query for spot prices.
if (config.DynamicPricingMode == "SpotPrice")
{
var installation = Db.GetInstallationById(installationId);
config.NetworkProvider = installation?.NetworkProvider;
}
string configString = product switch string configString = product switch
{ {
0 => config.GetConfigurationSalimax(), // Salimax 0 => config.GetConfigurationSalimax(), // Salimax

View File

@ -24,9 +24,15 @@ public class Configuration
// Sinexcel Dynamic Pricing (under GridPriority) — strings for demo; engine will parse later. // Sinexcel Dynamic Pricing (under GridPriority) — strings for demo; engine will parse later.
public string? DynamicPricingMode { get; set; } public string? DynamicPricingMode { get; set; }
public string? NetworkProvider { get; set; }
public string? CurrentPrice { get; set; } public string? CurrentPrice { get; set; }
public string? PriceToSell { get; set; } public string? PriceToSell { get; set; }
public string? PriceToBuy { get; set; } public string? PriceToBuy { get; set; }
// TOU windows stored as "HH:mm" strings
public string? TimeToSellFrom { get; set; }
public string? TimeToSellTo { get; set; }
public string? TimeToBuyFrom { get; set; }
public string? TimeToBuyTo { get; set; }
public String GetConfigurationString() public String GetConfigurationString()
{ {
@ -55,7 +61,8 @@ public class Configuration
return $"MinimumSoC: {MinimumSoC}, MaximumDischargingCurrent: {MaximumDischargingCurrent}, MaximumChargingCurrent: {MaximumChargingCurrent}, OperatingPriority: {OperatingPriority}, " + 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}, "+ $"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}"; $"DynamicPricingMode: {DynamicPricingMode}, NetworkProvider: {NetworkProvider}, CurrentPrice: {CurrentPrice}, PriceToSell: {PriceToSell}, PriceToBuy: {PriceToBuy}, " +
$"TimeToSell: {TimeToSellFrom}-{TimeToSellTo}, TimeToBuy: {TimeToBuyFrom}-{TimeToBuyTo}";
} }
// TODO: SodistoreGrid — update configuration fields when defined // TODO: SodistoreGrid — update configuration fields when defined

View File

@ -16,5 +16,16 @@ public class Configuration
public Single TimeChargeandDischargePower { get; set; } public Single TimeChargeandDischargePower { get; set; }
public Boolean ControlPermission { get; set; } public Boolean ControlPermission { get; set; }
// Dynamic Pricing (under GridPriority) — strings for demo; engine parses when needed.
public String? DynamicPricingMode { get; set; }
public String? NetworkProvider { get; set; }
public String? CurrentPrice { get; set; }
public String? PriceToSell { get; set; }
public String? PriceToBuy { get; set; }
public String? TimeToSellFrom { get; set; }
public String? TimeToSellTo { get; set; }
public String? TimeToBuyFrom { get; set; }
public String? TimeToBuyTo { get; set; }
} }

View File

@ -639,6 +639,15 @@ internal static class Program
status.Config.PvNumber = config.PvNumber; status.Config.PvNumber = config.PvNumber;
status.Config.ControlPermission = config.ControlPermission; status.Config.ControlPermission = config.ControlPermission;
status.Config.DynamicPricingMode = config.DynamicPricingMode;
status.Config.NetworkProvider = config.NetworkProvider;
status.Config.CurrentPrice = config.CurrentPrice;
status.Config.PriceToSell = config.PriceToSell;
status.Config.PriceToBuy = config.PriceToBuy;
status.Config.TimeToSellFrom = config.TimeToSellFrom;
status.Config.TimeToSellTo = config.TimeToSellTo;
status.Config.TimeToBuyFrom = config.TimeToBuyFrom;
status.Config.TimeToBuyTo = config.TimeToBuyTo;
} }
private static async Task<Boolean> SaveModbusTcpFile(StatusRecord status) private static async Task<Boolean> SaveModbusTcpFile(StatusRecord status)

View File

@ -36,6 +36,17 @@ public class Config
public required Single TimeChargeandDischargePower { get; set; } public required Single TimeChargeandDischargePower { get; set; }
public required Boolean ControlPermission { get; set; } public required Boolean ControlPermission { get; set; }
// Dynamic Pricing (under GridPriority) — strings for demo; engine parses when needed.
public String? DynamicPricingMode { get; set; }
public String? NetworkProvider { get; set; }
public String? CurrentPrice { get; set; }
public String? PriceToSell { get; set; }
public String? PriceToBuy { get; set; }
public String? TimeToSellFrom { get; set; }
public String? TimeToSellTo { get; set; }
public String? TimeToBuyFrom { get; set; }
public String? TimeToBuyTo { get; set; }
public required S3Config? S3 { get; set; } public required S3Config? S3 { get; set; }

View File

@ -715,6 +715,11 @@ export type ConfigurationValues = {
currentPrice?: string; currentPrice?: string;
priceToSell?: string; priceToSell?: string;
priceToBuy?: string; priceToBuy?: string;
// TOU time windows stored as "HH:mm" strings
timeToSellFrom?: string;
timeToSellTo?: string;
timeToBuyFrom?: string;
timeToBuyTo?: string;
}; };
// //
// export interface Pv { // export interface Pv {

View File

@ -28,14 +28,13 @@ import { UserContext } from '../../../contexts/userContext';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider'; import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
import { I_Installation } from 'src/interfaces/InstallationTypes'; import { I_Installation } from 'src/interfaces/InstallationTypes';
import { import {
INSTALLATION_PRESETS,
buildSodistoreProPreset, buildSodistoreProPreset,
getPresetsForDevice, getPresetsForDevice,
PresetConfig PresetConfig
} 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';
import {DateTimePicker } from '@mui/x-date-pickers'; import { DateTimePicker, TimePicker } from '@mui/x-date-pickers';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import Switch from '@mui/material/Switch'; import Switch from '@mui/material/Switch';
import FormControlLabel from '@mui/material/FormControlLabel'; import FormControlLabel from '@mui/material/FormControlLabel';
@ -70,7 +69,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
const DynamicPricingOptions = ['Disabled', 'SpotPrice', 'Tou'] as const; const DynamicPricingOptions = ['Disabled', 'SpotPrice', 'Tou'] as const;
const dynamicPricingLabelKey: Record<string, string> = { const dynamicPricingLabelKey: Record<string, string> = {
Disabled: 'dynamicPricingOff', Disabled: 'dynamicPricingOff',
SpotPrice: 'dynamicPricingOn', SpotPrice: 'dynamicPricingSpotPrice',
Tou: 'dynamicPricingTou', Tou: 'dynamicPricingTou',
}; };
@ -140,6 +139,10 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
currentPrice: (props.values.Config as any).CurrentPrice?.toString() ?? '', currentPrice: (props.values.Config as any).CurrentPrice?.toString() ?? '',
priceToSell: (props.values.Config as any).PriceToSell?.toString() ?? '', priceToSell: (props.values.Config as any).PriceToSell?.toString() ?? '',
priceToBuy: (props.values.Config as any).PriceToBuy?.toString() ?? '', priceToBuy: (props.values.Config as any).PriceToBuy?.toString() ?? '',
timeToSellFrom: (props.values.Config as any).TimeToSellFrom ?? '',
timeToSellTo: (props.values.Config as any).TimeToSellTo ?? '',
timeToBuyFrom: (props.values.Config as any).TimeToBuyFrom ?? '',
timeToBuyTo: (props.values.Config as any).TimeToBuyTo ?? '',
}; };
}; };
@ -286,6 +289,10 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
currentPrice: formValues.currentPrice, currentPrice: formValues.currentPrice,
priceToSell: formValues.priceToSell, priceToSell: formValues.priceToSell,
priceToBuy: formValues.priceToBuy, priceToBuy: formValues.priceToBuy,
timeToSellFrom: formValues.timeToSellFrom,
timeToSellTo: formValues.timeToSellTo,
timeToBuyFrom: formValues.timeToBuyFrom,
timeToBuyTo: formValues.timeToBuyTo,
}; };
setLoading(true); setLoading(true);
@ -692,9 +699,9 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
</> </>
)} )}
{/* --- Sinexcel + GridPriority: Dynamic Pricing --- */} {/* --- Sinexcel + LoadPriority: Dynamic Pricing --- */}
{device === 4 && {device === 4 &&
OperatingPriorityOptions[formValues.operatingPriority] === 'GridPriority' && ( OperatingPriorityOptions[formValues.operatingPriority] === 'LoadPriority' && (
<> <>
<Typography variant="h6" sx={{ mt: 3, mb: 1 }}> <Typography variant="h6" sx={{ mt: 3, mb: 1 }}>
<FormattedMessage id="dynamicPricing" defaultMessage="Dynamic Pricing" /> <FormattedMessage id="dynamicPricing" defaultMessage="Dynamic Pricing" />
@ -725,6 +732,59 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
</FormControl> </FormControl>
</div> </div>
{formValues.dynamicPricingMode === 'Tou' && (
<>
{(() => {
const renderTimeField = (
labelId: string,
key: 'timeToSellFrom' | 'timeToSellTo' | 'timeToBuyFrom' | 'timeToBuyTo'
) => {
const raw = formValues[key];
const parsed = raw ? dayjs(raw, 'HH:mm') : null;
return (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<TimePicker
ampm={false}
label={intl.formatMessage({ id: labelId })}
value={parsed && parsed.isValid() ? parsed : null}
onChange={(newValue) => {
setFormDirty(true);
setFormValues((prev) => ({
...prev,
[key]: newValue ? newValue.format('HH:mm') : '',
}));
}}
renderInput={(params) => (
<TextField {...params} sx={{ marginTop: 2, width: '100%' }} />
)}
/>
</LocalizationProvider>
);
};
return (
<>
<Typography variant="subtitle2" sx={{ mt: 2, ml: 1 }}>
<FormattedMessage id="timeToSell" defaultMessage="Time to Sell" />
</Typography>
<div style={{ display: 'flex', gap: 8, marginLeft: 8 }}>
{renderTimeField('timeFrom', 'timeToSellFrom')}
{renderTimeField('timeTo', 'timeToSellTo')}
</div>
<Typography variant="subtitle2" sx={{ mt: 2, ml: 1 }}>
<FormattedMessage id="timeToBuy" defaultMessage="Time to Buy" />
</Typography>
<div style={{ display: 'flex', gap: 8, marginLeft: 8 }}>
{renderTimeField('timeFrom', 'timeToBuyFrom')}
{renderTimeField('timeTo', 'timeToBuyTo')}
</div>
</>
);
})()}
</>
)}
{formValues.dynamicPricingMode === 'SpotPrice' && ( {formValues.dynamicPricingMode === 'SpotPrice' && (
<> <>
<div style={{ marginBottom: '5px' }}> <div style={{ marginBottom: '5px' }}>

View File

@ -532,11 +532,15 @@
"dynamicPricing": "Dynamische Preisgestaltung", "dynamicPricing": "Dynamische Preisgestaltung",
"dynamicPricingMode": "Modus der dynamischen Preisgestaltung", "dynamicPricingMode": "Modus der dynamischen Preisgestaltung",
"dynamicPricingOff": "Aus", "dynamicPricingOff": "Aus",
"dynamicPricingOn": "Ein", "dynamicPricingSpotPrice": "Spot-Preis",
"dynamicPricingTou": "TOU", "dynamicPricingTou": "TOU",
"currentPrice": "Aktueller Preis", "currentPrice": "Aktueller Preis",
"priceToSell": "Verkaufspreis", "priceToSell": "Verkaufspreis",
"priceToBuy": "Kaufpreis", "priceToBuy": "Kaufpreis",
"timeToSell": "Verkaufszeit",
"timeToBuy": "Kaufzeit",
"timeFrom": "Von",
"timeTo": "Bis",
"networkProviderSetOnInformationTab": "Im Informations-Tab festlegen", "networkProviderSetOnInformationTab": "Im Informations-Tab festlegen",
"tourLanguageTitle": "Sprache", "tourLanguageTitle": "Sprache",
"tourLanguageContent": "Wählen Sie Ihre bevorzugte Sprache. Die Oberfläche unterstützt Englisch, Deutsch, Französisch und Italienisch.", "tourLanguageContent": "Wählen Sie Ihre bevorzugte Sprache. Die Oberfläche unterstützt Englisch, Deutsch, Französisch und Italienisch.",

View File

@ -280,11 +280,15 @@
"dynamicPricing": "Dynamic Pricing", "dynamicPricing": "Dynamic Pricing",
"dynamicPricingMode": "Dynamic Pricing Mode", "dynamicPricingMode": "Dynamic Pricing Mode",
"dynamicPricingOff": "Off", "dynamicPricingOff": "Off",
"dynamicPricingOn": "On", "dynamicPricingSpotPrice": "Spot Price",
"dynamicPricingTou": "TOU", "dynamicPricingTou": "TOU",
"currentPrice": "Current Price", "currentPrice": "Current Price",
"priceToSell": "Price to Sell", "priceToSell": "Price to Sell",
"priceToBuy": "Price to Buy", "priceToBuy": "Price to Buy",
"timeToSell": "Time to Sell",
"timeToBuy": "Time to Buy",
"timeFrom": "From",
"timeTo": "To",
"networkProviderSetOnInformationTab": "Set on Information tab", "networkProviderSetOnInformationTab": "Set on Information tab",
"tourLanguageTitle": "Language", "tourLanguageTitle": "Language",
"tourLanguageContent": "Choose your preferred language. The interface supports English, German, French, and Italian.", "tourLanguageContent": "Choose your preferred language. The interface supports English, German, French, and Italian.",

View File

@ -532,11 +532,15 @@
"dynamicPricing": "Tarification dynamique", "dynamicPricing": "Tarification dynamique",
"dynamicPricingMode": "Mode de tarification dynamique", "dynamicPricingMode": "Mode de tarification dynamique",
"dynamicPricingOff": "Désactivé", "dynamicPricingOff": "Désactivé",
"dynamicPricingOn": "Activé", "dynamicPricingSpotPrice": "Prix spot",
"dynamicPricingTou": "TOU", "dynamicPricingTou": "TOU",
"currentPrice": "Prix actuel", "currentPrice": "Prix actuel",
"priceToSell": "Prix de vente", "priceToSell": "Prix de vente",
"priceToBuy": "Prix d'achat", "priceToBuy": "Prix d'achat",
"timeToSell": "Heure de vente",
"timeToBuy": "Heure d'achat",
"timeFrom": "De",
"timeTo": "À",
"networkProviderSetOnInformationTab": "À définir dans l'onglet Informations", "networkProviderSetOnInformationTab": "À définir dans l'onglet Informations",
"tourLanguageTitle": "Langue", "tourLanguageTitle": "Langue",
"tourLanguageContent": "Choisissez votre langue préférée. L'interface est disponible en anglais, allemand, français et italien.", "tourLanguageContent": "Choisissez votre langue préférée. L'interface est disponible en anglais, allemand, français et italien.",

View File

@ -532,11 +532,15 @@
"dynamicPricing": "Prezzi dinamici", "dynamicPricing": "Prezzi dinamici",
"dynamicPricingMode": "Modalità prezzi dinamici", "dynamicPricingMode": "Modalità prezzi dinamici",
"dynamicPricingOff": "Off", "dynamicPricingOff": "Off",
"dynamicPricingOn": "On", "dynamicPricingSpotPrice": "Prezzo spot",
"dynamicPricingTou": "TOU", "dynamicPricingTou": "TOU",
"currentPrice": "Prezzo attuale", "currentPrice": "Prezzo attuale",
"priceToSell": "Prezzo di vendita", "priceToSell": "Prezzo di vendita",
"priceToBuy": "Prezzo di acquisto", "priceToBuy": "Prezzo di acquisto",
"timeToSell": "Orario di vendita",
"timeToBuy": "Orario di acquisto",
"timeFrom": "Da",
"timeTo": "A",
"networkProviderSetOnInformationTab": "Imposta nella scheda Informazioni", "networkProviderSetOnInformationTab": "Imposta nella scheda Informazioni",
"tourLanguageTitle": "Lingua", "tourLanguageTitle": "Lingua",
"tourLanguageContent": "Scegli la tua lingua preferita. L'interfaccia supporta inglese, tedesco, francese e italiano.", "tourLanguageContent": "Scegli la tua lingua preferita. L'interfaccia supporta inglese, tedesco, francese e italiano.",