using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; 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 System.Text.Json; using RabbitMQ.Client; 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 = "194.182.190.208"; private static IPAddress? _controllerIpAddress; private static UdpClient _udpListener = null!; private static ConnectionFactory? _factory ; private static IConnection ? _connection; private static IModel? _channel; private static Boolean _subscribedToQueue = false; private static Boolean _subscribeToQueueForTheFirstTime = false; private static SalimaxAlarmState _prevSalimaxState = 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) { while (true) { //CreateAverage(); try { InitializeCommunicationToMiddleware(); await Run(); } catch (Exception e) { e.LogError(); } } // ReSharper disable once FunctionNeverReturns } private static void InitializeCommunicationToMiddleware() { _controllerIpAddress = FindVpnIp(); if (Equals(IPAddress.None, _controllerIpAddress)) { Console.WriteLine("There is no VPN interface, exiting..."); } const Int32 udpPort = 9000; var endPoint = new IPEndPoint(_controllerIpAddress, udpPort); _udpListener = new UdpClient(); _udpListener.Client.Blocking = false; _udpListener.Client.Bind(endPoint); } 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 pvOnDcDevice = new AmptDevices(PvOnDc); var pvOnAcGridDevice = new AmptDevices(PvOnAcGrid); var pvOnAcIslandDevice = new AmptDevices(PvOnAcIsland); var saliMaxRelaysDevice = new RelaysDevice(RelaysChannel); 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 }, //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) 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) .Select(_ => RunIteration()) .SelectMany(r => UploadCsv(r, DateTime.Now.Round(UpdateInterval))) .SelectError() .ToTask(); } StatusRecord RunIteration() { Watchdog.NotifyAlive(); var record = ReadStatus(); var currentSalimaxState = GetSalimaxStateAlarm(record); SendSalimaxStateAlarm(currentSalimaxState); record.ControlConstants(); record.ControlSystemState(); var essControl = record.ControlEss().WriteLine().LogInfo(); 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() .LogInfo(); 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 void SendSalimaxStateAlarm(SalimaxAlarmState currentSalimaxState) { var s3Bucket = Config.Load().S3?.Bucket; //When the controller boots, it tries to subscribe to the queue if (_subscribeToQueueForTheFirstTime==false) { _subscribeToQueueForTheFirstTime = true; SubscribeToQueue(currentSalimaxState, s3Bucket); if (_subscribedToQueue && currentSalimaxState != _prevSalimaxState) { _prevSalimaxState = currentSalimaxState; } } //If already subscribed to the queue and the status has been changed, update the queue else if (_subscribedToQueue && currentSalimaxState != _prevSalimaxState) { _prevSalimaxState = currentSalimaxState; if (s3Bucket != null) InformMiddleware(s3Bucket, (Int32)currentSalimaxState); } //If there is an available message from the RabbitMQ Broker, subscribe to the queue if (_udpListener.Available > 0) { IPEndPoint? serverEndpoint = null; var replyMessage = "ACK"; var replyData = Encoding.UTF8.GetBytes(replyMessage); var udpMessage = _udpListener.Receive(ref serverEndpoint); var message = Encoding.UTF8.GetString(udpMessage); Console.WriteLine($"Received a message: {message}"); // Send the reply to the sender's endpoint _udpListener.Send(replyData, replyData.Length, serverEndpoint); Console.WriteLine($"Replied to {serverEndpoint}: {replyMessage}"); SubscribeToQueue(currentSalimaxState, s3Bucket); } } private static void SubscribeToQueue(SalimaxAlarmState currentSalimaxState, String? s3Bucket) { try { _factory = new ConnectionFactory { HostName = VpnServerIp }; _connection = _factory.CreateConnection(); _channel = _connection.CreateModel(); _channel.QueueDeclare(queue: "statusQueue", durable: true, exclusive: false, autoDelete: false, arguments: null); Console.WriteLine("The controller sends its status to the middleware for the first time"); if (s3Bucket != null) InformMiddleware(s3Bucket, (Int32)currentSalimaxState); _subscribedToQueue = true; } catch (Exception ex) { Console.WriteLine("An error occurred while connecting to the RabbitMQ queue: " + ex.Message); } } private static IPAddress FindVpnIp() { const String interfaceName = "innovenergy"; var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); foreach (var networkInterface in networkInterfaces) { if (networkInterface.Name == interfaceName) { var ipProps = networkInterface.GetIPProperties(); var uniCastIPs = ipProps.UnicastAddresses; var controllerIpAddress = uniCastIPs[0].Address; Console.WriteLine("VPN IP is: "+ uniCastIPs[0].Address); return controllerIpAddress; } } return IPAddress.None; } private static void InformMiddleware(String? bucket, int status) { int.TryParse(bucket[0].ToString(), out var installationId); var jsonObject = new StatusMessage { InstallationId = installationId, Status = status, }; if (status == 2) { jsonObject.CreatedAt = DateTime.Now; jsonObject.Description = "Battery Temperature High"; jsonObject.CreatedBy = "Battery/1"; } else if (status == 1) { jsonObject.CreatedAt = DateTime.Now; jsonObject.Description = "Temp warning message"; jsonObject.CreatedBy = "Battery/4"; } var message = JsonSerializer.Serialize(jsonObject); var body = Encoding.UTF8.GetBytes(message); _channel.BasicPublish(exchange: string.Empty, routingKey: "statusQueue", basicProperties: null, body: body); Console.WriteLine($"Producer sent message: {message}"); } private static SalimaxAlarmState GetSalimaxStateAlarm(StatusRecord record) { var alarmCondition = record.DetectAlarmStates(); if (alarmCondition is not null) { alarmCondition.LogInfo(); } // record.Log = new SystemLog // { // Led = alarmCondition is null ? LedState.Green : LedState.Red, // Message = alarmCondition // }; var salimaxAlarmsState = (record.Battery is not null && record.Battery.Warnings.Any()) | record.AcDc.Warnings.Any() | record.AcDc.SystemControl.Warnings.Any() | record.DcDc.Warnings.Any() ? SalimaxAlarmState.Orange : SalimaxAlarmState.Green; // this will be replaced by LedState salimaxAlarmsState = (record.Battery is not null && record.Battery.Alarms.Any()) | record.AcDc.Alarms.Any() | record.AcDc.SystemControl.Alarms.Any() | record.DcDc.Alarms.Any() | alarmCondition is not null ? SalimaxAlarmState.Red : salimaxAlarmsState; // this will be replaced by LedState return salimaxAlarmsState; } 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 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 = configFile.MaxBatteryDischargingCurrent); 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(); } // 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 s3Config = status.Config.S3; var csv = status.ToCsv().LogInfo(); if (s3Config is null) return false; var s3Path = timeStamp.ToUnixTime() + ".csv"; var request = s3Config.CreatePutRequest(s3Path); var response = await request.PutAsync(new StringContent(csv)); // This is temporary for Wittman //await File.WriteAllTextAsync("/var/www/html/status.csv", csv.SplitLines().Where(l => !l.Contains("Secret")).JoinLines()); if (response.StatusCode != 200) { Console.WriteLine("ERROR: PUT"); var error = await response.GetStringAsync(); Console.WriteLine(error); } return true; } private static void CreateAverage() { string myDirectory = "LogDirectoryNew"; List> csvDataList = new List>(); // Get all CSV files in the specified directory string[] csvFiles = Directory.GetFiles(myDirectory, "*.csv"); List socList = new List(); List pvPowerList = new List(); List batteryPowerList = new List(); foreach (var csvFile in csvFiles) { using (var reader = new StreamReader(csvFile)) { // Read the CSV file and store data in dictionary Dictionary csvData = new Dictionary(); while (!reader.EndOfStream) { var line = reader.ReadLine(); var values = line?.Split(';'); // Assuming there are always three columns (variable name and its value) if (values is { Length: 3 }) { String variableName = values[0].Trim(); String variableValue = values[1].Trim(); // Check if variableValue is a valid number if (IsSoc(variableName)) { // Add to the dictionary only if variableValue is a number socList.Add(double.TryParse(variableValue, out double v)? v: 0); } if (IsPvPower(variableName)) { // Add to the dictionary only if variableValue is a number pvPowerList.Add(double.TryParse(variableValue, out double v)? v: 0); } if (IsBatteryPower(variableName)) { // Add to the dictionary only if variableValue is a number batteryPowerList.Add(double.TryParse(variableValue, out double v)? v: 0); } else { // Handle cases where variableValue is not a valid number // Console.WriteLine($"Invalid numeric value for variable {variableName}: {variableValue}"); } } else { // Handle invalid CSV format //Console.WriteLine($"Invalid format in file: {csvFile}"); //break; } } } } double socAverage = CalculateAverage(socList); double pvPowerAverage = CalculateAverage(pvPowerList); double batteryPowerAverage = CalculateAverage(batteryPowerList); // Print the stored CSV data for verification Console.WriteLine($"SOC: {socAverage}"); Console.WriteLine($"PvPower: {pvPowerAverage}"); Console.WriteLine($"Battery: {batteryPowerAverage}"); Console.WriteLine("----------"); Console.WriteLine("CSV data reading and storage completed."); } // Custom method to check if a string is numeric private static bool IsSoc(string value) { return value == "/Battery/Soc"; } private static bool IsPvPower(string value) { return value == "/PvOnDc/Dc/Power"; } private static bool IsBatteryPower(string value) { return value == "/Battery/Dc/Power"; } // Method to calculate average for a variableValue in a dictionary static double CalculateAverage( List data) { // Calculate and return the moving average double movingAverage = data.Average(); return movingAverage; } }