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 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()) .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 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 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; 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); } } }