From da9d2b1f4026b11bce8dfbf773816f51f6e7da07 Mon Sep 17 00:00:00 2001 From: atef Date: Fri, 27 Feb 2026 11:10:55 +0100 Subject: [PATCH 1/8] Add The DCDC control to Kaco project. Add the Three kabinet to our Battery Reading. Add ESS and Machine state for SodiStoreGrid --- csharp/App/KacoCommunication/ESS/EssMode.cs | 6 + .../App/KacoCommunication/ESS/StatusRecord.cs | 23 +- csharp/App/KacoCommunication/Program.cs | 295 +++++++++++------- .../KacoCommunication/System/StateMachine.cs | 9 + .../KacoCommunication/SystemConfig/Config.cs | 7 +- csharp/Lib/Devices/Kaco92L3/KacoRecord.Api.cs | 5 + .../Lib/Devices/Kaco92L3/KacoRecord.modbus.cs | 2 +- 7 files changed, 213 insertions(+), 134 deletions(-) create mode 100644 csharp/App/KacoCommunication/ESS/EssMode.cs create mode 100644 csharp/App/KacoCommunication/System/StateMachine.cs diff --git a/csharp/App/KacoCommunication/ESS/EssMode.cs b/csharp/App/KacoCommunication/ESS/EssMode.cs new file mode 100644 index 000000000..c33112305 --- /dev/null +++ b/csharp/App/KacoCommunication/ESS/EssMode.cs @@ -0,0 +1,6 @@ +namespace InnovEnergy.App.KacoCommunication.ESS; + +public enum EssMode +{ + +} \ No newline at end of file diff --git a/csharp/App/KacoCommunication/ESS/StatusRecord.cs b/csharp/App/KacoCommunication/ESS/StatusRecord.cs index 787d95d26..6a9720404 100644 --- a/csharp/App/KacoCommunication/ESS/StatusRecord.cs +++ b/csharp/App/KacoCommunication/ESS/StatusRecord.cs @@ -1,3 +1,4 @@ +using InnovEnergy.App.KacoCommunication.System; using InnovEnergy.App.KacoCommunication.SystemConfig; using InnovEnergy.Lib.Devices.BatteryDeligreen; using InnovEnergy.Lib.Devices.Kaco92L3; @@ -5,19 +6,19 @@ using InnovEnergy.Lib.Devices.PLVario2Meter; using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc; namespace InnovEnergy.App.KacoCommunication.ESS; - +using ListOfBatteriesRecord = List; public class StatusRecord { - public required KacoRecord? InverterRecord { get; set; } - public required PlVarioMeterRecord? GridMeterRecord { get; set; } - public required DcDcDevicesRecord? DcDc { get; init; } + public required KacoRecord? InverterRecord { get; set; } + public required PlVarioMeterRecord? GridMeterRecord { get; set; } + public required DcDcDevicesRecord? DcDc { get; init; } + public required ListOfBatteriesRecord? ListOfBatteriesRecord { get; set; } + // public required BatteryDeligreenRecords? BatteryKabinet1 { get; set; } + // public required BatteryDeligreenRecords? BatteryKabinet2 { get; set; } + // public required BatteryDeligreenRecords? BatteryKabinet3 { get; set; } - public required BatteryDeligreenRecords? BatteryKabinet1 { get; set; } - public required BatteryDeligreenRecords? BatteryKabinet2 { get; set; } - public required BatteryDeligreenRecords? BatteryKabinet3 { get; set; } - - - - public required Config Config { get; set; } + public required Config Config { get; set; } + public required StateMachine StateMachine { get; init; } + } diff --git a/csharp/App/KacoCommunication/Program.cs b/csharp/App/KacoCommunication/Program.cs index d17e6d3bc..6f9c3b956 100644 --- a/csharp/App/KacoCommunication/Program.cs +++ b/csharp/App/KacoCommunication/Program.cs @@ -1,5 +1,4 @@ // See https://aka.ms/new-console-template for more information - using System.IO.Compression; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; @@ -11,16 +10,21 @@ using InnovEnergy.App.KacoCommunication.DataTypes; using InnovEnergy.App.KacoCommunication.Devices; using InnovEnergy.App.KacoCommunication.ESS; using InnovEnergy.App.KacoCommunication.MiddlewareClasses; +using InnovEnergy.App.KacoCommunication.System; using InnovEnergy.App.KacoCommunication.SystemConfig; using InnovEnergy.Lib.Devices.BatteryDeligreen; using InnovEnergy.Lib.Devices.Kaco92L3; using InnovEnergy.Lib.Devices.Kaco92L3.DataType; using InnovEnergy.Lib.Devices.PLVario2Meter; +using InnovEnergy.Lib.Devices.Trumpf.SystemControl; +using InnovEnergy.Lib.Devices.Trumpf.SystemControl.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 Newtonsoft.Json; +using DeviceState = InnovEnergy.App.KacoCommunication.Devices.DeviceState; using Formatting = Newtonsoft.Json.Formatting; using JsonSerializer = System.Text.Json.JsonSerializer; @@ -30,26 +34,25 @@ internal static class Program { private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(5); private const UInt16 NbrOfFileToConcatenate = 15; // add this to config file - private static UInt16 _fileCounter = 0; + private static UInt16 _fileCounter = 0; + + + private static SodistoreAlarmState _prevSodiohomeAlarmState = SodistoreAlarmState.Green; + private static SodistoreAlarmState _sodiAlarmState = SodistoreAlarmState.Green; - - private static SodistoreAlarmState _prevSodiohomeAlarmState = SodistoreAlarmState.Green; - private static SodistoreAlarmState _sodiAlarmState = SodistoreAlarmState.Green; - - private static readonly IReadOnlyList BatteryNodes; private static readonly Channel KacoChannel; private static readonly Channel GridMeterChannel; private static readonly Channel DcDcChannel; - private const String Port1Cabinet = "/dev/ttyUSB0"; // move to a config file - private const String Port2Cabinet = "/dev/ttyUSB1"; // move to a config file - private const String Port3Cabinet = "/dev/ttyUSB2"; // move to a config file + private const String Port1Cabinet = "/dev/ttyUSB0"; // move to a config file + private const String Port2Cabinet = "/dev/ttyUSB1"; // move to a config file + private const String Port3Cabinet = "/dev/ttyUSB2"; // move to a config file - private static readonly String SwVersionNumber = " V1.00." + DateTime.Today; - private const String VpnServerIp = "10.2.0.11"; - public static Boolean _subscribedToQueue = false; + private static readonly String SwVersionNumber = " V1.00." + DateTime.Today; + private const String VpnServerIp = "10.2.0.11"; + public static Boolean _subscribedToQueue = false; public static Boolean _subscribeToQueueForTheFirstTime = false; private static Int32 _failsCounter = 0; // move to a config file @@ -65,22 +68,20 @@ internal static class Program ? new NullChannel() : new TcpChannel(device); - BatteryNodes = config + BatteryNodes = config .Devices .BatteryNodes .Select(n => n.ConvertTo()) .ToArray(config.Devices.BatteryNodes.Length); - + KacoChannel = CreateChannel(d.KacoIp); GridMeterChannel = CreateChannel(d.GridMeterIp); DcDcChannel = CreateChannel(d.DcDcIp); - } public static async Task Main(String[] args) { - while (true) { try @@ -89,7 +90,7 @@ internal static class Program } catch (Exception e) { - // e.LogError(); + e.LogError(); } } // ReSharper disable once FunctionNeverReturns @@ -105,39 +106,49 @@ internal static class Program var gridMeterDevice = new PlVarioMeterDevice(GridMeterChannel); var dcDcDevices = new TruConvertDcDcDevices(DcDcChannel); - var firstCabinetBatteriesDevice = BatteryNodes.Select(n => new BatteryDeligreenDevice(Port1Cabinet, n)).ToList(); - var secondCabinetBatteriesDevice = BatteryNodes.Select(n => new BatteryDeligreenDevice(Port2Cabinet, n)).ToList(); - var thirdCabinetBatteriesDevice = BatteryNodes.Select(n => new BatteryDeligreenDevice(Port3Cabinet, n)).ToList(); - - var batteryDevices1 = new BatteryDeligreenDevices(firstCabinetBatteriesDevice); - var batteryDevices2 = new BatteryDeligreenDevices(secondCabinetBatteriesDevice); - var batteryDevices3 = new BatteryDeligreenDevices(thirdCabinetBatteriesDevice); + var firstCabinetBatteriesDevice = + BatteryNodes.Select(n => new BatteryDeligreenDevice(Port1Cabinet, n)).ToList(); + var secondCabinetBatteriesDevice = + BatteryNodes.Select(n => new BatteryDeligreenDevice(Port2Cabinet, n)).ToList(); + var thirdCabinetBatteriesDevice = + BatteryNodes.Select(n => new BatteryDeligreenDevice(Port3Cabinet, n)).ToList(); + + var batteryDevices1 = new BatteryDeligreenDevices(firstCabinetBatteriesDevice); + var batteryDevices2 = new BatteryDeligreenDevices(secondCabinetBatteriesDevice); + var batteryDevices3 = new BatteryDeligreenDevices(thirdCabinetBatteriesDevice); StatusRecord? ReadStatus() { PlVarioMeterRecord? gridRecord = null; - var config = Config.Load(); - var kacoRecord = kacoDevice.Read(); - var gridrawRecord = gridMeterDevice.Read(); - var dcDcRecord = dcDcDevices.Read(); + var config = Config.Load(); + var kacoRecord = kacoDevice.Read(); + var gridrawRecord = gridMeterDevice.Read(); + var dcDcRecord = dcDcDevices.Read(); + if (gridrawRecord != null) { gridRecord = new PlVarioMeterRecord(gridrawRecord); } - - var batteryKabinet1 = batteryDevices1.Read(); - var batteryKabinet2 = batteryDevices2.Read(); - var batteryKabinet3 = batteryDevices3.Read(); + + var batteryKabinet1 = batteryDevices1.Read(); + var batteryKabinet2 = batteryDevices2.Read(); + var batteryKabinet3 = batteryDevices3.Read(); + + var listOfBatteriesRecord = new List(); + if (batteryKabinet1 != null) listOfBatteriesRecord.Add(batteryKabinet1); + if (batteryKabinet2 != null) listOfBatteriesRecord.Add(batteryKabinet2); + if (batteryKabinet3 != null) listOfBatteriesRecord.Add(batteryKabinet3); + return new StatusRecord { - InverterRecord = kacoRecord, + InverterRecord = kacoRecord, GridMeterRecord = gridRecord, - DcDc = dcDcRecord, - BatteryKabinet1 = batteryKabinet1, - BatteryKabinet2 = batteryKabinet2, - BatteryKabinet3 = batteryKabinet3, - Config = config // load from disk every iteration, so config can be changed while running + DcDc = dcDcRecord, + ListOfBatteriesRecord = listOfBatteriesRecord, + StateMachine = StateMachine.Default, + + Config = config // load from disk every iteration, so config can be changed while running }; } @@ -153,7 +164,7 @@ internal static class Program .SelectError() .ToTask(); } - + StatusRecord? RunIteration() { @@ -167,56 +178,60 @@ internal static class Program Console.WriteLine(startTime.ToString("HH:mm:ss.fff")); // the order matter of the next three lines var statusrecord = ReadStatus(); - statusrecord?.CreateSimpleTopologyTextBlock().WriteLine(); + statusrecord?.CreateSimpleTopologyTextBlock().WriteLine(); + + statusrecord?.StateMachine.State.WriteLine(" state"); + statusrecord?.StateMachine.Message.WriteLine(" Message"); - - // statusrecord?.DcDc?.Dc.Battery.Power .WriteLine(" Power"); - // statusrecord?.DcDc?.Dc.Battery.Voltage .WriteLine(" Voltage"); - // statusrecord?.DcDc?.Dc.Battery.Current .WriteLine(" Current"); - // statusrecord?.DcDc?.Dc.Link.Voltage .WriteLine(" Dc link Voltage"); - - - statusrecord?.GridMeterRecord?.Frequency .WriteLine(" Frequency"); - statusrecord?.GridMeterRecord?.VoltageU1 .WriteLine(" VoltageU1"); - statusrecord?.GridMeterRecord?.VoltageU2 .WriteLine(" VoltageU2"); - statusrecord?.GridMeterRecord?.VoltageU3 .WriteLine(" VoltageU3"); - - statusrecord?.GridMeterRecord?.CurrentI1 .WriteLine(" CurrentI1"); - statusrecord?.GridMeterRecord?.CurrentI2 .WriteLine(" CurrentI2"); - statusrecord?.GridMeterRecord?.CurrentI3 .WriteLine(" CurrentI3"); - - statusrecord?.GridMeterRecord?.ActivePowerL1 .WriteLine(" ActivePowerL1"); - statusrecord?.GridMeterRecord?.ActivePowerL2 .WriteLine(" ActivePowerL2"); - statusrecord?.GridMeterRecord?.ActivePowerL3 .WriteLine(" ActivePowerL3"); - statusrecord?.GridMeterRecord?.ActivePowerTotal .WriteLine(" ActivePowerTotal"); - + statusrecord?.InverterRecord?.BatteryLimitsEnable.WriteLine(" BatteryLimitsEnable"); + + Console.WriteLine(" **************** DcDc **********************"); + statusrecord?.DcDc?.Dc.Battery.Power .WriteLine(" DC Battery Power"); + statusrecord?.DcDc?.Dc.Battery.Voltage .WriteLine("DC Battery Voltage"); + statusrecord?.DcDc?.Dc.Battery.Current .WriteLine("DC Battery Current"); + statusrecord?.DcDc?.Dc.Link.Voltage .WriteLine(" Dc link Voltage"); + + Console.WriteLine(" ********************************* Kaco Inverter *********************************"); + + statusrecord?.InverterRecord?.ActivePowerW.WriteLine(" Inverter Power"); statusrecord?.InverterRecord?.CurrentState.WriteLine(" CurrentState"); statusrecord?.InverterRecord?.RequestedState.WriteLine(" RequestedState"); statusrecord?.InverterRecord?.PcuError.WriteLine(" PcuError"); - statusrecord?.InverterRecord?.PcuState.WriteLine(" PcuState"); - - statusrecord?.InverterRecord?.BattCharId.WriteLine(" _battCharId"); - statusrecord?.InverterRecord?.BattCharLength.WriteLine(" _battCharLength"); + statusrecord?.InverterRecord?.PcuState.WriteLine(" PcuState"); statusrecord?.InverterRecord?.MinDischargeVoltage.WriteLine(" MinDischargeVoltage"); statusrecord?.InverterRecord?.MaxDischargeCurrent.WriteLine(" MaxDischargeCurrent"); statusrecord?.InverterRecord?.DischargeCutoffCurrent.WriteLine(" DischargeCutoffCurrent"); - + statusrecord?.InverterRecord?.MaxChargeVoltage.WriteLine(" MaxChargeVoltage"); statusrecord?.InverterRecord?.MaxChargeCurrent.WriteLine(" MaxChargeCurrent"); statusrecord?.InverterRecord?.ChargeCutoffCurrent.WriteLine(" ChargeCutoffCurrent"); + statusrecord?.InverterRecord?.ActivePowerSetPercent.WriteLine( "ActivePowerSetPercent"); - statusrecord?.InverterRecord?.ActivePowerSetPercent.WriteLine(" ActivePowerSetPercent"); - statusrecord?.InverterRecord?.ReactivePowerSetPercent.WriteLine(" ReactivePowerSetPercent"); - statusrecord?.InverterRecord?.WatchdogSeconds.WriteLine(" WatchdogSeconds"); + statusrecord?.ControlSystemState(); + var i = 0; + foreach (var d in statusrecord.DcDc.Devices) + { + i++; + Console.WriteLine("before DcDc is " + i + d.Control.PowerStageEnable); + d.Control.ResetAlarmsAndWarnings = true; + d.Control.PowerStageEnable = true; + } + + + statusrecord?.DcDc?.SystemControl.ApplyDcDcDefaultSettings(); InitializeKacoStartup(statusrecord); + foreach (var d in statusrecord.DcDc.Devices) + { + Console.WriteLine("After DcDc is " + d.Control.PowerStageEnable); + } - Console.WriteLine( " ************************************ We are writing ************************************"); + + Console.WriteLine(" ************************************ We are writing ************************************"); statusrecord?.Config.Save(); // save the config file if (statusrecord?.InverterRecord != null) kacoDevice.Write(statusrecord.InverterRecord); - Console.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff")); - + return statusrecord; } catch (Exception e) @@ -227,11 +242,10 @@ internal static class Program } } } - + private static async Task SavingLocalCsvFile(Int64 timestamp, String csv) { - const String directoryPath = "/home/inesco/salimax/csvFile"; - + var directoryPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "csvFiles"); // Ensure directory exists if (!Directory.Exists(directoryPath)) { @@ -268,49 +282,49 @@ internal static class Program var filePath = Path.Combine(directoryPath, timestamp + ".csv"); await File.WriteAllTextAsync(filePath, filteredCsv); } - + private static async Task DataLogging(StatusRecord status, DateTime timeStamp) { var csv = status.ToCsv(); - + // for debug, only to be deleted. //foreach (var item in csv.SplitLines()) //{ // Console.WriteLine(item + ""); //} - + await SavingLocalCsvFile(timeStamp.ToUnixTime(), csv); var jsonData = new Dictionary(); ConvertToJson(csv, jsonData).LogInfo(); - + var s3Config = status.Config.S3; - + if (s3Config is null) return false; //Concatenating 15 files in one file return await ConcatinatingAndCompressingFiles(timeStamp.ToUnixTime(), s3Config); } - + private static String ConvertToJson(String csv, Dictionary jsonData) { 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() : ""; + + var parts = line.Split(';'); + var keyPath = parts[0]; + var value = parts[1]; + var unit = parts.Length > 2 ? parts[2].Trim() : ""; InsertIntoJson(jsonData, keyPath.Split('/'), value); } var jsonOutput = JsonConvert.SerializeObject(jsonData, Formatting.None); return jsonOutput; } - + private static async Task ConcatinatingAndCompressingFiles(Int64 timeStamp, S3Config s3Config) { if (_fileCounter >= NbrOfFileToConcatenate) @@ -368,12 +382,13 @@ internal static class Program return false; } } + _fileCounter++; return true; } - + private static void Heartbit() - { + { var s3Bucket = Config.Load().S3?.Bucket; var tryParse = int.TryParse(s3Bucket?.Split("-")[0], out var installationId); if (tryParse) @@ -381,15 +396,15 @@ internal static class Program var returnedStatus = new StatusMessage { InstallationId = installationId, - Product = 3, - Status = _sodiAlarmState, - Type = MessageType.Heartbit, + Product = 3, + Status = _sodiAlarmState, + Type = MessageType.Heartbit, }; if (s3Bucket != null) RabbitMqManager.InformMiddleware(returnedStatus); } } - + private static async Task SaveToLocalCompressedFallback(Byte[] compressedData, String fileNameWithoutExtension) { try @@ -408,7 +423,7 @@ internal static class Program Console.WriteLine("Failed to save compressed file locally: " + ex.Message); } } - + private static Byte[] CompresseBytes(String jsonToSend) { //Compress JSON data to a byte array @@ -416,19 +431,20 @@ internal static class Program //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 + 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); } } - + var compressedBytes = memoryStream.ToArray(); return compressedBytes; } - + private static async Task ResendLocalFailedFilesAsync(S3Config s3Config) { var fallbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FailedUploads"); @@ -472,18 +488,20 @@ internal static class Program private static async Task SaveModbusTcpFile(StatusRecord status) { var modbusData = new Dictionary(); - + // SYSTEM DATA - var result1 = ConvertToModbusRegisters((status.Config.MinSoc * 10), "UInt16", 30001); // this to be updated to modbusTCP version + var result1 = + ConvertToModbusRegisters((status.Config.MinSoc * 10), "UInt16", + 30001); // this to be updated to modbusTCP version var result2 = ConvertToModbusRegisters(status.InverterRecord!.PcuError, "UInt32", 30002); - + // Merge all results into one dictionary - + var allResults = new[] { - result1,result2 + result1, result2 }; - + foreach (var result in allResults) { foreach (var entry in result) @@ -491,17 +509,18 @@ internal static class Program modbusData[entry.Key] = entry.Value; } } + // Write to JSON var json = JsonSerializer.Serialize(modbusData, new JsonSerializerOptions { WriteIndented = true }); await File.WriteAllTextAsync("/home/inesco/SodiStoreHome/ModbusTCP/modbus_tcp_data.json", json); //Console.WriteLine("JSON file written successfully."); //Console.WriteLine(json); - var stopTime = DateTime.Now; - Console.WriteLine(stopTime.ToString("HH:mm:ss.fff" )+ " Finish the loop"); + var stopTime = DateTime.Now; + Console.WriteLine(stopTime.ToString("HH:mm:ss.fff") + " Finish the loop"); return true; } - + private static Dictionary ConvertToModbusRegisters(Object value, String outputType, Int32 startingAddress) { @@ -515,13 +534,13 @@ internal static class Program case "Int16": var int16Val = Convert.ToInt16(value); - registers[startingAddress.ToString()] = (UInt16)int16Val; // reinterpret signed as ushort + registers[startingAddress.ToString()] = (UInt16)int16Val; // reinterpret signed as ushort break; case "UInt32": var uint32Val = Convert.ToUInt32(value); - registers[startingAddress.ToString()] = (UInt16)(uint32Val & 0xFFFF); // Low word - registers[(startingAddress + 1).ToString()] = (UInt16)(uint32Val >> 16); // High word + registers[startingAddress.ToString()] = (UInt16)(uint32Val & 0xFFFF); // Low word + registers[(startingAddress + 1).ToString()] = (UInt16)(uint32Val >> 16); // High word break; case "Int32": @@ -534,9 +553,15 @@ internal static class Program default: throw new ArgumentException("Unsupported output type: " + outputType); } + return registers; } - + + private static void ForAll(this IEnumerable ts, Action action) + { + foreach (var t in ts) + action(t); + } private static void InitializeKacoStartup(StatusRecord? statusRecord) { @@ -544,12 +569,23 @@ internal static class Program // 1. Apply DC – This part is physical and cannot be done in software. // We assume DC power is already present. // - + Console.WriteLine("1. Apply DC"); // - // 2. Send valid battery limits (Model 64202) + statusRecord?.DcDc?.Devices + .Select(d => d.Control) + .ForAll(c => c.PowerStageEnable = true); + + statusRecord?.DcDc?.Devices + .Select(d => d.Control ) + .ForAll(c => c.ControlMode = DcControlMode.VoltageDroop); + // + // // 2. Send valid battery limits (Model 64202) // All values temporarily set to "1" as requested. // You will replace them later with real values. // + + Console.WriteLine("2. Send real value"); + if (statusRecord?.InverterRecord != null) { statusRecord.InverterRecord.MinDischargeVoltage = 700f; // 64202.DisMinV @@ -559,12 +595,13 @@ internal static class Program statusRecord.InverterRecord.MaxChargeVoltage = 800f; // 64202.ChaMaxV statusRecord.InverterRecord.MaxChargeCurrent = 140f; // 64202.ChaMaxA statusRecord.InverterRecord.ChargeCutoffCurrent = 10f; // 64202.ChaCutoffA - + statusRecord.InverterRecord.WatchdogSeconds = 30; // this is additional from my seid // // 3. Enable limits (EnLimit) - // + + Console.WriteLine("3. Enable limits "); statusRecord.InverterRecord.BatteryLimitsEnable = EnableDisableEnum.Enabled; // @@ -578,6 +615,9 @@ internal static class Program // - After valid limits: CurrentState == 8 (STANDBY) // - Then after grid/DC conditions: CurrentState == 1 (OFF) or 11 (GRID_CONNECTED) // + + Console.WriteLine("3. Read current state"); + var state = statusRecord.InverterRecord.CurrentState; @@ -586,6 +626,7 @@ internal static class Program switch (state) { case CurrentState.Standby: + Console.WriteLine("Device is in STANDBY (8) — battery limits accepted."); break; @@ -601,9 +642,26 @@ internal static class Program Console.WriteLine("Device in unexpected state: " + state); break; } - //Thread.Sleep(2000); } } + + private static void ApplyDcDcDefaultSettings(this SystemControlRegisters? sc) + { + + if (sc is null) + return; + + sc.SystemConfig = Lib.Devices.Trumpf.SystemControl.DataTypes.SystemConfig.DcDcOnly; + sc.CommunicationTimeout = TimeSpan.FromSeconds(20); + + sc.PowerSetPointActivation = PowerSetPointActivation.Immediate; + sc.UseSlaveIdForAddressing = true; + sc.SlaveErrorHandling = SlaveErrorHandling.Relaxed; + sc.SubSlaveErrorHandling = SubSlaveErrorHandling.Off; + sc.TargetSlave = 0; + sc.ResetAlarmsAndWarnings = true; + } + private static void InsertIntoJson(Dictionary jsonDict, String[] keys, String value) { var currentDict = jsonDict; @@ -617,11 +675,10 @@ internal static class Program 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 + 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 { diff --git a/csharp/App/KacoCommunication/System/StateMachine.cs b/csharp/App/KacoCommunication/System/StateMachine.cs new file mode 100644 index 000000000..987b34432 --- /dev/null +++ b/csharp/App/KacoCommunication/System/StateMachine.cs @@ -0,0 +1,9 @@ +namespace InnovEnergy.App.KacoCommunication.System; + +public record StateMachine +{ + public required String Message { get; set; } // TODO: init only + public required Int32 State { get; set; } // TODO: init only + + public static StateMachine Default { get; } = new StateMachine { State = 100, Message = "Unknown State" }; +} \ No newline at end of file diff --git a/csharp/App/KacoCommunication/SystemConfig/Config.cs b/csharp/App/KacoCommunication/SystemConfig/Config.cs index 84c6c6b89..3e20f7174 100644 --- a/csharp/App/KacoCommunication/SystemConfig/Config.cs +++ b/csharp/App/KacoCommunication/SystemConfig/Config.cs @@ -14,7 +14,8 @@ public class Config private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; public required Double MinSoc { get; set; } - + public required float ActivePowerPercent { get; set; } + /* public required Double MaximumDischargingCurrent { get; set; } public required Double MaximumChargingCurrent { get; set; } public required Int16 BatteriesCount { get; set; } @@ -29,8 +30,8 @@ public class Config public static Config Default => new() { MinSoc = 20, - /* MaximumChargingCurrent = 180, - MaximumDischargingCurrent = 180, + ActivePowerPercent = 0f, + /* MaximumDischargingCurrent = 180, BatteriesCount = 0, ModbusProtcolNumber = 1.2,*/ Devices = new () diff --git a/csharp/Lib/Devices/Kaco92L3/KacoRecord.Api.cs b/csharp/Lib/Devices/Kaco92L3/KacoRecord.Api.cs index c88766213..2118b7941 100644 --- a/csharp/Lib/Devices/Kaco92L3/KacoRecord.Api.cs +++ b/csharp/Lib/Devices/Kaco92L3/KacoRecord.Api.cs @@ -176,6 +176,11 @@ public partial class KacoRecord /// public Int16 BatteryCurrentScaleFactor => _battCharASf; + + public Int16 ActivePowerW => _activePowerW; + public Int16 ReactivePowerVar => _reactivePowerVar; + public Int16 LineFrequencyHz => _lineFrequencyHz; + // Helper wrappers for scaled values private float ScaleBattVoltage(UInt16 raw) => ScaleSunspec(raw, _battCharVSf); private float ScaleBattCurrent(UInt16 raw) => ScaleSunspec(raw, _battCharASf); diff --git a/csharp/Lib/Devices/Kaco92L3/KacoRecord.modbus.cs b/csharp/Lib/Devices/Kaco92L3/KacoRecord.modbus.cs index f164ed577..16e382af1 100644 --- a/csharp/Lib/Devices/Kaco92L3/KacoRecord.modbus.cs +++ b/csharp/Lib/Devices/Kaco92L3/KacoRecord.modbus.cs @@ -21,7 +21,7 @@ public partial class KacoRecord // State control [HoldingRegister(41065, writable: true)] private UInt16 _requestedState; // 0xA069 – RequestedState (enum16, RW) [HoldingRegister(41066)] private UInt16 _currentState; // 0xA06A – CurrentState (enum16, R) - [HoldingRegister(41067, writable: true)] private UInt16 _controlMode; // 0xA06B – ControlMode (enum16, RW) + [HoldingRegister(41067, writable: true)] private UInt16 _controlMode; // 0xA06B – ControlMode (enum16, RW) Power Control mode (zunächst 0=Q RPC_Local / 1 RPC_Remote Qfix 64201) [HoldingRegister(41068)] private UInt16 _reserved7; // 0xA06C – Reserved // Watchdog / setpoints From cbd4801568d6d73ebbf036aaa3061e60b790ca1f Mon Sep 17 00:00:00 2001 From: atef Date: Fri, 27 Feb 2026 11:15:04 +0100 Subject: [PATCH 2/8] Add Machine state Controller --- .../KacoCommunication/System/Controller.cs | 376 ++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 csharp/App/KacoCommunication/System/Controller.cs diff --git a/csharp/App/KacoCommunication/System/Controller.cs b/csharp/App/KacoCommunication/System/Controller.cs new file mode 100644 index 000000000..ef14e4eb7 --- /dev/null +++ b/csharp/App/KacoCommunication/System/Controller.cs @@ -0,0 +1,376 @@ +using InnovEnergy.App.KacoCommunication.ESS; +using InnovEnergy.Lib.Devices.Kaco92L3.DataType; +#pragma warning disable CS8602 // Dereference of a possibly null reference. + +namespace InnovEnergy.App.KacoCommunication.System; + +public static class KacoCurrentStateController +{ + // Call every 2 seconds + public static Boolean ControlSystemState(this StatusRecord s) + { + var cs = s.InverterRecord.CurrentState; // 64201.CurrentState (1..12) + s.StateMachine.State = (int)cs; + + return cs switch + { + CurrentState.Off => State_Off(s), + CurrentState.Sleeping => State_Sleeping(s), + CurrentState.Starting => State_Starting(s), + CurrentState.Mppt => State_Mppt(s), + CurrentState.Throttled => State_Throttled(s), + CurrentState.ShuttingDown => State_ShuttingDown(s), + CurrentState.Fault => State_Fault(s), + CurrentState.Standby => State_Standby(s), + CurrentState.Precharge => State_Precharge(s), + CurrentState.GridPreConnected=> State_GridPreConnected(s), + CurrentState.GridConnected => State_GridConnected(s), + CurrentState.NoErrorPending => State_NoErrorPending(s), + _ => UnknownState(s) + }; + } + + // ───────────────────────────────────────────── + // Global rule: only allow power writes in 11 or 5 + // ───────────────────────────────────────────── + private static void EnforcePowerRules(StatusRecord s) + { + var cs = s.InverterRecord.CurrentState; + if (cs is not (CurrentState.GridConnected or CurrentState.Throttled)) + { + // must be 0 outside (11) or (5) + s.InverterRecord.ActivePowerSetPercent = 0f; + //s.InverterRecord.ReactivePowerSetPercent = 0f; + } + } + + // ───────────────────────────────────────────── + // State handlers (based purely on CurrentState) + // ───────────────────────────────────────────── + + private static bool State_Off(StatusRecord s) + { + s.StateMachine.Message = "OFF: write limits (once) and request connect (11)."; + EnforcePowerRules(s); + + // Write limits (ignore details) + // WriteLimits(); + + // Always aim for running + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + + private static bool State_Sleeping(StatusRecord s) + { + s.StateMachine.Message = "SLEEPING: write limits (once) and request connect (11)."; + EnforcePowerRules(s); + + // s.InverterRecord.WriteLimits(); + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + + private static bool State_Standby(StatusRecord s) + { + s.StateMachine.Message = "STANDBY: write limits (once) and request connect (11)."; + EnforcePowerRules(s); + + // s.InverterRecord.WriteLimits(); + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + + private static bool State_Mppt(StatusRecord s) + { + s.StateMachine.Message = "MPPT: keep requesting connect (11)."; + EnforcePowerRules(s); + + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + + private static bool State_Starting(StatusRecord s) + { + s.StateMachine.Message = "STARTING: keep requesting connect (11), wait for 10/11/5."; + EnforcePowerRules(s); + + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + + private static bool State_Precharge(StatusRecord s) + { + s.StateMachine.Message = "PRECHARGE: keep requesting connect (11), wait."; + EnforcePowerRules(s); + + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + + private static bool State_GridPreConnected(StatusRecord s) + { + s.StateMachine.Message = "GRID_PRE_CONNECTED: keep requesting connect (11), wait for 11/5."; + EnforcePowerRules(s); + + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + + private static bool State_GridConnected(StatusRecord s) + { + s.StateMachine.Message = "GRID_CONNECTED: running. Power writes allowed."; + + // Keep request latched + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + + // Here you may write power setpoints (your own targets) + // Example: + // s.InverterRecord.ControlMode = ControlModeEnum.RpcRemote; + s.InverterRecord.ActivePowerSetPercent = s.Config.ActivePowerPercent; + // s.InverterRecord.ReactivePowerSetPercent = s.Targets.ReactivePowerPercent; + + return true; // end goal reached + } + + private static bool State_Throttled(StatusRecord s) + { + s.StateMachine.Message = "THROTTLED: still running. Power writes allowed."; + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + + // Power writes allowed here too + return true; + } + + private static bool State_ShuttingDown(StatusRecord s) + { + s.StateMachine.Message = "SHUTTING_DOWN: keep requesting connect (11); will reconnect after reaching 8/1."; + EnforcePowerRules(s); + + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + + private static bool State_Fault(StatusRecord s) + { + s.StateMachine.Message = "FAULT: power=0 and acknowledge with RequestedState=1 (OFF)."; + EnforcePowerRules(s); + + // Per doc: acknowledge uses RequestedState=1 + s.InverterRecord.RequestedState = ReuqestedState.Off; + return false; + } + + private static bool State_NoErrorPending(StatusRecord s) + { + s.StateMachine.Message = "NO_ERROR_PENDING: acknowledge with RequestedState=1 then controller will request 11 next cycles."; + EnforcePowerRules(s); + + // Per doc Step 8: set RequestedState to 1 to acknowledge + s.InverterRecord.RequestedState = ReuqestedState.Off; + return false; + } + + private static bool UnknownState(StatusRecord s) + { + s.StateMachine.Message = $"UNKNOWN CurrentState={s.InverterRecord.CurrentState}. For safety, power=0 and request 11."; + EnforcePowerRules(s); + + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } +} + + +/* + +public static class Controller +{ + private static UInt16 GetSystemState(this StatusRecord r) + { + if (r.InverterRecord != null) + { + return (UInt16)r.InverterRecord.CurrentState; + } + else + { + return (UInt16)StateMachine.Default.State; + } + + } + + public static Boolean ControlSystemState(this StatusRecord s) + { + s.StateMachine.State = s.GetSystemState(); + + var cs = s.InverterRecord?.CurrentState; // 64201.CurrentState (1..12) + s.StateMachine.State = (UInt16)cs; + + return s.StateMachine.State switch + { + 1 => State_Off(s), + 2 => State_Sleeping(s), + 3 => State_Starting(s), + 4 => State_Mppt(s), + 5 => State_Throttled(s), + 6 => State_ShuttingDown(s), + 7 => State_Fault(s), + 8 => State_Standby(s), + 9 => State_Precharge(s), + 10 => State_GridPreConnected(s), + 11 => State_GridConnected(s), + 12 => State_NoErrorPending(s), + _ => UnknownState(s) + }; + } + + // ───────────────────────────────────────────── + // Global rule: only allow power writes in 11 or 5 + // ───────────────────────────────────────────── + private static void EnforcePowerRules(StatusRecord s) + { + var cs = s.InverterRecord?.CurrentState; + + // must be 0 outside (11) or (5) + s.InverterRecord.ActivePowerSetPercent = 0f; + s.InverterRecord.ReactivePowerSetPercent = 0f; } + } + + // ───────────────────────────────────────────── + // State handlers (based purely on CurrentState) + // ───────────────────────────────────────────── + + private static Boolean State_Off(StatusRecord s) + { + s.StateMachine.Message = "OFF: write limits (once) and request connect (11)."; + EnforcePowerRules(s); + + // Write limits (ignore details) + s.InverterRecord.WriteLimits(); + + // Always aim for running + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + + + private static bool State_Sleeping(StatusRecord s) + { + s.StateMachine.Message = "SLEEPING: write limits (once) and request connect (11)."; + EnforcePowerRules(s); + + s.InverterRecord.WriteLimits(); + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + + private static bool State_Standby(StatusRecord s) + { + s.StateMachine.Message = "STANDBY: write limits (once) and request connect (11)."; + EnforcePowerRules(s); + + s.InverterRecord.WriteLimits(); + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + + private static bool State_Mppt(StatusRecord s) + { + s.StateMachine.Message = "MPPT: keep requesting connect (11)."; + EnforcePowerRules(s); + + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + + private static bool State_Starting(StatusRecord s) + { + s.StateMachine.Message = "STARTING: keep requesting connect (11), wait for 10/11/5."; + EnforcePowerRules(s); + + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + + private static bool State_Precharge(StatusRecord s) + { + s.StateMachine.Message = "PRECHARGE: keep requesting connect (11), wait."; + EnforcePowerRules(s); + + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + + private static bool State_GridPreConnected(StatusRecord s) + { + s.StateMachine.Message = "GRID_PRE_CONNECTED: keep requesting connect (11), wait for 11/5."; + EnforcePowerRules(s); + + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + + private static bool State_GridConnected(StatusRecord s) + { + s.StateMachine.Message = "GRID_CONNECTED: running. Power writes allowed."; + + // Keep request latched + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + + // Here you may write power setpoints (your own targets) + // Example: + // s.InverterRecord.ControlMode = ControlModeEnum.RpcRemote; + // s.InverterRecord.ActivePowerSetPercent = s.Targets.ActivePowerPercent; + // s.InverterRecord.ReactivePowerSetPercent = s.Targets.ReactivePowerPercent; + + return true; // end goal reached + } + + private static bool State_Throttled(StatusRecord s) + { + s.StateMachine.Message = "THROTTLED: still running. Power writes allowed."; + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + + // Power writes allowed here too + return true; + } + + private static bool State_ShuttingDown(StatusRecord s) + { + s.StateMachine.Message = "SHUTTING_DOWN: keep requesting connect (11); will reconnect after reaching 8/1."; + EnforcePowerRules(s); + + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + + private static bool State_Fault(StatusRecord s) + { + s.StateMachine.Message = "FAULT: power=0 and acknowledge with RequestedState=1 (OFF)."; + EnforcePowerRules(s); + + // Per doc: acknowledge uses RequestedState=1 + s.InverterRecord.RequestedState = ReuqestedState.Off; + return false; + } + + private static bool State_NoErrorPending(StatusRecord s) + { + s.StateMachine.Message = "NO_ERROR_PENDING: acknowledge with RequestedState=1 then controller will request 11 next cycles."; + EnforcePowerRules(s); + + // Per doc Step 8: set RequestedState to 1 to acknowledge + s.InverterRecord.RequestedState = ReuqestedState.Off; + return false; + } + + private static bool UnknownState(StatusRecord s) + { + s.StateMachine.Message = $"UNKNOWN CurrentState={s.InverterRecord.CurrentState}. For safety, power=0 and request 11."; + EnforcePowerRules(s); + + s.InverterRecord.RequestedState = ReuqestedState.GridConnected; + return false; + } + +}*/ \ No newline at end of file From 545bb96b9d854c03c5a8b08a485329471a52fd25 Mon Sep 17 00:00:00 2001 From: atef Date: Fri, 27 Feb 2026 11:20:32 +0100 Subject: [PATCH 3/8] Update webscoket server failling --- csharp/App/GrowattCommunication/Program.cs | 65 ++- csharp/App/SinexcelCommunication/Program.cs | 408 +++++++++++++++--- .../Protocols/Modbus/Slaves/ModbusDevice.cs | 2 +- 3 files changed, 396 insertions(+), 79 deletions(-) diff --git a/csharp/App/GrowattCommunication/Program.cs b/csharp/App/GrowattCommunication/Program.cs index f643a613a..78461c1c9 100644 --- a/csharp/App/GrowattCommunication/Program.cs +++ b/csharp/App/GrowattCommunication/Program.cs @@ -170,8 +170,6 @@ public static class Program statusrecord.InverterRecord.WarningMainCode.WriteLine(" = WarningMainCode"); // 30408 this the duration statusrecord.InverterRecord.WarningSubCode.WriteLine(" = WarningSubCode"); // 30408 this the duration - - EssModeControl(statusrecord); statusrecord.ApplyDefaultSettings(); @@ -182,7 +180,6 @@ public static class Program statusrecord.Config.Save(); // save the config file growattDeviceT415K.Write(statusrecord.InverterRecord); Console.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff")); - } return statusrecord; } @@ -342,8 +339,7 @@ public static class Program var s3Bucket = Config.Load().S3?.Bucket; var subscribedNow = false; - //When the controller boots, it tries to subscribe to the queue - if (_subscribeToQueueForTheFirstTime == false) + if (_subscribeToQueueForTheFirstTime == false) { subscribedNow = true; _subscribeToQueueForTheFirstTime = true; @@ -356,12 +352,33 @@ public static class Program var stateChanged = currentSalimaxState.Status != _prevSodiohomeAlarmState; var contentChanged = HasErrorsOrWarningsChanged(currentSalimaxState); var needsHeartbeat = (DateTime.Now - _lastHeartbeatTime).TotalSeconds >= HeartbeatIntervalSeconds; + Console.WriteLine($"subscribedNow={subscribedNow}"); + Console.WriteLine($"_subscribedToQueue={_subscribedToQueue}"); + Console.WriteLine($"stateChanged={stateChanged}"); + Console.WriteLine($"contentChanged={contentChanged}"); + Console.WriteLine($"needsHeartbeat={needsHeartbeat}"); + Console.WriteLine($"s3Bucket null? {s3Bucket == null}"); - // Send message if: state changed, content changed, OR heartbeat needed - if (!subscribedNow && _subscribedToQueue && (stateChanged || contentChanged || needsHeartbeat)) + if (s3Bucket == null) + { + Console.WriteLine("⚠ S3 bucket not configured. Skipping middleware send."); + LogMiddlewareFailure(new Exception("S3 Bucket not configured")); + return; + } + + // Ensure connection FIRST + if (!RabbitMqManager.EnsureConnected(currentSalimaxState, s3Bucket, VpnServerIp)) + { + Console.WriteLine($"❌ RabbitMQ EnsureConnected FAILED at {DateTime.Now:HH:mm:ss.fff}"); + LogMiddlewareFailure(new Exception("EnsureConnected returned false")); + return; + } + + //If already subscribed to the queue and the status has been changed, update the queue + if (!subscribedNow && (stateChanged || contentChanged || needsHeartbeat)) { _prevSodiohomeAlarmState = currentSalimaxState.Status; - + // Set appropriate message type if (stateChanged || contentChanged) { @@ -374,13 +391,18 @@ public static class Program Console.WriteLine($"Sending Heartbeat message - {HeartbeatIntervalSeconds}s interval reached"); _lastHeartbeatTime = DateTime.Now; } - - if (s3Bucket != null) // why this is based on s3 bucket? + + try { RabbitMqManager.InformMiddleware(currentSalimaxState); + //LogMiddlewareFailure(new Exception($"✅ Middleware message sent at {DateTime.Now:HH:mm:ss.fff}")); + } + catch (Exception ex) + { + Console.WriteLine($"❌ Failed to send middleware message: {ex.Message}"); + LogMiddlewareFailure(ex); } } - //If there is an available message from the RabbitMQ Broker, apply the configuration file Configuration? config = SetConfigurationFile(); if (config != null) @@ -438,7 +460,28 @@ public static class Program } return registers; } + + private static void LogMiddlewareFailure(Exception ex) + { + try + { + var logPath = "/home/inesco/SodiStoreHome/middleware_failures.log"; + var logEntry = + $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}]\n" + + $"Exception: {ex.GetType().FullName}\n" + + $"Message: {ex.Message}\n" + + $"StackTrace:\n{ex.StackTrace}\n" + + $"--------------------------------------------------\n"; + + File.AppendAllText(logPath, logEntry); + } + catch + { + // Never allow logging to crash the service + } + } + [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize>(System.Collections.Generic.Dictionary, System.Text.Json.JsonSerializerOptions?)")] private static async Task SaveModbusTcpFile(StatusRecord status) { diff --git a/csharp/App/SinexcelCommunication/Program.cs b/csharp/App/SinexcelCommunication/Program.cs index 26b210dd4..40e269f3e 100644 --- a/csharp/App/SinexcelCommunication/Program.cs +++ b/csharp/App/SinexcelCommunication/Program.cs @@ -25,6 +25,8 @@ using JsonSerializer = System.Text.Json.JsonSerializer; using static InnovEnergy.App.SinexcelCommunication.MiddlewareClasses.MiddlewareAgent; using System.Diagnostics.CodeAnalysis; using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType; +using InnovEnergy.Lib.Protocols.Modbus.Protocol; +using static InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType.WorkingMode; #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. @@ -36,17 +38,28 @@ internal static class Program private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(5); private const UInt16 NbrOfFileToConcatenate = 15; // add this to config file private static UInt16 _fileCounter = 0; - private static Channel _sinexcelChannel; + private static Channel _sinexcelChannel1; + private static Channel _sinexcelChannel2; - private const String SwVersionNumber =" V1.00.131025 beta"; + private static readonly String SwVersionNumber = " V1.00." + DateTime.Today; private const String VpnServerIp = "10.2.0.11"; private static Boolean _subscribedToQueue = false; private static Boolean _subscribeToQueueForTheFirstTime = false; private static SodistoreAlarmState _prevSodiohomeAlarmState = SodistoreAlarmState.Green; private static SodistoreAlarmState _sodiohomeAlarmState = SodistoreAlarmState.Green; + // Tracking for error/warning content changes + private static List _prevErrorCodes = new List(); + private static List _prevWarningCodes = new List(); + + // Heartbeat tracking + private static DateTime _lastHeartbeatTime = DateTime.MinValue; + private const Int32 HeartbeatIntervalSeconds = 60; + + // move all this to config file - private const String Port = "/dev/ttyUSB0"; + private const String Port1 = "/dev/ttyUSB0"; + private const String Port2 = "/dev/ttyUSB1"; private const Byte SlaveId = 1; private const Parity Parity = 0; //none private const Int32 StopBits = 1; @@ -55,7 +68,9 @@ internal static class Program public static async Task Main(String[] args) { - _sinexcelChannel = new SerialPortChannel(Port, BaudRate, Parity, DataBits, StopBits); + _sinexcelChannel1 = new SerialPortChannel(Port1, BaudRate, Parity, DataBits, StopBits); + _sinexcelChannel2 = new SerialPortChannel(Port2, BaudRate, Parity, DataBits, StopBits); + InitializeCommunicationToMiddleware(); while (true) { @@ -75,19 +90,22 @@ internal static class Program { Watchdog.NotifyReady(); - Console.WriteLine("Starting Sinexcel Communication"); + Console.WriteLine("Starting Sinexcel Communication, SW Version : " + SwVersionNumber); - var sinexcelDevice = new SinexcelDevice(_sinexcelChannel, SlaveId); - // var sinexcelDevice = new sinexcelDevices(new List { growattDeviceT415K }); + var sinexcelDevice1 = new SinexcelDevice(_sinexcelChannel1, SlaveId); + var sinexcelDevice2 = new SinexcelDevice(_sinexcelChannel2, SlaveId); StatusRecord? ReadStatus() { var config = Config.Load(); - var sinexcelRecord = sinexcelDevice.Read(); + var sinexcelRecord1 = sinexcelDevice1.Read(); + var sinexcelRecord2 = sinexcelDevice2.Read(); + return new StatusRecord { - InverterRecord = sinexcelRecord, + InverterRecord1 = sinexcelRecord1, + InverterRecord2 = sinexcelRecord2, Config = config // load from disk every iteration, so config can be changed while running }; } @@ -120,63 +138,176 @@ internal static class Program if (statusrecord == null) return null; - // Console.WriteLine( statusrecord.InverterRecord.TimedChargeAndDischargeOff + " TimedChargeAndDischargeOff "); - Console.WriteLine( statusrecord.InverterRecord.Battery1Power + " Battery1Power "); - Console.WriteLine( statusrecord.InverterRecord.Battery1Soc + " Battery1Soc "); - Console.WriteLine( statusrecord.InverterRecord.RepetitiveWeeks + " RepetitiveWeeks "); - Console.WriteLine( statusrecord.InverterRecord.EffectiveStartDate + " EffectiveStartDate "); - Console.WriteLine( statusrecord.InverterRecord.EffectiveEndDate + " EffectiveEndDate "); - Console.WriteLine( statusrecord.InverterRecord.ChargingPowerPeriod1 + " ChargingPowerPeriod1 "); - Console.WriteLine( statusrecord.InverterRecord.ChargeStartTimePeriod1 + " ChargeStartTimePeriod1 "); - Console.WriteLine( statusrecord.InverterRecord.ChargeEndTimePeriod1 + " ChargeEndTimePeriod1 "); - - Console.WriteLine( statusrecord.InverterRecord.DischargeStartTimePeriod1 + " DischargeStartTimePeriod1 "); - Console.WriteLine( statusrecord.InverterRecord.DischargeEndTimePeriod1 + " DischargeEndTimePeriod1 "); + Console.WriteLine(" ************************************************ Inverter 1 ************************************************ "); + Console.WriteLine( statusrecord.InverterRecord1.SystemDateTime + " SystemDateTime "); + + Console.WriteLine( statusrecord.InverterRecord1.TotalPhotovoltaicPower + " TotalPhotovoltaicPower "); + Console.WriteLine( statusrecord.InverterRecord1.TotalBatteryPower + " TotalBatteryPower "); + Console.WriteLine( statusrecord.InverterRecord1.TotalLoadPower + " TotalLoadPower "); + Console.WriteLine( statusrecord.InverterRecord1.TotalGridPower + " TotalGridPower "); - Console.WriteLine( statusrecord.InverterRecord.PowerOn + " PowerOn "); - Console.WriteLine( statusrecord.InverterRecord.PowerOff + " PowerOff "); - Console.WriteLine( statusrecord.InverterRecord.WorkingMode + " WorkingMode "); - // Console.WriteLine( statusrecord.InverterRecord.FaultClearing + " FaultClearing "); - SendSalimaxStateAlarm(GetSodiHomeStateAlarm(statusrecord),statusrecord); + + Console.WriteLine( statusrecord.InverterRecord1.Battery1Power + " Battery1Power "); + Console.WriteLine( statusrecord.InverterRecord1.Battery1Soc + " Battery1Soc "); + Console.WriteLine( statusrecord.InverterRecord1.Battery1BackupSoc + " Battery1BackupSoc "); + Console.WriteLine( statusrecord.InverterRecord1.Battery1MinSoc + " Battery1MinSoc "); + + Console.WriteLine( statusrecord.InverterRecord1.Battery2Power + " Battery2Power "); + Console.WriteLine( statusrecord.InverterRecord1.Battery2Soc + " Battery2Soc "); + Console.WriteLine( statusrecord.InverterRecord1.Battery2BackupSoc + " Battery2BackupSoc "); + Console.WriteLine( statusrecord.InverterRecord1.Battery2MinSoc + " Battery2MinSoc "); + + Console.WriteLine( statusrecord.InverterRecord1.EnableGridExport + " EnableGridExport "); + Console.WriteLine( statusrecord.InverterRecord1.PowerGridExportLimit + " PowerGridExportLimit "); + + Console.WriteLine( statusrecord.InverterRecord1.PowerOn + " PowerOn "); + Console.WriteLine( statusrecord.InverterRecord1.PowerOff + " PowerOff "); + + + Console.WriteLine( statusrecord.InverterRecord1.WorkingMode + " WorkingMode "); + + Console.WriteLine( statusrecord.InverterRecord1.GridSwitchMethod + " GridSwitchMethod "); + + Console.WriteLine( statusrecord.InverterRecord1.ThreePhaseWireSystem + " ThreePhaseWireSystem "); + + Console.WriteLine(" ************************************************ Inverter 2 ************************************************ "); + + Console.WriteLine( statusrecord.InverterRecord2.SystemDateTime + " SystemDateTime "); + Console.WriteLine( statusrecord.InverterRecord2.TotalPhotovoltaicPower + " TotalPhotovoltaicPower "); + Console.WriteLine( statusrecord.InverterRecord2.TotalBatteryPower + " TotalBatteryPower "); + Console.WriteLine( statusrecord.InverterRecord2.TotalLoadPower + " TotalLoadPower "); + Console.WriteLine( statusrecord.InverterRecord2.TotalGridPower + " TotalGridPower "); + Console.WriteLine( statusrecord.InverterRecord2.Battery1Power + " Battery1Power "); + Console.WriteLine( statusrecord.InverterRecord2.Battery1Soc + " Battery1Soc "); + Console.WriteLine( statusrecord.InverterRecord2.Battery1BackupSoc + " Battery1BackupSoc "); + Console.WriteLine( statusrecord.InverterRecord2.Battery1MinSoc + " Battery1MinSoc "); + Console.WriteLine( statusrecord.InverterRecord2.Battery2Power + " Battery2Power "); + Console.WriteLine( statusrecord.InverterRecord2.Battery2Soc + " Battery2Soc "); + Console.WriteLine( statusrecord.InverterRecord2.Battery2BackupSoc + " Battery2BackupSoc "); + Console.WriteLine( statusrecord.InverterRecord2.Battery2MinSoc + " Battery2MinSoc "); + + Console.WriteLine( statusrecord.InverterRecord2.EnableGridExport + " EnableGridExport "); + Console.WriteLine( statusrecord.InverterRecord2.PowerGridExportLimit + " PowerGridExportLimit "); + Console.WriteLine( statusrecord.InverterRecord2.PowerOn + " PowerOn "); + Console.WriteLine( statusrecord.InverterRecord2.PowerOff + " PowerOff "); + Console.WriteLine( statusrecord.InverterRecord2.WorkingMode + " WorkingMode "); + Console.WriteLine( statusrecord.InverterRecord2.GridSwitchMethod + " GridSwitchMethod "); + Console.WriteLine( statusrecord.InverterRecord2.ThreePhaseWireSystem + " ThreePhaseWireSystem "); + /* + Console.WriteLine( statusrecord.InverterRecord1.RepetitiveWeeks + " RepetitiveWeeks "); + Console.WriteLine( statusrecord.InverterRecord1.EffectiveStartDate + " EffectiveStartDate "); + Console.WriteLine( statusrecord.InverterRecord1.EffectiveEndDate + " EffectiveEndDate "); + Console.WriteLine( statusrecord.InverterRecord1.ChargingPowerPeriod1 + " ChargingPowerPeriod1 "); + Console.WriteLine( statusrecord.InverterRecord1.DishargingPowerPeriod1 + " dischargingPowerPeriod1 "); + Console.WriteLine( statusrecord.InverterRecord1.ChargeStartTimePeriod1 + " ChargeStartTimePeriod1 "); + Console.WriteLine( statusrecord.InverterRecord1.ChargeEndTimePeriod1 + " ChargeEndTimePeriod1 "); + + Console.WriteLine( statusrecord.InverterRecord1.DischargeStartTimePeriod1 + " DischargeStartTimePeriod1 "); + Console.WriteLine( statusrecord.InverterRecord1.DischargeEndTimePeriod1 + " DischargeEndTimePeriod1 ");*/ + + SendSalimaxStateAlarm(GetSodiHomeStateAlarm(statusrecord),statusrecord); + statusrecord.ControlConstants(); Console.WriteLine( " ************************************ We are writing ************************************"); var startWritingTime = DateTime.Now; Console.WriteLine(startWritingTime.ToString("HH:mm:ss.fff ") +"start Writing"); statusrecord?.Config.Save(); // save the config file - - if (statusrecord != null) + + if (statusrecord is { Config.ControlPermission: true }) { - statusrecord.InverterRecord.WorkingMode = WorkingMode.TimeChargeDischarge; + Console.WriteLine(" We have the Right to Write"); + sinexcelDevice1.Write(statusrecord.InverterRecord1); + sinexcelDevice2.Write(statusrecord.InverterRecord2); - //statusrecord.InverterRecord.TimedChargeAndDischargeOff = 0; - statusrecord.InverterRecord.RepetitiveWeeks = SinexcelWeekDays.All; - statusrecord.InverterRecord.EffectiveStartDate = DateTime.Today.AddDays(-1); - statusrecord.InverterRecord.EffectiveEndDate = DateTime.Today.AddDays(2); - statusrecord.InverterRecord.ChargingPowerPeriod1 = 3; - statusrecord.InverterRecord.DishargingPowerPeriod1 = 3; - statusrecord.InverterRecord.DischargeStartTimePeriod1 = new TimeSpan(16, 00, 0); - statusrecord.InverterRecord.DischargeEndTimePeriod1 = new TimeSpan(17, 03, 0); - statusrecord.InverterRecord.PowerOn = 1; - statusrecord.InverterRecord.PowerOff = 0; - //statusrecord.InverterRecord.FaultClearing = 1; } - if (statusrecord?.InverterRecord != null) sinexcelDevice.Write(statusrecord.InverterRecord); - - var stopTime = DateTime.Now; - Console.WriteLine(stopTime.ToString("HH:mm:ss.fff ")+ "run iteration time finish"); + else + { + Console.WriteLine(" Nooooooo We cant' have the Right to Write"); + } + return statusrecord; } + catch (CrcException e) + { + Console.WriteLine(e); + throw; // restart only on CRC + } catch (Exception e) { - // Handle exception and print the error - Console.WriteLine(e ); - return null; + Console.WriteLine(e); + return null; // keep running for non-critical errors } - } } + + private static void ControlConstants(this StatusRecord? statusrecord) + { + if (statusrecord == null) return; + + statusrecord.InverterRecord1.Battery1BackupSoc = (Single)statusrecord.Config.MinSoc ; + statusrecord.InverterRecord1.Battery2BackupSoc = (Single)statusrecord.Config.MinSoc ; + statusrecord.InverterRecord1.RepetitiveWeeks = SinexcelWeekDays.All; + + + var isChargePeriod = IsNowInsideDateAndTime(statusrecord.Config.StartTimeChargeandDischargeDayandTime, statusrecord.Config.StopTimeChargeandDischargeDayandTime); + + + Console.WriteLine("Are we inside the charge/Discharge time " + isChargePeriod); + + if (statusrecord.Config.OperatingPriority != TimeChargeDischarge) + { + statusrecord.InverterRecord1.WorkingMode = statusrecord.Config.OperatingPriority; + } + else if (statusrecord.Config.OperatingPriority == TimeChargeDischarge && isChargePeriod) + { + statusrecord.InverterRecord1.WorkingMode = statusrecord.Config.OperatingPriority; + + if (statusrecord.Config.TimeChargeandDischargePower > 0) + { + statusrecord.InverterRecord1.EffectiveStartDate = statusrecord.Config.StartTimeChargeandDischargeDayandTime.Date; + statusrecord.InverterRecord1.EffectiveEndDate = statusrecord.Config.StopTimeChargeandDischargeDayandTime.Date; + statusrecord.InverterRecord1.ChargingPowerPeriod1 = Math.Abs(statusrecord.Config.TimeChargeandDischargePower); + statusrecord.InverterRecord1.ChargeStartTimePeriod1 = statusrecord.Config.StartTimeChargeandDischargeDayandTime.TimeOfDay; + statusrecord.InverterRecord1.ChargeEndTimePeriod1 = statusrecord.Config.StopTimeChargeandDischargeDayandTime.TimeOfDay; + + statusrecord.InverterRecord1.DischargeStartTimePeriod1 = TimeSpan.Zero; + statusrecord.InverterRecord1.DischargeEndTimePeriod1 = TimeSpan.Zero; + } + else + { + statusrecord.InverterRecord1.EffectiveStartDate = statusrecord.Config.StartTimeChargeandDischargeDayandTime.Date; + statusrecord.InverterRecord1.EffectiveEndDate = statusrecord.Config.StopTimeChargeandDischargeDayandTime.Date; + statusrecord.InverterRecord1.DishargingPowerPeriod1 = Math.Abs(statusrecord.Config.TimeChargeandDischargePower); + statusrecord.InverterRecord1.DischargeStartTimePeriod1 = statusrecord.Config.StartTimeChargeandDischargeDayandTime.TimeOfDay; + statusrecord.InverterRecord1.DischargeEndTimePeriod1 = statusrecord.Config.StopTimeChargeandDischargeDayandTime.TimeOfDay; + + statusrecord.InverterRecord1.ChargeStartTimePeriod1 = TimeSpan.Zero; + statusrecord.InverterRecord1.ChargeEndTimePeriod1 = TimeSpan.Zero; + } + + } + else + { + statusrecord.InverterRecord1.WorkingMode = SpontaneousSelfUse; + } + statusrecord.InverterRecord1.PowerOn = 1; + statusrecord.InverterRecord1.PowerOff = 0; + //statusrecord.InverterRecord.FaultClearing = 1; + } - private static StatusMessage GetSodiHomeStateAlarm(StatusRecord? record) + private static bool IsNowInsideDateAndTime(DateTime effectiveStart, DateTime effectiveEnd) + { + DateTime now = DateTime.Now; + + // Date check + if (now < effectiveStart || now > effectiveEnd) + return false; + + return true; + } + + + private static StatusMessage GetSodiHomeStateAlarm(StatusRecord? record) { var s3Bucket = Config.Load().S3?.Bucket; @@ -231,6 +362,43 @@ internal static class Program return returnedStatus; } + /// + /// Checks if the error or warning content has changed compared to the previous state. + /// This allows detection of new/cleared errors even when the overall alarm state (Red/Orange/Green) remains the same. + /// + private static Boolean HasErrorsOrWarningsChanged(StatusMessage currentState) + { + // Get current error codes (descriptions) + var currentErrors = currentState.Alarms? + .Select(a => a.Description ?? String.Empty) + .OrderBy(d => d) // Sort for consistent comparison + .ToList() ?? new List(); + + // Get current warning codes (descriptions) + var currentWarnings = currentState.Warnings? + .Select(w => w.Description ?? String.Empty) + .OrderBy(d => d) // Sort for consistent comparison + .ToList() ?? new List(); + + // Check if lists have changed (new items added or existing items removed) + var errorsChanged = !currentErrors.SequenceEqual(_prevErrorCodes); + var warningsChanged = !currentWarnings.SequenceEqual(_prevWarningCodes); + + // Update tracking if changes detected + if (errorsChanged || warningsChanged) + { + Console.WriteLine($"Error/Warning content changed:"); + Console.WriteLine($" Errors: {String.Join(", ", currentErrors)} (was: {String.Join(", ", _prevErrorCodes)})"); + Console.WriteLine($" Warnings: {String.Join(", ", currentWarnings)} (was: {String.Join(", ", _prevWarningCodes)})"); + + _prevErrorCodes = currentErrors; + _prevWarningCodes = currentWarnings; + return true; + } + + return false; + } + private static Int32 GetInstallationId(String s3Bucket) { var part = s3Bucket.Split('-').FirstOrDefault(); @@ -249,14 +417,63 @@ internal static class Program _subscribeToQueueForTheFirstTime = true; _prevSodiohomeAlarmState = currentSalimaxState.Status; _subscribedToQueue = RabbitMqManager.SubscribeToQueue(currentSalimaxState, s3Bucket, VpnServerIp); + _lastHeartbeatTime = DateTime.Now; // Initialize heartbeat timer } + // Check if we should send a message + var stateChanged = currentSalimaxState.Status != _prevSodiohomeAlarmState; + var contentChanged = HasErrorsOrWarningsChanged(currentSalimaxState); + var needsHeartbeat = (DateTime.Now - _lastHeartbeatTime).TotalSeconds >= HeartbeatIntervalSeconds; + Console.WriteLine($"subscribedNow={subscribedNow}"); + Console.WriteLine($"_subscribedToQueue={_subscribedToQueue}"); + Console.WriteLine($"stateChanged={stateChanged}"); + Console.WriteLine($"contentChanged={contentChanged}"); + Console.WriteLine($"needsHeartbeat={needsHeartbeat}"); + Console.WriteLine($"s3Bucket null? {s3Bucket == null}"); + + if (s3Bucket == null) + { + Console.WriteLine("⚠ S3 bucket not configured. Skipping middleware send."); + LogMiddlewareFailure(new Exception("S3 Bucket not configured")); + return; + } + + // Ensure connection FIRST + if (!RabbitMqManager.EnsureConnected(currentSalimaxState, s3Bucket, VpnServerIp)) + { + Console.WriteLine($"❌ RabbitMQ EnsureConnected FAILED at {DateTime.Now:HH:mm:ss.fff}"); + LogMiddlewareFailure(new Exception("EnsureConnected returned false")); + return; + } + //If already subscribed to the queue and the status has been changed, update the queue - if (!subscribedNow && _subscribedToQueue && currentSalimaxState.Status != _prevSodiohomeAlarmState) + if (!subscribedNow && (stateChanged || contentChanged || needsHeartbeat)) { _prevSodiohomeAlarmState = currentSalimaxState.Status; - if (s3Bucket != null) + + // Set appropriate message type + if (stateChanged || contentChanged) + { + currentSalimaxState.Type = MessageType.AlarmOrWarning; + Console.WriteLine($"Sending AlarmOrWarning message - StateChanged: {stateChanged}, ContentChanged: {contentChanged}"); + } + else if (needsHeartbeat) + { + currentSalimaxState.Type = MessageType.Heartbit; + Console.WriteLine($"Sending Heartbeat message - {HeartbeatIntervalSeconds}s interval reached"); + _lastHeartbeatTime = DateTime.Now; + } + + try + { RabbitMqManager.InformMiddleware(currentSalimaxState); + LogMiddlewareFailure(new Exception($"✅ Middleware message sent at {DateTime.Now:HH:mm:ss.fff}")); + } + catch (Exception ex) + { + Console.WriteLine($"❌ Failed to send middleware message: {ex.Message}"); + LogMiddlewareFailure(ex); + } } //If there is an available message from the RabbitMQ Broker, apply the configuration file @@ -267,31 +484,89 @@ internal static class Program } } + private static void LogMiddlewareFailure(Exception ex) + { + try + { + var logPath = "/home/inesco/SodiStoreHome/middleware_failures.log"; + + var logEntry = + $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}]\n" + + $"Exception: {ex.GetType().FullName}\n" + + $"Message: {ex.Message}\n" + + $"StackTrace:\n{ex.StackTrace}\n" + + $"--------------------------------------------------\n"; + + File.AppendAllText(logPath, logEntry); + } + catch + { + // Never allow logging to crash the service + } + } + + private static void ApplyConfigFile(this StatusRecord? status, Configuration? config) { if (config == null) return; if (status == null) return; - status.Config.MinSoc = config.MinimumSoC; - status.Config.MaximumChargingCurrent = config.MaximumChargingCurrent; - status.Config.MaximumDischargingCurrent = config.MaximumDischargingCurrent; - status.Config.OperatingPriority = config.OperatingPriority; - status.Config.BatteriesCount = config.BatteriesCount; + status.Config.MinSoc = config.MinimumSoC; + status.Config.MaximumChargingCurrent = config.MaximumChargingCurrent; + status.Config.MaximumDischargingCurrent = config.MaximumDischargingCurrent; + status.Config.OperatingPriority = config.OperatingPriority; + status.Config.BatteriesCount = config.BatteriesCount; + status.Config.ClusterNumber = config.ClusterNumber; + status.Config.StartTimeChargeandDischargeDayandTime = config.StartTimeChargeandDischargeDayandTime; + status.Config.StopTimeChargeandDischargeDayandTime = config.StopTimeChargeandDischargeDayandTime; + status.Config.TimeChargeandDischargePower = config.TimeChargeandDischargePower; + + status.Config.PvNumber = config.PvNumber; + status.Config.ControlPermission = config.ControlPermission; + } - private static async Task SaveModbusTcpFile(StatusRecord status) + private static async Task SaveModbusTcpFile(StatusRecord status) { var modbusData = new Dictionary(); // SYSTEM DATA var result1 = ConvertToModbusRegisters((status.Config.ModbusProtcolNumber * 10), "UInt16", 30001); // this to be updated to modbusTCP version - var result2 = ConvertToModbusRegisters(status.InverterRecord.SystemDateTime.ToUnixTime(), "UInt32", 30002); - - // Merge all results into one dictionary + var result2 = ConvertToModbusRegisters(status.InverterRecord1.SystemDateTime.ToUnixTime(), "UInt32", 30002); + // SYSTEM DATA + var result3 = ConvertToModbusRegisters(status.InverterRecord1.WorkingMode, "UInt16", 30004); + + // BATTERY SUMMARY (assuming single battery [0]) // this to be improved + + var result4 = ConvertToModbusRegisters((status.Config.BatteriesCount), "UInt16", 31000); + var result8 = ConvertToModbusRegisters((status.InverterRecord1.Battery1Voltage.Value * 10), "UInt16", 31001); + var result12 = ConvertToModbusRegisters((status.InverterRecord1.Battery2Voltage.Value * 10), "Int16", 31002); + var result13 = ConvertToModbusRegisters((status.InverterRecord1.Battery1Current.Value * 10), "Int32", 31003); + var result16 = ConvertToModbusRegisters((status.InverterRecord1.Battery2Current.Value * 10), "Int32", 31005); + var result9 = ConvertToModbusRegisters((status.InverterRecord1.Battery1Soc.Value * 100), "UInt16", 31007); + var result14 = ConvertToModbusRegisters((status.InverterRecord1.Battery2Soc.Value * 100), "UInt16", 31008); + var result5 = ConvertToModbusRegisters((status.InverterRecord1.TotalBatteryPower.Value * 10), "Int32", 31009); + + var result7 = ConvertToModbusRegisters((status.InverterRecord1.Battery1BackupSoc * 100), "UInt16", 31011); + var result20 = ConvertToModbusRegisters((status.InverterRecord1.Battery2BackupSoc * 100), "UInt16", 31012); + var result15 = ConvertToModbusRegisters((status.InverterRecord1.Battery1Soh.Value * 100), "UInt16", 31013); + var result26 = ConvertToModbusRegisters((status.InverterRecord1.Battery2Soh.Value * 100), "UInt16", 31014); + var result21 = ConvertToModbusRegisters((status.InverterRecord1.Battery1MaxChargingCurrent * 10), "UInt16", 31016); + var result22 = ConvertToModbusRegisters((status.InverterRecord1.Battery1MaxDischargingCurrent * 10), "UInt16", 31017); + + var result18 = ConvertToModbusRegisters((status.InverterRecord1.PvTotalPower * 10), "UInt32", 32000); + var result19 = ConvertToModbusRegisters((status.InverterRecord1.GridPower * 10), "Int32", 33000); + var result23 = ConvertToModbusRegisters((status.InverterRecord1.GridVoltageFrequency * 10), "UInt16", 33002); + var result24 = ConvertToModbusRegisters((status.InverterRecord1.WorkingMode), "UInt16", 34000); + var result25 = ConvertToModbusRegisters((status.InverterRecord1.InverterActivePower * 10), "Int32", 34001); + var result29 = ConvertToModbusRegisters((status.InverterRecord1.EnableGridExport ), "UInt16", 34003); + var result27 = ConvertToModbusRegisters((status.InverterRecord1.PowerGridExportLimit ), "Int16", 34004); + + // Merge all results into one dictionary var allResults = new[] { - result1,result2 + result1, result2, result3, result4, result5, result23, result24, result25, result29, result27, result26, result7, result8, result9, result16, result20, result12, result13, result14, result15, result18, result19, result21, result22 }; foreach (var result in allResults) @@ -518,7 +793,7 @@ internal static class Program Console.WriteLine(error); await SaveToLocalCompressedFallback(compressedBytes, fileNameWithoutExtension); - Heartbit(); + // Heartbit(); return false; } @@ -526,7 +801,7 @@ internal static class Program Console.WriteLine("✅ File uploaded to S3 successfully."); Console.WriteLine("---------------------------------------- Resending FailedUploadedFiles----------------------------------------"); - Heartbit(); + // Heartbit(); await ResendLocalFailedFilesAsync(s3Config); // retry any pending failed files } @@ -539,7 +814,7 @@ internal static class Program await SaveToLocalCompressedFallback(compressedBytes, fileNameWithoutExtension); } - Heartbit(); + //Heartbit(); return false; } } @@ -548,11 +823,10 @@ internal static class Program return true; } - private static void Heartbit() + /* 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 @@ -565,7 +839,7 @@ internal static class Program if (s3Bucket != null) RabbitMqManager.InformMiddleware(returnedStatus); } - } + }*/ private static async Task SaveToLocalCompressedFallback(Byte[] compressedData, String fileNameWithoutExtension) { diff --git a/csharp/Lib/Protocols/Modbus/Slaves/ModbusDevice.cs b/csharp/Lib/Protocols/Modbus/Slaves/ModbusDevice.cs index d8ba2cabc..63bcf8a58 100644 --- a/csharp/Lib/Protocols/Modbus/Slaves/ModbusDevice.cs +++ b/csharp/Lib/Protocols/Modbus/Slaves/ModbusDevice.cs @@ -55,7 +55,7 @@ public class ModbusDevice<[DynamicallyAccessedMembers(All)] R> where R : notnull { _modbusClient.Channel.ClearBuffers(); batch.Write(record); - //Thread.Sleep(50); // this added mainly for Growatt reading + //Thread.Sleep(50); // this added mainly for Growatt writing } } } \ No newline at end of file From 1657cb2b612920d2b82e11e1cd892df233282b0c Mon Sep 17 00:00:00 2001 From: atef Date: Fri, 27 Feb 2026 12:55:02 +0100 Subject: [PATCH 4/8] Create ensure connection with webscoket --- .../MiddlewareClasses/RabbitMQManager.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/csharp/App/GrowattCommunication/MiddlewareClasses/RabbitMQManager.cs b/csharp/App/GrowattCommunication/MiddlewareClasses/RabbitMQManager.cs index 01c5ff692..76a220494 100644 --- a/csharp/App/GrowattCommunication/MiddlewareClasses/RabbitMQManager.cs +++ b/csharp/App/GrowattCommunication/MiddlewareClasses/RabbitMQManager.cs @@ -44,6 +44,30 @@ public static class RabbitMqManager return true; } + public static Boolean EnsureConnected(StatusMessage currentSalimaxState, string? s3Bucket, string vpnServerIp) + { + try + { + if (_connection == null || !_connection.IsOpen || + _channel == null || _channel.IsClosed) + { + Console.WriteLine("⚠ RabbitMQ connection lost. Reconnecting..."); + + _connection?.Dispose(); + _channel?.Dispose(); + + return SubscribeToQueue(currentSalimaxState, s3Bucket, vpnServerIp); + } + + return true; + } + catch (Exception ex) + { + Console.WriteLine("❌ Error while ensuring RabbitMQ connection: " + ex); + return false; + } + } + public static void InformMiddleware(StatusMessage status) { var message = JsonSerializer.Serialize(status); From d9de727c4126bb92230d80d0681c2aa3328907d6 Mon Sep 17 00:00:00 2001 From: atef Date: Fri, 27 Feb 2026 12:55:25 +0100 Subject: [PATCH 5/8] add writable PowerStage Enable for DcDc --- csharp/Lib/Devices/Trumpf/TruConvertDc/DcDcRecord.Modbus.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csharp/Lib/Devices/Trumpf/TruConvertDc/DcDcRecord.Modbus.cs b/csharp/Lib/Devices/Trumpf/TruConvertDc/DcDcRecord.Modbus.cs index f0d1a7747..c184d39ed 100644 --- a/csharp/Lib/Devices/Trumpf/TruConvertDc/DcDcRecord.Modbus.cs +++ b/csharp/Lib/Devices/Trumpf/TruConvertDc/DcDcRecord.Modbus.cs @@ -12,7 +12,7 @@ namespace InnovEnergy.Lib.Devices.Trumpf.TruConvertDc; public partial class DcDcRecord { //[Coil(4000)] - [HoldingRegister(4000)] internal Boolean PowerStageEnable ; + [HoldingRegister(4000, writable: true)] internal Boolean PowerStageEnable ; //[Coil(4002)] [HoldingRegister(4002, writable: true)] internal Boolean ResetAlarmsAndWarnings ; [HoldingRegister(4100, writable: true, Scale = .01)] internal Double MaxBatteryVoltage ; From b6b1cfccb92dd14d9d69ef5b43570943800c4c9d Mon Sep 17 00:00:00 2001 From: atef Date: Fri, 27 Feb 2026 12:55:39 +0100 Subject: [PATCH 6/8] cleare unused tojson function --- csharp/App/SaliMax/src/Ess/StatusRecord.cs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/csharp/App/SaliMax/src/Ess/StatusRecord.cs b/csharp/App/SaliMax/src/Ess/StatusRecord.cs index 386680a7b..230928d8a 100644 --- a/csharp/App/SaliMax/src/Ess/StatusRecord.cs +++ b/csharp/App/SaliMax/src/Ess/StatusRecord.cs @@ -32,24 +32,4 @@ public record StatusRecord public required EssControl EssControl { get; set; } // TODO: init only public required StateMachine StateMachine { get; init; } - - public string ToJson() - { - // Try to get the "Battery" property via reflection - // var batteryProperty = thing.GetType().GetProperty("Battery"); - // if (batteryProperty == null) - // throw new InvalidOperationException("The object does not have a 'Battery' property."); - // - // // Retrieve the value of the Battery property - // var batteryValue = Battery.GetValue(thing); - var jsonOptions = new JsonSerializerOptions { WriteIndented = true }; - - // Serialize the Battery property - Console.WriteLine("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); - string json = JsonSerializer.Serialize(this.Battery, jsonOptions); - Console.WriteLine(json); - - - return json; - } } \ No newline at end of file From 4d27515c14998c37ed45e959f75cd6dead2e8148 Mon Sep 17 00:00:00 2001 From: atef Date: Fri, 27 Feb 2026 12:55:54 +0100 Subject: [PATCH 7/8] add writable systemcontrol --- .../Trumpf/SystemControl/SystemControlRegisters.Modbus.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/csharp/Lib/Devices/Trumpf/SystemControl/SystemControlRegisters.Modbus.cs b/csharp/Lib/Devices/Trumpf/SystemControl/SystemControlRegisters.Modbus.cs index 1eeee0e5f..a21fca5c4 100644 --- a/csharp/Lib/Devices/Trumpf/SystemControl/SystemControlRegisters.Modbus.cs +++ b/csharp/Lib/Devices/Trumpf/SystemControl/SystemControlRegisters.Modbus.cs @@ -19,14 +19,14 @@ public partial record SystemControlRegisters { private const UInt16 NoTimeout = UInt16.MaxValue; - [HoldingRegister(1016)] private UInt16 _CommunicationTimeoutSeconds; + [HoldingRegister(1016, writable: true)] private UInt16 _CommunicationTimeoutSeconds; - [HoldingRegister(1018)] public SystemConfig SystemConfig { get; set; } + [HoldingRegister(1018, writable: true)] public SystemConfig SystemConfig { get; set; } //[Coil(4002)] - [HoldingRegister(4002)] public Boolean ResetAlarmsAndWarnings { get; set; } + [HoldingRegister(4002, writable: true)] public Boolean ResetAlarmsAndWarnings { get; set; } - [HoldingRegister(4007)] public UInt16 TargetSlave { get; set; } + [HoldingRegister(4007, writable: true)] public UInt16 TargetSlave { get; set; } //[Coil(4011)] [HoldingRegister(4011, writable: true)] public Boolean UseSlaveIdForAddressing { get; set; } From 0db9406b9c0fe8fd1f6fcd2ef23984ea86e5fbbb Mon Sep 17 00:00:00 2001 From: atef Date: Fri, 27 Feb 2026 12:56:18 +0100 Subject: [PATCH 8/8] create Topology display for Kaco product --- csharp/App/KacoCommunication/Topology.cs | 70 +++++------------------- 1 file changed, 15 insertions(+), 55 deletions(-) diff --git a/csharp/App/KacoCommunication/Topology.cs b/csharp/App/KacoCommunication/Topology.cs index 3f6dce2ad..d1a952e44 100644 --- a/csharp/App/KacoCommunication/Topology.cs +++ b/csharp/App/KacoCommunication/Topology.cs @@ -32,40 +32,6 @@ namespace InnovEnergy.App.KacoCommunication; // └──────┘ // -// New (simplified) topology: -// -// ┌────┐ -// │ PV │ -// └────┘ -// V -// V -// (i) 13.2 kW -// V -// V -// ┌─────────┐ ┌─────────┐ (h) ┌────────┐ (k) ┌───────┐ (l) ┌────────────┐ ┌────────────┐ ┌────────────┐ -// │ Grid │<<<│ AC/DC │<<<<<<<<<<<<<│ Dc Bus │>>>>>>>>│ DC/DC │>>>>>>>>│ Battery K1 │ │ Battery K2 │ │ Battery K3 │ -// ├─────────┤ ├─────────┤ ├────────┤ ├───────┤ ├────────────┤ ├────────────┤ ├────────────┤ -// │ -3205 W │ │ -6646 W │ │ 776 V │ │ 56 V │ │ 52.3 V ... │ │ 52.3 V ... │ │ 52.3 V ... │ -// │ -3507 W │ │ -5071 W │ └────────┘ └───────┘ └────────────┘ └────────────┘ └────────────┘ -// │ -3605 W │ -// └─────────┘ -// -// V -// V -// (j) 0 W -// V -// V -// ┌──────┐ -// │ Load │ -// └──────┘ -// -// Notes: -// - (a) is grid power (to/from grid) -// - (h) is AC/DC -> DC link power (or your chosen link variable) -// - (i) PV -> DC bus -// - (j) DC load -// - (k) DC bus -> DC/DC link -// - (l) DC/DC -> battery power (or total battery power) public static class SimpleTopology { @@ -74,17 +40,17 @@ public static class SimpleTopology // Keep the same variable letters as your diagrams (where possible) var a = status.GridMeterRecord?.ActivePowerTotal; - // In your existing code, "AC/DC column" shows per-device AC power; // and "h" is a separate link (AcDcToDcLink?.Power.Value). - var h = 0; + var h = status.InverterRecord?.ActivePowerW; var i = 0; var j = 0; - var k = 0; + var k = status.DcDc?.Dc.Battery.Power.Value; // You mentioned this changed: l is now equal total battery power - var l = status.BatteryKabinet1.Power; + + var l = status.ListOfBatteriesRecord?.Sum(r => r.Power); var grid = status.CreateGridColumn(a); var acdc = status.CreateAcDcColumn(h); @@ -138,7 +104,7 @@ public static class SimpleTopology // └─────────┘ (h) flow to DC Bus var acdcBox = TextBlock - .AlignLeft(status.InverterRecord?.ActivePowerSetPercent.ToString() ?? "???") + .AlignLeft(status.InverterRecord?.ActivePowerW.ToString() ?? "???") .TitleBox("AC/DC"); var flowToDcBus = Flow.Horizontal(h); @@ -175,7 +141,7 @@ public static class SimpleTopology var pvToBus = Flow.Vertical(i); // DC bus box (voltage from your DcDc record matches your existing code) - var dcBusVoltage = 0.0; + var dcBusVoltage = status.DcDc.Dc.Link.Voltage.Value; var dcBusBox = dcBusVoltage .ToString(CultureInfo.InvariantCulture) .Apply(TextBlock.FromString) @@ -203,7 +169,7 @@ public static class SimpleTopology // │ 56 V │ // └───────┘ (l) flow to batteries - var dc48Voltage =0.0; + var dc48Voltage = status.DcDc?.Dc.Battery.Voltage; var dcdcBox = TextBlock .AlignLeft(dc48Voltage) @@ -219,13 +185,13 @@ public static class SimpleTopology // Battery K1 | Battery K2 | Battery K3 (side-by-side) // Each box: voltage, soc, current, temp, etc. (you can tailor) - var bat = status.BatteryKabinet1; + var bat = status.ListOfBatteriesRecord; if (bat is null) return TextBlock.AlignLeft("no battery").Box(); // If you actually have relay names K1/K2/K3 per battery, wire them here. // For now we label by index as "Battery K{n}" to match your picture. - var boxes = bat.Devices + var boxes = bat .Select((b, idx) => CreateBatteryKBox(b, idx)) .ToReadOnlyList(); @@ -235,30 +201,24 @@ public static class SimpleTopology : TextBlock.AlignLeft("no battery devices").Box(); } - private static TextBlock CreateBatteryKBox(BatteryDeligreenRecord battery, int idx) + private static TextBlock CreateBatteryKBox(BatteryDeligreenRecords battery, int idx) { - // Minimal “K-style” battery box matching your diagram fields - var data = battery.BatteryDeligreenDataRecord; // Some of your sample screen values look like: // 52.3 V, 99.1 %, 490 mA, 250 °C, 445 A // Map these to whatever fields you trust in your record. - var voltage = data.BusVoltage.ToDisplayString(); - var soc = data.Soc.ToDisplayString(); - var current = data.BusCurrent.ToDisplayString(); - var temp = data.TemperaturesList.PowerTemperature.ToDisplayString(); + var voltage = battery.Voltage.ToDisplayString(); + var soc = battery.Soc.ToDisplayString(); + var current = battery.Current.ToDisplayString(); + var count = battery.Devices.Count; - // If you have a better “pack current” field, replace this line. - // Keeping it as a separate line to mimic the picture’s extra current-like line. - var extraCurrent = data.BusCurrent.ToDisplayString(); return TextBlock .AlignLeft( voltage, soc, current, - temp, - extraCurrent + count ) .TitleBox($"Battery K{idx + 1}"); }