#undef Amax #undef GridLimit using System.IO.Compression; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; using System.Text; using Flurl.Http; using InnovEnergy.App.SodiStoreMax.Devices; using InnovEnergy.App.SodiStoreMax.Ess; using InnovEnergy.App.SodiStoreMax.MiddlewareClasses; using InnovEnergy.App.SodiStoreMax.SaliMaxRelays; using InnovEnergy.App.SodiStoreMax.System; using InnovEnergy.App.SodiStoreMax.SystemConfig; using InnovEnergy.Lib.Devices.AMPT; using InnovEnergy.Lib.Devices.BatteryDeligreen; 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.Devices.Doepke; using InnovEnergy.Lib.Protocols.Modbus.Channels; using InnovEnergy.Lib.Units; using InnovEnergy.Lib.Utils; using InnovEnergy.App.SodiStoreMax.DataTypes; using Newtonsoft.Json; using static InnovEnergy.App.SodiStoreMax.AggregationService.Aggregator; using static InnovEnergy.App.SodiStoreMax.MiddlewareClasses.MiddlewareAgent; using static InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes.SystemConfig; using DeviceState = InnovEnergy.App.SodiStoreMax.Devices.DeviceState; // ReSharper disable PossibleLossOfFraction #pragma warning disable IL2026 namespace InnovEnergy.App.SodiStoreMax; 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 SecondPvOnDc; private static readonly Channel PvOnAcGrid; private static readonly Channel PvOnAcIsland; private static readonly Channel RelaysChannel; private static readonly Channel BatteriesChannel; private static readonly Channel DoepkeChannel; // private const String SwVersionNumber =" V1.35.220925 beta"; private static Boolean _curtailFlag = false; 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; // add this to config file private static UInt16 _fileCounter = 0; private static SalimaxAlarmState _salimaxAlarmState = SalimaxAlarmState.Green; private const String Port = "/dev/ttyUSB0"; // move to a config file private static Int32 _failsCounter = 0; // move to a config file 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); SecondPvOnDc = CreateChannel(d.SecondPvOnDc); PvOnAcGrid = CreateChannel(d.PvOnAcGrid); PvOnAcIsland = CreateChannel(d.PvOnAcIsland); RelaysChannel = CreateChannel(d.RelaysIp); BatteriesChannel = CreateChannel(d.BatteryIp); DoepkeChannel = CreateChannel(d.DoepkeIp); 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 SodiStore Max".WriteLine(); Watchdog.NotifyReady(); var batteryDeligreenDevice = BatteryNodes.Select(n => new BatteryDeligreenDevice(Port, n)) .ToList(); var batteryDevices = new BatteryDeligreenDevices(batteryDeligreenDevice); 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 secondPvOnDcDevice = new AmptDevices(SecondPvOnDc); var pvOnAcGridDevice = new AmptDevices(PvOnAcGrid); var pvOnAcIslandDevice = new AmptDevices(PvOnAcIsland); var doepkeDevice = new DoepkeDevice(DoepkeChannel); #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 firstPvOnDc = pvOnDcDevice.Read(); var secondPvOnDc = secondPvOnDcDevice.Read(); var battery = batteryDevices.Read(); var pvOnAcGrid = pvOnAcGridDevice.Read(); var pvOnAcIsland = pvOnAcIslandDevice.Read(); var doepek = doepkeDevice.Read(); IReadOnlyList pvOnDc = new List { firstPvOnDc, secondPvOnDc }; 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, Doepke = doepek, 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(); SendSalimaxStateAlarm(GetSalimaxStateAlarm(record), record); // to improve record.ControlConstants(); record.ControlSystemState(); record.AcDc.SystemControl.ApplyAcDcDefaultSettings(); record.DcDc.SystemControl.ApplyDcDcDefaultSettings(); // Retries Control //Console.WriteLine(" Fails Counter = " + _failsCounter); /* if (record.StateMachine.State is not (28 or 23) ) { // add a case 1 > RCD fail // add a case 2 > overload if (_failsCounter > 60) // 2 min { Console.WriteLine(" Fails retries reached threshold"); record.EnableSafeDefaults(); return record; } _failsCounter++; } else { _failsCounter = 0; } //record.ControlPvPower(record.Config.CurtailP, record.Config.PvInstalledPower); */ var essControl = record.ControlEss().WriteLine(); record.EssControl = essControl; DistributePower(record, essControl); //record.PerformLed(); WriteControl(record); record.Battery.Eoc.WriteLine(" Total EOC"); $"{record.StateMachine.State}: {record.StateMachine.Message}".WriteLine(); $"{DateTime.Now.Round(UpdateInterval).ToUnixTime()} : {DateTime.Now.Round(UpdateInterval):dd/MM/yyyy HH:mm:ss} : {SwVersionNumber}".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; //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); } //If there is an available message from the RabbitMQ Broker, apply the configuration file Configuration? config = SetConfigurationFile(); if (config != null) { record.ApplyConfigFile(config); } } // This preparing a message to send to salimax monitor 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(); if (record.Battery is { MonomerHighVoltageAlarm: true }) { warningList.Add(new AlarmOrWarning { Date = DateTime.Now.ToString("yyyy-MM-dd"), Time = DateTime.Now.ToString("HH:mm:ss"), CreatedBy = "Charging Battery System", Description = "dynCCL Active: Max Battery Charging is 10 * N" }); } if (record.Battery is { MonomerLowVoltageAlarm: true }) { warningList.Add(new AlarmOrWarning { Date = DateTime.Now.ToString("yyyy-MM-dd"), Time = DateTime.Now.ToString("HH:mm:ss"), CreatedBy = "Battery System", Description = "dynDCL Active: Max Battery Discharging is 10 * N" }); } 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 }); } alarmList.AddRange(record.AcDc.Alarms .Select(alarm => new AlarmOrWarning { Date = DateTime.Now.ToString("yyyy-MM-dd"), Time = DateTime.Now.ToString("HH:mm:ss"), CreatedBy = "AcDc", Description = alarm.ToString() })); alarmList.AddRange(record.DcDc.Alarms .Select(alarm => new AlarmOrWarning { Date = DateTime.Now.ToString("yyyy-MM-dd"), Time = DateTime.Now.ToString("HH:mm:ss"), CreatedBy = "DcDc", Description = alarm.ToString() })); warningList.AddRange(record.AcDc.Warnings .Select(warning => new AlarmOrWarning { Date = DateTime.Now.ToString("yyyy-MM-dd"), Time = DateTime.Now.ToString("HH:mm:ss"), CreatedBy = "AcDc", Description = warning.ToString() })); warningList.AddRange(record.DcDc.Warnings .Select(warning => 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 var installationId = GetInstallationId(s3Bucket ?? string.Empty); var returnedStatus = new StatusMessage { InstallationId = installationId, Product = 3, Status = _salimaxAlarmState, Type = MessageType.AlarmOrWarning, Alarms = alarmList, Warnings = warningList }; return returnedStatus; } private static Int32 GetInstallationId(String s3Bucket) { var part = s3Bucket.Split('-').FirstOrDefault(); return int.TryParse(part, out var id) ? id : 0; // is 0 a default safe value? check with Marios } 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 devicesConfig = r.AcDc.Devices.All(d => d.Control.Ac.GridType == GridType.GridTied400V50Hz) ? configFile.GridTie : configFile.IslandMode; // TODO if any of the grid tie mode Double maxBatteryChargingCurrentLiveByDcDc ; //used with deligreenBattery for limiting charging Double maxBatteryDischargingCurrentLivebyDcDc; //used with deligreenBattery for limiting discharging var dcCount = r.DcDc.Devices.Count; if (r.Battery != null) { // Deligreen upper current limitation dynCCL if ( dcCount != 0 && r.Battery.MonomerHighVoltageAlarm ) { maxBatteryChargingCurrentLiveByDcDc = 20.0 / dcCount ; // Max charging current is 10 A * Number of batteries Console.WriteLine("dynCCL Active: Max Charging current for one Battery is "+ maxBatteryChargingCurrentLiveByDcDc * dcCount); // multiply by dcCount for display purpose } else { maxBatteryChargingCurrentLiveByDcDc = devicesConfig.DcDc.MaxBatteryChargingCurrent/r.Battery.Devices.Count; } // Deligreen lower current limitation dynDCL if ( dcCount != 0 && r.Battery.MonomerLowVoltageAlarm ) { maxBatteryDischargingCurrentLivebyDcDc = 20.0 / dcCount ; // Max discharging current is 10 A * Number of batteries Console.WriteLine("dynDCL Active: Max disCharging current for one Battery is "+ maxBatteryDischargingCurrentLivebyDcDc * dcCount); // multiply by dcCount for display purpose } else { maxBatteryDischargingCurrentLivebyDcDc = devicesConfig.DcDc.MaxBatteryDischargingCurrent/r.Battery.Devices.Count; } maxBatteryChargingCurrentLiveByDcDc = (maxBatteryChargingCurrentLiveByDcDc * r.Battery.AvailableChBatteries); // Adapt the current to the amount of available battery maxBatteryDischargingCurrentLivebyDcDc = (maxBatteryDischargingCurrentLivebyDcDc * r.Battery.AvailableDischBatteries); // Adapt the current to the amount of available battery (r.Battery?.AvailableChBatteries)?.WriteLine(" Available Charging Batteries"); (r.Battery?.AvailableDischBatteries)?.WriteLine(" Available Discharging Batteries"); (r.Battery?.ChargeModeBatteries)?.WriteLine(" Batteries on charge mode"); (r.Battery?.DischargeModeBatteries)?.WriteLine(" Batteries on discharge mode"); maxBatteryChargingCurrentLiveByDcDc.WriteLine(" maxBatteryChargingCurrentLive by DcDC"); maxBatteryDischargingCurrentLivebyDcDc.WriteLine(" maxBatteryDischargingCurrentLive by DcDc"); } else { maxBatteryChargingCurrentLiveByDcDc = 0; maxBatteryDischargingCurrentLivebyDcDc = 0; } // This random value 5 is applied when the current is limited to 0. The 0 limitations will create an oscillation between discharging and charging if (maxBatteryChargingCurrentLiveByDcDc == 0) { maxBatteryChargingCurrentLiveByDcDc = 5 / dcCount; } else if (maxBatteryDischargingCurrentLivebyDcDc == 0) { maxBatteryDischargingCurrentLivebyDcDc = 5 / dcCount; } 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 = maxBatteryChargingCurrentLiveByDcDc); dcDevices.ForEach(d => d.Control.CurrentControl.MaxBatteryDischargingCurrent = maxBatteryDischargingCurrentLivebyDcDc); dcDevices.ForEach(d => d.Control.MaxDcPower = devicesConfig.DcDc.MaxDcPower); dcDevices.ForEach(d => d.Control.VoltageLimits.MaxBatteryVoltage = devicesConfig.DcDc.MaxChargeBatteryVoltage); dcDevices.ForEach(d => d.Control.VoltageLimits.MinBatteryVoltage = devicesConfig.DcDc.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, UInt16 exportLimit = 0, UInt16 pvInstalledPower = 20) { // Maybe add a condition to do this only if we are in optimised Self consumption, this is not true if (r.GridMeter?.Ac.Power.Active == null) { Console.WriteLine(" No reading from Grid meter"); return; } if (pvInstalledPower == 0) { Console.WriteLine(" No curtailing, because Pv installed is equal to 0"); return; } const Int32 constantDeadBand = 5000; // magic number const Double voltageRange = 100; // 100 Voltage configured rang for PV slope, if the configured slope change this must change also var configFile = r.Config; var inverters = r.AcDc.Devices; var systemExportLimit = - exportLimit * 1000 ; // Conversion from Kw in W // the config file value is positive and limit should be negative from 0 to ... var stepSize = ClampStepSize((UInt16)Math.Floor(voltageRange/ pvInstalledPower)); // in Voltage per 1 Kw var deadBand = constantDeadBand/stepSize; // LINQ query to select distinct ActiveUpperVoltage var result = r.AcDc.Devices .Select(device => device?.Status?.DcVoltages?.Active?.ActiveUpperVoltage) .Select(voltage => voltage.Value) // Extract the value since we've confirmed it's non-null .Distinct() .ToList(); Double upperVoltage; if (result.Count == 1) { upperVoltage = result[0]; } else { Console.WriteLine(" Different ActiveUpperVoltage between inverters "); // this should be reported to salimax Alarm return; } /************* For debugging purposes ********************/ systemExportLimit.WriteLine(" Export Limit in W"); upperVoltage.WriteLine(" Upper Voltage"); r.GridMeter.Ac.Power.Active.WriteLine(" Active Export"); Console.WriteLine(" ****************** "); /*********************************************************/ if (r.GridMeter.Ac.Power.Active < systemExportLimit) { _curtailFlag = true; upperVoltage = IncreaseInverterUpperLimit(upperVoltage, stepSize); upperVoltage.WriteLine("Upper Voltage Increased: New Upper limit"); } else { if (_curtailFlag) { if (r.GridMeter.Ac.Power.Active > (systemExportLimit + deadBand)) { upperVoltage = DecreaseInverterUpperLimit(upperVoltage, stepSize); if (upperVoltage <= configFile.GridTie.AcDc.MaxDcLinkVoltage) { _curtailFlag = false; upperVoltage = configFile.GridTie.AcDc.MaxDcLinkVoltage; upperVoltage.WriteLine(" New Upper limit"); Console.WriteLine("Upper Voltage decreased: Smaller than the default value, value clamped"); } else { Console.WriteLine("Upper Voltage decreased: New Upper limit"); upperVoltage.WriteLine(" New Upper limit"); } } else { deadBand.WriteLine("W :We are in Dead band area"); upperVoltage.WriteLine(" same Upper limit from last cycle"); } } else { Console.WriteLine("Curtail Flag is false , no need to curtail"); upperVoltage.WriteLine(" same Upper limit from last cycle"); } } inverters.ForEach(d => d.Control.Dc.MaxVoltage = upperVoltage); Console.WriteLine(" ****************** "); } // why 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; }); } // To test, most probably the curtailing flag will not work /*private static void PerformLed(this StatusRecord record) { if (record.StateMachine.State == 23) { switch (record.EssControl.Mode) { case EssMode.CalibrationCharge: record.Relays?.PerformSlowFlashingGreenLed(); break; case EssMode.OptimizeSelfConsumption when !_curtailFlag: record.Relays?.PerformSolidGreenLed(); break; case EssMode.Off: break; case EssMode.OffGrid: break; case EssMode.HeatBatteries: break; case EssMode.ReachMinSoc: break; case EssMode.NoGridMeter: break; default: { if (_curtailFlag) { record.Relays?.PerformFastFlashingGreenLed(); } break; } } } else if (record.Battery?.Soc != null && record.StateMachine.State != 23 && record.Battery.Soc > 50) { record.Relays?.PerformSolidOrangeLed(); } else if (record.Battery?.Soc != null && record.StateMachine.State != 23 && record.Battery.Soc < 50 && record.Battery.Soc > 20) { record.Relays?.PerformSlowFlashingOrangeLed(); } else if (record.Battery?.Soc != null && record.StateMachine.State != 23 && record.Battery.Soc < 20) { record.Relays?.PerformFastFlashingOrangeLed(); } var criticalAlarm = record.DetectAlarmStates(); if (criticalAlarm is not null) { record.Relays?.PerformFastFlashingRedLed(); } }*/ private static Double IncreaseInverterUpperLimit(Double upperLimit, Double stepSize) { return upperLimit + stepSize; } private static Double DecreaseInverterUpperLimit(Double upperLimit, Double stepSize) { return upperLimit - stepSize; } private static UInt16 ClampStepSize(UInt16 stepSize) { return stepSize switch { > 5 => 5, <= 1 => 1, _ => stepSize }; } 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 void InsertIntoJson(Dictionary jsonDict, String[] keys, String value) { var currentDict = jsonDict; for (Int16 i = 1; i < keys.Length; i++) // Start at 1 to skip empty root { var key = keys[i]; if (!currentDict.ContainsKey(key)) { currentDict[key] = new Dictionary(); } if (i == keys.Length - 1) // Last key, store the value { if (!value.Contains(",") && double.TryParse(value, out Double doubleValue)) // Try to parse value as a number { currentDict[key] = Math.Round(doubleValue, 2); // Round to 2 decimal places } else { currentDict[key] = value; // Store as string if not a number } } else { currentDict = (Dictionary)currentDict[key]; } } } private static async Task UploadCsv(StatusRecord status, DateTime timeStamp) { var csv = status.ToCsv(); var jsonData = new Dictionary(); foreach (var line in csv.Split('\n')) { if (string.IsNullOrWhiteSpace(line)) continue; var parts = line.Split(';'); var keyPath = parts[0]; var value = parts[1]; var unit = parts.Length > 2 ? parts[2].Trim() : ""; //Console.WriteLine(line); // Console.WriteLine($"Key: {keyPath}, Value: {value}, Unit: {unit}"); InsertIntoJson(jsonData, keyPath.Split('/'), value); } var jsonOutput = JsonConvert.SerializeObject(jsonData, Formatting.None); jsonOutput.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 (_fileCounter >= NbrOfFileToConcatenate) { _fileCounter = 0; var logFileConcatenator = new LogFileConcatenator(); var s3Path = timeStamp.ToUnixTime() + ".json"; var jsonToSend = logFileConcatenator.ConcatenateFiles(NbrOfFileToConcatenate); var request = s3Config.CreatePutRequest(s3Path); //Use this for no compression var compressedBytes = CompresseBytes(jsonToSend); // Encode the compressed byte array as a Base64 string var 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(); return false; } Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------"); Heartbit(); } _fileCounter++; return true; }*/ private static async Task ConcatinatingAndCompressingFiles(DateTime timeStamp, S3Config s3Config) { if (_fileCounter >= NbrOfFileToConcatenate) { _fileCounter = 0; var logFileConcatenator = new LogFileConcatenator(); var jsontoSend = logFileConcatenator.ConcatenateFiles(NbrOfFileToConcatenate); var fileNameWithoutExtension = timeStamp.ToUnixTime().ToString(); // used for both S3 and local var s3Path = fileNameWithoutExtension + ".json"; var request = s3Config.CreatePutRequest(s3Path); var compressedBytes = CompresseBytes(jsontoSend); var base64String = Convert.ToBase64String(compressedBytes); var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64"); var uploadSucceeded = false; try { var response = await request.PutAsync(stringContent); if (response.StatusCode != 200) { Console.WriteLine("ERROR: PUT"); var error = await response.GetStringAsync(); Console.WriteLine(error); await SaveToLocalCompressedFallback(compressedBytes, fileNameWithoutExtension); Heartbit(); return false; } uploadSucceeded = true; Console.WriteLine("✅ File uploaded to S3 successfully."); Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------"); Heartbit(); await ResendLocalFailedFilesAsync(s3Config); // retry any pending failed files } catch (Exception ex) { Console.WriteLine("Upload exception: " + ex.Message); if (!uploadSucceeded) { await SaveToLocalCompressedFallback(compressedBytes, fileNameWithoutExtension); } Heartbit(); return false; } } _fileCounter++; return true; } private static async Task SaveToLocalCompressedFallback(Byte[] compressedData, String fileNameWithoutExtension) { try { var fallbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FailedUploads"); Directory.CreateDirectory(fallbackDir); var fileName = fileNameWithoutExtension + ".json"; // Save as .json, but still compressed var fullPath = Path.Combine(fallbackDir, fileName); await File.WriteAllBytesAsync(fullPath, compressedData); // Compressed data Console.WriteLine($"Saved compressed failed upload to: {fullPath}"); } catch (Exception ex) { Console.WriteLine("Failed to save compressed file locally: " + ex.Message); } } private static async Task ResendLocalFailedFilesAsync(S3Config s3Config) { var fallbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FailedUploads"); if (!Directory.Exists(fallbackDir)) return; var files = Directory.GetFiles(fallbackDir, "*.json"); foreach (var filePath in files) { var fileName = Path.GetFileName(filePath); // e.g., "1720023600.json" var s3Key = fileName; // same name for S3 try { byte[] compressedBytes = await File.ReadAllBytesAsync(filePath); var base64String = Convert.ToBase64String(compressedBytes); var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64"); var request = s3Config.CreatePutRequest(s3Key); var response = await request.PutAsync(stringContent); if (response.StatusCode == 200) { File.Delete(filePath); Console.WriteLine($"✅ Successfully resent and deleted: {fileName}"); } else { Console.WriteLine($"❌ Failed to resend {fileName}, status: {response.StatusCode}"); } } catch (Exception ex) { Console.WriteLine($"⚠️ Exception while resending {fileName}: {ex.Message}"); } } } private static void Heartbit() { var s3Bucket = Config.Load().S3?.Bucket; var tryParse = int.TryParse(s3Bucket?.Split("-")[0], out var installationId); if (tryParse) { var returnedStatus = new StatusMessage { InstallationId = installationId, Product = 3, // Salimax is always 0 Status = _salimaxAlarmState, Type = MessageType.Heartbit, }; if (s3Bucket != null) RabbitMqManager.InformMiddleware(returnedStatus); } } private static Byte[] CompresseBytes(String jsonToSend) { //Compress JSON 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.json", CompressionLevel.SmallestSize); // Add JSON data to the ZIP archive using (var entryStream = entry.Open()) using (var writer = new StreamWriter(entryStream)) { writer.Write(jsonToSend); } } 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); } var 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) { if (config == null) return; 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; } if (config.CalibrationDischargeState == CalibrationDischargeType.RepetitivelyEvery) { status.Config.DownDayAndTimeForRepetitiveCalibration = config.CalibrationDischargeDate; } else if (config.CalibrationDischargeState == CalibrationDischargeType.AdditionallyOnce) { status.Config.DownDayAndTimeForAdditionalCalibration = config.CalibrationDischargeDate; } } }