Merge branch 'main' of 91.92.155.224:Innovenergy/Innovenergy_trunk
This commit is contained in:
commit
a86dc963b2
|
|
@ -44,6 +44,30 @@ public static class RabbitMqManager
|
|||
return true;
|
||||
}
|
||||
|
||||
public static Boolean EnsureConnected(StatusMessage currentSalimaxState, string? s3Bucket, string vpnServerIp)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_connection == null || !_connection.IsOpen ||
|
||||
_channel == null || _channel.IsClosed)
|
||||
{
|
||||
Console.WriteLine("⚠ RabbitMQ connection lost. Reconnecting...");
|
||||
|
||||
_connection?.Dispose();
|
||||
_channel?.Dispose();
|
||||
|
||||
return SubscribeToQueue(currentSalimaxState, s3Bucket, vpnServerIp);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("❌ Error while ensuring RabbitMQ connection: " + ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void InformMiddleware(StatusMessage status)
|
||||
{
|
||||
var message = JsonSerializer.Serialize(status);
|
||||
|
|
|
|||
|
|
@ -170,8 +170,6 @@ public static class Program
|
|||
statusrecord.InverterRecord.WarningMainCode.WriteLine(" = WarningMainCode"); // 30408 this the duration
|
||||
statusrecord.InverterRecord.WarningSubCode.WriteLine(" = WarningSubCode"); // 30408 this the duration
|
||||
|
||||
|
||||
|
||||
EssModeControl(statusrecord);
|
||||
|
||||
statusrecord.ApplyDefaultSettings();
|
||||
|
|
@ -182,7 +180,6 @@ public static class Program
|
|||
statusrecord.Config.Save(); // save the config file
|
||||
growattDeviceT415K.Write(statusrecord.InverterRecord);
|
||||
Console.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff"));
|
||||
|
||||
}
|
||||
return statusrecord;
|
||||
}
|
||||
|
|
@ -342,7 +339,6 @@ public static class Program
|
|||
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;
|
||||
|
|
@ -356,9 +352,30 @@ public static class Program
|
|||
var stateChanged = currentSalimaxState.Status != _prevSodiohomeAlarmState;
|
||||
var contentChanged = HasErrorsOrWarningsChanged(currentSalimaxState);
|
||||
var needsHeartbeat = (DateTime.Now - _lastHeartbeatTime).TotalSeconds >= HeartbeatIntervalSeconds;
|
||||
Console.WriteLine($"subscribedNow={subscribedNow}");
|
||||
Console.WriteLine($"_subscribedToQueue={_subscribedToQueue}");
|
||||
Console.WriteLine($"stateChanged={stateChanged}");
|
||||
Console.WriteLine($"contentChanged={contentChanged}");
|
||||
Console.WriteLine($"needsHeartbeat={needsHeartbeat}");
|
||||
Console.WriteLine($"s3Bucket null? {s3Bucket == null}");
|
||||
|
||||
// Send message if: state changed, content changed, OR heartbeat needed
|
||||
if (!subscribedNow && _subscribedToQueue && (stateChanged || contentChanged || needsHeartbeat))
|
||||
if (s3Bucket == null)
|
||||
{
|
||||
Console.WriteLine("⚠ S3 bucket not configured. Skipping middleware send.");
|
||||
LogMiddlewareFailure(new Exception("S3 Bucket not configured"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure connection FIRST
|
||||
if (!RabbitMqManager.EnsureConnected(currentSalimaxState, s3Bucket, VpnServerIp))
|
||||
{
|
||||
Console.WriteLine($"❌ RabbitMQ EnsureConnected FAILED at {DateTime.Now:HH:mm:ss.fff}");
|
||||
LogMiddlewareFailure(new Exception("EnsureConnected returned false"));
|
||||
return;
|
||||
}
|
||||
|
||||
//If already subscribed to the queue and the status has been changed, update the queue
|
||||
if (!subscribedNow && (stateChanged || contentChanged || needsHeartbeat))
|
||||
{
|
||||
_prevSodiohomeAlarmState = currentSalimaxState.Status;
|
||||
|
||||
|
|
@ -375,12 +392,17 @@ public static class Program
|
|||
_lastHeartbeatTime = DateTime.Now;
|
||||
}
|
||||
|
||||
if (s3Bucket != null) // why this is based on s3 bucket?
|
||||
try
|
||||
{
|
||||
RabbitMqManager.InformMiddleware(currentSalimaxState);
|
||||
//LogMiddlewareFailure(new Exception($"✅ Middleware message sent at {DateTime.Now:HH:mm:ss.fff}"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ Failed to send middleware message: {ex.Message}");
|
||||
LogMiddlewareFailure(ex);
|
||||
}
|
||||
}
|
||||
|
||||
//If there is an available message from the RabbitMQ Broker, apply the configuration file
|
||||
Configuration? config = SetConfigurationFile();
|
||||
if (config != null)
|
||||
|
|
@ -439,6 +461,27 @@ public static class Program
|
|||
return registers;
|
||||
}
|
||||
|
||||
private static void LogMiddlewareFailure(Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
var logPath = "/home/inesco/SodiStoreHome/middleware_failures.log";
|
||||
|
||||
var logEntry =
|
||||
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}]\n" +
|
||||
$"Exception: {ex.GetType().FullName}\n" +
|
||||
$"Message: {ex.Message}\n" +
|
||||
$"StackTrace:\n{ex.StackTrace}\n" +
|
||||
$"--------------------------------------------------\n";
|
||||
|
||||
File.AppendAllText(logPath, logEntry);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Never allow logging to crash the service
|
||||
}
|
||||
}
|
||||
|
||||
[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<Boolean> SaveModbusTcpFile(StatusRecord status)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace InnovEnergy.App.KacoCommunication.ESS;
|
||||
|
||||
public enum EssMode
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using InnovEnergy.App.KacoCommunication.System;
|
||||
using InnovEnergy.App.KacoCommunication.SystemConfig;
|
||||
using InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
using InnovEnergy.Lib.Devices.Kaco92L3;
|
||||
|
|
@ -5,19 +6,19 @@ using InnovEnergy.Lib.Devices.PLVario2Meter;
|
|||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
||||
|
||||
namespace InnovEnergy.App.KacoCommunication.ESS;
|
||||
|
||||
using ListOfBatteriesRecord = List<BatteryDeligreenRecords>;
|
||||
public class StatusRecord
|
||||
{
|
||||
public required KacoRecord? InverterRecord { get; set; }
|
||||
public required PlVarioMeterRecord? GridMeterRecord { get; set; }
|
||||
public required DcDcDevicesRecord? DcDc { get; init; }
|
||||
|
||||
public required BatteryDeligreenRecords? BatteryKabinet1 { get; set; }
|
||||
public required BatteryDeligreenRecords? BatteryKabinet2 { get; set; }
|
||||
public required BatteryDeligreenRecords? BatteryKabinet3 { get; set; }
|
||||
|
||||
|
||||
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 Config Config { get; set; }
|
||||
public required StateMachine StateMachine { get; init; }
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
// See https://aka.ms/new-console-template for more information
|
||||
|
||||
using System.IO.Compression;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Threading.Tasks;
|
||||
|
|
@ -11,16 +10,21 @@ 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;
|
||||
|
||||
|
|
@ -33,7 +37,6 @@ internal static class Program
|
|||
private static UInt16 _fileCounter = 0;
|
||||
|
||||
|
||||
|
||||
private static SodistoreAlarmState _prevSodiohomeAlarmState = SodistoreAlarmState.Green;
|
||||
private static SodistoreAlarmState _sodiAlarmState = SodistoreAlarmState.Green;
|
||||
|
||||
|
|
@ -74,13 +77,11 @@ internal static class Program
|
|||
KacoChannel = CreateChannel(d.KacoIp);
|
||||
GridMeterChannel = CreateChannel(d.GridMeterIp);
|
||||
DcDcChannel = CreateChannel(d.DcDcIp);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static async Task Main(String[] args)
|
||||
{
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
|
|
@ -89,7 +90,7 @@ internal static class Program
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// e.LogError();
|
||||
e.LogError();
|
||||
}
|
||||
}
|
||||
// ReSharper disable once FunctionNeverReturns
|
||||
|
|
@ -105,9 +106,12 @@ internal static class Program
|
|||
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 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);
|
||||
|
|
@ -120,6 +124,7 @@ internal static class Program
|
|||
var kacoRecord = kacoDevice.Read();
|
||||
var gridrawRecord = gridMeterDevice.Read();
|
||||
var dcDcRecord = dcDcDevices.Read();
|
||||
|
||||
if (gridrawRecord != null)
|
||||
{
|
||||
gridRecord = new PlVarioMeterRecord(gridrawRecord);
|
||||
|
|
@ -129,14 +134,20 @@ internal static class Program
|
|||
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,
|
||||
BatteryKabinet1 = batteryKabinet1,
|
||||
BatteryKabinet2 = batteryKabinet2,
|
||||
BatteryKabinet3 = batteryKabinet3,
|
||||
ListOfBatteriesRecord = listOfBatteriesRecord,
|
||||
StateMachine = StateMachine.Default,
|
||||
|
||||
Config = config // load from disk every iteration, so config can be changed while running
|
||||
};
|
||||
}
|
||||
|
|
@ -169,35 +180,25 @@ internal static class Program
|
|||
var statusrecord = ReadStatus();
|
||||
statusrecord?.CreateSimpleTopologyTextBlock().WriteLine();
|
||||
|
||||
statusrecord?.StateMachine.State.WriteLine(" state");
|
||||
statusrecord?.StateMachine.Message.WriteLine(" Message");
|
||||
|
||||
// statusrecord?.DcDc?.Dc.Battery.Power .WriteLine(" Power");
|
||||
// statusrecord?.DcDc?.Dc.Battery.Voltage .WriteLine(" Voltage");
|
||||
// statusrecord?.DcDc?.Dc.Battery.Current .WriteLine(" Current");
|
||||
// statusrecord?.DcDc?.Dc.Link.Voltage .WriteLine(" Dc link Voltage");
|
||||
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");
|
||||
|
||||
statusrecord?.GridMeterRecord?.Frequency .WriteLine(" Frequency");
|
||||
statusrecord?.GridMeterRecord?.VoltageU1 .WriteLine(" VoltageU1");
|
||||
statusrecord?.GridMeterRecord?.VoltageU2 .WriteLine(" VoltageU2");
|
||||
statusrecord?.GridMeterRecord?.VoltageU3 .WriteLine(" VoltageU3");
|
||||
|
||||
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");
|
||||
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?.BattCharId.WriteLine(" _battCharId");
|
||||
statusrecord?.InverterRecord?.BattCharLength.WriteLine(" _battCharLength");
|
||||
|
||||
statusrecord?.InverterRecord?.MinDischargeVoltage.WriteLine(" MinDischargeVoltage");
|
||||
statusrecord?.InverterRecord?.MaxDischargeCurrent.WriteLine(" MaxDischargeCurrent");
|
||||
statusrecord?.InverterRecord?.DischargeCutoffCurrent.WriteLine(" DischargeCutoffCurrent");
|
||||
|
|
@ -205,17 +206,31 @@ internal static class Program
|
|||
statusrecord?.InverterRecord?.MaxChargeVoltage.WriteLine(" MaxChargeVoltage");
|
||||
statusrecord?.InverterRecord?.MaxChargeCurrent.WriteLine(" MaxChargeCurrent");
|
||||
statusrecord?.InverterRecord?.ChargeCutoffCurrent.WriteLine(" ChargeCutoffCurrent");
|
||||
|
||||
statusrecord?.InverterRecord?.ActivePowerSetPercent.WriteLine( "ActivePowerSetPercent");
|
||||
statusrecord?.InverterRecord?.ReactivePowerSetPercent.WriteLine(" ReactivePowerSetPercent");
|
||||
statusrecord?.InverterRecord?.WatchdogSeconds.WriteLine(" WatchdogSeconds");
|
||||
|
||||
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);
|
||||
Console.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff"));
|
||||
|
||||
return statusrecord;
|
||||
}
|
||||
|
|
@ -230,8 +245,7 @@ internal static class Program
|
|||
|
||||
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
|
||||
if (!Directory.Exists(directoryPath))
|
||||
{
|
||||
|
|
@ -368,6 +382,7 @@ internal static class Program
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_fileCounter++;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -416,7 +431,8 @@ internal static class Program
|
|||
//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
|
||||
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))
|
||||
{
|
||||
|
|
@ -474,7 +490,9 @@ internal static class Program
|
|||
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 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
|
||||
|
|
@ -491,6 +509,7 @@ internal static class Program
|
|||
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);
|
||||
|
|
@ -534,9 +553,15 @@ internal static class Program
|
|||
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)
|
||||
{
|
||||
|
|
@ -544,12 +569,23 @@ internal static class Program
|
|||
// 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");
|
||||
//
|
||||
// 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.
|
||||
// You will replace them later with real values.
|
||||
//
|
||||
|
||||
Console.WriteLine("2. Send real value");
|
||||
|
||||
if (statusRecord?.InverterRecord != null)
|
||||
{
|
||||
statusRecord.InverterRecord.MinDischargeVoltage = 700f; // 64202.DisMinV
|
||||
|
|
@ -564,7 +600,8 @@ internal static class Program
|
|||
|
||||
//
|
||||
// 3. Enable limits (EnLimit)
|
||||
//
|
||||
|
||||
Console.WriteLine("3. Enable limits ");
|
||||
statusRecord.InverterRecord.BatteryLimitsEnable = EnableDisableEnum.Enabled;
|
||||
|
||||
//
|
||||
|
|
@ -579,6 +616,9 @@ internal static class Program
|
|||
// - 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}");
|
||||
|
|
@ -586,6 +626,7 @@ internal static class Program
|
|||
switch (state)
|
||||
{
|
||||
case CurrentState.Standby:
|
||||
|
||||
Console.WriteLine("Device is in STANDBY (8) — battery limits accepted.");
|
||||
break;
|
||||
|
||||
|
|
@ -601,9 +642,26 @@ internal static class Program
|
|||
Console.WriteLine("Device in unexpected state: " + state);
|
||||
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)
|
||||
{
|
||||
var currentDict = jsonDict;
|
||||
|
|
@ -617,11 +675,10 @@ internal static class Program
|
|||
|
||||
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
|
||||
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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}*/
|
||||
|
|
@ -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" };
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ public class Config
|
|||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
|
||||
public required Double MinSoc { get; set; }
|
||||
public required float ActivePowerPercent { get; set; }
|
||||
|
||||
/* public required Double MaximumDischargingCurrent { get; set; }
|
||||
public required Double MaximumChargingCurrent { get; set; }
|
||||
|
|
@ -29,8 +30,8 @@ public class Config
|
|||
public static Config Default => new()
|
||||
{
|
||||
MinSoc = 20,
|
||||
/* MaximumChargingCurrent = 180,
|
||||
MaximumDischargingCurrent = 180,
|
||||
ActivePowerPercent = 0f,
|
||||
/* MaximumDischargingCurrent = 180,
|
||||
BatteriesCount = 0,
|
||||
ModbusProtcolNumber = 1.2,*/
|
||||
Devices = new ()
|
||||
|
|
|
|||
|
|
@ -32,40 +32,6 @@ namespace InnovEnergy.App.KacoCommunication;
|
|||
// └──────┘
|
||||
//
|
||||
|
||||
// New (simplified) topology:
|
||||
//
|
||||
// ┌────┐
|
||||
// │ PV │
|
||||
// └────┘
|
||||
// V
|
||||
// V
|
||||
// (i) 13.2 kW
|
||||
// V
|
||||
// V
|
||||
// ┌─────────┐ ┌─────────┐ (h) ┌────────┐ (k) ┌───────┐ (l) ┌────────────┐ ┌────────────┐ ┌────────────┐
|
||||
// │ Grid │<<<│ AC/DC │<<<<<<<<<<<<<│ Dc Bus │>>>>>>>>│ DC/DC │>>>>>>>>│ Battery K1 │ │ Battery K2 │ │ Battery K3 │
|
||||
// ├─────────┤ ├─────────┤ ├────────┤ ├───────┤ ├────────────┤ ├────────────┤ ├────────────┤
|
||||
// │ -3205 W │ │ -6646 W │ │ 776 V │ │ 56 V │ │ 52.3 V ... │ │ 52.3 V ... │ │ 52.3 V ... │
|
||||
// │ -3507 W │ │ -5071 W │ └────────┘ └───────┘ └────────────┘ └────────────┘ └────────────┘
|
||||
// │ -3605 W │
|
||||
// └─────────┘
|
||||
//
|
||||
// V
|
||||
// V
|
||||
// (j) 0 W
|
||||
// V
|
||||
// V
|
||||
// ┌──────┐
|
||||
// │ Load │
|
||||
// └──────┘
|
||||
//
|
||||
// Notes:
|
||||
// - (a) is grid power (to/from grid)
|
||||
// - (h) is AC/DC -> DC link power (or your chosen link variable)
|
||||
// - (i) PV -> DC bus
|
||||
// - (j) DC load
|
||||
// - (k) DC bus -> DC/DC link
|
||||
// - (l) DC/DC -> battery power (or total battery power)
|
||||
|
||||
public static class SimpleTopology
|
||||
{
|
||||
|
|
@ -74,17 +40,17 @@ public static class SimpleTopology
|
|||
// Keep the same variable letters as your diagrams (where possible)
|
||||
var a = status.GridMeterRecord?.ActivePowerTotal;
|
||||
|
||||
// In your existing code, "AC/DC column" shows per-device AC power;
|
||||
// and "h" is a separate link (AcDcToDcLink?.Power.Value).
|
||||
var h = 0;
|
||||
var h = status.InverterRecord?.ActivePowerW;
|
||||
|
||||
var i = 0;
|
||||
var j = 0;
|
||||
|
||||
var k = 0;
|
||||
var k = status.DcDc?.Dc.Battery.Power.Value;
|
||||
|
||||
// You mentioned this changed: l is now equal total battery power
|
||||
var l = status.BatteryKabinet1.Power;
|
||||
|
||||
var l = status.ListOfBatteriesRecord?.Sum(r => r.Power);
|
||||
|
||||
var grid = status.CreateGridColumn(a);
|
||||
var acdc = status.CreateAcDcColumn(h);
|
||||
|
|
@ -138,7 +104,7 @@ public static class SimpleTopology
|
|||
// └─────────┘ (h) flow to DC Bus
|
||||
|
||||
var acdcBox = TextBlock
|
||||
.AlignLeft(status.InverterRecord?.ActivePowerSetPercent.ToString() ?? "???")
|
||||
.AlignLeft(status.InverterRecord?.ActivePowerW.ToString() ?? "???")
|
||||
.TitleBox("AC/DC");
|
||||
|
||||
var flowToDcBus = Flow.Horizontal(h);
|
||||
|
|
@ -175,7 +141,7 @@ public static class SimpleTopology
|
|||
var pvToBus = Flow.Vertical(i);
|
||||
|
||||
// DC bus box (voltage from your DcDc record matches your existing code)
|
||||
var dcBusVoltage = 0.0;
|
||||
var dcBusVoltage = status.DcDc.Dc.Link.Voltage.Value;
|
||||
var dcBusBox = dcBusVoltage
|
||||
.ToString(CultureInfo.InvariantCulture)
|
||||
.Apply(TextBlock.FromString)
|
||||
|
|
@ -203,7 +169,7 @@ public static class SimpleTopology
|
|||
// │ 56 V │
|
||||
// └───────┘ (l) flow to batteries
|
||||
|
||||
var dc48Voltage =0.0;
|
||||
var dc48Voltage = status.DcDc?.Dc.Battery.Voltage;
|
||||
|
||||
var dcdcBox = TextBlock
|
||||
.AlignLeft(dc48Voltage)
|
||||
|
|
@ -219,13 +185,13 @@ public static class SimpleTopology
|
|||
// Battery K1 | Battery K2 | Battery K3 (side-by-side)
|
||||
// Each box: voltage, soc, current, temp, etc. (you can tailor)
|
||||
|
||||
var bat = status.BatteryKabinet1;
|
||||
var bat = status.ListOfBatteriesRecord;
|
||||
if (bat is null)
|
||||
return TextBlock.AlignLeft("no battery").Box();
|
||||
|
||||
// If you actually have relay names K1/K2/K3 per battery, wire them here.
|
||||
// For now we label by index as "Battery K{n}" to match your picture.
|
||||
var boxes = bat.Devices
|
||||
var boxes = bat
|
||||
.Select((b, idx) => CreateBatteryKBox(b, idx))
|
||||
.ToReadOnlyList();
|
||||
|
||||
|
|
@ -235,30 +201,24 @@ public static class SimpleTopology
|
|||
: TextBlock.AlignLeft("no battery devices").Box();
|
||||
}
|
||||
|
||||
private static TextBlock CreateBatteryKBox(BatteryDeligreenRecord battery, int idx)
|
||||
private static TextBlock CreateBatteryKBox(BatteryDeligreenRecords battery, int idx)
|
||||
{
|
||||
// Minimal “K-style” battery box matching your diagram fields
|
||||
var data = battery.BatteryDeligreenDataRecord;
|
||||
|
||||
// Some of your sample screen values look like:
|
||||
// 52.3 V, 99.1 %, 490 mA, 250 °C, 445 A
|
||||
// Map these to whatever fields you trust in your record.
|
||||
var voltage = data.BusVoltage.ToDisplayString();
|
||||
var soc = data.Soc.ToDisplayString();
|
||||
var current = data.BusCurrent.ToDisplayString();
|
||||
var temp = data.TemperaturesList.PowerTemperature.ToDisplayString();
|
||||
var voltage = battery.Voltage.ToDisplayString();
|
||||
var soc = battery.Soc.ToDisplayString();
|
||||
var current = battery.Current.ToDisplayString();
|
||||
var count = battery.Devices.Count;
|
||||
|
||||
// If you have a better “pack current” field, replace this line.
|
||||
// Keeping it as a separate line to mimic the picture’s extra current-like line.
|
||||
var extraCurrent = data.BusCurrent.ToDisplayString();
|
||||
|
||||
return TextBlock
|
||||
.AlignLeft(
|
||||
voltage,
|
||||
soc,
|
||||
current,
|
||||
temp,
|
||||
extraCurrent
|
||||
count
|
||||
)
|
||||
.TitleBox($"Battery K{idx + 1}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,24 +32,4 @@ public record StatusRecord
|
|||
public required EssControl EssControl { get; set; } // TODO: init only
|
||||
public required StateMachine StateMachine { get; init; }
|
||||
|
||||
|
||||
public string ToJson()
|
||||
{
|
||||
// Try to get the "Battery" property via reflection
|
||||
// var batteryProperty = thing.GetType().GetProperty("Battery");
|
||||
// if (batteryProperty == null)
|
||||
// throw new InvalidOperationException("The object does not have a 'Battery' property.");
|
||||
//
|
||||
// // Retrieve the value of the Battery property
|
||||
// var batteryValue = Battery.GetValue(thing);
|
||||
var jsonOptions = new JsonSerializerOptions { WriteIndented = true };
|
||||
|
||||
// Serialize the Battery property
|
||||
Console.WriteLine("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee");
|
||||
string json = JsonSerializer.Serialize(this.Battery, jsonOptions);
|
||||
Console.WriteLine(json);
|
||||
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,8 @@ using JsonSerializer = System.Text.Json.JsonSerializer;
|
|||
using static InnovEnergy.App.SinexcelCommunication.MiddlewareClasses.MiddlewareAgent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType;
|
||||
using InnovEnergy.Lib.Protocols.Modbus.Protocol;
|
||||
using static InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType.WorkingMode;
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
|
||||
|
|
@ -36,17 +38,28 @@ 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 Channel _sinexcelChannel;
|
||||
private static Channel _sinexcelChannel1;
|
||||
private static Channel _sinexcelChannel2;
|
||||
|
||||
private const String SwVersionNumber =" V1.00.131025 beta";
|
||||
private static readonly String SwVersionNumber = " V1.00." + DateTime.Today;
|
||||
private const String VpnServerIp = "10.2.0.11";
|
||||
private static Boolean _subscribedToQueue = false;
|
||||
private static Boolean _subscribeToQueueForTheFirstTime = false;
|
||||
private static SodistoreAlarmState _prevSodiohomeAlarmState = SodistoreAlarmState.Green;
|
||||
private static SodistoreAlarmState _sodiohomeAlarmState = SodistoreAlarmState.Green;
|
||||
|
||||
// Tracking for error/warning content changes
|
||||
private static List<String> _prevErrorCodes = new List<String>();
|
||||
private static List<String> _prevWarningCodes = new List<String>();
|
||||
|
||||
// Heartbeat tracking
|
||||
private static DateTime _lastHeartbeatTime = DateTime.MinValue;
|
||||
private const Int32 HeartbeatIntervalSeconds = 60;
|
||||
|
||||
|
||||
// move all this to config file
|
||||
private const String Port = "/dev/ttyUSB0";
|
||||
private const String Port1 = "/dev/ttyUSB0";
|
||||
private const String Port2 = "/dev/ttyUSB1";
|
||||
private const Byte SlaveId = 1;
|
||||
private const Parity Parity = 0; //none
|
||||
private const Int32 StopBits = 1;
|
||||
|
|
@ -55,7 +68,9 @@ internal static class Program
|
|||
|
||||
public static async Task Main(String[] args)
|
||||
{
|
||||
_sinexcelChannel = new SerialPortChannel(Port, BaudRate, Parity, DataBits, StopBits);
|
||||
_sinexcelChannel1 = new SerialPortChannel(Port1, BaudRate, Parity, DataBits, StopBits);
|
||||
_sinexcelChannel2 = new SerialPortChannel(Port2, BaudRate, Parity, DataBits, StopBits);
|
||||
|
||||
InitializeCommunicationToMiddleware();
|
||||
while (true)
|
||||
{
|
||||
|
|
@ -75,19 +90,22 @@ internal static class Program
|
|||
{
|
||||
Watchdog.NotifyReady();
|
||||
|
||||
Console.WriteLine("Starting Sinexcel Communication");
|
||||
Console.WriteLine("Starting Sinexcel Communication, SW Version : " + SwVersionNumber);
|
||||
|
||||
var sinexcelDevice = new SinexcelDevice(_sinexcelChannel, SlaveId);
|
||||
// var sinexcelDevice = new sinexcelDevices(new List<sinexcelDevice> { growattDeviceT415K });
|
||||
var sinexcelDevice1 = new SinexcelDevice(_sinexcelChannel1, SlaveId);
|
||||
var sinexcelDevice2 = new SinexcelDevice(_sinexcelChannel2, SlaveId);
|
||||
|
||||
StatusRecord? ReadStatus()
|
||||
{
|
||||
var config = Config.Load();
|
||||
var sinexcelRecord = sinexcelDevice.Read();
|
||||
var sinexcelRecord1 = sinexcelDevice1.Read();
|
||||
var sinexcelRecord2 = sinexcelDevice2.Read();
|
||||
|
||||
|
||||
return new StatusRecord
|
||||
{
|
||||
InverterRecord = sinexcelRecord,
|
||||
InverterRecord1 = sinexcelRecord1,
|
||||
InverterRecord2 = sinexcelRecord2,
|
||||
Config = config // load from disk every iteration, so config can be changed while running
|
||||
};
|
||||
}
|
||||
|
|
@ -120,61 +138,174 @@ internal static class Program
|
|||
if (statusrecord == null)
|
||||
return null;
|
||||
|
||||
// Console.WriteLine( statusrecord.InverterRecord.TimedChargeAndDischargeOff + " TimedChargeAndDischargeOff ");
|
||||
Console.WriteLine( statusrecord.InverterRecord.Battery1Power + " Battery1Power ");
|
||||
Console.WriteLine( statusrecord.InverterRecord.Battery1Soc + " Battery1Soc ");
|
||||
Console.WriteLine( statusrecord.InverterRecord.RepetitiveWeeks + " RepetitiveWeeks ");
|
||||
Console.WriteLine( statusrecord.InverterRecord.EffectiveStartDate + " EffectiveStartDate ");
|
||||
Console.WriteLine( statusrecord.InverterRecord.EffectiveEndDate + " EffectiveEndDate ");
|
||||
Console.WriteLine( statusrecord.InverterRecord.ChargingPowerPeriod1 + " ChargingPowerPeriod1 ");
|
||||
Console.WriteLine( statusrecord.InverterRecord.ChargeStartTimePeriod1 + " ChargeStartTimePeriod1 ");
|
||||
Console.WriteLine( statusrecord.InverterRecord.ChargeEndTimePeriod1 + " ChargeEndTimePeriod1 ");
|
||||
Console.WriteLine(" ************************************************ Inverter 1 ************************************************ ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.SystemDateTime + " SystemDateTime ");
|
||||
|
||||
Console.WriteLine( statusrecord.InverterRecord.DischargeStartTimePeriod1 + " DischargeStartTimePeriod1 ");
|
||||
Console.WriteLine( statusrecord.InverterRecord.DischargeEndTimePeriod1 + " DischargeEndTimePeriod1 ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.TotalPhotovoltaicPower + " TotalPhotovoltaicPower ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.TotalBatteryPower + " TotalBatteryPower ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.TotalLoadPower + " TotalLoadPower ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.TotalGridPower + " TotalGridPower ");
|
||||
|
||||
|
||||
Console.WriteLine( statusrecord.InverterRecord1.Battery1Power + " Battery1Power ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.Battery1Soc + " Battery1Soc ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.Battery1BackupSoc + " Battery1BackupSoc ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.Battery1MinSoc + " Battery1MinSoc ");
|
||||
|
||||
Console.WriteLine( statusrecord.InverterRecord1.Battery2Power + " Battery2Power ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.Battery2Soc + " Battery2Soc ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.Battery2BackupSoc + " Battery2BackupSoc ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.Battery2MinSoc + " Battery2MinSoc ");
|
||||
|
||||
Console.WriteLine( statusrecord.InverterRecord1.EnableGridExport + " EnableGridExport ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.PowerGridExportLimit + " PowerGridExportLimit ");
|
||||
|
||||
Console.WriteLine( statusrecord.InverterRecord1.PowerOn + " PowerOn ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.PowerOff + " PowerOff ");
|
||||
|
||||
|
||||
Console.WriteLine( statusrecord.InverterRecord1.WorkingMode + " WorkingMode ");
|
||||
|
||||
Console.WriteLine( statusrecord.InverterRecord1.GridSwitchMethod + " GridSwitchMethod ");
|
||||
|
||||
Console.WriteLine( statusrecord.InverterRecord1.ThreePhaseWireSystem + " ThreePhaseWireSystem ");
|
||||
|
||||
Console.WriteLine(" ************************************************ Inverter 2 ************************************************ ");
|
||||
|
||||
Console.WriteLine( statusrecord.InverterRecord2.SystemDateTime + " SystemDateTime ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.TotalPhotovoltaicPower + " TotalPhotovoltaicPower ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.TotalBatteryPower + " TotalBatteryPower ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.TotalLoadPower + " TotalLoadPower ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.TotalGridPower + " TotalGridPower ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.Battery1Power + " Battery1Power ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.Battery1Soc + " Battery1Soc ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.Battery1BackupSoc + " Battery1BackupSoc ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.Battery1MinSoc + " Battery1MinSoc ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.Battery2Power + " Battery2Power ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.Battery2Soc + " Battery2Soc ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.Battery2BackupSoc + " Battery2BackupSoc ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.Battery2MinSoc + " Battery2MinSoc ");
|
||||
|
||||
Console.WriteLine( statusrecord.InverterRecord2.EnableGridExport + " EnableGridExport ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.PowerGridExportLimit + " PowerGridExportLimit ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.PowerOn + " PowerOn ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.PowerOff + " PowerOff ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.WorkingMode + " WorkingMode ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.GridSwitchMethod + " GridSwitchMethod ");
|
||||
Console.WriteLine( statusrecord.InverterRecord2.ThreePhaseWireSystem + " ThreePhaseWireSystem ");
|
||||
/*
|
||||
Console.WriteLine( statusrecord.InverterRecord1.RepetitiveWeeks + " RepetitiveWeeks ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.EffectiveStartDate + " EffectiveStartDate ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.EffectiveEndDate + " EffectiveEndDate ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.ChargingPowerPeriod1 + " ChargingPowerPeriod1 ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.DishargingPowerPeriod1 + " dischargingPowerPeriod1 ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.ChargeStartTimePeriod1 + " ChargeStartTimePeriod1 ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.ChargeEndTimePeriod1 + " ChargeEndTimePeriod1 ");
|
||||
|
||||
Console.WriteLine( statusrecord.InverterRecord1.DischargeStartTimePeriod1 + " DischargeStartTimePeriod1 ");
|
||||
Console.WriteLine( statusrecord.InverterRecord1.DischargeEndTimePeriod1 + " DischargeEndTimePeriod1 ");*/
|
||||
|
||||
Console.WriteLine( statusrecord.InverterRecord.PowerOn + " PowerOn ");
|
||||
Console.WriteLine( statusrecord.InverterRecord.PowerOff + " PowerOff ");
|
||||
Console.WriteLine( statusrecord.InverterRecord.WorkingMode + " WorkingMode ");
|
||||
// Console.WriteLine( statusrecord.InverterRecord.FaultClearing + " FaultClearing ");
|
||||
SendSalimaxStateAlarm(GetSodiHomeStateAlarm(statusrecord),statusrecord);
|
||||
statusrecord.ControlConstants();
|
||||
|
||||
Console.WriteLine( " ************************************ We are writing ************************************");
|
||||
var startWritingTime = DateTime.Now;
|
||||
Console.WriteLine(startWritingTime.ToString("HH:mm:ss.fff ") +"start Writing");
|
||||
statusrecord?.Config.Save(); // save the config file
|
||||
|
||||
if (statusrecord != null)
|
||||
if (statusrecord is { Config.ControlPermission: true })
|
||||
{
|
||||
statusrecord.InverterRecord.WorkingMode = WorkingMode.TimeChargeDischarge;
|
||||
Console.WriteLine(" We have the Right to Write");
|
||||
sinexcelDevice1.Write(statusrecord.InverterRecord1);
|
||||
sinexcelDevice2.Write(statusrecord.InverterRecord2);
|
||||
|
||||
//statusrecord.InverterRecord.TimedChargeAndDischargeOff = 0;
|
||||
statusrecord.InverterRecord.RepetitiveWeeks = SinexcelWeekDays.All;
|
||||
statusrecord.InverterRecord.EffectiveStartDate = DateTime.Today.AddDays(-1);
|
||||
statusrecord.InverterRecord.EffectiveEndDate = DateTime.Today.AddDays(2);
|
||||
statusrecord.InverterRecord.ChargingPowerPeriod1 = 3;
|
||||
statusrecord.InverterRecord.DishargingPowerPeriod1 = 3;
|
||||
statusrecord.InverterRecord.DischargeStartTimePeriod1 = new TimeSpan(16, 00, 0);
|
||||
statusrecord.InverterRecord.DischargeEndTimePeriod1 = new TimeSpan(17, 03, 0);
|
||||
statusrecord.InverterRecord.PowerOn = 1;
|
||||
statusrecord.InverterRecord.PowerOff = 0;
|
||||
//statusrecord.InverterRecord.FaultClearing = 1;
|
||||
}
|
||||
if (statusrecord?.InverterRecord != null) sinexcelDevice.Write(statusrecord.InverterRecord);
|
||||
else
|
||||
{
|
||||
Console.WriteLine(" Nooooooo We cant' have the Right to Write");
|
||||
}
|
||||
|
||||
var stopTime = DateTime.Now;
|
||||
Console.WriteLine(stopTime.ToString("HH:mm:ss.fff ")+ "run iteration time finish");
|
||||
return statusrecord;
|
||||
}
|
||||
catch (CrcException e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw; // restart only on CRC
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Handle exception and print the error
|
||||
Console.WriteLine(e);
|
||||
return null;
|
||||
return null; // keep running for non-critical errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ControlConstants(this StatusRecord? statusrecord)
|
||||
{
|
||||
if (statusrecord == null) return;
|
||||
|
||||
statusrecord.InverterRecord1.Battery1BackupSoc = (Single)statusrecord.Config.MinSoc ;
|
||||
statusrecord.InverterRecord1.Battery2BackupSoc = (Single)statusrecord.Config.MinSoc ;
|
||||
statusrecord.InverterRecord1.RepetitiveWeeks = SinexcelWeekDays.All;
|
||||
|
||||
|
||||
var isChargePeriod = IsNowInsideDateAndTime(statusrecord.Config.StartTimeChargeandDischargeDayandTime, statusrecord.Config.StopTimeChargeandDischargeDayandTime);
|
||||
|
||||
|
||||
Console.WriteLine("Are we inside the charge/Discharge time " + isChargePeriod);
|
||||
|
||||
if (statusrecord.Config.OperatingPriority != TimeChargeDischarge)
|
||||
{
|
||||
statusrecord.InverterRecord1.WorkingMode = statusrecord.Config.OperatingPriority;
|
||||
}
|
||||
else if (statusrecord.Config.OperatingPriority == TimeChargeDischarge && isChargePeriod)
|
||||
{
|
||||
statusrecord.InverterRecord1.WorkingMode = statusrecord.Config.OperatingPriority;
|
||||
|
||||
if (statusrecord.Config.TimeChargeandDischargePower > 0)
|
||||
{
|
||||
statusrecord.InverterRecord1.EffectiveStartDate = statusrecord.Config.StartTimeChargeandDischargeDayandTime.Date;
|
||||
statusrecord.InverterRecord1.EffectiveEndDate = statusrecord.Config.StopTimeChargeandDischargeDayandTime.Date;
|
||||
statusrecord.InverterRecord1.ChargingPowerPeriod1 = Math.Abs(statusrecord.Config.TimeChargeandDischargePower);
|
||||
statusrecord.InverterRecord1.ChargeStartTimePeriod1 = statusrecord.Config.StartTimeChargeandDischargeDayandTime.TimeOfDay;
|
||||
statusrecord.InverterRecord1.ChargeEndTimePeriod1 = statusrecord.Config.StopTimeChargeandDischargeDayandTime.TimeOfDay;
|
||||
|
||||
statusrecord.InverterRecord1.DischargeStartTimePeriod1 = TimeSpan.Zero;
|
||||
statusrecord.InverterRecord1.DischargeEndTimePeriod1 = TimeSpan.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
statusrecord.InverterRecord1.EffectiveStartDate = statusrecord.Config.StartTimeChargeandDischargeDayandTime.Date;
|
||||
statusrecord.InverterRecord1.EffectiveEndDate = statusrecord.Config.StopTimeChargeandDischargeDayandTime.Date;
|
||||
statusrecord.InverterRecord1.DishargingPowerPeriod1 = Math.Abs(statusrecord.Config.TimeChargeandDischargePower);
|
||||
statusrecord.InverterRecord1.DischargeStartTimePeriod1 = statusrecord.Config.StartTimeChargeandDischargeDayandTime.TimeOfDay;
|
||||
statusrecord.InverterRecord1.DischargeEndTimePeriod1 = statusrecord.Config.StopTimeChargeandDischargeDayandTime.TimeOfDay;
|
||||
|
||||
statusrecord.InverterRecord1.ChargeStartTimePeriod1 = TimeSpan.Zero;
|
||||
statusrecord.InverterRecord1.ChargeEndTimePeriod1 = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
statusrecord.InverterRecord1.WorkingMode = SpontaneousSelfUse;
|
||||
}
|
||||
statusrecord.InverterRecord1.PowerOn = 1;
|
||||
statusrecord.InverterRecord1.PowerOff = 0;
|
||||
//statusrecord.InverterRecord.FaultClearing = 1;
|
||||
}
|
||||
|
||||
private static bool IsNowInsideDateAndTime(DateTime effectiveStart, DateTime effectiveEnd)
|
||||
{
|
||||
DateTime now = DateTime.Now;
|
||||
|
||||
// Date check
|
||||
if (now < effectiveStart || now > effectiveEnd)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private static StatusMessage GetSodiHomeStateAlarm(StatusRecord? record)
|
||||
{
|
||||
|
|
@ -231,6 +362,43 @@ internal static class Program
|
|||
return returnedStatus;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the error or warning content has changed compared to the previous state.
|
||||
/// This allows detection of new/cleared errors even when the overall alarm state (Red/Orange/Green) remains the same.
|
||||
/// </summary>
|
||||
private static Boolean HasErrorsOrWarningsChanged(StatusMessage currentState)
|
||||
{
|
||||
// Get current error codes (descriptions)
|
||||
var currentErrors = currentState.Alarms?
|
||||
.Select(a => a.Description ?? String.Empty)
|
||||
.OrderBy(d => d) // Sort for consistent comparison
|
||||
.ToList() ?? new List<String>();
|
||||
|
||||
// Get current warning codes (descriptions)
|
||||
var currentWarnings = currentState.Warnings?
|
||||
.Select(w => w.Description ?? String.Empty)
|
||||
.OrderBy(d => d) // Sort for consistent comparison
|
||||
.ToList() ?? new List<String>();
|
||||
|
||||
// Check if lists have changed (new items added or existing items removed)
|
||||
var errorsChanged = !currentErrors.SequenceEqual(_prevErrorCodes);
|
||||
var warningsChanged = !currentWarnings.SequenceEqual(_prevWarningCodes);
|
||||
|
||||
// Update tracking if changes detected
|
||||
if (errorsChanged || warningsChanged)
|
||||
{
|
||||
Console.WriteLine($"Error/Warning content changed:");
|
||||
Console.WriteLine($" Errors: {String.Join(", ", currentErrors)} (was: {String.Join(", ", _prevErrorCodes)})");
|
||||
Console.WriteLine($" Warnings: {String.Join(", ", currentWarnings)} (was: {String.Join(", ", _prevWarningCodes)})");
|
||||
|
||||
_prevErrorCodes = currentErrors;
|
||||
_prevWarningCodes = currentWarnings;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Int32 GetInstallationId(String s3Bucket)
|
||||
{
|
||||
var part = s3Bucket.Split('-').FirstOrDefault();
|
||||
|
|
@ -249,14 +417,63 @@ internal static class Program
|
|||
_subscribeToQueueForTheFirstTime = true;
|
||||
_prevSodiohomeAlarmState = currentSalimaxState.Status;
|
||||
_subscribedToQueue = RabbitMqManager.SubscribeToQueue(currentSalimaxState, s3Bucket, VpnServerIp);
|
||||
_lastHeartbeatTime = DateTime.Now; // Initialize heartbeat timer
|
||||
}
|
||||
|
||||
// Check if we should send a message
|
||||
var stateChanged = currentSalimaxState.Status != _prevSodiohomeAlarmState;
|
||||
var contentChanged = HasErrorsOrWarningsChanged(currentSalimaxState);
|
||||
var needsHeartbeat = (DateTime.Now - _lastHeartbeatTime).TotalSeconds >= HeartbeatIntervalSeconds;
|
||||
Console.WriteLine($"subscribedNow={subscribedNow}");
|
||||
Console.WriteLine($"_subscribedToQueue={_subscribedToQueue}");
|
||||
Console.WriteLine($"stateChanged={stateChanged}");
|
||||
Console.WriteLine($"contentChanged={contentChanged}");
|
||||
Console.WriteLine($"needsHeartbeat={needsHeartbeat}");
|
||||
Console.WriteLine($"s3Bucket null? {s3Bucket == null}");
|
||||
|
||||
if (s3Bucket == null)
|
||||
{
|
||||
Console.WriteLine("⚠ S3 bucket not configured. Skipping middleware send.");
|
||||
LogMiddlewareFailure(new Exception("S3 Bucket not configured"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure connection FIRST
|
||||
if (!RabbitMqManager.EnsureConnected(currentSalimaxState, s3Bucket, VpnServerIp))
|
||||
{
|
||||
Console.WriteLine($"❌ RabbitMQ EnsureConnected FAILED at {DateTime.Now:HH:mm:ss.fff}");
|
||||
LogMiddlewareFailure(new Exception("EnsureConnected returned false"));
|
||||
return;
|
||||
}
|
||||
|
||||
//If already subscribed to the queue and the status has been changed, update the queue
|
||||
if (!subscribedNow && _subscribedToQueue && currentSalimaxState.Status != _prevSodiohomeAlarmState)
|
||||
if (!subscribedNow && (stateChanged || contentChanged || needsHeartbeat))
|
||||
{
|
||||
_prevSodiohomeAlarmState = currentSalimaxState.Status;
|
||||
if (s3Bucket != null)
|
||||
|
||||
// Set appropriate message type
|
||||
if (stateChanged || contentChanged)
|
||||
{
|
||||
currentSalimaxState.Type = MessageType.AlarmOrWarning;
|
||||
Console.WriteLine($"Sending AlarmOrWarning message - StateChanged: {stateChanged}, ContentChanged: {contentChanged}");
|
||||
}
|
||||
else if (needsHeartbeat)
|
||||
{
|
||||
currentSalimaxState.Type = MessageType.Heartbit;
|
||||
Console.WriteLine($"Sending Heartbeat message - {HeartbeatIntervalSeconds}s interval reached");
|
||||
_lastHeartbeatTime = DateTime.Now;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RabbitMqManager.InformMiddleware(currentSalimaxState);
|
||||
LogMiddlewareFailure(new Exception($"✅ Middleware message sent at {DateTime.Now:HH:mm:ss.fff}"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ Failed to send middleware message: {ex.Message}");
|
||||
LogMiddlewareFailure(ex);
|
||||
}
|
||||
}
|
||||
|
||||
//If there is an available message from the RabbitMQ Broker, apply the configuration file
|
||||
|
|
@ -267,6 +484,28 @@ internal static class Program
|
|||
}
|
||||
}
|
||||
|
||||
private static void LogMiddlewareFailure(Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
var logPath = "/home/inesco/SodiStoreHome/middleware_failures.log";
|
||||
|
||||
var logEntry =
|
||||
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}]\n" +
|
||||
$"Exception: {ex.GetType().FullName}\n" +
|
||||
$"Message: {ex.Message}\n" +
|
||||
$"StackTrace:\n{ex.StackTrace}\n" +
|
||||
$"--------------------------------------------------\n";
|
||||
|
||||
File.AppendAllText(logPath, logEntry);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Never allow logging to crash the service
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void ApplyConfigFile(this StatusRecord? status, Configuration? config)
|
||||
{
|
||||
if (config == null) return;
|
||||
|
|
@ -277,6 +516,14 @@ internal static class Program
|
|||
status.Config.MaximumDischargingCurrent = config.MaximumDischargingCurrent;
|
||||
status.Config.OperatingPriority = config.OperatingPriority;
|
||||
status.Config.BatteriesCount = config.BatteriesCount;
|
||||
status.Config.ClusterNumber = config.ClusterNumber;
|
||||
status.Config.StartTimeChargeandDischargeDayandTime = config.StartTimeChargeandDischargeDayandTime;
|
||||
status.Config.StopTimeChargeandDischargeDayandTime = config.StopTimeChargeandDischargeDayandTime;
|
||||
status.Config.TimeChargeandDischargePower = config.TimeChargeandDischargePower;
|
||||
|
||||
status.Config.PvNumber = config.PvNumber;
|
||||
status.Config.ControlPermission = config.ControlPermission;
|
||||
|
||||
}
|
||||
|
||||
private static async Task<Boolean> SaveModbusTcpFile(StatusRecord status)
|
||||
|
|
@ -285,13 +532,41 @@ internal static class Program
|
|||
|
||||
// SYSTEM DATA
|
||||
var result1 = ConvertToModbusRegisters((status.Config.ModbusProtcolNumber * 10), "UInt16", 30001); // this to be updated to modbusTCP version
|
||||
var result2 = ConvertToModbusRegisters(status.InverterRecord.SystemDateTime.ToUnixTime(), "UInt32", 30002);
|
||||
var result2 = ConvertToModbusRegisters(status.InverterRecord1.SystemDateTime.ToUnixTime(), "UInt32", 30002);
|
||||
// SYSTEM DATA
|
||||
var result3 = ConvertToModbusRegisters(status.InverterRecord1.WorkingMode, "UInt16", 30004);
|
||||
|
||||
// BATTERY SUMMARY (assuming single battery [0]) // this to be improved
|
||||
|
||||
var result4 = ConvertToModbusRegisters((status.Config.BatteriesCount), "UInt16", 31000);
|
||||
var result8 = ConvertToModbusRegisters((status.InverterRecord1.Battery1Voltage.Value * 10), "UInt16", 31001);
|
||||
var result12 = ConvertToModbusRegisters((status.InverterRecord1.Battery2Voltage.Value * 10), "Int16", 31002);
|
||||
var result13 = ConvertToModbusRegisters((status.InverterRecord1.Battery1Current.Value * 10), "Int32", 31003);
|
||||
var result16 = ConvertToModbusRegisters((status.InverterRecord1.Battery2Current.Value * 10), "Int32", 31005);
|
||||
var result9 = ConvertToModbusRegisters((status.InverterRecord1.Battery1Soc.Value * 100), "UInt16", 31007);
|
||||
var result14 = ConvertToModbusRegisters((status.InverterRecord1.Battery2Soc.Value * 100), "UInt16", 31008);
|
||||
var result5 = ConvertToModbusRegisters((status.InverterRecord1.TotalBatteryPower.Value * 10), "Int32", 31009);
|
||||
|
||||
var result7 = ConvertToModbusRegisters((status.InverterRecord1.Battery1BackupSoc * 100), "UInt16", 31011);
|
||||
var result20 = ConvertToModbusRegisters((status.InverterRecord1.Battery2BackupSoc * 100), "UInt16", 31012);
|
||||
var result15 = ConvertToModbusRegisters((status.InverterRecord1.Battery1Soh.Value * 100), "UInt16", 31013);
|
||||
var result26 = ConvertToModbusRegisters((status.InverterRecord1.Battery2Soh.Value * 100), "UInt16", 31014);
|
||||
var result21 = ConvertToModbusRegisters((status.InverterRecord1.Battery1MaxChargingCurrent * 10), "UInt16", 31016);
|
||||
var result22 = ConvertToModbusRegisters((status.InverterRecord1.Battery1MaxDischargingCurrent * 10), "UInt16", 31017);
|
||||
|
||||
var result18 = ConvertToModbusRegisters((status.InverterRecord1.PvTotalPower * 10), "UInt32", 32000);
|
||||
var result19 = ConvertToModbusRegisters((status.InverterRecord1.GridPower * 10), "Int32", 33000);
|
||||
var result23 = ConvertToModbusRegisters((status.InverterRecord1.GridVoltageFrequency * 10), "UInt16", 33002);
|
||||
|
||||
var result24 = ConvertToModbusRegisters((status.InverterRecord1.WorkingMode), "UInt16", 34000);
|
||||
var result25 = ConvertToModbusRegisters((status.InverterRecord1.InverterActivePower * 10), "Int32", 34001);
|
||||
var result29 = ConvertToModbusRegisters((status.InverterRecord1.EnableGridExport ), "UInt16", 34003);
|
||||
var result27 = ConvertToModbusRegisters((status.InverterRecord1.PowerGridExportLimit ), "Int16", 34004);
|
||||
|
||||
// Merge all results into one dictionary
|
||||
|
||||
var allResults = new[]
|
||||
{
|
||||
result1,result2
|
||||
result1, result2, result3, result4, result5, result23, result24, result25, result29, result27, result26, result7, result8, result9, result16, result20, result12, result13, result14, result15, result18, result19, result21, result22
|
||||
};
|
||||
|
||||
foreach (var result in allResults)
|
||||
|
|
@ -518,7 +793,7 @@ internal static class Program
|
|||
Console.WriteLine(error);
|
||||
|
||||
await SaveToLocalCompressedFallback(compressedBytes, fileNameWithoutExtension);
|
||||
Heartbit();
|
||||
// Heartbit();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -526,7 +801,7 @@ internal static class Program
|
|||
Console.WriteLine("✅ File uploaded to S3 successfully.");
|
||||
|
||||
Console.WriteLine("---------------------------------------- Resending FailedUploadedFiles----------------------------------------");
|
||||
Heartbit();
|
||||
// Heartbit();
|
||||
|
||||
await ResendLocalFailedFilesAsync(s3Config); // retry any pending failed files
|
||||
}
|
||||
|
|
@ -539,7 +814,7 @@ internal static class Program
|
|||
await SaveToLocalCompressedFallback(compressedBytes, fileNameWithoutExtension);
|
||||
}
|
||||
|
||||
Heartbit();
|
||||
//Heartbit();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -548,11 +823,10 @@ internal static class Program
|
|||
return true;
|
||||
}
|
||||
|
||||
private static void Heartbit()
|
||||
/* 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
|
||||
|
|
@ -565,7 +839,7 @@ internal static class Program
|
|||
if (s3Bucket != null)
|
||||
RabbitMqManager.InformMiddleware(returnedStatus);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
private static async Task SaveToLocalCompressedFallback(Byte[] compressedData, String fileNameWithoutExtension)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -176,6 +176,11 @@ public partial class KacoRecord
|
|||
/// </summary>
|
||||
public Int16 BatteryCurrentScaleFactor => _battCharASf;
|
||||
|
||||
|
||||
public Int16 ActivePowerW => _activePowerW;
|
||||
public Int16 ReactivePowerVar => _reactivePowerVar;
|
||||
public Int16 LineFrequencyHz => _lineFrequencyHz;
|
||||
|
||||
// Helper wrappers for scaled values
|
||||
private float ScaleBattVoltage(UInt16 raw) => ScaleSunspec(raw, _battCharVSf);
|
||||
private float ScaleBattCurrent(UInt16 raw) => ScaleSunspec(raw, _battCharASf);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public partial class KacoRecord
|
|||
// State control
|
||||
[HoldingRegister<UInt16>(41065, writable: true)] private UInt16 _requestedState; // 0xA069 – RequestedState (enum16, RW)
|
||||
[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
|
||||
|
||||
// Watchdog / setpoints
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@ public partial record SystemControlRegisters
|
|||
{
|
||||
private const UInt16 NoTimeout = UInt16.MaxValue;
|
||||
|
||||
[HoldingRegister(1016)] private UInt16 _CommunicationTimeoutSeconds;
|
||||
[HoldingRegister(1016, writable: true)] private UInt16 _CommunicationTimeoutSeconds;
|
||||
|
||||
[HoldingRegister(1018)] public SystemConfig SystemConfig { get; set; }
|
||||
[HoldingRegister(1018, writable: true)] public SystemConfig SystemConfig { get; set; }
|
||||
|
||||
//[Coil(4002)]
|
||||
[HoldingRegister(4002)] public Boolean ResetAlarmsAndWarnings { get; set; }
|
||||
[HoldingRegister(4002, writable: true)] public Boolean ResetAlarmsAndWarnings { get; set; }
|
||||
|
||||
[HoldingRegister(4007)] public UInt16 TargetSlave { get; set; }
|
||||
[HoldingRegister(4007, writable: true)] public UInt16 TargetSlave { get; set; }
|
||||
|
||||
//[Coil(4011)]
|
||||
[HoldingRegister(4011, writable: true)] public Boolean UseSlaveIdForAddressing { get; set; }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
|||
public partial class DcDcRecord
|
||||
{
|
||||
//[Coil(4000)]
|
||||
[HoldingRegister(4000)] internal Boolean PowerStageEnable ;
|
||||
[HoldingRegister(4000, writable: true)] internal Boolean PowerStageEnable ;
|
||||
//[Coil(4002)]
|
||||
[HoldingRegister(4002, writable: true)] internal Boolean ResetAlarmsAndWarnings ;
|
||||
[HoldingRegister(4100, writable: true, Scale = .01)] internal Double MaxBatteryVoltage ;
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ public class ModbusDevice<[DynamicallyAccessedMembers(All)] R> where R : notnull
|
|||
{
|
||||
_modbusClient.Channel.ClearBuffers();
|
||||
batch.Write(record);
|
||||
//Thread.Sleep(50); // this added mainly for Growatt reading
|
||||
//Thread.Sleep(50); // this added mainly for Growatt writing
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue