From 711f054a0654935d4d55a19ff38a85ef69672ab2 Mon Sep 17 00:00:00 2001 From: atef Date: Mon, 20 Oct 2025 14:56:12 +0200 Subject: [PATCH] New project sinexcel --- .../DataLogging/LogFileConcatenator.cs | 33 + .../DataLogging/Logfile.cs | 48 ++ .../DataLogging/Logger.cs | 39 ++ .../DataTypes/AlarmOrWarning.cs | 9 + .../DataTypes/Configuration.cs | 12 + .../DataTypes/SodistoreAlarmState.cs | 8 + .../DataTypes/StatusMessage.cs | 17 + .../App/SinexcelCommunication/DeviceState.cs | 8 + .../SinexcelCommunication/ESS/StatusRecord.cs | 10 + .../MiddlewareClasses/MiddlewareAgent.cs | 88 +++ .../MiddlewareClasses/RabbitMQManager.cs | 64 ++ csharp/App/SinexcelCommunication/Program.cs | 566 ++++++++++++++++ .../SinexcelCommunication/SalimaxDevice.cs | 13 + .../SinexcelCommunication.csproj | 25 + .../SystemConfig/Config.cs | 93 +++ .../SystemConfig/S3Config.cs | 79 +++ csharp/App/SinexcelCommunication/deploy.sh | 32 + .../DataType/AccreditedCountry.cs | 25 + .../Sinexcel 12K TL/DataType/BatteryType.cs | 68 ++ .../DataType/GridSwitchMethode.cs | 8 + .../DataType/InputFrequency.cs | 7 + .../Sinexcel 12K TL/DataType/MachineType.cs | 52 ++ .../Sinexcel 12K TL/DataType/SinexcelModel.cs | 21 + .../DataType/ThreePhaseWireSystem.cs | 7 + .../Sinexcel 12K TL/DataType/WorkingMode.cs | 18 + csharp/Sinexcel 12K TL/Sinexcel 12K TL.csproj | 16 + csharp/Sinexcel 12K TL/SinexcelDevice.cs | 46 ++ csharp/Sinexcel 12K TL/SinexcelRecord.Api.cs | 610 ++++++++++++++++++ .../Sinexcel 12K TL/SinexcelRecord.Modbus.cs | 313 +++++++++ 29 files changed, 2335 insertions(+) create mode 100644 csharp/App/SinexcelCommunication/DataLogging/LogFileConcatenator.cs create mode 100644 csharp/App/SinexcelCommunication/DataLogging/Logfile.cs create mode 100644 csharp/App/SinexcelCommunication/DataLogging/Logger.cs create mode 100644 csharp/App/SinexcelCommunication/DataTypes/AlarmOrWarning.cs create mode 100644 csharp/App/SinexcelCommunication/DataTypes/Configuration.cs create mode 100644 csharp/App/SinexcelCommunication/DataTypes/SodistoreAlarmState.cs create mode 100644 csharp/App/SinexcelCommunication/DataTypes/StatusMessage.cs create mode 100644 csharp/App/SinexcelCommunication/DeviceState.cs create mode 100644 csharp/App/SinexcelCommunication/ESS/StatusRecord.cs create mode 100644 csharp/App/SinexcelCommunication/MiddlewareClasses/MiddlewareAgent.cs create mode 100644 csharp/App/SinexcelCommunication/MiddlewareClasses/RabbitMQManager.cs create mode 100644 csharp/App/SinexcelCommunication/Program.cs create mode 100644 csharp/App/SinexcelCommunication/SalimaxDevice.cs create mode 100644 csharp/App/SinexcelCommunication/SinexcelCommunication.csproj create mode 100644 csharp/App/SinexcelCommunication/SystemConfig/Config.cs create mode 100644 csharp/App/SinexcelCommunication/SystemConfig/S3Config.cs create mode 100755 csharp/App/SinexcelCommunication/deploy.sh create mode 100644 csharp/Sinexcel 12K TL/DataType/AccreditedCountry.cs create mode 100644 csharp/Sinexcel 12K TL/DataType/BatteryType.cs create mode 100644 csharp/Sinexcel 12K TL/DataType/GridSwitchMethode.cs create mode 100644 csharp/Sinexcel 12K TL/DataType/InputFrequency.cs create mode 100644 csharp/Sinexcel 12K TL/DataType/MachineType.cs create mode 100644 csharp/Sinexcel 12K TL/DataType/SinexcelModel.cs create mode 100644 csharp/Sinexcel 12K TL/DataType/ThreePhaseWireSystem.cs create mode 100644 csharp/Sinexcel 12K TL/DataType/WorkingMode.cs create mode 100644 csharp/Sinexcel 12K TL/Sinexcel 12K TL.csproj create mode 100644 csharp/Sinexcel 12K TL/SinexcelDevice.cs create mode 100644 csharp/Sinexcel 12K TL/SinexcelRecord.Api.cs create mode 100644 csharp/Sinexcel 12K TL/SinexcelRecord.Modbus.cs diff --git a/csharp/App/SinexcelCommunication/DataLogging/LogFileConcatenator.cs b/csharp/App/SinexcelCommunication/DataLogging/LogFileConcatenator.cs new file mode 100644 index 000000000..62e165ff1 --- /dev/null +++ b/csharp/App/SinexcelCommunication/DataLogging/LogFileConcatenator.cs @@ -0,0 +1,33 @@ +using System.Text; + +namespace InnovEnergy.App.SinexcelCommunication.DataLogging; + +public class LogFileConcatenator +{ + private readonly String _LogDirectory; + + public LogFileConcatenator(String logDirectory = "JsonLogDirectory/") + { + _LogDirectory = logDirectory; + } + + public String ConcatenateFiles(int numberOfFiles) + { + var logFiles = Directory + .GetFiles(_LogDirectory, "log_*.json") + .OrderByDescending(file => file) + .Take(numberOfFiles) + .OrderBy(file => file) + .ToList(); + + var concatenatedContent = new StringBuilder(); + + foreach (var fileContent in logFiles.Select(File.ReadAllText)) + { + concatenatedContent.AppendLine(fileContent); + //concatenatedContent.AppendLine(); // Append an empty line to separate the files // maybe we don't need this + } + + return concatenatedContent.ToString(); + } +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/DataLogging/Logfile.cs b/csharp/App/SinexcelCommunication/DataLogging/Logfile.cs new file mode 100644 index 000000000..935f6d38f --- /dev/null +++ b/csharp/App/SinexcelCommunication/DataLogging/Logfile.cs @@ -0,0 +1,48 @@ +using InnovEnergy.Lib.Utils; +using Microsoft.Extensions.Logging; +namespace InnovEnergy.App.SinexcelCommunication.DataLogging; + +public class CustomLogger : ILogger +{ + private readonly String _LogFilePath; + //private readonly Int64 _maxFileSizeBytes; + private readonly Int32 _MaxLogFileCount; + private Int64 _CurrentFileSizeBytes; + + public CustomLogger(String logFilePath, Int32 maxLogFileCount) + { + _LogFilePath = logFilePath; + _MaxLogFileCount = maxLogFileCount; + _CurrentFileSizeBytes = File.Exists(logFilePath) ? new FileInfo(logFilePath).Length : 0; + } + + public IDisposable? BeginScope(TState state) where TState : notnull => throw new NotImplementedException(); + + public Boolean IsEnabled(LogLevel logLevel) => true; // Enable logging for all levels + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + var logMessage = formatter(state, exception!); + + // Check the log file count and delete the oldest file if necessary + var logFileDir = Path.GetDirectoryName(_LogFilePath)!; + var logFileExt = Path.GetExtension(_LogFilePath); + var logFileBaseName = Path.GetFileNameWithoutExtension(_LogFilePath); + + var logFiles = Directory + .GetFiles(logFileDir, $"{logFileBaseName}_*{logFileExt}") + .OrderBy(file => file) + .ToList(); + + if (logFiles.Count >= _MaxLogFileCount) + { + File.Delete(logFiles.First()); + } + + var roundedUnixTimestamp = DateTime.Now.ToUnixTime() % 2 == 0 ? DateTime.Now.ToUnixTime() : DateTime.Now.ToUnixTime() + 1; + var timestamp = "Timestamp;" + roundedUnixTimestamp + Environment.NewLine; + + var logFileBackupPath = Path.Combine(logFileDir, $"{logFileBaseName}_{DateTime.Now.ToUnixTime()}{logFileExt}"); + File.AppendAllText(logFileBackupPath, timestamp + logMessage + Environment.NewLine); + } +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/DataLogging/Logger.cs b/csharp/App/SinexcelCommunication/DataLogging/Logger.cs new file mode 100644 index 000000000..92f8855a5 --- /dev/null +++ b/csharp/App/SinexcelCommunication/DataLogging/Logger.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Logging; + +namespace InnovEnergy.App.SinexcelCommunication.DataLogging; + +public static class Logger +{ + // Specify the maximum log file size in bytes (e.g., 1 MB) + + //private const Int32 MaxFileSizeBytes = 2024 * 30; // TODO: move to settings + private const Int32 MaxLogFileCount = 5000; // TODO: move to settings + private const String LogFilePath = "JsonLogDirectory/log.json"; // TODO: move to settings + + // ReSharper disable once InconsistentNaming + private static readonly ILogger _logger = new CustomLogger(LogFilePath, MaxLogFileCount); + + public static T LogInfo(this T t) where T : notnull + { + _logger.LogInformation(t.ToString()); // TODO: check warning + return t; + } + + public static T LogDebug(this T t) where T : notnull + { + // _logger.LogDebug(t.ToString()); // TODO: check warning + return t; + } + + public static T LogError(this T t) where T : notnull + { + // _logger.LogError(t.ToString()); // TODO: check warning + return t; + } + + public static T LogWarning(this T t) where T : notnull + { + // _logger.LogWarning(t.ToString()); // TODO: check warning + return t; + } +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/DataTypes/AlarmOrWarning.cs b/csharp/App/SinexcelCommunication/DataTypes/AlarmOrWarning.cs new file mode 100644 index 000000000..fb87dfc05 --- /dev/null +++ b/csharp/App/SinexcelCommunication/DataTypes/AlarmOrWarning.cs @@ -0,0 +1,9 @@ +namespace InnovEnergy.App.SinexcelCommunication.DataTypes; + +public class AlarmOrWarning +{ + public String? Date { get; set; } + public String? Time { get; set; } + public String? Description { get; set; } + public String? CreatedBy { get; set; } +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/DataTypes/Configuration.cs b/csharp/App/SinexcelCommunication/DataTypes/Configuration.cs new file mode 100644 index 000000000..24af7e5fc --- /dev/null +++ b/csharp/App/SinexcelCommunication/DataTypes/Configuration.cs @@ -0,0 +1,12 @@ +namespace InnovEnergy.App.SinexcelCommunication.DataTypes; + +public class Configuration +{ + public Double MinimumSoC { get; set; } + public Double GridSetPoint { get; set; } + // public Double MaximumDischargingCurrent { get; set; } + // public Double MaximumChargingCurrent { get; set; } + // public EssMode OperatingPriority { get; set; } + // public required Int16 BatteriesCount { get; set; } + +} diff --git a/csharp/App/SinexcelCommunication/DataTypes/SodistoreAlarmState.cs b/csharp/App/SinexcelCommunication/DataTypes/SodistoreAlarmState.cs new file mode 100644 index 000000000..886a74c79 --- /dev/null +++ b/csharp/App/SinexcelCommunication/DataTypes/SodistoreAlarmState.cs @@ -0,0 +1,8 @@ +namespace InnovEnergy.App.SinexcelCommunication.DataTypes; + +public enum SodistoreAlarmState +{ + Green, + Orange, + Red +} diff --git a/csharp/App/SinexcelCommunication/DataTypes/StatusMessage.cs b/csharp/App/SinexcelCommunication/DataTypes/StatusMessage.cs new file mode 100644 index 000000000..013269324 --- /dev/null +++ b/csharp/App/SinexcelCommunication/DataTypes/StatusMessage.cs @@ -0,0 +1,17 @@ +namespace InnovEnergy.App.SinexcelCommunication.DataTypes; + +public class StatusMessage +{ + public required Int32 InstallationId { get; set; } + public required Int32 Product { get; set; } + public required SodistoreAlarmState Status { get; set; } + public required MessageType Type { get; set; } + public List? Warnings { get; set; } + public List? Alarms { get; set; } +} + +public enum MessageType +{ + AlarmOrWarning, + Heartbit +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/DeviceState.cs b/csharp/App/SinexcelCommunication/DeviceState.cs new file mode 100644 index 000000000..839b21bb8 --- /dev/null +++ b/csharp/App/SinexcelCommunication/DeviceState.cs @@ -0,0 +1,8 @@ +namespace InnovEnergy.App.SinexcelCommunication; + +public enum DeviceState +{ + Disabled, + Measured, + Computed +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/ESS/StatusRecord.cs b/csharp/App/SinexcelCommunication/ESS/StatusRecord.cs new file mode 100644 index 000000000..5941bf755 --- /dev/null +++ b/csharp/App/SinexcelCommunication/ESS/StatusRecord.cs @@ -0,0 +1,10 @@ +using InnovEnergy.App.SinexcelCommunication.SystemConfig; +using InnovEnergy.Lib.Devices.Sinexcel_12K_TL; + +namespace InnovEnergy.App.SinexcelCommunication.ESS; + +public record StatusRecord +{ + public required SinexcelRecord SinexcelRecord { 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 new file mode 100644 index 000000000..4fa8a114a --- /dev/null +++ b/csharp/App/SinexcelCommunication/MiddlewareClasses/MiddlewareAgent.cs @@ -0,0 +1,88 @@ +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; +using System.Text.Json; +using InnovEnergy.App.SinexcelCommunication.DataTypes; + +namespace InnovEnergy.App.SinexcelCommunication.MiddlewareClasses; + +public static class MiddlewareAgent +{ + private static UdpClient _udpListener = null!; + private static IPAddress? _controllerIpAddress; + private static EndPoint? _endPoint; + + public static void InitializeCommunicationToMiddleware() + { + _controllerIpAddress = FindVpnIp(); + if (Equals(IPAddress.None, _controllerIpAddress)) + { + Console.WriteLine("There is no VPN interface, exiting..."); + } + + const Int32 udpPort = 9000; + _endPoint = new IPEndPoint(_controllerIpAddress, udpPort); + + _udpListener = new UdpClient(); + _udpListener.Client.Blocking = false; + _udpListener.Client.Bind(_endPoint); + } + + private static IPAddress FindVpnIp() + { + const String interfaceName = "innovenergy"; + + var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); + + foreach (var networkInterface in networkInterfaces) + { + if (networkInterface.Name == interfaceName) + { + var ipProps = networkInterface.GetIPProperties(); + var uniCastIPs = ipProps.UnicastAddresses; + var controllerIpAddress = uniCastIPs[0].Address; + + Console.WriteLine("VPN IP is: "+ uniCastIPs[0].Address); + return controllerIpAddress; + } + } + + return IPAddress.None; + } + + public static Configuration? SetConfigurationFile() + { + if (_udpListener.Available > 0) + { + IPEndPoint? serverEndpoint = null; + + var replyMessage = "ACK"; + var replyData = Encoding.UTF8.GetBytes(replyMessage); + + var udpMessage = _udpListener.Receive(ref serverEndpoint); + var message = Encoding.UTF8.GetString(udpMessage); + + var config = JsonSerializer.Deserialize(message); + + if (config != null) + { + Console.WriteLine($"Received a configuration message: GridSetPoint is " + config.GridSetPoint + + ", MinimumSoC is " + config.MinimumSoC + " and operating priorty is " ); + + // Send the reply to the sender's endpoint + _udpListener.Send(replyData, replyData.Length, serverEndpoint); + Console.WriteLine($"Replied to {serverEndpoint}: {replyMessage}"); + return config; + } + } + + if (_endPoint != null && !_endPoint.Equals(_udpListener.Client.LocalEndPoint as IPEndPoint)) + { + Console.WriteLine("UDP address has changed, rebinding..."); + InitializeCommunicationToMiddleware(); + } + return null; + } + +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/MiddlewareClasses/RabbitMQManager.cs b/csharp/App/SinexcelCommunication/MiddlewareClasses/RabbitMQManager.cs new file mode 100644 index 000000000..759e08477 --- /dev/null +++ b/csharp/App/SinexcelCommunication/MiddlewareClasses/RabbitMQManager.cs @@ -0,0 +1,64 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.Json; +using InnovEnergy.App.SinexcelCommunication.DataTypes; +using RabbitMQ.Client; + +namespace InnovEnergy.App.SinexcelCommunication.MiddlewareClasses; + + +[SuppressMessage("Trimming", "IL2026:Members annotated with \'RequiresUnreferencedCodeAttribute\' require dynamic access otherwise can break functionality when trimming application code")] +public static class RabbitMqManager +{ + public static ConnectionFactory? Factory ; + public static IConnection ? Connection; + public static IModel? Channel; + + public static Boolean SubscribeToQueue(StatusMessage currentSalimaxState, String? s3Bucket,String VpnServerIp) + { + try + { + //_factory = new ConnectionFactory { HostName = VpnServerIp }; + + Factory = new ConnectionFactory + { + HostName = VpnServerIp, + Port = 5672, + VirtualHost = "/", + UserName = "producer", + Password = "b187ceaddb54d5485063ddc1d41af66f", + + }; + + Connection = Factory.CreateConnection(); + Channel = Connection.CreateModel(); + Channel.QueueDeclare(queue: "statusQueue", durable: true, exclusive: false, autoDelete: false, arguments: null); + + Console.WriteLine("The controller sends its status to the middleware for the first time"); + if (s3Bucket != null) InformMiddleware(currentSalimaxState); + + + } + catch (Exception ex) + { + Console.WriteLine("An error occurred while connecting to the RabbitMQ queue: " + ex.Message); + return false; + } + return true; + } + + public static void InformMiddleware(StatusMessage status) + { + var message = JsonSerializer.Serialize(status); + var body = Encoding.UTF8.GetBytes(message); + + Channel.BasicPublish(exchange: string.Empty, + routingKey: "statusQueue", + basicProperties: null, + body: body); + + Console.WriteLine($"Producer sent message: {message}"); + } + + +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/Program.cs b/csharp/App/SinexcelCommunication/Program.cs new file mode 100644 index 000000000..483b6a95b --- /dev/null +++ b/csharp/App/SinexcelCommunication/Program.cs @@ -0,0 +1,566 @@ +using System.IO.Compression; +using System.IO.Ports; +using System.Text; +using System.Text.Json; + +using Flurl.Http; + +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; + +using InnovEnergy.Lib.Devices.Sinexcel_12K_TL; +using InnovEnergy.App.SinexcelCommunication.DataLogging; +using InnovEnergy.App.SinexcelCommunication.ESS; +using InnovEnergy.App.SinexcelCommunication.MiddlewareClasses; +using InnovEnergy.App.SinexcelCommunication.SystemConfig; +using InnovEnergy.Lib.Protocols.Modbus.Channels; +using InnovEnergy.App.SinexcelCommunication.DataTypes; + +using InnovEnergy.Lib.Units; +using InnovEnergy.Lib.Utils; + +using Newtonsoft.Json; +using Formatting = Newtonsoft.Json.Formatting; +using JsonSerializer = System.Text.Json.JsonSerializer; +using static InnovEnergy.App.SinexcelCommunication.MiddlewareClasses.MiddlewareAgent; +using System.Diagnostics.CodeAnalysis; +#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. + +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(4); + private const UInt16 NbrOfFileToConcatenate = 15; // add this to config file + private static UInt16 _fileCounter = 0; + private static Channel _sinexcelChannel; + + private const String SwVersionNumber =" V1.00.131025 beta"; + 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; + + // move all this to config file + private const String Port = "/dev/ttyUSB0"; + 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) + { + _sinexcelChannel = new SerialPortChannel(Port, BaudRate, Parity, DataBits, StopBits); + + while (true) + { + try + { + await Run(); + } + catch (Exception e) + { + e.WriteLine(); + } + } + // ReSharper disable once FunctionNeverReturns + } + + private static async Task Run() + { + Watchdog.NotifyReady(); + + Console.WriteLine("Starting Sinexcel Communication"); + + var sinexcelDevice = new SinexcelDevice(_sinexcelChannel, SlaveId); + // var sinexcelDevice = new sinexcelDevices(new List { growattDeviceT415K }); + + StatusRecord? ReadStatus() + { + var config = Config.Load(); + var sinexcelRecord = sinexcelDevice.Read(); + + return new StatusRecord + { + SinexcelRecord = sinexcelRecord, + Config = config // load from disk every iteration, so config can be changed while running + }; + } + + while (true) + { + await Observable + .Interval(UpdateInterval) + .Select(_ => RunIteration()) + .SelectMany(status => + DataLogging(status, DateTime.Now.Round(UpdateInterval)) + .ContinueWith(_ => status)) // back to StatusRecord + .SelectMany(SaveModbusTcpFile) + .SelectError() + .ToTask(); + } + + StatusRecord? RunIteration() + { + try + { + Watchdog.NotifyAlive(); + + var startTime = DateTime.Now; + Console.WriteLine("***************************** Reading Battery Data *********************************************"); + Console.WriteLine(startTime.ToString("HH:mm:ss.fff")); + + // the order matter of the next three lines + var statusrecord = ReadStatus(); + if (statusrecord == null) + return null; + + statusrecord.SinexcelRecord.GridAPhaseVoltage.WriteLine(" = Grid A PhaseVoltage"); + statusrecord.SinexcelRecord.GridBPhaseVoltage.WriteLine(" = Grid B PhaseVoltage"); + statusrecord.SinexcelRecord.GridCPhaseVoltage.WriteLine(" = Grid C PhaseVoltage"); + + statusrecord.SinexcelRecord.GridAPhaseCurrent.WriteLine(" = Grid A PhaseCurrent"); + statusrecord.SinexcelRecord.GridBPhaseCurrent.WriteLine(" = Grid B PhaseCurrent"); + statusrecord.SinexcelRecord.GridCPhaseCurrent.WriteLine(" = Grid C PhaseCurrent"); + + + statusrecord.SinexcelRecord.GridAPhaseActivePower.WriteLine(" = A Grid Active Power"); + statusrecord.SinexcelRecord.GridBPhaseActivePower.WriteLine(" = B Grid Active Power"); + statusrecord.SinexcelRecord.GridCPhaseActivePower.WriteLine(" = C Grid Active Power"); + + statusrecord.SinexcelRecord.GridVoltageFrequency.WriteLine(" = Frequency"); + + statusrecord.SinexcelRecord.ElectricMeterCPhaseActivePower.WriteLine(" Meter Phase C Power"); + statusrecord.SinexcelRecord.ElectricMeterBPhaseActivePower.WriteLine(" Meter Phase B Power"); + statusrecord.SinexcelRecord.ElectricMeterAPhaseActivePower.WriteLine(" Meter Phase A Power"); + + statusrecord.SinexcelRecord.ElectricMeterAPhaseCurrent.WriteLine(" Meter Phase C Current "); + statusrecord.SinexcelRecord.ElectricMeterBPhaseCurrent.WriteLine(" Meter Phase B Current "); + statusrecord.SinexcelRecord.ElectricMeterBPhaseCurrent.WriteLine(" Meter Phase A Current "); + statusrecord.SinexcelRecord.WorkingMode.WriteLine(" workingmode"); + + statusrecord.SinexcelRecord.BatteryVoltage.WriteLine(" BatteryVoltage"); + statusrecord.SinexcelRecord.BatteryVoltage1.WriteLine(" BatteryVoltage1"); + statusrecord.SinexcelRecord.BatteryVoltage2.WriteLine(" BatteryVoltage2"); + + statusrecord.SinexcelRecord.BatteryPower1.WriteLine(" BatteryPower1"); + statusrecord.SinexcelRecord.BatteryPower2.WriteLine(" BatteryPower2"); + statusrecord.SinexcelRecord.TotalBatteryPower.WriteLine(" TotalBatteryPower"); + + statusrecord.SinexcelRecord.EnableBattery1.WriteLine(" EnableBattery1"); + statusrecord.SinexcelRecord.EnableBattery2.WriteLine(" EnableBattery2"); + + statusrecord.SinexcelRecord.MaxChargingCurrentBattery1.WriteLine(" MaxChargingCurrentBattery1"); + statusrecord.SinexcelRecord.MaxChargingCurrentBattery2.WriteLine(" MaxChargingCurrentBattery2"); + + statusrecord.SinexcelRecord.MaxDischargingCurrentBattery1.WriteLine(" MaxChargingCurrentBattery1"); + statusrecord.SinexcelRecord.MaxDischargingCurrentBattery2.WriteLine(" MaxChargingCurrentBattery2"); + + //EssModeControl(statusrecord); + // statusrecord.ApplyDefaultSettings(); + Console.WriteLine( " ************************************ We are writing ************************************"); + statusrecord?.Config.Save(); // save the config file + //sinexcelDevice.Write(statusrecord?.SinexcelRecord); + var stopTime = DateTime.Now; + Console.WriteLine(stopTime.ToString("HH:mm:ss.fff")); + return statusrecord; + } + catch (Exception e) + { + // Handle exception and print the error + Console.WriteLine(e ); + return null; + } + + } + } + + private static Int32 GetInstallationId(String s3Bucket) + { + var part = s3Bucket.Split('-').FirstOrDefault(); + return int.TryParse(part, out var id) ? id : 0; // is 0 a default safe value? check with Marios + } + + private static void SendSalimaxStateAlarm(StatusMessage currentSalimaxState, StatusRecord? record) + { + var s3Bucket = Config.Load().S3?.Bucket; + var subscribedNow = false; + + //When the controller boots, it tries to subscribe to the queue + if (_subscribeToQueueForTheFirstTime == false) + { + subscribedNow = true; + _subscribeToQueueForTheFirstTime = true; + _prevSodiohomeAlarmState = currentSalimaxState.Status; + _subscribedToQueue = RabbitMqManager.SubscribeToQueue(currentSalimaxState, s3Bucket, VpnServerIp); + } + + //If already subscribed to the queue and the status has been changed, update the queue + if (!subscribedNow && _subscribedToQueue && currentSalimaxState.Status != _prevSodiohomeAlarmState) + { + _prevSodiohomeAlarmState = currentSalimaxState.Status; + if (s3Bucket != null) + RabbitMqManager.InformMiddleware(currentSalimaxState); + } + + //If there is an available message from the RabbitMQ Broker, apply the configuration file + Configuration? config = SetConfigurationFile(); + if (config != null) + { + record.ApplyConfigFile(config); + } + } + + private static void ApplyConfigFile(this StatusRecord? status, Configuration? config) + { + if (config == null) return; + if (status == null) return; + + status.Config.MinSoc = config.MinimumSoC; + status.Config.GridSetPoint = config.GridSetPoint * 1000; // converted from kW to W + //status.Config.MaximumChargingCurrent = config.MaximumChargingCurrent; + //status.Config.MaximumDischargingCurrent = config.MaximumDischargingCurrent; + //status.Config.OperatingPriority = config.OperatingPriority; + //status.Config.BatteriesCount = config.BatteriesCount; + } + + 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 result2 = ConvertToModbusRegisters(status.SinexcelRecord.GridAPhaseVoltage, "UInt32", 30002); + + // Merge all results into one dictionary + var allResults = new[] + { + result1,result2 + }; + + foreach (var result in allResults) + { + foreach (var entry in result) + { + 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); + return true; + } + + private static Dictionary ConvertToModbusRegisters(Object value, String outputType, Int32 startingAddress) + { + var registers = new Dictionary(); + + switch (outputType) + { + case "UInt16": + registers[startingAddress.ToString()] = Convert.ToUInt16(value); + break; + + case "Int16": + var int16Val = Convert.ToInt16(value); + 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 + break; + + case "Int32": + var int32Val = Convert.ToInt32(value); + var raw = unchecked((UInt32)int32Val); // reinterprets signed int as unsigned + registers[startingAddress.ToString()] = (UInt16)(raw & 0xFFFF); + registers[(startingAddress + 1).ToString()] = (UInt16)(raw >> 16); + break; + + default: + throw new ArgumentException("Unsupported output type: " + outputType); + } + return registers; + } + + 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 void InsertIntoJson(Dictionary jsonDict, String[] keys, String value) + { + var currentDict = jsonDict; + for (Int16 i = 1; i < keys.Length; i++) // Start at 1 to skip empty root + { + var key = keys[i]; + if (!currentDict.ContainsKey(key)) + { + currentDict[key] = new Dictionary(); + } + + if (i == keys.Length - 1) // Last key, store the value + { + + if (!value.Contains(",") && double.TryParse(value, out Double doubleValue)) // Try to parse value as a number + { + currentDict[key] = Math.Round(doubleValue, 2); // Round to 2 decimal places + + } + else + { + currentDict[key] = value; // Store as string if not a number + } + } + else + { + currentDict = (Dictionary)currentDict[key]; + } + } + } + + private static 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() : ""; + InsertIntoJson(jsonData, keyPath.Split('/'), value); + } + + var jsonOutput = JsonConvert.SerializeObject(jsonData, Formatting.None); + return jsonOutput; + } + + private static async Task SavingLocalCsvFile(Int64 timestamp, String csv) + { + const String directoryPath = "/home/inesco/SodiStoreHome/csvFile"; + + // Ensure directory exists + if (!Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + // Get all .csv files ordered by creation time (oldest first) + var csvFiles = new DirectoryInfo(directoryPath) + .GetFiles("*.csv") + .OrderBy(f => f.CreationTimeUtc) + .ToList(); + + // If more than 5000 files, delete the oldest + if (csvFiles.Count >= 5000) + { + var oldestFile = csvFiles.First(); + try + { + oldestFile.Delete(); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to delete file: {oldestFile.FullName}, Error: {ex.Message}"); + } + } + + // Prepare the filtered CSV content + var filteredCsv = csv + .SplitLines() + .Where(l => !l.Contains("Secret")) + .JoinLines(); + + // Save the new CSV file + var filePath = Path.Combine(directoryPath, timestamp + ".csv"); + await File.WriteAllTextAsync(filePath, filteredCsv); + } + + private static async Task ConcatinatingAndCompressingFiles(Int64 timeStamp, S3Config s3Config) + { + if (_fileCounter >= NbrOfFileToConcatenate) + { + _fileCounter = 0; + + var logFileConcatenator = new LogFileConcatenator(); + var jsontoSend = logFileConcatenator.ConcatenateFiles(NbrOfFileToConcatenate); + + var fileNameWithoutExtension = timeStamp.ToString(); // used for both S3 and local + var s3Path = fileNameWithoutExtension + ".json"; + + var request = s3Config.CreatePutRequest(s3Path); + + var compressedBytes = CompresseBytes(jsontoSend); + var base64String = Convert.ToBase64String(compressedBytes); + var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64"); + + var uploadSucceeded = false; + + try + { + var response = await request.PutAsync(stringContent); + + if (response.StatusCode != 200) + { + Console.WriteLine("ERROR: PUT"); + var error = await response.GetStringAsync(); + Console.WriteLine(error); + + await SaveToLocalCompressedFallback(compressedBytes, fileNameWithoutExtension); + Heartbit(); + return false; + } + + uploadSucceeded = true; + Console.WriteLine("✅ File uploaded to S3 successfully."); + + Console.WriteLine("---------------------------------------- Resending FailedUploadedFiles----------------------------------------"); + Heartbit(); + + await ResendLocalFailedFilesAsync(s3Config); // retry any pending failed files + } + catch (Exception ex) + { + Console.WriteLine("Upload exception: " + ex.Message); + + if (!uploadSucceeded) + { + await SaveToLocalCompressedFallback(compressedBytes, fileNameWithoutExtension); + } + + Heartbit(); + return false; + } + } + + _fileCounter++; + return true; + } + + private static void Heartbit() + { + var s3Bucket = Config.Load().S3?.Bucket; + var tryParse = int.TryParse(s3Bucket?.Split("-")[0], out var installationId); + + if (tryParse) + { + var returnedStatus = new StatusMessage + { + InstallationId = installationId, + Product = 2, + Status = _sodiohomeAlarmState, + Type = MessageType.Heartbit, + }; + if (s3Bucket != null) + RabbitMqManager.InformMiddleware(returnedStatus); + } + } + + private static async Task SaveToLocalCompressedFallback(Byte[] compressedData, String fileNameWithoutExtension) + { + try + { + var fallbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FailedUploads"); + Directory.CreateDirectory(fallbackDir); + + var fileName = fileNameWithoutExtension + ".json"; // Save as .json, but still compressed + var fullPath = Path.Combine(fallbackDir, fileName); + + await File.WriteAllBytesAsync(fullPath, compressedData); // Compressed data + Console.WriteLine($"Saved compressed failed upload to: {fullPath}"); + } + catch (Exception ex) + { + Console.WriteLine("Failed to save compressed file locally: " + ex.Message); + } + } + + private static async Task ResendLocalFailedFilesAsync(S3Config s3Config) + { + var fallbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FailedUploads"); + + if (!Directory.Exists(fallbackDir)) + return; + + var files = Directory.GetFiles(fallbackDir, "*.json"); + files.Length.WriteLine(" Number of failed files, to upload"); + + foreach (var filePath in files) + { + var fileName = Path.GetFileName(filePath); // e.g., "1720023600.json" + + try + { + byte[] compressedBytes = await File.ReadAllBytesAsync(filePath); + var base64String = Convert.ToBase64String(compressedBytes); + var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64"); + + var request = s3Config.CreatePutRequest(fileName); + var response = await request.PutAsync(stringContent); + + if (response.StatusCode == 200) + { + File.Delete(filePath); + Console.WriteLine($"✅ Successfully resent and deleted: {fileName}"); + } + else + { + Console.WriteLine($"❌ Failed to resend {fileName}, status: {response.StatusCode}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"⚠️ Exception while resending {fileName}: {ex.Message}"); + } + } + } + private static Byte[] CompresseBytes(String jsonToSend) + { + //Compress JSON data to a byte array + using var memoryStream = new MemoryStream(); + //Create a zip directory and put the compressed file inside + using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) + { + var entry = archive.CreateEntry("data.json", CompressionLevel.SmallestSize); // Add JSON data to the ZIP archive + using (var entryStream = entry.Open()) + using (var writer = new StreamWriter(entryStream)) + { + writer.Write(jsonToSend); + } + } + + var compressedBytes = memoryStream.ToArray(); + + return compressedBytes; + } + +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/SalimaxDevice.cs b/csharp/App/SinexcelCommunication/SalimaxDevice.cs new file mode 100644 index 000000000..f019ab5e6 --- /dev/null +++ b/csharp/App/SinexcelCommunication/SalimaxDevice.cs @@ -0,0 +1,13 @@ +using InnovEnergy.Lib.Utils.Net; + +namespace InnovEnergy.App.SinexcelCommunication; + +public class SalimaxDevice : Ip4Address +{ + public required DeviceState DeviceState { get; init; } + + public override String ToString() => $"{base.ToString()} ({DeviceState})"; + + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), DeviceState); + +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/SinexcelCommunication.csproj b/csharp/App/SinexcelCommunication/SinexcelCommunication.csproj new file mode 100644 index 000000000..bb80ae8cb --- /dev/null +++ b/csharp/App/SinexcelCommunication/SinexcelCommunication.csproj @@ -0,0 +1,25 @@ + + + + InnovEnergy.App.SinexcelCommunication + + + + + + + + + + + + + + + + + + + + + diff --git a/csharp/App/SinexcelCommunication/SystemConfig/Config.cs b/csharp/App/SinexcelCommunication/SystemConfig/Config.cs new file mode 100644 index 000000000..db1d6d9c6 --- /dev/null +++ b/csharp/App/SinexcelCommunication/SystemConfig/Config.cs @@ -0,0 +1,93 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using InnovEnergy.Lib.Utils; +using static System.Text.Json.JsonSerializer; + + +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 Double MinSoc { get; set; } + public required Double GridSetPoint { get; set; } + + public required S3Config? S3 { get; set; } + + + private static String? LastSavedData { get; set; } + + public static Config Default => new() + { + MinSoc = 20, + GridSetPoint = 0, + S3 = new() + { + Bucket = "1-3e5b3069-214a-43ee-8d85-57d72000c19d", + Region = "sos-ch-dk-2", + Provider = "exo.io", + Key = "EXObb5a49acb1061781761895e7", + Secret = "sKhln0w8ii3ezZ1SJFF33yeDo8NWR1V4w2H0D4-350I", + ContentType = "text/plain; charset=utf-8" + } + }; + + public void Save(String? path = null) + { + var configFilePath = path ?? DefaultConfigFilePath; + + try + { + var jsonString = Serialize(this, JsonOptions); + + if (LastSavedData == jsonString) + return; + + LastSavedData = jsonString; + + File.WriteAllText(configFilePath, jsonString); + } + catch (Exception e) + { + $"Failed to write config file {configFilePath}\n{e}".WriteLine(); + throw; + } + } + + public static Config Load(String? path = null) + { + var configFilePath = path ?? DefaultConfigFilePath; + try + { + var jsonString = File.ReadAllText(configFilePath); + return Deserialize(jsonString)!; + } + catch (Exception e) + { + $"Failed to read config file {configFilePath}, using default config\n{e}".WriteLine(); + return Default; + } + } + + public static async Task LoadAsync(String? path = null) + { + var configFilePath = path ?? DefaultConfigFilePath; + try + { + var jsonString = await File.ReadAllTextAsync(configFilePath); + return Deserialize(jsonString)!; + } + catch (Exception e) + { + Console.WriteLine($"Couldn't read config file {configFilePath}, using default config"); + e.Message.WriteLine(); + return Default; + } + } +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/SystemConfig/S3Config.cs b/csharp/App/SinexcelCommunication/SystemConfig/S3Config.cs new file mode 100644 index 000000000..5b2668c83 --- /dev/null +++ b/csharp/App/SinexcelCommunication/SystemConfig/S3Config.cs @@ -0,0 +1,79 @@ +using System.Security.Cryptography; +using Flurl; +using Flurl.Http; +using InnovEnergy.Lib.Utils; +using static System.Text.Encoding; +using Convert = System.Convert; + +namespace InnovEnergy.App.SinexcelCommunication.SystemConfig; + +public record S3Config +{ + public required String Bucket { get; init; } + public required String Region { get; init; } + public required String Provider { get; init; } + public required String Key { get; init; } + public required String Secret { get; init; } + public required String ContentType { get; init; } + + private String Host => $"{Bucket}.{Region}.{Provider}"; + private String Url => $"https://{Host}"; + + public IFlurlRequest CreatePutRequest(String s3Path) => CreateRequest("PUT", s3Path); + public IFlurlRequest CreateGetRequest(String s3Path) => CreateRequest("GET", s3Path); + + private IFlurlRequest CreateRequest(String method, String s3Path) + { + var date = DateTime.UtcNow.ToString("r"); + var auth = CreateAuthorization(method, s3Path, date); + + return Url + .AppendPathSegment(s3Path) + .WithHeader("Host", Host) + .WithHeader("Date", date) + .WithHeader("Authorization", auth) + .AllowAnyHttpStatus(); + } + + private String CreateAuthorization(String method, + String s3Path, + String date) + { + return CreateAuthorization + ( + method : method, + bucket : Bucket, + s3Path : s3Path, + date : date, + s3Key : Key, + s3Secret : Secret, + contentType: ContentType + ); + } + + + + private static String CreateAuthorization(String method, + String bucket, + String s3Path, + String date, + String s3Key, + String s3Secret, + String contentType = "application/base64", + String md5Hash = "") + { + + contentType = "application/base64; charset=utf-8"; + //contentType = "text/plain; charset=utf-8"; //this to use when sending plain csv to S3 + + var payload = $"{method}\n{md5Hash}\n{contentType}\n{date}\n/{bucket.Trim('/')}/{s3Path.Trim('/')}"; + using var hmacSha1 = new HMACSHA1(UTF8.GetBytes(s3Secret)); + + var signature = UTF8 + .GetBytes(payload) + .Apply(hmacSha1.ComputeHash) + .Apply(Convert.ToBase64String); + + return $"AWS {s3Key}:{signature}"; + } +} \ No newline at end of file diff --git a/csharp/App/SinexcelCommunication/deploy.sh b/csharp/App/SinexcelCommunication/deploy.sh new file mode 100755 index 000000000..4fdb2df97 --- /dev/null +++ b/csharp/App/SinexcelCommunication/deploy.sh @@ -0,0 +1,32 @@ +#!/bin/bash +dotnet_version='net6.0' +salimax_ip="$1" +is_release="$2" # Pass --release if this is a real release +username='inesco' +root_password='Sodistore0918425' + +release_flag_file="./bin/Release/$dotnet_version/linux-arm64/publish/.release.flag" + +set -e + +echo -e "\n============================ Build ============================\n" + +dotnet publish \ + ./SinexcelCommunication.csproj \ + -p:PublishTrimmed=false \ + -c Release \ + -r linux-arm64 + +echo -e "\n============================ Deploy ============================\n" + +rsync -v \ + --exclude '*.pdb' \ + ./bin/Release/$dotnet_version/linux-arm64/publish/* \ + $username@"$salimax_ip":~/SodiStoreHome + + if [[ "$is_release" == "--release" ]]; then + echo -e "\n✅ Real release. Triggering sync to server..." + touch "$release_flag_file" + else + echo -e "\n Test build. Not syncing to main release server." + fi \ No newline at end of file diff --git a/csharp/Sinexcel 12K TL/DataType/AccreditedCountry.cs b/csharp/Sinexcel 12K TL/DataType/AccreditedCountry.cs new file mode 100644 index 000000000..827ea13d0 --- /dev/null +++ b/csharp/Sinexcel 12K TL/DataType/AccreditedCountry.cs @@ -0,0 +1,25 @@ +namespace InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType; + +public enum AccreditedCountry +{ +// 0x30C4 – Accredited Countries + Germany = 0, + UnitedKingdom = 1, + EuropeanStandard = 2, + SouthAfrica = 3, + Poland = 4, + Austria = 5, + Italy = 6, + Belgium = 7, + Spain = 8, + Israel = 9, + ThaiPEA = 10, + AustraliaA = 11, + AustraliaB = 12, + AustraliaC = 13, + NewZealand = 14, + Vietnam = 15, + ThailandMEA = 16, + China = 17, + NorthAmerica = 18 +} \ No newline at end of file diff --git a/csharp/Sinexcel 12K TL/DataType/BatteryType.cs b/csharp/Sinexcel 12K TL/DataType/BatteryType.cs new file mode 100644 index 000000000..5a761c7be --- /dev/null +++ b/csharp/Sinexcel 12K TL/DataType/BatteryType.cs @@ -0,0 +1,68 @@ +namespace InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType; + +// 0x3030 – Battery Access Method +public enum SinexcelBatteryAccessMethod +{ + Independent = 0, // 0: Independent access + Parallel = 1 // 1: Parallel access +} + +// 0x3032 – Meter Access Enable +public enum SinexcelMeterAccessEnable +{ + Disable = 0, + Enable = 1 // Default +} + +// 0x3034 / 0x3036 – Battery Enable +public enum SinexcelBatteryEnable +{ + Disable = 0, + Enable = 1 // Default +} + +// 0x3038 / 0x303A – PV Enable +public enum SinexcelPvEnable +{ + Disable = 0, + Enable = 1 // Default +} + +// 0x303C – Battery Type +public enum SinexcelBatteryType +{ + Lithium = 0, // Default + Supercapacitor = 1, + LeadBattery = 2, + SodiumIon = 3 +} + +// 0x304A – Active Charge / Discharge +public enum SinexcelActiveChargeDischarge +{ + Disable = 0, + ActiveCharge = 1, + ActiveDischarge = 2 +} + +// 0x305C – Enable Island Protection +public enum SinexcelIslandProtection +{ + Disable = 0, + Enable = 1 +} + +// 0x305E – PV Access Mode +public enum SinexcelPvAccessMode +{ + IndependentAccess = 0, + SemiParallel = 1, + FullParallel = 2 +} + +// 0x3CA2 – Battery Rating +public enum SinexcelBatteryRating +{ + HighVoltage = 0, // 0: High voltage battery + LowVoltage = 1 // 1: Low voltage battery +} diff --git a/csharp/Sinexcel 12K TL/DataType/GridSwitchMethode.cs b/csharp/Sinexcel 12K TL/DataType/GridSwitchMethode.cs new file mode 100644 index 000000000..308fd3a73 --- /dev/null +++ b/csharp/Sinexcel 12K TL/DataType/GridSwitchMethode.cs @@ -0,0 +1,8 @@ +namespace InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType; + +public enum GridSwitchMethod +{ + Automatic = 0, // 0: Automatic mode (default) + ManualOffGrid = 1, // 1: Manual off-grid mode + ManualOnGrid = 2 // 2: Manual on-grid mode +} \ No newline at end of file diff --git a/csharp/Sinexcel 12K TL/DataType/InputFrequency.cs b/csharp/Sinexcel 12K TL/DataType/InputFrequency.cs new file mode 100644 index 000000000..ded917fb4 --- /dev/null +++ b/csharp/Sinexcel 12K TL/DataType/InputFrequency.cs @@ -0,0 +1,7 @@ +namespace InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType; + +public enum InputFrequency +{ + Hz50 = 0, // 0: 50 Hz (default) + Hz60 = 1 // 1: 60 Hz +} \ No newline at end of file diff --git a/csharp/Sinexcel 12K TL/DataType/MachineType.cs b/csharp/Sinexcel 12K TL/DataType/MachineType.cs new file mode 100644 index 000000000..38b276fcd --- /dev/null +++ b/csharp/Sinexcel 12K TL/DataType/MachineType.cs @@ -0,0 +1,52 @@ +namespace InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType; + +public enum SinexcelMachineType +{ + ThreePhase = 0, // 0: Three phase + SinglePhase = 1, // 1: Single phase + SplitPhase = 2, // 2: Split phase + OffGrid = 3, // 3: Off-grid +} + +// ─────────────────────────────────────────────── +// 0x3010 – Enable On-Grid Unbalance Compensation +// ─────────────────────────────────────────────── +public enum SinexcelUnbalanceCompensation +{ + Disable = 0, // 0: Disabled + Enable = 1 // 1: Enabled (default) +} + +// ─────────────────────────────────────────────── +// 0x3012 – Temperature Drop Control +// ─────────────────────────────────────────────── +public enum SinexcelTemperatureDrop +{ + Disable = 0, // 0: Disabled + Enable = 1 // 1: Enabled (default) +} + +// ─────────────────────────────────────────────── +// 0x3014 – HVRT (High Voltage Ride-Through) +// ─────────────────────────────────────────────── +public enum SinexcelHvrt +{ + Disable = 0, // 0: Disabled (default) + Enable = 1 // 1: Enabled +} + +// ─────────────────────────────────────────────── +// 0x3016 – LVRT (Low Voltage Ride-Through) +// ─────────────────────────────────────────────── +public enum SinexcelLvrt +{ + Disable = 0, // 0: Disabled (default) + Enable = 1 // 1: Enabled +} + +// 0x30BE – Single or Parallel Machine +public enum SinexcelMachineMode +{ + Single = 0, // Default + Parallel = 1 +} \ No newline at end of file diff --git a/csharp/Sinexcel 12K TL/DataType/SinexcelModel.cs b/csharp/Sinexcel 12K TL/DataType/SinexcelModel.cs new file mode 100644 index 000000000..7cbabda75 --- /dev/null +++ b/csharp/Sinexcel 12K TL/DataType/SinexcelModel.cs @@ -0,0 +1,21 @@ +namespace InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType; + +public enum SinexcelModel +{ + T_TH = 0, // Isuna 3000T–20000T + S_SL = 1, // Isuna 3000S–6000S + U_PH = 2, + D_TH = 3, + D_TL = 4, + D_SH = 5, + D_SL = 6, + O_SOL = 7, + O_SOL_P = 8, + L_TH = 9, + O_SOU_G2 = 10, + O_SOL_G2 = 11, + O_SOL_G2B = 12, // Duplicate in documentation (0x200D: 11 & 12 both O-SOL-G2) + O_TOH = 13, + D_SL_S = 14, + D_TL_S = 15 +} diff --git a/csharp/Sinexcel 12K TL/DataType/ThreePhaseWireSystem.cs b/csharp/Sinexcel 12K TL/DataType/ThreePhaseWireSystem.cs new file mode 100644 index 000000000..855472f78 --- /dev/null +++ b/csharp/Sinexcel 12K TL/DataType/ThreePhaseWireSystem.cs @@ -0,0 +1,7 @@ +namespace InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType; + +public enum ThreePhaseWireSystem +{ + ThreePhaseFourWire = 0, // 0: Three-phase four-wire system (default) + ThreePhaseThreeWire = 1 // 1: Three-phase three-wire system +} \ No newline at end of file diff --git a/csharp/Sinexcel 12K TL/DataType/WorkingMode.cs b/csharp/Sinexcel 12K TL/DataType/WorkingMode.cs new file mode 100644 index 000000000..6eed09279 --- /dev/null +++ b/csharp/Sinexcel 12K TL/DataType/WorkingMode.cs @@ -0,0 +1,18 @@ +namespace InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType; + +public enum WorkingMode +{ + SpontaneousSelfUse = 0, // 0: Spontaneous self-use mode (default) + TimeChargeDischarge = 1, // 1: Time charge & discharge mode + TimeOfUsePowerPrice = 2, // 2: Time-of-use power price mode (future) + DisasterStandby = 3, // 3: Disaster standby mode + ManualControl = 4, // 4: Manual control mode (future) + PvPriorityCharging = 5, // 5: PV Priority Charging + PrioritySellElectricity = 6 // 6: Priority mode for selling electricity +} + +public enum MeterStatus +{ + Online = 1, + Offline = 0 +} \ No newline at end of file diff --git a/csharp/Sinexcel 12K TL/Sinexcel 12K TL.csproj b/csharp/Sinexcel 12K TL/Sinexcel 12K TL.csproj new file mode 100644 index 000000000..63ffc2cae --- /dev/null +++ b/csharp/Sinexcel 12K TL/Sinexcel 12K TL.csproj @@ -0,0 +1,16 @@ + + + + net6.0 + InnovEnergy.Lib.Devices.Sinexcel_12K_TL; + enable + enable + preview + + + + + + + + diff --git a/csharp/Sinexcel 12K TL/SinexcelDevice.cs b/csharp/Sinexcel 12K TL/SinexcelDevice.cs new file mode 100644 index 000000000..b5bea27f9 --- /dev/null +++ b/csharp/Sinexcel 12K TL/SinexcelDevice.cs @@ -0,0 +1,46 @@ +using System.IO.Ports; +using InnovEnergy.Lib.Protocols.Modbus.Channels; +using InnovEnergy.Lib.Protocols.Modbus.Clients; +using InnovEnergy.Lib.Protocols.Modbus.Slaves; +using InnovEnergy.Lib.Utils; + +namespace InnovEnergy.Lib.Devices.Sinexcel_12K_TL; + +public class SinexcelDevice : ModbusDevice +{ + private const Parity Parity = 0; //none + private const Int32 StopBits = 1; + private const Int32 BaudRate = 115200; + private const Int32 DataBits = 8; + + public Byte SlaveId { get; } + + public SinexcelDevice(String tty, Byte slaveId, SshHost host) : this + ( + channel: new RemoteSerialChannel(host, tty, BaudRate, Parity, DataBits, StopBits), + slaveId + ) + {} + + public SinexcelDevice(String tty, Byte slaveId, String? host = null) : this + ( + channel: host switch + { + null => new SerialPortChannel ( tty, BaudRate, Parity, DataBits, StopBits), + _ => new RemoteSerialChannel(host, tty, BaudRate, Parity, DataBits, StopBits) + }, + slaveId + ) + {} + + public SinexcelDevice(Channel channel, Byte slaveId) : this + ( + client: new ModbusRtuClient(channel, slaveId) + ) + {} + + public SinexcelDevice(ModbusClient client): base(client) + { + SlaveId = client.SlaveId; + } +} \ No newline at end of file diff --git a/csharp/Sinexcel 12K TL/SinexcelRecord.Api.cs b/csharp/Sinexcel 12K TL/SinexcelRecord.Api.cs new file mode 100644 index 000000000..7d6909d9e --- /dev/null +++ b/csharp/Sinexcel 12K TL/SinexcelRecord.Api.cs @@ -0,0 +1,610 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType; +using InnovEnergy.Lib.Units; +using InnovEnergy.Lib.Units.Power; + +namespace InnovEnergy.Lib.Devices.Sinexcel_12K_TL; + +public partial class SinexcelRecord +{ + + // ─────────────────────────────────────────────── + // Public API — Decoded Float Values + // ─────────────────────────────────────────────── + // ─────────────────────────────────────────────── + // Grid Measurements + // ─────────────────────────────────────────────── + public Voltage GridAPhaseVoltage => ConvertBitPatternToFloat(_gridAPhaseVoltage); + public Voltage GridBPhaseVoltage => UIntToFloat(_grid_B_Phase_Voltage); + public Voltage GridCPhaseVoltage => UIntToFloat(_grid_C_Phase_Voltage); + + public Voltage GridAbWireVoltage => UIntToFloat(_grid_AB_Wire_Voltage); + public Voltage GridBcWireVoltage => UIntToFloat(_grid_BC_Wire_Voltage); + public Voltage GridCaWireVoltage => UIntToFloat(_grid_CA_Wire_Voltage); + + public Frequency GridVoltageFrequency => UIntToFloat(_grid_Voltage_Frequency); + + public Current GridAPhaseCurrent => UIntToFloat(_grid_A_Phase_Current); + public Current GridBPhaseCurrent => UIntToFloat(_grid_B_Phase_Current); + public Current GridCPhaseCurrent => UIntToFloat(_grid_C_Phase_Current); + public Current GridNWireCurrent => UIntToFloat(_grid_N_Wire_Current); + + public ApparentPower GridAPhaseApparentPower => UIntToFloat(_gridAPhaseApparentPower); + public ApparentPower GridBPhaseApparentPower => UIntToFloat(_gridBPhaseApparentPower); + public ApparentPower GridCPhaseApparentPower => UIntToFloat(_gridCPhaseApparentPower); + + public ActivePower GridAPhaseActivePower => UIntToFloat(_gridAPhaseActivePower); + public ActivePower GridBPhaseActivePower => UIntToFloat(_gridBPhaseActivePower); + public ActivePower GridCPhaseActivePower => UIntToFloat(_gridCPhaseActivePower); + + public ReactivePower GridAPhaseReactivePower => UIntToFloat(_gridAPhaseReactivePower); + public ReactivePower GridBPhaseReactivePower => UIntToFloat(_gridBPhaseReactivePower); + public ReactivePower GridCPhaseReactivePower => UIntToFloat(_gridCPhaseReactivePower); + + // ─────────────────────────────────────────────── + // Load Measurements + // ─────────────────────────────────────────────── + public Voltage LoadAPhaseVoltage => UIntToFloat(_loadAPhaseVoltage); + public Voltage LoadBPhaseVoltage => UIntToFloat(_loadBPhaseVoltage); + public Voltage LoadCPhaseVoltage => UIntToFloat(_loadCPhaseVoltage); + + public Voltage LoadAbWireVoltage => UIntToFloat(_loadABWireVoltage); + public Voltage LoadBcWireVoltage => UIntToFloat(_loadBCWireVoltage); + public Voltage LoadCaWireVoltage => UIntToFloat(_loadCAWireVoltage); + + public Frequency LoadVoltageFrequency => UIntToFloat(_loadVoltageFrequency); + + public Current LoadAPhaseCurrent => UIntToFloat(_loadAPhaseCurrent); + public Current LoadBPhaseCurrent => UIntToFloat(_loadBPhaseCurrent); + public Current LoadCPhaseCurrent => UIntToFloat(_loadCPhaseCurrent); + public Current LoadNWireCurrent => UIntToFloat(_loadNWireCurrent); + + public ApparentPower LoadAPhaseApparentPower => UIntToFloat(_loadAPhaseApparentPower); + public ApparentPower LoadBPhaseApparentPower => UIntToFloat(_loadBPhaseApparentPower); + public ApparentPower LoadCPhaseApparentPower => UIntToFloat(_loadCPhaseApparentPower); + + public ActivePower LoadAPhaseActivePower => UIntToFloat(_loadAPhaseActivePower); + public ActivePower LoadBPhaseActivePower => UIntToFloat(_loadBPhaseActivePower); + public ActivePower LoadCPhaseActivePower => UIntToFloat(_loadCPhaseActivePower); + + public ReactivePower LoadAPhaseReactivePower => UIntToFloat(_loadAPhaseReactivePower); + public ReactivePower LoadBPhaseReactivePower => UIntToFloat(_loadBPhaseReactivePower); + public ReactivePower LoadCPhaseReactivePower => UIntToFloat(_loadCPhaseReactivePower); + + // ─────────────────────────────────────────────── + // Inverter Measurements + // ─────────────────────────────────────────────── + public Voltage InverterAPhaseVoltage => UIntToFloat(_inverterAPhaseVoltage); + public Voltage InverterBPhaseVoltage => UIntToFloat(_inverterBPhaseVoltage); + public Voltage InverterCPhaseVoltage => UIntToFloat(_inverterCPhaseVoltage); + + public Voltage InverterAbWireVoltage => UIntToFloat(_inverterABWireVoltage); + public Voltage InverterBcWireVoltage => UIntToFloat(_inverterBCWireVoltage); + public Voltage InverterCaWireVoltage => UIntToFloat(_inverterCAWireVoltage); + + public ActivePower InverterAPhaseActivePower => UIntToFloat(_inverterAPhaseActivePower); + public ActivePower InverterBPhaseActivePower => UIntToFloat(_inverterBPhaseActivePower); + public ActivePower InverterCPhaseActivePower => UIntToFloat(_inverterCPhaseActivePower); + + // ─────────────────────────────────────────────── + // DC/AC Temperature Sensors (°C) + // ─────────────────────────────────────────────── + public Temperature DcacTemperature1 => UIntToFloat(_dcacTemperature1); + public Temperature DcacTemperature2 => UIntToFloat(_dcacTemperature2); + public Temperature DcacTemperature3 => UIntToFloat(_dcacTemperature3); + public Temperature DcacTemperature4 => UIntToFloat(_dcacTemperature4); + public Temperature DcacTemperature5 => UIntToFloat(_dcacTemperature5); + + // ─────────────────────────────────────────────── + // Date / Time Information + // ─────────────────────────────────────────────── + public Single Year => UIntToFloat(_year); + public Single Month => UIntToFloat(_month); + public Single Day => UIntToFloat(_day); + public Single Hour => UIntToFloat(_hour); + public Single Minute => UIntToFloat(_minute); + public Single Second => UIntToFloat(_second); + + // ─────────────────────────────────────────────── + // Diesel Generator Measurements + // ─────────────────────────────────────────────── + public Voltage DieselGenAPhaseVoltage => ConvertBitPatternToFloat(_dieselGenAPhaseVoltage); + public Voltage DieselGenBPhaseVoltage => ConvertBitPatternToFloat(_dieselGenBPhaseVoltage); + public Voltage DieselGenCPhaseVoltage => ConvertBitPatternToFloat(_dieselGenCPhaseVoltage); + public Voltage DieselGenABWireVoltage => ConvertBitPatternToFloat(_dieselGenABWireVoltage); + public Voltage DieselGenBCWireVoltage => ConvertBitPatternToFloat(_dieselGenBCWireVoltage); + public Voltage DieselGenCAWireVoltage => ConvertBitPatternToFloat(_dieselGenCAWireVoltage); + + public Frequency DieselGenVoltageFrequency => ConvertBitPatternToFloat(_dieselGenVoltageFrequency); + + public Current DieselGenAPhaseCurrent => ConvertBitPatternToFloat(_dieselGenAPhaseCurrent); + public Current DieselGenBPhaseCurrent => ConvertBitPatternToFloat(_dieselGenBPhaseCurrent); + public Current DieselGenCPhaseCurrent => ConvertBitPatternToFloat(_dieselGenCPhaseCurrent); + public Current DieselGenNWireCurrent => ConvertBitPatternToFloat(_dieselGenNWireCurrent); + + public ApparentPower DieselGenAPhaseApparentPower => ConvertBitPatternToFloat(_dieselGenAPhaseApparentPower); + public ApparentPower DieselGenBPhaseApparentPower => ConvertBitPatternToFloat(_dieselGenBPhaseApparentPower); + public ApparentPower DieselGenCPhaseApparentPower => ConvertBitPatternToFloat(_dieselGenCPhaseApparentPower); + + public ActivePower DieselGenAPhaseActivePower => ConvertBitPatternToFloat(_dieselGenAPhaseActivePower); + public ActivePower DieselGenBPhaseActivePower => ConvertBitPatternToFloat(_dieselGenBPhaseActivePower); + public ActivePower DieselGenCPhaseActivePower => ConvertBitPatternToFloat(_dieselGenCPhaseActivePower); + + public ReactivePower DieselGenAPhaseReactivePower => ConvertBitPatternToFloat(_dieselGenAPhaseReactivePower); + public ReactivePower DieselGenBPhaseReactivePower => ConvertBitPatternToFloat(_dieselGenBPhaseReactivePower); + public ReactivePower DieselGenCPhaseReactivePower => ConvertBitPatternToFloat(_dieselGenCPhaseReactivePower); + + // ─────────────────────────────────────────────── + // Photovoltaic and Battery Measurements + // ─────────────────────────────────────────────── + public Voltage PvVoltage1 => ConvertBitPatternToFloat(_pvVoltage1); + public Current PvCurrent1 => ConvertBitPatternToFloat(_pvCurrent1); + public ActivePower PvPower1 => ConvertBitPatternToFloat(_pvPower1); + public Voltage PvVoltage2 => ConvertBitPatternToFloat(_pvVoltage2); + public Current PvCurrent2 => ConvertBitPatternToFloat(_pvCurrent2); + public ActivePower PvPower2 => ConvertBitPatternToFloat(_pvPower2); + + public Voltage BatteryVoltage1 => ConvertBitPatternToFloat(_batteryVoltage1); + public Current BatteryCurrent1 => ConvertBitPatternToFloat(_batteryCurrent1); + public ActivePower BatteryPower1 => ConvertBitPatternToFloat(_batteryPower1); + public Percent BatterySoc1 => ConvertBitPatternToFloat(_batterySoc1); + public float BatteryFullLoadDuration1 => ConvertBitPatternToFloat(_batteryFullLoadDuration1); + + public Voltage BatteryVoltage2 => ConvertBitPatternToFloat(_batteryVoltage2); + public Current BatteryCurrent2 => ConvertBitPatternToFloat(_batteryCurrent2); + public ActivePower BatteryPower2 => ConvertBitPatternToFloat(_batteryPower2); + public Percent BatterySoc2 => ConvertBitPatternToFloat(_batterySoc2); + public float BatteryFullLoadDuration2 => ConvertBitPatternToFloat(_batteryFullLoadDuration2); + + public Temperature DcdcTemperature1 => ConvertBitPatternToFloat(_dcdcTemperature1); + public Temperature DcdcTemperature2 => ConvertBitPatternToFloat(_dcdcTemperature2); + public Temperature DcdcTemperature3 => ConvertBitPatternToFloat(_dcdcTemperature3); + public Temperature DcdcTemperature4 => ConvertBitPatternToFloat(_dcdcTemperature4); + public Temperature DcdcTemperature5 => ConvertBitPatternToFloat(_dcdcTemperature5); + + // ─────────────────────────────────────────────── + // Energy and Power Summary + // ─────────────────────────────────────────────── + public Energy ElectricityPurchased => ConvertBitPatternToFloat(_electricityPurchased); + public Energy ElectricityFed => ConvertBitPatternToFloat(_electricityFed); + public Energy SelfGeneratedElectricity => ConvertBitPatternToFloat(_selfGeneratedElectricity); + public Energy BatteryCharge => ConvertBitPatternToFloat(_batteryCharge); + public Energy BatteryDischarge => ConvertBitPatternToFloat(_batteryDischarge); + public Energy LoadPowerConsumption => ConvertBitPatternToFloat(_loadPowerConsumption); + public Energy DailySelfGeneratedElectricity => ConvertBitPatternToFloat(_dailySelfGeneratedElectricity); + public Energy DailyElectricityPurchased => ConvertBitPatternToFloat(_dailyElectricityPurchased); + public Energy DailyElectricityFed => ConvertBitPatternToFloat(_dailyElectricityFed); + public Energy DailyBatteryCharge => ConvertBitPatternToFloat(_dailyBatteryCharge); + public Energy DailyBatteryDischarge => ConvertBitPatternToFloat(_dailyBatteryDischarge); + public Energy DailyLoadPowerConsumption => ConvertBitPatternToFloat(_dailyLoadPowerConsumption); + public ActivePower TotalPhotovoltaicPower => ConvertBitPatternToFloat(_totalPhotovoltaicPower); + public ActivePower TotalBatteryPower => ConvertBitPatternToFloat(_totalBatteryPower); + public ActivePower TotalLoadPower => ConvertBitPatternToFloat(_totalLoadPower); + public ActivePower TotalGridPower => ConvertBitPatternToFloat(_totalGridPower); + public ActivePower ImportantLoadTotalPower => ConvertBitPatternToFloat(_importantLoadTotalPower); + public ActivePower GeneralLoadTotalPower => ConvertBitPatternToFloat(_generalLoadTotalPower); + public Voltage Pv3Voltage => ConvertBitPatternToFloat(_pv3Voltage); + public Current Pv3Current => ConvertBitPatternToFloat(_pv3Current); + public ActivePower Pv3Power => ConvertBitPatternToFloat(_pv3Power); + public Voltage Pv4Voltage => ConvertBitPatternToFloat(_pv4Voltage); + public Current Pv4Current => ConvertBitPatternToFloat(_pv4Current); + public ActivePower Pv4Power => ConvertBitPatternToFloat(_pv4Power); + public ActivePower GeneratorTotalPower => ConvertBitPatternToFloat(_generatorTotalPower); + + // ─────────────────────────────────────────────── + // Manufacturer Information & Software Versions + // ─────────────────────────────────────────────── + public UInt16 ProtocolVersionNo => _protocolVersionNo; // 0x2000 + public UInt16 DcacSoftwareVersionNo => _dcacSoftwareVersionNo; // 0x2001 + public UInt16 DcdcSoftwareVersionNo => _dcdcSoftwareVersionNo; // 0x2002 + public UInt16 ArmSoftwareVersionNo => _armSoftwareVersionNo; // 0x2003 + public UInt16 CpldVersion => _cpldVersion; // 0x2004 + public UInt16 AfciSoftwareVersionNo => _afciSoftwareVersionNo; // 0x2005 + public SinexcelMachineType MachineType => (SinexcelMachineType)_machineType; // 0x2006 + + // ─────────────────────────────────────────────── + // Device Info & Model + // ─────────────────────────────────────────────── + public ApparentPower RatedPowerKva => _ratedPower / 100f; // 0x2008 (value / 100) + public SinexcelModel Model => (SinexcelModel)_model; // 0x200D + + public ThreePhaseWireSystem ThreePhaseWireSystem + { + get => (ThreePhaseWireSystem)_threePhaseWireSystem; + set => _threePhaseWireSystem = (UInt32)value; + } + + public InputFrequency InputFrequencyClass + { + get => (InputFrequency)_inputFrequencyClass; + set => _inputFrequencyClass = (UInt32)value; + } + + public WorkingMode WorkingMode + { + get => (WorkingMode)ConvertBitPatternToFloat(_workingMode); + set => _workingMode = (UInt32)value; + } + + public GridSwitchMethod GridSwitchMethod + { + get => (GridSwitchMethod)_methodSwitchMode; + set => _methodSwitchMode = (UInt32)value; + } + + // ─────────────────────────────────────────────── + // Inverter Control Configuration + // ─────────────────────────────────────────────── + public SinexcelUnbalanceCompensation EnableOnGridUnbalanceCompensation + { + get => (SinexcelUnbalanceCompensation)_enableOnGridUnbalanceCompensation; + set => _enableOnGridUnbalanceCompensation = (UInt32)value; + } + + public SinexcelTemperatureDrop TemperatureDrop + { + get => (SinexcelTemperatureDrop)ConvertBitPatternToFloat(_temperatureDrop); + set => _temperatureDrop = (UInt32)value; + } + + public SinexcelHvrt Hvrt + { + get => (SinexcelHvrt)_hvrt; + set => _hvrt = (UInt32)value; + } + + public SinexcelLvrt Lvrt + { + get => (SinexcelLvrt)_lvrt; + set => _lvrt = (UInt32)value; + } + +// Fan Gear — numeric [0.5 ~ 1.0], default 1.0 + public float FanGear + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_fanGear)); + set => _fanGear = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + // ─────────────────────────────────────────────── + // Battery Configuration + // ─────────────────────────────────────────────── + public SinexcelBatteryAccessMethod BatteryAccessMethod + { + get => (SinexcelBatteryAccessMethod)_batteryAccessMethod; + set => _batteryAccessMethod = (UInt32)value; + } + + public SinexcelMeterAccessEnable MeterAccessEnable + { + get => (SinexcelMeterAccessEnable)ConvertBitPatternToFloat(_meterAccessEnable); + set => _meterAccessEnable = (UInt32)value; + } + + public SinexcelBatteryEnable EnableBattery1 + { + get => (SinexcelBatteryEnable)ConvertBitPatternToFloat(_enableBattery1); + set => _enableBattery1 = (UInt32)value; + } + + public SinexcelBatteryEnable EnableBattery2 + { + get => (SinexcelBatteryEnable)ConvertBitPatternToFloat(_enableBattery2); + set => _enableBattery2 = (UInt32)value; + } + + public SinexcelPvEnable EnablePv1 + { + get => (SinexcelPvEnable)ConvertBitPatternToFloat(_enablePv1); + set => _enablePv1 = (UInt32)value; + } + + public SinexcelPvEnable EnablePv2 + { + get => (SinexcelPvEnable)ConvertBitPatternToFloat(_enablePv2); + set => _enablePv2 = (UInt32)value; + } + + public SinexcelBatteryType BatteryType + { + get => (SinexcelBatteryType)_batteryType; + set => _batteryType = (UInt32)value; + } + + public float BatteryCapacity1 + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCapacity1)); + set => _batteryCapacity1 = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public float MaxChargingCurrentBattery1 + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_maxChargingCurrentBattery1)); + set => _maxChargingCurrentBattery1 = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public float MaxDischargingCurrentBattery1 + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_maxDischargingCurrentBattery1)); + set => _maxDischargingCurrentBattery1 = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public float RatedBatteryVoltage1 + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_ratedBatteryVoltage1)); + set => _ratedBatteryVoltage1 = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public float MinSocBattery1 + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_minSocBattery1)); + set => _minSocBattery1 = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public float SetValueBattery1 + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_setValueBattery1)); + set => _setValueBattery1 = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public SinexcelActiveChargeDischarge ActiveChargeDischarge + { + get => (SinexcelActiveChargeDischarge)ConvertBitPatternToFloat(_activeChargeDischarge); + set => _activeChargeDischarge = (UInt32)value; + } + + public float ActiveChargeDischargePower + { + get => ConvertBitPatternToFloat(_activeChargeDischargePower); + set => _activeChargeDischargePower = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + // ─────────────────────────────────────────────── + // Protection & PV Mode + // ─────────────────────────────────────────────── + public SinexcelIslandProtection EnableIslandProtection + { + get => (SinexcelIslandProtection)_enableIslandProtection; + set => _enableIslandProtection = (UInt32)value; + } + + public SinexcelPvAccessMode PvAccessMode + { + get => (SinexcelPvAccessMode)_pvAccessMode; + set => _pvAccessMode = (UInt32)value; + } + + // ─────────────────────────────────────────────── + // System-Level Parameters + // ─────────────────────────────────────────────── + public float OutputVoltageAdjustmentFactor + { + get => BitConverter.Int32BitsToSingle(unchecked((int)_outputVoltageAdjustmentFactor)); + set => _outputVoltageAdjustmentFactor = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public float SetValueBatteryUndervoltage1 + { + get => BitConverter.Int32BitsToSingle(unchecked((int)_setValueBatteryUndervoltage1)); + set => _setValueBatteryUndervoltage1 = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public float InverterPowerLimit + { + get => BitConverter.Int32BitsToSingle(unchecked((int)_inverterPowerLimit)); + set => _inverterPowerLimit = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + // ─────────────────────────────────────────────── + // Battery 2 Parameters + // ─────────────────────────────────────────────── + public float Battery2Capacity + { + get => BitConverter.Int32BitsToSingle(unchecked((int)_battery2Capacity)); + set => _battery2Capacity = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public float MaxChargingCurrentBattery2 + { + get => BitConverter.Int32BitsToSingle(unchecked((int)_maxChargingCurrentBattery2)); + set => _maxChargingCurrentBattery2 = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public float MaxDischargingCurrentBattery2 + { + get => BitConverter.Int32BitsToSingle(unchecked((int)_maxDischargingCurrentBattery2)); + set => _maxDischargingCurrentBattery2 = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public float Battery2RatedVoltage + { + get => BitConverter.Int32BitsToSingle(unchecked((int)_battery2RatedVoltage)); + set => _battery2RatedVoltage = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public float Battery2MinSoc + { + get => BitConverter.Int32BitsToSingle(unchecked((int)_battery2MinSoc)); + set => _battery2MinSoc = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public float Battery2OverVoltageSetting + { + get => BitConverter.Int32BitsToSingle(unchecked((int)_battery2OverVoltageSetting)); + set => _battery2OverVoltageSetting = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public float Battery2UnderVoltageSetpoint + { + get => BitConverter.Int32BitsToSingle(unchecked((int)_battery2UnderVoltageSetpoint)); + set => _battery2UnderVoltageSetpoint = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + // ─────────────────────────────────────────────── + // Parallel / System Settings + // ─────────────────────────────────────────────── + public SinexcelMachineMode MachineMode + { + get => (SinexcelMachineMode)ConvertBitPatternToFloat(_singleOrParallelMachine); + set => _singleOrParallelMachine = (UInt32)value; + } + + public UInt32 NumberOfSystemModules + { + get => (UInt32)ConvertBitPatternToFloat(_numberOfSystemModules); + set => _numberOfSystemModules = value; + } + + public UInt32 ParallelModuleMachineNumber + { + get => (UInt32)ConvertBitPatternToFloat(_parallelModuleMachineNumber); + set => _parallelModuleMachineNumber = value; + } + + public AccreditedCountry AccreditedCountry + { + get => (AccreditedCountry)ConvertBitPatternToFloat(_accreditedCountries); + set => _accreditedCountries = (UInt32)value; + } + + // ─────────────────────────────────────────────── + // Control Commands + // ─────────────────────────────────────────────── + public float PowerOn + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_powerOn)); + set => _powerOn = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public float PowerOff + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_powerOff)); + set => _powerOff = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public float FaultClearing + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_faultClearing)); + set => _faultClearing = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + // ─────────────────────────────────────────────── + // Meter & Battery Control + // ─────────────────────────────────────────────── + public float MeterReverseManualDetection + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_meterReverseManualDetection)); + set => _meterReverseManualDetection = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public Voltage BatteryVoltage + { + get => (Voltage)ConvertBitPatternToFloat(_batteryVoltage); + set => _batteryVoltage = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public SinexcelBatteryRating BatteryRating + { + get => (SinexcelBatteryRating)ConvertBitPatternToFloat(_batteryRating); + set => _batteryRating = (UInt32)value; + } + + public float Battery1Activation + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_battery1Activation)); + set => _battery1Activation = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + public float Battery2Activation + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_battery2Activation)); + set => _battery2Activation = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + } + + // ─────────────────────────────────────────────── + // Electric Meter Operating State + // ─────────────────────────────────────────────── + public MeterStatus ElectricMeterOperatingState + { + get => (MeterStatus)BitConverter.Int32BitsToSingle(unchecked((Int32)_electricMeterOperatingState)); // 0 = Offline, 1 = Online + } + + // ─────────────────────────────────────────────── + // Electric Meter Voltages (V) + // ─────────────────────────────────────────────── + public Voltage ElectricMeterAPhaseVoltage + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_electricMeterAPhaseVoltage)); + } + + public Voltage ElectricMeterBPhaseVoltage + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_electricMeterBPhaseVoltage)); + } + + public Voltage ElectricMeterCPhaseVoltage + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_electricMeterCPhaseVoltage)); + } + + // ─────────────────────────────────────────────── + // Electric Meter Currents (A) + // ─────────────────────────────────────────────── + public Current ElectricMeterAPhaseCurrent + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_electricMeterAPhaseCurrent)); + } + + public Current ElectricMeterBPhaseCurrent + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_electricMeterBPhaseCurrent)); + } + + public Current ElectricMeterCPhaseCurrent + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_electricMeterCPhaseCurrent)); + } + + // ─────────────────────────────────────────────── + // Electric Meter Active Power (kW) + // ─────────────────────────────────────────────── + public ActivePower ElectricMeterAPhaseActivePower + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_electricMeterAPhaseActivePower)); + } + + public ActivePower ElectricMeterBPhaseActivePower + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_electricMeterBPhaseActivePower)); + } + + public ActivePower ElectricMeterCPhaseActivePower + { + get => BitConverter.Int32BitsToSingle(unchecked((Int32)_electricMeterCPhaseActivePower)); + } + + + // ─────────────────────────────────────────────── + // Conversion methodes + // ─────────────────────────────────────────────── + private static Single IntToFloat(Int32 intValue) + { + byte[] intBytes = BitConverter.GetBytes(intValue); + float f = BitConverter.ToSingle(intBytes, 0); + return f; + } + + private static Single UIntToFloat(UInt32 uintValue) + { + byte[] uintBytes = BitConverter.GetBytes(uintValue); + float f = BitConverter.ToSingle(uintBytes, 0); + return f; + } + public static float ConvertBitPatternToFloat(uint rawValue) + { + byte[] bytes = BitConverter.GetBytes(rawValue); + return BitConverter.ToSingle(bytes, 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 new file mode 100644 index 000000000..2fe68ae1d --- /dev/null +++ b/csharp/Sinexcel 12K TL/SinexcelRecord.Modbus.cs @@ -0,0 +1,313 @@ +using System.Diagnostics.CodeAnalysis; +using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; +using InnovEnergy.Lib.Units; +using InnovEnergy.Lib.Units.Power; +// ReSharper disable InconsistentNaming +#pragma warning disable CS0169 // Field is never used +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + +namespace InnovEnergy.Lib.Devices.Sinexcel_12K_TL; + +[BigEndian] +public partial class SinexcelRecord +{ + /****************************** Input registers ****************************/ + // Voltages + [HoldingRegister(4096)] public UInt32 _gridAPhaseVoltage; // 0x1000 + [HoldingRegister(4098)] private UInt32 _grid_B_Phase_Voltage; // 0x1002 + [HoldingRegister(4100)] private UInt32 _grid_C_Phase_Voltage; // 0x1004 + + // Line-to-line Voltages (V) + [HoldingRegister(4102)] private UInt32 _grid_AB_Wire_Voltage; // 0x1006 + [HoldingRegister(4104)] private UInt32 _grid_BC_Wire_Voltage; // 0x1008 + [HoldingRegister(4106)] private UInt32 _grid_CA_Wire_Voltage; // 0x100A + + // Frequency (Hz) + [HoldingRegister(4114)] private UInt32 _grid_Voltage_Frequency; // 0x1012 + + // Currents (A) + [HoldingRegister(4116)] private UInt32 _grid_A_Phase_Current; // 0x1014 + [HoldingRegister(4118)] private UInt32 _grid_B_Phase_Current; // 0x1016 + [HoldingRegister(4120)] private UInt32 _grid_C_Phase_Current; // 0x1018 + [HoldingRegister(4122)] private UInt32 _grid_N_Wire_Current; // 0x101A*/ + + // ─────────────────────────────────────────────── + // Power Measurements + // ─────────────────────────────────────────────── + + // Apparent Power (kVA) + [HoldingRegister(4136)] private UInt32 _gridAPhaseApparentPower; // 0x1028 + [HoldingRegister(4138)] private UInt32 _gridBPhaseApparentPower; // 0x102A + [HoldingRegister(4140)] private UInt32 _gridCPhaseApparentPower; // 0x102C + + // Active Power (kW) + [HoldingRegister(4142)] private UInt32 _gridAPhaseActivePower; // 0x102E + [HoldingRegister(4144)] private UInt32 _gridBPhaseActivePower; // 0x1030 + [HoldingRegister(4146)] private UInt32 _gridCPhaseActivePower; // 0x1032 + + // Reactive Power (kVar) + [HoldingRegister(4148)] private UInt32 _gridAPhaseReactivePower; // 0x1034 + [HoldingRegister(4150)] private UInt32 _gridBPhaseReactivePower; // 0x1036 + [HoldingRegister(4152)] private UInt32 _gridCPhaseReactivePower; // 0x1038 + + // ─────────────────────────────────────────────── + // Load Voltages (V) + // ─────────────────────────────────────────────── + [HoldingRegister(4166)] private UInt32 _loadAPhaseVoltage; // 0x1046 + [HoldingRegister(4168)] private UInt32 _loadBPhaseVoltage; // 0x1048 + [HoldingRegister(4170)] private UInt32 _loadCPhaseVoltage; // 0x104A + [HoldingRegister(4172)] private UInt32 _loadABWireVoltage; // 0x104C + [HoldingRegister(4174)] private UInt32 _loadBCWireVoltage; // 0x104E + [HoldingRegister(4176)] private UInt32 _loadCAWireVoltage; // 0x1050 + [HoldingRegister(4184)] private UInt32 _loadVoltageFrequency; // 0x1058 + + // ─────────────────────────────────────────────── + // Load Currents (A) + // ─────────────────────────────────────────────── + [HoldingRegister(4186)] private UInt32 _loadAPhaseCurrent; // 0x105A + [HoldingRegister(4188)] private UInt32 _loadBPhaseCurrent; // 0x105C + [HoldingRegister(4190)] private UInt32 _loadCPhaseCurrent; // 0x105E + [HoldingRegister(4192)] private UInt32 _loadNWireCurrent; // 0x1060 + + // ─────────────────────────────────────────────── + // Load Apparent Power (kVA) + // ─────────────────────────────────────────────── + [HoldingRegister(4206)] private UInt32 _loadAPhaseApparentPower; // 0x106E + [HoldingRegister(4208)] private UInt32 _loadBPhaseApparentPower; // 0x1070 + [HoldingRegister(4210)] private UInt32 _loadCPhaseApparentPower; // 0x1072 + + // ─────────────────────────────────────────────── + // Load Active Power (kW) + // ─────────────────────────────────────────────── + [HoldingRegister(4212)] private UInt32 _loadAPhaseActivePower; // 0x1074 + [HoldingRegister(4214)] private UInt32 _loadBPhaseActivePower; // 0x1076 + [HoldingRegister(4216)] private UInt32 _loadCPhaseActivePower; // 0x1078 + + // ─────────────────────────────────────────────── + // Load Reactive Power (kVar) + // ─────────────────────────────────────────────── + [HoldingRegister(4218)] private UInt32 _loadAPhaseReactivePower; // 0x107A + [HoldingRegister(4220)] private UInt32 _loadBPhaseReactivePower; // 0x107C + [HoldingRegister(4222)] private UInt32 _loadCPhaseReactivePower; // 0x107E + + // ─────────────────────────────────────────────── + // Inverter Voltages (V) + // ─────────────────────────────────────────────── + [HoldingRegister(4242)] private UInt32 _inverterAPhaseVoltage; // 0x1092 + [HoldingRegister(4244)] private UInt32 _inverterBPhaseVoltage; // 0x1094 + [HoldingRegister(4246)] private UInt32 _inverterCPhaseVoltage; // 0x1096 + [HoldingRegister(4248)] private UInt32 _inverterABWireVoltage; // 0x1098 + [HoldingRegister(4250)] private UInt32 _inverterBCWireVoltage; // 0x109A + [HoldingRegister(4252)] private UInt32 _inverterCAWireVoltage; // 0x109C + + // ─────────────────────────────────────────────── + // Inverter Active Power (kW) + // ─────────────────────────────────────────────── + [HoldingRegister(4288)] private UInt32 _inverterAPhaseActivePower; // 0x10C0 + [HoldingRegister(4290)] private UInt32 _inverterBPhaseActivePower; // 0x10C2 + [HoldingRegister(4292)] private UInt32 _inverterCPhaseActivePower; // 0x10C4 + + // ─────────────────────────────────────────────── + // DC/AC Temperatures (°C) + // ─────────────────────────────────────────────── + [HoldingRegister(4318)] private UInt32 _dcacTemperature1; // 0x10DE + [HoldingRegister(4320)] private UInt32 _dcacTemperature2; // 0x10E0 + [HoldingRegister(4322)] private UInt32 _dcacTemperature3; // 0x10E2 + [HoldingRegister(4324)] private UInt32 _dcacTemperature4; // 0x10E4 + [HoldingRegister(4326)] private UInt32 _dcacTemperature5; // 0x10E6 + + // ─────────────────────────────────────────────── + // 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 + + // ─────────────────────────────────────────────── + // Diesel Generator Measurements + // ─────────────────────────────────────────────── + [HoldingRegister(4362)] private UInt32 _dieselGenAPhaseVoltage; // 0x110A + [HoldingRegister(4364)] private UInt32 _dieselGenBPhaseVoltage; // 0x110C + [HoldingRegister(4366)] private UInt32 _dieselGenCPhaseVoltage; // 0x110E + [HoldingRegister(4368)] private UInt32 _dieselGenABWireVoltage; // 0x1110 + [HoldingRegister(4370)] private UInt32 _dieselGenBCWireVoltage; // 0x1112 + [HoldingRegister(4372)] private UInt32 _dieselGenCAWireVoltage; // 0x1114 + [HoldingRegister(4380)] private UInt32 _dieselGenVoltageFrequency; // 0x111C + [HoldingRegister(4382)] private UInt32 _dieselGenAPhaseCurrent; // 0x111E + [HoldingRegister(4384)] private UInt32 _dieselGenBPhaseCurrent; // 0x1120 + [HoldingRegister(4386)] private UInt32 _dieselGenCPhaseCurrent; // 0x1122 + [HoldingRegister(4388)] private UInt32 _dieselGenNWireCurrent; // 0x1124 + [HoldingRegister(4402)] private UInt32 _dieselGenAPhaseApparentPower; // 0x1132 + [HoldingRegister(4404)] private UInt32 _dieselGenBPhaseApparentPower; // 0x1134 + [HoldingRegister(4406)] private UInt32 _dieselGenCPhaseApparentPower; // 0x1136 + [HoldingRegister(4408)] private UInt32 _dieselGenAPhaseActivePower; // 0x1138 + [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 + + // ─────────────────────────────────────────────── + // Photovoltaic and Battery Measurements + // ─────────────────────────────────────────────── + [HoldingRegister(4608)] private UInt32 _pvVoltage1; // 0x1200 + [HoldingRegister(4610)] private UInt32 _pvCurrent1; // 0x1202 + [HoldingRegister(4612)] private UInt32 _pvPower1; // 0x1204 + [HoldingRegister(4614)] private UInt32 _pvVoltage2; // 0x1206 + [HoldingRegister(4616)] private UInt32 _pvCurrent2; // 0x1208 + [HoldingRegister(4618)] private UInt32 _pvPower2; // 0x120A + + [HoldingRegister(4620)] private UInt32 _batteryVoltage1; // 0x120C + [HoldingRegister(4622)] private UInt32 _batteryCurrent1; // 0x120E + [HoldingRegister(4624)] private UInt32 _batteryPower1; // 0x1210 + [HoldingRegister(4626)] private UInt32 _batterySoc1; // 0x1212 + [HoldingRegister(4628)] private UInt32 _batteryFullLoadDuration1; // 0x1214 + + [HoldingRegister(4630)] private UInt32 _batteryVoltage2; // 0x1216 + [HoldingRegister(4632)] private UInt32 _batteryCurrent2; // 0x1218 + [HoldingRegister(4634)] private UInt32 _batteryPower2; // 0x121A + [HoldingRegister(4636)] private UInt32 _batterySoc2; // 0x121C + [HoldingRegister(4638)] private UInt32 _batteryFullLoadDuration2; // 0x121E + + [HoldingRegister(4640)] private UInt32 _dcdcTemperature1; // 0x1220 + [HoldingRegister(4642)] private UInt32 _dcdcTemperature2; // 0x1222 + [HoldingRegister(4644)] private UInt32 _dcdcTemperature3; // 0x1224 + [HoldingRegister(4646)] private UInt32 _dcdcTemperature4; // 0x1226 + [HoldingRegister(4648)] private UInt32 _dcdcTemperature5; // 0x1228 + + // ─────────────────────────────────────────────── + // Energy and Power Summary + // ─────────────────────────────────────────────── + [HoldingRegister(4672)] private UInt32 _electricityPurchased; // 0x1240 + [HoldingRegister(4674)] private UInt32 _electricityFed; // 0x1242 + [HoldingRegister(4700)] private UInt32 _selfGeneratedElectricity; // 0x125C + [HoldingRegister(4702)] private UInt32 _batteryCharge; // 0x125E + [HoldingRegister(4704)] private UInt32 _batteryDischarge; // 0x1260 + [HoldingRegister(4706)] private UInt32 _loadPowerConsumption; // 0x1262 + [HoldingRegister(4708)] private UInt32 _dailySelfGeneratedElectricity; // 0x1264 + [HoldingRegister(4710)] private UInt32 _dailyElectricityPurchased; // 0x1266 + [HoldingRegister(4712)] private UInt32 _dailyElectricityFed; // 0x1268 + [HoldingRegister(4714)] private UInt32 _dailyBatteryCharge; // 0x126A + [HoldingRegister(4716)] private UInt32 _dailyBatteryDischarge; // 0x126C + [HoldingRegister(4718)] private UInt32 _dailyLoadPowerConsumption; // 0x126E + [HoldingRegister(4720)] private UInt32 _totalPhotovoltaicPower; // 0x1270 + [HoldingRegister(4722)] private UInt32 _totalBatteryPower; // 0x1272 + [HoldingRegister(4724)] private UInt32 _totalLoadPower; // 0x1274 + [HoldingRegister(4726)] private UInt32 _totalGridPower; // 0x1276 + [HoldingRegister(4728)] private UInt32 _importantLoadTotalPower; // 0x1278 + [HoldingRegister(4730)] private UInt32 _generalLoadTotalPower; // 0x127A + [HoldingRegister(4732)] private UInt32 _pv3Voltage; // 0x127C + [HoldingRegister(4734)] private UInt32 _pv3Current; // 0x127E + [HoldingRegister(4736)] private UInt32 _pv3Power; // 0x1280 + [HoldingRegister(4738)] private UInt32 _pv4Voltage; // 0x1282 + [HoldingRegister(4740)] private UInt32 _pv4Current; // 0x1284 + [HoldingRegister(4742)] private UInt32 _pv4Power; // 0x1286 + [HoldingRegister(4744)] private UInt32 _generatorTotalPower; // 0x128C + // ─────────────────────────────────────────────── + // Manufacturer Information & Software Versions + // ─────────────────────────────────────────────── + [HoldingRegister(8192)] private UInt16 _protocolVersionNo; // 0x2000 + [HoldingRegister(8193)] private UInt16 _dcacSoftwareVersionNo; // 0x2001 + [HoldingRegister(8194)] private UInt16 _dcdcSoftwareVersionNo; // 0x2002 + [HoldingRegister(8195)] private UInt16 _armSoftwareVersionNo; // 0x2003 + [HoldingRegister(8196)] private UInt16 _cpldVersion; // 0x2004 + [HoldingRegister(8197)] private UInt16 _afciSoftwareVersionNo; // 0x2005 + [HoldingRegister(8198)] private UInt16 _machineType; // 0x2006 + // ─────────────────────────────────────────────── + // Device Info & Model + // ─────────────────────────────────────────────── + [HoldingRegister(8199)] private UInt16 _remain; // 0x2007 + [HoldingRegister(8200)] private UInt16 _ratedPower; // 0x2008 (value / 100 = kVA) + [HoldingRegister(8205)] private UInt16 _model; // 0x200D + + // ─────────────────────────────────────────────── + // System configuration / operation registers + // ─────────────────────────────────────────────── + [HoldingRegister(12294, writable: true)] private UInt32 _threePhaseWireSystem; // 0x3006 + [HoldingRegister(12296, writable: true)] private UInt32 _remainnotused; // 0x3008 + [HoldingRegister(12298, writable: true)] private UInt32 _inputFrequencyClass; // 0x300A + [HoldingRegister(12300, writable: true)] private UInt32 _workingMode; // 0x300C + [HoldingRegister(12302, writable: true)] private UInt32 _methodSwitchMode; // 0x300E + // ─────────────────────────────────────────────── + // Inverter Control and Protection Configuration + // ─────────────────────────────────────────────── + [HoldingRegister(12304, writable: true)] private UInt32 _enableOnGridUnbalanceCompensation; // 0x3010 + [HoldingRegister(12306, writable: true)] private UInt32 _temperatureDrop; // 0x3012 + [HoldingRegister(12308, writable: true)] private UInt32 _hvrt; // 0x3014 + [HoldingRegister(12310, writable: true)] private UInt32 _lvrt; // 0x3016 + [HoldingRegister(12312, writable: true)] private UInt32 _fanGear; // 0x3018 + + // ─────────────────────────────────────────────── + // Battery & PV Configuration + // ─────────────────────────────────────────────── + [HoldingRegister(12336, writable: true)] private UInt32 _batteryAccessMethod; // 0x3030 + [HoldingRegister(12338, writable: true)] private UInt32 _meterAccessEnable; // 0x3032 + [HoldingRegister(12340, writable: true)] private UInt32 _enableBattery1; // 0x3034 + [HoldingRegister(12342, writable: true)] private UInt32 _enableBattery2; // 0x3036 + [HoldingRegister(12344, writable: true)] private UInt32 _enablePv1; // 0x3038 + [HoldingRegister(12346, writable: true)] private UInt32 _enablePv2; // 0x303A + [HoldingRegister(12348, writable: true)] private UInt32 _batteryType; // 0x303C + [HoldingRegister(12350, writable: true)] private UInt32 _batteryCapacity1; // 0x303E + [HoldingRegister(12352, writable: true)] private UInt32 _maxChargingCurrentBattery1; // 0x3040 + [HoldingRegister(12354, writable: true)] private UInt32 _maxDischargingCurrentBattery1; // 0x3042 + [HoldingRegister(12362, writable: true)] private UInt32 _ratedBatteryVoltage1; // 0x304A + [HoldingRegister(12358, writable: true)] private UInt32 _minSocBattery1; // 0x3046 + [HoldingRegister(12360, writable: true)] private UInt32 _setValueBattery1; // 0x3048 + [HoldingRegister(12364, writable: true)] private UInt32 _activeChargeDischarge; // 0x304A + [HoldingRegister(12366, writable: true)] private UInt32 _activeChargeDischargePower; // 0x304C + [HoldingRegister(12380, writable: true)] private UInt32 _enableIslandProtection; // 0x305C + [HoldingRegister(12382, writable: true)] private UInt32 _pvAccessMode; // 0x305E + + // ─────────────────────────────────────────────── + // System & Battery-2 Configuration + // ─────────────────────────────────────────────── + [HoldingRegister(12384, writable: true)] private UInt32 _outputVoltageAdjustmentFactor; // 0x3060 + [HoldingRegister(12386, writable: true)] private UInt32 _setValueBatteryUndervoltage1; // 0x3062 + [HoldingRegister(12388, writable: true)] private UInt32 _inverterPowerLimit; // 0x3064 + [HoldingRegister(12400, writable: true)] private UInt32 _battery2Capacity; // 0x30B0 + [HoldingRegister(12402, writable: true)] private UInt32 _maxChargingCurrentBattery2; // 0x30B2 + [HoldingRegister(12404, writable: true)] private UInt32 _maxDischargingCurrentBattery2; // 0x30B4 + [HoldingRegister(12406, writable: true)] private UInt32 _battery2RatedVoltage; // 0x30B6 + [HoldingRegister(12408, writable: true)] private UInt32 _battery2MinSoc; // 0x30B8 + [HoldingRegister(12410, writable: true)] private UInt32 _battery2OverVoltageSetting; // 0x30BA + [HoldingRegister(12412, writable: true)] private UInt32 _battery2UnderVoltageSetpoint; // 0x30BC + + [HoldingRegister(12414, writable: true)] private UInt32 _singleOrParallelMachine; // 0x30BE + [HoldingRegister(12416, writable: true)] private UInt32 _numberOfSystemModules; // 0x30C0 + [HoldingRegister(12418, writable: true)] private UInt32 _parallelModuleMachineNumber; // 0x30C2 + [HoldingRegister(12420, writable: true)] private UInt32 _accreditedCountries; // 0x30C4 + + // ─────────────────────────────────────────────── + // System Control & Diagnostic Registers + // ─────────────────────────────────────────────── + [HoldingRegister(15426, writable: true)] private UInt32 _powerOn; // 0x3C42 + [HoldingRegister(15428, writable: true)] private UInt32 _powerOff; // 0x3C44 + [HoldingRegister(15430, writable: true)] private UInt32 _faultClearing; // 0x3C46 + [HoldingRegister(15518, writable: true)] private UInt32 _meterReverseManualDetection; // 0x3C9E + [HoldingRegister(15520, writable: true)] private UInt32 _batteryVoltage; // 0x3CA0 + [HoldingRegister(15522, writable: true)] private UInt32 _batteryRating; // 0x3CA2 + [HoldingRegister(15524, writable: true)] private UInt32 _battery1Activation; // 0x3CA4 + [HoldingRegister(15526, writable: true)] private UInt32 _battery2Activation; // 0x3CA6 + + + // ─────────────────────────────────────────────── + // Electric Meter Measurements (Read-Only) + // ─────────────────────────────────────────────── + [HoldingRegister(40960)] private UInt32 _electricMeterOperatingState; // 0xA000 + + [HoldingRegister(41216)] private UInt32 _electricMeterAPhaseVoltage; // 0xA100 + [HoldingRegister(41218)] private UInt32 _electricMeterBPhaseVoltage; // 0xA102 + [HoldingRegister(41220)] private UInt32 _electricMeterCPhaseVoltage; // 0xA104 + + [HoldingRegister(41222)] private UInt32 _electricMeterAPhaseCurrent; // 0xA106 + [HoldingRegister(41224)] private UInt32 _electricMeterBPhaseCurrent; // 0xA108 + [HoldingRegister(41226)] private UInt32 _electricMeterCPhaseCurrent; // 0xA10A + + [HoldingRegister(41234)] private UInt32 _electricMeterAPhaseActivePower; // 0xA112 + [HoldingRegister(41236)] private UInt32 _electricMeterBPhaseActivePower; // 0xA114 + [HoldingRegister(41238)] private UInt32 _electricMeterCPhaseActivePower; // 0xA116 + +} \ No newline at end of file