Create the new Growatt project.
Src and library files added
This commit is contained in:
parent
bea24fde97
commit
6343af3468
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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>(TState state) where TState : notnull => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Boolean IsEnabled(LogLevel logLevel) => true; // Enable logging for all levels
|
||||||
|
|
||||||
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception, String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<T>(this T t) where T : notnull
|
||||||
|
{
|
||||||
|
_logger.LogInformation(t.ToString()); // TODO: check warning
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T LogDebug<T>(this T t) where T : notnull
|
||||||
|
{
|
||||||
|
// _logger.LogDebug(t.ToString()); // TODO: check warning
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T LogError<T>(this T t) where T : notnull
|
||||||
|
{
|
||||||
|
// _logger.LogError(t.ToString()); // TODO: check warning
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T LogWarning<T>(this T t) where T : notnull
|
||||||
|
{
|
||||||
|
// _logger.LogWarning(t.ToString()); // TODO: check warning
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace InnovEnergy.App.GrowattCommunication.DataTypes;
|
||||||
|
|
||||||
|
public enum SodistoreAlarmState
|
||||||
|
{
|
||||||
|
Green,
|
||||||
|
Orange,
|
||||||
|
Red
|
||||||
|
}
|
||||||
|
|
@ -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<AlarmOrWarning>? Warnings { get; set; }
|
||||||
|
public List<AlarmOrWarning>? Alarms { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum MessageType
|
||||||
|
{
|
||||||
|
AlarmOrWarning,
|
||||||
|
Heartbit
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<RootNamespace>InnovEnergy.App.GrowattCommunication</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="../InnovEnergy.App.props" />
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\Lib\Devices\WITGrowatt4-15K\WITGrowatt4-15K.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Lib\Protocols\Modbus\Modbus.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Lib\Units\Units.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Lib\Utils\Utils.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Flurl.Http" Version="4.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -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 = "<Pending>")]
|
||||||
|
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<AlarmOrWarning>();
|
||||||
|
var warningList = new List<AlarmOrWarning>();
|
||||||
|
|
||||||
|
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<String, UInt16> ConvertToModbusRegisters(Object value, String outputType, Int32 startingAddress)
|
||||||
|
{
|
||||||
|
var registers = new Dictionary<String, UInt16>();
|
||||||
|
|
||||||
|
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<string, ushort>>(System.Collections.Generic.Dictionary<string, ushort>, System.Text.Json.JsonSerializerOptions?)")]
|
||||||
|
private static async Task SaveModbusTcpFile(StatusRecord status)
|
||||||
|
{
|
||||||
|
var modbusData = new Dictionary<String, UInt16>();
|
||||||
|
|
||||||
|
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<Boolean> 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<String, Object>();
|
||||||
|
|
||||||
|
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<String, Object> 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<String, Object> 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<String, Object>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String, Object>)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<Boolean> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Config>(jsonString)!;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
$"Failed to read config file {configFilePath}, using default config\n{e}".WriteLine();
|
||||||
|
return Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<Config> LoadAsync(String? path = null)
|
||||||
|
{
|
||||||
|
var configFilePath = path ?? DefaultConfigFilePath;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jsonString = await File.ReadAllTextAsync(configFilePath);
|
||||||
|
return Deserialize<Config>(jsonString)!;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Couldn't read config file {configFilePath}, using default config");
|
||||||
|
e.Message.WriteLine();
|
||||||
|
return Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -99,6 +99,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeligreenBatteryCommunicati
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SodiStoreMax", "App\SodiStoreMax\SodiStoreMax.csproj", "{39B83793-49DB-4940-9C25-A7F944607407}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SodiStoreMax", "App\SodiStoreMax\SodiStoreMax.csproj", "{39B83793-49DB-4940-9C25-A7F944607407}"
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{39B83793-49DB-4940-9C25-A7F944607407}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{CF4834CB-91B7-4172-AC13-ECDA8613CD17} = {145597B4-3E30-45E6-9F72-4DD43194539A}
|
{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}
|
{1045AC74-D4D8-4581-AAE3-575DF26060E6} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
|
||||||
{11ED6871-5B7D-462F-8710-B5D85DEC464A} = {145597B4-3E30-45E6-9F72-4DD43194539A}
|
{11ED6871-5B7D-462F-8710-B5D85DEC464A} = {145597B4-3E30-45E6-9F72-4DD43194539A}
|
||||||
{39B83793-49DB-4940-9C25-A7F944607407} = {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
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
||||||
|
|
@ -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<BatteryRecord> 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<BatteryRecord>? 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))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace InnovEnergy.Lib.Devices.WITGrowatt4_15K.DataType;
|
||||||
|
|
||||||
|
public enum OperatingPriority
|
||||||
|
{
|
||||||
|
LoadPriority = 0,
|
||||||
|
BatteryPriority = 1,
|
||||||
|
GridPriority = 2,
|
||||||
|
}
|
||||||
|
|
@ -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<WITGrowatRecord>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<BatteryRecord> 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<BatteryRecord> Batteries => new List<BatteryRecord>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
@ -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<Int32>(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<Int32>(31100, Scale = 0.1)] private Int32 _ActivePower; // Positive: feed to grid, Negative: draw from the grid
|
||||||
|
[InputRegister<Int32>(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<Int32>(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<UInt32>(31118, Scale = 0.1)] private UInt32 _EnergyToUser; // consumption
|
||||||
|
[InputRegister<UInt32>(31120, Scale = 0.1)] private UInt32 _TotalEnergyToUser;
|
||||||
|
[InputRegister<UInt32>(31122, Scale = 0.1)] private UInt32 _EnergyToGrid; // exportation
|
||||||
|
[InputRegister<UInt32>(31124, Scale = 0.1)] private UInt32 _TotalEnergyToGrid;
|
||||||
|
|
||||||
|
// 31200–31299 — First Battery Cluster Info (incl. BDC and BMS)
|
||||||
|
[InputRegister<Int32>(31200)] private Int32 _BatteryPower1; // positive Charge, Negative Discharge
|
||||||
|
[InputRegister<UInt32>(31202)] private UInt32 _DailyChargeEnergy1; // 0.1 kw
|
||||||
|
[InputRegister<UInt32>(31204)] private UInt32 _AccumulatedChargeEnergy1; // 0.1kw
|
||||||
|
[InputRegister<UInt32>(31206)] private UInt32 _DailyDischargeEnergy1; //0.1kw
|
||||||
|
[InputRegister<UInt32>(31208)] private UInt32 _AccumulatedDishargeEnergy1; // 0.1kw
|
||||||
|
[InputRegister<UInt32>(31210)] private UInt32 _BatteryMaxAllowableChargePower1; //
|
||||||
|
[InputRegister<UInt32>(31212)] private UInt32 _BatteryMaxAllowableDischargePower1; //
|
||||||
|
[InputRegister(31214)] private Int16 _BatteryVoltage1; //
|
||||||
|
[InputRegister<Int32>(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<Int32>(31300)] private Int32 _BatteryPower2; // positive Charge, Negative Discharge
|
||||||
|
[InputRegister<UInt32>(31302)] private UInt32 _DailyChargeEnergy2;
|
||||||
|
[InputRegister<UInt32>(31304)] private UInt32 _AccumulatedChargeEnergy2;
|
||||||
|
[InputRegister<UInt32>(31206)] private UInt32 _DailyDischargeEnergy2; //0.1kw
|
||||||
|
[InputRegister<UInt32>(31308)] private UInt32 _AccumulatedDischargeEnergy2;
|
||||||
|
[InputRegister<UInt32>(31310)] private UInt32 _BatteryMaxAllowableChargePower2; //
|
||||||
|
[InputRegister<UInt32>(31312)] private UInt32 _BatteryMaxAllowableDischargePower2; //
|
||||||
|
[InputRegister(31314)] private Int16 _BatteryVoltage2; //
|
||||||
|
[InputRegister<Int32>(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<UInt32>(30016, Scale = 0.1)] private UInt32 _RatedPower;
|
||||||
|
[HoldingRegister<UInt32>(30018, Scale = 0.1)] private UInt32 _MaxActivePower;
|
||||||
|
[HoldingRegister<UInt32>(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<UInt32>(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<UInt32>(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<UInt32>(30400 , writable: true)] private UInt32 _BatteryMaxChargePower; //
|
||||||
|
[HoldingRegister<UInt32>(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<Int16>(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; //
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<Import Project="../../InnovEnergy.Lib.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<RootNamespace>InnovEnergy.Lib.Devices.WITGrowatt4_15K</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\Protocols\Modbus\Modbus.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Units\Units.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Loading…
Reference in New Issue