From 6343af3468b7547a4b7e0ca75b8bc3c645c74d50 Mon Sep 17 00:00:00 2001 From: atef Date: Thu, 31 Jul 2025 14:26:34 +0200 Subject: [PATCH] Create the new Growatt project. Src and library files added --- .../DataLogging/LogFileConcatenator.cs | 34 + .../DataLogging/Logfile.cs | 49 ++ .../DataLogging/Logger.cs | 40 ++ .../DataTypes/AlarmOrWarning.cs | 9 + .../DataTypes/SodistoreAlarmState.cs | 8 + .../DataTypes/StatusMessage.cs | 17 + .../GrowattCommunication/ESS/StatusRecord.cs | 10 + .../GrowattCommunication.csproj | 21 + csharp/App/GrowattCommunication/Program.cs | 639 ++++++++++++++++++ .../SystemConfig/Config.cs | 87 +++ .../SystemConfig/S3Config.cs | 80 +++ csharp/App/GrowattCommunication/deploy.sh | 32 + .../GrowattCommunication/sync-myRelease.sh | 21 + csharp/InnovEnergy.sln | 14 + .../WITGrowatt4-15K/BatteriesRecord.cs | 38 ++ .../Devices/WITGrowatt4-15K/BatteryRecord.cs | 22 + .../DataType/BatteryoperatinStatus.cs | 11 + .../DataType/GrowattSystemStatus.cs | 15 + .../DataType/OperatingPriority.cs | 8 + .../WITGrowatt4-15K/WITGrowatDevice.cs | 48 ++ .../WITGrowatt4-15K/WITGrowatRecord.Api.cs | 308 +++++++++ .../WITGrowatt4-15K/WITGrowatRecord.Modbus.cs | 180 +++++ .../WITGrowatt4-15K/WITGrowatt4-15K.csproj | 14 + 23 files changed, 1705 insertions(+) create mode 100644 csharp/App/GrowattCommunication/DataLogging/LogFileConcatenator.cs create mode 100644 csharp/App/GrowattCommunication/DataLogging/Logfile.cs create mode 100644 csharp/App/GrowattCommunication/DataLogging/Logger.cs create mode 100644 csharp/App/GrowattCommunication/DataTypes/AlarmOrWarning.cs create mode 100644 csharp/App/GrowattCommunication/DataTypes/SodistoreAlarmState.cs create mode 100644 csharp/App/GrowattCommunication/DataTypes/StatusMessage.cs create mode 100644 csharp/App/GrowattCommunication/ESS/StatusRecord.cs create mode 100644 csharp/App/GrowattCommunication/GrowattCommunication.csproj create mode 100644 csharp/App/GrowattCommunication/Program.cs create mode 100644 csharp/App/GrowattCommunication/SystemConfig/Config.cs create mode 100644 csharp/App/GrowattCommunication/SystemConfig/S3Config.cs create mode 100755 csharp/App/GrowattCommunication/deploy.sh create mode 100755 csharp/App/GrowattCommunication/sync-myRelease.sh create mode 100644 csharp/Lib/Devices/WITGrowatt4-15K/BatteriesRecord.cs create mode 100644 csharp/Lib/Devices/WITGrowatt4-15K/BatteryRecord.cs create mode 100644 csharp/Lib/Devices/WITGrowatt4-15K/DataType/BatteryoperatinStatus.cs create mode 100644 csharp/Lib/Devices/WITGrowatt4-15K/DataType/GrowattSystemStatus.cs create mode 100644 csharp/Lib/Devices/WITGrowatt4-15K/DataType/OperatingPriority.cs create mode 100644 csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatDevice.cs create mode 100644 csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatRecord.Api.cs create mode 100644 csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatRecord.Modbus.cs create mode 100644 csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatt4-15K.csproj diff --git a/csharp/App/GrowattCommunication/DataLogging/LogFileConcatenator.cs b/csharp/App/GrowattCommunication/DataLogging/LogFileConcatenator.cs new file mode 100644 index 000000000..930e1d3b7 --- /dev/null +++ b/csharp/App/GrowattCommunication/DataLogging/LogFileConcatenator.cs @@ -0,0 +1,34 @@ +using System.Text; + +namespace InnovEnergy.App.GrowattCommunication.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(); + } +} + diff --git a/csharp/App/GrowattCommunication/DataLogging/Logfile.cs b/csharp/App/GrowattCommunication/DataLogging/Logfile.cs new file mode 100644 index 000000000..b2f6cab16 --- /dev/null +++ b/csharp/App/GrowattCommunication/DataLogging/Logfile.cs @@ -0,0 +1,49 @@ +using InnovEnergy.Lib.Utils; +using Microsoft.Extensions.Logging; + +namespace InnovEnergy.App.GrowattCommunication.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); + } +} diff --git a/csharp/App/GrowattCommunication/DataLogging/Logger.cs b/csharp/App/GrowattCommunication/DataLogging/Logger.cs new file mode 100644 index 000000000..2f18f0f87 --- /dev/null +++ b/csharp/App/GrowattCommunication/DataLogging/Logger.cs @@ -0,0 +1,40 @@ + +using Microsoft.Extensions.Logging; + +namespace InnovEnergy.App.GrowattCommunication.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/GrowattCommunication/DataTypes/AlarmOrWarning.cs b/csharp/App/GrowattCommunication/DataTypes/AlarmOrWarning.cs new file mode 100644 index 000000000..c01a11899 --- /dev/null +++ b/csharp/App/GrowattCommunication/DataTypes/AlarmOrWarning.cs @@ -0,0 +1,9 @@ +namespace InnovEnergy.App.GrowattCommunication.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/GrowattCommunication/DataTypes/SodistoreAlarmState.cs b/csharp/App/GrowattCommunication/DataTypes/SodistoreAlarmState.cs new file mode 100644 index 000000000..f846a723a --- /dev/null +++ b/csharp/App/GrowattCommunication/DataTypes/SodistoreAlarmState.cs @@ -0,0 +1,8 @@ +namespace InnovEnergy.App.GrowattCommunication.DataTypes; + +public enum SodistoreAlarmState +{ + Green, + Orange, + Red +} diff --git a/csharp/App/GrowattCommunication/DataTypes/StatusMessage.cs b/csharp/App/GrowattCommunication/DataTypes/StatusMessage.cs new file mode 100644 index 000000000..4c4f27e60 --- /dev/null +++ b/csharp/App/GrowattCommunication/DataTypes/StatusMessage.cs @@ -0,0 +1,17 @@ +namespace InnovEnergy.App.GrowattCommunication.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/GrowattCommunication/ESS/StatusRecord.cs b/csharp/App/GrowattCommunication/ESS/StatusRecord.cs new file mode 100644 index 000000000..8b34c42ff --- /dev/null +++ b/csharp/App/GrowattCommunication/ESS/StatusRecord.cs @@ -0,0 +1,10 @@ +using InnovEnergy.App.GrowattCommunication.SystemConfig; +using InnovEnergy.Lib.Devices.WITGrowatt4_15K; + +namespace InnovEnergy.App.GrowattCommunication.ESS; + +public record StatusRecord +{ + public required WITGrowatRecord AcDcGrowatt { get; set; } + public required Config Config { get; set; } +} \ No newline at end of file diff --git a/csharp/App/GrowattCommunication/GrowattCommunication.csproj b/csharp/App/GrowattCommunication/GrowattCommunication.csproj new file mode 100644 index 000000000..8a12f8a3b --- /dev/null +++ b/csharp/App/GrowattCommunication/GrowattCommunication.csproj @@ -0,0 +1,21 @@ + + + + InnovEnergy.App.GrowattCommunication + + + + + + + + + + + + + + + + + diff --git a/csharp/App/GrowattCommunication/Program.cs b/csharp/App/GrowattCommunication/Program.cs new file mode 100644 index 000000000..42e4203f1 --- /dev/null +++ b/csharp/App/GrowattCommunication/Program.cs @@ -0,0 +1,639 @@ +using System.IO.Compression; +using InnovEnergy.Lib.Devices.WITGrowatt4_15K; +using System.IO.Ports; +using System.Text; +using System.Text.Json; +using System.Xml; +using Flurl.Http; +using InnovEnergy.App.GrowattCommunication.DataLogging; +using InnovEnergy.App.GrowattCommunication.ESS; +using InnovEnergy.App.GrowattCommunication.SystemConfig; +using InnovEnergy.Lib.Protocols.Modbus.Channels; +using InnovEnergy.Lib.Units; +using InnovEnergy.Lib.Utils; +using Newtonsoft.Json; +using Formatting = Newtonsoft.Json.Formatting; +using JsonSerializer = System.Text.Json.JsonSerializer; +using System.Diagnostics.CodeAnalysis; +using InnovEnergy.App.GrowattCommunication.DataTypes; +using InnovEnergy.Lib.Devices.WITGrowatt4_15K.DataType; + +namespace InnovEnergy.App.GrowattCommunication; + +public static class Program +{ + private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(2); + private const UInt16 NbrOfFileToConcatenate = 30; // add this to config file + private static UInt16 _fileCounter = 0; + // + private static Channel _growattChannel; + + private const String SwVersionNumber =" V1.00.310725 beta"; + private const String VpnServerIp = "10.2.0.11"; + private static Boolean _subscribedToQueue = false; + private static Boolean _subscribeToQueueForTheFirstTime = false; + private static Int32 _failsCounter = 0; // move to a config file + private static SodistoreAlarmState _prevSodistoreAlarmState = SodistoreAlarmState.Green; + private static SodistoreAlarmState _sodistoreAlarmState = 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 = 9600; + private const Int32 DataBits = 8; + + [UnconditionalSuppressMessage("Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "")] + public static async Task Main(String[] args) + { + _growattChannel = new SerialPortChannel(Port, BaudRate, Parity, DataBits, StopBits); + + while (true) + { + try + { + await Run(); + } + catch (Exception e) + { + e.LogError(); + } + } + + } + + private static async Task Run() + { + Watchdog.NotifyReady(); + + Console.WriteLine("Starting Growatt Communication"); + + var growatrrDevicet415K = new WITGrowatDevice(_growattChannel, SlaveId); + + StatusRecord ReadStatus() + { + var config = Config.Load(); + var growattRecord = growatrrDevicet415K.Read(); + + return new StatusRecord + { + AcDcGrowatt = growattRecord, + Config = config // load from disk every iteration, so config can be changed while running + }; + } + + while (true) + { + try + { + Watchdog.NotifyAlive(); + + var timestamp = DateTime.Now.Round(UpdateInterval).ToUnixTime(); + + $"{timestamp} : {DateTime.Now.Round(UpdateInterval):dd/MM/yyyy HH:mm:ss}".WriteLine(); + + 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(); + await DataLogging(statusrecord, timestamp); // save a csv file locally + await SaveModbusTcpFile(statusrecord); // save the json file for modbuscTCP + + + statusrecord.AcDcGrowatt.EnableCommand.WriteLine(" = EnableCommand"); + statusrecord.AcDcGrowatt.ControlPermession.WriteLine(" ControlPermession"); + statusrecord.AcDcGrowatt.GridMeterPower.WriteLine(" GridMeterPower"); + statusrecord.AcDcGrowatt.InverterActivePower.WriteLine(" InverterActivePower"); + statusrecord.AcDcGrowatt.BatteryPower1.WriteLine(" BatteryPower1"); // 30408 this the duration + statusrecord.AcDcGrowatt.PhaseACurrent.WriteLine(" PhaseACurrent "); //30409 we set power here + statusrecord.AcDcGrowatt.GridAbLineVoltage.WriteLine(" GridAbLineVoltage "); //30409 we set power here + statusrecord.AcDcGrowatt.RemotePowerControlChargeDuration.WriteLine(" = RemotePowerControlChargeDuration"); + statusrecord.AcDcGrowatt.Batteries[0].Soc.WriteLine(" SOC"); + statusrecord.AcDcGrowatt.Batteries[0].Power.WriteLine(" Battery Power"); + statusrecord.AcDcGrowatt.Batteries[0].Current.WriteLine(" Battery Current"); + statusrecord.AcDcGrowatt.Batteries[0].Voltage.WriteLine(" Battery Voltage"); + statusrecord.AcDcGrowatt.BatteryMaxChargePower.WriteLine(" BatteryMaxChargePower "); //30409 we set power here + statusrecord.AcDcGrowatt.BatteryMaxDischargePower.WriteLine(" BatteryMaxDischargePower "); //30409 we set power here + + statusrecord.AcDcGrowatt.SystemOperatingMode.WriteLine(" = SystemOperatingMode"); + statusrecord.AcDcGrowatt.BatteryOperatingMode.WriteLine(" BatteryOperatingMode"); + statusrecord.AcDcGrowatt.OperatingPriority.WriteLine(" OperatingPriority"); // 30408 this the duration + + var stopTime = DateTime.Now; + Console.WriteLine(stopTime.ToString("HH:mm:ss.fff")); + Console.WriteLine("***************************** Finish Battery Data *********************************************"); + statusrecord.AcDcGrowatt.EnableCommand = true; + statusrecord.AcDcGrowatt.ControlPermession = true; + statusrecord.AcDcGrowatt.RemotePowerControl = true; + statusrecord.AcDcGrowatt.RemotePowerControlChargeDuration = 0; // 30408 this the duration + statusrecord.AcDcGrowatt.ActivePowerPercent = 50; // 30408 this the duration + statusrecord.AcDcGrowatt.ActivePowerPercentDerating = 50; // 30408 this the duration + + statusrecord.AcDcGrowatt.RemoteChargDischargePower = 50; //30409 we set power here + statusrecord.AcDcGrowatt.ActualChargeDischargePowerControlValue.WriteLine(" register 30474"); + + + statusrecord.ApplyDefaultSettings(); + + statusrecord.Config.Save(); // save the config file + + Console.WriteLine( " ************************************ We are writing ************************************"); + growatrrDevicet415K.Write(statusrecord.AcDcGrowatt); + + // Wait for 2 seconds before the next reading + // await Task.Delay(1000); // Delay in milliseconds (1000ms = 1 seconds) + await Task.Delay(2000); // Delay in milliseconds (1000ms = 1 seconds) + } + catch (Exception e) + { + // Handle exception and print the error + Console.WriteLine(e ); + // await Task.Delay(2000); // Delay in milliseconds (2000ms = 2 seconds) + } + + } + } + + + private static StatusMessage GetSalimaxStateAlarm(StatusRecord record) + { + var s3Bucket = Config.Load().S3?.Bucket; + + var alarmList = new List(); + var warningList = new List(); + + if (record.AcDcGrowatt.SystemOperatingMode == GrowattSystemStatus.Fault) + { + if (record.AcDcGrowatt.FaultMainCode != 0) + { + alarmList.Add(new AlarmOrWarning + { + Date = DateTime.Now.ToString("yyyy-MM-dd"), + Time = DateTime.Now.ToString("HH:mm:ss"), + CreatedBy = "Growatt Inverter", + Description = record.AcDcGrowatt.WarningMainCode.ToString(), // to add the sub code + }); + } + + if (record.AcDcGrowatt.FaultSubCode != 0) + { + warningList.Add(new AlarmOrWarning + { + Date = DateTime.Now.ToString("yyyy-MM-dd"), + Time = DateTime.Now.ToString("HH:mm:ss"), + CreatedBy = "Growatt inverter", + Description = record.AcDcGrowatt.FaultMainCode.ToString(), //to add the sub code + }); + } + } + + _sodistoreAlarmState = warningList.Any() + ? SodistoreAlarmState.Orange + : SodistoreAlarmState.Green; // this will be replaced by LedState + + _sodistoreAlarmState = alarmList.Any() + ? SodistoreAlarmState.Red + : _sodistoreAlarmState; // this will be replaced by LedState + + var installationId = GetInstallationId(s3Bucket ?? string.Empty); + + var returnedStatus = new StatusMessage + { + InstallationId = installationId, + Product = 3, + Status = _sodistoreAlarmState, + Type = MessageType.AlarmOrWarning, + Alarms = alarmList, + Warnings = warningList + }; + + return returnedStatus; + } + + private static Int32 GetInstallationId(String s3Bucket) + { + var part = s3Bucket.Split('-').FirstOrDefault(); + return int.TryParse(part, out var id) ? id : 0; // is 0 a default safe value? check with Marios + } + + private static 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; + _prevSodistoreAlarmState = 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 != _prevSodistoreAlarmState) + { + _prevSodistoreAlarmState = 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 ApplyDefaultSettings(this StatusRecord? st) + { + if (st is null) + return; + + st.AcDcGrowatt.EmsCommunicationFailureTime = 20; // 20 sec + st.AcDcGrowatt.EnableEmsCommunicationFailureTime = true; + st.AcDcGrowatt.EnableCommand = true; + st.AcDcGrowatt.ControlPermession = true; + st.AcDcGrowatt.BatteryChargeCutoffVoltage = 100; //st.Config.BatteryChargeCutoffVoltage; + st.AcDcGrowatt.BatteryDischargeCutoffVoltage = 20; //st.Config.BatteryDischargeCutoffVoltage; + st.AcDcGrowatt.BatteryMaxChargeCurrent = 150; //st.Config.BatteryChargeCutoffVoltage; + st.AcDcGrowatt.BatteryMaxdischargeCurrent = 150; //st.Config.BatteryChargeCutoffVoltage; + + } + + 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; + } + + [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize>(System.Collections.Generic.Dictionary, System.Text.Json.JsonSerializerOptions?)")] + private static async Task SaveModbusTcpFile(StatusRecord status) + { + var modbusData = new Dictionary(); + + Console.WriteLine(new DateTimeOffset(status.AcDcGrowatt.SystemDateTime).ToUnixTimeSeconds() + " This Growatt time"); + // SYSTEM DATA + var result1 = ConvertToModbusRegisters((status.AcDcGrowatt.VppProtocolVerNumber * 10), "UInt16", 30001); + var result2 = ConvertToModbusRegisters(status.AcDcGrowatt.SystemDateTime.ToUnixTime(), "UInt32", 30002); + var result3 = ConvertToModbusRegisters(status.AcDcGrowatt.SystemOperatingMode, "Int16", 30004); + + // BATTERY SUMMARY (assuming single battery [0]) + var battery = status.AcDcGrowatt.BatteriesRecords!.Batteries[0]; + + var result4 = ConvertToModbusRegisters((status.AcDcGrowatt.BatteriesRecords!.Batteries.Count ), "UInt16", 31000); + var result5 = ConvertToModbusRegisters((battery.Power.Value * 10), "Int32", 31001); + var result6 = ConvertToModbusRegisters((battery.DailyChargeEnergy.Value * 10), "UInt32", 31003); + var result7 = ConvertToModbusRegisters((battery.AccumulatedChargeEnergy.Value * 10), "UInt32", 31005); + var result8 = ConvertToModbusRegisters((battery.DailyDischargeEnergy.Value * 10), "UInt32", 31007); + var result9 = ConvertToModbusRegisters((battery.AccumulatedDischargeEnergy.Value * 10), "UInt32", 31009); + var result10 = ConvertToModbusRegisters((battery.MaxAllowableDischargePower.Value * 10), "UInt32", 31011); + var result11 = ConvertToModbusRegisters((battery.MaxAllowableDischargePower.Value * 10), "UInt32", 31013); + + var result12 = ConvertToModbusRegisters((battery.Voltage.Value * 10), "Int16", 31015); + var result13 = ConvertToModbusRegisters((battery.Current.Value * 10), "Int32", 31016); + var result14 = ConvertToModbusRegisters((battery.Soc.Value * 100), "UInt16", 31018); + var result15 = ConvertToModbusRegisters((status.AcDcGrowatt.BatteriesRecords!.AverageSoh * 100), "UInt16", 31019); + var result16 = ConvertToModbusRegisters((battery.BatteryAmbientTemperature.Value * 100), "UInt16", 31021); + + + // Merge all results into one dictionary + var allResults = new[] + { + result1, result2, result3, result4, result5, result6, result7, result8, + result9, result10, result11, result12, result13, result14, result15, result16 + }; + + 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/ModbusTCP/modbus_tcp_data.json", json); + + //Console.WriteLine("JSON file written successfully."); + //Console.WriteLine(json); + } + + private static async Task DataLogging(StatusRecord status, Int64 timestamp) + { + var csv = status.ToCsv(); + + // for debug, only to be deleted. + //foreach (var item in csv.SplitLines()) + //{ + // Console.WriteLine(item + ""); + //} + + await SavingLocalCsvFile(timestamp, 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, s3Config); + } + + private static String ConvertToJson(String csv, Dictionary jsonData) + { + foreach (var line in csv.Split('\n')) + { + if (string.IsNullOrWhiteSpace(line)) continue; + + var parts = line.Split(';'); + var keyPath = parts[0]; + var value = parts[1]; + var unit = parts.Length > 2 ? parts[2].Trim() : ""; + InsertIntoJson(jsonData, keyPath.Split('/'), value); + } + + var jsonOutput = JsonConvert.SerializeObject(jsonData, Formatting.None); + return jsonOutput; + } + + private static void InsertIntoJson(Dictionary jsonDict, String[] keys, String value) + { + var currentDict = jsonDict; + for (Int16 i = 1; i < keys.Length; i++) // Start at 1 to skip empty root + { + var key = keys[i]; + if (!currentDict.ContainsKey(key)) + { + currentDict[key] = new Dictionary(); + } + + if (i == keys.Length - 1) // Last key, store the value + { + + if (!value.Contains(",") && double.TryParse(value, out Double doubleValue)) // Try to parse value as a number + { + currentDict[key] = Math.Round(doubleValue, 2); // Round to 2 decimal places + + } + else + { + currentDict[key] = value; // Store as string if not a number + } + } + else + { + currentDict = (Dictionary)currentDict[key]; + } + } + } + private static async Task 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 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; + } + + private static byte[] ComputeCrc16Appended(byte[] data) + { + ushort crc = 0xFFFF; + + foreach (byte b in data) + { + crc ^= b; + for (int i = 0; i < 8; i++) + { + bool lsb = (crc & 0x0001) != 0; + crc >>= 1; + if (lsb) + { + crc ^= 0xA001; + } + } + } + + byte crcLow = (byte)(crc & 0xFF); + byte crcHigh = (byte)((crc >> 8) & 0xFF); + + // Create a new array with space for CRC + byte[] result = new byte[data.Length + 2]; + Array.Copy(data, result, data.Length); + result[result.Length - 2] = crcLow; + result[result.Length - 1] = crcHigh; + + return result; + } +} \ No newline at end of file diff --git a/csharp/App/GrowattCommunication/SystemConfig/Config.cs b/csharp/App/GrowattCommunication/SystemConfig/Config.cs new file mode 100644 index 000000000..50f5bd257 --- /dev/null +++ b/csharp/App/GrowattCommunication/SystemConfig/Config.cs @@ -0,0 +1,87 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using InnovEnergy.Lib.Utils; +using static System.Text.Json.JsonSerializer; + +namespace InnovEnergy.App.GrowattCommunication.SystemConfig; + +public class Config //TODO: let IE choose from config files (Json) and connect to GUI +{ + private static String DefaultConfigFilePath => Path.Combine(Environment.CurrentDirectory, "config.json"); + private static DateTime DefaultDatetime => new(2024, 03, 11, 09, 00, 00); + + private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; + + public required Double MinSoc { get; set; } + public required S3Config? S3 { get; set; } + + + private static String? LastSavedData { get; set; } + + public static Config Default => new() + { + MinSoc = 20, + 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/GrowattCommunication/SystemConfig/S3Config.cs b/csharp/App/GrowattCommunication/SystemConfig/S3Config.cs new file mode 100644 index 000000000..901b1d933 --- /dev/null +++ b/csharp/App/GrowattCommunication/SystemConfig/S3Config.cs @@ -0,0 +1,80 @@ +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.GrowattCommunication.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; } + + public String Host => $"{Bucket}.{Region}.{Provider}"; + public 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/GrowattCommunication/deploy.sh b/csharp/App/GrowattCommunication/deploy.sh new file mode 100755 index 000000000..a919ab29c --- /dev/null +++ b/csharp/App/GrowattCommunication/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 \ + ./GrowattCommunication.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/App/GrowattCommunication/sync-myRelease.sh b/csharp/App/GrowattCommunication/sync-myRelease.sh new file mode 100755 index 000000000..1c80c3fed --- /dev/null +++ b/csharp/App/GrowattCommunication/sync-myRelease.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +WATCHDIR="$HOME/sync/work/Code/CSharp/git_trunk/csharp/App/GrowattCommunication/bin/Release/net6.0/linux-arm64/publish" +DEST="ubuntu@91.92.155.224:/home/ubuntu/Releases" + +echo "👀 Watching for real releases in $WATCHDIR..." + +inotifywait -m -e close_write --format '%w%f' "$WATCHDIR" | while read file; do + filename="$(basename "$file")" + + if [[ "$filename" == ".release.flag" ]]; then + echo "🚀 Release flag detected. Syncing full release to $DEST..." + + rm "$file" + rsync -avz \ + --exclude '*.pdb' \ + "$WATCHDIR/" "$DEST/" + + echo "✅ Sync completed and flag cleared." + fi +done diff --git a/csharp/InnovEnergy.sln b/csharp/InnovEnergy.sln index 291c6023b..c5ec2b4fa 100644 --- a/csharp/InnovEnergy.sln +++ b/csharp/InnovEnergy.sln @@ -99,6 +99,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeligreenBatteryCommunicati EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SodiStoreMax", "App\SodiStoreMax\SodiStoreMax.csproj", "{39B83793-49DB-4940-9C25-A7F944607407}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GrowattCommunication", "App\GrowattCommunication\GrowattCommunication.csproj", "{DC0BE34A-368F-46DC-A081-70C9A1EFE9C0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WITGrowatt4-15K", "Lib\Devices\WITGrowatt4-15K\WITGrowatt4-15K.csproj", "{44DD9E5E-2AD3-4579-A47D-7A40FD28D369}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -262,6 +266,14 @@ Global {39B83793-49DB-4940-9C25-A7F944607407}.Debug|Any CPU.Build.0 = Debug|Any CPU {39B83793-49DB-4940-9C25-A7F944607407}.Release|Any CPU.ActiveCfg = Release|Any CPU {39B83793-49DB-4940-9C25-A7F944607407}.Release|Any CPU.Build.0 = Release|Any CPU + {DC0BE34A-368F-46DC-A081-70C9A1EFE9C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC0BE34A-368F-46DC-A081-70C9A1EFE9C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC0BE34A-368F-46DC-A081-70C9A1EFE9C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC0BE34A-368F-46DC-A081-70C9A1EFE9C0}.Release|Any CPU.Build.0 = Release|Any CPU + {44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {CF4834CB-91B7-4172-AC13-ECDA8613CD17} = {145597B4-3E30-45E6-9F72-4DD43194539A} @@ -307,5 +319,7 @@ Global {1045AC74-D4D8-4581-AAE3-575DF26060E6} = {4931A385-24DC-4E78-BFF4-356F8D6D5183} {11ED6871-5B7D-462F-8710-B5D85DEC464A} = {145597B4-3E30-45E6-9F72-4DD43194539A} {39B83793-49DB-4940-9C25-A7F944607407} = {145597B4-3E30-45E6-9F72-4DD43194539A} + {DC0BE34A-368F-46DC-A081-70C9A1EFE9C0} = {145597B4-3E30-45E6-9F72-4DD43194539A} + {44DD9E5E-2AD3-4579-A47D-7A40FD28D369} = {4931A385-24DC-4E78-BFF4-356F8D6D5183} EndGlobalSection EndGlobal diff --git a/csharp/Lib/Devices/WITGrowatt4-15K/BatteriesRecord.cs b/csharp/Lib/Devices/WITGrowatt4-15K/BatteriesRecord.cs new file mode 100644 index 000000000..a0758c7cd --- /dev/null +++ b/csharp/Lib/Devices/WITGrowatt4-15K/BatteriesRecord.cs @@ -0,0 +1,38 @@ +using InnovEnergy.Lib.Devices.WITGrowatt4_15K.DataType; +using InnovEnergy.Lib.Units; +using InnovEnergy.Lib.Units.Power; + +namespace InnovEnergy.Lib.Devices.WITGrowatt4_15K; + +public class BatteriesRecord +{ + public required IReadOnlyList Batteries { get; init; } + + public required Percent AverageSoc { get; init; } + public required Double AverageSoh { get; init; } + public required Percent LowestSoc { get; init; } + public required DcPower Power { get; init; } + public required DcPower TotalMaxCharge { get; init; } + public required DcPower TotalMaxDischarge { get; init; } + public required Energy TotalChargeEnergy { get; init; } + public required Energy TotalDischargeEnergy { get; init; } + + public static BatteriesRecord? FromBatteries(IReadOnlyList? records) + { + if (records is null || records.Count == 0) + return null; + + return new BatteriesRecord + { + Batteries = records, + AverageSoc = records.Average(r => r.Soc.Value), + AverageSoh = records.Average(b => b.Soh), + LowestSoc = new Percent(records.Min(b => b.Soc.Value)), + Power = new DcPower(records.Sum(b => b.Power.Value)), + TotalMaxCharge = new DcPower(records.Sum(b => b.MaxAllowableChargePower.Value)), + TotalMaxDischarge = new DcPower(records.Sum(b => b.MaxAllowableDischargePower.Value)), + TotalChargeEnergy = new Energy(records.Sum(b => b.DailyChargeEnergy.Value)), + TotalDischargeEnergy = new Energy(records.Sum(b => b.DailyDischargeEnergy.Value)) + }; + } +} diff --git a/csharp/Lib/Devices/WITGrowatt4-15K/BatteryRecord.cs b/csharp/Lib/Devices/WITGrowatt4-15K/BatteryRecord.cs new file mode 100644 index 000000000..00f7efe82 --- /dev/null +++ b/csharp/Lib/Devices/WITGrowatt4-15K/BatteryRecord.cs @@ -0,0 +1,22 @@ +using InnovEnergy.Lib.Units; +using InnovEnergy.Lib.Units.Power; + +namespace InnovEnergy.Lib.Devices.WITGrowatt4_15K; + +public class BatteryRecord +{ + public required Percent Soc { get; init; } + public required Double Soh { get; init; } + // public required UInt16 ClusterTotalNumber { get; init; } + public required Current Current { get; init; } + public required Voltage Voltage { get; init; } + public required DcPower Power { get; init; } + public required DcPower MaxAllowableChargePower { get; init; } + public required DcPower MaxAllowableDischargePower { get; init; } + public required Energy DailyChargeEnergy { get; init; } + public required Energy DailyDischargeEnergy { get; init; } + public required Energy AccumulatedChargeEnergy { get; init; } + public required Energy AccumulatedDischargeEnergy { get; init; } + public required Temperature BatteryAmbientTemperature { get; init; } + +} \ No newline at end of file diff --git a/csharp/Lib/Devices/WITGrowatt4-15K/DataType/BatteryoperatinStatus.cs b/csharp/Lib/Devices/WITGrowatt4-15K/DataType/BatteryoperatinStatus.cs new file mode 100644 index 000000000..cc1d6f99b --- /dev/null +++ b/csharp/Lib/Devices/WITGrowatt4-15K/DataType/BatteryoperatinStatus.cs @@ -0,0 +1,11 @@ +namespace InnovEnergy.Lib.Devices.WITGrowatt4_15K.DataType; + +public enum BatteryoperatinStatus +{ + Standby = 0, + Disconnected = 1, + Charging = 2, + Discharging = 3, + Fault = 4, + Upgrade = 5 +} \ No newline at end of file diff --git a/csharp/Lib/Devices/WITGrowatt4-15K/DataType/GrowattSystemStatus.cs b/csharp/Lib/Devices/WITGrowatt4-15K/DataType/GrowattSystemStatus.cs new file mode 100644 index 000000000..330abe0bb --- /dev/null +++ b/csharp/Lib/Devices/WITGrowatt4-15K/DataType/GrowattSystemStatus.cs @@ -0,0 +1,15 @@ +namespace InnovEnergy.Lib.Devices.WITGrowatt4_15K.DataType; + +public enum GrowattSystemStatus +{ + Standby = 0, + SelfTest = 1, + Reserved = 2, + Fault = 3, + Upgrade = 4, + PvOnlineBatteryOffline = 5, + BatteryOnline = 6, // pv offline or online + PvBatteryOnlineOffGrid = 7, + BatteryOnlineOfflineOffGrid = 8, + Bypass = 9, +} \ No newline at end of file diff --git a/csharp/Lib/Devices/WITGrowatt4-15K/DataType/OperatingPriority.cs b/csharp/Lib/Devices/WITGrowatt4-15K/DataType/OperatingPriority.cs new file mode 100644 index 000000000..dcbb0af99 --- /dev/null +++ b/csharp/Lib/Devices/WITGrowatt4-15K/DataType/OperatingPriority.cs @@ -0,0 +1,8 @@ +namespace InnovEnergy.Lib.Devices.WITGrowatt4_15K.DataType; + +public enum OperatingPriority +{ + LoadPriority = 0, + BatteryPriority = 1, + GridPriority = 2, +} \ No newline at end of file diff --git a/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatDevice.cs b/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatDevice.cs new file mode 100644 index 000000000..d73410d27 --- /dev/null +++ b/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatDevice.cs @@ -0,0 +1,48 @@ + + +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.WITGrowatt4_15K; + +public class WITGrowatDevice : ModbusDevice +{ + private const Parity Parity = 0; //none + private const Int32 StopBits = 1; + private const Int32 BaudRate = 9600; + private const Int32 DataBits = 8; + + public Byte SlaveId { get; } + + public WITGrowatDevice(String tty, Byte slaveId, SshHost host) : this + ( + channel: new RemoteSerialChannel(host, tty, BaudRate, Parity, DataBits, StopBits), + slaveId + ) + {} + + public WITGrowatDevice(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 WITGrowatDevice(Channel channel, Byte slaveId) : this + ( + client: new ModbusRtuClient(channel, slaveId) + ) + {} + + public WITGrowatDevice(ModbusClient client): base(client) + { + SlaveId = client.SlaveId; + } +} \ No newline at end of file diff --git a/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatRecord.Api.cs b/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatRecord.Api.cs new file mode 100644 index 000000000..637332914 --- /dev/null +++ b/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatRecord.Api.cs @@ -0,0 +1,308 @@ + +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using InnovEnergy.Lib.Devices.WITGrowatt4_15K.DataType; +using InnovEnergy.Lib.Units; +using InnovEnergy.Lib.Units.Power; + +namespace InnovEnergy.Lib.Devices.WITGrowatt4_15K; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +[SuppressMessage("ReSharper", "ConvertToAutoProperty")] +public partial class WITGrowatRecord +{ + // private List Batteries { get; set; } = new(); + //public BatteriesRecord? BatteriesRecords => BatteriesRecord.FromBatteries(Batteries); + public ActivePower InverterActivePower => _ActivePower; + public ReactivePower InverterReactivePower => _ReactivePower; + public Frequency Frequency => _Frequency; + + public VoltageRms GridAbLineVoltage => _GridAbLineVoltage; + public VoltageRms GridBcLineVoltage => _GridBcLineVoltage; + public VoltageRms GridCaLineVoltage => _GridCaLineVoltage; + public CurrentRms PhaseACurrent => _PhaseACurrent; + public CurrentRms PhaseBCurrent => _PhaseBCurrent; + public CurrentRms PhaseCCurrent => _PhaseCCurrent; + + public Int32 MeterPower => _MeterPower; + public ActivePower GridMeterPower => _GridPower; + + public Int32 BatteryPower1 + { + get => _BatteryPower1; + set => _BatteryPower1 = value; + } + + public Temperature InverterTemperature => _InverterTemperature; + + public Energy EnergyToUser => _EnergyToUser; + public Energy TotalEnergyToUser => _TotalEnergyToUser; + public Energy EnergyToGrid => _EnergyToGrid; + public Energy TotalEnergyToGrid => _TotalEnergyToGrid; + + public GrowattSystemStatus SystemOperatingMode => (GrowattSystemStatus)_SystemOperatingMode; + public BatteryoperatinStatus BatteryOperatingMode => (BatteryoperatinStatus) _BatteryOperatingMode; + public OperatingPriority OperatingPriority => (OperatingPriority)_OperatingPriority; + + public UInt16 FaultMainCode => _FaultMainCode; // need to pre proceesed + public UInt16 FaultSubCode => _FaultSubCode; // need to pre proceesed + public UInt16 WarningMainCode => _WarningMainCode; // need to pre proceesed + public UInt16 WarningSubCode => _WarningSubCode; // need to pre proceesed + + public Voltage Pv1Voltage => _Pv1Voltage; + public Current Pv1Current => _Pv1Current; + public Voltage Pv2Voltage => _Pv2Voltage; + public Current Pv2Current => _Pv2Current; + public DcPower Pv1InpuPower => _Pv1InpuPower; + + // ********************************** Holding Registers (Control) ************************************************************* + + // public UInt16 DeviceModel => _DeviceModel; + public UInt32 RatedPower + { + get => _RatedPower; + } + + public UInt32 MaxActivePower + { + get => _MaxActivePower; + } + + public UInt32 PvInputMaxPower + { + get => _PvInputMaxPower; + } + + public UInt16 BatteryType + { + get => _BatteryType; + } + + public UInt16 VppProtocolVerNumber + { + get => _VppProtocolVerNumber; + } + + public Boolean ControlPermession + { + get => _ControlPermession; + set => _ControlPermession = value; + } + + public Boolean EnableCommand + { + get => _EnableCommand; + set => _EnableCommand = value; + } + + public DateTime SystemDateTime + { + get + { + var systemTime1 = _SystemTime1 + 2000; // We add 2000 years to fit a correct epoch time + return new DateTime(systemTime1, _SystemTime2, _SystemTime3, _SystemTime4, _SystemTime5, + _SystemTime6); + } + } +/* + public Boolean EnableSyn + { + get => _EnableSyn ; + set => _EnableSyn = value; + }*/ + + public Percent ActivePowerPercentDerating + { + get => _ActivePowerPercentDerating; + set => _ActivePowerPercentDerating = (UInt16)value; + } + + public Percent ActivePowerPercent + { + get => _ActivePowerPercent; + set => _ActivePowerPercent = (UInt16)value; + } + public UInt16 PowerFactor + { + get => _PowerFactor; + set => _PowerFactor = value; + } + + public UInt16 EmsCommunicationFailureTime + { + get => _EmsCommunicationFailureTime; + set => _EmsCommunicationFailureTime = value; + } + + public Boolean EnableEmsCommunicationFailureTime + { + get => _EnableEmsCommunicationFailureTime; + set => _EnableEmsCommunicationFailureTime = value; + } + + public UInt16 BatteryClusterIndex + { + get => _BatteryClusterIndex; + set => _BatteryClusterIndex = value; + } + + public UInt32 BatteryMaxChargePower + { + get => _BatteryMaxChargePower; + set => _BatteryMaxChargePower = value; + } + + public UInt32 BatteryMaxDischargePower + { + get => _BatteryMaxDischargePower; + set => _BatteryMaxDischargePower = value; + } + + public Percent ChargeCutoffSoc + { + get => _ChargeCutoffSoc; + set => _ChargeCutoffSoc = (UInt16)value; + } + + public Percent DischargeCutoffSoc + { + get => _DischargeCutoffSoc; + set => _DischargeCutoffSoc = (UInt16)value; + } + + public Percent LoadPriorityDischargeCutoffSoc + { + get => _LoadPriorityDischargeCutoffSoc; + set => _LoadPriorityDischargeCutoffSoc = (UInt16)value; + } + + public Boolean RemotePowerControl + { + get => _RemotePowerControl; + set => _RemotePowerControl = value; + } + + public UInt16 RemotePowerControlChargeDuration + { + get => _RemotePowerControlChargeDuration; + set => _RemotePowerControlChargeDuration = value; + } + + public Int16 RemoteChargDischargePower + { + get => _RemoteChargDischargePower; + set => _RemoteChargDischargePower = (Int16)value; + } + + public Boolean AcChargeEnable + { + get => _AcChargeEnable; + set => _AcChargeEnable = value; + } + + public Percent ActualChargeDischargePowerControlValue + { + get => _ActualChargeDischargePowerControlValue; + } + + public Percent OffGridDischargeCutoffSoc + { + get => _OffGridDischargeCutoffSoc; + set => _OffGridDischargeCutoffSoc = (UInt16)(value); + } + + public UInt16 BatteryChargeCutoffVoltage + { + get => _BatteryChargeCutoffVoltage; + set => _BatteryChargeCutoffVoltage = value; + } + + public UInt16 BatteryDischargeCutoffVoltage + { + get => _BatteryDischargeCutoffVoltage; + set => _BatteryDischargeCutoffVoltage = value; + } + + public UInt16 BatteryMaxChargeCurrent + { + get => _BatteryMaxChargeCurrent; + set => _BatteryMaxChargeCurrent = value; + } + + public UInt16 BatteryMaxdischargeCurrent + { + get => _BatteryMaxdischargeCurrent; + set => _BatteryMaxdischargeCurrent = value; + } + + + + + public IReadOnlyList Batteries => new List + { + new BatteryRecord + { + Soc = _BatterySoc1, + Soh = _BatterySoh1, + //ClusterTotalNumber = _ClusterTotalNumber1, + Current = _BatteryCurrent1, + Voltage = _BatteryVoltage1, + Power = _BatteryPower1, + MaxAllowableChargePower = _BatteryMaxAllowableChargePower1, + MaxAllowableDischargePower = _BatteryMaxAllowableDischargePower1, + DailyChargeEnergy = _DailyChargeEnergy1, + DailyDischargeEnergy = _DailyDischargeEnergy1, + AccumulatedChargeEnergy = _AccumulatedChargeEnergy1, + AccumulatedDischargeEnergy = _AccumulatedDishargeEnergy1, + BatteryAmbientTemperature = _BatteryAmbientTemperature1 + + }, + /* + new BatteryRecord + { + Soc = _BatterySoc2, + Soh = _BatterySoh2, + // ClusterTotalNumber = _ClusterTotalNumber2, + Current = _BatteryCurrent2, + Voltage = _BatteryVoltage2, + Power = _BatteryPower2, + MaxAllowableChargePower = _BatteryMaxAllowableChargePower2, + MaxAllowableDischargePower = _BatteryMaxAllowableDischargePower2, + DailyChargeEnergy = _DailyChargeEnergy2, + DailyDischargeEnergy = _DailyDischargeEnergy2, + AccumulatedChargeEnergy = _AccumulatedChargeEnergy2, + AccumulatedDischargeEnergy = _AccumulatedDischargeEnergy2, + BatteryAmbientTemperature = _BatteryAmbientTemperature2 + + },*/ + /* + new BatteryRecord + { + Soc = _BatterySoc3, + Soh = _BatterySoh3, + ClusterTotalNumber = _ClusterTotalNumber3, + Current = _BatteryCurrent3, + Voltage = _BatteryVoltage3, + Power = _BatteryPower3, + MaxAllowableChargePower = _BatteryMaxAllowableChargePower3, + MaxAllowableDischargePower = _BatteryMaxAllowableDischargePower3, + DailyChargeEnergy = _DailyChargeEnergy3, + DailyDischargeEnergy = _DailyDischargeEnergy3 + }, + + new BatteryRecord + { + Soc = _BatterySoc4, + Soh = _BatterySoh4, + ClusterTotalNumber = _ClusterTotalNumber4, + Current = _BatteryCurrent4, + Voltage = _BatteryVoltage4, + Power = _BatteryPower4, + MaxAllowableChargePower = _BatteryMaxAllowableChargePower4, + MaxAllowableDischargePower = _BatteryMaxAllowableDischargePower4, + DailyChargeEnergy = _DailyChargeEnergy4, + DailyDischargeEnergy = _DailyDischargeEnergy4 + }*/ + }; + public BatteriesRecord? BatteriesRecords => BatteriesRecord.FromBatteries(Batteries); +} diff --git a/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatRecord.Modbus.cs b/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatRecord.Modbus.cs new file mode 100644 index 000000000..d565928c5 --- /dev/null +++ b/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatRecord.Modbus.cs @@ -0,0 +1,180 @@ +using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; + +namespace InnovEnergy.Lib.Devices.WITGrowatt4_15K; + +#pragma warning disable CS0169, CS0649 +[BigEndian] + +// ReSharper disable once InconsistentNaming +public partial class WITGrowatRecord +{ + + /****************************** Input registers ****************************/ + [InputRegister(35, Scale = 0.1)] private Int32 _GridPower; + + // 31000–31009 — Operating Status Info + [InputRegister(31000)] private UInt16 _SystemOperatingMode; + [InputRegister(31001)] private UInt16 _BatteryOperatingMode; + [InputRegister(31002)] private UInt16 _OperatingPriority; + [InputRegister(31003)] private UInt16 _Reserved1; + [InputRegister(31004)] private UInt16 _Reserved2; + [InputRegister(31005)] private UInt16 _FaultMainCode;// Can we change this to warning? + [InputRegister(31006)] private UInt16 _FaultSubCode; // Can we change this to warning? + [InputRegister(31007)] private UInt16 _WarningMainCode; + [InputRegister(31008)] private UInt16 _WarningSubCode; + [InputRegister(31009)] private UInt16 _Reserved3; + // 31010–31099 — PV Parameters + [InputRegister(31010)] private Int16 _Pv1Voltage; + [InputRegister(31011)] private Int16 _Pv1Current; + [InputRegister(31012)] private Int16 _Pv2Voltage; + [InputRegister(31013)] private Int16 _Pv2Current; + [InputRegister(31058)] private Int16 _Pv1InpuPower; + + //— AC Side 31100 - 31199 + [InputRegister(31100, Scale = 0.1)] private Int32 _ActivePower; // Positive: feed to grid, Negative: draw from the grid + [InputRegister(31102, Scale = 0.1)] private Int32 _ReactivePower; // Positive: capacitive, Negative: Inductive + [InputRegister(31104, Scale = 0.1)] private Int16 _Reserved4; + [InputRegister(31105, Scale = 0.01)] private UInt16 _Frequency; + + [InputRegister(31106, Scale = 0.1)] private UInt16 _GridAbLineVoltage; + [InputRegister(31107, Scale = 0.1)] private UInt16 _GridBcLineVoltage; + [InputRegister(31108, Scale = 0.1)] private UInt16 _GridCaLineVoltage; + + [InputRegister(31109, Scale = 0.1)] private Int16 _PhaseACurrent; + [InputRegister(31110, Scale = 0.1)] private Int16 _PhaseBCurrent; + [InputRegister(31111, Scale = 0.1)] private Int16 _PhaseCCurrent; + + [InputRegister(31112)] private Int32 _MeterPower; //Positive: draw from grid; Negative: feed to grid + [InputRegister(31114, Scale = 0.1)] private Int16 _InverterTemperature; // -400, 150 + [InputRegister(31115, Scale = 0.1)] private Int16 _Reserved5; + [InputRegister(31116, Scale = 0.1)] private Int16 _Reserved6; + [InputRegister(31117, Scale = 0.1)] private Int16 _Reserved7; + [InputRegister(31118, Scale = 0.1)] private UInt32 _EnergyToUser; // consumption + [InputRegister(31120, Scale = 0.1)] private UInt32 _TotalEnergyToUser; + [InputRegister(31122, Scale = 0.1)] private UInt32 _EnergyToGrid; // exportation + [InputRegister(31124, Scale = 0.1)] private UInt32 _TotalEnergyToGrid; + + // 31200–31299 — First Battery Cluster Info (incl. BDC and BMS) + [InputRegister(31200)] private Int32 _BatteryPower1; // positive Charge, Negative Discharge + [InputRegister(31202)] private UInt32 _DailyChargeEnergy1; // 0.1 kw + [InputRegister(31204)] private UInt32 _AccumulatedChargeEnergy1; // 0.1kw + [InputRegister(31206)] private UInt32 _DailyDischargeEnergy1; //0.1kw + [InputRegister(31208)] private UInt32 _AccumulatedDishargeEnergy1; // 0.1kw + [InputRegister(31210)] private UInt32 _BatteryMaxAllowableChargePower1; // + [InputRegister(31212)] private UInt32 _BatteryMaxAllowableDischargePower1; // + [InputRegister(31214)] private Int16 _BatteryVoltage1; // + [InputRegister(31215, Scale = 0.1)] private Int32 _BatteryCurrent1; // + [InputRegister(31217)] private UInt16 _BatterySoc1; // + [InputRegister(31218)] private UInt16 _BatterySoh1; // + [InputRegister(31219)] private UInt32 _FullyChargedCapacity; // + [InputRegister(31221)] private UInt32 _BatteryRemainingCapacity; // + [InputRegister(31223)] private Int16 _BatteryAmbientTemperature1; // + [InputRegister(31224)] private UInt16 _BatteryHighestTemperature; // + [InputRegister(31225)] private UInt16 _ClusterTotalNumber1; // + [InputRegister(31226)] private UInt16 _ModulesPerCluster; // + [InputRegister(31227)] private UInt16 _ModuleRatedVoltage; // + [InputRegister(31228)] private UInt16 _ModuleRatedCapacity; // + + + + + // 31300–31399 — Second Battery Cluster Info (incl. BDC and BMS) + /* [InputRegister(31300)] private Int32 _BatteryPower2; // positive Charge, Negative Discharge + [InputRegister(31302)] private UInt32 _DailyChargeEnergy2; + [InputRegister(31304)] private UInt32 _AccumulatedChargeEnergy2; + [InputRegister(31206)] private UInt32 _DailyDischargeEnergy2; //0.1kw + [InputRegister(31308)] private UInt32 _AccumulatedDischargeEnergy2; + [InputRegister(31310)] private UInt32 _BatteryMaxAllowableChargePower2; // + [InputRegister(31312)] private UInt32 _BatteryMaxAllowableDischargePower2; // + [InputRegister(31314)] private Int16 _BatteryVoltage2; // + [InputRegister(31315)] private Int32 _BatteryCurrent2; // + [InputRegister(31317)] private UInt16 _BatterySoc2; // + [InputRegister(31318)] private UInt16 _BatterySoh2; // + //[InputRegister(31325)] public UInt16 _ClusterTotalNumber2; // + + [InputRegister(31323, Scale = 0.1)] private Int16 _BatteryAmbientTemperature2; // */ + + + // 31400–31499 — Third Battery Cluster Info (incl. BDC and BMS) + /* [InputRegister(31400, Scale = 0.1)] private Int32 _BatteryPower3; // positive Charge, Negative Discharge + [InputRegister(31402, Scale = 0.1)] private UInt32 _DailyChargeEnergy3; + [InputRegister(31404, Scale = 0.1)] private UInt32 _DailyDischargeEnergy3; + [InputRegister(31410, Scale = 0.1)] private UInt32 _BatteryMaxAllowableChargePower3; // + [InputRegister(31412, Scale = 0.1)] private UInt32 _BatteryMaxAllowableDischargePower3; // + + [InputRegister(31414, Scale = 0.1)] private Int16 _BatteryVoltage3; // + [InputRegister(31415, Scale = 0.1)] private Int32 _BatteryCurrent3; // + [InputRegister(31417)] private UInt16 _BatterySoc3; // + [InputRegister(31418)] private UInt16 _BatterySoh3; // + [InputRegister(31425)] private UInt16 _ClusterTotalNumber3; // + + // [InputRegister(31423, Scale = 0.1)] public Int16 BatteryAmbientTemperature3; // + + // 31400–31499 — Third Battery Cluster Info (incl. BDC and BMS) + [InputRegister(31500, Scale = 0.1)] private Int32 _BatteryPower4; // positive Charge, Negative Discharge + [InputRegister(31502, Scale = 0.1)] private UInt32 _DailyChargeEnergy4; + [InputRegister(31504, Scale = 0.1)] private UInt32 _DailyDischargeEnergy4; + [InputRegister(31510, Scale = 0.1)] private UInt32 _BatteryMaxAllowableChargePower4; // + [InputRegister(31512, Scale = 0.1)] private UInt32 _BatteryMaxAllowableDischargePower4; // + + [InputRegister(31514, Scale = 0.1)] private Int16 _BatteryVoltage4; // + [InputRegister(31515, Scale = 0.1)] private Int32 _BatteryCurrent4; // + [InputRegister(31517)] private UInt16 _BatterySoc4; // + [InputRegister(31518)] private UInt16 _BatterySoh4; // + [InputRegister(31525)] private UInt16 _ClusterTotalNumber4; // + + // [InputRegister(31523, Scale = 0.1)] public Int16 BatteryAmbientTemperature4; // + + */ + /****************************** Holding registers ****************************/ + + // [HoldingRegister(30000)] private UInt16 _DeviceModel; + [HoldingRegister(30016, Scale = 0.1)] private UInt32 _RatedPower; + [HoldingRegister(30018, Scale = 0.1)] private UInt32 _MaxActivePower; + [HoldingRegister(30028, Scale = 0.1)] private UInt32 _PvInputMaxPower; + [HoldingRegister(30030)] private UInt16 _BatteryType; + + [HoldingRegister(30099)] private UInt16 _VppProtocolVerNumber; + [HoldingRegister(30100, writable: true)] private Boolean _ControlPermession; // 0 Disabled, 1 enabled + [HoldingRegister(30101, writable: true)] private Boolean _EnableCommand; // 0: Off, 1: On; Defaut is 1; not stored, must enable this register to control inverter + [HoldingRegister(30102)] private UInt16 _CountryRegionCode; + [HoldingRegister(30103)] private UInt16 _Reserved8; + [HoldingRegister(30104)] private UInt16 _SystemTime1; + [HoldingRegister(30105)] private UInt16 _SystemTime2; + [HoldingRegister(30106)] private UInt16 _SystemTime3; + [HoldingRegister(30107)] private UInt16 _SystemTime4; + [HoldingRegister(30108)] private UInt16 _SystemTime5; + [HoldingRegister(30109)] private UInt16 _SystemTime6; + [HoldingRegister(30110)] private UInt32 _Reserved9; + [HoldingRegister(30112)] private UInt16 _ComunnicationAddress; + [HoldingRegister(30113)] private UInt16 _CommunicationBaudeRate; + [HoldingRegister(30114)] private UInt16 _Reserved10; + // [HoldingRegister(30115, writable: true)] private Boolean _EnableSyn; //off grid Box : enable = 1, Disable = 0 , Default =0 This is looks like cannot be written + + [HoldingRegister(30151, writable: true)] private UInt16 _ActivePowerPercentDerating; // % [0,100] + //[HoldingRegister(30152, writable: true)] private UInt16 _Reserved11; // + [HoldingRegister(30154, writable: true)] private UInt16 _ActivePowerPercent; // Limit percentage: [0, 100]; Default: 100; takes the smaller value of 30151 and 30154 as actual active limit; Not stored + + [HoldingRegister(30162, Offset = 10000, Scale = 0.0001)] private UInt16 _PowerFactor; // [0, 2000] ∪ [18000, 20000]; Default: 20000; Actual PF = (Register Value - 10000) + [HoldingRegister(30203, writable : true)] private UInt16 _EmsCommunicationFailureTime; // [1,300] TODO to 30 + [HoldingRegister(30204, writable : true)] private Boolean _EnableEmsCommunicationFailureTime; // 0: disabled, 1 = enabled we should enable this TODO + [HoldingRegister(30300)] private UInt16 _BatteryClusterIndex; // [0..3] + + [HoldingRegister(30400 , writable: true)] private UInt32 _BatteryMaxChargePower; // + [HoldingRegister(30402 , writable: true)] private UInt32 _BatteryMaxDischargePower; // + [HoldingRegister(30404, writable: true)] private UInt16 _ChargeCutoffSoc; // + [HoldingRegister(30405, writable: true)] private UInt16 _DischargeCutoffSoc; // + [HoldingRegister(30406, writable: true)] private UInt16 _LoadPriorityDischargeCutoffSoc; // + [HoldingRegister(30407, writable: true)] private Boolean _RemotePowerControl; // + [HoldingRegister(30408, writable: true)] private UInt16 _RemotePowerControlChargeDuration; // + [HoldingRegister(30409, writable: true)] private Int16 _RemoteChargDischargePower; // + [HoldingRegister(30410, writable: true)] private Boolean _AcChargeEnable; // + + [HoldingRegister(30474)] private UInt16 _ActualChargeDischargePowerControlValue; // + [HoldingRegister(30475)] private UInt16 _OffGridDischargeCutoffSoc; // + + [HoldingRegister(30496, writable: true, Scale = 0.1)] private UInt16 _BatteryChargeCutoffVoltage; // + [HoldingRegister(30497, writable: true, Scale = 0.1)] private UInt16 _BatteryDischargeCutoffVoltage; // + [HoldingRegister(30498, writable: true, Scale = 0.1)] private UInt16 _BatteryMaxChargeCurrent; // + [HoldingRegister(30499, writable: true, Scale = 0.1)] private UInt16 _BatteryMaxdischargeCurrent; // +} \ No newline at end of file diff --git a/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatt4-15K.csproj b/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatt4-15K.csproj new file mode 100644 index 000000000..4c370aa2c --- /dev/null +++ b/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatt4-15K.csproj @@ -0,0 +1,14 @@ + + + + + + InnovEnergy.Lib.Devices.WITGrowatt4_15K + + + + + + + +