Create SodiStore solution and update Battery communication unit

This commit is contained in:
atef 2025-02-28 16:08:12 +01:00
parent ac54fc6e2e
commit 0e94d9c60d
74 changed files with 7951 additions and 120 deletions

View File

@ -9,6 +9,7 @@
<ItemGroup>
<ProjectReference Include="../../Lib/Devices/BatteryDeligreen/BatteryDeligreen.csproj" />
<ProjectReference Include="..\..\Lib\Units\Units.csproj" />
</ItemGroup>
<ItemGroup>

View File

@ -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)
}
}
}
}

View File

@ -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.

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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:])

View File

@ -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

View File

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCed5ANekhbdV/8nEwFyaqxbPGON+NZKAkZXKx2aMAbX6jYQpusXSf4lKxEp4vHX9q2ScWycluUEhlzwe9vaWIK6mxEG9gjtU0/tKIavqZ6qpcuiglal750e8tlDh+lAgg5K3v4tvV4uVEfFc42UzSC9cIBBKPBC41dc0xQKyFIDsSH6Qha1nyncKRC3OXUkOiiRvmbd4PVc9A5ah2vt+661pghZE19Qeh5ROn/Sma9C+9QIyUDCylezqptnT+Jdvs+JMCHk8nKK2A0bz1w0a8zzO7M1RLHfBLQ6o1SQAdV/Pmon8uQ9vLHc86l5r7WSTMEcjAqY3lGE9mdxsSZWNmp InnovEnergy

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
// }
// }
}

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -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
}

View File

@ -0,0 +1,8 @@
using InnovEnergy.Lib.Units.Composite;
namespace InnovEnergy.App.SodiStoreMax.Devices;
public class AcPowerDevice
{
public required AcPower Power { get; init; }
}

View File

@ -0,0 +1,8 @@
using InnovEnergy.Lib.Units.Power;
namespace InnovEnergy.App.SodiStoreMax.Devices;
public class DcPowerDevice
{
public required DcPower Power { get; init; }
}

View File

@ -0,0 +1,8 @@
namespace InnovEnergy.App.SodiStoreMax.Devices;
public enum DeviceState
{
Disabled,
Measured,
Computed
}

View File

@ -0,0 +1,8 @@
using InnovEnergy.Lib.Utils.Net;
namespace InnovEnergy.App.SodiStoreMax.Devices;
public class SalimaxDevice : Ip4Address
{
public required DeviceState DeviceState { get; init; }
}

View File

@ -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;
}
}

View File

@ -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
};
}
}

View File

@ -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";

View File

@ -0,0 +1,12 @@
namespace InnovEnergy.App.SodiStoreMax.Ess;
public enum EssMode
{
Off,
OffGrid,
HeatBatteries,
CalibrationCharge,
ReachMinSoc,
NoGridMeter,
OptimizeSelfConsumption
}

View File

@ -0,0 +1,8 @@
namespace InnovEnergy.App.SodiStoreMax.Ess;
public enum SalimaxAlarmState
{
Green,
Orange,
Red
}

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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}");
}
}

View File

@ -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;
// }
}
}

View File

@ -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}";
}
}

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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
);
}
}

View File

@ -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;
}
}

View File

@ -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" };
}

View File

@ -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; }
}

View File

@ -0,0 +1,8 @@
namespace InnovEnergy.App.SodiStoreMax.SystemConfig;
public enum CalibrationChargeType
{
RepetitivelyEvery,
AdditionallyOnce,
ChargePermanently
}

View File

@ -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;
}
}
}

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -0,0 +1,7 @@
namespace InnovEnergy.App.SodiStoreMax.SystemConfig;
public class DevicesConfig
{
public required AcDcConfig AcDc { get; init; }
public required DcDcConfig DcDc { get; init; }
}

View File

@ -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};
}
}

View File

@ -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.

View File

@ -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

View File

@ -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:])

View File

@ -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

View File

@ -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" }
};
}

View File

@ -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;
}*/
}

View File

@ -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;
// }
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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),
};
}
}

View File

@ -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()

View File

@ -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()

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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;
}
}
}