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 ResultOrNull(this Task 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); } } }