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.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 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); 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(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(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 = AcDevicePower.Null; var pvOnAcIsland = AcDevicePower.Null; var loadOnAcGrid = pvOnAcGrid.Power + pvOnAcIsland.Power + (gridMeter is null ? AcPower.Null : gridMeter.Ac.Power) + (loadOnAcIsland is null ? AcPower.Null : loadOnAcIsland.Ac.Power); var dcPowers = new[] { acDc?.Dc.Power.Value, pvOnDc?.Dc?.Power.Value, dcDc?.Dc.Link.Power.Value }; var loadOnDc = dcPowers.Any(p => p is null) ? null : new DcDevicePower { Power = dcPowers.Sum(p => p)!} ; 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, LoadOnAcGrid = new AcDevicePower { Power = -loadOnAcGrid }, LoadOnAcIsland = loadOnAcIsland, LoadOnDc = loadOnDc, Config = Config.Load() // load from disk every iteration, so config can be changed while running }; } // async Task ReadStatus() // { // var acDcTask = Task.Run(() => acDcDevices.Read()); // var dcDcTask = Task.Run(() => dcDcDevices.Read()); // var batteryTask = Task.Run(() => batteryDevices.Read()); // var relaysTask = Task.Run(() => saliMaxRelaysDevice.Read()); // var loadOnAcIslandTask = Task.Run(() => acIslandLoadMeter.Read()); // var gridMeterTask = Task.Run(() => gridMeterDevice.Read()); // var pvOnDcTask = Task.Run(() => amptDevice.Read()); // // // var timeout = Task.Delay(TimeSpan.FromSeconds(4)); // var whenAll = Task // .WhenAll // ( // acDcTask, // dcDcTask, // batteryTask, // relaysTask, // loadOnAcIslandTask, // gridMeterTask, // pvOnDcTask // ); // // // await Task.WhenAny(whenAll, timeout); // // var acDc = await acDcTask.ResultOrNull() ; // var dcDc = await dcDcTask.ResultOrNull(); // var battery = await batteryTask.ResultOrNull(); // var relays = await relaysTask.ResultOrNull(); // var loadOnAcIsland = await loadOnAcIslandTask.ResultOrNull(); // var gridMeter = await gridMeterTask.ResultOrNull(); // var pvOnDc = await pvOnDcTask.ResultOrNull(); // // // var pvOnAcGrid = AcDevicePower.Null; // var pvOnAcIsland = AcDevicePower.Null; // var loadOnAcGrid = pvOnAcGrid.Power + // pvOnAcIsland.Power + // (gridMeter is null ? AcPower.Null : gridMeter.Ac.Power) + // (loadOnAcIsland is null ? AcPower.Null : loadOnAcIsland.Ac.Power); // // // var dcPowers = new[] // { // acDc?.Dc.Power.Value, // pvOnDc?.Dc?.Power.Value, // dcDc?.Dc.Link.Power.Value // }; // // var loadOnDc = dcPowers.Any(p => p is null) // ? null // : new DcDevicePower { Power = dcPowers.Sum(p => p)!} ; // // // 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, // // LoadOnAcGrid = new AcDevicePower { Power = -loadOnAcGrid }, // LoadOnAcIsland = loadOnAcIsland, // LoadOnDc = loadOnDc, // // 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(); var emuMeterRegisters = record.GridMeter; if (emuMeterRegisters is not null) { emuMeterRegisters.Ac.Power.Active.WriteLine("Grid Active"); emuMeterRegisters.Ac.Power.Reactive.WriteLine("Grid Reactive"); } record.AcDc.ResetAlarms(); record.DcDc.ResetAlarms(); record.ControlConstants(); record.ControlSystemState(); Console.WriteLine($"{record.StateMachine.State}: {record.StateMachine.Message}"); var essControl = record.ControlEss(); record.EssControl = essControl; record.AcDc.SystemControl.ApplyDefaultSettings(); record.DcDc.SystemControl.ApplyDefaultSettings(); DistributePower(record, essControl); WriteControl(record); await UploadCsv(record, t); record.Config.Save(); "===========================================".WriteLine(); } // ReSharper disable once FunctionNeverReturns } 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); } 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 ApplyDefaultSettings(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(10); #endif sc.PowerSetPointActivation = PowerSetPointActivation.Immediate; sc.UseSlaveIdForAddressing = true; sc.SlaveErrorHandling = SlaveErrorHandling.Relaxed; sc.SubSlaveErrorHandling = SubSlaveErrorHandling.Off; sc.ResetAlarmsAndWarnings = true; } private static DcDcDevicesRecord ResetAlarms(this DcDcDevicesRecord dcDcStatus) { 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; } private static AcDcDevicesRecord ResetAlarms(this AcDcDevicesRecord acDcRecord) { var sc = acDcRecord.SystemControl; if (sc is not null) sc.ResetAlarmsAndWarnings = sc.Alarms.Any() || sc.Warnings.Any(); foreach (var d in acDcRecord.Devices) d.Control.ResetAlarmsAndWarnings = d.Status.Alarms.Any() || d.Status.Warnings.Any(); return acDcRecord; } private static async Task UploadCsv(StatusRecord status, UnixTime timeStamp) { timeStamp.WriteLine(); var csv = status.ToCsv().WriteLine(); 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); } } }