2023-02-16 12:57:06 +00:00
|
|
|
using System.Diagnostics;
|
2023-06-13 10:53:17 +00:00
|
|
|
using System.Runtime.InteropServices;
|
2023-02-16 12:57:06 +00:00
|
|
|
using System.Text.Json;
|
|
|
|
using System.Text.Json.Nodes;
|
|
|
|
using System.Text.Json.Serialization;
|
|
|
|
using Flurl.Http;
|
2023-06-13 10:53:17 +00:00
|
|
|
using InnovEnergy.App.SaliMax.Ess;
|
2023-02-25 14:53:58 +00:00
|
|
|
using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
2023-06-13 10:53:17 +00:00
|
|
|
using InnovEnergy.App.SaliMax.System;
|
2023-02-25 14:53:58 +00:00
|
|
|
using InnovEnergy.App.SaliMax.SystemConfig;
|
|
|
|
using InnovEnergy.Lib.Devices.AMPT;
|
|
|
|
using InnovEnergy.Lib.Devices.Battery48TL;
|
2023-02-16 12:57:06 +00:00
|
|
|
using InnovEnergy.Lib.Devices.EmuMeter;
|
2023-06-13 10:53:17 +00:00
|
|
|
using InnovEnergy.Lib.Devices.Trumpf.SystemControl;
|
|
|
|
using InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes;
|
2023-02-16 12:57:06 +00:00
|
|
|
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
2023-06-13 10:53:17 +00:00
|
|
|
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc.DataTypes;
|
2023-02-16 12:57:06 +00:00
|
|
|
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
2023-06-13 10:53:17 +00:00
|
|
|
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
2023-02-25 14:53:58 +00:00
|
|
|
using InnovEnergy.Lib.Time.Unix;
|
2023-06-13 10:53:17 +00:00
|
|
|
using InnovEnergy.Lib.Units;
|
|
|
|
using InnovEnergy.Lib.Units.Composite;
|
2023-04-04 14:37:37 +00:00
|
|
|
using InnovEnergy.Lib.Utils;
|
2023-06-13 10:53:17 +00:00
|
|
|
using static InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes.SystemConfig;
|
|
|
|
using Exception = System.Exception;
|
2023-02-23 12:45:09 +00:00
|
|
|
|
2023-02-16 12:57:06 +00:00
|
|
|
#pragma warning disable IL2026
|
|
|
|
|
2023-02-25 14:53:58 +00:00
|
|
|
namespace InnovEnergy.App.SaliMax;
|
2023-02-16 12:57:06 +00:00
|
|
|
|
|
|
|
internal static class Program
|
|
|
|
{
|
2023-06-13 10:53:17 +00:00
|
|
|
[DllImport("libsystemd.so.0")]
|
|
|
|
private static extern Int32 sd_notify(Int32 unsetEnvironment, String state);
|
|
|
|
|
2023-02-16 12:57:06 +00:00
|
|
|
private const UInt32 UpdateIntervalSeconds = 2;
|
2023-06-13 10:53:17 +00:00
|
|
|
|
|
|
|
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";
|
|
|
|
// private const String TruConvertAcIp = "10.0.2.1"; // "192.168.1.2";
|
|
|
|
// private const String TruConvertDcIp = "10.0.3.1"; // "192.168.1.3";
|
|
|
|
// private const String GridMeterIp = "10.0.4.1"; // "192.168.1.241";
|
|
|
|
// private const String InternalMeter = "10.0.4.2"; // "192.168.1.241";
|
|
|
|
// private const String AmptIp = "10.0.5.1"; // "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);
|
2023-02-23 12:45:09 +00:00
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
|
|
|
|
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"
|
|
|
|
};
|
|
|
|
|
2023-02-16 12:57:06 +00:00
|
|
|
public static async Task Main(String[] args)
|
|
|
|
{
|
2023-06-13 10:53:17 +00:00
|
|
|
while (true)
|
2023-02-16 12:57:06 +00:00
|
|
|
{
|
2023-06-13 10:53:17 +00:00
|
|
|
try
|
|
|
|
{
|
|
|
|
await Run();
|
|
|
|
}
|
|
|
|
catch (Exception e)
|
|
|
|
{
|
|
|
|
Console.WriteLine(e);
|
|
|
|
}
|
2023-02-16 12:57:06 +00:00
|
|
|
}
|
2023-06-13 10:53:17 +00:00
|
|
|
|
2023-02-16 12:57:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private static async Task Run()
|
|
|
|
{
|
|
|
|
Console.WriteLine("Starting SaliMax");
|
2023-04-04 14:37:37 +00:00
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
// Send the initial "service started" message to systemd
|
|
|
|
var sdNotifyReturn = sd_notify(0, "READY=1");
|
2023-04-04 14:37:37 +00:00
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
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 criticalLoadMeterDevice = new EmuMeterDevice(AcOutLoadChannel);
|
|
|
|
var amptDevice = new AmptDevices(AmptChannel);
|
|
|
|
var saliMaxRelaysDevice = new RelaysDevice(RelaysChannel);
|
|
|
|
|
|
|
|
StatusRecord ReadStatus() => new()
|
2023-02-16 12:57:06 +00:00
|
|
|
{
|
2023-06-13 10:53:17 +00:00
|
|
|
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
|
2023-02-16 12:57:06 +00:00
|
|
|
};
|
2023-04-04 14:37:37 +00:00
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
void WriteControl(StatusRecord r)
|
2023-02-16 12:57:06 +00:00
|
|
|
{
|
2023-06-13 10:53:17 +00:00
|
|
|
if (r.Relays is not null)
|
|
|
|
saliMaxRelaysDevice.Write(r.Relays);
|
2023-04-04 14:37:37 +00:00
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
acDcDevices.Write(r.AcDc);
|
|
|
|
dcDcDevices.Write(r.DcDc);
|
2023-02-16 12:57:06 +00:00
|
|
|
}
|
|
|
|
|
2023-02-23 12:45:09 +00:00
|
|
|
|
2023-02-16 12:57:06 +00:00
|
|
|
Console.WriteLine("press ctrl-C to stop");
|
2023-06-13 10:53:17 +00:00
|
|
|
|
2023-02-16 12:57:06 +00:00
|
|
|
while (true)
|
|
|
|
{
|
2023-06-13 10:53:17 +00:00
|
|
|
sd_notify(0, "WATCHDOG=1");
|
|
|
|
|
|
|
|
var t = UnixTime.FromTicks(UnixTime.Now.Ticks / 2 * 2);
|
|
|
|
|
|
|
|
t.ToUtcDateTime().WriteLine();
|
|
|
|
|
|
|
|
var record = ReadStatus();
|
|
|
|
|
|
|
|
record.AcDc.ResetAlarms();
|
|
|
|
record.DcDc.ResetAlarms();
|
|
|
|
|
|
|
|
record.ControlSystemState();
|
|
|
|
|
|
|
|
var essControl = record.ControlEss();
|
|
|
|
|
|
|
|
record.Ess = essControl;
|
|
|
|
|
|
|
|
record.AcDc.SystemControl.ApplyDefaultSettings();
|
|
|
|
record.DcDc.SystemControl.ApplyDefaultSettings();
|
|
|
|
|
|
|
|
DistributePower(record, essControl);
|
2023-02-23 12:45:09 +00:00
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
"===========================================".WriteLine();
|
2023-02-16 12:57:06 +00:00
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
WriteControl(record);
|
2023-02-16 12:57:06 +00:00
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
await UploadCsv(record, t);
|
2023-02-23 12:45:09 +00:00
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
var emuMeterRegisters = record.GridMeter;
|
|
|
|
if (emuMeterRegisters is not null)
|
|
|
|
{
|
|
|
|
emuMeterRegisters.Ac.Power.Active.WriteLine();
|
|
|
|
emuMeterRegisters.Ac.Power.Reactive.WriteLine();
|
|
|
|
}
|
2023-02-16 12:57:06 +00:00
|
|
|
}
|
|
|
|
// ReSharper disable once FunctionNeverReturns
|
|
|
|
}
|
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
private static void DistributePower(StatusRecord record, EssControl essControl)
|
2023-02-16 12:57:06 +00:00
|
|
|
{
|
2023-06-13 10:53:17 +00:00
|
|
|
var nInverters = record.AcDc.Devices.Count;
|
|
|
|
|
|
|
|
var powerPerInverterPhase = nInverters > 0
|
|
|
|
? AcPower.FromActiveReactive(essControl.PowerSetpoint / nInverters / 3, 0)
|
|
|
|
: AcPower.Null;
|
|
|
|
|
|
|
|
//var powerPerInverterPhase = AcPower.Null;
|
2023-02-16 12:57:06 +00:00
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
powerPerInverterPhase.WriteLine("powerPerInverterPhase");
|
|
|
|
|
|
|
|
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;
|
|
|
|
});
|
|
|
|
}
|
2023-02-16 12:57:06 +00:00
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
private static void ApplyDefaultSettings(this SystemControlRegisters? sc)
|
2023-02-16 12:57:06 +00:00
|
|
|
{
|
2023-06-13 10:53:17 +00:00
|
|
|
if (sc is null)
|
|
|
|
return;
|
|
|
|
|
|
|
|
sc.ReferenceFrame = ReferenceFrame.Consumer;
|
|
|
|
sc.SystemConfig = AcDcAndDcDc;
|
|
|
|
|
|
|
|
#if DEBUG
|
|
|
|
sc.CommunicationTimeout = TimeSpan.FromMinutes(10);
|
|
|
|
#else
|
|
|
|
sc.CommunicationTimeout = TimeSpan.FromSeconds(10);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
sc.PowerSetPointActivation = PowerSetPointActivation.Immediate;
|
|
|
|
sc.UseSlaveIdForAddressing = true;
|
|
|
|
sc.SlaveErrorHandling = SlaveErrorHandling.Relaxed;
|
|
|
|
sc.SubSlaveErrorHandling = SubSlaveErrorHandling.Off;
|
|
|
|
sc.ResetAlarmsAndWarnings = true;
|
2023-02-16 12:57:06 +00:00
|
|
|
}
|
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
private static DcDcDevicesRecord ResetAlarms(this DcDcDevicesRecord dcDcStatus)
|
2023-02-16 12:57:06 +00:00
|
|
|
{
|
2023-06-13 10:53:17 +00:00
|
|
|
var sc = dcDcStatus.SystemControl;
|
|
|
|
|
|
|
|
if (sc is not null)
|
|
|
|
sc.ResetAlarmsAndWarnings = sc.Alarms.Any();
|
|
|
|
|
|
|
|
foreach (var d in dcDcStatus.Devices)
|
|
|
|
d.Control.ResetAlarmsAndWarnings = d.Status.Alarms.Any() || d.Status.Warnings.Any();
|
|
|
|
|
|
|
|
return dcDcStatus;
|
2023-02-16 12:57:06 +00:00
|
|
|
}
|
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
private static AcDcDevicesRecord ResetAlarms(this AcDcDevicesRecord acDcRecord)
|
2023-02-16 12:57:06 +00:00
|
|
|
{
|
2023-06-13 10:53:17 +00:00
|
|
|
var sc = acDcRecord.SystemControl;
|
2023-02-16 12:57:06 +00:00
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
if (sc is not null)
|
|
|
|
sc.ResetAlarmsAndWarnings = sc.Alarms.Any() || sc.Warnings.Any();
|
2023-02-16 12:57:06 +00:00
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
foreach (var d in acDcRecord.Devices)
|
|
|
|
d.Control.ResetAlarmsAndWarnings = d.Status.Alarms.Any() || d.Status.Warnings.Any();
|
2023-02-16 12:57:06 +00:00
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
return acDcRecord;
|
2023-02-16 12:57:06 +00:00
|
|
|
}
|
2023-02-23 12:45:09 +00:00
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
private static async Task UploadCsv(StatusRecord status, UnixTime timeStamp)
|
2023-02-23 12:45:09 +00:00
|
|
|
{
|
2023-06-13 10:53:17 +00:00
|
|
|
var csv = status.ToCsv();
|
|
|
|
var s3Path = timeStamp + ".csv";
|
|
|
|
var request = S3Config.CreatePutRequest(s3Path);
|
|
|
|
var response = await request.PutAsync(new StringContent(csv));
|
2023-02-23 12:45:09 +00:00
|
|
|
|
2023-06-13 10:53:17 +00:00
|
|
|
csv.WriteLine();
|
|
|
|
timeStamp.Ticks.WriteLine();
|
|
|
|
|
2023-02-23 12:45:09 +00:00
|
|
|
if (response.StatusCode != 200)
|
|
|
|
{
|
|
|
|
Console.WriteLine("ERROR: PUT");
|
|
|
|
var error = response.GetStringAsync();
|
|
|
|
Console.WriteLine(error);
|
|
|
|
}
|
|
|
|
}
|
2023-06-13 10:53:17 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|