506 lines
21 KiB
C#
506 lines
21 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 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();
|
|
|
|
// Send the initial "service started" message to systemd
|
|
var sdNotifyReturn = sd_notify(0, "READY=1");
|
|
|
|
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()
|
|
{
|
|
"Reading AcDC".LogInfo();
|
|
var acDc = acDcDevices.Read();
|
|
|
|
"Reading dcDc".LogInfo();
|
|
var dcDc = dcDcDevices.Read();
|
|
|
|
"Reading battery".LogInfo();
|
|
var battery = batteryDevices.Read();
|
|
|
|
"Reading relays".LogInfo();
|
|
var relays = saliMaxRelaysDevice.Read();
|
|
|
|
"Reading loadOnAcIsland".LogInfo();
|
|
var loadOnAcIsland = acIslandLoadMeter.Read();
|
|
|
|
"Reading gridMeter".LogInfo();
|
|
var gridMeter = gridMeterDevice.Read();
|
|
|
|
"Reading pvOnDc".LogInfo();
|
|
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);
|
|
}
|
|
|
|
const Int32 delayTime = 10;
|
|
|
|
Console.WriteLine("press ctrl-C to stop");
|
|
while (true)
|
|
{
|
|
sd_notify(0, "WATCHDOG=1");
|
|
|
|
var t = UnixTime.Now;
|
|
while (t.Ticks % UpdateIntervalSeconds != 0)
|
|
{
|
|
await Task.Delay(delayTime);
|
|
t = UnixTime.Now;
|
|
}
|
|
|
|
|
|
|
|
var record = ReadStatus();
|
|
|
|
if (record.Relays is not null)
|
|
record.Relays.ToCsv().LogInfo();
|
|
|
|
record.ControlConstants();
|
|
|
|
record.ControlSystemState();
|
|
|
|
(t + "\n").LogInfo();
|
|
$"{record.StateMachine.State}: {record.StateMachine.Message}".LogInfo();
|
|
$"Batteries SOC: {record.Battery.Soc}".LogInfo();
|
|
var essControl = record.ControlEss().LogInfo();
|
|
|
|
record.EssControl = essControl;
|
|
|
|
record.AcDc.SystemControl.ApplyAcDcDefaultSettings();
|
|
record.DcDc.SystemControl.ApplyDcDcDefaultSettings();
|
|
|
|
DistributePower(record, essControl);
|
|
|
|
WriteControl(record);
|
|
|
|
PrintTopology(record);
|
|
|
|
await UploadCsv(record, t);
|
|
|
|
record.Config.Save();
|
|
|
|
"===========================================".LogInfo();
|
|
}
|
|
// ReSharper disable once FunctionNeverReturns
|
|
}
|
|
|
|
private static void PrintTopology(StatusRecord s)
|
|
{
|
|
// Power Measurement Values
|
|
var gridPower = s.GridMeter is not null ? s.GridMeter!.Ac.Power.Active : 0;
|
|
var inverterPower = s.AcDc.Ac.Power.Active;
|
|
var islandLoadPower = s.LoadOnAcIsland is not null ? s.LoadOnAcIsland.Ac.Power.Active : 0;
|
|
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;
|
|
|
|
TextBlock gridBusColumn;
|
|
TextBlock gridBox;
|
|
TextBlock totalBoxes;
|
|
|
|
|
|
if (s.GridMeter is not null)
|
|
{
|
|
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());
|
|
|
|
gridBusColumn = ColumnBox("Pv", "Grid Bus", "Load" , gridVoltageByPhase , gridLoadPower);
|
|
gridBox = TextBlock.AlignLeft(gridPowerByPhase).TitleBox("Grid");
|
|
|
|
}
|
|
else
|
|
{
|
|
gridBusColumn = TextBlock.Spacer(0);
|
|
gridBox = TextBlock.Spacer(0);
|
|
}
|
|
|
|
|
|
|
|
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.AlignCenterHorizontal("",
|
|
s.DcDc.Dc.Link.Voltage.ToDisplayString(),
|
|
"");
|
|
|
|
var dc48Voltage = s.DcDc.Dc.Battery.Voltage.ToDisplayString();
|
|
var batteryVoltage = s.Battery.Dc.Voltage.Value.RoundToSignificantDigits(3);
|
|
var batterySoc = s.Battery.Devices.Any()? s.Battery.Devices.Average(b=>b.Soc).Percent().ToDisplayString() : "0";
|
|
var batteryCurrent = s.Battery.Dc.Current.ToDisplayString();
|
|
var batteryTemp = s.Battery.Temperature.ToDisplayString();
|
|
var batteryHeatingCurrent = s.Battery.HeatingCurrent.ToDisplayString();
|
|
var anyBatteryAlarm = s.Battery.Alarms.Any();
|
|
var anyBatteryWarning = s.Battery.Warnings.Any();
|
|
|
|
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 inverterBox = TextBlock.AlignLeft(inverterPowerByAcDc).TitleBox("AC/DC");
|
|
var dcDcBox = TextBlock.AlignLeft(dc48Voltage).TitleBox("DC/DC");
|
|
var batteryAvgBox = TextBlock.AlignLeft(batteryVoltage,
|
|
batterySoc,
|
|
batteryCurrent,
|
|
batteryTemp,
|
|
batteryHeatingCurrent,
|
|
anyBatteryWarning,
|
|
anyBatteryAlarm)
|
|
.TitleBox("Battery");
|
|
|
|
|
|
//////////////////// Batteries /////////////////////////
|
|
|
|
IReadOnlyList<TextBlock> batteryBoxes = s.Battery
|
|
.Devices
|
|
.Select(CreateIndividualBattery)
|
|
.ToArray(s.Battery.Devices.Count);
|
|
|
|
var individualBatteries = batteryBoxes.Any()
|
|
? TextBlock.AlignLeft(batteryBoxes)
|
|
: TextBlock.Spacer(1);
|
|
|
|
if (s.GridMeter is not null)
|
|
{
|
|
totalBoxes = TextBlock.AlignCenterVertical(gridBox,
|
|
gridBusFlow,
|
|
gridBusColumn,
|
|
flowGridBusToIslandBus,
|
|
islandBusColumn,
|
|
flowIslandBusToInverter,
|
|
inverterBox,
|
|
flowInverterToDcBus,
|
|
dcBusColumn,
|
|
flowDcBusToDcDc,
|
|
dcDcBox,
|
|
flowDcDcToBattery,
|
|
batteryAvgBox,
|
|
individualBatteries);
|
|
}
|
|
else
|
|
{
|
|
totalBoxes = TextBlock.AlignCenterVertical(
|
|
islandBusColumn,
|
|
flowIslandBusToInverter,
|
|
inverterBox,
|
|
flowInverterToDcBus,
|
|
dcBusColumn,
|
|
flowDcBusToDcDc,
|
|
dcDcBox,
|
|
flowDcDcToBattery,
|
|
batteryAvgBox,
|
|
individualBatteries);
|
|
}
|
|
|
|
totalBoxes.WriteLine();
|
|
}
|
|
|
|
private static TextBlock CreateIndividualBattery(Battery48TlRecord battery, Int32 i)
|
|
{
|
|
var batteryWarnings = battery.Warnings.Any();
|
|
var batteryAlarms = battery.Alarms.Any();
|
|
|
|
var content = TextBlock.AlignLeft(battery.Dc.Voltage.ToDisplayString(),
|
|
battery.Soc.ToDisplayString(),
|
|
battery.Dc.Current.ToDisplayString(),
|
|
battery.Temperatures.Cells.Average.ToDisplayString(),
|
|
// battery.BusCurrent.ToDisplayString(),
|
|
battery.HeatingCurrent.ToDisplayString(),
|
|
batteryWarnings,
|
|
batteryAlarms);
|
|
|
|
var box = content.TitleBox($"Battery {i + 1}");
|
|
|
|
var flow = Flow.Horizontal(battery.Dc.Power);
|
|
|
|
return TextBlock.AlignCenterVertical(flow, box);
|
|
}
|
|
|
|
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.FromString(pvTitle).Box();
|
|
var pvToBus = Flow.Vertical(pvPower);
|
|
var busBox = TextBlock.AlignLeft(dataBox).TitleBox(busTitle);
|
|
var busToLoad = Flow.Vertical(loadPower);
|
|
var loadBox = TextBlock.FromString(loadTitle).Box();
|
|
|
|
return TextBlock.AlignCenterHorizontal(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;
|
|
var dcDevices = r.DcDc.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);
|
|
|
|
// dcDevices.ForEach(d => d.Control. Dc.MaxVoltage = r.Config.MaxDcBusVoltage);
|
|
// dcDevices.ForEach(d => d.Control. Dc.MinVoltage = r.Config.MinDcBusVoltage);
|
|
// dcDevices.ForEach(d => d.Control. Dc.ReferenceVoltage = r.Config.ReferenceDcBusVoltage);
|
|
|
|
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 UploadCsv(StatusRecord status, UnixTime timeStamp)
|
|
{
|
|
var s3Config = status.Config.S3;
|
|
if (s3Config is null)
|
|
return;
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|