Compare commits

..

3 Commits

Author SHA1 Message Date
atef d81ef73bcf Merge remote-tracking branch 'origin/main' 2026-02-27 11:16:56 +01:00
atef cbd4801568 Add Machine state Controller 2026-02-27 11:15:04 +01:00
atef da9d2b1f40 Add The DCDC control to Kaco project.
Add the Three kabinet to our Battery Reading.
Add ESS and Machine state for SodiStoreGrid
2026-02-27 11:10:55 +01:00
8 changed files with 589 additions and 134 deletions

View File

@ -0,0 +1,6 @@
namespace InnovEnergy.App.KacoCommunication.ESS;
public enum EssMode
{
}

View File

@ -1,3 +1,4 @@
using InnovEnergy.App.KacoCommunication.System;
using InnovEnergy.App.KacoCommunication.SystemConfig; using InnovEnergy.App.KacoCommunication.SystemConfig;
using InnovEnergy.Lib.Devices.BatteryDeligreen; using InnovEnergy.Lib.Devices.BatteryDeligreen;
using InnovEnergy.Lib.Devices.Kaco92L3; using InnovEnergy.Lib.Devices.Kaco92L3;
@ -5,19 +6,19 @@ using InnovEnergy.Lib.Devices.PLVario2Meter;
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc; using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
namespace InnovEnergy.App.KacoCommunication.ESS; namespace InnovEnergy.App.KacoCommunication.ESS;
using ListOfBatteriesRecord = List<BatteryDeligreenRecords>;
public class StatusRecord public class StatusRecord
{ {
public required KacoRecord? InverterRecord { get; set; } public required KacoRecord? InverterRecord { get; set; }
public required PlVarioMeterRecord? GridMeterRecord { get; set; } public required PlVarioMeterRecord? GridMeterRecord { get; set; }
public required DcDcDevicesRecord? DcDc { get; init; } public required DcDcDevicesRecord? DcDc { get; init; }
public required ListOfBatteriesRecord? ListOfBatteriesRecord { get; set; }
// public required BatteryDeligreenRecords? BatteryKabinet1 { get; set; }
// public required BatteryDeligreenRecords? BatteryKabinet2 { get; set; }
// public required BatteryDeligreenRecords? BatteryKabinet3 { get; set; }
public required BatteryDeligreenRecords? BatteryKabinet1 { get; set; } public required Config Config { get; set; }
public required BatteryDeligreenRecords? BatteryKabinet2 { get; set; } public required StateMachine StateMachine { get; init; }
public required BatteryDeligreenRecords? BatteryKabinet3 { get; set; }
public required Config Config { get; set; }
} }

View File

