diff --git a/csharp/App/GpioTestingProject/GpioTestingProject.csproj b/csharp/App/GpioTestingProject/GpioTestingProject.csproj
new file mode 100644
index 000000000..a4f20b48e
--- /dev/null
+++ b/csharp/App/GpioTestingProject/GpioTestingProject.csproj
@@ -0,0 +1,16 @@
+
+
+
+ InnovEnergy.App.GpioTestingProject
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/csharp/App/GpioTestingProject/Program.cs b/csharp/App/GpioTestingProject/Program.cs
new file mode 100644
index 000000000..aba8600a8
--- /dev/null
+++ b/csharp/App/GpioTestingProject/Program.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Threading;
+using GPIORaspberryPI4;
+
+namespace InnovEnergy.App.GpioTestingProject;
+
+static class Program
+{
+ static void Main(string[] args)
+ {
+ Console.WriteLine("GPIO17 Relay Test Starting...");
+
+ using IRelayOutput relay = new RelayOutput(pin: 27, activeLow: false);
+ using IDigitalInput input = new DigitalInput(pin: 17, pullUp: true, activeLow: true);
+
+ try
+ {
+ while (true)
+ {
+ if (input.Read())
+ {
+ relay.On();
+ Console.Write("\rInput ACTIVE -> Relay ON ");
+ }
+ else
+ {
+ relay.Off();
+ Console.Write("\rInput INACTIVE -> Relay OFF ");
+ }
+
+ Thread.Sleep(100);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"ERROR: {ex.Message}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/csharp/App/GpioTestingProject/deploy.sh b/csharp/App/GpioTestingProject/deploy.sh
new file mode 100755
index 000000000..3208a0c80
--- /dev/null
+++ b/csharp/App/GpioTestingProject/deploy.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+dotnet_version='net6.0'
+salimax_ip="$1"
+is_release="$2" # Pass --release if this is a real release
+username='inesco'
+root_password='Sodistore0918425'
+
+ DOTNET="/snap/dotnet-sdk_60/current/dotnet"
+
+
+set -e
+
+echo -e "\n============================ Build ============================\n"
+
+"$DOTNET" publish \
+ ./GpioTestingProject.csproj \
+ -p:PublishTrimmed=false \
+ -c Release \
+ -r linux-arm64
+
+echo -e "\n============================ Deploy ============================\n"
+
+rsync -v \
+ --exclude '*.pdb' \
+ ./bin/Release/$dotnet_version/linux-arm64/publish/* \
+ $username@"$salimax_ip":~/
+
\ No newline at end of file
diff --git a/csharp/App/GrowattCommunication/AggregationService/AggregateDataFileWriter.cs b/csharp/App/GrowattCommunication/AggregationService/AggregateDataFileWriter.cs
new file mode 100644
index 000000000..b289b82d7
--- /dev/null
+++ b/csharp/App/GrowattCommunication/AggregationService/AggregateDataFileWriter.cs
@@ -0,0 +1,76 @@
+using System;
+using System.IO;
+using System.Text.Json;
+
+namespace InnovEnergy.App.GrowattCommunication.AggregationService;
+
+
+public class HourlyEnergyData
+{
+ public String Type { get; set; } = "Hourly";
+ public DateTime Timestamp { get; set; }
+
+ public double SelfGeneratedElectricity { get; set; }
+ public double ElectricityPurchased { get; set; }
+ public double ElectricityFed { get; set; }
+ public double BatteryChargeEnergy { get; set; }
+ public double BatteryDischargeEnergy { get; set; }
+ public double LoadPowerConsumption { get; set; }
+}
+
+public class DailyEnergyData
+{
+ public String Type { get; set; } = "Daily";
+ public DateTime Timestamp { get; set; }
+
+ public double DailySelfGeneratedElectricity { get; set; }
+ public double DailyElectricityPurchased { get; set; }
+ public double DailyElectricityFed { get; set; }
+ public double BatteryDailyChargeEnergy { get; set; }
+ public double BatteryDailyDischargeEnergy { get; set; }
+ public double DailyLoadPowerConsumption { get; set; }
+}
+
+public static class AggregatedDataFileWriter
+{
+ private static bool _folderCreated = false;
+
+ private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
+ {
+ WriteIndented = false
+ };
+
+ public static void AppendHourlyData(HourlyEnergyData data, string baseFolder)
+ {
+ var filePath = GetDailyFilePath(data.Timestamp, baseFolder);
+ AppendJsonLine(filePath, data);
+ Console.WriteLine($"Hourly data appended to {filePath}");
+ }
+
+ public static void AppendDailyData(DailyEnergyData data, string baseFolder)
+ {
+ var filePath = GetDailyFilePath(data.Timestamp, baseFolder);
+ AppendJsonLine(filePath, data);
+ Console.WriteLine($"Daily data appended to {filePath}");
+ }
+
+ private static string GetDailyFilePath(DateTime timestamp, string baseFolder)
+ {
+ var folder = Path.Combine(baseFolder, "AggregatedData");
+
+ if (!_folderCreated)
+ {
+ Directory.CreateDirectory(folder);
+ _folderCreated = true;
+ }
+
+ var fileName = timestamp.ToString("ddMMyyyy") + ".json";
+ return Path.Combine(folder, fileName);
+ }
+
+ private static void AppendJsonLine(string filePath, T data)
+ {
+ var json = JsonSerializer.Serialize(data, JsonOptions);
+ File.AppendAllText(filePath, json + Environment.NewLine);
+ }
+}
\ No newline at end of file
diff --git a/csharp/App/GrowattCommunication/AggregationService/HourlyEnergyData.cs b/csharp/App/GrowattCommunication/AggregationService/HourlyEnergyData.cs
new file mode 100644
index 000000000..c6b20d919
--- /dev/null
+++ b/csharp/App/GrowattCommunication/AggregationService/HourlyEnergyData.cs
@@ -0,0 +1,160 @@
+using System;
+using System.Collections.Generic;
+using InnovEnergy.App.GrowattCommunication.ESS;
+
+namespace InnovEnergy.App.GrowattCommunication.AggregationService;
+
+public class HourlyAccumulator
+{
+ public DateTime HourStart { get; set; }
+
+ public double StartSelfGeneratedElectricity { get; set; }
+ public double StartElectricityPurchased { get; set; }
+ public double StartElectricityFed { get; set; }
+ public double StartBatteryChargeEnergy { get; set; }
+ public double StartBatteryDischargeEnergy { get; set; }
+ public double StartLoadPowerConsumption { get; set; }
+
+ public double LastSelfGeneratedElectricity { get; set; }
+ public double LastElectricityPurchased { get; set; }
+ public double LastElectricityFed { get; set; }
+ public double LastBatteryChargeEnergy { get; set; }
+ public double LastBatteryDischargeEnergy { get; set; }
+ public double LastLoadPowerConsumption { get; set; }
+}
+
+public static class EnergyAggregation
+{
+ private static HourlyAccumulator? _currentHourAccumulator;
+ private static DateTime? _lastDailySaveDate;
+/*
+
+ public static HourlyEnergyData? ProcessHourlyData(StatusRecord statusRecord, DateTime timestamp)
+ {
+ var r = statusRecord.InverterRecord;
+ var hourStart = new DateTime(timestamp.Year, timestamp.Month, timestamp.Day, timestamp.Hour, 0, 0);
+
+ // First call
+ if (_currentHourAccumulator == null)
+ {
+ _currentHourAccumulator = new HourlyAccumulator
+ {
+ HourStart = hourStart,
+
+ StartSelfGeneratedElectricity = r.SelfGeneratedElectricity,
+ StartElectricityPurchased = r.ElectricityPurchased,
+ StartElectricityFed = r.ElectricityFed,
+ StartBatteryChargeEnergy = r.BatteryChargeEnergy,
+ StartBatteryDischargeEnergy = r.BatteryDischargeEnergy,
+ StartLoadPowerConsumption = r.LoadPowerConsumption,
+
+ LastSelfGeneratedElectricity = r.SelfGeneratedElectricity,
+ LastElectricityPurchased = r.ElectricityPurchased,
+ LastElectricityFed = r.ElectricityFed,
+ LastBatteryChargeEnergy = r.BatteryChargeEnergy,
+ LastBatteryDischargeEnergy = r.BatteryDischargeEnergy,
+ LastLoadPowerConsumption = r.LoadPowerConsumption
+ };
+
+ return null;
+ }
+
+ // Still same hour → just update last values
+ if (_currentHourAccumulator.HourStart == hourStart)
+ {
+ _currentHourAccumulator.LastSelfGeneratedElectricity = r.SelfGeneratedElectricity;
+ _currentHourAccumulator.LastElectricityPurchased = r.ElectricityPurchased;
+ _currentHourAccumulator.LastElectricityFed = r.ElectricityFed;
+ _currentHourAccumulator.LastBatteryChargeEnergy = r.BatteryChargeEnergy;
+ _currentHourAccumulator.LastBatteryDischargeEnergy = r.BatteryDischargeEnergy;
+ _currentHourAccumulator.LastLoadPowerConsumption = r.LoadPowerConsumption;
+
+ return null;
+ }
+
+ // Hour changed → finalize previous hour
+ var completedHour = new HourlyEnergyData
+ {
+ Timestamp = _currentHourAccumulator.HourStart,
+
+ SelfGeneratedElectricity = SafeDiff(
+ _currentHourAccumulator.LastSelfGeneratedElectricity,
+ _currentHourAccumulator.StartSelfGeneratedElectricity),
+
+ ElectricityPurchased = SafeDiff(
+ _currentHourAccumulator.LastElectricityPurchased,
+ _currentHourAccumulator.StartElectricityPurchased),
+
+ ElectricityFed = SafeDiff(
+ _currentHourAccumulator.LastElectricityFed,
+ _currentHourAccumulator.StartElectricityFed),
+
+ BatteryChargeEnergy = SafeDiff(
+ _currentHourAccumulator.LastBatteryChargeEnergy,
+ _currentHourAccumulator.StartBatteryChargeEnergy),
+
+ BatteryDischargeEnergy = SafeDiff(
+ _currentHourAccumulator.LastBatteryDischargeEnergy,
+ _currentHourAccumulator.StartBatteryDischargeEnergy),
+
+ LoadPowerConsumption = SafeDiff(
+ _currentHourAccumulator.LastLoadPowerConsumption,
+ _currentHourAccumulator.StartLoadPowerConsumption)
+ };
+
+ // Start new hour with current sample
+ _currentHourAccumulator = new HourlyAccumulator
+ {
+ HourStart = hourStart,
+
+ StartSelfGeneratedElectricity = r.SelfGeneratedElectricity,
+ StartElectricityPurchased = r.ElectricityPurchased,
+ StartElectricityFed = r.ElectricityFed,
+ StartBatteryChargeEnergy = r.BatteryChargeEnergy,
+ StartBatteryDischargeEnergy = r.BatteryDischargeEnergy,
+ StartLoadPowerConsumption = r.LoadPowerConsumption,
+
+ LastSelfGeneratedElectricity = r.SelfGeneratedElectricity,
+ LastElectricityPurchased = r.ElectricityPurchased,
+ LastElectricityFed = r.ElectricityFed,
+ LastBatteryChargeEnergy = r.BatteryChargeEnergy,
+ LastBatteryDischargeEnergy = r.BatteryDischargeEnergy,
+ LastLoadPowerConsumption = r.LoadPowerConsumption
+ };
+
+ return completedHour;
+ }*/
+
+ public static DailyEnergyData? TryCreateDailyData(StatusRecord statusRecord, DateTime timestamp)
+ {
+ if (timestamp is { Hour: 23, Minute: 59 })
+ {
+ if (_lastDailySaveDate != timestamp.Date)
+ {
+ _lastDailySaveDate = timestamp.Date;
+
+ var r = statusRecord.InverterRecord;
+
+ return new DailyEnergyData
+ {
+ Timestamp = timestamp,
+
+ // DailySelfGeneratedElectricity = r.DailySelfGeneratedElectricity,
+ DailyElectricityPurchased = r.EnergyToUser,
+ DailyElectricityFed = r.EnergyToGrid,
+ BatteryDailyChargeEnergy = r.BatteryDailyChargeEnergy,
+ BatteryDailyDischargeEnergy = r.BatteryDailyDischargeEnergy,
+ // DailyLoadPowerConsumption = r.DailyLoadPowerConsumption
+ };
+ }
+ }
+
+ return null;
+ }
+
+ private static double SafeDiff(double endValue, double startValue)
+ {
+ var diff = endValue - startValue;
+ return diff < 0 ? 0 : diff;
+ }
+}
\ No newline at end of file
diff --git a/csharp/App/GrowattCommunication/MiddlewareClasses/MiddlewareAgent.cs b/csharp/App/GrowattCommunication/MiddlewareClasses/MiddlewareAgent.cs
index 279e39052..84a6ef8fc 100644
--- a/csharp/App/GrowattCommunication/MiddlewareClasses/MiddlewareAgent.cs
+++ b/csharp/App/GrowattCommunication/MiddlewareClasses/MiddlewareAgent.cs
@@ -9,24 +9,41 @@ namespace InnovEnergy.App.GrowattCommunication.MiddlewareClasses;
public static class MiddlewareAgent
{
- private static UdpClient _udpListener = null!;
+ private static UdpClient _udpListener = null!;
private static IPAddress? _controllerIpAddress;
- private static EndPoint? _endPoint;
+ private static EndPoint? _endPoint;
- public static void InitializeCommunicationToMiddleware()
+ public static bool InitializeCommunicationToMiddleware()
{
- _controllerIpAddress = FindVpnIp();
- if (Equals(IPAddress.None, _controllerIpAddress))
+ try
{
- Console.WriteLine("There is no VPN interface, exiting...");
+ _controllerIpAddress = FindVpnIp();
+ if (Equals(IPAddress.None, _controllerIpAddress))
+ {
+ Console.WriteLine("There is no VPN interface.");
+ _udpListener = null;
+ return false;
+ }
+
+ const int udpPort = 9000;
+ _endPoint = new IPEndPoint(_controllerIpAddress, udpPort);
+
+ _udpListener?.Close();
+ _udpListener?.Dispose();
+
+ _udpListener = new UdpClient();
+ _udpListener.Client.Blocking = false;
+ _udpListener.Client.Bind(_endPoint);
+
+ Console.WriteLine($"UDP listener bound to {_endPoint}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Failed to initialize middleware communication: {ex}");
+ _udpListener = null;
+ return false;
}
-
- const Int32 udpPort = 9000;
- _endPoint = new IPEndPoint(_controllerIpAddress, udpPort);
-
- _udpListener = new UdpClient();
- _udpListener.Client.Blocking = false;
- _udpListener.Client.Bind(_endPoint);
}
private static IPAddress FindVpnIp()
@@ -50,40 +67,92 @@ public static class MiddlewareAgent
return IPAddress.None;
}
-
public static Configuration? SetConfigurationFile()
{
- if (_udpListener.Available > 0)
+ try
{
+ // Ensure listener is initialized
+ if (_udpListener == null)
+ {
+ Console.WriteLine("UDP listener not initialized, trying to initialize...");
+ InitializeCommunicationToMiddleware();
+
+ if (_udpListener == null)
+ {
+ Console.WriteLine("Failed to initialize UDP listener.");
+ return null;
+ }
+ }
+
+ // Check if data is available
+ if (_udpListener.Available <= 0)
+ return null;
+
IPEndPoint? serverEndpoint = null;
-
- var replyMessage = "ACK";
- var replyData = Encoding.UTF8.GetBytes(replyMessage);
-
+
var udpMessage = _udpListener.Receive(ref serverEndpoint);
- var message = Encoding.UTF8.GetString(udpMessage);
-
+ var message = Encoding.UTF8.GetString(udpMessage);
+
+ Console.WriteLine($"Received raw UDP message from {serverEndpoint}: {message}");
+
var config = JsonSerializer.Deserialize(message);
-
+
if (config != null)
{
- Console.WriteLine($"Received a configuration message: " +
- "MinimumSoC is " + config.MinimumSoC + " and operating priorty is " +config.OperatingPriority + "Number of batteries is " + config.BatteriesCount
- + "MaximumChargingCurrent is " + config.MaximumChargingCurrent + "MaximumDischargingCurrent " + config.MaximumDischargingCurrent + " Control permission is" + config.ControlPermission );
-
- // Send the reply to the sender's endpoint
+ Console.WriteLine(
+ $"Received a configuration message:\n" +
+ $"MinimumSoC: {config.MinimumSoC}\n" +
+ $"OperatingPriority: {config.OperatingPriority}\n" +
+ $"Number of batteries: {config.BatteriesCount}\n" +
+ $"Maximum Charging current: {config.MaximumChargingCurrent}\n" +
+ $"Maximum Discharging current: {config.MaximumDischargingCurrent}\n" +
+ $"ControlPermission: {config.ControlPermission}"
+ );
+
+ // Send ACK
+ var replyMessage = "ACK";
+ var replyData = Encoding.UTF8.GetBytes(replyMessage);
+
_udpListener.Send(replyData, replyData.Length, serverEndpoint);
Console.WriteLine($"Replied to {serverEndpoint}: {replyMessage}");
+
return config;
}
+ else
+ {
+ Console.WriteLine("Received UDP message but failed to deserialize Configuration.");
+ return null;
+ }
}
-
- if (_endPoint != null && !_endPoint.Equals(_udpListener.Client.LocalEndPoint as IPEndPoint))
+ catch (SocketException ex)
{
- Console.WriteLine("UDP address has changed, rebinding...");
- InitializeCommunicationToMiddleware();
- }
- return null;
- }
+ Console.WriteLine($"Socket error in SetConfigurationFile: {ex}");
+
+ // Recover by reinitializing
+ try
+ {
+ _udpListener?.Close();
+ _udpListener?.Dispose();
+ }
+ catch
+ {
+ // ignored
+ }
+ _udpListener = null;
+ InitializeCommunicationToMiddleware();
+
+ return null;
+ }
+ catch (JsonException ex)
+ {
+ Console.WriteLine($"JSON deserialization error: {ex}");
+ return null;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Unexpected error in SetConfigurationFile: {ex}");
+ return null;
+ }
+ }
}
\ No newline at end of file
diff --git a/csharp/App/GrowattCommunication/Program.cs b/csharp/App/GrowattCommunication/Program.cs
index 78461c1c9..01de544a2 100644
--- a/csharp/App/GrowattCommunication/Program.cs
+++ b/csharp/App/GrowattCommunication/Program.cs
@@ -32,7 +32,7 @@ namespace InnovEnergy.App.GrowattCommunication;
public static class Program
{
- private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(6);
+ private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(10);
private const UInt16 NbrOfFileToConcatenate = 15; // add this to config file
private static UInt16 _fileCounter = 0;
//
@@ -165,6 +165,10 @@ public static class Program
statusrecord.InverterRecord.BatteryOperatingMode.WriteLine(" = BatteryOperatingMode");
statusrecord.InverterRecord.OperatingMode.WriteLine(" = OperatingPriority"); // 30408 this the duration
+ statusrecord.InverterRecord.ExportLimitationEnabled.WriteLine(" = ExportLimitationEnabled");
+ statusrecord.InverterRecord.ExportLimitationPowerRate.WriteLine(" = ExportLimitationPowerRate"); // 30408 this the duration
+
+
statusrecord.InverterRecord.FaultMainCode.WriteLine(" = FaultMainCode"); // 30408 this the duration
statusrecord.InverterRecord.FaultSubCode.WriteLine(" = FaultSubCode"); // 30408 this the duration
statusrecord.InverterRecord.WarningMainCode.WriteLine(" = WarningMainCode"); // 30408 this the duration
@@ -352,12 +356,7 @@ 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}");
+
if (s3Bucket == null)
{
@@ -423,7 +422,7 @@ public static class Program
st.InverterRecord.EnableEmsCommunicationFailureTime = false;
st.InverterRecord.EnableCommand = true;
st.InverterRecord.ControlPermission = st.Config.ControlPermission;;
- st.InverterRecord.MaxSoc = 100; //st.Config.MaxSoc;
+ // st.InverterRecord.MaxSoc = 100; //st.Config.MaxSoc;
}
private static Dictionary ConvertToModbusRegisters(Object value, String outputType,
@@ -488,12 +487,10 @@ public static class Program
var modbusData = new Dictionary();
// SYSTEM DATA
- var result1 =
- ConvertToModbusRegisters((status.Config.ModbusProtcolNumber * 10), "UInt16",
- 30001); // this to be updated to modbusTCP version
+ 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 result3 = ConvertToModbusRegisters(status.InverterRecord.SystemOperatingMode, "UInt16", 30004);
- var result17 = ConvertToModbusRegisters(status.InverterRecord.OperatingMode, "UInt16", 30005);
+ //var result3 = ConvertToModbusRegisters(status.InverterRecord.SystemOperatingMode, "UInt16", 30005);
+ var result17 = ConvertToModbusRegisters(status.InverterRecord.OperatingMode, "UInt16", 30004);
// BATTERY SUMMARY (assuming single battery [0]) // this to be improved
@@ -505,24 +502,29 @@ public static class Program
var result5 = ConvertToModbusRegisters((status.InverterRecord.Battery1Power.Value * 10), "Int32", 31006);
var result7 = ConvertToModbusRegisters((status.InverterRecord.MinSoc * 100), "UInt16", 31008);
- var result20 = ConvertToModbusRegisters((status.InverterRecord.ChargeCutoffSocVoltage * 100), "UInt16", 31009);
- var result15 =
- ConvertToModbusRegisters((status.InverterRecord.Battery1Soh * 100), "UInt16", 310010);
+ var result20 = ConvertToModbusRegisters((status.InverterRecord.MaxSoc * 100), "UInt16", 31009);
+ var result15 = ConvertToModbusRegisters((status.InverterRecord.Battery1Soh * 100), "UInt16", 31010);
var result16 = ConvertToModbusRegisters((status.InverterRecord.Battery1AmbientTemperature.Value * 100), "UInt16", 31011);
var result21 = ConvertToModbusRegisters((status.InverterRecord.BatteryMaxChargingCurrent * 10), "UInt16", 31012);
var result22 = ConvertToModbusRegisters((status.InverterRecord.BatteryMaxDischargingCurrent * 10), "UInt16", 31013);
- var result23 = ConvertToModbusRegisters((status.InverterRecord.MaxSoc * 10), "UInt16", 31014);
-
+
+ var result23 = ConvertToModbusRegisters((status.InverterRecord.ChargeCutoffSocVoltage * 10), "UInt16", 31014);
var result18 = ConvertToModbusRegisters((status.InverterRecord.PvPower.Value * 10), "UInt32", 32000);
var result19 = ConvertToModbusRegisters((status.InverterRecord.GridPower * 10), "Int32", 33000);
+ var result3 = ConvertToModbusRegisters((status.InverterRecord.Frequency * 10), "Int32", 33002);
-
+ var result24 = ConvertToModbusRegisters((status.InverterRecord.OperatingMode ), "UInt16", 34000);
+ var result25 = ConvertToModbusRegisters((status.InverterRecord.InverterActivePower * 10), "Int32", 34001);
+ var result26 = ConvertToModbusRegisters((status.Config.GridSetPoint * 10), "Int32", 35000);
+ var result27 = ConvertToModbusRegisters((status.InverterRecord.ExportLimitationEnabled * 10), "Int32", 35002);
+ var result28 = ConvertToModbusRegisters((status.InverterRecord.ExportLimitationPowerRate * 10), "Int32", 35003);
+
// Merge all results into one dictionary
var allResults = new[]
{
result1, result2, result3, result17, result4, result5, result7, result8,
result12, result13, result14, result15, result16, result18, result19, result20,
- result21, result22, result23
+ result21, result22, result23, result24, result25, result26, result27, result28,
};
foreach (var result in allResults)
@@ -717,24 +719,7 @@ public static class Program
return true;
}
- /* private static void Heartbit()
- {
- var s3Bucket = Config.Load().S3?.Bucket;
- var tryParse = int.TryParse(s3Bucket?.Split("-")[0], out var installationId);
- if (tryParse)
- {
- var returnedStatus = new StatusMessage
- {
- InstallationId = installationId,
- Product = 2,
- Status = _sodiohomeAlarmState,
- Type = MessageType.Heartbit,
- };
- if (s3Bucket != null)
- RabbitMqManager.InformMiddleware(returnedStatus);
- }
- }*/
private static async Task SaveToLocalCompressedFallback(Byte[] compressedData, String fileNameWithoutExtension)
{
diff --git a/csharp/App/GrowattCommunication/deploy.sh b/csharp/App/GrowattCommunication/deploy.sh
index a919ab29c..05bd7293a 100755
--- a/csharp/App/GrowattCommunication/deploy.sh
+++ b/csharp/App/GrowattCommunication/deploy.sh
@@ -6,12 +6,13 @@ username='inesco'
root_password='Sodistore0918425'
release_flag_file="./bin/Release/$dotnet_version/linux-arm64/publish/.release.flag"
+DOTNET="/snap/dotnet-sdk_60/current/dotnet"
set -e
echo -e "\n============================ Build ============================\n"
-dotnet publish \
+"$DOTNET" publish \
./GrowattCommunication.csproj \
-p:PublishTrimmed=false \
-c Release \
diff --git a/csharp/App/GrowattCommunication/sync-myRelease.sh b/csharp/App/GrowattCommunication/sync-myRelease.sh
index 1c80c3fed..c5d92ba4e 100755
--- a/csharp/App/GrowattCommunication/sync-myRelease.sh
+++ b/csharp/App/GrowattCommunication/sync-myRelease.sh
@@ -1,7 +1,7 @@
#!/bin/bash
WATCHDIR="$HOME/sync/work/Code/CSharp/git_trunk/csharp/App/GrowattCommunication/bin/Release/net6.0/linux-arm64/publish"
-DEST="ubuntu@91.92.155.224:/home/ubuntu/Releases"
+DEST="ubuntu@91.92.155.224:/home/ubuntu/GrowattReleases"
echo "👀 Watching for real releases in $WATCHDIR..."
diff --git a/csharp/App/KacoCommunication/Program.cs b/csharp/App/KacoCommunication/Program.cs
index 6f9c3b956..ba74d9246 100644
--- a/csharp/App/KacoCommunication/Program.cs
+++ b/csharp/App/KacoCommunication/Program.cs
@@ -142,11 +142,11 @@ internal static class Program
return new StatusRecord
{
- InverterRecord = kacoRecord,
- GridMeterRecord = gridRecord,
- DcDc = dcDcRecord,
+ InverterRecord = kacoRecord,
+ GridMeterRecord = gridRecord,
+ DcDc = dcDcRecord,
ListOfBatteriesRecord = listOfBatteriesRecord,
- StateMachine = StateMachine.Default,
+ StateMachine = StateMachine.Default,
Config = config // load from disk every iteration, so config can be changed while running
};
@@ -179,7 +179,7 @@ internal static class Program
// the order matter of the next three lines
var statusrecord = ReadStatus();
statusrecord?.CreateSimpleTopologyTextBlock().WriteLine();
-
+
statusrecord?.StateMachine.State.WriteLine(" state");
statusrecord?.StateMachine.Message.WriteLine(" Message");
@@ -194,6 +194,7 @@ internal static class Program
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");
@@ -206,31 +207,20 @@ 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?.ActivePowerSetPercent.WriteLine( "ActivePowerSetPercent");
statusrecord?.ControlSystemState();
- var i = 0;
- foreach (var d in statusrecord.DcDc.Devices)
- {
- i++;
- Console.WriteLine("before DcDc is " + i + d.Control.PowerStageEnable);
- d.Control.ResetAlarmsAndWarnings = true;
- d.Control.PowerStageEnable = true;
- }
-
-
- statusrecord?.DcDc?.SystemControl.ApplyDcDcDefaultSettings();
+ //Maybe Introduce a condition to run it only when it's not in a runing mode
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);
+
+ kacoDevice.Write(statusrecord.InverterRecord);
+ dcDcDevices.Write(statusrecord.DcDc);
+
return statusrecord;
}
@@ -578,6 +568,10 @@ internal static class Program
statusRecord?.DcDc?.Devices
.Select(d => d.Control )
.ForAll(c => c.ControlMode = DcControlMode.VoltageDroop);
+
+ //Add the DcDc configuration
+ statusRecord?.DcDc?.SystemControl.ApplyDcDcDefaultSettings();
+
//
// // 2. Send valid battery limits (Model 64202)
// All values temporarily set to "1" as requested.
@@ -659,7 +653,7 @@ internal static class Program
sc.SlaveErrorHandling = SlaveErrorHandling.Relaxed;
sc.SubSlaveErrorHandling = SubSlaveErrorHandling.Off;
sc.TargetSlave = 0;
- sc.ResetAlarmsAndWarnings = true;
+ sc.ResetAlarmsAndWarnings = true; // is this enough or shoud reset in each device
}
private static void InsertIntoJson(Dictionary jsonDict, String[] keys, String value)
diff --git a/csharp/App/KacoCommunication/System/Controller.cs b/csharp/App/KacoCommunication/System/Controller.cs
index ef14e4eb7..0e002a6ca 100644
--- a/csharp/App/KacoCommunication/System/Controller.cs
+++ b/csharp/App/KacoCommunication/System/Controller.cs
@@ -135,8 +135,10 @@ public static class KacoCurrentStateController
private static bool State_Throttled(StatusRecord s)
{
+
s.StateMachine.Message = "THROTTLED: still running. Power writes allowed.";
s.InverterRecord.RequestedState = ReuqestedState.GridConnected;
+ s.InverterRecord.ActivePowerSetPercent = s.Config.ActivePowerPercent;
// Power writes allowed here too
return true;
@@ -180,197 +182,3 @@ public static class KacoCurrentStateController
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;
- }
-
-}*/
\ No newline at end of file
diff --git a/csharp/App/KacoCommunication/Topology.cs b/csharp/App/KacoCommunication/Topology.cs
index d1a952e44..8f22e8225 100644
--- a/csharp/App/KacoCommunication/Topology.cs
+++ b/csharp/App/KacoCommunication/Topology.cs
@@ -22,14 +22,14 @@ namespace InnovEnergy.App.KacoCommunication;
// │ -3205 W │<<<<<<<<<│ -6646 W │<<<<<<<<<<├────────┤>>>>>>>>>>├───────┤>>>>>>>>>>│ 490 mA │ │ 490 mA │ │ 490 mA │
// │ -3507 W │ (a) │ -5071 W │ (h) │ 776 V │ (k) │ 56 V │ (l) │ 250 °C │ │ 250 °C │ │ 250 °C │
// │ -3605 W │ └─────────┘ └────────┘ └───────┘ │ 445 A │ │ 445 A │ │ 445 A │
-// └─────────┘ V │ │ │ │ │ │
-// V │ │ │ │ │ │
-// (j) 0 W └────────────┘ └────────────┘ └────────────┘
-// V
-// V
-// ┌──────┐
-// │ Load │
-// └──────┘
+// └─────────┘ V │ │ │ │ │ │
+// V │ │ │ │ │ │
+// (j) 0 W └────────────┘ └────────────┘ └────────────┘
+// V
+// V
+// ┌──────┐
+// │ Load │
+// └──────┘
//
@@ -44,7 +44,7 @@ public static class SimpleTopology
var h = status.InverterRecord?.ActivePowerW;
var i = 0;
- var j = 0;
+ var j = h - a;
var k = status.DcDc?.Dc.Battery.Power.Value;
@@ -53,8 +53,8 @@ public static class SimpleTopology
var l = status.ListOfBatteriesRecord?.Sum(r => r.Power);
var grid = status.CreateGridColumn(a);
- var acdc = status.CreateAcDcColumn(h);
- var dcBus = status.CreateDcBusColumn(i, j, k);
+ var acdc = status.CreateAcDcColumn(h,j);
+ var dcBus = status.CreateDcBusColumn( k);
var dcdc = status.CreateDcDcColumn(l);
var batteries = status.CreateBatteriesRow();
@@ -94,7 +94,7 @@ public static class SimpleTopology
return TextBlock.AlignCenterVertical(gridBox, flow);
}
- private static TextBlock CreateAcDcColumn(this StatusRecord status, ActivePower? h)
+ private static TextBlock CreateAcDcColumn(this StatusRecord status, ActivePower? h, ActivePower? j)
{
// ┌─────────┐
// │ AC/DC │
@@ -102,33 +102,6 @@ public static class SimpleTopology
// │ dev1 P │
// │ dev2 P │
// └─────────┘ (h) flow to DC Bus
-
- var acdcBox = TextBlock
- .AlignLeft(status.InverterRecord?.ActivePowerW.ToString() ?? "???")
- .TitleBox("AC/DC");
-
- var flowToDcBus = Flow.Horizontal(h);
-
- return TextBlock.AlignCenterVertical(acdcBox, flowToDcBus);
- }
-
- private static TextBlock CreateDcBusColumn(
- this StatusRecord status,
- ActivePower? i,
- ActivePower? j,
- ActivePower? k)
- {
- // ┌────┐
- // │ PV │
- // └────┘
- // V
- // (i) 13.2 kW
- // V
- // ┌────────┐ (k) >>>>>>>>> to DC/DC
- // │ Dc Bus │>>>>>>>>>>>>>>>>>>>
- // ├────────┤
- // │ 776 V │
- // └────────┘
// V
// (j) 0 W
// V
@@ -136,9 +109,42 @@ public static class SimpleTopology
// │ Load │
// └──────┘
+ var acdcBox = TextBlock
+ .AlignLeft(status.InverterRecord?.ActivePowerW.ToString() ?? "???")
+ .TitleBox("AC/DC");
+
+ ////////////// top //////////////
+ ActivePower i = 0;
+ var pvBox = TextBlock.FromString("PV").Box();
+ var pvFlow = Flow.Vertical(i);
+
+ // Load box + vertical flow
+ var busToLoad = Flow.Vertical(j);
+ var loadBox = TextBlock.FromString("Load").Box();
+
+ var flowToDcBus = Flow.Horizontal(h);
+
+ return TextBlock.AlignCenterVertical (
+ TextBlock.AlignCenterHorizontal(pvBox, pvFlow, acdcBox, busToLoad, loadBox),
+ flowToDcBus
+ );
+ }
+
+ private static TextBlock CreateDcBusColumn(
+ this StatusRecord status,
+ ActivePower? k)
+ {
+
+ // ┌────────┐ (k) >>>>>>>>> to DC/DC
+ // │ Dc Bus │>>>>>>>>>>>>>>>>>>>
+ // ├────────┤
+ // │ 776 V │
+ // └────────┘
+
+
// PV box + vertical flow
- var pvBox = TextBlock.FromString("PV").Box();
- var pvToBus = Flow.Vertical(i);
+ // var pvBox = TextBlock.FromString("PV").Box();
+ // var pvToBus = Flow.Vertical(i);
// DC bus box (voltage from your DcDc record matches your existing code)
var dcBusVoltage = status.DcDc.Dc.Link.Voltage.Value;
@@ -150,13 +156,10 @@ public static class SimpleTopology
// Horizontal flow from DC Bus to DC/DC
var busToDcDc = Flow.Horizontal(k);
- // Load box + vertical flow
- var busToLoad = Flow.Vertical(j);
- var loadBox = TextBlock.FromString("Load").Box();
// Assemble: put PV above DC Bus, Load below DC Bus, and the (k) flow beside the bus.
return TextBlock.AlignCenterVertical(
- TextBlock.AlignCenterHorizontal(pvBox, pvToBus, dcBusBox, busToLoad, loadBox),
+ TextBlock.AlignCenterHorizontal( dcBusBox),
busToDcDc
);
}
diff --git a/csharp/App/KacoCommunication/tunnelSalimax.sh b/csharp/App/KacoCommunication/tunnelSalimax.sh
new file mode 100755
index 000000000..8a6323ce0
--- /dev/null
+++ b/csharp/App/KacoCommunication/tunnelSalimax.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+host="ie-entwicklung@$1"
+
+tunnel() {
+ name=$1
+ ip=$2
+ rPort=$3
+ lPort=$4
+
+ echo -n "$name @ $ip mapped to localhost:$lPort "
+ ssh -nNTL "$lPort:$ip:$rPort" "$host" 2> /dev/null &
+
+ until nc -vz 127.0.0.1 $lPort 2> /dev/null
+ do
+ echo -n .
+ sleep 0.5
+ done
+
+ echo "ok"
+}
+
+echo ""
+
+tunnel "Trumpf DCDC (http) " 10.0.2.1 80 8002
+tunnel "Kaco Inverter (http) " 10.0.3.1 80 8003
+tunnel "Int Emu Meter (http) " 10.0.4.2 80 8004
+
+echo
+echo "press any key to close the tunnels ..."
+read -r -n 1 -s
+kill $(jobs -p)
+echo "done"
+
diff --git a/csharp/App/SinexcelCommunication/AggregationService/AggregateDataFileWriter.cs b/csharp/App/SinexcelCommunication/AggregationService/AggregateDataFileWriter.cs
new file mode 100644
index 000000000..a83f26f69
--- /dev/null
+++ b/csharp/App/SinexcelCommunication/AggregationService/AggregateDataFileWriter.cs
@@ -0,0 +1,75 @@
+using System;
+using System.IO;
+using System.Text.Json;
+
+namespace InnovEnergy.App.SinexcelCommunication.AggregationService;
+
+public class HourlyEnergyData
+{
+ public String Type { get; set; } = "Hourly";
+ public DateTime Timestamp { get; set; }
+
+ public double SelfGeneratedElectricity { get; set; }
+ public double ElectricityPurchased { get; set; }
+ public double ElectricityFed { get; set; }
+ public double BatteryChargeEnergy { get; set; }
+ public double BatteryDischargeEnergy { get; set; }
+ public double LoadPowerConsumption { get; set; }
+}
+
+public class DailyEnergyData
+{
+ public String Type { get; set; } = "Daily";
+ public DateTime Timestamp { get; set; }
+
+ public double DailySelfGeneratedElectricity { get; set; }
+ public double DailyElectricityPurchased { get; set; }
+ public double DailyElectricityFed { get; set; }
+ public double BatteryDailyChargeEnergy { get; set; }
+ public double BatteryDailyDischargeEnergy { get; set; }
+ public double DailyLoadPowerConsumption { get; set; }
+}
+
+public static class AggregatedDataFileWriter
+{
+ private static bool _folderCreated = false;
+
+ private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
+ {
+ WriteIndented = false
+ };
+
+ public static void AppendHourlyData(HourlyEnergyData data, string baseFolder)
+ {
+ var filePath = GetDailyFilePath(data.Timestamp, baseFolder);
+ AppendJsonLine(filePath, data);
+ Console.WriteLine($"Hourly data appended to {filePath}");
+ }
+
+ public static void AppendDailyData(DailyEnergyData data, string baseFolder)
+ {
+ var filePath = GetDailyFilePath(data.Timestamp, baseFolder);
+ AppendJsonLine(filePath, data);
+ Console.WriteLine($"Daily data appended to {filePath}");
+ }
+
+ private static string GetDailyFilePath(DateTime timestamp, string baseFolder)
+ {
+ var folder = Path.Combine(baseFolder, "AggregatedData");
+
+ if (!_folderCreated)
+ {
+ Directory.CreateDirectory(folder);
+ _folderCreated = true;
+ }
+
+ var fileName = timestamp.ToString("ddMMyyyy") + ".json";
+ return Path.Combine(folder, fileName);
+ }
+
+ private static void AppendJsonLine(string filePath, T data)
+ {
+ var json = JsonSerializer.Serialize(data, JsonOptions);
+ File.AppendAllText(filePath, json + Environment.NewLine);
+ }
+}
\ No newline at end of file
diff --git a/csharp/App/SinexcelCommunication/DataTypes/DynamicPricingMode.cs b/csharp/App/SinexcelCommunication/DataTypes/DynamicPricingMode.cs
new file mode 100644
index 000000000..2391f93b4
--- /dev/null
+++ b/csharp/App/SinexcelCommunication/DataTypes/DynamicPricingMode.cs
@@ -0,0 +1,8 @@
+namespace InnovEnergy.App.SinexcelCommunication.DataTypes;
+
+public enum DynamicPricingMode
+{
+ Disabled,
+ Tou,
+ SpotPrice
+}
\ No newline at end of file
diff --git a/csharp/App/SinexcelCommunication/ESS/DynamicPriceDataType.cs b/csharp/App/SinexcelCommunication/ESS/DynamicPriceDataType.cs
new file mode 100644
index 000000000..bb2638b7e
--- /dev/null
+++ b/csharp/App/SinexcelCommunication/ESS/DynamicPriceDataType.cs
@@ -0,0 +1,10 @@
+namespace InnovEnergy.App.SinexcelCommunication.ESS;
+
+internal enum DesiredAction
+{
+ OptimizeSelfUse,
+ Charge,
+ Discharge
+}
+
+
diff --git a/csharp/App/SinexcelCommunication/ESS/DynamicPricingEngine.cs b/csharp/App/SinexcelCommunication/ESS/DynamicPricingEngine.cs
new file mode 100644
index 000000000..52e95809b
--- /dev/null
+++ b/csharp/App/SinexcelCommunication/ESS/DynamicPricingEngine.cs
@@ -0,0 +1,203 @@
+using System;
+using InnovEnergy.App.SinexcelCommunication.DataTypes;
+using InnovEnergy.App.SinexcelCommunication.SystemConfig;
+using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType;
+
+namespace InnovEnergy.App.SinexcelCommunication.ESS;
+/*
+public static class DynamicPricingEngine
+ {
+
+ public static readonly TimeSpan CheapStart = TimeSpan.FromHours(22); // 22:00
+ public static readonly TimeSpan CheapEnd = TimeSpan.FromHours(6); // 06:00
+
+ // Expensive (High tariff)
+ public static readonly TimeSpan HighStart = TimeSpan.FromHours(17); // 17:00
+ public static readonly TimeSpan HighEnd = TimeSpan.FromHours(21); // 21:00
+
+ ///
+ /// Call this from your main loop. It sets statusrecord.Mode.
+ ///
+ /// liveSpotPrice is only needed when DynamicPricingMode == SpotPrice.
+ /// If your inverter cannot directly force Charge/Discharge, set inverterSupportsDirectForce=false
+ /// and it will execute via TimeChargeDischarge + rolling short time window.
+ ///
+ public static void Apply(
+ DateTime nowLocal,
+ Decimal? liveSpotPrice,
+ StatusRecord statusrecord,
+ Boolean inverterSupportsDirectForce,
+ Int32 rollingWindowMinutes = 10)
+ {
+ if (statusrecord == null) throw new ArgumentNullException(nameof(statusrecord));
+ if (statusrecord.Config == null) throw new ArgumentNullException(nameof(statusrecord.Config));
+
+ var c = statusrecord.Config;
+
+ // 0) Manual override (optional)
+ if (!c.DynamicPricingEnabled)
+ {
+ Console.WriteLine(" Dynamic pricing is disabled");
+ return;
+ }
+
+ if (statusrecord.InverterRecord.OperatingPriority == OperatingPriority.ModeNotSynched)
+ {
+ Console.WriteLine(" Inverter mode are not synched ");
+ return;
+ }
+/*
+
+ }
+
+ // 1) Base operating mode: explicit modes ignore dynamic pricing
+ if (c.OperatingPriority == OperatingPriority.GridPriority)
+ {
+ SetMode(statusrecord, OperatingPriority.GridPriority);
+ return;
+ }
+
+ if (c.OperatingPriority == OperatingPriority.BatteryPriority)
+ {
+ SetMode(statusrecord, OperatingPriority.BatteryPriority);
+ return;
+ }
+
+ // 2) OperatingMode == OptimizeSelfUse -> dynamic pricing can apply
+ var desired = DecideDesiredAction(nowLocal, liveSpotPrice, c);
+
+ if (desired == DesiredAction.OptimizeSelfUse)
+ {
+ SetMode(statusrecord, OperatingPriority.LoadPriority);
+ return;
+ }
+
+ // 3) Execute desired action
+ if (inverterSupportsDirectForce)
+ {
+ statusrecord.Mode = desired == DesiredAction.Charge
+ ? BatteryMode.Charge
+ : BatteryMode.Discharge;
+ return;
+ }
+
+ // 4) Inverter limitation: execute via TimeChargeDischarge rolling window
+ statusrecord.Mode = BatteryMode.TimeChargeDischarge;
+
+ var (start, end) = MakeRollingWindow(nowLocal, rollingWindowMinutes);
+
+ if (desired == DesiredAction.Charge)
+ {
+ c.TimeChargeStart = start;
+ c.TimeChargeEnd = end;
+
+ // clear discharge window to avoid overlap
+ c.TimeDischargeStart = TimeSpan.Zero;
+ c.TimeDischargeEnd = TimeSpan.Zero;
+ }
+ else // Discharge
+ {
+ c.TimeDischargeStart = start;
+ c.TimeDischargeEnd = end;
+
+ // clear charge window to avoid overlap
+ c.TimeChargeStart = TimeSpan.Zero;
+ c.TimeChargeEnd = TimeSpan.Zero;
+ }
+ }
+
+
+ private static void SetMode( StatusRecord statusrecord, OperatingPriority o)
+ {
+ var operatingMode = o switch
+ {
+ OperatingPriority.LoadPriority => WorkingMode.SpontaneousSelfUse,
+ OperatingPriority.BatteryPriority => WorkingMode.TimeChargeDischarge,
+ OperatingPriority.GridPriority => WorkingMode.PrioritySellElectricity,
+ _ => WorkingMode.SpontaneousSelfUse
+ };
+
+ if (statusrecord.InverterRecord.OperatingPriority != OperatingPriority.ModeNotSynched)
+ {
+ foreach (var inv in statusrecord?.InverterRecord.Devices)
+ {
+ inv.WorkingMode = operatingMode;
+ }
+ }
+ else
+ {
+ foreach (var inv in statusrecord?.InverterRecord.Devices)
+ {
+ Console.WriteLine(" Inverter mode are not synched");
+ inv.WorkingMode = WorkingMode.SpontaneousSelfUse;
+ }
+ }
+ return;
+ }
+
+ // ----------------------------
+ // Decision logic
+ // ----------------------------
+ private static DesiredAction DecideDesiredAction(DateTime nowLocal, decimal? liveSpotPrice, Config c)
+ {
+ if (c.DynamicPricingMode == DynamicPricingMode.Disabled)
+ return DesiredAction.OptimizeSelfUse;
+
+ TimeSpan now = nowLocal.TimeOfDay;
+
+ if (c.DynamicPricingMode == DynamicPricingMode.Tou)
+ {
+ bool isCheap = IsInTimeWindow(now,CheapStart, CheapEnd);
+ bool isHigh = IsInTimeWindow(now, HighStart, HighEnd);
+
+ // Priority: cheap -> charge, then high -> discharge
+ if (isCheap) return DesiredAction.Charge;
+ if (isHigh) return DesiredAction.Discharge;
+ return DesiredAction.OptimizeSelfUse;
+ }
+
+ if (c.DynamicPricingMode == DynamicPricingMode.SpotPrice)
+ {
+ if (!liveSpotPrice.HasValue)
+ return DesiredAction.OptimizeSelfUse; // safe fallback
+
+ if (c.CheapPrice >= c.HighPrice)
+ throw new ArgumentException("Config error: CheapPrice must be lower than HighPrice.");
+
+ decimal p = liveSpotPrice.Value;
+
+ if (p <= c.CheapPrice) return DesiredAction.Charge;
+ if (p >= c.HighPrice) return DesiredAction.Discharge;
+ }
+
+ return DesiredAction.OptimizeSelfUse;
+ }
+
+ // ----------------------------
+ // Helpers
+ // ----------------------------
+ ///
+ /// [start, end) window, supports overnight. start==end means disabled.
+ ///
+ private static Boolean IsInTimeWindow(TimeSpan now, TimeSpan start, TimeSpan end)
+ {
+ if (start == end) return false;
+
+ // Same-day
+ if (start < end)
+ return now >= start && now < end;
+
+ // Overnight
+ return now >= start || now < end;
+ }
+
+ private static (TimeSpan start, TimeSpan end) MakeRollingWindow(DateTime nowLocal, int minutes)
+ {
+ if (minutes <= 0) throw new ArgumentOutOfRangeException(nameof(minutes));
+
+ var start = nowLocal.TimeOfDay;
+ var end = (nowLocal + TimeSpan.FromMinutes(minutes)).TimeOfDay;
+ return (start, end);
+ }
+ }
+}*/
\ No newline at end of file
diff --git a/csharp/App/SinexcelCommunication/Program.cs b/csharp/App/SinexcelCommunication/Program.cs
index b7ca31ada..ffbccabad 100644
--- a/csharp/App/SinexcelCommunication/Program.cs
+++ b/csharp/App/SinexcelCommunication/Program.cs
@@ -79,7 +79,11 @@ internal static class Program
CreateChannel(d.Inverter1),
CreateChannel(d.Inverter2),
CreateChannel(d.Inverter3),
- CreateChannel(d.Inverter4)
+ CreateChannel(d.Inverter4),
+ CreateChannel(d.Inverter5),
+ CreateChannel(d.Inverter6),
+ CreateChannel(d.Inverter7),
+ CreateChannel(d.Inverter8)
};
@@ -112,9 +116,12 @@ internal static class Program
StatusRecord? ReadStatus()
{
var config = Config.Load();
- var listOfInverterRecord = devices
- .Select(device => device.Read())
- .ToList();
+
+ var readTasks = devices
+ .Select(device => Task.Run(() => device.Read()))
+ .ToArray();
+
+ var listOfInverterRecord = Task.WhenAll(readTasks).GetAwaiter().GetResult().ToList();
InverterRecords? inverterRecords = InverterRecords.FromInverters(listOfInverterRecord);
@@ -212,11 +219,7 @@ internal static class Program
// 1) Finalize previous hour if hour changed
var hourlyData = EnergyAggregation.ProcessHourlyData(statusRecord, now);
- /*if (hourlyData != null)
- {
- AggregatedDataFileWriter.AppendHourlyData(hourlyData, baseFolder);
- }*/
- if (hourlyData != null)
+ if (hourlyData != null)
{
AggregatedDataFileWriter.AppendHourlyData(hourlyData, baseFolder);
@@ -313,6 +316,7 @@ internal static class Program
{
if (statusrecord?.InverterRecord?.Devices == null) return;
+
// Compute once (same for all inverters)
var config = statusrecord.Config;
@@ -324,8 +328,11 @@ internal static class Program
foreach (var inverter in statusrecord.InverterRecord.Devices)
{
// constants for every inverter
- inverter.Battery1BackupSoc = (float)config.MinSoc;
- inverter.Battery2BackupSoc = (float)config.MinSoc;
+ inverter.Battery1BackupSoc = (Single)config.MinSoc;
+ inverter.Battery2BackupSoc = (Single)config.MinSoc;
+
+ inverter.Battery1MinSoc = 5;
+ inverter.Battery2MinSoc = 5;
inverter.RepetitiveWeeks = SinexcelWeekDays.All;
var operatingMode = config.OperatingPriority switch
@@ -466,7 +473,7 @@ internal static class Program
var returnedStatus = new StatusMessage
{
InstallationId = installationId,
- Product = 2,
+ Product = 2, // 2 for Sodistorehome amd 5 for Sodistorepro
Status = _sodiohomeAlarmState,
Type = MessageType.AlarmOrWarning,
Alarms = alarmList,
@@ -653,16 +660,15 @@ internal static class Program
var result13 = ConvertToModbusRegisters((status.InverterRecord.TotalBatteryCurrent.Value * 10), "Int32", 31003);
var result16 = ConvertToModbusRegisters((status.InverterRecord.AvgBatterySoc.Value * 100), "UInt16", 31005);
var result9 = ConvertToModbusRegisters((status.InverterRecord.TotalBatteryPower.Value * 10), "Int32", 31006);
+
var result14 = ConvertToModbusRegisters((status.InverterRecord.MinSoc.Value * 100), "UInt16", 31008);
var result55 = ConvertToModbusRegisters(100 * 100, "UInt16", 31009); //this is ignored as dosen't exist in Sinexcel
- var result5 = ConvertToModbusRegisters((status.InverterRecord.AvgBatterySoh.Value * 100), "UInt16", 31009);
-
-
+ var result5 = ConvertToModbusRegisters((status.InverterRecord.AvgBatterySoh.Value * 100), "UInt16", 31010);
var result7 = ConvertToModbusRegisters((status.InverterRecord.AvgBatteryTemp.Value * 100), "Int16", 31011);
var result20 = ConvertToModbusRegisters((status.InverterRecord.MaxChargeCurrent.Value * 10), "UInt16", 31012);
var result15 = ConvertToModbusRegisters((status.InverterRecord.MaxDischargingCurrent.Value * 10), "UInt16", 31013);
+
var result26 = ConvertToModbusRegisters(60 * 10, "UInt16", 31014); //this is ignored as dosen't exist in Sinexcel
-
var result18 = ConvertToModbusRegisters((status.InverterRecord.TotalPhotovoltaicPower.Value * 10), "UInt32", 32000);
var result19 = ConvertToModbusRegisters((status.InverterRecord.TotalGridPower.Value * 10), "Int32", 33000);
var result23 = ConvertToModbusRegisters((status.InverterRecord.GridFrequency.Value * 10), "UInt16", 33002);
diff --git a/csharp/App/SinexcelCommunication/SystemConfig/Config.cs b/csharp/App/SinexcelCommunication/SystemConfig/Config.cs
index 0c5a7fffa..5c6dfe708 100644
--- a/csharp/App/SinexcelCommunication/SystemConfig/Config.cs
+++ b/csharp/App/SinexcelCommunication/SystemConfig/Config.cs
@@ -48,9 +48,13 @@ public class Config
{
Serial = new() {BaudRate = 115200, Parity = 0, StopBits = 1, DataBits = 8},
Inverter1 = new() {DeviceState = DeviceState.Measured, Port = "/dev/ttyUSB0", SlaveId = 1},
- Inverter2 = new() {DeviceState = DeviceState.Measured, Port = "/dev/ttyUSB1", SlaveId = 1},
- Inverter3 = new() {DeviceState = DeviceState.Measured, Port = "/dev/ttyUSB3", SlaveId = 1},
- Inverter4 = new() {DeviceState = DeviceState.Measured, Port = "/dev/ttyUSB4", SlaveId = 1},
+ Inverter2 = new() {DeviceState = DeviceState.Disabled, Port = "/dev/ttyUSB1", SlaveId = 1},
+ Inverter3 = new() {DeviceState = DeviceState.Disabled, Port = "/dev/ttyUSB2", SlaveId = 1},
+ Inverter4 = new() {DeviceState = DeviceState.Disabled, Port = "/dev/ttyUSB3", SlaveId = 1},
+ Inverter5 = new() {DeviceState = DeviceState.Disabled, Port = "/dev/ttyUSB5", SlaveId = 1},
+ Inverter6 = new() {DeviceState = DeviceState.Disabled, Port = "/dev/ttyUSB6", SlaveId = 1},
+ Inverter7 = new() {DeviceState = DeviceState.Disabled, Port = "/dev/ttyUSB7", SlaveId = 1},
+ Inverter8 = new() {DeviceState = DeviceState.Disabled, Port = "/dev/ttyUSB8", SlaveId = 1},
},
//DynamicPricingEnabled = false,
//DynamicPricingMode = DynamicPricingMode.Disabled,
diff --git a/csharp/App/SinexcelCommunication/SystemConfig/DeviceConfig.cs b/csharp/App/SinexcelCommunication/SystemConfig/DeviceConfig.cs
index e82c5f847..db377ce98 100644
--- a/csharp/App/SinexcelCommunication/SystemConfig/DeviceConfig.cs
+++ b/csharp/App/SinexcelCommunication/SystemConfig/DeviceConfig.cs
@@ -8,4 +8,8 @@ public record DeviceConfig
public required SodiDevice Inverter2 { get; init; }
public required SodiDevice Inverter3 { get; init; }
public required SodiDevice Inverter4 { get; init; }
+ public required SodiDevice Inverter5 { get; init; }
+ public required SodiDevice Inverter6 { get; init; }
+ public required SodiDevice Inverter7 { get; init; }
+ public required SodiDevice Inverter8 { get; init; }
}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/AggregationService/Aggregator.cs b/csharp/App/SodiStoreMax/src/AggregationService/Aggregator.cs
index b857fb6ca..7aecd0cc5 100644
--- a/csharp/App/SodiStoreMax/src/AggregationService/Aggregator.cs
+++ b/csharp/App/SodiStoreMax/src/AggregationService/Aggregator.cs
@@ -70,7 +70,7 @@ public static class Aggregator
dailyAggregatedData.Save("DailyData");
if (await dailyAggregatedData.PushToS3())
{
- DeleteHourlyData("HourlyData",currentTime.ToUnixTime());
+ //DeleteHourlyData("HourlyData",currentTime.ToUnixTime());
//AggregatedData.DeleteDailyData("DailyData");
}
diff --git a/csharp/InnovEnergy.sln b/csharp/InnovEnergy.sln
index 6c48122ab..91dc5abf0 100644
--- a/csharp/InnovEnergy.sln
+++ b/csharp/InnovEnergy.sln
@@ -1,27 +1,26 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-#
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Collector", "App\Collector\Collector.csproj", "{E3A5F3A3-72A5-47CC-85C6-2D8E962A0EC1}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Collector", "App/Collector/Collector.csproj", "{E3A5F3A3-72A5-47CC-85C6-2D8E962A0EC1}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenVpnCertificatesServer", "App\OpenVpnCertificatesServer\OpenVpnCertificatesServer.csproj", "{CF4834CB-91B7-4172-AC13-ECDA8613CD17}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenVpnCertificatesServer", "App/OpenVpnCertificatesServer/OpenVpnCertificatesServer.csproj", "{CF4834CB-91B7-4172-AC13-ECDA8613CD17}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RemoteSupportConsole", "App\RemoteSupportConsole\RemoteSupportConsole.csproj", "{B1268C03-66EB-4486-8BFC-B439225D9D54}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RemoteSupportConsole", "App/RemoteSupportConsole/RemoteSupportConsole.csproj", "{B1268C03-66EB-4486-8BFC-B439225D9D54}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SysTools", "Lib\SysTools\SysTools.csproj", "{4A67D79F-F0C9-4BBC-9601-D5948E6C05D3}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SysTools", "Lib/SysTools/SysTools.csproj", "{4A67D79F-F0C9-4BBC-9601-D5948E6C05D3}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebServer", "Lib\WebServer\WebServer.csproj", "{B2627B9F-41DF-44F7-A0D1-CA71FF4A007A}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebServer", "Lib/WebServer/WebServer.csproj", "{B2627B9F-41DF-44F7-A0D1-CA71FF4A007A}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmuMeterDriver", "App\EmuMeterDriver\EmuMeterDriver.csproj", "{F65F33B0-3522-4008-8D1E-47EF8E4C7AC7}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmuMeterDriver", "App/EmuMeterDriver/EmuMeterDriver.csproj", "{F65F33B0-3522-4008-8D1E-47EF8E4C7AC7}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BmsTunnel", "App\BmsTunnel\BmsTunnel.csproj", "{40B45363-BE34-420B-8F87-775EE6EE3513}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BmsTunnel", "App/BmsTunnel/BmsTunnel.csproj", "{40B45363-BE34-420B-8F87-775EE6EE3513}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "App", "App", "{145597B4-3E30-45E6-9F72-4DD43194539A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lib", "Lib", "{AD5B98A8-AB7F-4DA2-B66D-5B4E63E7D854}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SaliMax", "App\SaliMax\SaliMax.csproj", "{25073794-D859-4824-9984-194C7E928496}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SaliMax", "App/SaliMax/SaliMax.csproj", "{25073794-D859-4824-9984-194C7E928496}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StatusApi", "Lib\StatusApi\StatusApi.csproj", "{9D17E78C-8A70-43DB-A619-DC12D20D023D}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StatusApi", "Lib/StatusApi/StatusApi.csproj", "{9D17E78C-8A70-43DB-A619-DC12D20D023D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Devices", "Devices", "{4931A385-24DC-4E78-BFF4-356F8D6D5183}"
EndProject
@@ -31,35 +30,35 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Victron", "Victron", "{BD8C
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Trumpf", "Trumpf", "{DDDBEFD0-5DEA-4C7C-A9F2-FDB4636CF092}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TruConvertAc", "Lib\Devices\Trumpf\TruConvertAc\TruConvertAc.csproj", "{1F4B445E-459E-44CD-813E-6D725EBB81E8}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TruConvertAc", "Lib/Devices/Trumpf/TruConvertAc/TruConvertAc.csproj", "{1F4B445E-459E-44CD-813E-6D725EBB81E8}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TruConvertDc", "Lib\Devices\Trumpf\TruConvertDc\TruConvertDc.csproj", "{F6F29829-C31A-4994-A698-E441BEA631C6}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TruConvertDc", "Lib/Devices/Trumpf/TruConvertDc/TruConvertDc.csproj", "{F6F29829-C31A-4994-A698-E441BEA631C6}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DBus", "Lib\Protocols\DBus\DBus.csproj", "{8C3C620A-087D-4DD6-B493-A47FC643F8DC}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DBus", "Lib/Protocols/DBus/DBus.csproj", "{8C3C620A-087D-4DD6-B493-A47FC643F8DC}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modbus", "Lib\Protocols\Modbus\Modbus.csproj", "{E4AE6A33-0DEB-48EB-9D57-C0C7C63FC267}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modbus", "Lib/Protocols/Modbus/Modbus.csproj", "{E4AE6A33-0DEB-48EB-9D57-C0C7C63FC267}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VeDBus", "Lib\Victron\VeDBus\VeDBus.csproj", "{50B26E29-1B99-4D07-BCA5-359CD550BBAA}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VeDBus", "Lib/Victron/VeDBus/VeDBus.csproj", "{50B26E29-1B99-4D07-BCA5-359CD550BBAA}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VictronVRM", "Lib\Victron\VictronVRM\VictronVRM.csproj", "{FE05DF69-B5C7-4C2E-8FB9-7776441A7622}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VictronVRM", "Lib/Victron/VictronVRM/VictronVRM.csproj", "{FE05DF69-B5C7-4C2E-8FB9-7776441A7622}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ampt", "Lib\Devices\AMPT\Ampt.csproj", "{77AF3A64-2878-4150-BCD0-F16530783165}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ampt", "Lib/Devices/AMPT/Ampt.csproj", "{77AF3A64-2878-4150-BCD0-F16530783165}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Battery48TL", "Lib\Devices\Battery48TL\Battery48TL.csproj", "{1C3F443A-B339-4B08-80E6-8A84817FFEC9}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Battery48TL", "Lib/Devices/Battery48TL/Battery48TL.csproj", "{1C3F443A-B339-4B08-80E6-8A84817FFEC9}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmuMeter", "Lib\Devices\EmuMeter\EmuMeter.csproj", "{152A4168-F612-493C-BBEA-8EB26E6E2D34}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmuMeter", "Lib/Devices/EmuMeter/EmuMeter.csproj", "{152A4168-F612-493C-BBEA-8EB26E6E2D34}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utils", "Lib\Utils\Utils.csproj", "{89A3E29C-4E57-47FE-A800-12AC68418264}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utils", "Lib/Utils/Utils.csproj", "{89A3E29C-4E57-47FE-A800-12AC68418264}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Adam6060", "Lib\Devices\Adam6060\Adam6060.csproj", "{4AFDB799-E6A4-4DCA-8B6D-8C0F98398461}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Adam6060", "Lib/Devices/Adam6060/Adam6060.csproj", "{4AFDB799-E6A4-4DCA-8B6D-8C0F98398461}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Channels", "Lib\Channels\Channels.csproj", "{AF7E8DCA-8D48-498E-AB3D-208061B244DC}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Channels", "Lib/Channels/Channels.csproj", "{AF7E8DCA-8D48-498E-AB3D-208061B244DC}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend", "App\Backend\Backend.csproj", "{A56F58C2-B265-435B-A985-53B4D6F49B1A}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend", "App/Backend/Backend.csproj", "{A56F58C2-B265-435B-A985-53B4D6F49B1A}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Units", "Lib\Units\Units.csproj", "{C04FB6DA-23C6-46BB-9B21-8F4FBA32FFF7}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Units", "Lib/Units/Units.csproj", "{C04FB6DA-23C6-46BB-9B21-8F4FBA32FFF7}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SystemControl", "Lib\Devices\Trumpf\SystemControl\SystemControl.csproj", "{B816BB44-E97E-4E02-B80A-BEDB5B923A96}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SystemControl", "Lib/Devices/Trumpf/SystemControl/SystemControl.csproj", "{B816BB44-E97E-4E02-B80A-BEDB5B923A96}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Meta", "Meta", "{AED84693-C389-44C9-B2C0-ACB560189CF2}"
ProjectSection(SolutionItems) = preProject
@@ -88,6 +87,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Doepke", "Lib\Devices\Doepk
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amax5070", "Lib\Devices\Amax5070\Amax5070.csproj", "{09E280B0-43D3-47BD-AF15-CF4FCDD24FE6}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SofarInverter", "Lib\Devices\SofarInverter\SofarInverter.csproj", "{2C7F3D89-402B-43CB-988E-8D2D853BEF44}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SchneiderMeterDriver", "App\SchneiderMeterDriver\SchneiderMeterDriver.csproj", "{2E7E7657-3A53-4B62-8927-FE9A082B81DE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Battery250UP", "Lib\Devices\Battery250UP\Battery250UP.csproj", "{F2967439-A590-4D5E-9208-1B973C83AA1C}"
@@ -108,6 +109,17 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SinexcelCommunication", "Ap
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sinexcel 12K TL", "Sinexcel 12K TL\Sinexcel 12K TL.csproj", "{28C16B43-E498-40DB-8ACF-D7F2A88A402F}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kaco92L3", "Lib\Devices\Kaco92L3\Kaco92L3.csproj", "{E60412AA-F88C-4CB7-AEFC-78427B1ADA13}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KacoCommunication", "App\KacoCommunication\KacoCommunication.csproj", "{0380E4B0-2A0C-4E3B-8536-499B72B23179}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PLVario2Meter", "Lib\Devices\PLVario2Meter\PLVario2Meter.csproj", "{D6D07FC5-2925-4B13-9F65-22123E07F8CC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GPIORaspberryPI4", "Lib\Devices\GPIORaspberryPI4\GPIORaspberryPI4.csproj", "{5E7A867E-D026-43B4-BDB9-240E4331CA23}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpioTestingProject", "App\GpioTestingProject\GpioTestingProject.csproj", "{C6E3B901-3730-4B04-B821-85A6673C3D25}"
+EndProject
+
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -246,6 +258,10 @@ Global
{09E280B0-43D3-47BD-AF15-CF4FCDD24FE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{09E280B0-43D3-47BD-AF15-CF4FCDD24FE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09E280B0-43D3-47BD-AF15-CF4FCDD24FE6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2C7F3D89-402B-43CB-988E-8D2D853BEF44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2C7F3D89-402B-43CB-988E-8D2D853BEF44}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2C7F3D89-402B-43CB-988E-8D2D853BEF44}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2C7F3D89-402B-43CB-988E-8D2D853BEF44}.Release|Any CPU.Build.0 = Release|Any CPU
{2E7E7657-3A53-4B62-8927-FE9A082B81DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2E7E7657-3A53-4B62-8927-FE9A082B81DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E7E7657-3A53-4B62-8927-FE9A082B81DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -286,6 +302,26 @@ Global
{28C16B43-E498-40DB-8ACF-D7F2A88A402F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28C16B43-E498-40DB-8ACF-D7F2A88A402F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28C16B43-E498-40DB-8ACF-D7F2A88A402F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E60412AA-F88C-4CB7-AEFC-78427B1ADA13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E60412AA-F88C-4CB7-AEFC-78427B1ADA13}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E60412AA-F88C-4CB7-AEFC-78427B1ADA13}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E60412AA-F88C-4CB7-AEFC-78427B1ADA13}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0380E4B0-2A0C-4E3B-8536-499B72B23179}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0380E4B0-2A0C-4E3B-8536-499B72B23179}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0380E4B0-2A0C-4E3B-8536-499B72B23179}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0380E4B0-2A0C-4E3B-8536-499B72B23179}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D6D07FC5-2925-4B13-9F65-22123E07F8CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D6D07FC5-2925-4B13-9F65-22123E07F8CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D6D07FC5-2925-4B13-9F65-22123E07F8CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D6D07FC5-2925-4B13-9F65-22123E07F8CC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5E7A867E-D026-43B4-BDB9-240E4331CA23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5E7A867E-D026-43B4-BDB9-240E4331CA23}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5E7A867E-D026-43B4-BDB9-240E4331CA23}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5E7A867E-D026-43B4-BDB9-240E4331CA23}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C6E3B901-3730-4B04-B821-85A6673C3D25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C6E3B901-3730-4B04-B821-85A6673C3D25}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C6E3B901-3730-4B04-B821-85A6673C3D25}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C6E3B901-3730-4B04-B821-85A6673C3D25}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{CF4834CB-91B7-4172-AC13-ECDA8613CD17} = {145597B4-3E30-45E6-9F72-4DD43194539A}
@@ -325,6 +361,7 @@ Global
{73B97F6E-2BDC-40DA-84A7-7FB0264387D6} = {AD5B98A8-AB7F-4DA2-B66D-5B4E63E7D854}
{C2B14CD4-1BCA-4933-96D9-92F40EACD2B9} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
{09E280B0-43D3-47BD-AF15-CF4FCDD24FE6} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
+ {2C7F3D89-402B-43CB-988E-8D2D853BEF44} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
{2E7E7657-3A53-4B62-8927-FE9A082B81DE} = {145597B4-3E30-45E6-9F72-4DD43194539A}
{F2967439-A590-4D5E-9208-1B973C83AA1C} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
{1045AC74-D4D8-4581-AAE3-575DF26060E6} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
@@ -335,5 +372,10 @@ Global
{6069D487-DBAB-4253-BFA1-CF994B84BE49} = {145597B4-3E30-45E6-9F72-4DD43194539A}
{93084D79-2977-47A1-9CAC-3E2DC6423F5B} = {145597B4-3E30-45E6-9F72-4DD43194539A}
{28C16B43-E498-40DB-8ACF-D7F2A88A402F} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
+ {E60412AA-F88C-4CB7-AEFC-78427B1ADA13} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
+ {0380E4B0-2A0C-4E3B-8536-499B72B23179} = {145597B4-3E30-45E6-9F72-4DD43194539A}
+ {D6D07FC5-2925-4B13-9F65-22123E07F8CC} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
+ {5E7A867E-D026-43B4-BDB9-240E4331CA23} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
+ {C6E3B901-3730-4B04-B821-85A6673C3D25} = {145597B4-3E30-45E6-9F72-4DD43194539A}
EndGlobalSection
EndGlobal
diff --git a/csharp/Lib/Devices/GPIORaspberryPI4/DigitalInput.cs b/csharp/Lib/Devices/GPIORaspberryPI4/DigitalInput.cs
new file mode 100644
index 000000000..26301fadd
--- /dev/null
+++ b/csharp/Lib/Devices/GPIORaspberryPI4/DigitalInput.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Device.Gpio;
+
+namespace GPIORaspberryPI4;
+
+public sealed class DigitalInput : IDigitalInput
+{
+ private readonly GpioController _gpio;
+ private readonly int _pin;
+ private readonly bool _activeLow;
+ private bool _disposed;
+
+ public int Pin => _pin;
+
+ public bool IsActive => Read();
+
+ public DigitalInput(int pin, bool pullUp = true, bool activeLow = true)
+ {
+ _pin = pin;
+ _activeLow = activeLow;
+
+ _gpio = new GpioController();
+
+ var mode = pullUp ? PinMode.InputPullUp : PinMode.Input;
+
+ _gpio.OpenPin(_pin, mode);
+ }
+
+ public bool Read()
+ {
+ ThrowIfDisposed();
+
+ var value = _gpio.Read(_pin);
+
+ return _activeLow
+ ? value == PinValue.Low
+ : value == PinValue.High;
+ }
+
+ private void ThrowIfDisposed()
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(DigitalInput));
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ return;
+
+ if (_gpio.IsPinOpen(_pin))
+ _gpio.ClosePin(_pin);
+
+ _gpio.Dispose();
+ _disposed = true;
+ }
+}
\ No newline at end of file
diff --git a/csharp/Lib/Devices/GPIORaspberryPI4/GPIORaspberryPI4.csproj b/csharp/Lib/Devices/GPIORaspberryPI4/GPIORaspberryPI4.csproj
new file mode 100644
index 000000000..9b12551de
--- /dev/null
+++ b/csharp/Lib/Devices/GPIORaspberryPI4/GPIORaspberryPI4.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/csharp/Lib/Devices/GPIORaspberryPI4/IDigitalInput.cs b/csharp/Lib/Devices/GPIORaspberryPI4/IDigitalInput.cs
new file mode 100644
index 000000000..5102bfae2
--- /dev/null
+++ b/csharp/Lib/Devices/GPIORaspberryPI4/IDigitalInput.cs
@@ -0,0 +1,8 @@
+namespace GPIORaspberryPI4;
+
+public interface IDigitalInput : IDisposable
+{
+ int Pin { get; }
+ bool IsActive { get; }
+ bool Read();
+}
\ No newline at end of file
diff --git a/csharp/Lib/Devices/GPIORaspberryPI4/IRelayOutput.cs b/csharp/Lib/Devices/GPIORaspberryPI4/IRelayOutput.cs
new file mode 100644
index 000000000..351919cda
--- /dev/null
+++ b/csharp/Lib/Devices/GPIORaspberryPI4/IRelayOutput.cs
@@ -0,0 +1,9 @@
+namespace GPIORaspberryPI4;
+
+public interface IRelayOutput : IDisposable
+{
+ void On();
+ void Off();
+ void Set(bool on);
+ bool IsOn { get; }
+}
\ No newline at end of file
diff --git a/csharp/Lib/Devices/GPIORaspberryPI4/RelayOutput.cs b/csharp/Lib/Devices/GPIORaspberryPI4/RelayOutput.cs
new file mode 100644
index 000000000..e0c1e2b65
--- /dev/null
+++ b/csharp/Lib/Devices/GPIORaspberryPI4/RelayOutput.cs
@@ -0,0 +1,81 @@
+using System.Device.Gpio;
+
+namespace GPIORaspberryPI4;
+
+public sealed class RelayOutput : IRelayOutput
+{
+ private readonly GpioController _gpio;
+ private readonly int _pin;
+ private readonly bool _activeLow;
+ private bool _disposed;
+
+ public bool IsOn { get; private set; }
+
+ public RelayOutput(int pin, bool activeLow = false)
+ {
+ _pin = pin;
+ _activeLow = activeLow;
+
+ _gpio = new GpioController();
+ _gpio.OpenPin(_pin, PinMode.Output);
+
+ // Safe default state
+ WriteInternal(false);
+ }
+
+ public void On()
+ {
+ ThrowIfDisposed();
+ WriteInternal(true);
+ }
+
+ public void Off()
+ {
+ ThrowIfDisposed();
+ WriteInternal(false);
+ }
+
+ public void Set(bool on)
+ {
+ ThrowIfDisposed();
+ WriteInternal(on);
+ }
+
+ private void WriteInternal(bool on)
+ {
+ var pinValue = _activeLow
+ ? (on ? PinValue.Low : PinValue.High)
+ : (on ? PinValue.High : PinValue.Low);
+
+ _gpio.Write(_pin, pinValue);
+ IsOn = on;
+ }
+
+ private void ThrowIfDisposed()
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(RelayOutput));
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ return;
+
+ try
+ {
+ // Fail-safe: relay OFF on dispose
+ WriteInternal(false);
+ }
+ catch
+ {
+ // Ignore cleanup errors
+ }
+
+ if (_gpio.IsPinOpen(_pin))
+ _gpio.ClosePin(_pin);
+
+ _gpio.Dispose();
+ _disposed = true;
+ }
+}
\ No newline at end of file
diff --git a/csharp/Lib/Devices/Kaco92L3/KacoRecord.Api.cs b/csharp/Lib/Devices/Kaco92L3/KacoRecord.Api.cs
index 2118b7941..b4ce04063 100644
--- a/csharp/Lib/Devices/Kaco92L3/KacoRecord.Api.cs
+++ b/csharp/Lib/Devices/Kaco92L3/KacoRecord.Api.cs
@@ -175,9 +175,8 @@ public partial class KacoRecord
/// Scale factor for battery currents (A_SF).
///
public Int16 BatteryCurrentScaleFactor => _battCharASf;
-
+ public Single ActivePowerW => ScaleSunspec( (Int16)(_activePowerW * -1), _wSf); // this to correct the sign to fit in our sign system
- public Int16 ActivePowerW => _activePowerW;
public Int16 ReactivePowerVar => _reactivePowerVar;
public Int16 LineFrequencyHz => _lineFrequencyHz;
diff --git a/csharp/Lib/Devices/Kaco92L3/KacoRecord.modbus.cs b/csharp/Lib/Devices/Kaco92L3/KacoRecord.modbus.cs
index 16e382af1..2bf510f12 100644
--- a/csharp/Lib/Devices/Kaco92L3/KacoRecord.modbus.cs
+++ b/csharp/Lib/Devices/Kaco92L3/KacoRecord.modbus.cs
@@ -36,42 +36,41 @@ public partial class KacoRecord
[HoldingRegister(41074)] private UInt16 _reserved13; // 0xA072
[HoldingRegister(41075)] private UInt16 _reserved14; // 0xA073
[HoldingRegister(41076)] private UInt16 _reserved15; // 0xA074
- [HoldingRegister(41077)] private UInt16 _reserved16; // 0xA075
// Status / error
- [HoldingRegister(41078)] private UInt16 _stVnd; // 0xA076 – StVnd (enum16, R) PrologState
- [HoldingRegister(41079)] private UInt16 _stPu; // 0xA077 – StPu (enum16, R) Power Unit State (DSP)
- [HoldingRegister(41080)] private UInt16 _stPcu; // 0xA078 – StPcu (enum16, R) pcu state
- [HoldingRegister(41081)] private UInt16 _errPcu; // 0xA079 – ErrPcu (enum16, R) pcu error
+ [HoldingRegister(41077)] private UInt16 _stVnd; // 0xA076 – StVnd (enum16, R) PrologState
+ [HoldingRegister(41078)] private UInt16 _stPu; // 0xA077 – StPu (enum16, R) Power Unit State (DSP)
+ [HoldingRegister(41079)] private UInt16 _stPcu; // 0xA078 – StPcu (enum16, R) pcu state
+ [HoldingRegister(41080)] private UInt16 _errPcu; // 0xA079 – ErrPcu (enum16, R) pcu error
// Active power ramp parameters
- [HoldingRegister(41082, writable: true)] private UInt16 _wParamRmpTms; // 0xA07A – WParamRmpTms (uint16, RW, s)
- [HoldingRegister(41083, writable: true)] private UInt16 _wParamRmpDecTmn; // 0xA07B – WParamRmpDecTmn (uint16, RW, %ref/min)
- [HoldingRegister(41084, writable: true)] private UInt16 _wParamRmpIncTmn; // 0xA07C – WParamRmpIncTmn (uint16, RW, %ref/min)
+ [HoldingRegister(41081, writable: true)] private UInt16 _wParamRmpTms; // 0xA07A – WParamRmpTms (uint16, RW, s)
+ [HoldingRegister(41082, writable: true)] private UInt16 _wParamRmpDecTmn; // 0xA07B – WParamRmpDecTmn (uint16, RW, %ref/min)
+ [HoldingRegister(41083, writable: true)] private UInt16 _wParamRmpIncTmn; // 0xA07C – WParamRmpIncTmn (uint16, RW, %ref/min)
- [HoldingRegister(41085)] private UInt16 _reserved24; // 0xA07D – Reserved
- [HoldingRegister(41086)] private UInt16 _reserved25; // 0xA07E – Reserved
+ [HoldingRegister(41084)] private UInt16 _reserved24; // 0xA07D – Reserved
+ [HoldingRegister(41085)] private UInt16 _reserved25; // 0xA07E – Reserved
- [HoldingRegister(41087, writable: true)] private UInt16 _wParamEna; // 0xA07F – WParam_Ena (enum16, RW) WSet_Ena control 0 or 1
+ [HoldingRegister(41086, writable: true)] private UInt16 _wParamEna; // 0xA07F – WParam_Ena (enum16, RW) WSet_Ena control 0 or 1
// Reactive power ramp parameters
- [HoldingRegister(41088, writable: true)] private UInt16 _varParamRmpTms; // 0xA080 – VarParamRmpTms (uint16, RW, s)
- [HoldingRegister(41089, writable: true)] private UInt16 _varParamRmpDecTmn; // 0xA081 – VarParamRmpDecTmn (uint16, RW, %ref/min)
- [HoldingRegister(41090, writable: true)] private UInt16 _varParamRmpIncTmn; // 0xA082 – VarParamRmpIncTmn (uint16, RW, %ref/min)
+ [HoldingRegister(41087, writable: true)] private UInt16 _varParamRmpTms; // 0xA080 – VarParamRmpTms (uint16, RW, s)
+ [HoldingRegister(41088, writable: true)] private UInt16 _varParamRmpDecTmn; // 0xA081 – VarParamRmpDecTmn (uint16, RW, %ref/min)
+ [HoldingRegister(41089, writable: true)] private UInt16 _varParamRmpIncTmn; // 0xA082 – VarParamRmpIncTmn (uint16, RW, %ref/min)
- [HoldingRegister(41091)] private UInt16 _reserved30; // 0xA083 – Reserved
- [HoldingRegister(41092)] private UInt16 _reserved31; // 0xA084 – Reserved
+ [HoldingRegister(41090)] private UInt16 _reserved30; // 0xA083 – Reserved
+ [HoldingRegister(41091)] private UInt16 _reserved31; // 0xA084 – Reserved
- [HoldingRegister(41093, writable: true)] private UInt16 _varParamEna; // 0xA085 – VarParam_Ena (enum16, RW) Enumerated valued. Percent limit VAr enable/disable control.
+ [HoldingRegister(41092, writable: true)] private UInt16 _varParamEna; // 0xA085 – VarParam_Ena (enum16, RW) Enumerated valued. Percent limit VAr enable/disable control.
// Measurements (read-only)
- [HoldingRegister(41094)] private UInt16 _phaseVoltageAN; // 0xA086 – PhVphA (uint16, R, V, V_SF)
- [HoldingRegister(41095)] private UInt16 _phaseVoltageBN; // 0xA087 – PhVphB (uint16, R, V, V_SF)
- [HoldingRegister(41096)] private UInt16 _phaseVoltageCN; // 0xA088 – PhVphC (uint16, R, V, V_SF)
+ [HoldingRegister(41093)] private UInt16 _phaseVoltageAN; // 0xA086 – PhVphA (uint16, R, V, V_SF)
+ [HoldingRegister(41094)] private UInt16 _phaseVoltageBN; // 0xA087 – PhVphB (uint16, R, V, V_SF)
+ [HoldingRegister(41095)] private UInt16 _phaseVoltageCN; // 0xA088 – PhVphC (uint16, R, V, V_SF)
- [HoldingRegister (41097)] private Int16 _activePowerW; // 0xA089 – W (int16, R, W, W_SF)
- [HoldingRegister (41098)] private Int16 _reactivePowerVar; // 0xA08A – VAR (int16, R, var, Var_SF)
- [HoldingRegister (41099)] private Int16 _lineFrequencyHz; // 0xA08B – Hz (int16, R, Hz, Hz_SF)
+ [HoldingRegister (41096)] private Int16 _activePowerW; // 0xA089 – W (int16, R, W, W_SF)
+ [HoldingRegister (41097)] private Int16 _reactivePowerVar; // 0xA08A – VAR (int16, R, var, Var_SF)
+ [HoldingRegister (41098)] private Int16 _lineFrequencyHz; // 0xA08B – Hz (int16, R, Hz, Hz_SF)
// Scale factors (SunSpec sunsf)
// Scale factor for active power percent.
@@ -82,7 +81,7 @@ public partial class KacoRecord
[HoldingRegister(41109)] private Int16 _rmpTmsSf; // 0xA0F5 – RmpTms_SF
// Scale factor for increment and decrement ramps.
[HoldingRegister(41110)] private Int16 _rmpIncDecSf; // 0xA0F6 – RmpIncDec_SF
-
+ [HoldingRegister(41112)] private Int16 _wSf; // W_SF
// Header
[HoldingRegister(41115)] private UInt16 _battCharId; // ID = 64202
[HoldingRegister(41116)] private UInt16 _battCharLength; // L = 6 + (RBCount * 8)
diff --git a/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatRecord.Api.cs b/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatRecord.Api.cs
index 4d29fc83d..b5c1e7b0a 100644
--- a/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatRecord.Api.cs
+++ b/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatRecord.Api.cs
@@ -141,6 +141,20 @@ public partial class WITGrowatRecord
//set => _PowerFactor = value;
}
+
+
+ public UInt16 ExportLimitationEnabled
+ {
+ get => _ExportLimitationEnabled;
+ set => _ExportLimitationEnabled = value;
+ }
+
+
+ public Int16 ExportLimitationPowerRate
+ {
+ get => _ExportLimitationPowerRate;
+ set => _ExportLimitationPowerRate = value;
+ }
public UInt16 EmsCommunicationFailureTime
{
diff --git a/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatRecord.Modbus.cs b/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatRecord.Modbus.cs
index b31a63090..02f4f4329 100644
--- a/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatRecord.Modbus.cs
+++ b/csharp/Lib/Devices/WITGrowatt4-15K/WITGrowatRecord.Modbus.cs
@@ -165,7 +165,11 @@ public partial class WITGrowatRecord
//[HoldingRegister(30152, writable: true)] private UInt16 _Reserved11; //
[HoldingRegister(30154, writable: true)] private UInt16 _ActivePowerPercent; // Limit percentage: [0, 100]; Default: 100; takes the smaller value of 30151 and 30154 as actual active limit; Not stored
- [HoldingRegister(30162)] private UInt16 _PowerFactor; // [0, 2000] ∪ [18000, 20000]; Default: 20000; Actual PF = (Register Value - 10000)
+ [HoldingRegister(30162)] private UInt16 _PowerFactor; // [0, 2000] ∪ [18000, 20000]; Default: 20000; Actual PF = (Register Value - 10000)
+
+ [HoldingRegister(30200, writable : true)] private UInt16 _ExportLimitationEnabled; // // 0: not enabled // 1: single machine Export Limitation enable
+ [HoldingRegister(30201, writable : true)] private Int16 _ExportLimitationPowerRate; // [-100,100] // Default value: 0 Positive value is backflow, negative value is fair current
+
[HoldingRegister(30203, writable : true)] private UInt16 _EmsCommunicationFailureTime; // [1,300] TODO to 30
[HoldingRegister(30204, writable : true)] private Boolean _EnableEmsCommunicationFailureTime; // 0: disabled, 1 = enabled we should not enable this the naming is not correct
[HoldingRegister(30300)] private UInt16 _BatteryClusterIndex; // [0..3]
diff --git a/csharp/Sinexcel 12K TL/SinexcelRecord.Api.cs b/csharp/Sinexcel 12K TL/SinexcelRecord.Api.cs
index 0826b112e..b71d30a21 100644
--- a/csharp/Sinexcel 12K TL/SinexcelRecord.Api.cs
+++ b/csharp/Sinexcel 12K TL/SinexcelRecord.Api.cs
@@ -195,6 +195,8 @@ public partial class SinexcelRecord
private readonly Int16 _factorFromKwtoW = 1000;
+ private readonly Int16 _correctingSign = -1;
+
// ───────────────────────────────────────────────
// Public API — Decoded Float Values
// ───────────────────────────────────────────────
@@ -375,7 +377,7 @@ public partial class SinexcelRecord
public ActivePower TotalPhotovoltaicPower => ConvertBitPatternToFloat(_totalPhotovoltaicPower) * _factorFromKwtoW;
public ActivePower TotalBatteryPower => ConvertBitPatternToFloat(_totalBatteryPower) * _factorFromKwtoW;
public ActivePower TotalLoadPower => ConvertBitPatternToFloat(_totalLoadPower) * _factorFromKwtoW ;
- public ActivePower TotalGridPower => ConvertBitPatternToFloat(_totalGridPower) * _factorFromKwtoW ;
+ public ActivePower TotalGridPower => ConvertBitPatternToFloat(_totalGridPower) * _factorFromKwtoW * _correctingSign ; // we correct sign
public ActivePower ImportantLoadTotalPower => ConvertBitPatternToFloat(_importantLoadTotalPower)* _factorFromKwtoW;
public ActivePower GeneralLoadTotalPower => ConvertBitPatternToFloat(_generalLoadTotalPower)* _factorFromKwtoW;
public Voltage PvVoltage3 => ConvertBitPatternToFloat(_pv3Voltage);
@@ -663,7 +665,7 @@ public partial class SinexcelRecord
// ───────────────────────────────────────────────
// Parallel / System Settings
// ───────────────────────────────────────────────
- public SinexcelMachineMode MachineMode
+ /* public SinexcelMachineMode MachineMode
{
get => (SinexcelMachineMode)ConvertBitPatternToFloat(_singleOrParallelMachine);
set => _singleOrParallelMachine = (UInt32)value;
@@ -686,7 +688,7 @@ public partial class SinexcelRecord
get => (AccreditedCountry)(Int32)BitConverter.Int32BitsToSingle(unchecked((Int32)_accreditedCountries));
set => _accreditedCountries = BitConverter.ToUInt32(BitConverter.GetBytes((Single)value), 0);
}
-
+ */
// ───────────────────────────────────────────────
// Control Commands
// ───────────────────────────────────────────────
diff --git a/csharp/Sinexcel 12K TL/SinexcelRecord.Modbus.cs b/csharp/Sinexcel 12K TL/SinexcelRecord.Modbus.cs
index f94990f32..0318fb7c7 100644
--- a/csharp/Sinexcel 12K TL/SinexcelRecord.Modbus.cs
+++ b/csharp/Sinexcel 12K TL/SinexcelRecord.Modbus.cs
@@ -440,24 +440,24 @@ public partial class SinexcelRecord
[HoldingRegister(12384, writable: true)] private UInt32 _outputVoltageAdjustmentFactor; // 0x3060
[HoldingRegister(12386, writable: true)] private UInt32 _setValueBatteryUndervoltage1; // 0x3062
[HoldingRegister(12388, writable: true)] private UInt32 _inverterPowerLimit; // 0x3064
- [HoldingRegister(12400, writable: true)] private UInt32 _battery2Capacity; // 0x30B0
- [HoldingRegister(12402, writable: true)] private UInt32 _maxChargingCurrentBattery2; // 0x30B2
- [HoldingRegister(12404, writable: true)] private UInt32 _maxDischargingCurrentBattery2; // 0x30B4
- [HoldingRegister(12406, writable: true)] private UInt32 _battery2RatedVoltage; // 0x30B6
- [HoldingRegister(12408, writable: true)] private UInt32 _battery2MinSoc; // 0x30B8
- [HoldingRegister(12410, writable: true)] private UInt32 _battery2OverVoltageSetting; // 0x30BA
- [HoldingRegister(12412, writable: true)] private UInt32 _battery2UnderVoltageSetpoint; // 0x30BC
+ [HoldingRegister(12464, writable: true)] private UInt32 _battery2Capacity; // 0x30B0
+ [HoldingRegister(12466, writable: true)] private UInt32 _maxChargingCurrentBattery2; // 0x30B2
+ [HoldingRegister(12468, writable: true)] private UInt32 _maxDischargingCurrentBattery2; // 0x30B4
+ [HoldingRegister(12470, writable: true)] private UInt32 _battery2RatedVoltage; // 0x30B6
+ [HoldingRegister(12472, writable: true)] private UInt32 _battery2MinSoc; // 0x30B8
+ [HoldingRegister(12474, writable: true)] private UInt32 _battery2OverVoltageSetting; // 0x30BA
+ [HoldingRegister(12476, writable: true)] private UInt32 _battery2UnderVoltageSetpoint; // 0x30BC
//
- [HoldingRegister(12414, writable: true)] private UInt32 _singleOrParallelMachine; // 0x30BE
- [HoldingRegister(12416, writable: true)] private UInt32 _numberOfSystemModules; // 0x30C0
- [HoldingRegister(12418, writable: true)] private UInt32 _parallelModuleMachineNumber; // 0x30C2
- [HoldingRegister(12420, writable: true)] private UInt32 _accreditedCountries; // 0x30C4
+ /* [HoldingRegister(12478, writable: true)] private UInt32 _singleOrParallelMachine; // 0x30BE // to be verified, writing not allowed
+ [HoldingRegister(12480, writable: true)] private UInt32 _numberOfSystemModules; // 0x30C0 // to be verified, writing not allowed
+ [HoldingRegister(12482, writable: true)] private UInt32 _parallelModuleMachineNumber; // 0x30C2 // to be verified, writing not allowed
+ [HoldingRegister(12484, writable: true)] private UInt32 _accreditedCountries; */ // 0x30C4 // to be verified, writing not allowed
[HoldingRegister(12618, writable: true)] private UInt32 _battery1BackupSOC; // 0x314A
[HoldingRegister(12620, writable: true)] private UInt32 _battery2BackupSOC; // 0x314C
- [HoldingRegister(12746, writable: true)] private UInt32 _enableGridExport; // 0x314A
- [HoldingRegister(12748, writable: true)] private UInt32 _powerGridExportLimit; // 0x314C
+ [HoldingRegister(12746, writable: true)] private UInt32 _enableGridExport; // 0x31CA
+ [HoldingRegister(12748, writable: true)] private UInt32 _powerGridExportLimit; // 0x31CC
// ───────────────────────────────────────────────