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 /// /// 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. /// 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 // ---------------------------- /// /// [start, end) window, supports overnight. start==end means disabled. /// 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); } } }*/