Salimax V2
This commit is contained in:
parent
43e628502c
commit
eeaefb0f54
|
@ -102,7 +102,7 @@ public class BmsTunnel : IDisposable
|
|||
{
|
||||
// TODO: this should go into outer loop instead of returning magic value CrcError
|
||||
|
||||
Console.WriteLine(BitConverter.ToString(response).Replace("-", " "));
|
||||
//Console.WriteLine(BitConverter.ToString(response).Replace("-", " "));
|
||||
return CrcError;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,24 +6,24 @@
|
|||
<Import Project="../InnovEnergy.App.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../Lib/Devices/Adam6060/Adam6060.csproj" />
|
||||
<ProjectReference Include="../../Lib/Devices/AMPT/Ampt.csproj" />
|
||||
<ProjectReference Include="../../Lib/Devices/Battery48TL/Battery48TL.csproj" />
|
||||
<ProjectReference Include="../../Lib/Devices/EmuMeter/EmuMeter.csproj" />
|
||||
<ProjectReference Include="../../Lib/Devices/Trumpf/TruConvertAc/TruConvertAc.csproj" />
|
||||
<ProjectReference Include="../../Lib/Devices/Trumpf/TruConvertDc/TruConvertDc.csproj" />
|
||||
<ProjectReference Include="../../Lib/Devices/Trumpf/TruConvert/TruConvert.csproj" />
|
||||
<ProjectReference Include="../../Lib/StatusApi/StatusApi.csproj" />
|
||||
<ProjectReference Include="../../Lib/Utils/Utils.csproj" />
|
||||
<ProjectReference Include="../../Lib/Time/Time.csproj" />
|
||||
<PackageReference Version="3.6.0" Include="CliWrap" />
|
||||
<PackageReference Version="3.2.4" Include="Flurl.Http" />
|
||||
<PackageReference Version="7.0.0" Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Version="7.0.0" Include="System.IO.Ports" />
|
||||
<PackageReference Version="13.0.3" Include="Newtonsoft.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CliWrap" Version="3.6.0" />
|
||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||
<PackageReference Include="System.IO.Ports" Version="7.0.0" />
|
||||
<PackageReference Include="DecimalMath.DecimalEx" Version="1.0.2" />
|
||||
<ProjectReference Include="../../Lib/Devices/Adam6360D/Adam6360D.csproj" />
|
||||
<ProjectReference Include="../../Lib/Devices/AMPT/Ampt.csproj" />
|
||||
<ProjectReference Include="../../Lib/Devices/Battery48TL/Battery48TL.csproj" />
|
||||
<ProjectReference Include="../../Lib/Devices/EmuMeter/EmuMeter.csproj" />
|
||||
<ProjectReference Include="../../Lib/Devices/Trumpf/SystemControl/SystemControl.csproj" />
|
||||
<ProjectReference Include="../../Lib/Devices/Trumpf/TruConvertAc/TruConvertAc.csproj" />
|
||||
<ProjectReference Include="../../Lib/Devices/Trumpf/TruConvertDc/TruConvertDc.csproj" />
|
||||
<ProjectReference Include="../../Lib/Time/Time.csproj" />
|
||||
<ProjectReference Include="../../Lib/Units/Units.csproj" />
|
||||
<ProjectReference Include="../../Lib/Utils/Utils.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
|
@ -1,37 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
|
||||
dotnet_version='net6.0'
|
||||
|
||||
set -e
|
||||
|
||||
echo -e "\n============================ Build ============================\n"
|
||||
|
||||
dotnet publish \
|
||||
./SaliMax.csproj \
|
||||
-c Release \
|
||||
-r linux-x64
|
||||
|
||||
echo -e "\n============================ Deploy ============================\n"
|
||||
|
||||
rsync -v \
|
||||
./bin/Release/$dotnet_version/linux-x64/publish/* \
|
||||
ie-entwicklung@10.2.3.49:~/salimax
|
||||
|
||||
|
||||
# debian@10.2.1.87:~/salimax
|
||||
|
||||
echo -e "\n============================ Restart Salimax sevice ============================\n"
|
||||
|
||||
ssh -tt \
|
||||
ie-entwicklung@10.2.3.49 \
|
||||
sudo systemctl restart salimax.service
|
||||
|
||||
|
||||
echo -e "\n============================ Print service output ============================\n"
|
||||
|
||||
ssh -tt \
|
||||
ie-entwicklung@10.2.3.49 \
|
||||
journalctl -f -u salimax.service
|
||||
|
||||
|
|
@ -1,79 +1,77 @@
|
|||
using InnovEnergy.Lib.Units;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
|
||||
namespace InnovEnergy.App.SaliMax;
|
||||
|
||||
public static class AsciiArt
|
||||
{
|
||||
|
||||
public static String CreateBox(params Object[] elements)
|
||||
{
|
||||
var aligned = elements
|
||||
.Select(e => e.ToString()!)
|
||||
.JoinLines()
|
||||
.AlignLeft();
|
||||
|
||||
var w = aligned.Width();
|
||||
|
||||
var line = "".PadRight(w + 2, '─');
|
||||
var top = "┌" + line + "┐";
|
||||
var bottom = "└" + line + "┘";
|
||||
|
||||
return aligned
|
||||
.SplitLines()
|
||||
.Select(l => l.SurroundWith(" "))
|
||||
.Select(l => l.SurroundWith("│"))
|
||||
.Prepend(top)
|
||||
.Append(bottom)
|
||||
.JoinLines();
|
||||
}
|
||||
|
||||
public static String CreateHorizontalArrow(Decimal value, String separator)
|
||||
{
|
||||
var valueToString = " " + value.W();
|
||||
|
||||
if (value == 0)
|
||||
{
|
||||
valueToString = "";
|
||||
}
|
||||
|
||||
var contentWidth = separator.Length;
|
||||
|
||||
var horizontal = "".PadRight(contentWidth, ' ');
|
||||
|
||||
var v = valueToString.PadRight(contentWidth);
|
||||
var s = separator.PadRight(contentWidth);
|
||||
|
||||
return StringUtils.JoinLines(
|
||||
horizontal,
|
||||
v,
|
||||
s,
|
||||
horizontal
|
||||
);
|
||||
}
|
||||
|
||||
public static String CreateTransitionPadLeft(String value, String separator)
|
||||
{
|
||||
var contentWidth = separator.Length + 2;
|
||||
|
||||
var horizontal = "".PadLeft(contentWidth, ' ');
|
||||
|
||||
var v = value.PadLeft(contentWidth);
|
||||
var s = separator.PadLeft(contentWidth);
|
||||
|
||||
return StringUtils.JoinLines(
|
||||
horizontal,
|
||||
v,
|
||||
s,
|
||||
horizontal
|
||||
);
|
||||
}
|
||||
|
||||
public static String CreateVerticalArrow(Decimal power, Int32 width = 0)
|
||||
{
|
||||
var flow = "V".NewLine() + "V".NewLine() + power.W().NewLine() + "V".NewLine() + "V";
|
||||
|
||||
return flow.AlignCenterHorizontal(width);
|
||||
}
|
||||
|
||||
}
|
||||
// using InnovEnergy.Lib.Utils;
|
||||
//
|
||||
// namespace InnovEnergy.App.SaliMax;
|
||||
//
|
||||
// public static class AsciiArt
|
||||
// {
|
||||
//
|
||||
// public static String CreateBox(params Object[] elements)
|
||||
// {
|
||||
// var aligned = elements
|
||||
// .Select(e => e.ToString()!)
|
||||
// .JoinLines()
|
||||
// .AlignLeft();
|
||||
//
|
||||
// var w = aligned.Width();
|
||||
//
|
||||
// var line = "".PadRight(w + 2, '─');
|
||||
// var top = "┌" + line + "┐";
|
||||
// var bottom = "└" + line + "┘";
|
||||
//
|
||||
// return aligned
|
||||
// .SplitLines()
|
||||
// .Select(l => l.SurroundWith(" "))
|
||||
// .Select(l => l.SurroundWith("│"))
|
||||
// .Prepend(top)
|
||||
// .Append(bottom)
|
||||
// .JoinLines();
|
||||
// }
|
||||
//
|
||||
// public static String CreateHorizontalArrow(Decimal value, String separator)
|
||||
// {
|
||||
// var valueToString = " " + value.W();
|
||||
//
|
||||
// var contentWidth = separator.Length;
|
||||
//
|
||||
// var horizontal = "".PadRight(contentWidth, ' ');
|
||||
//
|
||||
// var v = valueToString.PadRight(contentWidth);
|
||||
// var s = separator.PadRight(contentWidth);
|
||||
//
|
||||
// return StringUtils.JoinLines(
|
||||
// horizontal,
|
||||
// horizontal,
|
||||
// horizontal,
|
||||
// v,
|
||||
// s,
|
||||
// horizontal,
|
||||
// horizontal,
|
||||
// horizontal
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// public static String CreateTransitionPadLeft(String value, String separator)
|
||||
// {
|
||||
// var contentWidth = separator.Length + 2;
|
||||
//
|
||||
// var horizontal = "".PadLeft(contentWidth, ' ');
|
||||
//
|
||||
// var v = value.PadLeft(contentWidth);
|
||||
// var s = separator.PadLeft(contentWidth);
|
||||
//
|
||||
// return StringUtils.JoinLines(
|
||||
// horizontal,
|
||||
// v,
|
||||
// s,
|
||||
// horizontal
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// public static String CreateVerticalArrow(Decimal power, Int32 width = 0)
|
||||
// {
|
||||
// var flow = "V".NewLine() + "V".NewLine() + power.W().NewLine() + "V".NewLine() + "V";
|
||||
//
|
||||
// return flow.AlignCenterHorizontal(width);
|
||||
// }
|
||||
//
|
||||
// }
|
|
@ -1,56 +0,0 @@
|
|||
using InnovEnergy.Lib.Devices.Battery48TL;
|
||||
using InnovEnergy.Lib.StatusApi;
|
||||
using InnovEnergy.Lib.Units;
|
||||
using InnovEnergy.Lib.Units.Composite;
|
||||
using static InnovEnergy.Lib.Devices.Battery48TL.TemperatureState;
|
||||
|
||||
namespace InnovEnergy.App.SaliMax.Controller;
|
||||
|
||||
public static class AvgBatteriesStatus
|
||||
{
|
||||
public static CombinedStatus<Battery48TLStatus>? Combine(this IReadOnlyList<Battery48TLStatus> stati)
|
||||
{
|
||||
var combined = stati.Count == 0
|
||||
? null
|
||||
: new Battery48TLStatus
|
||||
{
|
||||
Soc = stati.Min(b => b.Soc),
|
||||
Temperature = stati.Average(b => b.Temperature),
|
||||
Dc = new DcBus
|
||||
{
|
||||
Voltage = stati.Average(b => b.Dc.Voltage),
|
||||
Current = stati.Sum(b => b.Dc.Current),
|
||||
},
|
||||
|
||||
Alarms = stati.SelectMany(b => b.Alarms).Distinct().ToList(),
|
||||
Warnings = stati.SelectMany(b => b.Warnings).Distinct().ToList(),
|
||||
|
||||
MaxChargingPower = stati.Sum(b => b.MaxChargingPower),
|
||||
MaxDischargingPower = stati.Sum(b => b.MaxDischargingPower),
|
||||
|
||||
Heating = stati.Any(b => b.Heating),
|
||||
|
||||
AmberLed = LedState.Off, // not used for combined battery
|
||||
BlueLed = LedState.Off,
|
||||
RedLed = LedState.Off,
|
||||
GreenLed = LedState.Off,
|
||||
|
||||
CellsVoltage = stati.Average(b => b.CellsVoltage),
|
||||
ConnectedToDc = stati.Any(b => b.ConnectedToDc),
|
||||
|
||||
TemperatureState = stati.Any(b => b.TemperatureState == OperatingTemperature) // TODO: revisit when we have the overheated state
|
||||
? OperatingTemperature
|
||||
: Cold,
|
||||
|
||||
TotalCurrent = stati.Average(b => b.TotalCurrent),
|
||||
|
||||
EocReached = stati.All(b => b.EocReached),
|
||||
};
|
||||
|
||||
return new CombinedStatus<Battery48TLStatus>
|
||||
{
|
||||
Combined = combined!,
|
||||
Children = stati
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
namespace InnovEnergy.App.SaliMax.Controller;
|
||||
|
||||
public static class Control
|
||||
{
|
||||
|
||||
public static Decimal ControlGridPower(this StatusRecord status, Decimal targetPower)
|
||||
{
|
||||
return ControlPower(status.GridMeterStatus!.Ac.ActivePower, targetPower, status.SalimaxConfig!.PConstant);
|
||||
}
|
||||
|
||||
public static Decimal ControlInverterPower(this StatusRecord status, Decimal targetInverterPower)
|
||||
{
|
||||
var s = status.InverterStatus!;
|
||||
var totalInverterAcPower = s.Ac.ActivePower;
|
||||
|
||||
return ControlPower(totalInverterAcPower, targetInverterPower,status.SalimaxConfig!.PConstant);
|
||||
}
|
||||
|
||||
public static Decimal ControlBatteryPower(this StatusRecord status, Decimal targetBatteryPower, UInt16 i = 0) //this will use the avg batteries
|
||||
{
|
||||
return ControlPower(status.BatteriesStatus!.Combined.Dc.Power, targetBatteryPower, status.SalimaxConfig!.PConstant);
|
||||
}
|
||||
|
||||
public static Decimal ControlLowBatterySoc(this StatusRecord status)
|
||||
{
|
||||
return ControlBatteryPower(status, HoldMinSocCurve(status));
|
||||
}
|
||||
|
||||
public static Decimal LowerLimit(params Decimal[] deltas) => deltas.Max();
|
||||
public static Decimal UpperLimit(params Decimal[] deltas) => deltas.Min();
|
||||
|
||||
private static Decimal HoldMinSocCurve(StatusRecord s)
|
||||
{
|
||||
// TODO: explain LowSOC curve
|
||||
|
||||
var a = -2 * s.SalimaxConfig!.SelfDischargePower / s.SalimaxConfig.HoldSocZone;
|
||||
var b = -a * (s.SalimaxConfig.MinSoc + s.SalimaxConfig.HoldSocZone);
|
||||
|
||||
return s.BatteriesStatus!.Combined.Soc * a + b; //this will use the avg batteries
|
||||
}
|
||||
|
||||
private static Decimal ControlPower(Decimal measurement, Decimal target, Decimal p)
|
||||
{
|
||||
var error = target - measurement;
|
||||
return error * p;
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||
using InnovEnergy.App.SaliMax.SystemConfig;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
||||
|
||||
namespace InnovEnergy.App.SaliMax.Controller;
|
||||
|
||||
public class ControlRecord
|
||||
{
|
||||
public TruConvertAcControl? AcControlRecord { get; init; }
|
||||
public TruConvertDcControl? DcControlRecord { get; init; }
|
||||
public SalimaxConfig? SalimaxConfig { get; init; } // we may have to create record of each
|
||||
public SaliMaxRelayStatus? SalimaxRelays { get; init; } // we may have to create record of each
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
namespace InnovEnergy.App.SaliMax.Controller;
|
||||
|
||||
public enum ControlTarget // TODO to delete
|
||||
{
|
||||
None = 0,
|
||||
GridAc = 1,
|
||||
BatteryDc = 2,
|
||||
InverterAc = 3,
|
||||
InverterDc = 4,
|
||||
}
|
|
@ -1,514 +0,0 @@
|
|||
using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||
using InnovEnergy.App.SaliMax.SystemConfig;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvert;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
||||
using InnovEnergy.Lib.Time.Unix;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc.Enums;
|
||||
|
||||
using static InnovEnergy.App.SaliMax.SaliMaxRelays.RelayState;
|
||||
|
||||
|
||||
namespace InnovEnergy.App.SaliMax.Controller;
|
||||
|
||||
public static class Controller
|
||||
{
|
||||
private static readonly UnixTimeSpan MaxTimeWithoutEoc = UnixTimeSpan.FromDays(7);
|
||||
|
||||
private static Boolean _mustChargeFlag = false;
|
||||
|
||||
private static readonly TimeSpan CommunicationTimeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
public static readonly Int16 MaxmimumAllowedBatteryTemp = 315;
|
||||
|
||||
private static UInt16 _numberOfInverters;
|
||||
|
||||
private static UInt16 GetSaliMaxState(StatusRecord statusRecord)
|
||||
{
|
||||
if (statusRecord.SaliMaxRelayStatus is null)
|
||||
throw new ArgumentNullException(nameof(SaliMaxRelayStatus) + " is not available"); //TODO
|
||||
|
||||
if (statusRecord.InverterStatus is null)
|
||||
throw new ArgumentNullException(nameof(statusRecord.InverterStatus) + " is not available"); //TODO
|
||||
|
||||
return statusRecord switch
|
||||
{
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Open,
|
||||
InverterStatus.MainState: not MainState.Operation
|
||||
} => 1,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Closed,
|
||||
InverterStatus.MainState: not MainState.Operation
|
||||
} => 2,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Open,
|
||||
InverterStatus.MainState: not MainState.Operation
|
||||
} => 3,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Closed,
|
||||
InverterStatus.MainState: not MainState.Operation
|
||||
} => 4,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Open,
|
||||
InverterStatus.MainState: not MainState.Operation
|
||||
} => 5,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Closed,
|
||||
InverterStatus.MainState: not MainState.Operation
|
||||
} => 6,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Open,
|
||||
InverterStatus.MainState: not MainState.Operation
|
||||
} => 7,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Closed,
|
||||
InverterStatus.MainState: not MainState.Operation
|
||||
} => 8,
|
||||
|
||||
//Grid-Tied 400V/50 Hz
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Open,
|
||||
InverterStatus.GridType: AcDcGridType.GridTied400V50Hz
|
||||
} => 9,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Open,
|
||||
InverterStatus.GridType: AcDcGridType.GridTied400V50Hz
|
||||
} => 10,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Open,
|
||||
InverterStatus.GridType: AcDcGridType.GridTied400V50Hz
|
||||
} => 11,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Open,
|
||||
InverterStatus.GridType: AcDcGridType.GridTied400V50Hz
|
||||
} => 12,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Closed,
|
||||
InverterStatus.GridType: AcDcGridType.GridTied400V50Hz
|
||||
} => 13,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Closed,
|
||||
InverterStatus.GridType: AcDcGridType.GridTied400V50Hz
|
||||
} => 14,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Closed,
|
||||
InverterStatus.GridType: AcDcGridType.GridTied400V50Hz
|
||||
} => 15,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Closed,
|
||||
InverterStatus.GridType: AcDcGridType.GridTied400V50Hz
|
||||
} => 16,
|
||||
|
||||
//Island 400V / 50Hz
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Open,
|
||||
InverterStatus.GridType: AcDcGridType.Island400V50Hz
|
||||
} => 17,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Open,
|
||||
InverterStatus.GridType: AcDcGridType.Island400V50Hz
|
||||
} => 18,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Open,
|
||||
InverterStatus.GridType: AcDcGridType.Island400V50Hz
|
||||
} => 19,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Open,
|
||||
InverterStatus.GridType: AcDcGridType.Island400V50Hz
|
||||
} => 20,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Closed, //this is wrong
|
||||
InverterStatus.GridType: AcDcGridType.Island400V50Hz
|
||||
} => 21,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Closed,
|
||||
InverterStatus.GridType: AcDcGridType.Island400V50Hz
|
||||
} => 22,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Closed,
|
||||
InverterStatus.GridType: AcDcGridType.Island400V50Hz
|
||||
} => 23,
|
||||
{
|
||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Closed,
|
||||
InverterStatus.GridType: AcDcGridType.Island400V50Hz
|
||||
} => 24,
|
||||
|
||||
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(statusRecord), statusRecord, null)
|
||||
};
|
||||
}
|
||||
|
||||
public static ControlRecord SaliMaxControl(StatusRecord statusRecord)
|
||||
{
|
||||
var currentSaliMaxState = GetSaliMaxState(statusRecord);
|
||||
|
||||
UInt16 acSlaveId = 1;
|
||||
var resetInverterAlarm = CheckInverterAlarms(statusRecord, currentSaliMaxState).WriteLine(" reset Alarm");
|
||||
var resetDcAlarm = CheckDcDcAlarms(statusRecord);
|
||||
|
||||
var lastEocTime = GetLastEocTime(statusRecord);
|
||||
var timeSinceLastEoc = UnixTime.Now - lastEocTime;
|
||||
|
||||
_numberOfInverters = (UInt16)statusRecord.InverterStatus!.NumberOfConnectedSlaves ;
|
||||
_mustChargeFlag = timeSinceLastEoc > MaxTimeWithoutEoc;
|
||||
|
||||
var noGridMeter = statusRecord.GridMeterStatus == null;
|
||||
var saliMaxConfig = statusRecord.SalimaxConfig with { LastEoc = lastEocTime };
|
||||
|
||||
ExplainState(currentSaliMaxState);
|
||||
|
||||
const RelayState k2Relay = Closed;
|
||||
|
||||
var acPowerStageEnable = StateConfig.AcPowerStageEnableStates.Contains(currentSaliMaxState); //this is logical incorrect, find better way
|
||||
|
||||
var dcPowerStageEnable = statusRecord.BatteriesStatus is not null; // TODO this is to check, Can be the batteries Status be null?
|
||||
|
||||
var salimaxRelay = statusRecord.SaliMaxRelayStatus! with { K2 = k2Relay }; // to check // this is must be control
|
||||
|
||||
if (resetInverterAlarm)
|
||||
{
|
||||
acPowerStageEnable = !resetInverterAlarm ;
|
||||
acSlaveId = 0;
|
||||
}
|
||||
|
||||
if (resetDcAlarm)
|
||||
{
|
||||
dcPowerStageEnable = !resetDcAlarm ;
|
||||
}
|
||||
|
||||
acSlaveId.WriteLine(" AcSlave @");
|
||||
|
||||
if (statusRecord.BatteriesStatus == null)
|
||||
{
|
||||
Console.WriteLine(" No batteries");
|
||||
return new ControlRecord
|
||||
{
|
||||
AcControlRecord = Defaults.TruConvertAcControl with
|
||||
{
|
||||
SignedPowerNominalValue = 0,
|
||||
PowerStageEnable = acPowerStageEnable,
|
||||
CommunicationTimeout = CommunicationTimeout,
|
||||
SlaveAddress = acSlaveId,
|
||||
ResetsAlarmAndWarning = resetInverterAlarm
|
||||
},
|
||||
DcControlRecord = Defaults.TruConvertDcControl with
|
||||
{
|
||||
PowerStageEnable = dcPowerStageEnable,
|
||||
ResetsAlarmAndWarning = resetDcAlarm,
|
||||
TimeoutForCommunication = CommunicationTimeout
|
||||
|
||||
},
|
||||
SalimaxConfig = saliMaxConfig, // must create a control of each
|
||||
SalimaxRelays = salimaxRelay, // must create a control of each
|
||||
};
|
||||
}
|
||||
|
||||
if (noGridMeter)
|
||||
{
|
||||
// Blackout ( no grid meter and K1 opened automatically
|
||||
if (statusRecord.SaliMaxRelayStatus?.K1 == Open)
|
||||
{
|
||||
Console.WriteLine("Blackout occured");
|
||||
//FromGridTieToIsland();
|
||||
}
|
||||
// Grid meter not detected ( broken)
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Grid meter not detected");
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
var newPowerSetPoint = CalculateNewPowerSetPoint(statusRecord);
|
||||
|
||||
////////////////////////// Control Record //////////////////////////
|
||||
|
||||
var acControlRecord = Defaults.TruConvertAcControl with
|
||||
{
|
||||
PowerStageEnable = acPowerStageEnable,
|
||||
CommunicationTimeout = CommunicationTimeout,
|
||||
SignedPowerNominalValue = newPowerSetPoint,
|
||||
SlaveAddress = acSlaveId,
|
||||
ResetsAlarmAndWarning = resetInverterAlarm
|
||||
};
|
||||
|
||||
var dcControlRecord = Defaults.TruConvertDcControl with
|
||||
{
|
||||
PowerStageEnable = dcPowerStageEnable,
|
||||
ResetsAlarmAndWarning = resetDcAlarm,
|
||||
TimeoutForCommunication = CommunicationTimeout
|
||||
};
|
||||
|
||||
|
||||
return new ControlRecord
|
||||
{
|
||||
AcControlRecord = acControlRecord,
|
||||
DcControlRecord = dcControlRecord,
|
||||
SalimaxConfig = saliMaxConfig,
|
||||
SalimaxRelays = salimaxRelay
|
||||
};
|
||||
}
|
||||
|
||||
private static Decimal CalculateNewPowerSetPoint(StatusRecord statusRecord)
|
||||
{
|
||||
var currentPowerSetPoint = statusRecord.InverterStatus!.AcSignedPowerValue;
|
||||
|
||||
var limitReason = "no limit";
|
||||
var goal = "no goal";
|
||||
var delta = 0m;
|
||||
|
||||
if (_mustChargeFlag)
|
||||
{
|
||||
goal = "Calibration Charge";
|
||||
delta = statusRecord.ControlInverterPower(statusRecord.SalimaxConfig.MaxInverterPower);
|
||||
}
|
||||
else if (statusRecord.BatteriesStatus!.Combined.Soc < statusRecord.SalimaxConfig.MinSoc) // TODO
|
||||
{
|
||||
goal = $"reach min SOC (Min soc: {statusRecord.SalimaxConfig.MinSoc})";
|
||||
delta = statusRecord.ControlInverterPower(statusRecord.SalimaxConfig
|
||||
.MaxInverterPower); // this the new mustChargeToMinSoc
|
||||
}
|
||||
else
|
||||
{
|
||||
goal = $"optimize self consumption (Grid set point: {statusRecord.SalimaxConfig.GridSetPoint})";
|
||||
delta = statusRecord.ControlGridPower(statusRecord.SalimaxConfig.GridSetPoint);
|
||||
}
|
||||
|
||||
////////////////////////// Upper Limits //////////////////////////
|
||||
|
||||
var inverterAc2DcLimitPower = statusRecord.ControlInverterPower(statusRecord.SalimaxConfig.MaxInverterPower);
|
||||
|
||||
if (delta > inverterAc2DcLimitPower)
|
||||
{
|
||||
limitReason = "limited by max inverter Ac to Dc power";
|
||||
delta = inverterAc2DcLimitPower;
|
||||
}
|
||||
|
||||
var batteryChargingLimitPower = statusRecord.ControlBatteryPower(statusRecord.BatteriesStatus!.Combined.MaxChargingPower);
|
||||
|
||||
if (delta > batteryChargingLimitPower)
|
||||
{
|
||||
limitReason = "limited by max battery charging power";
|
||||
delta = batteryChargingLimitPower;
|
||||
}
|
||||
|
||||
////////////////////////// Lower Limits //////////////////////////
|
||||
|
||||
var inverterDc2AcLimitPower = statusRecord.ControlInverterPower(-statusRecord.SalimaxConfig.MaxInverterPower);
|
||||
|
||||
if (delta < inverterDc2AcLimitPower)
|
||||
{
|
||||
limitReason = $"limited by max inverter Dc to Ac power: {-statusRecord.SalimaxConfig.MaxInverterPower}W";
|
||||
delta = inverterDc2AcLimitPower;
|
||||
}
|
||||
|
||||
var batteryDischargingLimitPower =
|
||||
statusRecord.ControlBatteryPower(statusRecord.BatteriesStatus.Combined.MaxDischargingPower); // TODO change to avg battery
|
||||
|
||||
if (delta < batteryDischargingLimitPower)
|
||||
{
|
||||
limitReason =
|
||||
$"limited by max battery discharging power: {statusRecord.BatteriesStatus.Combined.MaxDischargingPower}";// TODO change to avg battery
|
||||
|
||||
delta = batteryDischargingLimitPower;
|
||||
}
|
||||
|
||||
var keepMinSocLimitDelta = statusRecord.ControlLowBatterySoc();
|
||||
if (delta < keepMinSocLimitDelta)
|
||||
{
|
||||
limitReason =
|
||||
$"limiting discharging power in order to stay above min SOC: {statusRecord.SalimaxConfig.MinSoc}%";
|
||||
delta = keepMinSocLimitDelta;
|
||||
}
|
||||
|
||||
// if (statusRecord.BatteriesStatus[0]!.Temperature >= 300) //must not reduce the delta
|
||||
// {
|
||||
// var softLandingFactor = (MaxmimumAllowedBatteryTemp - statusRecord.BatteriesStatus[0]!.Temperature) / 15; //starting softlanding from 300 degree
|
||||
// limitReason =
|
||||
// $"limiting discharging power in order to stay keep the battery temp below 315°: {statusRecord.BatteriesStatus[0]!.Temperature}°" + " Softlanding factor: " + softLandingFactor;
|
||||
// delta *= softLandingFactor;
|
||||
// }
|
||||
|
||||
var newPowerSetPoint =
|
||||
DistributePower(currentPowerSetPoint + delta, statusRecord.SalimaxConfig.MaxInverterPower);
|
||||
|
||||
////////////////////// Print Data for Debug purpose //////////////////////////
|
||||
|
||||
//
|
||||
goal.WriteLine();
|
||||
limitReason.WriteLine(" Limit reason");
|
||||
delta.WriteLine(" Delta");
|
||||
// "============".WriteLine();
|
||||
return newPowerSetPoint;
|
||||
}
|
||||
|
||||
private static State TargetState(State currentState)
|
||||
{
|
||||
return currentState switch
|
||||
{
|
||||
State.State1 => State.State17,
|
||||
State.State17 => State.State21,
|
||||
State.State21 => State.State22,
|
||||
State.State22 => State.State6,
|
||||
State.State6 => State.State2,
|
||||
State.State2 => State.State4,
|
||||
State.State4 => State.State12,
|
||||
State.State12 => State.State16,
|
||||
State.State16 => State.State15,
|
||||
State.State15 => State.State13,
|
||||
State.State13 => State.State9,
|
||||
State.State9 => State.State1,
|
||||
_ => throw new Exception("Unknown State!") // maybe not throwing an exception, instead write on the log file
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static void ExplainState(UInt16 s)
|
||||
{
|
||||
Console.WriteLine("State: " + s);
|
||||
switch (s)
|
||||
{
|
||||
case 1:
|
||||
Console.WriteLine(" Inverter is Off");
|
||||
Console.WriteLine(" Turning on power stage of inverter");
|
||||
Console.WriteLine(" grid type = island 400V / 50Hz");
|
||||
break;
|
||||
case 17:
|
||||
Console.WriteLine(" Waiting for K3 to close");
|
||||
break;
|
||||
case 21:
|
||||
Console.WriteLine(" Inverter is in Island Mode");
|
||||
Console.WriteLine(" Waiting for K1 to close to leave it");
|
||||
break;
|
||||
case 22:
|
||||
Console.WriteLine(" K1 is closed");
|
||||
Console.WriteLine(" Turning off power stage of inverter");
|
||||
break;
|
||||
case 6:
|
||||
Console.WriteLine(" Waiting for K3 to open");
|
||||
break;
|
||||
case 2:
|
||||
Console.WriteLine(" K3 is open");
|
||||
Console.WriteLine(" Closing the K2");
|
||||
break;
|
||||
case 4:
|
||||
Console.WriteLine(" K2 is Closed");
|
||||
Console.WriteLine(" Turning on power stage of inverter");
|
||||
Console.WriteLine(" grid type = grid-tied 400V / 50Hz");
|
||||
break;
|
||||
case 12:
|
||||
Console.WriteLine(" Waiting for K3 to close");
|
||||
break;
|
||||
case 16:
|
||||
Console.WriteLine(" Inverter is in grid-tie");
|
||||
Console.WriteLine(" Waiting for K1 to open to leave it");
|
||||
break;
|
||||
case 15:
|
||||
Console.WriteLine(" K1 is open");
|
||||
Console.WriteLine(" Opening the K2");
|
||||
break;
|
||||
case 13:
|
||||
Console.WriteLine(" K2 is open");
|
||||
Console.WriteLine(" Waiting for K3 to open");
|
||||
break;
|
||||
case 9:
|
||||
Console.WriteLine(" K3 is open");
|
||||
Console.WriteLine(" Turning off power stage of inverter");
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine("Unknown State!");
|
||||
File.AppendAllTextAsync(Config.LogSalimaxLog, String.Join(Environment.NewLine, UnixTime.Now + "Unknown State!"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void WriteControlRecord(ControlRecord controlRecord,
|
||||
TruConvertAcDevice acDevice,
|
||||
TruConvertDcDevice dcDevice,
|
||||
SaliMaxRelaysDevice saliMaxRelaysDevice)
|
||||
{
|
||||
controlRecord.SalimaxConfig?.Save();
|
||||
|
||||
var acControlRecord = controlRecord.AcControlRecord;
|
||||
var dcControlRecord = controlRecord.DcControlRecord;
|
||||
|
||||
if (acControlRecord != null && dcControlRecord != null)
|
||||
{
|
||||
acDevice.WriteControl(acControlRecord);
|
||||
|
||||
dcDevice.WriteControl(dcControlRecord);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("AcControl and DcControl record is empty");
|
||||
File.AppendAllTextAsync(Config.LogSalimaxLog, String.Join(Environment.NewLine, UnixTime.Now + "AcControl and DcControl record is empty!"));
|
||||
}
|
||||
}
|
||||
|
||||
private static UnixTime GetLastEocTime(StatusRecord statusRecord)
|
||||
{ if (statusRecord.BatteriesStatus != null)
|
||||
{
|
||||
if (statusRecord.BatteriesStatus!.Combined.EocReached)
|
||||
{
|
||||
Console.WriteLine("battery has reached EOC");
|
||||
File.AppendAllTextAsync(Config.LogSalimaxLog,
|
||||
String.Join(Environment.NewLine,
|
||||
UnixTime.Now + "battery has reached EOC"));
|
||||
return UnixTime.Now;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("No battery Detected");
|
||||
}
|
||||
return statusRecord.SalimaxConfig.LastEoc;
|
||||
}
|
||||
|
||||
private static Decimal DistributePower(Decimal powerSetPoint, Int32 maximumPowerSetPoint)
|
||||
{
|
||||
var inverterPowerSetPoint = powerSetPoint / _numberOfInverters;
|
||||
return inverterPowerSetPoint.Clamp(-maximumPowerSetPoint, maximumPowerSetPoint);
|
||||
;
|
||||
}
|
||||
|
||||
private static Boolean CheckDcDcAlarms(StatusRecord s)
|
||||
{
|
||||
s.DcDcStatus?.Alarms.Count.WriteLine(" Dc Alarm count");
|
||||
if ( s.DcDcStatus?.Alarms.Count > 0 &&
|
||||
s.DcDcStatus?.PowerOperation == false)
|
||||
{
|
||||
File.AppendAllTextAsync(Config.LogSalimaxLog, UnixTime.Now + " " + s.DcDcStatus.Alarms);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Boolean CheckInverterAlarms(StatusRecord s, UInt16 state)
|
||||
{
|
||||
s.InverterStatus?.Alarms.Count.WriteLine(" Ac Alarm count");
|
||||
if ( s.InverterStatus?.Alarms.Count > 0 )
|
||||
{
|
||||
File.AppendAllTextAsync(Config.LogSalimaxLog, UnixTime.Now + " " + s.InverterStatus.Alarms[0]); // Todo write every alarm in he alarm list
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Boolean FromGridTieToIsland(StatusRecord s) //this is must be called when the K1 open
|
||||
{
|
||||
//check again the K1 is open
|
||||
//s.sal = true;
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//TODO: Include number of connected batteries
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
namespace InnovEnergy.App.SaliMax.Controller;
|
||||
|
||||
public struct SaliMaxState
|
||||
{
|
||||
private Int32 State { get; }
|
||||
|
||||
public SaliMaxState(Int32 state)
|
||||
{
|
||||
if (state < 1 || state >24)
|
||||
throw new ArgumentOutOfRangeException(nameof(state));
|
||||
|
||||
State = state;
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
|
||||
namespace InnovEnergy.App.SaliMax.Controller;
|
||||
|
||||
public enum State : Int16
|
||||
{
|
||||
State1 = 1,
|
||||
State2 = 2,
|
||||
State3 = 3,
|
||||
State4 = 4,
|
||||
State5 = 5,
|
||||
State6 = 6,
|
||||
State7 = 7,
|
||||
State8 = 8,
|
||||
State9 = 9,
|
||||
State10 = 10,
|
||||
State11 = 11,
|
||||
State12 = 12,
|
||||
State13 = 13,
|
||||
State14 = 14,
|
||||
State15 = 15,
|
||||
State16 = 16,
|
||||
State17 = 17,
|
||||
State18 = 18,
|
||||
State19 = 19,
|
||||
State20 = 20,
|
||||
State21 = 21,
|
||||
State22 = 22,
|
||||
State23 = 23,
|
||||
State24 = 24
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
|
||||
namespace InnovEnergy.App.SaliMax.Controller;
|
||||
|
||||
public static class StateConfig
|
||||
{
|
||||
public static readonly IReadOnlyList<Int32> AcPowerStageEnableStates = new[] {8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24 };
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||
using InnovEnergy.App.SaliMax.SystemConfig;
|
||||
using InnovEnergy.Lib.Devices.AMPT;
|
||||
using InnovEnergy.Lib.Devices.Battery48TL;
|
||||
using InnovEnergy.Lib.Devices.EmuMeter;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
||||
using InnovEnergy.Lib.StatusApi;
|
||||
|
||||
namespace InnovEnergy.App.SaliMax.Controller;
|
||||
|
||||
public record StatusRecord
|
||||
{
|
||||
public TruConvertAcStatus? InverterStatus { get; init; }
|
||||
public TruConvertDcStatus? DcDcStatus { get; init; }
|
||||
public CombinedStatus<Battery48TLStatus>? BatteriesStatus { get; init; }
|
||||
|
||||
public EmuMeterStatus? GridMeterStatus { get; init; }
|
||||
public SaliMaxRelayStatus? SaliMaxRelayStatus { get; init; }
|
||||
public AmptCommunicationUnitStatus? AmptStatus { get; init; }
|
||||
public EmuMeterStatus? AcInToAcOutMeterStatus { get; init; }
|
||||
public SalimaxConfig SalimaxConfig { get; init; } = null!;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
// using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||
// using InnovEnergy.App.SaliMax.SystemConfig;
|
||||
//
|
||||
// namespace InnovEnergy.App.SaliMax.Controller;
|
||||
//
|
||||
// public class ControlRecord
|
||||
// {
|
||||
// public TruConvertAcControl? AcControlRecord { get; init; }
|
||||
// public TruConvertDcControl? DcControlRecord { get; init; }
|
||||
// public Config? SalimaxConfig { get; init; } // we may have to create record of each
|
||||
// public SaliMaxRelayControl? SalimaxRelays { get; init; } // we may have to create record of each
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
|
@ -0,0 +1,242 @@
|
|||
using InnovEnergy.Lib.Devices.Battery48TL.DataTypes;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc.DataTypes;
|
||||
using InnovEnergy.Lib.Time.Unix;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
|
||||
|
||||
namespace InnovEnergy.App.SaliMax.Ess;
|
||||
|
||||
public static class Controller
|
||||
{
|
||||
private static readonly UnixTimeSpan MaxTimeWithoutEoc = UnixTimeSpan.FromDays(7); // TODO: move to config
|
||||
private static readonly TimeSpan CommunicationTimeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
|
||||
|
||||
|
||||
public static EssMode SelectControlMode(this StatusRecord s)
|
||||
{
|
||||
return EssMode.OptimizeSelfConsumption;
|
||||
|
||||
// return s.SystemState.Id != 16 ? EssMode.Off
|
||||
// : s.MustHeatBatteries() ? EssMode.HeatBatteries
|
||||
// : s.MustDoCalibrationCharge() ? EssMode.CalibrationCharge
|
||||
// : s.MustReachMinSoc() ? EssMode.ReachMinSoc
|
||||
// : s.GridMeter is null ? EssMode.NoGridMeter
|
||||
// : EssMode.OptimizeSelfConsumption;
|
||||
}
|
||||
|
||||
|
||||
public static EssControl ControlEss(this StatusRecord s)
|
||||
{
|
||||
// var hasPreChargeAlarm = s.HasPreChargeAlarm();
|
||||
//
|
||||
// if (hasPreChargeAlarm)
|
||||
// "PreChargeAlarm".Log();
|
||||
|
||||
var mode = s.SelectControlMode();
|
||||
|
||||
if (mode is EssMode.Off or EssMode.NoGridMeter)
|
||||
return new EssControl(mode, EssLimit.NoLimit, PowerCorrection: 0, PowerSetpoint: 0);
|
||||
|
||||
var essDelta = s.ComputePowerDelta(mode);
|
||||
|
||||
var unlimitedControl = new EssControl(mode, EssLimit.NoLimit, essDelta, 0);
|
||||
var limitedControl = unlimitedControl
|
||||
.LimitChargePower(s)
|
||||
.LimitDischargePower(s);
|
||||
|
||||
var currentPowerSetPoint = s.CurrentPowerSetPoint();
|
||||
var setpoint = currentPowerSetPoint + limitedControl.PowerCorrection;
|
||||
//var setpoint = -11000;
|
||||
|
||||
return limitedControl with { PowerSetpoint = setpoint };
|
||||
}
|
||||
|
||||
private static EssControl LimitChargePower(this EssControl control, StatusRecord s)
|
||||
{
|
||||
var maxInverterChargePower = s.ControlInverterPower(s.Config.MaxInverterPower);
|
||||
var maxBatteryChargePower = s.MaxBatteryChargePower();
|
||||
|
||||
return control
|
||||
.LimitChargePower(maxInverterChargePower, EssLimit.ChargeLimitedByInverterPower)
|
||||
.LimitChargePower(maxBatteryChargePower, EssLimit.ChargeLimitedByBatteryPower);
|
||||
}
|
||||
|
||||
private static EssControl LimitChargePower(this EssControl control, Double controlDelta, EssLimit reason)
|
||||
{
|
||||
return control.PowerCorrection > controlDelta
|
||||
? control with { LimitedBy = reason, PowerCorrection = controlDelta }
|
||||
: control;
|
||||
}
|
||||
|
||||
|
||||
private static EssControl LimitDischargePower(this EssControl control, StatusRecord s)
|
||||
{
|
||||
var maxInverterDischargeDelta = s.ControlInverterPower(-s.Config.MaxInverterPower);
|
||||
var maxBatteryDischargeDelta = s.Battery.Devices.Sum(b => b.MaxDischargePower);
|
||||
var keepMinSocLimitDelta = s.ControlBatteryPower(s.HoldMinSocPower());
|
||||
|
||||
return control
|
||||
.LimitDischargePower(maxInverterDischargeDelta, EssLimit.DischargeLimitedByInverterPower)
|
||||
.LimitDischargePower(maxBatteryDischargeDelta , EssLimit.DischargeLimitedByBatteryPower)
|
||||
.LimitDischargePower(keepMinSocLimitDelta , EssLimit.DischargeLimitedByMinSoc);
|
||||
}
|
||||
|
||||
|
||||
private static EssControl LimitDischargePower(this EssControl control, Double controlDelta, EssLimit reason)
|
||||
{
|
||||
return control.PowerCorrection < controlDelta
|
||||
? control with { LimitedBy = reason, PowerCorrection = controlDelta }
|
||||
: control;
|
||||
}
|
||||
|
||||
|
||||
private static Double ComputePowerDelta(this StatusRecord s, EssMode mode) => mode switch
|
||||
{
|
||||
EssMode.HeatBatteries => s.ControlInverterPower(s.Config.MaxInverterPower),
|
||||
EssMode.CalibrationCharge => s.ControlInverterPower(s.Config.MaxInverterPower),
|
||||
EssMode.ReachMinSoc => s.ControlInverterPower(s.Config.MaxInverterPower),
|
||||
EssMode.OptimizeSelfConsumption => s.ControlGridPower(s.Config.GridSetPoint),
|
||||
_ => throw new ArgumentException(null, nameof(mode))
|
||||
};
|
||||
|
||||
|
||||
|
||||
private static Boolean HasPreChargeAlarm(this StatusRecord statusRecord)
|
||||
{
|
||||
return statusRecord.DcDc.Alarms.Contains(Lib.Devices.Trumpf.TruConvertDc.Status.AlarmMessage.DcDcPrecharge);
|
||||
}
|
||||
|
||||
private static Boolean MustHeatBatteries(this StatusRecord s)
|
||||
{
|
||||
var batteries = s.Battery.Devices;
|
||||
|
||||
if (batteries.Count <= 0)
|
||||
return true; // batteries might be there but BMS is without power
|
||||
|
||||
return batteries
|
||||
.Select(b => b.Temperatures.State)
|
||||
.Contains(TemperatureState.Cold);
|
||||
}
|
||||
|
||||
private static Double MaxBatteryChargePower(this StatusRecord s)
|
||||
{
|
||||
return s.Battery.Devices.Sum(b => b.MaxChargePower);
|
||||
}
|
||||
|
||||
private static Double CurrentPowerSetPoint(this StatusRecord s)
|
||||
{
|
||||
return s
|
||||
.AcDc
|
||||
.Devices
|
||||
.Select(d =>
|
||||
{
|
||||
var acPowerControl = d.Control.Ac.Power;
|
||||
|
||||
return acPowerControl.L1.Active
|
||||
+ acPowerControl.L2.Active
|
||||
+ acPowerControl.L3.Active;
|
||||
})
|
||||
.Sum(p => p);
|
||||
}
|
||||
|
||||
private static Boolean MustReachMinSoc(this StatusRecord s)
|
||||
{
|
||||
var batteries = s.Battery.Devices;
|
||||
|
||||
return batteries.Count > 0
|
||||
&& batteries.Any(b => b.Soc < s.Config.MinSoc);
|
||||
}
|
||||
|
||||
|
||||
private static Boolean MustDoCalibrationCharge(this StatusRecord statusRecord)
|
||||
{
|
||||
var config = statusRecord.Config;
|
||||
|
||||
if (statusRecord.Battery.Eoc)
|
||||
{
|
||||
"Batteries have reached EOC".Log();
|
||||
config.LastEoc = UnixTime.Now;
|
||||
return false;
|
||||
}
|
||||
|
||||
return UnixTime.Now - statusRecord.Config.LastEoc > MaxTimeWithoutEoc;
|
||||
}
|
||||
|
||||
private static Double DistributePower(this StatusRecord s, Double powerSetPoint)
|
||||
{
|
||||
var inverterPowerSetPoint = powerSetPoint / s.AcDc.Devices.Count;
|
||||
return inverterPowerSetPoint.Clamp(-s.Config.MaxInverterPower, s.Config.MaxInverterPower);
|
||||
}
|
||||
|
||||
|
||||
public static Double ControlGridPower(this StatusRecord status, Double targetPower)
|
||||
{
|
||||
return ControlPower
|
||||
(
|
||||
measurement: status.GridMeter!.Ac.Power.Active,
|
||||
target: targetPower,
|
||||
pConstant: status.Config.PConstant
|
||||
);
|
||||
}
|
||||
|
||||
public static Double ControlInverterPower(this StatusRecord status, Double targetInverterPower)
|
||||
{
|
||||
return ControlPower
|
||||
(
|
||||
measurement: status.AcDc.Ac.Power.Active,
|
||||
target: targetInverterPower,
|
||||
pConstant: status.Config.PConstant
|
||||
);
|
||||
}
|
||||
|
||||
public static Double ControlBatteryPower(this StatusRecord status, Double targetBatteryPower)
|
||||
{
|
||||
return ControlPower
|
||||
(
|
||||
measurement: status.Battery.Devices.Sum(b => b.Dc.Power),
|
||||
target: targetBatteryPower,
|
||||
pConstant: status.Config.PConstant
|
||||
);
|
||||
}
|
||||
|
||||
private static Double HoldMinSocPower(this StatusRecord s)
|
||||
{
|
||||
// TODO: explain LowSOC curve
|
||||
|
||||
var batteries = s.Battery.Devices;
|
||||
|
||||
if (batteries.Count == 0)
|
||||
return Double.NegativeInfinity;
|
||||
|
||||
var a = -2 * s.Config.SelfDischargePower * batteries.Count / s.Config.HoldSocZone;
|
||||
var b = -a * (s.Config.MinSoc + s.Config.HoldSocZone);
|
||||
|
||||
return batteries.Min(d => d.Soc.Value) * a + b;
|
||||
}
|
||||
|
||||
private static Double ControlPower(Double measurement, Double target, Double pConstant)
|
||||
{
|
||||
var error = target - measurement;
|
||||
return error * pConstant;
|
||||
}
|
||||
|
||||
private static Double ControlPowerWithIntegral(Double measurement, Double target, Double p, Double i)
|
||||
{
|
||||
var errorSum = 0; // this is must be sum of error
|
||||
var error = target - measurement;
|
||||
var kp = p * error;
|
||||
var ki = i * errorSum;
|
||||
return ki + kp;
|
||||
}
|
||||
|
||||
private static IEnumerable<InverterState> InverterStates(this AcDcDevicesRecord acDcStatus)
|
||||
{
|
||||
return acDcStatus
|
||||
.Devices
|
||||
.Select(d => d.Status.InverterState.Current);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
using InnovEnergy.Lib.Units.Power;
|
||||
|
||||
namespace InnovEnergy.App.SaliMax.Ess;
|
||||
|
||||
public record EssControl
|
||||
(
|
||||
EssMode Mode,
|
||||
EssLimit LimitedBy,
|
||||
ActivePower PowerCorrection,
|
||||
ActivePower PowerSetpoint
|
||||
);
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
namespace InnovEnergy.App.SaliMax.Ess;
|
||||
|
||||
public enum EssLimit
|
||||
{
|
||||
NoLimit,
|
||||
DischargeLimitedByMinSoc,
|
||||
DischargeLimitedByBatteryPower,
|
||||
DischargeLimitedByInverterPower,
|
||||
ChargeLimitedByInverterPower,
|
||||
ChargeLimitedByBatteryPower,
|
||||
}
|
||||
|
||||
|
||||
// limitedBy = $"limiting discharging power in order to stay above min SOC: {s.Config.MinSoc}%";
|
||||
// limitedBy = $"limited by max battery discharging power: {maxDischargePower}";
|
||||
// limitedBy = $"limited by max inverter Dc to Ac power: {-s.Config.MaxInverterPower}W";
|
||||
// limitedBy = $"limited by max battery charging power: {maxChargePower}";
|
||||
// limitedBy = "limited by max inverter Ac to Dc power";
|
|
@ -0,0 +1,11 @@
|
|||
namespace InnovEnergy.App.SaliMax.Ess;
|
||||
|
||||
public enum EssMode
|
||||
{
|
||||
Off,
|
||||
HeatBatteries,
|
||||
CalibrationCharge,
|
||||
ReachMinSoc,
|
||||
NoGridMeter,
|
||||
OptimizeSelfConsumption
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||
using InnovEnergy.App.SaliMax.System;
|
||||
using InnovEnergy.App.SaliMax.SystemConfig;
|
||||
using InnovEnergy.Lib.Devices.AMPT;
|
||||
using InnovEnergy.Lib.Devices.Battery48TL;
|
||||
using InnovEnergy.Lib.Devices.EmuMeter;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
||||
|
||||
namespace InnovEnergy.App.SaliMax.Ess;
|
||||
|
||||
public record StatusRecord
|
||||
{
|
||||
public AcDcDevicesRecord AcDc { get; init; } = null!;
|
||||
public DcDcDevicesRecord DcDc { get; init; } = null!;
|
||||
public Battery48TlRecords Battery { get; init; } = null!;
|
||||
public EmuMeterRegisters? GridMeter { get; init; }
|
||||
public EmuMeterRegisters? CriticalLoad { get; init; }
|
||||
public RelaysRecord? Relays { get; init; }
|
||||
public AmptStatus Mppt { get; init; } = null!;
|
||||
public Config Config { get; init; } = null!;
|
||||
public SystemState SystemState { get; } = new SystemState();
|
||||
public EssControl Ess { get; set; } = null!;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace InnovEnergy.App.SaliMax;
|
||||
|
||||
public static class Logger
|
||||
{
|
||||
// Specify the maximum log file size in bytes (e.g., 1 MB)
|
||||
|
||||
private const Int32 MaxFileSizeBytes = 1024 * 1024; // TODO: move to settings
|
||||
private const Int32 MaxLogFileCount = 1000; // TODO: move to settings
|
||||
private const String LogFilePath = "LogDirectory/log.txt"; // TODO: move to settings
|
||||
|
||||
private static readonly ILogger _logger = new CustomLogger(LogFilePath, MaxFileSizeBytes, MaxLogFileCount);
|
||||
|
||||
public static T Log<T>(this T t) where T : notnull
|
||||
{
|
||||
// _logger.LogInformation(t.ToString()); // TODO: check warning
|
||||
return t;
|
||||
}
|
||||
}
|
|
@ -1,195 +1,246 @@
|
|||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using Flurl.Http;
|
||||
using InnovEnergy.App.SaliMax.Controller;
|
||||
using InnovEnergy.App.SaliMax.Ess;
|
||||
using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||
using InnovEnergy.App.SaliMax.System;
|
||||
using InnovEnergy.App.SaliMax.SystemConfig;
|
||||
using InnovEnergy.Lib.Devices.AMPT;
|
||||
using InnovEnergy.Lib.Devices.Battery48TL;
|
||||
using InnovEnergy.Lib.Devices.EmuMeter;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.SystemControl;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc.DataTypes;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
||||
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||
using InnovEnergy.Lib.Time.Unix;
|
||||
using InnovEnergy.Lib.Units;
|
||||
using InnovEnergy.Lib.Units.Composite;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using static InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes.SystemConfig;
|
||||
using Exception = System.Exception;
|
||||
|
||||
#pragma warning disable IL2026
|
||||
|
||||
|
||||
namespace InnovEnergy.App.SaliMax;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
[DllImport("libsystemd.so.0")]
|
||||
private static extern Int32 sd_notify(Int32 unsetEnvironment, String state);
|
||||
|
||||
private const UInt32 UpdateIntervalSeconds = 2;
|
||||
|
||||
private static readonly Byte[] BatteryNodes = { 2, 3, 4, 5, 6 };
|
||||
private const String BatteryTty = "/dev/ttyUSB0";
|
||||
|
||||
// private const String RelaysIp = "10.0.1.1"; // "192.168.1.242";
|
||||
// private const String TruConvertAcIp = "10.0.2.1"; // "192.168.1.2";
|
||||
// private const String TruConvertDcIp = "10.0.3.1"; // "192.168.1.3";
|
||||
// private const String GridMeterIp = "10.0.4.1"; // "192.168.1.241";
|
||||
// private const String InternalMeter = "10.0.4.2"; // "192.168.1.241";
|
||||
// private const String AmptIp = "10.0.5.1"; // "192.168.1.249";
|
||||
|
||||
|
||||
private static readonly TcpChannel TruConvertAcChannel = new TcpChannel("localhost", 5001);
|
||||
private static readonly TcpChannel TruConvertDcChannel = new TcpChannel("localhost", 5002);
|
||||
private static readonly TcpChannel GridMeterChannel = new TcpChannel("localhost", 5003);
|
||||
private static readonly TcpChannel AcOutLoadChannel = new TcpChannel("localhost", 5004);
|
||||
private static readonly TcpChannel AmptChannel = new TcpChannel("localhost", 5005);
|
||||
private static readonly TcpChannel RelaysChannel = new TcpChannel("localhost", 5006);
|
||||
private static readonly TcpChannel BatteriesChannel = new TcpChannel("localhost", 5007);
|
||||
|
||||
|
||||
private static readonly S3Config S3Config = new S3Config
|
||||
{
|
||||
Bucket = "saliomameiringen",
|
||||
Region = "sos-ch-dk-2",
|
||||
Provider = "exo.io",
|
||||
ContentType = "text/plain; charset=utf-8",
|
||||
Key = "EXO2bf0cbd97fbfa75aa36ed46f",
|
||||
Secret = "Bn1CDPqOG-XpDSbYjfIJxojcHTm391vZTc8z8l_fEPs"
|
||||
};
|
||||
|
||||
public static async Task Main(String[] args)
|
||||
{
|
||||
try
|
||||
while (true)
|
||||
{
|
||||
await Run();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await File.AppendAllTextAsync(Config.LogSalimaxLog,
|
||||
String.Join(Environment.NewLine, UnixTime.Now + " \n" + e));
|
||||
throw;
|
||||
try
|
||||
{
|
||||
await Run();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static async Task Run()
|
||||
{
|
||||
Console.WriteLine("Starting SaliMax");
|
||||
|
||||
var batteryNodes = new Byte[] { 2, 3 };
|
||||
// Send the initial "service started" message to systemd
|
||||
var sdNotifyReturn = sd_notify(0, "READY=1");
|
||||
|
||||
var batteryTty = "/dev/ttyUSB0";
|
||||
var battery48TlDevices = BatteryNodes
|
||||
.Select(n => new Battery48TlDevice(BatteriesChannel, n))
|
||||
.ToList();
|
||||
|
||||
var relaysIp = "10.0.1.1";
|
||||
var truConvertAcIp = "10.0.2.1";
|
||||
var truConvertDcIp = "10.0.3.1";
|
||||
var gridMeterIp = "10.0.4.1";
|
||||
var internalMeter = "10.0.4.2";
|
||||
var amptIp = "10.0.5.1";
|
||||
var batteryDevices = new Battery48TlDevices(battery48TlDevices);
|
||||
var acDcDevices = new TruConvertAcDcDevices(TruConvertAcChannel);
|
||||
var dcDcDevices = new TruConvertDcDcDevices(TruConvertDcChannel);
|
||||
var gridMeterDevice = new EmuMeterDevice(GridMeterChannel);
|
||||
var criticalLoadMeterDevice = new EmuMeterDevice(AcOutLoadChannel);
|
||||
var amptDevice = new AmptDevices(AmptChannel);
|
||||
var saliMaxRelaysDevice = new RelaysDevice(RelaysChannel);
|
||||
|
||||
|
||||
var s3Config = new S3Config
|
||||
StatusRecord ReadStatus() => new()
|
||||
{
|
||||
Bucket = "saliomameiringen",
|
||||
Region = "sos-ch-dk-2",
|
||||
Provider = "exo.io",
|
||||
ContentType = "text/plain; charset=utf-8",
|
||||
Key = "EXO2bf0cbd97fbfa75aa36ed46f",
|
||||
Secret = "Bn1CDPqOG-XpDSbYjfIJxojcHTm391vZTc8z8l_fEPs"
|
||||
AcDc = acDcDevices.Read(),
|
||||
DcDc = dcDcDevices.Read(),
|
||||
Battery = batteryDevices.Read(),
|
||||
Relays = saliMaxRelaysDevice.Read(),
|
||||
CriticalLoad = criticalLoadMeterDevice.Read(),
|
||||
GridMeter = gridMeterDevice.Read(),
|
||||
Mppt = amptDevice.Read(),
|
||||
Config = Config.Load() // load from disk every iteration, so config can be changed while running
|
||||
};
|
||||
|
||||
#if DEBUG
|
||||
var inverterDevice = new TruConvertAcDevice("127.0.0.1", 5001);
|
||||
var dcDcDevice = new TruConvertDcDevice("127.0.0.1", 5002);
|
||||
var gridMeterDevice = new EmuMeterDevice("127.0.0.1", 5003);
|
||||
var saliMaxRelaysDevice = new SaliMaxRelaysDevice("127.0.0.1", 5004);
|
||||
var amptDevice = new AmptCommunicationUnit("127.0.0.1", 5005);
|
||||
var acInToAcOutMeterDevice = new EmuMeterDevice("127.0.0.1", 5003); // TODO: use real device
|
||||
var secondBattery48TlDevice = Battery48TlDevice.Fake();
|
||||
var firstBattery48TlDevice =Battery48TlDevice.Fake();;
|
||||
var salimaxConfig = new SalimaxConfig();
|
||||
#else
|
||||
|
||||
var batteries = batteryNodes.Select(n => new Battery48TlDevice(batteryTty, n)).ToList();
|
||||
|
||||
|
||||
var inverterDevice = new TruConvertAcDevice(truConvertAcIp);
|
||||
var dcDcDevice = new TruConvertDcDevice(truConvertDcIp);
|
||||
|
||||
var gridMeterDevice = new EmuMeterDevice(gridMeterIp);
|
||||
var acInToAcOutMeterDevice = new EmuMeterDevice(internalMeter); // TODO: use real device
|
||||
|
||||
var amptDevice = new AmptCommunicationUnit(amptIp);
|
||||
|
||||
var saliMaxRelaysDevice = new SaliMaxRelaysDevice(relaysIp);
|
||||
var salimaxConfig = new SalimaxConfig();
|
||||
#endif
|
||||
// This is will be always add manually ? or do we need to read devices automatically in a range of IP @
|
||||
|
||||
|
||||
|
||||
StatusRecord ReadStatus()
|
||||
void WriteControl(StatusRecord r)
|
||||
{
|
||||
var combinedBatteryStatus = batteries
|
||||
.Select(b => b.ReadStatus())
|
||||
.NotNull()
|
||||
.ToList()
|
||||
.Combine();
|
||||
if (r.Relays is not null)
|
||||
saliMaxRelaysDevice.Write(r.Relays);
|
||||
|
||||
// var dcDcStatusArray = dcDcDevices.Select(b => b.ReadStatus()).NotNull().ToArray();
|
||||
// var inverterStatusArray = inverterDevices.Select(b => b.ReadStatus()).NotNull().ToArray();
|
||||
|
||||
return new StatusRecord
|
||||
{
|
||||
InverterStatus = inverterDevice.ReadStatus(),
|
||||
DcDcStatus = dcDcDevice.ReadStatus(),
|
||||
BatteriesStatus = combinedBatteryStatus,
|
||||
AcInToAcOutMeterStatus = acInToAcOutMeterDevice.ReadStatus(),
|
||||
GridMeterStatus = gridMeterDevice.ReadStatus(),
|
||||
SaliMaxRelayStatus = saliMaxRelaysDevice.ReadStatus(),
|
||||
AmptStatus = amptDevice.ReadStatus(),
|
||||
SalimaxConfig = salimaxConfig.Load().Result,
|
||||
};
|
||||
acDcDevices.Write(r.AcDc);
|
||||
dcDcDevices.Write(r.DcDc);
|
||||
}
|
||||
|
||||
|
||||
var startTime = UnixTime.Now;
|
||||
const Int32 delayTime = 10;
|
||||
|
||||
Console.WriteLine("press ctrl-C to stop");
|
||||
|
||||
|
||||
while (true)
|
||||
{
|
||||
var t = UnixTime.Now;
|
||||
while (t.Ticks % UpdateIntervalSeconds != 0)
|
||||
sd_notify(0, "WATCHDOG=1");
|
||||
|
||||
var t = UnixTime.FromTicks(UnixTime.Now.Ticks / 2 * 2);
|
||||
|
||||
t.ToUtcDateTime().WriteLine();
|
||||
|
||||
var record = ReadStatus();
|
||||
|
||||
record.AcDc.ResetAlarms();
|
||||
record.DcDc.ResetAlarms();
|
||||
|
||||
record.ControlSystemState();
|
||||
|
||||
var essControl = record.ControlEss();
|
||||
|
||||
record.Ess = essControl;
|
||||
|
||||
record.AcDc.SystemControl.ApplyDefaultSettings();
|
||||
record.DcDc.SystemControl.ApplyDefaultSettings();
|
||||
|
||||
DistributePower(record, essControl);
|
||||
|
||||
"===========================================".WriteLine();
|
||||
|
||||
WriteControl(record);
|
||||
|
||||
await UploadCsv(record, t);
|
||||
|
||||
var emuMeterRegisters = record.GridMeter;
|
||||
if (emuMeterRegisters is not null)
|
||||
{
|
||||
await Task.Delay(delayTime);
|
||||
t = UnixTime.Now;
|
||||
emuMeterRegisters.Ac.Power.Active.WriteLine();
|
||||
emuMeterRegisters.Ac.Power.Reactive.WriteLine();
|
||||
}
|
||||
|
||||
var status = ReadStatus();
|
||||
#if BatteriesAllowed
|
||||
|
||||
var jsonLog = status.ToLog(t);
|
||||
await UploadTimeSeries(s3Config, jsonLog, t);
|
||||
var controlRecord = Controller.Controller.SaliMaxControl(status);
|
||||
Controller.Controller.WriteControlRecord(controlRecord, inverterDevice, dcDcDevice, saliMaxRelaysDevice);
|
||||
|
||||
//JsonSerializer.Serialize(jsonLog, JsonOptions).WriteLine(ConsoleColor.DarkBlue);
|
||||
#endif
|
||||
Topology.Print(status);
|
||||
|
||||
while (UnixTime.Now == t)
|
||||
await Task.Delay(delayTime);
|
||||
}
|
||||
// ReSharper disable once FunctionNeverReturns
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// to delete not used anymore
|
||||
[Conditional("RELEASE")]
|
||||
private static void ReleaseWriteLog(JsonObject jsonLog, UnixTime timestamp)
|
||||
private static void DistributePower(StatusRecord record, EssControl essControl)
|
||||
{
|
||||
// WriteToFile(jsonLog, "/home/debian/DataSaliMax/" + timestamp); // this is was for beaglebone TODO
|
||||
var nInverters = record.AcDc.Devices.Count;
|
||||
|
||||
var powerPerInverterPhase = nInverters > 0
|
||||
? AcPower.FromActiveReactive(essControl.PowerSetpoint / nInverters / 3, 0)
|
||||
: AcPower.Null;
|
||||
|
||||
//var powerPerInverterPhase = AcPower.Null;
|
||||
|
||||
powerPerInverterPhase.WriteLine("powerPerInverterPhase");
|
||||
|
||||
record.AcDc.Devices.ForEach(d =>
|
||||
{
|
||||
d.Control.Ac.PhaseControl = PhaseControl.Asymmetric;
|
||||
d.Control.Ac.Power.L1 = powerPerInverterPhase;
|
||||
d.Control.Ac.Power.L2 = powerPerInverterPhase;
|
||||
d.Control.Ac.Power.L3 = powerPerInverterPhase;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private static void DebugWriteLog(JsonObject jsonLog, UnixTime timestamp)
|
||||
private static void ApplyDefaultSettings(this SystemControlRegisters? sc)
|
||||
{
|
||||
WriteToFile(jsonLog, "/home/atef/JsonData/" + timestamp);
|
||||
if (sc is null)
|
||||
return;
|
||||
|
||||
sc.ReferenceFrame = ReferenceFrame.Consumer;
|
||||
sc.SystemConfig = AcDcAndDcDc;
|
||||
|
||||
#if DEBUG
|
||||
sc.CommunicationTimeout = TimeSpan.FromMinutes(10);
|
||||
#else
|
||||
sc.CommunicationTimeout = TimeSpan.FromSeconds(10);
|
||||
#endif
|
||||
|
||||
sc.PowerSetPointActivation = PowerSetPointActivation.Immediate;
|
||||
sc.UseSlaveIdForAddressing = true;
|
||||
sc.SlaveErrorHandling = SlaveErrorHandling.Relaxed;
|
||||
sc.SubSlaveErrorHandling = SubSlaveErrorHandling.Off;
|
||||
sc.ResetAlarmsAndWarnings = true;
|
||||
}
|
||||
|
||||
private static void WriteToFile(Object obj, String fileName)
|
||||
private static DcDcDevicesRecord ResetAlarms(this DcDcDevicesRecord dcDcStatus)
|
||||
{
|
||||
var jsonString = JsonSerializer.Serialize(obj, JsonOptions);
|
||||
File.WriteAllText(fileName, jsonString);
|
||||
var sc = dcDcStatus.SystemControl;
|
||||
|
||||
if (sc is not null)
|
||||
sc.ResetAlarmsAndWarnings = sc.Alarms.Any();
|
||||
|
||||
foreach (var d in dcDcStatus.Devices)
|
||||
d.Control.ResetAlarmsAndWarnings = d.Status.Alarms.Any() || d.Status.Warnings.Any();
|
||||
|
||||
return dcDcStatus;
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
private static AcDcDevicesRecord ResetAlarms(this AcDcDevicesRecord acDcRecord)
|
||||
{
|
||||
WriteIndented = true,
|
||||
IgnoreReadOnlyProperties = false,
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||
//TODO
|
||||
};
|
||||
var sc = acDcRecord.SystemControl;
|
||||
|
||||
if (sc is not null)
|
||||
sc.ResetAlarmsAndWarnings = sc.Alarms.Any() || sc.Warnings.Any();
|
||||
|
||||
private static async Task UploadTimeSeries(S3Config config, JsonObject json, UnixTime unixTime)
|
||||
foreach (var d in acDcRecord.Devices)
|
||||
d.Control.ResetAlarmsAndWarnings = d.Status.Alarms.Any() || d.Status.Warnings.Any();
|
||||
|
||||
return acDcRecord;
|
||||
}
|
||||
|
||||
private static async Task UploadCsv(StatusRecord status, UnixTime timeStamp)
|
||||
{
|
||||
var payload = JsonSerializer.Serialize(json, JsonOptions);
|
||||
var s3Path = unixTime.Ticks + ".json";
|
||||
var request = config.CreatePutRequest(s3Path);
|
||||
var response = await request.PutAsync(new StringContent(payload));
|
||||
var csv = status.ToCsv();
|
||||
var s3Path = timeStamp + ".csv";
|
||||
var request = S3Config.CreatePutRequest(s3Path);
|
||||
var response = await request.PutAsync(new StringContent(csv));
|
||||
|
||||
csv.WriteLine();
|
||||
timeStamp.Ticks.WriteLine();
|
||||
|
||||
if (response.StatusCode != 200)
|
||||
{
|
||||
|
@ -199,18 +250,5 @@ internal static class Program
|
|||
}
|
||||
}
|
||||
|
||||
private static async Task UploadTopology(S3Config config, JsonObject json, UnixTime unixTime)
|
||||
{
|
||||
var payload = JsonSerializer.Serialize(json, JsonOptions);
|
||||
var s3Path = "topology" + unixTime.Ticks + ".json";
|
||||
var request = config.CreatePutRequest(s3Path);
|
||||
var response = await request.PutAsync(new StringContent(payload));
|
||||
|
||||
if (response.StatusCode != 200)
|
||||
{
|
||||
Console.WriteLine("ERROR: PUT");
|
||||
var error = response.GetStringAsync();
|
||||
Console.WriteLine(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||
|
||||
public enum RelayState
|
||||
{
|
||||
Open = 0,
|
||||
Closed = 1
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
using InnovEnergy.Lib.Devices.Adam6360D;
|
||||
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||
|
||||
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||
|
||||
public class RelaysDevice
|
||||
{
|
||||
private Adam6360DDevice AdamDevice { get; }
|
||||
|
||||
public RelaysDevice(String hostname) => AdamDevice = new Adam6360DDevice(hostname, 2);
|
||||
public RelaysDevice(Channel channel) => AdamDevice = new Adam6360DDevice(channel, 2);
|
||||
|
||||
public RelaysRecord? Read()
|
||||
{
|
||||
try
|
||||
{
|
||||
return AdamDevice.Read();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
$"Failed to read from {nameof(RelaysDevice)}\n{e}".Log();
|
||||
|
||||
// TODO: log
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(RelaysRecord r)
|
||||
{
|
||||
try
|
||||
{
|
||||
AdamDevice.Write(r);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
$"Failed to write to {nameof(RelaysDevice)}\n{e}".Log();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
using InnovEnergy.Lib.Devices.Adam6360D;
|
||||
|
||||
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||
|
||||
public enum InvertersAreConnectedToAc
|
||||
{
|
||||
None,
|
||||
Some,
|
||||
All
|
||||
}
|
||||
|
||||
public class RelaysRecord
|
||||
{
|
||||
private readonly Adam6360DRegisters _Regs;
|
||||
|
||||
public RelaysRecord(Adam6360DRegisters regs) => _Regs = regs;
|
||||
|
||||
|
||||
public Boolean K1GridBusIsConnectedToGrid => _Regs.DigitalInput6;
|
||||
public Boolean K2IslandBusIsConnectedToGridBus => !_Regs.DigitalInput4;
|
||||
|
||||
|
||||
public IEnumerable<Boolean> K3InverterIsConnectedToIslandBus
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return K3Inverter1IsConnectedToIslandBus;
|
||||
yield return K3Inverter2IsConnectedToIslandBus;
|
||||
yield return K3Inverter3IsConnectedToIslandBus;
|
||||
yield return K3Inverter4IsConnectedToIslandBus;
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean K3Inverter1IsConnectedToIslandBus => !_Regs.DigitalInput0;
|
||||
public Boolean K3Inverter2IsConnectedToIslandBus => !_Regs.DigitalInput1;
|
||||
public Boolean K3Inverter3IsConnectedToIslandBus => !_Regs.DigitalInput2;
|
||||
public Boolean K3Inverter4IsConnectedToIslandBus => !_Regs.DigitalInput3;
|
||||
|
||||
public Boolean FiWarning => !_Regs.DigitalInput5;
|
||||
public Boolean FiError => !_Regs.DigitalInput7;
|
||||
|
||||
public Boolean K2ConnectIslandBusToGridBus { get => _Regs.Relay0; set => _Regs.Relay0 = value;}
|
||||
|
||||
public static implicit operator Adam6360DRegisters(RelaysRecord d) => d._Regs;
|
||||
public static implicit operator RelaysRecord(Adam6360DRegisters d) => new RelaysRecord(d);
|
||||
|
||||
//
|
||||
// public HighActivePinState F1Inverter1 => _Regs.DigitalInput8.ConvertTo<HighActivePinState>(); // 1 = Closed , 0 = open
|
||||
// public HighActivePinState F2Inverter2 => _Regs.DigitalInput9.ConvertTo<HighActivePinState>(); // 1 = Closed , 0 = open
|
||||
// public HighActivePinState F3Inverter3 => _Regs.DigitalInput10.ConvertTo<HighActivePinState>(); // 1 = Closed , 0 = open
|
||||
// public HighActivePinState F4Inverter4 => _Regs.DigitalInput11.ConvertTo<HighActivePinState>(); // 1 = Closed , 0 = open
|
||||
//
|
||||
// public HighActivePinState Di12 => _Regs.DigitalInput12.ConvertTo<HighActivePinState>();
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
using InnovEnergy.Lib.Devices.Adam6060;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
|
||||
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||
|
||||
public class SaliMaxRelaysDevice
|
||||
{
|
||||
private Adam6060Device AdamDevice { get; }
|
||||
|
||||
public SaliMaxRelaysDevice (String hostname, UInt16 port = 502)//TODO
|
||||
{
|
||||
AdamDevice = new Adam6060Device(hostname, port);
|
||||
}
|
||||
|
||||
|
||||
public SaliMaxRelayStatus? ReadStatus()
|
||||
{
|
||||
// Console.WriteLine("Reading Relay Status");
|
||||
|
||||
var adamStatus = AdamDevice.ReadStatus();
|
||||
|
||||
if (adamStatus is null)
|
||||
return null;
|
||||
|
||||
return new SaliMaxRelayStatus
|
||||
{
|
||||
K1 = adamStatus.DigitalInput0.ConvertTo<RelayState>(),
|
||||
K2 = adamStatus.DigitalInput1.ConvertTo<RelayState>(),
|
||||
K3 = adamStatus.DigitalInput2.ConvertTo<RelayState>()
|
||||
};
|
||||
}
|
||||
|
||||
public void WriteControl(Boolean k2State) //this to improve
|
||||
{
|
||||
Console.WriteLine("Writing Relay Status");
|
||||
|
||||
var relayControlStatus = new Adam6060Control
|
||||
{
|
||||
Relay2 = k2State
|
||||
};
|
||||
|
||||
AdamDevice.WriteControl(relayControlStatus);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||
|
||||
public record SaliMaxRelayStatus
|
||||
{
|
||||
public RelayState K1 { get; init; } = RelayState.Closed; // Address on Adam(0X) 00002
|
||||
public RelayState K2 { get; init; } = RelayState.Closed; // Address on Adam(0X) 00003
|
||||
public RelayState K3 { get; init; } = RelayState.Closed; // Address on Adam(0X) 00004
|
||||
}
|
|
@ -0,0 +1,470 @@
|
|||
using InnovEnergy.App.SaliMax.Ess;
|
||||
using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||
using InnovEnergy.Lib.Devices.Battery48TL;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
||||
using static InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes.GridType;
|
||||
|
||||
namespace InnovEnergy.App.SaliMax.System;
|
||||
|
||||
public static class Controller
|
||||
{
|
||||
private static Int32 GetSystemState(this StatusRecord r)
|
||||
{
|
||||
var relays = r.Relays;
|
||||
|
||||
if (relays is null)
|
||||
return 101; // Message = "Panic: relay device is not available!",
|
||||
|
||||
var acDcs = r.AcDc;
|
||||
|
||||
if (acDcs.NotAvailable())
|
||||
return 102;
|
||||
|
||||
var k4 = acDcs.AllDisabled() ? 0
|
||||
: acDcs.AllGridTied() ? 1
|
||||
: acDcs.AllIsland() ? 2
|
||||
: 4;
|
||||
|
||||
if (k4 == 4)
|
||||
return 103; //Message = "Panic: ACDCs have unequal grid types",
|
||||
|
||||
var nInverters = r.AcDc.Devices.Count;
|
||||
|
||||
var k1 = relays.K1GridBusIsConnectedToGrid ? 1 : 0;
|
||||
var k2 = relays.K2IslandBusIsConnectedToGridBus ? 1 : 0;
|
||||
var k3 = relays.K3InverterIsConnectedToIslandBus.Take(nInverters).Any(c => c) ? 1 : 0;
|
||||
|
||||
// states as defined in states excel sheet
|
||||
return 1
|
||||
+ 1*k1
|
||||
+ 2*k2
|
||||
+ 4*k3
|
||||
+ 8*k4;
|
||||
}
|
||||
|
||||
public static Boolean ControlSystemState(this StatusRecord s)
|
||||
{
|
||||
s.SystemState.Id = s.GetSystemState();
|
||||
|
||||
return s.SystemState.Id switch
|
||||
{
|
||||
1 => State1(s),
|
||||
2 => State2(s),
|
||||
4 => State4(s),
|
||||
6 => State6(s),
|
||||
9 => State9(s),
|
||||
//10 => State10(s),
|
||||
12 => State12(s),
|
||||
13 => State13(s),
|
||||
15 => State15(s),
|
||||
16 => State16(s),
|
||||
17 => State17(s),
|
||||
18 => State18(s),
|
||||
21 => State21(s),
|
||||
|
||||
101 => State101(s),
|
||||
102 => State102(s),
|
||||
103 => State103(s),
|
||||
_ => UnknownState(s)
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
private static Boolean NotAvailable(this AcDcDevicesRecord acDcs)
|
||||
{
|
||||
return acDcs.SystemControl == null || acDcs.Devices.Count == 0;
|
||||
}
|
||||
|
||||
private static Boolean NotAvailable(this DcDcDevicesRecord dcDcs)
|
||||
{
|
||||
return dcDcs.SystemControl == null || dcDcs.Devices.Count == 0;
|
||||
}
|
||||
|
||||
|
||||
private static Boolean NotAvailable(this Battery48TlRecords batteries)
|
||||
{
|
||||
return batteries.Devices.Count <= 0;
|
||||
}
|
||||
|
||||
private static Boolean State1(StatusRecord s)
|
||||
{
|
||||
s.SystemState.Message = "Inverters are off. Switching to Island Mode.";
|
||||
|
||||
s.DcDc.Enable();
|
||||
s.AcDc.Enable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 17
|
||||
}
|
||||
|
||||
private static Boolean State2(StatusRecord s)
|
||||
{
|
||||
s.SystemState.Message = "Inverters are disconnected from Island Bus. Switching to GridTie Mode. C";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.ConnectIslandBusToGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 10
|
||||
}
|
||||
|
||||
private static Boolean State4(StatusRecord s)
|
||||
{
|
||||
s.SystemState.Message = "Turning on Inverters";
|
||||
|
||||
s.DcDc.Enable();
|
||||
s.AcDc.Enable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.ConnectIslandBusToGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 12
|
||||
}
|
||||
|
||||
|
||||
private static Boolean State6(StatusRecord s)
|
||||
{
|
||||
s.SystemState.Message = "Inverters are off. Waiting for them to disconnect from Island Bus.";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return true;
|
||||
|
||||
// => 2
|
||||
}
|
||||
|
||||
|
||||
private static Boolean State9(StatusRecord s)
|
||||
{
|
||||
|
||||
s.SystemState.Message = "Inverters have disconnected from Island Bus. Turning them off.";
|
||||
|
||||
s.DcDc.Disable(); // TODO: leave enabled?
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return true;
|
||||
|
||||
// => 1
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// private static Boolean State10(StatusRecord s)
|
||||
// {
|
||||
//
|
||||
// s.SystemState.Message = "Inverters have disconnected from AcOut. Turning them off.";
|
||||
//
|
||||
// s.DcDc.Disable(); // TODO: leave enabled?
|
||||
// s.AcDc.Disable();
|
||||
// s.AcDc.EnableGridTieMode();
|
||||
// s.Relays.DisconnectIslandBusFromGrid();
|
||||
//
|
||||
// return true;
|
||||
//
|
||||
// // => 12
|
||||
// }
|
||||
|
||||
private static Boolean State12(StatusRecord s)
|
||||
{
|
||||
s.SystemState.Message = "Waiting for Inverters to connect to Island Bus";
|
||||
|
||||
s.DcDc.Enable();
|
||||
s.AcDc.Enable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.ConnectIslandBusToGrid();
|
||||
|
||||
return true;
|
||||
|
||||
// => 16
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static Boolean State13(StatusRecord s)
|
||||
{
|
||||
s.SystemState.Message = "Disconnected from AcIn (K2), awaiting inverters to disconnect from AcOut (K3)";
|
||||
|
||||
s.DcDc.Enable();
|
||||
s.AcDc.Enable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return true;
|
||||
|
||||
// => 9
|
||||
}
|
||||
|
||||
private static Boolean State15(StatusRecord s)
|
||||
{
|
||||
s.SystemState.Message = "Grid has been lost, disconnecting AcIn from AcOut (K2)";
|
||||
|
||||
s.DcDc.Enable();
|
||||
s.AcDc.Enable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return true;
|
||||
|
||||
// => 13
|
||||
}
|
||||
|
||||
private static Boolean State16(StatusRecord s)
|
||||
{
|
||||
// return new
|
||||
// (
|
||||
// " Inverter is in grid-tie\n Waiting for K1AcInIsConnectedToGrid to open to leave it",
|
||||
// AcPowerStageEnable: true,
|
||||
// DcPowerStageEnable: true,
|
||||
// GridType.GridTied400V50Hz,
|
||||
// HighActivePinState.Closed
|
||||
// );
|
||||
|
||||
s.SystemState.Message = "ESS";
|
||||
|
||||
s.DcDc.Enable();
|
||||
s.AcDc.Enable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.ConnectIslandBusToGrid();
|
||||
|
||||
return true;
|
||||
|
||||
// => 15
|
||||
}
|
||||
|
||||
|
||||
private static Boolean State17(StatusRecord s)
|
||||
{
|
||||
s.SystemState.Message = "Inverters are in Island Mode. Waiting for them to connect to AcIn.";
|
||||
|
||||
s.DcDc.Enable();
|
||||
s.AcDc.Enable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return true;
|
||||
|
||||
// => 21
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static Boolean State18(StatusRecord s)
|
||||
{
|
||||
// return new
|
||||
// (
|
||||
// " Didn't succeed to go to Island mode and K1AcInIsConnectedToGrid close\n Turning off power stage of inverter\n Moving to Grid Tie",
|
||||
// AcPowerStageEnable: false,
|
||||
// DcPowerStageEnable: false,
|
||||
// GridType.GridTied400V50Hz,
|
||||
// HighActivePinState.Open
|
||||
// );
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Boolean State21(StatusRecord s)
|
||||
{
|
||||
s.SystemState.Message = "Island Mode";
|
||||
|
||||
s.DcDc.Enable();
|
||||
s.AcDc.Enable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 22
|
||||
}
|
||||
|
||||
private static Boolean State22(StatusRecord s)
|
||||
{
|
||||
s.SystemState.Message = "Grid became available (K1). Turning off inverters.";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 6
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private static Boolean State101(StatusRecord s)
|
||||
{
|
||||
s.SystemState.Message = "Relay device is not available";
|
||||
return s.EnableSafeDefaults();
|
||||
}
|
||||
|
||||
private static Boolean State102(StatusRecord s)
|
||||
{
|
||||
s.SystemState.Message = "ACDCs not available";
|
||||
return s.EnableSafeDefaults();
|
||||
}
|
||||
|
||||
|
||||
private static Boolean State103(StatusRecord s)
|
||||
{
|
||||
s.SystemState.Message = "Panic: ACDCs have unequal grid types";
|
||||
return s.EnableSafeDefaults();
|
||||
}
|
||||
|
||||
private static Boolean State104(StatusRecord s)
|
||||
{
|
||||
s.SystemState.Message = "Panic: DCDCs not available";
|
||||
return s.EnableSafeDefaults();
|
||||
}
|
||||
|
||||
|
||||
private static Boolean UnknownState(StatusRecord s)
|
||||
{
|
||||
// "Unknown System State"
|
||||
|
||||
return s.EnableSafeDefaults();
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static Boolean AllDisabled(this AcDcDevicesRecord acDcs)
|
||||
{
|
||||
return acDcs.Devices.All(d => !d.Control.PowerStageEnable);
|
||||
}
|
||||
|
||||
private static Boolean AllGridTied(this AcDcDevicesRecord acDcs)
|
||||
{
|
||||
return acDcs.Devices.All(d => d.Status.ActiveGridType is GridTied380V60Hz)
|
||||
|| acDcs.Devices.All(d => d.Status.ActiveGridType is GridTied400V50Hz)
|
||||
|| acDcs.Devices.All(d => d.Status.ActiveGridType is GridTied480V60Hz);
|
||||
}
|
||||
|
||||
private static Boolean AllIsland(this AcDcDevicesRecord acDcs)
|
||||
{
|
||||
return acDcs.Devices.All(d => d.Status.ActiveGridType is Island400V50Hz)
|
||||
|| acDcs.Devices.All(d => d.Status.ActiveGridType is Island480V60Hz);
|
||||
}
|
||||
|
||||
private static void ForAll<T>(this IEnumerable<T> ts, Action<T> action)
|
||||
{
|
||||
foreach (var t in ts)
|
||||
action(t);
|
||||
}
|
||||
|
||||
private static void Disable(this AcDcDevicesRecord acDc)
|
||||
{
|
||||
acDc.Devices
|
||||
.Select(d => d.Control)
|
||||
.ForAll(c => c.PowerStageEnable = false);
|
||||
}
|
||||
|
||||
|
||||
private static void Disable(this DcDcDevicesRecord dcDc)
|
||||
{
|
||||
dcDc.Devices
|
||||
.Select(d => d.Control)
|
||||
.ForAll(c => c.PowerStageEnable = false);
|
||||
}
|
||||
|
||||
private static void Enable(this AcDcDevicesRecord acDc)
|
||||
{
|
||||
acDc.Devices
|
||||
.Select(d => d.Control)
|
||||
.ForAll(c => c.PowerStageEnable = true);
|
||||
}
|
||||
|
||||
|
||||
private static void Enable(this DcDcDevicesRecord dcDc)
|
||||
{
|
||||
dcDc.Devices
|
||||
.Select(d => d.Control)
|
||||
.ForAll(c => c.PowerStageEnable = true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static void EnableGridTieMode(this AcDcDevicesRecord acDc)
|
||||
{
|
||||
acDc.Devices
|
||||
.Select(d => d.Control)
|
||||
.ForAll(c => c.Ac.GridType = GridTied400V50Hz); // TODO: config grid type
|
||||
}
|
||||
|
||||
|
||||
private static void EnableIslandMode(this AcDcDevicesRecord acDc)
|
||||
{
|
||||
acDc.Devices
|
||||
.Select(d => d.Control)
|
||||
.ForAll(c => c.Ac.GridType = Island400V50Hz); // TODO: config grid type
|
||||
}
|
||||
|
||||
private static void DisconnectIslandBusFromGrid(this RelaysRecord? relays)
|
||||
{
|
||||
if (relays is not null)
|
||||
relays.K2ConnectIslandBusToGridBus = false;
|
||||
}
|
||||
|
||||
private static void ConnectIslandBusToGrid(this RelaysRecord? relays)
|
||||
{
|
||||
if (relays is not null)
|
||||
relays.K2ConnectIslandBusToGridBus = true;
|
||||
}
|
||||
|
||||
|
||||
private static Boolean EnableSafeDefaults(this StatusRecord s)
|
||||
{
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static DcDcDevicesRecord ResetAlarms(this DcDcDevicesRecord dcDcStatus)
|
||||
{
|
||||
var sc = dcDcStatus.SystemControl;
|
||||
|
||||
if (sc is not null)
|
||||
sc.ResetAlarmsAndWarnings = sc.Alarms.Any();
|
||||
|
||||
foreach (var d in dcDcStatus.Devices)
|
||||
d.Control.ResetAlarmsAndWarnings = d.Status.Alarms.Any() || d.Status.Warnings.Any();
|
||||
|
||||
return dcDcStatus;
|
||||
}
|
||||
|
||||
private static AcDcDevicesRecord ResetAlarms(this AcDcDevicesRecord acDcStatus)
|
||||
{
|
||||
var sc = acDcStatus.SystemControl;
|
||||
|
||||
if (sc is not null)
|
||||
sc.ResetAlarmsAndWarnings = sc.Alarms.Any() || sc.Warnings.Any();
|
||||
|
||||
foreach (var d in acDcStatus.Devices)
|
||||
d.Control.ResetAlarmsAndWarnings = d.Status.Alarms.Any() || d.Status.Warnings.Any();
|
||||
|
||||
return acDcStatus;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace InnovEnergy.App.SaliMax.System;
|
||||
|
||||
public class SystemState
|
||||
{
|
||||
public String Message { get; set; } = "Panic: Unknown State!";
|
||||
public Int32 Id { get; set; } = 100;
|
||||
}
|
|
@ -1,6 +1,91 @@
|
|||
using System.Text.Json;
|
||||
using InnovEnergy.Lib.Time.Unix;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using static System.Text.Json.JsonSerializer;
|
||||
|
||||
namespace InnovEnergy.App.SaliMax.SystemConfig;
|
||||
|
||||
public static class Config
|
||||
// shut up trim warnings
|
||||
#pragma warning disable IL2026
|
||||
|
||||
public class Config //TODO: let IE choose from config files (Json) and connect to GUI
|
||||
{
|
||||
public const String LogSalimaxLog = "/home/ie-entwicklung/Salimax.log"; // todo remove ie-entwicklung
|
||||
private static String DefaultConfigFilePath => Path.Combine(Environment.CurrentDirectory, "config.json");
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
|
||||
public Double MinSoc { get; set; }
|
||||
public UnixTime LastEoc { get; set; }
|
||||
public Double PConstant { get; set; }
|
||||
public Double ForceChargePower { get; set; }
|
||||
public Double ForceDischargePower { get; set; }
|
||||
public Double MaxInverterPower { get; set; }
|
||||
public Double GridSetPoint { get; set; }
|
||||
public Double SelfDischargePower { get; set; }
|
||||
public Double HoldSocZone { get; set; }
|
||||
public Double ControllerPConstant { get; set; }
|
||||
|
||||
public static Config Default => new()
|
||||
{
|
||||
MinSoc = 20,
|
||||
LastEoc = UnixTime.Epoch,
|
||||
PConstant = .5,
|
||||
ForceChargePower = 1_000_000,
|
||||
ForceDischargePower = -1_000_000,
|
||||
MaxInverterPower = 32_000,
|
||||
GridSetPoint = 0.0,
|
||||
SelfDischargePower = 200, // TODO: multiple batteries
|
||||
HoldSocZone = 1, // TODO: find better name,
|
||||
ControllerPConstant = 0.5
|
||||
};
|
||||
|
||||
|
||||
public void Save(String? path = null)
|
||||
{
|
||||
var configFilePath = path ?? DefaultConfigFilePath;
|
||||
|
||||
try
|
||||
{
|
||||
var jsonString = Serialize(this, JsonOptions);
|
||||
File.WriteAllText(configFilePath, jsonString);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
$"Failed to write config file {configFilePath}\n{e}".Log();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static Config Load(String? path = null)
|
||||
{
|
||||
var configFilePath = path ?? DefaultConfigFilePath;
|
||||
try
|
||||
{
|
||||
var jsonString = File.ReadAllText(configFilePath);
|
||||
return Deserialize<Config>(jsonString)!;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
$"Failed to read config file {configFilePath}, using default config\n{e}".Log();
|
||||
return Default;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static async Task<Config> LoadAsync(String? path = null)
|
||||
{
|
||||
var configFilePath = path ?? DefaultConfigFilePath;
|
||||
try
|
||||
{
|
||||
var jsonString = await File.ReadAllTextAsync(configFilePath);
|
||||
return Deserialize<Config>(jsonString)!;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Couldn't read config file {configFilePath}, using default config");
|
||||
e.Message.WriteLine();
|
||||
return Default;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,120 +1,123 @@
|
|||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
||||
|
||||
namespace InnovEnergy.App.SaliMax.SystemConfig;
|
||||
|
||||
public static class Defaults
|
||||
{
|
||||
public static readonly TruConvertAcControl TruConvertAcControl = new()
|
||||
{
|
||||
Date = default, // TODO
|
||||
Time = default, // TODO,
|
||||
IpAddress = default, // 0x C0A80102;
|
||||
Subnet = default, //= 0x FFFFFF00;
|
||||
Gateway = default, //= 0x C0A80102;
|
||||
ResetParamToDefault = false, // Coil
|
||||
CommunicationTimeout = TimeSpan.FromSeconds(10),
|
||||
FactoryResetParameters = false,
|
||||
ConnectedSystemConfig = Lib.Devices.Trumpf.TruConvert.SystemConfig.AcDcAndDcDc,
|
||||
UpdateSwTrigger = 0,
|
||||
AutomaticSwUpdate = 0,
|
||||
CustomerValuesSaveReset = 0,
|
||||
// SerialNumberSystemControl = 0,
|
||||
// SerialNumberAcDc = 0,
|
||||
IntegrationLevel = 16,
|
||||
// IlBuildnumber = 0,
|
||||
PowerStageEnable = true,
|
||||
SetValueConfig = SymmetricAcOperationMode.Symmetric, // Asymmetric = 0, // this is can not be seen in UI
|
||||
ResetsAlarmAndWarning = true,
|
||||
PreChargeDcLinkConfig = PreChargeDcLinkConfig.Internal, // 1 = internal
|
||||
PowerFactorConvention = PowerFactorConvention.Producer, // 0 = producer
|
||||
SlaveAddress = 1,
|
||||
ErrorHandlingPolicy = AcErrorPolicy.Relaxed, // 0 = relaxed
|
||||
GridType = AcDcGridType.GridTied400V50Hz,
|
||||
// SubSlaveAddress = 0, // Broadcast
|
||||
UseModbusSlaveIdForAddressing = false,
|
||||
SubSlaveErrorPolicy = 0,
|
||||
SignedPowerNominalValue = 0, //signedPowerValue
|
||||
SignedPowerSetValueL1 = 0,
|
||||
SignedPowerSetValueL2 = 0,
|
||||
SignedPowerSetValueL3 = 0,
|
||||
// PowerSetValue = 0,
|
||||
// PowerSetValueL1 = 0,
|
||||
// PowerSetValueL2 = 0,
|
||||
// PowerSetValueL3 = 0,
|
||||
MaximumGridCurrentRmsL1 = 15,
|
||||
MaximumGridCurrentRmsL2 = 15,
|
||||
MaximumGridCurrentRmsL3 = 15,
|
||||
CosPhiSetValueL1 = 0,
|
||||
CosPhiSetValueL2 = 0,
|
||||
CosPhiSetValueL3 = 0,
|
||||
PhaseL1IsCapacitive = false,
|
||||
PhaseL2IsCapacitive = false,
|
||||
PhaseL3IsCapacitive = false,
|
||||
PhasesAreCapacitive = false,
|
||||
SetPointCosPhi = 0,
|
||||
SetPointSinPhi = 0,
|
||||
SetPointSinPhiL1 = 0,
|
||||
SetPointSinPhiL2 = 0,
|
||||
SetPointSinPhiL3 = 0,
|
||||
FrequencyOffsetIm = 0,
|
||||
VoltageAdjustmentFactorIm = 0,
|
||||
PreChargeDcLinkVoltage = 10,
|
||||
MaxPeakCurrentVoltageControlL1 = 0,
|
||||
MaxPeakCurrentVoltageControlL2 = 0,
|
||||
MaxPeakCurrentVoltageControlL3 = 0,
|
||||
GridFormingMode = 0, // 0 = not grid-forming (grid-tied) ,1 = grid-forming TODO enum
|
||||
|
||||
//remove DC stuff from AC
|
||||
DcLinkRefVoltage = 800,
|
||||
DcLinkMinVoltage = 780,
|
||||
DcLinkMaxVoltage = 820,
|
||||
DcVoltageRefUs = 900,
|
||||
DcMinVoltageUs = 880,
|
||||
DcMaxVoltageUs = 920,
|
||||
AcDcGcBypassMode = 0,
|
||||
AcDcGcPMaxThresholdPercent = 150,
|
||||
AcDcGcStartupRampEnable = 0,
|
||||
DcConfigModule = DcStageConfiguration.Off,
|
||||
DcDcPowerDistribution = 100,
|
||||
AcDcDistributionMode = AcDcDistributionMode.Auto,
|
||||
};
|
||||
|
||||
public static readonly TruConvertDcControl TruConvertDcControl = new()
|
||||
{
|
||||
Date = default,
|
||||
Time = default,
|
||||
IpAddress = default,
|
||||
Subnet = default,
|
||||
Gateway = default,
|
||||
ResetParamToDefault = false ,
|
||||
TimeoutForCommunication = TimeSpan.FromSeconds(10) ,
|
||||
RestartFlag = false ,
|
||||
ConnectedSystemConfig = Lib.Devices.Trumpf.TruConvert.SystemConfig.DcDcOnly,
|
||||
UpdateSwTrigger = 0,
|
||||
AutomaticSwUpdate = 0,
|
||||
CustomerValuesSaveReset = 0,
|
||||
SerialNumberSystemControl = 0,
|
||||
SerialNumberDcDc = 0,
|
||||
MaterialNumberDcDc = 0,
|
||||
PowerStageEnable = true,
|
||||
ResetsAlarmAndWarning = false,
|
||||
SlaveAddress = 1,
|
||||
SubSlaveAddress = 0,
|
||||
ModbusSlaveId = false,
|
||||
MaximumBatteryVoltage = 56,
|
||||
MinimumBatteryVoltage = 42,
|
||||
MaximumBatteryChargingCurrent = 208,
|
||||
MaximumBatteryDischargingCurrent = 208,
|
||||
MaximumVoltageAlarmThreshold = 60,
|
||||
MinimumVoltageAlarmThreshold = 0,
|
||||
MaximalPowerAtDc = 9000,
|
||||
BatteryCurrentSet = 0,
|
||||
DynamicCurrentPerMillisecond = 2,
|
||||
DcLinkControlMode = 1,
|
||||
ReferenceVoltage = 800,
|
||||
UpperVoltageWindow = 40,
|
||||
LowerVoltageWindow = 40,
|
||||
VoltageDeadBand = 0,
|
||||
};
|
||||
}
|
||||
// using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||
//
|
||||
// namespace InnovEnergy.App.SaliMax.SystemConfig;
|
||||
//
|
||||
// public static class Defaults
|
||||
// {
|
||||
// public static readonly TruConvertAcControl TruConvertAcControl = new()
|
||||
// {
|
||||
// Date = default, // TODO
|
||||
// Time = default, // TODO,
|
||||
// IpAddress = default, // 0x C0A80102;
|
||||
// Subnet = default, //= 0x FFFFFF00;
|
||||
// Gateway = default, //= 0x C0A80102;
|
||||
// ResetParamToDefault = false, // Coil
|
||||
// CommunicationTimeout = TimeSpan.FromSeconds(20),
|
||||
// CpuReset = false,
|
||||
// ConnectedSystemConfig = Lib.Devices.Trumpf.TruConvert.SystemConfig.AcDcAndDcDc,
|
||||
// UpdateSwTrigger = 0,
|
||||
// AutomaticSwUpdate = 0,
|
||||
// CustomerValuesSaveReset = 0,
|
||||
// // SerialNumberSystemControl = 0,
|
||||
// // SerialNumberAcDc = 0,
|
||||
// IntegrationLevel = 16,
|
||||
// // IlBuildnumber = 0,
|
||||
// PowerStageEnable = true,
|
||||
// SetValueConfig = SymmetricAcOperationMode.Symmetric, // Asymmetric = 0, // this is can not be seen in UI
|
||||
// ResetsAlarmAndWarning = false,
|
||||
// PreChargeDcLinkConfig = PreChargeDcLinkConfig.Internal, // 1 = internal
|
||||
// PowerFactorConvention = PowerFactorConvention.Producer, // 0 = producer
|
||||
// SlaveAddress = 1,
|
||||
// ErrorHandlingPolicy = AcErrorPolicy.Relaxed, // 0 = relaxed
|
||||
// GridType = AcDcGridType.GridTied400V50Hz,
|
||||
// // SubSlaveAddress = 0, // Broadcast
|
||||
// UseModbusSlaveIdForAddressing = false,
|
||||
// SubSlaveErrorPolicy = 0,
|
||||
// SignedPowerNominalValue = 0, //signedPowerValue
|
||||
// SignedPowerSetValueL1 = 0,
|
||||
// SignedPowerSetValueL2 = 0,
|
||||
// SignedPowerSetValueL3 = 0,
|
||||
// // PowerSetValue = 0,
|
||||
// // PowerSetValueL1 = 0,
|
||||
// // PowerSetValueL2 = 0,
|
||||
// // PowerSetValueL3 = 0,
|
||||
// MaximumGridCurrentRmsL1 = 80, // update to the default one
|
||||
// MaximumGridCurrentRmsL2 = 80, // update to the default one
|
||||
// MaximumGridCurrentRmsL3 = 80, // update to the default one
|
||||
// CosPhiSetValueL1 = 0,
|
||||
// CosPhiSetValueL2 = 0,
|
||||
// CosPhiSetValueL3 = 0,
|
||||
// PhaseL1IsCapacitive = false,
|
||||
// PhaseL2IsCapacitive = false,
|
||||
// PhaseL3IsCapacitive = false,
|
||||
// PhasesAreCapacitive = false,
|
||||
// SetPointCosPhi = 1,
|
||||
// SetPointSinPhi = 0,
|
||||
// SetPointSinPhiL1 = 0,
|
||||
// SetPointSinPhiL2 = 0,
|
||||
// SetPointSinPhiL3 = 0,
|
||||
// FrequencyOffsetIm = 0,
|
||||
// VoltageAdjustmentFactorIm = 100,
|
||||
// PreChargeDcLinkVoltage = 10,
|
||||
// MaxPeakCurrentVoltageControlL1 = 0,
|
||||
// MaxPeakCurrentVoltageControlL2 = 0,
|
||||
// MaxPeakCurrentVoltageControlL3 = 0,
|
||||
// GridFormingMode = 1.ConvertTo<AcGridFormingMode>(), // 0 = not grid-forming (grid-tied) ,1 = grid-forming TODO enum
|
||||
//
|
||||
// DcLinkRefVoltage = 720,
|
||||
// DcLinkMinVoltage = 690,
|
||||
// DcLinkMaxVoltage = 780,
|
||||
// DcVoltageRefUs = 870,
|
||||
// DcMinVoltageUs = 880,
|
||||
// DcMaxVoltageUs = 920,
|
||||
// //AcDcGcBypassMode = 0,
|
||||
// //AcDcGcPMaxThresholdPercent = 150,
|
||||
// //AcDcGcStartupRampEnable = 0,
|
||||
// DcConfigModule = DcStageConfiguration.Off,
|
||||
// DcDcPowerDistribution = 100,
|
||||
// AcDcDistributionMode = AcDcDistributionMode.Auto,
|
||||
// };
|
||||
//
|
||||
// public static readonly TruConvertDcControl TruConvertDcControl = new()
|
||||
// {
|
||||
// Date = default,
|
||||
// Time = default,
|
||||
// IpAddress = default,
|
||||
// Subnet = default,
|
||||
// Gateway = default,
|
||||
// ResetParamToDefault = false ,
|
||||
// TimeoutForCommunication = TimeSpan.FromSeconds(20) ,
|
||||
// RestartFlag = false ,
|
||||
// ConnectedSystemConfig = Lib.Devices.Trumpf.TruConvert.SystemConfig.DcDcOnly,
|
||||
// UpdateSwTrigger = 0,
|
||||
// AutomaticSwUpdate = 0,
|
||||
// CustomerValuesSaveReset = 0,
|
||||
// SerialNumberSystemControl = 0,
|
||||
// SerialNumberDcDc = 0,
|
||||
// MaterialNumberDcDc = 0,
|
||||
// PowerStageEnable = true,
|
||||
// ResetsAlarmAndWarning = false,
|
||||
// SlaveAddress = 1,
|
||||
// SubSlaveAddress = 0,
|
||||
// ModbusSlaveId = false,
|
||||
// MaximumBatteryVoltage = 57m,
|
||||
// MinimumBatteryVoltage = 42,
|
||||
// MaximumBatteryChargingCurrent = 210,
|
||||
// MaximumBatteryDischargingCurrent = 210,
|
||||
// MaximumVoltageAlarmThreshold = 60,
|
||||
// MinimumVoltageAlarmThreshold = 0,
|
||||
// MaximalPowerAtDc = 10000,
|
||||
// BatteryCurrentSet = 0,
|
||||
// DynamicCurrentPerMillisecond = 100,
|
||||
// DcLinkControlMode = 1,
|
||||
// ReferenceVoltage = 720,
|
||||
// UpperVoltageWindow = 55,
|
||||
// LowerVoltageWindow = 55,
|
||||
// VoltageDeadBand = 0,
|
||||
// };
|
||||
//
|
||||
// public static readonly SaliMaxRelayControl SaliMaxRelayControl = new()
|
||||
// {
|
||||
// K2Control = HighActivePinState.Closed
|
||||
// };
|
||||
// }
|
|
@ -1,65 +0,0 @@
|
|||
using System.Text.Json;
|
||||
using InnovEnergy.Lib.Time.Unix;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using static System.Text.Json.JsonSerializer;
|
||||
|
||||
namespace InnovEnergy.App.SaliMax.SystemConfig;
|
||||
|
||||
// shut up trim warnings
|
||||
#pragma warning disable IL2026
|
||||
|
||||
public record SalimaxConfig //TODO: let IE choose from config files (Json) and connect to GUI
|
||||
{
|
||||
private static String DefaultConfigFilePath => Path.Combine(Environment.CurrentDirectory, "config.json");
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
|
||||
public Decimal MinSoc { get; set; }
|
||||
public UnixTime LastEoc { get; set; }
|
||||
public Decimal PConstant { get; set; }
|
||||
public Decimal ForceChargePower { get; set; }
|
||||
public Decimal ForceDischargePower { get; set; }
|
||||
public Int32 MaxInverterPower { get; set; }
|
||||
public Decimal GridSetPoint { get; set; }
|
||||
public Decimal SelfDischargePower { get; set; }
|
||||
public Decimal HoldSocZone { get; set; }
|
||||
public Decimal ControllerPConstant { get; set; }
|
||||
|
||||
public static SalimaxConfig Default => new()
|
||||
{
|
||||
MinSoc = 20m,
|
||||
LastEoc = UnixTime.Epoch,
|
||||
PConstant = .5m,
|
||||
ForceChargePower = 1_000_000m,
|
||||
ForceDischargePower = -1_000_000m,
|
||||
MaxInverterPower = 32_000,
|
||||
GridSetPoint = 0.0m,
|
||||
SelfDischargePower = 200m, // TODO: multiple batteries
|
||||
HoldSocZone = 1m, // TODO: find better name,
|
||||
ControllerPConstant = 0.5m
|
||||
};
|
||||
|
||||
|
||||
public Task Save(String? path = null)
|
||||
{
|
||||
//DefaultConfigFilePath.WriteLine("Saving data");
|
||||
var jsonString = Serialize(this, JsonOptions);
|
||||
return File.WriteAllTextAsync(path ?? DefaultConfigFilePath, jsonString);
|
||||
}
|
||||
|
||||
public async Task<SalimaxConfig> Load(String? path = null)
|
||||
{
|
||||
var configFilePath = path ?? DefaultConfigFilePath;
|
||||
try
|
||||
{
|
||||
var jsonString = await File.ReadAllTextAsync(configFilePath);
|
||||
return Deserialize<SalimaxConfig>(jsonString)!;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Couldn't read config file {configFilePath}, using default config");
|
||||
e.Message.WriteLine();
|
||||
return Default;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,241 +0,0 @@
|
|||
#define BatteriesAllowed
|
||||
|
||||
using InnovEnergy.App.SaliMax.Controller;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using InnovEnergy.Lib.Units;
|
||||
|
||||
|
||||
namespace InnovEnergy.App.SaliMax;
|
||||
|
||||
public static class Topology
|
||||
{
|
||||
private static String Separator(Decimal power)
|
||||
{
|
||||
const String chargingSeparator = ">>>>>>>>>>";
|
||||
const String dischargingSeparator = "<<<<<<<<<";
|
||||
|
||||
return power > 0 ? chargingSeparator : dischargingSeparator;
|
||||
}
|
||||
|
||||
public static Decimal Round3(this Decimal d)
|
||||
{
|
||||
return d.RoundToSignificantDigits(3);
|
||||
}
|
||||
|
||||
public static void Print(StatusRecord s)
|
||||
{
|
||||
const Int32 height = 25;
|
||||
|
||||
var calculatedActivePwr = - s.InverterStatus!.Ac.ActivePower;
|
||||
var measuredActivePwr = (s.InverterStatus.SumActivePowerL1 + s.InverterStatus.SumActivePowerL2 +
|
||||
s.InverterStatus.SumActivePowerL3) * -1;
|
||||
|
||||
measuredActivePwr.WriteLine(" : measured Sum of Active Pwr ");
|
||||
|
||||
var setValueCosPhi = s.InverterStatus.CosPhiSetValue;
|
||||
var setValueApparentPower = s.InverterStatus.ApparentPowerSetValue;
|
||||
|
||||
|
||||
#if AmptAvailable
|
||||
var pvPower = (s.AmptStatus!.Devices[0].Dc.Voltage * s.AmptStatus.Devices[0].Dc.Current + s.AmptStatus!.Devices[1].Dc.Voltage * s.AmptStatus.Devices[1].Dc.Current).Round0(); // TODO using one Ampt
|
||||
#else
|
||||
var pvPower = 0;
|
||||
#endif
|
||||
var criticalLoadPower = (s.AcInToAcOutMeterStatus!.Ac.ActivePower.Value).Round3();
|
||||
|
||||
var dcTotalPower = -s.DcDcStatus!.TotalDcPower;
|
||||
var gridSeparator = Separator(s.GridMeterStatus!.Ac.ActivePower);
|
||||
var inverterSeparator = Separator(measuredActivePwr);
|
||||
var dcSeparator = Separator(dcTotalPower);
|
||||
var something = measuredActivePwr + criticalLoadPower;
|
||||
var gridLoadPower = (s.GridMeterStatus!.Ac.ActivePower - something).Value.Round3();
|
||||
|
||||
|
||||
|
||||
////////////////// Grid //////////////////////
|
||||
var boxGrid = AsciiArt.CreateBox
|
||||
(
|
||||
"Grid",
|
||||
s.GridMeterStatus.Ac.L1.Voltage.Value.V(),
|
||||
s.GridMeterStatus.Ac.L2.Voltage.Value.V(),
|
||||
s.GridMeterStatus.Ac.L3.Voltage.Value.V()
|
||||
).AlignCenterVertical(height);
|
||||
|
||||
var gridAcBusArrow = AsciiArt.CreateHorizontalArrow(s.GridMeterStatus!.Ac.ActivePower, gridSeparator)
|
||||
.AlignCenterVertical(height);
|
||||
|
||||
|
||||
////////////////// Ac Bus //////////////////////
|
||||
var boxAcBus = AsciiArt.CreateBox
|
||||
(
|
||||
"AC Bus",
|
||||
s.InverterStatus.Ac.L1.Voltage.Value.V(),
|
||||
s.InverterStatus.Ac.L2.Voltage.Value.V(),
|
||||
s.InverterStatus.Ac.L3.Voltage.Value.V()
|
||||
);
|
||||
|
||||
var boxLoad = AsciiArt.CreateBox
|
||||
(
|
||||
"",
|
||||
"LOAD",
|
||||
""
|
||||
);
|
||||
|
||||
var loadRect = StringUtils.AlignBottom(CreateRect(boxAcBus, boxLoad, gridLoadPower), height);
|
||||
|
||||
var acBusInvertArrow = AsciiArt.CreateHorizontalArrow(measuredActivePwr, inverterSeparator)
|
||||
.AlignCenterVertical(height);
|
||||
|
||||
//////////////////// Inverter /////////////////////////
|
||||
var inverterBox = AsciiArt.CreateBox
|
||||
(
|
||||
"",
|
||||
"Inverter",
|
||||
""
|
||||
).AlignCenterVertical(height);
|
||||
|
||||
var inverterArrow = AsciiArt.CreateHorizontalArrow(measuredActivePwr, inverterSeparator)
|
||||
.AlignCenterVertical(height);
|
||||
|
||||
|
||||
//////////////////// DC Bus /////////////////////////
|
||||
var dcBusBox = AsciiArt.CreateBox
|
||||
(
|
||||
"DC Bus",
|
||||
(s.InverterStatus.ActualDcLinkVoltageLowerHalfExt.Value + s.InverterStatus.ActualDcLinkVoltageUpperHalfExt.Value).V(),
|
||||
""
|
||||
);
|
||||
|
||||
var pvBox = AsciiArt.CreateBox
|
||||
(
|
||||
"MPPT",
|
||||
((s.AmptStatus!.Devices[0].Strings[0].Voltage.Value + s.AmptStatus!.Devices[0].Strings[1].Voltage.Value) / 2).V(),
|
||||
""
|
||||
);
|
||||
|
||||
var pvRect = StringUtils.AlignTop(CreateRect(pvBox, dcBusBox, pvPower), height);
|
||||
|
||||
var dcBusArrow = AsciiArt.CreateHorizontalArrow(-s.DcDcStatus!.Left.Power, dcSeparator)
|
||||
.AlignCenterVertical(height);
|
||||
|
||||
//////////////////// Dc/Dc /////////////////////////
|
||||
|
||||
var dcBox = AsciiArt.CreateBox( "Dc/Dc", s.DcDcStatus.Right.Voltage.Value.V(), "").AlignCenterVertical(height);
|
||||
|
||||
var topology = "";
|
||||
|
||||
if (s.BatteriesStatus != null)
|
||||
{
|
||||
var numBatteries = s.BatteriesStatus.Children.Count;
|
||||
|
||||
// Create an array of battery arrows using LINQ
|
||||
var dcArrows = s
|
||||
.BatteriesStatus.Children
|
||||
.Select(b => AsciiArt.CreateHorizontalArrow(b.Dc.Power, Separator(b.Dc.Power)))
|
||||
.ToArray();
|
||||
|
||||
// Create a rectangle from the array of arrows and align it vertically
|
||||
var dcArrowRect = CreateRect(dcArrows).AlignCenterVertical(height);
|
||||
|
||||
//////////////////// Batteries /////////////////////////
|
||||
|
||||
var batteryBox = new String[numBatteries];
|
||||
|
||||
for (var i = 0; i < numBatteries; i++)
|
||||
{
|
||||
if (s.BatteriesStatus.Children[i] != null)
|
||||
{
|
||||
batteryBox[i] = AsciiArt.CreateBox
|
||||
(
|
||||
"Battery " + (i+1),
|
||||
s.BatteriesStatus.Children[i].Dc.Voltage .Value.V(),
|
||||
s.BatteriesStatus.Children[i].Soc .Value.Percent(),
|
||||
s.BatteriesStatus.Children[i].Temperature .Value.Celsius(),
|
||||
s.BatteriesStatus.Children[i].Dc.Current .Value.A(),
|
||||
s.BatteriesStatus.Children[i].TotalCurrent.Value.A()
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
batteryBox[i] = AsciiArt.CreateBox
|
||||
(
|
||||
"Battery " + (i+1),
|
||||
"not detected"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var batteryRect = CreateRect(batteryBox).AlignCenterVertical(height);
|
||||
|
||||
|
||||
var avgBatteryBox = "";
|
||||
|
||||
if (s.BatteriesStatus.Combined != null)
|
||||
{
|
||||
avgBatteryBox = AsciiArt.CreateBox
|
||||
(
|
||||
"Batteries",
|
||||
s.BatteriesStatus.Combined.CellsVoltage,
|
||||
s.BatteriesStatus.Combined.Soc,
|
||||
s.BatteriesStatus.Combined.Temperature,
|
||||
s.BatteriesStatus.Combined.Dc.Current,
|
||||
s.BatteriesStatus.Combined.Alarms.Count > 0 ? String.Join(Environment.NewLine, s.BatteriesStatus.Combined.Alarms) : "No Alarm"
|
||||
).AlignCenterVertical(height);
|
||||
}
|
||||
|
||||
|
||||
topology = boxGrid.SideBySideWith(gridAcBusArrow, "")
|
||||
.SideBySideWith(loadRect, "")
|
||||
.SideBySideWith(acBusInvertArrow, "")
|
||||
.SideBySideWith(inverterBox, "")
|
||||
.SideBySideWith(inverterArrow, "")
|
||||
.SideBySideWith(pvRect, "")
|
||||
.SideBySideWith(dcBusArrow, "")
|
||||
.SideBySideWith(dcBox, "")
|
||||
.SideBySideWith(dcArrowRect, "")
|
||||
.SideBySideWith(batteryRect, "")
|
||||
.SideBySideWith(avgBatteryBox, "")+ "\n";
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
topology = boxGrid.SideBySideWith(gridAcBusArrow, "")
|
||||
.SideBySideWith(loadRect, "")
|
||||
.SideBySideWith(acBusInvertArrow, "")
|
||||
.SideBySideWith(inverterBox, "")
|
||||
.SideBySideWith(inverterArrow, "")
|
||||
.SideBySideWith(pvRect, "")
|
||||
.SideBySideWith(dcBusArrow, "")
|
||||
.SideBySideWith(dcBox, "") + "\n";
|
||||
}
|
||||
|
||||
Console.WriteLine(topology);
|
||||
}
|
||||
|
||||
private static String CreateRect(String boxTop, String boxBottom, Decimal power)
|
||||
{
|
||||
var powerArrow = AsciiArt.CreateVerticalArrow(power);
|
||||
var boxes = new[] { boxTop, powerArrow, boxBottom };
|
||||
var maxWidth = boxes.Max(l => l.Width());
|
||||
|
||||
var rect = boxes.Select(l => l.AlignCenterHorizontal(maxWidth)).JoinLines();
|
||||
return rect;
|
||||
}
|
||||
|
||||
private static String CreateRect(String boxTop, String boxBottom)
|
||||
{
|
||||
var boxes = new[] { boxTop, boxBottom };
|
||||
var maxWidth = boxes.Max(l => l.Width());
|
||||
|
||||
var rect = boxes.Select(l => l.AlignCenterHorizontal(maxWidth)).JoinLines();
|
||||
return rect;
|
||||
}
|
||||
|
||||
private static String CreateRect(String[] boxes)
|
||||
{
|
||||
var maxWidth = boxes.Max(l => l.Width());
|
||||
var rect = boxes.Select(l => l.AlignCenterHorizontal(maxWidth)).JoinLines();
|
||||
return rect;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
host=ie-entwicklung@10.2.3.104
|
||||
|
||||
tunnel() {
|
||||
name=$1
|
||||
ip=$2
|
||||
rPort=$3
|
||||
lPort=$4
|
||||
|
||||
echo -n "localhost:$lPort $name "
|
||||
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.3
|
||||
done
|
||||
|
||||
echo "ok"
|
||||
}
|
||||
|
||||
echo ""
|
||||
|
||||
tunnel "Trumpf Inverter (http) " 192.168.1.2 80 7001
|
||||
tunnel "Trumpf DCDC (http) " 192.168.1.3 80 7002
|
||||
tunnel "Emu Meter (http) " 192.168.1.241 80 7003
|
||||
tunnel "ADAM (http) " 192.168.1.242 80 7004
|
||||
tunnel "AMPT (http) " 192.168.1.249 8080 7005
|
||||
|
||||
tunnel "Trumpf Inverter (modbus)" 192.168.1.2 502 5001
|
||||
tunnel "Trumpf DCDC (modbus) " 192.168.1.3 502 5002
|
||||
tunnel "Emu Meter (modbus) " 192.168.1.241 502 5003
|
||||
tunnel "ADAM (modbus) " 192.168.1.242 502 5004
|
||||
tunnel "AMPT (modbus) " 192.168.1.249 502 5005
|
||||
|
||||
echo
|
||||
echo "press any key to close the tunnels ..."
|
||||
read -r -n 1 -s
|
||||
kill $(jobs -p)
|
||||
echo "done"
|
|
@ -1,43 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
host=ie-entwicklung@10.2.3.115
|
||||
|
||||
tunnel() {
|
||||
name=$1
|
||||
ip=$2
|
||||
rPort=$3
|
||||
lPort=$4
|
||||
|
||||
echo -n "localhost:$lPort $name "
|
||||
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.3
|
||||
done
|
||||
|
||||
echo "ok"
|
||||
}
|
||||
|
||||
echo ""
|
||||
|
||||
tunnel "Trumpf Inverter (http) " 10.0.2.1 80 8001
|
||||
tunnel "Trumpf DCDC (http) " 10.0.3.1 80 8002
|
||||
tunnel "Ext Emu Meter (http) " 10.0.4.1 80 8003
|
||||
tunnel "Int Emu Meter (http) " 10.0.4.2 80 8004
|
||||
tunnel "AMPT (http) " 10.0.5.1 8080 8005
|
||||
|
||||
tunnel "Trumpf Inverter (modbus)" 10.0.2.1 502 5001
|
||||
tunnel "Trumpf DCDC (modbus) " 10.0.3.1 502 5002
|
||||
tunnel "Ext Emu Meter (modbus) " 10.0.4.1 502 5003
|
||||
tunnel "Int Emu Meter " 10.0.4.2 502 5004
|
||||
tunnel "AMPT (modbus) " 10.0.5.1 502 5005
|
||||
|
||||
|
||||
|
||||
echo
|
||||
echo "press any key to close the tunnels ..."
|
||||
read -r -n 1 -s
|
||||
kill $(jobs -p)
|
||||
echo "done"
|
Loading…
Reference in New Issue