203 lines
7.4 KiB
C#
203 lines
7.4 KiB
C#
using System;
|
|
using InnovEnergy.App.SinexcelCommunication.DataTypes;
|
|
using InnovEnergy.App.SinexcelCommunication.SystemConfig;
|
|
using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType;
|
|
|
|
namespace InnovEnergy.App.SinexcelCommunication.ESS;
|
|
/*
|
|
public static class DynamicPricingEngine
|
|
{
|
|
|
|
public static readonly TimeSpan CheapStart = TimeSpan.FromHours(22); // 22:00
|
|
public static readonly TimeSpan CheapEnd = TimeSpan.FromHours(6); // 06:00
|
|
|
|
// Expensive (High tariff)
|
|
public static readonly TimeSpan HighStart = TimeSpan.FromHours(17); // 17:00
|
|
public static readonly TimeSpan HighEnd = TimeSpan.FromHours(21); // 21:00
|
|
|
|
/// <summary>
|
|
/// Call this from your main loop. It sets statusrecord.Mode.
|
|
///
|
|
/// liveSpotPrice is only needed when DynamicPricingMode == SpotPrice.
|
|
/// If your inverter cannot directly force Charge/Discharge, set inverterSupportsDirectForce=false
|
|
/// and it will execute via TimeChargeDischarge + rolling short time window.
|
|
/// </summary>
|
|
public static void Apply(
|
|
DateTime nowLocal,
|
|
Decimal? liveSpotPrice,
|
|
StatusRecord statusrecord,
|
|
Boolean inverterSupportsDirectForce,
|
|
Int32 rollingWindowMinutes = 10)
|
|
{
|
|
if (statusrecord == null) throw new ArgumentNullException(nameof(statusrecord));
|
|
if (statusrecord.Config == null) throw new ArgumentNullException(nameof(statusrecord.Config));
|
|
|
|
var c = statusrecord.Config;
|
|
|
|
// 0) Manual override (optional)
|
|
if (!c.DynamicPricingEnabled)
|
|
{
|
|
Console.WriteLine(" Dynamic pricing is disabled");
|
|
return;
|
|
}
|
|
|
|
if (statusrecord.InverterRecord.OperatingPriority == OperatingPriority.ModeNotSynched)
|
|
{
|
|
Console.WriteLine(" Inverter mode are not synched ");
|
|
return;
|
|
}
|
|
/*
|
|
|
|
}
|
|
|
|
// 1) Base operating mode: explicit modes ignore dynamic pricing
|
|
if (c.OperatingPriority == OperatingPriority.GridPriority)
|
|
{
|
|
SetMode(statusrecord, OperatingPriority.GridPriority);
|
|
return;
|
|
}
|
|
|
|
if (c.OperatingPriority == OperatingPriority.BatteryPriority)
|
|
{
|
|
SetMode(statusrecord, OperatingPriority.BatteryPriority);
|
|
return;
|
|
}
|
|
|
|
// 2) OperatingMode == OptimizeSelfUse -> dynamic pricing can apply
|
|
var desired = DecideDesiredAction(nowLocal, liveSpotPrice, c);
|
|
|
|
if (desired == DesiredAction.OptimizeSelfUse)
|
|
{
|
|
SetMode(statusrecord, OperatingPriority.LoadPriority);
|
|
return;
|
|
}
|
|
|
|
// 3) Execute desired action
|
|
if (inverterSupportsDirectForce)
|
|
{
|
|
statusrecord.Mode = desired == DesiredAction.Charge
|
|
? BatteryMode.Charge
|
|
: BatteryMode.Discharge;
|
|
return;
|
|
}
|
|
|
|
// 4) Inverter limitation: execute via TimeChargeDischarge rolling window
|
|
statusrecord.Mode = BatteryMode.TimeChargeDischarge;
|
|
|
|
var (start, end) = MakeRollingWindow(nowLocal, rollingWindowMinutes);
|
|
|
|
if (desired == DesiredAction.Charge)
|
|
{
|
|
c.TimeChargeStart = start;
|
|
c.TimeChargeEnd = end;
|
|
|
|
// clear discharge window to avoid overlap
|
|
c.TimeDischargeStart = TimeSpan.Zero;
|
|
c.TimeDischargeEnd = TimeSpan.Zero;
|
|
}
|
|
else // Discharge
|
|
{
|
|
c.TimeDischargeStart = start;
|
|
c.TimeDischargeEnd = end;
|
|
|
|
// clear charge window to avoid overlap
|
|
c.TimeChargeStart = TimeSpan.Zero;
|
|
c.TimeChargeEnd = TimeSpan.Zero;
|
|
}
|
|
}
|
|
|
|
|
|
private static void SetMode( StatusRecord statusrecord, OperatingPriority o)
|
|
{
|
|
var operatingMode = o switch
|
|
{
|
|
OperatingPriority.LoadPriority => WorkingMode.SpontaneousSelfUse,
|
|
OperatingPriority.BatteryPriority => WorkingMode.TimeChargeDischarge,
|
|
OperatingPriority.GridPriority => WorkingMode.PrioritySellElectricity,
|
|
_ => WorkingMode.SpontaneousSelfUse
|
|
};
|
|
|
|
if (statusrecord.InverterRecord.OperatingPriority != OperatingPriority.ModeNotSynched)
|
|
{
|
|
foreach (var inv in statusrecord?.InverterRecord.Devices)
|
|
{
|
|
inv.WorkingMode = operatingMode;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (var inv in statusrecord?.InverterRecord.Devices)
|
|
{
|
|
Console.WriteLine(" Inverter mode are not synched");
|
|
inv.WorkingMode = WorkingMode.SpontaneousSelfUse;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// ----------------------------
|
|
// Decision logic
|
|
// ----------------------------
|
|
private static DesiredAction DecideDesiredAction(DateTime nowLocal, decimal? liveSpotPrice, Config c)
|
|
{
|
|
if (c.DynamicPricingMode == DynamicPricingMode.Disabled)
|
|
return DesiredAction.OptimizeSelfUse;
|
|
|
|
TimeSpan now = nowLocal.TimeOfDay;
|
|
|
|
if (c.DynamicPricingMode == DynamicPricingMode.Tou)
|
|
{
|
|
bool isCheap = IsInTimeWindow(now,CheapStart, CheapEnd);
|
|
bool isHigh = IsInTimeWindow(now, HighStart, HighEnd);
|
|
|
|
// Priority: cheap -> charge, then high -> discharge
|
|
if (isCheap) return DesiredAction.Charge;
|
|
if (isHigh) return DesiredAction.Discharge;
|
|
return DesiredAction.OptimizeSelfUse;
|
|
}
|
|
|
|
if (c.DynamicPricingMode == DynamicPricingMode.SpotPrice)
|
|
{
|
|
if (!liveSpotPrice.HasValue)
|
|
return DesiredAction.OptimizeSelfUse; // safe fallback
|
|
|
|
if (c.CheapPrice >= c.HighPrice)
|
|
throw new ArgumentException("Config error: CheapPrice must be lower than HighPrice.");
|
|
|
|
decimal p = liveSpotPrice.Value;
|
|
|
|
if (p <= c.CheapPrice) return DesiredAction.Charge;
|
|
if (p >= c.HighPrice) return DesiredAction.Discharge;
|
|
}
|
|
|
|
return DesiredAction.OptimizeSelfUse;
|
|
}
|
|
|
|
// ----------------------------
|
|
// Helpers
|
|
// ----------------------------
|
|
/// <summary>
|
|
/// [start, end) window, supports overnight. start==end means disabled.
|
|
/// </summary>
|
|
private static Boolean IsInTimeWindow(TimeSpan now, TimeSpan start, TimeSpan end)
|
|
{
|
|
if (start == end) return false;
|
|
|
|
// Same-day
|
|
if (start < end)
|
|
return now >= start && now < end;
|
|
|
|
// Overnight
|
|
return now >= start || now < end;
|
|
}
|
|
|
|
private static (TimeSpan start, TimeSpan end) MakeRollingWindow(DateTime nowLocal, int minutes)
|
|
{
|
|
if (minutes <= 0) throw new ArgumentOutOfRangeException(nameof(minutes));
|
|
|
|
var start = nowLocal.TimeOfDay;
|
|
var end = (nowLocal + TimeSpan.FromMinutes(minutes)).TimeOfDay;
|
|
return (start, end);
|
|
}
|
|
}
|
|
}*/ |