#define Amax #undef GridLimit using System.Diagnostics; using System.IO.Compression; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; using System.Reflection.Metadata; using System.Security; using System.Text; using Flurl.Http; using InnovEnergy.App.SaliMax.Devices; using InnovEnergy.App.SaliMax.Ess; using InnovEnergy.App.SaliMax.MiddlewareClasses; 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.Devices.Trumpf.TruConvertDc.Control; using InnovEnergy.Lib.Protocols.Modbus.Channels; using InnovEnergy.Lib.Units; using InnovEnergy.Lib.Utils; using InnovEnergy.App.SaliMax.DataTypes; using static System.Int32; using static InnovEnergy.App.SaliMax.AggregationService.Aggregator; using static InnovEnergy.App.SaliMax.MiddlewareClasses.MiddlewareAgent; using static InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes.SystemConfig; using DeviceState = InnovEnergy.App.SaliMax.Devices.DeviceState; #pragma warning disable IL2026 namespace InnovEnergy.App.SaliMax; internal static class Program { private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(2); private static readonly IReadOnlyList BatteryNodes; private static readonly Channel TruConvertAcChannel; private static readonly Channel TruConvertDcChannel; private static readonly Channel GridMeterChannel; private static readonly Channel IslandBusLoadChannel; private static readonly Channel PvOnDc; private static readonly Channel PvOnAcGrid; private static readonly Channel PvOnAcIsland; private static readonly Channel RelaysChannel; private static readonly Channel BatteriesChannel; private const String VpnServerIp = "10.2.0.11"; private static Boolean _subscribedToQueue = false; private static Boolean _subscribeToQueueForTheFirstTime = false; private static SalimaxAlarmState _prevSalimaxState = SalimaxAlarmState.Green; private const UInt16 NbrOfFileToConcatenate = 30; private static UInt16 _counterOfFile = 0; private static SalimaxAlarmState _salimaxAlarmState = SalimaxAlarmState.Green; static Program() { var config = Config.Load(); var d = config.Devices; Channel CreateChannel(SalimaxDevice device) => device.DeviceState == DeviceState.Disabled ? new NullChannel() : new TcpChannel(device); TruConvertAcChannel = CreateChannel(d.TruConvertAcIp); TruConvertDcChannel = CreateChannel(d.TruConvertDcIp); GridMeterChannel = CreateChannel(d.GridMeterIp); IslandBusLoadChannel = CreateChannel(d.IslandBusLoadMeterIp); PvOnDc = CreateChannel(d.PvOnDc); PvOnAcGrid = CreateChannel(d.PvOnAcGrid); PvOnAcIsland = CreateChannel(d.PvOnAcIsland); RelaysChannel = CreateChannel(d.RelaysIp); BatteriesChannel = CreateChannel(d.BatteryIp); BatteryNodes = config .Devices .BatteryNodes .Select(n => n.ConvertTo()) .ToArray(config.Devices.BatteryNodes.Length); } public static async Task Main(String[] args) { //Do not await HourlyDataAggregationManager() .ContinueWith(t=>t.Exception.WriteLine(), TaskContinuationOptions.OnlyOnFaulted) .SupressAwaitWarning(); DailyDataAggregationManager() .ContinueWith(t=>t.Exception.WriteLine(), TaskContinuationOptions.OnlyOnFaulted) .SupressAwaitWarning(); InitializeCommunicationToMiddleware(); while (true) { try { await Run(); } catch (Exception e) { e.LogError(); } } } private static async Task Run() { "Starting SaliMax".WriteLine(); 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 pvOnDcDevice = new AmptDevices(PvOnDc); var pvOnAcGridDevice = new AmptDevices(PvOnAcGrid); var pvOnAcIslandDevice = new AmptDevices(PvOnAcIsland); #if Amax var saliMaxRelaysDevice = new RelaysDeviceAmax(RelaysChannel); #else var saliMaxRelaysDevice = new RelaysDevice(RelaysChannel); #endif StatusRecord ReadStatus() { var config = Config.Load(); var devices = config.Devices; var acDc = acDcDevices.Read(); var dcDc = dcDcDevices.Read(); var relays = saliMaxRelaysDevice.Read(); var loadOnAcIsland = acIslandLoadMeter.Read(); var gridMeter = gridMeterDevice.Read(); var pvOnDc = pvOnDcDevice.Read(); var battery = batteryDevices.Read(); var pvOnAcGrid = pvOnAcGridDevice.Read(); var pvOnAcIsland = pvOnAcIslandDevice.Read(); var gridBusToIslandBus = Topology.CalculateGridBusToIslandBusPower(pvOnAcIsland, loadOnAcIsland, acDc); var gridBusLoad = devices.LoadOnAcGrid.DeviceState == DeviceState.Disabled ? new AcPowerDevice { Power = 0 } : Topology.CalculateGridBusLoad(gridMeter, pvOnAcGrid, gridBusToIslandBus); var dcLoad = devices.LoadOnDc.DeviceState == DeviceState.Disabled ? new DcPowerDevice { Power = 0 } : Topology.CalculateDcLoad(acDc, pvOnDc, dcDc); var acDcToDcLink = devices.LoadOnDc.DeviceState == DeviceState.Disabled ? Topology.CalculateAcDcToDcLink(pvOnDc, dcDc, acDc) : new DcPowerDevice{ Power = acDc.Dc.Power}; return new StatusRecord { AcDc = acDc, DcDc = dcDc, Battery = battery, Relays = relays, GridMeter = gridMeter, PvOnAcGrid = pvOnAcGrid, PvOnAcIsland = pvOnAcIsland, PvOnDc = pvOnDc, AcGridToAcIsland = gridBusToIslandBus, AcDcToDcLink = acDcToDcLink, LoadOnAcGrid = gridBusLoad, LoadOnAcIsland = loadOnAcIsland, LoadOnDc = dcLoad, StateMachine = StateMachine.Default, EssControl = EssControl.Default, Log = new SystemLog { SalimaxAlarmState = SalimaxAlarmState.Green, Message = null, SalimaxAlarms = null, SalimaxWarnings = null}, //TODO: Put real stuff Config = config // load from disk every iteration, so config can be changed while running }; } void WriteControl(StatusRecord r) { if (r.Relays is not null) #if Amax saliMaxRelaysDevice.Write((RelaysRecordAmax)r.Relays); #else ((RelaysDevice)saliMaxRelaysDevice).Write((RelaysRecord)r.Relays); #endif acDcDevices.Write(r.AcDc); dcDcDevices.Write(r.DcDc); } Console.WriteLine("press ctrl-c to stop"); while (true) { await Observable .Interval(UpdateInterval) .Select(_ => RunIteration()) .SelectMany(r => UploadCsv(r, DateTime.Now.Round(UpdateInterval))) .SelectError() .ToTask(); } StatusRecord RunIteration() { Watchdog.NotifyAlive(); var record = ReadStatus(); /******************************************** For Battery Debug *************************************/ var currentSalimaxState = GetSalimaxStateAlarm(record); SendSalimaxStateAlarm(currentSalimaxState, record); //record.ControlPvPower(record.Config.CurtailP); record.ControlConstants(); record.ControlSystemState(); var essControl = record.ControlEss().WriteLine(); record.EssControl = essControl; record.AcDc.SystemControl.ApplyAcDcDefaultSettings(); record.DcDc.SystemControl.ApplyDcDcDefaultSettings(); DistributePower(record, essControl); WriteControl(record); $"{DateTime.Now.Round(UpdateInterval).ToUnixTime()} : {record.StateMachine.State}: {record.StateMachine.Message}".WriteLine(); 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(); "===========================================".WriteLine(); return record; } // ReSharper disable once FunctionNeverReturns } private static void SendSalimaxStateAlarm(StatusMessage currentSalimaxState, StatusRecord record) { var s3Bucket = Config.Load().S3?.Bucket; var subscribedNow = false; //Every 15 iterations(30 seconds), the installation sends a heartbit message to the queue //_heartBitInterval++; //When the controller boots, it tries to subscribe to the queue if (_subscribeToQueueForTheFirstTime == false) { subscribedNow = true; _subscribeToQueueForTheFirstTime = true; _prevSalimaxState = currentSalimaxState.Status; _subscribedToQueue = RabbitMqManager.SubscribeToQueue(currentSalimaxState, s3Bucket, VpnServerIp); } //If already subscribed to the queue and the status has been changed, update the queue if (!subscribedNow && _subscribedToQueue && currentSalimaxState.Status != _prevSalimaxState) { _prevSalimaxState = currentSalimaxState.Status; if (s3Bucket != null) RabbitMqManager.InformMiddleware(currentSalimaxState); } // else if (_subscribedToQueue && _heartBitInterval >= 30) // { // //Send a heartbit to the backend // Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------"); // _heartBitInterval = 0; // currentSalimaxState.Type = MessageType.Heartbit; // // if (s3Bucket != null) // RabbitMqManager.InformMiddleware(currentSalimaxState); // } //If there is an available message from the RabbitMQ Broker, apply the configuration file Configuration? config = SetConfigurationFile(); if (config != null) { record.ApplyConfigFile(config); } } private static StatusMessage GetSalimaxStateAlarm(StatusRecord record) { var alarmCondition = record.DetectAlarmStates(); // this need to be emailed to support or customer var s3Bucket = Config.Load().S3?.Bucket; var alarmList = new List(); var warningList = new List(); var bAlarmList = new List(); var bWarningList = new List(); if (record.Battery != null) { var i = 0; foreach (var battery in record.Battery.Devices) { var devicesBatteryNode = record.Config.Devices.BatteryNodes[i]; if (battery.LimpBitMap == 0) { // "All String are Active".WriteLine(); } else if (IsPowerOfTwo(battery.LimpBitMap)) { "1 String is disabled".WriteLine(); warningList.Add(new AlarmOrWarning { Date = DateTime.Now.ToString("yyyy-MM-dd"), Time = DateTime.Now.ToString("HH:mm:ss"), CreatedBy = "Battery node" + devicesBatteryNode, Description = "1 String is disabled" }); bWarningList.Add("/"+i+1 + "/1 String is disabled"); // battery id instead ( i +1 ) of node id: requested from the frontend } else { "2 or more string are disabled".WriteLine(); alarmList.Add(new AlarmOrWarning { Date = DateTime.Now.ToString("yyyy-MM-dd"), Time = DateTime.Now.ToString("HH:mm:ss"), CreatedBy = "Battery node" + devicesBatteryNode, Description = "2 or more string are disabled" }); bAlarmList.Add(i +";2 or more string are disabled"); } foreach (var warning in record.Battery.Warnings) { warningList.Add(new AlarmOrWarning { Date = DateTime.Now.ToString("yyyy-MM-dd"), Time = DateTime.Now.ToString("HH:mm:ss"), CreatedBy = "Battery node" + devicesBatteryNode, Description = warning }); bWarningList.Add(i +";" + warning); } foreach (var alarm in battery.Alarms) { alarmList.Add(new AlarmOrWarning { Date = DateTime.Now.ToString("yyyy-MM-dd"), Time = DateTime.Now.ToString("HH:mm:ss"), CreatedBy = "Battery node" + devicesBatteryNode, Description = alarm }); bWarningList.Add(i +";" + alarm); } i++; } } if (alarmCondition is not null) { alarmCondition.WriteLine(); alarmList.Add(new AlarmOrWarning { Date = DateTime.Now.ToString("yyyy-MM-dd"), Time = DateTime.Now.ToString("HH:mm:ss"), CreatedBy = "Salimax", Description = alarmCondition }); } foreach (var alarm in record.AcDc.Alarms) { alarmList.Add(new AlarmOrWarning { Date = DateTime.Now.ToString("yyyy-MM-dd"), Time = DateTime.Now.ToString("HH:mm:ss"), CreatedBy = "AcDc", Description = alarm.ToString() }); } foreach (var alarm in record.DcDc.Alarms) { alarmList.Add(new AlarmOrWarning { Date = DateTime.Now.ToString("yyyy-MM-dd"), Time = DateTime.Now.ToString("HH:mm:ss"), CreatedBy = "DcDc", Description = alarm.ToString() }); } foreach (var warning in record.AcDc.Warnings) { warningList.Add(new AlarmOrWarning { Date = DateTime.Now.ToString("yyyy-MM-dd"), Time = DateTime.Now.ToString("HH:mm:ss"), CreatedBy = "AcDc", Description = warning.ToString() }); } foreach (var warning in record.DcDc.Warnings) { warningList.Add(new AlarmOrWarning { Date = DateTime.Now.ToString("yyyy-MM-dd"), Time = DateTime.Now.ToString("HH:mm:ss"), CreatedBy = "DcDc", Description = warning.ToString() }); } _salimaxAlarmState = warningList.Any() ? SalimaxAlarmState.Orange : SalimaxAlarmState.Green; // this will be replaced by LedState _salimaxAlarmState = alarmList.Any() ? SalimaxAlarmState.Red : _salimaxAlarmState; // this will be replaced by LedState TryParse(s3Bucket?.Split("-")[0], out var installationId); var returnedStatus = new StatusMessage { InstallationId = installationId, Product = 0, Status = _salimaxAlarmState, Type = MessageType.AlarmOrWarning, Alarms = alarmList, Warnings = warningList }; return returnedStatus; } private static String? DetectAlarmStates(this StatusRecord r) => r.Relays switch { { K2ConnectIslandBusToGridBus: false, K2IslandBusIsConnectedToGridBus: true } => " Contradiction: R0 is opening the K2 but the K2 is still close ", { K1GridBusIsConnectedToGrid : false, K2IslandBusIsConnectedToGridBus: true } => " Contradiction: K1 is open but the K2 is still close ", { FiError: true, K2IslandBusIsConnectedToGridBus: true } => " Contradiction: Fi error occured but the K2 is still close ", _ => null }; private static void ControlConstants(this StatusRecord r) { var inverters = r.AcDc.Devices; var dcDevices = r.DcDc.Devices; var configFile = r.Config; var maxBatteryDischargingCurrentLive = 0.0; // This adapting the max discharging current to the current Active Strings if (r.Battery != null) { const Int32 stringsByBattery = 5; var numberOfBatteriesConfigured = r.Config.Devices.BatteryNodes.Length; var numberOfTotalStrings = stringsByBattery * numberOfBatteriesConfigured; var dischargingCurrentByString = configFile.MaxBatteryDischargingCurrent / numberOfTotalStrings; var boolList = new List(); foreach (var stringActive in r.Battery.Devices.Select(b => b.BatteryStrings).ToList()) { boolList.Add(stringActive.String1Active); boolList.Add(stringActive.String2Active); boolList.Add(stringActive.String3Active); boolList.Add(stringActive.String4Active); boolList.Add(stringActive.String5Active); } var numberOfBatteriesStringActive = boolList.Count(b => b); if (numberOfTotalStrings != 0) { maxBatteryDischargingCurrentLive = dischargingCurrentByString * numberOfBatteriesStringActive; } } // TODO The discharging current is well calculated but not communicated to live. But Written in S3 var devicesConfig = r.AcDc.Devices.All(d => d.Control.Ac.GridType == GridType.GridTied400V50Hz) ? configFile.GridTie : configFile.IslandMode; // TODO if any of the grid tie mode inverters.ForEach(d => d.Control.Dc.MaxVoltage = devicesConfig.AcDc.MaxDcLinkVoltage); inverters.ForEach(d => d.Control.Dc.MinVoltage = devicesConfig.AcDc.MinDcLinkVoltage); inverters.ForEach(d => d.Control.Dc.ReferenceVoltage = devicesConfig.AcDc.ReferenceDcLinkVoltage); inverters.ForEach(d => d.Control.Dc.PrechargeConfig = DcPrechargeConfig.PrechargeDcWithInternal); dcDevices.ForEach(d => d.Control.DroopControl.UpperVoltage = devicesConfig.DcDc.UpperDcLinkVoltage); dcDevices.ForEach(d => d.Control.DroopControl.LowerVoltage = devicesConfig.DcDc.LowerDcLinkVoltage); dcDevices.ForEach(d => d.Control.DroopControl.ReferenceVoltage = devicesConfig.DcDc.ReferenceDcLinkVoltage); dcDevices.ForEach(d => d.Control.CurrentControl.MaxBatteryChargingCurrent = configFile.MaxBatteryChargingCurrent); dcDevices.ForEach(d => d.Control.CurrentControl.MaxBatteryDischargingCurrent = maxBatteryDischargingCurrentLive); dcDevices.ForEach(d => d.Control.MaxDcPower = configFile.MaxDcPower); dcDevices.ForEach(d => d.Control.VoltageLimits.MaxBatteryVoltage = configFile.MaxChargeBatteryVoltage); dcDevices.ForEach(d => d.Control.VoltageLimits.MinBatteryVoltage = configFile.MinDischargeBatteryVoltage); dcDevices.ForEach(d => d.Control.ControlMode = DcControlMode.VoltageDroop); r.DcDc.ResetAlarms(); r.AcDc.ResetAlarms(); } // This will be used for provider throttling, this example is only for either 100% or 0 % private static void ControlPvPower(this StatusRecord r, Int16 exportLimit = 100) { UInt16 stableFactor = 500; var inverters = r.AcDc.Devices; var dcDevices = r.DcDc.Devices; var configFile = r.Config; var systemProduction = inverters.Count * 25000; var limitSystemProduction = systemProduction * exportLimit / 100; var devicesConfig = r.AcDc.Devices.All(d => d.Control.Ac.GridType == GridType.GridTied400V50Hz) ? configFile.GridTie : configFile.IslandMode; // TODO if any of the grid tie mode var targetReferenceVoltage = devicesConfig.AcDc.ReferenceDcLinkVoltage; if (r.PvOnDc.Dc.Power != null && r.PvOnDc.Dc.Power > limitSystemProduction) { exportLimit.WriteLine(" exportLimit"); systemProduction.WriteLine(" systemProduction"); limitSystemProduction.WriteLine(" limitSystemexport"); targetReferenceVoltage.WriteLine("targetReferenceVoltage"); if (r.GridMeter?.Ac.Power.Active != null) { if (r.GridMeter.Ac.Power.Active < -limitSystemProduction) { "We are openning the window".WriteLine(); r.Config.GridSetPoint = -limitSystemProduction + stableFactor; r.Config.GridSetPoint.WriteLine(" Grid set point"); var maxDcLinkVoltage = (UInt16)(r.Config.GridTie.AcDc.MaxDcLinkVoltage + 10); var minDcLinkVoltage = (UInt16)(r.Config.GridTie.AcDc.MinDcLinkVoltage - 10); var maxDcDcLinkVoltage = (UInt16)(r.Config.GridTie.DcDc.UpperDcLinkVoltage + 10); var minDcDcLinkVoltage = (UInt16)(r.Config.GridTie.DcDc.LowerDcLinkVoltage + 10); r.Config.GridTie.AcDc.MaxDcLinkVoltage = maxDcLinkVoltage; r.Config.GridTie.AcDc.MinDcLinkVoltage = minDcLinkVoltage; r.Config.GridTie.DcDc.UpperDcLinkVoltage = maxDcDcLinkVoltage; r.Config.GridTie.DcDc.LowerDcLinkVoltage = minDcDcLinkVoltage; maxDcLinkVoltage.WriteLine("maxDcLinkVoltage"); minDcLinkVoltage.WriteLine("minxDcLinkVoltage"); maxDcDcLinkVoltage.WriteLine("maxDcDcLinkVoltage"); minDcDcLinkVoltage.WriteLine("minDcDcLinkVoltage"); } else if (r.GridMeter.Ac.Power.Active > -limitSystemProduction + stableFactor * 2) { "We are closing the window".WriteLine(); r.Config.GridSetPoint = -limitSystemProduction + stableFactor; r.Config.GridSetPoint.WriteLine(" Grid set point"); if ((r.Config.GridTie.AcDc.MaxDcLinkVoltage - r.Config.GridTie.AcDc.MinDcLinkVoltage) > 60) { var maxDcLinkVoltage = (UInt16)(r.Config.GridTie.AcDc.MaxDcLinkVoltage - 10); var minDcLinkVoltage = (UInt16)(r.Config.GridTie.AcDc.MinDcLinkVoltage + 10); var maxDcDcLinkVoltage = (UInt16)(r.Config.GridTie.DcDc.UpperDcLinkVoltage - 10); var minDcDcLinkVoltage = (UInt16)(r.Config.GridTie.DcDc.LowerDcLinkVoltage - 10); r.Config.GridTie.AcDc.MaxDcLinkVoltage = maxDcLinkVoltage; r.Config.GridTie.AcDc.MinDcLinkVoltage = minDcLinkVoltage; r.Config.GridTie.DcDc.UpperDcLinkVoltage = maxDcDcLinkVoltage; r.Config.GridTie.DcDc.LowerDcLinkVoltage = minDcDcLinkVoltage; maxDcLinkVoltage.WriteLine("maxDcLinkVoltage"); minDcLinkVoltage.WriteLine("minxDcLinkVoltage"); maxDcDcLinkVoltage.WriteLine("maxDcDcLinkVoltage"); minDcDcLinkVoltage.WriteLine("minDcDcLinkVoltage"); } else { "do nothing".WriteLine(); } } else { r.Config.GridTie.AcDc.MaxDcLinkVoltage.WriteLine("maxDcLinkVoltage"); r.Config.GridTie.AcDc.MinDcLinkVoltage.WriteLine("minxDcLinkVoltage"); r.Config.GridTie.DcDc.UpperDcLinkVoltage.WriteLine("maxDcDcLinkVoltage"); r.Config.GridTie.DcDc.LowerDcLinkVoltage.WriteLine("minDcDcLinkVoltage"); } } if (exportLimit == 100) { r.Config.GridSetPoint = 0; } } } // 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; 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, DateTime timeStamp) { var csv = status.ToCsv().LogInfo(); await RestApiSavingFile(csv); var s3Config = status.Config.S3; if (s3Config is null) return false; //Concatenating 15 files in one file return await ConcatinatingAndCompressingFiles(timeStamp, s3Config); } private static async Task ConcatinatingAndCompressingFiles(DateTime timeStamp, S3Config s3Config) { if (_counterOfFile >= NbrOfFileToConcatenate) { _counterOfFile = 0; var logFileConcatenator = new LogFileConcatenator(); var s3Path = timeStamp.ToUnixTime() + ".csv"; s3Path.WriteLine(""); var csvToSend = logFileConcatenator.ConcatenateFiles(NbrOfFileToConcatenate); var request = s3Config.CreatePutRequest(s3Path); //Use this for no compression //var response = await request.PutAsync(new StringContent(csv)); var compressedBytes = CompresseBytes(csvToSend); // Encode the compressed byte array as a Base64 string string base64String = Convert.ToBase64String(compressedBytes); // Create StringContent from Base64 string var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64"); // Upload the compressed data (ZIP archive) to S3 var response = await request.PutAsync(stringContent); if (response.StatusCode != 200) { Console.WriteLine("ERROR: PUT"); var error = await response.GetStringAsync(); Console.WriteLine(error); Heartbit(new DateTime(0)); return false; } Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------"); Heartbit(timeStamp); } _counterOfFile++; return true; } private static void Heartbit(DateTime timeStamp) { var s3Bucket = Config.Load().S3?.Bucket; var tryParse = TryParse(s3Bucket?.Split("-")[0], out var installationId); var parse = TryParse(timeStamp.ToUnixTime().ToString(), out var nameOfCsvFile); if (tryParse) { var returnedStatus = new StatusMessage { InstallationId = installationId, Product = 0, // Salimax is always 0 Status = _salimaxAlarmState, Type = MessageType.Heartbit, Timestamp = nameOfCsvFile }; if (s3Bucket != null) RabbitMqManager.InformMiddleware(returnedStatus); } } private static Byte[] CompresseBytes(String csvToSend) { //Compress CSV data to a byte array Byte[] compressedBytes; using (var memoryStream = new MemoryStream()) { //Create a zip directory and put the compressed file inside using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) { var entry = archive.CreateEntry("data.csv", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive using (var entryStream = entry.Open()) using (var writer = new StreamWriter(entryStream)) { writer.Write(csvToSend); } } compressedBytes = memoryStream.ToArray(); } return compressedBytes; } private static async Task RestApiSavingFile(String csv) { // This is for the Rest API // Check if the directory exists, and create it if it doesn't const String directoryPath = "/var/www/html"; if (!Directory.Exists(directoryPath)) { Directory.CreateDirectory(directoryPath); } string filePath = Path.Combine(directoryPath, "status.csv"); await File.WriteAllTextAsync(filePath, csv.SplitLines().Where(l => !l.Contains("Secret")).JoinLines()); } private static Boolean IsPowerOfTwo(Int32 n) { return n > 0 && (n & (n - 1)) == 0; } private static void ApplyConfigFile(this StatusRecord status, Configuration? config) { status.Config.MinSoc = config.MinimumSoC; status.Config.GridSetPoint = config.GridSetPoint * 1000; // converted from kW to W status.Config.ForceCalibrationChargeState = config.CalibrationChargeState; if (config.CalibrationChargeState == CalibrationChargeType.RepetitivelyEvery) { status.Config.DayAndTimeForRepetitiveCalibration = config.CalibrationChargeDate; } else if (config.CalibrationChargeState == CalibrationChargeType.AdditionallyOnce) { status.Config.DayAndTimeForAdditionalCalibration = config.CalibrationChargeDate; } } }