694 lines
26 KiB
C#
694 lines
26 KiB
C#
// See https://aka.ms/new-console-template for more information
|
||
using System.IO.Compression;
|
||
using System.Reactive.Linq;
|
||
using System.Reactive.Threading.Tasks;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
using Flurl.Http;
|
||
using InnovEnergy.App.KacoCommunication.DataLogging;
|
||
using InnovEnergy.App.KacoCommunication.DataTypes;
|
||
using InnovEnergy.App.KacoCommunication.Devices;
|
||
using InnovEnergy.App.KacoCommunication.ESS;
|
||
using InnovEnergy.App.KacoCommunication.MiddlewareClasses;
|
||
using InnovEnergy.App.KacoCommunication.System;
|
||
using InnovEnergy.App.KacoCommunication.SystemConfig;
|
||
using InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||
using InnovEnergy.Lib.Devices.Kaco92L3;
|
||
using InnovEnergy.Lib.Devices.Kaco92L3.DataType;
|
||
using InnovEnergy.Lib.Devices.PLVario2Meter;
|
||
using InnovEnergy.Lib.Devices.Trumpf.SystemControl;
|
||
using InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes;
|
||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc.Control;
|
||
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||
using InnovEnergy.Lib.Units;
|
||
using InnovEnergy.Lib.Utils;
|
||
using Newtonsoft.Json;
|
||
using DeviceState = InnovEnergy.App.KacoCommunication.Devices.DeviceState;
|
||
using Formatting = Newtonsoft.Json.Formatting;
|
||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||
|
||
namespace InnovEnergy.App.KacoCommunication;
|
||
|
||
internal static class Program
|
||
{
|
||
private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(5);
|
||
private const UInt16 NbrOfFileToConcatenate = 15; // add this to config file
|
||
private static UInt16 _fileCounter = 0;
|
||
|
||
|
||
private static SodistoreAlarmState _prevSodiohomeAlarmState = SodistoreAlarmState.Green;
|
||
private static SodistoreAlarmState _sodiAlarmState = SodistoreAlarmState.Green;
|
||
|
||
|
||
private static readonly IReadOnlyList<Byte> BatteryNodes;
|
||
private static readonly Channel KacoChannel;
|
||
private static readonly Channel GridMeterChannel;
|
||
private static readonly Channel DcDcChannel;
|
||
|
||
private const String Port1Cabinet = "/dev/ttyUSB0"; // move to a config file
|
||
private const String Port2Cabinet = "/dev/ttyUSB1"; // move to a config file
|
||
private const String Port3Cabinet = "/dev/ttyUSB2"; // move to a config file
|
||
|
||
private static readonly String SwVersionNumber = " V1.00." + DateTime.Today;
|
||
private const String VpnServerIp = "10.2.0.11";
|
||
public static Boolean _subscribedToQueue = false;
|
||
public static Boolean _subscribeToQueueForTheFirstTime = false;
|
||
|
||
private static Int32 _failsCounter = 0; // move to a config file
|
||
// private static SodistoreAlarmState _prevSodiohomeAlarmState = SodistoreAlarmState.Green;
|
||
// private static SodistoreAlarmState _sodiohomeAlarmState = SodistoreAlarmState.Green;
|
||
|
||
static Program()
|
||
{
|
||
var config = Config.Load();
|
||
var d = config.Devices;
|
||
|
||
Channel CreateChannel(SalimaxDevice device) => device.DeviceState == DeviceState.Disabled
|
||
? new NullChannel()
|
||
: new TcpChannel(device);
|
||
|
||
BatteryNodes = config
|
||
.Devices
|
||
.BatteryNodes
|
||
.Select(n => n.ConvertTo<Byte>())
|
||
.ToArray(config.Devices.BatteryNodes.Length);
|
||
|
||
KacoChannel = CreateChannel(d.KacoIp);
|
||
GridMeterChannel = CreateChannel(d.GridMeterIp);
|
||
DcDcChannel = CreateChannel(d.DcDcIp);
|
||
}
|
||
|
||
|
||
public static async Task Main(String[] args)
|
||
{
|
||
while (true)
|
||
{
|
||
try
|
||
{
|
||
await Run();
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
e.LogError();
|
||
}
|
||
}
|
||
// ReSharper disable once FunctionNeverReturns
|
||
}
|
||
|
||
private static async Task Run()
|
||
{
|
||
Watchdog.NotifyReady();
|
||
|
||
Console.WriteLine("Starting Kaco Communication");
|
||
|
||
var kacoDevice = new KacoDevice(KacoChannel);
|
||
var gridMeterDevice = new PlVarioMeterDevice(GridMeterChannel);
|
||
var dcDcDevices = new TruConvertDcDcDevices(DcDcChannel);
|
||
|
||
var firstCabinetBatteriesDevice =
|
||
BatteryNodes.Select(n => new BatteryDeligreenDevice(Port1Cabinet, n)).ToList();
|
||
var secondCabinetBatteriesDevice =
|
||
BatteryNodes.Select(n => new BatteryDeligreenDevice(Port2Cabinet, n)).ToList();
|
||
var thirdCabinetBatteriesDevice =
|
||
BatteryNodes.Select(n => new BatteryDeligreenDevice(Port3Cabinet, n)).ToList();
|
||
|
||
var batteryDevices1 = new BatteryDeligreenDevices(firstCabinetBatteriesDevice);
|
||
var batteryDevices2 = new BatteryDeligreenDevices(secondCabinetBatteriesDevice);
|
||
var batteryDevices3 = new BatteryDeligreenDevices(thirdCabinetBatteriesDevice);
|
||
|
||
StatusRecord? ReadStatus()
|
||
{
|
||
PlVarioMeterRecord? gridRecord = null;
|
||
var config = Config.Load();
|
||
var kacoRecord = kacoDevice.Read();
|
||
var gridrawRecord = gridMeterDevice.Read();
|
||
var dcDcRecord = dcDcDevices.Read();
|
||
|
||
if (gridrawRecord != null)
|
||
{
|
||
gridRecord = new PlVarioMeterRecord(gridrawRecord);
|
||
}
|
||
|
||
var batteryKabinet1 = batteryDevices1.Read();
|
||
var batteryKabinet2 = batteryDevices2.Read();
|
||
var batteryKabinet3 = batteryDevices3.Read();
|
||
|
||
var listOfBatteriesRecord = new List<BatteryDeligreenRecords>();
|
||
if (batteryKabinet1 != null) listOfBatteriesRecord.Add(batteryKabinet1);
|
||
if (batteryKabinet2 != null) listOfBatteriesRecord.Add(batteryKabinet2);
|
||
if (batteryKabinet3 != null) listOfBatteriesRecord.Add(batteryKabinet3);
|
||
|
||
|
||
return new StatusRecord
|
||
{
|
||
InverterRecord = kacoRecord,
|
||
GridMeterRecord = gridRecord,
|
||
DcDc = dcDcRecord,
|
||
ListOfBatteriesRecord = listOfBatteriesRecord,
|
||
StateMachine = StateMachine.Default,
|
||
|
||
Config = config // load from disk every iteration, so config can be changed while running
|
||
};
|
||
}
|
||
|
||
while (true)
|
||
{
|
||
await Observable
|
||
.Interval(UpdateInterval)
|
||
.Select(_ => RunIteration())
|
||
.SelectMany(status =>
|
||
DataLogging(status, DateTime.Now.Round(UpdateInterval))
|
||
.ContinueWith(_ => status)) // back to StatusRecord
|
||
.SelectMany(SaveModbusTcpFile)
|
||
.SelectError()
|
||
.ToTask();
|
||
}
|
||
|
||
|
||
StatusRecord? RunIteration()
|
||
{
|
||
try
|
||
{
|
||
Watchdog.NotifyAlive();
|
||
|
||
var startTime = DateTime.Now;
|
||
Console.WriteLine(
|
||
"***************************** Reading Kaco Data *********************************************");
|
||
Console.WriteLine(startTime.ToString("HH:mm:ss.fff"));
|
||
// the order matter of the next three lines
|
||
var statusrecord = ReadStatus();
|
||
statusrecord?.CreateSimpleTopologyTextBlock().WriteLine();
|
||
|
||
statusrecord?.StateMachine.State.WriteLine(" state");
|
||
statusrecord?.StateMachine.Message.WriteLine(" Message");
|
||
|
||
statusrecord?.InverterRecord?.BatteryLimitsEnable.WriteLine(" BatteryLimitsEnable");
|
||
|
||
Console.WriteLine(" **************** DcDc **********************");
|
||
statusrecord?.DcDc?.Dc.Battery.Power .WriteLine(" DC Battery Power");
|
||
statusrecord?.DcDc?.Dc.Battery.Voltage .WriteLine("DC Battery Voltage");
|
||
statusrecord?.DcDc?.Dc.Battery.Current .WriteLine("DC Battery Current");
|
||
statusrecord?.DcDc?.Dc.Link.Voltage .WriteLine(" Dc link Voltage");
|
||
|
||
Console.WriteLine(" ********************************* Kaco Inverter *********************************");
|
||
|
||
statusrecord?.InverterRecord?.ActivePowerW.WriteLine(" Inverter Power");
|
||
statusrecord?.InverterRecord?.CurrentState.WriteLine(" CurrentState");
|
||
statusrecord?.InverterRecord?.RequestedState.WriteLine(" RequestedState");
|
||
statusrecord?.InverterRecord?.PcuError.WriteLine(" PcuError");
|
||
statusrecord?.InverterRecord?.PcuState.WriteLine(" PcuState");
|
||
|
||
statusrecord?.InverterRecord?.MinDischargeVoltage.WriteLine(" MinDischargeVoltage");
|
||
statusrecord?.InverterRecord?.MaxDischargeCurrent.WriteLine(" MaxDischargeCurrent");
|
||
statusrecord?.InverterRecord?.DischargeCutoffCurrent.WriteLine(" DischargeCutoffCurrent");
|
||
|
||
statusrecord?.InverterRecord?.MaxChargeVoltage.WriteLine(" MaxChargeVoltage");
|
||
statusrecord?.InverterRecord?.MaxChargeCurrent.WriteLine(" MaxChargeCurrent");
|
||
statusrecord?.InverterRecord?.ChargeCutoffCurrent.WriteLine(" ChargeCutoffCurrent");
|
||
statusrecord?.InverterRecord?.ActivePowerSetPercent.WriteLine( "ActivePowerSetPercent");
|
||
|
||
statusrecord?.ControlSystemState();
|
||
var i = 0;
|
||
foreach (var d in statusrecord.DcDc.Devices)
|
||
{
|
||
i++;
|
||
Console.WriteLine("before DcDc is " + i + d.Control.PowerStageEnable);
|
||
d.Control.ResetAlarmsAndWarnings = true;
|
||
d.Control.PowerStageEnable = true;
|
||
}
|
||
|
||
|
||
statusrecord?.DcDc?.SystemControl.ApplyDcDcDefaultSettings();
|
||
InitializeKacoStartup(statusrecord);
|
||
foreach (var d in statusrecord.DcDc.Devices)
|
||
{
|
||
Console.WriteLine("After DcDc is " + d.Control.PowerStageEnable);
|
||
}
|
||
|
||
|
||
Console.WriteLine(" ************************************ We are writing ************************************");
|
||
|
||
statusrecord?.Config.Save(); // save the config file
|
||
if (statusrecord?.InverterRecord != null) kacoDevice.Write(statusrecord.InverterRecord);
|
||
|
||
return statusrecord;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
// Handle exception and print the error
|
||
Console.WriteLine(e);
|
||
return null;
|
||
}
|
||
}
|
||
}
|
||
|
||
private static async Task SavingLocalCsvFile(Int64 timestamp, String csv)
|
||
{
|
||
var directoryPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "csvFiles");
|
||
// 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> DataLogging(StatusRecord status, DateTime timeStamp)
|
||
{
|
||
var csv = status.ToCsv();
|
||
|
||
// for debug, only to be deleted.
|
||
//foreach (var item in csv.SplitLines())
|
||
//{
|
||
// Console.WriteLine(item + "");
|
||
//}
|
||
|
||
await SavingLocalCsvFile(timeStamp.ToUnixTime(), csv);
|
||
|
||
var jsonData = new Dictionary<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.ToUnixTime(), 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 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 void Heartbit()
|
||
{
|
||
var s3Bucket = Config.Load().S3?.Bucket;
|
||
var tryParse = int.TryParse(s3Bucket?.Split("-")[0], out var installationId);
|
||
if (tryParse)
|
||
{
|
||
var returnedStatus = new StatusMessage
|
||
{
|
||
InstallationId = installationId,
|
||
Product = 3,
|
||
Status = _sodiAlarmState,
|
||
Type = MessageType.Heartbit,
|
||
};
|
||
if (s3Bucket != null)
|
||
RabbitMqManager.InformMiddleware(returnedStatus);
|
||
}
|
||
}
|
||
|
||
private static async Task SaveToLocalCompressedFallback(Byte[] compressedData, String fileNameWithoutExtension)
|
||
{
|
||
try
|
||
{
|
||
var fallbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FailedUploads");
|
||
Directory.CreateDirectory(fallbackDir);
|
||
|
||
var fileName = fileNameWithoutExtension + ".json"; // Save as .json, but still compressed
|
||
var fullPath = Path.Combine(fallbackDir, fileName);
|
||
|
||
await File.WriteAllBytesAsync(fullPath, compressedData); // Compressed data
|
||
Console.WriteLine($"Saved compressed failed upload to: {fullPath}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine("Failed to save compressed file locally: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
private static 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 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 async Task<Boolean> SaveModbusTcpFile(StatusRecord status)
|
||
{
|
||
var modbusData = new Dictionary<String, UInt16>();
|
||
|
||
// SYSTEM DATA
|
||
var result1 =
|
||
ConvertToModbusRegisters((status.Config.MinSoc * 10), "UInt16",
|
||
30001); // this to be updated to modbusTCP version
|
||
var result2 = ConvertToModbusRegisters(status.InverterRecord!.PcuError, "UInt32", 30002);
|
||
|
||
// Merge all results into one dictionary
|
||
|
||
var allResults = new[]
|
||
{
|
||
result1, result2
|
||
};
|
||
|
||
foreach (var result in allResults)
|
||
{
|
||
foreach (var entry in result)
|
||
{
|
||
modbusData[entry.Key] = entry.Value;
|
||
}
|
||
}
|
||
|
||
// Write to JSON
|
||
var json = JsonSerializer.Serialize(modbusData, new JsonSerializerOptions { WriteIndented = true });
|
||
await File.WriteAllTextAsync("/home/inesco/SodiStoreHome/ModbusTCP/modbus_tcp_data.json", json);
|
||
|
||
//Console.WriteLine("JSON file written successfully.");
|
||
//Console.WriteLine(json);
|
||
var stopTime = DateTime.Now;
|
||
Console.WriteLine(stopTime.ToString("HH:mm:ss.fff") + " Finish the loop");
|
||
return true;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
private static void ForAll<T>(this IEnumerable<T> ts, Action<T> action)
|
||
{
|
||
foreach (var t in ts)
|
||
action(t);
|
||
}
|
||
|
||
private static void InitializeKacoStartup(StatusRecord? statusRecord)
|
||
{
|
||
//
|
||
// 1. Apply DC – This part is physical and cannot be done in software.
|
||
// We assume DC power is already present.
|
||
//
|
||
Console.WriteLine("1. Apply DC");
|
||
//
|
||
statusRecord?.DcDc?.Devices
|
||
.Select(d => d.Control)
|
||
.ForAll(c => c.PowerStageEnable = true);
|
||
|
||
statusRecord?.DcDc?.Devices
|
||
.Select(d => d.Control )
|
||
.ForAll(c => c.ControlMode = DcControlMode.VoltageDroop);
|
||
//
|
||
// // 2. Send valid battery limits (Model 64202)
|
||
// All values temporarily set to "1" as requested.
|
||
// You will replace them later with real values.
|
||
//
|
||
|
||
Console.WriteLine("2. Send real value");
|
||
|
||
if (statusRecord?.InverterRecord != null)
|
||
{
|
||
statusRecord.InverterRecord.MinDischargeVoltage = 700f; // 64202.DisMinV
|
||
statusRecord.InverterRecord.MaxDischargeCurrent = 140f; // 64202.DisMaxA
|
||
statusRecord.InverterRecord.DischargeCutoffCurrent = 10f; // 64202.DisCutoffA
|
||
|
||
statusRecord.InverterRecord.MaxChargeVoltage = 800f; // 64202.ChaMaxV
|
||
statusRecord.InverterRecord.MaxChargeCurrent = 140f; // 64202.ChaMaxA
|
||
statusRecord.InverterRecord.ChargeCutoffCurrent = 10f; // 64202.ChaCutoffA
|
||
|
||
statusRecord.InverterRecord.WatchdogSeconds = 30; // this is additional from my seid
|
||
|
||
//
|
||
// 3. Enable limits (EnLimit)
|
||
|
||
Console.WriteLine("3. Enable limits ");
|
||
statusRecord.InverterRecord.BatteryLimitsEnable = EnableDisableEnum.Enabled;
|
||
|
||
//
|
||
// After writing all values in software, send them to the inverter
|
||
//
|
||
//
|
||
// 4. Read model 64201 to observe CurrentState transition
|
||
//
|
||
// Expected sequence:
|
||
// - Before valid limits: CurrentState == 7 (ERROR)
|
||
// - After valid limits: CurrentState == 8 (STANDBY)
|
||
// - Then after grid/DC conditions: CurrentState == 1 (OFF) or 11 (GRID_CONNECTED)
|
||
//
|
||
|
||
Console.WriteLine("3. Read current state");
|
||
|
||
|
||
var state = statusRecord.InverterRecord.CurrentState;
|
||
|
||
Console.WriteLine($"KACO 64201.CurrentState = {state}");
|
||
|
||
switch (state)
|
||
{
|
||
case CurrentState.Standby:
|
||
|
||
Console.WriteLine("Device is in STANDBY (8) — battery limits accepted.");
|
||
break;
|
||
|
||
case CurrentState.Off:
|
||
Console.WriteLine("Device is OFF (1) — OK for non-battery operation.");
|
||
break;
|
||
|
||
case CurrentState.GridConnected:
|
||
Console.WriteLine("Device is GRID CONNECTED (11).");
|
||
break;
|
||
|
||
default:
|
||
Console.WriteLine("Device in unexpected state: " + state);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
private static void ApplyDcDcDefaultSettings(this SystemControlRegisters? sc)
|
||
{
|
||
|
||
if (sc is null)
|
||
return;
|
||
|
||
sc.SystemConfig = Lib.Devices.Trumpf.SystemControl.DataTypes.SystemConfig.DcDcOnly;
|
||
sc.CommunicationTimeout = TimeSpan.FromSeconds(20);
|
||
|
||
sc.PowerSetPointActivation = PowerSetPointActivation.Immediate;
|
||
sc.UseSlaveIdForAddressing = true;
|
||
sc.SlaveErrorHandling = SlaveErrorHandling.Relaxed;
|
||
sc.SubSlaveErrorHandling = SubSlaveErrorHandling.Off;
|
||
sc.TargetSlave = 0;
|
||
sc.ResetAlarmsAndWarnings = true;
|
||
}
|
||
|
||
private static void InsertIntoJson(Dictionary<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];
|
||
}
|
||
}
|
||
}
|
||
} |