@ -1,5 +1,4 @@
// See https://aka.ms/new-console-template for more information // See https://aka.ms/new-console-template for more information
using System.IO.Compression; using System.IO.Compression;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Threading.Tasks; using System.Reactive.Threading.Tasks;
@ -11,16 +10,21 @@ using InnovEnergy.App.KacoCommunication.DataTypes;
using InnovEnergy.App.KacoCommunication.Devices; using InnovEnergy.App.KacoCommunication.Devices;
using InnovEnergy.App.KacoCommunication.ESS; using InnovEnergy.App.KacoCommunication.ESS;
using InnovEnergy.App.KacoCommunication.MiddlewareClasses; using InnovEnergy.App.KacoCommunication.MiddlewareClasses;
using InnovEnergy.App.KacoCommunication.System;
using InnovEnergy.App.KacoCommunication.SystemConfig; using InnovEnergy.App.KacoCommunication.SystemConfig;
using InnovEnergy.Lib.Devices.BatteryDeligreen; using InnovEnergy.Lib.Devices.BatteryDeligreen;
using InnovEnergy.Lib.Devices.Kaco92L3; using InnovEnergy.Lib.Devices.Kaco92L3;
using InnovEnergy.Lib.Devices.Kaco92L3.DataType; using InnovEnergy.Lib.Devices.Kaco92L3.DataType;
using InnovEnergy.Lib.Devices.PLVario2Meter; 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;
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc.Control;
using InnovEnergy.Lib.Protocols.Modbus.Channels; using InnovEnergy.Lib.Protocols.Modbus.Channels;
using InnovEnergy.Lib.Units; using InnovEnergy.Lib.Units;
using InnovEnergy.Lib.Utils; using InnovEnergy.Lib.Utils;
using Newtonsoft.Json; using Newtonsoft.Json;
using DeviceState = InnovEnergy.App.KacoCommunication.Devices.DeviceState;
using Formatting = Newtonsoft.Json.Formatting; using Formatting = Newtonsoft.Json.Formatting;
using JsonSerializer = System.Text.Json.JsonSerializer; using JsonSerializer = System.Text.Json.JsonSerializer;
@ -30,26 +34,25 @@ internal static class Program
{ {
private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(5); private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(5);
private const UInt16 NbrOfFileToConcatenate = 15; // add this to config file private const UInt16 NbrOfFileToConcatenate = 15; // add this to config file
private static UInt16 _fileCounter = 0; private static UInt16 _fileCounter = 0;
private static SodistoreAlarmState _prevSodiohomeAlarmState = SodistoreAlarmState.Green;
private static SodistoreAlarmState _sodiAlarmState = SodistoreAlarmState.Green;
private static SodistoreAlarmState _prevSodiohomeAlarmState = SodistoreAlarmState.Green;
private static SodistoreAlarmState _sodiAlarmState = SodistoreAlarmState.Green;
private static readonly IReadOnlyList<Byte> BatteryNodes; private static readonly IReadOnlyList<Byte> BatteryNodes;
private static readonly Channel KacoChannel; private static readonly Channel KacoChannel;
private static readonly Channel GridMeterChannel; private static readonly Channel GridMeterChannel;
private static readonly Channel DcDcChannel; private static readonly Channel DcDcChannel;
private const String Port1Cabinet = "/dev/ttyUSB0"; // move to a config file 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 Port2Cabinet = "/dev/ttyUSB1"; // move to a config file
private const String Port3Cabinet = "/dev/ttyUSB2"; // 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 static readonly String SwVersionNumber = " V1.00." + DateTime.Today;
private const String VpnServerIp = "10.2.0.11"; private const String VpnServerIp = "10.2.0.11";
public static Boolean _subscribedToQueue = false; public static Boolean _subscribedToQueue = false;
public static Boolean _subscribeToQueueForTheFirstTime = false; public static Boolean _subscribeToQueueForTheFirstTime = false;
private static Int32 _failsCounter = 0; // move to a config file private static Int32 _failsCounter = 0; // move to a config file
@ -65,22 +68,20 @@ internal static class Program
? new NullChannel() ? new NullChannel()
: new TcpChannel(device); : new TcpChannel(device);
BatteryNodes = config BatteryNodes = config
.Devices .Devices
.BatteryNodes .BatteryNodes
.Select(n => n.ConvertTo<Byte>()) .Select(n => n.ConvertTo<Byte>())
.ToArray(config.Devices.BatteryNodes.Length); .ToArray(config.Devices.BatteryNodes.Length);
KacoChannel = CreateChannel(d.KacoIp); KacoChannel = CreateChannel(d.KacoIp);
GridMeterChannel = CreateChannel(d.GridMeterIp); GridMeterChannel = CreateChannel(d.GridMeterIp);
DcDcChannel = CreateChannel(d.DcDcIp); DcDcChannel = CreateChannel(d.DcDcIp);
} }
public static async Task Main(String[] args) public static async Task Main(String[] args)
{ {
while (true) while (true)
{ {
try try
@ -89,7 +90,7 @@ internal static class Program
} }
catch (Exception e) catch (Exception e)
{ {
// e.LogError(); e.LogError();
} }
} }
// ReSharper disable once FunctionNeverReturns // ReSharper disable once FunctionNeverReturns
@ -105,39 +106,49 @@ internal static class Program
var gridMeterDevice = new PlVarioMeterDevice(GridMeterChannel); var gridMeterDevice = new PlVarioMeterDevice(GridMeterChannel);
var dcDcDevices = new TruConvertDcDcDevices(DcDcChannel); var dcDcDevices = new TruConvertDcDcDevices(DcDcChannel);
var firstCabinetBatteriesDevice = BatteryNodes.Select(n => new BatteryDeligreenDevice(Port1Cabinet, n)).ToList(); var firstCabinetBatteriesDevice =
var secondCabinetBatteriesDevice = BatteryNodes.Select(n => new BatteryDeligreenDevice(Port2Cabinet, n)).ToList(); BatteryNodes.Select(n => new BatteryDeligreenDevice(Port1Cabinet, n)).ToList();
var thirdCabinetBatteriesDevice = BatteryNodes.Select(n => new BatteryDeligreenDevice(Port3Cabinet, n)).ToList(); var secondCabinetBatteriesDevice =
BatteryNodes.Select(n => new BatteryDeligreenDevice(Port2Cabinet, n)).ToList();
var batteryDevices1 = new BatteryDeligreenDevices(firstCabinetBatteriesDevice); var thirdCabinetBatteriesDevice =
var batteryDevices2 = new BatteryDeligreenDevices(secondCabinetBatteriesDevice); BatteryNodes.Select(n => new BatteryDeligreenDevice(Port3Cabinet, n)).ToList();
var batteryDevices3 = new BatteryDeligreenDevices(thirdCabinetBatteriesDevice);
var batteryDevices1 = new BatteryDeligreenDevices(firstCabinetBatteriesDevice);
var batteryDevices2 = new BatteryDeligreenDevices(secondCabinetBatteriesDevice);
var batteryDevices3 = new BatteryDeligreenDevices(thirdCabinetBatteriesDevice);
StatusRecord? ReadStatus() StatusRecord? ReadStatus()
{ {
PlVarioMeterRecord? gridRecord = null; PlVarioMeterRecord? gridRecord = null;
var config = Config.Load(); var config = Config.Load();
var kacoRecord = kacoDevice.Read(); var kacoRecord = kacoDevice.Read();
var gridrawRecord = gridMeterDevice.Read(); var gridrawRecord = gridMeterDevice.Read();
var dcDcRecord = dcDcDevices.Read(); var dcDcRecord = dcDcDevices.Read();
if (gridrawRecord != null) if (gridrawRecord != null)
{ {
gridRecord = new PlVarioMeterRecord(gridrawRecord); gridRecord = new PlVarioMeterRecord(gridrawRecord);
} }
var batteryKabinet1 = batteryDevices1.Read(); var batteryKabinet1 = batteryDevices1.Read();
var batteryKabinet2 = batteryDevices2.Read(); var batteryKabinet2 = batteryDevices2.Read();
var batteryKabinet3 = batteryDevices3.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 return new StatusRecord
{ {
InverterRecord = kacoRecord, InverterRecord = kacoRecord,
GridMeterRecord = gridRecord, GridMeterRecord = gridRecord,
DcDc = dcDcRecord, DcDc = dcDcRecord,
BatteryKabinet1 = batteryKabinet1, ListOfBatteriesRecord = listOfBatteriesRecord,
BatteryKabinet2 = batteryKabinet2, StateMachine = StateMachine.Default,
BatteryKabinet3 = batteryKabinet3,
Config = config // load from disk every iteration, so config can be changed while running Config = config // load from disk every iteration, so config can be changed while running
}; };
} }
@ -153,7 +164,7 @@ internal static class Program
.SelectError() .SelectError()
.ToTask(); .ToTask();
} }
StatusRecord? RunIteration() StatusRecord? RunIteration()
{ {
@ -167,56 +178,60 @@ internal static class Program
Console.WriteLine(startTime.ToString("HH:mm:ss.fff")); Console.WriteLine(startTime.ToString("HH:mm:ss.fff"));
// the order matter of the next three lines // the order matter of the next three lines
var statusrecord = ReadStatus(); var statusrecord = ReadStatus();
statusrecord?.CreateSimpleTopologyTextBlock().WriteLine(); statusrecord?.CreateSimpleTopologyTextBlock().WriteLine();
statusrecord?.StateMachine.State.WriteLine(" state");
statusrecord?.StateMachine.Message.WriteLine(" Message");
statusrecord?.InverterRecord?.BatteryLimitsEnable.WriteLine(" BatteryLimitsEnable");
// statusrecord?.DcDc?.Dc.Battery.Power .WriteLine(" Power");
// statusrecord?.DcDc?.Dc.Battery.Voltage .WriteLine(" Voltage"); Console.WriteLine(" **************** DcDc **********************");
// statusrecord?.DcDc?.Dc.Battery.Current .WriteLine(" Current"); statusrecord?.DcDc?.Dc.Battery.Power .WriteLine(" DC Battery Power");
// statusrecord?.DcDc?.Dc.Link.Voltage .WriteLine(" Dc link Voltage"); 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");
statusrecord?.GridMeterRecord?.Frequency .WriteLine(" Frequency");
statusrecord?.GridMeterRecord?.VoltageU1 .WriteLine(" VoltageU1"); Console.WriteLine(" ********************************* Kaco Inverter *********************************");
statusrecord?.GridMeterRecord?.VoltageU2 .WriteLine(" VoltageU2");
statusrecord?.GridMeterRecord?.VoltageU3 .WriteLine(" VoltageU3"); statusrecord?.InverterRecord?.ActivePowerW.WriteLine(" Inverter Power");
statusrecord?.GridMeterRecord?.CurrentI1 .WriteLine(" CurrentI1");
statusrecord?.GridMeterRecord?.CurrentI2 .WriteLine(" CurrentI2");
statusrecord?.GridMeterRecord?.CurrentI3 .WriteLine(" CurrentI3");
statusrecord?.GridMeterRecord?.ActivePowerL1 .WriteLine(" ActivePowerL1");
statusrecord?.GridMeterRecord?.ActivePowerL2 .WriteLine(" ActivePowerL2");
statusrecord?.GridMeterRecord?.ActivePowerL3 .WriteLine(" ActivePowerL3");
statusrecord?.GridMeterRecord?.ActivePowerTotal .WriteLine(" ActivePowerTotal");
statusrecord?.InverterRecord?.CurrentState.WriteLine(" CurrentState"); statusrecord?.InverterRecord?.CurrentState.WriteLine(" CurrentState");
statusrecord?.InverterRecord?.RequestedState.WriteLine(" RequestedState"); statusrecord?.InverterRecord?.RequestedState.WriteLine(" RequestedState");
statusrecord?.InverterRecord?.PcuError.WriteLine(" PcuError"); statusrecord?.InverterRecord?.PcuError.WriteLine(" PcuError");
statusrecord?.InverterRecord?.PcuState.WriteLine(" PcuState"); statusrecord?.InverterRecord?.PcuState.WriteLine(" PcuState");
statusrecord?.InverterRecord?.BattCharId.WriteLine(" _battCharId");
statusrecord?.InverterRecord?.BattCharLength.WriteLine(" _battCharLength");
statusrecord?.InverterRecord?.MinDischargeVoltage.WriteLine(" MinDischargeVoltage"); statusrecord?.InverterRecord?.MinDischargeVoltage.WriteLine(" MinDischargeVoltage");
statusrecord?.InverterRecord?.MaxDischargeCurrent.WriteLine(" MaxDischargeCurrent"); statusrecord?.InverterRecord?.MaxDischargeCurrent.WriteLine(" MaxDischargeCurrent");
statusrecord?.InverterRecord?.DischargeCutoffCurrent.WriteLine(" DischargeCutoffCurrent"); statusrecord?.InverterRecord?.DischargeCutoffCurrent.WriteLine(" DischargeCutoffCurrent");
statusrecord?.InverterRecord?.MaxChargeVoltage.WriteLine(" MaxChargeVoltage"); statusrecord?.InverterRecord?.MaxChargeVoltage.WriteLine(" MaxChargeVoltage");
statusrecord?.InverterRecord?.MaxChargeCurrent.WriteLine(" MaxChargeCurrent"); statusrecord?.InverterRecord?.MaxChargeCurrent.WriteLine(" MaxChargeCurrent");
statusrecord?.InverterRecord?.ChargeCutoffCurrent.WriteLine(" ChargeCutoffCurrent"); statusrecord?.InverterRecord?.ChargeCutoffCurrent.WriteLine(" ChargeCutoffCurrent");
statusrecord?.InverterRecord?.ActivePowerSetPercent.WriteLine( "ActivePowerSetPercent");
statusrecord?.InverterRecord?.ActivePowerSetPercent.WriteLine(" ActivePowerSetPercent"); statusrecord?.ControlSystemState();
statusrecord?.InverterRecord?.ReactivePowerSetPercent.WriteLine(" ReactivePowerSetPercent"); var i = 0;
statusrecord?.InverterRecord?.WatchdogSeconds.WriteLine(" WatchdogSeconds"); 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); InitializeKacoStartup(statusrecord);
foreach (var d in statusrecord.DcDc.Devices)
{
Console.WriteLine("After DcDc is " + d.Control.PowerStageEnable);
}
Console.WriteLine( " ************************************ We are writing ************************************");
Console.WriteLine(" ************************************ We are writing ************************************");
statusrecord?.Config.Save(); // save the config file statusrecord?.Config.Save(); // save the config file
if (statusrecord?.InverterRecord != null) kacoDevice.Write(statusrecord.InverterRecord); if (statusrecord?.InverterRecord != null) kacoDevice.Write(statusrecord.InverterRecord);
Console.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff"));
return statusrecord; return statusrecord;
} }
catch (Exception e) catch (Exception e)
@ -227,11 +242,10 @@ internal static class Program
} }
} }
} }
private static async Task SavingLocalCsvFile(Int64 timestamp, String csv) private static async Task SavingLocalCsvFile(Int64 timestamp, String csv)
{ {
const String directoryPath = "/home/inesco/salimax/csvFile"; var directoryPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "csvFiles");
// Ensure directory exists // Ensure directory exists
if (!Directory.Exists(directoryPath)) if (!Directory.Exists(directoryPath))
{ {
@ -268,49 +282,49 @@ internal static class Program
var filePath = Path.Combine(directoryPath, timestamp + ".csv"); var filePath = Path.Combine(directoryPath, timestamp + ".csv");
await File.WriteAllTextAsync(filePath, filteredCsv); await File.WriteAllTextAsync(filePath, filteredCsv);
} }
private static async Task<Boolean> DataLogging(StatusRecord status, DateTime timeStamp) private static async Task<Boolean> DataLogging(StatusRecord status, DateTime timeStamp)
{ {
var csv = status.ToCsv(); var csv = status.ToCsv();
// for debug, only to be deleted. // for debug, only to be deleted.
//foreach (var item in csv.SplitLines()) //foreach (var item in csv.SplitLines())
//{ //{
// Console.WriteLine(item + ""); // Console.WriteLine(item + "");
//} //}
await SavingLocalCsvFile(timeStamp.ToUnixTime(), csv); await SavingLocalCsvFile(timeStamp.ToUnixTime(), csv);
var jsonData = new Dictionary<String, Object>(); var jsonData = new Dictionary<String, Object>();
ConvertToJson(csv, jsonData).LogInfo(); ConvertToJson(csv, jsonData).LogInfo();
var s3Config = status.Config.S3; var s3Config = status.Config.S3;
if (s3Config is null) if (s3Config is null)
return false; return false;
//Concatenating 15 files in one file //Concatenating 15 files in one file
return await ConcatinatingAndCompressingFiles(timeStamp.ToUnixTime(), s3Config); return await ConcatinatingAndCompressingFiles(timeStamp.ToUnixTime(), s3Config);
} }
private static String ConvertToJson(String csv, Dictionary<String, Object> jsonData) private static String ConvertToJson(String csv, Dictionary<String, Object> jsonData)
{ {
foreach (var line in csv.Split('\n')) foreach (var line in csv.Split('\n'))
{ {
if (string.IsNullOrWhiteSpace(line)) continue; if (string.IsNullOrWhiteSpace(line)) continue;
var parts = line.Split(';'); var parts = line.Split(';');
var keyPath = parts[0]; var keyPath = parts[0];
var value = parts[1]; var value = parts[1];
var unit = parts.Length > 2 ? parts[2].Trim() : ""; var unit = parts.Length > 2 ? parts[2].Trim() : "";
InsertIntoJson(jsonData, keyPath.Split('/'), value); InsertIntoJson(jsonData, keyPath.Split('/'), value);
} }
var jsonOutput = JsonConvert.SerializeObject(jsonData, Formatting.None); var jsonOutput = JsonConvert.SerializeObject(jsonData, Formatting.None);
return jsonOutput; return jsonOutput;
} }
private static async Task<Boolean> ConcatinatingAndCompressingFiles(Int64 timeStamp, S3Config s3Config) private static async Task<Boolean> ConcatinatingAndCompressingFiles(Int64 timeStamp, S3Config s3Config)
{ {
if (_fileCounter >= NbrOfFileToConcatenate) if (_fileCounter >= NbrOfFileToConcatenate)
@ -368,12 +382,13 @@ internal static class Program
return false; return false;
} }
} }
_fileCounter++; _fileCounter++;
return true; return true;
} }
private static void Heartbit() private static void Heartbit()
{ {
var s3Bucket = Config.Load().S3?.Bucket; var s3Bucket = Config.Load().S3?.Bucket;
var tryParse = int.TryParse(s3Bucket?.Split("-")[0], out var installationId); var tryParse = int.TryParse(s3Bucket?.Split("-")[0], out var installationId);
if (tryParse) if (tryParse)
@ -381,15 +396,15 @@ internal static class Program
var returnedStatus = new StatusMessage var returnedStatus = new StatusMessage
{ {
InstallationId = installationId, InstallationId = installationId,
Product = 3, Product = 3,
Status = _sodiAlarmState, Status = _sodiAlarmState,
Type = MessageType.Heartbit, Type = MessageType.Heartbit,
}; };
if (s3Bucket != null) if (s3Bucket != null)
RabbitMqManager.InformMiddleware(returnedStatus); RabbitMqManager.InformMiddleware(returnedStatus);
} }
} }
private static async Task SaveToLocalCompressedFallback(Byte[] compressedData, String fileNameWithoutExtension) private static async Task SaveToLocalCompressedFallback(Byte[] compressedData, String fileNameWithoutExtension)
{ {
try try
@ -408,7 +423,7 @@ internal static class Program
Console.WriteLine("Failed to save compressed file locally: " + ex.Message); Console.WriteLine("Failed to save compressed file locally: " + ex.Message);
} }
} }
private static Byte[] CompresseBytes(String jsonToSend) private static Byte[] CompresseBytes(String jsonToSend)
{ {
//Compress JSON data to a byte array //Compress JSON data to a byte array
@ -416,19 +431,20 @@ internal static class Program
//Create a zip directory and put the compressed file inside //Create a zip directory and put the compressed file inside
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
{ {
var entry = archive.CreateEntry("data.json", CompressionLevel.SmallestSize); // Add JSON data to the ZIP archive var entry = archive.CreateEntry("data.json",
CompressionLevel.SmallestSize); // Add JSON data to the ZIP archive
using (var entryStream = entry.Open()) using (var entryStream = entry.Open())
using (var writer = new StreamWriter(entryStream)) using (var writer = new StreamWriter(entryStream))
{ {
writer.Write(jsonToSend); writer.Write(jsonToSend);
} }
} }
var compressedBytes = memoryStream.ToArray(); var compressedBytes = memoryStream.ToArray();
return compressedBytes; return compressedBytes;
} }
private static async Task ResendLocalFailedFilesAsync(S3Config s3Config) private static async Task ResendLocalFailedFilesAsync(S3Config s3Config)
{ {
var fallbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FailedUploads"); var fallbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FailedUploads");
@ -472,18 +488,20 @@ internal static class Program
private static async Task<Boolean> SaveModbusTcpFile(StatusRecord status) private static async Task<Boolean> SaveModbusTcpFile(StatusRecord status)
{ {
var modbusData = new Dictionary<String, UInt16>(); var modbusData = new Dictionary<String, UInt16>();
// SYSTEM DATA // SYSTEM DATA
var result1 = ConvertToModbusRegisters((status.Config.MinSoc * 10), "UInt16", 30001); // this to be updated to modbusTCP version var result1 =
ConvertToModbusRegisters((status.Config.MinSoc * 10), "UInt16",
30001); // this to be updated to modbusTCP version
var result2 = ConvertToModbusRegisters(status.InverterRecord!.PcuError, "UInt32", 30002); var result2 = ConvertToModbusRegisters(status.InverterRecord!.PcuError, "UInt32", 30002);
// Merge all results into one dictionary // Merge all results into one dictionary
var allResults = new[] var allResults = new[]
{ {
result1,result2 result1, result2
}; };
foreach (var result in allResults) foreach (var result in allResults)
{ {
foreach (var entry in result) foreach (var entry in result)
@ -491,17 +509,18 @@ internal static class Program
modbusData[entry.Key] = entry.Value; modbusData[entry.Key] = entry.Value;
} }
} }
// Write to JSON // Write to JSON
var json = JsonSerializer.Serialize(modbusData, new JsonSerializerOptions { WriteIndented = true }); var json = JsonSerializer.Serialize(modbusData, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync("/home/inesco/SodiStoreHome/ModbusTCP/modbus_tcp_data.json", json); await File.WriteAllTextAsync("/home/inesco/SodiStoreHome/ModbusTCP/modbus_tcp_data.json", json);
//Console.WriteLine("JSON file written successfully."); //Console.WriteLine("JSON file written successfully.");
//Console.WriteLine(json); //Console.WriteLine(json);
var stopTime = DateTime.Now; var stopTime = DateTime.Now;
Console.WriteLine(stopTime.ToString("HH:mm:ss.fff" )+ " Finish the loop"); Console.WriteLine(stopTime.ToString("HH:mm:ss.fff") + " Finish the loop");
return true; return true;
} }
private static Dictionary<String, UInt16> ConvertToModbusRegisters(Object value, String outputType, private static Dictionary<String, UInt16> ConvertToModbusRegisters(Object value, String outputType,
Int32 startingAddress) Int32 startingAddress)
{ {
@ -515,13 +534,13 @@ internal static class Program
case "Int16": case "Int16":
var int16Val = Convert.ToInt16(value); var int16Val = Convert.ToInt16(value);
registers[startingAddress.ToString()] = (UInt16)int16Val; // reinterpret signed as ushort registers[startingAddress.ToString()] = (UInt16)int16Val; // reinterpret signed as ushort
break; break;
case "UInt32": case "UInt32":
var uint32Val = Convert.ToUInt32(value); var uint32Val = Convert.ToUInt32(value);
registers[startingAddress.ToString()] = (UInt16)(uint32Val & 0xFFFF); // Low word registers[startingAddress.ToString()] = (UInt16)(uint32Val & 0xFFFF); // Low word
registers[(startingAddress + 1).ToString()] = (UInt16)(uint32Val >> 16); // High word registers[(startingAddress + 1).ToString()] = (UInt16)(uint32Val >> 16); // High word
break; break;
case "Int32": case "Int32":
@ -534,9 +553,15 @@ internal static class Program
default: default:
throw new ArgumentException("Unsupported output type: " + outputType); throw new ArgumentException("Unsupported output type: " + outputType);
} }
return registers; 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) private static void InitializeKacoStartup(StatusRecord? statusRecord)
{ {
@ -544,12 +569,23 @@ internal static class Program
// 1. Apply DC This part is physical and cannot be done in software. // 1. Apply DC This part is physical and cannot be done in software.
// We assume DC power is already present. // We assume DC power is already present.
// //
Console.WriteLine("1. Apply DC");
// //
// 2. Send valid battery limits (Model 64202) 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. // All values temporarily set to "1" as requested.
// You will replace them later with real values. // You will replace them later with real values.
// //
Console.WriteLine("2. Send real value");
if (statusRecord?.InverterRecord != null) if (statusRecord?.InverterRecord != null)
{ {
statusRecord.InverterRecord.MinDischargeVoltage = 700f; // 64202.DisMinV statusRecord.InverterRecord.MinDischargeVoltage = 700f; // 64202.DisMinV
@ -559,12 +595,13 @@ internal static class Program
statusRecord.InverterRecord.MaxChargeVoltage = 800f; // 64202.ChaMaxV statusRecord.InverterRecord.MaxChargeVoltage = 800f; // 64202.ChaMaxV
statusRecord.InverterRecord.MaxChargeCurrent = 140f; // 64202.ChaMaxA statusRecord.InverterRecord.MaxChargeCurrent = 140f; // 64202.ChaMaxA
statusRecord.InverterRecord.ChargeCutoffCurrent = 10f; // 64202.ChaCutoffA statusRecord.InverterRecord.ChargeCutoffCurrent = 10f; // 64202.ChaCutoffA
statusRecord.InverterRecord.WatchdogSeconds = 30; // this is additional from my seid statusRecord.InverterRecord.WatchdogSeconds = 30; // this is additional from my seid
// //
// 3. Enable limits (EnLimit) // 3. Enable limits (EnLimit)
//
Console.WriteLine("3. Enable limits ");
statusRecord.InverterRecord.BatteryLimitsEnable = EnableDisableEnum.Enabled; statusRecord.InverterRecord.BatteryLimitsEnable = EnableDisableEnum.Enabled;
// //
@ -578,6 +615,9 @@ internal static class Program
// - After valid limits: CurrentState == 8 (STANDBY) // - After valid limits: CurrentState == 8 (STANDBY)
// - Then after grid/DC conditions: CurrentState == 1 (OFF) or 11 (GRID_CONNECTED) // - Then after grid/DC conditions: CurrentState == 1 (OFF) or 11 (GRID_CONNECTED)
// //
Console.WriteLine("3. Read current state");
var state = statusRecord.InverterRecord.CurrentState; var state = statusRecord.InverterRecord.CurrentState;
@ -586,6 +626,7 @@ internal static class Program
switch (state) switch (state)
{ {
case CurrentState.Standby: case CurrentState.Standby:
Console.WriteLine("Device is in STANDBY (8) — battery limits accepted."); Console.WriteLine("Device is in STANDBY (8) — battery limits accepted.");
break; break;
@ -601,9 +642,26 @@ internal static class Program
Console.WriteLine("Device in unexpected state: " + state); Console.WriteLine("Device in unexpected state: " + state);
break; break;
} }
//Thread.Sleep(2000);
} }
} }
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) private static void InsertIntoJson(Dictionary<String, Object> jsonDict, String[] keys, String value)
{ {
var currentDict = jsonDict; var currentDict = jsonDict;
@ -617,11 +675,10 @@ internal static class Program
if (i == keys.Length - 1) // Last key, store the value if (i == keys.Length - 1) // Last key, store the value
{ {
if (!value.Contains(",") &&
if (!value.Contains(",") && double.TryParse(value, out Double doubleValue)) // Try to parse value as a number double.TryParse(value, out Double doubleValue)) // Try to parse value as a number
{ {
currentDict[key] = Math.Round(doubleValue, 2); // Round to 2 decimal places currentDict[key] = Math.Round(doubleValue, 2); // Round to 2 decimal places
} }
else else
{ {

View File

@ -0,0 +1,376 @@
using InnovEnergy.App.KacoCommunication.ESS;
using InnovEnergy.Lib.Devices.Kaco92L3.DataType;
#pragma warning disable CS8602 // Dereference of a possibly null reference.
namespace InnovEnergy.App.KacoCommunication.System;
public static class KacoCurrentStateController
{
// Call every 2 seconds
public static Boolean ControlSystemState(this StatusRecord s)
{
var cs = s.InverterRecord.CurrentState; // 64201.CurrentState (1..12)
s.StateMachine.State = (int)cs;
return cs switch
{
CurrentState.Off => State_Off(s),
CurrentState.Sleeping => State_Sleeping(s),
CurrentState.Starting => State_Starting(s),
CurrentState.Mppt => State_Mppt(s),
CurrentState.Throttled => State_Throttled(s),
CurrentState.ShuttingDown => State_ShuttingDown(s),
CurrentState.Fault => State_Fault(s),
CurrentState.Standby => State_Standby(s),
CurrentState.Precharge => State_Precharge(s),
CurrentState.GridPreConnected=> State_GridPreConnected(s),
CurrentState.GridConnected => State_GridConnected(s),
CurrentState.NoErrorPending => State_NoErrorPending(s),
_ => UnknownState(s)
};
}
// ─────────────────────────────────────────────
// Global rule: only allow power writes in 11 or 5
// ─────────────────────────────────────────────
private static void EnforcePowerRules(StatusRecord s)
{
var cs = s.InverterRecord.CurrentState;
if (cs is not (CurrentState.GridConnected or CurrentState.Throttled))
{
// must be 0 outside (11) or (5)
s.InverterRecord.ActivePowerSetPercent = 0f;
//s.InverterRecord.ReactivePowerSetPercent = 0f;
}
}
// ─────────────────────────────────────────────
// State handlers (based purely on CurrentState)
// ─────────────────────────────────────────────
private static bool State_Off(StatusRecord s)
{
s.StateMachine.Message = "OFF: write limits (once) and request connect (11).";
EnforcePowerRules(s);
// Write limits (ignore details)
// WriteLimits();
// Always aim for running
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
private static bool State_Sleeping(StatusRecord s)
{
s.StateMachine.Message = "SLEEPING: write limits (once) and request connect (11).";
EnforcePowerRules(s);
// s.InverterRecord.WriteLimits();
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
private static bool State_Standby(StatusRecord s)
{
s.StateMachine.Message = "STANDBY: write limits (once) and request connect (11).";
EnforcePowerRules(s);
// s.InverterRecord.WriteLimits();
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
private static bool State_Mppt(StatusRecord s)
{
s.StateMachine.Message = "MPPT: keep requesting connect (11).";
EnforcePowerRules(s);
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
private static bool State_Starting(StatusRecord s)
{
s.StateMachine.Message = "STARTING: keep requesting connect (11), wait for 10/11/5.";
EnforcePowerRules(s);
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
private static bool State_Precharge(StatusRecord s)
{
s.StateMachine.Message = "PRECHARGE: keep requesting connect (11), wait.";
EnforcePowerRules(s);
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
private static bool State_GridPreConnected(StatusRecord s)
{
s.StateMachine.Message = "GRID_PRE_CONNECTED: keep requesting connect (11), wait for 11/5.";
EnforcePowerRules(s);
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
private static bool State_GridConnected(StatusRecord s)
{
s.StateMachine.Message = "GRID_CONNECTED: running. Power writes allowed.";
// Keep request latched
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
// Here you may write power setpoints (your own targets)
// Example:
// s.InverterRecord.ControlMode = ControlModeEnum.RpcRemote;
s.InverterRecord.ActivePowerSetPercent = s.Config.ActivePowerPercent;
// s.InverterRecord.ReactivePowerSetPercent = s.Targets.ReactivePowerPercent;
return true; // end goal reached
}
private static bool State_Throttled(StatusRecord s)
{
s.StateMachine.Message = "THROTTLED: still running. Power writes allowed.";
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
// Power writes allowed here too
return true;
}
private static bool State_ShuttingDown(StatusRecord s)
{
s.StateMachine.Message = "SHUTTING_DOWN: keep requesting connect (11); will reconnect after reaching 8/1.";
EnforcePowerRules(s);
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
private static bool State_Fault(StatusRecord s)
{
s.StateMachine.Message = "FAULT: power=0 and acknowledge with RequestedState=1 (OFF).";
EnforcePowerRules(s);
// Per doc: acknowledge uses RequestedState=1
s.InverterRecord.RequestedState = ReuqestedState.Off;
return false;
}
private static bool State_NoErrorPending(StatusRecord s)
{
s.StateMachine.Message = "NO_ERROR_PENDING: acknowledge with RequestedState=1 then controller will request 11 next cycles.";
EnforcePowerRules(s);
// Per doc Step 8: set RequestedState to 1 to acknowledge
s.InverterRecord.RequestedState = ReuqestedState.Off;
return false;
}
private static bool UnknownState(StatusRecord s)
{
s.StateMachine.Message = $"UNKNOWN CurrentState={s.InverterRecord.CurrentState}. For safety, power=0 and request 11.";
EnforcePowerRules(s);
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
}
/*
public static class Controller
{
private static UInt16 GetSystemState(this StatusRecord r)
{
if (r.InverterRecord != null)
{
return (UInt16)r.InverterRecord.CurrentState;
}
else
{
return (UInt16)StateMachine.Default.State;
}
}
public static Boolean ControlSystemState(this StatusRecord s)
{
s.StateMachine.State = s.GetSystemState();
var cs = s.InverterRecord?.CurrentState; // 64201.CurrentState (1..12)
s.StateMachine.State = (UInt16)cs;
return s.StateMachine.State switch
{
1 => State_Off(s),
2 => State_Sleeping(s),
3 => State_Starting(s),
4 => State_Mppt(s),
5 => State_Throttled(s),
6 => State_ShuttingDown(s),
7 => State_Fault(s),
8 => State_Standby(s),
9 => State_Precharge(s),
10 => State_GridPreConnected(s),
11 => State_GridConnected(s),
12 => State_NoErrorPending(s),
_ => UnknownState(s)
};
}
// ─────────────────────────────────────────────
// Global rule: only allow power writes in 11 or 5
// ─────────────────────────────────────────────
private static void EnforcePowerRules(StatusRecord s)
{
var cs = s.InverterRecord?.CurrentState;
// must be 0 outside (11) or (5)
s.InverterRecord.ActivePowerSetPercent = 0f;
s.InverterRecord.ReactivePowerSetPercent = 0f; }
}
// ─────────────────────────────────────────────
// State handlers (based purely on CurrentState)
// ─────────────────────────────────────────────
private static Boolean State_Off(StatusRecord s)
{
s.StateMachine.Message = "OFF: write limits (once) and request connect (11).";
EnforcePowerRules(s);
// Write limits (ignore details)
s.InverterRecord.WriteLimits();
// Always aim for running
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
private static bool State_Sleeping(StatusRecord s)
{
s.StateMachine.Message = "SLEEPING: write limits (once) and request connect (11).";
EnforcePowerRules(s);
s.InverterRecord.WriteLimits();
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
private static bool State_Standby(StatusRecord s)
{
s.StateMachine.Message = "STANDBY: write limits (once) and request connect (11).";
EnforcePowerRules(s);
s.InverterRecord.WriteLimits();
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
private static bool State_Mppt(StatusRecord s)
{
s.StateMachine.Message = "MPPT: keep requesting connect (11).";
EnforcePowerRules(s);
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
private static bool State_Starting(StatusRecord s)
{
s.StateMachine.Message = "STARTING: keep requesting connect (11), wait for 10/11/5.";
EnforcePowerRules(s);
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
private static bool State_Precharge(StatusRecord s)
{
s.StateMachine.Message = "PRECHARGE: keep requesting connect (11), wait.";
EnforcePowerRules(s);
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
private static bool State_GridPreConnected(StatusRecord s)
{
s.StateMachine.Message = "GRID_PRE_CONNECTED: keep requesting connect (11), wait for 11/5.";
EnforcePowerRules(s);
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
private static bool State_GridConnected(StatusRecord s)
{
s.StateMachine.Message = "GRID_CONNECTED: running. Power writes allowed.";
// Keep request latched
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
// Here you may write power setpoints (your own targets)
// Example:
// s.InverterRecord.ControlMode = ControlModeEnum.RpcRemote;
// s.InverterRecord.ActivePowerSetPercent = s.Targets.ActivePowerPercent;
// s.InverterRecord.ReactivePowerSetPercent = s.Targets.ReactivePowerPercent;
return true; // end goal reached
}
private static bool State_Throttled(StatusRecord s)
{
s.StateMachine.Message = "THROTTLED: still running. Power writes allowed.";
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
// Power writes allowed here too
return true;
}
private static bool State_ShuttingDown(StatusRecord s)
{
s.StateMachine.Message = "SHUTTING_DOWN: keep requesting connect (11); will reconnect after reaching 8/1.";
EnforcePowerRules(s);
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
private static bool State_Fault(StatusRecord s)
{
s.StateMachine.Message = "FAULT: power=0 and acknowledge with RequestedState=1 (OFF).";
EnforcePowerRules(s);
// Per doc: acknowledge uses RequestedState=1
s.InverterRecord.RequestedState = ReuqestedState.Off;
return false;
}
private static bool State_NoErrorPending(StatusRecord s)
{
s.StateMachine.Message = "NO_ERROR_PENDING: acknowledge with RequestedState=1 then controller will request 11 next cycles.";
EnforcePowerRules(s);
// Per doc Step 8: set RequestedState to 1 to acknowledge
s.InverterRecord.RequestedState = ReuqestedState.Off;
return false;
}
private static bool UnknownState(StatusRecord s)
{
s.StateMachine.Message = $"UNKNOWN CurrentState={s.InverterRecord.CurrentState}. For safety, power=0 and request 11.";
EnforcePowerRules(s);
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
return false;
}
}*/

View File

@ -0,0 +1,9 @@
namespace InnovEnergy.App.KacoCommunication.System;
public record StateMachine
{
public required String Message { get; set; } // TODO: init only
public required Int32 State { get; set; } // TODO: init only
public static StateMachine Default { get; } = new StateMachine { State = 100, Message = "Unknown State" };
}

View File

@ -14,7 +14,8 @@ public class Config
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
public required Double MinSoc { get; set; } public required Double MinSoc { get; set; }
public required float ActivePowerPercent { get; set; }
/* public required Double MaximumDischargingCurrent { get; set; } /* public required Double MaximumDischargingCurrent { get; set; }
public required Double MaximumChargingCurrent { get; set; } public required Double MaximumChargingCurrent { get; set; }
public required Int16 BatteriesCount { get; set; } public required Int16 BatteriesCount { get; set; }
@ -29,8 +30,8 @@ public class Config
public static Config Default => new() public static Config Default => new()
{ {
MinSoc = 20, MinSoc = 20,
/* MaximumChargingCurrent = 180, ActivePowerPercent = 0f,
MaximumDischargingCurrent = 180, /* MaximumDischargingCurrent = 180,
BatteriesCount = 0, BatteriesCount = 0,
ModbusProtcolNumber = 1.2,*/ ModbusProtcolNumber = 1.2,*/
Devices = new () Devices = new ()

View File

@ -176,6 +176,11 @@ public partial class KacoRecord
/// </summary> /// </summary>
public Int16 BatteryCurrentScaleFactor => _battCharASf; public Int16 BatteryCurrentScaleFactor => _battCharASf;
public Int16 ActivePowerW => _activePowerW;
public Int16 ReactivePowerVar => _reactivePowerVar;
public Int16 LineFrequencyHz => _lineFrequencyHz;
// Helper wrappers for scaled values // Helper wrappers for scaled values
private float ScaleBattVoltage(UInt16 raw) => ScaleSunspec(raw, _battCharVSf); private float ScaleBattVoltage(UInt16 raw) => ScaleSunspec(raw, _battCharVSf);
private float ScaleBattCurrent(UInt16 raw) => ScaleSunspec(raw, _battCharASf); private float ScaleBattCurrent(UInt16 raw) => ScaleSunspec(raw, _battCharASf);

View File

@ -21,7 +21,7 @@ public partial class KacoRecord
// State control // State control
[HoldingRegister<UInt16>(41065, writable: true)] private UInt16 _requestedState; // 0xA069 RequestedState (enum16, RW) [HoldingRegister<UInt16>(41065, writable: true)] private UInt16 _requestedState; // 0xA069 RequestedState (enum16, RW)
[HoldingRegister<UInt16>(41066)] private UInt16 _currentState; // 0xA06A CurrentState (enum16, R) [HoldingRegister<UInt16>(41066)] private UInt16 _currentState; // 0xA06A CurrentState (enum16, R)
[HoldingRegister<UInt16>(41067, writable: true)] private UInt16 _controlMode; // 0xA06B ControlMode (enum16, RW) [HoldingRegister<UInt16>(41067, writable: true)] private UInt16 _controlMode; // 0xA06B ControlMode (enum16, RW) Power Control mode (zunächst 0=Q RPC_Local / 1 RPC_Remote Qfix 64201)
[HoldingRegister<UInt16>(41068)] private UInt16 _reserved7; // 0xA06C Reserved [HoldingRegister<UInt16>(41068)] private UInt16 _reserved7; // 0xA06C Reserved
// Watchdog / setpoints // Watchdog / setpoints