Innovenergy_trunk/csharp/App/SaliMax/src/Program.cs

341 lines
13 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.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()
{
var battery = batteryDevices.Read();
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 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 loadOnAcGrid = gridPower
+ pvOnAcGrid.Power
+ pvOnAcIsland.Power
- islandLoadPower
- inverterAcPower;
var gridBusToIslandBusPower = gridPower
+ pvOnAcGrid.Power
- loadOnAcGrid;
// var dcPower = acDc.Dc.Power.Value
// + pvOnDc.Dc?.Power.Value ?? 0
// - dcDc.Dc.Link.Power.Value;
var dcPower = 0;
var loadOnDc = new DcPowerDevice { Power = dcPower} ;
return new StatusRecord
{
AcDc = acDc ,
DcDc = dcDc ,
Battery = battery ,
Relays = relays,
GridMeter = gridMeter,
PvOnAcGrid = pvOnAcGrid,
PvOnAcIsland = pvOnAcIsland,
PvOnDc = pvOnDc,
AcGridToAcIsland = new AcPowerDevice { Power = gridBusToIslandBusPower },
LoadOnAcGrid = new AcPowerDevice { Power = loadOnAcGrid },
LoadOnAcIsland = loadOnAcIsland,
LoadOnDc = loadOnDc,
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 = response.GetStringAsync();
Console.WriteLine(error);
}
return true;
}
}