using System.Reactive.Linq; using System.Reactive.Threading.Tasks; 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.Devices.Trumpf.TruConvertDc.Control; 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 { private static readonly UnixTimeSpan UpdateInterval = UnixTimeSpan.FromSeconds(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(); Watchdog.NotifyReady(); 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() { var acDc = acDcDevices.Read(); var dcDc = dcDcDevices.Read(); var relays = saliMaxRelaysDevice.Read(); var loadOnAcIsland = acIslandLoadMeter.Read(); var gridMeter = gridMeterDevice.Read(); var pvOnDc = amptDevice.Read(); var battery = batteryDevices.Read(); var pvOnAcGrid = new AcPowerDevice { Power = 0 }; // TODO var pvOnAcIsland = new AcPowerDevice { Power = 0 }; // TODO var gridBusToIslandBus = Topology.CalculateGridBusToIslandBusPower(pvOnAcIsland, loadOnAcIsland, acDc); var gridBusLoad = Topology.CalculateGridBusLoad(gridMeter, pvOnAcGrid, gridBusToIslandBus); var dcLoad = Topology.CalculateDcLoad(acDc, pvOnDc, dcDc); return new StatusRecord { AcDc = acDc , DcDc = dcDc , Battery = battery , Relays = relays, GridMeter = gridMeter, PvOnAcGrid = pvOnAcGrid, PvOnAcIsland = pvOnAcIsland, PvOnDc = pvOnDc, AcGridToAcIsland = gridBusToIslandBus, LoadOnAcGrid = gridBusLoad, LoadOnAcIsland = loadOnAcIsland, LoadOnDc = dcLoad, 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) { await Observable .Interval(UpdateInterval.ToTimeSpan()) .Select(_ => RunIteration()) .SelectMany(r => UploadCsv(r, UnixTime.Now.RoundTo(UpdateInterval))) .SelectError() .ToTask(); } // var iterations = from _ in Observable.Interval(UpdateInterval.ToTimeSpan()) // let t = UnixTime.Now.RoundTo(UpdateInterval) // let record = RunIteration() // from uploaded in UploadCsv(record, t) // select uploaded; // // using var running = iterations.Subscribe(); StatusRecord RunIteration() { Watchdog.NotifyAlive(); var record = ReadStatus(); int i = 1; // For Debug purpose, will be deleted after foreach (var device in record.Battery.Devices) { Console.WriteLine(" Battery " + i); device.TimeSinceTOC.WriteLine(" : Value of register 54"); device.Soc.WriteLine(" : SOC"); device.CalibrationChargeFlag.WriteLine(" : Calibration Charge Flag"); i++; "====================".WriteLine(); } record.ControlConstants(); record.ControlSystemState(); $"{record.StateMachine.State}: {record.StateMachine.Message}".LogInfo(); var essControl = record.ControlEss().LogInfo(); record.EssControl = essControl; record.AcDc.SystemControl.ApplyAcDcDefaultSettings(); record.DcDc.SystemControl.ApplyDcDcDefaultSettings(); DistributePower(record, essControl); WriteControl(record); record.CreateTopologyTextBlock().WriteLine(); (record.Relays is null ? "No relay Data available" : record.Relays.FiWarning ? "Alert: Fi Warning Detected" : "No Fi Warning Detected").WriteLine(); (record.Relays is null ? "No relay Data available" : record.Relays.FiError ? "Alert: Fi Error Detected" : "No Fi Error Detected") .WriteLine(); record.Config.Save(); "===========================================".LogInfo(); return record; } // 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; var dcDevices = r.DcDc.Devices; inverters.ForEach(d => d.Control.Dc.MaxVoltage = r.Config.MaxDcLinkVoltageFromAcDc); inverters.ForEach(d => d.Control.Dc.MinVoltage = r.Config.MinDcLinkVoltageFromAcDc); inverters.ForEach(d => d.Control.Dc.ReferenceVoltage = r.Config.ReferenceDcLinkVoltageFromAcDc); inverters.ForEach(d => d.Control.Dc.PrechargeConfig = DcPrechargeConfig.PrechargeDcWithInternal); dcDevices.ForEach(d => d.Control.DroopControl.UpperVoltage = r.Config.UpperDcLinkVoltageFromDc); dcDevices.ForEach(d => d.Control.DroopControl.LowerVoltage = r.Config.LowerDcLinkVoltageFromDc); dcDevices.ForEach(d => d.Control.DroopControl.ReferenceVoltage = r.Config.ReferenceDcLinkVoltageFromDc); dcDevices.ForEach(d => d.Control.CurrentControl.MaxBatteryChargingCurrent = r.Config.MaxBatteryChargingCurrent); dcDevices.ForEach(d => d.Control.CurrentControl.MaxBatteryDischargingCurrent = r.Config.MaxBatteryDischargingCurrent); dcDevices.ForEach(d => d.Control.VoltageLimits.MaxBatteryVoltage = r.Config.MaxChargeBatteryVoltage); dcDevices.ForEach(d => d.Control.VoltageLimits.MinBatteryVoltage = r.Config.MinDischargeBatteryVoltage); dcDevices.ForEach(d => d.Control.ControlMode = DcControlMode.VoltageDroop); 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 ? essControl.PowerSetpoint / nInverters / 3 : 0; //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 false; 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 = await response.GetStringAsync(); Console.WriteLine(error); } return true; } }