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.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 battery = batteryDevices.Read(); 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 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 , DcDc = dcDc , Battery = battery , Relays = relays, GridMeter = gridMeter, PvOnAcGrid = pvOnAcGrid, PvOnAcIsland = pvOnAcIsland, PvOnDc = pvOnDc, 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) { 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 t = UnixTime.Now; var record = ReadStatus(); 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.CreateTopology().WriteLine(); //record.ToCsv().WriteLine(); //await UploadCsv(record, t); 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 ? 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 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 = response.GetStringAsync(); Console.WriteLine(error); } return true; } }