Merge branch 'main' of 91.92.155.224:Innovenergy/Innovenergy_trunk
This commit is contained in:
commit
1e7c500f90
|
|
@ -0,0 +1,160 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using InnovEnergy.App.SinexcelCommunication.ESS;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SinexcelCommunication.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.DailyElectricityPurchased,
|
||||||
|
DailyElectricityFed = r.DailyElectricityFed,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ public class Configuration
|
||||||
public Double MinimumSoC { get; set; }
|
public Double MinimumSoC { get; set; }
|
||||||
public Double MaximumDischargingCurrent { get; set; }
|
public Double MaximumDischargingCurrent { get; set; }
|
||||||
public Double MaximumChargingCurrent { get; set; }
|
public Double MaximumChargingCurrent { get; set; }
|
||||||
public WorkingMode OperatingPriority { get; set; }
|
public OperatingPriority OperatingPriority { get; set; }
|
||||||
public Int16 BatteriesCount { get; set; }
|
public Int16 BatteriesCount { get; set; }
|
||||||
public Int16 ClusterNumber { get; set; }
|
public Int16 ClusterNumber { get; set; }
|
||||||
public Int16 PvNumber { get; set; }
|
public Int16 PvNumber { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace InnovEnergy.App.SinexcelCommunication.DataTypes;
|
||||||
|
|
||||||
|
public enum OperatingPriority
|
||||||
|
{
|
||||||
|
ModeNotSynched = -1,
|
||||||
|
LoadPriority = 0,
|
||||||
|
BatteryPriority = 1,
|
||||||
|
GridPriority = 2,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
using InnovEnergy.App.SinexcelCommunication.DataTypes;
|
||||||
|
using InnovEnergy.Lib.Devices.Sinexcel_12K_TL;
|
||||||
|
using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType;
|
||||||
|
using InnovEnergy.Lib.Units;
|
||||||
|
using InnovEnergy.Lib.Units.Power;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SinexcelCommunication.ESS;
|
||||||
|
|
||||||
|
public class InverterRecords
|
||||||
|
{
|
||||||
|
|
||||||
|
public required Energy SelfGeneratedElectricity { get; init; }
|
||||||
|
public required Energy ElectricityPurchased { get; init; }
|
||||||
|
public required Energy ElectricityFed { get; init; }
|
||||||
|
public required Energy BatteryChargeEnergy { get; init; }
|
||||||
|
public required Energy BatteryDischargeEnergy { get; init; }
|
||||||
|
public required Energy LoadPowerConsumption { get; init; }
|
||||||
|
|
||||||
|
public required Energy DailySelfGeneratedElectricity { get; init; }
|
||||||
|
public required Energy DailyElectricityPurchased { get; init; }
|
||||||
|
public required Energy DailyElectricityFed { get; init; }
|
||||||
|
public required Energy BatteryDailyChargeEnergy { get; init; }
|
||||||
|
public required Energy BatteryDailyDischargeEnergy { get; init; }
|
||||||
|
public required Energy DailyLoadPowerConsumption { get; init; }
|
||||||
|
|
||||||
|
// public required ActivePower TotalConsumptionPower { get; init; }
|
||||||
|
public required ActivePower TotalPhotovoltaicPower { get; init; }
|
||||||
|
public required ActivePower TotalBatteryPower { get; init; }
|
||||||
|
public required ActivePower TotalLoadPower { get; init; }
|
||||||
|
public required ActivePower TotalGridPower { get; init; }
|
||||||
|
|
||||||
|
public required OperatingPriority OperatingPriority { get; init; }
|
||||||
|
public required Voltage AvgBatteryVoltage { get; init; }
|
||||||
|
public required Current TotalBatteryCurrent { get; init; }
|
||||||
|
public required Percent AvgBatterySoc { get; init; }
|
||||||
|
public required Percent AvgBatterySoh { get; init; }
|
||||||
|
public required Temperature AvgBatteryTemp { get; init; }
|
||||||
|
public required Percent MinSoc { get; init; }
|
||||||
|
|
||||||
|
public required Current MaxChargeCurrent { get; init; }
|
||||||
|
public required Current MaxDischargingCurrent { get; init; }
|
||||||
|
public required ActivePower GridPower { get; init; }
|
||||||
|
public required Frequency GridFrequency { get; init; }
|
||||||
|
public required ActivePower InverterPower { get; init; }
|
||||||
|
public required EnablePowerLimitation EnableGridExport { get; init; }
|
||||||
|
public required ActivePower GridExportPower { get; init; }
|
||||||
|
|
||||||
|
|
||||||
|
public required IReadOnlyList<SinexcelRecord> Devices { get; init; }
|
||||||
|
|
||||||
|
public static InverterRecords? FromInverters(IReadOnlyList<SinexcelRecord>? records)
|
||||||
|
{
|
||||||
|
if (records is null || records.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
|
||||||
|
return new InverterRecords
|
||||||
|
{
|
||||||
|
Devices = records,
|
||||||
|
|
||||||
|
DailySelfGeneratedElectricity = records.Sum(r => r.DailySelfGeneratedElectricity),
|
||||||
|
DailyElectricityPurchased = records.Sum(r => r.DailyElectricityPurchased),
|
||||||
|
DailyElectricityFed = records.Sum(r => r.DailyElectricityFed),
|
||||||
|
BatteryDailyChargeEnergy = records.Sum(r => r.BatteryDailyChargeEnergy),
|
||||||
|
BatteryDailyDischargeEnergy = records.Sum(r => r.BatteryDailyDischargeEnergy),
|
||||||
|
DailyLoadPowerConsumption = records.Sum(r => r.DailyLoadPowerConsumption),
|
||||||
|
|
||||||
|
SelfGeneratedElectricity = records.Sum(r => r.SelfGeneratedElectricity),
|
||||||
|
ElectricityPurchased = records.Sum(r => r.TotalEnergyToUser),
|
||||||
|
ElectricityFed = records.Sum(r => r.TotalEnergyToGrid),
|
||||||
|
BatteryChargeEnergy = records.Sum(r => r.BatteryCharge),
|
||||||
|
BatteryDischargeEnergy = records.Sum(r => r.BatteryDischarge),
|
||||||
|
LoadPowerConsumption = records.Sum(r => r.LoadPowerConsumption),
|
||||||
|
|
||||||
|
// TotalConsumptionPower = records.Sum(r => r.ConsumptionPower), // consumption same as load
|
||||||
|
TotalPhotovoltaicPower = records.Sum(r => r.TotalPhotovoltaicPower),
|
||||||
|
TotalBatteryPower = records.Sum(r => r.TotalBatteryPower),
|
||||||
|
TotalLoadPower = records.Sum(r => r.TotalLoadPower),
|
||||||
|
TotalGridPower = records.Sum(b => b.TotalGridPower),
|
||||||
|
OperatingPriority = records.Select(r => r.WorkingMode).Distinct().Count() == 1 ? (OperatingPriority)records.First().WorkingMode: OperatingPriority.ModeNotSynched,
|
||||||
|
AvgBatteryVoltage = records.SelectMany(r => new[] { r.Battery1Voltage.Value, r.Battery2Voltage.Value }).Where(v => v > 0).DefaultIfEmpty(0).Average(),
|
||||||
|
TotalBatteryCurrent = records.SelectMany(r => new [] { r.Battery1Current.Value, r.Battery2Current.Value}).Sum(),
|
||||||
|
AvgBatterySoc = records.SelectMany(r => new[] { r.Battery1Soc.Value, r.Battery2Soc.Value }).Average(),
|
||||||
|
AvgBatterySoh = records.SelectMany(r => new[] { r.Battery1Soh.Value, r.Battery2Soh.Value }).Average(),
|
||||||
|
MinSoc = records.SelectMany(r => new[] { r.Battery1BackupSoc, r.Battery2BackupSoc}).Min(),
|
||||||
|
AvgBatteryTemp = records.SelectMany(r => new[] { r.Battery1Temperature.Value, r.Battery2Temperature.Value }).Average(),
|
||||||
|
MaxChargeCurrent = records.SelectMany(r => new[] { r.Battery1MaxChargingCurrent, r.Battery2MaxChargingCurrent }).Min(),
|
||||||
|
MaxDischargingCurrent = records.SelectMany(r => new[] { r.Battery1MaxDischargingCurrent, r.Battery2MaxDischargingCurrent }).Max(),
|
||||||
|
GridPower = records.Sum(r => r.TotalGridPower),
|
||||||
|
GridFrequency = records.Average(r => r.GridVoltageFrequency),
|
||||||
|
InverterPower = records.Sum( r => r.InverterActivePower ),
|
||||||
|
EnableGridExport = records.Select(r => r.EnableGridExport).Distinct().Count() == 1 ? records.First().EnableGridExport: EnablePowerLimitation.Prohibited,
|
||||||
|
GridExportPower = records.Sum(r => r.PowerGridExportLimit)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ namespace InnovEnergy.App.SinexcelCommunication.ESS;
|
||||||
|
|
||||||
public record StatusRecord
|
public record StatusRecord
|
||||||
{
|
{
|
||||||
public required SinexcelRecord InverterRecord { get; set; }
|
public required InverterRecords? InverterRecord { get; set; }
|
||||||
|
|
||||||
public required Config Config { get; set; }
|
public required Config Config { get; set; }
|
||||||
}
|
}
|
||||||
|
|
@ -13,20 +13,37 @@ public static class MiddlewareAgent
|
||||||
private static IPAddress? _controllerIpAddress;
|
private static IPAddress? _controllerIpAddress;
|
||||||
private static EndPoint? _endPoint;
|
private static EndPoint? _endPoint;
|
||||||
|
|
||||||
public static void InitializeCommunicationToMiddleware()
|
public static bool InitializeCommunicationToMiddleware()
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_controllerIpAddress = FindVpnIp();
|
_controllerIpAddress = FindVpnIp();
|
||||||
if (Equals(IPAddress.None, _controllerIpAddress))
|
if (Equals(IPAddress.None, _controllerIpAddress))
|
||||||
{
|
{
|
||||||
Console.WriteLine("There is no VPN interface, exiting...");
|
Console.WriteLine("There is no VPN interface.");
|
||||||
|
_udpListener = null;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Int32 udpPort = 9000;
|
const int udpPort = 9000;
|
||||||
_endPoint = new IPEndPoint(_controllerIpAddress, udpPort);
|
_endPoint = new IPEndPoint(_controllerIpAddress, udpPort);
|
||||||
|
|
||||||
|
_udpListener?.Close();
|
||||||
|
_udpListener?.Dispose();
|
||||||
|
|
||||||
_udpListener = new UdpClient();
|
_udpListener = new UdpClient();
|
||||||
_udpListener.Client.Blocking = false;
|
_udpListener.Client.Blocking = false;
|
||||||
_udpListener.Client.Bind(_endPoint);
|
_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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IPAddress FindVpnIp()
|
private static IPAddress FindVpnIp()
|
||||||
|
|
@ -50,42 +67,96 @@ public static class MiddlewareAgent
|
||||||
|
|
||||||
return IPAddress.None;
|
return IPAddress.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Configuration? SetConfigurationFile()
|
public static Configuration? SetConfigurationFile()
|
||||||
{
|
{
|
||||||
if (_udpListener.Available > 0)
|
try
|
||||||
{
|
{
|
||||||
IPEndPoint? serverEndpoint = null;
|
// Ensure listener is initialized
|
||||||
|
if (_udpListener == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("UDP listener not initialized, trying to initialize...");
|
||||||
|
InitializeCommunicationToMiddleware();
|
||||||
|
|
||||||
var replyMessage = "ACK";
|
if (_udpListener == null)
|
||||||
var replyData = Encoding.UTF8.GetBytes(replyMessage);
|
{
|
||||||
|
Console.WriteLine("Failed to initialize UDP listener.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if data is available
|
||||||
|
if (_udpListener.Available <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
IPEndPoint? serverEndpoint = null;
|
||||||
|
|
||||||
var udpMessage = _udpListener.Receive(ref serverEndpoint);
|
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<Configuration>(message);
|
var config = JsonSerializer.Deserialize<Configuration>(message);
|
||||||
|
|
||||||
if (config != null)
|
if (config != null)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Received a configuration message: " +
|
Console.WriteLine(
|
||||||
"MinimumSoC is " + config.MinimumSoC + " and operating priorty is " +config.OperatingPriority + "Number of batteries is " + config.BatteriesCount
|
$"Received a configuration message:\n" +
|
||||||
+ "Maximum Charging current is "+ config.MaximumChargingCurrent + "/n" + "Maximum Discharging current is " + config.MaximumDischargingCurrent
|
$"MinimumSoC: {config.MinimumSoC}\n" +
|
||||||
+ "StartTimeChargeandDischargeDayandTime is" + config.StartTimeChargeandDischargeDayandTime + "StopTimeChargeandDischargeDayandTime is" + config.StopTimeChargeandDischargeDayandTime
|
$"OperatingPriority: {config.OperatingPriority}\n" +
|
||||||
+ "TimeChargeandDischargePowert is " + config.TimeChargeandDischargePower + " Control permission is" + config.ControlPermission);
|
$"Number of batteries: {config.BatteriesCount}\n" +
|
||||||
|
$"Maximum Charging current: {config.MaximumChargingCurrent}\n" +
|
||||||
|
$"Maximum Discharging current: {config.MaximumDischargingCurrent}\n" +
|
||||||
|
$"StartTimeChargeandDischargeDayandTime: {config.StartTimeChargeandDischargeDayandTime}\n" +
|
||||||
|
$"StopTimeChargeandDischargeDayandTime: {config.StopTimeChargeandDischargeDayandTime}\n" +
|
||||||
|
$"TimeChargeandDischargePower: {config.TimeChargeandDischargePower}\n" +
|
||||||
|
$"ControlPermission: {config.ControlPermission}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send ACK
|
||||||
|
var replyMessage = "ACK";
|
||||||
|
var replyData = Encoding.UTF8.GetBytes(replyMessage);
|
||||||
|
|
||||||
// Send the reply to the sender's endpoint
|
|
||||||
_udpListener.Send(replyData, replyData.Length, serverEndpoint);
|
_udpListener.Send(replyData, replyData.Length, serverEndpoint);
|
||||||
Console.WriteLine($"Replied to {serverEndpoint}: {replyMessage}");
|
Console.WriteLine($"Replied to {serverEndpoint}: {replyMessage}");
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
|
|
||||||
if (_endPoint != null && !_endPoint.Equals(_udpListener.Client.LocalEndPoint as IPEndPoint))
|
|
||||||
{
|
{
|
||||||
Console.WriteLine("UDP address has changed, rebinding...");
|
Console.WriteLine("Received UDP message but failed to deserialize Configuration.");
|
||||||
InitializeCommunicationToMiddleware();
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch (SocketException ex)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.IO.Compression;
|
using System.Diagnostics;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.IO.Ports;
|
using System.IO.Ports;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
@ -24,6 +25,7 @@ using Formatting = Newtonsoft.Json.Formatting;
|
||||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||||
using static InnovEnergy.App.SinexcelCommunication.MiddlewareClasses.MiddlewareAgent;
|
using static InnovEnergy.App.SinexcelCommunication.MiddlewareClasses.MiddlewareAgent;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using InnovEnergy.App.SinexcelCommunication.AggregationService;
|
||||||
using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType;
|
using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Protocol;
|
using InnovEnergy.Lib.Protocols.Modbus.Protocol;
|
||||||
using static InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType.WorkingMode;
|
using static InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType.WorkingMode;
|
||||||
|
|
@ -35,12 +37,12 @@ namespace InnovEnergy.App.SinexcelCommunication;
|
||||||
[SuppressMessage("Trimming", "IL2026:Members annotated with \'RequiresUnreferencedCodeAttribute\' require dynamic access otherwise can break functionality when trimming application code")]
|
[SuppressMessage("Trimming", "IL2026:Members annotated with \'RequiresUnreferencedCodeAttribute\' require dynamic access otherwise can break functionality when trimming application code")]
|
||||||
internal static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(5);
|
private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(10);
|
||||||
private const UInt16 NbrOfFileToConcatenate = 15; // add this to config file
|
private const UInt16 NbrOfFileToConcatenate = 15; // add this to config file
|
||||||
private static UInt16 _fileCounter = 0;
|
private static UInt16 _fileCounter = 0;
|
||||||
private static Channel _sinexcelChannel1;
|
private static List<Channel> _sinexcelChannel;
|
||||||
private static Channel _sinexcelChannel2;
|
private static DateTime? _lastUploadedAggregatedDate;
|
||||||
|
private static DailyEnergyData? _pendingDailyData;
|
||||||
private static readonly String SwVersionNumber = " V1.00." + DateTime.Today;
|
private static readonly String SwVersionNumber = " V1.00." + DateTime.Today;
|
||||||
private const String VpnServerIp = "10.2.0.11";
|
private const String VpnServerIp = "10.2.0.11";
|
||||||
private static Boolean _subscribedToQueue = false;
|
private static Boolean _subscribedToQueue = false;
|
||||||
|
|
@ -57,19 +59,29 @@ internal static class Program
|
||||||
private const Int32 HeartbeatIntervalSeconds = 60;
|
private const Int32 HeartbeatIntervalSeconds = 60;
|
||||||
|
|
||||||
|
|
||||||
// move all this to config file
|
|
||||||
private const String Port1 = "/dev/ttyUSB0";
|
|
||||||
private const String Port2 = "/dev/ttyUSB1";
|
|
||||||
private const Byte SlaveId = 1;
|
private const Byte SlaveId = 1;
|
||||||
private const Parity Parity = 0; //none
|
|
||||||
private const Int32 StopBits = 1;
|
|
||||||
private const Int32 BaudRate = 115200;
|
|
||||||
private const Int32 DataBits = 8;
|
|
||||||
|
|
||||||
public static async Task Main(String[] args)
|
public static async Task Main(String[] args)
|
||||||
{
|
{
|
||||||
_sinexcelChannel1 = new SerialPortChannel(Port1, BaudRate, Parity, DataBits, StopBits);
|
var config = Config.Load();
|
||||||
_sinexcelChannel2 = new SerialPortChannel(Port2, BaudRate, Parity, DataBits, StopBits);
|
var d = config.Devices;
|
||||||
|
var serial = d.Serial;
|
||||||
|
|
||||||
|
|
||||||
|
Channel CreateChannel(SodiDevice device) => device.DeviceState == DeviceState.Disabled
|
||||||
|
? new NullChannel()
|
||||||
|
: new SerialPortChannel(device.Port,serial.BaudRate,serial.Parity,serial.DataBits,serial.StopBits);
|
||||||
|
|
||||||
|
_sinexcelChannel = new List<Channel>()
|
||||||
|
{
|
||||||
|
CreateChannel(d.Inverter1),
|
||||||
|
CreateChannel(d.Inverter2),
|
||||||
|
CreateChannel(d.Inverter3),
|
||||||
|
CreateChannel(d.Inverter4)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
InitializeCommunicationToMiddleware();
|
InitializeCommunicationToMiddleware();
|
||||||
while (true)
|
while (true)
|
||||||
|
|
@ -92,20 +104,23 @@ internal static class Program
|
||||||
|
|
||||||
Console.WriteLine("Starting Sinexcel Communication, SW Version : " + SwVersionNumber);
|
Console.WriteLine("Starting Sinexcel Communication, SW Version : " + SwVersionNumber);
|
||||||
|
|
||||||
var sinexcelDevice1 = new SinexcelDevice(_sinexcelChannel1, SlaveId);
|
var devices = _sinexcelChannel
|
||||||
var sinexcelDevice2 = new SinexcelDevice(_sinexcelChannel2, SlaveId);
|
.Where(ch => ch is not NullChannel)
|
||||||
|
.Select(ch => new SinexcelDevice(ch, SlaveId))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
StatusRecord? ReadStatus()
|
StatusRecord? ReadStatus()
|
||||||
{
|
{
|
||||||
var config = Config.Load();
|
var config = Config.Load();
|
||||||
var sinexcelRecord1 = sinexcelDevice1.Read();
|
var listOfInverterRecord = devices
|
||||||
var sinexcelRecord2 = sinexcelDevice2.Read();
|
.Select(device => device.Read())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
InverterRecords? inverterRecords = InverterRecords.FromInverters(listOfInverterRecord);
|
||||||
|
|
||||||
return new StatusRecord
|
return new StatusRecord
|
||||||
{
|
{
|
||||||
InverterRecord1 = sinexcelRecord1,
|
InverterRecord = inverterRecords,
|
||||||
InverterRecord2 = sinexcelRecord2,
|
|
||||||
Config = config // load from disk every iteration, so config can be changed while running
|
Config = config // load from disk every iteration, so config can be changed while running
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -128,82 +143,23 @@ internal static class Program
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Watchdog.NotifyAlive();
|
Watchdog.NotifyAlive();
|
||||||
|
|
||||||
var startTime = DateTime.Now;
|
var startTime = DateTime.Now;
|
||||||
Console.WriteLine("***************************** Reading Battery Data *********************************************");
|
Console.WriteLine("***************************** Reading Battery Data *********************************************");
|
||||||
Console.WriteLine(startTime.ToString("HH:mm:ss.fff ")+ "Start Reading");
|
Console.WriteLine(startTime.ToString("HH:mm:ss.fff ")+ "Start Reading");
|
||||||
|
|
||||||
// the order matter of the next three lines
|
|
||||||
var statusrecord = ReadStatus();
|
var statusrecord = ReadStatus();
|
||||||
if (statusrecord == null)
|
if (statusrecord == null)
|
||||||
return null;
|
return null;
|
||||||
|
_ = CreateAggregatedData(statusrecord);
|
||||||
|
|
||||||
Console.WriteLine(" ************************************************ Inverter 1 ************************************************ ");
|
var invDevices = statusrecord.InverterRecord?.Devices;
|
||||||
Console.WriteLine( statusrecord.InverterRecord1.SystemDateTime + " SystemDateTime ");
|
|
||||||
|
|
||||||
Console.WriteLine( statusrecord.InverterRecord1.TotalPhotovoltaicPower + " TotalPhotovoltaicPower ");
|
if (invDevices != null)
|
||||||
Console.WriteLine( statusrecord.InverterRecord1.TotalBatteryPower + " TotalBatteryPower ");
|
{
|
||||||
Console.WriteLine( statusrecord.InverterRecord1.TotalLoadPower + " TotalLoadPower ");
|
var index = 1;
|
||||||
Console.WriteLine( statusrecord.InverterRecord1.TotalGridPower + " TotalGridPower ");
|
foreach (var inverter in invDevices)
|
||||||
|
PrintInverterData(inverter, index++);
|
||||||
|
}
|
||||||
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 ");*/
|
|
||||||
|
|
||||||
SendSalimaxStateAlarm(GetSodiHomeStateAlarm(statusrecord),statusrecord);
|
SendSalimaxStateAlarm(GetSodiHomeStateAlarm(statusrecord),statusrecord);
|
||||||
statusrecord.ControlConstants();
|
statusrecord.ControlConstants();
|
||||||
|
|
@ -213,18 +169,20 @@ internal static class Program
|
||||||
Console.WriteLine(startWritingTime.ToString("HH:mm:ss.fff ") +"start Writing");
|
Console.WriteLine(startWritingTime.ToString("HH:mm:ss.fff ") +"start Writing");
|
||||||
statusrecord?.Config.Save(); // save the config file
|
statusrecord?.Config.Save(); // save the config file
|
||||||
|
|
||||||
if (statusrecord is { Config.ControlPermission: true })
|
if (statusrecord is { Config.ControlPermission: true, InverterRecord.Devices: not null })
|
||||||
{
|
{
|
||||||
Console.WriteLine("We have the Right to Write");
|
Console.WriteLine("We have the Right to Write");
|
||||||
sinexcelDevice1.Write(statusrecord.InverterRecord1);
|
|
||||||
sinexcelDevice2.Write(statusrecord.InverterRecord2);
|
|
||||||
|
|
||||||
|
foreach (var pair in devices.Zip(statusrecord.InverterRecord.Devices))
|
||||||
|
pair.First.Write(pair.Second);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.WriteLine(" Nooooooo We cant' have the Right to Write");
|
Console.WriteLine("Nooooooo We can't have the Right to Write");
|
||||||
}
|
}
|
||||||
|
var stop = DateTime.Now;
|
||||||
|
Console.WriteLine("***************************** Writing finished *********************************************");
|
||||||
|
Console.WriteLine(stop.ToString("HH:mm:ss.fff ")+ "Cycle end");
|
||||||
return statusrecord;
|
return statusrecord;
|
||||||
}
|
}
|
||||||
catch (CrcException e)
|
catch (CrcException e)
|
||||||
|
|
@ -240,59 +198,215 @@ internal static class Program
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this is synchronous because :
|
||||||
|
//
|
||||||
|
// it only appends one line once per hour and once per day
|
||||||
|
//
|
||||||
|
// it should not meaningfully affect a 10-second loop
|
||||||
|
|
||||||
|
private static async Task CreateAggregatedData(StatusRecord statusRecord)
|
||||||
|
{
|
||||||
|
|
||||||
|
DateTime now = DateTime.Now;
|
||||||
|
string baseFolder = AppContext.BaseDirectory;
|
||||||
|
|
||||||
|
// 1) Finalize previous hour if hour changed
|
||||||
|
var hourlyData = EnergyAggregation.ProcessHourlyData(statusRecord, now);
|
||||||
|
/*if (hourlyData != null)
|
||||||
|
{
|
||||||
|
AggregatedDataFileWriter.AppendHourlyData(hourlyData, baseFolder);
|
||||||
|
}*/
|
||||||
|
if (hourlyData != null)
|
||||||
|
{
|
||||||
|
AggregatedDataFileWriter.AppendHourlyData(hourlyData, baseFolder);
|
||||||
|
|
||||||
|
if (_pendingDailyData != null && hourlyData.Timestamp.Hour == 23)
|
||||||
|
{
|
||||||
|
AggregatedDataFileWriter.AppendDailyData(_pendingDailyData, baseFolder);
|
||||||
|
_pendingDailyData = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Save daily line near end of day
|
||||||
|
var dailyData = EnergyAggregation.TryCreateDailyData(statusRecord, now);
|
||||||
|
if (dailyData != null)
|
||||||
|
{
|
||||||
|
_pendingDailyData = dailyData;
|
||||||
|
//AggregatedDataFileWriter.AppendDailyData(dailyData, baseFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) After midnight, upload yesterday's completed file once
|
||||||
|
var yesterday = now.Date.AddDays(-1);
|
||||||
|
|
||||||
|
if (now.Hour == 0 && now.Minute == 0)
|
||||||
|
{
|
||||||
|
if (_lastUploadedAggregatedDate != yesterday)
|
||||||
|
{
|
||||||
|
Console.WriteLine(" We are inside the lastuploaded Aggregate");
|
||||||
|
string filePath = Path.Combine(
|
||||||
|
baseFolder,
|
||||||
|
"AggregatedData",
|
||||||
|
yesterday.ToString("ddMMyyyy") + ".json");
|
||||||
|
|
||||||
|
bool uploaded = await PushAggregatedFileToS3(filePath, statusRecord);
|
||||||
|
|
||||||
|
if (uploaded)
|
||||||
|
{
|
||||||
|
_lastUploadedAggregatedDate = yesterday;
|
||||||
|
Console.WriteLine($"Uploaded aggregated file for {yesterday:ddMMyyyy}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Uploaded failed for {yesterday:ddMMyyyy}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Boolean> PushAggregatedFileToS3( String localFilePath, StatusRecord statusRecord)
|
||||||
|
{
|
||||||
|
var s3Config = statusRecord.Config.S3;
|
||||||
|
if (s3Config is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(localFilePath))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"File not found: {localFilePath}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonString = await File.ReadAllTextAsync(localFilePath);
|
||||||
|
|
||||||
|
// Example S3 object name: 09032026.json
|
||||||
|
var s3Path = Path.GetFileName(localFilePath);
|
||||||
|
var request = s3Config.CreatePutRequest(s3Path);
|
||||||
|
var base64String = Convert.ToBase64String(Encoding.UTF8.GetBytes(jsonString));
|
||||||
|
|
||||||
|
using var content = new StringContent(base64String, Encoding.UTF8, "application/base64");
|
||||||
|
|
||||||
|
Console.WriteLine("Sending Content-Type: application/base64; charset=utf-8");
|
||||||
|
Console.WriteLine($"S3 Path: {s3Path}");
|
||||||
|
|
||||||
|
var response = await request.PutAsync(content);
|
||||||
|
|
||||||
|
if (response.StatusCode != 200)
|
||||||
|
{
|
||||||
|
Console.WriteLine("ERROR: PUT");
|
||||||
|
var error = await response.GetStringAsync();
|
||||||
|
Console.WriteLine(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Uploaded successfully: {s3Path}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"PushAggregatedFileToS3 failed: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void ControlConstants(this StatusRecord? statusrecord)
|
private static void ControlConstants(this StatusRecord? statusrecord)
|
||||||
{
|
{
|
||||||
if (statusrecord == null) return;
|
if (statusrecord?.InverterRecord?.Devices == null) return;
|
||||||
|
|
||||||
statusrecord.InverterRecord1.Battery1BackupSoc = (Single)statusrecord.Config.MinSoc ;
|
// Compute once (same for all inverters)
|
||||||
statusrecord.InverterRecord1.Battery2BackupSoc = (Single)statusrecord.Config.MinSoc ;
|
var config = statusrecord.Config;
|
||||||
statusrecord.InverterRecord1.RepetitiveWeeks = SinexcelWeekDays.All;
|
|
||||||
|
|
||||||
|
var isChargePeriod = IsNowInsideDateAndTime(
|
||||||
|
config.StartTimeChargeandDischargeDayandTime,
|
||||||
|
config.StopTimeChargeandDischargeDayandTime
|
||||||
|
);
|
||||||
|
|
||||||
var isChargePeriod = IsNowInsideDateAndTime(statusrecord.Config.StartTimeChargeandDischargeDayandTime, statusrecord.Config.StopTimeChargeandDischargeDayandTime);
|
foreach (var inverter in statusrecord.InverterRecord.Devices)
|
||||||
|
|
||||||
|
|
||||||
Console.WriteLine("Are we inside the charge/Discharge time " + isChargePeriod);
|
|
||||||
|
|
||||||
if (statusrecord.Config.OperatingPriority != TimeChargeDischarge)
|
|
||||||
{
|
{
|
||||||
statusrecord.InverterRecord1.WorkingMode = statusrecord.Config.OperatingPriority;
|
// constants for every inverter
|
||||||
|
inverter.Battery1BackupSoc = (float)config.MinSoc;
|
||||||
|
inverter.Battery2BackupSoc = (float)config.MinSoc;
|
||||||
|
inverter.RepetitiveWeeks = SinexcelWeekDays.All;
|
||||||
|
|
||||||
|
var operatingMode = config.OperatingPriority switch
|
||||||
|
{
|
||||||
|
OperatingPriority.LoadPriority => SpontaneousSelfUse,
|
||||||
|
OperatingPriority.BatteryPriority => TimeChargeDischarge,
|
||||||
|
OperatingPriority.GridPriority => PrioritySellElectricity,
|
||||||
|
_ => SpontaneousSelfUse
|
||||||
|
};
|
||||||
|
|
||||||
|
if (operatingMode!= TimeChargeDischarge)
|
||||||
|
{
|
||||||
|
inverter.WorkingMode = operatingMode;
|
||||||
}
|
}
|
||||||
else if (statusrecord.Config.OperatingPriority == TimeChargeDischarge && isChargePeriod)
|
else if (isChargePeriod)
|
||||||
{
|
{
|
||||||
statusrecord.InverterRecord1.WorkingMode = statusrecord.Config.OperatingPriority;
|
inverter.WorkingMode = operatingMode;
|
||||||
|
inverter.EffectiveStartDate = config.StartTimeChargeandDischargeDayandTime.Date;
|
||||||
|
inverter.EffectiveEndDate = config.StopTimeChargeandDischargeDayandTime.Date;
|
||||||
|
|
||||||
if (statusrecord.Config.TimeChargeandDischargePower > 0)
|
var power = config.TimeChargeandDischargePower;
|
||||||
|
|
||||||
|
if (power > 0)
|
||||||
{
|
{
|
||||||
statusrecord.InverterRecord1.EffectiveStartDate = statusrecord.Config.StartTimeChargeandDischargeDayandTime.Date;
|
inverter.ChargingPowerPeriod1 = Math.Abs(power);
|
||||||
statusrecord.InverterRecord1.EffectiveEndDate = statusrecord.Config.StopTimeChargeandDischargeDayandTime.Date;
|
inverter.ChargeStartTimePeriod1 = config.StartTimeChargeandDischargeDayandTime.TimeOfDay;
|
||||||
statusrecord.InverterRecord1.ChargingPowerPeriod1 = Math.Abs(statusrecord.Config.TimeChargeandDischargePower);
|
inverter.ChargeEndTimePeriod1 = config.StopTimeChargeandDischargeDayandTime.TimeOfDay;
|
||||||
statusrecord.InverterRecord1.ChargeStartTimePeriod1 = statusrecord.Config.StartTimeChargeandDischargeDayandTime.TimeOfDay;
|
|
||||||
statusrecord.InverterRecord1.ChargeEndTimePeriod1 = statusrecord.Config.StopTimeChargeandDischargeDayandTime.TimeOfDay;
|
|
||||||
|
|
||||||
statusrecord.InverterRecord1.DischargeStartTimePeriod1 = TimeSpan.Zero;
|
inverter.DischargeStartTimePeriod1 = TimeSpan.Zero;
|
||||||
statusrecord.InverterRecord1.DischargeEndTimePeriod1 = TimeSpan.Zero;
|
inverter.DischargeEndTimePeriod1 = TimeSpan.Zero;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
statusrecord.InverterRecord1.EffectiveStartDate = statusrecord.Config.StartTimeChargeandDischargeDayandTime.Date;
|
inverter.DishargingPowerPeriod1 = Math.Abs(power);
|
||||||
statusrecord.InverterRecord1.EffectiveEndDate = statusrecord.Config.StopTimeChargeandDischargeDayandTime.Date;
|
inverter.DischargeStartTimePeriod1 = config.StartTimeChargeandDischargeDayandTime.TimeOfDay;
|
||||||
statusrecord.InverterRecord1.DishargingPowerPeriod1 = Math.Abs(statusrecord.Config.TimeChargeandDischargePower);
|
inverter.DischargeEndTimePeriod1 = config.StopTimeChargeandDischargeDayandTime.TimeOfDay;
|
||||||
statusrecord.InverterRecord1.DischargeStartTimePeriod1 = statusrecord.Config.StartTimeChargeandDischargeDayandTime.TimeOfDay;
|
|
||||||
statusrecord.InverterRecord1.DischargeEndTimePeriod1 = statusrecord.Config.StopTimeChargeandDischargeDayandTime.TimeOfDay;
|
|
||||||
|
|
||||||
statusrecord.InverterRecord1.ChargeStartTimePeriod1 = TimeSpan.Zero;
|
inverter.ChargeStartTimePeriod1 = TimeSpan.Zero;
|
||||||
statusrecord.InverterRecord1.ChargeEndTimePeriod1 = TimeSpan.Zero;
|
inverter.ChargeEndTimePeriod1 = TimeSpan.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
statusrecord.InverterRecord1.WorkingMode = SpontaneousSelfUse;
|
inverter.WorkingMode = SpontaneousSelfUse;
|
||||||
}
|
}
|
||||||
statusrecord.InverterRecord1.PowerOn = 1;
|
|
||||||
statusrecord.InverterRecord1.PowerOff = 0;
|
inverter.PowerOn = 1;
|
||||||
//statusrecord.InverterRecord.FaultClearing = 1;
|
inverter.PowerOff = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PrintInverterData(SinexcelRecord r, int index)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" ************************************************ Inverter {index} ************************************************ ");
|
||||||
|
|
||||||
|
//Console.WriteLine($"{r.SystemDateTime} SystemDateTime");
|
||||||
|
Console.WriteLine($"{r.TotalPhotovoltaicPower} TotalPhotovoltaicPower");
|
||||||
|
Console.WriteLine($"{r.TotalBatteryPower} TotalBatteryPower");
|
||||||
|
Console.WriteLine($"{r.TotalLoadPower} TotalLoadPower");
|
||||||
|
Console.WriteLine($"{r.TotalGridPower} TotalGridPower");
|
||||||
|
|
||||||
|
Console.WriteLine($"{r.Battery1Power} Battery1Power");
|
||||||
|
Console.WriteLine($"{r.Battery1Soc} Battery1Soc");
|
||||||
|
Console.WriteLine($"{r.Battery1BackupSoc} Battery1BackupSoc");
|
||||||
|
Console.WriteLine($"{r.Battery1MinSoc} Battery1MinSoc");
|
||||||
|
|
||||||
|
Console.WriteLine($"{r.Battery2Power} Battery2Power");
|
||||||
|
Console.WriteLine($"{r.Battery2Soc} Battery2Soc");
|
||||||
|
Console.WriteLine($"{r.Battery2BackupSoc} Battery2BackupSoc");
|
||||||
|
Console.WriteLine($"{r.Battery2MinSoc} Battery2MinSoc");
|
||||||
|
|
||||||
|
Console.WriteLine($"{r.EnableGridExport} EnableGridExport");
|
||||||
|
Console.WriteLine($"{r.PowerGridExportLimit} PowerGridExportLimit");
|
||||||
|
|
||||||
|
Console.WriteLine($"{r.PowerOn} PowerOn");
|
||||||
|
Console.WriteLine($"{r.PowerOff} PowerOff");
|
||||||
|
Console.WriteLine($"{r.WorkingMode} WorkingMode");
|
||||||
|
Console.WriteLine($"{r.GridSwitchMethod} GridSwitchMethod");
|
||||||
|
Console.WriteLine($"{r.ThreePhaseWireSystem} ThreePhaseWireSystem");
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsNowInsideDateAndTime(DateTime effectiveStart, DateTime effectiveEnd)
|
private static bool IsNowInsideDateAndTime(DateTime effectiveStart, DateTime effectiveEnd)
|
||||||
|
|
@ -309,7 +423,7 @@ internal static class Program
|
||||||
|
|
||||||
private static StatusMessage GetSodiHomeStateAlarm(StatusRecord? record)
|
private static StatusMessage GetSodiHomeStateAlarm(StatusRecord? record)
|
||||||
{
|
{
|
||||||
var s3Bucket = Config.Load().S3?.Bucket;
|
var s3Bucket = record?.Config.S3?.Bucket; // this should not load the config file, only use the one from status record TO change this in other project
|
||||||
|
|
||||||
var alarmList = new List<AlarmOrWarning>();
|
var alarmList = new List<AlarmOrWarning>();
|
||||||
var warningList = new List<AlarmOrWarning>();
|
var warningList = new List<AlarmOrWarning>();
|
||||||
|
|
@ -424,12 +538,6 @@ internal static class Program
|
||||||
var stateChanged = currentSalimaxState.Status != _prevSodiohomeAlarmState;
|
var stateChanged = currentSalimaxState.Status != _prevSodiohomeAlarmState;
|
||||||
var contentChanged = HasErrorsOrWarningsChanged(currentSalimaxState);
|
var contentChanged = HasErrorsOrWarningsChanged(currentSalimaxState);
|
||||||
var needsHeartbeat = (DateTime.Now - _lastHeartbeatTime).TotalSeconds >= HeartbeatIntervalSeconds;
|
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)
|
if (s3Bucket == null)
|
||||||
{
|
{
|
||||||
|
|
@ -530,43 +638,44 @@ internal static class Program
|
||||||
{
|
{
|
||||||
var modbusData = new Dictionary<String, UInt16>();
|
var modbusData = new Dictionary<String, UInt16>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
// SYSTEM DATA
|
// 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.InverterRecord1.SystemDateTime.ToUnixTime(), "UInt32", 30002);
|
var result2 = ConvertToModbusRegisters(DateTimeOffset.Now.ToUnixTimeSeconds(), "UInt32", 30002);
|
||||||
// SYSTEM DATA
|
// SYSTEM DATA
|
||||||
var result3 = ConvertToModbusRegisters(status.InverterRecord1.WorkingMode, "UInt16", 30004);
|
var result3 = ConvertToModbusRegisters(status.InverterRecord.OperatingPriority, "UInt16", 30004);
|
||||||
|
|
||||||
// BATTERY SUMMARY (assuming single battery [0]) // this to be improved
|
|
||||||
|
|
||||||
var result4 = ConvertToModbusRegisters((status.Config.BatteriesCount), "UInt16", 31000);
|
var result4 = ConvertToModbusRegisters((status.Config.BatteriesCount), "UInt16", 31000);
|
||||||
var result8 = ConvertToModbusRegisters((status.InverterRecord1.Battery1Voltage.Value * 10), "UInt16", 31001);
|
var result8 = ConvertToModbusRegisters((0), "UInt16", 31001); // this is ignored as dosen't exist in Sinexcel
|
||||||
var result12 = ConvertToModbusRegisters((status.InverterRecord1.Battery2Voltage.Value * 10), "Int16", 31002);
|
var result12 = ConvertToModbusRegisters((status.InverterRecord.AvgBatteryVoltage.Value * 10), "Int16", 31002);
|
||||||
var result13 = ConvertToModbusRegisters((status.InverterRecord1.Battery1Current.Value * 10), "Int32", 31003);
|
var result13 = ConvertToModbusRegisters((status.InverterRecord.TotalBatteryCurrent.Value * 10), "Int32", 31003);
|
||||||
var result16 = ConvertToModbusRegisters((status.InverterRecord1.Battery2Current.Value * 10), "Int32", 31005);
|
var result16 = ConvertToModbusRegisters((status.InverterRecord.AvgBatterySoc.Value * 100), "UInt16", 31005);
|
||||||
var result9 = ConvertToModbusRegisters((status.InverterRecord1.Battery1Soc.Value * 100), "UInt16", 31007);
|
var result9 = ConvertToModbusRegisters((status.InverterRecord.TotalBatteryPower.Value * 10), "Int32", 31006);
|
||||||
var result14 = ConvertToModbusRegisters((status.InverterRecord1.Battery2Soc.Value * 100), "UInt16", 31008);
|
var result14 = ConvertToModbusRegisters((status.InverterRecord.MinSoc.Value * 100), "UInt16", 31008);
|
||||||
var result5 = ConvertToModbusRegisters((status.InverterRecord1.TotalBatteryPower.Value * 10), "Int32", 31009);
|
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 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 result7 = ConvertToModbusRegisters((status.InverterRecord.AvgBatteryTemp.Value * 100), "Int16", 31011);
|
||||||
var result19 = ConvertToModbusRegisters((status.InverterRecord1.GridPower * 10), "Int32", 33000);
|
var result20 = ConvertToModbusRegisters((status.InverterRecord.MaxChargeCurrent.Value * 10), "UInt16", 31012);
|
||||||
var result23 = ConvertToModbusRegisters((status.InverterRecord1.GridVoltageFrequency * 10), "UInt16", 33002);
|
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 result24 = ConvertToModbusRegisters((status.InverterRecord1.WorkingMode), "UInt16", 34000);
|
var result18 = ConvertToModbusRegisters((status.InverterRecord.TotalPhotovoltaicPower.Value * 10), "UInt32", 32000);
|
||||||
var result25 = ConvertToModbusRegisters((status.InverterRecord1.InverterActivePower * 10), "Int32", 34001);
|
var result19 = ConvertToModbusRegisters((status.InverterRecord.TotalGridPower.Value * 10), "Int32", 33000);
|
||||||
var result29 = ConvertToModbusRegisters((status.InverterRecord1.EnableGridExport ), "UInt16", 34003);
|
var result23 = ConvertToModbusRegisters((status.InverterRecord.GridFrequency.Value * 10), "UInt16", 33002);
|
||||||
var result27 = ConvertToModbusRegisters((status.InverterRecord1.PowerGridExportLimit ), "Int16", 34004);
|
|
||||||
|
|
||||||
|
var result24 = ConvertToModbusRegisters((status.InverterRecord.OperatingPriority), "UInt16", 34000);
|
||||||
|
var result25 = ConvertToModbusRegisters((status.InverterRecord.InverterPower.Value * 10), "Int32", 34001);
|
||||||
|
var result29 = ConvertToModbusRegisters((status.InverterRecord.EnableGridExport ), "UInt16", 35002);
|
||||||
|
var result27 = ConvertToModbusRegisters((status.InverterRecord.GridExportPower.Value ), "Int16", 35003);
|
||||||
// Merge all results into one dictionary
|
// Merge all results into one dictionary
|
||||||
var allResults = new[]
|
var allResults = new[]
|
||||||
{
|
{
|
||||||
result1, result2, result3, result4, result5, result23, result24, result25, result29, result27, result26, result7, result8, result9, result16, result20, result12, result13, result14, result15, result18, result19, result21, result22
|
result1, result2, result3, result4, result5, result23, result24, result25, result29, result27, result26, result7, result8, result9, result16, result20, result12, result13, result14, result15, result18, result19
|
||||||
|
, result55
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var result in allResults)
|
foreach (var result in allResults)
|
||||||
|
|
@ -582,10 +691,15 @@ internal static class Program
|
||||||
|
|
||||||
// Console.WriteLine("JSON file written successfully.");
|
// Console.WriteLine("JSON file written successfully.");
|
||||||
// Console.WriteLine(json);
|
// Console.WriteLine(json);
|
||||||
var stopTime = DateTime.Now;
|
|
||||||
Console.WriteLine(stopTime.ToString("HH:mm:ss.fff" )+ " Finish the loop");
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private static Dictionary<string, ushort> ConvertToModbusRegisters(object value, string outputType, int startingAddress)
|
private static Dictionary<string, ushort> ConvertToModbusRegisters(object value, string outputType, int startingAddress)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using InnovEnergy.App.SinexcelCommunication.DataTypes;
|
||||||
using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType;
|
using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType;
|
||||||
using InnovEnergy.Lib.Utils;
|
using InnovEnergy.Lib.Utils;
|
||||||
using static System.Text.Json.JsonSerializer;
|
using static System.Text.Json.JsonSerializer;
|
||||||
|
|
@ -10,17 +11,21 @@ namespace InnovEnergy.App.SinexcelCommunication.SystemConfig;
|
||||||
[SuppressMessage("Trimming", "IL2026:Members annotated with \'RequiresUnreferencedCodeAttribute\' require dynamic access otherwise can break functionality when trimming application code")]
|
[SuppressMessage("Trimming", "IL2026:Members annotated with \'RequiresUnreferencedCodeAttribute\' require dynamic access otherwise can break functionality when trimming application code")]
|
||||||
public class Config
|
public class Config
|
||||||
{
|
{
|
||||||
|
|
||||||
private static String DefaultConfigFilePath => Path.Combine(Environment.CurrentDirectory, "config.json");
|
private static String DefaultConfigFilePath => Path.Combine(Environment.CurrentDirectory, "config.json");
|
||||||
private static DateTime DefaultDatetime => new(2025, 01, 01, 09, 00, 00);
|
private static DateTime DefaultDatetime => new(2025, 01, 01, 09, 00, 00);
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||||
|
|
||||||
|
public required DeviceConfig Devices { get; set; }
|
||||||
|
//public required Boolean DynamicPricingEnabled { get; set; }
|
||||||
|
//public required DynamicPricingMode DynamicPricingMode { get; set; }
|
||||||
|
//public required Decimal CheapPrice { get; set; }
|
||||||
|
//public required Decimal HighPrice { get; set; }
|
||||||
public required Double MinSoc { get; set; }
|
public required Double MinSoc { get; set; }
|
||||||
public required Double GridSetPoint { get; set; }
|
public required Double GridSetPoint { get; set; }
|
||||||
public required Double MaximumDischargingCurrent { get; set; }
|
public required Double MaximumDischargingCurrent { get; set; }
|
||||||
public required Double MaximumChargingCurrent { get; set; }
|
public required Double MaximumChargingCurrent { get; set; }
|
||||||
public required WorkingMode OperatingPriority { get; set; }
|
public required OperatingPriority OperatingPriority { get; set; }
|
||||||
public required Int16 BatteriesCount { get; set; }
|
public required Int16 BatteriesCount { get; set; }
|
||||||
public required Int16 ClusterNumber { get; set; }
|
public required Int16 ClusterNumber { get; set; }
|
||||||
public required Int16 PvNumber { get; set; }
|
public required Int16 PvNumber { get; set; }
|
||||||
|
|
@ -35,14 +40,25 @@ public class Config
|
||||||
public required S3Config? S3 { get; set; }
|
public required S3Config? S3 { get; set; }
|
||||||
|
|
||||||
private static String? LastSavedData { get; set; }
|
private static String? LastSavedData { get; set; }
|
||||||
|
private static DateTime? LoadedWriteTimeUtc { get; set; }
|
||||||
|
|
||||||
public static Config Default => new()
|
public static Config Default => new()
|
||||||
{
|
{
|
||||||
|
Devices = new ()
|
||||||
|
{
|
||||||
|
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},
|
||||||
|
},
|
||||||
|
//DynamicPricingEnabled = false,
|
||||||
|
//DynamicPricingMode = DynamicPricingMode.Disabled,
|
||||||
MinSoc = 20,
|
MinSoc = 20,
|
||||||
GridSetPoint = 0,
|
GridSetPoint = 0,
|
||||||
MaximumChargingCurrent = 180,
|
MaximumChargingCurrent = 180,
|
||||||
MaximumDischargingCurrent = 180,
|
MaximumDischargingCurrent = 180,
|
||||||
OperatingPriority = WorkingMode.TimeChargeDischarge,
|
OperatingPriority = OperatingPriority.LoadPriority,
|
||||||
BatteriesCount = 0,
|
BatteriesCount = 0,
|
||||||
ClusterNumber = 0,
|
ClusterNumber = 0,
|
||||||
PvNumber = 0,
|
PvNumber = 0,
|
||||||
|
|
@ -67,6 +83,10 @@ public class Config
|
||||||
{
|
{
|
||||||
var configFilePath = path ?? DefaultConfigFilePath;
|
var configFilePath = path ?? DefaultConfigFilePath;
|
||||||
|
|
||||||
|
var currentWriteTime = File.GetLastWriteTimeUtc(configFilePath);
|
||||||
|
if (currentWriteTime != LoadedWriteTimeUtc)
|
||||||
|
throw new IOException("Config file changed on disk since it was loaded; refusing to overwrite."); // to prevent an overwriting while an external changes happended in the meantime
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var jsonString = Serialize(this, JsonOptions);
|
var jsonString = Serialize(this, JsonOptions);
|
||||||
|
|
@ -84,13 +104,15 @@ public class Config
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
public static Config Load(String? path = null)
|
public static Config Load(String? path = null)
|
||||||
{
|
{
|
||||||
var configFilePath = path ?? DefaultConfigFilePath;
|
var configFilePath = path ?? DefaultConfigFilePath;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var jsonString = File.ReadAllText(configFilePath);
|
var jsonString = File.ReadAllText(configFilePath);
|
||||||
|
// LoadedWriteTimeUtc = File.GetLastWriteTimeUtc(configFilePath);
|
||||||
|
|
||||||
return Deserialize<Config>(jsonString)!;
|
return Deserialize<Config>(jsonString)!;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|
@ -98,6 +120,31 @@ public class Config
|
||||||
$"Failed to read config file {configFilePath}, using default config\n{e}".WriteLine();
|
$"Failed to read config file {configFilePath}, using default config\n{e}".WriteLine();
|
||||||
return Default;
|
return Default;
|
||||||
}
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
public static Config Load(string? path = null)
|
||||||
|
{
|
||||||
|
var configFilePath = path ?? DefaultConfigFilePath; // ✅ handle null first
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Now safe to call any File/Path API
|
||||||
|
var json = File.ReadAllText(configFilePath);
|
||||||
|
|
||||||
|
var cfg = Deserialize<Config>(json, JsonOptions)
|
||||||
|
?? throw new InvalidOperationException("Config deserialized to null.");
|
||||||
|
|
||||||
|
// Optional: store last write time / last saved json
|
||||||
|
LoadedWriteTimeUtc = File.GetLastWriteTimeUtc(configFilePath);
|
||||||
|
LastSavedData = Serialize(cfg, JsonOptions); // if you use the save-skip logic
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
$"Failed to read config file {configFilePath}\n{e}".WriteLine();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<Config> LoadAsync(String? path = null)
|
public static async Task<Config> LoadAsync(String? path = null)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
namespace InnovEnergy.App.SinexcelCommunication.SystemConfig;
|
||||||
|
|
||||||
|
public record DeviceConfig
|
||||||
|
{
|
||||||
|
public required SerialLineConfig Serial { get; init; }
|
||||||
|
|
||||||
|
public required SodiDevice Inverter1 { get; init; }
|
||||||
|
public required SodiDevice Inverter2 { get; init; }
|
||||||
|
public required SodiDevice Inverter3 { get; init; }
|
||||||
|
public required SodiDevice Inverter4 { get; init; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace InnovEnergy.App.SinexcelCommunication.SystemConfig;
|
||||||
|
|
||||||
|
public enum DeviceState
|
||||||
|
{
|
||||||
|
Disabled,
|
||||||
|
Measured,
|
||||||
|
Computed
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System.IO.Ports;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SinexcelCommunication.SystemConfig;
|
||||||
|
|
||||||
|
public sealed class SerialLineConfig
|
||||||
|
{
|
||||||
|
public required Int32 BaudRate { get; init; } = 115200;
|
||||||
|
public required Parity Parity { get; init; } = 0; //none
|
||||||
|
public required Int32 DataBits { get; init; } = 8;
|
||||||
|
public required Int32 StopBits { get; init; } = 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace InnovEnergy.App.SinexcelCommunication.SystemConfig;
|
||||||
|
|
||||||
|
public class SodiDevice
|
||||||
|
{
|
||||||
|
public required DeviceState DeviceState { get; init; }
|
||||||
|
public required String Port { get; init; }
|
||||||
|
public required Byte SlaveId { get; init; }
|
||||||
|
}
|
||||||
|
|
@ -6,12 +6,14 @@ username='inesco'
|
||||||
root_password='Sodistore0918425'
|
root_password='Sodistore0918425'
|
||||||
|
|
||||||
release_flag_file="./bin/Release/$dotnet_version/linux-arm64/publish/.release.flag"
|
release_flag_file="./bin/Release/$dotnet_version/linux-arm64/publish/.release.flag"
|
||||||
|
DOTNET="/snap/dotnet-sdk_60/current/dotnet"
|
||||||
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo -e "\n============================ Build ============================\n"
|
echo -e "\n============================ Build ============================\n"
|
||||||
|
|
||||||
dotnet publish \
|
"$DOTNET" publish \
|
||||||
./SinexcelCommunication.csproj \
|
./SinexcelCommunication.csproj \
|
||||||
-p:PublishTrimmed=false \
|
-p:PublishTrimmed=false \
|
||||||
-c Release \
|
-c Release \
|
||||||
|
|
|
||||||
|
|
@ -50,3 +50,9 @@ public enum SinexcelMachineMode
|
||||||
Single = 0, // Default
|
Single = 0, // Default
|
||||||
Parallel = 1
|
Parallel = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum EnablePowerLimitation
|
||||||
|
{
|
||||||
|
Prohibited = 0, // Default
|
||||||
|
Enable = 1
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.ComponentModel;
|
||||||
using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType;
|
using InnovEnergy.Lib.Devices.Sinexcel_12K_TL.DataType;
|
||||||
using InnovEnergy.Lib.Units;
|
using InnovEnergy.Lib.Units;
|
||||||
using InnovEnergy.Lib.Units.Power;
|
using InnovEnergy.Lib.Units.Power;
|
||||||
|
|
@ -193,7 +194,7 @@ public partial class SinexcelRecord
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private Int16 _factorFromKwtoW = 1000;
|
private readonly Int16 _factorFromKwtoW = 1000;
|
||||||
// ───────────────────────────────────────────────
|
// ───────────────────────────────────────────────
|
||||||
// Public API — Decoded Float Values
|
// Public API — Decoded Float Values
|
||||||
// ───────────────────────────────────────────────
|
// ───────────────────────────────────────────────
|
||||||
|
|
@ -295,11 +296,12 @@ public partial class SinexcelRecord
|
||||||
public UInt16 Minute => (UInt16) ConvertBitPatternToFloat(_minute);
|
public UInt16 Minute => (UInt16) ConvertBitPatternToFloat(_minute);
|
||||||
public UInt16 Second => (UInt16) ConvertBitPatternToFloat(_second);
|
public UInt16 Second => (UInt16) ConvertBitPatternToFloat(_second);
|
||||||
|
|
||||||
public DateTime SystemDateTime => new(Year, Month, Day, Hour, Minute, Second);
|
// public DateTime SystemDateTime => new(Year, Month, Day, Hour, Minute, Second);
|
||||||
|
|
||||||
// ───────────────────────────────────────────────
|
// ───────────────────────────────────────────────
|
||||||
// Diesel Generator Measurements
|
// Diesel Generator Measurements
|
||||||
// ───────────────────────────────────────────────
|
// ───────────────────────────────────────────────
|
||||||
|
/*
|
||||||
public Voltage DieselGenAPhaseVoltage => ConvertBitPatternToFloat(_dieselGenAPhaseVoltage);
|
public Voltage DieselGenAPhaseVoltage => ConvertBitPatternToFloat(_dieselGenAPhaseVoltage);
|
||||||
public Voltage DieselGenBPhaseVoltage => ConvertBitPatternToFloat(_dieselGenBPhaseVoltage);
|
public Voltage DieselGenBPhaseVoltage => ConvertBitPatternToFloat(_dieselGenBPhaseVoltage);
|
||||||
public Voltage DieselGenCPhaseVoltage => ConvertBitPatternToFloat(_dieselGenCPhaseVoltage);
|
public Voltage DieselGenCPhaseVoltage => ConvertBitPatternToFloat(_dieselGenCPhaseVoltage);
|
||||||
|
|
@ -324,7 +326,7 @@ public partial class SinexcelRecord
|
||||||
|
|
||||||
public ReactivePower DieselGenAPhaseReactivePower => ConvertBitPatternToFloat(_dieselGenAPhaseReactivePower) * _factorFromKwtoW;
|
public ReactivePower DieselGenAPhaseReactivePower => ConvertBitPatternToFloat(_dieselGenAPhaseReactivePower) * _factorFromKwtoW;
|
||||||
public ReactivePower DieselGenBPhaseReactivePower => ConvertBitPatternToFloat(_dieselGenBPhaseReactivePower) * _factorFromKwtoW;
|
public ReactivePower DieselGenBPhaseReactivePower => ConvertBitPatternToFloat(_dieselGenBPhaseReactivePower) * _factorFromKwtoW;
|
||||||
public ReactivePower DieselGenCPhaseReactivePower => ConvertBitPatternToFloat(_dieselGenCPhaseReactivePower) * _factorFromKwtoW;
|
public ReactivePower DieselGenCPhaseReactivePower => ConvertBitPatternToFloat(_dieselGenCPhaseReactivePower) * _factorFromKwtoW;*/
|
||||||
|
|
||||||
// ───────────────────────────────────────────────
|
// ───────────────────────────────────────────────
|
||||||
// Photovoltaic and Battery Measurements
|
// Photovoltaic and Battery Measurements
|
||||||
|
|
@ -645,14 +647,14 @@ public partial class SinexcelRecord
|
||||||
set => _battery2BackupSOC = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0);
|
set => _battery2BackupSOC = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0);
|
||||||
}
|
}
|
||||||
// to be tested
|
// to be tested
|
||||||
public float EnableGridExport
|
public EnablePowerLimitation EnableGridExport
|
||||||
|
|
||||||
{
|
{
|
||||||
get => BitConverter.Int32BitsToSingle(unchecked((int)_enableGridExport));
|
get => (EnablePowerLimitation)ConvertBitPatternToFloat(_enableGridExport); //(Boolean)BitConverter.Int32BitsToSingle(unchecked((int)_enableGridExport));
|
||||||
set => _enableGridExport = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0);
|
set => _enableGridExport = (UInt32)value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public float PowerGridExportLimit
|
public ActivePower PowerGridExportLimit
|
||||||
{
|
{
|
||||||
get => BitConverter.Int32BitsToSingle(unchecked((int)_powerGridExportLimit));
|
get => BitConverter.Int32BitsToSingle(unchecked((int)_powerGridExportLimit));
|
||||||
set => _powerGridExportLimit = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0);
|
set => _powerGridExportLimit = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0);
|
||||||
|
|
@ -813,6 +815,7 @@ public partial class SinexcelRecord
|
||||||
public Percent Battery1SocSecondvalue => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab1Soc)); // 0xB106 %
|
public Percent Battery1SocSecondvalue => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab1Soc)); // 0xB106 %
|
||||||
public Percent Battery1Soh => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab1Soh)); // 0xB108 %
|
public Percent Battery1Soh => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab1Soh)); // 0xB108 %
|
||||||
|
|
||||||
|
|
||||||
// Energy (kW·h)
|
// Energy (kW·h)
|
||||||
public float Battery2TotalChargingEnergy => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2TotalChargingEnergy)); // 0xB1FC
|
public float Battery2TotalChargingEnergy => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2TotalChargingEnergy)); // 0xB1FC
|
||||||
public float Battery2TotalDischargedEnergy => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2TotalDischargedEnergy)); // 0xB1FE
|
public float Battery2TotalDischargedEnergy => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2TotalDischargedEnergy)); // 0xB1FE
|
||||||
|
|
@ -820,6 +823,7 @@ public partial class SinexcelRecord
|
||||||
// Pack Voltage / Current / Temperature
|
// Pack Voltage / Current / Temperature
|
||||||
public Voltage Battery2PackTotalVoltage => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2PackTotalVoltage)); // 0xB200 (0.01 V resolution)
|
public Voltage Battery2PackTotalVoltage => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2PackTotalVoltage)); // 0xB200 (0.01 V resolution)
|
||||||
public Current Battery2PackTotalCurrent => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2PackTotalCurrent)); // 0xB202 (0.01 A resolution)
|
public Current Battery2PackTotalCurrent => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2PackTotalCurrent)); // 0xB202 (0.01 A resolution)
|
||||||
|
public Temperature Battery2Temperature => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2Temperature)); // 0xB104 (0.01 °C resolution per spec)
|
||||||
public Percent Battery2Socsecondvalue => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2Soc)); // 0xB206 %
|
public Percent Battery2Socsecondvalue => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2Soc)); // 0xB206 %
|
||||||
public Percent Battery2Soh => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2Soh)); // 0xB208 %
|
public Percent Battery2Soh => BitConverter.Int32BitsToSingle(unchecked((Int32)_batteryCab2Soh)); // 0xB208 %
|
||||||
|
|
||||||
|
|
@ -931,4 +935,8 @@ public partial class SinexcelRecord
|
||||||
return BitConverter.ToSingle(bytes, 0);
|
return BitConverter.ToSingle(bytes, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static UInt32 ConvertFloatToBitPattern(float value)
|
||||||
|
{
|
||||||
|
return BitConverter.ToUInt32(BitConverter.GetBytes(value), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -290,16 +290,17 @@ public partial class SinexcelRecord
|
||||||
// ───────────────────────────────────────────────
|
// ───────────────────────────────────────────────
|
||||||
// Date / Time Information
|
// Date / Time Information
|
||||||
// ───────────────────────────────────────────────
|
// ───────────────────────────────────────────────
|
||||||
[HoldingRegister<UInt32>(4338)] private UInt32 _year; // 0x10F2
|
[HoldingRegister<UInt32>(4338/*, writable: true*/)] private UInt32 _year; // 0x10F2
|
||||||
[HoldingRegister<UInt32>(4340)] private UInt32 _month; // 0x10F4
|
[HoldingRegister<UInt32>(4340/*, writable: true*/)] private UInt32 _month; // 0x10F4
|
||||||
[HoldingRegister<UInt32>(4342)] private UInt32 _day; // 0x10F6
|
[HoldingRegister<UInt32>(4342/*, writable: true*/)] private UInt32 _day; // 0x10F6
|
||||||
[HoldingRegister<UInt32>(4344)] private UInt32 _hour; // 0x10F8
|
[HoldingRegister<UInt32>(4344/*, writable: true*/)] private UInt32 _hour; // 0x10F8
|
||||||
[HoldingRegister<UInt32>(4346)] private UInt32 _minute; // 0x10FA
|
[HoldingRegister<UInt32>(4346/*, writable: true*/)] private UInt32 _minute; // 0x10FA
|
||||||
[HoldingRegister<UInt32>(4348)] private UInt32 _second; // 0x10FC
|
[HoldingRegister<UInt32>(4348/*, writable: true*/)] private UInt32 _second; // 0x10FC
|
||||||
|
|
||||||
// ───────────────────────────────────────────────
|
// ───────────────────────────────────────────────
|
||||||
// Diesel Generator Measurements
|
// Diesel Generator Measurements
|
||||||
// ───────────────────────────────────────────────
|
// ───────────────────────────────────────────────
|
||||||
|
/*
|
||||||
[HoldingRegister<UInt32>(4362)] private UInt32 _dieselGenAPhaseVoltage; // 0x110A
|
[HoldingRegister<UInt32>(4362)] private UInt32 _dieselGenAPhaseVoltage; // 0x110A
|
||||||
[HoldingRegister<UInt32>(4364)] private UInt32 _dieselGenBPhaseVoltage; // 0x110C
|
[HoldingRegister<UInt32>(4364)] private UInt32 _dieselGenBPhaseVoltage; // 0x110C
|
||||||
[HoldingRegister<UInt32>(4366)] private UInt32 _dieselGenCPhaseVoltage; // 0x110E
|
[HoldingRegister<UInt32>(4366)] private UInt32 _dieselGenCPhaseVoltage; // 0x110E
|
||||||
|
|
@ -318,8 +319,8 @@ public partial class SinexcelRecord
|
||||||
[HoldingRegister<UInt32>(4410)] private UInt32 _dieselGenBPhaseActivePower; // 0x113A
|
[HoldingRegister<UInt32>(4410)] private UInt32 _dieselGenBPhaseActivePower; // 0x113A
|
||||||
[HoldingRegister<UInt32>(4412)] private UInt32 _dieselGenCPhaseActivePower; // 0x113C
|
[HoldingRegister<UInt32>(4412)] private UInt32 _dieselGenCPhaseActivePower; // 0x113C
|
||||||
[HoldingRegister<UInt32>(4414)] private UInt32 _dieselGenAPhaseReactivePower; // 0x113E
|
[HoldingRegister<UInt32>(4414)] private UInt32 _dieselGenAPhaseReactivePower; // 0x113E
|
||||||
[HoldingRegister<UInt32>(4416)] private UInt32 _dieselGenBPhaseReactivePower; // 0x1140
|
[HoldingRegister<UInt32>(4416)] private UInt32 _dieselGenBPhaseReactivePower; // 0x1140*
|
||||||
[HoldingRegister<UInt32>(4418)] private UInt32 _dieselGenCPhaseReactivePower; // 0x1142
|
[HoldingRegister<UInt32>(4418)] private UInt32 _dieselGenCPhaseReactivePower; // 0x1142*/
|
||||||
|
|
||||||
// ───────────────────────────────────────────────
|
// ───────────────────────────────────────────────
|
||||||
// Photovoltaic and Battery Measurements
|
// Photovoltaic and Battery Measurements
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue