398 lines
17 KiB
C#
398 lines
17 KiB
C#
using System.Reactive.Linq;
|
|
using System.Reactive.Threading.Tasks;
|
|
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;
|
|
using InnovEnergy.Lib.Devices.Trumpf.SystemControl;
|
|
using InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes;
|
|
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
|
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc.DataTypes;
|
|
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
|
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc.Control;
|
|
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
|
using InnovEnergy.Lib.Time.Unix;
|
|
using InnovEnergy.Lib.Units;
|
|
using InnovEnergy.Lib.Units.Power;
|
|
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
|
|
|
|
namespace InnovEnergy.App.SaliMax;
|
|
|
|
internal static class Program
|
|
{
|
|
private static readonly UnixTimeSpan UpdateInterval = UnixTimeSpan.FromSeconds(2);
|
|
|
|
private static readonly IReadOnlyList<Byte> BatteryNodes;
|
|
|
|
private static readonly TcpChannel TruConvertAcChannel ;
|
|
private static readonly TcpChannel TruConvertDcChannel ;
|
|
private static readonly TcpChannel GridMeterChannel ;
|
|
private static readonly TcpChannel IslandBusLoadChannel;
|
|
private static readonly TcpChannel AmptChannel ;
|
|
private static readonly TcpChannel RelaysChannel ;
|
|
private static readonly TcpChannel BatteriesChannel ;
|
|
|
|
static Program()
|
|
{
|
|
var config = Config.Load();
|
|
var d = config.Devices;
|
|
|
|
TruConvertAcChannel = new TcpChannel(d.TruConvertAcIp);
|
|
TruConvertDcChannel = new TcpChannel(d.TruConvertDcIp);
|
|
GridMeterChannel = new TcpChannel(d.GridMeterIp);
|
|
IslandBusLoadChannel = new TcpChannel(d.IslandBusLoadMeterIp);
|
|
AmptChannel = new TcpChannel(d.AmptIp);
|
|
RelaysChannel = new TcpChannel(d.RelaysIp);
|
|
BatteriesChannel = new TcpChannel(d.BatteryIp);
|
|
|
|
BatteryNodes = config
|
|
.Devices
|
|
.BatteryNodes
|
|
.Select(n => n.ConvertTo<Byte>())
|
|
.ToArray(config.Devices.BatteryNodes.Length);
|
|
}
|
|
|
|
public static async Task Main(String[] args)
|
|
{
|
|
while (true)
|
|
{
|
|
try
|
|
{
|
|
await Run();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
e.LogError();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static async Task Run()
|
|
{
|
|
"Starting SaliMax".LogInfo();
|
|
|
|
Watchdog.NotifyReady();
|
|
|
|
var battery48TlDevices = BatteryNodes
|
|
.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 acIslandLoadMeter = new EmuMeterDevice(IslandBusLoadChannel);
|
|
var amptDevice = new AmptDevices(AmptChannel);
|
|
var saliMaxRelaysDevice = new RelaysDevice(RelaysChannel);
|
|
|
|
StatusRecord ReadStatus()
|
|
{
|
|
|
|
// ┌────┐ ┌────┐
|
|
// │ Pv │ │ Pv │ ┌────┐
|
|
// └────┘ └────┘ │ Pv │
|
|
// V V └────┘
|
|
// V V V
|
|
// (b) 0 W (e) 0 W V
|
|
// V V (i) 13.2 kW ┌────────────┐
|
|
// V V V │ Battery │
|
|
// ┌─────────┐ ┌──────────┐ ┌────────────┐ ┌─────────┐ V ├────────────┤
|
|
// │ Grid │ │ Grid Bus │ │ Island Bus │ │ AC/DC │ ┌────────┐ ┌───────┐ │ 52.3 V │
|
|
// ├─────────┤ -10.3 kW ├──────────┤ -11.7 kW ├────────────┤ -11.7 kW ├─────────┤ -11.7 kW │ Dc Bus │ 1008 W │ DC/DC │ 1008 W │ 99.1 % │
|
|
// │ -3205 W │<<<<<<<<<<│ 244 V │<<<<<<<<<<│ 244 V │<<<<<<<<<<│ -6646 W │<<<<<<<<<<├────────┤>>>>>>>>>>├───────┤>>>>>>>>>>│ 490 mA │
|
|
// │ -3507 W │ (a) │ 244 V │ (d) │ 244 V │ (g) │ -5071 W │ (h) │ 776 V │ (k) │ 56 V │ (l) │ 250 °C │
|
|
// │ -3605 W │ K1 │ 246 V │ K2 │ 246 V │ K3 └─────────┘ └────────┘ └───────┘ │ 445 A │
|
|
// └─────────┘ └──────────┘ └────────────┘ V │ 0 Warnings │
|
|
// V V V │ 0 Alarms │
|
|
// V V (j) 0 W └────────────┘
|
|
// (c) 1400 W (f) 0 W V
|
|
// V V V
|
|
// V V ┌──────┐
|
|
// ┌──────┐ ┌──────┐ │ Load │
|
|
// │ Load │ │ Load │ └──────┘
|
|
// └──────┘ └──────┘
|
|
|
|
|
|
// AC side
|
|
// a + b - c - d = 0 [eq1]
|
|
// d + e - f - g = 0 [eq2]
|
|
//
|
|
// c & d are not measured!
|
|
//
|
|
// d = f + g - e [eq2]
|
|
// c = a + b - d [eq1]
|
|
//
|
|
// DC side
|
|
// h + i - j - k = 0 [eq3]
|
|
//
|
|
// g = h assuming no losses in ACDC
|
|
// k = l assuming no losses in DCDC
|
|
// j = h + i - k [eq3]
|
|
|
|
|
|
|
|
var acDc = acDcDevices.Read();
|
|
var dcDc = dcDcDevices.Read();
|
|
var relays = saliMaxRelaysDevice.Read();
|
|
var loadOnAcIsland = acIslandLoadMeter.Read();
|
|
var gridMeter = gridMeterDevice.Read();
|
|
var pvOnDc = amptDevice.Read();
|
|
var battery = batteryDevices.Read();
|
|
|
|
var pvOnAcGrid = AcPowerDevice.Null;
|
|
var pvOnAcIsland = AcPowerDevice.Null;
|
|
var gridPower = gridMeter is null ? AcPower.Null : gridMeter.Ac.Power;
|
|
var islandLoadPower = loadOnAcIsland is null ? AcPower.Null : loadOnAcIsland.Ac.Power;
|
|
var inverterAcPower = acDc.Ac.Power;
|
|
var inverterDcPower = acDc.Dc.Power;
|
|
|
|
var a = gridPower;
|
|
var b = pvOnAcGrid.Power;
|
|
var e = pvOnAcIsland.Power;
|
|
var f = islandLoadPower;
|
|
var g = inverterAcPower;
|
|
var h = inverterDcPower;
|
|
var i = pvOnDc?.Dc.Power;
|
|
var k = dcDc.Dc.Link.Power;
|
|
var l = k;
|
|
var j = Sum(h, i, -k);
|
|
var d = Sum(f, g, -e);
|
|
var c = Sum(a, b, -d);
|
|
|
|
return new StatusRecord
|
|
{
|
|
AcDc = acDc ,
|
|
DcDc = dcDc ,
|
|
Battery = battery ,
|
|
Relays = relays,
|
|
GridMeter = gridMeter,
|
|
|
|
PvOnAcGrid = pvOnAcGrid,
|
|
PvOnAcIsland = pvOnAcIsland,
|
|
PvOnDc = pvOnDc,
|
|
|
|
AcGridToAcIsland = new AcPowerDevice { Power = d },
|
|
LoadOnAcGrid = new AcPowerDevice { Power = c },
|
|
LoadOnAcIsland = loadOnAcIsland,
|
|
LoadOnDc = new DcPowerDevice { Power = j.Value},
|
|
|
|
StateMachine = StateMachine.Default,
|
|
EssControl = EssControl.Default,
|
|
Config = Config.Load() // load from disk every iteration, so config can be changed while running
|
|
};
|
|
}
|
|
|
|
void WriteControl(StatusRecord r)
|
|
{
|
|
if (r.Relays is not null)
|
|
saliMaxRelaysDevice.Write(r.Relays);
|
|
|
|
acDcDevices.Write(r.AcDc);
|
|
dcDcDevices.Write(r.DcDc);
|
|
}
|
|
|
|
Console.WriteLine("press ctrl-c to stop");
|
|
|
|
while (true)
|
|
{
|
|
await Observable
|
|
.Interval(UpdateInterval.ToTimeSpan())
|
|
.Select(_ => RunIteration())
|
|
.SelectMany(r => UploadCsv(r, UnixTime.Now.RoundTo(UpdateInterval)))
|
|
.SelectError()
|
|
.ToTask();
|
|
}
|
|
|
|
|
|
// var iterations = from _ in Observable.Interval(UpdateInterval.ToTimeSpan())
|
|
// let t = UnixTime.Now.RoundTo(UpdateInterval)
|
|
// let record = RunIteration()
|
|
// from uploaded in UploadCsv(record, t)
|
|
// select uploaded;
|
|
//
|
|
// using var running = iterations.Subscribe();
|
|
|
|
|
|
StatusRecord RunIteration()
|
|
{
|
|
Watchdog.NotifyAlive();
|
|
|
|
var t = UnixTime.Now;
|
|
var record = ReadStatus();
|
|
|
|
record.ControlConstants();
|
|
record.ControlSystemState();
|
|
|
|
$"{record.StateMachine.State}: {record.StateMachine.Message}".LogInfo();
|
|
|
|
var essControl = record.ControlEss().LogInfo();
|
|
|
|
record.EssControl = essControl;
|
|
|
|
record.AcDc.SystemControl.ApplyAcDcDefaultSettings();
|
|
record.DcDc.SystemControl.ApplyDcDcDefaultSettings();
|
|
|
|
DistributePower(record, essControl);
|
|
|
|
WriteControl(record);
|
|
|
|
record.CreateTopology().WriteLine();
|
|
|
|
//record.ToCsv().WriteLine();
|
|
|
|
//await UploadCsv(record, t);
|
|
|
|
record.Config.Save();
|
|
|
|
"===========================================".LogInfo();
|
|
|
|
return record;
|
|
}
|
|
|
|
// ReSharper disable once FunctionNeverReturns
|
|
}
|
|
|
|
|
|
|
|
private static async Task<T?> ResultOrNull<T>(this Task<T> task)
|
|
{
|
|
if (task.Status == TaskStatus.RanToCompletion)
|
|
return await task;
|
|
|
|
return default;
|
|
}
|
|
|
|
private static void ControlConstants(this StatusRecord r)
|
|
{
|
|
var inverters = r.AcDc.Devices;
|
|
var dcDevices = r.DcDc.Devices;
|
|
|
|
inverters.ForEach(d => d.Control.Dc.MaxVoltage = r.Config.MaxDcLinkVoltageFromAcDc);
|
|
inverters.ForEach(d => d.Control.Dc.MinVoltage = r.Config.MinDcLinkVoltageFromAcDc);
|
|
inverters.ForEach(d => d.Control.Dc.ReferenceVoltage = r.Config.ReferenceDcLinkVoltageFromAcDc);
|
|
inverters.ForEach(d => d.Control.Dc.PrechargeConfig = DcPrechargeConfig.PrechargeDcWithInternal);
|
|
|
|
dcDevices.ForEach(d => d.Control.DroopControl.UpperVoltage = r.Config.UpperDcLinkVoltageFromDc);
|
|
dcDevices.ForEach(d => d.Control.DroopControl.LowerVoltage = r.Config.LowerDcLinkVoltageFromDc);
|
|
dcDevices.ForEach(d => d.Control.DroopControl.ReferenceVoltage = r.Config.ReferenceDcLinkVoltageFromDc);
|
|
dcDevices.ForEach(d => d.Control.CurrentControl.MaxBatteryChargingCurrent = r.Config.MaxBatteryChargingCurrent);
|
|
dcDevices.ForEach(d => d.Control.CurrentControl.MaxBatteryDischargingCurrent = r.Config.MaxBatteryDischargingCurrent);
|
|
dcDevices.ForEach(d => d.Control.VoltageLimits.MaxBatteryVoltage = r.Config.MaxChargeBatteryVoltage);
|
|
dcDevices.ForEach(d => d.Control.VoltageLimits.MinBatteryVoltage = r.Config.MinDischargeBatteryVoltage);
|
|
dcDevices.ForEach(d => d.Control.ControlMode = DcControlMode.VoltageDroop);
|
|
|
|
r.DcDc.ResetAlarms();
|
|
r.AcDc.ResetAlarms();
|
|
}
|
|
|
|
|
|
// why this is not in Controller?
|
|
private static void DistributePower(StatusRecord record, EssControl essControl)
|
|
{
|
|
var nInverters = record.AcDc.Devices.Count;
|
|
|
|
var powerPerInverterPhase = nInverters > 0
|
|
? AcPower.FromActiveReactive(essControl.PowerSetpoint / nInverters / 3, 0)
|
|
: AcPower.Null;
|
|
|
|
//var powerPerInverterPhase = AcPower.Null;
|
|
|
|
record.AcDc.Devices.ForEach(d =>
|
|
{
|
|
d.Control.Ac.PhaseControl = PhaseControl.Asymmetric;
|
|
d.Control.Ac.Power.L1 = powerPerInverterPhase;
|
|
d.Control.Ac.Power.L2 = powerPerInverterPhase;
|
|
d.Control.Ac.Power.L3 = powerPerInverterPhase;
|
|
});
|
|
}
|
|
|
|
private static void ApplyAcDcDefaultSettings(this SystemControlRegisters? sc)
|
|
{
|
|
if (sc is null)
|
|
return;
|
|
|
|
sc.ReferenceFrame = ReferenceFrame.Consumer;
|
|
sc.SystemConfig = AcDcAndDcDc;
|
|
|
|
#if DEBUG
|
|
sc.CommunicationTimeout = TimeSpan.FromMinutes(2);
|
|
#else
|
|
sc.CommunicationTimeout = TimeSpan.FromSeconds(20);
|
|
#endif
|
|
|
|
sc.PowerSetPointActivation = PowerSetPointActivation.Immediate;
|
|
sc.UseSlaveIdForAddressing = true;
|
|
sc.SlaveErrorHandling = SlaveErrorHandling.Relaxed;
|
|
sc.SubSlaveErrorHandling = SubSlaveErrorHandling.Off;
|
|
|
|
sc.ResetAlarmsAndWarnings = true;
|
|
}
|
|
|
|
private static void ApplyDcDcDefaultSettings(this SystemControlRegisters? sc)
|
|
{
|
|
|
|
if (sc is null)
|
|
return;
|
|
|
|
sc.SystemConfig = DcDcOnly;
|
|
#if DEBUG
|
|
sc.CommunicationTimeout = TimeSpan.FromMinutes(2);
|
|
#else
|
|
sc.CommunicationTimeout = TimeSpan.FromSeconds(20);
|
|
#endif
|
|
|
|
sc.PowerSetPointActivation = PowerSetPointActivation.Immediate;
|
|
sc.UseSlaveIdForAddressing = true;
|
|
sc.SlaveErrorHandling = SlaveErrorHandling.Relaxed;
|
|
sc.SubSlaveErrorHandling = SubSlaveErrorHandling.Off;
|
|
|
|
sc.ResetAlarmsAndWarnings = true;
|
|
}
|
|
|
|
private static async Task<Boolean> UploadCsv(StatusRecord status, UnixTime timeStamp)
|
|
{
|
|
var s3Config = status.Config.S3;
|
|
if (s3Config is null)
|
|
return false;
|
|
|
|
var csv = status.ToCsv();
|
|
var s3Path = timeStamp + ".csv";
|
|
var request = s3Config.CreatePutRequest(s3Path);
|
|
var response = await request.PutAsync(new StringContent(csv));
|
|
|
|
if (response.StatusCode != 200)
|
|
{
|
|
Console.WriteLine("ERROR: PUT");
|
|
var error = await response.GetStringAsync();
|
|
Console.WriteLine(error);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static ActivePower? Sum(ActivePower? e, ActivePower? f, ActivePower? g)
|
|
{
|
|
if (e is null || f is null || g is null)
|
|
return null;
|
|
|
|
return f + g + e;
|
|
}
|
|
|
|
private static AcPower? Sum(AcPower? e, AcPower? f, AcPower? g)
|
|
{
|
|
if (e is null || f is null || g is null)
|
|
return null;
|
|
|
|
return f + g + e;
|
|
}
|
|
} |