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

420 lines
18 KiB
C#

using System.Runtime.InteropServices;
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.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
{
[DllImport("libsystemd.so.0")]
private static extern Int32 sd_notify(Int32 unsetEnvironment, String state);
private const UInt32 UpdateIntervalSeconds = 2;
private static readonly Byte[] BatteryNodes = { 2, 3, 4, 5, 6 };
private const String BatteryTty = "/dev/ttyUSB0";
private static readonly TcpChannel RelaysChannel = new TcpChannel("10.0.1.1", 502); // "192.168.1.242";
private static readonly TcpChannel TruConvertAcChannel = new TcpChannel("10.0.2.1", 502); // "192.168.1.2";
private static readonly TcpChannel TruConvertDcChannel = new TcpChannel("10.0.3.1", 502); // "192.168.1.3";
private static readonly TcpChannel GridMeterChannel = new TcpChannel("10.0.4.1", 502); // "192.168.1.241";
private static readonly TcpChannel AcOutLoadChannel = new TcpChannel("10.0.4.2", 502); // "192.168.1.241";
private static readonly TcpChannel AmptChannel = new TcpChannel("10.0.5.1", 502); // "192.168.1.249";
//private static readonly TcpChannel TruConvertAcChannel = new TcpChannel("localhost", 5001);
//private static readonly TcpChannel TruConvertDcChannel = new TcpChannel("localhost", 5002);
//private static readonly TcpChannel GridMeterChannel = new TcpChannel("localhost", 5003);
//private static readonly TcpChannel AcOutLoadChannel = new TcpChannel("localhost", 5004);
//private static readonly TcpChannel AmptChannel = new TcpChannel("localhost", 5005);
//private static readonly TcpChannel RelaysChannel = new TcpChannel("localhost", 5006);
//private static readonly TcpChannel BatteriesChannel = new TcpChannel("localhost", 5007);
private static readonly S3Config S3Config = new S3Config
{
Bucket = "saliomameiringen",
Region = "sos-ch-dk-2",
Provider = "exo.io",
ContentType = "text/plain; charset=utf-8",
Key = "EXO2bf0cbd97fbfa75aa36ed46f",
Secret = "Bn1CDPqOG-XpDSbYjfIJxojcHTm391vZTc8z8l_fEPs"
};
public static async Task Main(String[] args)
{
while (true)
{
try
{
await Run();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
private static async Task Run()
{
Console.WriteLine("Starting SaliMax");
// Send the initial "service started" message to systemd
var sdNotifyReturn = sd_notify(0, "READY=1");
var battery48TlDevices = BatteryNodes
.Select(n => new Battery48TlDevice(BatteryTty, 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(AcOutLoadChannel);
var amptDevice = new AmptDevices(AmptChannel);
var saliMaxRelaysDevice = new RelaysDevice(RelaysChannel);
StatusRecord ReadStatus()
{
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 = 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 ?? AcDcDevicesRecord.Null,
DcDc = dcDc ?? DcDcDevicesRecord.Null,
Battery = battery ?? Battery48TlRecords.Null,
Relays = relays,
GridMeter = gridMeter,
PvOnAcGrid = pvOnAcGrid,
PvOnAcIsland = pvOnAcIsland,
PvOnDc = pvOnDc ?? AmptStatus.Null,
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)
{
sd_notify(0, "WATCHDOG=1");
var t = UnixTime.FromTicks(UnixTime.Now.Ticks / 2 * 2);
//t.ToUtcDateTime().WriteLine();
var record = ReadStatus();
PrintTopology(record);
if (record.Relays is not null)
record.Relays.ToCsv().WriteLine();
var emuMeterRegisters = record.GridMeter;
if (emuMeterRegisters is not null)
{
emuMeterRegisters.Ac.Power.Active.WriteLine("Grid Active");
//emuMeterRegisters.Ac.Power.Reactive.WriteLine("Grid Reactive");
}
record.ControlConstants();
record.ControlSystemState();
Console.WriteLine($"{record.StateMachine.State}: {record.StateMachine.Message}");
var essControl = record.ControlEss().WriteLine();
record.EssControl = essControl;
record.AcDc.SystemControl.ApplyAcDcDefaultSettings();
record.DcDc.SystemControl.ApplyDcDcDefaultSettings();
DistributePower(record, essControl);
WriteControl(record);
await UploadCsv(record, t);
record.Config.Save();
"===========================================".WriteLine();
}
// ReSharper disable once FunctionNeverReturns
}
private static void PrintTopology(StatusRecord s)
{
// Power Measurement Values
var gridPower = s.GridMeter!.Ac.Power.Active;
var inverterPower = s.AcDc.Ac.Power.Active;
var islandLoadPower = s.LoadOnAcIsland is null ? 0 : s.LoadOnAcIsland.Ac.Power.Active;
var dcBatteryPower = s.DcDc.Dc.Battery.Power;
var dcdcPower = s.DcDc.Dc.Link.Power;
var pvOnDcPower = s.PvOnDc.Dc!.Power.Value;
// Power Calculated Values
var islandToGridBusPower = inverterPower + islandLoadPower;
var gridLoadPower = s.LoadOnAcGrid is null ? 0: s.LoadOnAcGrid.Power.Active;
var gridPowerByPhase = TextBlock.AlignLeft(s.GridMeter.Ac.L1.Power.Active.ToDisplayString(),
s.GridMeter.Ac.L2.Power.Active.ToDisplayString(),
s.GridMeter.Ac.L3.Power.Active.ToDisplayString());
var gridVoltageByPhase = TextBlock.AlignLeft(s.GridMeter.Ac.L1.Voltage.ToDisplayString(),
s.GridMeter.Ac.L2.Voltage.ToDisplayString(),
s.GridMeter.Ac.L3.Voltage.ToDisplayString());
var inverterPowerByPhase = TextBlock.AlignLeft(s.AcDc.Ac.L1.Power.Active.ToDisplayString(),
s.AcDc.Ac.L2.Power.Active.ToDisplayString(),
s.AcDc.Ac.L3.Power.Active.ToDisplayString());
// ReSharper disable once CoVariantArrayConversion
var inverterPowerByAcDc = TextBlock.AlignLeft(s.AcDc.Devices
.Select(s1 => s1.Status.Ac.Power)
.ToArray());
var dcLinkVoltage = TextBlock.CenterHorizontal("",
s.DcDc.Dc.Link.Voltage.ToDisplayString(),
"");
//var inverterPowerByPhase = new ActivePower[(Int32)s.AcDc.Ac.L1.Power.Active, (Int32)s.AcDc.Ac.L2.Power.Active, (Int32)s.AcDc.Ac.L3.Power.Active];
// Voltage Measurement Values
//var inverterVoltage = new Voltage [(Int32)s.AcDc.Ac.L1.Voltage, (Int32)s.AcDc.Ac.L2.Voltage, (Int32)s.AcDc.Ac.L3.Voltage];
//var dcLinkVoltage = s.DcDc.Dc.Link.Voltage;
var dc48Voltage = s.DcDc.Dc.Battery.Voltage;
var batteryVoltage = s.Battery.Dc.Voltage;
var batterySoc = s.Battery.Soc;
var batteryCurrent = s.Battery.Dc.Current;
var batteryTemp = s.Battery.Temperature;
var gridBusColumn = ColumnBox("Pv", "Grid Bus", "Load" , gridVoltageByPhase , gridLoadPower);
var islandBusColumn = ColumnBox("Pv", "Island Bus", "Load" , inverterPowerByPhase, islandLoadPower);
var dcBusColumn = ColumnBox("Pv", "Dc Bus", "Load" , dcLinkVoltage, 0, pvOnDcPower);
var gridBusFlow = Flow.Horizontal(gridPower);
var flowGridBusToIslandBus = Flow.Horizontal((ActivePower)islandToGridBusPower);
var flowIslandBusToInverter = Flow.Horizontal(inverterPower);
var flowInverterToDcBus = Flow.Horizontal(inverterPower);
var flowDcBusToDcDc = Flow.Horizontal(dcdcPower);
var flowDcDcToBattery = Flow.Horizontal(dcBatteryPower);
var gridBox = TextBlock.AlignLeft(gridPowerByPhase).TitleBox("Grid");
var inverterBox = TextBlock.AlignLeft(inverterPowerByAcDc).TitleBox("Inverter");
var dcDcBox = TextBlock.AlignLeft(dc48Voltage).TitleBox("DC/DC");
var batteryBox = TextBlock.AlignLeft(batteryVoltage.ToDisplayString(), batterySoc.ToDisplayString(), batteryCurrent.ToDisplayString(), batteryTemp.ToDisplayString()).TitleBox("Battery");
var totalBoxes = TextBlock.CenterVertical(gridBox,
gridBusFlow,
gridBusColumn,
flowGridBusToIslandBus,
islandBusColumn,
flowIslandBusToInverter,
inverterBox,
flowInverterToDcBus,
dcBusColumn,
flowDcBusToDcDc,
dcDcBox,
flowDcDcToBattery,
batteryBox);
totalBoxes.WriteLine();
}
private static TextBlock ColumnBox(String pvTitle, String busTitle, String loadTitle, TextBlock dataBox)
{
return ColumnBox(pvTitle, busTitle, loadTitle, dataBox, 0);
}
private static TextBlock ColumnBox(String pvTitle, String busTitle, String loadTitle, TextBlock dataBox, ActivePower loadPower)
{
return ColumnBox(pvTitle, busTitle, loadTitle, dataBox, loadPower, 0);
}
private static TextBlock ColumnBox(String pvTitle, String busTitle, String loadTitle, TextBlock dataBox, ActivePower loadPower, ActivePower pvPower)
{
var pvBox = TextBlock.AlignLeft("").TitleBox(pvTitle);
var pvToBus = Flow.Vertical(pvPower);
var busBox = TextBlock.AlignLeft(dataBox).TitleBox(busTitle);
var busToLoad = Flow.Vertical(loadPower);
var loadBox = TextBlock.AlignLeft("").TitleBox(loadTitle);
return TextBlock.CenterHorizontal(pvBox, pvToBus, busBox, busToLoad, loadBox);
}
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;
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);
}
// 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 UploadCsv(StatusRecord status, UnixTime timeStamp)
{
timeStamp.WriteLine();
var csv = status.ToCsv();
var s3Path = timeStamp + ".csv";
var request = S3Config.CreatePutRequest(s3Path);
var response = await request.PutAsync(new StringContent(csv));
//csv.WriteLine();
//timeStamp.Ticks.WriteLine();
if (response.StatusCode != 200)
{
Console.WriteLine("ERROR: PUT");
var error = response.GetStringAsync();
Console.WriteLine(error);
}
}
}