using System.Diagnostics; using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; 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.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.Composite; using InnovEnergy.Lib.Utils; using static InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes.SystemConfig; 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 criticalLoadMeterDevice = new EmuMeterDevice(AcOutLoadChannel); var amptDevice = new AmptDevices(AmptChannel); var saliMaxRelaysDevice = new RelaysDevice(RelaysChannel); StatusRecord ReadStatus() => new() { 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 }; 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(); 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); "===========================================".WriteLine(); WriteControl(record); await UploadCsv(record, t); var emuMeterRegisters = record.GridMeter; if (emuMeterRegisters is not null) { emuMeterRegisters.Ac.Power.Active.WriteLine(); emuMeterRegisters.Ac.Power.Reactive.WriteLine(); } } // ReSharper disable once FunctionNeverReturns } 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; 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; }); } 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(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; } 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) { 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); } } }