Create SodiStore solution and update Battery communication unit
This commit is contained in:
parent
ac54fc6e2e
commit
0e94d9c60d
|
@ -9,6 +9,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../Lib/Devices/BatteryDeligreen/BatteryDeligreen.csproj" />
|
||||
<ProjectReference Include="..\..\Lib\Units\Units.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
using InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
|
||||
namespace InnovEnergy.App.DeligreenBatteryCommunication;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(2);
|
||||
|
||||
// private static readonly Channel? BatteriesChannel;
|
||||
// private static readonly Channel? BatteriesChannel;
|
||||
|
||||
private const String Port = "/dev/ttyUSB0";
|
||||
|
||||
|
@ -13,34 +15,57 @@ internal static class Program
|
|||
{
|
||||
Console.WriteLine("Hello, Deligreen World!");
|
||||
|
||||
// BatteriesChannel = new SerialPortChannel(Port, BaudRate, Parity, DataBits, StopBits);
|
||||
// BatteriesChannel = new SerialPortChannel(Port, BaudRate, Parity, DataBits, StopBits);
|
||||
|
||||
}
|
||||
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("Starting Battery Communication");
|
||||
|
||||
var batteryDevices = new BatteryDeligreenDevice(Port);
|
||||
|
||||
while (true)
|
||||
var listOfBatteries = new List<BatteryDeligreenDevice>
|
||||
{
|
||||
new BatteryDeligreenDevice(Port, 0),
|
||||
new BatteryDeligreenDevice(Port, 1),
|
||||
new BatteryDeligreenDevice(Port, 2),
|
||||
new BatteryDeligreenDevice(Port, 3),
|
||||
new BatteryDeligreenDevice(Port, 4)
|
||||
};
|
||||
|
||||
var batteryDevices = new BatteryDeligreenDevices(listOfBatteries);
|
||||
|
||||
Console.WriteLine("Starting Battery Communication");
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine("***************************** New Frame *********************************");
|
||||
Console.WriteLine($"First Reading Timestamp: {DateTime.Now:HH:mm:ss.fff}");
|
||||
// Read telemetry data asynchronously
|
||||
await batteryDevices.ReadTelemetryData();
|
||||
Console.WriteLine($"Last Timestamp: {DateTime.Now:HH:mm:ss.fff}");
|
||||
var startTime = DateTime.Now;
|
||||
Console.WriteLine("***************************** Reading Battery Data *********************************************");
|
||||
Console.WriteLine($"Start Reading all Batteries: {startTime}");
|
||||
var batteriesRecord = batteryDevices.Read();
|
||||
var stopTime = DateTime.Now;
|
||||
Console.WriteLine($"Finish Reading all Batteries: {stopTime}");
|
||||
|
||||
Console.WriteLine("Time used for reading all batteries:" + (stopTime - startTime));
|
||||
|
||||
Console.WriteLine("Average SOC " + batteriesRecord?.Soc);
|
||||
Console.WriteLine("SOC Battery 0 : " + batteriesRecord?.Devices[0].BatteryDeligreenDataRecord.Soc);
|
||||
Console.WriteLine("SOC Battery 1 : " + batteriesRecord?.Devices[1].BatteryDeligreenDataRecord.Soc);
|
||||
Console.WriteLine("SOC Battery 2 : " + batteriesRecord?.Devices[2].BatteryDeligreenDataRecord.Soc);
|
||||
Console.WriteLine("SOC Battery 3 : " + batteriesRecord?.Devices[3].BatteryDeligreenDataRecord.Soc);
|
||||
Console.WriteLine("SOC Battery 4 : " + batteriesRecord?.Devices[4].BatteryDeligreenDataRecord.Soc);
|
||||
Console.WriteLine("Min Soc " + batteriesRecord?.CurrentMinSoc);
|
||||
Console.WriteLine("count " + batteriesRecord?.Devices.Count);
|
||||
|
||||
// Wait for 2 seconds before the next reading
|
||||
await Task.Delay(2000); // Delay in milliseconds (2000ms = 2 seconds)
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Handle exception and print the error
|
||||
Console.WriteLine(e);
|
||||
Console.WriteLine(e + " This the first try loop ");
|
||||
await Task.Delay(2000); // Delay in milliseconds (2000ms = 2 seconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
"MinSoc": Number, 0 - 100 this is the minimum State of Charge that the batteries must not go below,
|
||||
"ForceCalibrationCharge": Boolean (true or false), A flag to force a calibration charge,
|
||||
"DisplayIndividualBatteries": Boolean (true or false), To display the indvidual batteries
|
||||
"PConstant": Number 0 - 1, P value of our controller.
|
||||
"GridSetPoint": Number in Watts, The set point of our controller.
|
||||
"BatterySelfDischargePower": Number, 200, this a physical measurement of the self discharging power.
|
||||
"HoldSocZone": Number, 1, This is magic number for the soft landing factor.
|
||||
"IslandMode": { // Dc Link Voltage in Island mode
|
||||
"AcDc": {
|
||||
"MaxDcLinkVoltage": Number, 810, Max Dc Link Voltage,
|
||||
"MinDcLinkVoltage": Number, 690, Min Dc Link Voltage,
|
||||
"ReferenceDcLinkVoltage": Number, 750, Reference Dc Link
|
||||
},
|
||||
"DcDc": {
|
||||
"LowerDcLinkVoltage": Number, 50, Lower Dc Link Window ,
|
||||
"ReferenceDcLinkVoltage": 750, reference Dc Link
|
||||
"UpperDcLinkVoltage": Number, 50, Upper Dc Link Window ,
|
||||
}
|
||||
},
|
||||
"GridTie": {// Dc Link Voltage in GrieTie mode
|
||||
"AcDc": {
|
||||
"MaxDcLinkVoltage":Number, 780, Max Dc Link Voltage,
|
||||
"MinDcLinkVoltage": Number, 690, Min Dc Link Voltage,
|
||||
"ReferenceDcLinkVoltage": Number, 750, Reference Dc Link
|
||||
},
|
||||
"DcDc": {
|
||||
"LowerDcLinkVoltage": Number, 20, Lower Dc Link Window ,
|
||||
"ReferenceDcLinkVoltage": 750, reference Dc Link
|
||||
"UpperDcLinkVoltage": Number, 20, Upper Dc Link Window ,
|
||||
}
|
||||
},
|
||||
"MaxBatteryChargingCurrent":Number, 0 - 210, Max Charging current by DcDc
|
||||
"MaxBatteryDischargingCurrent":Number, 0 - 210, Max Discharging current by DcDc
|
||||
"MaxDcPower": Number, 0 - 10000, Max Power exported/imported by DcDc (10000 is the maximum)
|
||||
"MaxChargeBatteryVoltage": Number, 57, Max Charging battery Voltage
|
||||
"MinDischargeBatteryVoltage": Number, 0, Min Charging Battery Voltage
|
||||
"Devices": { This is All Salimax devices (including offline ones)
|
||||
"RelaysIp": {
|
||||
"DeviceState": 1, // 0: is not present, 1: Present and Can be mesured, 2: Present but must be computed/calculted
|
||||
"Host": "10.0.1.1", // Ip @ of the device in the local network
|
||||
"Port": 502 // port
|
||||
},
|
||||
"GridMeterIp": {
|
||||
"DeviceState": 1,
|
||||
"Host": "10.0.4.1",
|
||||
"Port": 502
|
||||
},
|
||||
"PvOnAcGrid": {
|
||||
"DeviceState": 0, // If a device is not present
|
||||
"Host": "false", // this is not important
|
||||
"Port": 0 // this is not important
|
||||
},
|
||||
"LoadOnAcGrid": {
|
||||
"DeviceState": 2, // this is a computed device
|
||||
"Host": "true",
|
||||
"Port": 0
|
||||
},
|
||||
"PvOnAcIsland": {
|
||||
"DeviceState": 0,
|
||||
"Host": "false",
|
||||
"Port": 0
|
||||
},
|
||||
"IslandBusLoadMeterIp": {
|
||||
"DeviceState": 1,
|
||||
"Host": "10.0.4.2",
|
||||
"Port": 502
|
||||
},
|
||||
"TruConvertAcIp": {
|
||||
"DeviceState": 1,
|
||||
"Host": "10.0.2.1",
|
||||
"Port": 502
|
||||
},
|
||||
"PvOnDc": {
|
||||
"DeviceState": 1,
|
||||
"Host": "10.0.5.1",
|
||||
"Port": 502
|
||||
},
|
||||
"LoadOnDc": {
|
||||
"DeviceState": 0,
|
||||
"Host": "false",
|
||||
"Port": 0
|
||||
},
|
||||
"TruConvertDcIp": {
|
||||
"DeviceState": 1,
|
||||
"Host": "10.0.3.1",
|
||||
"Port": 502
|
||||
},
|
||||
"BatteryIp": {
|
||||
"DeviceState": 1,
|
||||
"Host": "localhost",
|
||||
"Port": 6855
|
||||
},
|
||||
"BatteryNodes": [ // this is a list of nodes
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6
|
||||
]
|
||||
},
|
||||
"S3": { // this is parameters of S3 Buckets and co
|
||||
"Bucket": "8-3e5b3069-214a-43ee-8d85-57d72000c19d",
|
||||
"Region": "sos-ch-dk-2",
|
||||
"Provider": "exo.io",
|
||||
"Key": "EXO502627299197f83e8b090f63",
|
||||
"Secret": "jUNYJL6B23WjndJnJlgJj4rc1i7uh981u5Aba5xdA5s",
|
||||
"ContentType": "text/plain; charset=utf-8",
|
||||
"Host": "8-3e5b3069-214a-43ee-8d85-57d72000c19d.sos-ch-dk-2.exo.io",
|
||||
"Url": "https://8-3e5b3069-214a-43ee-8d85-57d72000c19d.sos-ch-dk-2.exo.io"
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,501 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
|
||||
<!--Created by yEd 3.20.1-->
|
||||
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
|
||||
<key for="port" id="d1" yfiles.type="portgraphics"/>
|
||||
<key for="port" id="d2" yfiles.type="portgeometry"/>
|
||||
<key for="port" id="d3" yfiles.type="portuserdata"/>
|
||||
<key attr.name="url" attr.type="string" for="node" id="d4"/>
|
||||
<key attr.name="description" attr.type="string" for="node" id="d5"/>
|
||||
<key for="node" id="d6" yfiles.type="nodegraphics"/>
|
||||
<key for="graphml" id="d7" yfiles.type="resources"/>
|
||||
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
|
||||
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
|
||||
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
|
||||
<graph edgedefault="directed" id="G">
|
||||
<data key="d0"/>
|
||||
<node id="n0">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="1093.125" y="-372.5"/>
|
||||
<y:Fill color="#B4B4FF" transparent="false"/>
|
||||
<y:BorderStyle color="#000068" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B4B4FF" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000068" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">19</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000068" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✓
|
||||
K2 ✓
|
||||
K3 ✘<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n1">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="923.125" y="-372.5"/>
|
||||
<y:Fill color="#B4B4FF" transparent="false"/>
|
||||
<y:BorderStyle color="#000068" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B4B4FF" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000068" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="29.567977905273438" xml:space="preserve" y="4.0">3</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000068" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✓
|
||||
K2 ✓
|
||||
K3 ✘<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="561.625" y="-350.0"/>
|
||||
<y:Fill color="#FFEB9C" transparent="false"/>
|
||||
<y:BorderStyle color="#683A00" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFEB9C" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#683A00" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="29.567977905273438" xml:space="preserve" y="4.0">9</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#683A00" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✓
|
||||
K2 ✘
|
||||
K3 ✘<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n3">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="753.125" y="-372.5"/>
|
||||
<y:Fill color="#B4B4FF" transparent="false"/>
|
||||
<y:BorderStyle color="#000068" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B4B4FF" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000068" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="29.567977905273438" xml:space="preserve" y="4.0">1</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000068" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✓
|
||||
K2 ✘
|
||||
K3 ✘<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n4">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="361.5" y="-207.5"/>
|
||||
<y:Fill color="#FFEB9C" transparent="false"/>
|
||||
<y:BorderStyle color="#683A00" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFEB9C" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#683A00" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">13</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#683A00" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✓
|
||||
K2 ✘
|
||||
K3 ✓<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n5">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="170.0" y="-230.0"/>
|
||||
<y:Fill color="#FFEB9C" transparent="false"/>
|
||||
<y:BorderStyle color="#683A00" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFEB9C" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#683A00" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">29</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#683A00" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✓
|
||||
K2 ✘
|
||||
K3 ✓<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n6">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="1284.625" y="-395.0"/>
|
||||
<y:Fill color="#B4B4FF" transparent="false"/>
|
||||
<y:BorderStyle color="#000068" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B4B4FF" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000068" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">23</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000068" verticalTextPosition="top" visible="true" width="30.628189086914062" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✓
|
||||
K2 ✓
|
||||
K3 ✓<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n7">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="923.125" y="-492.5"/>
|
||||
<y:Fill color="#B4B4FF" transparent="false"/>
|
||||
<y:BorderStyle color="#000068" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B4B4FF" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000068" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="29.567977905273438" xml:space="preserve" y="4.0">5</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000068" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✓
|
||||
K2 ✘
|
||||
K3 ✓<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n8">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="1093.125" y="-492.5"/>
|
||||
<y:Fill color="#B4B4FF" transparent="false"/>
|
||||
<y:BorderStyle color="#000068" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B4B4FF" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000068" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="29.567977905273438" xml:space="preserve" y="4.0">7</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000068" verticalTextPosition="top" visible="true" width="30.628189086914062" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✓
|
||||
K2 ✓
|
||||
K3 ✓<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n9">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="361.5" y="-350.0"/>
|
||||
<y:Fill color="#FFEB9C" transparent="false"/>
|
||||
<y:BorderStyle color="#683A00" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFEB9C" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#683A00" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">11</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#683A00" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✓
|
||||
K2 ✓
|
||||
K3 ✘<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n10">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="170.0" y="-110.0"/>
|
||||
<y:Fill color="#FFEB9C" transparent="false"/>
|
||||
<y:BorderStyle color="#683A00" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFEB9C" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#683A00" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">15</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#683A00" verticalTextPosition="top" visible="true" width="30.628189086914062" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✓
|
||||
K2 ✓
|
||||
K3 ✓<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n11">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="753.125" y="-492.5"/>
|
||||
<y:Fill color="#B4B4FF" transparent="false"/>
|
||||
<y:BorderStyle color="#000068" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B4B4FF" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000068" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">21</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000068" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✓
|
||||
K2 ✘
|
||||
K3 ✓<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n12">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="561.625" y="-470.0"/>
|
||||
<y:Fill color="#B4B4FF" transparent="false"/>
|
||||
<y:BorderStyle color="#000068" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B4B4FF" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000068" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">17</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000068" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✓
|
||||
K2 ✘
|
||||
K3 ✘<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n13">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="361.5" y="-470.0"/>
|
||||
<y:Fill color="#FFEB9C" transparent="false"/>
|
||||
<y:BorderStyle color="#683A00" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFEB9C" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#683A00" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">25</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#683A00" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✓
|
||||
K2 ✘
|
||||
K3 ✘<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n14">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="170.0" y="-350.0"/>
|
||||
<y:Fill color="#FFEB9C" transparent="false"/>
|
||||
<y:BorderStyle color="#683A00" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFEB9C" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#683A00" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">27</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#683A00" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✓
|
||||
K2 ✓
|
||||
K3 ✘<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n15">
|
||||
<data key="d4" xml:space="preserve"/>
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="0.0" y="-110.0"/>
|
||||
<y:Fill color="#FFEB9C" color2="#FF0000" transparent="false"/>
|
||||
<y:BorderStyle color="#683A00" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFEB9C" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#683A00" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">31</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#683A00" verticalTextPosition="top" visible="true" width="30.628189086914062" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✓
|
||||
K2 ✓
|
||||
K3 ✓<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<edge id="e0" source="n5" target="n4">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-22.5"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="delta"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="side_slider" preferredPlacement="center" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.108367919921875" x="33.19581604003906" xml:space="preserve" y="2.0">turn off
|
||||
Inverters<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e1" source="n2" target="n3">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="22.5"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="center" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="57.61639404296875" x="31.941802978515625" xml:space="preserve" y="2.0">switch to
|
||||
grid tie<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e2" source="n4" target="n2">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="30.0">
|
||||
<y:Point x="482.0" y="-162.5"/>
|
||||
<y:Point x="511.125" y="-275.0"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" anchorX="52.86815850220398" anchorY="-29.097424414959733" configuration="AutoFlippingLabel" distance="5.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="center" ratio="0.5" textColor="#000000" upX="-0.9680839425312168" upY="-0.25062617623308175" verticalTextPosition="bottom" visible="true" width="58.68438720703125" x="33.173348119879954" xml:space="preserve" y="-91.00760492412827">K3's open<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="5.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="true" ratio="0.5" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e3" source="n3" target="n1">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="center" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="50.044342041015625" x="24.977828979492188" xml:space="preserve" y="2.0">close K2<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e4" source="n1" target="n0">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="center" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.108367919921875" x="22.445816040039062" xml:space="preserve" y="2.0">turn on
|
||||
Inverters<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e5" source="n0" target="n6">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="22.5"/>
|
||||
<y:LineStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="center" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="58.492401123046875" x="31.503799438476562" xml:space="preserve" y="2.0">K3's close<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e6" source="n9" target="n2">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="50.236328125" x="39.9443359375" xml:space="preserve" y="-22.344114303588867">open K2<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="true" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e7" source="n10" target="n4">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="22.5">
|
||||
<y:Point x="290.5" y="-65.0"/>
|
||||
<y:Point x="311.0" y="-140.0"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" anchorX="75.68078078809617" anchorY="-7.379352680589193" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="center" ratio="0.5" textColor="#000000" upX="-0.9646152654368906" upY="-0.26366150588608345" verticalTextPosition="bottom" visible="true" width="50.236328125" x="56.056537569061355" xml:space="preserve" y="-61.202041482663645">open K2<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e8" source="n8" target="n6">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-22.5">
|
||||
<y:Point x="1213.625" y="-447.5"/>
|
||||
<y:Point x="1234.125" y="-372.5"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" anchorX="16.16576645645864" anchorY="21.121410140198805" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="center" ratio="0.5" textColor="#000000" upX="0.9646152654368907" upY="-0.263661505886083" verticalTextPosition="bottom" visible="true" width="55.108367919921875" x="16.16576645645864" xml:space="preserve" y="11.448136537337454">turn on
|
||||
Inverters<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e9" source="n7" target="n8">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="center" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="50.044342041015625" x="24.977828979492188" xml:space="preserve" y="2.0">close K2<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e10" source="n12" target="n3">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-22.5">
|
||||
<y:Point x="682.125" y="-425.0"/>
|
||||
<y:Point x="702.625" y="-350.0"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" anchorX="17.05167531189511" anchorY="24.362540099112607" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="center" ratio="0.5" textColor="#000000" upX="0.9646152654368907" upY="-0.263661505886083" verticalTextPosition="bottom" visible="true" width="48.38832092285156" x="17.05167531189511" xml:space="preserve" y="14.689266496251257">turn off
|
||||
inverter<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e11" source="n15" target="n10">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="side_slider" preferredPlacement="center" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="54.13636779785156" x="22.93181610107422" xml:space="preserve" y="2.0">turn off
|
||||
inverters<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e12" source="n11" target="n7">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="center" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="54.13636779785156" x="22.93181610107422" xml:space="preserve" y="2.0">turn off
|
||||
inverters<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e13" source="n14" target="n9">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="center" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="54.13636779785156" x="33.68181610107422" xml:space="preserve" y="2.0">turn off
|
||||
inverters<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e14" source="n13" target="n2">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-30.0">
|
||||
<y:Point x="482.0" y="-425.0"/>
|
||||
<y:Point x="511.125" y="-335.0"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" anchorX="19.919637075116952" anchorY="31.15848493507559" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="center" ratio="0.5" textColor="#000000" upX="0.9514217507056354" upY="-0.3078906498811288" verticalTextPosition="bottom" visible="true" width="54.13636779785156" x="19.919637075116952" xml:space="preserve" y="19.86252238622422">turn off
|
||||
inverters<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
</graph>
|
||||
<data key="d7">
|
||||
<y:Resources/>
|
||||
</data>
|
||||
</graphml>
|
|
@ -0,0 +1,487 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
|
||||
<!--Created by yEd 3.20.1-->
|
||||
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
|
||||
<key for="port" id="d1" yfiles.type="portgraphics"/>
|
||||
<key for="port" id="d2" yfiles.type="portgeometry"/>
|
||||
<key for="port" id="d3" yfiles.type="portuserdata"/>
|
||||
<key attr.name="url" attr.type="string" for="node" id="d4"/>
|
||||
<key attr.name="description" attr.type="string" for="node" id="d5"/>
|
||||
<key for="node" id="d6" yfiles.type="nodegraphics"/>
|
||||
<key for="graphml" id="d7" yfiles.type="resources"/>
|
||||
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
|
||||
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
|
||||
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
|
||||
<graph edgedefault="directed" id="G">
|
||||
<data key="d0"/>
|
||||
<node id="n0">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="1099.0" y="-470.0"/>
|
||||
<y:Fill color="#FFEB9C" transparent="false"/>
|
||||
<y:BorderStyle color="#683A00" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFEB9C" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#683A00" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">28</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#683A00" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✘
|
||||
K2 ✘
|
||||
K3 ✓<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n1">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="929.0" y="-470.0"/>
|
||||
<y:Fill color="#FFEB9C" transparent="false"/>
|
||||
<y:BorderStyle color="#683A00" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFEB9C" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#683A00" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">24</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#683A00" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✘
|
||||
K2 ✘
|
||||
K3 ✘<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n2">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="759.0" y="-470.0"/>
|
||||
<y:Fill color="#FFEB9C" transparent="false"/>
|
||||
<y:BorderStyle color="#683A00" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFEB9C" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#683A00" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="29.567977905273438" xml:space="preserve" y="4.0">8</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#683A00" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✘
|
||||
K2 ✘
|
||||
K3 ✘<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n3">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="170.0" y="-110.0"/>
|
||||
<y:Fill color="#B4B4FF" transparent="false"/>
|
||||
<y:BorderStyle color="#000068" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B4B4FF" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000068" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="29.567977905273438" xml:space="preserve" y="4.0">6</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000068" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✘
|
||||
K2 ✓
|
||||
K3 ✓<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n4">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="534.5" y="-230.0"/>
|
||||
<y:Fill color="#B4B4FF" transparent="false"/>
|
||||
<y:BorderStyle color="#000068" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B4B4FF" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000068" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="29.567977905273438" xml:space="preserve" y="4.0">0</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000068" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✘
|
||||
K2 ✘
|
||||
K3 ✘<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n5">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="340.0" y="-110.0"/>
|
||||
<y:Fill color="#B4B4FF" transparent="false"/>
|
||||
<y:BorderStyle color="#000068" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B4B4FF" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000068" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="29.567977905273438" xml:space="preserve" y="4.0">4</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000068" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✘
|
||||
K2 ✘
|
||||
K3 ✓<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n6">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="0.0" y="-110.0"/>
|
||||
<y:Fill color="#B4B4FF" transparent="false"/>
|
||||
<y:BorderStyle color="#000068" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B4B4FF" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000068" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">22</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000068" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✘
|
||||
K2 ✓
|
||||
K3 ✓<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n7">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="340.0" y="-230.0"/>
|
||||
<y:Fill color="#B4B4FF" transparent="false"/>
|
||||
<y:BorderStyle color="#000068" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B4B4FF" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000068" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">16</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000068" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✘
|
||||
K2 ✘
|
||||
K3 ✘<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n8">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="170.0" y="-230.0"/>
|
||||
<y:Fill color="#B4B4FF" transparent="false"/>
|
||||
<y:BorderStyle color="#000068" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B4B4FF" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000068" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">20</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000068" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✘
|
||||
K2 ✘
|
||||
K3 ✓<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n9">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="170.0" y="-350.0"/>
|
||||
<y:Fill color="#B4B4FF" transparent="false"/>
|
||||
<y:BorderStyle color="#000068" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B4B4FF" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000068" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">18</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000068" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✘
|
||||
K2 ✓
|
||||
K3 ✘<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n10">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="340.0" y="-350.0"/>
|
||||
<y:Fill color="#B4B4FF" transparent="false"/>
|
||||
<y:BorderStyle color="#000068" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B4B4FF" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000068" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="29.567977905273438" xml:space="preserve" y="4.0">2</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000068" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✘
|
||||
K2 ✓
|
||||
K3 ✘<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n11">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="534.5" y="-470.0"/>
|
||||
<y:Fill color="#FFEB9C" transparent="false"/>
|
||||
<y:BorderStyle color="#683A00" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFEB9C" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#683A00" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">10</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#683A00" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✘
|
||||
K2 ✓
|
||||
K3 ✘<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n12">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="534.5" y="-590.0"/>
|
||||
<y:Fill color="#FFEB9C" transparent="false"/>
|
||||
<y:BorderStyle color="#683A00" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFEB9C" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#683A00" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">12</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#683A00" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✘
|
||||
K2 ✘
|
||||
K3 ✓<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n13">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="340.0" y="-590.0"/>
|
||||
<y:Fill color="#FFEB9C" transparent="false"/>
|
||||
<y:BorderStyle color="#683A00" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFEB9C" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#683A00" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">14</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#683A00" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✘
|
||||
K2 ✓
|
||||
K3 ✓<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n14">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="340.0" y="-470.0"/>
|
||||
<y:Fill color="#FFEB9C" transparent="false"/>
|
||||
<y:BorderStyle color="#683A00" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFEB9C" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#683A00" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">26</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#683A00" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✘
|
||||
K2 ✓
|
||||
K3 ✘<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<node id="n15">
|
||||
<data key="d5"/>
|
||||
<data key="d6">
|
||||
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
|
||||
<y:Geometry height="90.0" width="70.0" x="170.0" y="-590.0"/>
|
||||
<y:Fill color="#FFEB9C" transparent="false"/>
|
||||
<y:BorderStyle color="#683A00" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#FFEB9C" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#683A00" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="26.135955810546875" xml:space="preserve" y="4.0">30</y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="53.0323429107666" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#683A00" verticalTextPosition="top" visible="true" width="31.144195556640625" x="2.0" xml:space="preserve" y="32.34411430358887">K1 ✘
|
||||
K2 ✓
|
||||
K3 ✓<y:LabelModel><y:ErdAttributesNodeLabelModel/></y:LabelModel><y:ModelParameter><y:ErdAttributesNodeLabelModelParameter/></y:ModelParameter></y:NodeLabel>
|
||||
<y:StyleProperties>
|
||||
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="false"/>
|
||||
</y:StyleProperties>
|
||||
</y:GenericNode>
|
||||
</data>
|
||||
</node>
|
||||
<edge id="e0" source="n8" target="n7">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="58.68438720703125" x="20.657806396484375" xml:space="preserve" y="-22.344114303588896">K3's open<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="true" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="left|right" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e1" source="n1" target="n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:Arrows source="none" target="delta"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="58.492401123046875" x="20.753799438476562" xml:space="preserve" y="2.0">K3's close<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="left|right" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e2" source="n7" target="n4">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.108367919921875" x="5.0" xml:space="preserve" y="-38.688228607177734">turn off
|
||||
Inverters<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="true" ratio="0.0" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="left|right" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e3" source="n4" target="n2">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="30.0">
|
||||
<y:Point x="655.0" y="-185.0"/>
|
||||
<y:Point x="708.5" y="-395.0"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" anchorX="105.70300540936205" anchorY="-59.97368347153517" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" ratio="0.5" textColor="#000000" upX="-0.9690470117615817" upY="-0.24687626252021255" verticalTextPosition="bottom" visible="true" width="73.21649169921875" x="70.1503871107507" xml:space="preserve" y="-139.9813587213569">switch to
|
||||
island mode<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5000000000000004" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="left|right" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e4" source="n2" target="n1">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="54.13636779785156" x="22.93181610107422" xml:space="preserve" y="2.0">turn on
|
||||
inverters<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="left|right" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e5" source="n6" target="n3">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="delta"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.108367919921875" x="22.445816040039062" xml:space="preserve" y="1.999999999999993">turn off
|
||||
Inverters<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="left|right" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e6" source="n5" target="n4">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="30.0">
|
||||
<y:Point x="460.5" y="-65.0"/>
|
||||
<y:Point x="484.0" y="-155.0"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" anchorX="53.242913557579016" anchorY="-18.421155878144106" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" ratio="0.5" textColor="#000000" upX="-0.9675601644592253" upY="-0.25264070960879775" verticalTextPosition="bottom" visible="true" width="55.984375" x="33.55875897622129" xml:space="preserve" y="-77.72915843431224">K3 opens<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="true" ratio="0.5" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="left|right" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e7" source="n3" target="n5">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="50.236328125" x="24.8818359375" xml:space="preserve" y="2.0">open K2<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="left|right" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e8" source="n9" target="n10">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="54.13636779785156" x="22.93181610107422" xml:space="preserve" y="-38.688228607177734">turn off
|
||||
inverters<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="true" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="left|right" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e9" source="n10" target="n4">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-30.0">
|
||||
<y:Point x="460.5" y="-305.0"/>
|
||||
<y:Point x="484.0" y="-215.0"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" anchorX="57.83924953609818" anchorY="20.191383629556128" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" ratio="0.5" textColor="#000000" upX="0.9675601644592253" upY="-0.25264070960879753" verticalTextPosition="bottom" visible="true" width="50.236328125" x="57.83924953609818" xml:space="preserve" y="15.05163215553495">open K2<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="true" ratio="0.5" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="left|right" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e10" source="n11" target="n2">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="50.236328125" x="52.1318359375" xml:space="preserve" y="2.0">open K2<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="left|right" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e11" source="n12" target="n2">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-30.0">
|
||||
<y:Point x="655.0" y="-545.0"/>
|
||||
<y:Point x="708.5" y="-455.0"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" anchorX="64.66573623876036" anchorY="19.916163239593743" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" ratio="0.5" textColor="#000000" upX="0.8595925806889443" upY="-0.5109800340762061" verticalTextPosition="bottom" visible="true" width="55.984375" x="64.66573623876036" xml:space="preserve" y="9.520727019495672">K3 opens<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="true" ratio="0.5" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="left|right" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e12" source="n13" target="n12">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="50.236328125" x="37.1318359375" xml:space="preserve" y="2.0">open K2<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="left|right" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e13" source="n14" target="n11">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="54.13636779785156" x="35.18181610107422" xml:space="preserve" y="2.0">turn off
|
||||
inverters<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="left|right" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e14" source="n15" target="n13">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="35.0" sy="-0.0" tx="-35.0" ty="-0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="54.13636779785156" x="22.93181610107422" xml:space="preserve" y="2.0">turn off
|
||||
inverters<y:LabelModel><y:RotatedSliderEdgeLabelModel angle="0.0" autoRotationEnabled="true" distance="2.0" distanceRelativeToEdge="true" mode="side_slider"/></y:LabelModel><y:ModelParameter><y:RotatedSliderEdgeLabelModelParameter invertingSign="false" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="left|right" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
</graph>
|
||||
<data key="d7">
|
||||
<y:Resources/>
|
||||
</data>
|
||||
</graphml>
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
Prototype ie-entwicklung@10.2.3.115 Prototype
|
||||
Salimax0001 ie-entwicklung@10.2.3.104 Marti Technik (Bern)
|
||||
Salimax0002 ie-entwicklung@10.2.4.29 Weidmann d (ZG)
|
||||
Salimax0003 ie-entwicklung@10.2.4.33 Elektrotechnik Stefan GmbH
|
||||
Salimax0004 ie-entwicklung@10.2.4.32 Biohof Gubelmann (Walde)
|
||||
Salimax0004A ie-entwicklung@10.2.4.153
|
||||
Salimax0005 ie-entwicklung@10.2.4.36 Schreinerei Schönthal (Thun)
|
||||
Salimax0006 ie-entwicklung@10.2.4.35 Steakhouse Mettmenstetten
|
||||
Salimax0007 ie-entwicklung@10.2.4.154 LerchenhofHerr Twannberg
|
||||
Salimax0008 ie-entwicklung@10.2.4.113 Wittmann Kottingbrunn
|
||||
Salimax0010 ie-entwicklung@10.2.4.211 Mohatech 1 (Beat Moser)
|
||||
Salimax0011 ie-entwicklung@10.2.4.239 Thomas Tschirren (Enggistein)
|
||||
SalidomoServer ig@134.209.238.170
|
|
@ -0,0 +1,32 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="../InnovEnergy.App.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>InnovEnergy.App.SodiStoreMax</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Version="3.2.4" Include="Flurl.Http" />
|
||||
<PackageReference Version="7.0.0" Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="RabbitMQ.Client" Version="6.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../Lib/Devices/Adam6360D/Adam6360D.csproj" />
|
||||
<ProjectReference Include="../../Lib/Devices/AMPT/Ampt.csproj" />
|
||||
<ProjectReference Include="../../Lib/Devices/BatteryDeligreen/BatteryDeligreen.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/Units/Units.csproj" />
|
||||
<ProjectReference Include="../../Lib/Utils/Utils.csproj" />
|
||||
<ProjectReference Include="../../Lib/Devices/Amax5070/Amax5070.csproj" />
|
||||
<ProjectReference Include="../../Lib/Devices/Adam6060/Adam6060.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="resources\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
dotnet_version='net6.0'
|
||||
salimax_ip="$1"
|
||||
username='ie-entwicklung'
|
||||
root_password='Salimax4x25'
|
||||
set -e
|
||||
|
||||
echo -e "\n============================ Build ============================\n"
|
||||
|
||||
dotnet publish \
|
||||
./SodiStoreMax.csproj \
|
||||
-p:PublishTrimmed=false \
|
||||
-c Release \
|
||||
-r linux-x64
|
||||
|
||||
echo -e "\n============================ Deploy ============================\n"
|
||||
|
||||
rsync -v \
|
||||
--exclude '*.pdb' \
|
||||
./bin/Release/$dotnet_version/linux-x64/publish/* \
|
||||
$username@"$salimax_ip":~/salimax
|
|
@ -0,0 +1,37 @@
|
|||
#!/bin/bash
|
||||
|
||||
dotnet_version='net6.0'
|
||||
salimax_ip="$1"
|
||||
username='ie-entwicklung'
|
||||
root_password='Salimax4x25'
|
||||
|
||||
set -e
|
||||
|
||||
echo -e "\n============================ Build ============================\n"
|
||||
|
||||
dotnet publish \
|
||||
./SaliMax.csproj \
|
||||
-p:PublishTrimmed=false \
|
||||
-c Release \
|
||||
-r linux-x64
|
||||
|
||||
echo -e "\n============================ Deploy ============================\n"
|
||||
#ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.29" "10.2.4.33" "10.2.4.32" "10.2.4.36" "10.2.4.35" "10.2.4.154" "10.2.4.113" "10.2.4.211")
|
||||
#ip_addresses=("10.2.4.154" "10.2.4.29")
|
||||
ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.29" "10.2.4.33" "10.2.4.32" "10.2.4.36" "10.2.4.35" "10.2.4.154" )
|
||||
|
||||
|
||||
|
||||
for ip_address in "${ip_addresses[@]}"; do
|
||||
rsync -v \
|
||||
--exclude '*.pdb' \
|
||||
./bin/Release/$dotnet_version/linux-x64/publish/* \
|
||||
$username@"$ip_address":~/salimax
|
||||
|
||||
ssh "$username"@"$ip_address" "cd salimax && echo '$root_password' | sudo -S ./restart"
|
||||
|
||||
echo "Deployed and ran commands on $ip_address"
|
||||
done
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
#!/usr/bin/python2 -u
|
||||
# coding=utf-8
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import serial
|
||||
import logging
|
||||
from sys import argv, exit
|
||||
from datetime import datetime
|
||||
from pymodbus.pdu import ModbusRequest, ModbusResponse, ExceptionResponse
|
||||
from pymodbus.other_message import ReportSlaveIdRequest
|
||||
from pymodbus.exceptions import ModbusException
|
||||
from pymodbus.pdu import ExceptionResponse
|
||||
from pymodbus.factory import ClientDecoder
|
||||
from pymodbus.client import ModbusSerialClient as Modbus
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
|
||||
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import List, Optional, NoReturn
|
||||
|
||||
RESET_REGISTER = 0x2087
|
||||
FIRMWARE_VERSION_REGISTER = 1054
|
||||
SERIAL_STARTER_DIR = '/opt/victronenergy/serial-starter/'
|
||||
INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name'
|
||||
OUTPUT_DIR = '/data/innovenergy'
|
||||
|
||||
|
||||
class ReadLogRequest(ModbusRequest):
|
||||
|
||||
function_code = 0x42
|
||||
_rtu_frame_size = 5 # not used
|
||||
|
||||
def __init__(self, address = None, **kwargs):
|
||||
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.sub_function = 0 if address is None else 1
|
||||
self.address = address
|
||||
|
||||
# FUGLY as hell, but necessary bcs PyModbus cannot deal
|
||||
# with responses that have lengths depending on the sub_function.
|
||||
# it goes without saying that this isn't thread-safe
|
||||
ReadLogResponse._rtu_frame_size = 9 if self.sub_function == 0 else 9+128
|
||||
|
||||
def encode(self):
|
||||
|
||||
if self.sub_function == 0:
|
||||
return struct.pack('>B', self.sub_function)
|
||||
else:
|
||||
return struct.pack('>BI', self.sub_function, self.address)
|
||||
|
||||
def decode(self, data):
|
||||
self.sub_function = struct.unpack('>B', data)
|
||||
|
||||
def execute(self, context):
|
||||
print("EXECUTE1")
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
return ReadLogResponse._rtu_frame_size - 3
|
||||
|
||||
def __str__(self):
|
||||
return "ReadLogAddressRequest"
|
||||
|
||||
|
||||
class ReadLogResponse(ModbusResponse):
|
||||
|
||||
function_code = 0x42
|
||||
_rtu_frame_size = 9 # the WHOLE frame incl crc
|
||||
|
||||
def __init__(self, sub_function=0, address=b'\x00', data=None, **kwargs):
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.sub_function = sub_function
|
||||
self.address = address
|
||||
self.data = data
|
||||
|
||||
def encode(self):
|
||||
pass
|
||||
|
||||
def decode(self, data):
|
||||
self.address, self.address = struct.unpack_from(">BI", data)
|
||||
self.data = data[5:]
|
||||
|
||||
def __str__(self):
|
||||
arguments = (self.function_code, self.address)
|
||||
return "ReadLogAddressResponse(%s, %s)" % arguments
|
||||
|
||||
# unfortunately we have to monkey-patch this global table because
|
||||
# the current (victron) version of PyModbus does not have a
|
||||
# way to "register" new function-codes yet
|
||||
ClientDecoder.function_table.append(ReadLogResponse)
|
||||
|
||||
|
||||
class LockTTY(object):
|
||||
|
||||
def __init__(self, tty):
|
||||
# type: (str) -> None
|
||||
self.tty = tty
|
||||
|
||||
def __enter__(self):
|
||||
os.system(SERIAL_STARTER_DIR + 'stop-tty.sh ' + self.tty)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
os.system(SERIAL_STARTER_DIR + 'start-tty.sh ' + self.tty)
|
||||
|
||||
|
||||
def wrap_try_except(error_msg):
|
||||
def decorate(f):
|
||||
def applicator(*args, **kwargs):
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except:
|
||||
print(error_msg)
|
||||
exit(1)
|
||||
return applicator
|
||||
return decorate
|
||||
|
||||
|
||||
def init_modbus(tty):
|
||||
# type: (str) -> Modbus
|
||||
|
||||
return Modbus(
|
||||
port='/dev/' + tty,
|
||||
method='rtu',
|
||||
baudrate=115200,
|
||||
stopbits=1,
|
||||
bytesize=8,
|
||||
timeout=0.5, # seconds
|
||||
parity=serial.PARITY_ODD)
|
||||
|
||||
|
||||
@wrap_try_except("Failed to download BMS log!")
|
||||
def download_log(modbus, node_id, battery_id):
|
||||
# type: (Modbus, int, str) -> NoReturn
|
||||
|
||||
# Get address of latest log entry
|
||||
# request = ReadLogRequest(unit=slave_id)
|
||||
|
||||
print ('downloading BMS log from node ' + str(node_id) + ' ...')
|
||||
|
||||
progress = -1
|
||||
log_file = battery_id + "-node" + str(node_id) + "-" + datetime.now().strftime('%d-%m-%Y') + ".bin"
|
||||
print(log_file)
|
||||
|
||||
with open(log_file, 'w') as f:
|
||||
|
||||
eof = 0x200000
|
||||
record = 0x40
|
||||
for address in range(0, eof, 2*record):
|
||||
|
||||
percent = int(100*address/eof)
|
||||
|
||||
if percent != progress:
|
||||
progress = percent
|
||||
print('\r{}% '.format(progress),end='')
|
||||
|
||||
request = ReadLogRequest(address, slave=node_id)
|
||||
result = modbus.execute(request) # type: ReadLogResponse
|
||||
|
||||
address1 = "{:06X}".format(address)
|
||||
address2 = "{:06X}".format(address+record)
|
||||
|
||||
data1 = result.data[:record]
|
||||
data2 = result.data[record:]
|
||||
|
||||
line1 = address1 + ":" + ''.join('{:02X}'.format(byte) for byte in data1)
|
||||
line2 = address2 + ":" + ''.join('{:02X}'.format(byte) for byte in data2)
|
||||
|
||||
lines = line1 + "\n" + line2 + "\n"
|
||||
f.write(lines)
|
||||
|
||||
print("\r100%")
|
||||
print("done")
|
||||
print("wrote log to " + log_file)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@wrap_try_except("Failed to contact battery!")
|
||||
def identify_battery(modbus, node_id):
|
||||
# type: (Modbus, int) -> str
|
||||
|
||||
target = 'battery #' + str(node_id)
|
||||
print('contacting ' + target + ' ...')
|
||||
|
||||
request = ReportSlaveIdRequest(slave=node_id)
|
||||
response = modbus.execute(request)
|
||||
|
||||
index_of_ff = response.identifier.find(b'\xff')
|
||||
sid_response = response.identifier[index_of_ff + 1:].decode('utf-8').split(' ')
|
||||
|
||||
response = modbus.read_input_registers(address=FIRMWARE_VERSION_REGISTER, count=1, slave=node_id)
|
||||
|
||||
fw = '{0:0>4X}'.format(response.registers[0])
|
||||
print("log string is",sid_response[0]+"-"+sid_response[1]+"-"+fw)
|
||||
|
||||
#return re.sub(" +", "-", sid + " " + fw)
|
||||
return sid_response[0]+"-"+sid_response[1]+"-"+fw
|
||||
|
||||
|
||||
def is_int(value):
|
||||
# type: (str) -> bool
|
||||
try:
|
||||
_ = int(value)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def print_usage():
|
||||
print ('Usage: ' + __file__ + ' <slave id> <serial device>')
|
||||
print ('Example: ' + __file__ + ' 2 ttyUSB0')
|
||||
print ('')
|
||||
print ('You can omit the "ttyUSB" prefix of the serial device:')
|
||||
print (' ' + __file__ + ' 2 0')
|
||||
print ('')
|
||||
print ('You can omit the serial device entirely when the "com.victronenergy.battery.<serial device>" service is running:')
|
||||
print (' ' + __file__ + ' 2')
|
||||
print ('')
|
||||
|
||||
|
||||
def get_tty_from_battery_service_name():
|
||||
# type: () -> Optional[str]
|
||||
|
||||
import dbus
|
||||
bus = dbus.SystemBus()
|
||||
|
||||
tty = (
|
||||
name.split('.')[-1]
|
||||
for name in bus.list_names()
|
||||
if name.startswith('com.victronenergy.battery.')
|
||||
)
|
||||
|
||||
return next(tty, None)
|
||||
|
||||
|
||||
def parse_tty(tty):
|
||||
# type: (Optional[str]) -> str
|
||||
|
||||
if tty is None:
|
||||
return get_tty_from_battery_service_name()
|
||||
|
||||
if is_int(tty):
|
||||
return 'ttyUSB' + argv[1]
|
||||
else:
|
||||
return tty
|
||||
|
||||
|
||||
def parse_cmdline_args(argv):
|
||||
# type: (List[str]) -> (str, int)
|
||||
|
||||
slave_id = element_at_or_none(argv, 0)
|
||||
tty = parse_tty(element_at_or_none(argv, 1))
|
||||
|
||||
if slave_id is None or tty is None:
|
||||
print_usage()
|
||||
exit(2)
|
||||
|
||||
print("tty=",tty)
|
||||
print("slave id= ",slave_id)
|
||||
|
||||
return tty, int(slave_id)
|
||||
|
||||
|
||||
def element_at_or_none(lst, index):
|
||||
return next(iter(lst[index:]), None)
|
||||
|
||||
|
||||
def main(argv):
|
||||
# type: (List[str]) -> ()
|
||||
|
||||
tty, node_id = parse_cmdline_args(argv)
|
||||
|
||||
with init_modbus(tty) as modbus:
|
||||
battery_id = identify_battery(modbus, node_id)
|
||||
download_log(modbus, node_id, battery_id)
|
||||
|
||||
exit(0)
|
||||
|
||||
|
||||
main(argv[1:])
|
|
@ -0,0 +1,70 @@
|
|||
#!/bin/bash
|
||||
|
||||
#Prototype 10.2.3.115 Prototype
|
||||
#Salimax0001 10.2.3.104 Marti Technik (Bern)
|
||||
#Salimax0002 10.2.4.29 Weidmann d (ZG)
|
||||
#Salimax0003 10.2.4.33 Elektrotechnik Stefan GmbH
|
||||
#Salimax0004 10.2.4.32 Biohof Gubelmann (Walde)
|
||||
#Salimax0005 10.2.4.36 Schreinerei Schönthal (Thun)
|
||||
#Salimax0006 10.2.4.35 Steakhouse Mettmenstetten
|
||||
#Salimax0007 10.2.4.154 LerchenhofHerr Twannberg
|
||||
#Salimax0008 10.2.4.113 Wittmann Kottingbrunn
|
||||
|
||||
dotnet_version='net6.0'
|
||||
ip_address="$1"
|
||||
battery_ids="$2"
|
||||
username='ie-entwicklung'
|
||||
root_password='Salimax4x25'
|
||||
|
||||
if [ "$#" -lt 2 ]; then
|
||||
echo "Error: Insufficient arguments. Usage: $0 <ip_address> <battery_ids>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to expand battery ids from a range
|
||||
expand_battery_ids() {
|
||||
local range="$1"
|
||||
local expanded_ids=()
|
||||
|
||||
IFS='-' read -r start end <<< "$range"
|
||||
for ((i = start; i <= end; i++)); do
|
||||
expanded_ids+=("$i")
|
||||
done
|
||||
|
||||
echo "${expanded_ids[@]}"
|
||||
}
|
||||
|
||||
# Check if battery_ids_arg contains a hyphen indicating a range
|
||||
if [[ "$battery_ids" == *-* ]]; then
|
||||
# Expand battery ids from the range
|
||||
battery_ids=$(expand_battery_ids "$battery_ids")
|
||||
else
|
||||
# Use the provided battery ids
|
||||
battery_ids=("$battery_ids")
|
||||
fi
|
||||
|
||||
echo "ip_address: $ip_address"
|
||||
echo "Battery_ids: ${battery_ids[@]}"
|
||||
|
||||
#ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.33" "10.2.4.32" "10.2.4.36" "10.2.4.35" "10.2.4.154" "10.2.4.113" "10.2.4.29")
|
||||
#battery_ids=("2" "3" "4" "5" "6" "7" "8" "9" "10" "11")
|
||||
|
||||
set -e
|
||||
|
||||
scp download-bms-log "$username"@"$ip_address":/home/"$username"
|
||||
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S systemctl stop battery.service"
|
||||
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S apt install python3-pip -y"
|
||||
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S pip3 install pymodbus"
|
||||
|
||||
for battery in "${battery_ids[@]}"; do
|
||||
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S python3 download-bms-log " "$battery" " ttyUSB0"
|
||||
done
|
||||
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S systemctl start battery.service"
|
||||
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S rm download-bms-log"
|
||||
scp "$username"@"$ip_address":/home/"$username/*.bin" .
|
||||
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S rm *.bin"
|
||||
|
||||
echo "Deployed and ran commands on $ip_address"
|
||||
done
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCed5ANekhbdV/8nEwFyaqxbPGON+NZKAkZXKx2aMAbX6jYQpusXSf4lKxEp4vHX9q2ScWycluUEhlzwe9vaWIK6mxEG9gjtU0/tKIavqZ6qpcuiglal750e8tlDh+lAgg5K3v4tvV4uVEfFc42UzSC9cIBBKPBC41dc0xQKyFIDsSH6Qha1nyncKRC3OXUkOiiRvmbd4PVc9A5ah2vt+661pghZE19Qeh5ROn/Sma9C+9QIyUDCylezqptnT+Jdvs+JMCHk8nKK2A0bz1w0a8zzO7M1RLHfBLQ6o1SQAdV/Pmon8uQ9vLHc86l5r7WSTMEcjAqY3lGE9mdxsSZWNmp InnovEnergy
|
|
@ -0,0 +1,13 @@
|
|||
[Unit]
|
||||
Description=Salimax Controller
|
||||
Wants=battery.service
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/home/ie-entwicklung/salimax
|
||||
ExecStart=/home/ie-entwicklung/salimax/SaliMax
|
||||
WatchdogSec=30s
|
||||
Restart=always
|
||||
RestartSec=500ms
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -0,0 +1,381 @@
|
|||
using InnovEnergy.App.SodiStoreMax.Ess;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using static System.Double;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.AggregationService;
|
||||
|
||||
public static class Aggregator
|
||||
{
|
||||
|
||||
public static async Task HourlyDataAggregationManager()
|
||||
{
|
||||
var currentDateTime = DateTime.Now;
|
||||
var nextRoundedHour = currentDateTime.AddHours(1).AddMinutes(-currentDateTime.Minute).AddSeconds(-currentDateTime.Second);
|
||||
|
||||
// Calculate the time until the next rounded hour
|
||||
var timeUntilNextHour = nextRoundedHour - currentDateTime;
|
||||
|
||||
// Output the current and next rounded hour times
|
||||
Console.WriteLine("------------------------------------------HourlyDataAggregationManager-------------------------------------------");
|
||||
Console.WriteLine("Current Date and Time: " + currentDateTime);
|
||||
Console.WriteLine("Next Rounded Hour: " + nextRoundedHour);
|
||||
// Output the time until the next rounded hour
|
||||
Console.WriteLine("Waiting for " + timeUntilNextHour.TotalMinutes + " minutes...");
|
||||
Console.WriteLine("-----------------------------------------------------------------------------------------------------------------");
|
||||
|
||||
// Wait until the next rounded hour
|
||||
await Task.Delay(timeUntilNextHour);
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
AggregatedData hourlyAggregatedData = CreateHourlyData("LogDirectory",DateTime.Now.AddHours(-1).ToUnixTime(),DateTime.Now.ToUnixTime());
|
||||
hourlyAggregatedData.Save("HourlyData");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine("An error has occured when calculating hourly aggregated data, exception is:\n" + e);
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromHours(1));
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task DailyDataAggregationManager()
|
||||
{
|
||||
var currentDateTime = DateTime.Now;
|
||||
var nextRoundedHour = currentDateTime.AddDays(1).AddHours(-currentDateTime.Hour).AddMinutes(-currentDateTime.Minute).AddSeconds(-currentDateTime.Second);
|
||||
|
||||
// Calculate the time until the next rounded hour
|
||||
var timeUntilNextDay = nextRoundedHour - currentDateTime;
|
||||
Console.WriteLine("------------------------------------------DailyDataAggregationManager-------------------------------------------");
|
||||
// Output the current and next rounded hour times
|
||||
Console.WriteLine("Current Date and Time: " + currentDateTime);
|
||||
Console.WriteLine("Next Rounded Hour: " + nextRoundedHour);
|
||||
// Output the time until the next rounded hour
|
||||
Console.WriteLine("Waiting for " + timeUntilNextDay.TotalHours + " hours...");
|
||||
Console.WriteLine("-----------------------------------------------------------------------------------------------------------------");
|
||||
|
||||
// Wait until the next rounded hour
|
||||
await Task.Delay(timeUntilNextDay);
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentTime = DateTime.Now;
|
||||
AggregatedData dailyAggregatedData = CreateDailyData("HourlyData",currentTime.AddDays(-1).ToUnixTime(),currentTime.ToUnixTime());
|
||||
dailyAggregatedData.Save("DailyData");
|
||||
if (await dailyAggregatedData.PushToS3())
|
||||
{
|
||||
//DeleteHourlyData("HourlyData",currentTime.ToUnixTime());
|
||||
//AggregatedData.DeleteDailyData("DailyData");
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine("An error has occured when calculating daily aggregated data, exception is:\n" + e);
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromDays(1));
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteHourlyData(String myDirectory, Int64 beforeTimestamp)
|
||||
{
|
||||
var csvFiles = Directory.GetFiles(myDirectory, "*.csv");
|
||||
Console.WriteLine("Delete data before"+beforeTimestamp);
|
||||
foreach (var csvFile in csvFiles)
|
||||
{
|
||||
if (IsFileWithinTimeRange(csvFile, 0, beforeTimestamp))
|
||||
{
|
||||
File.Delete(csvFile);
|
||||
Console.WriteLine($"Deleted hourly data file: {csvFile}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this for test
|
||||
private static AggregatedData CreateHourlyData(String myDirectory, Int64 afterTimestamp, Int64 beforeTimestamp)
|
||||
{
|
||||
// Get all CSV files in the specified directory
|
||||
var csvFiles = Directory.GetFiles(myDirectory, "*.csv");
|
||||
var batterySoc = new List<Double>();
|
||||
var pvPowerSum = new List<Double>();
|
||||
var heatingPower = new List<Double>();
|
||||
var gridPowerImport = new List<Double>();
|
||||
var gridPowerExport = new List<Double>();
|
||||
var batteryDischargePower = new List<Double>();
|
||||
var batteryChargePower = new List<Double>();
|
||||
|
||||
|
||||
Console.WriteLine("File timestamp should start after "+ afterTimestamp);
|
||||
|
||||
foreach (var csvFile in csvFiles)
|
||||
{
|
||||
if (csvFile == "LogDirectory/log.csv")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsFileWithinTimeRange(csvFile, afterTimestamp, beforeTimestamp))
|
||||
{
|
||||
using var reader = new StreamReader(csvFile);
|
||||
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
|
||||
var line = reader.ReadLine();
|
||||
var lines = line?.Split(';');
|
||||
|
||||
// Assuming there are always three columns (variable name and its value)
|
||||
if (lines is { Length: 3 })
|
||||
{
|
||||
var variableName = lines[0].Trim();
|
||||
|
||||
if (TryParse(lines[1].Trim(), out var value))
|
||||
{
|
||||
switch (variableName)
|
||||
{
|
||||
case "/Battery/Soc":
|
||||
batterySoc.Add(value);
|
||||
break;
|
||||
|
||||
case "/PvOnDc/DcWh" :
|
||||
pvPowerSum.Add(value);
|
||||
break;
|
||||
|
||||
case "/Battery/Dc/Power":
|
||||
|
||||
if (value < 0)
|
||||
{
|
||||
batteryDischargePower.Add(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
batteryChargePower.Add(value);
|
||||
|
||||
}
|
||||
break;
|
||||
|
||||
case "/GridMeter/ActivePowerExportT3":
|
||||
// we are using different register to check which value from the grid meter we need to use
|
||||
// At the moment register 8002 amd 8012. in KWh
|
||||
gridPowerExport.Add(value);
|
||||
break;
|
||||
case "/GridMeter/ActivePowerImportT3":
|
||||
gridPowerImport.Add(value);
|
||||
break;
|
||||
case "/Battery/HeatingPower":
|
||||
heatingPower.Add(value);
|
||||
break;
|
||||
// Add more cases as needed
|
||||
default:
|
||||
// Code to execute when variableName doesn't match any condition
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
//Handle cases where variableValue is not a valid number
|
||||
// Console.WriteLine(
|
||||
// $"Invalid numeric value for variable {variableName}:{lines[1].Trim()}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle invalid column format
|
||||
//Console.WriteLine("Invalid format in column");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Average Power (Watts)= Sum of Power Readings/Number of Readings
|
||||
|
||||
//Then, you can use the average power in the energy formula:
|
||||
//
|
||||
//Energy (kWh)= (Average Power / 3600) × Time (1 seconds)
|
||||
//
|
||||
// Dividing the Average power readings by 3600 converts the result from watt-seconds to kilowatt-hours.
|
||||
|
||||
var dischargingEnergy = (batteryDischargePower.Any() ? batteryDischargePower.Average() : 0.0) / 3600;
|
||||
var chargingEnergy = (batteryChargePower.Any() ? batteryChargePower.Average() : 0.0) / 3600;
|
||||
var heatingPowerAvg = (heatingPower.Any() ? heatingPower.Average() : 0.0) / 3600;
|
||||
|
||||
var dMaxSoc = batterySoc.Any() ? batterySoc.Max() : 0.0;
|
||||
var dMinSoc = batterySoc.Any() ? batterySoc.Min() : 0.0;
|
||||
var dSumGridExportPower = gridPowerExport.Any() ? gridPowerExport.Max() - gridPowerExport.Min(): 0.0;
|
||||
var dSumGridImportPower = gridPowerImport.Any() ? gridPowerImport.Max() - gridPowerImport.Min(): 0.0;
|
||||
var dSumPvPower = pvPowerSum.Any() ? pvPowerSum.Max() : 0.0;
|
||||
|
||||
|
||||
AggregatedData aggregatedData = new AggregatedData
|
||||
{
|
||||
MaxSoc = dMaxSoc,
|
||||
MinSoc = dMinSoc,
|
||||
DischargingBatteryPower = dischargingEnergy,
|
||||
ChargingBatteryPower = chargingEnergy,
|
||||
GridExportPower = dSumGridExportPower,
|
||||
GridImportPower = dSumGridImportPower,
|
||||
PvPower = dSumPvPower,
|
||||
HeatingPower = heatingPowerAvg
|
||||
};
|
||||
|
||||
// Print the stored CSV data for verification
|
||||
Console.WriteLine($"Max SOC: {aggregatedData.MaxSoc}");
|
||||
Console.WriteLine($"Min SOC: {aggregatedData.MinSoc}");
|
||||
|
||||
Console.WriteLine($"DischargingBatteryBattery: {aggregatedData.DischargingBatteryPower}");
|
||||
Console.WriteLine($"ChargingBatteryPower: {aggregatedData.ChargingBatteryPower}");
|
||||
|
||||
Console.WriteLine($"SumGridExportPower: {aggregatedData.GridExportPower}");
|
||||
Console.WriteLine($"SumGridImportPower: {aggregatedData.GridImportPower}");
|
||||
|
||||
Console.WriteLine($"Min SOC: {aggregatedData.MinSoc}");
|
||||
|
||||
|
||||
Console.WriteLine("CSV data reading and storage completed.");
|
||||
Console.WriteLine("-----------------------------------------------------------------------------------------------------------------");
|
||||
|
||||
return aggregatedData;
|
||||
}
|
||||
|
||||
private static AggregatedData CreateDailyData(String myDirectory, Int64 afterTimestamp, Int64 beforeTimestamp)
|
||||
{
|
||||
// Get all CSV files in the specified directory
|
||||
var csvFiles = Directory.GetFiles(myDirectory, "*.csv");
|
||||
var batterySoc = new List<Double>();
|
||||
var pvPower = new List<Double>();
|
||||
var gridPowerImport = new List<Double>();
|
||||
var gridPowerExport = new List<Double>();
|
||||
var batteryDischargePower = new List<Double>();
|
||||
var batteryChargePower = new List<Double>();
|
||||
var heatingPowerAvg = new List<Double>();
|
||||
|
||||
|
||||
|
||||
Console.WriteLine("File timestamp should start after "+ afterTimestamp);
|
||||
|
||||
foreach (var csvFile in csvFiles)
|
||||
{
|
||||
if (csvFile == "LogDirectory/log.csv")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsFileWithinTimeRange(csvFile, afterTimestamp, beforeTimestamp))
|
||||
{
|
||||
using var reader = new StreamReader(csvFile);
|
||||
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
|
||||
var line = reader.ReadLine();
|
||||
var lines = line?.Split(';');
|
||||
|
||||
// Assuming there are always three columns (variable name and its value)
|
||||
if (lines is { Length: 3 })
|
||||
{
|
||||
var variableName = lines[0].Trim();
|
||||
|
||||
if (TryParse(lines[1].Trim(), out var value))
|
||||
{
|
||||
switch (variableName)
|
||||
{
|
||||
case "/MinSoc" or "/MaxSoc":
|
||||
batterySoc.Add(value);
|
||||
break;
|
||||
|
||||
case "/PvPower":
|
||||
pvPower.Add(value);
|
||||
break;
|
||||
|
||||
case "/DischargingBatteryPower" :
|
||||
batteryDischargePower.Add(value);
|
||||
break;
|
||||
|
||||
case "/ChargingBatteryPower" :
|
||||
batteryChargePower.Add(value);
|
||||
break;
|
||||
|
||||
case "/GridExportPower":
|
||||
gridPowerExport.Add(value);
|
||||
break;
|
||||
|
||||
case "/GridImportPower":
|
||||
gridPowerImport.Add(value);
|
||||
break;
|
||||
|
||||
case "/HeatingPower":
|
||||
heatingPowerAvg.Add(value);
|
||||
break;
|
||||
// Add more cases as needed
|
||||
default:
|
||||
// Code to execute when variableName doesn't match any condition
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
//Handle cases where variableValue is not a valid number
|
||||
// Console.WriteLine(
|
||||
// $"Invalid numeric value for variable {variableName}:{lines[1].Trim()}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle invalid column format
|
||||
//Console.WriteLine("Invalid format in column");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AggregatedData aggregatedData = new AggregatedData
|
||||
{
|
||||
MaxSoc = batterySoc.Any() ? batterySoc.Max() : 0.0,
|
||||
MinSoc = batterySoc.Any() ? batterySoc.Min() : 0.0,
|
||||
DischargingBatteryPower = batteryDischargePower.Any() ? batteryDischargePower.Average(): 0.0,
|
||||
ChargingBatteryPower = batteryChargePower.Any() ? batteryChargePower.Average() : 0.0,
|
||||
GridExportPower = gridPowerExport.Any() ? gridPowerExport.Sum() : 0.0,
|
||||
GridImportPower = gridPowerImport.Any() ? gridPowerImport.Sum() : 0.0,
|
||||
PvPower = pvPower.Any() ? pvPower.Last() : 0.0,
|
||||
HeatingPower = heatingPowerAvg.Any() ? heatingPowerAvg.Average() : 0.0,
|
||||
};
|
||||
|
||||
// Print the stored CSV data for verification
|
||||
Console.WriteLine($"Pv Power: {aggregatedData.PvPower}");
|
||||
Console.WriteLine($"Heating Power: {aggregatedData.HeatingPower}");
|
||||
Console.WriteLine($"Max SOC: {aggregatedData.MaxSoc}");
|
||||
Console.WriteLine($"Min SOC: {aggregatedData.MinSoc}");
|
||||
|
||||
Console.WriteLine($"ChargingBatteryPower: {aggregatedData.DischargingBatteryPower}");
|
||||
Console.WriteLine($"DischargingBatteryBattery: {aggregatedData.ChargingBatteryPower}");
|
||||
|
||||
Console.WriteLine($"SumGridExportPower: {aggregatedData.GridExportPower}");
|
||||
Console.WriteLine($"SumGridImportPower: {aggregatedData.GridImportPower}");
|
||||
|
||||
|
||||
|
||||
Console.WriteLine("CSV data reading and storage completed.");
|
||||
Console.WriteLine("-----------------------------------------------------------------------------------------------------------------");
|
||||
|
||||
return aggregatedData;
|
||||
}
|
||||
|
||||
// Custom method to check if a string is numeric
|
||||
private static Boolean GetVariable(String value, String path)
|
||||
{
|
||||
return value == path;
|
||||
}
|
||||
|
||||
private static Boolean IsFileWithinTimeRange(string filePath, long startTime, long endTime)
|
||||
{
|
||||
var fileTimestamp = long.TryParse(Path.GetFileNameWithoutExtension(filePath).Replace("log_", ""), out var fileTimestamp1) ? fileTimestamp1 : -1;
|
||||
|
||||
return fileTimestamp >= startTime && fileTimestamp < endTime;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Flurl.Http;
|
||||
using InnovEnergy.App.SodiStoreMax.Devices;
|
||||
using InnovEnergy.App.SodiStoreMax.SystemConfig;
|
||||
using InnovEnergy.Lib.Units;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using static System.Text.Json.JsonSerializer;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.AggregationService;
|
||||
// shut up trim warnings
|
||||
#pragma warning disable IL2026
|
||||
|
||||
public class AggregatedData
|
||||
{
|
||||
public required Double MinSoc { get; set; }
|
||||
public required Double MaxSoc { get; set; }
|
||||
public required Double PvPower { get; set; }
|
||||
public required Double DischargingBatteryPower { get; set; }
|
||||
public required Double ChargingBatteryPower { get; set; }
|
||||
public required Double GridExportPower { get; set; }
|
||||
public required Double GridImportPower { get; set; }
|
||||
public required Double HeatingPower { get; set; }
|
||||
|
||||
|
||||
private readonly S3Config? _S3Config = Config.Load().S3;
|
||||
|
||||
public void Save(String directory)
|
||||
{
|
||||
var date = DateTime.Now.ToUnixTime();
|
||||
var defaultHDataPath = Environment.CurrentDirectory + "/" + directory + "/";
|
||||
var dataFilePath = defaultHDataPath + date + ".csv";
|
||||
|
||||
if (!Directory.Exists(defaultHDataPath))
|
||||
{
|
||||
Directory.CreateDirectory(defaultHDataPath);
|
||||
Console.WriteLine("Directory created successfully.");
|
||||
}
|
||||
Console.WriteLine("data file path is " + dataFilePath);
|
||||
|
||||
try
|
||||
{
|
||||
var csvString = this.ToCsv();
|
||||
File.WriteAllText(dataFilePath, csvString);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
$"Failed to write config file {dataFilePath}\n{e}".WriteLine();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static void DeleteDailyData(String directory)
|
||||
{
|
||||
|
||||
var csvFiles = Directory.GetFiles(directory, "*.csv");
|
||||
foreach (var csvFile in csvFiles)
|
||||
{
|
||||
File.Delete(csvFile);
|
||||
Console.WriteLine($"Deleted daily data file: {csvFile}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Boolean> PushToS3()
|
||||
{
|
||||
var csv = this.ToCsv();
|
||||
if (_S3Config is null)
|
||||
return false;
|
||||
|
||||
var s3Path = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd") + ".csv";
|
||||
var request = _S3Config.CreatePutRequest(s3Path);
|
||||
|
||||
// Compress CSV data to a byte array
|
||||
byte[] compressedBytes;
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
//Create a zip directory and put the compressed file inside
|
||||
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
|
||||
{
|
||||
var entry = archive.CreateEntry("data.csv", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive
|
||||
using (var entryStream = entry.Open())
|
||||
using (var writer = new StreamWriter(entryStream))
|
||||
{
|
||||
writer.Write(csv);
|
||||
}
|
||||
}
|
||||
|
||||
compressedBytes = memoryStream.ToArray();
|
||||
}
|
||||
|
||||
// Encode the compressed byte array as a Base64 string
|
||||
string base64String = Convert.ToBase64String(compressedBytes);
|
||||
|
||||
// Create StringContent from Base64 string
|
||||
var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64");
|
||||
|
||||
// Upload the compressed data (ZIP archive) to S3
|
||||
var response = await request.PutAsync(stringContent);
|
||||
|
||||
//
|
||||
// var request = _S3Config.CreatePutRequest(s3Path);
|
||||
// var response = await request.PutAsync(new StringContent(csv));
|
||||
|
||||
if (response.StatusCode != 200)
|
||||
{
|
||||
Console.WriteLine("ERROR: PUT");
|
||||
var error = await response.GetStringAsync();
|
||||
Console.WriteLine(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// public static HourlyData? Load(String dataFilePath)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// var csvString = File.ReadAllText(dataFilePath);
|
||||
// return Deserialize<HourlyData>(jsonString)!;
|
||||
// }
|
||||
// catch (Exception e)
|
||||
// {
|
||||
// $"Failed to read config file {dataFilePath}, using default config\n{e}".WriteLine();
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace InnovEnergy.App.SodiStoreMax.DataTypes;
|
||||
|
||||
public class AlarmOrWarning
|
||||
{
|
||||
public String? Date { get; set; }
|
||||
public String? Time { get; set; }
|
||||
public String? Description { get; set; }
|
||||
public String? CreatedBy { get; set; }
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
using InnovEnergy.App.SodiStoreMax.SystemConfig;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.DataTypes;
|
||||
|
||||
public class Configuration
|
||||
{
|
||||
public Double MinimumSoC { get; set; }
|
||||
public Double GridSetPoint { get; set; }
|
||||
public CalibrationChargeType CalibrationChargeState { get; set; }
|
||||
public DateTime CalibrationChargeDate { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
using InnovEnergy.App.SodiStoreMax.Ess;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.DataTypes;
|
||||
|
||||
public class StatusMessage
|
||||
{
|
||||
public required Int32 InstallationId { get; set; }
|
||||
public required Int32 Product { get; set; }
|
||||
public required SalimaxAlarmState Status { get; set; }
|
||||
public required MessageType Type { get; set; }
|
||||
public List<AlarmOrWarning>? Warnings { get; set; }
|
||||
public List<AlarmOrWarning>? Alarms { get; set; }
|
||||
public Int32 Timestamp { get; set; }
|
||||
}
|
||||
|
||||
public enum MessageType
|
||||
{
|
||||
AlarmOrWarning,
|
||||
Heartbit
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
using InnovEnergy.Lib.Units.Composite;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.Devices;
|
||||
|
||||
public class AcPowerDevice
|
||||
{
|
||||
public required AcPower Power { get; init; }
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
using InnovEnergy.Lib.Units.Power;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.Devices;
|
||||
|
||||
public class DcPowerDevice
|
||||
{
|
||||
public required DcPower Power { get; init; }
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
namespace InnovEnergy.App.SodiStoreMax.Devices;
|
||||
|
||||
public enum DeviceState
|
||||
{
|
||||
Disabled,
|
||||
Measured,
|
||||
Computed
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
using InnovEnergy.Lib.Utils.Net;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.Devices;
|
||||
|
||||
public class SalimaxDevice : Ip4Address
|
||||
{
|
||||
public required DeviceState DeviceState { get; init; }
|
||||
}
|
|
@ -0,0 +1,273 @@
|
|||
using InnovEnergy.App.SodiStoreMax.SystemConfig;
|
||||
using InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc.DataTypes;
|
||||
using InnovEnergy.Lib.Units;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.Ess;
|
||||
|
||||
public static class Controller
|
||||
{
|
||||
private static readonly Double MaxDischargePower = -4000; // By battery TODO: move to config
|
||||
private static readonly Double MaxChargePower = 3500; // By battery TODO: move to config
|
||||
|
||||
public static EssMode SelectControlMode(this StatusRecord s)
|
||||
{
|
||||
//return EssMode.OptimizeSelfConsumption;
|
||||
|
||||
return s.StateMachine.State != 23 ? EssMode.Off
|
||||
: s.MustReachMinSoc() ? EssMode.ReachMinSoc
|
||||
: s.GridMeter is null ? EssMode.NoGridMeter
|
||||
: EssMode.OptimizeSelfConsumption;
|
||||
}
|
||||
|
||||
|
||||
public static EssControl ControlEss(this StatusRecord s)
|
||||
{
|
||||
var mode = s.SelectControlMode().WriteLine();
|
||||
|
||||
if (mode is EssMode.Off) // to test on prototype
|
||||
{
|
||||
if (s.StateMachine.State == 28 )
|
||||
{
|
||||
return new EssControl
|
||||
{
|
||||
LimitedBy = EssLimit.NoLimit,
|
||||
Mode = EssMode.OffGrid,
|
||||
PowerCorrection = 0,
|
||||
PowerSetpoint = 0
|
||||
};
|
||||
}
|
||||
return EssControl.Default;
|
||||
}
|
||||
|
||||
// if we have no reading from the Grid meter, but we have a grid power (K1 is close),
|
||||
// then we do only heat the battery to avoid discharging the battery and the oscillation between reach min soc and off mode
|
||||
if (mode is EssMode.NoGridMeter)
|
||||
return new EssControl
|
||||
{
|
||||
LimitedBy = EssLimit.NoLimit,
|
||||
Mode = EssMode.NoGridMeter,
|
||||
PowerCorrection = 0,
|
||||
PowerSetpoint = 0, //s.Battery == null ? 1000 : s.Battery.Devices.Count * s.Config.BatterySelfDischargePower // 1000 default value for heating the battery
|
||||
};
|
||||
|
||||
var essDelta = s.ComputePowerDelta(mode);
|
||||
essDelta.WriteLine("Power Correction");
|
||||
|
||||
var unlimitedControl = new EssControl
|
||||
{
|
||||
Mode = mode,
|
||||
LimitedBy = EssLimit.NoLimit,
|
||||
PowerCorrection = essDelta,
|
||||
PowerSetpoint = 0
|
||||
};
|
||||
|
||||
var limitedControl = unlimitedControl
|
||||
.LimitChargePower(s)
|
||||
.LimitDischargePower(s)
|
||||
.LimitInverterPower(s);
|
||||
|
||||
var currentPowerSetPoint = s.CurrentPowerSetPoint();
|
||||
|
||||
return limitedControl with { PowerSetpoint = currentPowerSetPoint + limitedControl.PowerCorrection };
|
||||
}
|
||||
|
||||
private static EssControl LimitInverterPower(this EssControl control, StatusRecord s)
|
||||
{
|
||||
var powerDelta = control.PowerCorrection.Value;
|
||||
|
||||
var acDcs = s.AcDc.Devices;
|
||||
|
||||
var nInverters = acDcs.Count;
|
||||
|
||||
if (nInverters < 2)
|
||||
return control; // current loop cannot happen
|
||||
|
||||
var nominalPower = acDcs.Average(d => d.Status.Nominal.Power);
|
||||
var maxStep = nominalPower / 25; //TODO magic number to config
|
||||
|
||||
var clampedPowerDelta = powerDelta.Clamp(-maxStep, maxStep);
|
||||
|
||||
var dcLimited = acDcs.Any(d => d.Status.PowerLimitedBy == PowerLimit.DcLink);
|
||||
|
||||
if (!dcLimited)
|
||||
return control with { PowerCorrection = clampedPowerDelta };
|
||||
|
||||
var maxPower = acDcs.Max(d => d.Status.Ac.Power.Active.Value);
|
||||
var minPower = acDcs.Min(d => d.Status.Ac.Power.Active.Value);
|
||||
|
||||
var powerDifference = maxPower - minPower;
|
||||
|
||||
if (powerDifference < maxStep)
|
||||
return control with { PowerCorrection = clampedPowerDelta };
|
||||
|
||||
var correction = powerDifference / 4; //TODO magic number to config
|
||||
|
||||
|
||||
// find out if we reach the lower or upper Dc limit by comparing the current Dc voltage to the reference voltage
|
||||
return s.AcDc.Dc.Voltage > s.Config.GridTie.AcDc.ReferenceDcLinkVoltage
|
||||
? control with { PowerCorrection = clampedPowerDelta.ClampMax(-correction), LimitedBy = EssLimit.ChargeLimitedByMaxDcBusVoltage }
|
||||
: control with { PowerCorrection = clampedPowerDelta.ClampMin(correction), LimitedBy = EssLimit.DischargeLimitedByMinDcBusVoltage };
|
||||
}
|
||||
|
||||
|
||||
private static EssControl LimitChargePower(this EssControl control, StatusRecord s)
|
||||
{
|
||||
|
||||
//var maxInverterChargePower = s.ControlInverterPower(s.Config.MaxInverterPower);
|
||||
var maxBatteryChargePower = s.MaxBatteryChargePower();
|
||||
maxBatteryChargePower.WriteLine(" Max Battery Charge Power");
|
||||
|
||||
return control
|
||||
//.LimitChargePower(, EssLimit.ChargeLimitedByInverterPower)
|
||||
.LimitChargePower(maxBatteryChargePower, EssLimit.ChargeLimitedByBatteryPower);
|
||||
|
||||
}
|
||||
|
||||
private static EssControl LimitDischargePower(this EssControl control, StatusRecord s)
|
||||
{
|
||||
var maxBatteryDischargeDelta = s.Battery?.Devices.Count * MaxDischargePower ?? 0;
|
||||
var keepMinSocLimitDelta = s.ControlBatteryPower(s.HoldMinSocPower());
|
||||
maxBatteryDischargeDelta.WriteLine(" Max Battery Discharge Power");
|
||||
|
||||
|
||||
return control
|
||||
.LimitDischargePower(maxBatteryDischargeDelta , EssLimit.DischargeLimitedByBatteryPower)
|
||||
.LimitDischargePower(keepMinSocLimitDelta , EssLimit.DischargeLimitedByMinSoc);
|
||||
}
|
||||
|
||||
private static Double ComputePowerDelta(this StatusRecord s, EssMode mode)
|
||||
{
|
||||
var chargePower = s.AcDc.Devices.Sum(d => d.Status.Nominal.Power.Value);
|
||||
|
||||
|
||||
s.Config.GridSetPoint.WriteLine(" GridSetPoint");
|
||||
|
||||
return mode switch
|
||||
{
|
||||
EssMode.ReachMinSoc => s.ControlInverterPower(chargePower),
|
||||
EssMode.OptimizeSelfConsumption => s.ControlGridPower(s.Config.GridSetPoint),
|
||||
EssMode.Off => 0,
|
||||
EssMode.OffGrid => 0,
|
||||
EssMode.NoGridMeter => 0,
|
||||
_ => throw new ArgumentException(null, nameof(mode))
|
||||
};
|
||||
}
|
||||
|
||||
// private static Boolean MustHeatBatteries(this StatusRecord s)
|
||||
// {
|
||||
// var batteries = s.GetBatteries();
|
||||
//
|
||||
// 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)
|
||||
{
|
||||
// This introduces a limit when we don't have communication with batteries
|
||||
// Otherwise the limit will be 0 and the batteries will be not heated
|
||||
|
||||
var batteries = s.GetBatteries();
|
||||
|
||||
var maxChargePower = batteries.Count == 0
|
||||
? 0
|
||||
: batteries.Count * MaxChargePower;
|
||||
|
||||
return 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.GetBatteries();
|
||||
|
||||
return batteries.Count > 0
|
||||
&& batteries.Any(b => b.BatteryDeligreenDataRecord.Soc < s.Config.MinSoc);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<BatteryDeligreenRecord> GetBatteries(this StatusRecord s)
|
||||
{
|
||||
return s.Battery?.Devices ?? Array.Empty<BatteryDeligreenRecord>();
|
||||
}
|
||||
|
||||
private static Double ControlGridPower(this StatusRecord status, Double targetPower)
|
||||
{
|
||||
return ControlPower
|
||||
(
|
||||
measurement : status.GridMeter!.Ac.Power.Active,
|
||||
target : targetPower,
|
||||
pConstant : status.Config.PConstant
|
||||
);
|
||||
}
|
||||
|
||||
private static Double ControlInverterPower(this StatusRecord status, Double targetInverterPower)
|
||||
{
|
||||
return ControlPower
|
||||
(
|
||||
measurement : status.AcDc.Ac.Power.Active,
|
||||
target : targetInverterPower,
|
||||
pConstant : status.Config.PConstant
|
||||
);
|
||||
}
|
||||
|
||||
private static Double ControlBatteryPower(this StatusRecord status, Double targetBatteryPower)
|
||||
{
|
||||
return ControlPower
|
||||
(
|
||||
measurement: status.GetBatteries().Sum(b => b.BatteryDeligreenDataRecord.Power),
|
||||
target: targetBatteryPower,
|
||||
pConstant: status.Config.PConstant
|
||||
);
|
||||
}
|
||||
|
||||
private static Double HoldMinSocPower(this StatusRecord s)
|
||||
{
|
||||
// TODO: explain LowSOC curve
|
||||
|
||||
var batteries = s.GetBatteries();
|
||||
|
||||
if (batteries.Count == 0)
|
||||
return Double.NegativeInfinity;
|
||||
|
||||
var a = -2 * s.Config.BatterySelfDischargePower * batteries.Count / s.Config.HoldSocZone;
|
||||
var b = -a * (s.Config.MinSoc + s.Config.HoldSocZone);
|
||||
|
||||
return batteries.Min(d => d.BatteryDeligreenDataRecord.Soc.Value) * a + b;
|
||||
}
|
||||
|
||||
private static Double ControlPower(Double measurement, Double target, Double pConstant)
|
||||
{
|
||||
var error = target - measurement;
|
||||
return error * pConstant;
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Local, TODO
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
using InnovEnergy.Lib.Units.Power;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.Ess;
|
||||
|
||||
public record EssControl
|
||||
{
|
||||
public required EssMode Mode { get; init; }
|
||||
public required EssLimit LimitedBy { get; init; }
|
||||
public required ActivePower PowerCorrection { get; init; }
|
||||
public required ActivePower PowerSetpoint { get; init; }
|
||||
|
||||
public static EssControl Default { get; } = new()
|
||||
{
|
||||
Mode = EssMode.Off,
|
||||
LimitedBy = EssLimit.NoLimit,
|
||||
PowerCorrection = 0,
|
||||
PowerSetpoint = 0
|
||||
};
|
||||
|
||||
|
||||
public EssControl LimitChargePower(Double controlDelta, EssLimit reason)
|
||||
{
|
||||
var overload = PowerCorrection - controlDelta;
|
||||
|
||||
if (overload <= 0)
|
||||
return this;
|
||||
|
||||
return this with
|
||||
{
|
||||
LimitedBy = reason,
|
||||
PowerCorrection = controlDelta,
|
||||
PowerSetpoint = PowerSetpoint - overload
|
||||
};
|
||||
}
|
||||
|
||||
public EssControl LimitDischargePower(Double controlDelta, EssLimit reason)
|
||||
{
|
||||
var overload = PowerCorrection - controlDelta;
|
||||
|
||||
if (overload >= 0)
|
||||
return this;
|
||||
|
||||
return this with
|
||||
{
|
||||
LimitedBy = reason,
|
||||
PowerCorrection = controlDelta,
|
||||
PowerSetpoint = PowerSetpoint - overload
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
namespace InnovEnergy.App.SodiStoreMax.Ess;
|
||||
|
||||
public enum EssLimit
|
||||
{
|
||||
NoLimit,
|
||||
DischargeLimitedByMinSoc,
|
||||
DischargeLimitedByBatteryPower,
|
||||
DischargeLimitedByInverterPower,
|
||||
ChargeLimitedByInverterPower,
|
||||
ChargeLimitedByBatteryPower,
|
||||
ChargeLimitedByMaxDcBusVoltage,
|
||||
DischargeLimitedByMinDcBusVoltage,
|
||||
}
|
||||
|
||||
|
||||
// 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,12 @@
|
|||
namespace InnovEnergy.App.SodiStoreMax.Ess;
|
||||
|
||||
public enum EssMode
|
||||
{
|
||||
Off,
|
||||
OffGrid,
|
||||
HeatBatteries,
|
||||
CalibrationCharge,
|
||||
ReachMinSoc,
|
||||
NoGridMeter,
|
||||
OptimizeSelfConsumption
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
namespace InnovEnergy.App.SodiStoreMax.Ess;
|
||||
|
||||
public enum SalimaxAlarmState
|
||||
{
|
||||
Green,
|
||||
Orange,
|
||||
Red
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
using InnovEnergy.App.SodiStoreMax.Devices;
|
||||
using InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
|
||||
using InnovEnergy.App.SodiStoreMax.System;
|
||||
using InnovEnergy.App.SodiStoreMax.SystemConfig;
|
||||
using InnovEnergy.Lib.Devices.AMPT;
|
||||
using InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
using InnovEnergy.Lib.Devices.EmuMeter;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.Ess;
|
||||
|
||||
public record StatusRecord
|
||||
{
|
||||
public required AcDcDevicesRecord AcDc { get; init; }
|
||||
public required DcDcDevicesRecord DcDc { get; init; }
|
||||
public required BatteryDeligreenRecords? Battery { get; init; }
|
||||
public required EmuMeterRegisters? GridMeter { get; init; }
|
||||
public required EmuMeterRegisters? LoadOnAcIsland { get; init; }
|
||||
public required AcPowerDevice? LoadOnAcGrid { get; init; }
|
||||
public required AmptStatus? PvOnAcGrid { get; init; }
|
||||
public required AmptStatus? PvOnAcIsland { get; init; }
|
||||
public required AcPowerDevice? AcGridToAcIsland { get; init; }
|
||||
public required DcPowerDevice? AcDcToDcLink { get; init; }
|
||||
public required DcPowerDevice? LoadOnDc { get; init; }
|
||||
public required IRelaysRecord? Relays { get; init; }
|
||||
public required AmptStatus? PvOnDc { get; init; }
|
||||
public required Config Config { get; set; }
|
||||
public required SystemLog Log { get; init; } // TODO: init only
|
||||
|
||||
public required EssControl EssControl { get; set; } // TODO: init only
|
||||
public required StateMachine StateMachine { get; init; }
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using InnovEnergy.App.SodiStoreMax.DataTypes;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.Ess;
|
||||
|
||||
public record SystemLog
|
||||
{
|
||||
public required String? Message { get; init; }
|
||||
public required SalimaxAlarmState SalimaxAlarmState { get; init; }
|
||||
public required List<AlarmOrWarning>? SalimaxAlarms { get; set; }
|
||||
public required List<AlarmOrWarning>? SalimaxWarnings { get; set; }
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using InnovEnergy.Lib.Units;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax;
|
||||
|
||||
public static class Flow
|
||||
{
|
||||
private static readonly String RightArrowChar = ">";
|
||||
private static readonly String LeftArrowChar = "<";
|
||||
private static readonly String DownArrowChar = "V";
|
||||
private static readonly String UpArrowChar = "^";
|
||||
private static readonly String UnknownArrowChar = "?";
|
||||
|
||||
public static TextBlock Horizontal(Unit? amount) => Horizontal(amount, 10);
|
||||
|
||||
public static TextBlock Horizontal(Unit? amount, Int32 width)
|
||||
{
|
||||
var label = amount?.ToDisplayString() ?? "";
|
||||
|
||||
var arrowChar = amount switch
|
||||
{
|
||||
{ Value: < 0 } => LeftArrowChar,
|
||||
{ Value: >= 0 } => RightArrowChar,
|
||||
_ => UnknownArrowChar,
|
||||
};
|
||||
|
||||
//var arrowChar = amount.Value < 0 ? LeftArrowChar : RightArrowChar;
|
||||
var arrow = Enumerable.Repeat(arrowChar, width).Join();
|
||||
|
||||
// note : appending "fake label" below to make it vertically symmetric
|
||||
return TextBlock.AlignCenterHorizontal(label, arrow, "");
|
||||
}
|
||||
|
||||
public static TextBlock Vertical(Unit? amount) => Vertical(amount, 4);
|
||||
|
||||
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
|
||||
[SuppressMessage("ReSharper", "CoVariantArrayConversion")]
|
||||
public static TextBlock Vertical(Unit? amount, Int32 height)
|
||||
{
|
||||
var label = amount?.ToDisplayString() ?? UnknownArrowChar;
|
||||
var arrowChar = amount switch
|
||||
{
|
||||
{ Value: < 0 } => UpArrowChar,
|
||||
{ Value: >= 0 } => DownArrowChar,
|
||||
_ => UnknownArrowChar,
|
||||
};
|
||||
|
||||
// var arrowChar = amount is null ? UnknownArrowChar
|
||||
// : amount.Value < 0 ? UpArrowChar
|
||||
// : DownArrowChar;
|
||||
|
||||
return TextBlock.AlignCenterHorizontal(arrowChar, arrowChar, label, arrowChar, arrowChar);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
using System.Text;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax;
|
||||
|
||||
public class LogFileConcatenator
|
||||
{
|
||||
private readonly string _logDirectory;
|
||||
|
||||
public LogFileConcatenator(String logDirectory = "LogDirectory/")
|
||||
{
|
||||
_logDirectory = logDirectory;
|
||||
}
|
||||
|
||||
public String ConcatenateFiles(int numberOfFiles)
|
||||
{
|
||||
var logFiles = Directory
|
||||
.GetFiles(_logDirectory, "log_*.csv")
|
||||
.OrderByDescending(file => file)
|
||||
.Take(numberOfFiles)
|
||||
.OrderBy(file => file)
|
||||
.ToList();
|
||||
|
||||
var concatenatedContent = new StringBuilder();
|
||||
|
||||
foreach (var fileContent in logFiles.Select(File.ReadAllText))
|
||||
{
|
||||
concatenatedContent.AppendLine(fileContent);
|
||||
//concatenatedContent.AppendLine(); // Append an empty line to separate the files // maybe we don't need this
|
||||
}
|
||||
|
||||
return concatenatedContent.ToString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
using InnovEnergy.Lib.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax;
|
||||
|
||||
public class CustomLogger : ILogger
|
||||
{
|
||||
private readonly String _logFilePath;
|
||||
//private readonly Int64 _maxFileSizeBytes;
|
||||
private readonly Int32 _maxLogFileCount;
|
||||
private Int64 _currentFileSizeBytes;
|
||||
|
||||
public CustomLogger(String logFilePath, Int32 maxLogFileCount)
|
||||
{
|
||||
_logFilePath = logFilePath;
|
||||
_maxLogFileCount = maxLogFileCount;
|
||||
_currentFileSizeBytes = File.Exists(logFilePath) ? new FileInfo(logFilePath).Length : 0;
|
||||
}
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => throw new NotImplementedException();
|
||||
|
||||
public Boolean IsEnabled(LogLevel logLevel) => true; // Enable logging for all levels
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception, String> formatter)
|
||||
{
|
||||
var logMessage = formatter(state, exception!);
|
||||
|
||||
// Check the log file count and delete the oldest file if necessary
|
||||
var logFileDir = Path.GetDirectoryName(_logFilePath)!;
|
||||
var logFileExt = Path.GetExtension(_logFilePath);
|
||||
var logFileBaseName = Path.GetFileNameWithoutExtension(_logFilePath);
|
||||
|
||||
var logFiles = Directory
|
||||
.GetFiles(logFileDir, $"{logFileBaseName}_*{logFileExt}")
|
||||
.OrderBy(file => file)
|
||||
.ToList();
|
||||
|
||||
if (logFiles.Count >= _maxLogFileCount)
|
||||
{
|
||||
File.Delete(logFiles.First());
|
||||
}
|
||||
|
||||
var roundedUnixTimestamp = DateTime.Now.ToUnixTime() % 2 == 0 ? DateTime.Now.ToUnixTime() : DateTime.Now.ToUnixTime() + 1;
|
||||
var timestamp = "Timestamp;" + roundedUnixTimestamp + Environment.NewLine;
|
||||
|
||||
var logFileBackupPath = Path.Combine(logFileDir, $"{logFileBaseName}_{DateTime.Now.ToUnixTime()}{logFileExt}");
|
||||
File.AppendAllText(logFileBackupPath, timestamp + logMessage + Environment.NewLine);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
using InnovEnergy.App.SodiStoreMax;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax;
|
||||
|
||||
public static class Logger
|
||||
{
|
||||
// Specify the maximum log file size in bytes (e.g., 1 MB)
|
||||
|
||||
//private const Int32 MaxFileSizeBytes = 2024 * 30; // TODO: move to settings
|
||||
private const Int32 MaxLogFileCount = 5000; // TODO: move to settings
|
||||
private const String LogFilePath = "LogDirectory/log.csv"; // TODO: move to settings
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static readonly ILogger _logger = new CustomLogger(LogFilePath, MaxLogFileCount);
|
||||
|
||||
public static T LogInfo<T>(this T t) where T : notnull
|
||||
{
|
||||
_logger.LogInformation(t.ToString()); // TODO: check warning
|
||||
return t;
|
||||
}
|
||||
|
||||
public static T LogDebug<T>(this T t) where T : notnull
|
||||
{
|
||||
_logger.LogDebug(t.ToString()); // TODO: check warning
|
||||
return t;
|
||||
}
|
||||
|
||||
public static T LogError<T>(this T t) where T : notnull
|
||||
{
|
||||
_logger.LogError(t.ToString()); // TODO: check warning
|
||||
return t;
|
||||
}
|
||||
|
||||
public static T LogWarning<T>(this T t) where T : notnull
|
||||
{
|
||||
_logger.LogWarning(t.ToString()); // TODO: check warning
|
||||
return t;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using InnovEnergy.App.SodiStoreMax.DataTypes;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.MiddlewareClasses;
|
||||
|
||||
public static class MiddlewareAgent
|
||||
{
|
||||
private static UdpClient _udpListener = null!;
|
||||
private static IPAddress? _controllerIpAddress;
|
||||
private static EndPoint? _endPoint;
|
||||
|
||||
public static void InitializeCommunicationToMiddleware()
|
||||
{
|
||||
_controllerIpAddress = FindVpnIp();
|
||||
if (Equals(IPAddress.None, _controllerIpAddress))
|
||||
{
|
||||
Console.WriteLine("There is no VPN interface, exiting...");
|
||||
}
|
||||
|
||||
const Int32 udpPort = 9000;
|
||||
_endPoint = new IPEndPoint(_controllerIpAddress, udpPort);
|
||||
|
||||
_udpListener = new UdpClient();
|
||||
_udpListener.Client.Blocking = false;
|
||||
_udpListener.Client.Bind(_endPoint);
|
||||
}
|
||||
|
||||
private static IPAddress FindVpnIp()
|
||||
{
|
||||
const String interfaceName = "innovenergy";
|
||||
|
||||
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||
|
||||
foreach (var networkInterface in networkInterfaces)
|
||||
{
|
||||
if (networkInterface.Name == interfaceName)
|
||||
{
|
||||
var ipProps = networkInterface.GetIPProperties();
|
||||
var uniCastIPs = ipProps.UnicastAddresses;
|
||||
var controllerIpAddress = uniCastIPs[0].Address;
|
||||
|
||||
Console.WriteLine("VPN IP is: "+ uniCastIPs[0].Address);
|
||||
return controllerIpAddress;
|
||||
}
|
||||
}
|
||||
|
||||
return IPAddress.None;
|
||||
}
|
||||
|
||||
public static Configuration? SetConfigurationFile()
|
||||
{
|
||||
if (_udpListener.Available > 0)
|
||||
{
|
||||
|
||||
IPEndPoint? serverEndpoint = null;
|
||||
|
||||
var replyMessage = "ACK";
|
||||
var replyData = Encoding.UTF8.GetBytes(replyMessage);
|
||||
|
||||
var udpMessage = _udpListener.Receive(ref serverEndpoint);
|
||||
var message = Encoding.UTF8.GetString(udpMessage);
|
||||
|
||||
var config = JsonSerializer.Deserialize<Configuration>(message);
|
||||
|
||||
if (config != null)
|
||||
{
|
||||
Console.WriteLine($"Received a configuration message: GridSetPoint is " + config.GridSetPoint +
|
||||
", MinimumSoC is " + config.MinimumSoC + " and ForceCalibrationCharge is " +
|
||||
config.CalibrationChargeState + " and CalibrationChargeDate is " +
|
||||
config.CalibrationChargeDate);
|
||||
|
||||
// Send the reply to the sender's endpoint
|
||||
_udpListener.Send(replyData, replyData.Length, serverEndpoint);
|
||||
Console.WriteLine($"Replied to {serverEndpoint}: {replyMessage}");
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
if (_endPoint != null && !_endPoint.Equals(_udpListener.Client.LocalEndPoint as IPEndPoint))
|
||||
{
|
||||
Console.WriteLine("UDP address has changed, rebinding...");
|
||||
InitializeCommunicationToMiddleware();
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using InnovEnergy.App.SodiStoreMax.DataTypes;
|
||||
using RabbitMQ.Client;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.MiddlewareClasses;
|
||||
|
||||
public static class RabbitMqManager
|
||||
{
|
||||
public static ConnectionFactory? Factory ;
|
||||
public static IConnection ? Connection;
|
||||
public static IModel? Channel;
|
||||
|
||||
public static Boolean SubscribeToQueue(StatusMessage currentSalimaxState, String? s3Bucket,String VpnServerIp)
|
||||
{
|
||||
try
|
||||
{
|
||||
//_factory = new ConnectionFactory { HostName = VpnServerIp };
|
||||
|
||||
Factory = new ConnectionFactory
|
||||
{
|
||||
HostName = VpnServerIp,
|
||||
Port = 5672,
|
||||
VirtualHost = "/",
|
||||
UserName = "producer",
|
||||
Password = "b187ceaddb54d5485063ddc1d41af66f",
|
||||
|
||||
};
|
||||
|
||||
Connection = Factory.CreateConnection();
|
||||
Channel = Connection.CreateModel();
|
||||
Channel.QueueDeclare(queue: "statusQueue", durable: true, exclusive: false, autoDelete: false, arguments: null);
|
||||
|
||||
Console.WriteLine("The controller sends its status to the middleware for the first time");
|
||||
if (s3Bucket != null) InformMiddleware(currentSalimaxState);
|
||||
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("An error occurred while connecting to the RabbitMQ queue: " + ex.Message);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void InformMiddleware(StatusMessage status)
|
||||
{
|
||||
var message = JsonSerializer.Serialize(status);
|
||||
var body = Encoding.UTF8.GetBytes(message);
|
||||
|
||||
Channel.BasicPublish(exchange: string.Empty,
|
||||
routingKey: "statusQueue",
|
||||
basicProperties: null,
|
||||
body: body);
|
||||
|
||||
Console.WriteLine($"Producer sent message: {message}");
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,962 @@
|
|||
#undef Amax
|
||||
#undef GridLimit
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.IO.Compression;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Threading.Tasks;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using Flurl.Http;
|
||||
using InnovEnergy.App.SodiStoreMax;
|
||||
using InnovEnergy.App.SodiStoreMax.Devices;
|
||||
using InnovEnergy.App.SodiStoreMax.Ess;
|
||||
using InnovEnergy.App.SodiStoreMax.MiddlewareClasses;
|
||||
using InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
|
||||
using InnovEnergy.App.SodiStoreMax.System;
|
||||
using InnovEnergy.App.SodiStoreMax.SystemConfig;
|
||||
using InnovEnergy.Lib.Devices.AMPT;
|
||||
using InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
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.Devices.Trumpf.TruConvertDc.Control;
|
||||
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||
using InnovEnergy.Lib.Units;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using InnovEnergy.App.SodiStoreMax.DataTypes;
|
||||
using InnovEnergy.Lib.Utils.Net;
|
||||
using static System.Int32;
|
||||
using static InnovEnergy.App.SodiStoreMax.AggregationService.Aggregator;
|
||||
using static InnovEnergy.App.SodiStoreMax.MiddlewareClasses.MiddlewareAgent;
|
||||
using static InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes.SystemConfig;
|
||||
using DeviceState = InnovEnergy.App.SodiStoreMax.Devices.DeviceState;
|
||||
|
||||
#pragma warning disable IL2026
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(2);
|
||||
|
||||
private static readonly IReadOnlyList<Byte> BatteryNodes;
|
||||
|
||||
private static readonly Channel TruConvertAcChannel;
|
||||
private static readonly Channel TruConvertDcChannel;
|
||||
private static readonly Channel GridMeterChannel;
|
||||
private static readonly Channel IslandBusLoadChannel;
|
||||
private static readonly Channel PvOnDc;
|
||||
private static readonly Channel PvOnAcGrid;
|
||||
private static readonly Channel PvOnAcIsland;
|
||||
private static readonly Channel RelaysChannel;
|
||||
private static readonly Channel RelaysTsChannel;
|
||||
private static readonly Channel BatteriesChannel;
|
||||
|
||||
private static Boolean _curtailFlag = false;
|
||||
private const String VpnServerIp = "10.2.0.11";
|
||||
private static Boolean _subscribedToQueue = false;
|
||||
private static Boolean _subscribeToQueueForTheFirstTime = false;
|
||||
private static SalimaxAlarmState _prevSalimaxState = SalimaxAlarmState.Green;
|
||||
private const UInt16 NbrOfFileToConcatenate = 30;
|
||||
private static UInt16 _counterOfFile = 0;
|
||||
private static SalimaxAlarmState _salimaxAlarmState = SalimaxAlarmState.Green;
|
||||
private const String Port = "/dev/ttyUSB0";
|
||||
|
||||
|
||||
static Program()
|
||||
{
|
||||
var config = Config.Load();
|
||||
var d = config.Devices;
|
||||
|
||||
Channel CreateChannel(SalimaxDevice device) => device.DeviceState == DeviceState.Disabled
|
||||
? new NullChannel()
|
||||
: new TcpChannel(device);
|
||||
|
||||
|
||||
TruConvertAcChannel = CreateChannel(d.TruConvertAcIp);
|
||||
TruConvertDcChannel = CreateChannel(d.TruConvertDcIp);
|
||||
GridMeterChannel = CreateChannel(d.GridMeterIp);
|
||||
IslandBusLoadChannel = CreateChannel(d.IslandBusLoadMeterIp);
|
||||
PvOnDc = CreateChannel(d.PvOnDc);
|
||||
PvOnAcGrid = CreateChannel(d.PvOnAcGrid);
|
||||
PvOnAcIsland = CreateChannel(d.PvOnAcIsland);
|
||||
RelaysChannel = CreateChannel(d.RelaysIp);
|
||||
RelaysTsChannel = CreateChannel(d.TsRelaysIp);
|
||||
BatteriesChannel = CreateChannel(d.BatteryIp);
|
||||
|
||||
BatteryNodes = config
|
||||
.Devices
|
||||
.BatteryNodes
|
||||
.Select(n => n.ConvertTo<Byte>())
|
||||
.ToArray(config.Devices.BatteryNodes.Length);
|
||||
}
|
||||
|
||||
public static async Task Main(String[] args)
|
||||
{
|
||||
//Do not await
|
||||
HourlyDataAggregationManager()
|
||||
.ContinueWith(t=>t.Exception.WriteLine(), TaskContinuationOptions.OnlyOnFaulted)
|
||||
.SupressAwaitWarning();
|
||||
|
||||
DailyDataAggregationManager()
|
||||
.ContinueWith(t=>t.Exception.WriteLine(), TaskContinuationOptions.OnlyOnFaulted)
|
||||
.SupressAwaitWarning();
|
||||
|
||||
InitializeCommunicationToMiddleware();
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Run();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.LogError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static async Task Run()
|
||||
{
|
||||
"Starting SodiStore Max".WriteLine();
|
||||
|
||||
Watchdog.NotifyReady();
|
||||
|
||||
var batteryDeligreenDevice = BatteryNodes.Select(n => new BatteryDeligreenDevice(Port, n))
|
||||
.ToList();
|
||||
|
||||
var batteryDevices = new BatteryDeligreenDevices(batteryDeligreenDevice);
|
||||
var acDcDevices = new TruConvertAcDcDevices(TruConvertAcChannel);
|
||||
var dcDcDevices = new TruConvertDcDcDevices(TruConvertDcChannel);
|
||||
var gridMeterDevice = new EmuMeterDevice(GridMeterChannel);
|
||||
var acIslandLoadMeter = new EmuMeterDevice(IslandBusLoadChannel);
|
||||
var pvOnDcDevice = new AmptDevices(PvOnDc);
|
||||
var pvOnAcGridDevice = new AmptDevices(PvOnAcGrid);
|
||||
var pvOnAcIslandDevice = new AmptDevices(PvOnAcIsland);
|
||||
var saliMaxTsRelaysDevice = new RelaysDeviceAdam6060(RelaysTsChannel);
|
||||
|
||||
|
||||
#if Amax
|
||||
var saliMaxRelaysDevice = new RelaysDeviceAmax(RelaysChannel);
|
||||
#else
|
||||
var saliMaxRelaysDevice = new RelaysDeviceAdam6360(RelaysChannel);
|
||||
#endif
|
||||
|
||||
|
||||
StatusRecord ReadStatus()
|
||||
{
|
||||
var config = Config.Load();
|
||||
var devices = config.Devices;
|
||||
var acDc = acDcDevices.Read();
|
||||
var dcDc = dcDcDevices.Read();
|
||||
var relays = saliMaxRelaysDevice.Read();
|
||||
var tsRelays = saliMaxTsRelaysDevice.Read();
|
||||
var loadOnAcIsland = acIslandLoadMeter.Read();
|
||||
var gridMeter = gridMeterDevice.Read();
|
||||
var pvOnDc = pvOnDcDevice.Read();
|
||||
var battery = batteryDevices.Read();
|
||||
|
||||
var pvOnAcGrid = pvOnAcGridDevice.Read();
|
||||
var pvOnAcIsland = pvOnAcIslandDevice.Read();
|
||||
|
||||
var gridBusToIslandBus = Topology.CalculateGridBusToIslandBusPower(pvOnAcIsland, loadOnAcIsland, acDc);
|
||||
|
||||
var gridBusLoad = devices.LoadOnAcGrid.DeviceState == DeviceState.Disabled
|
||||
? new AcPowerDevice { Power = 0 }
|
||||
: Topology.CalculateGridBusLoad(gridMeter, pvOnAcGrid, gridBusToIslandBus);
|
||||
|
||||
var dcLoad = devices.LoadOnDc.DeviceState == DeviceState.Disabled
|
||||
? new DcPowerDevice { Power = 0 }
|
||||
: Topology.CalculateDcLoad(acDc, pvOnDc, dcDc);
|
||||
|
||||
var acDcToDcLink = devices.LoadOnDc.DeviceState == DeviceState.Disabled ?
|
||||
Topology.CalculateAcDcToDcLink(pvOnDc, dcDc, acDc)
|
||||
: new DcPowerDevice{ Power = acDc.Dc.Power};
|
||||
|
||||
#if Amax
|
||||
var combinedRelays = relays;
|
||||
#else
|
||||
var combinedRelays = new CombinedAdamRelaysRecord(tsRelays, relays);
|
||||
#endif
|
||||
|
||||
return new StatusRecord
|
||||
{
|
||||
AcDc = acDc,
|
||||
DcDc = dcDc,
|
||||
Battery = battery,
|
||||
Relays = combinedRelays,
|
||||
GridMeter = gridMeter,
|
||||
PvOnAcGrid = pvOnAcGrid,
|
||||
PvOnAcIsland = pvOnAcIsland,
|
||||
PvOnDc = pvOnDc,
|
||||
AcGridToAcIsland = gridBusToIslandBus,
|
||||
AcDcToDcLink = acDcToDcLink,
|
||||
LoadOnAcGrid = gridBusLoad,
|
||||
LoadOnAcIsland = loadOnAcIsland,
|
||||
LoadOnDc = dcLoad,
|
||||
StateMachine = StateMachine.Default,
|
||||
EssControl = EssControl.Default,
|
||||
Log = new SystemLog { SalimaxAlarmState = SalimaxAlarmState.Green, Message = null, SalimaxAlarms = null, SalimaxWarnings = null}, //TODO: Put real stuff
|
||||
Config = config // load from disk every iteration, so config can be changed while running
|
||||
};
|
||||
}
|
||||
|
||||
void WriteControl(StatusRecord r)
|
||||
{
|
||||
if (r.Relays is not null)
|
||||
{
|
||||
#if Amax
|
||||
saliMaxRelaysDevice.Write((RelaysRecordAmax)r.Relays);
|
||||
#else
|
||||
|
||||
if (r.Relays is CombinedAdamRelaysRecord adamRelays)
|
||||
{
|
||||
saliMaxRelaysDevice.Write(adamRelays.GetAdam6360DRecord() ?? throw new InvalidOperationException());
|
||||
saliMaxTsRelaysDevice.Write(adamRelays.GetAdam6060Record() ?? throw new InvalidOperationException());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
acDcDevices.Write(r.AcDc);
|
||||
dcDcDevices.Write(r.DcDc);
|
||||
}
|
||||
|
||||
Console.WriteLine("press ctrl-c to stop");
|
||||
|
||||
while (true)
|
||||
{
|
||||
await Observable
|
||||
.Interval(UpdateInterval)
|
||||
.Select(_ => RunIteration())
|
||||
.SelectMany(r => UploadCsv(r, DateTime.Now.Round(UpdateInterval)))
|
||||
.SelectError()
|
||||
.ToTask();
|
||||
}
|
||||
|
||||
|
||||
StatusRecord RunIteration()
|
||||
{
|
||||
Watchdog.NotifyAlive();
|
||||
|
||||
var record = ReadStatus();
|
||||
/*
|
||||
if (record.Relays != null)
|
||||
{
|
||||
record.Relays.Do0StartPulse = true;
|
||||
|
||||
record.Relays.PulseOut0HighTime = 20000;
|
||||
record.Relays.PulseOut0LowTime = 20000;
|
||||
record.Relays.DigitalOutput0Mode = 2;
|
||||
|
||||
record.Relays.LedGreen = false;
|
||||
|
||||
record.Relays.Do0StartPulse.WriteLine(" = start pulse 0");
|
||||
|
||||
record.Relays.PulseOut0HighTime.WriteLine(" = PulseOut0HighTime");
|
||||
|
||||
record.Relays.PulseOut0LowTime.WriteLine(" = PulseOut0LowTime");
|
||||
|
||||
record.Relays.DigitalOutput0Mode.WriteLine(" = DigitalOutput0Mode");
|
||||
|
||||
record.Relays.LedGreen.WriteLine(" = LedGreen");
|
||||
|
||||
record.Relays.LedRed.WriteLine(" = LedRed");
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
" Relays are null".WriteLine();
|
||||
}*/
|
||||
|
||||
SendSalimaxStateAlarm(GetSalimaxStateAlarm(record), record); // to improve
|
||||
|
||||
record.ControlConstants();
|
||||
record.ControlSystemState();
|
||||
|
||||
record.ControlPvPower(record.Config.CurtailP, record.Config.PvInstalledPower);
|
||||
|
||||
var essControl = record.ControlEss().WriteLine();
|
||||
|
||||
record.EssControl = essControl;
|
||||
|
||||
record.AcDc.SystemControl.ApplyAcDcDefaultSettings();
|
||||
record.DcDc.SystemControl.ApplyDcDcDefaultSettings();
|
||||
|
||||
DistributePower(record, essControl);
|
||||
|
||||
record.PerformLed();
|
||||
|
||||
WriteControl(record);
|
||||
|
||||
$"{DateTime.Now.Round(UpdateInterval).ToUnixTime()} : {record.StateMachine.State}: {record.StateMachine.Message}".WriteLine();
|
||||
|
||||
record.CreateTopologyTextBlock().WriteLine();
|
||||
|
||||
(record.Relays is null ? "No relay Data available" : record.Relays.FiWarning ? "Alert: Fi Warning Detected" : "No Fi Warning Detected").WriteLine();
|
||||
(record.Relays is null ? "No relay Data available" : record.Relays.FiError ? "Alert: Fi Error Detected" : "No Fi Error Detected") .WriteLine();
|
||||
|
||||
record.Config.Save();
|
||||
|
||||
"===========================================".WriteLine();
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
// ReSharper disable once FunctionNeverReturns
|
||||
}
|
||||
|
||||
private static void SendSalimaxStateAlarm(StatusMessage currentSalimaxState, StatusRecord record)
|
||||
{
|
||||
var s3Bucket = Config.Load().S3?.Bucket;
|
||||
var subscribedNow = false;
|
||||
|
||||
//Every 15 iterations(30 seconds), the installation sends a heartbit message to the queue
|
||||
//_heartBitInterval++;
|
||||
|
||||
//When the controller boots, it tries to subscribe to the queue
|
||||
if (_subscribeToQueueForTheFirstTime == false)
|
||||
{
|
||||
subscribedNow = true;
|
||||
_subscribeToQueueForTheFirstTime = true;
|
||||
_prevSalimaxState = currentSalimaxState.Status;
|
||||
_subscribedToQueue = RabbitMqManager.SubscribeToQueue(currentSalimaxState, s3Bucket, VpnServerIp);
|
||||
}
|
||||
|
||||
//If already subscribed to the queue and the status has been changed, update the queue
|
||||
if (!subscribedNow && _subscribedToQueue && currentSalimaxState.Status != _prevSalimaxState)
|
||||
{
|
||||
_prevSalimaxState = currentSalimaxState.Status;
|
||||
if (s3Bucket != null)
|
||||
RabbitMqManager.InformMiddleware(currentSalimaxState);
|
||||
}
|
||||
// else if (_subscribedToQueue && _heartBitInterval >= 30)
|
||||
// {
|
||||
// //Send a heartbit to the backend
|
||||
// Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------");
|
||||
// _heartBitInterval = 0;
|
||||
// currentSalimaxState.Type = MessageType.Heartbit;
|
||||
//
|
||||
// if (s3Bucket != null)
|
||||
// RabbitMqManager.InformMiddleware(currentSalimaxState);
|
||||
// }
|
||||
|
||||
//If there is an available message from the RabbitMQ Broker, apply the configuration file
|
||||
Configuration? config = SetConfigurationFile();
|
||||
if (config != null)
|
||||
{
|
||||
record.ApplyConfigFile(config);
|
||||
}
|
||||
}
|
||||
|
||||
// This preparing a message to send to salimax monitor
|
||||
private static StatusMessage GetSalimaxStateAlarm(StatusRecord record)
|
||||
{
|
||||
var alarmCondition = record.DetectAlarmStates(); // this need to be emailed to support or customer
|
||||
var s3Bucket = Config.Load().S3?.Bucket;
|
||||
|
||||
var alarmList = new List<AlarmOrWarning>();
|
||||
var warningList = new List<AlarmOrWarning>();
|
||||
var bAlarmList = new List<String>();
|
||||
var bWarningList = new List<String>();
|
||||
|
||||
/*
|
||||
if (record.Battery != null)
|
||||
{
|
||||
var i = 0;
|
||||
|
||||
foreach (var battery in record.Battery.Devices)
|
||||
{
|
||||
var devicesBatteryNode = record.Config.Devices.BatteryNodes[i];
|
||||
|
||||
if (battery.LimpBitMap == 0)
|
||||
{
|
||||
// "All String are Active".WriteLine();
|
||||
}
|
||||
else if (IsPowerOfTwo(battery.LimpBitMap))
|
||||
{
|
||||
"1 String is disabled".WriteLine();
|
||||
Console.WriteLine(" ****************** ");
|
||||
|
||||
warningList.Add(new AlarmOrWarning
|
||||
{
|
||||
Date = DateTime.Now.ToString("yyyy-MM-dd"),
|
||||
Time = DateTime.Now.ToString("HH:mm:ss"),
|
||||
CreatedBy = "Battery node" + devicesBatteryNode,
|
||||
Description = "1 String is disabled"
|
||||
});
|
||||
|
||||
bWarningList.Add("/"+i+1 + "/1 String is disabled"); // battery id instead ( i +1 ) of node id: requested from the frontend
|
||||
}
|
||||
else
|
||||
{
|
||||
"2 or more string are disabled".WriteLine();
|
||||
Console.WriteLine(" ****************** ");
|
||||
|
||||
alarmList.Add(new AlarmOrWarning
|
||||
{
|
||||
Date = DateTime.Now.ToString("yyyy-MM-dd"),
|
||||
Time = DateTime.Now.ToString("HH:mm:ss"),
|
||||
CreatedBy = "Battery node" + devicesBatteryNode,
|
||||
Description = "2 or more string are disabled"
|
||||
});
|
||||
bAlarmList.Add(i +";2 or more string are disabled");
|
||||
}
|
||||
|
||||
foreach (var warning in record.Battery.Warnings)
|
||||
{
|
||||
warningList.Add(new AlarmOrWarning
|
||||
{
|
||||
Date = DateTime.Now.ToString("yyyy-MM-dd"),
|
||||
Time = DateTime.Now.ToString("HH:mm:ss"),
|
||||
CreatedBy = "Battery node" + devicesBatteryNode,
|
||||
Description = warning
|
||||
});
|
||||
bWarningList.Add(i +";" + warning);
|
||||
}
|
||||
|
||||
foreach (var alarm in battery.Alarms)
|
||||
{
|
||||
alarmList.Add(new AlarmOrWarning
|
||||
{
|
||||
Date = DateTime.Now.ToString("yyyy-MM-dd"),
|
||||
Time = DateTime.Now.ToString("HH:mm:ss"),
|
||||
CreatedBy = "Battery node" + devicesBatteryNode,
|
||||
Description = alarm
|
||||
});
|
||||
bWarningList.Add(i +";" + alarm);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}*/
|
||||
|
||||
if (alarmCondition is not null)
|
||||
{
|
||||
alarmCondition.WriteLine();
|
||||
|
||||
alarmList.Add(new AlarmOrWarning
|
||||
{
|
||||
Date = DateTime.Now.ToString("yyyy-MM-dd"),
|
||||
Time = DateTime.Now.ToString("HH:mm:ss"),
|
||||
CreatedBy = "Salimax",
|
||||
Description = alarmCondition
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var alarm in record.AcDc.Alarms)
|
||||
{
|
||||
alarmList.Add(new AlarmOrWarning
|
||||
{
|
||||
Date = DateTime.Now.ToString("yyyy-MM-dd"),
|
||||
Time = DateTime.Now.ToString("HH:mm:ss"),
|
||||
CreatedBy = "AcDc",
|
||||
Description = alarm.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var alarm in record.DcDc.Alarms)
|
||||
{
|
||||
alarmList.Add(new AlarmOrWarning
|
||||
{
|
||||
Date = DateTime.Now.ToString("yyyy-MM-dd"),
|
||||
Time = DateTime.Now.ToString("HH:mm:ss"),
|
||||
CreatedBy = "DcDc",
|
||||
Description = alarm.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var warning in record.AcDc.Warnings)
|
||||
{
|
||||
warningList.Add(new AlarmOrWarning
|
||||
{
|
||||
Date = DateTime.Now.ToString("yyyy-MM-dd"),
|
||||
Time = DateTime.Now.ToString("HH:mm:ss"),
|
||||
CreatedBy = "AcDc",
|
||||
Description = warning.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var warning in record.DcDc.Warnings)
|
||||
{
|
||||
warningList.Add(new AlarmOrWarning
|
||||
{
|
||||
Date = DateTime.Now.ToString("yyyy-MM-dd"),
|
||||
Time = DateTime.Now.ToString("HH:mm:ss"),
|
||||
CreatedBy = "DcDc",
|
||||
Description = warning.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
_salimaxAlarmState = warningList.Any()
|
||||
? SalimaxAlarmState.Orange
|
||||
: SalimaxAlarmState.Green; // this will be replaced by LedState
|
||||
|
||||
_salimaxAlarmState = alarmList.Any()
|
||||
? SalimaxAlarmState.Red
|
||||
: _salimaxAlarmState; // this will be replaced by LedState
|
||||
|
||||
TryParse(s3Bucket?.Split("-")[0], out var installationId);
|
||||
|
||||
var returnedStatus = new StatusMessage
|
||||
{
|
||||
InstallationId = installationId,
|
||||
Product = 0,
|
||||
Status = _salimaxAlarmState,
|
||||
Type = MessageType.AlarmOrWarning,
|
||||
Alarms = alarmList,
|
||||
Warnings = warningList
|
||||
};
|
||||
|
||||
return returnedStatus;
|
||||
}
|
||||
|
||||
private static String? DetectAlarmStates(this StatusRecord r) => r.Relays switch
|
||||
{
|
||||
{ K2ConnectIslandBusToGridBus: false, K2IslandBusIsConnectedToGridBus: true } => " Contradiction: R0 is opening the K2 but the K2 is still close ",
|
||||
{ K1GridBusIsConnectedToGrid : false, K2IslandBusIsConnectedToGridBus: true } => " Contradiction: K1 is open but the K2 is still close ",
|
||||
{ FiError: true, K2IslandBusIsConnectedToGridBus: true } => " Contradiction: Fi error occured but the K2 is still close ",
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static void ControlConstants(this StatusRecord r)
|
||||
{
|
||||
var inverters = r.AcDc.Devices;
|
||||
var dcDevices = r.DcDc.Devices;
|
||||
var configFile = r.Config;
|
||||
//var maxBatteryDischargingCurrentLive = 0.0; //never used with deligreenBattery
|
||||
var devicesConfig = r.AcDc.Devices.All(d => d.Control.Ac.GridType == GridType.GridTied400V50Hz) ? configFile.GridTie : configFile.IslandMode; // TODO if any of the grid tie mode
|
||||
/*
|
||||
// This adapting the max discharging current to the current Active Strings
|
||||
if (r.Battery != null)
|
||||
{
|
||||
const Int32 stringsByBattery = 5;
|
||||
var numberOfBatteriesConfigured = r.Config.Devices.BatteryNodes.Length;
|
||||
var numberOfTotalStrings = stringsByBattery * numberOfBatteriesConfigured;
|
||||
var dischargingCurrentByString = devicesConfig.DcDc.MaxBatteryDischargingCurrent / numberOfTotalStrings;
|
||||
|
||||
var boolList = new List<Boolean>();
|
||||
|
||||
foreach (var stringActive in r.Battery.Devices.Select(b => b.BatteryStrings).ToList())
|
||||
{
|
||||
boolList.Add(stringActive.String1Active);
|
||||
boolList.Add(stringActive.String2Active);
|
||||
boolList.Add(stringActive.String3Active);
|
||||
boolList.Add(stringActive.String4Active);
|
||||
boolList.Add(stringActive.String5Active);
|
||||
}
|
||||
|
||||
var numberOfBatteriesStringActive = boolList.Count(b => b);
|
||||
|
||||
if (numberOfTotalStrings != 0)
|
||||
{
|
||||
maxBatteryDischargingCurrentLive = dischargingCurrentByString * numberOfBatteriesStringActive;
|
||||
}
|
||||
}
|
||||
*/
|
||||
// TODO The discharging current is well calculated but not communicated to live. But Written in S3
|
||||
|
||||
|
||||
inverters.ForEach(d => d.Control.Dc.MaxVoltage = devicesConfig.AcDc.MaxDcLinkVoltage);
|
||||
inverters.ForEach(d => d.Control.Dc.MinVoltage = devicesConfig.AcDc.MinDcLinkVoltage);
|
||||
inverters.ForEach(d => d.Control.Dc.ReferenceVoltage = devicesConfig.AcDc.ReferenceDcLinkVoltage);
|
||||
|
||||
inverters.ForEach(d => d.Control.Dc.PrechargeConfig = DcPrechargeConfig.PrechargeDcWithInternal);
|
||||
|
||||
dcDevices.ForEach(d => d.Control.DroopControl.UpperVoltage = devicesConfig.DcDc.UpperDcLinkVoltage);
|
||||
dcDevices.ForEach(d => d.Control.DroopControl.LowerVoltage = devicesConfig.DcDc.LowerDcLinkVoltage);
|
||||
dcDevices.ForEach(d => d.Control.DroopControl.ReferenceVoltage = devicesConfig.DcDc.ReferenceDcLinkVoltage);
|
||||
|
||||
dcDevices.ForEach(d => d.Control.CurrentControl.MaxBatteryChargingCurrent = devicesConfig.DcDc.MaxBatteryChargingCurrent);
|
||||
dcDevices.ForEach(d => d.Control.CurrentControl.MaxBatteryDischargingCurrent = devicesConfig.DcDc.MaxBatteryDischargingCurrent);
|
||||
dcDevices.ForEach(d => d.Control.MaxDcPower = devicesConfig.DcDc.MaxDcPower);
|
||||
|
||||
dcDevices.ForEach(d => d.Control.VoltageLimits.MaxBatteryVoltage = devicesConfig.DcDc.MaxChargeBatteryVoltage);
|
||||
dcDevices.ForEach(d => d.Control.VoltageLimits.MinBatteryVoltage = devicesConfig.DcDc.MinDischargeBatteryVoltage);
|
||||
dcDevices.ForEach(d => d.Control.ControlMode = DcControlMode.VoltageDroop);
|
||||
|
||||
r.DcDc.ResetAlarms();
|
||||
r.AcDc.ResetAlarms();
|
||||
}
|
||||
|
||||
// This will be used for provider throttling, this example is only for either 100% or 0 %
|
||||
private static void ControlPvPower(this StatusRecord r, UInt16 exportLimit = 0, UInt16 pvInstalledPower = 20)
|
||||
{
|
||||
// Maybe add a condition to do this only if we are in optimised Self consumption, this is not true
|
||||
|
||||
if (r.GridMeter?.Ac.Power.Active == null)
|
||||
{
|
||||
Console.WriteLine(" No reading from Grid meter");
|
||||
return;
|
||||
}
|
||||
|
||||
if (pvInstalledPower == 0)
|
||||
{
|
||||
Console.WriteLine(" No curtailing, because Pv installed is equal to 0");
|
||||
return;
|
||||
}
|
||||
|
||||
const Int32 constantDeadBand = 5000; // magic number
|
||||
const Double voltageRange = 100; // 100 Voltage configured rang for PV slope, if the configured slope change this must change also
|
||||
var configFile = r.Config;
|
||||
var inverters = r.AcDc.Devices;
|
||||
var systemExportLimit = - exportLimit * 1000 ; // Conversion from Kw in W // the config file value is positive and limit should be negative from 0 to ...
|
||||
var stepSize = ClampStepSize((UInt16)Math.Floor(voltageRange/ pvInstalledPower)); // in Voltage per 1 Kw
|
||||
var deadBand = constantDeadBand/stepSize;
|
||||
|
||||
// LINQ query to select distinct ActiveUpperVoltage
|
||||
var result = r.AcDc.Devices
|
||||
.Select(device => device?.Status?.DcVoltages?.Active?.ActiveUpperVoltage)
|
||||
.Select(voltage => voltage.Value) // Extract the value since we've confirmed it's non-null
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
Double upperVoltage;
|
||||
|
||||
if (result.Count == 1)
|
||||
{
|
||||
upperVoltage = result[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(" Different ActiveUpperVoltage between inverters "); // this should be reported to salimax Alarm
|
||||
return;
|
||||
}
|
||||
|
||||
/************* For debugging purposes ********************/
|
||||
|
||||
systemExportLimit.WriteLine(" Export Limit in W");
|
||||
upperVoltage.WriteLine(" Upper Voltage");
|
||||
r.GridMeter.Ac.Power.Active.WriteLine(" Active Export");
|
||||
Console.WriteLine(" ****************** ");
|
||||
|
||||
/*********************************************************/
|
||||
|
||||
if (r.GridMeter.Ac.Power.Active < systemExportLimit)
|
||||
{
|
||||
_curtailFlag = true;
|
||||
upperVoltage = IncreaseInverterUpperLimit(upperVoltage, stepSize);
|
||||
upperVoltage.WriteLine("Upper Voltage Increased: New Upper limit");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_curtailFlag)
|
||||
{
|
||||
if (r.GridMeter.Ac.Power.Active > (systemExportLimit + deadBand))
|
||||
{
|
||||
upperVoltage = DecreaseInverterUpperLimit(upperVoltage, stepSize);
|
||||
|
||||
if (upperVoltage <= configFile.GridTie.AcDc.MaxDcLinkVoltage)
|
||||
{
|
||||
_curtailFlag = false;
|
||||
upperVoltage = configFile.GridTie.AcDc.MaxDcLinkVoltage;
|
||||
upperVoltage.WriteLine(" New Upper limit");
|
||||
Console.WriteLine("Upper Voltage decreased: Smaller than the default value, value clamped");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Upper Voltage decreased: New Upper limit");
|
||||
upperVoltage.WriteLine(" New Upper limit");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
deadBand.WriteLine("W :We are in Dead band area");
|
||||
upperVoltage.WriteLine(" same Upper limit from last cycle");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Curtail Flag is false , no need to curtail");
|
||||
upperVoltage.WriteLine(" same Upper limit from last cycle");
|
||||
}
|
||||
}
|
||||
inverters.ForEach(d => d.Control.Dc.MaxVoltage = upperVoltage);
|
||||
Console.WriteLine(" ****************** ");
|
||||
}
|
||||
|
||||
// why this is not in Controller?
|
||||
private static void DistributePower(StatusRecord record, EssControl essControl)
|
||||
{
|
||||
var nInverters = record.AcDc.Devices.Count;
|
||||
|
||||
var powerPerInverterPhase = nInverters > 0
|
||||
? essControl.PowerSetpoint / nInverters / 3
|
||||
: 0;
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
// To test, most probably the curtailing flag will not work
|
||||
private static void PerformLed(this StatusRecord record)
|
||||
{
|
||||
if (record.StateMachine.State == 23)
|
||||
{
|
||||
switch (record.EssControl.Mode)
|
||||
{
|
||||
case EssMode.CalibrationCharge:
|
||||
record.Relays?.PerformSlowFlashingGreenLed();
|
||||
break;
|
||||
case EssMode.OptimizeSelfConsumption when !_curtailFlag:
|
||||
record.Relays?.PerformSolidGreenLed();
|
||||
break;
|
||||
case EssMode.Off:
|
||||
break;
|
||||
case EssMode.OffGrid:
|
||||
break;
|
||||
case EssMode.HeatBatteries:
|
||||
break;
|
||||
case EssMode.ReachMinSoc:
|
||||
break;
|
||||
case EssMode.NoGridMeter:
|
||||
break;
|
||||
default:
|
||||
{
|
||||
if (_curtailFlag)
|
||||
{
|
||||
record.Relays?.PerformFastFlashingGreenLed();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (record.Battery?.Soc != null && record.StateMachine.State != 23 && record.Battery.Soc > 50)
|
||||
{
|
||||
record.Relays?.PerformSolidOrangeLed();
|
||||
}
|
||||
else if (record.Battery?.Soc != null && record.StateMachine.State != 23 && record.Battery.Soc < 50 && record.Battery.Soc > 20)
|
||||
{
|
||||
record.Relays?.PerformSlowFlashingOrangeLed();
|
||||
}
|
||||
else if (record.Battery?.Soc != null && record.StateMachine.State != 23 && record.Battery.Soc < 20)
|
||||
{
|
||||
record.Relays?.PerformFastFlashingOrangeLed();
|
||||
}
|
||||
|
||||
var criticalAlarm = record.DetectAlarmStates();
|
||||
|
||||
if (criticalAlarm is not null)
|
||||
{
|
||||
record.Relays?.PerformFastFlashingRedLed();
|
||||
}
|
||||
}
|
||||
|
||||
private static Double IncreaseInverterUpperLimit(Double upperLimit, Double stepSize)
|
||||
{
|
||||
return upperLimit + stepSize;
|
||||
}
|
||||
|
||||
private static Double DecreaseInverterUpperLimit(Double upperLimit, Double stepSize)
|
||||
{
|
||||
return upperLimit - stepSize;
|
||||
}
|
||||
|
||||
private static UInt16 ClampStepSize(UInt16 stepSize)
|
||||
{
|
||||
return stepSize switch
|
||||
{
|
||||
> 5 => 5,
|
||||
<= 1 => 1,
|
||||
_ => stepSize
|
||||
};
|
||||
}
|
||||
|
||||
private static void ApplyAcDcDefaultSettings(this SystemControlRegisters? sc)
|
||||
{
|
||||
if (sc is null)
|
||||
return;
|
||||
|
||||
sc.ReferenceFrame = ReferenceFrame.Consumer;
|
||||
sc.SystemConfig = AcDcAndDcDc;
|
||||
|
||||
#if DEBUG
|
||||
sc.CommunicationTimeout = TimeSpan.FromMinutes(2);
|
||||
#else
|
||||
sc.CommunicationTimeout = TimeSpan.FromSeconds(20);
|
||||
#endif
|
||||
|
||||
sc.PowerSetPointActivation = PowerSetPointActivation.Immediate;
|
||||
sc.UseSlaveIdForAddressing = true;
|
||||
sc.SlaveErrorHandling = SlaveErrorHandling.Relaxed;
|
||||
sc.SubSlaveErrorHandling = SubSlaveErrorHandling.Off;
|
||||
|
||||
sc.ResetAlarmsAndWarnings = true;
|
||||
}
|
||||
|
||||
private static void ApplyDcDcDefaultSettings(this SystemControlRegisters? sc)
|
||||
{
|
||||
|
||||
if (sc is null)
|
||||
return;
|
||||
|
||||
sc.SystemConfig = DcDcOnly;
|
||||
#if DEBUG
|
||||
sc.CommunicationTimeout = TimeSpan.FromMinutes(2);
|
||||
#else
|
||||
sc.CommunicationTimeout = TimeSpan.FromSeconds(20);
|
||||
#endif
|
||||
|
||||
sc.PowerSetPointActivation = PowerSetPointActivation.Immediate;
|
||||
sc.UseSlaveIdForAddressing = true;
|
||||
sc.SlaveErrorHandling = SlaveErrorHandling.Relaxed;
|
||||
sc.SubSlaveErrorHandling = SubSlaveErrorHandling.Off;
|
||||
|
||||
sc.ResetAlarmsAndWarnings = true;
|
||||
}
|
||||
|
||||
private static async Task<Boolean> UploadCsv(StatusRecord status, DateTime timeStamp)
|
||||
{
|
||||
|
||||
var csv = status.ToCsv().LogInfo();
|
||||
|
||||
await RestApiSavingFile(csv);
|
||||
|
||||
var s3Config = status.Config.S3;
|
||||
|
||||
if (s3Config is null)
|
||||
return false;
|
||||
|
||||
//Concatenating 15 files in one file
|
||||
return await ConcatinatingAndCompressingFiles(timeStamp, s3Config);
|
||||
}
|
||||
|
||||
private static async Task<Boolean> ConcatinatingAndCompressingFiles(DateTime timeStamp, S3Config s3Config)
|
||||
{
|
||||
if (_counterOfFile >= NbrOfFileToConcatenate)
|
||||
{
|
||||
_counterOfFile = 0;
|
||||
|
||||
var logFileConcatenator = new LogFileConcatenator();
|
||||
|
||||
var s3Path = timeStamp.ToUnixTime() + ".csv";
|
||||
s3Path.WriteLine("");
|
||||
var csvToSend = logFileConcatenator.ConcatenateFiles(NbrOfFileToConcatenate);
|
||||
|
||||
var request = s3Config.CreatePutRequest(s3Path);
|
||||
|
||||
//Use this for no compression
|
||||
//var response = await request.PutAsync(new StringContent(csv));
|
||||
var compressedBytes = CompresseBytes(csvToSend);
|
||||
|
||||
// Encode the compressed byte array as a Base64 string
|
||||
string base64String = Convert.ToBase64String(compressedBytes);
|
||||
|
||||
// Create StringContent from Base64 string
|
||||
var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64");
|
||||
|
||||
// Upload the compressed data (ZIP archive) to S3
|
||||
var response = await request.PutAsync(stringContent);
|
||||
|
||||
if (response.StatusCode != 200)
|
||||
{
|
||||
Console.WriteLine("ERROR: PUT");
|
||||
var error = await response.GetStringAsync();
|
||||
Console.WriteLine(error);
|
||||
Heartbit(new DateTime(0));
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------");
|
||||
|
||||
Heartbit(timeStamp);
|
||||
}
|
||||
_counterOfFile++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void Heartbit(DateTime timeStamp)
|
||||
{
|
||||
var s3Bucket = Config.Load().S3?.Bucket;
|
||||
var tryParse = TryParse(s3Bucket?.Split("-")[0], out var installationId);
|
||||
var parse = TryParse(timeStamp.ToUnixTime().ToString(), out var nameOfCsvFile);
|
||||
|
||||
if (tryParse)
|
||||
{
|
||||
var returnedStatus = new StatusMessage
|
||||
{
|
||||
InstallationId = installationId,
|
||||
Product = 0, // Salimax is always 0
|
||||
Status = _salimaxAlarmState,
|
||||
Type = MessageType.Heartbit,
|
||||
Timestamp = nameOfCsvFile
|
||||
};
|
||||
if (s3Bucket != null)
|
||||
RabbitMqManager.InformMiddleware(returnedStatus);
|
||||
}
|
||||
}
|
||||
|
||||
private static Byte[] CompresseBytes(String csvToSend)
|
||||
{
|
||||
//Compress CSV data to a byte array
|
||||
Byte[] compressedBytes;
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
//Create a zip directory and put the compressed file inside
|
||||
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
|
||||
{
|
||||
var entry = archive.CreateEntry("data.csv", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive
|
||||
using (var entryStream = entry.Open())
|
||||
using (var writer = new StreamWriter(entryStream))
|
||||
{
|
||||
writer.Write(csvToSend);
|
||||
}
|
||||
}
|
||||
|
||||
compressedBytes = memoryStream.ToArray();
|
||||
}
|
||||
|
||||
return compressedBytes;
|
||||
}
|
||||
|
||||
private static async Task RestApiSavingFile(String csv)
|
||||
{
|
||||
// This is for the Rest API
|
||||
// Check if the directory exists, and create it if it doesn't
|
||||
const String directoryPath = "/var/www/html";
|
||||
|
||||
if (!Directory.Exists(directoryPath))
|
||||
{
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
}
|
||||
|
||||
string filePath = Path.Combine(directoryPath, "status.csv");
|
||||
|
||||
await File.WriteAllTextAsync(filePath, csv.SplitLines().Where(l => !l.Contains("Secret")).JoinLines());
|
||||
}
|
||||
|
||||
private static Boolean IsPowerOfTwo(Int32 n)
|
||||
{
|
||||
return n > 0 && (n & (n - 1)) == 0;
|
||||
}
|
||||
|
||||
private static void ApplyConfigFile(this StatusRecord status, Configuration? config)
|
||||
{
|
||||
if (config == null) return;
|
||||
|
||||
status.Config.MinSoc = config.MinimumSoC;
|
||||
status.Config.GridSetPoint = config.GridSetPoint * 1000; // converted from kW to W
|
||||
// status.Config.ForceCalibrationChargeState = config.CalibrationChargeState;
|
||||
//
|
||||
// if (config.CalibrationChargeState == CalibrationChargeType.RepetitivelyEvery)
|
||||
// {
|
||||
// status.Config.DayAndTimeForRepetitiveCalibration = config.CalibrationChargeDate;
|
||||
// }
|
||||
// else if (config.CalibrationChargeState == CalibrationChargeType.AdditionallyOnce)
|
||||
// {
|
||||
// status.Config.DayAndTimeForAdditionalCalibration = config.CalibrationChargeDate;
|
||||
// }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
using System.Security.Cryptography;
|
||||
using Flurl;
|
||||
using Flurl.Http;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using static System.Text.Encoding;
|
||||
using Convert = System.Convert;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax;
|
||||
|
||||
public record S3Config
|
||||
{
|
||||
public required String Bucket { get; init; }
|
||||
public required String Region { get; init; }
|
||||
public required String Provider { get; init; }
|
||||
public required String Key { get; init; }
|
||||
public required String Secret { get; init; }
|
||||
public required String ContentType { get; init; }
|
||||
|
||||
public String Host => $"{Bucket}.{Region}.{Provider}";
|
||||
public String Url => $"https://{Host}";
|
||||
|
||||
public IFlurlRequest CreatePutRequest(String s3Path) => CreateRequest("PUT", s3Path);
|
||||
public IFlurlRequest CreateGetRequest(String s3Path) => CreateRequest("GET", s3Path);
|
||||
|
||||
private IFlurlRequest CreateRequest(String method, String s3Path)
|
||||
{
|
||||
var date = DateTime.UtcNow.ToString("r");
|
||||
var auth = CreateAuthorization(method, s3Path, date);
|
||||
|
||||
return Url
|
||||
.AppendPathSegment(s3Path)
|
||||
.WithHeader("Host", Host)
|
||||
.WithHeader("Date", date)
|
||||
.WithHeader("Authorization", auth)
|
||||
.AllowAnyHttpStatus();
|
||||
}
|
||||
|
||||
private String CreateAuthorization(String method,
|
||||
String s3Path,
|
||||
String date)
|
||||
{
|
||||
return CreateAuthorization
|
||||
(
|
||||
method : method,
|
||||
bucket : Bucket,
|
||||
s3Path : s3Path,
|
||||
date : date,
|
||||
s3Key : Key,
|
||||
s3Secret : Secret,
|
||||
contentType: ContentType
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static String CreateAuthorization(String method,
|
||||
String bucket,
|
||||
String s3Path,
|
||||
String date,
|
||||
String s3Key,
|
||||
String s3Secret,
|
||||
String contentType = "application/base64",
|
||||
String md5Hash = "")
|
||||
{
|
||||
|
||||
contentType = "application/base64; charset=utf-8";
|
||||
//contentType = "text/plain; charset=utf-8"; //this to use when sending plain csv to S3
|
||||
|
||||
var payload = $"{method}\n{md5Hash}\n{contentType}\n{date}\n/{bucket.Trim('/')}/{s3Path.Trim('/')}";
|
||||
using var hmacSha1 = new HMACSHA1(UTF8.GetBytes(s3Secret));
|
||||
|
||||
var signature = UTF8
|
||||
.GetBytes(payload)
|
||||
.Apply(hmacSha1.ComputeHash)
|
||||
.Apply(Convert.ToBase64String);
|
||||
|
||||
return $"AWS {s3Key}:{signature}";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
using System.Reflection.Metadata;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
|
||||
#pragma warning disable CS8602 // Dereference of a possibly null reference.
|
||||
|
||||
public class CombinedAdamRelaysRecord : IRelaysRecord
|
||||
{
|
||||
private const UInt16 SlowFreq = 3000;
|
||||
private const UInt16 HighFreq = 1000;
|
||||
|
||||
public CombinedAdamRelaysRecord(RelaysRecordAdam6060? relaysRecordAdam6060, RelaysRecordAdam6360D? relaysRecordAdam6360D)
|
||||
{
|
||||
_recordAdam6060 = relaysRecordAdam6060;
|
||||
_recordAdam6360D = relaysRecordAdam6360D;
|
||||
}
|
||||
|
||||
private static RelaysRecordAdam6060? _recordAdam6060;
|
||||
private static RelaysRecordAdam6360D? _recordAdam6360D;
|
||||
|
||||
public static IRelaysRecord Instance { get; } = new CombinedAdamRelaysRecord(_recordAdam6060, _recordAdam6360D);
|
||||
|
||||
|
||||
public Boolean K1GridBusIsConnectedToGrid => _recordAdam6360D.K1GridBusIsConnectedToGrid;
|
||||
|
||||
public Boolean K2IslandBusIsConnectedToGridBus => _recordAdam6360D.K2IslandBusIsConnectedToGridBus;
|
||||
public Boolean FiWarning => _recordAdam6360D.FiWarning;
|
||||
public Boolean FiError => _recordAdam6360D.FiError;
|
||||
public Boolean K2ConnectIslandBusToGridBus
|
||||
{
|
||||
get => _recordAdam6360D.K2ConnectIslandBusToGridBus;
|
||||
set => _recordAdam6360D.K2ConnectIslandBusToGridBus = value;
|
||||
}
|
||||
|
||||
public Boolean Inverter1WagoStatus => _recordAdam6360D.Inverter1WagoStatus;
|
||||
public Boolean Inverter2WagoStatus => _recordAdam6360D.Inverter2WagoStatus;
|
||||
public Boolean Inverter3WagoStatus => _recordAdam6360D.Inverter3WagoStatus;
|
||||
public Boolean Inverter4WagoStatus => _recordAdam6360D.Inverter4WagoStatus;
|
||||
|
||||
public Boolean Dc1WagoStatus => _recordAdam6060.Dc1WagoStatus;
|
||||
public Boolean Dc2WagoStatus => _recordAdam6060.Dc2WagoStatus;
|
||||
public Boolean Dc3WagoStatus => _recordAdam6060.Dc3WagoStatus;
|
||||
public Boolean Dc4WagoStatus => _recordAdam6060.Dc4WagoStatus;
|
||||
public Boolean DcSystemControlWagoStatus => _recordAdam6060.DcSystemControlWagoStatus;
|
||||
|
||||
public Boolean LedGreen { get => _recordAdam6360D.LedGreen; set => _recordAdam6360D.LedGreen = value;}
|
||||
public Boolean LedRed { get => _recordAdam6360D.LedRed; set => _recordAdam6360D.LedRed = value;}
|
||||
public Boolean Harvester1Step => _recordAdam6360D.Harvester1Step;
|
||||
public Boolean Harvester2Step => _recordAdam6360D.Harvester2Step;
|
||||
public Boolean Harvester3Step => _recordAdam6360D.Harvester3Step;
|
||||
public Boolean Harvester4Step => _recordAdam6360D.Harvester4Step;
|
||||
|
||||
public UInt16 DigitalOutput0Mode { get => _recordAdam6360D.DigitalOutput0Mode; set => _recordAdam6360D.DigitalOutput0Mode = value; }
|
||||
|
||||
public UInt16 DigitalOutput1Mode
|
||||
{
|
||||
get => _recordAdam6360D.DigitalOutput1Mode;
|
||||
set => _recordAdam6360D.DigitalOutput1Mode = value;
|
||||
}
|
||||
|
||||
public UInt16 DigitalOutput2Mode
|
||||
{
|
||||
get => _recordAdam6360D.DigitalOutput2Mode;
|
||||
set => _recordAdam6360D.DigitalOutput2Mode = value;
|
||||
}
|
||||
|
||||
public UInt16 DigitalOutput3Mode
|
||||
{
|
||||
get => _recordAdam6360D.DigitalOutput3Mode;
|
||||
set => _recordAdam6360D.DigitalOutput3Mode = value;
|
||||
}
|
||||
|
||||
public UInt16 DigitalOutput4Mode
|
||||
{
|
||||
get => _recordAdam6360D.DigitalOutput4Mode;
|
||||
set => _recordAdam6360D.DigitalOutput4Mode = value;
|
||||
}
|
||||
|
||||
public UInt16 DigitalOutput5Mode
|
||||
{
|
||||
get => _recordAdam6360D.DigitalOutput5Mode;
|
||||
set => _recordAdam6360D.DigitalOutput5Mode = value;
|
||||
}
|
||||
|
||||
public Boolean Do0StartPulse { get => _recordAdam6360D.Do0Pulse; set => _recordAdam6360D.Do0Pulse = value; }
|
||||
public Boolean Do1StartPulse { get => _recordAdam6360D.Do1Pulse; set => _recordAdam6360D.Do1Pulse = value; }
|
||||
public Boolean Do2StartPulse { get => _recordAdam6360D.Do2Pulse; set => _recordAdam6360D.Do2Pulse = value; }
|
||||
public Boolean Do3StartPulse { get => _recordAdam6360D.Do3Pulse; set => _recordAdam6360D.Do3Pulse = value; }
|
||||
public Boolean Do4StartPulse { get => _recordAdam6360D.Do4Pulse; set => _recordAdam6360D.Do4Pulse = value; }
|
||||
public Boolean Do5StartPulse { get => _recordAdam6360D.Do5Pulse; set => _recordAdam6360D.Do5Pulse = value; }
|
||||
|
||||
|
||||
public UInt16 PulseOut0LowTime { get => _recordAdam6360D.PulseOut0LowTime; set => _recordAdam6360D.PulseOut0LowTime = value; }
|
||||
public UInt16 PulseOut1LowTime { get => _recordAdam6360D.PulseOut1LowTime; set => _recordAdam6360D.PulseOut1LowTime = value; }
|
||||
public UInt16 PulseOut2LowTime { get => _recordAdam6360D.PulseOut2LowTime; set => _recordAdam6360D.PulseOut2LowTime = value; }
|
||||
public UInt16 PulseOut3LowTime { get => _recordAdam6360D.PulseOut3LowTime; set => _recordAdam6360D.PulseOut3LowTime = value; }
|
||||
public UInt16 PulseOut4LowTime { get => _recordAdam6360D.PulseOut4LowTime; set => _recordAdam6360D.PulseOut4LowTime = value; }
|
||||
public UInt16 PulseOut5LowTime { get => _recordAdam6360D.PulseOut5LowTime; set => _recordAdam6360D.PulseOut5LowTime = value; }
|
||||
|
||||
public UInt16 PulseOut0HighTime { get => _recordAdam6360D.PulseOut0HighTime; set => _recordAdam6360D.PulseOut0HighTime = value; }
|
||||
public UInt16 PulseOut1HighTime { get => _recordAdam6360D.PulseOut1HighTime; set => _recordAdam6360D.PulseOut1HighTime = value; }
|
||||
public UInt16 PulseOut2HighTime { get => _recordAdam6360D.PulseOut2HighTime; set => _recordAdam6360D.PulseOut2HighTime = value; }
|
||||
public UInt16 PulseOut3HighTime { get => _recordAdam6360D.PulseOut3HighTime; set => _recordAdam6360D.PulseOut3HighTime = value; }
|
||||
public UInt16 PulseOut4HighTime { get => _recordAdam6360D.PulseOut4HighTime; set => _recordAdam6360D.PulseOut4HighTime = value; }
|
||||
public UInt16 PulseOut5HighTime { get => _recordAdam6360D.PulseOut5HighTime; set => _recordAdam6360D.PulseOut5HighTime = value; }
|
||||
|
||||
/**************************** Green LED *********************************/
|
||||
|
||||
public void PerformSolidGreenLed()
|
||||
{
|
||||
DigitalOutput0Mode = 0;
|
||||
DigitalOutput1Mode = 0;
|
||||
LedGreen = true;
|
||||
LedRed = false;
|
||||
}
|
||||
|
||||
public void PerformSlowFlashingGreenLed()
|
||||
{
|
||||
PulseOut0HighTime = SlowFreq;
|
||||
PulseOut0LowTime = SlowFreq;
|
||||
DigitalOutput0Mode = 2;
|
||||
Do0StartPulse = true;
|
||||
Do1StartPulse = false; // make sure the red LED is off
|
||||
|
||||
Console.WriteLine("Green Slow Flashing Starting");
|
||||
}
|
||||
|
||||
public void PerformFastFlashingGreenLed()
|
||||
{
|
||||
PulseOut0HighTime = HighFreq;
|
||||
PulseOut0LowTime = HighFreq;
|
||||
DigitalOutput0Mode = 2;
|
||||
Do0StartPulse = true;
|
||||
Do1StartPulse = false;// make sure the red LED is off
|
||||
|
||||
Console.WriteLine("Green Slow Flashing Starting");
|
||||
}
|
||||
|
||||
/**************************** Orange LED *********************************/
|
||||
|
||||
public void PerformSolidOrangeLed()
|
||||
{
|
||||
DigitalOutput0Mode = 0;
|
||||
DigitalOutput1Mode = 0;
|
||||
LedGreen = true;
|
||||
LedRed = true;
|
||||
}
|
||||
|
||||
public void PerformSlowFlashingOrangeLed()
|
||||
{
|
||||
PerformSlowFlashingGreenLed();
|
||||
PerformSlowFlashingRedLed();
|
||||
Do0StartPulse = true;
|
||||
Do1StartPulse = true;
|
||||
|
||||
Console.WriteLine("Orange Slow Flashing Starting");
|
||||
}
|
||||
|
||||
public void PerformFastFlashingOrangeLed()
|
||||
{
|
||||
PerformFastFlashingGreenLed();
|
||||
PerformFastFlashingRedLed();
|
||||
Do0StartPulse = true;
|
||||
Do1StartPulse = true;
|
||||
Console.WriteLine("Orange Fast Flashing Starting");
|
||||
}
|
||||
|
||||
/**************************** RED LED *********************************/
|
||||
|
||||
public void PerformSolidRedLed()
|
||||
{
|
||||
DigitalOutput0Mode = 0;
|
||||
DigitalOutput1Mode = 0;
|
||||
LedGreen = false;
|
||||
LedRed = true;
|
||||
}
|
||||
|
||||
public void PerformSlowFlashingRedLed()
|
||||
{
|
||||
PulseOut1HighTime = SlowFreq;
|
||||
PulseOut1LowTime = SlowFreq;
|
||||
DigitalOutput1Mode = 2;
|
||||
Do0StartPulse = false; // make sure the green LED is off
|
||||
Do1StartPulse = true;
|
||||
|
||||
Console.WriteLine("Red Slow Flashing Starting");
|
||||
}
|
||||
|
||||
public void PerformFastFlashingRedLed()
|
||||
{
|
||||
PulseOut1HighTime = HighFreq;
|
||||
PulseOut1LowTime = HighFreq;
|
||||
DigitalOutput1Mode = 2;
|
||||
Do0StartPulse = false; // make sure the green LED is off
|
||||
Do1StartPulse = true;
|
||||
|
||||
Console.WriteLine("Red Fast Flashing Starting");
|
||||
}
|
||||
|
||||
public RelaysRecordAdam6360D? GetAdam6360DRecord()
|
||||
{
|
||||
return _recordAdam6360D;
|
||||
}
|
||||
|
||||
public RelaysRecordAdam6060? GetAdam6060Record()
|
||||
{
|
||||
return _recordAdam6060;
|
||||
}
|
||||
|
||||
public IEnumerable<Boolean> K3InverterIsConnectedToIslandBus => _recordAdam6360D.K3InverterIsConnectedToIslandBus;
|
||||
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
namespace InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
|
||||
|
||||
|
||||
public interface IRelaysRecord
|
||||
{
|
||||
Boolean K1GridBusIsConnectedToGrid { get; }
|
||||
Boolean K2IslandBusIsConnectedToGridBus { get; }
|
||||
IEnumerable<Boolean> K3InverterIsConnectedToIslandBus { get; }
|
||||
Boolean FiWarning { get; }
|
||||
Boolean FiError { get; }
|
||||
Boolean K2ConnectIslandBusToGridBus { get; set; }
|
||||
|
||||
// Boolean Inverter1WagoRelay { get; set; } // to add in the future
|
||||
// Boolean Inverter2WagoRelay { get; set; } // to add in the future
|
||||
// Boolean Inverter3WagoRelay { get; set; } // to add in the future
|
||||
// Boolean Inverter4WagoRelay { get; set; } // to add in the future
|
||||
|
||||
Boolean Inverter1WagoStatus { get; }
|
||||
Boolean Inverter2WagoStatus { get; }
|
||||
Boolean Inverter3WagoStatus { get; }
|
||||
Boolean Inverter4WagoStatus { get; }
|
||||
|
||||
Boolean Dc1WagoStatus { get; } // to test
|
||||
Boolean Dc2WagoStatus { get; } // to test
|
||||
Boolean Dc3WagoStatus { get; } // to test
|
||||
Boolean Dc4WagoStatus { get; } // to test
|
||||
|
||||
Boolean DcSystemControlWagoStatus { get; } // to test
|
||||
|
||||
Boolean LedGreen { get; set; }
|
||||
Boolean LedRed { get; }
|
||||
Boolean Harvester1Step { get; }
|
||||
Boolean Harvester2Step { get; }
|
||||
Boolean Harvester3Step { get; }
|
||||
Boolean Harvester4Step { get; }
|
||||
|
||||
Boolean Do0StartPulse { get; set; }
|
||||
Boolean Do1StartPulse { get; set; }
|
||||
Boolean Do2StartPulse { get; set; }
|
||||
Boolean Do3StartPulse { get; set; }
|
||||
Boolean Do4StartPulse { get; set; }
|
||||
Boolean Do5StartPulse { get; set; }
|
||||
|
||||
UInt16 DigitalOutput0Mode { get; set; }
|
||||
UInt16 DigitalOutput1Mode { get; set; }
|
||||
UInt16 DigitalOutput2Mode { get; set; }
|
||||
UInt16 DigitalOutput3Mode { get; set; }
|
||||
UInt16 DigitalOutput4Mode { get; set; }
|
||||
UInt16 DigitalOutput5Mode { get; set; }
|
||||
|
||||
UInt16 PulseOut0LowTime { get; set; }
|
||||
UInt16 PulseOut1LowTime { get; set; }
|
||||
UInt16 PulseOut2LowTime { get; set; }
|
||||
UInt16 PulseOut3LowTime { get; set; }
|
||||
UInt16 PulseOut4LowTime { get; set; }
|
||||
UInt16 PulseOut5LowTime { get; set; }
|
||||
|
||||
UInt16 PulseOut0HighTime { get; set; }
|
||||
UInt16 PulseOut1HighTime { get; set; }
|
||||
UInt16 PulseOut2HighTime { get; set; }
|
||||
UInt16 PulseOut3HighTime { get; set; }
|
||||
UInt16 PulseOut4HighTime { get; set; }
|
||||
UInt16 PulseOut5HighTime { get; set; }
|
||||
|
||||
void PerformSolidGreenLed();
|
||||
void PerformSlowFlashingGreenLed();
|
||||
void PerformFastFlashingGreenLed();
|
||||
|
||||
|
||||
void PerformSolidOrangeLed();
|
||||
void PerformSlowFlashingOrangeLed();
|
||||
void PerformFastFlashingOrangeLed();
|
||||
|
||||
void PerformSolidRedLed();
|
||||
void PerformSlowFlashingRedLed();
|
||||
void PerformFastFlashingRedLed();
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
using InnovEnergy.Lib.Devices.Adam6360D;
|
||||
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
|
||||
|
||||
public class RelaysDeviceAdam6360
|
||||
{
|
||||
private Adam6360DDevice AdamDevice6360D { get; }
|
||||
|
||||
public RelaysDeviceAdam6360(String hostname) => AdamDevice6360D = new Adam6360DDevice(hostname, 2);
|
||||
public RelaysDeviceAdam6360(Channel channel) => AdamDevice6360D = new Adam6360DDevice(channel, 2);
|
||||
|
||||
|
||||
public RelaysRecordAdam6360D? Read()
|
||||
{
|
||||
try
|
||||
{
|
||||
return AdamDevice6360D.Read();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
$"Failed to read from {nameof(RelaysDeviceAdam6360)}\n{e}".LogError();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(RelaysRecordAdam6360D r)
|
||||
{
|
||||
try
|
||||
{
|
||||
AdamDevice6360D.Write(r);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
$"Failed to write to {nameof(RelaysDeviceAdam6360)}\n{e}".LogError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
using InnovEnergy.Lib.Devices.Adam6060;
|
||||
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
|
||||
|
||||
public class RelaysDeviceAdam6060
|
||||
{
|
||||
private Adam6060Device AdamDevice6060 { get; }
|
||||
|
||||
public RelaysDeviceAdam6060(String hostname) => AdamDevice6060 = new Adam6060Device(hostname, 2);
|
||||
public RelaysDeviceAdam6060(Channel channel) => AdamDevice6060 = new Adam6060Device(channel, 2);
|
||||
|
||||
|
||||
public RelaysRecordAdam6060? Read()
|
||||
{
|
||||
try
|
||||
{
|
||||
return AdamDevice6060.Read();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
$"Failed to read from {nameof(RelaysDeviceAdam6060)}\n{e}".LogError();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(RelaysRecordAdam6060 r)
|
||||
{
|
||||
try
|
||||
{
|
||||
AdamDevice6060.Write(r);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
$"Failed to write to {nameof(RelaysDeviceAdam6060)}\n{e}".LogError();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
using InnovEnergy.Lib.Devices.Amax5070;
|
||||
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
|
||||
|
||||
public class RelaysDeviceAmax
|
||||
{
|
||||
private Amax5070Device AmaxDevice { get; }
|
||||
|
||||
public RelaysDeviceAmax(Channel channel) => AmaxDevice = new Amax5070Device(channel);
|
||||
|
||||
public RelaysRecordAmax? Read()
|
||||
{
|
||||
try
|
||||
{
|
||||
return AmaxDevice.Read();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
$"Failed to read from {nameof(RelaysDeviceAmax)}\n{e}".LogError();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(RelaysRecordAmax r)
|
||||
{
|
||||
try
|
||||
{
|
||||
AmaxDevice.Write(r);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
$"Failed to write to {nameof(RelaysDeviceAmax)}\n{e}".LogError();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
using InnovEnergy.Lib.Devices.Adam6060;
|
||||
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
|
||||
|
||||
public class RelaysRecordAdam6060
|
||||
{
|
||||
private readonly Adam6060Registers _Regs;
|
||||
|
||||
private RelaysRecordAdam6060(Adam6060Registers regs) => _Regs = regs;
|
||||
|
||||
|
||||
public Boolean Dc1WagoStatus => _Regs.DigitalInput0; // to test
|
||||
public Boolean Dc2WagoStatus => _Regs.DigitalInput1; // to test
|
||||
public Boolean Dc3WagoStatus => _Regs.DigitalInput4; // to test
|
||||
public Boolean Dc4WagoStatus => _Regs.DigitalInput5; // to test
|
||||
|
||||
public Boolean DcSystemControlWagoStatus => _Regs.DigitalInput3; // to test
|
||||
|
||||
|
||||
public static implicit operator Adam6060Registers(RelaysRecordAdam6060 d) => d._Regs;
|
||||
public static implicit operator RelaysRecordAdam6060(Adam6060Registers d) => new RelaysRecordAdam6060(d);
|
||||
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
using InnovEnergy.Lib.Devices.Adam6360D;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
|
||||
|
||||
public class RelaysRecordAdam6360D
|
||||
{
|
||||
private readonly Adam6360DRegisters _Regs;
|
||||
|
||||
private RelaysRecordAdam6360D(Adam6360DRegisters regs) => _Regs = regs;
|
||||
|
||||
public Boolean K1GridBusIsConnectedToGrid => _Regs.DigitalInput6;
|
||||
public Boolean K2IslandBusIsConnectedToGridBus => !_Regs.DigitalInput4;
|
||||
|
||||
public Boolean Inverter1WagoStatus => _Regs.DigitalInput8;
|
||||
public Boolean Inverter2WagoStatus => _Regs.DigitalInput9;
|
||||
public Boolean Inverter3WagoStatus => _Regs.DigitalInput10;
|
||||
public Boolean Inverter4WagoStatus => _Regs.DigitalInput11;
|
||||
|
||||
public IEnumerable<Boolean> K3InverterIsConnectedToIslandBus
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return K3Inverter1IsConnectedToIslandBus;
|
||||
yield return K3Inverter2IsConnectedToIslandBus;
|
||||
yield return K3Inverter3IsConnectedToIslandBus;
|
||||
yield return K3Inverter4IsConnectedToIslandBus;
|
||||
}
|
||||
}
|
||||
|
||||
private Boolean K3Inverter1IsConnectedToIslandBus => !_Regs.DigitalInput0; // change it to private should be ok
|
||||
private Boolean K3Inverter2IsConnectedToIslandBus => !_Regs.DigitalInput1;
|
||||
private Boolean K3Inverter3IsConnectedToIslandBus => !_Regs.DigitalInput2;
|
||||
private Boolean K3Inverter4IsConnectedToIslandBus => !_Regs.DigitalInput3;
|
||||
|
||||
public Boolean FiWarning => !_Regs.DigitalInput5;
|
||||
public Boolean FiError => !_Regs.DigitalInput7;
|
||||
|
||||
public Boolean Harvester1Step =>_Regs.DigitalOutput2;
|
||||
public Boolean Harvester2Step =>_Regs.DigitalOutput3;
|
||||
public Boolean Harvester3Step =>_Regs.DigitalOutput4;
|
||||
public Boolean Harvester4Step =>_Regs.DigitalOutput5;
|
||||
|
||||
public Boolean LedGreen { get =>_Regs.DigitalOutput0; set => _Regs.DigitalOutput0 = value;}
|
||||
public Boolean LedRed { get =>_Regs.DigitalOutput1; set => _Regs.DigitalOutput1 = value;}
|
||||
|
||||
public Boolean Do0Pulse { get => _Regs.Do0Pulse; set => _Regs.Do0Pulse = value;}
|
||||
public Boolean Do1Pulse { get => _Regs.Do1Pulse; set => _Regs.Do1Pulse = value;}
|
||||
public Boolean Do2Pulse { get => _Regs.Do2Pulse; set => _Regs.Do2Pulse = value;}
|
||||
public Boolean Do3Pulse { get => _Regs.Do3Pulse; set => _Regs.Do3Pulse = value;}
|
||||
public Boolean Do4Pulse { get => _Regs.Do4Pulse; set => _Regs.Do4Pulse = value;}
|
||||
public Boolean Do5Pulse { get => _Regs.Do5Pulse; set => _Regs.Do5Pulse = value;}
|
||||
|
||||
public UInt16 PulseOut0LowTime { get => _Regs.PulseOut0LowTime; set => _Regs.PulseOut0LowTime = value;} //in milleseconds
|
||||
public UInt16 PulseOut1LowTime { get => _Regs.PulseOut1LowTime; set => _Regs.PulseOut1LowTime = value;}
|
||||
public UInt16 PulseOut2LowTime { get => _Regs.PulseOut2LowTime; set => _Regs.PulseOut2LowTime = value;}
|
||||
public UInt16 PulseOut3LowTime { get => _Regs.PulseOut3LowTime; set => _Regs.PulseOut3LowTime = value;}
|
||||
public UInt16 PulseOut4LowTime { get => _Regs.PulseOut4LowTime; set => _Regs.PulseOut4LowTime = value;}
|
||||
public UInt16 PulseOut5LowTime { get => _Regs.PulseOut5LowTime; set => _Regs.PulseOut5LowTime = value;}
|
||||
|
||||
public UInt16 PulseOut0HighTime { get => _Regs.PulseOut0HighTime; set => _Regs.PulseOut0HighTime = value;} // in milleseconds
|
||||
public UInt16 PulseOut1HighTime { get => _Regs.PulseOut1HighTime; set => _Regs.PulseOut1HighTime = value;}
|
||||
public UInt16 PulseOut2HighTime { get => _Regs.PulseOut2HighTime; set => _Regs.PulseOut2HighTime = value;}
|
||||
public UInt16 PulseOut3HighTime { get => _Regs.PulseOut3HighTime; set => _Regs.PulseOut3HighTime = value;}
|
||||
public UInt16 PulseOut4HighTime { get => _Regs.PulseOut4HighTime; set => _Regs.PulseOut4HighTime = value;}
|
||||
public UInt16 PulseOut5HighTime { get => _Regs.PulseOut5HighTime; set => _Regs.PulseOut5HighTime = value;}
|
||||
|
||||
public UInt16 DigitalOutput0Mode { get => _Regs.DigitalOutput0Mode; set => _Regs.DigitalOutput0Mode = value;} // To test: 0, 1 or 2
|
||||
public UInt16 DigitalOutput1Mode { get => _Regs.DigitalOutput1Mode; set => _Regs.DigitalOutput1Mode = value;}
|
||||
public UInt16 DigitalOutput2Mode { get => _Regs.DigitalOutput2Mode; set => _Regs.DigitalOutput2Mode = value;}
|
||||
public UInt16 DigitalOutput3Mode { get => _Regs.DigitalOutput3Mode; set => _Regs.DigitalOutput3Mode = value;}
|
||||
public UInt16 DigitalOutput4Mode { get => _Regs.DigitalOutput4Mode; set => _Regs.DigitalOutput4Mode = value;}
|
||||
public UInt16 DigitalOutput5Mode { get => _Regs.DigitalOutput5Mode; set => _Regs.DigitalOutput5Mode = value;}
|
||||
|
||||
public Boolean K2ConnectIslandBusToGridBus { get => _Regs.Relay0; set => _Regs.Relay0 = value;}
|
||||
|
||||
public static implicit operator Adam6360DRegisters(RelaysRecordAdam6360D d) => d._Regs;
|
||||
public static implicit operator RelaysRecordAdam6360D(Adam6360DRegisters d) => new RelaysRecordAdam6360D(d);
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
using InnovEnergy.Lib.Devices.Amax5070;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
|
||||
|
||||
public class RelaysRecordAmax : IRelaysRecord
|
||||
{
|
||||
private readonly Amax5070Registers _Regs;
|
||||
|
||||
private RelaysRecordAmax(Amax5070Registers regs) => _Regs = regs;
|
||||
|
||||
public Boolean K1GridBusIsConnectedToGrid => _Regs.DigitalInput22;
|
||||
public Boolean K2IslandBusIsConnectedToGridBus => !_Regs.DigitalInput20;
|
||||
|
||||
public Boolean Inverter1WagoStatus => _Regs.DigitalInput0;
|
||||
public Boolean Inverter2WagoStatus => _Regs.DigitalInput1;
|
||||
public Boolean Inverter3WagoStatus => _Regs.DigitalInput2;
|
||||
public Boolean Inverter4WagoStatus => _Regs.DigitalInput3;
|
||||
|
||||
public Boolean Dc1WagoStatus => _Regs.DigitalInput6;
|
||||
public Boolean Dc2WagoStatus => _Regs.DigitalInput7;
|
||||
public Boolean Dc3WagoStatus => _Regs.DigitalInput10;
|
||||
public Boolean Dc4WagoStatus => _Regs.DigitalInput11;
|
||||
public Boolean DcSystemControlWagoStatus => _Regs.DigitalInput9;
|
||||
|
||||
public Boolean LedGreen
|
||||
{
|
||||
get => _Regs.DigitalOutput0;
|
||||
set => _Regs.DigitalOutput0 = value;
|
||||
}
|
||||
|
||||
public Boolean LedRed => _Regs.DigitalOutput1;
|
||||
public Boolean Harvester1Step => _Regs.DigitalOutput2;
|
||||
public Boolean Harvester2Step => _Regs.DigitalOutput3;
|
||||
public Boolean Harvester3Step => _Regs.DigitalOutput4;
|
||||
public Boolean Harvester4Step => _Regs.DigitalOutput5;
|
||||
public Boolean Do0StartPulse { get; set; }
|
||||
public Boolean Do1StartPulse { get; set; }
|
||||
public Boolean Do2StartPulse { get; set; }
|
||||
public Boolean Do3StartPulse { get; set; }
|
||||
public Boolean Do4StartPulse { get; set; }
|
||||
public Boolean Do5StartPulse { get; set; }
|
||||
public UInt16 DigitalOutput0Mode { get; set; }
|
||||
public UInt16 DigitalOutput1Mode { get; set; }
|
||||
public UInt16 DigitalOutput2Mode { get; set; }
|
||||
public UInt16 DigitalOutput3Mode { get; set; }
|
||||
public UInt16 DigitalOutput4Mode { get; set; }
|
||||
public UInt16 DigitalOutput5Mode { get; set; }
|
||||
public UInt16 PulseOut0LowTime { get; set; }
|
||||
public UInt16 PulseOut1LowTime { get; set; }
|
||||
public UInt16 PulseOut2LowTime { get; set; }
|
||||
public UInt16 PulseOut3LowTime { get; set; }
|
||||
public UInt16 PulseOut4LowTime { get; set; }
|
||||
public UInt16 PulseOut5LowTime { get; set; }
|
||||
public UInt16 PulseOut0HighTime { get; set; }
|
||||
public UInt16 PulseOut1HighTime { get; set; }
|
||||
public UInt16 PulseOut2HighTime { get; set; }
|
||||
public UInt16 PulseOut3HighTime { get; set; }
|
||||
public UInt16 PulseOut4HighTime { get; set; }
|
||||
public UInt16 PulseOut5HighTime { get; set; }
|
||||
|
||||
public void PerformSolidGreenLed()
|
||||
{
|
||||
Console.WriteLine("Solid Green: This is not yet implemented ");
|
||||
}
|
||||
|
||||
public void PerformSlowFlashingGreenLed()
|
||||
{
|
||||
Console.WriteLine("Slow Flashing Green: This is not yet implemented ");
|
||||
}
|
||||
|
||||
public void PerformFastFlashingGreenLed()
|
||||
{
|
||||
Console.WriteLine("Fast Flashing Green: This is not yet implemented ");
|
||||
}
|
||||
|
||||
public void PerformSolidOrangeLed()
|
||||
{
|
||||
Console.WriteLine("Solid Orange: This is not yet implemented ");
|
||||
}
|
||||
|
||||
public void PerformSlowFlashingOrangeLed()
|
||||
{
|
||||
Console.WriteLine("Slow Flashing Orange: This is not yet implemented ");
|
||||
}
|
||||
|
||||
public void PerformFastFlashingOrangeLed()
|
||||
{
|
||||
Console.WriteLine("Fast Flashing Orange: This is not yet implemented ");
|
||||
}
|
||||
|
||||
public void PerformSolidRedLed()
|
||||
{
|
||||
Console.WriteLine("Solid Red: This is not yet implemented ");
|
||||
}
|
||||
|
||||
public void PerformSlowFlashingRedLed()
|
||||
{
|
||||
Console.WriteLine("Slow Flashing Red: This is not yet implemented ");
|
||||
}
|
||||
|
||||
public void PerformFastFlashingRedLed()
|
||||
{
|
||||
Console.WriteLine("Fast Flashing Red: This is not yet implemented ");
|
||||
}
|
||||
|
||||
public IEnumerable<Boolean> K3InverterIsConnectedToIslandBus
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return K3Inverter1IsConnectedToIslandBus;
|
||||
yield return K3Inverter2IsConnectedToIslandBus;
|
||||
yield return K3Inverter3IsConnectedToIslandBus;
|
||||
yield return K3Inverter4IsConnectedToIslandBus;
|
||||
}
|
||||
}
|
||||
|
||||
private Boolean K3Inverter1IsConnectedToIslandBus => !_Regs.DigitalInput16;
|
||||
private Boolean K3Inverter2IsConnectedToIslandBus => !_Regs.DigitalInput17;
|
||||
private Boolean K3Inverter3IsConnectedToIslandBus => !_Regs.DigitalInput18;
|
||||
private Boolean K3Inverter4IsConnectedToIslandBus => !_Regs.DigitalInput19;
|
||||
|
||||
public Boolean FiWarning => !_Regs.DigitalInput21;
|
||||
public Boolean FiError => !_Regs.DigitalInput23;
|
||||
|
||||
public Boolean K2ConnectIslandBusToGridBus
|
||||
{
|
||||
get => _Regs.Relay23;
|
||||
set => _Regs.Relay23 = value;
|
||||
}
|
||||
|
||||
public static implicit operator Amax5070Registers(RelaysRecordAmax d) => d._Regs;
|
||||
public static implicit operator RelaysRecordAmax(Amax5070Registers d) => new RelaysRecordAmax(d);
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
using InnovEnergy.Lib.Utils;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax;
|
||||
|
||||
public static class Switch
|
||||
{
|
||||
public static TextBlock Open(String name)
|
||||
{
|
||||
return TextBlock.AlignCenterHorizontal
|
||||
(
|
||||
" __╱ __ ",
|
||||
name
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,726 @@
|
|||
using InnovEnergy.App.SodiStoreMax.Ess;
|
||||
using InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
||||
using static InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes.GridType;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.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.AllGridTied() ? 0
|
||||
: acDcs.AllIsland() ? 1
|
||||
: 4;
|
||||
|
||||
var k5 = acDcs.AllDisabled() ? 0
|
||||
: acDcs.AllEnabled() ? 1
|
||||
: 4;
|
||||
|
||||
if (k4 == 4 || k5 == 4)
|
||||
return 103; //Message = "Panic: ACDCs have unequal grid types or power stage",
|
||||
|
||||
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 * k1
|
||||
+ 2 * k2
|
||||
+ 4 * k3
|
||||
+ 8 * k4
|
||||
+ 16 * k5;
|
||||
}
|
||||
|
||||
public static Boolean ControlSystemState(this StatusRecord s)
|
||||
{
|
||||
s.StateMachine.State = s.GetSystemState();
|
||||
|
||||
return s.StateMachine.State switch
|
||||
{
|
||||
0 => State0(s),
|
||||
1 => State1(s),
|
||||
2 => State2(s),
|
||||
3 => State3(s),
|
||||
4 => State4(s),
|
||||
5 => State5(s),
|
||||
6 => State6(s),
|
||||
7 => State7(s),
|
||||
8 => State8(s),
|
||||
9 => State9(s),
|
||||
10 => State10(s),
|
||||
11 => State11(s),
|
||||
12 => State12(s),
|
||||
13 => State13(s),
|
||||
14 => State14(s),
|
||||
15 => State15(s),
|
||||
16 => State16(s),
|
||||
17 => State17(s),
|
||||
18 => State18(s),
|
||||
19 => State19(s),
|
||||
20 => State20(s),
|
||||
21 => State21(s),
|
||||
22 => State22(s),
|
||||
23 => State23(s),
|
||||
24 => State24(s),
|
||||
25 => State25(s),
|
||||
26 => State26(s),
|
||||
27 => State27(s),
|
||||
28 => State28(s),
|
||||
29 => State29(s),
|
||||
30 => State30(s),
|
||||
31 => State31(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 State0(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "Ac/Dc are off. Switching to Island Mode.";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 8
|
||||
}
|
||||
|
||||
|
||||
private static Boolean State1(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "Grid Tied mode active, closing k2";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.ConnectIslandBusToGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 3
|
||||
}
|
||||
|
||||
private static Boolean State2(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 0
|
||||
}
|
||||
|
||||
private static Boolean State3(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "K2 closed, Turning on Ac/Dc";
|
||||
|
||||
s.DcDc.Enable();
|
||||
s.AcDc.Enable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.ConnectIslandBusToGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 19
|
||||
}
|
||||
|
||||
|
||||
private static Boolean State4(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "K2 is open, waiting K3 to open";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return true;
|
||||
|
||||
// => 0
|
||||
}
|
||||
|
||||
private static Boolean State5(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => ?
|
||||
}
|
||||
|
||||
private static Boolean State6(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "Inverters are off, opening K2";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return true;
|
||||
|
||||
// => 4
|
||||
}
|
||||
|
||||
private static Boolean State7(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.ConnectIslandBusToGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => ?
|
||||
}
|
||||
|
||||
private static Boolean State8(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "Ac/Dc are off and in Island Mode.";
|
||||
|
||||
s.DcDc.Enable();
|
||||
s.AcDc.Enable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return true;
|
||||
// => 24
|
||||
}
|
||||
|
||||
private static Boolean State9(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "Ac/Dc are disconnected from Island Bus. Switching to GridTie Mode.";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 1
|
||||
}
|
||||
|
||||
private static Boolean State10(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 8
|
||||
}
|
||||
|
||||
private static Boolean State11(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 9
|
||||
}
|
||||
|
||||
private static Boolean State12(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 8
|
||||
}
|
||||
|
||||
private static Boolean State13(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "Ac/Dc are off. Waiting for them to disconnect from Island Bus.";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return true;
|
||||
|
||||
// => 9
|
||||
}
|
||||
|
||||
private static Boolean State14(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 12
|
||||
}
|
||||
|
||||
|
||||
private static Boolean State15(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 13
|
||||
}
|
||||
|
||||
|
||||
private static Boolean State16(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 0
|
||||
}
|
||||
|
||||
|
||||
private static Boolean State17(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 1
|
||||
}
|
||||
|
||||
|
||||
private static Boolean State18(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 0
|
||||
}
|
||||
|
||||
private static Boolean State19(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "Waiting for Ac/Dc to connect to Island Bus";
|
||||
|
||||
s.DcDc.Enable();
|
||||
s.AcDc.Enable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.ConnectIslandBusToGrid();
|
||||
|
||||
return true;
|
||||
|
||||
// => 23
|
||||
}
|
||||
|
||||
|
||||
private static Boolean State20(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 4
|
||||
}
|
||||
|
||||
private static Boolean State21(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 5
|
||||
}
|
||||
|
||||
private static Boolean State22(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "K1 opened, switching inverters off";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.ConnectIslandBusToGrid();
|
||||
|
||||
return true;
|
||||
|
||||
// => 6
|
||||
}
|
||||
|
||||
private static Boolean State23(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "ESS";
|
||||
|
||||
s.DcDc.Enable();
|
||||
s.AcDc.Enable();
|
||||
s.AcDc.EnableGridTieMode();
|
||||
s.Relays.ConnectIslandBusToGrid();
|
||||
|
||||
return true;
|
||||
|
||||
// => 22
|
||||
}
|
||||
|
||||
|
||||
private static Boolean State24(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "Inverter are on waiting for k3 to close";
|
||||
|
||||
s.DcDc.Enable();
|
||||
s.AcDc.Enable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 28
|
||||
}
|
||||
|
||||
private static Boolean State25(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 9
|
||||
}
|
||||
|
||||
private static Boolean State26(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 10
|
||||
}
|
||||
|
||||
private static Boolean State27(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "K2 open and enable island mode";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 9
|
||||
}
|
||||
|
||||
private static Boolean State28(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "Island Mode";
|
||||
|
||||
s.DcDc.Enable();
|
||||
s.AcDc.Enable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 29
|
||||
}
|
||||
|
||||
private static Boolean State29(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "K1 closed, Switching off Inverters and moving to grid tie";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 13
|
||||
}
|
||||
|
||||
private static Boolean State30(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 14
|
||||
}
|
||||
|
||||
private static Boolean State31(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "";
|
||||
|
||||
s.DcDc.Disable();
|
||||
s.AcDc.Disable();
|
||||
s.AcDc.EnableIslandMode();
|
||||
s.Relays.DisconnectIslandBusFromGrid();
|
||||
|
||||
return false;
|
||||
|
||||
// => 13
|
||||
}
|
||||
|
||||
private static Boolean State101(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "Relay device is not available";
|
||||
return s.EnableSafeDefaults();
|
||||
}
|
||||
|
||||
private static Boolean State102(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "ACDCs not available";
|
||||
return s.EnableSafeDefaults();
|
||||
}
|
||||
|
||||
private static Boolean State103(StatusRecord s)
|
||||
{
|
||||
s.StateMachine.Message = "Panic: ACDCs have unequal grid types or PowerStage";
|
||||
return s.EnableSafeDefaults();
|
||||
}
|
||||
|
||||
// private static Boolean State104(StatusRecord s)
|
||||
// {
|
||||
// s.StateMachine.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 AllEnabled(this AcDcDevicesRecord acDcs)
|
||||
{
|
||||
return acDcs.Devices.All(d => d.Control.PowerStageEnable);
|
||||
}
|
||||
|
||||
|
||||
private static Boolean AllTheSame(this AcDcDevicesRecord acDcs)
|
||||
{
|
||||
return acDcs.Devices
|
||||
.Select(d => d.Control.PowerStageEnable)
|
||||
.Distinct()
|
||||
.Count() == 1;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
//this is must be deleted
|
||||
private static void Disable(this DcDcDevicesRecord dcDc)
|
||||
{
|
||||
// For Test purpose, The transition from island mode to grid tier and vis versa , may not need to disable Dc/Dc.
|
||||
// This will keep the Dc link powered.
|
||||
|
||||
// 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 IRelaysRecord? relays)
|
||||
{
|
||||
if (relays is not null)
|
||||
relays.K2ConnectIslandBusToGridBus = false;
|
||||
}
|
||||
|
||||
private static void ConnectIslandBusToGrid(this IRelaysRecord? relays)
|
||||
{
|
||||
if (relays is not null)
|
||||
relays.K2ConnectIslandBusToGridBus = true;
|
||||
}
|
||||
|
||||
|
||||
private static Boolean EnableSafeDefaults(this StatusRecord s)
|
||||
{
|
||||
// After some tests, the safe state is switch off inverter and keep the last state of K2 , Dc/Dc and Grid type to avoid conflict.
|
||||
|
||||
// s.DcDc.Disable();
|
||||
s.AcDc.Disable(); // Maybe comment this to avoid opening/closing K3
|
||||
// s.AcDc.EnableGridTieMode();
|
||||
// s.Relays.DisconnectIslandBusFromGrid();
|
||||
return false;
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
public 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,9 @@
|
|||
namespace InnovEnergy.App.SodiStoreMax.System;
|
||||
|
||||
public record StateMachine
|
||||
{
|
||||
public required String Message { get; set; } // TODO: init only
|
||||
public required Int32 State { get; set; } // TODO: init only
|
||||
|
||||
public static StateMachine Default { get; } = new StateMachine { State = 100, Message = "Unknown State" };
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
namespace InnovEnergy.App.SodiStoreMax.SystemConfig;
|
||||
|
||||
public class AcDcConfig
|
||||
{
|
||||
public required Double MaxDcLinkVoltage { get; set; }
|
||||
public required Double MinDcLinkVoltage { get; set; }
|
||||
public required Double ReferenceDcLinkVoltage { get; init; }
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
namespace InnovEnergy.App.SodiStoreMax.SystemConfig;
|
||||
|
||||
public enum CalibrationChargeType
|
||||
{
|
||||
RepetitivelyEvery,
|
||||
AdditionallyOnce,
|
||||
ChargePermanently
|
||||
}
|
|
@ -0,0 +1,272 @@
|
|||
using System.Text.Json;
|
||||
using InnovEnergy.App.SodiStoreMax.Devices;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using static System.Text.Json.JsonSerializer;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.SystemConfig;
|
||||
|
||||
// shut up trim warnings
|
||||
#pragma warning disable IL2026
|
||||
|
||||
public class Config //TODO: let IE choose from config files (Json) and connect to GUI
|
||||
{
|
||||
private static String DefaultConfigFilePath => Path.Combine(Environment.CurrentDirectory, "config.json");
|
||||
private static DateTime DefaultDatetime => new(2024, 03, 11, 09, 00, 00);
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
|
||||
public required Double MinSoc { get; set; }
|
||||
public required UInt16 CurtailP { get; set; }// in Kw
|
||||
public required UInt16 PvInstalledPower { get; set; }// in Kw
|
||||
public required CalibrationChargeType ForceCalibrationChargeState { get; set; }
|
||||
public required DateTime DayAndTimeForRepetitiveCalibration { get; set; }
|
||||
public required DateTime DayAndTimeForAdditionalCalibration { get; set; }
|
||||
public required Boolean DisplayIndividualBatteries { get; set; }
|
||||
public required Double PConstant { get; set; }
|
||||
public required Double GridSetPoint { get; set; }
|
||||
public required Double BatterySelfDischargePower { get; set; }
|
||||
public required Double HoldSocZone { get; set; }
|
||||
public required DevicesConfig IslandMode { get; set; }
|
||||
public required DevicesConfig GridTie { get; set; }
|
||||
|
||||
public required DeviceConfig Devices { get; set; }
|
||||
public required S3Config? S3 { get; set; }
|
||||
|
||||
private static String? LastSavedData { get; set; }
|
||||
|
||||
#if DEBUG
|
||||
public static Config Default => new()
|
||||
{
|
||||
MinSoc = 20,
|
||||
CurtailP = 0,
|
||||
PvInstalledPower = 20,
|
||||
ForceCalibrationChargeState = CalibrationChargeType.RepetitivelyEvery,
|
||||
DayAndTimeForRepetitiveCalibration = DefaultDatetime,
|
||||
DayAndTimeForAdditionalCalibration = DefaultDatetime,
|
||||
DisplayIndividualBatteries = false,
|
||||
PConstant = .5,
|
||||
GridSetPoint = 0,
|
||||
BatterySelfDischargePower = 200,
|
||||
HoldSocZone = 1, // TODO: find better name,
|
||||
IslandMode = new()
|
||||
{
|
||||
AcDc = new ()
|
||||
{
|
||||
MinDcLinkVoltage = 690,
|
||||
ReferenceDcLinkVoltage = 750,
|
||||
MaxDcLinkVoltage = 810,
|
||||
},
|
||||
|
||||
DcDc = new ()
|
||||
{
|
||||
UpperDcLinkVoltage = 50,
|
||||
LowerDcLinkVoltage = 50,
|
||||
ReferenceDcLinkVoltage = 750,
|
||||
|
||||
MaxBatteryChargingCurrent = 210,
|
||||
MaxBatteryDischargingCurrent = 210,
|
||||
MaxDcPower = 10000,
|
||||
|
||||
MaxChargeBatteryVoltage = 57,
|
||||
MinDischargeBatteryVoltage = 0,
|
||||
},
|
||||
},
|
||||
|
||||
GridTie = new()
|
||||
{
|
||||
AcDc = new ()
|
||||
{
|
||||
MinDcLinkVoltage = 720,
|
||||
ReferenceDcLinkVoltage = 750,
|
||||
MaxDcLinkVoltage = 810,
|
||||
},
|
||||
|
||||
DcDc = new ()
|
||||
{
|
||||
UpperDcLinkVoltage = 50,
|
||||
LowerDcLinkVoltage = 50,
|
||||
ReferenceDcLinkVoltage = 750,
|
||||
|
||||
MaxBatteryChargingCurrent = 210,
|
||||
MaxBatteryDischargingCurrent = 210,
|
||||
MaxDcPower = 10000,
|
||||
|
||||
MaxChargeBatteryVoltage = 57,
|
||||
MinDischargeBatteryVoltage = 0,
|
||||
},
|
||||
},
|
||||
|
||||
Devices = new ()
|
||||
{
|
||||
RelaysIp = new() { Host = "localhost", Port = 5006, DeviceState = DeviceState.Measured},
|
||||
TsRelaysIp = new() { Host = "localhost", Port = 5006, DeviceState = DeviceState.Measured},
|
||||
GridMeterIp = new() { Host = "localhost", Port = 5003, DeviceState = DeviceState.Measured},
|
||||
PvOnAcGrid = new() { Host = "false" , Port = 0 , DeviceState = DeviceState.Measured},
|
||||
LoadOnAcGrid = new() { Host = "false" , Port = 0 , DeviceState = DeviceState.Measured},
|
||||
PvOnAcIsland = new() { Host = "true" , Port = 0 , DeviceState = DeviceState.Measured},
|
||||
IslandBusLoadMeterIp = new() { Host = "localhost", Port = 5004, DeviceState = DeviceState.Measured},
|
||||
TruConvertAcIp = new() { Host = "localhost", Port = 5001, DeviceState = DeviceState.Measured},
|
||||
PvOnDc = new() { Host = "localhost", Port = 5005, DeviceState = DeviceState.Measured},
|
||||
LoadOnDc = new() { Host = "false" , Port = 0 , DeviceState = DeviceState.Measured},
|
||||
TruConvertDcIp = new() { Host = "localhost", Port = 5002, DeviceState = DeviceState.Measured},
|
||||
BatteryIp = new() { Host = "localhost", Port = 5007, DeviceState = DeviceState.Measured},
|
||||
BatteryNodes = new []{ 2, 3, 4, 5, 6 }
|
||||
},
|
||||
|
||||
S3 = new()
|
||||
{
|
||||
Bucket = "1-3e5b3069-214a-43ee-8d85-57d72000c19d",
|
||||
Region = "sos-ch-dk-2",
|
||||
Provider = "exo.io",
|
||||
ContentType = "text/plain; charset=utf-8",
|
||||
Key = "EXO4ec5faf1a7650b79b5722fb5",
|
||||
Secret = "LUxu1PGEA-POEIckoEyq6bYyz0RnenW6tmqccMKgkHQ"
|
||||
},
|
||||
};
|
||||
#else
|
||||
public static Config Default => new()
|
||||
{
|
||||
MinSoc = 20,
|
||||
CurtailP = 0,
|
||||
PvInstalledPower = 20,
|
||||
ForceCalibrationChargeState = CalibrationChargeType.RepetitivelyEvery,
|
||||
DayAndTimeForRepetitiveCalibration = DefaultDatetime,
|
||||
DayAndTimeForAdditionalCalibration = DefaultDatetime,
|
||||
DisplayIndividualBatteries = false,
|
||||
PConstant = .5,
|
||||
GridSetPoint = 0,
|
||||
BatterySelfDischargePower = 200,
|
||||
HoldSocZone = 1, // TODO: find better name,
|
||||
IslandMode = new()
|
||||
{
|
||||
AcDc = new ()
|
||||
{
|
||||
MinDcLinkVoltage = 690,
|
||||
ReferenceDcLinkVoltage = 750,
|
||||
MaxDcLinkVoltage = 810,
|
||||
},
|
||||
|
||||
DcDc = new ()
|
||||
{
|
||||
UpperDcLinkVoltage = 50,
|
||||
LowerDcLinkVoltage = 50,
|
||||
ReferenceDcLinkVoltage = 750,
|
||||
|
||||
MaxBatteryChargingCurrent = 210,
|
||||
MaxBatteryDischargingCurrent = 210,
|
||||
MaxDcPower = 10000,
|
||||
|
||||
MaxChargeBatteryVoltage = 57,
|
||||
MinDischargeBatteryVoltage = 0,
|
||||
},
|
||||
},
|
||||
|
||||
GridTie = new()
|
||||
{
|
||||
AcDc = new ()
|
||||
{
|
||||
MinDcLinkVoltage = 720,
|
||||
ReferenceDcLinkVoltage = 750,
|
||||
MaxDcLinkVoltage = 780,
|
||||
},
|
||||
|
||||
DcDc = new ()
|
||||
{
|
||||
UpperDcLinkVoltage = 20,
|
||||
LowerDcLinkVoltage = 20,
|
||||
ReferenceDcLinkVoltage = 750,
|
||||
|
||||
MaxBatteryChargingCurrent = 210,
|
||||
MaxBatteryDischargingCurrent = 210,
|
||||
MaxDcPower = 10000,
|
||||
|
||||
MaxChargeBatteryVoltage = 57,
|
||||
MinDischargeBatteryVoltage = 0,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
S3 = new()
|
||||
{
|
||||
Bucket = "1-3e5b3069-214a-43ee-8d85-57d72000c19d",
|
||||
Region = "sos-ch-dk-2",
|
||||
Provider = "exo.io",
|
||||
Key = "EXObb5a49acb1061781761895e7",
|
||||
Secret = "sKhln0w8ii3ezZ1SJFF33yeDo8NWR1V4w2H0D4-350I",
|
||||
ContentType = "text/plain; charset=utf-8"
|
||||
},
|
||||
|
||||
Devices = new ()
|
||||
{
|
||||
RelaysIp = new() { Host = "10.0.1.1", Port = 502, DeviceState = DeviceState.Measured},
|
||||
TsRelaysIp = new() { Host = "10.0.1.2", Port = 502, DeviceState = DeviceState.Measured},
|
||||
GridMeterIp = new() { Host = "10.0.4.1", Port = 502, DeviceState = DeviceState.Measured},
|
||||
PvOnAcGrid = new() { Host = "false" , Port = 0 , DeviceState = DeviceState.Measured},
|
||||
LoadOnAcGrid = new() { Host = "true" , Port = 0 , DeviceState = DeviceState.Measured},
|
||||
PvOnAcIsland = new() { Host = "false" , Port = 0 , DeviceState = DeviceState.Measured},
|
||||
IslandBusLoadMeterIp = new() { Host = "10.0.4.2", Port = 502, DeviceState = DeviceState.Measured},
|
||||
TruConvertAcIp = new() { Host = "10.0.2.1", Port = 502, DeviceState = DeviceState.Measured},
|
||||
PvOnDc = new() { Host = "10.0.5.1", Port = 502, DeviceState = DeviceState.Measured},
|
||||
LoadOnDc = new() { Host = "false" , Port = 0 , DeviceState = DeviceState.Measured},
|
||||
TruConvertDcIp = new() { Host = "10.0.3.1", Port = 502, DeviceState = DeviceState.Measured},
|
||||
BatteryIp = new() { Host = "localhost", Port = 6855, DeviceState = DeviceState.Measured },
|
||||
BatteryNodes = new []{ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
|
||||
},
|
||||
};
|
||||
#endif
|
||||
|
||||
public void Save(String? path = null)
|
||||
{
|
||||
var configFilePath = path ?? DefaultConfigFilePath;
|
||||
|
||||
try
|
||||
{
|
||||
var jsonString = Serialize(this, JsonOptions);
|
||||
|
||||
if (LastSavedData == jsonString)
|
||||
return;
|
||||
|
||||
LastSavedData = jsonString;
|
||||
|
||||
File.WriteAllText(configFilePath, jsonString);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
$"Failed to write config file {configFilePath}\n{e}".WriteLine();
|
||||
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}".WriteLine();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
namespace InnovEnergy.App.SodiStoreMax.SystemConfig;
|
||||
|
||||
public class DcDcConfig
|
||||
{
|
||||
public required Double LowerDcLinkVoltage { get; set; }
|
||||
public required Double ReferenceDcLinkVoltage { get; init; }
|
||||
public required Double UpperDcLinkVoltage { get; set; }
|
||||
|
||||
public required Double MaxBatteryChargingCurrent { get; set; }
|
||||
public required Double MaxBatteryDischargingCurrent { get; set; }
|
||||
public required Double MaxDcPower { get; set; }
|
||||
|
||||
public required Double MaxChargeBatteryVoltage { get; set; }
|
||||
public required Double MinDischargeBatteryVoltage { get; set; }
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
using InnovEnergy.App.SodiStoreMax.Devices;
|
||||
using InnovEnergy.Lib.Utils.Net;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax.SystemConfig;
|
||||
|
||||
public class DeviceConfig
|
||||
{
|
||||
public required SalimaxDevice RelaysIp { get; init; }
|
||||
public required SalimaxDevice TsRelaysIp { get; init; }
|
||||
public required SalimaxDevice GridMeterIp { get; init; }
|
||||
public required SalimaxDevice PvOnAcGrid { get; init; }
|
||||
public required SalimaxDevice LoadOnAcGrid { get; init; }
|
||||
public required SalimaxDevice PvOnAcIsland { get; init; }
|
||||
public required SalimaxDevice IslandBusLoadMeterIp { get; init; }
|
||||
public required SalimaxDevice TruConvertAcIp { get; init; }
|
||||
public required SalimaxDevice PvOnDc { get; init; }
|
||||
public required SalimaxDevice LoadOnDc { get; init; }
|
||||
public required SalimaxDevice TruConvertDcIp { get; init; }
|
||||
public required SalimaxDevice BatteryIp { get; init; }
|
||||
public required Int32[] BatteryNodes { get; init; }
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace InnovEnergy.App.SodiStoreMax.SystemConfig;
|
||||
|
||||
public class DevicesConfig
|
||||
{
|
||||
public required AcDcConfig AcDc { get; init; }
|
||||
public required DcDcConfig DcDc { get; init; }
|
||||
}
|
|
@ -0,0 +1,530 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using InnovEnergy.App.SodiStoreMax.Devices;
|
||||
using InnovEnergy.App.SodiStoreMax.Ess;
|
||||
using InnovEnergy.Lib.Devices.AMPT;
|
||||
using InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
using InnovEnergy.Lib.Devices.EmuMeter;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
||||
using InnovEnergy.Lib.Units;
|
||||
using InnovEnergy.Lib.Units.Power;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using Ac3Bus = InnovEnergy.Lib.Units.Composite.Ac3Bus;
|
||||
|
||||
namespace InnovEnergy.App.SodiStoreMax;
|
||||
|
||||
// ┌────┐ ┌────┐
|
||||
// │ Pv │ │ Pv │ ┌────┐
|
||||
// └────┘ └────┘ │ Pv │
|
||||
// V V └────┘
|
||||
// V V V
|
||||
// (b) 0 W (e) 0 W V
|
||||
// V V (i) 13.2 kW ┌────────────┐
|
||||
// V V V │ Battery │
|
||||
// ┌─────────┐ ┌──────────┐ ┌────────────┐ ┌─────────┐ V ├────────────┤
|
||||
// │ Grid │ │ Grid Bus │ │ Island Bus │ │ AC/DC │ ┌────────┐ ┌───────┐ │ 52.3 V │
|
||||
// ├─────────┤ -10.3 kW ├──────────┤ -11.7 kW ├────────────┤ -11.7 kW ├─────────┤ -11.7 kW │ Dc Bus │ 1008 W │ DC/DC │ 1008 W │ 99.1 % │
|
||||
// │ -3205 W │<<<<<<<<<<│ 244 V │<<<<<<<<<<│ 244 V │<<<<<<<<<<│ -6646 W │<<<<<<<<<<├────────┤>>>>>>>>>>├───────┤>>>>>>>>>>│ 490 mA │
|
||||
// │ -3507 W │ (a) │ 244 V │ (d) │ 244 V │ (g) │ -5071 W │ (h) │ 776 V │ (k) │ 56 V │ (l) │ 250 °C │
|
||||
// │ -3605 W │ K1 │ 246 V │ K2 │ 246 V │ K3 └─────────┘ └────────┘ └───────┘ │ 445 A │
|
||||
// └─────────┘ └──────────┘ └────────────┘ V │ 0 Warnings │
|
||||
// V V V │ 0 Alarms │
|
||||
// V V (j) 0 W └────────────┘
|
||||
// (c) 1400 W (f) 0 W V
|
||||
// V V V
|
||||
// V V ┌──────┐
|
||||
// ┌──────┐ ┌──────┐ │ Load │
|
||||
// │ Load │ │ Load │ └──────┘
|
||||
// └──────┘ └──────┘
|
||||
|
||||
|
||||
// Calculated values: c,d & h
|
||||
// ==========================
|
||||
//
|
||||
//
|
||||
// AC side
|
||||
// a + b - c - d = 0 [eq1]
|
||||
// d + e - f - g = 0 [eq2]
|
||||
//
|
||||
// c & d are not measured!
|
||||
//
|
||||
// d = f + g - e [eq2]
|
||||
// c = a + b - d [eq1]
|
||||
//
|
||||
// DC side
|
||||
// h + i - j - k = 0 [eq3]
|
||||
//
|
||||
// if Dc load not existing, h = i - k [eq4]
|
||||
|
||||
// k = l assuming no losses in DCDC // this is changed now l is equal total battery power
|
||||
// j = h + i - k [eq3]
|
||||
|
||||
|
||||
public static class Topology
|
||||
{
|
||||
|
||||
public static TextBlock CreateTopologyTextBlock(this StatusRecord status)
|
||||
{
|
||||
var a = status.GridMeter?.Ac.Power.Active;
|
||||
var b = status.PvOnAcGrid?.Dc.Power.Value;
|
||||
var e = status.PvOnAcIsland?.Dc.Power.Value;
|
||||
var f = status.LoadOnAcIsland?.Ac.Power.Active;
|
||||
var g = status.AcDc.Dc.Power.Value;
|
||||
var h = status.AcDcToDcLink?.Power.Value;
|
||||
var i = status.PvOnDc?.Dc.Power.Value;
|
||||
var k = status.DcDc.Dc.Link.Power.Value;
|
||||
var l = status.Battery is not null ? status.Battery.Power : 0;
|
||||
var j = status.LoadOnDc?.Power.Value;
|
||||
var d = status.AcGridToAcIsland?.Power.Active;
|
||||
var c = status.LoadOnAcGrid?.Power.Active;
|
||||
|
||||
/////////////////////////////
|
||||
|
||||
var grid = status.CreateGridColumn(a);
|
||||
var gridBus = status.CreateGridBusColumn(b, c, d);
|
||||
var islandBus = status.CreateIslandBusColumn(e, f, g);
|
||||
var inverter = status.CreateInverterColumn(h);
|
||||
var dcBus = status.CreateDcBusColumn(i, j, k);
|
||||
var dcDc = status.CreateDcDcColumn(l);
|
||||
var batteries = status.CreateBatteryColumn();
|
||||
|
||||
return TextBlock.AlignCenterVertical
|
||||
(
|
||||
grid,
|
||||
gridBus,
|
||||
islandBus,
|
||||
inverter,
|
||||
dcBus,
|
||||
dcDc,
|
||||
batteries
|
||||
);
|
||||
}
|
||||
|
||||
private static TextBlock CreateGridColumn(this StatusRecord status, ActivePower? a)
|
||||
{
|
||||
// ┌─────────┐
|
||||
// │ Grid │
|
||||
// ├─────────┤ -10.3 kW
|
||||
// │ -3205 W │<<<<<<<<<<
|
||||
// │ -3507 W │ (a)
|
||||
// │ -3605 W │ K1
|
||||
// └─────────┘
|
||||
|
||||
var gridMeterAc = status.GridMeter?.Ac;
|
||||
var k1 = status.Relays?.K1GridBusIsConnectedToGrid;
|
||||
|
||||
var gridBox = PhasePowersActive(gridMeterAc).TitleBox("Grid");
|
||||
var gridFlow = SwitchedFlow(k1, a, "K1");
|
||||
|
||||
return TextBlock.AlignCenterVertical(gridBox, gridFlow);
|
||||
}
|
||||
|
||||
|
||||
private static TextBlock CreateGridBusColumn(this StatusRecord status,
|
||||
ActivePower? b,
|
||||
ActivePower? c,
|
||||
ActivePower? d)
|
||||
{
|
||||
|
||||
// ┌────┐
|
||||
// │ Pv │
|
||||
// └────┘
|
||||
// V
|
||||
// V
|
||||
// (b) 0 W
|
||||
// V
|
||||
// V
|
||||
// ┌──────────┐
|
||||
// │ Grid Bus │
|
||||
// ├──────────┤ -11.7 kW
|
||||
// │ 244 V │<<<<<<<<<<
|
||||
// │ 244 V │ (d)
|
||||
// │ 246 V │ K2
|
||||
// └──────────┘
|
||||
// V
|
||||
// V
|
||||
// (c) 1400 W
|
||||
// V
|
||||
// V
|
||||
// ┌──────┐
|
||||
// │ Load │
|
||||
// └──────┘
|
||||
|
||||
|
||||
////////////// top //////////////
|
||||
|
||||
var pvBox = TextBlock.FromString("PV").Box();
|
||||
var pvFlow = Flow.Vertical(b);
|
||||
|
||||
////////////// center //////////////
|
||||
|
||||
// on IslandBus show voltages measured by inverter
|
||||
// on GridBus show voltages measured by grid meter
|
||||
// ought to be approx the same
|
||||
|
||||
var gridMeterAc = status.GridMeter?.Ac;
|
||||
var k2 = status.Relays?.K2IslandBusIsConnectedToGridBus;
|
||||
|
||||
var busBox = PhaseVoltages(gridMeterAc).TitleBox("Grid Bus");
|
||||
var busFlow = SwitchedFlow(k2, d, "K2");
|
||||
|
||||
////////////// bottom //////////////
|
||||
|
||||
var loadFlow = Flow.Vertical(c);
|
||||
var loadBox = TextBlock.FromString("Load").Box();
|
||||
|
||||
////////////// assemble //////////////
|
||||
|
||||
return TextBlock.AlignCenterVertical
|
||||
(
|
||||
TextBlock.AlignCenterHorizontal(pvBox, pvFlow, busBox, loadFlow, loadBox),
|
||||
busFlow
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static TextBlock CreateIslandBusColumn(this StatusRecord status,
|
||||
ActivePower? e,
|
||||
ActivePower? f,
|
||||
ActivePower? g)
|
||||
{
|
||||
|
||||
// ┌────┐
|
||||
// │ Pv │
|
||||
// └────┘
|
||||
// V
|
||||
// V
|
||||
// (e) 0 W
|
||||
// V
|
||||
// V
|
||||
// ┌────────────┐
|
||||
// │ Island Bus │
|
||||
// ├────────────┤ -11.7 kW
|
||||
// │ 244 V │<<<<<<<<<<
|
||||
// │ 244 V │ (g)
|
||||
// │ 246 V │ K3
|
||||
// └────────────┘
|
||||
// V
|
||||
// V
|
||||
// (f) 0 W
|
||||
// V
|
||||
// V
|
||||
// ┌──────┐
|
||||
// │ Load │
|
||||
// └──────┘
|
||||
|
||||
|
||||
////////////// top //////////////
|
||||
|
||||
var pvBox = TextBlock.FromString("PV").Box();
|
||||
var pvFlow = Flow.Vertical(e);
|
||||
|
||||
////////////// center //////////////
|
||||
|
||||
// on IslandBus show voltages measured by inverter
|
||||
// on GridBus show voltages measured by grid meter
|
||||
// ought to be approx the same
|
||||
|
||||
var inverterAc = status.AcDc.Ac;
|
||||
var busBox = PhaseVoltages(inverterAc).TitleBox("Island Bus");
|
||||
var busFlow = status.IslandBusToInverterConnection(g);
|
||||
|
||||
////////////// bottom //////////////
|
||||
|
||||
var loadFlow = Flow.Vertical(f);
|
||||
var loadBox = TextBlock.FromString("Load").Box();
|
||||
|
||||
////////////// assemble //////////////
|
||||
|
||||
return TextBlock.AlignCenterVertical
|
||||
(
|
||||
TextBlock.AlignCenterHorizontal(pvBox, pvFlow, busBox, loadFlow, loadBox),
|
||||
busFlow
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static TextBlock CreateInverterColumn(this StatusRecord status, ActivePower? h)
|
||||
{
|
||||
// ┌─────────┐
|
||||
// │ AC/DC │
|
||||
// ├─────────┤ -11.7 kW
|
||||
// │ -6646 W │<<<<<<<<<<
|
||||
// │ -5071 W │ (h)
|
||||
// └─────────┘
|
||||
|
||||
var inverterBox = status
|
||||
.AcDc
|
||||
.Devices
|
||||
.Select(d => d.Status.Ac.Power.Active)
|
||||
.Apply(TextBlock.AlignLeft)
|
||||
.TitleBox("AC/DC");
|
||||
|
||||
var dcFlow = Flow.Horizontal(h);
|
||||
|
||||
return TextBlock.AlignCenterVertical(inverterBox, dcFlow);
|
||||
}
|
||||
|
||||
|
||||
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
|
||||
private static TextBlock IslandBusToInverterConnection(this StatusRecord status, ActivePower? g)
|
||||
{
|
||||
if (status.Relays is null)
|
||||
return TextBlock.FromString("????????");
|
||||
|
||||
var nInverters = status.AcDc.Devices.Count;
|
||||
|
||||
var k3S = status
|
||||
.Relays
|
||||
.K3InverterIsConnectedToIslandBus
|
||||
.Take(nInverters);
|
||||
|
||||
if (k3S.Prepend(true).All(s => s)) // TODO: display when no ACDC present
|
||||
return Flow.Horizontal(g);
|
||||
|
||||
return Switch.Open("K3");
|
||||
}
|
||||
|
||||
private static TextBlock CreateDcDcColumn(this StatusRecord status, ActivePower? p)
|
||||
{
|
||||
var dc48Voltage = status.DcDc.Dc.Battery.Voltage.ToDisplayString();
|
||||
|
||||
var busBox = TextBlock
|
||||
.AlignLeft(dc48Voltage)
|
||||
.TitleBox("DC/DC");
|
||||
|
||||
var busFlow = Flow.Horizontal(p);
|
||||
|
||||
return TextBlock.AlignCenterVertical(busBox, busFlow);
|
||||
}
|
||||
|
||||
private static TextBlock CreateDcBusColumn(this StatusRecord status,
|
||||
ActivePower? i,
|
||||
ActivePower? j,
|
||||
ActivePower? k)
|
||||
{
|
||||
// ┌────┐
|
||||
// │ Pv │
|
||||
// └────┘
|
||||
// V
|
||||
// V
|
||||
// (i) 13.2 kW
|
||||
// V
|
||||
// V
|
||||
// ┌────────┐
|
||||
// │ Dc Bus │ 1008 W
|
||||
// ├────────┤>>>>>>>>>>
|
||||
// │ 776 V │ (k)
|
||||
// └────────┘
|
||||
// V
|
||||
// V
|
||||
// (j) 0 W
|
||||
// V
|
||||
// V
|
||||
// ┌──────┐
|
||||
// │ Load │
|
||||
// └──────┘
|
||||
//
|
||||
|
||||
|
||||
/////////////////// top ///////////////////
|
||||
|
||||
var mppt = status.PvOnDc;
|
||||
|
||||
var nStrings = mppt is not null
|
||||
? "x" + mppt.Strings.Count
|
||||
: "?";
|
||||
|
||||
var pvBox = TextBlock.FromString($"PV {nStrings}").Box();
|
||||
var pvToBus = Flow.Vertical(i);
|
||||
|
||||
/////////////////// center ///////////////////
|
||||
|
||||
var dcBusVoltage = status.DcDc.Dc.Link.Voltage;
|
||||
|
||||
var dcBusBox = dcBusVoltage
|
||||
.ToDisplayString()
|
||||
.Apply(TextBlock.FromString)
|
||||
.TitleBox("DC Bus ");
|
||||
|
||||
var busFlow = Flow.Horizontal(k);
|
||||
|
||||
/////////////////// bottom ///////////////////
|
||||
|
||||
var busToLoad = Flow.Vertical(j);
|
||||
var loadBox = TextBlock.FromString("DC Load").Box();
|
||||
|
||||
////////////// assemble //////////////
|
||||
|
||||
return TextBlock.AlignCenterVertical
|
||||
(
|
||||
TextBlock.AlignCenterHorizontal(pvBox, pvToBus, dcBusBox, busToLoad, loadBox),
|
||||
busFlow
|
||||
);
|
||||
}
|
||||
|
||||
private static TextBlock CreateBatteryColumn(this StatusRecord status)
|
||||
{
|
||||
var bat = status.Battery;
|
||||
if (bat is null)
|
||||
return TextBlock.AlignLeft("no battery").Box();
|
||||
|
||||
|
||||
var batteryAvgBox = CreateAveragedBatteryBox(bat);
|
||||
|
||||
var batteryBoxes = bat
|
||||
.Devices
|
||||
.Select(CreateBatteryBox)
|
||||
.ToReadOnlyList();
|
||||
|
||||
|
||||
var individualWithAvgBox = TextBlock
|
||||
.AlignCenterVertical
|
||||
(
|
||||
batteryAvgBox ,
|
||||
batteryBoxes.Any()
|
||||
? TextBlock.AlignLeft(batteryBoxes)
|
||||
: TextBlock.Empty
|
||||
);
|
||||
|
||||
return status.Config.DisplayIndividualBatteries ? individualWithAvgBox : batteryAvgBox;
|
||||
}
|
||||
|
||||
private static TextBlock CreateAveragedBatteryBox(BatteryDeligreenRecords bat)
|
||||
{
|
||||
var voltage = bat.Voltage.ToDisplayString();
|
||||
var soc = bat.Devices.Any() ? bat.Devices.Average(b => b.BatteryDeligreenDataRecord.Soc).Percent().ToDisplayString() : "0"; // TODO
|
||||
var current = bat.Current.ToDisplayString();
|
||||
var busCurrent = bat.Devices.Any() ? bat.Devices.Sum(b => b.BatteryDeligreenDataRecord.BusCurrent).A().ToDisplayString() : "0";
|
||||
var temp = bat.TemperatureCell1.ToDisplayString();
|
||||
//var alarms = bat.Alarms.Count + " Alarms";
|
||||
//var warnings = bat.Warnings.Count + " Warnings";
|
||||
var nBatteries = bat.Devices.Count;
|
||||
|
||||
return TextBlock
|
||||
.AlignLeft
|
||||
(
|
||||
voltage,
|
||||
soc,
|
||||
current,
|
||||
busCurrent,
|
||||
temp
|
||||
)
|
||||
.TitleBox($"Battery x{nBatteries}");
|
||||
}
|
||||
|
||||
private static TextBlock PhaseVoltages(Ac3Bus? ac)
|
||||
{
|
||||
return TextBlock.AlignLeft
|
||||
(
|
||||
ac?.L1.Voltage.ToDisplayString() ?? "???",
|
||||
ac?.L2.Voltage.ToDisplayString() ?? "???",
|
||||
ac?.L3.Voltage.ToDisplayString() ?? "???"
|
||||
);
|
||||
}
|
||||
|
||||
private static TextBlock PhasePowersActive(Ac3Bus? ac)
|
||||
{
|
||||
return TextBlock.AlignLeft
|
||||
(
|
||||
ac?.L1.Power.Active.ToDisplayString() ?? "???",
|
||||
ac?.L2.Power.Active.ToDisplayString() ?? "???",
|
||||
ac?.L3.Power.Active.ToDisplayString() ?? "???"
|
||||
);
|
||||
}
|
||||
|
||||
private static TextBlock CreateBatteryBox(BatteryDeligreenRecord battery, Int32 i)
|
||||
{
|
||||
var batteryWarnings = "";// battery.Warnings.Any();
|
||||
var batteryAlarms = "";// battery.Alarms.Any();
|
||||
|
||||
var content = TextBlock.AlignLeft
|
||||
(
|
||||
battery.BatteryDeligreenDataRecord.BusVoltage.ToDisplayString(),
|
||||
battery.BatteryDeligreenDataRecord.Soc.ToDisplayString(),
|
||||
battery.BatteryDeligreenDataRecord.BusCurrent.ToDisplayString() + " C/D",
|
||||
battery.BatteryDeligreenDataRecord.TemperaturesList.PowerTemperature.ToDisplayString(),
|
||||
battery.BatteryDeligreenDataRecord.BatteryCapacity.ToString(CultureInfo.CurrentCulture) ,
|
||||
batteryWarnings,
|
||||
batteryAlarms
|
||||
);
|
||||
|
||||
var box = content.TitleBox($"Battery {i + 1}");
|
||||
var flow = Flow.Horizontal(battery.BatteryDeligreenDataRecord.Power);
|
||||
|
||||
return TextBlock.AlignCenterVertical(flow, box);
|
||||
}
|
||||
|
||||
|
||||
private static TextBlock SwitchedFlow(Boolean? switchClosed, ActivePower? power, String kx)
|
||||
{
|
||||
return switchClosed is null ? TextBlock.FromString("??????????")
|
||||
: !switchClosed.Value ? Switch.Open(kx)
|
||||
: power is null ? TextBlock.FromString("??????????")
|
||||
: Flow.Horizontal(power);
|
||||
}
|
||||
|
||||
//We are fake using the ampt instead of PvOnAc, We dont have a Pv on Ac at the moment and we don't have it classes :TODO
|
||||
public static AcPowerDevice? CalculateGridBusLoad(EmuMeterRegisters? gridMeter, AmptStatus? pvOnAcGrid, AcPowerDevice? gridBusToIslandBusPower)
|
||||
{
|
||||
var a = gridMeter ?.Ac.Power;
|
||||
var b = pvOnAcGrid is not null? pvOnAcGrid?.Dc.Power.Value: 0;
|
||||
var d = gridBusToIslandBusPower?.Power;
|
||||
|
||||
if (a is null || b is null || d is null)
|
||||
return null;
|
||||
|
||||
var c = a + b - d; // [eq1]
|
||||
|
||||
return new AcPowerDevice { Power = c };
|
||||
}
|
||||
|
||||
//We are fake using the ampt instead of PvOnAc, We dont have a Pv on Ac at the moment and we don't have it classes :TODO
|
||||
public static DcPowerDevice? CalculateAcDcToDcLink(AmptStatus? pvOnDc, DcDcDevicesRecord? dcDc, AcDcDevicesRecord acDc)
|
||||
{
|
||||
var i = pvOnDc?.Dc.Power;
|
||||
var k = dcDc?.Dc.Link.Power; // We don't check on the DcDc because this device is mandatory
|
||||
var g = acDc?.Dc.Power;
|
||||
|
||||
if (i is null || k is null )
|
||||
{
|
||||
return new DcPowerDevice { Power = g };
|
||||
}
|
||||
|
||||
var h = -(i - k); // [eq4]
|
||||
|
||||
return new DcPowerDevice { Power = h };
|
||||
}
|
||||
|
||||
//We are fake using the ampt instead of PvOnAc, We dont have a Pv on Ac at the moment and we don't have it classes :TODO
|
||||
public static AcPowerDevice? CalculateGridBusToIslandBusPower(AmptStatus? pvOnAcIsland, EmuMeterRegisters? loadOnAcIsland, AcDcDevicesRecord? acDc)
|
||||
{
|
||||
var e = pvOnAcIsland is not null? pvOnAcIsland?.Dc.Power.Value: 0;
|
||||
var f = loadOnAcIsland is not null? loadOnAcIsland?.Ac.Power : 0;
|
||||
var g = acDc ?.Ac.Power; // We don't check on the AcDc because this device is mandatory, if this does not exist the system will not start
|
||||
|
||||
if (e is null || f is null || g is null)
|
||||
return null;
|
||||
|
||||
var d = f + g - e; // [eq2]
|
||||
|
||||
return new AcPowerDevice { Power = d };
|
||||
}
|
||||
|
||||
public static DcPowerDevice? CalculateDcLoad(AcDcDevicesRecord? acDc, AmptStatus? pvOnDc, DcDcDevicesRecord? dcDc)
|
||||
{
|
||||
var h = acDc?.Dc.Power; // We don't check on the AcDc because this device is mandatory
|
||||
var i = pvOnDc is not null? pvOnDc?.Dc.Power: 0;
|
||||
var k = dcDc?.Dc.Link.Power; // We don't check on the DcDc because this device is mandatory
|
||||
|
||||
if (h is null || i is null || k is null)
|
||||
return null;
|
||||
|
||||
var j = h + i - k; // [eq3]
|
||||
|
||||
return new DcPowerDevice { Power = j};
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
#!/bin/bash
|
||||
|
||||
host="ie-entwicklung@$1"
|
||||
|
||||
tunnel() {
|
||||
name=$1
|
||||
ip=$2
|
||||
rPort=$3
|
||||
lPort=$4
|
||||
|
||||
echo -n "$name @ $ip mapped to localhost:$lPort "
|
||||
ssh -nNTL "$lPort:$ip:$rPort" "$host" 2> /dev/null &
|
||||
|
||||
until nc -vz 127.0.0.1 $lPort 2> /dev/null
|
||||
do
|
||||
echo -n .
|
||||
sleep 0.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
|
||||
tunnel "Adam " 10.0.1.1 502 5006 #for AMAX is 10.0.1.3
|
||||
tunnel "Batteries " 127.0.0.1 6855 5007
|
||||
|
||||
echo
|
||||
echo "press any key to close the tunnels ..."
|
||||
read -r -n 1 -s
|
||||
kill $(jobs -p)
|
||||
echo "done"
|
||||
|
Binary file not shown.
|
@ -0,0 +1,29 @@
|
|||
#!/bin/bash
|
||||
|
||||
dotnet_version='net6.0'
|
||||
salimax_ip="$1"
|
||||
username='ie-entwicklung'
|
||||
root_password='Salimax4x25'
|
||||
|
||||
set -e
|
||||
|
||||
ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.33" "10.2.4.32" "10.2.4.36" "10.2.4.35" "10.2.4.154" "10.2.4.113" "10.2.4.29")
|
||||
battery_ids=("2" "3" "4" "5" "6" "7" "8" "9" "10" "11")
|
||||
|
||||
|
||||
for ip_address in "${ip_addresses[@]}"; do
|
||||
scp upload-bms-firmware AF0A.bin "$username"@"$ip_address":/home/"$username"
|
||||
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S systemctl stop battery.service"
|
||||
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S apt install python3-pip -y"
|
||||
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S pip3 install pymodbus"
|
||||
|
||||
for battery in "${battery_ids[@]}"; do
|
||||
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S python3 upload-bms-firmware ttyUSB0 " "$battery" " AF0A.bin"
|
||||
done
|
||||
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S systemctl start battery.service"
|
||||
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S systemctl rm upload-bms-firmware AF0A.bin"
|
||||
|
||||
echo "Deployed and ran commands on $ip_address"
|
||||
done
|
||||
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
#!/usr/bin/python2 -u
|
||||
# coding=utf-8
|
||||
|
||||
import os
|
||||
import struct
|
||||
from time import sleep
|
||||
|
||||
import serial
|
||||
from os import system
|
||||
import logging
|
||||
|
||||
from pymodbus.client import ModbusSerialClient as Modbus
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from os.path import dirname, abspath
|
||||
from sys import path, argv, exit
|
||||
|
||||
path.append(dirname(dirname(abspath(__file__))))
|
||||
|
||||
PAGE_SIZE = 0x100
|
||||
HALF_PAGE =int( PAGE_SIZE / 2)
|
||||
WRITE_ENABLE = [1]
|
||||
FIRMWARE_VERSION_REGISTER = 1054
|
||||
|
||||
ERASE_FLASH_REGISTER = 0x2084
|
||||
RESET_REGISTER = 0x2087
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import List, NoReturn, Iterable, Optional
|
||||
|
||||
def calc_stm32_crc_round(crc, data):
|
||||
# type: (int, int) -> int
|
||||
crc = crc ^ data
|
||||
for _ in range(32):
|
||||
xor = (crc & 0x80000000) != 0
|
||||
crc = (crc & 0x7FFFFFFF) << 1 # clear bit 31 because python ints have "infinite" bits
|
||||
if xor:
|
||||
crc = crc ^ 0x04C11DB7
|
||||
|
||||
return crc
|
||||
|
||||
|
||||
def calc_stm32_crc(data):
|
||||
# type: (Iterable[int]) -> int
|
||||
crc = 0xFFFFFFFF
|
||||
|
||||
for dw in data:
|
||||
crc = calc_stm32_crc_round(crc, dw)
|
||||
|
||||
return crc
|
||||
|
||||
|
||||
def init_modbus(tty):
|
||||
# type: (str) -> Modbus
|
||||
|
||||
return Modbus(
|
||||
port='/dev/' + tty,
|
||||
method='rtu',
|
||||
baudrate=115200,
|
||||
stopbits=1,
|
||||
bytesize=8,
|
||||
timeout=0.5, # seconds
|
||||
parity=serial.PARITY_ODD)
|
||||
|
||||
|
||||
def failed(response):
|
||||
# type: (ModbusResponse) -> bool
|
||||
|
||||
# Todo 'ModbusIOException' object has no attribute 'function_code'
|
||||
return response.function_code > 0x80
|
||||
|
||||
|
||||
def clear_flash(modbus, slave_address):
|
||||
# type: (Modbus, int) -> bool
|
||||
|
||||
print ('erasing flash...')
|
||||
|
||||
write_response = modbus.write_registers(address=0x2084, values=[1], slave=slave_address)
|
||||
|
||||
if failed(write_response):
|
||||
print('erasing flash FAILED')
|
||||
return False
|
||||
|
||||
flash_countdown = 17
|
||||
while flash_countdown > 0:
|
||||
read_response = modbus.read_holding_registers(address=0x2085, count=1, slave=slave_address)
|
||||
|
||||
if failed(read_response):
|
||||
print('erasing flash FAILED')
|
||||
return False
|
||||
|
||||
if read_response.registers[0] != flash_countdown:
|
||||
flash_countdown = read_response.registers[0]
|
||||
|
||||
msg = str(100 * (16 - flash_countdown) / 16) + '%'
|
||||
print('\r{0} '.format(msg), end=' ')
|
||||
|
||||
print('done!')
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def bytes_to_words(bytes):
|
||||
# type: (str) -> List[int]
|
||||
return list(struct.unpack('>' + int(len(bytes)/2) * 'H', bytes))
|
||||
|
||||
|
||||
def send_half_page_1(modbus, slave_address, data, page):
|
||||
# type: (Modbus, int, str, int) -> NoReturn
|
||||
|
||||
first_half = [page] + bytes_to_words(data[:HALF_PAGE])
|
||||
write_first_half = modbus.write_registers(0x2000, first_half, slave=slave_address)
|
||||
|
||||
if failed(write_first_half):
|
||||
raise Exception("Failed to write page " + str(page))
|
||||
|
||||
|
||||
def send_half_page_2(modbus, slave_address, data, page):
|
||||
# type: (Modbus, int, str, int) -> NoReturn
|
||||
|
||||
registers = bytes_to_words(data[HALF_PAGE:]) + calc_crc(page, data) + WRITE_ENABLE
|
||||
result = modbus.write_registers(0x2041, registers, slave=slave_address)
|
||||
|
||||
if failed(result):
|
||||
raise Exception("Failed to write page " + str(page))
|
||||
|
||||
|
||||
def get_fw_name(fw_path):
|
||||
# type: (str) -> str
|
||||
return fw_path.split('/')[-1].split('.')[0]
|
||||
|
||||
|
||||
def upload_fw(modbus, slave_id, fw_path, fw_name):
|
||||
# type: (Modbus, int, str, str) -> NoReturn
|
||||
|
||||
with open(fw_path, "rb") as f:
|
||||
|
||||
size = os.fstat(f.fileno()).st_size
|
||||
n_pages = int(size / PAGE_SIZE)
|
||||
|
||||
print('uploading firmware ' + fw_name + ' to BMS ...')
|
||||
|
||||
for page in range(0, n_pages):
|
||||
page_data = f.read(PAGE_SIZE)
|
||||
|
||||
msg = "page " + str(page + 1) + '/' + str(n_pages) + ' ' + str(100 * page / n_pages + 1) + '%'
|
||||
print('\r{0} '.format(msg), end=' ')
|
||||
|
||||
if is_page_empty(page_data):
|
||||
continue
|
||||
sleep(0.01)
|
||||
send_half_page_1(modbus, slave_id, page_data, page)
|
||||
sleep(0.01)
|
||||
send_half_page_2(modbus, slave_id, page_data, page)
|
||||
|
||||
|
||||
def is_page_empty(page):
|
||||
# type: (str) -> bool
|
||||
return page.count(b'\xff') == len(page)
|
||||
|
||||
|
||||
def reset_bms(modbus, slave_id):
|
||||
# type: (Modbus, int) -> bool
|
||||
|
||||
print ('resetting BMS...')
|
||||
|
||||
result = modbus.write_registers(RESET_REGISTER, [1], slave=slave_id)
|
||||
|
||||
# expecting a ModbusIOException (timeout)
|
||||
# BMS can no longer reply because it is already reset
|
||||
success = isinstance(result, ModbusIOException)
|
||||
|
||||
if success:
|
||||
print('done')
|
||||
else:
|
||||
print('FAILED to reset battery!')
|
||||
|
||||
return success
|
||||
|
||||
|
||||
def calc_crc(page, data):
|
||||
# type: (int, str) -> List[int]
|
||||
|
||||
crc = calc_stm32_crc([page] + bytes_to_words(data))
|
||||
crc_bytes = struct.pack('>L', crc)
|
||||
|
||||
return bytes_to_words(crc_bytes)
|
||||
|
||||
|
||||
def identify_battery(modbus, slave_id):
|
||||
# type: (Modbus, int) -> Optional[str]
|
||||
print("slave id=",slave_id)
|
||||
target = 'battery ' + str(slave_id) + ' at ' + '502'
|
||||
|
||||
try:
|
||||
|
||||
print(('contacting ...'))
|
||||
|
||||
response = modbus.read_input_registers(address=FIRMWARE_VERSION_REGISTER, count=1, slave=slave_id)
|
||||
fw = '{0:0>4X}'.format(response.registers[0])
|
||||
|
||||
print(('found battery with firmware ' + fw))
|
||||
|
||||
return fw
|
||||
|
||||
except:
|
||||
print(('failed to communicate with '))
|
||||
return None
|
||||
|
||||
|
||||
def print_usage():
|
||||
print(('Usage: ' + __file__ + ' <serial device> <battery id> <firmware>'))
|
||||
print(('Example: ' + __file__ + ' ttyUSB0 2 A08C.bin'))
|
||||
|
||||
|
||||
def parse_cmdline_args(argv):
|
||||
# type: (List[str]) -> (str, str, str, str)
|
||||
|
||||
def fail_with(msg):
|
||||
print(msg)
|
||||
print_usage()
|
||||
exit(1)
|
||||
|
||||
if len(argv) < 1:
|
||||
fail_with('missing argument for tty device')
|
||||
|
||||
if len(argv) < 2:
|
||||
fail_with('missing argument for battery ID')
|
||||
|
||||
if len(argv) < 3:
|
||||
fail_with('missing argument for firmware')
|
||||
|
||||
return argv[0], int(argv[1]), argv[2], get_fw_name(argv[2])
|
||||
|
||||
|
||||
def verify_firmware(modbus, battery_id, fw_name):
|
||||
# type: (Modbus, int, str) -> NoReturn
|
||||
|
||||
fw_verify = identify_battery(modbus, battery_id)
|
||||
|
||||
if fw_verify == fw_name:
|
||||
print('SUCCESS')
|
||||
else:
|
||||
print('FAILED to verify uploaded firmware!')
|
||||
if fw_verify is not None:
|
||||
print('expected firmware version ' + fw_name + ' but got ' + fw_verify)
|
||||
|
||||
|
||||
def wait_for_bms_reboot():
|
||||
# type: () -> NoReturn
|
||||
|
||||
# wait 20s for the battery to reboot
|
||||
|
||||
print('waiting for BMS to reboot...')
|
||||
|
||||
for t in range(20, 0, -1):
|
||||
print('\r{0} '.format(t), end=' ')
|
||||
sleep(1)
|
||||
|
||||
print('0')
|
||||
|
||||
|
||||
def main(argv):
|
||||
# type: (List[str]) -> NoReturn
|
||||
|
||||
tty, battery_id, fw_path, fw_name = parse_cmdline_args(argv)
|
||||
with init_modbus(tty) as modbus:
|
||||
|
||||
if identify_battery(modbus, battery_id) is None:
|
||||
return
|
||||
|
||||
clear_flash(modbus, battery_id)
|
||||
upload_fw(modbus, battery_id, fw_path, fw_name)
|
||||
|
||||
if not reset_bms(modbus, battery_id):
|
||||
return
|
||||
|
||||
wait_for_bms_reboot()
|
||||
|
||||
verify_firmware(modbus, battery_id, fw_name)
|
||||
|
||||
|
||||
main(argv[1:])
|
|
@ -97,6 +97,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BatteryDeligreen", "Lib\Dev
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeligreenBatteryCommunication", "App\DeligreenBatteryCommunication\DeligreenBatteryCommunication.csproj", "{11ED6871-5B7D-462F-8710-B5D85DEC464A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SodiStoreMax", "App\SodiStoreMax\SodiStoreMax.csproj", "{39B83793-49DB-4940-9C25-A7F944607407}"
|
||||
EndProject
|
||||
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
@ -256,6 +258,10 @@ Global
|
|||
{11ED6871-5B7D-462F-8710-B5D85DEC464A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{11ED6871-5B7D-462F-8710-B5D85DEC464A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{11ED6871-5B7D-462F-8710-B5D85DEC464A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{39B83793-49DB-4940-9C25-A7F944607407}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{39B83793-49DB-4940-9C25-A7F944607407}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{39B83793-49DB-4940-9C25-A7F944607407}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{39B83793-49DB-4940-9C25-A7F944607407}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{CF4834CB-91B7-4172-AC13-ECDA8613CD17} = {145597B4-3E30-45E6-9F72-4DD43194539A}
|
||||
|
@ -300,5 +306,6 @@ Global
|
|||
{F2967439-A590-4D5E-9208-1B973C83AA1C} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
|
||||
{1045AC74-D4D8-4581-AAE3-575DF26060E6} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
|
||||
{11ED6871-5B7D-462F-8710-B5D85DEC464A} = {145597B4-3E30-45E6-9F72-4DD43194539A}
|
||||
{39B83793-49DB-4940-9C25-A7F944607407} = {145597B4-3E30-45E6-9F72-4DD43194539A}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,21 @@
|
|||
using InnovEnergy.Lib.Units;
|
||||
|
||||
namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
|
||||
|
||||
public class Alarms
|
||||
{
|
||||
public struct CellAlarm
|
||||
{
|
||||
public Int32 CellNumber { get; set; }
|
||||
public String AlarmDescription { get; set; }
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, string> ByteAlarmCodes = new Dictionary<string, string>
|
||||
{
|
||||
{ "00", "Normal, no alarm" },
|
||||
{ "01", "Alarm that analog quantity reaches the lower limit" },
|
||||
{ "02", "Alarm that analog quantity reaches the upper limit" },
|
||||
{ "F0", "Other alarms" }
|
||||
};
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
|
||||
public class BatteryDeligreenAlarmRecord
|
||||
{/*
|
||||
public String FwVersion { get; set; }
|
||||
|
||||
public TemperaturesList TemperaturesList { get; set; }
|
||||
// public Dc_ Dc { get; set; }
|
||||
|
||||
public BatteryDeligreenAlarmRecord(Voltage busVoltage, Current busCurrent ,String fwVersion, Percent soc, UInt16 numberOfCycles, Double batteryCapacity, Double ratedCapacity, Voltage totalBatteryVoltage, Percent soh, Double residualCapacity, List<Double> cellVoltage, TemperaturesList temperaturesList)
|
||||
{
|
||||
BusVoltage = busVoltage;
|
||||
BusCurrent = busCurrent;
|
||||
FwVersion = fwVersion;
|
||||
TotalBatteryVoltage = totalBatteryVoltage;
|
||||
ResidualCapacity = residualCapacity;
|
||||
BatteryCapacity = batteryCapacity;
|
||||
Soc = soc;
|
||||
RatedCapacity = ratedCapacity;
|
||||
NumberOfCycles = numberOfCycles;
|
||||
Soh = soh;
|
||||
CellVoltage = cellVoltage;
|
||||
TemperaturesList = temperaturesList;
|
||||
Power = busVoltage * busCurrent;
|
||||
}*/
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
using InnovEnergy.Lib.Units.Power;
|
||||
using static InnovEnergy.Lib.Devices.BatteryDeligreen.Temperatures;
|
||||
|
||||
namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
using InnovEnergy.Lib.Units;
|
||||
|
||||
using Strings = IReadOnlyList<String>;
|
||||
|
||||
public class BatteryDeligreenDataRecord
|
||||
{
|
||||
|
||||
// public Strings Warnings => ParseWarnings().OrderBy(w => w).ToList();
|
||||
// public Strings Alarms => ParseAlarms() .OrderBy(w => w).ToList();
|
||||
|
||||
public String FwVersion { get; set; }
|
||||
public Voltage BusVoltage { get; set; }
|
||||
public Current BusCurrent { get; set; }
|
||||
public ActivePower Power { get; set; }
|
||||
public Voltage TotalBatteryVoltage { get; set; }
|
||||
public Double ResidualCapacity { get; set; }
|
||||
public Double BatteryCapacity { get; set; }
|
||||
public Percent Soc { get; set; }
|
||||
public Double RatedCapacity { get; set; }
|
||||
public UInt16 NumberOfCycles { get; set; }
|
||||
public Percent Soh { get; set; }
|
||||
public List<Double> CellVoltage { get; set; }
|
||||
public TemperaturesList TemperaturesList { get; set; }
|
||||
// public Dc_ Dc { get; set; }
|
||||
|
||||
public BatteryDeligreenDataRecord(Voltage busVoltage, Current busCurrent ,String fwVersion, Percent soc, UInt16 numberOfCycles, Double batteryCapacity, Double ratedCapacity, Voltage totalBatteryVoltage, Percent soh, Double residualCapacity, List<Double> cellVoltage, TemperaturesList temperaturesList)
|
||||
{
|
||||
BusVoltage = busVoltage;
|
||||
BusCurrent = busCurrent;
|
||||
FwVersion = fwVersion;
|
||||
TotalBatteryVoltage = totalBatteryVoltage;
|
||||
ResidualCapacity = residualCapacity;
|
||||
BatteryCapacity = batteryCapacity;
|
||||
Soc = soc;
|
||||
RatedCapacity = ratedCapacity;
|
||||
NumberOfCycles = numberOfCycles;
|
||||
Soh = soh;
|
||||
CellVoltage = cellVoltage;
|
||||
TemperaturesList = temperaturesList;
|
||||
Power = busVoltage * busCurrent;
|
||||
}
|
||||
|
||||
// public struct Dc_
|
||||
// {
|
||||
// public Voltage Voltage => BusVoltage;
|
||||
// public Current Current => BusCurrent;
|
||||
// public ActivePower Power => BusVoltage * BusCurrent;
|
||||
|
||||
// }
|
||||
}
|
|
@ -1,40 +1,65 @@
|
|||
namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
|
||||
using System;
|
||||
using System.IO.Ports;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
|
||||
namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
|
||||
public class BatteryDeligreenDevice
|
||||
{
|
||||
private const Parity Parity = System.IO.Ports.Parity.None;
|
||||
private const Parity Parity = System.IO.Ports.Parity.None;
|
||||
private const StopBits StopBits = System.IO.Ports.StopBits.One;
|
||||
private const Int32 BaudRate = 19200;
|
||||
private const Int32 DataBits = 8;
|
||||
private const Int32 BaudRate = 19200;
|
||||
private const Int32 DataBits = 8;
|
||||
|
||||
private readonly SerialPort _serialPort;
|
||||
|
||||
// Constructor for local serial port connection
|
||||
public BatteryDeligreenDevice(String tty)
|
||||
{
|
||||
_serialPort = new SerialPort(tty, BaudRate, Parity, DataBits, StopBits)
|
||||
{
|
||||
ReadTimeout = 1000, // 1 second timeout for reads
|
||||
WriteTimeout = 1000 // 1 second timeout for writes
|
||||
};
|
||||
public UInt16 SlaveId { get; }
|
||||
|
||||
try
|
||||
// Dynamically construct the frame to send
|
||||
private const String FrameStart = "7E"; // Starting of the frame
|
||||
private const String Version = "3230"; // Protocol version
|
||||
private const String DeviceCode = "3436"; // Device Code
|
||||
private const String TelemetryFunctionCode = "3432";
|
||||
private const String TelcommandFunctionCode = "3434";
|
||||
|
||||
private static SerialPort _sharedSerialPort;
|
||||
|
||||
private static SerialPort GetSharedPort(string tty)
|
||||
{
|
||||
if (_sharedSerialPort == null)
|
||||
{
|
||||
// Open the serial port
|
||||
_serialPort.Open();
|
||||
Console.WriteLine("Serial port opened successfully.");
|
||||
_sharedSerialPort = new SerialPort(tty, BaudRate, Parity, DataBits, StopBits)
|
||||
{
|
||||
ReadTimeout = 1000, // 1 second timeout for reads
|
||||
WriteTimeout = 1000 // 1 second timeout for writes
|
||||
};
|
||||
try
|
||||
{
|
||||
// Open the shared serial port
|
||||
_sharedSerialPort.Open();
|
||||
Console.WriteLine("Shared Serial Port opened successfully.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
|
||||
|
||||
return _sharedSerialPort;
|
||||
}
|
||||
|
||||
|
||||
public static void CloseSharedPort()
|
||||
{
|
||||
_sharedSerialPort?.Close();
|
||||
Console.WriteLine("Shared Serial Port closed.");
|
||||
}
|
||||
|
||||
// Constructor for local serial port connection
|
||||
public BatteryDeligreenDevice(String tty, UInt16 slaveId)
|
||||
{
|
||||
SlaveId = slaveId;
|
||||
_serialPort = GetSharedPort(tty);
|
||||
}
|
||||
|
||||
// Method to send data to the device
|
||||
private void Write(String hexCommand)
|
||||
{
|
||||
|
@ -45,7 +70,7 @@ public class BatteryDeligreenDevice
|
|||
|
||||
// Send the command
|
||||
_serialPort.Write(commandBytes, 0, commandBytes.Length);
|
||||
Console.WriteLine("Command sent successfully.");
|
||||
//Console.WriteLine("Write Command sent successfully.");
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
|
@ -67,7 +92,7 @@ public class BatteryDeligreenDevice
|
|||
var buffer = new Byte[bufferSize];
|
||||
var bytesRead = _serialPort.Read(buffer, 0, bufferSize);
|
||||
|
||||
Console.WriteLine($"Read {bytesRead} bytes from the device.");
|
||||
//Console.WriteLine($"Read {bytesRead} bytes from the device.");
|
||||
|
||||
// Return only the received bytes
|
||||
var responseData = new Byte[bytesRead];
|
||||
|
@ -90,7 +115,7 @@ public class BatteryDeligreenDevice
|
|||
{
|
||||
return BitConverter.ToString(byteArray).Replace("-", "").ToUpper();
|
||||
}
|
||||
|
||||
|
||||
// Helper method to convert a hex string to a byte array
|
||||
private static Byte[] HexStringToByteArray(string hex)
|
||||
{
|
||||
|
@ -102,6 +127,7 @@ public class BatteryDeligreenDevice
|
|||
{
|
||||
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
|
@ -114,29 +140,37 @@ public class BatteryDeligreenDevice
|
|||
Console.WriteLine("Serial port closed.");
|
||||
}
|
||||
}
|
||||
|
||||
// Read telemetry data from the connected device
|
||||
public async Task<Byte[]> ReadTelemetryData()
|
||||
{
|
||||
const String frameToSend = "7E3230303034363432453030323030464433370D"; // Example custom frame
|
||||
|
||||
// Read telemetry data from the connected device
|
||||
private async Task<BatteryDeligreenDataRecord?> ReadTelemetryData(UInt16 batteryId)
|
||||
{
|
||||
String frameToSend = batteryId switch
|
||||
{
|
||||
0 => "7E3230303034363432453030323030464433370D",
|
||||
1 => "7E3230303134363432453030323031464433350D",
|
||||
2 => "7E3230303234363432453030323032464433330D",
|
||||
3 => "7E3230303334363432453030323033464433310D",
|
||||
4 => "7E3230303434363432453030323034464432460D",
|
||||
5 => "7E3230303534363432453030323035464432440D",
|
||||
6 => "7E3230303634363432453030323036464432420D",
|
||||
7 => "7E3230303734363432453030323037464432390D",
|
||||
8 => "7E3230303834363432453030323038464432370D",
|
||||
9 => "7E3230303934363432453030323039464432350D",
|
||||
_ => "0"
|
||||
};
|
||||
// var frameToSend = ConstructFrameToSend(batteryId,TelemetryFunctionCode);
|
||||
|
||||
try
|
||||
{
|
||||
// Write the frame to the channel (send it to the device)
|
||||
await Task.Run(() => Write(frameToSend));
|
||||
|
||||
Write(frameToSend);
|
||||
// Read the response from the channel (assuming max response size)
|
||||
var responseBytes = await Task.Run(() => Read(1024)); // Assuming Read can be executed asynchronously
|
||||
var responseBytes = await ReadFullResponse(168, 64);
|
||||
|
||||
// Convert the byte array to a hexadecimal string
|
||||
var responseHex = BytesToHexString(responseBytes);
|
||||
|
||||
new TelemetryFrameParser().ParsingTelemetryFrame(responseHex);
|
||||
|
||||
// Parse the ASCII response (you can implement any custom parsing logic)
|
||||
var responseData = ParseAsciiResponse(responseBytes.ToArray());
|
||||
|
||||
return responseData;
|
||||
return new TelemetryFrameParser().ParsingTelemetryFrame(responseHex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -145,34 +179,83 @@ public class BatteryDeligreenDevice
|
|||
}
|
||||
}
|
||||
|
||||
public Byte[] ReadTelecomandData()
|
||||
private Task<Byte[]> ReadFullResponse(Int32 totalBytes, Int32 chunkSize)
|
||||
{
|
||||
const String frameToSend = "7E3230303034363434453030323030464433350D"; // Example custom frame
|
||||
|
||||
// Write the frame to the channel (send it to the device)
|
||||
Write(frameToSend);
|
||||
var responseBuffer = new List<Byte>();
|
||||
while (responseBuffer.Count < totalBytes)
|
||||
{
|
||||
// Calculate how many more bytes need to be read
|
||||
var bytesToRead = Math.Min(chunkSize, totalBytes - responseBuffer.Count);
|
||||
var chunk = Read(bytesToRead);
|
||||
|
||||
// Read the response from the channel (assuming max response size)
|
||||
var responseBytes = Read(1024); // Adjust this size if needed
|
||||
if (chunk.Length == 0)
|
||||
{
|
||||
throw new TimeoutException("Failed to read the expected number of bytes from the device.");
|
||||
}
|
||||
|
||||
// Parse the ASCII response (you can implement any custom parsing logic)
|
||||
var responseData = ParseAsciiResponse(responseBytes.ToArray());
|
||||
responseBuffer.AddRange(chunk);
|
||||
}
|
||||
|
||||
return responseData;
|
||||
return Task.FromResult(responseBuffer.ToArray());
|
||||
}
|
||||
|
||||
// Helper method to parse the ASCII response (you can add any parsing logic here)
|
||||
private static byte[] ParseAsciiResponse(byte[] responseBytes)
|
||||
private async Task<BatteryDeligreenAlarmRecord> ReadTelecomandData(UInt16 batteryId)
|
||||
{
|
||||
Console.WriteLine($"Last Timestamp: {DateTime.Now:HH:mm:ss.fff}");
|
||||
// Convert the byte array to a hex string for display
|
||||
var hexResponse = BitConverter.ToString(responseBytes).Replace("-", " ");
|
||||
//Console.WriteLine($"Response (Hex): {hexResponse}");
|
||||
// Implement custom parsing logic if necessary based on the protocol's frame
|
||||
// For now, we return the raw response bytes
|
||||
return responseBytes;
|
||||
var frameToSend = batteryId switch
|
||||
{
|
||||
0 => "7E3230303034363434453030323030464433350D",
|
||||
1 => "7E3230303134363434453030323031464433330D",
|
||||
2 => "7E3230303234363434453030323032464433310D",
|
||||
3 => "7E3230303334363434453030323033464432460D",
|
||||
4 => "7E3230303434363434453030323034464432440D",
|
||||
5 => "7E3230303534363434453030323035464432420D",
|
||||
6 => "7E3230303634363434453030323036464432390D",
|
||||
7 => "7E3230303734363434453030323037464432370D",
|
||||
8 => "7E3230303834363434453030323038464432350D",
|
||||
9 => "7E3230303934363434453030323039464432330D",
|
||||
_ => "0"
|
||||
};
|
||||
try
|
||||
{
|
||||
// Write the frame to the channel (send it to the device)
|
||||
Write(frameToSend);
|
||||
// await Task.Delay(delayFrame2);
|
||||
// Read the response from the channel (assuming max response size)
|
||||
var responseBytes = await ReadFullResponse(116, 64); // Assuming Read can be executed asynchronously
|
||||
// Convert the byte array to a hexadecimal string
|
||||
var responseHex = BytesToHexString(responseBytes);
|
||||
|
||||
var response = new TelecommandFrameParser().ParsingTelecommandFrame(responseHex);
|
||||
|
||||
return new BatteryDeligreenAlarmRecord();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error during Telecomnd data retrieval: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BatteryDeligreenRecord?> Reads()
|
||||
{
|
||||
var dataRecord = ReadTelemetryData(SlaveId).Result;
|
||||
var alarmRecord = ReadTelecomandData(SlaveId).Result;
|
||||
await Task.Delay(5); // looks like this is need. A time delay needed between each frame to send to each battery
|
||||
|
||||
return dataRecord != null ? new BatteryDeligreenRecord(dataRecord, alarmRecord) : null;
|
||||
}
|
||||
|
||||
private static String ConstructFrameToSend(UInt16 batteryId, String functionCode)
|
||||
{
|
||||
// Convert batteryId to a 2-character ASCII string
|
||||
var batteryIdAscii = $"{(Char)(batteryId / 10 + '0')}{(char)(batteryId % 10 + '0')}";
|
||||
var batteryIdHex = string.Concat(batteryIdAscii.Select(c => ((Int32)c).ToString("X2")));
|
||||
Console.WriteLine("Battery ID " + batteryIdHex);
|
||||
|
||||
var frameToSend =
|
||||
FrameStart + Version + batteryIdHex + DeviceCode + functionCode +
|
||||
"453030323030464433370D"; // Example custom frame with dynamic batteryId
|
||||
Console.WriteLine(frameToSend);
|
||||
return frameToSend;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
using InnovEnergy.Lib.Utils;
|
||||
|
||||
namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
|
||||
public class BatteryDeligreenDevices
|
||||
{
|
||||
private readonly IReadOnlyList<BatteryDeligreenDevice> _devices;
|
||||
|
||||
public BatteryDeligreenDevices(IReadOnlyList<BatteryDeligreenDevice> devices) => _devices = devices;
|
||||
|
||||
public BatteryDeligreenRecords? Read()
|
||||
{
|
||||
var records = _devices
|
||||
.Select(TryRead)
|
||||
.NotNull()
|
||||
.ToList();
|
||||
|
||||
return BatteryDeligreenRecords.FromBatteries(records);
|
||||
}
|
||||
|
||||
private static BatteryDeligreenRecord? TryRead(BatteryDeligreenDevice d)
|
||||
{
|
||||
try
|
||||
{
|
||||
return d.Reads().Result;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Failed to read Battery node {d.SlaveId}\n{e.Message}");
|
||||
// TODO: log
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
|
||||
public class BatteryDeligreenRecord
|
||||
{
|
||||
public readonly BatteryDeligreenDataRecord BatteryDeligreenDataRecord;
|
||||
public readonly BatteryDeligreenAlarmRecord BatteryDeligreenAlarmRecord;
|
||||
|
||||
public BatteryDeligreenRecord(BatteryDeligreenDataRecord batteryDeligreenDataRecord, BatteryDeligreenAlarmRecord batteryDeligreenAlarmRecord)
|
||||
{
|
||||
BatteryDeligreenDataRecord = batteryDeligreenDataRecord;
|
||||
BatteryDeligreenAlarmRecord = batteryDeligreenAlarmRecord;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
using InnovEnergy.Lib.Units;
|
||||
using InnovEnergy.Lib.Units.Composite;
|
||||
using InnovEnergy.Lib.Units.Power;
|
||||
|
||||
namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
|
||||
public class BatteryDeligreenRecords
|
||||
{
|
||||
//public required DcBus Dc { get; init; }
|
||||
public required Current Current { get; init; }
|
||||
public required Voltage Voltage { get; init; }
|
||||
public required Percent Soc { get; init; }
|
||||
public required Double Soh { get; init; }
|
||||
public required Percent CurrentMinSoc { get; init; }
|
||||
public required Temperature TemperatureCell1 { get; init; }
|
||||
public required Double Power { get; init; }
|
||||
|
||||
// public required Temperature TemperatureCell2 { get; init; }
|
||||
// public required Temperature TemperatureCell3 { get; init; }
|
||||
// public required Temperature TemperatureCell4 { get; init; }
|
||||
// to continue other temperature
|
||||
|
||||
public required IReadOnlyList<BatteryDeligreenRecord> Devices { get; init; }
|
||||
|
||||
public static BatteryDeligreenRecords? FromBatteries(IReadOnlyList<BatteryDeligreenRecord>? records)
|
||||
{
|
||||
if (records is null || records.Count == 0)
|
||||
{
|
||||
Console.WriteLine("FromBatteries: either record is null or empty");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BatteryDeligreenRecords
|
||||
{
|
||||
Devices = records,
|
||||
Soc = records.Average(r => r.BatteryDeligreenDataRecord.Soc.Value),
|
||||
Soh = records.Average(r => r.BatteryDeligreenDataRecord.Soh),
|
||||
CurrentMinSoc = records.Min(r => r.BatteryDeligreenDataRecord.Soc.Value),
|
||||
TemperatureCell1 = records.Average(b => b.BatteryDeligreenDataRecord.TemperaturesList.CellTemperature1),
|
||||
Current = records.Sum(r =>r.BatteryDeligreenDataRecord.BusCurrent),
|
||||
Voltage = records.Average(r =>r.BatteryDeligreenDataRecord.BusVoltage),
|
||||
Power = records.Sum(r => r.BatteryDeligreenDataRecord.Power),
|
||||
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,480 @@
|
|||
import serial
|
||||
import csv
|
||||
|
||||
TELECOMMAND_FILE_PATH = "Telecommand_Return_Record.csv"
|
||||
|
||||
# Table 3
|
||||
CID1_DEVICE_CODES = {
|
||||
"46": "Lithium iron phosphate battery BMS",
|
||||
}
|
||||
|
||||
# Table 4
|
||||
CID2_COMMAND_CODES = {
|
||||
"42": "Acquisition of telemetering information",
|
||||
"44": "Acquisition of telecommand information",
|
||||
"45": "Telecontrol command",
|
||||
"47": "Acquisition of teleregulation information",
|
||||
"49": "Setting of teleregulation information",
|
||||
"4F": "Acquisition of the communication protocol version number",
|
||||
"51": "Acquisition of device vendor information",
|
||||
"4B": "Acquisition of historical data",
|
||||
"4D": "Acquisition time",
|
||||
"4E": "Synchronization time",
|
||||
"A0": "Production calibration",
|
||||
"A1": "Production setting",
|
||||
"A2": "Regular recording"
|
||||
}
|
||||
|
||||
# Table 5
|
||||
CID2_RETURN_CODES = {
|
||||
"00": "Normal",
|
||||
"01": "VER error",
|
||||
"02": "CHKSUM error",
|
||||
"03": "LCHKSUM error",
|
||||
"04": "CID2 invalid",
|
||||
"05": "Command format error",
|
||||
"06": "Data invalid (parameter setting)",
|
||||
"07": "No data (history)",
|
||||
"E1": "CID1 invalid",
|
||||
"E2": "Command execution failure",
|
||||
"E3": "Device fault",
|
||||
"E4": "Invalid permissions"
|
||||
}
|
||||
|
||||
|
||||
# Table 12
|
||||
BYTE_ALARM_CODES = {
|
||||
"00": "Normal, no alarm",
|
||||
"01": "Alarm that analog quantity reaches the lower limit",
|
||||
"02": "Alarm that analog quantity reaches the upper limit",
|
||||
"F0": "Other alarms"
|
||||
}
|
||||
|
||||
# Table 13
|
||||
BIT_ALARM_CODES = {
|
||||
"Alarm event 1": (
|
||||
"Voltage sensor fault",
|
||||
"Temperature sensor fault",
|
||||
"Current sensor fault",
|
||||
"Key switch fault",
|
||||
"Cell voltage dropout fault",
|
||||
"Charge switch fault",
|
||||
"Discharge switch fault",
|
||||
"Current limit switch fault"
|
||||
),
|
||||
"Alarm event 2": (
|
||||
"Monomer high voltage alarm",
|
||||
"Monomer overvoltage protection",
|
||||
"Monomer low voltage alarm",
|
||||
"Monomer under voltage protection",
|
||||
"High voltage alarm for total voltage",
|
||||
"Overvoltage protection for total voltage",
|
||||
"Low voltage alarm for total voltage",
|
||||
"Under voltage protection for total voltage"
|
||||
),
|
||||
"Alarm event 3": (
|
||||
"Charge high temperature alarm",
|
||||
"Charge over temperature protection",
|
||||
"Charge low temperature alarm",
|
||||
"Charge under temperature protection",
|
||||
"Discharge high temperature alarm",
|
||||
"Discharge over temperature protection",
|
||||
"Discharge low temperature alarm",
|
||||
"Discharge under temperature protection"
|
||||
),
|
||||
"Alarm event 4": (
|
||||
"Environment high temperature alarm",
|
||||
"Environment over temperature protection",
|
||||
"Environment low temperature alarm",
|
||||
"Environment under temperature protection",
|
||||
"Power over temperature protection",
|
||||
"Power high temperature alarm",
|
||||
"Cell low temperature heating",
|
||||
"Reservation bit"
|
||||
),
|
||||
"Alarm event 5": (
|
||||
"Charge over current alarm",
|
||||
"Charge over current protection",
|
||||
"Discharge over current alarm",
|
||||
"Discharge over current protection",
|
||||
"Transient over current protection",
|
||||
"Output short circuit protection",
|
||||
"Transient over current lockout",
|
||||
"Output short circuit lockout"
|
||||
),
|
||||
"Alarm event 6": (
|
||||
"Charge high voltage protection",
|
||||
"Intermittent recharge waiting",
|
||||
"Residual capacity alarm",
|
||||
"Residual capacity protection",
|
||||
"Cell low voltage charging prohibition",
|
||||
"Output reverse polarity protection",
|
||||
"Output connection fault",
|
||||
"Inside bit"
|
||||
),
|
||||
"On-off state": (
|
||||
"Discharge switch state",
|
||||
"Charge switch state",
|
||||
"Current limit switch state",
|
||||
"Heating switch state",
|
||||
"Reservation bit",
|
||||
"Reservation bit",
|
||||
"Reservation bit",
|
||||
"Reservation bit"
|
||||
),
|
||||
"Equilibrium state 1": (
|
||||
"Cell 01 equilibrium",
|
||||
"Cell 02 equilibrium",
|
||||
"Cell 03 equilibrium",
|
||||
"Cell 04 equilibrium",
|
||||
"Cell 05 equilibrium",
|
||||
"Cell 06 equilibrium",
|
||||
"Cell 07 equilibrium",
|
||||
"Cell 08 equilibrium"
|
||||
),
|
||||
"Equilibrium state 2": (
|
||||
"Cell 09 equilibrium",
|
||||
"Cell 10 equilibrium",
|
||||
"Cell 11 equilibrium",
|
||||
"Cell 12 equilibrium",
|
||||
"Cell 13 equilibrium",
|
||||
"Cell 14 equilibrium",
|
||||
"Cell 15 equilibrium",
|
||||
"Cell 16 equilibrium"
|
||||
),
|
||||
"System state": (
|
||||
"Discharge",
|
||||
"Charge",
|
||||
"Floating charge",
|
||||
"Reservation bit",
|
||||
"Standby",
|
||||
"Shutdown",
|
||||
"Reservation bit",
|
||||
"Reservation bit"
|
||||
),
|
||||
"Disconnection state 1": (
|
||||
"Cell 01 disconnection",
|
||||
"Cell 02 disconnection",
|
||||
"Cell 03 disconnection",
|
||||
"Cell 04 disconnection",
|
||||
"Cell 05 disconnection",
|
||||
"Cell 06 disconnection",
|
||||
"Cell 07 disconnection",
|
||||
"Cell 08 disconnection"
|
||||
),
|
||||
"Disconnection state 2": (
|
||||
"Cell 09 disconnection",
|
||||
"Cell 10 disconnection",
|
||||
"Cell 11 disconnection",
|
||||
"Cell 12 disconnection",
|
||||
"Cell 13 disconnection",
|
||||
"Cell 14 disconnection",
|
||||
"Cell 15 disconnection",
|
||||
"Cell 16 disconnection"
|
||||
),
|
||||
"Alarm event 7": (
|
||||
"Inside bit",
|
||||
"Inside bit",
|
||||
"Inside bit",
|
||||
"Inside bit",
|
||||
"Automatic charging waiting",
|
||||
"Manual charging waiting",
|
||||
"Inside bit",
|
||||
"Inside bit"
|
||||
),
|
||||
"Alarm event 3": (
|
||||
"EEP storage fault",
|
||||
"RTC error",
|
||||
"Voltage calibration not performed",
|
||||
"Current calibration not performed",
|
||||
"Zero calibration not performed",
|
||||
"Inside bit",
|
||||
"Inside bit",
|
||||
"Inside bit"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def parse_start_code(frame):
|
||||
soi = frame[0:1]
|
||||
if soi == "~":
|
||||
return "ok!"
|
||||
else:
|
||||
raise ValueError(f"Invalid start identifier! ({soi})")
|
||||
|
||||
def parse_version_code(frame):
|
||||
ver = frame[1:3]
|
||||
return f"Protocol Version V{ver[0]}.{ver[1]}"
|
||||
|
||||
def parse_address_code(frame):
|
||||
adr = frame[3:5]
|
||||
if 0 <= int(adr) <= 15:
|
||||
return adr
|
||||
else:
|
||||
raise ValueError(f"Invalid address: {adr} (out of range 0-15)")
|
||||
|
||||
def parse_device_code(frame):
|
||||
cid1 = frame[5:7]
|
||||
return CID1_DEVICE_CODES.get(cid1, "Unknown!")
|
||||
|
||||
def parse_function_code(frame):
|
||||
cid2 = frame[7:9]
|
||||
if cid2 in CID2_COMMAND_CODES:
|
||||
return f"Command -> {CID2_COMMAND_CODES.get(cid2)}"
|
||||
elif cid2 in CID2_RETURN_CODES:
|
||||
return f"Return -> {CID2_RETURN_CODES.get(cid2)}"
|
||||
else:
|
||||
return f"Unknown CID2: {cid2}"
|
||||
|
||||
def parse_lchksum(length_code):
|
||||
# implements chapter 3.2.2 of the Protocol Specification
|
||||
lchksum = int(length_code[0], 16)
|
||||
# Compute lchksum
|
||||
d11d10d09d08 = int(length_code[1])
|
||||
d07d06d05d04 = int(length_code[2])
|
||||
d03d0ld01d00 = int(length_code[3])
|
||||
sum = d11d10d09d08 + d07d06d05d04 + d03d0ld01d00
|
||||
remainder = sum % 16
|
||||
inverted = ~remainder & 0xF
|
||||
computed_lchksum = (inverted + 1) & 0xF
|
||||
if computed_lchksum == lchksum:
|
||||
return "ok!"
|
||||
else:
|
||||
raise ValueError(f"Invalid LCHKSUM: {lchksum} (computed: {computed_lchksum})")
|
||||
|
||||
def parse_lenid(length_code):
|
||||
# implements chapter 3.2.1 of the Protocol Specification
|
||||
d11d10d09d08 = int(length_code[1])
|
||||
d07d06d05d04 = int(length_code[2])
|
||||
d03d0ld01d00 = int(length_code[3])
|
||||
lenid = d11d10d09d08 << 8 | d07d06d05d04 << 4 | d03d0ld01d00
|
||||
return lenid >> 1
|
||||
|
||||
def parse_length_code(frame):
|
||||
# implements chapter 3.2 of the Protocol Specification
|
||||
length_code = frame[9:13]
|
||||
lchksum = parse_lchksum(length_code)
|
||||
lenid = parse_lenid(length_code)
|
||||
return { "LCHKSUM": lchksum, "LENID": lenid }
|
||||
|
||||
def parse_info(frame):
|
||||
cid2 = frame[7:9]
|
||||
lenid = parse_lenid(frame[9:13])
|
||||
info = frame[13:13+lenid*2]
|
||||
|
||||
if cid2 == '00' and lenid == 49:
|
||||
return parse_telecommand_return(info)
|
||||
elif cid2 == '00' and lenid == 75:
|
||||
return parse_telemetry_return(info)
|
||||
else:
|
||||
return info
|
||||
|
||||
def parse_telecommand_return(info_raw, info={}, index=0):
|
||||
|
||||
info["DATA FLAG"] = info_raw[index:index+2]
|
||||
index += 2
|
||||
|
||||
info["COMMAND GROUP"] = info_raw[index:index+2]
|
||||
index += 2
|
||||
|
||||
num_of_cells = int(info_raw[index:index+2], 16)
|
||||
info["Number of cells"] = num_of_cells
|
||||
index += 2
|
||||
|
||||
for cell in range(info["Number of cells"]):
|
||||
alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2])
|
||||
info[f"Cell {cell +1} alarm"] = alarm
|
||||
index += 2
|
||||
|
||||
num_of_temperatures = int(info_raw[index:index+2], 16)
|
||||
info["Number of temperatures"] = num_of_temperatures
|
||||
index += 2
|
||||
|
||||
for sensor in range(4):
|
||||
alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2])
|
||||
info[f"Cell temperature alarm {sensor}"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2])
|
||||
info["Environment temperature alarm"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2])
|
||||
info["Power temperature alarm 1"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2])
|
||||
info["Charge/discharge current alarm"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2])
|
||||
info["Total battery voltage alarm"] = alarm
|
||||
index += 2
|
||||
|
||||
num_custom = int(info_raw[index:index+2], 16)
|
||||
info["Number of custom alarms"] = num_custom
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Alarm event 1"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Alarm event 2"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Alarm event 3"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Alarm event 4"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Alarm event 5"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Alarm event 6"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["On-off state"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Equilibrium state 1"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Equilibrium state 2"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["System state"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Disconnection state 1"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Disconnection state 2"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Alarm event 7"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Alarm event 8"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Reservation extention 1"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Reservation extention 2"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Reservation extention 3"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Reservation extention 4"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Reservation extention 5"] = alarm
|
||||
index += 2
|
||||
|
||||
alarm = info_raw[index:index+2]
|
||||
info["Reservation extention 6"] = alarm
|
||||
index += 2
|
||||
|
||||
save_dict_to_csv(TELECOMMAND_FILE_PATH, info)
|
||||
return f"Telecommand Return Data saved in ./{TELECOMMAND_FILE_PATH}"
|
||||
|
||||
|
||||
def save_dict_to_csv(file_path, data):
|
||||
with open(file_path, mode='a+', newline='') as csvfile:
|
||||
csvfile.seek(0)
|
||||
has_header = csvfile.read(1) != ""
|
||||
csvfile.seek(0, 2)
|
||||
writer = csv.DictWriter(csvfile, fieldnames=data.keys())
|
||||
if not has_header:
|
||||
writer.writeheader()
|
||||
writer.writerow(data)
|
||||
|
||||
def parse_checksum(frame):
|
||||
"""implements section 3.3 of the Protocol Specification"""
|
||||
chksum = int(frame[-6:-1], 16)
|
||||
data = frame[1:-5]
|
||||
# Compute chksum
|
||||
ascii_sum = sum(ord(char) for char in data)
|
||||
remainder = ascii_sum % 65536
|
||||
inverted = ~remainder & 0xFFFF
|
||||
computed_chksum = (inverted + 1) & 0xFFFF
|
||||
# Compare with CHKSUM in frame
|
||||
if computed_chksum == chksum:
|
||||
return "ok!"
|
||||
else:
|
||||
raise ValueError(f"Invalid CHKSUM: {chksum} (computed: {computed_chksum})")
|
||||
|
||||
def parse_end_code(frame):
|
||||
eoi = frame[-1]
|
||||
if eoi == "\r":
|
||||
return "ok!"
|
||||
else:
|
||||
raise ValueError(f"Invalid end identifier! ({eoi})")
|
||||
|
||||
def parse_modbus_ascii_frame(frame, parsed_data = {}):
|
||||
frame = bytes.fromhex(frame).decode('ascii')
|
||||
parsed_data["SOI"] = parse_start_code(frame)
|
||||
parsed_data["VER"] = parse_version_code(frame)
|
||||
parsed_data["ADR"] = parse_address_code(frame)
|
||||
parsed_data["CID1"] = parse_device_code(frame)
|
||||
parsed_data["CID2"] = parse_function_code(frame)
|
||||
parsed_data["LENGTH"] = parse_length_code(frame)
|
||||
parsed_data["INFO"] = parse_info(frame)
|
||||
parsed_data["CHKSUM"] = parse_checksum(frame)
|
||||
parsed_data["EOI"] = parse_end_code(frame)
|
||||
return parsed_data
|
||||
|
||||
def send_command():
|
||||
|
||||
# Define the serial port and baud rate
|
||||
port = 'COM9' # Replace with your actual port
|
||||
baudrate = 19200 # Replace with the correct baud rate for your BMS
|
||||
|
||||
# Create the serial connection
|
||||
try:
|
||||
with serial.Serial(port, baudrate, timeout=1) as ser:
|
||||
# Convert the hex string to bytes
|
||||
command = bytes.fromhex("7E3230303034363434453030323030464433350D")
|
||||
|
||||
# Send the command
|
||||
ser.write(command)
|
||||
print("Command sent successfully.")
|
||||
|
||||
# Wait for and read the response
|
||||
response = ser.read(200) # Adjust the number of bytes to read as needed
|
||||
if response:
|
||||
hex_response = response.hex()
|
||||
print("Response received:", hex_response)
|
||||
# Process the response to check details
|
||||
parsed_result = parse_modbus_ascii_frame(hex_response)
|
||||
for key, value in parsed_result.items():
|
||||
print(f"{key}: {value}")
|
||||
else:
|
||||
print("No response received.")
|
||||
except serial.SerialException as e:
|
||||
print(f"Error opening serial port: {e}")
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
send_command()
|
|
@ -0,0 +1,277 @@
|
|||
import serial
|
||||
|
||||
def check_starting_byte_and_extract_details(response):
|
||||
# Ensure the response is a valid hex string
|
||||
if not response or len(response) < 38 + (16 * 8) + 4 + (8 * 14) + 4: # Update minimum length check
|
||||
print("Response is too short to contain valid data.")
|
||||
return
|
||||
|
||||
# Extract the first byte and check if it's '7E'
|
||||
starting_byte = response[:2]
|
||||
if starting_byte.upper() == "7E":
|
||||
print(f"Starting byte: {starting_byte} (Hex)")
|
||||
else:
|
||||
print(f"Incorrect starting byte: {starting_byte}")
|
||||
return
|
||||
|
||||
# Extract the next two bytes for the firmware version
|
||||
version_bytes = response[2:6]
|
||||
try:
|
||||
version_ascii = bytes.fromhex(version_bytes).decode('ascii')
|
||||
print(f"Firmware version: {version_bytes} (Hex), ASCII: {version_ascii}")
|
||||
except ValueError:
|
||||
print(f"Failed to decode firmware version from bytes: {version_bytes}")
|
||||
return
|
||||
|
||||
# Extract the next two bytes for the address
|
||||
address_bytes = response[6:10]
|
||||
try:
|
||||
address_ascii = bytes.fromhex(address_bytes).decode('ascii')
|
||||
address_decimal = int(address_ascii, 16)
|
||||
print(f"Device Address: {address_bytes} (Hex), ASCII: {address_ascii}, Decimal: {address_decimal}")
|
||||
except ValueError:
|
||||
print(f"Failed to decode device address from bytes: {address_bytes}")
|
||||
return
|
||||
|
||||
# Extract the next two bytes for CID1 (Device Code)
|
||||
cid1_bytes = response[10:14]
|
||||
try:
|
||||
cid1_ascii = bytes.fromhex(cid1_bytes).decode('ascii')
|
||||
cid1_decimal = int(cid1_ascii, 16)
|
||||
print(f"Device Code (CID1): {cid1_bytes} (Hex), ASCII: {cid1_ascii}, Decimal: {cid1_decimal}")
|
||||
except ValueError:
|
||||
print(f"Failed to decode device code from bytes: {cid1_bytes}")
|
||||
|
||||
# Extract the next two bytes for the Function Code
|
||||
function_code_bytes = response[14:18]
|
||||
try:
|
||||
function_code_ascii = bytes.fromhex(function_code_bytes).decode('ascii')
|
||||
function_code_decimal = int(function_code_ascii, 16)
|
||||
print(f"Function Code: {function_code_bytes} (Hex), ASCII: {function_code_ascii}, Decimal: {function_code_decimal}")
|
||||
except ValueError:
|
||||
print(f"Failed to decode function code from bytes: {function_code_bytes}")
|
||||
|
||||
# Extract the next 4 bytes for the Length Code
|
||||
length_code_bytes = response[18:26]
|
||||
try:
|
||||
length_ascii = bytes.fromhex(length_code_bytes).decode('ascii')
|
||||
length_decimal = int(length_ascii, 16)
|
||||
print(f"Length Code: {length_code_bytes} (Hex), ASCII: {length_ascii}, Decimal: {length_decimal}")
|
||||
except ValueError:
|
||||
print(f"Failed to decode length code from bytes: {length_code_bytes}")
|
||||
|
||||
# Extract the next 2 bytes for the Data Flag
|
||||
data_flag_bytes = response[26:30]
|
||||
try:
|
||||
data_flag_ascii = bytes.fromhex(data_flag_bytes).decode('ascii')
|
||||
data_flag_decimal = int(data_flag_ascii, 16)
|
||||
print(f"Data Flag: {data_flag_bytes} (Hex), ASCII: {data_flag_ascii}, Decimal: {data_flag_decimal}")
|
||||
except ValueError:
|
||||
print(f"Failed to decode data flag from bytes: {data_flag_bytes}")
|
||||
|
||||
# Extract the next 2 bytes for the Command Group
|
||||
command_group_bytes = response[30:34]
|
||||
try:
|
||||
command_group_ascii = bytes.fromhex(command_group_bytes).decode('ascii')
|
||||
command_group_decimal = int(command_group_ascii, 16)
|
||||
print(f"Command Group: {command_group_bytes} (Hex), ASCII: {command_group_ascii}, Decimal: {command_group_decimal}")
|
||||
except ValueError:
|
||||
print(f"Failed to decode command group from bytes: {command_group_bytes}")
|
||||
|
||||
# Extract the next 2 bytes for the Number of Cells
|
||||
num_cells_bytes = response[34:38]
|
||||
try:
|
||||
num_cells_ascii = bytes.fromhex(num_cells_bytes).decode('ascii')
|
||||
num_cells_decimal = int(num_cells_ascii, 16)
|
||||
print(f"Number of Cells: {num_cells_bytes} (Hex), ASCII: {num_cells_ascii}, Decimal: {num_cells_decimal}")
|
||||
except ValueError:
|
||||
print(f"Failed to decode number of cells from bytes: {num_cells_bytes}")
|
||||
|
||||
# Extract and process the voltages for all 16 cells
|
||||
for cell_index in range(16):
|
||||
start = 38 + (cell_index * 8)
|
||||
end = start + 8
|
||||
cell_voltage_bytes = response[start:end]
|
||||
try:
|
||||
cell_voltage_ascii = bytes.fromhex(cell_voltage_bytes).decode('ascii')
|
||||
cell_voltage_decimal = int(cell_voltage_ascii, 16) / 1000.0 # Convert to volts
|
||||
print(f"Voltage of Cell {cell_index + 1}: {cell_voltage_bytes} (Hex), ASCII: {cell_voltage_ascii}, Voltage: {cell_voltage_decimal:.3f} V")
|
||||
except ValueError:
|
||||
print(f"Failed to decode Voltage of Cell {cell_index + 1} from bytes: {cell_voltage_bytes}")
|
||||
|
||||
# Extract the number of temperature sensors (4 hex bytes)
|
||||
num_temp_start = 38 + (16 * 8)
|
||||
num_temp_end = num_temp_start + 4
|
||||
num_temp_bytes = response[num_temp_start:num_temp_end]
|
||||
try:
|
||||
num_temp_ascii = bytes.fromhex(num_temp_bytes).decode('ascii')
|
||||
num_temp_decimal = int(num_temp_ascii, 16)
|
||||
print(f"Number of Temperature Sensors: {num_temp_bytes} (Hex), ASCII: {num_temp_ascii}, Decimal: {num_temp_decimal}")
|
||||
except ValueError:
|
||||
print(f"Failed to decode number of temperature sensors from bytes: {num_temp_bytes}")
|
||||
|
||||
# Extract and process additional temperature and battery information
|
||||
current_index = num_temp_end
|
||||
|
||||
# Cell Temperature 1
|
||||
for temp_index in range(1, 5):
|
||||
temp_bytes = response[current_index:current_index + 8]
|
||||
try:
|
||||
temp_ascii = bytes.fromhex(temp_bytes).decode('ascii')
|
||||
temp_decimal = (int(temp_ascii, 16)- 2731 )/ 10.0 # Convert to Celsius
|
||||
print(f"Cell Temperature {temp_index}: {temp_bytes} (Hex), ASCII: {temp_ascii}, Temperature: {temp_decimal:.2f} °C")
|
||||
except ValueError:
|
||||
print(f"Failed to decode Cell Temperature {temp_index} from bytes: {temp_bytes}")
|
||||
current_index += 8
|
||||
|
||||
# Environment Temperature
|
||||
env_temp_bytes = response[current_index:current_index + 8]
|
||||
try:
|
||||
env_temp_ascii = bytes.fromhex(env_temp_bytes).decode('ascii')
|
||||
env_temp_decimal = (int(env_temp_ascii, 16) - 2731 )/ 10.0
|
||||
print(f"Environment Temperature: {env_temp_bytes} (Hex), ASCII: {env_temp_ascii}, Temperature: {env_temp_decimal:.2f} °C")
|
||||
except ValueError:
|
||||
print(f"Failed to decode Environment Temperature from bytes: {env_temp_bytes}")
|
||||
current_index += 8
|
||||
|
||||
# Power Temperature
|
||||
power_temp_bytes = response[current_index:current_index + 8]
|
||||
try:
|
||||
power_temp_ascii = bytes.fromhex(power_temp_bytes).decode('ascii')
|
||||
power_temp_decimal = (int(power_temp_ascii, 16)- 2731 )/ 10.0
|
||||
print(f"Power Temperature: {power_temp_bytes} (Hex), ASCII: {power_temp_ascii}, Temperature: {power_temp_decimal:.2f} °C")
|
||||
except ValueError:
|
||||
print(f"Failed to decode Power Temperature from bytes: {power_temp_bytes}")
|
||||
current_index += 8
|
||||
|
||||
# Charge/Discharge Current
|
||||
current_bytes = response[current_index:current_index + 8]
|
||||
try:
|
||||
current_ascii = bytes.fromhex(current_bytes).decode('ascii')
|
||||
current_decimal = int(current_ascii, 16) / 100.0
|
||||
print(f"Charge/Discharge Current: {current_bytes} (Hex), ASCII: {current_ascii}, Current: {current_decimal:.3f} A")
|
||||
except ValueError:
|
||||
print(f"Failed to decode Charge/Discharge Current from bytes: {current_bytes}")
|
||||
current_index += 8
|
||||
|
||||
# Total Battery Voltage
|
||||
total_voltage_bytes = response[current_index:current_index + 8]
|
||||
try:
|
||||
total_voltage_ascii = bytes.fromhex(total_voltage_bytes).decode('ascii')
|
||||
total_voltage_decimal = int(total_voltage_ascii, 16) / 100.0
|
||||
print(f"Total Battery Voltage: {total_voltage_bytes} (Hex), ASCII: {total_voltage_ascii}, Voltage: {total_voltage_decimal:.3f} V")
|
||||
except ValueError:
|
||||
print(f"Failed to decode Total Battery Voltage from bytes: {total_voltage_bytes}")
|
||||
current_index += 8
|
||||
|
||||
# Residual Capacity
|
||||
residual_capacity_bytes = response[current_index:current_index + 8]
|
||||
try:
|
||||
residual_capacity_ascii = bytes.fromhex(residual_capacity_bytes).decode('ascii')
|
||||
residual_capacity_decimal = int(residual_capacity_ascii, 16) / 100.0
|
||||
print(f"Residual Capacity: {residual_capacity_bytes} (Hex), ASCII: {residual_capacity_ascii}, Capacity: {residual_capacity_decimal:.3f} Ah")
|
||||
except ValueError:
|
||||
print(f"Failed to decode Residual Capacity from bytes: {residual_capacity_bytes}")
|
||||
current_index += 8
|
||||
|
||||
# Custom Number
|
||||
custom_number_bytes = response[current_index:current_index + 4]
|
||||
try:
|
||||
custom_number_ascii = bytes.fromhex(custom_number_bytes).decode('ascii')
|
||||
custom_number_decimal = int(custom_number_ascii, 16)
|
||||
print(f"Custom Number: {custom_number_bytes} (Hex), ASCII: {custom_number_ascii}, Decimal: {custom_number_decimal}")
|
||||
except ValueError:
|
||||
print(f"Failed to decode Custom Number from bytes: {custom_number_bytes}")
|
||||
current_index += 4
|
||||
|
||||
# Battery Capacity
|
||||
battery_capacity_bytes = response[current_index:current_index + 8]
|
||||
try:
|
||||
battery_capacity_ascii = bytes.fromhex(battery_capacity_bytes).decode('ascii')
|
||||
battery_capacity_decimal = int(battery_capacity_ascii, 16) / 100.0
|
||||
print(f"Battery Capacity: {battery_capacity_bytes} (Hex), ASCII: {battery_capacity_ascii}, Capacity: {battery_capacity_decimal:.3f} Ah")
|
||||
except ValueError:
|
||||
print(f"Failed to decode Battery Capacity from bytes: {battery_capacity_bytes}")
|
||||
current_index += 8
|
||||
|
||||
# SOC
|
||||
soc_bytes = response[current_index:current_index + 8]
|
||||
try:
|
||||
soc_ascii = bytes.fromhex(soc_bytes).decode('ascii')
|
||||
soc_decimal = int(soc_ascii, 16) / 10.0
|
||||
print(f"SOC: {soc_bytes} (Hex), ASCII: {soc_ascii}, SOC: {soc_decimal:.2f}%")
|
||||
except ValueError:
|
||||
print(f"Failed to decode SOC from bytes: {soc_bytes}")
|
||||
current_index += 8
|
||||
|
||||
# Rated Capacity
|
||||
rated_capacity_bytes = response[current_index:current_index + 8]
|
||||
try:
|
||||
rated_capacity_ascii = bytes.fromhex(rated_capacity_bytes).decode('ascii')
|
||||
rated_capacity_decimal = int(rated_capacity_ascii, 16) / 100.0
|
||||
print(f"Rated Capacity: {rated_capacity_bytes} (Hex), ASCII: {rated_capacity_ascii}, Capacity: {rated_capacity_decimal:.3f} Ah")
|
||||
except ValueError:
|
||||
print(f"Failed to decode Rated Capacity from bytes: {rated_capacity_bytes}")
|
||||
current_index += 8
|
||||
|
||||
# Number of Cycles
|
||||
num_cycles_bytes = response[current_index:current_index + 8]
|
||||
try:
|
||||
num_cycles_ascii = bytes.fromhex(num_cycles_bytes).decode('ascii')
|
||||
num_cycles_decimal = int(num_cycles_ascii, 16)
|
||||
print(f"Number of Cycles: {num_cycles_bytes} (Hex), ASCII: {num_cycles_ascii}, Cycles: {num_cycles_decimal}")
|
||||
except ValueError:
|
||||
print(f"Failed to decode Number of Cycles from bytes: {num_cycles_bytes}")
|
||||
current_index += 8
|
||||
|
||||
# SOH
|
||||
soh_bytes = response[current_index:current_index + 8]
|
||||
try:
|
||||
soh_ascii = bytes.fromhex(soh_bytes).decode('ascii')
|
||||
soh_decimal = int(soh_ascii, 16) / 10.0
|
||||
print(f"SOH: {soh_bytes} (Hex), ASCII: {soh_ascii}, SOH: {soh_decimal:.2f}%")
|
||||
except ValueError:
|
||||
print(f"Failed to decode SOH from bytes: {soh_bytes}")
|
||||
current_index += 8
|
||||
|
||||
# bus voltage
|
||||
bus_bytes = response[current_index:current_index + 8]
|
||||
try:
|
||||
bus_ascii = bytes.fromhex(bus_bytes).decode('ascii')
|
||||
bus_decimal = int(bus_ascii, 16) / 100.0
|
||||
print(f"bus voltage: {bus_bytes} (Hex), ASCII: {bus_ascii}, bus voltage: {bus_decimal:.2f}V")
|
||||
except ValueError:
|
||||
print(f"Failed to decode bus voltage from bytes: {bus_bytes}")
|
||||
|
||||
|
||||
def send_command():
|
||||
# Define the serial port and baud rate
|
||||
port = '/dev/ttyUSB0' # Ensure the full path is correct
|
||||
baudrate = 19200 # Replace with the correct baud rate for your BMS
|
||||
|
||||
# Create the serial connection
|
||||
try:
|
||||
with serial.Serial(port, baudrate, timeout=1) as ser:
|
||||
# Convert the hex string to bytes
|
||||
command = bytes.fromhex("7E3230303134363432453030323031464433350D")
|
||||
|
||||
# Send the command
|
||||
ser.write(command)
|
||||
print("Command sent successfully.")
|
||||
|
||||
# Wait for and read the response
|
||||
response = ser.read(200) # Adjust the number of bytes to read as needed
|
||||
if response:
|
||||
hex_response = response.hex()
|
||||
print("Response received:", hex_response)
|
||||
# Process the response to check details
|
||||
check_starting_byte_and_extract_details(hex_response)
|
||||
else:
|
||||
print("No response received.")
|
||||
except serial.SerialException as e:
|
||||
print(f"Error opening serial port: {e}")
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
send_command()
|
|
@ -1,6 +1,145 @@
|
|||
using System.Globalization;
|
||||
|
||||
namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class TelecommandFrameParser
|
||||
{
|
||||
private static Int32 _currentIndex;
|
||||
private const Int32 FrameLength = 232;
|
||||
|
||||
public Boolean ParsingTelecommandFrame(String response)
|
||||
{
|
||||
_currentIndex = 0; // Reset currentIndex to the start
|
||||
|
||||
if (string.IsNullOrEmpty(response) || response.Length < FrameLength)
|
||||
{
|
||||
Console.WriteLine("Response is too short to contain valid data.");
|
||||
Console.WriteLine(" Fixed Length" + FrameLength);
|
||||
Console.WriteLine(" response Length" + response.Length);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check starting byte
|
||||
string startingByte = response.Substring(_currentIndex, 2).ToUpper();
|
||||
if (startingByte == "7E")
|
||||
{
|
||||
// Console.WriteLine($"Starting byte: {startingByte} (Hex)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Incorrect starting byte: {startingByte}");
|
||||
return false;
|
||||
}
|
||||
|
||||
_currentIndex += 2;
|
||||
|
||||
// Extract firmware version
|
||||
var versionBytes = response.Substring(_currentIndex, 4);
|
||||
try
|
||||
{
|
||||
var versionAscii = HexToAscii(versionBytes);
|
||||
// Console.WriteLine($"Firmware version: {versionBytes} (Hex), ASCII: {versionAscii}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Console.WriteLine($"Failed to decode firmware version from bytes: {versionBytes}");
|
||||
return false;
|
||||
}
|
||||
|
||||
_currentIndex += 4;
|
||||
|
||||
// Extract and parse other fields
|
||||
ParseAndPrintHexField(response, "Device Address", 4);
|
||||
ParseAndPrintHexField(response, "Device Code (CID1)", 4);
|
||||
ParseAndPrintHexField(response, "Function Code", 4);
|
||||
ParseAndPrintHexField(response, "Length Code", 8);
|
||||
ParseAndPrintHexField(response, "Data Flag", 4);
|
||||
ParseAndPrintHexField(response, "Command Group", 4);
|
||||
ParseAndPrintHexField(response, "Number of Cells", 4);
|
||||
ExtractCellAlarm(response);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
private static void ExtractCellAlarm(String response)
|
||||
{
|
||||
|
||||
Dictionary<string, string> byteAlarmCodes = new Dictionary<string, string>
|
||||
{
|
||||
{ "00", "Normal, no alarm" },
|
||||
{ "01", "Alarm that analog quantity reaches the lower limit" },
|
||||
{ "02", "Alarm that analog quantity reaches the upper limit" },
|
||||
{ "F0", "Other alarms" }
|
||||
};
|
||||
|
||||
// Process Alarms for all 16 cells
|
||||
for (var i = 0; i < 16; i++)
|
||||
{
|
||||
var cellAlarm = response.Substring(_currentIndex, 4);
|
||||
try
|
||||
{
|
||||
var alarmAscii = HexToAscii(cellAlarm);
|
||||
var cellVoltageDecimal = HexToDecimal(alarmAscii);
|
||||
string alarmMessage = byteAlarmCodes.ContainsKey(alarmAscii) ? byteAlarmCodes[alarmAscii] : "Unknown alarm code";
|
||||
|
||||
// Console.WriteLine($"Cell {i + 1}: Alarm Code {cellAlarm}, Status: {alarmMessage}");
|
||||
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Console.WriteLine($"Failed to decode Voltage of Cell {i + 1} from bytes: {cellAlarm}");
|
||||
}
|
||||
_currentIndex += 4;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseAndPrintHexField(String response, String fieldName, int length)
|
||||
{
|
||||
var hexBytes = response.Substring(_currentIndex, length);
|
||||
try
|
||||
{
|
||||
var asciiValue = HexToAscii(hexBytes);
|
||||
var decimalValue = int.Parse(asciiValue, NumberStyles.HexNumber);
|
||||
// Console.WriteLine($"{fieldName}: {hexBytes} (Hex), ASCII: {asciiValue}, Decimal: {decimalValue}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Console.WriteLine($"Failed to decode {fieldName} from bytes: {hexBytes}");
|
||||
}
|
||||
_currentIndex += length;
|
||||
}
|
||||
private static void ParseAndPrintField(String response, String fieldName, Int32 length, Func<Double, Double> conversion, String unit)
|
||||
{
|
||||
var fieldBytes = response.Substring(_currentIndex, length);
|
||||
try
|
||||
{
|
||||
var fieldAscii = HexToAscii(fieldBytes);
|
||||
var fieldDecimal = conversion(HexToDecimal(fieldAscii));
|
||||
Console.WriteLine($"{fieldName}: {fieldBytes} (Hex), ASCII: {fieldAscii}, {fieldName}: {fieldDecimal:F3} {unit}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Console.WriteLine($"Failed to decode {fieldName} from bytes: {fieldBytes}");
|
||||
}
|
||||
_currentIndex += length;
|
||||
}
|
||||
|
||||
private static String HexToAscii(String hex)
|
||||
{
|
||||
var bytes = new Byte[hex.Length / 2];
|
||||
for (var i = 0; i < hex.Length; i += 2)
|
||||
{
|
||||
bytes[i / 2] = byte.Parse(hex.Substring(i, 2), NumberStyles.HexNumber);
|
||||
}
|
||||
return System.Text.Encoding.ASCII.GetString(bytes);
|
||||
}
|
||||
|
||||
private static double HexToDecimal(String hex)
|
||||
{
|
||||
return int.Parse(hex, NumberStyles.HexNumber);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +1,54 @@
|
|||
using System.Globalization;
|
||||
using InnovEnergy.Lib.Units;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using static InnovEnergy.Lib.Devices.BatteryDeligreen.BatteryDeligreenDataRecord;
|
||||
using static InnovEnergy.Lib.Devices.BatteryDeligreen.Temperatures;
|
||||
|
||||
namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
|
||||
public class TelemetryFrameParser
|
||||
{
|
||||
private static Int32 _currentIndex;
|
||||
private const Int32 FrameLenght = 286;
|
||||
private const Int32 FrameLenght = 336;
|
||||
|
||||
public void ParsingTelemetryFrame(String response)
|
||||
public BatteryDeligreenDataRecord? ParsingTelemetryFrame(String response)
|
||||
{
|
||||
|
||||
_currentIndex = 0; // Reset currentIndex to the start
|
||||
|
||||
if (string.IsNullOrEmpty(response) || response.Length < FrameLenght)
|
||||
{
|
||||
Console.WriteLine("Response is too short to contain valid data.");
|
||||
return;
|
||||
Console.WriteLine("length " + response.Length);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check starting byte
|
||||
string startingByte = response.Substring(_currentIndex, 2).ToUpper();
|
||||
var startingByte = response.Substring(_currentIndex, 2).ToUpper();
|
||||
if (startingByte == "7E")
|
||||
{
|
||||
Console.WriteLine($"Starting byte: {startingByte} (Hex)");
|
||||
//Console.WriteLine($"Starting byte: {startingByte} (Hex)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Incorrect starting byte: {startingByte}");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
_currentIndex += 2;
|
||||
|
||||
// Extract firmware version
|
||||
var versionBytes = response.Substring(_currentIndex, 4);
|
||||
var versionAscii = "";
|
||||
try
|
||||
{
|
||||
String versionAscii = HexToAscii(versionBytes);
|
||||
Console.WriteLine($"Firmware version: {versionBytes} (Hex), ASCII: {versionAscii}");
|
||||
versionAscii = HexToAscii(versionBytes);
|
||||
// Console.WriteLine($"Firmware version: {versionBytes} (Hex), ASCII: {versionAscii}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Console.WriteLine($"Failed to decode firmware version from bytes: {versionBytes}");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
_currentIndex += 4;
|
||||
|
||||
// Extract and parse other fields
|
||||
|
@ -54,15 +60,54 @@ public class TelemetryFrameParser
|
|||
ParseAndPrintHexField(response, "Command Group", 4);
|
||||
ParseAndPrintHexField(response, "Number of Cells", 4);
|
||||
|
||||
var cellVoltages = ExtractCellVoltage(response);
|
||||
|
||||
// Parse other fields
|
||||
ParseAndPrintHexField(response, "Number of Temperature Sensors", 4);
|
||||
var cellTemperature = new List<Double>();
|
||||
|
||||
// Parse cell temperatures
|
||||
for (var i = 1; i <= 4; i++)
|
||||
{
|
||||
cellTemperature.Add(ParseAndPrintTemperatureField(response, $"Cell Temperature {i}"));
|
||||
}
|
||||
|
||||
// Parse other temperature and battery information
|
||||
var environmentTemp = ParseAndPrintTemperatureField(response, "Environment Temperature");
|
||||
var powerTemp = ParseAndPrintTemperatureField(response, "Power Temperature");
|
||||
var current = ParseAndPrintField(response, "Charge/Discharge Current" , 8, value => value / 100.0, "A");
|
||||
var totalBatteryVoltage = ParseAndPrintField(response, "Total Battery Voltage" , 8, value => value / 100.0, "V");
|
||||
var residualCapacity = ParseAndPrintField(response, "Residual Capacity" , 8, value => value / 100.0, "Ah");
|
||||
var customNumber = ParseAndPrintHexField(response, "Custom Number" , 4);
|
||||
var batteryCapacity = ParseAndPrintField(response, "Battery Capacity" , 8, value => value / 100.0, "Ah");
|
||||
var soc = ParseAndPrintField(response, "SOC" , 8, value => value / 10.0, "%");
|
||||
var ratedCapacity = ParseAndPrintField(response, "Rated Capacity" , 8, value => value / 100.0, "Ah");
|
||||
var numberOfCycle = ParseAndPrintHexField(response, "Number of Cycles" , 8);
|
||||
var soh = ParseAndPrintField(response, "SOH" , 8, value => value / 10.0, "%");
|
||||
var busVoltage = ParseAndPrintField(response, "Bus Voltage" , 8, value => value / 100.0, "V");
|
||||
|
||||
var temperatures = new TemperaturesList(cellTemperature[0], cellTemperature[1], cellTemperature[2],
|
||||
cellTemperature[3], environmentTemp, powerTemp);
|
||||
|
||||
var batteryRecord = new BatteryDeligreenDataRecord(busVoltage, current, versionAscii, soc, numberOfCycle, batteryCapacity, ratedCapacity,
|
||||
totalBatteryVoltage, soh, residualCapacity, cellVoltages, temperatures);
|
||||
|
||||
return batteryRecord;
|
||||
}
|
||||
|
||||
private static List<Double> ExtractCellVoltage(String response)
|
||||
{
|
||||
var cellVoltages = new List<Double>();
|
||||
// Process voltages for all 16 cells
|
||||
for (var i = 0; i < 16; i++)
|
||||
{
|
||||
String cellVoltageBytes = response.Substring(_currentIndex, 8);
|
||||
var cellVoltageBytes = response.Substring(_currentIndex, 8);
|
||||
try
|
||||
{
|
||||
var cellVoltageAscii = HexToAscii(cellVoltageBytes);
|
||||
var cellVoltageDecimal = HexToDecimal(cellVoltageAscii) / 1000.0; // cell voltage are divided 1000
|
||||
Console.WriteLine($"Voltage of Cell {i + 1}: {cellVoltageBytes} (Hex), ASCII: {cellVoltageAscii}, Voltage: {cellVoltageDecimal:F3} V");
|
||||
cellVoltages.Add(cellVoltageDecimal);
|
||||
// Console.WriteLine($"Voltage of Cell {i + 1}: {cellVoltageBytes} (Hex), ASCII: {cellVoltageAscii}, Voltage: {cellVoltageDecimal:F3} V");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
@ -70,77 +115,77 @@ public class TelemetryFrameParser
|
|||
}
|
||||
_currentIndex += 8;
|
||||
}
|
||||
|
||||
// Parse other fields
|
||||
ParseAndPrintHexField(response, "Number of Temperature Sensors", 4);
|
||||
|
||||
// Parse cell temperatures
|
||||
for (var i = 1; i <= 4; i++)
|
||||
{
|
||||
ParseAndPrintTemperatureField(response, $"Cell Temperature {i}");
|
||||
}
|
||||
|
||||
// Parse other temperature and battery information
|
||||
ParseAndPrintTemperatureField(response, "Environment Temperature");
|
||||
ParseAndPrintTemperatureField(response, "Power Temperature");
|
||||
ParseAndPrintField(response, "Charge/Discharge Current", 8, value => value / 100.0, "A");
|
||||
ParseAndPrintField(response, "Total Battery Voltage", 8, value => value / 100.0, "V");
|
||||
ParseAndPrintField(response, "Residual Capacity", 8, value => value / 100.0, "Ah");
|
||||
ParseAndPrintHexField(response, "Custom Number", 4);
|
||||
ParseAndPrintField(response, "Battery Capacity", 8, value => value / 100.0, "Ah");
|
||||
ParseAndPrintField(response, "SOC", 8, value => value / 10.0, "%");
|
||||
ParseAndPrintField(response, "Rated Capacity", 8, value => value / 100.0, "Ah");
|
||||
ParseAndPrintHexField(response, "Number of Cycles", 8);
|
||||
ParseAndPrintField(response, "SOH", 8, value => value / 10.0, "%");
|
||||
ParseAndPrintField(response, "Bus Voltage", 8, value => value / 100.0, "V");
|
||||
return cellVoltages;
|
||||
}
|
||||
|
||||
private static void ParseAndPrintHexField(String response, String fieldName, int length)
|
||||
private static UInt16 ParseAndPrintHexField(String response, String fieldName, Int32 length)
|
||||
{
|
||||
var hexBytes = response.Substring(_currentIndex, length);
|
||||
var decimalValue = 0;
|
||||
try
|
||||
{
|
||||
var asciiValue = HexToAscii(hexBytes);
|
||||
var decimalValue = int.Parse(asciiValue, NumberStyles.HexNumber);
|
||||
Console.WriteLine($"{fieldName}: {hexBytes} (Hex), ASCII: {asciiValue}, Decimal: {decimalValue}");
|
||||
decimalValue = int.Parse(asciiValue, NumberStyles.HexNumber);
|
||||
// Console.WriteLine($"{fieldName}: {hexBytes} (Hex), ASCII: {asciiValue}, Decimal: {decimalValue}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Console.WriteLine($"Failed to decode {fieldName} from bytes: {hexBytes}");
|
||||
}
|
||||
_currentIndex += length;
|
||||
return (UInt16)decimalValue;
|
||||
|
||||
}
|
||||
|
||||
private static void ParseAndPrintTemperatureField(String response, String fieldName)
|
||||
private static Double ParseAndPrintTemperatureField(String response, String fieldName)
|
||||
{
|
||||
var tempBytes = response.Substring(_currentIndex, 8);
|
||||
var tempDecimal = 0.0;
|
||||
try
|
||||
{
|
||||
var tempAscii = HexToAscii(tempBytes);
|
||||
var tempDecimal = (HexToDecimal(tempAscii) - 2731) / 10.0;
|
||||
Console.WriteLine($"{fieldName}: {tempBytes} (Hex), ASCII: {tempAscii}, Temperature: {tempDecimal:F2} °C");
|
||||
tempDecimal = (HexToDecimal(tempAscii) - 2731) / 10.0;
|
||||
// Console.WriteLine($"{fieldName}: {tempBytes} (Hex), ASCII: {tempAscii}, Temperature: {tempDecimal:F2} °C");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Console.WriteLine($"Failed to decode {fieldName} from bytes: {tempBytes}");
|
||||
}
|
||||
_currentIndex += 8;
|
||||
return tempDecimal;
|
||||
}
|
||||
|
||||
private static void ParseAndPrintField(String response, String fieldName, Int32 length, Func<Double, Double> conversion, String unit)
|
||||
private static Double ParseAndPrintField(String response, String fieldName, Int32 length, Func<Double, Double> conversion, String unit)
|
||||
{
|
||||
var fieldBytes = response.Substring(_currentIndex, length);
|
||||
var value = 0.0;
|
||||
try
|
||||
{
|
||||
var fieldAscii = HexToAscii(fieldBytes);
|
||||
var fieldDecimal = conversion(HexToDecimal(fieldAscii));
|
||||
Console.WriteLine($"{fieldName}: {fieldBytes} (Hex), ASCII: {fieldAscii}, {fieldName}: {fieldDecimal:F3} {unit}");
|
||||
var fieldDouble = 0.0;
|
||||
|
||||
|
||||
// Convert from Hex to Integer using Two's Complement logic
|
||||
Int32 intValue = Convert.ToInt16(fieldAscii, 16);
|
||||
var bitLength = (length/2) * 4; // Each hex digit is 4 bits
|
||||
var maxPositiveValue = 1 << (bitLength - 1); // 2^(bitLength-1)
|
||||
|
||||
if (intValue >= maxPositiveValue)
|
||||
{
|
||||
intValue -= (1 << bitLength); // Apply two's complement conversion
|
||||
}
|
||||
|
||||
fieldDouble = conversion(intValue); // Store the converted negative value as string
|
||||
|
||||
//Console.WriteLine($"{fieldName}: {fieldBytes} (Hex), ASCII: {fieldAscii}, {fieldName}: {fieldDouble:F3} {unit}");
|
||||
value = fieldDouble;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Console.WriteLine($"Failed to decode {fieldName} from bytes: {fieldBytes}");
|
||||
}
|
||||
_currentIndex += length;
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String HexToAscii(String hex)
|
||||
|
@ -153,7 +198,7 @@ public class TelemetryFrameParser
|
|||
return System.Text.Encoding.ASCII.GetString(bytes);
|
||||
}
|
||||
|
||||
private static double HexToDecimal(String hex)
|
||||
private static Double HexToDecimal(String hex)
|
||||
{
|
||||
return int.Parse(hex, NumberStyles.HexNumber);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
using InnovEnergy.Lib.Units;
|
||||
|
||||
namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
|
||||
|
||||
public class Temperatures
|
||||
{
|
||||
public struct TemperaturesList
|
||||
{
|
||||
public Temperature CellTemperature1 {get;}
|
||||
public Temperature CellTemperature2 {get;}
|
||||
public Temperature CellTemperature3 {get;}
|
||||
public Temperature CellTemperature4 {get;}
|
||||
public Temperature EnvironmentTemperature {get;}
|
||||
public Temperature PowerTemperature {get;}
|
||||
|
||||
public TemperaturesList(Temperature cell1, Temperature cell2, Temperature cell3, Temperature cell4, Temperature environment, Temperature power)
|
||||
{
|
||||
CellTemperature1 = cell1;
|
||||
CellTemperature2 = cell2;
|
||||
CellTemperature3 = cell3;
|
||||
CellTemperature4 = cell4;
|
||||
EnvironmentTemperature = environment;
|
||||
PowerTemperature = power;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue