Merge branch 'main' of https://git.innov.energy/Innovenergy/git_trunk
This commit is contained in:
commit
30da19dadc
|
@ -7,39 +7,26 @@ namespace InnovEnergy.App.BmsTunnel;
|
||||||
|
|
||||||
using Nodes = IReadOnlyList<Byte>;
|
using Nodes = IReadOnlyList<Byte>;
|
||||||
|
|
||||||
public readonly struct BatteryConnection
|
public static class BatteryTty
|
||||||
{
|
{
|
||||||
public String Tty { get; }
|
const String DevDir = "/dev";
|
||||||
public Nodes Nodes { get; }
|
|
||||||
|
|
||||||
private BatteryConnection(String tty, Nodes nodes)
|
|
||||||
{
|
|
||||||
Tty = tty;
|
|
||||||
Nodes = nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private BatteryConnection(String tty, params Byte[] nodes) : this(tty, (Nodes) nodes)
|
public static async Task<String?> GetTty()
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static async Task<BatteryConnection?> Connect(SshHost? host = null)
|
|
||||||
{
|
{
|
||||||
Console.WriteLine("searching battery connection...");
|
Console.WriteLine("searching battery connection...");
|
||||||
|
|
||||||
return await GetConnectionFromDBus(host)
|
return await GetTtyFromDBus()
|
||||||
?? await GetConnectionFromDevTty(host);
|
?? await GetTtyFromDev();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<BatteryConnection?> GetConnectionFromDevTty(SshHost? host)
|
private static async Task<String?> GetTtyFromDev()
|
||||||
{
|
{
|
||||||
var fs = FileSystem.OnHost(host);
|
var ttys = await FileSystem
|
||||||
|
.Local
|
||||||
|
.GetFiles(DevDir, FileType.CharacterDevice);
|
||||||
|
|
||||||
var ttys = await fs.GetFiles("/dev", FileType.CharacterDevice);
|
|
||||||
|
|
||||||
var candidateTtys = ttys
|
var candidateTtys = ttys
|
||||||
.Where(f => f.Contains("ttyUSB"))
|
.Where(f => f.StartsWith(DevDir + "/ttyUSB"))
|
||||||
.Select(t => t.Split("/").LastOrDefault())
|
|
||||||
.NotNull()
|
.NotNull()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
@ -49,17 +36,15 @@ public readonly struct BatteryConnection
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var userSelectedTty = "Select TTY:".ChooseFrom(candidateTtys);
|
if (candidateTtys.Count == 1)
|
||||||
|
return candidateTtys[0];
|
||||||
|
|
||||||
return userSelectedTty != null
|
return "Select TTY:".ChooseFrom(candidateTtys);
|
||||||
? new BatteryConnection(userSelectedTty)
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<BatteryConnection?> GetConnectionFromDBus(SshHost? host)
|
private static async Task<String?> GetTtyFromDBus()
|
||||||
{
|
{
|
||||||
var tty = await LsDBus
|
var tty = await LsDBus
|
||||||
.OnHost(host)
|
|
||||||
.ExecuteBufferedAsync()
|
.ExecuteBufferedAsync()
|
||||||
.Select(ParseBatteryTty);
|
.Select(ParseBatteryTty);
|
||||||
|
|
||||||
|
@ -67,17 +52,15 @@ public readonly struct BatteryConnection
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
Console.WriteLine("found battery on DBus");
|
Console.WriteLine("found battery on DBus");
|
||||||
|
|
||||||
var availableNodes = await GetNodes(tty, host);
|
return $"{DevDir}/{tty}";
|
||||||
return new BatteryConnection(tty, availableNodes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CommandTask<Nodes> GetNodes(String tty, SshHost? host = null)
|
private static CommandTask<Nodes> GetNodes(String tty)
|
||||||
{
|
{
|
||||||
const String defaultArgs = "--system --print-reply --type=method_call / com.victronenergy.BusItem.GetValue";
|
const String defaultArgs = "--system --print-reply --type=method_call / com.victronenergy.BusItem.GetValue";
|
||||||
|
|
||||||
return DBusSend
|
return DBusSend
|
||||||
.OnHost(host)
|
|
||||||
.AppendArgument($"--dest=com.victronenergy.battery.{tty}")
|
.AppendArgument($"--dest=com.victronenergy.battery.{tty}")
|
||||||
.AppendArgument(defaultArgs)
|
.AppendArgument(defaultArgs)
|
||||||
.ExecuteBufferedAsync()
|
.ExecuteBufferedAsync()
|
|
@ -23,12 +23,12 @@ public class BmsTunnel : IDisposable
|
||||||
private const Byte TunnelCode = 0x41;
|
private const Byte TunnelCode = 0x41;
|
||||||
private const String CrcError = "?? CRC FAILED";
|
private const String CrcError = "?? CRC FAILED";
|
||||||
|
|
||||||
public BmsTunnel(String tty, Byte node, SshHost? host)
|
public BmsTunnel(String tty, Byte node)
|
||||||
{
|
{
|
||||||
Tty = tty;
|
Tty = tty;
|
||||||
Node = node;
|
Node = node;
|
||||||
|
|
||||||
StopSerialStarter(host);
|
StopSerialStarter();
|
||||||
|
|
||||||
SerialPort = new SerialPort(Tty, BaudRate, Parity, DataBits, StopBits);
|
SerialPort = new SerialPort(Tty, BaudRate, Parity, DataBits, StopBits);
|
||||||
SerialPort.ReadTimeout = 100;
|
SerialPort.ReadTimeout = 100;
|
||||||
|
@ -102,7 +102,7 @@ public class BmsTunnel : IDisposable
|
||||||
{
|
{
|
||||||
// TODO: this should go into outer loop instead of returning magic value CrcError
|
// TODO: this should go into outer loop instead of returning magic value CrcError
|
||||||
|
|
||||||
Console.WriteLine(BitConverter.ToString(response).Replace("-", " "));
|
//Console.WriteLine(BitConverter.ToString(response).Replace("-", " "));
|
||||||
return CrcError;
|
return CrcError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,11 +165,10 @@ public class BmsTunnel : IDisposable
|
||||||
.Concat(NewLine);
|
.Concat(NewLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StopSerialStarter(SshHost? host)
|
private void StopSerialStarter()
|
||||||
{
|
{
|
||||||
CliPrograms.StopTty
|
CliPrograms.StopTty
|
||||||
.WithArguments(Tty)
|
.WithArguments(Tty)
|
||||||
.OnHost(host)
|
|
||||||
.ExecuteBufferedAsync()
|
.ExecuteBufferedAsync()
|
||||||
.Task
|
.Task
|
||||||
.Wait(3000);
|
.Wait(3000);
|
||||||
|
|
|
@ -13,41 +13,19 @@ public static class Program
|
||||||
|
|
||||||
public static async Task<Int32> Main(String[] args)
|
public static async Task<Int32> Main(String[] args)
|
||||||
{
|
{
|
||||||
var hostName = args.FirstOrDefault();
|
var tty = await BatteryTty.GetTty();
|
||||||
|
|
||||||
BatteryConnection? connection = hostName is not null ? await ConnectToBms(hostName, new SshHost(hostName)): new BatteryConnection();
|
|
||||||
|
|
||||||
if (connection is null)
|
if (tty is null)
|
||||||
return 2;
|
return 2;
|
||||||
|
|
||||||
// connection.Tty;
|
|
||||||
Console.WriteLine("\nstarting BMS tunnel\n");
|
Console.WriteLine("\nstarting BMS tunnel\n");
|
||||||
|
|
||||||
var path = $"/dev/{connection.Value.Tty}";
|
using var tunnel = new BmsTunnel(tty, 2);
|
||||||
// if (hostName != null)
|
|
||||||
// path = $"root@{hostName}:" + path;
|
|
||||||
//
|
|
||||||
// var node = connection.Nodes.Any()
|
|
||||||
// ? connection.Nodes.First()
|
|
||||||
// : DefaultNode;
|
|
||||||
//
|
|
||||||
|
|
||||||
// TODO: Fixme
|
|
||||||
// var path = "";
|
|
||||||
Byte node = 2;
|
|
||||||
var nodes = new Byte[] { 1, 2, 3 };
|
|
||||||
|
|
||||||
// TODO: Fixme
|
|
||||||
|
|
||||||
using var tunnel = new BmsTunnel(path, node, hostName is not null ? new SshHost(hostName): null);
|
|
||||||
|
|
||||||
ExplainNode();
|
ExplainNode();
|
||||||
ExplainNodes();
|
|
||||||
ExplainExit();
|
ExplainExit();
|
||||||
|
|
||||||
Console.WriteLine("");
|
Console.WriteLine("");
|
||||||
|
|
||||||
ListNodes();
|
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
@ -73,14 +51,11 @@ public static class Program
|
||||||
|
|
||||||
Boolean ProcessLocalCommand(String cmd)
|
Boolean ProcessLocalCommand(String cmd)
|
||||||
{
|
{
|
||||||
cmd = cmd.TrimStart('/').Trim();
|
cmd = cmd.TrimStart('/').Trim().ToUpper();
|
||||||
|
|
||||||
if (cmd == "EXIT")
|
if (cmd == "EXIT")
|
||||||
return true;
|
return true;
|
||||||
|
if (cmd.StartsWith("NODE "))
|
||||||
if (cmd == "NODES")
|
|
||||||
ListNodes();
|
|
||||||
else if (cmd.StartsWith("NODE "))
|
|
||||||
ChangeNode(cmd);
|
ChangeNode(cmd);
|
||||||
else
|
else
|
||||||
Console.WriteLine("unrecognized command");
|
Console.WriteLine("unrecognized command");
|
||||||
|
@ -90,14 +65,6 @@ public static class Program
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
void ListNodes()
|
|
||||||
{
|
|
||||||
if(nodes.Length >= 250)
|
|
||||||
return;
|
|
||||||
|
|
||||||
nodes.Aggregate("available nodes:", (a, b) => $"{a} {b}")
|
|
||||||
.Apply(Console.WriteLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChangeNode(String cmd)
|
void ChangeNode(String cmd)
|
||||||
{
|
{
|
||||||
|
@ -109,32 +76,10 @@ public static class Program
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!nodes.Contains(newNode))
|
|
||||||
{
|
|
||||||
Console.WriteLine(newNode + " is not available");
|
|
||||||
ListNodes();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tunnel.Node = newNode;
|
tunnel.Node = newNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<BatteryConnection?> ConnectToBms(String? hostName, SshHost host)
|
|
||||||
{
|
|
||||||
|
|
||||||
if (await host.Ping())
|
|
||||||
return await BatteryConnection.Connect(host);
|
|
||||||
|
|
||||||
$"Cannot connect to {hostName}".WriteLine(ConsoleColor.Red);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static void ExplainExit() => Console.WriteLine("/exit exit bms cli");
|
private static void ExplainExit() => Console.WriteLine("/exit exit bms cli");
|
||||||
private static void ExplainNodes() => Console.WriteLine("/nodes list available nodes");
|
|
||||||
private static void ExplainNode() => Console.WriteLine("/node <nb> change to node number <nb>");
|
private static void ExplainNode() => Console.WriteLine("/node <nb> change to node number <nb>");
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,22 +0,0 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Collector", "Collector.csproj", "{52BC5E5E-C6CE-4130-AD72-E16B793CE39A}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utils", "..\Utils\Utils.csproj", "{11568A17-A10B-4A38-849F-92D9A4E2AA35}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{52BC5E5E-C6CE-4130-AD72-E16B793CE39A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{52BC5E5E-C6CE-4130-AD72-E16B793CE39A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{52BC5E5E-C6CE-4130-AD72-E16B793CE39A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{52BC5E5E-C6CE-4130-AD72-E16B793CE39A}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{11568A17-A10B-4A38-849F-92D9A4E2AA35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{11568A17-A10B-4A38-849F-92D9A4E2AA35}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{11568A17-A10B-4A38-849F-92D9A4E2AA35}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{11568A17-A10B-4A38-849F-92D9A4E2AA35}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
|
@ -4,6 +4,8 @@ using InnovEnergy.Lib.Victron.VeDBus;
|
||||||
|
|
||||||
namespace InnovEnergy.App.EmuMeterDriver;
|
namespace InnovEnergy.App.EmuMeterDriver;
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: Does not compile
|
||||||
public record Signal(Func<EmuMeterStatus, Object> Source, ObjectPath Path, String Format = "")
|
public record Signal(Func<EmuMeterStatus, Object> Source, ObjectPath Path, String Format = "")
|
||||||
{
|
{
|
||||||
public VeProperty ToVeProperty(EmuMeterStatus status)
|
public VeProperty ToVeProperty(EmuMeterStatus status)
|
||||||
|
@ -11,5 +13,4 @@ public record Signal(Func<EmuMeterStatus, Object> Source, ObjectPath Path, Strin
|
||||||
var value = Source(status);
|
var value = Source(status);
|
||||||
return new VeProperty(Path, value, String.Format($"{{0:{Format}}}", value));
|
return new VeProperty(Path, value, String.Format($"{{0:{Format}}}", value));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,16 +0,0 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenVpnCertificatesServer", "OpenVpnCertificatesServer.csproj", "{4C99C9B0-49F4-4DB7-8BC5-DA954EA68B5A}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{4C99C9B0-49F4-4DB7-8BC5-DA954EA68B5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{4C99C9B0-49F4-4DB7-8BC5-DA954EA68B5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{4C99C9B0-49F4-4DB7-8BC5-DA954EA68B5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{4C99C9B0-49F4-4DB7-8BC5-DA954EA68B5A}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
|
@ -0,0 +1,503 @@
|
||||||
|
<?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.22-->
|
||||||
|
<key attr.name="Beschreibung" 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" xml:space="preserve"/>
|
||||||
|
<node id="n0">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="697.1605173345667" y="405.28381777035247"/>
|
||||||
|
<y:Fill color="#00FF00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="82.78057861328125" x="-16.390289306640625" xml:space="preserve" y="-3.344114303588867">2
|
||||||
|
Aus (mit Netz)<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n1">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="879.0" y="0.0"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="19.567977905273438" xml:space="preserve" y="4.827942848205566">3<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n2">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="975.0" y="0.0"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="19.567977905273438" xml:space="preserve" y="4.827942848205566">4<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n3">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="879.0" y="76.0"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="19.567977905273438" xml:space="preserve" y="4.827942848205566">5<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n4">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="614.5284925005086" y="549.810518948541"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="19.567977905273438" xml:space="preserve" y="4.827942848205566">6<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n5">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="975.0" y="76.0"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="19.567977905273438" xml:space="preserve" y="4.827942848205566">7<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n6">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="879.0" y="152.0"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="19.567977905273438" xml:space="preserve" y="4.827942848205566">8<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n7">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="975.0" y="152.0"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="19.567977905273438" xml:space="preserve" y="4.827942848205566">9<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n8">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="698.4862736432007" y="238.80780973365088"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="19.567977905273438" xml:space="preserve" y="4.827942848205538">4<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n9">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="879.0" y="228.0"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="16.135955810546875" xml:space="preserve" y="4.827942848205566">11<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n10">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="618.1665026762843" y="92.98341783213155"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="16.135955810546875" xml:space="preserve" y="4.827942848205566">12<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n11">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="975.0" y="228.0"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="16.135955810546875" xml:space="preserve" y="4.827942848205566">13<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n12">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="879.0" y="304.0"/>
|
||||||
|
<y:Fill color="#FF6600" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="16.135955810546875" xml:space="preserve" y="4.827942848205566">14<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n13">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="975.0" y="304.0"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="16.135955810546875" xml:space="preserve" y="4.827942848205566">15<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n14">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="77.36328125" x="451.22588153454063" y="1.701171875"/>
|
||||||
|
<y:Fill color="#00FF00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="71.71649169921875" x="2.823394775390625" xml:space="preserve" y="-3.344114303588867">16
|
||||||
|
Netzparallel<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n15">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="879.0" y="380.0"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="16.135955810546875" xml:space="preserve" y="4.827942848205566">17<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n16">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="141.0" y="753.0"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="16.135955810546875" xml:space="preserve" y="4.827942848205566">18<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n17">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="879.0" y="456.0"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="16.135955810546875" xml:space="preserve" y="4.827942848205566">19<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n18">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="975.0" y="456.0"/>
|
||||||
|
<y:Fill color="#FF0000" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="16.135955810546875" xml:space="preserve" y="4.827942848205566">20<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n19">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="76.0390625" x="280.93624648347327" y="637.3193361295894"/>
|
||||||
|
<y:Fill color="#00FF00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="71.7764892578125" x="2.13128662109375" xml:space="preserve" y="-3.344114303588867">21
|
||||||
|
Inselbetrieb<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n20">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="471.73559161098973" y="635.4037799985899"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="16.135955810546875" xml:space="preserve" y="4.827942848205566">22<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n21">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="879.0" y="532.0"/>
|
||||||
|
<y:Fill color="#FF0000" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="16.135955810546875" xml:space="preserve" y="4.827942848205566">23<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n22">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="975.0" y="532.0"/>
|
||||||
|
<y:Fill color="#FF0000" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="16.135955810546875" xml:space="preserve" y="4.827942848205566">24<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n23">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="879.0" y="608.0"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="19.567977905273438" xml:space="preserve" y="4.827942848205566">1<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n24">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="61.54263199779115" y="234.33214836021364"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="10.864044189453125" x="19.567977905273438" xml:space="preserve" y="4.827942848205566">9<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n25">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="143.9037216673347" y="89.65087870145217"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="16.135955810546875" xml:space="preserve" y="4.827942848205566">13<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n26">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="286.5360014850319" y="3.7902262718789075"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="16.135955810546875" xml:space="preserve" y="4.827942848205566">15<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n27">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="141.1216446126329" y="546.4839942696246"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.72808837890625" x="16.135955810546875" xml:space="preserve" y="4.827942848205566">17<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n28">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="30.0" width="50.0" x="60.52879310560331" y="400.81034817125965"/>
|
||||||
|
<y:Fill color="#00FF00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="92.99264526367188" x="-21.496322631835938" xml:space="preserve" y="-3.344114303588867">1
|
||||||
|
Aus (ohne Netz)<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<edge id="e0" source="n19" target="n20">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.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="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="69.12448120117188" x="15.824865898022153" xml:space="preserve" y="-35.036341151196666">K1 schliesst<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="24.31996952808636" distanceToCenter="true" position="left" ratio="0.30376384873003637" segment="-1"/></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="e1" source="n20" target="n4">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.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="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="118.74481201171875" x="25.646208281298186" xml:space="preserve" y="-26.157313448346713">Uebergang nach Off<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" 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="e2" source="n0" target="n8">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.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="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="168.83316040039062" x="-53.87220169279908" xml:space="preserve" y="-78.41005687635527">Uebergang nach Netzparallel<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" 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="e3" source="n4" target="n0">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.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="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.44438171386719" x="39.57490318266241" xml:space="preserve" y="-67.43543566734769">K3 öffnet<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" 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="e4" source="n8" target="n10">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.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="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="69.12448120117188" x="-32.21047768505309" xml:space="preserve" y="-68.08425892066106">K2 schliesst<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" 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="e5" source="n10" target="n14">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.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="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="69.12448120117188" x="-51.89401572866859" xml:space="preserve" y="-55.41309939486179">K3 schliesst<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" 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="e6" source="n14" target="n26">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.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="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.44438171386719" x="-95.1423949145032" xml:space="preserve" y="45.84260385990581">K1 öffnet<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="55.22126027397235" distanceToCenter="true" position="left" ratio="0.593486463912793" segment="-1"/></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="n26" target="n25">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.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="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.44438171386719" x="-47.35869677784825" xml:space="preserve" y="60.68031400250993">K2 öffnet<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="50.575238644556514" distanceToCenter="true" position="left" ratio="0.5311621546375971" segment="-1"/></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="e8" source="n25" target="n24">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.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="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.44438171386719" x="-18.410514844562385" xml:space="preserve" y="54.234484019062165">K3 öffnet<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="39.955348289985295" distanceToCenter="true" position="left" 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="e9" source="n27" target="n19">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.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="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="69.12448120117188" x="37.19841552839728" xml:space="preserve" y="-28.671723625047207">K3 schliesst<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="52.56639077144928" distanceToCenter="true" position="left" ratio="0.4376756907248058" segment="-1"/></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="e10" source="n24" target="n28">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.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="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="118.74481201171875" x="-89.78853607177737" xml:space="preserve" y="58.06703779362971">Uebergang nach Off<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" 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="e11" source="n28" target="n27">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.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="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="161.5131072998047" x="-83.04388667680918" xml:space="preserve" y="47.66478081669459">Uebergan nach Inselbetrieb<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" 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="e12" source="n27" target="n16">
|
||||||
|
<data key="d9"/>
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.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="69.9644775390625" x="-65.03422989514043" xml:space="preserve" y="78.08593119395539">K1 Schliesst<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" 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="e13" source="n16" target="n0">
|
||||||
|
<data key="d9"/>
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
|
||||||
|
<y:Point x="754.5" y="768.0"/>
|
||||||
|
<y:Point x="762.5" y="415.0"/>
|
||||||
|
</y:Path>
|
||||||
|
<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="117.4608154296875" x="254.88479614257812" xml:space="preserve" y="19.827942848205566">Turnoff The inverter<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" 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>
|
||||||
|
</graph>
|
||||||
|
<data key="d7">
|
||||||
|
<y:Resources/>
|
||||||
|
</data>
|
||||||
|
</graphml>
|
Binary file not shown.
|
@ -6,24 +6,24 @@
|
||||||
<Import Project="../InnovEnergy.App.props" />
|
<Import Project="../InnovEnergy.App.props" />
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="../../Lib/Devices/Adam6060/Adam6060.csproj" />
|
<PackageReference Version="3.6.0" Include="CliWrap" />
|
||||||
<ProjectReference Include="../../Lib/Devices/AMPT/Ampt.csproj" />
|
<PackageReference Version="3.2.4" Include="Flurl.Http" />
|
||||||
<ProjectReference Include="../../Lib/Devices/Battery48TL/Battery48TL.csproj" />
|
<PackageReference Version="7.0.0" Include="Microsoft.Extensions.Logging" />
|
||||||
<ProjectReference Include="../../Lib/Devices/EmuMeter/EmuMeter.csproj" />
|
<PackageReference Version="7.0.0" Include="System.IO.Ports" />
|
||||||
<ProjectReference Include="../../Lib/Devices/Trumpf/TruConvertAc/TruConvertAc.csproj" />
|
<PackageReference Version="13.0.3" Include="Newtonsoft.Json" />
|
||||||
<ProjectReference Include="../../Lib/Devices/Trumpf/TruConvertDc/TruConvertDc.csproj" />
|
|
||||||
<ProjectReference Include="../../Lib/Devices/Trumpf/TruConvert/TruConvert.csproj" />
|
|
||||||
<ProjectReference Include="../../Lib/StatusApi/StatusApi.csproj" />
|
|
||||||
<ProjectReference Include="../../Lib/Utils/Utils.csproj" />
|
|
||||||
<ProjectReference Include="../../Lib/Time/Time.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="CliWrap" Version="3.6.0" />
|
|
||||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
|
||||||
<PackageReference Include="System.IO.Ports" Version="7.0.0" />
|
|
||||||
<PackageReference Include="DecimalMath.DecimalEx" Version="1.0.2" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../../Lib/Devices/Adam6360D/Adam6360D.csproj" />
|
||||||
|
<ProjectReference Include="../../Lib/Devices/AMPT/Ampt.csproj" />
|
||||||
|
<ProjectReference Include="../../Lib/Devices/Battery48TL/Battery48TL.csproj" />
|
||||||
|
<ProjectReference Include="../../Lib/Devices/EmuMeter/EmuMeter.csproj" />
|
||||||
|
<ProjectReference Include="../../Lib/Devices/Trumpf/SystemControl/SystemControl.csproj" />
|
||||||
|
<ProjectReference Include="../../Lib/Devices/Trumpf/TruConvertAc/TruConvertAc.csproj" />
|
||||||
|
<ProjectReference Include="../../Lib/Devices/Trumpf/TruConvertDc/TruConvertDc.csproj" />
|
||||||
|
<ProjectReference Include="../../Lib/Time/Time.csproj" />
|
||||||
|
<ProjectReference Include="../../Lib/Units/Units.csproj" />
|
||||||
|
<ProjectReference Include="../../Lib/Utils/Utils.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
|
@ -0,0 +1,33 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
dotnet_version='net6.0'
|
||||||
|
salimax_ip= '10.2.3.104'
|
||||||
|
username='ie-entwicklung@'
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo -e "\n============================ Build ============================\n"
|
||||||
|
|
||||||
|
dotnet publish \
|
||||||
|
./SaliMax.csproj \
|
||||||
|
-c Release \
|
||||||
|
-r linux-x64
|
||||||
|
|
||||||
|
echo -e "\n============================ Deploy ============================\n"
|
||||||
|
|
||||||
|
rsync -v \
|
||||||
|
./bin/Release/$dotnet_version/linux-x64/publish/* \
|
||||||
|
ie-entwicklung@10.2.3.104:~/salimax
|
||||||
|
|
||||||
|
echo -e "\n============================ Restart Salimax sevice ============================\n"
|
||||||
|
|
||||||
|
ssh -tt \
|
||||||
|
ie-entwicklung@10.2.3.104 \
|
||||||
|
sudo systemctl restart salimax.service
|
||||||
|
|
||||||
|
|
||||||
|
echo -e "\n============================ Print service output ============================\n"
|
||||||
|
|
||||||
|
ssh -tt \
|
||||||
|
ie-entwicklung@10.2.3.104 \
|
||||||
|
journalctl -f -u salimax.service
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
|
|
||||||
dotnet_version='net6.0'
|
dotnet_version='net6.0'
|
||||||
|
salimax_ip= '10.2.3.115'
|
||||||
|
username='ie-entwicklung@'
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
@ -16,22 +18,17 @@ echo -e "\n============================ Deploy ============================\n"
|
||||||
|
|
||||||
rsync -v \
|
rsync -v \
|
||||||
./bin/Release/$dotnet_version/linux-x64/publish/* \
|
./bin/Release/$dotnet_version/linux-x64/publish/* \
|
||||||
ie-entwicklung@10.2.3.49:~/salimax
|
ie-entwicklung@10.2.3.115:~/salimax
|
||||||
|
|
||||||
|
|
||||||
# debian@10.2.1.87:~/salimax
|
|
||||||
|
|
||||||
echo -e "\n============================ Restart Salimax sevice ============================\n"
|
echo -e "\n============================ Restart Salimax sevice ============================\n"
|
||||||
|
|
||||||
ssh -tt \
|
ssh -tt \
|
||||||
ie-entwicklung@10.2.3.49 \
|
ie-entwicklung@10.2.3.115 \
|
||||||
sudo systemctl restart salimax.service
|
sudo systemctl restart salimax.service
|
||||||
|
|
||||||
|
|
||||||
echo -e "\n============================ Print service output ============================\n"
|
echo -e "\n============================ Print service output ============================\n"
|
||||||
|
|
||||||
ssh -tt \
|
ssh -tt \
|
||||||
ie-entwicklung@10.2.3.49 \
|
ie-entwicklung@10.2.3.115 \
|
||||||
journalctl -f -u salimax.service
|
journalctl -f -u salimax.service
|
||||||
|
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
using InnovEnergy.Lib.Units;
|
|
||||||
using InnovEnergy.Lib.Utils;
|
|
||||||
|
|
||||||
namespace InnovEnergy.App.SaliMax;
|
|
||||||
|
|
||||||
public static class AsciiArt
|
|
||||||
{
|
|
||||||
|
|
||||||
public static String CreateBox(params Object[] elements)
|
|
||||||
{
|
|
||||||
var aligned = elements
|
|
||||||
.Select(e => e.ToString()!)
|
|
||||||
.JoinLines()
|
|
||||||
.AlignLeft();
|
|
||||||
|
|
||||||
var w = aligned.Width();
|
|
||||||
|
|
||||||
var line = "".PadRight(w + 2, '─');
|
|
||||||
var top = "┌" + line + "┐";
|
|
||||||
var bottom = "└" + line + "┘";
|
|
||||||
|
|
||||||
return aligned
|
|
||||||
.SplitLines()
|
|
||||||
.Select(l => l.SurroundWith(" "))
|
|
||||||
.Select(l => l.SurroundWith("│"))
|
|
||||||
.Prepend(top)
|
|
||||||
.Append(bottom)
|
|
||||||
.JoinLines();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String CreateHorizontalArrow(Decimal value, String separator)
|
|
||||||
{
|
|
||||||
var valueToString = " " + value.W();
|
|
||||||
|
|
||||||
if (value == 0)
|
|
||||||
{
|
|
||||||
valueToString = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
var contentWidth = separator.Length;
|
|
||||||
|
|
||||||
var horizontal = "".PadRight(contentWidth, ' ');
|
|
||||||
|
|
||||||
var v = valueToString.PadRight(contentWidth);
|
|
||||||
var s = separator.PadRight(contentWidth);
|
|
||||||
|
|
||||||
return StringUtils.JoinLines(
|
|
||||||
horizontal,
|
|
||||||
v,
|
|
||||||
s,
|
|
||||||
horizontal
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String CreateTransitionPadLeft(String value, String separator)
|
|
||||||
{
|
|
||||||
var contentWidth = separator.Length + 2;
|
|
||||||
|
|
||||||
var horizontal = "".PadLeft(contentWidth, ' ');
|
|
||||||
|
|
||||||
var v = value.PadLeft(contentWidth);
|
|
||||||
var s = separator.PadLeft(contentWidth);
|
|
||||||
|
|
||||||
return StringUtils.JoinLines(
|
|
||||||
horizontal,
|
|
||||||
v,
|
|
||||||
s,
|
|
||||||
horizontal
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String CreateVerticalArrow(Decimal power, Int32 width = 0)
|
|
||||||
{
|
|
||||||
var flow = "V".NewLine() + "V".NewLine() + power.W().NewLine() + "V".NewLine() + "V";
|
|
||||||
|
|
||||||
return flow.AlignCenterHorizontal(width);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
using InnovEnergy.Lib.Devices.Battery48TL;
|
|
||||||
using InnovEnergy.Lib.StatusApi;
|
|
||||||
using InnovEnergy.Lib.Units;
|
|
||||||
using InnovEnergy.Lib.Units.Composite;
|
|
||||||
using static InnovEnergy.Lib.Devices.Battery48TL.TemperatureState;
|
|
||||||
|
|
||||||
namespace InnovEnergy.App.SaliMax.Controller;
|
|
||||||
|
|
||||||
public static class AvgBatteriesStatus
|
|
||||||
{
|
|
||||||
public static CombinedStatus<Battery48TLStatus>? Combine(this IReadOnlyList<Battery48TLStatus> stati)
|
|
||||||
{
|
|
||||||
var combined = stati.Count == 0
|
|
||||||
? null
|
|
||||||
: new Battery48TLStatus
|
|
||||||
{
|
|
||||||
Soc = stati.Min(b => b.Soc),
|
|
||||||
Temperature = stati.Average(b => b.Temperature),
|
|
||||||
Dc = new DcBus
|
|
||||||
{
|
|
||||||
Voltage = stati.Average(b => b.Dc.Voltage),
|
|
||||||
Current = stati.Sum(b => b.Dc.Current),
|
|
||||||
},
|
|
||||||
|
|
||||||
Alarms = stati.SelectMany(b => b.Alarms).Distinct().ToList(),
|
|
||||||
Warnings = stati.SelectMany(b => b.Warnings).Distinct().ToList(),
|
|
||||||
|
|
||||||
MaxChargingPower = stati.Sum(b => b.MaxChargingPower),
|
|
||||||
MaxDischargingPower = stati.Sum(b => b.MaxDischargingPower),
|
|
||||||
|
|
||||||
Heating = stati.Any(b => b.Heating),
|
|
||||||
|
|
||||||
AmberLed = LedState.Off, // not used for combined battery
|
|
||||||
BlueLed = LedState.Off,
|
|
||||||
RedLed = LedState.Off,
|
|
||||||
GreenLed = LedState.Off,
|
|
||||||
|
|
||||||
CellsVoltage = stati.Average(b => b.CellsVoltage),
|
|
||||||
ConnectedToDc = stati.Any(b => b.ConnectedToDc),
|
|
||||||
|
|
||||||
TemperatureState = stati.Any(b => b.TemperatureState == OperatingTemperature) // TODO: revisit when we have the overheated state
|
|
||||||
? OperatingTemperature
|
|
||||||
: Cold,
|
|
||||||
|
|
||||||
TotalCurrent = stati.Average(b => b.TotalCurrent),
|
|
||||||
|
|
||||||
EocReached = stati.All(b => b.EocReached),
|
|
||||||
};
|
|
||||||
|
|
||||||
return new CombinedStatus<Battery48TLStatus>
|
|
||||||
{
|
|
||||||
Combined = combined!,
|
|
||||||
Children = stati
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
namespace InnovEnergy.App.SaliMax.Controller;
|
|
||||||
|
|
||||||
public static class Control
|
|
||||||
{
|
|
||||||
|
|
||||||
public static Decimal ControlGridPower(this StatusRecord status, Decimal targetPower)
|
|
||||||
{
|
|
||||||
return ControlPower(status.GridMeterStatus!.Ac.ActivePower, targetPower, status.SalimaxConfig!.PConstant);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Decimal ControlInverterPower(this StatusRecord status, Decimal targetInverterPower)
|
|
||||||
{
|
|
||||||
var s = status.InverterStatus!;
|
|
||||||
var totalInverterAcPower = s.Ac.ActivePower;
|
|
||||||
|
|
||||||
return ControlPower(totalInverterAcPower, targetInverterPower,status.SalimaxConfig!.PConstant);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Decimal ControlBatteryPower(this StatusRecord status, Decimal targetBatteryPower, UInt16 i = 0) //this will use the avg batteries
|
|
||||||
{
|
|
||||||
return ControlPower(status.BatteriesStatus!.Combined.Dc.Power, targetBatteryPower, status.SalimaxConfig!.PConstant);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Decimal ControlLowBatterySoc(this StatusRecord status)
|
|
||||||
{
|
|
||||||
return ControlBatteryPower(status, HoldMinSocCurve(status));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Decimal LowerLimit(params Decimal[] deltas) => deltas.Max();
|
|
||||||
public static Decimal UpperLimit(params Decimal[] deltas) => deltas.Min();
|
|
||||||
|
|
||||||
private static Decimal HoldMinSocCurve(StatusRecord s)
|
|
||||||
{
|
|
||||||
// TODO: explain LowSOC curve
|
|
||||||
|
|
||||||
var a = -2 * s.SalimaxConfig!.SelfDischargePower / s.SalimaxConfig.HoldSocZone;
|
|
||||||
var b = -a * (s.SalimaxConfig.MinSoc + s.SalimaxConfig.HoldSocZone);
|
|
||||||
|
|
||||||
return s.BatteriesStatus!.Combined.Soc * a + b; //this will use the avg batteries
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Decimal ControlPower(Decimal measurement, Decimal target, Decimal p)
|
|
||||||
{
|
|
||||||
var error = target - measurement;
|
|
||||||
return error * p;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
|
||||||
using InnovEnergy.App.SaliMax.SystemConfig;
|
|
||||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
|
||||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
|
||||||
|
|
||||||
namespace InnovEnergy.App.SaliMax.Controller;
|
|
||||||
|
|
||||||
public class ControlRecord
|
|
||||||
{
|
|
||||||
public TruConvertAcControl? AcControlRecord { get; init; }
|
|
||||||
public TruConvertDcControl? DcControlRecord { get; init; }
|
|
||||||
public SalimaxConfig? SalimaxConfig { get; init; } // we may have to create record of each
|
|
||||||
public SaliMaxRelayStatus? SalimaxRelays { get; init; } // we may have to create record of each
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
namespace InnovEnergy.App.SaliMax.Controller;
|
|
||||||
|
|
||||||
public enum ControlTarget // TODO to delete
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
GridAc = 1,
|
|
||||||
BatteryDc = 2,
|
|
||||||
InverterAc = 3,
|
|
||||||
InverterDc = 4,
|
|
||||||
}
|
|
|
@ -1,514 +0,0 @@
|
||||||
using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
|
||||||
using InnovEnergy.App.SaliMax.SystemConfig;
|
|
||||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvert;
|
|
||||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
|
||||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
|
||||||
using InnovEnergy.Lib.Time.Unix;
|
|
||||||
using InnovEnergy.Lib.Utils;
|
|
||||||
|
|
||||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc.Enums;
|
|
||||||
|
|
||||||
using static InnovEnergy.App.SaliMax.SaliMaxRelays.RelayState;
|
|
||||||
|
|
||||||
|
|
||||||
namespace InnovEnergy.App.SaliMax.Controller;
|
|
||||||
|
|
||||||
public static class Controller
|
|
||||||
{
|
|
||||||
private static readonly UnixTimeSpan MaxTimeWithoutEoc = UnixTimeSpan.FromDays(7);
|
|
||||||
|
|
||||||
private static Boolean _mustChargeFlag = false;
|
|
||||||
|
|
||||||
private static readonly TimeSpan CommunicationTimeout = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
public static readonly Int16 MaxmimumAllowedBatteryTemp = 315;
|
|
||||||
|
|
||||||
private static UInt16 _numberOfInverters;
|
|
||||||
|
|
||||||
private static UInt16 GetSaliMaxState(StatusRecord statusRecord)
|
|
||||||
{
|
|
||||||
if (statusRecord.SaliMaxRelayStatus is null)
|
|
||||||
throw new ArgumentNullException(nameof(SaliMaxRelayStatus) + " is not available"); //TODO
|
|
||||||
|
|
||||||
if (statusRecord.InverterStatus is null)
|
|
||||||
throw new ArgumentNullException(nameof(statusRecord.InverterStatus) + " is not available"); //TODO
|
|
||||||
|
|
||||||
return statusRecord switch
|
|
||||||
{
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Open,
|
|
||||||
InverterStatus.MainState: not MainState.Operation
|
|
||||||
} => 1,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Closed,
|
|
||||||
InverterStatus.MainState: not MainState.Operation
|
|
||||||
} => 2,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Open,
|
|
||||||
InverterStatus.MainState: not MainState.Operation
|
|
||||||
} => 3,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Closed,
|
|
||||||
InverterStatus.MainState: not MainState.Operation
|
|
||||||
} => 4,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Open,
|
|
||||||
InverterStatus.MainState: not MainState.Operation
|
|
||||||
} => 5,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Closed,
|
|
||||||
InverterStatus.MainState: not MainState.Operation
|
|
||||||
} => 6,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Open,
|
|
||||||
InverterStatus.MainState: not MainState.Operation
|
|
||||||
} => 7,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Closed,
|
|
||||||
InverterStatus.MainState: not MainState.Operation
|
|
||||||
} => 8,
|
|
||||||
|
|
||||||
//Grid-Tied 400V/50 Hz
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Open,
|
|
||||||
InverterStatus.GridType: AcDcGridType.GridTied400V50Hz
|
|
||||||
} => 9,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Open,
|
|
||||||
InverterStatus.GridType: AcDcGridType.GridTied400V50Hz
|
|
||||||
} => 10,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Open,
|
|
||||||
InverterStatus.GridType: AcDcGridType.GridTied400V50Hz
|
|
||||||
} => 11,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Open,
|
|
||||||
InverterStatus.GridType: AcDcGridType.GridTied400V50Hz
|
|
||||||
} => 12,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Closed,
|
|
||||||
InverterStatus.GridType: AcDcGridType.GridTied400V50Hz
|
|
||||||
} => 13,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Closed,
|
|
||||||
InverterStatus.GridType: AcDcGridType.GridTied400V50Hz
|
|
||||||
} => 14,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Closed,
|
|
||||||
InverterStatus.GridType: AcDcGridType.GridTied400V50Hz
|
|
||||||
} => 15,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Closed,
|
|
||||||
InverterStatus.GridType: AcDcGridType.GridTied400V50Hz
|
|
||||||
} => 16,
|
|
||||||
|
|
||||||
//Island 400V / 50Hz
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Open,
|
|
||||||
InverterStatus.GridType: AcDcGridType.Island400V50Hz
|
|
||||||
} => 17,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Open,
|
|
||||||
InverterStatus.GridType: AcDcGridType.Island400V50Hz
|
|
||||||
} => 18,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Open,
|
|
||||||
InverterStatus.GridType: AcDcGridType.Island400V50Hz
|
|
||||||
} => 19,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Open,
|
|
||||||
InverterStatus.GridType: AcDcGridType.Island400V50Hz
|
|
||||||
} => 20,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Closed, //this is wrong
|
|
||||||
InverterStatus.GridType: AcDcGridType.Island400V50Hz
|
|
||||||
} => 21,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Open, SaliMaxRelayStatus.K3: Closed,
|
|
||||||
InverterStatus.GridType: AcDcGridType.Island400V50Hz
|
|
||||||
} => 22,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Open, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Closed,
|
|
||||||
InverterStatus.GridType: AcDcGridType.Island400V50Hz
|
|
||||||
} => 23,
|
|
||||||
{
|
|
||||||
SaliMaxRelayStatus.K1: Closed, SaliMaxRelayStatus.K2: Closed, SaliMaxRelayStatus.K3: Closed,
|
|
||||||
InverterStatus.GridType: AcDcGridType.Island400V50Hz
|
|
||||||
} => 24,
|
|
||||||
|
|
||||||
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(statusRecord), statusRecord, null)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ControlRecord SaliMaxControl(StatusRecord statusRecord)
|
|
||||||
{
|
|
||||||
var currentSaliMaxState = GetSaliMaxState(statusRecord);
|
|
||||||
|
|
||||||
UInt16 acSlaveId = 1;
|
|
||||||
var resetInverterAlarm = CheckInverterAlarms(statusRecord, currentSaliMaxState).WriteLine(" reset Alarm");
|
|
||||||
var resetDcAlarm = CheckDcDcAlarms(statusRecord);
|
|
||||||
|
|
||||||
var lastEocTime = GetLastEocTime(statusRecord);
|
|
||||||
var timeSinceLastEoc = UnixTime.Now - lastEocTime;
|
|
||||||
|
|
||||||
_numberOfInverters = (UInt16)statusRecord.InverterStatus!.NumberOfConnectedSlaves ;
|
|
||||||
_mustChargeFlag = timeSinceLastEoc > MaxTimeWithoutEoc;
|
|
||||||
|
|
||||||
var noGridMeter = statusRecord.GridMeterStatus == null;
|
|
||||||
var saliMaxConfig = statusRecord.SalimaxConfig with { LastEoc = lastEocTime };
|
|
||||||
|
|
||||||
ExplainState(currentSaliMaxState);
|
|
||||||
|
|
||||||
const RelayState k2Relay = Closed;
|
|
||||||
|
|
||||||
var acPowerStageEnable = StateConfig.AcPowerStageEnableStates.Contains(currentSaliMaxState); //this is logical incorrect, find better way
|
|
||||||
|
|
||||||
var dcPowerStageEnable = statusRecord.BatteriesStatus is not null; // TODO this is to check, Can be the batteries Status be null?
|
|
||||||
|
|
||||||
var salimaxRelay = statusRecord.SaliMaxRelayStatus! with { K2 = k2Relay }; // to check // this is must be control
|
|
||||||
|
|
||||||
if (resetInverterAlarm)
|
|
||||||
{
|
|
||||||
acPowerStageEnable = !resetInverterAlarm ;
|
|
||||||
acSlaveId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resetDcAlarm)
|
|
||||||
{
|
|
||||||
dcPowerStageEnable = !resetDcAlarm ;
|
|
||||||
}
|
|
||||||
|
|
||||||
acSlaveId.WriteLine(" AcSlave @");
|
|
||||||
|
|
||||||
if (statusRecord.BatteriesStatus == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine(" No batteries");
|
|
||||||
return new ControlRecord
|
|
||||||
{
|
|
||||||
AcControlRecord = Defaults.TruConvertAcControl with
|
|
||||||
{
|
|
||||||
SignedPowerNominalValue = 0,
|
|
||||||
PowerStageEnable = acPowerStageEnable,
|
|
||||||
CommunicationTimeout = CommunicationTimeout,
|
|
||||||
SlaveAddress = acSlaveId,
|
|
||||||
ResetsAlarmAndWarning = resetInverterAlarm
|
|
||||||
},
|
|
||||||
DcControlRecord = Defaults.TruConvertDcControl with
|
|
||||||
{
|
|
||||||
PowerStageEnable = dcPowerStageEnable,
|
|
||||||
ResetsAlarmAndWarning = resetDcAlarm,
|
|
||||||
TimeoutForCommunication = CommunicationTimeout
|
|
||||||
|
|
||||||
},
|
|
||||||
SalimaxConfig = saliMaxConfig, // must create a control of each
|
|
||||||
SalimaxRelays = salimaxRelay, // must create a control of each
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (noGridMeter)
|
|
||||||
{
|
|
||||||
// Blackout ( no grid meter and K1 opened automatically
|
|
||||||
if (statusRecord.SaliMaxRelayStatus?.K1 == Open)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Blackout occured");
|
|
||||||
//FromGridTieToIsland();
|
|
||||||
}
|
|
||||||
// Grid meter not detected ( broken)
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine("Grid meter not detected");
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var newPowerSetPoint = CalculateNewPowerSetPoint(statusRecord);
|
|
||||||
|
|
||||||
////////////////////////// Control Record //////////////////////////
|
|
||||||
|
|
||||||
var acControlRecord = Defaults.TruConvertAcControl with
|
|
||||||
{
|
|
||||||
PowerStageEnable = acPowerStageEnable,
|
|
||||||
CommunicationTimeout = CommunicationTimeout,
|
|
||||||
SignedPowerNominalValue = newPowerSetPoint,
|
|
||||||
SlaveAddress = acSlaveId,
|
|
||||||
ResetsAlarmAndWarning = resetInverterAlarm
|
|
||||||
};
|
|
||||||
|
|
||||||
var dcControlRecord = Defaults.TruConvertDcControl with
|
|
||||||
{
|
|
||||||
PowerStageEnable = dcPowerStageEnable,
|
|
||||||
ResetsAlarmAndWarning = resetDcAlarm,
|
|
||||||
TimeoutForCommunication = CommunicationTimeout
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return new ControlRecord
|
|
||||||
{
|
|
||||||
AcControlRecord = acControlRecord,
|
|
||||||
DcControlRecord = dcControlRecord,
|
|
||||||
SalimaxConfig = saliMaxConfig,
|
|
||||||
SalimaxRelays = salimaxRelay
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Decimal CalculateNewPowerSetPoint(StatusRecord statusRecord)
|
|
||||||
{
|
|
||||||
var currentPowerSetPoint = statusRecord.InverterStatus!.AcSignedPowerValue;
|
|
||||||
|
|
||||||
var limitReason = "no limit";
|
|
||||||
var goal = "no goal";
|
|
||||||
var delta = 0m;
|
|
||||||
|
|
||||||
if (_mustChargeFlag)
|
|
||||||
{
|
|
||||||
goal = "Calibration Charge";
|
|
||||||
delta = statusRecord.ControlInverterPower(statusRecord.SalimaxConfig.MaxInverterPower);
|
|
||||||
}
|
|
||||||
else if (statusRecord.BatteriesStatus!.Combined.Soc < statusRecord.SalimaxConfig.MinSoc) // TODO
|
|
||||||
{
|
|
||||||
goal = $"reach min SOC (Min soc: {statusRecord.SalimaxConfig.MinSoc})";
|
|
||||||
delta = statusRecord.ControlInverterPower(statusRecord.SalimaxConfig
|
|
||||||
.MaxInverterPower); // this the new mustChargeToMinSoc
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
goal = $"optimize self consumption (Grid set point: {statusRecord.SalimaxConfig.GridSetPoint})";
|
|
||||||
delta = statusRecord.ControlGridPower(statusRecord.SalimaxConfig.GridSetPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////// Upper Limits //////////////////////////
|
|
||||||
|
|
||||||
var inverterAc2DcLimitPower = statusRecord.ControlInverterPower(statusRecord.SalimaxConfig.MaxInverterPower);
|
|
||||||
|
|
||||||
if (delta > inverterAc2DcLimitPower)
|
|
||||||
{
|
|
||||||
limitReason = "limited by max inverter Ac to Dc power";
|
|
||||||
delta = inverterAc2DcLimitPower;
|
|
||||||
}
|
|
||||||
|
|
||||||
var batteryChargingLimitPower = statusRecord.ControlBatteryPower(statusRecord.BatteriesStatus!.Combined.MaxChargingPower);
|
|
||||||
|
|
||||||
if (delta > batteryChargingLimitPower)
|
|
||||||
{
|
|
||||||
limitReason = "limited by max battery charging power";
|
|
||||||
delta = batteryChargingLimitPower;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////// Lower Limits //////////////////////////
|
|
||||||
|
|
||||||
var inverterDc2AcLimitPower = statusRecord.ControlInverterPower(-statusRecord.SalimaxConfig.MaxInverterPower);
|
|
||||||
|
|
||||||
if (delta < inverterDc2AcLimitPower)
|
|
||||||
{
|
|
||||||
limitReason = $"limited by max inverter Dc to Ac power: {-statusRecord.SalimaxConfig.MaxInverterPower}W";
|
|
||||||
delta = inverterDc2AcLimitPower;
|
|
||||||
}
|
|
||||||
|
|
||||||
var batteryDischargingLimitPower =
|
|
||||||
statusRecord.ControlBatteryPower(statusRecord.BatteriesStatus.Combined.MaxDischargingPower); // TODO change to avg battery
|
|
||||||
|
|
||||||
if (delta < batteryDischargingLimitPower)
|
|
||||||
{
|
|
||||||
limitReason =
|
|
||||||
$"limited by max battery discharging power: {statusRecord.BatteriesStatus.Combined.MaxDischargingPower}";// TODO change to avg battery
|
|
||||||
|
|
||||||
delta = batteryDischargingLimitPower;
|
|
||||||
}
|
|
||||||
|
|
||||||
var keepMinSocLimitDelta = statusRecord.ControlLowBatterySoc();
|
|
||||||
if (delta < keepMinSocLimitDelta)
|
|
||||||
{
|
|
||||||
limitReason =
|
|
||||||
$"limiting discharging power in order to stay above min SOC: {statusRecord.SalimaxConfig.MinSoc}%";
|
|
||||||
delta = keepMinSocLimitDelta;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (statusRecord.BatteriesStatus[0]!.Temperature >= 300) //must not reduce the delta
|
|
||||||
// {
|
|
||||||
// var softLandingFactor = (MaxmimumAllowedBatteryTemp - statusRecord.BatteriesStatus[0]!.Temperature) / 15; //starting softlanding from 300 degree
|
|
||||||
// limitReason =
|
|
||||||
// $"limiting discharging power in order to stay keep the battery temp below 315°: {statusRecord.BatteriesStatus[0]!.Temperature}°" + " Softlanding factor: " + softLandingFactor;
|
|
||||||
// delta *= softLandingFactor;
|
|
||||||
// }
|
|
||||||
|
|
||||||
var newPowerSetPoint =
|
|
||||||
DistributePower(currentPowerSetPoint + delta, statusRecord.SalimaxConfig.MaxInverterPower);
|
|
||||||
|
|
||||||
////////////////////// Print Data for Debug purpose //////////////////////////
|
|
||||||
|
|
||||||
//
|
|
||||||
goal.WriteLine();
|
|
||||||
limitReason.WriteLine(" Limit reason");
|
|
||||||
delta.WriteLine(" Delta");
|
|
||||||
// "============".WriteLine();
|
|
||||||
return newPowerSetPoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static State TargetState(State currentState)
|
|
||||||
{
|
|
||||||
return currentState switch
|
|
||||||
{
|
|
||||||
State.State1 => State.State17,
|
|
||||||
State.State17 => State.State21,
|
|
||||||
State.State21 => State.State22,
|
|
||||||
State.State22 => State.State6,
|
|
||||||
State.State6 => State.State2,
|
|
||||||
State.State2 => State.State4,
|
|
||||||
State.State4 => State.State12,
|
|
||||||
State.State12 => State.State16,
|
|
||||||
State.State16 => State.State15,
|
|
||||||
State.State15 => State.State13,
|
|
||||||
State.State13 => State.State9,
|
|
||||||
State.State9 => State.State1,
|
|
||||||
_ => throw new Exception("Unknown State!") // maybe not throwing an exception, instead write on the log file
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static void ExplainState(UInt16 s)
|
|
||||||
{
|
|
||||||
Console.WriteLine("State: " + s);
|
|
||||||
switch (s)
|
|
||||||
{
|
|
||||||
case 1:
|
|
||||||
Console.WriteLine(" Inverter is Off");
|
|
||||||
Console.WriteLine(" Turning on power stage of inverter");
|
|
||||||
Console.WriteLine(" grid type = island 400V / 50Hz");
|
|
||||||
break;
|
|
||||||
case 17:
|
|
||||||
Console.WriteLine(" Waiting for K3 to close");
|
|
||||||
break;
|
|
||||||
case 21:
|
|
||||||
Console.WriteLine(" Inverter is in Island Mode");
|
|
||||||
Console.WriteLine(" Waiting for K1 to close to leave it");
|
|
||||||
break;
|
|
||||||
case 22:
|
|
||||||
Console.WriteLine(" K1 is closed");
|
|
||||||
Console.WriteLine(" Turning off power stage of inverter");
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
Console.WriteLine(" Waiting for K3 to open");
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
Console.WriteLine(" K3 is open");
|
|
||||||
Console.WriteLine(" Closing the K2");
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
Console.WriteLine(" K2 is Closed");
|
|
||||||
Console.WriteLine(" Turning on power stage of inverter");
|
|
||||||
Console.WriteLine(" grid type = grid-tied 400V / 50Hz");
|
|
||||||
break;
|
|
||||||
case 12:
|
|
||||||
Console.WriteLine(" Waiting for K3 to close");
|
|
||||||
break;
|
|
||||||
case 16:
|
|
||||||
Console.WriteLine(" Inverter is in grid-tie");
|
|
||||||
Console.WriteLine(" Waiting for K1 to open to leave it");
|
|
||||||
break;
|
|
||||||
case 15:
|
|
||||||
Console.WriteLine(" K1 is open");
|
|
||||||
Console.WriteLine(" Opening the K2");
|
|
||||||
break;
|
|
||||||
case 13:
|
|
||||||
Console.WriteLine(" K2 is open");
|
|
||||||
Console.WriteLine(" Waiting for K3 to open");
|
|
||||||
break;
|
|
||||||
case 9:
|
|
||||||
Console.WriteLine(" K3 is open");
|
|
||||||
Console.WriteLine(" Turning off power stage of inverter");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Console.WriteLine("Unknown State!");
|
|
||||||
File.AppendAllTextAsync(Config.LogSalimaxLog, String.Join(Environment.NewLine, UnixTime.Now + "Unknown State!"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static void WriteControlRecord(ControlRecord controlRecord,
|
|
||||||
TruConvertAcDevice acDevice,
|
|
||||||
TruConvertDcDevice dcDevice,
|
|
||||||
SaliMaxRelaysDevice saliMaxRelaysDevice)
|
|
||||||
{
|
|
||||||
controlRecord.SalimaxConfig?.Save();
|
|
||||||
|
|
||||||
var acControlRecord = controlRecord.AcControlRecord;
|
|
||||||
var dcControlRecord = controlRecord.DcControlRecord;
|
|
||||||
|
|
||||||
if (acControlRecord != null && dcControlRecord != null)
|
|
||||||
{
|
|
||||||
acDevice.WriteControl(acControlRecord);
|
|
||||||
|
|
||||||
dcDevice.WriteControl(dcControlRecord);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine("AcControl and DcControl record is empty");
|
|
||||||
File.AppendAllTextAsync(Config.LogSalimaxLog, String.Join(Environment.NewLine, UnixTime.Now + "AcControl and DcControl record is empty!"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static UnixTime GetLastEocTime(StatusRecord statusRecord)
|
|
||||||
{ if (statusRecord.BatteriesStatus != null)
|
|
||||||
{
|
|
||||||
if (statusRecord.BatteriesStatus!.Combined.EocReached)
|
|
||||||
{
|
|
||||||
Console.WriteLine("battery has reached EOC");
|
|
||||||
File.AppendAllTextAsync(Config.LogSalimaxLog,
|
|
||||||
String.Join(Environment.NewLine,
|
|
||||||
UnixTime.Now + "battery has reached EOC"));
|
|
||||||
return UnixTime.Now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine("No battery Detected");
|
|
||||||
}
|
|
||||||
return statusRecord.SalimaxConfig.LastEoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Decimal DistributePower(Decimal powerSetPoint, Int32 maximumPowerSetPoint)
|
|
||||||
{
|
|
||||||
var inverterPowerSetPoint = powerSetPoint / _numberOfInverters;
|
|
||||||
return inverterPowerSetPoint.Clamp(-maximumPowerSetPoint, maximumPowerSetPoint);
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean CheckDcDcAlarms(StatusRecord s)
|
|
||||||
{
|
|
||||||
s.DcDcStatus?.Alarms.Count.WriteLine(" Dc Alarm count");
|
|
||||||
if ( s.DcDcStatus?.Alarms.Count > 0 &&
|
|
||||||
s.DcDcStatus?.PowerOperation == false)
|
|
||||||
{
|
|
||||||
File.AppendAllTextAsync(Config.LogSalimaxLog, UnixTime.Now + " " + s.DcDcStatus.Alarms);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean CheckInverterAlarms(StatusRecord s, UInt16 state)
|
|
||||||
{
|
|
||||||
s.InverterStatus?.Alarms.Count.WriteLine(" Ac Alarm count");
|
|
||||||
if ( s.InverterStatus?.Alarms.Count > 0 )
|
|
||||||
{
|
|
||||||
File.AppendAllTextAsync(Config.LogSalimaxLog, UnixTime.Now + " " + s.InverterStatus.Alarms[0]); // Todo write every alarm in he alarm list
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean FromGridTieToIsland(StatusRecord s) //this is must be called when the K1 open
|
|
||||||
{
|
|
||||||
//check again the K1 is open
|
|
||||||
//s.sal = true;
|
|
||||||
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: Include number of connected batteries
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
namespace InnovEnergy.App.SaliMax.Controller;
|
|
||||||
|
|
||||||
public struct SaliMaxState
|
|
||||||
{
|
|
||||||
private Int32 State { get; }
|
|
||||||
|
|
||||||
public SaliMaxState(Int32 state)
|
|
||||||
{
|
|
||||||
if (state < 1 || state >24)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(state));
|
|
||||||
|
|
||||||
State = state;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
|
|
||||||
namespace InnovEnergy.App.SaliMax.Controller;
|
|
||||||
|
|
||||||
public enum State : Int16
|
|
||||||
{
|
|
||||||
State1 = 1,
|
|
||||||
State2 = 2,
|
|
||||||
State3 = 3,
|
|
||||||
State4 = 4,
|
|
||||||
State5 = 5,
|
|
||||||
State6 = 6,
|
|
||||||
State7 = 7,
|
|
||||||
State8 = 8,
|
|
||||||
State9 = 9,
|
|
||||||
State10 = 10,
|
|
||||||
State11 = 11,
|
|
||||||
State12 = 12,
|
|
||||||
State13 = 13,
|
|
||||||
State14 = 14,
|
|
||||||
State15 = 15,
|
|
||||||
State16 = 16,
|
|
||||||
State17 = 17,
|
|
||||||
State18 = 18,
|
|
||||||
State19 = 19,
|
|
||||||
State20 = 20,
|
|
||||||
State21 = 21,
|
|
||||||
State22 = 22,
|
|
||||||
State23 = 23,
|
|
||||||
State24 = 24
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
|
|
||||||
namespace InnovEnergy.App.SaliMax.Controller;
|
|
||||||
|
|
||||||
public static class StateConfig
|
|
||||||
{
|
|
||||||
public static readonly IReadOnlyList<Int32> AcPowerStageEnableStates = new[] {8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24 };
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
|
||||||
using InnovEnergy.App.SaliMax.SystemConfig;
|
|
||||||
using InnovEnergy.Lib.Devices.AMPT;
|
|
||||||
using InnovEnergy.Lib.Devices.Battery48TL;
|
|
||||||
using InnovEnergy.Lib.Devices.EmuMeter;
|
|
||||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
|
||||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
|
||||||
using InnovEnergy.Lib.StatusApi;
|
|
||||||
|
|
||||||
namespace InnovEnergy.App.SaliMax.Controller;
|
|
||||||
|
|
||||||
public record StatusRecord
|
|
||||||
{
|
|
||||||
public TruConvertAcStatus? InverterStatus { get; init; }
|
|
||||||
public TruConvertDcStatus? DcDcStatus { get; init; }
|
|
||||||
public CombinedStatus<Battery48TLStatus>? BatteriesStatus { get; init; }
|
|
||||||
|
|
||||||
public EmuMeterStatus? GridMeterStatus { get; init; }
|
|
||||||
public SaliMaxRelayStatus? SaliMaxRelayStatus { get; init; }
|
|
||||||
public AmptCommunicationUnitStatus? AmptStatus { get; init; }
|
|
||||||
public EmuMeterStatus? AcInToAcOutMeterStatus { get; init; }
|
|
||||||
public SalimaxConfig SalimaxConfig { get; init; } = null!;
|
|
||||||
}
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SaliMax;
|
||||||
|
|
||||||
|
public class DeviceConfig
|
||||||
|
{
|
||||||
|
public String RelaysIp { get; set; }
|
||||||
|
public String TruConvertAcIp { get; set; }
|
||||||
|
public String TruConvertDcIp { get; set; }
|
||||||
|
public String GridMeterIp { get; set; }
|
||||||
|
public String InternalMeter { get; set; }
|
||||||
|
public String AmptIp { get; set; }
|
||||||
|
public byte[] BatteryNodes { get; set; }
|
||||||
|
public String BatteryTty { get; set; }
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public DeviceConfig(string relaysIp, string truConvertAcIp, string truConvertDcIp, string gridMeterIp, string internalMeter, string amptIp, byte[] batteryNodes, string batteryTty)
|
||||||
|
{
|
||||||
|
RelaysIp = relaysIp;
|
||||||
|
TruConvertAcIp = truConvertAcIp;
|
||||||
|
TruConvertDcIp = truConvertDcIp;
|
||||||
|
GridMeterIp = gridMeterIp;
|
||||||
|
InternalMeter = internalMeter;
|
||||||
|
AmptIp = amptIp;
|
||||||
|
BatteryNodes = batteryNodes;
|
||||||
|
BatteryTty = batteryTty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DeviceConfig? LoadDeviceConfig(String configFilePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(configFilePath);
|
||||||
|
return JsonConvert.DeserializeObject<DeviceConfig>(json);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
// using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||||
|
// using InnovEnergy.App.SaliMax.SystemConfig;
|
||||||
|
//
|
||||||
|
// namespace InnovEnergy.App.SaliMax.Controller;
|
||||||
|
//
|
||||||
|
// public class ControlRecord
|
||||||
|
// {
|
||||||
|
// public TruConvertAcControl? AcControlRecord { get; init; }
|
||||||
|
// public TruConvertDcControl? DcControlRecord { get; init; }
|
||||||
|
// public Config? SalimaxConfig { get; init; } // we may have to create record of each
|
||||||
|
// public SaliMaxRelayControl? SalimaxRelays { get; init; } // we may have to create record of each
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
|
@ -0,0 +1,260 @@
|
||||||
|
using InnovEnergy.Lib.Devices.Battery48TL.DataTypes;
|
||||||
|
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
||||||
|
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc.DataTypes;
|
||||||
|
using InnovEnergy.Lib.Time.Unix;
|
||||||
|
using InnovEnergy.Lib.Utils;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SaliMax.Ess;
|
||||||
|
|
||||||
|
public static class Controller
|
||||||
|
{
|
||||||
|
private static readonly UnixTimeSpan MaxTimeWithoutEoc = UnixTimeSpan.FromDays(7); // TODO: move to config
|
||||||
|
|
||||||
|
public static EssMode SelectControlMode(this StatusRecord s)
|
||||||
|
{
|
||||||
|
//return EssMode.OptimizeSelfConsumption;
|
||||||
|
|
||||||
|
return s.StateMachine.State != 16 ? EssMode.Off
|
||||||
|
: s.MustHeatBatteries() ? EssMode.HeatBatteries
|
||||||
|
: s.MustDoCalibrationCharge() ? EssMode.CalibrationCharge
|
||||||
|
: s.MustReachMinSoc() ? EssMode.ReachMinSoc
|
||||||
|
: s.GridMeter is null ? EssMode.NoGridMeter
|
||||||
|
: EssMode.OptimizeSelfConsumption;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static EssControl ControlEss(this StatusRecord s)
|
||||||
|
{
|
||||||
|
var mode = s.SelectControlMode();
|
||||||
|
|
||||||
|
mode.WriteLine();
|
||||||
|
|
||||||
|
if (mode is EssMode.Off or EssMode.NoGridMeter)
|
||||||
|
return new EssControl(mode, EssLimit.NoLimit, PowerCorrection: 0, PowerSetpoint: 0);
|
||||||
|
|
||||||
|
var essDelta = s.ComputePowerDelta(mode);
|
||||||
|
|
||||||
|
var unlimitedControl = new EssControl(mode, EssLimit.NoLimit, essDelta, 0);
|
||||||
|
|
||||||
|
var limitedControl = unlimitedControl
|
||||||
|
.LimitChargePower(s)
|
||||||
|
.LimitDischargePower(s)
|
||||||
|
.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).WriteLine("Max");
|
||||||
|
var minPower = acDcs.Min(d => d.Status.Ac.Power.Active.Value).WriteLine("Min");
|
||||||
|
|
||||||
|
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.ReferenceDcBusVoltage
|
||||||
|
? 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();
|
||||||
|
|
||||||
|
return control
|
||||||
|
//.LimitChargePower(, EssLimit.ChargeLimitedByInverterPower)
|
||||||
|
.LimitChargePower(maxBatteryChargePower, EssLimit.ChargeLimitedByBatteryPower);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EssControl LimitDischargePower(this EssControl control, StatusRecord s)
|
||||||
|
{
|
||||||
|
//var maxInverterDischargeDelta = s.ControlInverterPower(-s.Config.MaxInverterPower);
|
||||||
|
var maxBatteryDischargeDelta = s.Battery.Devices.Sum(b => b.MaxDischargePower);
|
||||||
|
var keepMinSocLimitDelta = s.ControlBatteryPower(s.HoldMinSocPower());
|
||||||
|
|
||||||
|
return control
|
||||||
|
// .LimitDischargePower(maxInverterDischargeDelta, EssLimit.DischargeLimitedByInverterPower)
|
||||||
|
.LimitDischargePower(maxBatteryDischargeDelta , EssLimit.DischargeLimitedByBatteryPower)
|
||||||
|
.LimitDischargePower(keepMinSocLimitDelta , EssLimit.DischargeLimitedByMinSoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static Double ComputePowerDelta(this StatusRecord s, EssMode mode)
|
||||||
|
{
|
||||||
|
var chargePower = s.AcDc.Devices.Sum(d => d.Status.Nominal.Power.Value);
|
||||||
|
|
||||||
|
return mode switch
|
||||||
|
{
|
||||||
|
EssMode.HeatBatteries => s.ControlInverterPower(chargePower),
|
||||||
|
EssMode.ReachMinSoc => s.ControlInverterPower(chargePower),
|
||||||
|
EssMode.CalibrationCharge => s.ControlInverterPower(chargePower),
|
||||||
|
EssMode.OptimizeSelfConsumption => s.ControlGridPower(s.Config.GridSetPoint),
|
||||||
|
_ => throw new ArgumentException(null, nameof(mode))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Boolean HasPreChargeAlarm(this StatusRecord statusRecord)
|
||||||
|
{
|
||||||
|
return statusRecord.DcDc.Alarms.Contains(Lib.Devices.Trumpf.TruConvertDc.Status.AlarmMessage.DcDcPrecharge);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean MustHeatBatteries(this StatusRecord s)
|
||||||
|
{
|
||||||
|
var batteries = s.Battery.Devices;
|
||||||
|
|
||||||
|
if (batteries.Count <= 0)
|
||||||
|
return true; // batteries might be there but BMS is without power
|
||||||
|
|
||||||
|
return batteries
|
||||||
|
.Select(b => b.Temperatures.State)
|
||||||
|
.Contains(TemperatureState.Cold);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Double MaxBatteryChargePower(this StatusRecord s)
|
||||||
|
{
|
||||||
|
return s.Battery.Devices.Sum(b => b.MaxChargePower);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Double CurrentPowerSetPoint(this StatusRecord s)
|
||||||
|
{
|
||||||
|
return s
|
||||||
|
.AcDc
|
||||||
|
.Devices
|
||||||
|
.Select(d =>
|
||||||
|
{
|
||||||
|
var acPowerControl = d.Control.Ac.Power;
|
||||||
|
|
||||||
|
return acPowerControl.L1.Active
|
||||||
|
+ acPowerControl.L2.Active
|
||||||
|
+ acPowerControl.L3.Active;
|
||||||
|
})
|
||||||
|
.Sum(p => p);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean MustReachMinSoc(this StatusRecord s)
|
||||||
|
{
|
||||||
|
var batteries = s.Battery.Devices;
|
||||||
|
|
||||||
|
return batteries.Count > 0
|
||||||
|
&& batteries.Any(b => b.Soc < s.Config.MinSoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Boolean MustDoCalibrationCharge(this StatusRecord statusRecord)
|
||||||
|
{
|
||||||
|
var config = statusRecord.Config;
|
||||||
|
|
||||||
|
if (statusRecord.Battery.Eoc)
|
||||||
|
{
|
||||||
|
"Batteries have reached EOC".Log();
|
||||||
|
config.LastEoc = UnixTime.Now;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return UnixTime.Now - statusRecord.Config.LastEoc > MaxTimeWithoutEoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Double ControlGridPower(this StatusRecord status, Double targetPower)
|
||||||
|
{
|
||||||
|
return ControlPower
|
||||||
|
(
|
||||||
|
measurement : status.GridMeter!.Ac.Power.Active,
|
||||||
|
target : targetPower,
|
||||||
|
pConstant : status.Config.PConstant
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Double ControlInverterPower(this StatusRecord status, Double targetInverterPower)
|
||||||
|
{
|
||||||
|
return ControlPower
|
||||||
|
(
|
||||||
|
measurement : status.AcDc.Ac.Power.Active,
|
||||||
|
target : targetInverterPower,
|
||||||
|
pConstant : status.Config.PConstant
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Double ControlBatteryPower(this StatusRecord status, Double targetBatteryPower)
|
||||||
|
{
|
||||||
|
return ControlPower
|
||||||
|
(
|
||||||
|
measurement: status.Battery.Devices.Sum(b => b.Dc.Power),
|
||||||
|
target: targetBatteryPower,
|
||||||
|
pConstant: status.Config.PConstant
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Double HoldMinSocPower(this StatusRecord s)
|
||||||
|
{
|
||||||
|
// TODO: explain LowSOC curve
|
||||||
|
|
||||||
|
var batteries = s.Battery.Devices;
|
||||||
|
|
||||||
|
if (batteries.Count == 0)
|
||||||
|
return Double.NegativeInfinity;
|
||||||
|
|
||||||
|
var a = -2 * s.Config.BatterySelfDischargePower * batteries.Count / s.Config.HoldSocZone;
|
||||||
|
var b = -a * (s.Config.MinSoc + s.Config.HoldSocZone);
|
||||||
|
|
||||||
|
return batteries.Min(d => d.Soc.Value) * a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Double ControlPower(Double measurement, Double target, Double pConstant)
|
||||||
|
{
|
||||||
|
var error = target - measurement;
|
||||||
|
return error * pConstant;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Double ControlPowerWithIntegral(Double measurement, Double target, Double p, Double i)
|
||||||
|
{
|
||||||
|
var errorSum = 0; // this is must be sum of error
|
||||||
|
var error = target - measurement;
|
||||||
|
var kp = p * error;
|
||||||
|
var ki = i * errorSum;
|
||||||
|
return ki + kp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<InverterState> InverterStates(this AcDcDevicesRecord acDcStatus)
|
||||||
|
{
|
||||||
|
return acDcStatus
|
||||||
|
.Devices
|
||||||
|
.Select(d => d.Status.InverterState.Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
using InnovEnergy.Lib.Units.Power;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SaliMax.Ess;
|
||||||
|
|
||||||
|
public record EssControl
|
||||||
|
(
|
||||||
|
EssMode Mode,
|
||||||
|
EssLimit LimitedBy,
|
||||||
|
ActivePower PowerCorrection,
|
||||||
|
ActivePower PowerSetpoint
|
||||||
|
)
|
||||||
|
{
|
||||||
|
public EssControl LimitChargePower(Double controlDelta, EssLimit reason)
|
||||||
|
{
|
||||||
|
var overload = PowerCorrection - controlDelta;
|
||||||
|
|
||||||
|
if (overload <= 0)
|
||||||
|
return this;
|
||||||
|
|
||||||
|
return this with
|
||||||
|
{
|
||||||
|
LimitedBy = reason,
|
||||||
|
PowerCorrection = controlDelta,
|
||||||
|
PowerSetpoint = PowerSetpoint - overload
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public EssControl LimitDischargePower(Double controlDelta, EssLimit reason)
|
||||||
|
{
|
||||||
|
var overload = PowerCorrection - controlDelta;
|
||||||
|
|
||||||
|
if (overload >= 0)
|
||||||
|
return this;
|
||||||
|
|
||||||
|
return this with
|
||||||
|
{
|
||||||
|
LimitedBy = reason,
|
||||||
|
PowerCorrection = controlDelta,
|
||||||
|
PowerSetpoint = PowerSetpoint - overload
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
namespace InnovEnergy.App.SaliMax.Ess;
|
||||||
|
|
||||||
|
public enum EssLimit
|
||||||
|
{
|
||||||
|
NoLimit,
|
||||||
|
DischargeLimitedByMinSoc,
|
||||||
|
DischargeLimitedByBatteryPower,
|
||||||
|
DischargeLimitedByInverterPower,
|
||||||
|
ChargeLimitedByInverterPower,
|
||||||
|
ChargeLimitedByBatteryPower,
|
||||||
|
ChargeLimitedByMaxDcBusVoltage,
|
||||||
|
DischargeLimitedByMinDcBusVoltage,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// limitedBy = $"limiting discharging power in order to stay above min SOC: {s.Config.MinSoc}%";
|
||||||
|
// limitedBy = $"limited by max battery discharging power: {maxDischargePower}";
|
||||||
|
// limitedBy = $"limited by max inverter Dc to Ac power: {-s.Config.MaxInverterPower}W";
|
||||||
|
// limitedBy = $"limited by max battery charging power: {maxChargePower}";
|
||||||
|
// limitedBy = "limited by max inverter Ac to Dc power";
|
|
@ -0,0 +1,11 @@
|
||||||
|
namespace InnovEnergy.App.SaliMax.Ess;
|
||||||
|
|
||||||
|
public enum EssMode
|
||||||
|
{
|
||||||
|
Off,
|
||||||
|
HeatBatteries,
|
||||||
|
CalibrationCharge,
|
||||||
|
ReachMinSoc,
|
||||||
|
NoGridMeter,
|
||||||
|
OptimizeSelfConsumption
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||||
|
using InnovEnergy.App.SaliMax.System;
|
||||||
|
using InnovEnergy.App.SaliMax.SystemConfig;
|
||||||
|
using InnovEnergy.App.SaliMax.VirtualDevices;
|
||||||
|
using InnovEnergy.Lib.Devices.AMPT;
|
||||||
|
using InnovEnergy.Lib.Devices.Battery48TL;
|
||||||
|
using InnovEnergy.Lib.Devices.EmuMeter;
|
||||||
|
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
||||||
|
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SaliMax.Ess;
|
||||||
|
|
||||||
|
public record StatusRecord
|
||||||
|
{
|
||||||
|
public AcDcDevicesRecord AcDc { get; init; } = null!;
|
||||||
|
public DcDcDevicesRecord DcDc { get; init; } = null!;
|
||||||
|
public Battery48TlRecords Battery { get; init; } = null!;
|
||||||
|
public EmuMeterRegisters? GridMeter { get; init; }
|
||||||
|
public EmuMeterRegisters? LoadOnAcIsland { get; init; }
|
||||||
|
public AcPowerDevice? LoadOnAcGrid { get; init; } = null!;
|
||||||
|
public AcPowerDevice? PvOnAcGrid { get; init; } = null!;
|
||||||
|
public AcPowerDevice? PvOnAcIsland { get; init; } = null!;
|
||||||
|
public AcPowerDevice? AcGridToAcIsland { get; init; } = null!;
|
||||||
|
public DcPowerDevice? LoadOnDc { get; init; } = null!;
|
||||||
|
public RelaysRecord? Relays { get; init; }
|
||||||
|
public AmptStatus PvOnDc { get; init; } = null!;
|
||||||
|
public Config Config { get; init; } = null!;
|
||||||
|
public EssControl EssControl { get; set; } = null!;
|
||||||
|
public StateMachine StateMachine { get; } = new StateMachine();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using InnovEnergy.Lib.Units;
|
||||||
|
using InnovEnergy.Lib.Utils;
|
||||||
|
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SaliMax;
|
||||||
|
|
||||||
|
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 = "^";
|
||||||
|
|
||||||
|
public static TextBlock Horizontal(Unit amount, Int32 width = 10)
|
||||||
|
{
|
||||||
|
var label = amount.ToStringRounded();
|
||||||
|
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.CenterHorizontal(label, arrow, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
|
||||||
|
[SuppressMessage("ReSharper", "CoVariantArrayConversion")]
|
||||||
|
public static TextBlock Vertical(Unit amount, Int32 height = 4)
|
||||||
|
{
|
||||||
|
var label = amount.ToStringRounded();
|
||||||
|
var arrowChar = amount.Value < 0 ? UpArrowChar : DownArrowChar;
|
||||||
|
var halfArrow = Enumerable.Repeat(arrowChar, height/2);
|
||||||
|
|
||||||
|
var lines = halfArrow
|
||||||
|
.Append(label)
|
||||||
|
.Concat(halfArrow)
|
||||||
|
.ToArray(height / 2 * 2 + 1);
|
||||||
|
|
||||||
|
return TextBlock.CenterHorizontal(lines);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
using InnovEnergy.Lib.Time.Unix;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SaliMax;
|
||||||
|
|
||||||
|
public class CustomLogger : ILogger
|
||||||
|
{
|
||||||
|
private readonly String _logFilePath;
|
||||||
|
private readonly Int64 _maxFileSizeBytes;
|
||||||
|
private readonly Int32 _maxLogFileCount;
|
||||||
|
private Int64 _currentFileSizeBytes;
|
||||||
|
|
||||||
|
public CustomLogger(String logFilePath, Int64 maxFileSizeBytes, Int32 maxLogFileCount)
|
||||||
|
{
|
||||||
|
_logFilePath = logFilePath;
|
||||||
|
_maxFileSizeBytes = maxFileSizeBytes;
|
||||||
|
_maxLogFileCount = maxLogFileCount;
|
||||||
|
_currentFileSizeBytes = File.Exists(logFilePath) ? new FileInfo(logFilePath).Length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable BeginScope<TState>(TState state)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean IsEnabled(LogLevel logLevel)
|
||||||
|
{
|
||||||
|
return 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 file size and rotate the log file if necessary
|
||||||
|
if (_currentFileSizeBytes + logMessage.Length >= _maxFileSizeBytes)
|
||||||
|
{
|
||||||
|
RotateLogFile();
|
||||||
|
_currentFileSizeBytes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the log message to the file
|
||||||
|
File.AppendAllText(_logFilePath, logMessage + Environment.NewLine);
|
||||||
|
_currentFileSizeBytes += logMessage.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RotateLogFile()
|
||||||
|
{
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename the current log file with a timestamp
|
||||||
|
var logFileBackupPath = Path.Combine(logFileDir, $"{logFileBaseName}_{UnixTime.Now}{logFileExt}");
|
||||||
|
File.Move(_logFilePath, logFileBackupPath);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SaliMax;
|
||||||
|
|
||||||
|
public static class Logger
|
||||||
|
{
|
||||||
|
// Specify the maximum log file size in bytes (e.g., 1 MB)
|
||||||
|
|
||||||
|
private const Int32 MaxFileSizeBytes = 1024 * 1024; // TODO: move to settings
|
||||||
|
private const Int32 MaxLogFileCount = 1000; // TODO: move to settings
|
||||||
|
private const String LogFilePath = "LogDirectory/log.txt"; // TODO: move to settings
|
||||||
|
|
||||||
|
private static readonly ILogger _logger = new CustomLogger(LogFilePath, MaxFileSizeBytes, MaxLogFileCount);
|
||||||
|
|
||||||
|
public static T Log<T>(this T t) where T : notnull
|
||||||
|
{
|
||||||
|
// _logger.LogInformation(t.ToString()); // TODO: check warning
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,196 +1,410 @@
|
||||||
using System.Diagnostics;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Nodes;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using Flurl.Http;
|
using Flurl.Http;
|
||||||
using InnovEnergy.App.SaliMax.Controller;
|
using InnovEnergy.App.SaliMax.Ess;
|
||||||
using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||||
|
using InnovEnergy.App.SaliMax.System;
|
||||||
using InnovEnergy.App.SaliMax.SystemConfig;
|
using InnovEnergy.App.SaliMax.SystemConfig;
|
||||||
|
using InnovEnergy.App.SaliMax.VirtualDevices;
|
||||||
using InnovEnergy.Lib.Devices.AMPT;
|
using InnovEnergy.Lib.Devices.AMPT;
|
||||||
using InnovEnergy.Lib.Devices.Battery48TL;
|
using InnovEnergy.Lib.Devices.Battery48TL;
|
||||||
using InnovEnergy.Lib.Devices.EmuMeter;
|
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;
|
||||||
|
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc.DataTypes;
|
||||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
using InnovEnergy.Lib.Time.Unix;
|
using InnovEnergy.Lib.Time.Unix;
|
||||||
|
using InnovEnergy.Lib.Units;
|
||||||
|
using InnovEnergy.Lib.Units.Power;
|
||||||
using InnovEnergy.Lib.Utils;
|
using InnovEnergy.Lib.Utils;
|
||||||
|
using static InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes.SystemConfig;
|
||||||
|
using AcPower = InnovEnergy.Lib.Units.Composite.AcPower;
|
||||||
|
using Exception = System.Exception;
|
||||||
|
|
||||||
#pragma warning disable IL2026
|
#pragma warning disable IL2026
|
||||||
|
|
||||||
|
|
||||||
namespace InnovEnergy.App.SaliMax;
|
namespace InnovEnergy.App.SaliMax;
|
||||||
|
|
||||||
internal static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
|
[DllImport("libsystemd.so.0")]
|
||||||
|
private static extern Int32 sd_notify(Int32 unsetEnvironment, String state);
|
||||||
|
|
||||||
private const UInt32 UpdateIntervalSeconds = 2;
|
private const UInt32 UpdateIntervalSeconds = 2;
|
||||||
|
|
||||||
|
private static readonly Byte[] BatteryNodes = { 2, 3, 4, 5, 6 };
|
||||||
|
private const String BatteryTty = "/dev/ttyUSB0";
|
||||||
|
|
||||||
|
private static readonly TcpChannel RelaysChannel = new TcpChannel("10.0.1.1", 502); // "192.168.1.242";
|
||||||
|
private static readonly TcpChannel TruConvertAcChannel = new TcpChannel("10.0.2.1", 502); // "192.168.1.2";
|
||||||
|
private static readonly TcpChannel TruConvertDcChannel = new TcpChannel("10.0.3.1", 502); // "192.168.1.3";
|
||||||
|
private static readonly TcpChannel GridMeterChannel = new TcpChannel("10.0.4.1", 502); // "192.168.1.241";
|
||||||
|
private static readonly TcpChannel AcOutLoadChannel = new TcpChannel("10.0.4.2", 502); // "192.168.1.241";
|
||||||
|
private static readonly TcpChannel AmptChannel = new TcpChannel("10.0.5.1", 502); // "192.168.1.249";
|
||||||
|
|
||||||
|
//private static readonly TcpChannel TruConvertAcChannel = new TcpChannel("localhost", 5001);
|
||||||
|
//private static readonly TcpChannel TruConvertDcChannel = new TcpChannel("localhost", 5002);
|
||||||
|
//private static readonly TcpChannel GridMeterChannel = new TcpChannel("localhost", 5003);
|
||||||
|
//private static readonly TcpChannel AcOutLoadChannel = new TcpChannel("localhost", 5004);
|
||||||
|
//private static readonly TcpChannel AmptChannel = new TcpChannel("localhost", 5005);
|
||||||
|
//private static readonly TcpChannel RelaysChannel = new TcpChannel("localhost", 5006);
|
||||||
|
//private static readonly TcpChannel BatteriesChannel = new TcpChannel("localhost", 5007);
|
||||||
|
|
||||||
|
|
||||||
|
private static readonly S3Config S3Config = new S3Config
|
||||||
|
{
|
||||||
|
Bucket = "saliomameiringen",
|
||||||
|
Region = "sos-ch-dk-2",
|
||||||
|
Provider = "exo.io",
|
||||||
|
ContentType = "text/plain; charset=utf-8",
|
||||||
|
Key = "EXO2bf0cbd97fbfa75aa36ed46f",
|
||||||
|
Secret = "Bn1CDPqOG-XpDSbYjfIJxojcHTm391vZTc8z8l_fEPs"
|
||||||
|
};
|
||||||
|
|
||||||
public static async Task Main(String[] args)
|
public static async Task Main(String[] args)
|
||||||
{
|
{
|
||||||
try
|
while (true)
|
||||||
{
|
{
|
||||||
await Run();
|
try
|
||||||
}
|
{
|
||||||
catch (Exception e)
|
await Run();
|
||||||
{
|
}
|
||||||
await File.AppendAllTextAsync(Config.LogSalimaxLog,
|
catch (Exception e)
|
||||||
String.Join(Environment.NewLine, UnixTime.Now + " \n" + e));
|
{
|
||||||
throw;
|
Console.WriteLine(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task Run()
|
private static async Task Run()
|
||||||
{
|
{
|
||||||
Console.WriteLine("Starting SaliMax");
|
Console.WriteLine("Starting SaliMax");
|
||||||
|
|
||||||
var batteryNodes = new Byte[] { 2, 3 };
|
|
||||||
|
|
||||||
var batteryTty = "/dev/ttyUSB0";
|
|
||||||
|
|
||||||
var relaysIp = "10.0.1.1";
|
// Send the initial "service started" message to systemd
|
||||||
var truConvertAcIp = "10.0.2.1";
|
var sdNotifyReturn = sd_notify(0, "READY=1");
|
||||||
var truConvertDcIp = "10.0.3.1";
|
|
||||||
var gridMeterIp = "10.0.4.1";
|
|
||||||
var internalMeter = "10.0.4.2";
|
|
||||||
var amptIp = "10.0.5.1";
|
|
||||||
|
|
||||||
|
var battery48TlDevices = BatteryNodes
|
||||||
|
.Select(n => new Battery48TlDevice(BatteryTty, n))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var batteryDevices = new Battery48TlDevices(battery48TlDevices);
|
||||||
|
var acDcDevices = new TruConvertAcDcDevices(TruConvertAcChannel);
|
||||||
|
var dcDcDevices = new TruConvertDcDcDevices(TruConvertDcChannel);
|
||||||
|
var gridMeterDevice = new EmuMeterDevice(GridMeterChannel);
|
||||||
|
var acIslandLoadMeter = new EmuMeterDevice(AcOutLoadChannel);
|
||||||
|
var amptDevice = new AmptDevices(AmptChannel);
|
||||||
|
var saliMaxRelaysDevice = new RelaysDevice(RelaysChannel);
|
||||||
|
|
||||||
var s3Config = new S3Config
|
|
||||||
{
|
|
||||||
Bucket = "saliomameiringen",
|
|
||||||
Region = "sos-ch-dk-2",
|
|
||||||
Provider = "exo.io",
|
|
||||||
ContentType = "text/plain; charset=utf-8",
|
|
||||||
Key = "EXO2bf0cbd97fbfa75aa36ed46f",
|
|
||||||
Secret = "Bn1CDPqOG-XpDSbYjfIJxojcHTm391vZTc8z8l_fEPs"
|
|
||||||
};
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
var inverterDevice = new TruConvertAcDevice("127.0.0.1", 5001);
|
|
||||||
var dcDcDevice = new TruConvertDcDevice("127.0.0.1", 5002);
|
|
||||||
var gridMeterDevice = new EmuMeterDevice("127.0.0.1", 5003);
|
|
||||||
var saliMaxRelaysDevice = new SaliMaxRelaysDevice("127.0.0.1", 5004);
|
|
||||||
var amptDevice = new AmptCommunicationUnit("127.0.0.1", 5005);
|
|
||||||
var acInToAcOutMeterDevice = new EmuMeterDevice("127.0.0.1", 5003); // TODO: use real device
|
|
||||||
var secondBattery48TlDevice = Battery48TlDevice.Fake();
|
|
||||||
var firstBattery48TlDevice =Battery48TlDevice.Fake();;
|
|
||||||
var salimaxConfig = new SalimaxConfig();
|
|
||||||
#else
|
|
||||||
|
|
||||||
var batteries = batteryNodes.Select(n => new Battery48TlDevice(batteryTty, n)).ToList();
|
|
||||||
|
|
||||||
|
|
||||||
var inverterDevice = new TruConvertAcDevice(truConvertAcIp);
|
|
||||||
var dcDcDevice = new TruConvertDcDevice(truConvertDcIp);
|
|
||||||
|
|
||||||
var gridMeterDevice = new EmuMeterDevice(gridMeterIp);
|
|
||||||
var acInToAcOutMeterDevice = new EmuMeterDevice(internalMeter); // TODO: use real device
|
|
||||||
|
|
||||||
var amptDevice = new AmptCommunicationUnit(amptIp);
|
|
||||||
|
|
||||||
var saliMaxRelaysDevice = new SaliMaxRelaysDevice(relaysIp);
|
|
||||||
var salimaxConfig = new SalimaxConfig();
|
|
||||||
#endif
|
|
||||||
// This is will be always add manually ? or do we need to read devices automatically in a range of IP @
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
StatusRecord ReadStatus()
|
StatusRecord ReadStatus()
|
||||||
{
|
{
|
||||||
var combinedBatteryStatus = batteries
|
var acDc = acDcDevices.Read();
|
||||||
.Select(b => b.ReadStatus())
|
var dcDc = dcDcDevices.Read();
|
||||||
.NotNull()
|
var battery = batteryDevices.Read();
|
||||||
.ToList()
|
var relays = saliMaxRelaysDevice.Read();
|
||||||
.Combine();
|
var loadOnAcIsland = acIslandLoadMeter.Read();
|
||||||
|
var gridMeter = gridMeterDevice.Read();
|
||||||
|
var pvOnDc = amptDevice.Read();
|
||||||
|
|
||||||
// var dcDcStatusArray = dcDcDevices.Select(b => b.ReadStatus()).NotNull().ToArray();
|
var pvOnAcGrid = AcPowerDevice.Null;
|
||||||
// var inverterStatusArray = inverterDevices.Select(b => b.ReadStatus()).NotNull().ToArray();
|
var pvOnAcIsland = AcPowerDevice.Null;
|
||||||
|
var gridPower = gridMeter is null ? AcPower.Null : gridMeter.Ac.Power;
|
||||||
|
var islandLoadPower = loadOnAcIsland is null ? AcPower.Null : loadOnAcIsland.Ac.Power;
|
||||||
|
var inverterAcPower = acDc.Ac.Power;
|
||||||
|
|
||||||
|
var loadOnAcGrid = gridPower
|
||||||
|
+ pvOnAcGrid.Power
|
||||||
|
+ pvOnAcIsland.Power
|
||||||
|
- islandLoadPower
|
||||||
|
- inverterAcPower;
|
||||||
|
|
||||||
|
var gridBusToIslandBusPower = gridPower
|
||||||
|
+ pvOnAcGrid.Power
|
||||||
|
- loadOnAcGrid;
|
||||||
|
|
||||||
|
// var dcPower = acDc.Dc.Power.Value
|
||||||
|
// + pvOnDc.Dc?.Power.Value ?? 0
|
||||||
|
// - dcDc.Dc.Link.Power.Value;
|
||||||
|
|
||||||
|
var dcPower = 0;
|
||||||
|
|
||||||
|
var loadOnDc = new DcPowerDevice { Power = dcPower} ;
|
||||||
|
|
||||||
|
|
||||||
return new StatusRecord
|
return new StatusRecord
|
||||||
{
|
{
|
||||||
InverterStatus = inverterDevice.ReadStatus(),
|
AcDc = acDc ?? AcDcDevicesRecord.Null,
|
||||||
DcDcStatus = dcDcDevice.ReadStatus(),
|
DcDc = dcDc ?? DcDcDevicesRecord.Null,
|
||||||
BatteriesStatus = combinedBatteryStatus,
|
Battery = battery ?? Battery48TlRecords.Null,
|
||||||
AcInToAcOutMeterStatus = acInToAcOutMeterDevice.ReadStatus(),
|
Relays = relays,
|
||||||
GridMeterStatus = gridMeterDevice.ReadStatus(),
|
GridMeter = gridMeter,
|
||||||
SaliMaxRelayStatus = saliMaxRelaysDevice.ReadStatus(),
|
|
||||||
AmptStatus = amptDevice.ReadStatus(),
|
PvOnAcGrid = pvOnAcGrid,
|
||||||
SalimaxConfig = salimaxConfig.Load().Result,
|
PvOnAcIsland = pvOnAcIsland,
|
||||||
|
PvOnDc = pvOnDc ?? AmptStatus.Null,
|
||||||
|
|
||||||
|
AcGridToAcIsland = new AcPowerDevice { Power = gridBusToIslandBusPower },
|
||||||
|
LoadOnAcGrid = new AcPowerDevice { Power = loadOnAcGrid },
|
||||||
|
LoadOnAcIsland = loadOnAcIsland,
|
||||||
|
LoadOnDc = loadOnDc,
|
||||||
|
|
||||||
|
Config = Config.Load() // load from disk every iteration, so config can be changed while running
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WriteControl(StatusRecord r)
|
||||||
|
{
|
||||||
|
if (r.Relays is not null)
|
||||||
|
saliMaxRelaysDevice.Write(r.Relays);
|
||||||
|
|
||||||
|
acDcDevices.Write(r.AcDc);
|
||||||
|
dcDcDevices.Write(r.DcDc);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var startTime = UnixTime.Now;
|
|
||||||
const Int32 delayTime = 10;
|
|
||||||
|
|
||||||
Console.WriteLine("press ctrl-C to stop");
|
Console.WriteLine("press ctrl-C to stop");
|
||||||
|
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var t = UnixTime.Now;
|
sd_notify(0, "WATCHDOG=1");
|
||||||
while (t.Ticks % UpdateIntervalSeconds != 0)
|
|
||||||
{
|
|
||||||
await Task.Delay(delayTime);
|
|
||||||
t = UnixTime.Now;
|
|
||||||
}
|
|
||||||
|
|
||||||
var status = ReadStatus();
|
|
||||||
#if BatteriesAllowed
|
|
||||||
|
|
||||||
var jsonLog = status.ToLog(t);
|
var t = UnixTime.FromTicks(UnixTime.Now.Ticks / 2 * 2);
|
||||||
await UploadTimeSeries(s3Config, jsonLog, t);
|
|
||||||
var controlRecord = Controller.Controller.SaliMaxControl(status);
|
//t.ToUtcDateTime().WriteLine();
|
||||||
Controller.Controller.WriteControlRecord(controlRecord, inverterDevice, dcDcDevice, saliMaxRelaysDevice);
|
|
||||||
|
var record = ReadStatus();
|
||||||
|
|
||||||
|
PrintTopology(record);
|
||||||
|
|
||||||
//JsonSerializer.Serialize(jsonLog, JsonOptions).WriteLine(ConsoleColor.DarkBlue);
|
if (record.Relays is not null)
|
||||||
#endif
|
record.Relays.ToCsv().WriteLine();
|
||||||
Topology.Print(status);
|
|
||||||
|
|
||||||
|
var emuMeterRegisters = record.GridMeter;
|
||||||
|
if (emuMeterRegisters is not null)
|
||||||
|
{
|
||||||
|
emuMeterRegisters.Ac.Power.Active.WriteLine("Grid Active");
|
||||||
|
//emuMeterRegisters.Ac.Power.Reactive.WriteLine("Grid Reactive");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
record.ControlConstants();
|
||||||
|
|
||||||
while (UnixTime.Now == t)
|
record.ControlSystemState();
|
||||||
await Task.Delay(delayTime);
|
|
||||||
|
Console.WriteLine($"{record.StateMachine.State}: {record.StateMachine.Message}");
|
||||||
|
|
||||||
|
var essControl = record.ControlEss().WriteLine();
|
||||||
|
|
||||||
|
record.EssControl = essControl;
|
||||||
|
|
||||||
|
record.AcDc.SystemControl.ApplyAcDcDefaultSettings();
|
||||||
|
record.DcDc.SystemControl.ApplyDcDcDefaultSettings();
|
||||||
|
|
||||||
|
DistributePower(record, essControl);
|
||||||
|
|
||||||
|
WriteControl(record);
|
||||||
|
|
||||||
|
await UploadCsv(record, t);
|
||||||
|
|
||||||
|
record.Config.Save();
|
||||||
|
|
||||||
|
"===========================================".WriteLine();
|
||||||
}
|
}
|
||||||
// ReSharper disable once FunctionNeverReturns
|
// ReSharper disable once FunctionNeverReturns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void PrintTopology(StatusRecord s)
|
||||||
|
|
||||||
|
|
||||||
// to delete not used anymore
|
|
||||||
[Conditional("RELEASE")]
|
|
||||||
private static void ReleaseWriteLog(JsonObject jsonLog, UnixTime timestamp)
|
|
||||||
{
|
{
|
||||||
// WriteToFile(jsonLog, "/home/debian/DataSaliMax/" + timestamp); // this is was for beaglebone TODO
|
// Power Measurement Values
|
||||||
|
var gridPower = s.GridMeter!.Ac.Power.Active;
|
||||||
|
var inverterPower = s.AcDc.Ac.Power.Active;
|
||||||
|
var islandLoadPower = s.LoadOnAcIsland is null ? 0 : s.LoadOnAcIsland.Ac.Power.Active;
|
||||||
|
var dcBatteryPower = s.DcDc.Dc.Battery.Power;
|
||||||
|
var dcdcPower = s.DcDc.Dc.Link.Power;
|
||||||
|
var pvOnDcPower = s.PvOnDc.Dc!.Power.Value;
|
||||||
|
|
||||||
|
// Power Calculated Values
|
||||||
|
var islandToGridBusPower = inverterPower + islandLoadPower;
|
||||||
|
var gridLoadPower = s.LoadOnAcGrid is null ? 0: s.LoadOnAcGrid.Power.Active;
|
||||||
|
|
||||||
|
var gridPowerByPhase = TextBlock.AlignLeft(s.GridMeter.Ac.L1.Power.Active.ToStringRounded(),
|
||||||
|
s.GridMeter.Ac.L2.Power.Active.ToStringRounded(),
|
||||||
|
s.GridMeter.Ac.L3.Power.Active.ToStringRounded());
|
||||||
|
|
||||||
|
var gridVoltageByPhase = TextBlock.AlignLeft(s.GridMeter.Ac.L1.Voltage.ToStringRounded(),
|
||||||
|
s.GridMeter.Ac.L2.Voltage.ToStringRounded(),
|
||||||
|
s.GridMeter.Ac.L3.Voltage.ToStringRounded());
|
||||||
|
|
||||||
|
var inverterPowerByPhase = TextBlock.AlignLeft(s.AcDc.Ac.L1.Power.Active.ToStringRounded(),
|
||||||
|
s.AcDc.Ac.L2.Power.Active.ToStringRounded(),
|
||||||
|
s.AcDc.Ac.L3.Power.Active.ToStringRounded());
|
||||||
|
|
||||||
|
// ReSharper disable once CoVariantArrayConversion
|
||||||
|
var inverterPowerByAcDc = TextBlock.AlignLeft(s.AcDc.Devices
|
||||||
|
.Select(s1 => s1.Status.Ac.Power)
|
||||||
|
.ToArray());
|
||||||
|
|
||||||
|
var dcLinkVoltage = TextBlock.CenterHorizontal("",
|
||||||
|
s.DcDc.Dc.Link.Voltage.ToStringRounded(),
|
||||||
|
"");
|
||||||
|
|
||||||
|
//var inverterPowerByPhase = new ActivePower[(Int32)s.AcDc.Ac.L1.Power.Active, (Int32)s.AcDc.Ac.L2.Power.Active, (Int32)s.AcDc.Ac.L3.Power.Active];
|
||||||
|
|
||||||
|
// Voltage Measurement Values
|
||||||
|
//var inverterVoltage = new Voltage [(Int32)s.AcDc.Ac.L1.Voltage, (Int32)s.AcDc.Ac.L2.Voltage, (Int32)s.AcDc.Ac.L3.Voltage];
|
||||||
|
//var dcLinkVoltage = s.DcDc.Dc.Link.Voltage;
|
||||||
|
var dc48Voltage = s.DcDc.Dc.Battery.Voltage;
|
||||||
|
var batteryVoltage = s.Battery.Dc.Voltage;
|
||||||
|
var batterySoc = s.Battery.Soc;
|
||||||
|
var batteryCurrent = s.Battery.Dc.Current;
|
||||||
|
var batteryTemp = s.Battery.Temperature;
|
||||||
|
|
||||||
|
var gridBusColumn = ColumnBox("Pv", "Grid Bus", "Load" , gridVoltageByPhase , gridLoadPower);
|
||||||
|
var islandBusColumn = ColumnBox("Pv", "Island Bus", "Load" , inverterPowerByPhase, islandLoadPower);
|
||||||
|
var dcBusColumn = ColumnBox("Pv", "Dc Bus", "Load" , dcLinkVoltage, 0, pvOnDcPower);
|
||||||
|
var gridBusFlow = Flow.Horizontal(gridPower);
|
||||||
|
var flowGridBusToIslandBus = Flow.Horizontal((ActivePower)islandToGridBusPower);
|
||||||
|
var flowIslandBusToInverter = Flow.Horizontal(inverterPower);
|
||||||
|
var flowInverterToDcBus = Flow.Horizontal(inverterPower);
|
||||||
|
var flowDcBusToDcDc = Flow.Horizontal(dcdcPower);
|
||||||
|
var flowDcDcToBattery = Flow.Horizontal(dcBatteryPower);
|
||||||
|
|
||||||
|
var gridBox = TextBlock.AlignLeft(gridPowerByPhase).TitleBox("Grid");
|
||||||
|
var inverterBox = TextBlock.AlignLeft(inverterPowerByAcDc).TitleBox("Inverter");
|
||||||
|
var dcDcBox = TextBlock.AlignLeft(dc48Voltage).TitleBox("DC/DC");
|
||||||
|
var batteryBox = TextBlock.AlignLeft(batteryVoltage.ToStringRounded(), batterySoc.ToStringRounded(), batteryCurrent.ToStringRounded(), batteryTemp.ToStringRounded()).TitleBox("Battery");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var totalBoxes = TextBlock.CenterVertical(gridBox,
|
||||||
|
gridBusFlow,
|
||||||
|
gridBusColumn,
|
||||||
|
flowGridBusToIslandBus,
|
||||||
|
islandBusColumn,
|
||||||
|
flowIslandBusToInverter,
|
||||||
|
inverterBox,
|
||||||
|
flowInverterToDcBus,
|
||||||
|
dcBusColumn,
|
||||||
|
flowDcBusToDcDc,
|
||||||
|
dcDcBox,
|
||||||
|
flowDcDcToBattery,
|
||||||
|
batteryBox);
|
||||||
|
|
||||||
|
totalBoxes.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TextBlock ColumnBox(String pvTitle, String busTitle, String loadTitle, TextBlock dataBox)
|
||||||
|
{
|
||||||
|
return ColumnBox(pvTitle, busTitle, loadTitle, dataBox, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TextBlock ColumnBox(String pvTitle, String busTitle, String loadTitle, TextBlock dataBox, ActivePower loadPower)
|
||||||
|
{
|
||||||
|
return ColumnBox(pvTitle, busTitle, loadTitle, dataBox, loadPower, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TextBlock ColumnBox(String pvTitle, String busTitle, String loadTitle, TextBlock dataBox, ActivePower loadPower, ActivePower pvPower)
|
||||||
|
{
|
||||||
|
var pvBox = TextBlock.AlignLeft("").TitleBox(pvTitle);
|
||||||
|
var pvToBus = Flow.Vertical(pvPower);
|
||||||
|
var busBox = TextBlock.AlignLeft(dataBox).TitleBox(busTitle);
|
||||||
|
var busToLoad = Flow.Vertical(loadPower);
|
||||||
|
var loadBox = TextBlock.AlignLeft("").TitleBox(loadTitle);
|
||||||
|
|
||||||
|
return TextBlock.CenterHorizontal(pvBox, pvToBus, busBox, busToLoad, loadBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<T?> ResultOrNull<T>(this Task<T> task)
|
||||||
|
{
|
||||||
|
if (task.Status == TaskStatus.RanToCompletion)
|
||||||
|
return await task;
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ControlConstants(this StatusRecord r)
|
||||||
|
{
|
||||||
|
var inverters = r.AcDc.Devices;
|
||||||
|
|
||||||
|
inverters.ForEach(d => d.Control.Dc.MaxVoltage = r.Config.MaxDcBusVoltage);
|
||||||
|
inverters.ForEach(d => d.Control.Dc.MinVoltage = r.Config.MinDcBusVoltage);
|
||||||
|
inverters.ForEach(d => d.Control.Dc.ReferenceVoltage = r.Config.ReferenceDcBusVoltage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
// why this is not in Controller?
|
||||||
private static void DebugWriteLog(JsonObject jsonLog, UnixTime timestamp)
|
private static void DistributePower(StatusRecord record, EssControl essControl)
|
||||||
{
|
{
|
||||||
WriteToFile(jsonLog, "/home/atef/JsonData/" + timestamp);
|
var nInverters = record.AcDc.Devices.Count;
|
||||||
|
|
||||||
|
var powerPerInverterPhase = nInverters > 0
|
||||||
|
? AcPower.FromActiveReactive(essControl.PowerSetpoint / nInverters / 3, 0)
|
||||||
|
: AcPower.Null;
|
||||||
|
|
||||||
|
//var powerPerInverterPhase = AcPower.Null;
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void WriteToFile(Object obj, String fileName)
|
private static void ApplyAcDcDefaultSettings(this SystemControlRegisters? sc)
|
||||||
{
|
{
|
||||||
var jsonString = JsonSerializer.Serialize(obj, JsonOptions);
|
if (sc is null)
|
||||||
File.WriteAllText(fileName, jsonString);
|
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 readonly JsonSerializerOptions JsonOptions = new()
|
private static async Task UploadCsv(StatusRecord status, UnixTime timeStamp)
|
||||||
{
|
{
|
||||||
WriteIndented = true,
|
timeStamp.WriteLine();
|
||||||
IgnoreReadOnlyProperties = false,
|
|
||||||
Converters = { new JsonStringEnumConverter() },
|
var csv = status.ToCsv();
|
||||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
var s3Path = timeStamp + ".csv";
|
||||||
//TODO
|
var request = S3Config.CreatePutRequest(s3Path);
|
||||||
};
|
var response = await request.PutAsync(new StringContent(csv));
|
||||||
|
|
||||||
|
|
||||||
private static async Task UploadTimeSeries(S3Config config, JsonObject json, UnixTime unixTime)
|
|
||||||
{
|
|
||||||
var payload = JsonSerializer.Serialize(json, JsonOptions);
|
|
||||||
var s3Path = unixTime.Ticks + ".json";
|
|
||||||
var request = config.CreatePutRequest(s3Path);
|
|
||||||
var response = await request.PutAsync(new StringContent(payload));
|
|
||||||
|
|
||||||
|
//csv.WriteLine();
|
||||||
|
//timeStamp.Ticks.WriteLine();
|
||||||
|
|
||||||
if (response.StatusCode != 200)
|
if (response.StatusCode != 200)
|
||||||
{
|
{
|
||||||
Console.WriteLine("ERROR: PUT");
|
Console.WriteLine("ERROR: PUT");
|
||||||
|
@ -199,18 +413,5 @@ internal static class Program
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task UploadTopology(S3Config config, JsonObject json, UnixTime unixTime)
|
}
|
||||||
{
|
|
||||||
var payload = JsonSerializer.Serialize(json, JsonOptions);
|
|
||||||
var s3Path = "topology" + unixTime.Ticks + ".json";
|
|
||||||
var request = config.CreatePutRequest(s3Path);
|
|
||||||
var response = await request.PutAsync(new StringContent(payload));
|
|
||||||
|
|
||||||
if (response.StatusCode != 200)
|
|
||||||
{
|
|
||||||
Console.WriteLine("ERROR: PUT");
|
|
||||||
var error = response.GetStringAsync();
|
|
||||||
Console.WriteLine(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
|
|
||||||
|
|
||||||
public enum RelayState
|
|
||||||
{
|
|
||||||
Open = 0,
|
|
||||||
Closed = 1
|
|
||||||
}
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
using InnovEnergy.Lib.Devices.Adam6360D;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||||
|
|
||||||
|
public class RelaysDevice
|
||||||
|
{
|
||||||
|
private Adam6360DDevice AdamDevice { get; }
|
||||||
|
|
||||||
|
public RelaysDevice(String hostname) => AdamDevice = new Adam6360DDevice(hostname, 2);
|
||||||
|
public RelaysDevice(Channel channel) => AdamDevice = new Adam6360DDevice(channel, 2);
|
||||||
|
|
||||||
|
public RelaysRecord? Read()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return AdamDevice.Read();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
$"Failed to read from {nameof(RelaysDevice)}\n{e}".Log();
|
||||||
|
|
||||||
|
// TODO: log
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write(RelaysRecord r)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AdamDevice.Write(r);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
$"Failed to write to {nameof(RelaysDevice)}\n{e}".Log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
using InnovEnergy.Lib.Devices.Adam6360D;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||||
|
|
||||||
|
public enum InvertersAreConnectedToAc
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Some,
|
||||||
|
All
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RelaysRecord
|
||||||
|
{
|
||||||
|
private readonly Adam6360DRegisters _Regs;
|
||||||
|
|
||||||
|
public RelaysRecord(Adam6360DRegisters regs) => _Regs = regs;
|
||||||
|
|
||||||
|
|
||||||
|
public Boolean K1GridBusIsConnectedToGrid => _Regs.DigitalInput6;
|
||||||
|
public Boolean K2IslandBusIsConnectedToGridBus => !_Regs.DigitalInput4;
|
||||||
|
|
||||||
|
|
||||||
|
public IEnumerable<Boolean> K3InverterIsConnectedToIslandBus
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
yield return K3Inverter1IsConnectedToIslandBus;
|
||||||
|
yield return K3Inverter2IsConnectedToIslandBus;
|
||||||
|
yield return K3Inverter3IsConnectedToIslandBus;
|
||||||
|
yield return K3Inverter4IsConnectedToIslandBus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean K3Inverter1IsConnectedToIslandBus => !_Regs.DigitalInput0;
|
||||||
|
public Boolean K3Inverter2IsConnectedToIslandBus => !_Regs.DigitalInput1;
|
||||||
|
public Boolean K3Inverter3IsConnectedToIslandBus => !_Regs.DigitalInput2;
|
||||||
|
public Boolean K3Inverter4IsConnectedToIslandBus => !_Regs.DigitalInput3;
|
||||||
|
|
||||||
|
public Boolean FiWarning => !_Regs.DigitalInput5;
|
||||||
|
public Boolean FiError => !_Regs.DigitalInput7;
|
||||||
|
|
||||||
|
public Boolean K2ConnectIslandBusToGridBus { get => _Regs.Relay0; set => _Regs.Relay0 = value;}
|
||||||
|
|
||||||
|
public static implicit operator Adam6360DRegisters(RelaysRecord d) => d._Regs;
|
||||||
|
public static implicit operator RelaysRecord(Adam6360DRegisters d) => new RelaysRecord(d);
|
||||||
|
|
||||||
|
//
|
||||||
|
// public HighActivePinState F1Inverter1 => _Regs.DigitalInput8.ConvertTo<HighActivePinState>(); // 1 = Closed , 0 = open
|
||||||
|
// public HighActivePinState F2Inverter2 => _Regs.DigitalInput9.ConvertTo<HighActivePinState>(); // 1 = Closed , 0 = open
|
||||||
|
// public HighActivePinState F3Inverter3 => _Regs.DigitalInput10.ConvertTo<HighActivePinState>(); // 1 = Closed , 0 = open
|
||||||
|
// public HighActivePinState F4Inverter4 => _Regs.DigitalInput11.ConvertTo<HighActivePinState>(); // 1 = Closed , 0 = open
|
||||||
|
//
|
||||||
|
// public HighActivePinState Di12 => _Regs.DigitalInput12.ConvertTo<HighActivePinState>();
|
||||||
|
}
|
|
@ -1,46 +0,0 @@
|
||||||
using InnovEnergy.Lib.Devices.Adam6060;
|
|
||||||
using InnovEnergy.Lib.Utils;
|
|
||||||
|
|
||||||
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
|
|
||||||
|
|
||||||
public class SaliMaxRelaysDevice
|
|
||||||
{
|
|
||||||
private Adam6060Device AdamDevice { get; }
|
|
||||||
|
|
||||||
public SaliMaxRelaysDevice (String hostname, UInt16 port = 502)//TODO
|
|
||||||
{
|
|
||||||
AdamDevice = new Adam6060Device(hostname, port);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public SaliMaxRelayStatus? ReadStatus()
|
|
||||||
{
|
|
||||||
// Console.WriteLine("Reading Relay Status");
|
|
||||||
|
|
||||||
var adamStatus = AdamDevice.ReadStatus();
|
|
||||||
|
|
||||||
if (adamStatus is null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new SaliMaxRelayStatus
|
|
||||||
{
|
|
||||||
K1 = adamStatus.DigitalInput0.ConvertTo<RelayState>(),
|
|
||||||
K2 = adamStatus.DigitalInput1.ConvertTo<RelayState>(),
|
|
||||||
K3 = adamStatus.DigitalInput2.ConvertTo<RelayState>()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void WriteControl(Boolean k2State) //this to improve
|
|
||||||
{
|
|
||||||
Console.WriteLine("Writing Relay Status");
|
|
||||||
|
|
||||||
var relayControlStatus = new Adam6060Control
|
|
||||||
{
|
|
||||||
Relay2 = k2State
|
|
||||||
};
|
|
||||||
|
|
||||||
AdamDevice.WriteControl(relayControlStatus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
|
|
||||||
|
|
||||||
public record SaliMaxRelayStatus
|
|
||||||
{
|
|
||||||
public RelayState K1 { get; init; } = RelayState.Closed; // Address on Adam(0X) 00002
|
|
||||||
public RelayState K2 { get; init; } = RelayState.Closed; // Address on Adam(0X) 00003
|
|
||||||
public RelayState K3 { get; init; } = RelayState.Closed; // Address on Adam(0X) 00004
|
|
||||||
}
|
|
|
@ -0,0 +1,470 @@
|
||||||
|
using InnovEnergy.App.SaliMax.Ess;
|
||||||
|
using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||||
|
using InnovEnergy.Lib.Devices.Battery48TL;
|
||||||
|
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
||||||
|
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
||||||
|
using static InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes.GridType;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SaliMax.System;
|
||||||
|
|
||||||
|
public static class Controller
|
||||||
|
{
|
||||||
|
private static Int32 GetSystemState(this StatusRecord r)
|
||||||
|
{
|
||||||
|
var relays = r.Relays;
|
||||||
|
|
||||||
|
if (relays is null)
|
||||||
|
return 101; // Message = "Panic: relay device is not available!",
|
||||||
|
|
||||||
|
var acDcs = r.AcDc;
|
||||||
|
|
||||||
|
if (acDcs.NotAvailable())
|
||||||
|
return 102;
|
||||||
|
|
||||||
|
var k4 = acDcs.AllDisabled() ? 0
|
||||||
|
: acDcs.AllGridTied() ? 1
|
||||||
|
: acDcs.AllIsland() ? 2
|
||||||
|
: 4;
|
||||||
|
|
||||||
|
if (k4 == 4)
|
||||||
|
return 103; //Message = "Panic: ACDCs have unequal grid types",
|
||||||
|
|
||||||
|
var nInverters = r.AcDc.Devices.Count;
|
||||||
|
|
||||||
|
var k1 = relays.K1GridBusIsConnectedToGrid ? 1 : 0;
|
||||||
|
var k2 = relays.K2IslandBusIsConnectedToGridBus ? 1 : 0;
|
||||||
|
var k3 = relays.K3InverterIsConnectedToIslandBus.Take(nInverters).Any(c => c) ? 1 : 0;
|
||||||
|
|
||||||
|
// states as defined in states excel sheet
|
||||||
|
return 1
|
||||||
|
+ 1*k1
|
||||||
|
+ 2*k2
|
||||||
|
+ 4*k3
|
||||||
|
+ 8*k4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Boolean ControlSystemState(this StatusRecord s)
|
||||||
|
{
|
||||||
|
s.StateMachine.State = s.GetSystemState();
|
||||||
|
|
||||||
|
return s.StateMachine.State switch
|
||||||
|
{
|
||||||
|
1 => State1(s),
|
||||||
|
2 => State2(s),
|
||||||
|
4 => State4(s),
|
||||||
|
6 => State6(s),
|
||||||
|
9 => State9(s),
|
||||||
|
//10 => State10(s),
|
||||||
|
12 => State12(s),
|
||||||
|
13 => State13(s),
|
||||||
|
15 => State15(s),
|
||||||
|
16 => State16(s),
|
||||||
|
17 => State17(s),
|
||||||
|
18 => State18(s),
|
||||||
|
21 => State21(s),
|
||||||
|
|
||||||
|
101 => State101(s),
|
||||||
|
102 => State102(s),
|
||||||
|
103 => State103(s),
|
||||||
|
_ => UnknownState(s)
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean NotAvailable(this AcDcDevicesRecord acDcs)
|
||||||
|
{
|
||||||
|
return acDcs.SystemControl == null || acDcs.Devices.Count == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean NotAvailable(this DcDcDevicesRecord dcDcs)
|
||||||
|
{
|
||||||
|
return dcDcs.SystemControl == null || dcDcs.Devices.Count == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Boolean NotAvailable(this Battery48TlRecords batteries)
|
||||||
|
{
|
||||||
|
return batteries.Devices.Count <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean State1(StatusRecord s)
|
||||||
|
{
|
||||||
|
s.StateMachine.Message = "Inverters are off. Switching to Island Mode.";
|
||||||
|
|
||||||
|
s.DcDc.Enable();
|
||||||
|
s.AcDc.Enable();
|
||||||
|
s.AcDc.EnableIslandMode();
|
||||||
|
s.Relays.DisconnectIslandBusFromGrid();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// => 17
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean State2(StatusRecord s)
|
||||||
|
{
|
||||||
|
s.StateMachine.Message = "Inverters are disconnected from Island Bus. Switching to GridTie Mode. C";
|
||||||
|
|
||||||
|
s.DcDc.Disable();
|
||||||
|
s.AcDc.Disable();
|
||||||
|
s.AcDc.EnableGridTieMode();
|
||||||
|
s.Relays.ConnectIslandBusToGrid();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// => 10
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean State4(StatusRecord s)
|
||||||
|
{
|
||||||
|
s.StateMachine.Message = "Turning on Inverters";
|
||||||
|
|
||||||
|
s.DcDc.Enable();
|
||||||
|
s.AcDc.Enable();
|
||||||
|
s.AcDc.EnableGridTieMode();
|
||||||
|
s.Relays.ConnectIslandBusToGrid();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// => 12
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Boolean State6(StatusRecord s)
|
||||||
|
{
|
||||||
|
s.StateMachine.Message = "Inverters are off. Waiting for them to disconnect from Island Bus.";
|
||||||
|
|
||||||
|
s.DcDc.Disable();
|
||||||
|
s.AcDc.Disable();
|
||||||
|
s.AcDc.EnableIslandMode();
|
||||||
|
s.Relays.DisconnectIslandBusFromGrid();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// => 2
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Boolean State9(StatusRecord s)
|
||||||
|
{
|
||||||
|
|
||||||
|
s.StateMachine.Message = "Inverters have disconnected from Island Bus. Turning them off.";
|
||||||
|
|
||||||
|
s.DcDc.Disable(); // TODO: leave enabled?
|
||||||
|
s.AcDc.Disable();
|
||||||
|
s.AcDc.EnableGridTieMode();
|
||||||
|
s.Relays.DisconnectIslandBusFromGrid();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// => 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// private static Boolean State10(StatusRecord s)
|
||||||
|
// {
|
||||||
|
//
|
||||||
|
// s.SystemState.Message = "Inverters have disconnected from AcOut. Turning them off.";
|
||||||
|
//
|
||||||
|
// s.DcDc.Disable(); // TODO: leave enabled?
|
||||||
|
// s.AcDc.Disable();
|
||||||
|
// s.AcDc.EnableGridTieMode();
|
||||||
|
// s.Relays.DisconnectIslandBusFromGrid();
|
||||||
|
//
|
||||||
|
// return true;
|
||||||
|
//
|
||||||
|
// // => 12
|
||||||
|
// }
|
||||||
|
|
||||||
|
private static Boolean State12(StatusRecord s)
|
||||||
|
{
|
||||||
|
s.StateMachine.Message = "Waiting for Inverters to connect to Island Bus";
|
||||||
|
|
||||||
|
s.DcDc.Enable();
|
||||||
|
s.AcDc.Enable();
|
||||||
|
s.AcDc.EnableGridTieMode();
|
||||||
|
s.Relays.ConnectIslandBusToGrid();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// => 16
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static Boolean State13(StatusRecord s)
|
||||||
|
{
|
||||||
|
s.StateMachine.Message = "Disconnected from AcIn (K2), awaiting inverters to disconnect from AcOut (K3)";
|
||||||
|
|
||||||
|
s.DcDc.Enable();
|
||||||
|
s.AcDc.Enable();
|
||||||
|
s.AcDc.EnableGridTieMode();
|
||||||
|
s.Relays.DisconnectIslandBusFromGrid();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// => 9
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean State15(StatusRecord s)
|
||||||
|
{
|
||||||
|
s.StateMachine.Message = "Grid has been lost, disconnecting AcIn from AcOut (K2)";
|
||||||
|
|
||||||
|
s.DcDc.Enable();
|
||||||
|
s.AcDc.Enable();
|
||||||
|
s.AcDc.EnableGridTieMode();
|
||||||
|
s.Relays.DisconnectIslandBusFromGrid();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// => 13
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean State16(StatusRecord s)
|
||||||
|
{
|
||||||
|
// return new
|
||||||
|
// (
|
||||||
|
// " Inverter is in grid-tie\n Waiting for K1AcInIsConnectedToGrid to open to leave it",
|
||||||
|
// AcPowerStageEnable: true,
|
||||||
|
// DcPowerStageEnable: true,
|
||||||
|
// GridType.GridTied400V50Hz,
|
||||||
|
// HighActivePinState.Closed
|
||||||
|
// );
|
||||||
|
|
||||||
|
s.StateMachine.Message = "ESS";
|
||||||
|
|
||||||
|
s.DcDc.Enable();
|
||||||
|
s.AcDc.Enable();
|
||||||
|
s.AcDc.EnableGridTieMode();
|
||||||
|
s.Relays.ConnectIslandBusToGrid();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// => 15
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Boolean State17(StatusRecord s)
|
||||||
|
{
|
||||||
|
s.StateMachine.Message = "Inverters are in Island Mode. Waiting for them to connect to AcIn.";
|
||||||
|
|
||||||
|
s.DcDc.Enable();
|
||||||
|
s.AcDc.Enable();
|
||||||
|
s.AcDc.EnableIslandMode();
|
||||||
|
s.Relays.DisconnectIslandBusFromGrid();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// => 21
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static Boolean State18(StatusRecord s)
|
||||||
|
{
|
||||||
|
// return new
|
||||||
|
// (
|
||||||
|
// " Didn't succeed to go to Island mode and K1AcInIsConnectedToGrid close\n Turning off power stage of inverter\n Moving to Grid Tie",
|
||||||
|
// AcPowerStageEnable: false,
|
||||||
|
// DcPowerStageEnable: false,
|
||||||
|
// GridType.GridTied400V50Hz,
|
||||||
|
// HighActivePinState.Open
|
||||||
|
// );
|
||||||
|
|
||||||
|
s.DcDc.Disable();
|
||||||
|
s.AcDc.Disable();
|
||||||
|
s.AcDc.EnableIslandMode();
|
||||||
|
s.Relays.DisconnectIslandBusFromGrid();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean State21(StatusRecord s)
|
||||||
|
{
|
||||||
|
s.StateMachine.Message = "Island Mode";
|
||||||
|
|
||||||
|
s.DcDc.Enable();
|
||||||
|
s.AcDc.Enable();
|
||||||
|
s.AcDc.EnableIslandMode();
|
||||||
|
s.Relays.DisconnectIslandBusFromGrid();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// => 22
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean State22(StatusRecord s)
|
||||||
|
{
|
||||||
|
s.StateMachine.Message = "Grid became available (K1). Turning off inverters.";
|
||||||
|
|
||||||
|
s.DcDc.Disable();
|
||||||
|
s.AcDc.Disable();
|
||||||
|
s.AcDc.EnableIslandMode();
|
||||||
|
s.Relays.DisconnectIslandBusFromGrid();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// => 6
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static Boolean State101(StatusRecord s)
|
||||||
|
{
|
||||||
|
s.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";
|
||||||
|
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 AllGridTied(this AcDcDevicesRecord acDcs)
|
||||||
|
{
|
||||||
|
return acDcs.Devices.All(d => d.Status.ActiveGridType is GridTied380V60Hz)
|
||||||
|
|| acDcs.Devices.All(d => d.Status.ActiveGridType is GridTied400V50Hz)
|
||||||
|
|| acDcs.Devices.All(d => d.Status.ActiveGridType is GridTied480V60Hz);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean AllIsland(this AcDcDevicesRecord acDcs)
|
||||||
|
{
|
||||||
|
return acDcs.Devices.All(d => d.Status.ActiveGridType is Island400V50Hz)
|
||||||
|
|| acDcs.Devices.All(d => d.Status.ActiveGridType is Island480V60Hz);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ForAll<T>(this IEnumerable<T> ts, Action<T> action)
|
||||||
|
{
|
||||||
|
foreach (var t in ts)
|
||||||
|
action(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Disable(this AcDcDevicesRecord acDc)
|
||||||
|
{
|
||||||
|
acDc.Devices
|
||||||
|
.Select(d => d.Control)
|
||||||
|
.ForAll(c => c.PowerStageEnable = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void Disable(this DcDcDevicesRecord dcDc)
|
||||||
|
{
|
||||||
|
dcDc.Devices
|
||||||
|
.Select(d => d.Control)
|
||||||
|
.ForAll(c => c.PowerStageEnable = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Enable(this AcDcDevicesRecord acDc)
|
||||||
|
{
|
||||||
|
acDc.Devices
|
||||||
|
.Select(d => d.Control)
|
||||||
|
.ForAll(c => c.PowerStageEnable = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void Enable(this DcDcDevicesRecord dcDc)
|
||||||
|
{
|
||||||
|
dcDc.Devices
|
||||||
|
.Select(d => d.Control)
|
||||||
|
.ForAll(c => c.PowerStageEnable = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static void EnableGridTieMode(this AcDcDevicesRecord acDc)
|
||||||
|
{
|
||||||
|
acDc.Devices
|
||||||
|
.Select(d => d.Control)
|
||||||
|
.ForAll(c => c.Ac.GridType = GridTied400V50Hz); // TODO: config grid type
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void EnableIslandMode(this AcDcDevicesRecord acDc)
|
||||||
|
{
|
||||||
|
acDc.Devices
|
||||||
|
.Select(d => d.Control)
|
||||||
|
.ForAll(c => c.Ac.GridType = Island400V50Hz); // TODO: config grid type
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DisconnectIslandBusFromGrid(this RelaysRecord? relays)
|
||||||
|
{
|
||||||
|
if (relays is not null)
|
||||||
|
relays.K2ConnectIslandBusToGridBus = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConnectIslandBusToGrid(this RelaysRecord? relays)
|
||||||
|
{
|
||||||
|
if (relays is not null)
|
||||||
|
relays.K2ConnectIslandBusToGridBus = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Boolean EnableSafeDefaults(this StatusRecord s)
|
||||||
|
{
|
||||||
|
s.DcDc.Disable();
|
||||||
|
s.AcDc.Disable();
|
||||||
|
s.AcDc.EnableGridTieMode();
|
||||||
|
s.Relays.DisconnectIslandBusFromGrid();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DcDcDevicesRecord ResetAlarms(this DcDcDevicesRecord dcDcStatus)
|
||||||
|
{
|
||||||
|
var sc = dcDcStatus.SystemControl;
|
||||||
|
|
||||||
|
if (sc is not null)
|
||||||
|
sc.ResetAlarmsAndWarnings = sc.Alarms.Any();
|
||||||
|
|
||||||
|
foreach (var d in dcDcStatus.Devices)
|
||||||
|
d.Control.ResetAlarmsAndWarnings = d.Status.Alarms.Any() || d.Status.Warnings.Any();
|
||||||
|
|
||||||
|
return dcDcStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AcDcDevicesRecord ResetAlarms(this AcDcDevicesRecord acDcStatus)
|
||||||
|
{
|
||||||
|
var sc = acDcStatus.SystemControl;
|
||||||
|
|
||||||
|
if (sc is not null)
|
||||||
|
sc.ResetAlarmsAndWarnings = sc.Alarms.Any() || sc.Warnings.Any();
|
||||||
|
|
||||||
|
foreach (var d in acDcStatus.Devices)
|
||||||
|
d.Control.ResetAlarmsAndWarnings = d.Status.Alarms.Any() || d.Status.Warnings.Any();
|
||||||
|
|
||||||
|
return acDcStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace InnovEnergy.App.SaliMax.System;
|
||||||
|
|
||||||
|
public class StateMachine
|
||||||
|
{
|
||||||
|
public String Message { get; set; } = "Panic: Unknown State!";
|
||||||
|
public Int32 State { get; set; } = 100;
|
||||||
|
}
|
|
@ -1,6 +1,90 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using InnovEnergy.Lib.Time.Unix;
|
||||||
|
using InnovEnergy.Lib.Utils;
|
||||||
|
using static System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
namespace InnovEnergy.App.SaliMax.SystemConfig;
|
namespace InnovEnergy.App.SaliMax.SystemConfig;
|
||||||
|
|
||||||
public static class Config
|
// shut up trim warnings
|
||||||
|
#pragma warning disable IL2026
|
||||||
|
|
||||||
|
public class Config //TODO: let IE choose from config files (Json) and connect to GUI
|
||||||
{
|
{
|
||||||
public const String LogSalimaxLog = "/home/ie-entwicklung/Salimax.log"; // todo remove ie-entwicklung
|
private static String DefaultConfigFilePath => Path.Combine(Environment.CurrentDirectory, "config.json");
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||||
|
|
||||||
|
public Double MinSoc { get; set; }
|
||||||
|
public UnixTime LastEoc { get; set; }
|
||||||
|
public Double PConstant { get; set; }
|
||||||
|
public Double GridSetPoint { get; set; }
|
||||||
|
public Double BatterySelfDischargePower { get; set; }
|
||||||
|
public Double HoldSocZone { get; set; }
|
||||||
|
|
||||||
|
public Double MaxDcBusVoltage { get; set; }
|
||||||
|
public Double MinDcBusVoltage { get; set; }
|
||||||
|
public Double ReferenceDcBusVoltage { get; set; }
|
||||||
|
|
||||||
|
public static Config Default => new()
|
||||||
|
{
|
||||||
|
MinSoc = 20,
|
||||||
|
LastEoc = UnixTime.Epoch,
|
||||||
|
PConstant = .5,
|
||||||
|
GridSetPoint = 0,
|
||||||
|
BatterySelfDischargePower = 200, // TODO: multiple batteries
|
||||||
|
HoldSocZone = 1, // TODO: find better name,
|
||||||
|
MinDcBusVoltage = 730,
|
||||||
|
ReferenceDcBusVoltage = 750,
|
||||||
|
MaxDcBusVoltage = 770,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public void Save(String? path = null)
|
||||||
|
{
|
||||||
|
var configFilePath = path ?? DefaultConfigFilePath;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jsonString = Serialize(this, JsonOptions);
|
||||||
|
File.WriteAllText(configFilePath, jsonString);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
$"Failed to write config file {configFilePath}\n{e}".Log();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Config Load(String? path = null)
|
||||||
|
{
|
||||||
|
var configFilePath = path ?? DefaultConfigFilePath;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jsonString = File.ReadAllText(configFilePath);
|
||||||
|
return Deserialize<Config>(jsonString)!;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
$"Failed to read config file {configFilePath}, using default config\n{e}".Log();
|
||||||
|
return Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static async Task<Config> LoadAsync(String? path = null)
|
||||||
|
{
|
||||||
|
var configFilePath = path ?? DefaultConfigFilePath;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jsonString = await File.ReadAllTextAsync(configFilePath);
|
||||||
|
return Deserialize<Config>(jsonString)!;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Couldn't read config file {configFilePath}, using default config");
|
||||||
|
e.Message.WriteLine();
|
||||||
|
return Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,120 +1,123 @@
|
||||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
|
// using InnovEnergy.App.SaliMax.SaliMaxRelays;
|
||||||
using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
|
//
|
||||||
|
// namespace InnovEnergy.App.SaliMax.SystemConfig;
|
||||||
namespace InnovEnergy.App.SaliMax.SystemConfig;
|
//
|
||||||
|
// public static class Defaults
|
||||||
public static class Defaults
|
// {
|
||||||
{
|
// public static readonly TruConvertAcControl TruConvertAcControl = new()
|
||||||
public static readonly TruConvertAcControl TruConvertAcControl = new()
|
// {
|
||||||
{
|
// Date = default, // TODO
|
||||||
Date = default, // TODO
|
// Time = default, // TODO,
|
||||||
Time = default, // TODO,
|
// IpAddress = default, // 0x C0A80102;
|
||||||
IpAddress = default, // 0x C0A80102;
|
// Subnet = default, //= 0x FFFFFF00;
|
||||||
Subnet = default, //= 0x FFFFFF00;
|
// Gateway = default, //= 0x C0A80102;
|
||||||
Gateway = default, //= 0x C0A80102;
|
// ResetParamToDefault = false, // Coil
|
||||||
ResetParamToDefault = false, // Coil
|
// CommunicationTimeout = TimeSpan.FromSeconds(20),
|
||||||
CommunicationTimeout = TimeSpan.FromSeconds(10),
|
// CpuReset = false,
|
||||||
FactoryResetParameters = false,
|
// ConnectedSystemConfig = Lib.Devices.Trumpf.TruConvert.SystemConfig.AcDcAndDcDc,
|
||||||
ConnectedSystemConfig = Lib.Devices.Trumpf.TruConvert.SystemConfig.AcDcAndDcDc,
|
// UpdateSwTrigger = 0,
|
||||||
UpdateSwTrigger = 0,
|
// AutomaticSwUpdate = 0,
|
||||||
AutomaticSwUpdate = 0,
|
// CustomerValuesSaveReset = 0,
|
||||||
CustomerValuesSaveReset = 0,
|
// // SerialNumberSystemControl = 0,
|
||||||
// SerialNumberSystemControl = 0,
|
// // SerialNumberAcDc = 0,
|
||||||
// SerialNumberAcDc = 0,
|
// IntegrationLevel = 16,
|
||||||
IntegrationLevel = 16,
|
// // IlBuildnumber = 0,
|
||||||
// IlBuildnumber = 0,
|
// PowerStageEnable = true,
|
||||||
PowerStageEnable = true,
|
// SetValueConfig = SymmetricAcOperationMode.Symmetric, // Asymmetric = 0, // this is can not be seen in UI
|
||||||
SetValueConfig = SymmetricAcOperationMode.Symmetric, // Asymmetric = 0, // this is can not be seen in UI
|
// ResetsAlarmAndWarning = false,
|
||||||
ResetsAlarmAndWarning = true,
|
// PreChargeDcLinkConfig = PreChargeDcLinkConfig.Internal, // 1 = internal
|
||||||
PreChargeDcLinkConfig = PreChargeDcLinkConfig.Internal, // 1 = internal
|
// PowerFactorConvention = PowerFactorConvention.Producer, // 0 = producer
|
||||||
PowerFactorConvention = PowerFactorConvention.Producer, // 0 = producer
|
// SlaveAddress = 1,
|
||||||
SlaveAddress = 1,
|
// ErrorHandlingPolicy = AcErrorPolicy.Relaxed, // 0 = relaxed
|
||||||
ErrorHandlingPolicy = AcErrorPolicy.Relaxed, // 0 = relaxed
|
// GridType = AcDcGridType.GridTied400V50Hz,
|
||||||
GridType = AcDcGridType.GridTied400V50Hz,
|
// // SubSlaveAddress = 0, // Broadcast
|
||||||
// SubSlaveAddress = 0, // Broadcast
|
// UseModbusSlaveIdForAddressing = false,
|
||||||
UseModbusSlaveIdForAddressing = false,
|
// SubSlaveErrorPolicy = 0,
|
||||||
SubSlaveErrorPolicy = 0,
|
// SignedPowerNominalValue = 0, //signedPowerValue
|
||||||
SignedPowerNominalValue = 0, //signedPowerValue
|
// SignedPowerSetValueL1 = 0,
|
||||||
SignedPowerSetValueL1 = 0,
|
// SignedPowerSetValueL2 = 0,
|
||||||
SignedPowerSetValueL2 = 0,
|
// SignedPowerSetValueL3 = 0,
|
||||||
SignedPowerSetValueL3 = 0,
|
// // PowerSetValue = 0,
|
||||||
// PowerSetValue = 0,
|
// // PowerSetValueL1 = 0,
|
||||||
// PowerSetValueL1 = 0,
|
// // PowerSetValueL2 = 0,
|
||||||
// PowerSetValueL2 = 0,
|
// // PowerSetValueL3 = 0,
|
||||||
// PowerSetValueL3 = 0,
|
// MaximumGridCurrentRmsL1 = 80, // update to the default one
|
||||||
MaximumGridCurrentRmsL1 = 15,
|
// MaximumGridCurrentRmsL2 = 80, // update to the default one
|
||||||
MaximumGridCurrentRmsL2 = 15,
|
// MaximumGridCurrentRmsL3 = 80, // update to the default one
|
||||||
MaximumGridCurrentRmsL3 = 15,
|
// CosPhiSetValueL1 = 0,
|
||||||
CosPhiSetValueL1 = 0,
|
// CosPhiSetValueL2 = 0,
|
||||||
CosPhiSetValueL2 = 0,
|
// CosPhiSetValueL3 = 0,
|
||||||
CosPhiSetValueL3 = 0,
|
// PhaseL1IsCapacitive = false,
|
||||||
PhaseL1IsCapacitive = false,
|
// PhaseL2IsCapacitive = false,
|
||||||
PhaseL2IsCapacitive = false,
|
// PhaseL3IsCapacitive = false,
|
||||||
PhaseL3IsCapacitive = false,
|
// PhasesAreCapacitive = false,
|
||||||
PhasesAreCapacitive = false,
|
// SetPointCosPhi = 1,
|
||||||
SetPointCosPhi = 0,
|
// SetPointSinPhi = 0,
|
||||||
SetPointSinPhi = 0,
|
// SetPointSinPhiL1 = 0,
|
||||||
SetPointSinPhiL1 = 0,
|
// SetPointSinPhiL2 = 0,
|
||||||
SetPointSinPhiL2 = 0,
|
// SetPointSinPhiL3 = 0,
|
||||||
SetPointSinPhiL3 = 0,
|
// FrequencyOffsetIm = 0,
|
||||||
FrequencyOffsetIm = 0,
|
// VoltageAdjustmentFactorIm = 100,
|
||||||
VoltageAdjustmentFactorIm = 0,
|
// PreChargeDcLinkVoltage = 10,
|
||||||
PreChargeDcLinkVoltage = 10,
|
// MaxPeakCurrentVoltageControlL1 = 0,
|
||||||
MaxPeakCurrentVoltageControlL1 = 0,
|
// MaxPeakCurrentVoltageControlL2 = 0,
|
||||||
MaxPeakCurrentVoltageControlL2 = 0,
|
// MaxPeakCurrentVoltageControlL3 = 0,
|
||||||
MaxPeakCurrentVoltageControlL3 = 0,
|
// GridFormingMode = 1.ConvertTo<AcGridFormingMode>(), // 0 = not grid-forming (grid-tied) ,1 = grid-forming TODO enum
|
||||||
GridFormingMode = 0, // 0 = not grid-forming (grid-tied) ,1 = grid-forming TODO enum
|
//
|
||||||
|
// DcLinkRefVoltage = 720,
|
||||||
//remove DC stuff from AC
|
// DcLinkMinVoltage = 690,
|
||||||
DcLinkRefVoltage = 800,
|
// DcLinkMaxVoltage = 780,
|
||||||
DcLinkMinVoltage = 780,
|
// DcVoltageRefUs = 870,
|
||||||
DcLinkMaxVoltage = 820,
|
// DcMinVoltageUs = 880,
|
||||||
DcVoltageRefUs = 900,
|
// DcMaxVoltageUs = 920,
|
||||||
DcMinVoltageUs = 880,
|
// //AcDcGcBypassMode = 0,
|
||||||
DcMaxVoltageUs = 920,
|
// //AcDcGcPMaxThresholdPercent = 150,
|
||||||
AcDcGcBypassMode = 0,
|
// //AcDcGcStartupRampEnable = 0,
|
||||||
AcDcGcPMaxThresholdPercent = 150,
|
// DcConfigModule = DcStageConfiguration.Off,
|
||||||
AcDcGcStartupRampEnable = 0,
|
// DcDcPowerDistribution = 100,
|
||||||
DcConfigModule = DcStageConfiguration.Off,
|
// AcDcDistributionMode = AcDcDistributionMode.Auto,
|
||||||
DcDcPowerDistribution = 100,
|
// };
|
||||||
AcDcDistributionMode = AcDcDistributionMode.Auto,
|
//
|
||||||
};
|
// public static readonly TruConvertDcControl TruConvertDcControl = new()
|
||||||
|
// {
|
||||||
public static readonly TruConvertDcControl TruConvertDcControl = new()
|
// Date = default,
|
||||||
{
|
// Time = default,
|
||||||
Date = default,
|
// IpAddress = default,
|
||||||
Time = default,
|
// Subnet = default,
|
||||||
IpAddress = default,
|
// Gateway = default,
|
||||||
Subnet = default,
|
// ResetParamToDefault = false ,
|
||||||
Gateway = default,
|
// TimeoutForCommunication = TimeSpan.FromSeconds(20) ,
|
||||||
ResetParamToDefault = false ,
|
// RestartFlag = false ,
|
||||||
TimeoutForCommunication = TimeSpan.FromSeconds(10) ,
|
// ConnectedSystemConfig = Lib.Devices.Trumpf.TruConvert.SystemConfig.DcDcOnly,
|
||||||
RestartFlag = false ,
|
// UpdateSwTrigger = 0,
|
||||||
ConnectedSystemConfig = Lib.Devices.Trumpf.TruConvert.SystemConfig.DcDcOnly,
|
// AutomaticSwUpdate = 0,
|
||||||
UpdateSwTrigger = 0,
|
// CustomerValuesSaveReset = 0,
|
||||||
AutomaticSwUpdate = 0,
|
// SerialNumberSystemControl = 0,
|
||||||
CustomerValuesSaveReset = 0,
|
// SerialNumberDcDc = 0,
|
||||||
SerialNumberSystemControl = 0,
|
// MaterialNumberDcDc = 0,
|
||||||
SerialNumberDcDc = 0,
|
// PowerStageEnable = true,
|
||||||
MaterialNumberDcDc = 0,
|
// ResetsAlarmAndWarning = false,
|
||||||
PowerStageEnable = true,
|
// SlaveAddress = 1,
|
||||||
ResetsAlarmAndWarning = false,
|
// SubSlaveAddress = 0,
|
||||||
SlaveAddress = 1,
|
// ModbusSlaveId = false,
|
||||||
SubSlaveAddress = 0,
|
// MaximumBatteryVoltage = 57m,
|
||||||
ModbusSlaveId = false,
|
// MinimumBatteryVoltage = 42,
|
||||||
MaximumBatteryVoltage = 56,
|
// MaximumBatteryChargingCurrent = 210,
|
||||||
MinimumBatteryVoltage = 42,
|
// MaximumBatteryDischargingCurrent = 210,
|
||||||
MaximumBatteryChargingCurrent = 208,
|
// MaximumVoltageAlarmThreshold = 60,
|
||||||
MaximumBatteryDischargingCurrent = 208,
|
// MinimumVoltageAlarmThreshold = 0,
|
||||||
MaximumVoltageAlarmThreshold = 60,
|
// MaximalPowerAtDc = 10000,
|
||||||
MinimumVoltageAlarmThreshold = 0,
|
// BatteryCurrentSet = 0,
|
||||||
MaximalPowerAtDc = 9000,
|
// DynamicCurrentPerMillisecond = 100,
|
||||||
BatteryCurrentSet = 0,
|
// DcLinkControlMode = 1,
|
||||||
DynamicCurrentPerMillisecond = 2,
|
// ReferenceVoltage = 720,
|
||||||
DcLinkControlMode = 1,
|
// UpperVoltageWindow = 55,
|
||||||
ReferenceVoltage = 800,
|
// LowerVoltageWindow = 55,
|
||||||
UpperVoltageWindow = 40,
|
// VoltageDeadBand = 0,
|
||||||
LowerVoltageWindow = 40,
|
// };
|
||||||
VoltageDeadBand = 0,
|
//
|
||||||
};
|
// public static readonly SaliMaxRelayControl SaliMaxRelayControl = new()
|
||||||
}
|
// {
|
||||||
|
// K2Control = HighActivePinState.Closed
|
||||||
|
// };
|
||||||
|
// }
|
|
@ -1,65 +0,0 @@
|
||||||
using System.Text.Json;
|
|
||||||
using InnovEnergy.Lib.Time.Unix;
|
|
||||||
using InnovEnergy.Lib.Utils;
|
|
||||||
using static System.Text.Json.JsonSerializer;
|
|
||||||
|
|
||||||
namespace InnovEnergy.App.SaliMax.SystemConfig;
|
|
||||||
|
|
||||||
// shut up trim warnings
|
|
||||||
#pragma warning disable IL2026
|
|
||||||
|
|
||||||
public record SalimaxConfig //TODO: let IE choose from config files (Json) and connect to GUI
|
|
||||||
{
|
|
||||||
private static String DefaultConfigFilePath => Path.Combine(Environment.CurrentDirectory, "config.json");
|
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
|
||||||
|
|
||||||
public Decimal MinSoc { get; set; }
|
|
||||||
public UnixTime LastEoc { get; set; }
|
|
||||||
public Decimal PConstant { get; set; }
|
|
||||||
public Decimal ForceChargePower { get; set; }
|
|
||||||
public Decimal ForceDischargePower { get; set; }
|
|
||||||
public Int32 MaxInverterPower { get; set; }
|
|
||||||
public Decimal GridSetPoint { get; set; }
|
|
||||||
public Decimal SelfDischargePower { get; set; }
|
|
||||||
public Decimal HoldSocZone { get; set; }
|
|
||||||
public Decimal ControllerPConstant { get; set; }
|
|
||||||
|
|
||||||
public static SalimaxConfig Default => new()
|
|
||||||
{
|
|
||||||
MinSoc = 20m,
|
|
||||||
LastEoc = UnixTime.Epoch,
|
|
||||||
PConstant = .5m,
|
|
||||||
ForceChargePower = 1_000_000m,
|
|
||||||
ForceDischargePower = -1_000_000m,
|
|
||||||
MaxInverterPower = 32_000,
|
|
||||||
GridSetPoint = 0.0m,
|
|
||||||
SelfDischargePower = 200m, // TODO: multiple batteries
|
|
||||||
HoldSocZone = 1m, // TODO: find better name,
|
|
||||||
ControllerPConstant = 0.5m
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
public Task Save(String? path = null)
|
|
||||||
{
|
|
||||||
//DefaultConfigFilePath.WriteLine("Saving data");
|
|
||||||
var jsonString = Serialize(this, JsonOptions);
|
|
||||||
return File.WriteAllTextAsync(path ?? DefaultConfigFilePath, jsonString);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SalimaxConfig> Load(String? path = null)
|
|
||||||
{
|
|
||||||
var configFilePath = path ?? DefaultConfigFilePath;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var jsonString = await File.ReadAllTextAsync(configFilePath);
|
|
||||||
return Deserialize<SalimaxConfig>(jsonString)!;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Couldn't read config file {configFilePath}, using default config");
|
|
||||||
e.Message.WriteLine();
|
|
||||||
return Default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,241 +0,0 @@
|
||||||
#define BatteriesAllowed
|
|
||||||
|
|
||||||
using InnovEnergy.App.SaliMax.Controller;
|
|
||||||
using InnovEnergy.Lib.Utils;
|
|
||||||
using InnovEnergy.Lib.Units;
|
|
||||||
|
|
||||||
|
|
||||||
namespace InnovEnergy.App.SaliMax;
|
|
||||||
|
|
||||||
public static class Topology
|
|
||||||
{
|
|
||||||
private static String Separator(Decimal power)
|
|
||||||
{
|
|
||||||
const String chargingSeparator = ">>>>>>>>>>";
|
|
||||||
const String dischargingSeparator = "<<<<<<<<<";
|
|
||||||
|
|
||||||
return power > 0 ? chargingSeparator : dischargingSeparator;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Decimal Round3(this Decimal d)
|
|
||||||
{
|
|
||||||
return d.RoundToSignificantDigits(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Print(StatusRecord s)
|
|
||||||
{
|
|
||||||
const Int32 height = 25;
|
|
||||||
|
|
||||||
var calculatedActivePwr = - s.InverterStatus!.Ac.ActivePower;
|
|
||||||
var measuredActivePwr = (s.InverterStatus.SumActivePowerL1 + s.InverterStatus.SumActivePowerL2 +
|
|
||||||
s.InverterStatus.SumActivePowerL3) * -1;
|
|
||||||
|
|
||||||
measuredActivePwr.WriteLine(" : measured Sum of Active Pwr ");
|
|
||||||
|
|
||||||
var setValueCosPhi = s.InverterStatus.CosPhiSetValue;
|
|
||||||
var setValueApparentPower = s.InverterStatus.ApparentPowerSetValue;
|
|
||||||
|
|
||||||
|
|
||||||
#if AmptAvailable
|
|
||||||
var pvPower = (s.AmptStatus!.Devices[0].Dc.Voltage * s.AmptStatus.Devices[0].Dc.Current + s.AmptStatus!.Devices[1].Dc.Voltage * s.AmptStatus.Devices[1].Dc.Current).Round0(); // TODO using one Ampt
|
|
||||||
#else
|
|
||||||
var pvPower = 0;
|
|
||||||
#endif
|
|
||||||
var criticalLoadPower = (s.AcInToAcOutMeterStatus!.Ac.ActivePower.Value).Round3();
|
|
||||||
|
|
||||||
var dcTotalPower = -s.DcDcStatus!.TotalDcPower;
|
|
||||||
var gridSeparator = Separator(s.GridMeterStatus!.Ac.ActivePower);
|
|
||||||
var inverterSeparator = Separator(measuredActivePwr);
|
|
||||||
var dcSeparator = Separator(dcTotalPower);
|
|
||||||
var something = measuredActivePwr + criticalLoadPower;
|
|
||||||
var gridLoadPower = (s.GridMeterStatus!.Ac.ActivePower - something).Value.Round3();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
////////////////// Grid //////////////////////
|
|
||||||
var boxGrid = AsciiArt.CreateBox
|
|
||||||
(
|
|
||||||
"Grid",
|
|
||||||
s.GridMeterStatus.Ac.L1.Voltage.Value.V(),
|
|
||||||
s.GridMeterStatus.Ac.L2.Voltage.Value.V(),
|
|
||||||
s.GridMeterStatus.Ac.L3.Voltage.Value.V()
|
|
||||||
).AlignCenterVertical(height);
|
|
||||||
|
|
||||||
var gridAcBusArrow = AsciiArt.CreateHorizontalArrow(s.GridMeterStatus!.Ac.ActivePower, gridSeparator)
|
|
||||||
.AlignCenterVertical(height);
|
|
||||||
|
|
||||||
|
|
||||||
////////////////// Ac Bus //////////////////////
|
|
||||||
var boxAcBus = AsciiArt.CreateBox
|
|
||||||
(
|
|
||||||
"AC Bus",
|
|
||||||
s.InverterStatus.Ac.L1.Voltage.Value.V(),
|
|
||||||
s.InverterStatus.Ac.L2.Voltage.Value.V(),
|
|
||||||
s.InverterStatus.Ac.L3.Voltage.Value.V()
|
|
||||||
);
|
|
||||||
|
|
||||||
var boxLoad = AsciiArt.CreateBox
|
|
||||||
(
|
|
||||||
"",
|
|
||||||
"LOAD",
|
|
||||||
""
|
|
||||||
);
|
|
||||||
|
|
||||||
var loadRect = StringUtils.AlignBottom(CreateRect(boxAcBus, boxLoad, gridLoadPower), height);
|
|
||||||
|
|
||||||
var acBusInvertArrow = AsciiArt.CreateHorizontalArrow(measuredActivePwr, inverterSeparator)
|
|
||||||
.AlignCenterVertical(height);
|
|
||||||
|
|
||||||
//////////////////// Inverter /////////////////////////
|
|
||||||
var inverterBox = AsciiArt.CreateBox
|
|
||||||
(
|
|
||||||
"",
|
|
||||||
"Inverter",
|
|
||||||
""
|
|
||||||
).AlignCenterVertical(height);
|
|
||||||
|
|
||||||
var inverterArrow = AsciiArt.CreateHorizontalArrow(measuredActivePwr, inverterSeparator)
|
|
||||||
.AlignCenterVertical(height);
|
|
||||||
|
|
||||||
|
|
||||||
//////////////////// DC Bus /////////////////////////
|
|
||||||
var dcBusBox = AsciiArt.CreateBox
|
|
||||||
(
|
|
||||||
"DC Bus",
|
|
||||||
(s.InverterStatus.ActualDcLinkVoltageLowerHalfExt.Value + s.InverterStatus.ActualDcLinkVoltageUpperHalfExt.Value).V(),
|
|
||||||
""
|
|
||||||
);
|
|
||||||
|
|
||||||
var pvBox = AsciiArt.CreateBox
|
|
||||||
(
|
|
||||||
"MPPT",
|
|
||||||
((s.AmptStatus!.Devices[0].Strings[0].Voltage.Value + s.AmptStatus!.Devices[0].Strings[1].Voltage.Value) / 2).V(),
|
|
||||||
""
|
|
||||||
);
|
|
||||||
|
|
||||||
var pvRect = StringUtils.AlignTop(CreateRect(pvBox, dcBusBox, pvPower), height);
|
|
||||||
|
|
||||||
var dcBusArrow = AsciiArt.CreateHorizontalArrow(-s.DcDcStatus!.Left.Power, dcSeparator)
|
|
||||||
.AlignCenterVertical(height);
|
|
||||||
|
|
||||||
//////////////////// Dc/Dc /////////////////////////
|
|
||||||
|
|
||||||
var dcBox = AsciiArt.CreateBox( "Dc/Dc", s.DcDcStatus.Right.Voltage.Value.V(), "").AlignCenterVertical(height);
|
|
||||||
|
|
||||||
var topology = "";
|
|
||||||
|
|
||||||
if (s.BatteriesStatus != null)
|
|
||||||
{
|
|
||||||
var numBatteries = s.BatteriesStatus.Children.Count;
|
|
||||||
|
|
||||||
// Create an array of battery arrows using LINQ
|
|
||||||
var dcArrows = s
|
|
||||||
.BatteriesStatus.Children
|
|
||||||
.Select(b => AsciiArt.CreateHorizontalArrow(b.Dc.Power, Separator(b.Dc.Power)))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
// Create a rectangle from the array of arrows and align it vertically
|
|
||||||
var dcArrowRect = CreateRect(dcArrows).AlignCenterVertical(height);
|
|
||||||
|
|
||||||
//////////////////// Batteries /////////////////////////
|
|
||||||
|
|
||||||
var batteryBox = new String[numBatteries];
|
|
||||||
|
|
||||||
for (var i = 0; i < numBatteries; i++)
|
|
||||||
{
|
|
||||||
if (s.BatteriesStatus.Children[i] != null)
|
|
||||||
{
|
|
||||||
batteryBox[i] = AsciiArt.CreateBox
|
|
||||||
(
|
|
||||||
"Battery " + (i+1),
|
|
||||||
s.BatteriesStatus.Children[i].Dc.Voltage .Value.V(),
|
|
||||||
s.BatteriesStatus.Children[i].Soc .Value.Percent(),
|
|
||||||
s.BatteriesStatus.Children[i].Temperature .Value.Celsius(),
|
|
||||||
s.BatteriesStatus.Children[i].Dc.Current .Value.A(),
|
|
||||||
s.BatteriesStatus.Children[i].TotalCurrent.Value.A()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
batteryBox[i] = AsciiArt.CreateBox
|
|
||||||
(
|
|
||||||
"Battery " + (i+1),
|
|
||||||
"not detected"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var batteryRect = CreateRect(batteryBox).AlignCenterVertical(height);
|
|
||||||
|
|
||||||
|
|
||||||
var avgBatteryBox = "";
|
|
||||||
|
|
||||||
if (s.BatteriesStatus.Combined != null)
|
|
||||||
{
|
|
||||||
avgBatteryBox = AsciiArt.CreateBox
|
|
||||||
(
|
|
||||||
"Batteries",
|
|
||||||
s.BatteriesStatus.Combined.CellsVoltage,
|
|
||||||
s.BatteriesStatus.Combined.Soc,
|
|
||||||
s.BatteriesStatus.Combined.Temperature,
|
|
||||||
s.BatteriesStatus.Combined.Dc.Current,
|
|
||||||
s.BatteriesStatus.Combined.Alarms.Count > 0 ? String.Join(Environment.NewLine, s.BatteriesStatus.Combined.Alarms) : "No Alarm"
|
|
||||||
).AlignCenterVertical(height);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
topology = boxGrid.SideBySideWith(gridAcBusArrow, "")
|
|
||||||
.SideBySideWith(loadRect, "")
|
|
||||||
.SideBySideWith(acBusInvertArrow, "")
|
|
||||||
.SideBySideWith(inverterBox, "")
|
|
||||||
.SideBySideWith(inverterArrow, "")
|
|
||||||
.SideBySideWith(pvRect, "")
|
|
||||||
.SideBySideWith(dcBusArrow, "")
|
|
||||||
.SideBySideWith(dcBox, "")
|
|
||||||
.SideBySideWith(dcArrowRect, "")
|
|
||||||
.SideBySideWith(batteryRect, "")
|
|
||||||
.SideBySideWith(avgBatteryBox, "")+ "\n";
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
topology = boxGrid.SideBySideWith(gridAcBusArrow, "")
|
|
||||||
.SideBySideWith(loadRect, "")
|
|
||||||
.SideBySideWith(acBusInvertArrow, "")
|
|
||||||
.SideBySideWith(inverterBox, "")
|
|
||||||
.SideBySideWith(inverterArrow, "")
|
|
||||||
.SideBySideWith(pvRect, "")
|
|
||||||
.SideBySideWith(dcBusArrow, "")
|
|
||||||
.SideBySideWith(dcBox, "") + "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine(topology);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String CreateRect(String boxTop, String boxBottom, Decimal power)
|
|
||||||
{
|
|
||||||
var powerArrow = AsciiArt.CreateVerticalArrow(power);
|
|
||||||
var boxes = new[] { boxTop, powerArrow, boxBottom };
|
|
||||||
var maxWidth = boxes.Max(l => l.Width());
|
|
||||||
|
|
||||||
var rect = boxes.Select(l => l.AlignCenterHorizontal(maxWidth)).JoinLines();
|
|
||||||
return rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String CreateRect(String boxTop, String boxBottom)
|
|
||||||
{
|
|
||||||
var boxes = new[] { boxTop, boxBottom };
|
|
||||||
var maxWidth = boxes.Max(l => l.Width());
|
|
||||||
|
|
||||||
var rect = boxes.Select(l => l.AlignCenterHorizontal(maxWidth)).JoinLines();
|
|
||||||
return rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String CreateRect(String[] boxes)
|
|
||||||
{
|
|
||||||
var maxWidth = boxes.Max(l => l.Width());
|
|
||||||
var rect = boxes.Select(l => l.AlignCenterHorizontal(maxWidth)).JoinLines();
|
|
||||||
return rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
using InnovEnergy.Lib.Units.Composite;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SaliMax.VirtualDevices;
|
||||||
|
|
||||||
|
public class AcPowerDevice
|
||||||
|
{
|
||||||
|
public AcPower Power { get; init; } = AcPower.Null;
|
||||||
|
|
||||||
|
public static AcPowerDevice Null => new AcPowerDevice();
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
using InnovEnergy.Lib.Units.Power;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SaliMax.VirtualDevices;
|
||||||
|
|
||||||
|
public class DcPowerDevice
|
||||||
|
{
|
||||||
|
public DcPower Power { get; init; } = DcPower.Null;
|
||||||
|
}
|
|
@ -1,41 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
host=ie-entwicklung@10.2.3.104
|
|
||||||
|
|
||||||
tunnel() {
|
|
||||||
name=$1
|
|
||||||
ip=$2
|
|
||||||
rPort=$3
|
|
||||||
lPort=$4
|
|
||||||
|
|
||||||
echo -n "localhost:$lPort $name "
|
|
||||||
ssh -nNTL "$lPort:$ip:$rPort" "$host" 2> /dev/null &
|
|
||||||
|
|
||||||
until nc -vz 127.0.0.1 $lPort 2> /dev/null
|
|
||||||
do
|
|
||||||
echo -n .
|
|
||||||
sleep 0.3
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "ok"
|
|
||||||
}
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
tunnel "Trumpf Inverter (http) " 192.168.1.2 80 7001
|
|
||||||
tunnel "Trumpf DCDC (http) " 192.168.1.3 80 7002
|
|
||||||
tunnel "Emu Meter (http) " 192.168.1.241 80 7003
|
|
||||||
tunnel "ADAM (http) " 192.168.1.242 80 7004
|
|
||||||
tunnel "AMPT (http) " 192.168.1.249 8080 7005
|
|
||||||
|
|
||||||
tunnel "Trumpf Inverter (modbus)" 192.168.1.2 502 5001
|
|
||||||
tunnel "Trumpf DCDC (modbus) " 192.168.1.3 502 5002
|
|
||||||
tunnel "Emu Meter (modbus) " 192.168.1.241 502 5003
|
|
||||||
tunnel "ADAM (modbus) " 192.168.1.242 502 5004
|
|
||||||
tunnel "AMPT (modbus) " 192.168.1.249 502 5005
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "press any key to close the tunnels ..."
|
|
||||||
read -r -n 1 -s
|
|
||||||
kill $(jobs -p)
|
|
||||||
echo "done"
|
|
5
csharp/App/SaliMax/tunneltoProto.sh → csharp/App/SaliMax/tunnelsToProto.sh
Normal file → Executable file
5
csharp/App/SaliMax/tunneltoProto.sh → csharp/App/SaliMax/tunnelsToProto.sh
Normal file → Executable file
|
@ -33,11 +33,12 @@ tunnel "Trumpf DCDC (modbus) " 10.0.3.1 502 5002
|
||||||
tunnel "Ext Emu Meter (modbus) " 10.0.4.1 502 5003
|
tunnel "Ext Emu Meter (modbus) " 10.0.4.1 502 5003
|
||||||
tunnel "Int Emu Meter " 10.0.4.2 502 5004
|
tunnel "Int Emu Meter " 10.0.4.2 502 5004
|
||||||
tunnel "AMPT (modbus) " 10.0.5.1 502 5005
|
tunnel "AMPT (modbus) " 10.0.5.1 502 5005
|
||||||
|
tunnel "Adam " 10.0.1.1 502 5006
|
||||||
|
tunnel "Batteries " 127.0.0.1 6855 5007
|
||||||
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "press any key to close the tunnels ..."
|
echo "press any key to close the tunnels ..."
|
||||||
read -r -n 1 -s
|
read -r -n 1 -s
|
||||||
kill $(jobs -p)
|
kill $(jobs -p)
|
||||||
echo "done"
|
echo "done"
|
|
@ -0,0 +1,45 @@
|
||||||
|
j#!/bin/bash
|
||||||
|
|
||||||
|
host=ie-entwicklung@10.2.3.104
|
||||||
|
|
||||||
|
tunnel() {
|
||||||
|
name=$1
|
||||||
|
ip=$2
|
||||||
|
rPort=$3
|
||||||
|
lPort=$4
|
||||||
|
|
||||||
|
echo -n "localhost:$lPort $name "
|
||||||
|
ssh -nNTL "$lPort:$ip:$rPort" "$host" 2> /dev/null &
|
||||||
|
|
||||||
|
until nc -vz 127.0.0.1 $lPort 2> /dev/null
|
||||||
|
do
|
||||||
|
echo -n .
|
||||||
|
sleep 0.3
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
tunnel "Trumpf Inverter (http) " 10.0.2.1 80 7001
|
||||||
|
tunnel "Trumpf DCDC (http) " 10.0.3.1 80 7002
|
||||||
|
tunnel "Ext Emu Meter (http) " 10.0.4.1 80 7003
|
||||||
|
tunnel "Int Emu Meter (http) " 10.0.4.2 80 7004
|
||||||
|
tunnel "AMPT (http) " 10.0.5.1 8080 7005
|
||||||
|
|
||||||
|
|
||||||
|
tunnel "Trumpf Inverter (modbus)" 10.0.2.1 502 4001
|
||||||
|
tunnel "Trumpf DCDC (modbus) " 10.0.3.1 502 4002
|
||||||
|
tunnel "Ext Emu Meter (modbus) " 10.0.4.1 502 4003
|
||||||
|
tunnel "Int Emu Meter " 10.0.4.2 502 4004
|
||||||
|
tunnel "AMPT (modbus) " 10.0.5.1 502 4005
|
||||||
|
tunnel "Adam " 10.0.1.1 502 4006
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "press any key to close the tunnels ..."
|
||||||
|
read -r -n 1 -s
|
||||||
|
kill $(jobs -p)
|
||||||
|
echo "done"
|
|
@ -9,12 +9,10 @@
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<InvariantGlobalization>true</InvariantGlobalization>
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
|
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
|
||||||
<RootNamespace>Please.reload.the.project.Rider.is.stupid</RootNamespace>
|
<RootNamespace>$([System.Text.RegularExpressions.Regex]::Match($(MSBuildProjectExtensionsPath), "[/\\]csharp[/\\].*[/\\]obj").ToString().Replace("/",".").Replace("\\",".").Replace(".csharp", $(Company)).Replace(".obj", ""))</RootNamespace>
|
||||||
<Authors>$(Company) Team</Authors>
|
<Authors>$(Company) Team</Authors>
|
||||||
|
<!-- <ProjectPath>$([System.Text.RegularExpressions.Regex]::Match($(MSBuildProjectExtensionsPath), "[/\\]csharp[/\\].*[/\\]obj").ToString().Replace("/",".").Replace("\\",".").Replace(".csharp", $(Company)).Replace(".obj", ""))</ProjectPath>-->
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(SolutionDir)' != ''">
|
|
||||||
<RootNamespace>$(Company).$(MSBuildProjectDirectory.Replace($(SolutionDir), "").Replace("/",".").Replace("\","."))</RootNamespace>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -73,7 +73,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Meta", "Meta", "{AED84693-C
|
||||||
../.gitignore = ../.gitignore
|
../.gitignore = ../.gitignore
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VrmGrabber", "App\VrmGrabber\VrmGrabber.csproj", "{4F9BB20B-8030-48AB-A37B-23796459D516}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Lib\Logging\Logging.csproj", "{1A56992B-CB72-490F-99A4-DF1186BA3A18}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SrcGen", "Lib\SrcGen\SrcGen.csproj", "{2E5409D6-59BD-446F-BB82-E7759DD8AADD}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Adam6360D", "Lib\Devices\Adam6360D\Adam6360D.csproj", "{A3C79247-4CAA-44BE-921E-7285AB39E71F}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|
||||||
Global
|
Global
|
||||||
|
@ -186,10 +190,18 @@ Global
|
||||||
{B816BB44-E97E-4E02-B80A-BEDB5B923A96}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B816BB44-E97E-4E02-B80A-BEDB5B923A96}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{B816BB44-E97E-4E02-B80A-BEDB5B923A96}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B816BB44-E97E-4E02-B80A-BEDB5B923A96}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{B816BB44-E97E-4E02-B80A-BEDB5B923A96}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B816BB44-E97E-4E02-B80A-BEDB5B923A96}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{4F9BB20B-8030-48AB-A37B-23796459D516}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{1A56992B-CB72-490F-99A4-DF1186BA3A18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{4F9BB20B-8030-48AB-A37B-23796459D516}.Release|Any CPU.Build.0 = Release|Any CPU
|
{1A56992B-CB72-490F-99A4-DF1186BA3A18}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{4F9BB20B-8030-48AB-A37B-23796459D516}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{1A56992B-CB72-490F-99A4-DF1186BA3A18}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{4F9BB20B-8030-48AB-A37B-23796459D516}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{1A56992B-CB72-490F-99A4-DF1186BA3A18}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{2E5409D6-59BD-446F-BB82-E7759DD8AADD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{2E5409D6-59BD-446F-BB82-E7759DD8AADD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{2E5409D6-59BD-446F-BB82-E7759DD8AADD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{2E5409D6-59BD-446F-BB82-E7759DD8AADD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A3C79247-4CAA-44BE-921E-7285AB39E71F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A3C79247-4CAA-44BE-921E-7285AB39E71F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A3C79247-4CAA-44BE-921E-7285AB39E71F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A3C79247-4CAA-44BE-921E-7285AB39E71F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{CF4834CB-91B7-4172-AC13-ECDA8613CD17} = {145597B4-3E30-45E6-9F72-4DD43194539A}
|
{CF4834CB-91B7-4172-AC13-ECDA8613CD17} = {145597B4-3E30-45E6-9F72-4DD43194539A}
|
||||||
|
@ -222,6 +234,8 @@ Global
|
||||||
{C04FB6DA-23C6-46BB-9B21-8F4FBA32FFF7} = {AD5B98A8-AB7F-4DA2-B66D-5B4E63E7D854}
|
{C04FB6DA-23C6-46BB-9B21-8F4FBA32FFF7} = {AD5B98A8-AB7F-4DA2-B66D-5B4E63E7D854}
|
||||||
{4A67D79F-F0C9-4BBC-9601-D5948E6C05D3} = {AD5B98A8-AB7F-4DA2-B66D-5B4E63E7D854}
|
{4A67D79F-F0C9-4BBC-9601-D5948E6C05D3} = {AD5B98A8-AB7F-4DA2-B66D-5B4E63E7D854}
|
||||||
{B816BB44-E97E-4E02-B80A-BEDB5B923A96} = {DDDBEFD0-5DEA-4C7C-A9F2-FDB4636CF092}
|
{B816BB44-E97E-4E02-B80A-BEDB5B923A96} = {DDDBEFD0-5DEA-4C7C-A9F2-FDB4636CF092}
|
||||||
{4F9BB20B-8030-48AB-A37B-23796459D516} = {145597B4-3E30-45E6-9F72-4DD43194539A}
|
{1A56992B-CB72-490F-99A4-DF1186BA3A18} = {AD5B98A8-AB7F-4DA2-B66D-5B4E63E7D854}
|
||||||
|
{2E5409D6-59BD-446F-BB82-E7759DD8AADD} = {AD5B98A8-AB7F-4DA2-B66D-5B4E63E7D854}
|
||||||
|
{A3C79247-4CAA-44BE-921E-7285AB39E71F} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
@ -11,12 +11,14 @@
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ccgxsupport/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=ccgxsupport/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=cpio/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=cpio/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=dcdc/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=dcdc/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Deadband/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Dicts/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Dicts/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=fslckout/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=fslckout/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=gdiff/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=gdiff/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Genset/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Genset/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Hmmss/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Hmmss/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ifconfig/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=ifconfig/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Igbt/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Innov/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Innov/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Insts/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Insts/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Leds/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Leds/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
@ -35,8 +37,10 @@
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=proxyport/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=proxyport/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=resultset/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=resultset/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Salimax/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Salimax/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Setpoint/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=signurl/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=signurl/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Smpt/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Smpt/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Stati/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Trumpf/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Trumpf/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ttyusb/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=ttyusb/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=tupled/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=tupled/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<!-- <Import Project="../../InnovEnergy.Lib.props"/>-->
|
||||||
<Configurations>Debug;Release;Release-Server</Configurations>
|
|
||||||
<Platforms>AnyCPU;linux-arm</Platforms>
|
<Import Project="../../../App/InnovEnergy.App.props" />
|
||||||
</PropertyGroup>
|
|
||||||
<Import Project="../../InnovEnergy.Lib.props" />
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="../../Protocols/Modbus/Modbus.csproj" />
|
<ProjectReference Include="../../Protocols/Modbus/Modbus.csproj" />
|
||||||
|
|
|
@ -1,118 +1,90 @@
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
// using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Connections;
|
// using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
using InnovEnergy.Lib.Units.Composite;
|
// using InnovEnergy.Lib.Protocols.Modbus.Conversions;
|
||||||
using static DecimalMath.DecimalEx;
|
// using InnovEnergy.Lib.Protocols.Modbus.Slaves;
|
||||||
|
// using InnovEnergy.Lib.Units.Composite;
|
||||||
namespace InnovEnergy.Lib.Devices.AMPT;
|
//
|
||||||
|
// namespace InnovEnergy.Lib.Devices.AMPT;
|
||||||
public class AmptCommunicationUnit
|
//
|
||||||
{
|
// public class AmptCommunicationUnit : ModbusDevice<CommunicationUnitRegisters>
|
||||||
private ModbusTcpClient? Modbus { get; set; }
|
// {
|
||||||
|
//
|
||||||
private const UInt16 RegistersPerDevice = 16;
|
// private const UInt16 RegistersPerDevice = 16;
|
||||||
private const UInt16 FirstDeviceOffset = 85;
|
// private const UInt16 FirstDeviceOffset = 85;
|
||||||
|
//
|
||||||
public String Hostname { get; }
|
//
|
||||||
public UInt16 Port { get; }
|
// public AmptCommunicationUnit(String hostname, UInt16 port = 502, Byte slaveAddress = 1) : this
|
||||||
public Byte SlaveAddress { get; }
|
// (
|
||||||
|
// channel: new TcpChannel(hostname, port),
|
||||||
public AmptCommunicationUnit(String hostname, UInt16 port = 502, Byte slaveAddress = 1)
|
// slaveAddress
|
||||||
{
|
// )
|
||||||
Hostname = hostname;
|
// {}
|
||||||
Port = port;
|
//
|
||||||
SlaveAddress = slaveAddress;
|
//
|
||||||
}
|
// public AmptCommunicationUnit(Channel channel, Byte slaveAddress) : this
|
||||||
|
// (
|
||||||
public AmptCommunicationUnitStatus? ReadStatus()
|
// client: new ModbusTcpClient(channel, slaveAddress)
|
||||||
{
|
// )
|
||||||
try
|
// {}
|
||||||
{
|
//
|
||||||
OpenConnection();
|
// public AmptCommunicationUnit(ModbusClient client) : base(client)
|
||||||
return TryReadStatus();
|
// {
|
||||||
}
|
// }
|
||||||
catch
|
//
|
||||||
{
|
//
|
||||||
CloseConnection();
|
// private AmptCommunicationUnitStatus TryReadStatus()
|
||||||
return null;
|
// {
|
||||||
}
|
// var r = new ModbusRegisters(116, Modbus.ReadHoldingRegisters(1, 116).ToArray()) ; // TODO
|
||||||
}
|
//
|
||||||
|
// var currentFactor = Pow(10.0m, r.GetInt16(73));
|
||||||
private void CloseConnection()
|
// var voltageFactor = Pow(10.0m, r.GetInt16(74));
|
||||||
{
|
// var energyFactor = Pow(10.0m, r.GetInt16(76) + 3); // +3 => converted from Wh to kWh
|
||||||
try
|
// var nbrOfDevices = r.GetUInt16(78);
|
||||||
{
|
//
|
||||||
Modbus?.CloseConnection();
|
// var devices = Enumerable
|
||||||
}
|
// .Range(0, nbrOfDevices)
|
||||||
catch
|
// .Select(ReadDeviceStatus)
|
||||||
{
|
// .ToList();
|
||||||
// ignored
|
//
|
||||||
}
|
// return new AmptCommunicationUnitStatus
|
||||||
|
// {
|
||||||
Modbus = null;
|
// Sid = r.GetUInt32(1),
|
||||||
}
|
// IdSunSpec = r.GetUInt16(3),
|
||||||
|
// Manufacturer = r.GetString(5, 16),
|
||||||
private void OpenConnection()
|
// Model = r.GetString(21, 16),
|
||||||
{
|
// Version = r.GetString(45, 8),
|
||||||
if (Modbus is null)
|
// SerialNumber = r.GetString(53, 16),
|
||||||
{
|
// DeviceAddress = r.GetInt16(69),
|
||||||
var connection = new ModbusTcpConnection(Hostname, Port);
|
// IdVendor = r.GetUInt16(71),
|
||||||
Modbus = new ModbusTcpClient(connection, SlaveAddress);
|
// Devices = devices
|
||||||
}
|
// };
|
||||||
}
|
//
|
||||||
|
// AmptStatus ReadDeviceStatus(Int32 deviceNumber)
|
||||||
private AmptCommunicationUnitStatus TryReadStatus()
|
// {
|
||||||
{
|
// var baseAddress = (UInt16)(FirstDeviceOffset + deviceNumber * RegistersPerDevice); // base address
|
||||||
var r = Modbus!.ReadHoldingRegisters(1, 116);
|
//
|
||||||
|
// return new AmptStatus
|
||||||
var currentFactor = Pow(10.0m, r.GetInt16(73));
|
// {
|
||||||
var voltageFactor = Pow(10.0m, r.GetInt16(74));
|
// Dc = new DcBus
|
||||||
var energyFactor = Pow(10.0m, r.GetInt16(76) + 3); // +3 => converted from Wh to kWh
|
// {
|
||||||
var nbrOfDevices = r.GetUInt16(78);
|
// Voltage = r.GetUInt32((UInt16)(baseAddress + 6)) * voltageFactor,
|
||||||
|
// Current = r.GetUInt16((UInt16)(baseAddress + 5)) * currentFactor
|
||||||
var devices = Enumerable
|
// },
|
||||||
.Range(0, nbrOfDevices)
|
// Strings = new DcBus[]
|
||||||
.Select(ReadDeviceStatus)
|
// {
|
||||||
.ToList();
|
// new()
|
||||||
|
// {
|
||||||
return new AmptCommunicationUnitStatus
|
// Voltage = r.GetUInt32((UInt16)(baseAddress + 8)) * voltageFactor,
|
||||||
{
|
// Current = r.GetUInt16((UInt16)(baseAddress + 14)) * currentFactor
|
||||||
Sid = r.GetUInt32(1),
|
// },
|
||||||
IdSunSpec = r.GetUInt16(3),
|
// new()
|
||||||
Manufacturer = r.GetString(5, 16),
|
// {
|
||||||
Model = r.GetString(21, 16),
|
// Voltage = r.GetUInt32((UInt16)(baseAddress + 9)) * voltageFactor,
|
||||||
Version = r.GetString(45, 8),
|
// Current = r.GetUInt16((UInt16)(baseAddress + 15)) * currentFactor
|
||||||
SerialNumber = r.GetString(53, 16),
|
// }
|
||||||
DeviceAddress = r.GetInt16(69),
|
// },
|
||||||
IdVendor = r.GetUInt16(71),
|
// ProductionToday = r.GetUInt32((UInt16)(baseAddress + 12)) * energyFactor,
|
||||||
Devices = devices
|
// };
|
||||||
};
|
// }
|
||||||
|
// }
|
||||||
AmptStatus ReadDeviceStatus(Int32 deviceNumber)
|
// }
|
||||||
{
|
|
||||||
var baseAddress = (UInt16)(FirstDeviceOffset + deviceNumber * RegistersPerDevice); // base address
|
|
||||||
|
|
||||||
return new AmptStatus
|
|
||||||
{
|
|
||||||
Dc = new DcBus
|
|
||||||
{
|
|
||||||
Voltage = r.GetUInt32((UInt16)(baseAddress + 6)) * voltageFactor,
|
|
||||||
Current = r.GetUInt16((UInt16)(baseAddress + 5)) * currentFactor
|
|
||||||
},
|
|
||||||
Strings = new DcBus[]
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Voltage = r.GetUInt32((UInt16)(baseAddress + 8)) * voltageFactor,
|
|
||||||
Current = r.GetUInt16((UInt16)(baseAddress + 14)) * currentFactor
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Voltage = r.GetUInt32((UInt16)(baseAddress + 9)) * voltageFactor,
|
|
||||||
Current = r.GetUInt16((UInt16)(baseAddress + 15)) * currentFactor
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ProductionToday = r.GetUInt32((UInt16)(baseAddress + 12)) * energyFactor,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
namespace InnovEnergy.Lib.Devices.AMPT;
|
namespace InnovEnergy.Lib.Devices.AMPT;
|
||||||
|
|
||||||
public record AmptCommunicationUnitStatus
|
public class AmptCommunicationUnitStatus
|
||||||
{
|
{
|
||||||
public UInt32 Sid { get; init; } // A well-known value 0x53756e53, uniquely identifies this as a SunSpec Modbus Map
|
public UInt32 Sid { get; init; } // A well-known value 0x53756e53, uniquely identifies this as a SunSpec Modbus Map
|
||||||
public UInt16 IdSunSpec { get; init; } // A well-known value 1, uniquely identifies this as a SunSpec Common Model
|
public UInt16 IdSunSpec { get; init; } // A well-known value 1, uniquely identifies this as a SunSpec Common Model
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Slaves;
|
||||||
|
using InnovEnergy.Lib.Units.Composite;
|
||||||
|
using InnovEnergy.Lib.Utils;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.AMPT;
|
||||||
|
|
||||||
|
public class AmptDevices
|
||||||
|
{
|
||||||
|
private readonly ModbusDevice<CommunicationUnitRegisters> _CommunicationUnit;
|
||||||
|
private readonly IEnumerable<ModbusDevice<StringOptimizerRegisters>> _StringOptimizers;
|
||||||
|
|
||||||
|
public AmptDevices(String hostname, UInt16 port = 502) : this(new TcpChannel(hostname, port))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmptDevices(Channel transport) : this(new ModbusTcpClient(transport, 2))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public AmptDevices(ModbusClient modbusClient)
|
||||||
|
{
|
||||||
|
_CommunicationUnit = new ModbusDevice<CommunicationUnitRegisters>(modbusClient);
|
||||||
|
_StringOptimizers = StringOptimizers(modbusClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmptStatus Read()
|
||||||
|
{
|
||||||
|
CommunicationUnitRegisters? cuStatus = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cuStatus = _CommunicationUnit.Read();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
// TODO: log
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommunicationUnit knows how many StringOptimizers are connected
|
||||||
|
var nStringOptimizers = cuStatus?.NumberOfStringOptimizers ?? 0;
|
||||||
|
|
||||||
|
// hardcoded: every SO has 2 strings (produced like this by AMPT)
|
||||||
|
var nStrings = nStringOptimizers * 2;
|
||||||
|
|
||||||
|
// read stati from optimizers
|
||||||
|
var soStati = _StringOptimizers
|
||||||
|
.Take(nStringOptimizers)
|
||||||
|
.Select(so => so.Read())
|
||||||
|
.ToArray(nStringOptimizers);
|
||||||
|
|
||||||
|
// every SO has 2 strings but ONE Dc Link Connection
|
||||||
|
// they are connected to a shared Dc Link, so Voltage seen by them should be approx the same.
|
||||||
|
// voltages are averaged, currents added
|
||||||
|
|
||||||
|
// TODO: alarm when we see substantially different voltages
|
||||||
|
|
||||||
|
var busVoltage = nStringOptimizers == 0 ? 0 : soStati.Average(r => r.Voltage);
|
||||||
|
var busCurrent = nStringOptimizers == 0 ? 0 : soStati.Sum (r => r.Current);
|
||||||
|
var dc = DcBus.FromVoltageCurrent(busVoltage, busCurrent);
|
||||||
|
|
||||||
|
// flatten the 2 strings of each SO into one array
|
||||||
|
var strings = soStati.SelectMany(GetStrings).ToArray(nStrings);
|
||||||
|
|
||||||
|
return new AmptStatus(dc, strings);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static IEnumerable<DcBus> GetStrings(StringOptimizerRegisters r)
|
||||||
|
{
|
||||||
|
// hardcoded: every SO has 2 strings (produced like this by AMPT)
|
||||||
|
|
||||||
|
yield return DcBus.FromVoltageCurrent(r.String1Voltage, r.String1Current);
|
||||||
|
yield return DcBus.FromVoltageCurrent(r.String2Voltage, r.String2Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static IEnumerable<ModbusDevice<StringOptimizerRegisters>> StringOptimizers(ModbusClient modbusClient)
|
||||||
|
{
|
||||||
|
var cache = new List<ModbusDevice<StringOptimizerRegisters>>();
|
||||||
|
|
||||||
|
ModbusDevice<StringOptimizerRegisters> GetOptimizer(Int32 i)
|
||||||
|
{
|
||||||
|
if (i < cache.Count)
|
||||||
|
return cache[i];
|
||||||
|
|
||||||
|
var modbusDevice = new ModbusDevice<StringOptimizerRegisters>(modbusClient, i * 16);
|
||||||
|
cache.Add(modbusDevice);
|
||||||
|
return modbusDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Enumerable
|
||||||
|
.Range(0, Byte.MaxValue)
|
||||||
|
.Select(GetOptimizer);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,38 @@
|
||||||
using InnovEnergy.Lib.StatusApi;
|
using InnovEnergy.Lib.Units.Composite;
|
||||||
using InnovEnergy.Lib.Units;
|
|
||||||
|
|
||||||
namespace InnovEnergy.Lib.Devices.AMPT;
|
namespace InnovEnergy.Lib.Devices.AMPT;
|
||||||
|
|
||||||
public record AmptStatus : MpptStatus
|
public class AmptStatus
|
||||||
{
|
{
|
||||||
public Energy ProductionToday { get; init; } // converted to kW in AmptCU class
|
public AmptStatus(DcBus? dc, IReadOnlyList<DcBus> strings)
|
||||||
|
{
|
||||||
|
Dc = dc;
|
||||||
|
Strings = strings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DcBus? Dc { get; }
|
||||||
|
public IReadOnlyList<DcBus> Strings { get; }
|
||||||
|
|
||||||
|
public static AmptStatus Null => new AmptStatus(null, Array.Empty<DcBus>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// public static AmptStatus Parallel(IReadOnlyList<AmptStatus> stati)
|
||||||
|
// {
|
||||||
|
// if (stati.Count == 0)
|
||||||
|
// {
|
||||||
|
// return new AmptStatus
|
||||||
|
// (
|
||||||
|
// Dc: DcBus.FromVoltageCurrent(0, 0),
|
||||||
|
// Strings: Array.Empty<DcBus>()
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var voltage = stati.Average(s => s.Dc.Voltage.Value);
|
||||||
|
// var current = stati.Sum(s => s.Dc.Current.Value);
|
||||||
|
// var dc = DcBus.FromVoltageCurrent(voltage, current);
|
||||||
|
//
|
||||||
|
// var strings = stati.SelectMany(s => s.Strings).ToList();
|
||||||
|
//
|
||||||
|
// return new AmptStatus(dc, strings);
|
||||||
|
// }
|
|
@ -0,0 +1,18 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.AMPT;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
|
||||||
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||||
|
|
||||||
|
|
||||||
|
[OneBasedAddressing]
|
||||||
|
public record CommunicationUnitRegisters
|
||||||
|
{
|
||||||
|
[HoldingRegister<Int16>(73)] public Int16 CurrentScaleFactor { get; private set; }
|
||||||
|
[HoldingRegister<Int16>(74)] public Int16 VoltageScaleFactor { get; private set; }
|
||||||
|
[HoldingRegister<Int16>(76)] public Int16 EnergyScaleFactor { get; private set; }
|
||||||
|
|
||||||
|
[HoldingRegister(78)] public UInt16 NumberOfStringOptimizers { get; private set; }
|
||||||
|
}
|
Binary file not shown.
|
@ -0,0 +1,412 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||||
|
<title>Modbus Map</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #565A5C;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.content {
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.textblock {
|
||||||
|
text-align: left;
|
||||||
|
padding: 0;
|
||||||
|
width: 40%;
|
||||||
|
position: relative;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.main {
|
||||||
|
border: solid black 2px;
|
||||||
|
border-spacing: 0;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 6px;
|
||||||
|
border: solid black 1px;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.even {
|
||||||
|
background: #EFEFEF;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.odd {
|
||||||
|
background: #DDDDDD;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.desc {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="content">
|
||||||
|
<br><br>
|
||||||
|
<h2>Modbus Map</h2>
|
||||||
|
<div class="textblock">
|
||||||
|
<p>These Modbus maps are for your reference. The modbus service runs on port 502.</p>
|
||||||
|
<p>Important note: The Ampt ModBus register map uses big endian values.</p>
|
||||||
|
<p><a href="smdx_64050.xml">SunSpec SMDX File</a></p>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<h2> SunSpec Registers </h2>
|
||||||
|
<table class="main" style="width:70%">
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td><strong>Start Offset</strong></td>
|
||||||
|
<td><strong>Size</strong></td>
|
||||||
|
<td><strong>Name</strong></td>
|
||||||
|
<td><strong>Type</strong></td>
|
||||||
|
<td><strong>R/W</strong></td>
|
||||||
|
<td><strong>Description</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>1</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td>SID</td>
|
||||||
|
<td>uint32</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">A well-known value 0x53756e53, uniquely identifies this as a SunSpec Modbus Map</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>3</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>ID</td>
|
||||||
|
<td>uint16</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">A well-known value 1, uniquely identifies this as a SunSpec Common Model</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>4</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>L</td>
|
||||||
|
<td>uint16</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">Well-known # of 16-bit registers to follow: 66</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>5</td>
|
||||||
|
<td>16</td>
|
||||||
|
<td>Manufacturer</td>
|
||||||
|
<td>string</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">A well-known value registered with SunSpec for compliance: "Ampt"</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>21</td>
|
||||||
|
<td>16</td>
|
||||||
|
<td>Model</td>
|
||||||
|
<td>string</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">Manufacturer specific value "Communication Unit"</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>37</td>
|
||||||
|
<td>8</td>
|
||||||
|
<td><em>Reserved</em></td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>45</td>
|
||||||
|
<td>8</td>
|
||||||
|
<td>Version</td>
|
||||||
|
<td>string</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">Software Version</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>53</td>
|
||||||
|
<td>16</td>
|
||||||
|
<td>Serial Number</td>
|
||||||
|
<td>string</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">Manufacturer specific value</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>69</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>Device Address</td>
|
||||||
|
<td>int16</td>
|
||||||
|
<td>R/W</td>
|
||||||
|
<td class="desc">Modbus Device ID</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>70</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td><em>Reserved</em></td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>71</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>ID</td>
|
||||||
|
<td>uint16</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">Ampt SunSpec Vendor Code 64050</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>72</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>L</td>
|
||||||
|
<td>uint16</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">Variable number of 16-bit registers to follow: 12 + N*16</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>73</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>DCA_SF</td>
|
||||||
|
<td>int16</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">Current scale factor</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>74</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>DCV_SF</td>
|
||||||
|
<td>int16</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">Voltage scale factor</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>75</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td><em>Reserved</em></td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>76</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>DCkWh_SF</td>
|
||||||
|
<td>int16</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">Energy Scale Factor</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>77</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td><em>Reserved</em></td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>78</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>N</td>
|
||||||
|
<td>uint16</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">Number of strings</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>79</td>
|
||||||
|
<td>6</td>
|
||||||
|
<td><em>Reserved</em></td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="6"><strong>1</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>85</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>String ID</td>
|
||||||
|
<td>int16</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">The string number</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>86</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td><em>Reserved</em></td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>88</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td>String data timestamp</td>
|
||||||
|
<td>uint32</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">The UTC timestamp of the measurements</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>90</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>OutDCA</td>
|
||||||
|
<td>int16</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">String output current in mA</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>91</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td>OutDCV</td>
|
||||||
|
<td>uint32</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">String output voltage in mV</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>93</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td>In1DCV</td>
|
||||||
|
<td>uint32</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">String input 1 voltage in mV</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>95</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td>In2DCV</td>
|
||||||
|
<td>uint32</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">String input 2 voltage in mV</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>97</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td>DCWh</td>
|
||||||
|
<td>uint32</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">Daily integrated string output energy in Wh</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>99</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>In1DCA</td>
|
||||||
|
<td>int16</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">String input 1 current in mA</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>100</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>In2DCA</td>
|
||||||
|
<td>int16</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">String input 2 current in mA</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="6"><strong>2</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>101</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>String ID</td>
|
||||||
|
<td>int16</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">The string number</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>102</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td><em>Reserved</em></td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>104</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td>String data timestamp</td>
|
||||||
|
<td>uint32</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">The UTC timestamp of the measurements</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>106</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>OutDCA</td>
|
||||||
|
<td>int16</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">String output current in mA</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>107</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td>OutDCV</td>
|
||||||
|
<td>uint32</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">String output voltage in mV</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>109</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td>In1DCV</td>
|
||||||
|
<td>uint32</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">String input 1 voltage in mV</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>111</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td>In2DCV</td>
|
||||||
|
<td>uint32</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">String input 2 voltage in mV</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>113</td>
|
||||||
|
<td>2</td>
|
||||||
|
<td>DCWh</td>
|
||||||
|
<td>uint32</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">Daily integrated string output energy in Wh</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td>115</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>In1DCA</td>
|
||||||
|
<td>int16</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">String input 1 current in mA</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
|
<td>116</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>In2DCA</td>
|
||||||
|
<td>int16</td>
|
||||||
|
<td>R</td>
|
||||||
|
<td class="desc">String input 2 current in mA</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<br><br><br>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,104 @@
|
||||||
|
<sunSpecModels v="1">
|
||||||
|
<!-- 64050: Ampt Communication Unit -->
|
||||||
|
<model id="64050" len="28">
|
||||||
|
<block len="12">
|
||||||
|
<point id="DCA_SF" offset="0" type="sunssf"/>
|
||||||
|
<point id="DCV_SF" offset="1" type="sunssf"/>
|
||||||
|
<point id="DCkWh_SF" offset="3" type="sunssf"/>
|
||||||
|
<point id="N" offset="5" type="uint16"/>
|
||||||
|
</block>
|
||||||
|
<block type="repeating" len="16">
|
||||||
|
<point id="StringID" offset="0" type="int16"/>
|
||||||
|
<point id="StringDataTimestamp" offset="3" type="uint32"/>
|
||||||
|
<point id="OutDCA" offset="5" type="int16" sf="DCA_SF" units="A"/>
|
||||||
|
<point id="OutDCV" offset="6" type="uint32" sf="DCV_SF" units="V"/>
|
||||||
|
<point id="In1DCV" offset="8" type="uint32" sf="DCV_SF" units="V"/>
|
||||||
|
<point id="In2DCV" offset="10" type="uint32" sf="DCV_SF" units="V"/>
|
||||||
|
<point id="DCkWh" offset="12" type="uint32" sf="DCkWh_SF" units="kWh"/>
|
||||||
|
<point id="In1DCA" offset="14" type="int16" sf="DCA_SF" units="A"/>
|
||||||
|
<point id="In2DCA" offset="15" type="int16" sf="DCA_SF" units="A"/>
|
||||||
|
</block>
|
||||||
|
</model>
|
||||||
|
<strings id="64050" locale="en">
|
||||||
|
<model>
|
||||||
|
<label>Ampt Communication Unit</label>
|
||||||
|
<description/>
|
||||||
|
<notes/>
|
||||||
|
</model>
|
||||||
|
<point id="ID">
|
||||||
|
<label>ID</label>
|
||||||
|
<description>Ampt SunSpec Vendor Code 64050</description>
|
||||||
|
<notes/>
|
||||||
|
</point>
|
||||||
|
<point id="L">
|
||||||
|
<label>L</label>
|
||||||
|
<description>Variable number of 16-bit registers to follow: 12 + N*16</description>
|
||||||
|
<notes/>
|
||||||
|
</point>
|
||||||
|
<point id="DCA_SF">
|
||||||
|
<label>DCA_SF</label>
|
||||||
|
<description>Current Scale Factor</description>
|
||||||
|
<notes/>
|
||||||
|
</point>
|
||||||
|
<point id="DCV_SF">
|
||||||
|
<label>DCV_SF</label>
|
||||||
|
<description>Voltage Scale Factor</description>
|
||||||
|
<notes/>
|
||||||
|
</point>
|
||||||
|
<point id="DCkWh_SF">
|
||||||
|
<label>DCkWh_SF</label>
|
||||||
|
<description>Energy Scale Factor</description>
|
||||||
|
<notes/>
|
||||||
|
</point>
|
||||||
|
<point id="N">
|
||||||
|
<label>N</label>
|
||||||
|
<description>Number of strings</description>
|
||||||
|
<notes/>
|
||||||
|
</point>
|
||||||
|
<point id="StringID">
|
||||||
|
<label>StringID</label>
|
||||||
|
<description>The String Number</description>
|
||||||
|
<notes/>
|
||||||
|
</point>
|
||||||
|
<point id="StringDataTimestamp">
|
||||||
|
<label>StringDataTimestamp</label>
|
||||||
|
<description>UTC timestamp of measurements</description>
|
||||||
|
<notes/>
|
||||||
|
</point>
|
||||||
|
<point id="OutDCA">
|
||||||
|
<label>OutDCA</label>
|
||||||
|
<description>String output current in mA</description>
|
||||||
|
<notes/>
|
||||||
|
</point>
|
||||||
|
<point id="OutDCV">
|
||||||
|
<label>OutDCV</label>
|
||||||
|
<description>String output current in mV</description>
|
||||||
|
<notes/>
|
||||||
|
</point>
|
||||||
|
<point id="In1DCV">
|
||||||
|
<label>In1DCV</label>
|
||||||
|
<description>String input 1 in mV</description>
|
||||||
|
<notes/>
|
||||||
|
</point>
|
||||||
|
<point id="In2DCV">
|
||||||
|
<label>In2DCV</label>
|
||||||
|
<description>String input 2 in mV</description>
|
||||||
|
<notes/>
|
||||||
|
</point>
|
||||||
|
<point id="DCkWh">
|
||||||
|
<label>DCkWh</label>
|
||||||
|
<description>Daily integrated string output energy in watt-hours</description>
|
||||||
|
<notes/>
|
||||||
|
</point>
|
||||||
|
<point id="In1DCA">
|
||||||
|
<label>In1DCA</label>
|
||||||
|
<description>String input 1 in mA</description>
|
||||||
|
<notes/>
|
||||||
|
</point>
|
||||||
|
<point id="In2DCA">
|
||||||
|
<label>In2DCA</label>
|
||||||
|
<description>String input 1 in mA</description>
|
||||||
|
<notes/>
|
||||||
|
</point>
|
||||||
|
</strings>
|
||||||
|
</sunSpecModels>
|
|
@ -0,0 +1,28 @@
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
|
using InnovEnergy.Lib.Units;
|
||||||
|
using InnovEnergy.Lib.Utils;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.AMPT;
|
||||||
|
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
public static Task Main(string[] args)
|
||||||
|
{
|
||||||
|
var ch = new TcpChannel("localhost", 5005);
|
||||||
|
var cl = new ModbusTcpClient(ch, 1);
|
||||||
|
var d = new AmptDevices(cl);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
AmptStatus x = d.Read();
|
||||||
|
|
||||||
|
x.ToCsv().WriteLine();
|
||||||
|
|
||||||
|
//Console.WriteLine(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Console.WriteLine(x);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.AMPT;
|
||||||
|
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||||
|
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
|
||||||
|
|
||||||
|
[OneBasedAddressing][BigEndian]
|
||||||
|
public record StringOptimizerRegisters
|
||||||
|
{
|
||||||
|
[HoldingRegister<UInt32>(88) ] public UInt32 Timestamp { get; private set; }
|
||||||
|
|
||||||
|
[HoldingRegister<Int16> (90, Scale = .001)] public Double Current { get; private set; }
|
||||||
|
[HoldingRegister<UInt32>(91, Scale = .001)] public Double Voltage { get; private set; }
|
||||||
|
|
||||||
|
[HoldingRegister<UInt32>(93, Scale = .001)] public Double String1Voltage { get; private set; }
|
||||||
|
[HoldingRegister<UInt32>(95, Scale = .001)] public Double String2Voltage { get; private set; }
|
||||||
|
|
||||||
|
[HoldingRegister<UInt32>(97)] public Double ProductionToday { get; private set; }
|
||||||
|
|
||||||
|
[HoldingRegister<Int16>(99, Scale = .001)] public Double String1Current { get; private set; }
|
||||||
|
[HoldingRegister<Int16>(100, Scale = .001)] public Double String2Current { get; private set; }
|
||||||
|
}
|
|
@ -1,15 +0,0 @@
|
||||||
using System;
|
|
||||||
namespace InnovEnergy.Lib.Devices.Adam6060;
|
|
||||||
|
|
||||||
public class Adam6060Control
|
|
||||||
{
|
|
||||||
internal const UInt16 RelaysStartRegister = 17;
|
|
||||||
internal const UInt16 NbRelays = 6;
|
|
||||||
|
|
||||||
public Boolean Relay0 { get; init; } // Address(0X) 00017
|
|
||||||
public Boolean Relay1 { get; init; } // Address(0X) 00018
|
|
||||||
public Boolean Relay2 { get; init; } // Address(0X) 00019
|
|
||||||
public Boolean Relay3 { get; init; } // Address(0X) 00020
|
|
||||||
public Boolean Relay4 { get; init; } // Address(0X) 00021
|
|
||||||
public Boolean Relay5 { get; init; } // Address(0X) 00022
|
|
||||||
}
|
|
|
@ -1,103 +1,18 @@
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Connections;
|
using InnovEnergy.Lib.Protocols.Modbus.Slaves;
|
||||||
using static InnovEnergy.Lib.Devices.Adam6060.Adam6060Status;
|
|
||||||
using static InnovEnergy.Lib.Devices.Adam6060.Adam6060Control;
|
|
||||||
|
|
||||||
namespace InnovEnergy.Lib.Devices.Adam6060;
|
namespace InnovEnergy.Lib.Devices.Adam6060;
|
||||||
|
|
||||||
public class Adam6060Device
|
public class Adam6060Device : ModbusDevice<Adam6060Registers>
|
||||||
{
|
{
|
||||||
public String Hostname { get; }
|
|
||||||
public UInt16 Port { get; }
|
|
||||||
public Byte SlaveAddress { get; }
|
|
||||||
|
|
||||||
private ModbusTcpClient? Modbus { get; set; }
|
public Adam6060Device(String hostname, Byte slaveId, UInt16 port = 502) :
|
||||||
|
this(new TcpChannel(hostname, port), slaveId)
|
||||||
public Adam6060Device(String hostname, UInt16 port = 5004, Byte slaveAddress = 2)
|
|
||||||
{
|
{
|
||||||
Hostname = hostname;
|
|
||||||
Port = port;
|
|
||||||
SlaveAddress = slaveAddress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenConnection()
|
public Adam6060Device(Channel channel, Byte slaveId) : base(new ModbusTcpClient(channel, slaveId))
|
||||||
{
|
{
|
||||||
if (Modbus is null)
|
|
||||||
{
|
|
||||||
var connection = new ModbusTcpConnection(Hostname, Port);
|
|
||||||
Modbus = new ModbusTcpClient(connection, SlaveAddress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Adam6060Status? ReadStatus()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
OpenConnection();
|
|
||||||
return TryReadStatus();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
CloseConnection();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CloseConnection()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Modbus?.CloseConnection();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
Modbus = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Adam6060Status TryReadStatus()
|
|
||||||
{
|
|
||||||
var inputs = Modbus!.ReadDiscreteInputs(DigitalInputsStartRegister, NbDigitalInputs);
|
|
||||||
var relays = Modbus!.ReadDiscreteInputs(RelaysStartRegister, NbRelays);
|
|
||||||
|
|
||||||
return new Adam6060Status
|
|
||||||
{
|
|
||||||
DigitalInput0 = inputs[0],
|
|
||||||
DigitalInput1 = inputs[1],
|
|
||||||
DigitalInput2 = inputs[2],
|
|
||||||
DigitalInput3 = inputs[3],
|
|
||||||
DigitalInput4 = inputs[4],
|
|
||||||
DigitalInput5 = inputs[5],
|
|
||||||
|
|
||||||
Relay0 = relays[0],
|
|
||||||
Relay1 = relays[1],
|
|
||||||
Relay2 = relays[2],
|
|
||||||
Relay3 = relays[3],
|
|
||||||
Relay4 = relays[4],
|
|
||||||
Relay5 = relays[5],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean WriteControl(Adam6060Control control)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
OpenConnection();
|
|
||||||
|
|
||||||
Modbus!.WriteMultipleCoils(RelaysStartRegister, control.Relay0,
|
|
||||||
control.Relay1,
|
|
||||||
control.Relay2,
|
|
||||||
control.Relay3,
|
|
||||||
control.Relay4,
|
|
||||||
control.Relay5);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
CloseConnection();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.Adam6060;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
|
||||||
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||||
|
public class Adam6060Registers
|
||||||
|
{
|
||||||
|
[DiscreteInput(1)] public Boolean DigitalInput0 { get; private set; }
|
||||||
|
[DiscreteInput(2)] public Boolean DigitalInput1 { get; private set; }
|
||||||
|
[DiscreteInput(3)] public Boolean DigitalInput2 { get; private set; }
|
||||||
|
[DiscreteInput(4)] public Boolean DigitalInput3 { get; private set; }
|
||||||
|
[DiscreteInput(5)] public Boolean DigitalInput4 { get; private set; }
|
||||||
|
[DiscreteInput(6)] public Boolean DigitalInput5 { get; private set; }
|
||||||
|
|
||||||
|
[Coil(17)] public Boolean Relay0 { get; set; }
|
||||||
|
[Coil(18)] public Boolean Relay1 { get; set; }
|
||||||
|
[Coil(19)] public Boolean Relay2 { get; set; }
|
||||||
|
[Coil(20)] public Boolean Relay3 { get; set; }
|
||||||
|
[Coil(21)] public Boolean Relay4 { get; set; }
|
||||||
|
[Coil(22)] public Boolean Relay5 { get; set; }
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
namespace InnovEnergy.Lib.Devices.Adam6060;
|
|
||||||
|
|
||||||
public class Adam6060Status : Adam6060Control
|
|
||||||
{
|
|
||||||
internal const UInt16 DigitalInputsStartRegister = 1;
|
|
||||||
internal const UInt16 NbDigitalInputs = 6;
|
|
||||||
|
|
||||||
public Boolean DigitalInput0 { get; init; } //Address(0X) 00001
|
|
||||||
public Boolean DigitalInput1 { get; init; } //Address(0X) 00002
|
|
||||||
public Boolean DigitalInput2 { get; init; } //Address(0X) 00003
|
|
||||||
public Boolean DigitalInput3 { get; init; } //Address(0X) 00004
|
|
||||||
public Boolean DigitalInput4 { get; init; } //Address(0X) 00005
|
|
||||||
public Boolean DigitalInput5 { get; init; } //Address(0X) 00006
|
|
||||||
}
|
|
Binary file not shown.
|
@ -0,0 +1,16 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<!-- <Import Project="../../InnovEnergy.Lib.props" />-->
|
||||||
|
|
||||||
|
<Import Project="../../../App/InnovEnergy.App.props" />
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../../Protocols/Modbus/Modbus.csproj" />
|
||||||
|
<ProjectReference Include="../../Units/Units.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Slaves;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.Adam6360D;
|
||||||
|
|
||||||
|
public class Adam6360DDevice : ModbusDevice<Adam6360DRegisters>
|
||||||
|
{
|
||||||
|
|
||||||
|
public Adam6360DDevice(String hostname, Byte slaveId, UInt16 port = 502) :
|
||||||
|
this(new TcpChannel(hostname, port), slaveId)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Adam6360DDevice(Channel channel, Byte slaveId) : base(new ModbusTcpClient(channel, slaveId))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.Adam6360D;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
|
||||||
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||||
|
[AddressOffset(-1)]
|
||||||
|
public class Adam6360DRegisters
|
||||||
|
{
|
||||||
|
[DiscreteInput(1)] public Boolean DigitalInput0 { get; private set; }
|
||||||
|
[DiscreteInput(2)] public Boolean DigitalInput1 { get; private set; }
|
||||||
|
[DiscreteInput(3)] public Boolean DigitalInput2 { get; private set; }
|
||||||
|
[DiscreteInput(4)] public Boolean DigitalInput3 { get; private set; }
|
||||||
|
[DiscreteInput(5)] public Boolean DigitalInput4 { get; private set; }
|
||||||
|
[DiscreteInput(6)] public Boolean DigitalInput5 { get; private set; }
|
||||||
|
[DiscreteInput(7)] public Boolean DigitalInput6 { get; private set; }
|
||||||
|
[DiscreteInput(8)] public Boolean DigitalInput7 { get; private set; }
|
||||||
|
[DiscreteInput(9)] public Boolean DigitalInput8 { get; private set; }
|
||||||
|
[DiscreteInput(10)] public Boolean DigitalInput9 { get; private set; }
|
||||||
|
[DiscreteInput(11)] public Boolean DigitalInput10 { get; private set; }
|
||||||
|
[DiscreteInput(12)] public Boolean DigitalInput11 { get; private set; }
|
||||||
|
[DiscreteInput(13)] public Boolean DigitalInput12 { get; private set; }
|
||||||
|
[DiscreteInput(14)] public Boolean DigitalInput13 { get; private set; }
|
||||||
|
|
||||||
|
[Coil(33)] public Boolean Relay0 { get; set; }
|
||||||
|
[Coil(34)] public Boolean Relay1 { get; set; }
|
||||||
|
[Coil(35)] public Boolean Relay2 { get; set; }
|
||||||
|
[Coil(36)] public Boolean Relay3 { get; set; }
|
||||||
|
[Coil(37)] public Boolean Relay4 { get; set; }
|
||||||
|
[Coil(38)] public Boolean Relay5 { get; set; }
|
||||||
|
[Coil(39)] public Boolean Relay6 { get; set; }
|
||||||
|
[Coil(40)] public Boolean Relay7 { get; set; }
|
||||||
|
}
|
Binary file not shown.
|
@ -0,0 +1,20 @@
|
||||||
|
using InnovEnergy.Lib.Units;
|
||||||
|
using InnovEnergy.Lib.Utils;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.Adam6360D;
|
||||||
|
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
public static Task Main(String[] args)
|
||||||
|
{
|
||||||
|
var d = new Adam6360DDevice("localhost", 2, 5006);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var x = d.Read();
|
||||||
|
x.ToCsv().WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,12 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
|
||||||
<Configurations>Debug;Release;Release-Server</Configurations>
|
|
||||||
<Platforms>AnyCPU;linux-arm</Platforms>
|
|
||||||
</PropertyGroup>
|
|
||||||
<Import Project="../../InnovEnergy.Lib.props" />
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="../../Utils/Utils.csproj" />
|
|
||||||
<ProjectReference Include="../../Protocols/Modbus/Modbus.csproj" />
|
|
||||||
<ProjectReference Include="../../StatusApi/StatusApi.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
<!-- <Import Project="../../InnovEnergy.Lib.props" />-->
|
||||||
|
<Import Project="../../../App/InnovEnergy.App.props" />
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../../Protocols/Modbus/Modbus.csproj" />
|
||||||
|
<ProjectReference Include="../../StatusApi/StatusApi.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Project>
|
|
@ -1,48 +1,46 @@
|
||||||
|
using System.IO.Ports;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Connections;
|
using InnovEnergy.Lib.Protocols.Modbus.Slaves;
|
||||||
using static InnovEnergy.Lib.Devices.Battery48TL.Constants;
|
using InnovEnergy.Lib.Utils;
|
||||||
|
using static System.IO.Ports.Parity;
|
||||||
|
|
||||||
namespace InnovEnergy.Lib.Devices.Battery48TL;
|
namespace InnovEnergy.Lib.Devices.Battery48TL;
|
||||||
|
|
||||||
public class Battery48TlDevice
|
public class Battery48TlDevice: ModbusDevice<Battery48TlRecord>
|
||||||
{
|
{
|
||||||
private ModbusClient Modbus { get; }
|
public const Parity Parity = Odd;
|
||||||
|
public const Int32 StopBits = 1;
|
||||||
|
public const Int32 BaudRate = 115200;
|
||||||
|
public const Int32 DataBits = 8;
|
||||||
|
|
||||||
|
|
||||||
|
public Battery48TlDevice(String tty, Byte slaveId, SshHost host) : this
|
||||||
|
(
|
||||||
|
channel: new RemoteSerialChannel(host, tty, BaudRate, Parity, DataBits, StopBits),
|
||||||
|
slaveId
|
||||||
|
)
|
||||||
|
{}
|
||||||
|
|
||||||
public Battery48TlDevice(String device, Byte nodeId)
|
public Battery48TlDevice(String tty, Byte slaveId, String? host = null) : this
|
||||||
|
(
|
||||||
|
channel: host switch
|
||||||
|
{
|
||||||
|
null => new SerialPortChannel ( tty, BaudRate, Parity, DataBits, StopBits),
|
||||||
|
_ => new RemoteSerialChannel(host, tty, BaudRate, Parity, DataBits, StopBits)
|
||||||
|
},
|
||||||
|
slaveId
|
||||||
|
)
|
||||||
|
{}
|
||||||
|
|
||||||
|
public Battery48TlDevice(Channel channel, Byte slaveId) : this
|
||||||
|
(
|
||||||
|
client: new ModbusRtuClient(channel, slaveId)
|
||||||
|
)
|
||||||
|
{}
|
||||||
|
|
||||||
|
public Battery48TlDevice(ModbusClient client): base(client)
|
||||||
{
|
{
|
||||||
var serialConnection = new ModbusSerialConnection(device,
|
|
||||||
BaudRate,
|
|
||||||
Parity,
|
|
||||||
DataBits,
|
|
||||||
StopBits,
|
|
||||||
Constants.Timeout);
|
|
||||||
|
|
||||||
Modbus = new ModbusRtuClient(serialConnection, nodeId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Battery48TlDevice(ModbusClient modbus) // TODO : remove nullable
|
|
||||||
{
|
|
||||||
Modbus = modbus;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Battery48TlDevice Fake() // TODO : remove nullable
|
|
||||||
{
|
|
||||||
return new Battery48TlDevice(null!);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Battery48TLStatus? ReadStatus() //Already try catch is implemented
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return Modbus
|
|
||||||
.ReadInputRegisters(BaseAddress, NoOfRegisters)
|
|
||||||
.ParseBatteryStatus();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine(e.Message + " Battery ");
|
|
||||||
Modbus.CloseConnection();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,49 +0,0 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using InnovEnergy.Lib.StatusApi;
|
|
||||||
using InnovEnergy.Lib.Units;
|
|
||||||
|
|
||||||
namespace InnovEnergy.Lib.Devices.Battery48TL;
|
|
||||||
|
|
||||||
using T = Battery48TLStatus;
|
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
|
||||||
public record Battery48TLStatus : BatteryStatus
|
|
||||||
{
|
|
||||||
public Voltage CellsVoltage { get; init; }
|
|
||||||
|
|
||||||
public Power MaxChargingPower { get; init; }
|
|
||||||
public Power MaxDischargingPower { get; init; }
|
|
||||||
|
|
||||||
public LedState GreenLed { get; init; }
|
|
||||||
public LedState AmberLed { get; init; }
|
|
||||||
public LedState BlueLed { get; init; }
|
|
||||||
public LedState RedLed { get; init; }
|
|
||||||
|
|
||||||
public IReadOnlyList<String> Warnings { get; init; } = Array.Empty<String>();
|
|
||||||
public IReadOnlyList<String> Alarms { get; init; } = Array.Empty<String>();
|
|
||||||
|
|
||||||
public Boolean EocReached { get; init; }
|
|
||||||
public Boolean ConnectedToDc { get; init; }
|
|
||||||
public Boolean Heating { get; init; }
|
|
||||||
public TemperatureState TemperatureState { get; init; } // cold | operating temperature | overheated
|
|
||||||
|
|
||||||
public Current TotalCurrent { get; init; }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: strings
|
|
||||||
// TODO
|
|
||||||
// public State LimitedBy { get; init; }
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// public Boolean AlarmOutActive { get; init; }
|
|
||||||
// public Boolean InternalFanActive { get; init; }
|
|
||||||
// public Boolean VoltMeasurementAllowed { get; init; }
|
|
||||||
// public Boolean AuxRelay { get; init; }
|
|
||||||
// public Boolean RemoteState { get; init; }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
using InnovEnergy.Lib.Utils;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.Battery48TL;
|
||||||
|
|
||||||
|
public class Battery48TlDevices
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyList<Battery48TlDevice> _Devices;
|
||||||
|
|
||||||
|
public Battery48TlDevices(IReadOnlyList<Battery48TlDevice> devices) => _Devices = devices;
|
||||||
|
|
||||||
|
public Battery48TlRecords Read()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var records = _Devices
|
||||||
|
.Select(d => d.Read())
|
||||||
|
.ToArray(_Devices.Count);
|
||||||
|
|
||||||
|
return new Battery48TlRecords(records);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
// TODO: log
|
||||||
|
|
||||||
|
return Battery48TlRecords.Null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,196 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using InnovEnergy.Lib.Devices.Battery48TL.DataTypes;
|
||||||
|
using InnovEnergy.Lib.Units;
|
||||||
|
using InnovEnergy.Lib.Units.Power;
|
||||||
|
using static InnovEnergy.Lib.Devices.Battery48TL.DataTypes.LedState;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.Battery48TL;
|
||||||
|
|
||||||
|
using Strings = IReadOnlyList<String>;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
[SuppressMessage("ReSharper", "ConvertToAutoProperty")]
|
||||||
|
public partial class Battery48TlRecord
|
||||||
|
{
|
||||||
|
public Dc_ Dc => new Dc_(this);
|
||||||
|
public Leds_ Leds => new Leds_(this);
|
||||||
|
public Temperatures_ Temperatures => new Temperatures_(this);
|
||||||
|
|
||||||
|
public Boolean ConnectedToDcBus => (_IoStates & 1) == 0;
|
||||||
|
public Boolean Eoc => Leds is { Green: On, Amber: Off, Blue : Off };
|
||||||
|
|
||||||
|
public Strings Warnings => ParseWarnings().OrderBy(w => w).ToList();
|
||||||
|
public Strings Alarms => ParseAlarms() .OrderBy(w => w).ToList();
|
||||||
|
|
||||||
|
public Percent Soc => _Soc;
|
||||||
|
|
||||||
|
public readonly struct Leds_
|
||||||
|
{
|
||||||
|
public LedState Blue => Self.ParseLed(LedColor.Blue);
|
||||||
|
public LedState Red => Self.ParseLed(LedColor.Red);
|
||||||
|
public LedState Green => Self.ParseLed(LedColor.Green);
|
||||||
|
public LedState Amber => Self.ParseLed(LedColor.Amber);
|
||||||
|
|
||||||
|
private Battery48TlRecord Self { get; }
|
||||||
|
internal Leds_(Battery48TlRecord self) => this.Self = self;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct Temperatures_
|
||||||
|
{
|
||||||
|
public Boolean Heating => (Self._IoStates & 64) != 0;
|
||||||
|
public Temperature Board => Self._TemperaturesBoard;
|
||||||
|
public Cells_ Cells => new Cells_(Self);
|
||||||
|
|
||||||
|
public TemperatureState State => Self.Leds switch
|
||||||
|
{
|
||||||
|
{ Green: >= Blinking, Blue: >= Blinking } => TemperatureState.Cold,
|
||||||
|
_ => TemperatureState.Operation,
|
||||||
|
// TODO: overheated
|
||||||
|
};
|
||||||
|
|
||||||
|
internal Temperatures_(Battery48TlRecord self) => Self = self;
|
||||||
|
private Battery48TlRecord Self { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct Cells_
|
||||||
|
{
|
||||||
|
public Temperature Center => Self._TemperaturesCellsCenter;
|
||||||
|
public Temperature Left => Self._TemperaturesCellsLeft;
|
||||||
|
public Temperature Right => Self._TemperaturesCellsRight;
|
||||||
|
public Temperature Average => Self._TemperaturesCellsAverage;
|
||||||
|
|
||||||
|
internal Cells_(Battery48TlRecord self) => Self = self;
|
||||||
|
private Battery48TlRecord Self { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct Dc_
|
||||||
|
{
|
||||||
|
public Voltage Voltage => Self._DcVoltage;
|
||||||
|
public Current Current => Self._DcCurrent;
|
||||||
|
public ActivePower Power => Self._DcVoltage * Self._DcCurrent;
|
||||||
|
|
||||||
|
internal Dc_(Battery48TlRecord self) => Self = self;
|
||||||
|
private Battery48TlRecord Self { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "StringLiteralTypo")]
|
||||||
|
private IEnumerable<String> ParseAlarms()
|
||||||
|
{
|
||||||
|
Boolean HasBit(Int16 bit) => (_AlarmFlags & 1uL << bit) > 0;
|
||||||
|
|
||||||
|
if (HasBit(0) ) yield return "Tam : BMS temperature too low";
|
||||||
|
if (HasBit(2) ) yield return "TaM2 : BMS temperature too high";
|
||||||
|
if (HasBit(3) ) yield return "Tbm : Battery temperature too low";
|
||||||
|
if (HasBit(5) ) yield return "TbM2 : Battery temperature too high";
|
||||||
|
if (HasBit(7) ) yield return "VBm2 : Bus voltage too low";
|
||||||
|
if (HasBit(9) ) yield return "VBM2 : Bus voltage too high";
|
||||||
|
if (HasBit(11)) yield return "IDM2 : Discharge current too high";
|
||||||
|
if (HasBit(12)) yield return "ISOB : Electrical insulation failure";
|
||||||
|
if (HasBit(13)) yield return "MSWE : Main switch failure";
|
||||||
|
if (HasBit(14)) yield return "FUSE : Main fuse blown";
|
||||||
|
if (HasBit(15)) yield return "HTRE : Battery failed to warm up";
|
||||||
|
if (HasBit(16)) yield return "TCPE : Temperature sensor failure";
|
||||||
|
if (HasBit(17)) yield return "STRE :";
|
||||||
|
if (HasBit(18)) yield return "CME : Current sensor failure";
|
||||||
|
if (HasBit(19)) yield return "HWFL : BMS hardware failure";
|
||||||
|
if (HasBit(20)) yield return "HWEM : Hardware protection tripped";
|
||||||
|
if (HasBit(21)) yield return "ThM : Heatsink temperature too high";
|
||||||
|
if (HasBit(22)) yield return "vsm1 : String voltage too low";
|
||||||
|
if (HasBit(23)) yield return "vsm2 : Low string voltage failure";
|
||||||
|
if (HasBit(25)) yield return "vsM2 : String voltage too high";
|
||||||
|
if (HasBit(27)) yield return "iCM2 : Charge current too high";
|
||||||
|
if (HasBit(29)) yield return "iDM2 : Discharge current too high";
|
||||||
|
if (HasBit(31)) yield return "MID2 : String voltage unbalance too high";
|
||||||
|
if (HasBit(33)) yield return "CCBF : Internal charger hardware failure";
|
||||||
|
if (HasBit(34)) yield return "AhFL :";
|
||||||
|
if (HasBit(36)) yield return "TbCM :";
|
||||||
|
if (HasBit(37)) yield return "BRNF :";
|
||||||
|
if (HasBit(42)) yield return "HTFS : Heater Fuse Blown";
|
||||||
|
if (HasBit(43)) yield return "DATA : Parameters out of range";
|
||||||
|
if (HasBit(45)) yield return "CELL2:";
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "StringLiteralTypo")]
|
||||||
|
private IEnumerable<String> ParseWarnings()
|
||||||
|
{
|
||||||
|
Boolean HasBit(Int16 bit) => (_WarningFlags & 1uL << bit) > 0;
|
||||||
|
|
||||||
|
if (HasBit(1) ) yield return "TaM1: BMS temperature high";
|
||||||
|
if (HasBit(4) ) yield return "TbM1: Battery temperature high";
|
||||||
|
if (HasBit(6) ) yield return "VBm1: Bus voltage low";
|
||||||
|
if (HasBit(8) ) yield return "VBM1: Bus voltage high";
|
||||||
|
if (HasBit(10)) yield return "IDM1: Discharge current high";
|
||||||
|
if (HasBit(24)) yield return "vsM1: String voltage high";
|
||||||
|
if (HasBit(26)) yield return "iCM1: Charge current high";
|
||||||
|
if (HasBit(28)) yield return "iDM1: Discharge current high";
|
||||||
|
if (HasBit(30)) yield return "MID1: String voltages unbalanced";
|
||||||
|
if (HasBit(32)) yield return "BLPW: Not enough charging power on bus";
|
||||||
|
if (HasBit(35)) yield return "Ah_W: String SOC low";
|
||||||
|
if (HasBit(38)) yield return "MPMM: Midpoint wiring problem";
|
||||||
|
if (HasBit(39)) yield return "TCMM:";
|
||||||
|
if (HasBit(40)) yield return "TCdi: Temperature difference between strings high";
|
||||||
|
if (HasBit(41)) yield return "WMTO:";
|
||||||
|
if (HasBit(44)) yield return "bit44:";
|
||||||
|
if (HasBit(46)) yield return "CELL1:";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Double CalcPowerLimitImposedByVoltageLimit(Double vLimit, Double rInt)
|
||||||
|
{
|
||||||
|
var v = Dc.Voltage;
|
||||||
|
var i = Dc.Current;
|
||||||
|
|
||||||
|
var dv = vLimit - v;
|
||||||
|
var di = dv / rInt;
|
||||||
|
|
||||||
|
return vLimit * (i + di);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Double CalcPowerLimitImposedByCurrentLimit(Double iLimit, Double rInt)
|
||||||
|
{
|
||||||
|
var v = Dc.Voltage;
|
||||||
|
var i = Dc.Current;
|
||||||
|
|
||||||
|
var di = iLimit - i;
|
||||||
|
var dv = di * rInt;
|
||||||
|
|
||||||
|
return iLimit * (v + dv);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DcPower MaxChargePower
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var pLimits = new[]
|
||||||
|
{
|
||||||
|
CalcPowerLimitImposedByVoltageLimit(Constants.VMax, Constants.RIntMin),
|
||||||
|
CalcPowerLimitImposedByVoltageLimit(Constants.VMax, Constants.RIntMax),
|
||||||
|
CalcPowerLimitImposedByCurrentLimit(Constants.IMax, Constants.RIntMin),
|
||||||
|
CalcPowerLimitImposedByCurrentLimit(Constants.IMax, Constants.RIntMax)
|
||||||
|
};
|
||||||
|
|
||||||
|
var pLimit = pLimits.Min();
|
||||||
|
|
||||||
|
return Math.Max(pLimit, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DcPower MaxDischargePower
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var pLimits = new[]
|
||||||
|
{
|
||||||
|
CalcPowerLimitImposedByVoltageLimit(Constants.VMin, Constants.RIntMin),
|
||||||
|
CalcPowerLimitImposedByVoltageLimit(Constants.VMin, Constants.RIntMax),
|
||||||
|
CalcPowerLimitImposedByCurrentLimit(-Constants.IMax, Constants.RIntMin),
|
||||||
|
CalcPowerLimitImposedByCurrentLimit(-Constants.IMax, Constants.RIntMax),
|
||||||
|
};
|
||||||
|
|
||||||
|
var pLimit = pLimits.Max();
|
||||||
|
|
||||||
|
return Math.Min(pLimit, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using InnovEnergy.Lib.Devices.Battery48TL.DataTypes;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
|
||||||
|
using InnovEnergy.Lib.SrcGen.Attributes;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.Battery48TL;
|
||||||
|
#pragma warning disable CS0169, CS0649
|
||||||
|
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||||
|
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
|
||||||
|
[BigEndian]
|
||||||
|
public partial class Battery48TlRecord
|
||||||
|
{
|
||||||
|
[InputRegister(1004)] private UInt16 _LedStates;
|
||||||
|
[InputRegister<UInt64>(1005)] private UInt64 _WarningFlags;
|
||||||
|
[InputRegister<UInt64>(1009)] private UInt64 _AlarmFlags;
|
||||||
|
[InputRegister(1013)] private UInt16 _IoStates;
|
||||||
|
|
||||||
|
[InputRegister(999, Scale = 0.01)] private Double _DcVoltage;
|
||||||
|
[InputRegister(1000, Scale = 0.01, Offset = -10000)] private Double _DcCurrent;
|
||||||
|
|
||||||
|
[InputRegister(1053, Scale = 0.1)] private Double _Soc;
|
||||||
|
|
||||||
|
[InputRegister(1014, Scale = 0.1, Offset = -400)] private Double _TemperaturesBoard;
|
||||||
|
[InputRegister(1015, Scale = 0.1, Offset = -400)] private Double _TemperaturesCellsCenter;
|
||||||
|
[InputRegister(1016, Scale = 0.1, Offset = -400)] private Double _TemperaturesCellsLeft;
|
||||||
|
[InputRegister(1017, Scale = 0.1, Offset = -400)] private Double _TemperaturesCellsRight;
|
||||||
|
[InputRegister(1003, Scale = 0.1, Offset = -400)] private Double _TemperaturesCellsAverage;
|
||||||
|
|
||||||
|
private LedState ParseLed(LedColor led) => (LedState)((_LedStates >> (Int32)led) & 3);
|
||||||
|
|
||||||
|
// public Decimal CellsVoltage { get; init; }
|
||||||
|
//
|
||||||
|
// public Decimal MaxChargingPower { get; init; }
|
||||||
|
// public Decimal MaxDischargingPower { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
using InnovEnergy.Lib.Units;
|
||||||
|
using InnovEnergy.Lib.Units.Composite;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.Battery48TL;
|
||||||
|
|
||||||
|
public class Battery48TlRecords
|
||||||
|
{
|
||||||
|
public Battery48TlRecords(IReadOnlyList<Battery48TlRecord> records)
|
||||||
|
{
|
||||||
|
var empty = records.Count == 0;
|
||||||
|
|
||||||
|
Devices = records;
|
||||||
|
Eoc = !empty && records.All(r => r.Eoc);
|
||||||
|
Warnings = records.SelectMany(r => r.Warnings).Distinct().ToList();
|
||||||
|
Alarms = records.SelectMany(r => r.Alarms).Distinct().ToList();
|
||||||
|
Soc = empty ? 0 : records.Min(r => r.Soc.Value);
|
||||||
|
Temperature = records.Any() ? records.Average(b => b.Temperatures.Cells.Average.Value) : 0;
|
||||||
|
|
||||||
|
Dc = empty
|
||||||
|
? DcBus.FromVoltageCurrent(0, 0)
|
||||||
|
: DcBus.FromVoltageCurrent
|
||||||
|
(
|
||||||
|
records.Average(r => r.Dc.Voltage),
|
||||||
|
records.Sum(r => r.Dc.Current)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DcBus Dc { get; init; }
|
||||||
|
public Boolean Eoc { get; init; }
|
||||||
|
public IReadOnlyList<String> Warnings { get; init; }
|
||||||
|
public IReadOnlyList<String> Alarms { get; init; }
|
||||||
|
public Percent Soc { get; init; }
|
||||||
|
public Temperature Temperature { get; init; }
|
||||||
|
|
||||||
|
public IReadOnlyList<Battery48TlRecord> Devices { get; init; }
|
||||||
|
|
||||||
|
public static Battery48TlRecords Null => new Battery48TlRecords(Array.Empty<Battery48TlRecord>());
|
||||||
|
}
|
|
@ -1,32 +0,0 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.IO.Ports;
|
|
||||||
using static System.IO.Ports.Parity;
|
|
||||||
using static System.IO.Ports.StopBits;
|
|
||||||
|
|
||||||
namespace InnovEnergy.Lib.Devices.Battery48TL;
|
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
|
||||||
public static class Constants
|
|
||||||
{
|
|
||||||
public const Int32 BaseAddress = 1000;
|
|
||||||
public const Int32 NoOfRegisters = 56;
|
|
||||||
|
|
||||||
public const Parity Parity = Odd;
|
|
||||||
public const StopBits StopBits = One;
|
|
||||||
public const Int32 BaudRate = 115200;
|
|
||||||
public const Int32 DataBits = 8;
|
|
||||||
public static TimeSpan Timeout { get; } = TimeSpan.FromMilliseconds(100);
|
|
||||||
|
|
||||||
public const Decimal VMax = 59.0m;
|
|
||||||
public const Decimal VMin = 42.0m;
|
|
||||||
public const Decimal AhPerString = 40.0m;
|
|
||||||
|
|
||||||
private const Decimal RStringMin = 0.125m;
|
|
||||||
private const Decimal RStringMax = 0.250m;
|
|
||||||
private const Decimal IMaxPerString = 20.0m;
|
|
||||||
private const UInt16 NumberOfStrings = 5;
|
|
||||||
|
|
||||||
public const Decimal RIntMin = RStringMin / NumberOfStrings;
|
|
||||||
public const Decimal RIntMax = RStringMax / NumberOfStrings;
|
|
||||||
public const Decimal IMax = NumberOfStrings * IMaxPerString;
|
|
||||||
}
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
using InnovEnergy.Lib.Units;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.Battery48TL.DataTypes;
|
||||||
|
|
||||||
|
public readonly struct CellTemperatures
|
||||||
|
{
|
||||||
|
public Temperature Center { get; internal init; }
|
||||||
|
public Temperature Left { get; internal init; }
|
||||||
|
public Temperature Right { get; internal init; }
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using static System.IO.Ports.Parity;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.Battery48TL.DataTypes;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
public const Int32 BaseAddress = 1000;
|
||||||
|
public const Int32 NoOfRegisters = 56;
|
||||||
|
|
||||||
|
public const Parity Parity = Odd;
|
||||||
|
public const Int32 StopBits = 1;
|
||||||
|
public const Int32 BaudRate = 115200;
|
||||||
|
public const Int32 DataBits = 8;
|
||||||
|
public static TimeSpan Timeout { get; } = TimeSpan.FromMilliseconds(100);
|
||||||
|
|
||||||
|
public const Double VMax = 59.0;
|
||||||
|
public const Double VMin = 42.0;
|
||||||
|
public const Double AhPerString = 40.0;
|
||||||
|
|
||||||
|
private const Double RStringMin = 0.125;
|
||||||
|
private const Double RStringMax = 0.250;
|
||||||
|
private const Double IMaxPerString = 20.0;
|
||||||
|
private const UInt16 NumberOfStrings = 5;
|
||||||
|
|
||||||
|
public const Double RIntMin = RStringMin / NumberOfStrings;
|
||||||
|
public const Double RIntMax = RStringMax / NumberOfStrings;
|
||||||
|
public const Double IMax = NumberOfStrings * IMaxPerString;
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace InnovEnergy.Lib.Devices.Battery48TL.DataTypes;
|
||||||
|
|
||||||
|
public enum LedColor
|
||||||
|
{
|
||||||
|
Green = 0, // don't change: numbers are important
|
||||||
|
Amber = 2,
|
||||||
|
Blue = 4,
|
||||||
|
Red = 6,
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace InnovEnergy.Lib.Devices.Battery48TL.DataTypes;
|
||||||
|
|
||||||
|
public enum LedState
|
||||||
|
{
|
||||||
|
Off = 0,
|
||||||
|
On = 1,
|
||||||
|
Blinking = 2,
|
||||||
|
BlinkingFast = 3
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace InnovEnergy.Lib.Devices.Battery48TL.DataTypes;
|
||||||
|
|
||||||
|
public readonly struct Leds
|
||||||
|
{
|
||||||
|
public LedState Blue { get; internal init; }
|
||||||
|
public LedState Green { get; internal init; }
|
||||||
|
public LedState Amber { get; internal init; }
|
||||||
|
public LedState Red { get; internal init; }
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace InnovEnergy.Lib.Devices.Battery48TL.DataTypes;
|
||||||
|
|
||||||
|
public enum TemperatureState
|
||||||
|
{
|
||||||
|
Cold = 0,
|
||||||
|
Operation = 1,
|
||||||
|
Overheated = 2,
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
stty -F /dev/ttyUSB0 <<< '0:0:1bb2:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0'
|
|
@ -1,9 +0,0 @@
|
||||||
namespace InnovEnergy.Lib.Devices.Battery48TL;
|
|
||||||
|
|
||||||
public enum LedColor
|
|
||||||
{
|
|
||||||
Green = 0,
|
|
||||||
Amber = 1,
|
|
||||||
Blue = 2,
|
|
||||||
Red = 3,
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
namespace InnovEnergy.Lib.Devices.Battery48TL;
|
|
||||||
|
|
||||||
public enum LedState
|
|
||||||
{
|
|
||||||
Off = 0,
|
|
||||||
On = 1,
|
|
||||||
BlinkingSlow = 2,
|
|
||||||
BlinkingFast = 3
|
|
||||||
}
|
|
|
@ -1,274 +0,0 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Text;
|
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Conversions;
|
|
||||||
using InnovEnergy.Lib.Units.Composite;
|
|
||||||
using InnovEnergy.Lib.Utils;
|
|
||||||
using static InnovEnergy.Lib.Devices.Battery48TL.LedState;
|
|
||||||
|
|
||||||
namespace InnovEnergy.Lib.Devices.Battery48TL;
|
|
||||||
|
|
||||||
public static class ModbusParser
|
|
||||||
{
|
|
||||||
internal static Battery48TLStatus ParseBatteryStatus(this ModbusRegisters data)
|
|
||||||
{
|
|
||||||
var greenLed = data.ParseLedState(register: 1005, led: LedColor.Green);
|
|
||||||
var amberLed = data.ParseLedState(register: 1005, led: LedColor.Amber);
|
|
||||||
var blueLed = data.ParseLedState(register: 1005, led: LedColor.Blue);
|
|
||||||
var redLed = data.ParseLedState(register: 1005, led: LedColor.Red);
|
|
||||||
|
|
||||||
var soc = data.ParseSoc();
|
|
||||||
|
|
||||||
// var eoc = greenLed is On
|
|
||||||
// && amberLed is Off
|
|
||||||
// && blueLed is Off;
|
|
||||||
|
|
||||||
var eoc = data.ParseEocReached();
|
|
||||||
|
|
||||||
var maxSoc = eoc ? 100m : 99.9m;
|
|
||||||
|
|
||||||
var batteryCold = greenLed >= BlinkingSlow
|
|
||||||
&& blueLed >= BlinkingSlow;
|
|
||||||
|
|
||||||
var temperatureState = batteryCold
|
|
||||||
? TemperatureState.Cold
|
|
||||||
: TemperatureState.OperatingTemperature; // TODO: overheated
|
|
||||||
|
|
||||||
|
|
||||||
return new Battery48TLStatus
|
|
||||||
{
|
|
||||||
Dc = data.ParseDcBus(),
|
|
||||||
Alarms = data.ParseAlarms().ToList(),
|
|
||||||
Warnings = data.ParseWarnings().ToList(),
|
|
||||||
Soc = Math.Min(soc, maxSoc),
|
|
||||||
Temperature = data.ParseDecimal(register: 1004, scaleFactor: 0.1m, offset: -400),
|
|
||||||
GreenLed = greenLed,
|
|
||||||
AmberLed = amberLed,
|
|
||||||
BlueLed = blueLed,
|
|
||||||
RedLed = redLed,
|
|
||||||
Heating = data.ParseBool(baseRegister: 1014, bit: 6),
|
|
||||||
ConnectedToDc = data.ParseBool(baseRegister: 1014, bit: 0),
|
|
||||||
TemperatureState = temperatureState,
|
|
||||||
MaxChargingPower = data.CalcMaxChargePower(),
|
|
||||||
MaxDischargingPower = data.CalcMaxDischargePower(),
|
|
||||||
CellsVoltage = data.ParseDecimal(register: 1000, scaleFactor: 0.01m),
|
|
||||||
TotalCurrent = data.ReadTotalCurrent(),
|
|
||||||
EocReached = eoc
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static Decimal ParseDecimal(this ModbusRegisters data, Int32 register, Decimal scaleFactor = 1.0m, Double offset = 0.0)
|
|
||||||
{
|
|
||||||
var value = data[register].ConvertTo<Int32>(); // widen to 32bit signed
|
|
||||||
|
|
||||||
if (value >= 0x8000)
|
|
||||||
value -= 0x10000; // Fiamm stores their integers signed AND with sign-offset @#%^&!
|
|
||||||
|
|
||||||
return (Decimal)(value + offset) * scaleFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Decimal ParseCurrent(this ModbusRegisters data)
|
|
||||||
{
|
|
||||||
return data.ParseDecimal(register: 1001, scaleFactor: 0.01m, offset: -10000);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Decimal ParseBusVoltage(this ModbusRegisters data)
|
|
||||||
{
|
|
||||||
return data.ParseDecimal(register: 1002, scaleFactor: 0.01m);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Decimal ReadTotalCurrent(this ModbusRegisters data)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return ParseDecimal(data, register: 1063, scaleFactor: 0.01m, offset: -100);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine(e + " Read Total current fail ");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Boolean ParseBool(this ModbusRegisters data, Int32 baseRegister, Int16 bit)
|
|
||||||
{
|
|
||||||
var x = bit / 16;
|
|
||||||
var y = bit % 16;
|
|
||||||
|
|
||||||
var value = (UInt32)data[baseRegister + x];
|
|
||||||
|
|
||||||
return (value & (1 << y)) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static LedState ParseLedState(this ModbusRegisters data, Int32 register, LedColor led)
|
|
||||||
{
|
|
||||||
var lo = data.ParseBool(register, (led.ConvertTo<Int16>() * 2 ).ConvertTo<Int16>());
|
|
||||||
var hi = data.ParseBool(register, (led.ConvertTo<Int16>() * 2 + 1).ConvertTo<Int16>());
|
|
||||||
|
|
||||||
return (hi, lo) switch
|
|
||||||
{
|
|
||||||
(false, false) => Off,
|
|
||||||
(false, true) => On,
|
|
||||||
(true, false) => BlinkingSlow,
|
|
||||||
(true, true) => BlinkingFast,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static String ParseString(this ModbusRegisters data, Int32 register, Int16 count)
|
|
||||||
{
|
|
||||||
return Enumerable
|
|
||||||
.Range(register, count)
|
|
||||||
.Select(i => data[i])
|
|
||||||
.Select(BitConverter.GetBytes)
|
|
||||||
.Select(Encoding.ASCII.GetString)
|
|
||||||
.Aggregate("", (a, b) => a + b[1] + b[0]); // endian swap
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Boolean ParseEocReached(this ModbusRegisters data)
|
|
||||||
{
|
|
||||||
var s = ParseString(data, 1061, 2);
|
|
||||||
return "EOC_" == s;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Decimal ParseSoc(this ModbusRegisters data)
|
|
||||||
{
|
|
||||||
return data.ParseDecimal(register: 1054, scaleFactor: 0.1m);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Decimal CalcPowerLimitImposedByVoltageLimit(Decimal v,Decimal i,Decimal vLimit,Decimal rInt)
|
|
||||||
{
|
|
||||||
var dv = vLimit - v;
|
|
||||||
var di = dv / rInt;
|
|
||||||
var pLimit = vLimit * (i + di);
|
|
||||||
|
|
||||||
return pLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Decimal CalcPowerLimitImposedByCurrentLimit(Decimal v, Decimal i, Decimal iLimit, Decimal rInt)
|
|
||||||
{
|
|
||||||
var di = iLimit - i;
|
|
||||||
var dv = di * rInt;
|
|
||||||
var pLimit = iLimit * (v + dv);
|
|
||||||
|
|
||||||
return pLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static Decimal CalcPowerLimitImposedByTempLimit(Decimal t, Decimal maxAllowedTemp, Decimal power , Decimal setpoint)
|
|
||||||
{
|
|
||||||
// const Int32 holdZone = 300;
|
|
||||||
// const Int32 maxAllowedTemp = 315;
|
|
||||||
|
|
||||||
var kp = 0.05m;
|
|
||||||
var error = setpoint - power;
|
|
||||||
var controlOutput = (kp * error) *(1 - Math.Abs((t-307.5m)/7.5m));
|
|
||||||
|
|
||||||
return controlOutput;
|
|
||||||
|
|
||||||
// var a = holdZone - maxAllowedTemp;
|
|
||||||
// var b = -a * maxAllowedTemp;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Decimal CalcMaxChargePower(this ModbusRegisters data)
|
|
||||||
{
|
|
||||||
var v = data.ParseDecimal(register: 1000, scaleFactor: 0.01m);
|
|
||||||
var i = ParseCurrent(data);
|
|
||||||
|
|
||||||
var pLimits = new[]
|
|
||||||
{
|
|
||||||
// TODO: review
|
|
||||||
CalcPowerLimitImposedByVoltageLimit(v, i, Constants.VMax, Constants.RIntMin),
|
|
||||||
CalcPowerLimitImposedByVoltageLimit(v, i, Constants.VMax, Constants.RIntMax),
|
|
||||||
CalcPowerLimitImposedByCurrentLimit(v, i, Constants.IMax, Constants.RIntMin),
|
|
||||||
CalcPowerLimitImposedByCurrentLimit(v, i, Constants.IMax, Constants.RIntMax)
|
|
||||||
};
|
|
||||||
|
|
||||||
var pLimit = pLimits.Min();
|
|
||||||
|
|
||||||
return Math.Max(pLimit, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static DcBus ParseDcBus(this ModbusRegisters data) => new()
|
|
||||||
{
|
|
||||||
Current = data.ParseCurrent(),
|
|
||||||
Voltage = data.ParseBusVoltage(),
|
|
||||||
};
|
|
||||||
|
|
||||||
internal static Decimal CalcMaxDischargePower(this ModbusRegisters data)
|
|
||||||
{
|
|
||||||
var v = data.ParseDecimal(register: 1000, scaleFactor: 0.01m);
|
|
||||||
var i = ParseCurrent(data);
|
|
||||||
|
|
||||||
var pLimits = new[]
|
|
||||||
{
|
|
||||||
// TODO: review
|
|
||||||
CalcPowerLimitImposedByVoltageLimit(v, i, Constants.VMin, Constants.RIntMin),
|
|
||||||
CalcPowerLimitImposedByVoltageLimit(v, i, Constants.VMin, Constants.RIntMax),
|
|
||||||
CalcPowerLimitImposedByCurrentLimit(v, i, -Constants.IMax, Constants.RIntMin),
|
|
||||||
CalcPowerLimitImposedByCurrentLimit(v, i, -Constants.IMax, Constants.RIntMax),
|
|
||||||
// CalcPowerLimitImposedByTempLimit(t,315,300)
|
|
||||||
};
|
|
||||||
|
|
||||||
var pLimit = pLimits.Max();
|
|
||||||
|
|
||||||
return Math.Min(pLimit, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "StringLiteralTypo")]
|
|
||||||
internal static IEnumerable<String> ParseAlarms(this ModbusRegisters data)
|
|
||||||
{
|
|
||||||
if (data.ParseBool(1010, 0)) yield return "Tam : BMS temperature too low";
|
|
||||||
if (data.ParseBool(1010, 2)) yield return "TaM2 : BMS temperature too high";
|
|
||||||
if (data.ParseBool(1010, 3)) yield return "Tbm : Battery temperature too low";
|
|
||||||
if (data.ParseBool(1010, 5)) yield return "TbM2 : Battery temperature too high";
|
|
||||||
if (data.ParseBool(1010, 7)) yield return "VBm2 : Bus voltage too low";
|
|
||||||
if (data.ParseBool(1010, 9)) yield return "VBM2 : Bus voltage too high";
|
|
||||||
if (data.ParseBool(1010, 11)) yield return "IDM2 : Discharge current too high";
|
|
||||||
if (data.ParseBool(1010, 12)) yield return "ISOB : Electrical insulation failure";
|
|
||||||
if (data.ParseBool(1010, 13)) yield return "MSWE : Main switch failure";
|
|
||||||
if (data.ParseBool(1010, 14)) yield return "FUSE : Main fuse blown";
|
|
||||||
if (data.ParseBool(1010, 15)) yield return "HTRE : Battery failed to warm up";
|
|
||||||
if (data.ParseBool(1010, 16)) yield return "TCPE : Temperature sensor failure";
|
|
||||||
if (data.ParseBool(1010, 17)) yield return "STRE :";
|
|
||||||
if (data.ParseBool(1010, 18)) yield return "CME : Current sensor failure";
|
|
||||||
if (data.ParseBool(1010, 19)) yield return "HWFL : BMS hardware failure";
|
|
||||||
if (data.ParseBool(1010, 20)) yield return "HWEM : Hardware protection tripped";
|
|
||||||
if (data.ParseBool(1010, 21)) yield return "ThM : Heatsink temperature too high";
|
|
||||||
if (data.ParseBool(1010, 22)) yield return "vsm1 : String voltage too low";
|
|
||||||
if (data.ParseBool(1010, 23)) yield return "vsm2 : Low string voltage failure";
|
|
||||||
if (data.ParseBool(1010, 25)) yield return "vsM2 : String voltage too high";
|
|
||||||
if (data.ParseBool(1010, 27)) yield return "iCM2 : Charge current too high";
|
|
||||||
if (data.ParseBool(1010, 29)) yield return "iDM2 : Discharge current too high";
|
|
||||||
if (data.ParseBool(1010, 31)) yield return "MID2 : String voltage unbalance too high";
|
|
||||||
if (data.ParseBool(1010, 33)) yield return "CCBF : Internal charger hardware failure";
|
|
||||||
if (data.ParseBool(1010, 34)) yield return "AhFL :";
|
|
||||||
if (data.ParseBool(1010, 36)) yield return "TbCM :";
|
|
||||||
if (data.ParseBool(1010, 37)) yield return "BRNF :";
|
|
||||||
if (data.ParseBool(1010, 42)) yield return "HTFS : If Heaters Fuse Blown";
|
|
||||||
if (data.ParseBool(1010, 43)) yield return "DATA : Parameters out of range";
|
|
||||||
if (data.ParseBool(1010, 45)) yield return "CELL2:";
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "StringLiteralTypo")]
|
|
||||||
internal static IEnumerable<String> ParseWarnings(this ModbusRegisters data)
|
|
||||||
{
|
|
||||||
if (data.ParseBool(1006, 1)) yield return "TaM1: BMS temperature high";
|
|
||||||
if (data.ParseBool(1006, 4)) yield return "TbM1: Battery temperature high";
|
|
||||||
if (data.ParseBool(1006, 6)) yield return "VBm1: Bus voltage low";
|
|
||||||
if (data.ParseBool(1006, 8)) yield return "VBM1: Bus voltage high";
|
|
||||||
if (data.ParseBool(1006, 10)) yield return "IDM1: Discharge current high";
|
|
||||||
if (data.ParseBool(1006, 24)) yield return "vsM1: String voltage high";
|
|
||||||
if (data.ParseBool(1006, 26)) yield return "iCM1: Charge current high";
|
|
||||||
if (data.ParseBool(1006, 28)) yield return "iDM1: Discharge current high";
|
|
||||||
if (data.ParseBool(1006, 30)) yield return "MID1: String voltages unbalanced";
|
|
||||||
if (data.ParseBool(1006, 32)) yield return "BLPW: Not enough charging power on bus";
|
|
||||||
if (data.ParseBool(1006, 35)) yield return "Ah_W: String SOC low";
|
|
||||||
if (data.ParseBool(1006, 38)) yield return "MPMM: Midpoint wiring problem";
|
|
||||||
if (data.ParseBool(1006, 39)) yield return "TCMM:";
|
|
||||||
if (data.ParseBool(1006, 40)) yield return "TCdi: Temperature difference between strings high";
|
|
||||||
if (data.ParseBool(1006, 41)) yield return "WMTO:";
|
|
||||||
if (data.ParseBool(1006, 44)) yield return "bit44:";
|
|
||||||
if (data.ParseBool(1006, 46)) yield return "CELL1:";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
|
using InnovEnergy.Lib.Units;
|
||||||
|
using InnovEnergy.Lib.Utils;
|
||||||
|
using static InnovEnergy.Lib.Devices.Battery48TL.Battery48TlDevice;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.Battery48TL;
|
||||||
|
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
public static Task Main(string[] args)
|
||||||
|
{
|
||||||
|
var host = new SshHost("10.2.3.115", "ie-entwicklung");
|
||||||
|
var channel = new RemoteSerialChannel(host, "ttyUSB0", BaudRate, Parity, DataBits, StopBits);
|
||||||
|
|
||||||
|
var nodes = new Byte[] { 2 };
|
||||||
|
|
||||||
|
var devices = nodes
|
||||||
|
.Select(n => new Battery48TlDevice(channel, n))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var d = new Battery48TlDevices(devices);
|
||||||
|
|
||||||
|
//var options = new JsonSerializerOptions { WriteIndented = true, Converters = { new JsonStringEnumConverter() }};
|
||||||
|
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var x = d.Read();
|
||||||
|
x.ToCsv().WriteLine();
|
||||||
|
|
||||||
|
//(x, options).Apply(JsonSerializer.Serialize).WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,8 +0,0 @@
|
||||||
namespace InnovEnergy.Lib.Devices.Battery48TL;
|
|
||||||
|
|
||||||
public enum TemperatureState
|
|
||||||
{
|
|
||||||
Cold = 0,
|
|
||||||
OperatingTemperature = 1,
|
|
||||||
Overheated = 2,
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Conversions;
|
|
||||||
using InnovEnergy.Lib.Utils;
|
|
||||||
|
|
||||||
namespace InnovEnergy.Lib.Devices.EmuMeter;
|
|
||||||
|
|
||||||
public static class Conversions
|
|
||||||
{
|
|
||||||
|
|
||||||
// TODO: integrate into ModbusRegisters
|
|
||||||
|
|
||||||
public static IReadOnlyList<Single> ToSingles(this ModbusRegisters regs)
|
|
||||||
{
|
|
||||||
return regs
|
|
||||||
.Chunk(2)
|
|
||||||
.Select(c => c.Reverse().SelectMany(BitConverter.GetBytes).ToArray())
|
|
||||||
.Select(d => BitConverter.ToSingle(d))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IReadOnlyList<Decimal> ToDecimals(this ModbusRegisters regs)
|
|
||||||
{
|
|
||||||
return regs
|
|
||||||
.Chunk(2)
|
|
||||||
.Select(c => c.Reverse().SelectMany(BitConverter.GetBytes).ToArray())
|
|
||||||
.Select(d => BitConverter.ToSingle(d))
|
|
||||||
.Select(d => d.ConvertTo<Decimal>())
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReSharper disable once InconsistentNaming
|
|
||||||
public static IReadOnlyList<UInt64> ToUInt64s(this ModbusRegisters regs)
|
|
||||||
{
|
|
||||||
return regs
|
|
||||||
.SelectMany(d => BitConverter.GetBytes(d).Reverse())
|
|
||||||
.Chunk(8)
|
|
||||||
.Select(c => c.Reverse().ToArray())
|
|
||||||
.Select(d => BitConverter.ToUInt64(d))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,9 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<!-- <Import Project="../../InnovEnergy.Lib.props" />-->
|
||||||
<Configurations>Debug;Release;Release-Server</Configurations>
|
|
||||||
<Platforms>AnyCPU;linux-arm</Platforms>
|
<Import Project="../../../App/InnovEnergy.App.props" />
|
||||||
</PropertyGroup>
|
|
||||||
<Import Project="../../InnovEnergy.Lib.props" />
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="../../Utils/Utils.csproj" />
|
<ProjectReference Include="../../Utils/Utils.csproj" />
|
||||||
|
|
|
@ -1,97 +1,49 @@
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Connections;
|
using InnovEnergy.Lib.Protocols.Modbus.Slaves;
|
||||||
using InnovEnergy.Lib.Units.Composite;
|
|
||||||
using static DecimalMath.DecimalEx;
|
|
||||||
|
|
||||||
namespace InnovEnergy.Lib.Devices.EmuMeter;
|
namespace InnovEnergy.Lib.Devices.EmuMeter;
|
||||||
|
|
||||||
public class EmuMeterDevice
|
public class EmuMeterDevice: ModbusDevice<EmuMeterRegisters>
|
||||||
{
|
{
|
||||||
private ModbusTcpClient Modbus { get; }
|
public EmuMeterDevice(String hostname, UInt16 port = 502, Byte slaveId = 1) : this(new TcpChannel(hostname, port), slaveId)
|
||||||
|
{
|
||||||
public EmuMeterDevice(String hostname, UInt16 port = 502, Byte slaveId = 1)
|
}
|
||||||
|
|
||||||
|
public EmuMeterDevice(Channel channel, Byte slaveId = 1) : base(new ModbusTcpClient(channel, slaveId))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public EmuMeterDevice(ModbusClient client) : base(client)
|
||||||
{
|
{
|
||||||
var connection = new ModbusTcpConnection(hostname, port);
|
|
||||||
Modbus = new ModbusTcpClient(connection, slaveId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmuMeterStatus? ReadStatus()
|
|
||||||
|
public new EmuMeterRegisters? Read()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return TryReadStatus();
|
return base.Read();
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Modbus.CloseConnection();
|
// TODO: Log
|
||||||
|
Console.WriteLine(e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//private static Decimal GetPhi(Decimal cosPhi) => cosPhi.Clamp(-1m, 1m).Apply(ACos);
|
|
||||||
|
|
||||||
private EmuMeterStatus TryReadStatus()
|
|
||||||
|
public new void Write(EmuMeterRegisters registers)
|
||||||
{
|
{
|
||||||
// Console.WriteLine("Reading Emu Meter Data");
|
try
|
||||||
|
|
||||||
|
|
||||||
// TODO: get SerialNb, depends on Little/Big Endian support in Modbus Lib
|
|
||||||
// var registers = Modbus.ReadHoldingRegisters(5001, 4);
|
|
||||||
// var id = registers.GetInt32(5001);
|
|
||||||
|
|
||||||
var powerCurrent = Modbus.ReadHoldingRegisters(9000, 108).ToDecimals(); // TODO "ModbusRegisters"
|
|
||||||
var voltageFreq = Modbus.ReadHoldingRegisters(9200, 112).ToDecimals(); // To check with Ivo
|
|
||||||
|
|
||||||
// var energyPhases = Modbus.ReadHoldingRegisters(6100, 104).ToUInt64s();
|
|
||||||
|
|
||||||
var activePowerL1 = powerCurrent[1];
|
|
||||||
var activePowerL2 = powerCurrent[2];
|
|
||||||
var activePowerL3 = powerCurrent[3];
|
|
||||||
var reactivePowerL1 = powerCurrent[6];
|
|
||||||
var reactivePowerL2 = powerCurrent[7];
|
|
||||||
var reactivePowerL3 = powerCurrent[8];
|
|
||||||
|
|
||||||
var currentL1 = powerCurrent[51];
|
|
||||||
var currentL2 = powerCurrent[52];
|
|
||||||
var currentL3 = powerCurrent[53];
|
|
||||||
|
|
||||||
var voltageL1N = voltageFreq[0];
|
|
||||||
var voltageL2N = voltageFreq[1];
|
|
||||||
var voltageL3N = voltageFreq[2];
|
|
||||||
var frequency = voltageFreq[55];
|
|
||||||
|
|
||||||
|
|
||||||
var l1 = new AcPhase
|
|
||||||
{
|
{
|
||||||
Current = currentL1,
|
base.Write(registers);
|
||||||
Voltage = voltageL1N,
|
}
|
||||||
Phi = ATan2(reactivePowerL1, activePowerL1) // TODO: check that this works
|
catch (Exception e)
|
||||||
};
|
|
||||||
var l2 = new AcPhase
|
|
||||||
{
|
{
|
||||||
Current = currentL2,
|
// TODO: Log
|
||||||
Voltage = voltageL2N,
|
Console.WriteLine(e);
|
||||||
Phi = ATan2(reactivePowerL2, activePowerL2)
|
}
|
||||||
};
|
|
||||||
var l3 = new AcPhase
|
|
||||||
{
|
|
||||||
Current = currentL3,
|
|
||||||
Voltage = voltageL3N,
|
|
||||||
Phi = ATan2(reactivePowerL3, activePowerL3)
|
|
||||||
};
|
|
||||||
|
|
||||||
return new EmuMeterStatus
|
|
||||||
{
|
|
||||||
Ac = new Ac3Bus
|
|
||||||
{
|
|
||||||
Frequency = frequency,
|
|
||||||
L1 = l1,
|
|
||||||
L2 = l2,
|
|
||||||
L3 = l3
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
|
||||||
|
using InnovEnergy.Lib.StatusApi.DeviceTypes;
|
||||||
|
using InnovEnergy.Lib.Units.Composite;
|
||||||
|
|
||||||
|
#pragma warning disable CS0649
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.EmuMeter;
|
||||||
|
|
||||||
|
using Float32 = Single;
|
||||||
|
|
||||||
|
|
||||||
|
[AddressOffset(-2)] // why?
|
||||||
|
public class EmuMeterRegisters : IAc3Meter
|
||||||
|
{
|
||||||
|
[HoldingRegister<Float32>(9002)] private Float32 _ActivePowerL1;
|
||||||
|
[HoldingRegister<Float32>(9004)] private Float32 _ActivePowerL2;
|
||||||
|
[HoldingRegister<Float32>(9006)] private Float32 _ActivePowerL3;
|
||||||
|
|
||||||
|
[HoldingRegister<Float32>(9012)] private Float32 _ReactivePowerL1;
|
||||||
|
[HoldingRegister<Float32>(9014)] private Float32 _ReactivePowerL2;
|
||||||
|
[HoldingRegister<Float32>(9016)] private Float32 _ReactivePowerL3;
|
||||||
|
|
||||||
|
[HoldingRegister<Float32>(9022)] private Float32 _ApparentPowerL1;
|
||||||
|
[HoldingRegister<Float32>(9024)] private Float32 _ApparentPowerL2;
|
||||||
|
[HoldingRegister<Float32>(9026)] private Float32 _ApparentPowerL3;
|
||||||
|
|
||||||
|
[HoldingRegister<Float32>(9102)] private Float32 _CurrentL1;
|
||||||
|
[HoldingRegister<Float32>(9104)] private Float32 _CurrentL2;
|
||||||
|
[HoldingRegister<Float32>(9106)] private Float32 _CurrentL3;
|
||||||
|
|
||||||
|
[HoldingRegister<Float32>(9200)] private Float32 _VoltageL1N;
|
||||||
|
[HoldingRegister<Float32>(9202)] private Float32 _VoltageL2N;
|
||||||
|
[HoldingRegister<Float32>(9204)] private Float32 _VoltageL3N;
|
||||||
|
|
||||||
|
[HoldingRegister<Float32>(9310)] private Float32 _Frequency;
|
||||||
|
|
||||||
|
public Ac3Bus Ac => Ac3Bus.FromPhasesAndFrequency
|
||||||
|
(
|
||||||
|
l1: AcPhase.FromVoltageCurrentActiveReactiveApparent
|
||||||
|
(
|
||||||
|
_VoltageL1N,
|
||||||
|
_CurrentL1,
|
||||||
|
_ActivePowerL1,
|
||||||
|
_ReactivePowerL1,
|
||||||
|
_ApparentPowerL1
|
||||||
|
),
|
||||||
|
l2: AcPhase.FromVoltageCurrentActiveReactiveApparent
|
||||||
|
(
|
||||||
|
_VoltageL2N,
|
||||||
|
_CurrentL2,
|
||||||
|
_ActivePowerL2,
|
||||||
|
_ReactivePowerL2,
|
||||||
|
_ApparentPowerL2
|
||||||
|
),
|
||||||
|
l3: AcPhase.FromVoltageCurrentActiveReactiveApparent
|
||||||
|
(
|
||||||
|
_VoltageL3N,
|
||||||
|
_CurrentL3,
|
||||||
|
_ActivePowerL3,
|
||||||
|
_ReactivePowerL3,
|
||||||
|
_ApparentPowerL3
|
||||||
|
),
|
||||||
|
frequency: _Frequency
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue