diff --git a/csharp/App/SinexcelCommunication/AggregationService/HourlyEnergyData.cs b/csharp/App/SinexcelCommunication/AggregationService/HourlyEnergyData.cs new file mode 100644 index 000000000..59b34e364 --- /dev/null +++ b/csharp/App/SinexcelCommunication/AggregationService/HourlyEnergyData.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using InnovEnergy.App.SinexcelCommunication.ESS; + +namespace InnovEnergy.App.SinexcelCommunication.AggregationService; + +public class HourlyAccumulator +{ + public DateTime HourStart { get; set; } + + public double StartSelfGeneratedElectricity { get; set; } + public double StartElectricityPurchased { get; set; } + public double StartElectricityFed { get; set; } + public double StartBatteryChargeEnergy { get; set; } + public double StartBatteryDischargeEnergy { get; set; } + public double StartLoadPowerConsumption { get; set; } + + public double LastSelfGeneratedElectricity { get; set; } + public double LastElectricityPurchased { get; set; } + public double LastElectricityFed { get; set; } + public double LastBatteryChargeEnergy { get; set; } + public double LastBatteryDischargeEnergy { get; set; } + public double LastLoadPowerConsumption { get; set; } +} + +public static class EnergyAggregation +{ + private static HourlyAccumulator? _currentHourAccumulator; + private static DateTime? _lastDailySaveDate; + + + public static HourlyEnergyData? ProcessHourlyData(StatusRecord statusRecord, DateTime timestamp) + { + var r = statusRecord.InverterRecord; + var hourStart = new DateTime(timestamp.Year, timestamp.Month, timestamp.Day, timestamp.Hour, 0, 0); + + // First call + if (_currentHourAccumulator == null) + { + _currentHourAccumulator = new HourlyAccumulator + { + HourStart = hourStart, + + StartSelfGeneratedElectricity = r.SelfGeneratedElectricity, + StartElectricityPurchased = r.ElectricityPurchased, + StartElectricityFed = r.ElectricityFed, + StartBatteryChargeEnergy = r.BatteryChargeEnergy, + StartBatteryDischargeEnergy = r.BatteryDischargeEnergy, + StartLoadPowerConsumption = r.LoadPowerConsumption, + + LastSelfGeneratedElectricity = r.SelfGeneratedElectricity, + LastElectricityPurchased = r.ElectricityPurchased, + LastElectricityFed = r.ElectricityFed, + LastBatteryChargeEnergy = r.BatteryChargeEnergy, + LastBatteryDischargeEnergy = r.BatteryDischargeEnergy, + LastLoadPowerConsumption = r.LoadPowerConsumption + }; + + return null; + } + + // Still same hour → just update last values + if (_currentHourAccumulator.HourStart == hourStart) + { + _currentHourAccumulator.LastSelfGeneratedElectricity = r.SelfGeneratedElectricity; + _currentHourAccumulator.LastElectricityPurchased = r.ElectricityPurchased; + _currentHourAccumulator.LastElectricityFed = r.ElectricityFed; + _currentHourAccumulator.LastBatteryChargeEnergy = r.BatteryChargeEnergy; + _currentHourAccumulator.LastBatteryDischargeEnergy = r.BatteryDischargeEnergy; + _currentHourAccumulator.LastLoadPowerConsumption = r.LoadPowerConsumption; + + return null; + } + + // Hour changed → finalize previous hour + var completedHour = new HourlyEnergyData + { + Timestamp = _currentHourAccumulator.HourStart, + + SelfGeneratedElectricity = SafeDiff( + _currentHourAccumulator.LastSelfGeneratedElectricity, + _currentHourAccumulator.StartSelfGeneratedElectricity), + + ElectricityPurchased = SafeDiff( + _currentHourAccumulator.LastElectricityPurchased, + _currentHourAccumulator.StartElectricityPurchased), + + ElectricityFed = SafeDiff( + _currentHourAccumulator.LastElectricityFed, + _currentHourAccumulator.StartElectricityFed), + + BatteryChargeEnergy = SafeDiff( + _currentHourAccumulator.LastBatteryChargeEnergy, + _currentHourAccumulator.StartBatteryChargeEnergy), + + BatteryDischargeEnergy = SafeDiff( + _currentHourAccumulator.LastBatteryDischargeEnergy, + _currentHourAccumulator.StartBatteryDischargeEnergy), + + LoadPowerConsumption = SafeDiff( + _currentHourAccumulator.LastLoadPowerConsumption, + _currentHourAccumulator.StartLoadPowerConsumption) + }; + + // Start new hour with current sample + _currentHourAccumulator = new HourlyAccumulator + { + HourStart = hourStart, + + StartSelfGeneratedElectricity = r.SelfGeneratedElectricity, + StartElectricityPurchased = r.ElectricityPurchased, + StartElectricityFed = r.ElectricityFed, + StartBatteryChargeEnergy = r.BatteryChargeEnergy, + StartBatteryDischargeEnergy = r.BatteryDischargeEnergy, + StartLoadPowerConsumption = r.LoadPowerConsumption, + + LastSelfGeneratedElectricity = r.SelfGeneratedElectricity, + LastElectricityPurchased = r.ElectricityPurchased, + LastElectricityFed = r.ElectricityFed, + LastBatteryChargeEnergy = r.BatteryChargeEnergy, + LastBatteryDischargeEnergy = r.BatteryDischargeEnergy, + LastLoadPowerConsumption = r.LoadPowerConsumption + }; + + return completedHour; + } + + public static DailyEnergyData? TryCreateDailyData(StatusRecord statusRecord, DateTime timestamp) + { + if (timestamp is { Hour: 23, Minute: 59 }) + { + if (_lastDailySaveDate != timestamp.Date) + { + _lastDailySaveDate = timestamp.Date; + + var r = statusRecord.InverterRecord; + + return new DailyEnergyData + { + Timestamp = timestamp, + + DailySelfGeneratedElectricity = r.DailySelfGeneratedElectricity, + DailyElectricityPurchased = r.DailyElectricityPurchased, + DailyElectricityFed = r.DailyElectricityFed, + BatteryDailyChargeEnergy = r.BatteryDailyChargeEnergy, + BatteryDailyDischargeEnergy = r.BatteryDailyDischargeEnergy, + DailyLoadPowerConsumption = r.DailyLoadPowerConsumption + }; + } + } + + return null; + } + + private static double SafeDiff(double endValue, double startValue) + { + var diff = endValue - startValue; + return diff < 0 ? 0 : diff; + } +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/DataTypes/Configuration.cs b/csharp/App/SinexcelCommunication/DataTypes/Configuration.cs index ea5dea85d..710cdcbd8 100644 --- a/csharp/App/SinexcelCommunication/DataTypes/Configuration.cs +++ b/csharp/App/SinexcelCommunication/DataTypes/Configuration.cs @@ -7,7 +7,7 @@ public class Configuration public Double MinimumSoC { get; set; } public Double MaximumDischargingCurrent { get; set; } public Double MaximumChargingCurrent { get; set; } - public WorkingMode OperatingPriority { get; set; } + public OperatingPriority OperatingPriority { get; set; } public Int16 BatteriesCount { get; set; } public Int16 ClusterNumber { get; set; } public Int16 PvNumber { get; set; } diff --git a/csharp/App/SinexcelCommunication/DataTypes/OperatingPriority.cs b/csharp/App/SinexcelCommunication/DataTypes/OperatingPriority.cs new file mode 100644 index 000000000..07757dafb --- /dev/null +++ b/csharp/App/SinexcelCommunication/DataTypes/OperatingPriority.cs @@ -0,0 +1,9 @@ +namespace InnovEnergy.App.SinexcelCommunication.DataTypes; + +public enum OperatingPriority +{ + ModeNotSynched = -1, + LoadPriority = 0, + BatteryPriority = 1, + GridPriority = 2, +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/ESS/InverterRecords.cs b/csharp/App/SinexcelCommunication/ESS/InverterRecords.cs new file mode 100644 index 000000000..bcfc00b55 --- /dev/null +++ b/csharp/App/SinexcelCommunication/ESS/InverterRecords.cs @@ -0,0 +1,96 @@ +using InnovEnergy.App.SinexcelCommunication.DataTypes; +using InnovEnergy.Lib.Devices.Sinexcel_12K_TL; +using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType; +using InnovEnergy.Lib.Units; +using InnovEnergy.Lib.Units.Power; + +namespace InnovEnergy.App.SinexcelCommunication.ESS; + +public class InverterRecords +{ + + public required Energy SelfGeneratedElectricity { get; init; } + public required Energy ElectricityPurchased { get; init; } + public required Energy ElectricityFed { get; init; } + public required Energy BatteryChargeEnergy { get; init; } + public required Energy BatteryDischargeEnergy { get; init; } + public required Energy LoadPowerConsumption { get; init; } + + public required Energy DailySelfGeneratedElectricity { get; init; } + public required Energy DailyElectricityPurchased { get; init; } + public required Energy DailyElectricityFed { get; init; } + public required Energy BatteryDailyChargeEnergy { get; init; } + public required Energy BatteryDailyDischargeEnergy { get; init; } + public required Energy DailyLoadPowerConsumption { get; init; } + + // public required ActivePower TotalConsumptionPower { get; init; } + public required ActivePower TotalPhotovoltaicPower { get; init; } + public required ActivePower TotalBatteryPower { get; init; } + public required ActivePower TotalLoadPower { get; init; } + public required ActivePower TotalGridPower { get; init; } + + public required OperatingPriority OperatingPriority { get; init; } + public required Voltage AvgBatteryVoltage { get; init; } + public required Current TotalBatteryCurrent { get; init; } + public required Percent AvgBatterySoc { get; init; } + public required Percent AvgBatterySoh { get; init; } + public required Temperature AvgBatteryTemp { get; init; } + public required Percent MinSoc { get; init; } + + public required Current MaxChargeCurrent { get; init; } + public required Current MaxDischargingCurrent { get; init; } + public required ActivePower GridPower { get; init; } + public required Frequency GridFrequency { get; init; } + public required ActivePower InverterPower { get; init; } + public required EnablePowerLimitation EnableGridExport { get; init; } + public required ActivePower GridExportPower { get; init; } + + + public required IReadOnlyList Devices { get; init; } + + public static InverterRecords? FromInverters(IReadOnlyList? records) + { + if (records is null || records.Count == 0) + return null; + + + return new InverterRecords + { + Devices = records, + + DailySelfGeneratedElectricity = records.Sum(r => r.DailySelfGeneratedElectricity), + DailyElectricityPurchased = records.Sum(r => r.DailyElectricityPurchased), + DailyElectricityFed = records.Sum(r => r.DailyElectricityFed), + BatteryDailyChargeEnergy = records.Sum(r => r.BatteryDailyChargeEnergy), + BatteryDailyDischargeEnergy = records.Sum(r => r.BatteryDailyDischargeEnergy), + DailyLoadPowerConsumption = records.Sum(r => r.DailyLoadPowerConsumption), + + SelfGeneratedElectricity = records.Sum(r => r.SelfGeneratedElectricity), + ElectricityPurchased = records.Sum(r => r.TotalEnergyToUser), + ElectricityFed = records.Sum(r => r.TotalEnergyToGrid), + BatteryChargeEnergy = records.Sum(r => r.BatteryCharge), + BatteryDischargeEnergy = records.Sum(r => r.BatteryDischarge), + LoadPowerConsumption = records.Sum(r => r.LoadPowerConsumption), + + // TotalConsumptionPower = records.Sum(r => r.ConsumptionPower), // consumption same as load + TotalPhotovoltaicPower = records.Sum(r => r.TotalPhotovoltaicPower), + TotalBatteryPower = records.Sum(r => r.TotalBatteryPower), + TotalLoadPower = records.Sum(r => r.TotalLoadPower), + TotalGridPower = records.Sum(b => b.TotalGridPower), + OperatingPriority = records.Select(r => r.WorkingMode).Distinct().Count() == 1 ? (OperatingPriority)records.First().WorkingMode: OperatingPriority.ModeNotSynched, + AvgBatteryVoltage = records.SelectMany(r => new[] { r.Battery1Voltage.Value, r.Battery2Voltage.Value }).Where(v => v > 0).DefaultIfEmpty(0).Average(), + TotalBatteryCurrent = records.SelectMany(r => new [] { r.Battery1Current.Value, r.Battery2Current.Value}).Sum(), + AvgBatterySoc = records.SelectMany(r => new[] { r.Battery1Soc.Value, r.Battery2Soc.Value }).Average(), + AvgBatterySoh = records.SelectMany(r => new[] { r.Battery1Soh.Value, r.Battery2Soh.Value }).Average(), + MinSoc = records.SelectMany(r => new[] { r.Battery1BackupSoc, r.Battery2BackupSoc}).Min(), + AvgBatteryTemp = records.SelectMany(r => new[] { r.Battery1Temperature.Value, r.Battery2Temperature.Value }).Average(), + MaxChargeCurrent = records.SelectMany(r => new[] { r.Battery1MaxChargingCurrent, r.Battery2MaxChargingCurrent }).Min(), + MaxDischargingCurrent = records.SelectMany(r => new[] { r.Battery1MaxDischargingCurrent, r.Battery2MaxDischargingCurrent }).Max(), + GridPower = records.Sum(r => r.TotalGridPower), + GridFrequency = records.Average(r => r.GridVoltageFrequency), + InverterPower = records.Sum( r => r.InverterActivePower ), + EnableGridExport = records.Select(r => r.EnableGridExport).Distinct().Count() == 1 ? records.First().EnableGridExport: EnablePowerLimitation.Prohibited, + GridExportPower = records.Sum(r => r.PowerGridExportLimit) + }; + } +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/ESS/StatusRecord.cs b/csharp/App/SinexcelCommunication/ESS/StatusRecord.cs index 88a9a04b3..d87948d15 100644 --- a/csharp/App/SinexcelCommunication/ESS/StatusRecord.cs +++ b/csharp/App/SinexcelCommunication/ESS/StatusRecord.cs @@ -5,6 +5,7 @@ namespace InnovEnergy.App.SinexcelCommunication.ESS; public record StatusRecord { - public required SinexcelRecord InverterRecord { get; set; } + public required InverterRecords? InverterRecord { get; set; } + public required Config Config { get; set; } } \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/MiddlewareClasses/MiddlewareAgent.cs b/csharp/App/SinexcelCommunication/MiddlewareClasses/MiddlewareAgent.cs index a80093a4f..cc63e8e60 100644 --- a/csharp/App/SinexcelCommunication/MiddlewareClasses/MiddlewareAgent.cs +++ b/csharp/App/SinexcelCommunication/MiddlewareClasses/MiddlewareAgent.cs @@ -13,20 +13,37 @@ public static class MiddlewareAgent private static IPAddress? _controllerIpAddress; private static EndPoint? _endPoint; - public static void InitializeCommunicationToMiddleware() + public static bool InitializeCommunicationToMiddleware() { - _controllerIpAddress = FindVpnIp(); - if (Equals(IPAddress.None, _controllerIpAddress)) + try { - Console.WriteLine("There is no VPN interface, exiting..."); + _controllerIpAddress = FindVpnIp(); + if (Equals(IPAddress.None, _controllerIpAddress)) + { + Console.WriteLine("There is no VPN interface."); + _udpListener = null; + return false; + } + + const int udpPort = 9000; + _endPoint = new IPEndPoint(_controllerIpAddress, udpPort); + + _udpListener?.Close(); + _udpListener?.Dispose(); + + _udpListener = new UdpClient(); + _udpListener.Client.Blocking = false; + _udpListener.Client.Bind(_endPoint); + + Console.WriteLine($"UDP listener bound to {_endPoint}"); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"Failed to initialize middleware communication: {ex}"); + _udpListener = null; + return false; } - - const Int32 udpPort = 9000; - _endPoint = new IPEndPoint(_controllerIpAddress, udpPort); - - _udpListener = new UdpClient(); - _udpListener.Client.Blocking = false; - _udpListener.Client.Bind(_endPoint); } private static IPAddress FindVpnIp() @@ -50,42 +67,96 @@ public static class MiddlewareAgent return IPAddress.None; } - public static Configuration? SetConfigurationFile() { - if (_udpListener.Available > 0) + try { + // Ensure listener is initialized + if (_udpListener == null) + { + Console.WriteLine("UDP listener not initialized, trying to initialize..."); + InitializeCommunicationToMiddleware(); + + if (_udpListener == null) + { + Console.WriteLine("Failed to initialize UDP listener."); + return null; + } + } + + // Check if data is available + if (_udpListener.Available <= 0) + return null; + IPEndPoint? serverEndpoint = null; - - var replyMessage = "ACK"; - var replyData = Encoding.UTF8.GetBytes(replyMessage); - + var udpMessage = _udpListener.Receive(ref serverEndpoint); - var message = Encoding.UTF8.GetString(udpMessage); - + var message = Encoding.UTF8.GetString(udpMessage); + + Console.WriteLine($"Received raw UDP message from {serverEndpoint}: {message}"); + var config = JsonSerializer.Deserialize(message); - + if (config != null) { - Console.WriteLine($"Received a configuration message: " + - "MinimumSoC is " + config.MinimumSoC + " and operating priorty is " +config.OperatingPriority + "Number of batteries is " + config.BatteriesCount - + "Maximum Charging current is "+ config.MaximumChargingCurrent + "/n" + "Maximum Discharging current is " + config.MaximumDischargingCurrent - + "StartTimeChargeandDischargeDayandTime is" + config.StartTimeChargeandDischargeDayandTime + "StopTimeChargeandDischargeDayandTime is" + config.StopTimeChargeandDischargeDayandTime - + "TimeChargeandDischargePowert is " + config.TimeChargeandDischargePower + " Control permission is" + config.ControlPermission); - - // Send the reply to the sender's endpoint + Console.WriteLine( + $"Received a configuration message:\n" + + $"MinimumSoC: {config.MinimumSoC}\n" + + $"OperatingPriority: {config.OperatingPriority}\n" + + $"Number of batteries: {config.BatteriesCount}\n" + + $"Maximum Charging current: {config.MaximumChargingCurrent}\n" + + $"Maximum Discharging current: {config.MaximumDischargingCurrent}\n" + + $"StartTimeChargeandDischargeDayandTime: {config.StartTimeChargeandDischargeDayandTime}\n" + + $"StopTimeChargeandDischargeDayandTime: {config.StopTimeChargeandDischargeDayandTime}\n" + + $"TimeChargeandDischargePower: {config.TimeChargeandDischargePower}\n" + + $"ControlPermission: {config.ControlPermission}" + ); + + // Send ACK + var replyMessage = "ACK"; + var replyData = Encoding.UTF8.GetBytes(replyMessage); + _udpListener.Send(replyData, replyData.Length, serverEndpoint); Console.WriteLine($"Replied to {serverEndpoint}: {replyMessage}"); + return config; } + else + { + Console.WriteLine("Received UDP message but failed to deserialize Configuration."); + return null; + } } - - if (_endPoint != null && !_endPoint.Equals(_udpListener.Client.LocalEndPoint as IPEndPoint)) + catch (SocketException ex) { - Console.WriteLine("UDP address has changed, rebinding..."); + Console.WriteLine($"Socket error in SetConfigurationFile: {ex}"); + + // Recover by reinitializing + try + { + _udpListener?.Close(); + _udpListener?.Dispose(); + } + catch + { + // ignored + } + + _udpListener = null; InitializeCommunicationToMiddleware(); + + return null; + } + catch (JsonException ex) + { + Console.WriteLine($"JSON deserialization error: {ex}"); + return null; + } + catch (Exception ex) + { + Console.WriteLine($"Unexpected error in SetConfigurationFile: {ex}"); + return null; } - return null; } } \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/Program.cs b/csharp/App/SinexcelCommunication/Program.cs index 40e269f3e..b7ca31ada 100644 --- a/csharp/App/SinexcelCommunication/Program.cs +++ b/csharp/App/SinexcelCommunication/Program.cs @@ -1,4 +1,5 @@ -using System.IO.Compression; +using System.Diagnostics; +using System.IO.Compression; using System.IO.Ports; using System.Text; using System.Text.Json; @@ -24,6 +25,7 @@ using Formatting = Newtonsoft.Json.Formatting; using JsonSerializer = System.Text.Json.JsonSerializer; using static InnovEnergy.App.SinexcelCommunication.MiddlewareClasses.MiddlewareAgent; using System.Diagnostics.CodeAnalysis; +using InnovEnergy.App.SinexcelCommunication.AggregationService; using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType; using InnovEnergy.Lib.Protocols.Modbus.Protocol; using static InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType.WorkingMode; @@ -35,12 +37,12 @@ namespace InnovEnergy.App.SinexcelCommunication; [SuppressMessage("Trimming", "IL2026:Members annotated with \'RequiresUnreferencedCodeAttribute\' require dynamic access otherwise can break functionality when trimming application code")] 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 _sinexcelChannel1; - private static Channel _sinexcelChannel2; - + private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(10); + private const UInt16 NbrOfFileToConcatenate = 15; // add this to config file + private static UInt16 _fileCounter = 0; + private static List _sinexcelChannel; + private static DateTime? _lastUploadedAggregatedDate; + private static DailyEnergyData? _pendingDailyData; private static readonly String SwVersionNumber = " V1.00." + DateTime.Today; private const String VpnServerIp = "10.2.0.11"; private static Boolean _subscribedToQueue = false; @@ -57,19 +59,29 @@ internal static class Program private const Int32 HeartbeatIntervalSeconds = 60; - // move all this to config file - 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; - private const Int32 BaudRate = 115200; - private const Int32 DataBits = 8; + public static async Task Main(String[] args) { - _sinexcelChannel1 = new SerialPortChannel(Port1, BaudRate, Parity, DataBits, StopBits); - _sinexcelChannel2 = new SerialPortChannel(Port2, BaudRate, Parity, DataBits, StopBits); + var config = Config.Load(); + var d = config.Devices; + var serial = d.Serial; + + + Channel CreateChannel(SodiDevice device) => device.DeviceState == DeviceState.Disabled + ? new NullChannel() + : new SerialPortChannel(device.Port,serial.BaudRate,serial.Parity,serial.DataBits,serial.StopBits); + + _sinexcelChannel = new List() + { + CreateChannel(d.Inverter1), + CreateChannel(d.Inverter2), + CreateChannel(d.Inverter3), + CreateChannel(d.Inverter4) + }; + InitializeCommunicationToMiddleware(); while (true) @@ -91,21 +103,24 @@ internal static class Program Watchdog.NotifyReady(); Console.WriteLine("Starting Sinexcel Communication, SW Version : " + SwVersionNumber); - - var sinexcelDevice1 = new SinexcelDevice(_sinexcelChannel1, SlaveId); - var sinexcelDevice2 = new SinexcelDevice(_sinexcelChannel2, SlaveId); + + var devices = _sinexcelChannel + .Where(ch => ch is not NullChannel) + .Select(ch => new SinexcelDevice(ch, SlaveId)) + .ToList(); StatusRecord? ReadStatus() { - var config = Config.Load(); - var sinexcelRecord1 = sinexcelDevice1.Read(); - var sinexcelRecord2 = sinexcelDevice2.Read(); + var config = Config.Load(); + var listOfInverterRecord = devices + .Select(device => device.Read()) + .ToList(); + InverterRecords? inverterRecords = InverterRecords.FromInverters(listOfInverterRecord); return new StatusRecord { - InverterRecord1 = sinexcelRecord1, - InverterRecord2 = sinexcelRecord2, + InverterRecord = inverterRecords, Config = config // load from disk every iteration, so config can be changed while running }; } @@ -128,103 +143,46 @@ internal static class Program try { Watchdog.NotifyAlive(); - var startTime = DateTime.Now; Console.WriteLine("***************************** Reading Battery Data *********************************************"); Console.WriteLine(startTime.ToString("HH:mm:ss.fff ")+ "Start Reading"); - // the order matter of the next three lines var statusrecord = ReadStatus(); if (statusrecord == null) return null; + _ = CreateAggregatedData(statusrecord); - 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 "); + var invDevices = statusrecord.InverterRecord?.Devices; - - 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 "); + if (invDevices != null) + { + var index = 1; + foreach (var inverter in invDevices) + PrintInverterData(inverter, index++); + } - 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(); + 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 is { Config.ControlPermission: true }) + if (statusrecord is { Config.ControlPermission: true, InverterRecord.Devices: not null }) { - Console.WriteLine(" We have the Right to Write"); - sinexcelDevice1.Write(statusrecord.InverterRecord1); - sinexcelDevice2.Write(statusrecord.InverterRecord2); + Console.WriteLine("We have the Right to Write"); + foreach (var pair in devices.Zip(statusrecord.InverterRecord.Devices)) + pair.First.Write(pair.Second); } else { - Console.WriteLine(" Nooooooo We cant' have the Right to Write"); + Console.WriteLine("Nooooooo We can't have the Right to Write"); } - + var stop = DateTime.Now; + Console.WriteLine("***************************** Writing finished *********************************************"); + Console.WriteLine(stop.ToString("HH:mm:ss.fff ")+ "Cycle end"); return statusrecord; } catch (CrcException e) @@ -239,60 +197,216 @@ internal static class Program } } } + + // this is synchronous because : + // + // it only appends one line once per hour and once per day + // + // it should not meaningfully affect a 10-second loop + + private static async Task CreateAggregatedData(StatusRecord statusRecord) + { + + DateTime now = DateTime.Now; + string baseFolder = AppContext.BaseDirectory; + + // 1) Finalize previous hour if hour changed + var hourlyData = EnergyAggregation.ProcessHourlyData(statusRecord, now); + /*if (hourlyData != null) + { + AggregatedDataFileWriter.AppendHourlyData(hourlyData, baseFolder); + }*/ + if (hourlyData != null) + { + AggregatedDataFileWriter.AppendHourlyData(hourlyData, baseFolder); + + if (_pendingDailyData != null && hourlyData.Timestamp.Hour == 23) + { + AggregatedDataFileWriter.AppendDailyData(_pendingDailyData, baseFolder); + _pendingDailyData = null; + } + } + + // 2) Save daily line near end of day + var dailyData = EnergyAggregation.TryCreateDailyData(statusRecord, now); + if (dailyData != null) + { + _pendingDailyData = dailyData; + //AggregatedDataFileWriter.AppendDailyData(dailyData, baseFolder); + } + + // 3) After midnight, upload yesterday's completed file once + var yesterday = now.Date.AddDays(-1); + + if (now.Hour == 0 && now.Minute == 0) + { + if (_lastUploadedAggregatedDate != yesterday) + { + Console.WriteLine(" We are inside the lastuploaded Aggregate"); + string filePath = Path.Combine( + baseFolder, + "AggregatedData", + yesterday.ToString("ddMMyyyy") + ".json"); + + bool uploaded = await PushAggregatedFileToS3(filePath, statusRecord); + + if (uploaded) + { + _lastUploadedAggregatedDate = yesterday; + Console.WriteLine($"Uploaded aggregated file for {yesterday:ddMMyyyy}"); + } + else + { + Console.WriteLine($"Uploaded failed for {yesterday:ddMMyyyy}"); + } + } + } + } + + private static async Task PushAggregatedFileToS3( String localFilePath, StatusRecord statusRecord) + { + var s3Config = statusRecord.Config.S3; + if (s3Config is null) + return false; + + try + { + if (!File.Exists(localFilePath)) + { + Console.WriteLine($"File not found: {localFilePath}"); + return false; + } + + var jsonString = await File.ReadAllTextAsync(localFilePath); + + // Example S3 object name: 09032026.json + var s3Path = Path.GetFileName(localFilePath); + var request = s3Config.CreatePutRequest(s3Path); + var base64String = Convert.ToBase64String(Encoding.UTF8.GetBytes(jsonString)); + + using var content = new StringContent(base64String, Encoding.UTF8, "application/base64"); + + Console.WriteLine("Sending Content-Type: application/base64; charset=utf-8"); + Console.WriteLine($"S3 Path: {s3Path}"); + + var response = await request.PutAsync(content); + + if (response.StatusCode != 200) + { + Console.WriteLine("ERROR: PUT"); + var error = await response.GetStringAsync(); + Console.WriteLine(error); + return false; + } + + Console.WriteLine($"Uploaded successfully: {s3Path}"); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"PushAggregatedFileToS3 failed: {ex.Message}"); + return false; + } + } private static void ControlConstants(this StatusRecord? statusrecord) { - if (statusrecord == null) return; + if (statusrecord?.InverterRecord?.Devices == null) return; + + // Compute once (same for all inverters) + var config = statusrecord.Config; + + var isChargePeriod = IsNowInsideDateAndTime( + config.StartTimeChargeandDischargeDayandTime, + config.StopTimeChargeandDischargeDayandTime + ); - 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) + foreach (var inverter in statusrecord.InverterRecord.Devices) { - statusrecord.InverterRecord1.WorkingMode = statusrecord.Config.OperatingPriority; - } - else if (statusrecord.Config.OperatingPriority == TimeChargeDischarge && isChargePeriod) - { - statusrecord.InverterRecord1.WorkingMode = statusrecord.Config.OperatingPriority; + // constants for every inverter + inverter.Battery1BackupSoc = (float)config.MinSoc; + inverter.Battery2BackupSoc = (float)config.MinSoc; + inverter.RepetitiveWeeks = SinexcelWeekDays.All; - if (statusrecord.Config.TimeChargeandDischargePower > 0) + var operatingMode = config.OperatingPriority switch { - 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; + OperatingPriority.LoadPriority => SpontaneousSelfUse, + OperatingPriority.BatteryPriority => TimeChargeDischarge, + OperatingPriority.GridPriority => PrioritySellElectricity, + _ => SpontaneousSelfUse + }; + + if (operatingMode!= TimeChargeDischarge) + { + inverter.WorkingMode = operatingMode; + } + else if (isChargePeriod) + { + inverter.WorkingMode = operatingMode; + inverter.EffectiveStartDate = config.StartTimeChargeandDischargeDayandTime.Date; + inverter.EffectiveEndDate = config.StopTimeChargeandDischargeDayandTime.Date; + + var power = config.TimeChargeandDischargePower; + + if (power > 0) + { + inverter.ChargingPowerPeriod1 = Math.Abs(power); + inverter.ChargeStartTimePeriod1 = config.StartTimeChargeandDischargeDayandTime.TimeOfDay; + inverter.ChargeEndTimePeriod1 = config.StopTimeChargeandDischargeDayandTime.TimeOfDay; + + inverter.DischargeStartTimePeriod1 = TimeSpan.Zero; + inverter.DischargeEndTimePeriod1 = TimeSpan.Zero; + } + else + { + inverter.DishargingPowerPeriod1 = Math.Abs(power); + inverter.DischargeStartTimePeriod1 = config.StartTimeChargeandDischargeDayandTime.TimeOfDay; + inverter.DischargeEndTimePeriod1 = config.StopTimeChargeandDischargeDayandTime.TimeOfDay; + + inverter.ChargeStartTimePeriod1 = TimeSpan.Zero; + inverter.ChargeEndTimePeriod1 = 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; + inverter.WorkingMode = SpontaneousSelfUse; } - + + inverter.PowerOn = 1; + inverter.PowerOff = 0; } - else - { - statusrecord.InverterRecord1.WorkingMode = SpontaneousSelfUse; - } - statusrecord.InverterRecord1.PowerOn = 1; - statusrecord.InverterRecord1.PowerOff = 0; - //statusrecord.InverterRecord.FaultClearing = 1; + } + + static void PrintInverterData(SinexcelRecord r, int index) + { + Console.WriteLine($" ************************************************ Inverter {index} ************************************************ "); + + //Console.WriteLine($"{r.SystemDateTime} SystemDateTime"); + Console.WriteLine($"{r.TotalPhotovoltaicPower} TotalPhotovoltaicPower"); + Console.WriteLine($"{r.TotalBatteryPower} TotalBatteryPower"); + Console.WriteLine($"{r.TotalLoadPower} TotalLoadPower"); + Console.WriteLine($"{r.TotalGridPower} TotalGridPower"); + + Console.WriteLine($"{r.Battery1Power} Battery1Power"); + Console.WriteLine($"{r.Battery1Soc} Battery1Soc"); + Console.WriteLine($"{r.Battery1BackupSoc} Battery1BackupSoc"); + Console.WriteLine($"{r.Battery1MinSoc} Battery1MinSoc"); + + Console.WriteLine($"{r.Battery2Power} Battery2Power"); + Console.WriteLine($"{r.Battery2Soc} Battery2Soc"); + Console.WriteLine($"{r.Battery2BackupSoc} Battery2BackupSoc"); + Console.WriteLine($"{r.Battery2MinSoc} Battery2MinSoc"); + + Console.WriteLine($"{r.EnableGridExport} EnableGridExport"); + Console.WriteLine($"{r.PowerGridExportLimit} PowerGridExportLimit"); + + Console.WriteLine($"{r.PowerOn} PowerOn"); + Console.WriteLine($"{r.PowerOff} PowerOff"); + Console.WriteLine($"{r.WorkingMode} WorkingMode"); + Console.WriteLine($"{r.GridSwitchMethod} GridSwitchMethod"); + Console.WriteLine($"{r.ThreePhaseWireSystem} ThreePhaseWireSystem"); + + Console.WriteLine(); } private static bool IsNowInsideDateAndTime(DateTime effectiveStart, DateTime effectiveEnd) @@ -309,7 +423,7 @@ internal static class Program private static StatusMessage GetSodiHomeStateAlarm(StatusRecord? record) { - var s3Bucket = Config.Load().S3?.Bucket; + var s3Bucket = record?.Config.S3?.Bucket; // this should not load the config file, only use the one from status record TO change this in other project var alarmList = new List(); var warningList = new List(); @@ -424,13 +538,7 @@ internal 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}"); - + if (s3Bucket == null) { Console.WriteLine("⚠ S3 bucket not configured. Skipping middleware send."); @@ -526,47 +634,48 @@ internal static class Program } - 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.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 + try + { + // SYSTEM DATA + var result1 = ConvertToModbusRegisters((status.Config.ModbusProtcolNumber * 10), "UInt16", 30001); // this to be updated to modbusTCP version + var result2 = ConvertToModbusRegisters(DateTimeOffset.Now.ToUnixTimeSeconds(), "UInt32", 30002); + // SYSTEM DATA + var result3 = ConvertToModbusRegisters(status.InverterRecord.OperatingPriority, "UInt16", 30004); + 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 result8 = ConvertToModbusRegisters((0), "UInt16", 31001); // this is ignored as dosen't exist in Sinexcel + var result12 = ConvertToModbusRegisters((status.InverterRecord.AvgBatteryVoltage.Value * 10), "Int16", 31002); + var result13 = ConvertToModbusRegisters((status.InverterRecord.TotalBatteryCurrent.Value * 10), "Int32", 31003); + var result16 = ConvertToModbusRegisters((status.InverterRecord.AvgBatterySoc.Value * 100), "UInt16", 31005); + var result9 = ConvertToModbusRegisters((status.InverterRecord.TotalBatteryPower.Value * 10), "Int32", 31006); + var result14 = ConvertToModbusRegisters((status.InverterRecord.MinSoc.Value * 100), "UInt16", 31008); + var result55 = ConvertToModbusRegisters(100 * 100, "UInt16", 31009); //this is ignored as dosen't exist in Sinexcel + var result5 = ConvertToModbusRegisters((status.InverterRecord.AvgBatterySoh.Value * 100), "UInt16", 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 result7 = ConvertToModbusRegisters((status.InverterRecord.AvgBatteryTemp.Value * 100), "Int16", 31011); + var result20 = ConvertToModbusRegisters((status.InverterRecord.MaxChargeCurrent.Value * 10), "UInt16", 31012); + var result15 = ConvertToModbusRegisters((status.InverterRecord.MaxDischargingCurrent.Value * 10), "UInt16", 31013); + var result26 = ConvertToModbusRegisters(60 * 10, "UInt16", 31014); //this is ignored as dosen't exist in Sinexcel - 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 result18 = ConvertToModbusRegisters((status.InverterRecord.TotalPhotovoltaicPower.Value * 10), "UInt32", 32000); + var result19 = ConvertToModbusRegisters((status.InverterRecord.TotalGridPower.Value * 10), "Int32", 33000); + var result23 = ConvertToModbusRegisters((status.InverterRecord.GridFrequency.Value * 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); - + var result24 = ConvertToModbusRegisters((status.InverterRecord.OperatingPriority), "UInt16", 34000); + var result25 = ConvertToModbusRegisters((status.InverterRecord.InverterPower.Value * 10), "Int32", 34001); + var result29 = ConvertToModbusRegisters((status.InverterRecord.EnableGridExport ), "UInt16", 35002); + var result27 = ConvertToModbusRegisters((status.InverterRecord.GridExportPower.Value ), "Int16", 35003); // Merge all results into one dictionary var allResults = new[] { - result1, result2, result3, result4, result5, result23, result24, result25, result29, result27, result26, result7, result8, result9, result16, result20, result12, result13, result14, result15, result18, result19, result21, result22 + result1, result2, result3, result4, result5, result23, result24, result25, result29, result27, result26, result7, result8, result9, result16, result20, result12, result13, result14, result15, result18, result19 + , result55 }; foreach (var result in allResults) @@ -580,11 +689,16 @@ internal static class Program 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"); + // Console.WriteLine("JSON file written successfully."); + // Console.WriteLine(json); return true; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } private static Dictionary ConvertToModbusRegisters(object value, string outputType, int startingAddress) diff --git a/csharp/App/SinexcelCommunication/SystemConfig/Config.cs b/csharp/App/SinexcelCommunication/SystemConfig/Config.cs index b1d244755..0c5a7fffa 100644 --- a/csharp/App/SinexcelCommunication/SystemConfig/Config.cs +++ b/csharp/App/SinexcelCommunication/SystemConfig/Config.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using InnovEnergy.App.SinexcelCommunication.DataTypes; using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType; using InnovEnergy.Lib.Utils; using static System.Text.Json.JsonSerializer; @@ -10,17 +11,21 @@ namespace InnovEnergy.App.SinexcelCommunication.SystemConfig; [SuppressMessage("Trimming", "IL2026:Members annotated with \'RequiresUnreferencedCodeAttribute\' require dynamic access otherwise can break functionality when trimming application code")] public class Config { - private static String DefaultConfigFilePath => Path.Combine(Environment.CurrentDirectory, "config.json"); private static DateTime DefaultDatetime => new(2025, 01, 01, 09, 00, 00); private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; + public required DeviceConfig Devices { get; set; } + //public required Boolean DynamicPricingEnabled { get; set; } + //public required DynamicPricingMode DynamicPricingMode { get; set; } + //public required Decimal CheapPrice { get; set; } + //public required Decimal HighPrice { get; set; } public required Double MinSoc { get; set; } public required Double GridSetPoint { get; set; } public required Double MaximumDischargingCurrent { get; set; } public required Double MaximumChargingCurrent { get; set; } - public required WorkingMode OperatingPriority { get; set; } + public required OperatingPriority OperatingPriority { get; set; } public required Int16 BatteriesCount { get; set; } public required Int16 ClusterNumber { get; set; } public required Int16 PvNumber { get; set; } @@ -34,15 +39,26 @@ public class Config public required S3Config? S3 { get; set; } - private static String? LastSavedData { get; set; } + private static String? LastSavedData { get; set; } + private static DateTime? LoadedWriteTimeUtc { get; set; } public static Config Default => new() { + Devices = new () + { + Serial = new() {BaudRate = 115200, Parity = 0, StopBits = 1, DataBits = 8}, + Inverter1 = new() {DeviceState = DeviceState.Measured, Port = "/dev/ttyUSB0", SlaveId = 1}, + Inverter2 = new() {DeviceState = DeviceState.Measured, Port = "/dev/ttyUSB1", SlaveId = 1}, + Inverter3 = new() {DeviceState = DeviceState.Measured, Port = "/dev/ttyUSB3", SlaveId = 1}, + Inverter4 = new() {DeviceState = DeviceState.Measured, Port = "/dev/ttyUSB4", SlaveId = 1}, + }, + //DynamicPricingEnabled = false, + //DynamicPricingMode = DynamicPricingMode.Disabled, MinSoc = 20, GridSetPoint = 0, MaximumChargingCurrent = 180, MaximumDischargingCurrent = 180, - OperatingPriority = WorkingMode.TimeChargeDischarge, + OperatingPriority = OperatingPriority.LoadPriority, BatteriesCount = 0, ClusterNumber = 0, PvNumber = 0, @@ -67,6 +83,10 @@ public class Config { var configFilePath = path ?? DefaultConfigFilePath; + var currentWriteTime = File.GetLastWriteTimeUtc(configFilePath); + if (currentWriteTime != LoadedWriteTimeUtc) + throw new IOException("Config file changed on disk since it was loaded; refusing to overwrite."); // to prevent an overwriting while an external changes happended in the meantime + try { var jsonString = Serialize(this, JsonOptions); @@ -84,13 +104,15 @@ public class Config throw; } } - +/* public static Config Load(String? path = null) { var configFilePath = path ?? DefaultConfigFilePath; try { var jsonString = File.ReadAllText(configFilePath); + // LoadedWriteTimeUtc = File.GetLastWriteTimeUtc(configFilePath); + return Deserialize(jsonString)!; } catch (Exception e) @@ -98,6 +120,31 @@ public class Config $"Failed to read config file {configFilePath}, using default config\n{e}".WriteLine(); return Default; } + }*/ + + public static Config Load(string? path = null) + { + var configFilePath = path ?? DefaultConfigFilePath; // ✅ handle null first + + try + { + // Now safe to call any File/Path API + var json = File.ReadAllText(configFilePath); + + var cfg = Deserialize(json, JsonOptions) + ?? throw new InvalidOperationException("Config deserialized to null."); + + // Optional: store last write time / last saved json + LoadedWriteTimeUtc = File.GetLastWriteTimeUtc(configFilePath); + LastSavedData = Serialize(cfg, JsonOptions); // if you use the save-skip logic + + return cfg; + } + catch (Exception e) + { + $"Failed to read config file {configFilePath}\n{e}".WriteLine(); + throw; + } } public static async Task LoadAsync(String? path = null) diff --git a/csharp/App/SinexcelCommunication/SystemConfig/DeviceConfig.cs b/csharp/App/SinexcelCommunication/SystemConfig/DeviceConfig.cs new file mode 100644 index 000000000..e82c5f847 --- /dev/null +++ b/csharp/App/SinexcelCommunication/SystemConfig/DeviceConfig.cs @@ -0,0 +1,11 @@ +namespace InnovEnergy.App.SinexcelCommunication.SystemConfig; + +public record DeviceConfig +{ + public required SerialLineConfig Serial { get; init; } + + public required SodiDevice Inverter1 { get; init; } + public required SodiDevice Inverter2 { get; init; } + public required SodiDevice Inverter3 { get; init; } + public required SodiDevice Inverter4 { get; init; } +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/SystemConfig/DeviceState.cs b/csharp/App/SinexcelCommunication/SystemConfig/DeviceState.cs new file mode 100644 index 000000000..8a5769fcd --- /dev/null +++ b/csharp/App/SinexcelCommunication/SystemConfig/DeviceState.cs @@ -0,0 +1,8 @@ +namespace InnovEnergy.App.SinexcelCommunication.SystemConfig; + +public enum DeviceState +{ + Disabled, + Measured, + Computed +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/SystemConfig/SerialLineConfig.cs b/csharp/App/SinexcelCommunication/SystemConfig/SerialLineConfig.cs new file mode 100644 index 000000000..5ac07e52f --- /dev/null +++ b/csharp/App/SinexcelCommunication/SystemConfig/SerialLineConfig.cs @@ -0,0 +1,12 @@ +using System.IO.Ports; + +namespace InnovEnergy.App.SinexcelCommunication.SystemConfig; + +public sealed class SerialLineConfig +{ + public required Int32 BaudRate { get; init; } = 115200; + public required Parity Parity { get; init; } = 0; //none + public required Int32 DataBits { get; init; } = 8; + public required Int32 StopBits { get; init; } = 1; + +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/SystemConfig/SodiDevice.cs b/csharp/App/SinexcelCommunication/SystemConfig/SodiDevice.cs new file mode 100644 index 000000000..e0c6f6661 --- /dev/null +++ b/csharp/App/SinexcelCommunication/SystemConfig/SodiDevice.cs @@ -0,0 +1,8 @@ +namespace InnovEnergy.App.SinexcelCommunication.SystemConfig; + +public class SodiDevice +{ + public required DeviceState DeviceState { get; init; } + public required String Port { get; init; } + public required Byte SlaveId { get; init; } +} diff --git a/csharp/App/SinexcelCommunication/deploy.sh b/csharp/App/SinexcelCommunication/deploy.sh index b471853c3..2c798a8bb 100755 --- a/csharp/App/SinexcelCommunication/deploy.sh +++ b/csharp/App/SinexcelCommunication/deploy.sh @@ -6,12 +6,14 @@ username='inesco' root_password='Sodistore0918425' release_flag_file="./bin/Release/$dotnet_version/linux-arm64/publish/.release.flag" + DOTNET="/snap/dotnet-sdk_60/current/dotnet" + set -e echo -e "\n============================ Build ============================\n" -dotnet publish \ +"$DOTNET" publish \ ./SinexcelCommunication.csproj \ -p:PublishTrimmed=false \ -c Release \ diff --git a/csharp/Sinexcel 12K TL/DataType/MachineType.cs b/csharp/Sinexcel 12K TL/DataType/MachineType.cs index 38b276fcd..796a1d0d6 100644 --- a/csharp/Sinexcel 12K TL/DataType/MachineType.cs +++ b/csharp/Sinexcel 12K TL/DataType/MachineType.cs @@ -49,4 +49,10 @@ public enum SinexcelMachineMode { Single = 0, // Default Parallel = 1 +} + +public enum EnablePowerLimitation +{ + Prohibited = 0, // Default + Enable = 1 } \ No newline at end of file diff --git a/csharp/Sinexcel 12K TL/SinexcelRecord.Api.cs b/csharp/Sinexcel 12K TL/SinexcelRecord.Api.cs index 735bb1545..0826b112e 100644 --- a/csharp/Sinexcel 12K TL/SinexcelRecord.Api.cs +++ b/csharp/Sinexcel 12K TL/SinexcelRecord.Api.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType; using InnovEnergy.Lib.Units; using InnovEnergy.Lib.Units.Power; @@ -193,7 +194,7 @@ public partial class SinexcelRecord - private Int16 _factorFromKwtoW = 1000; + private readonly Int16 _factorFromKwtoW = 1000; // ─────────────────────────────────────────────── // Public API — Decoded Float Values // ─────────────────────────────────────────────── @@ -295,11 +296,12 @@ public partial class SinexcelRecord public UInt16 Minute => (UInt16) ConvertBitPatternToFloat(_minute); public UInt16 Second => (UInt16) ConvertBitPatternToFloat(_second); - public DateTime SystemDateTime => new(Year, Month, Day, Hour, Minute, Second); + // public DateTime SystemDateTime => new(Year, Month, Day, Hour, Minute, Second); // ─────────────────────────────────────────────── // Diesel Generator Measurements // ─────────────────────────────────────────────── + /* public Voltage DieselGenAPhaseVoltage => ConvertBitPatternToFloat(_dieselGenAPhaseVoltage); public Voltage DieselGenBPhaseVoltage => ConvertBitPatternToFloat(_dieselGenBPhaseVoltage); public Voltage DieselGenCPhaseVoltage => ConvertBitPatternToFloat(_dieselGenCPhaseVoltage); @@ -324,7 +326,7 @@ public partial class SinexcelRecord public ReactivePower DieselGenAPhaseReactivePower => ConvertBitPatternToFloat(_dieselGenAPhaseReactivePower) * _factorFromKwtoW; public ReactivePower DieselGenBPhaseReactivePower => ConvertBitPatternToFloat(_dieselGenBPhaseReactivePower) * _factorFromKwtoW; - public ReactivePower DieselGenCPhaseReactivePower => ConvertBitPatternToFloat(_dieselGenCPhaseReactivePower) * _factorFromKwtoW; + public ReactivePower DieselGenCPhaseReactivePower => ConvertBitPatternToFloat(_dieselGenCPhaseReactivePower) * _factorFromKwtoW;*/ // ─────────────────────────────────────────────── // Photovoltaic and Battery Measurements @@ -645,14 +647,14 @@ public partial class SinexcelRecord set => _battery2BackupSOC = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); } // to be tested - public float EnableGridExport + public EnablePowerLimitation EnableGridExport { - get => BitConverter.Int32BitsToSingle(unchecked((int)_enableGridExport)); - set => _enableGridExport = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + get => (EnablePowerLimitation)ConvertBitPatternToFloat(_enableGridExport); //(Boolean)BitConverter.Int32BitsToSingle(unchecked((int)_enableGridExport)); + set => _enableGridExport = (UInt32)value; } - public float PowerGridExportLimit + public ActivePower PowerGridExportLimit { get => BitConverter.Int32BitsToSingle(unchecked((int)_powerGridExportLimit)); set => _powerGridExportLimit = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); @@ -813,15 +815,17 @@ public partial class SinexcelRecord public Percent Battery1SocSecondvalue => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab1Soc)); // 0xB106 % public Percent Battery1Soh => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab1Soh)); // 0xB108 % + // Energy (kW·h) public float Battery2TotalChargingEnergy => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2TotalChargingEnergy)); // 0xB1FC public float Battery2TotalDischargedEnergy => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2TotalDischargedEnergy)); // 0xB1FE // Pack Voltage / Current / Temperature - public Voltage Battery2PackTotalVoltage => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2PackTotalVoltage)); // 0xB200 (0.01 V resolution) - public Current Battery2PackTotalCurrent => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2PackTotalCurrent)); // 0xB202 (0.01 A resolution) - public Percent Battery2Socsecondvalue => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2Soc)); // 0xB206 % - public Percent Battery2Soh => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2Soh)); // 0xB208 % + public Voltage Battery2PackTotalVoltage => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2PackTotalVoltage)); // 0xB200 (0.01 V resolution) + public Current Battery2PackTotalCurrent => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2PackTotalCurrent)); // 0xB202 (0.01 A resolution) + public Temperature Battery2Temperature => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2Temperature)); // 0xB104 (0.01 °C resolution per spec) + public Percent Battery2Socsecondvalue => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2Soc)); // 0xB206 % + public Percent Battery2Soh => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2Soh)); // 0xB208 % // Repetitive-week mask (bit-mapped 0–6 = Sun–Sat) @@ -930,5 +934,9 @@ public partial class SinexcelRecord byte[] bytes = BitConverter.GetBytes(rawValue); return BitConverter.ToSingle(bytes, 0); } - + + private static UInt32 ConvertFloatToBitPattern(float value) + { + return BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } } \ No newline at end of file diff --git a/csharp/Sinexcel 12K TL/SinexcelRecord.Modbus.cs b/csharp/Sinexcel 12K TL/SinexcelRecord.Modbus.cs index ceeab2844..f94990f32 100644 --- a/csharp/Sinexcel 12K TL/SinexcelRecord.Modbus.cs +++ b/csharp/Sinexcel 12K TL/SinexcelRecord.Modbus.cs @@ -290,16 +290,17 @@ public partial class SinexcelRecord // ─────────────────────────────────────────────── // Date / Time Information // ─────────────────────────────────────────────── - [HoldingRegister(4338)] private UInt32 _year; // 0x10F2 - [HoldingRegister(4340)] private UInt32 _month; // 0x10F4 - [HoldingRegister(4342)] private UInt32 _day; // 0x10F6 - [HoldingRegister(4344)] private UInt32 _hour; // 0x10F8 - [HoldingRegister(4346)] private UInt32 _minute; // 0x10FA - [HoldingRegister(4348)] private UInt32 _second; // 0x10FC + [HoldingRegister(4338/*, writable: true*/)] private UInt32 _year; // 0x10F2 + [HoldingRegister(4340/*, writable: true*/)] private UInt32 _month; // 0x10F4 + [HoldingRegister(4342/*, writable: true*/)] private UInt32 _day; // 0x10F6 + [HoldingRegister(4344/*, writable: true*/)] private UInt32 _hour; // 0x10F8 + [HoldingRegister(4346/*, writable: true*/)] private UInt32 _minute; // 0x10FA + [HoldingRegister(4348/*, writable: true*/)] private UInt32 _second; // 0x10FC // ─────────────────────────────────────────────── // Diesel Generator Measurements // ─────────────────────────────────────────────── + /* [HoldingRegister(4362)] private UInt32 _dieselGenAPhaseVoltage; // 0x110A [HoldingRegister(4364)] private UInt32 _dieselGenBPhaseVoltage; // 0x110C [HoldingRegister(4366)] private UInt32 _dieselGenCPhaseVoltage; // 0x110E @@ -318,8 +319,8 @@ public partial class SinexcelRecord [HoldingRegister(4410)] private UInt32 _dieselGenBPhaseActivePower; // 0x113A [HoldingRegister(4412)] private UInt32 _dieselGenCPhaseActivePower; // 0x113C [HoldingRegister(4414)] private UInt32 _dieselGenAPhaseReactivePower; // 0x113E - [HoldingRegister(4416)] private UInt32 _dieselGenBPhaseReactivePower; // 0x1140 - [HoldingRegister(4418)] private UInt32 _dieselGenCPhaseReactivePower; // 0x1142 + [HoldingRegister(4416)] private UInt32 _dieselGenBPhaseReactivePower; // 0x1140* + [HoldingRegister(4418)] private UInt32 _dieselGenCPhaseReactivePower; // 0x1142*/ // ─────────────────────────────────────────────── // Photovoltaic and Battery Measurements