diff --git a/csharp/App/SaliMax/src/Ess/Controller.cs b/csharp/App/SaliMax/src/Ess/Controller.cs index e13c6667d..89a5071ce 100644 --- a/csharp/App/SaliMax/src/Ess/Controller.cs +++ b/csharp/App/SaliMax/src/Ess/Controller.cs @@ -4,104 +4,172 @@ using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc.DataTypes; using InnovEnergy.Lib.Time.Unix; using InnovEnergy.Lib.Utils; - namespace InnovEnergy.App.SaliMax.Ess; public static class Controller { private static readonly UnixTimeSpan MaxTimeWithoutEoc = UnixTimeSpan.FromDays(7); // TODO: move to config - private static readonly TimeSpan CommunicationTimeout = TimeSpan.FromSeconds(10); - - - - + public static EssMode SelectControlMode(this StatusRecord s) { - return EssMode.OptimizeSelfConsumption; + //return EssMode.OptimizeSelfConsumption; - // return s.SystemState.Id != 16 ? EssMode.Off - // : s.MustHeatBatteries() ? EssMode.HeatBatteries - // : s.MustDoCalibrationCharge() ? EssMode.CalibrationCharge - // : s.MustReachMinSoc() ? EssMode.ReachMinSoc - // : s.GridMeter is null ? EssMode.NoGridMeter - // : EssMode.OptimizeSelfConsumption; + return s.StateMachine.State != 16 ? EssMode.Off + : s.MustHeatBatteries() ? EssMode.HeatBatteries + : s.MustDoCalibrationCharge() ? EssMode.CalibrationCharge + : s.MustReachMinSoc() ? EssMode.ReachMinSoc + : s.GridMeter is null ? EssMode.NoGridMeter + : EssMode.OptimizeSelfConsumption; } public static EssControl ControlEss(this StatusRecord s) { - // var hasPreChargeAlarm = s.HasPreChargeAlarm(); - // - // if (hasPreChargeAlarm) - // "PreChargeAlarm".Log(); - var mode = s.SelectControlMode(); + mode.WriteLine(); + if (mode is EssMode.Off or EssMode.NoGridMeter) return new EssControl(mode, EssLimit.NoLimit, PowerCorrection: 0, PowerSetpoint: 0); var essDelta = s.ComputePowerDelta(mode); var unlimitedControl = new EssControl(mode, EssLimit.NoLimit, essDelta, 0); - var limitedControl = unlimitedControl - .LimitChargePower(s) - .LimitDischargePower(s); + + var limitedControl = unlimitedControl + .LimitChargePower(s) + .LimitDischargePower(s) + .LimitInverterPower(s); var currentPowerSetPoint = s.CurrentPowerSetPoint(); - var setpoint = currentPowerSetPoint + limitedControl.PowerCorrection; - //var setpoint = -11000; + + var essControl = limitedControl with { PowerSetpoint = currentPowerSetPoint + limitedControl.PowerCorrection }; - return limitedControl with { PowerSetpoint = setpoint }; + essControl.WriteLine(); + s.Battery.Soc.WriteLine("Soc"); + + return essControl; } + private static EssControl LimitInverterPower(this EssControl control, StatusRecord s) + { + var powerDelta = control.PowerCorrection.Value; + + var acDcs = s.AcDc.Devices; + + var nInverters = acDcs.Count; + + if (nInverters < 2) + return control; // current loop cannot happen + + var nominalPower = acDcs.Average(d => d.Status.Nominal.Power); + var maxStep = nominalPower / 25; + + var clampedPowerDelta = powerDelta.Clamp(-maxStep, maxStep); + + var dcLimited = acDcs.Any(d => d.Status.PowerLimitedBy == PowerLimit.DcLink); + + if (!dcLimited) + return control with { PowerCorrection = clampedPowerDelta }; + + var maxPower = acDcs.Max(d => d.Status.Ac.Power.Active.Value).WriteLine("Max"); + var minPower = acDcs.Min(d => d.Status.Ac.Power.Active.Value).WriteLine("Min"); + + var powerDifference = maxPower - minPower; + + if (powerDifference < maxStep) + return control with { PowerCorrection = clampedPowerDelta }; + + var correction = powerDifference / 4; + + return s.AcDc.Dc.Voltage > s.Config.ReferenceDcBusVoltage + ? control with { PowerCorrection = clampedPowerDelta.Clamp(-maxStep, -correction), LimitedBy = EssLimit.ChargeLimitedByMaxDcBusVoltage } + : control with { PowerCorrection = clampedPowerDelta.Clamp(correction, maxStep), LimitedBy = EssLimit.DischargeLimitedByMinDcBusVoltage }; + } + + // private static Double AdjustMaxChargePower(StatusRecord s, Double powerDelta) + // { + // var acDcs = s.AcDc.Devices; + // + // var nInverters = acDcs.Count; + // + // if (nInverters == 0) + // return 0; // no inverters present: we cannot charge at all + // + // var nominalPower = acDcs.Sum(d => d.Status.Nominal.Power); + // + // if (nInverters == 1) + // return powerDelta; // single inverter: current loop cannot happen + // + // acDcs.ForEach(d => d.Status.PowerLimitedBy.WriteLine()); + // + // var dcLimited = acDcs.Any(d => d.Status.PowerLimitedBy == PowerLimit.DcLink); + // + // if (expr) + // { + // + // } + // + // + // var maxPowerDifference = nominalPower / 25; + // + // var maxPower = acDcs.Max(d => d.Status.Ac.Power.Active.Value).WriteLine("Max"); + // var minPower = acDcs.Min(d => d.Status.Ac.Power.Active.Value).WriteLine("Min"); + // + // var sum = acDcs.Sum(d => d.Status.Ac.Power.Active.Value); + // + // var powerDifference = maxPower - minPower; + // + // if (powerDifference > maxPowerDifference) + // ChargePower = sum - powerDifference / 4; + // else + // ChargePower += maxPowerDifference / 2; + // + // $"HACK : ChargePower {ChargePower} Difference: {powerDifference}".WriteLine(); + // } + + private static EssControl LimitChargePower(this EssControl control, StatusRecord s) { - var maxInverterChargePower = s.ControlInverterPower(s.Config.MaxInverterPower); + + //var maxInverterChargePower = s.ControlInverterPower(s.Config.MaxInverterPower); var maxBatteryChargePower = s.MaxBatteryChargePower(); return control - .LimitChargePower(maxInverterChargePower, EssLimit.ChargeLimitedByInverterPower) + //.LimitChargePower(, EssLimit.ChargeLimitedByInverterPower) .LimitChargePower(maxBatteryChargePower, EssLimit.ChargeLimitedByBatteryPower); + } - - private static EssControl LimitChargePower(this EssControl control, Double controlDelta, EssLimit reason) - { - return control.PowerCorrection > controlDelta - ? control with { LimitedBy = reason, PowerCorrection = controlDelta } - : control; - } - private static EssControl LimitDischargePower(this EssControl control, StatusRecord s) { - var maxInverterDischargeDelta = s.ControlInverterPower(-s.Config.MaxInverterPower); + //var maxInverterDischargeDelta = s.ControlInverterPower(-s.Config.MaxInverterPower); var maxBatteryDischargeDelta = s.Battery.Devices.Sum(b => b.MaxDischargePower); var keepMinSocLimitDelta = s.ControlBatteryPower(s.HoldMinSocPower()); return control - .LimitDischargePower(maxInverterDischargeDelta, EssLimit.DischargeLimitedByInverterPower) + // .LimitDischargePower(maxInverterDischargeDelta, EssLimit.DischargeLimitedByInverterPower) .LimitDischargePower(maxBatteryDischargeDelta , EssLimit.DischargeLimitedByBatteryPower) .LimitDischargePower(keepMinSocLimitDelta , EssLimit.DischargeLimitedByMinSoc); } - private static EssControl LimitDischargePower(this EssControl control, Double controlDelta, EssLimit reason) - { - return control.PowerCorrection < controlDelta - ? control with { LimitedBy = reason, PowerCorrection = controlDelta } - : control; - } + - private static Double ComputePowerDelta(this StatusRecord s, EssMode mode) => mode switch + private static Double ComputePowerDelta(this StatusRecord s, EssMode mode) { - EssMode.HeatBatteries => s.ControlInverterPower(s.Config.MaxInverterPower), - EssMode.CalibrationCharge => s.ControlInverterPower(s.Config.MaxInverterPower), - EssMode.ReachMinSoc => s.ControlInverterPower(s.Config.MaxInverterPower), - EssMode.OptimizeSelfConsumption => s.ControlGridPower(s.Config.GridSetPoint), - _ => throw new ArgumentException(null, nameof(mode)) - }; - + var chargePower = s.AcDc.Devices.Sum(d => d.Status.Nominal.Power.Value); + + return mode switch + { + EssMode.HeatBatteries => s.ControlInverterPower(chargePower), + EssMode.ReachMinSoc => s.ControlInverterPower(chargePower), + EssMode.CalibrationCharge => s.ControlInverterPower(chargePower), + EssMode.OptimizeSelfConsumption => s.ControlGridPower(s.Config.GridSetPoint), + _ => throw new ArgumentException(null, nameof(mode)) + }; + } private static Boolean HasPreChargeAlarm(this StatusRecord statusRecord) @@ -165,20 +233,14 @@ public static class Controller return UnixTime.Now - statusRecord.Config.LastEoc > MaxTimeWithoutEoc; } - private static Double DistributePower(this StatusRecord s, Double powerSetPoint) - { - var inverterPowerSetPoint = powerSetPoint / s.AcDc.Devices.Count; - return inverterPowerSetPoint.Clamp(-s.Config.MaxInverterPower, s.Config.MaxInverterPower); - } - public static Double ControlGridPower(this StatusRecord status, Double targetPower) { return ControlPower ( - measurement: status.GridMeter!.Ac.Power.Active, - target: targetPower, - pConstant: status.Config.PConstant + measurement : status.GridMeter!.Ac.Power.Active, + target : targetPower, + pConstant : status.Config.PConstant ); } @@ -186,9 +248,9 @@ public static class Controller { return ControlPower ( - measurement: status.AcDc.Ac.Power.Active, - target: targetInverterPower, - pConstant: status.Config.PConstant + measurement : status.AcDc.Ac.Power.Active, + target : targetInverterPower, + pConstant : status.Config.PConstant ); } @@ -211,7 +273,7 @@ public static class Controller if (batteries.Count == 0) return Double.NegativeInfinity; - var a = -2 * s.Config.SelfDischargePower * batteries.Count / s.Config.HoldSocZone; + var a = -2 * s.Config.BatterySelfDischargePower * batteries.Count / s.Config.HoldSocZone; var b = -a * (s.Config.MinSoc + s.Config.HoldSocZone); return batteries.Min(d => d.Soc.Value) * a + b; diff --git a/csharp/App/SaliMax/src/Ess/EssControl.cs b/csharp/App/SaliMax/src/Ess/EssControl.cs index f2d9c9f19..6d362b5c5 100644 --- a/csharp/App/SaliMax/src/Ess/EssControl.cs +++ b/csharp/App/SaliMax/src/Ess/EssControl.cs @@ -4,11 +4,42 @@ namespace InnovEnergy.App.SaliMax.Ess; public record EssControl ( - EssMode Mode, - EssLimit LimitedBy, + EssMode Mode, + EssLimit LimitedBy, ActivePower PowerCorrection, ActivePower PowerSetpoint -); +) +{ + public EssControl LimitChargePower(Double controlDelta, EssLimit reason) + { + var overload = PowerCorrection - controlDelta; + + if (overload <= 0) + return this; + + return this with + { + LimitedBy = reason, + PowerCorrection = controlDelta, + PowerSetpoint = PowerSetpoint - overload + }; + } + + public EssControl LimitDischargePower(Double controlDelta, EssLimit reason) + { + var overload = PowerCorrection - controlDelta; + + if (overload >= 0) + return this; + + return this with + { + LimitedBy = reason, + PowerCorrection = controlDelta, + PowerSetpoint = PowerSetpoint - overload + }; + } +} diff --git a/csharp/App/SaliMax/src/Ess/EssLimit.cs b/csharp/App/SaliMax/src/Ess/EssLimit.cs index 8e2a3a582..3eb95b07c 100644 --- a/csharp/App/SaliMax/src/Ess/EssLimit.cs +++ b/csharp/App/SaliMax/src/Ess/EssLimit.cs @@ -8,6 +8,8 @@ public enum EssLimit DischargeLimitedByInverterPower, ChargeLimitedByInverterPower, ChargeLimitedByBatteryPower, + ChargeLimitedByMaxDcBusVoltage, + DischargeLimitedByMinDcBusVoltage, } diff --git a/csharp/App/SaliMax/src/Program.cs b/csharp/App/SaliMax/src/Program.cs index b3f7ee55a..f63255f38 100644 --- a/csharp/App/SaliMax/src/Program.cs +++ b/csharp/App/SaliMax/src/Program.cs @@ -1,13 +1,10 @@ -using System.Diagnostics; using System.Runtime.InteropServices; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using Flurl.Http; using InnovEnergy.App.SaliMax.Ess; using InnovEnergy.App.SaliMax.SaliMaxRelays; using InnovEnergy.App.SaliMax.System; using InnovEnergy.App.SaliMax.SystemConfig; +using InnovEnergy.App.SaliMax.VirtualDevices; using InnovEnergy.Lib.Devices.AMPT; using InnovEnergy.Lib.Devices.Battery48TL; using InnovEnergy.Lib.Devices.EmuMeter; @@ -19,9 +16,9 @@ using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc; using InnovEnergy.Lib.Protocols.Modbus.Channels; using InnovEnergy.Lib.Time.Unix; using InnovEnergy.Lib.Units; -using InnovEnergy.Lib.Units.Composite; using InnovEnergy.Lib.Utils; using static InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes.SystemConfig; +using AcPower = InnovEnergy.Lib.Units.Composite.AcPower; using Exception = System.Exception; #pragma warning disable IL2026 @@ -35,7 +32,7 @@ internal static class Program private const UInt32 UpdateIntervalSeconds = 2; - private static readonly Byte[] BatteryNodes = { 2, 3, 4, 5, 6 }; + private static readonly Byte[] BatteryNodes = { 2, 3, 4, 5, 6 }; private const String BatteryTty = "/dev/ttyUSB0"; // private const String RelaysIp = "10.0.1.1"; // "192.168.1.242"; @@ -92,25 +89,141 @@ internal static class Program .Select(n => new Battery48TlDevice(BatteriesChannel, n)) .ToList(); - var batteryDevices = new Battery48TlDevices(battery48TlDevices); - var acDcDevices = new TruConvertAcDcDevices(TruConvertAcChannel); - var dcDcDevices = new TruConvertDcDcDevices(TruConvertDcChannel); - var gridMeterDevice = new EmuMeterDevice(GridMeterChannel); - var criticalLoadMeterDevice = new EmuMeterDevice(AcOutLoadChannel); - var amptDevice = new AmptDevices(AmptChannel); - var saliMaxRelaysDevice = new RelaysDevice(RelaysChannel); + var batteryDevices = new Battery48TlDevices(battery48TlDevices); + var acDcDevices = new TruConvertAcDcDevices(TruConvertAcChannel); + var dcDcDevices = new TruConvertDcDcDevices(TruConvertDcChannel); + var gridMeterDevice = new EmuMeterDevice(GridMeterChannel); + var acIslandLoadMeter = new EmuMeterDevice(AcOutLoadChannel); + var amptDevice = new AmptDevices(AmptChannel); + var saliMaxRelaysDevice = new RelaysDevice(RelaysChannel); - StatusRecord ReadStatus() => new() + StatusRecord ReadStatus() { - AcDc = acDcDevices.Read(), - DcDc = dcDcDevices.Read(), - Battery = batteryDevices.Read(), - Relays = saliMaxRelaysDevice.Read(), - CriticalLoad = criticalLoadMeterDevice.Read(), - GridMeter = gridMeterDevice.Read(), - Mppt = amptDevice.Read(), - Config = Config.Load() // load from disk every iteration, so config can be changed while running - }; + var acDc = acDcDevices.Read(); + var dcDc = dcDcDevices.Read(); + var battery = batteryDevices.Read(); + var relays = saliMaxRelaysDevice.Read(); + var loadOnAcIsland = acIslandLoadMeter.Read(); + var gridMeter = gridMeterDevice.Read(); + var pvOnDc = amptDevice.Read(); + + + var pvOnAcGrid = AcDevicePower.Null; + var pvOnAcIsland = AcDevicePower.Null; + var loadOnAcGrid = pvOnAcGrid.Power + + pvOnAcIsland.Power + + (gridMeter is null ? AcPower.Null : gridMeter.Ac.Power) + + (loadOnAcIsland is null ? AcPower.Null : loadOnAcIsland.Ac.Power); + + + var dcPowers = new[] + { + acDc?.Dc.Power.Value, + pvOnDc?.Dc?.Power.Value, + dcDc?.Dc.Link.Power.Value + }; + + var loadOnDc = dcPowers.Any(p => p is null) + ? null + : new DcDevicePower { Power = dcPowers.Sum(p => p)!} ; + + + return new StatusRecord + { + AcDc = acDc ?? AcDcDevicesRecord.Null, + DcDc = dcDc ?? DcDcDevicesRecord.Null, + Battery = battery ?? Battery48TlRecords.Null, + Relays = relays, + GridMeter = gridMeter, + + PvOnAcGrid = pvOnAcGrid, + PvOnAcIsland = pvOnAcIsland, + PvOnDc = pvOnDc ?? AmptStatus.Null, + + LoadOnAcGrid = new AcDevicePower { Power = -loadOnAcGrid }, + LoadOnAcIsland = loadOnAcIsland, + LoadOnDc = loadOnDc, + + Config = Config.Load() // load from disk every iteration, so config can be changed while running + }; + } + + // async Task ReadStatus() + // { + // var acDcTask = Task.Run(() => acDcDevices.Read()); + // var dcDcTask = Task.Run(() => dcDcDevices.Read()); + // var batteryTask = Task.Run(() => batteryDevices.Read()); + // var relaysTask = Task.Run(() => saliMaxRelaysDevice.Read()); + // var loadOnAcIslandTask = Task.Run(() => acIslandLoadMeter.Read()); + // var gridMeterTask = Task.Run(() => gridMeterDevice.Read()); + // var pvOnDcTask = Task.Run(() => amptDevice.Read()); + // + // + // var timeout = Task.Delay(TimeSpan.FromSeconds(4)); + // var whenAll = Task + // .WhenAll + // ( + // acDcTask, + // dcDcTask, + // batteryTask, + // relaysTask, + // loadOnAcIslandTask, + // gridMeterTask, + // pvOnDcTask + // ); + // + // + // await Task.WhenAny(whenAll, timeout); + // + // var acDc = await acDcTask.ResultOrNull() ; + // var dcDc = await dcDcTask.ResultOrNull(); + // var battery = await batteryTask.ResultOrNull(); + // var relays = await relaysTask.ResultOrNull(); + // var loadOnAcIsland = await loadOnAcIslandTask.ResultOrNull(); + // var gridMeter = await gridMeterTask.ResultOrNull(); + // var pvOnDc = await pvOnDcTask.ResultOrNull(); + // + // + // var pvOnAcGrid = AcDevicePower.Null; + // var pvOnAcIsland = AcDevicePower.Null; + // var loadOnAcGrid = pvOnAcGrid.Power + + // pvOnAcIsland.Power + + // (gridMeter is null ? AcPower.Null : gridMeter.Ac.Power) + + // (loadOnAcIsland is null ? AcPower.Null : loadOnAcIsland.Ac.Power); + // + // + // var dcPowers = new[] + // { + // acDc?.Dc.Power.Value, + // pvOnDc?.Dc?.Power.Value, + // dcDc?.Dc.Link.Power.Value + // }; + // + // var loadOnDc = dcPowers.Any(p => p is null) + // ? null + // : new DcDevicePower { Power = dcPowers.Sum(p => p)!} ; + // + // + // return new StatusRecord + // { + // AcDc = acDc ?? AcDcDevicesRecord.Null, + // DcDc = dcDc ?? DcDcDevicesRecord.Null, + // Battery = battery ?? Battery48TlRecords.Null, + // Relays = relays, + // GridMeter = gridMeter, + // + // PvOnAcGrid = pvOnAcGrid, + // PvOnAcIsland = pvOnAcIsland, + // PvOnDc = pvOnDc ?? AmptStatus.Null, + // + // LoadOnAcGrid = new AcDevicePower { Power = -loadOnAcGrid }, + // LoadOnAcIsland = loadOnAcIsland, + // LoadOnDc = loadOnDc, + // + // Config = Config.Load() // load from disk every iteration, so config can be changed while running + // }; + // } + void WriteControl(StatusRecord r) { @@ -130,40 +243,64 @@ internal static class Program var t = UnixTime.FromTicks(UnixTime.Now.Ticks / 2 * 2); - t.ToUtcDateTime().WriteLine(); + //t.ToUtcDateTime().WriteLine(); var record = ReadStatus(); + var emuMeterRegisters = record.GridMeter; + if (emuMeterRegisters is not null) + { + emuMeterRegisters.Ac.Power.Active.WriteLine("Grid Active"); + emuMeterRegisters.Ac.Power.Reactive.WriteLine("Grid Reactive"); + } + record.AcDc.ResetAlarms(); - record.DcDc.ResetAlarms(); + record.DcDc.ResetAlarms(); + + record.ControlConstants(); record.ControlSystemState(); + Console.WriteLine($"{record.StateMachine.State}: {record.StateMachine.Message}"); + var essControl = record.ControlEss(); - record.Ess = essControl; + record.EssControl = essControl; record.AcDc.SystemControl.ApplyDefaultSettings(); record.DcDc.SystemControl.ApplyDefaultSettings(); DistributePower(record, essControl); - "===========================================".WriteLine(); - WriteControl(record); - + await UploadCsv(record, t); - var emuMeterRegisters = record.GridMeter; - if (emuMeterRegisters is not null) - { - emuMeterRegisters.Ac.Power.Active.WriteLine(); - emuMeterRegisters.Ac.Power.Reactive.WriteLine(); - } + record.Config.Save(); + + "===========================================".WriteLine(); } // ReSharper disable once FunctionNeverReturns } + private static async Task ResultOrNull(this Task task) + { + if (task.Status == TaskStatus.RanToCompletion) + return await task; + + return default; + } + + private static void ControlConstants(this StatusRecord r) + { + var inverters = r.AcDc.Devices; + + inverters.ForEach(d => d.Control.Dc.MaxVoltage = r.Config.MaxDcBusVoltage); + inverters.ForEach(d => d.Control.Dc.MinVoltage = r.Config.MinDcBusVoltage); + inverters.ForEach(d => d.Control.Dc.ReferenceVoltage = r.Config.ReferenceDcBusVoltage); + } + + private static void DistributePower(StatusRecord record, EssControl essControl) { var nInverters = record.AcDc.Devices.Count; @@ -173,8 +310,6 @@ internal static class Program : AcPower.Null; //var powerPerInverterPhase = AcPower.Null; - - powerPerInverterPhase.WriteLine("powerPerInverterPhase"); record.AcDc.Devices.ForEach(d => { @@ -194,7 +329,7 @@ internal static class Program sc.SystemConfig = AcDcAndDcDc; #if DEBUG - sc.CommunicationTimeout = TimeSpan.FromMinutes(10); + sc.CommunicationTimeout = TimeSpan.FromMinutes(2); #else sc.CommunicationTimeout = TimeSpan.FromSeconds(10); #endif @@ -234,13 +369,15 @@ internal static class Program private static async Task UploadCsv(StatusRecord status, UnixTime timeStamp) { - var csv = status.ToCsv(); + timeStamp.WriteLine(); + + var csv = status.ToCsv().WriteLine(); var s3Path = timeStamp + ".csv"; var request = S3Config.CreatePutRequest(s3Path); var response = await request.PutAsync(new StringContent(csv)); - csv.WriteLine(); - timeStamp.Ticks.WriteLine(); + //csv.WriteLine(); + //timeStamp.Ticks.WriteLine(); if (response.StatusCode != 200) {